mirror of
https://github.com/esphome/esphome.git
synced 2025-11-02 08:01:50 +00:00
Compare commits
23 Commits
jesserockz
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ab3cb9d2b | ||
|
|
5b3d61b4a6 | ||
|
|
727e8ca376 | ||
|
|
5ed77c10ae | ||
|
|
89b9bddf1b | ||
|
|
65cbb0d741 | ||
|
|
9533d52d86 | ||
|
|
6fe4ffa0cf | ||
|
|
576ce7ee35 | ||
|
|
8a45e877bb | ||
|
|
84607c1255 | ||
|
|
8664ec0a3b | ||
|
|
32d8c60a0b | ||
|
|
976a1e27b4 | ||
|
|
cc2c1b1d89 | ||
|
|
85495d38b7 | ||
|
|
84a77ee427 | ||
|
|
11a4115e30 | ||
|
|
121ed687f3 | ||
|
|
c602f3082e | ||
|
|
4a43f922c6 | ||
|
|
21e66b76e4 | ||
|
|
cdeed7afa7 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -21,6 +21,10 @@ body:
|
||||
|
||||
Provide a clear and concise description of what the problem is.
|
||||
|
||||
⚠️ **WARNING: Do NOT paste logs, stack traces, or error messages here!**
|
||||
Use the "Logs" section below instead. Issues with logs
|
||||
in this field will be automatically closed.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
@@ -79,7 +83,7 @@ body:
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Anything in the logs that might be useful for us?
|
||||
label: Logs
|
||||
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
|
||||
render: txt
|
||||
- type: textarea
|
||||
|
||||
248
.github/workflows/auto-close-logs-in-problem.yml
vendored
Normal file
248
.github/workflows/auto-close-logs-in-problem.yml
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
name: Auto-close issues with logs in problem field
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to check for logs'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
check-logs-in-problem:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.issue.state == 'open' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@esphomebot reopen')) || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- name: Check for logs and handle issue state
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
// Handle different trigger types
|
||||
let issue, isReassessment;
|
||||
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
// Manual dispatch - get issue from input
|
||||
const issueNumber = ${{ github.event.inputs.issue_number }};
|
||||
console.log('Manual dispatch for issue:', issueNumber);
|
||||
|
||||
const issueResponse = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(issueNumber)
|
||||
});
|
||||
|
||||
issue = issueResponse.data;
|
||||
isReassessment = false; // Treat manual dispatch as initial check
|
||||
} else {
|
||||
// Normal event-driven flow
|
||||
issue = context.payload.issue;
|
||||
isReassessment = context.eventName === 'issue_comment' && context.payload.comment.body.includes('@esphomebot reopen');
|
||||
}
|
||||
|
||||
console.log('Event type:', context.eventName);
|
||||
console.log('Is reassessment:', isReassessment);
|
||||
console.log('Issue state:', issue.state);
|
||||
|
||||
// Extract the problem section from the issue body
|
||||
const body = issue.body || '';
|
||||
|
||||
// Look for the problem section between "### The problem" and the next section
|
||||
const problemMatch = body.match(/### The problem\s*\n([\s\S]*?)(?=\n### |$)/i);
|
||||
|
||||
if (!problemMatch) {
|
||||
console.log('Could not find problem section');
|
||||
if (isReassessment) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '❌ Could not find the "The problem" section in the issue template. Please make sure you are using the proper issue template format.'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const problemText = problemMatch[1].trim();
|
||||
console.log('Problem text length:', problemText.length);
|
||||
|
||||
// Function to check if text contains logs
|
||||
function checkForLogs(text) {
|
||||
// Patterns that indicate logs/stack traces/error messages
|
||||
const logPatterns = [
|
||||
// ESPHome specific log patterns with brackets
|
||||
/^\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [D][component:123]: message
|
||||
/^\[\d{2}:\d{2}:\d{2}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56][D][component:123]: message
|
||||
/^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56.123][D][component:123]: message
|
||||
|
||||
// Common log prefixes
|
||||
/^\[[\d\s\-:\.]+\]/m, // [timestamp] format
|
||||
/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/m, // YYYY-MM-DD HH:MM:SS
|
||||
/^\w+\s+\d{2}:\d{2}:\d{2}/m, // INFO 12:34:56
|
||||
|
||||
// Error indicators
|
||||
/^(ERROR|WARN|WARNING|FATAL|DEBUG|INFO|TRACE)[\s:]/mi,
|
||||
/^(Exception|Error|Traceback|Stack trace)/mi,
|
||||
/at\s+[\w\.]+\([^)]*:\d+:\d+\)/m, // Stack trace format
|
||||
/^\s*File\s+"[^"]*",\s+line\s+\d+/m, // Python traceback
|
||||
|
||||
// Legacy ESPHome log patterns
|
||||
/^\[\d{2}:\d{2}:\d{2}\]\[/m, // [12:34:56][component]
|
||||
/^WARNING\s+[^:\s]+:/m, // WARNING component:
|
||||
/^ERROR\s+[^:\s]+:/m, // ERROR component:
|
||||
|
||||
// Multiple consecutive lines starting with similar patterns
|
||||
/(^(INFO|DEBUG|WARN|ERROR)[^\n]*\n){3,}/mi,
|
||||
/(^\[[DIWEVC]\]\[[^\]]+\][^\n]*\n){3,}/mi, // Multiple ESPHome log lines
|
||||
|
||||
// Hex dumps or binary data
|
||||
/0x[0-9a-f]{4,}/i,
|
||||
/[0-9a-f]{8,}/,
|
||||
|
||||
// Compilation errors
|
||||
/error:\s+/i,
|
||||
/:\d+:\d+:\s+(error|warning):/i,
|
||||
|
||||
// Very long lines (often log output)
|
||||
/.{200,}/
|
||||
];
|
||||
|
||||
const hasLogs = logPatterns.some(pattern => {
|
||||
const matches = pattern.test(text);
|
||||
if (matches) {
|
||||
console.log('Pattern matched:', pattern.toString());
|
||||
}
|
||||
return matches;
|
||||
});
|
||||
|
||||
// Additional heuristics
|
||||
const lineCount = text.split('\n').length;
|
||||
const hasLotsOfLines = lineCount > 20; // More than 20 lines might be logs
|
||||
|
||||
const hasCodeBlocks = (text.match(/```/g) || []).length >= 2;
|
||||
const longCodeBlock = hasCodeBlocks && text.length > 1000;
|
||||
|
||||
console.log(`Lines: ${lineCount}, Has logs: ${hasLogs}, Long code block: ${longCodeBlock}`);
|
||||
|
||||
return hasLogs || (hasLotsOfLines && longCodeBlock);
|
||||
}
|
||||
|
||||
const hasLogsInProblem = checkForLogs(problemText);
|
||||
|
||||
// Handle reassessment (when @esphomebot is mentioned)
|
||||
if (isReassessment) {
|
||||
if (!hasLogsInProblem) {
|
||||
// No logs found, check if issue was auto-closed and reopen it
|
||||
if (issue.state === 'closed') {
|
||||
// Check if it has the auto-closed label
|
||||
const labels = issue.labels.map(label => label.name);
|
||||
if (labels.includes('auto-closed')) {
|
||||
console.log('Reopening issue - logs have been moved');
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
// Remove auto-closed and invalid labels
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'auto-closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'invalid'
|
||||
});
|
||||
|
||||
// Find and edit the original auto-close comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number
|
||||
});
|
||||
|
||||
const autoCloseComment = comments.data.find(comment =>
|
||||
comment.user.login === 'github-actions[bot]' &&
|
||||
comment.body.includes('automatically closed because it appears to contain logs')
|
||||
);
|
||||
|
||||
if (autoCloseComment) {
|
||||
const updatedComment = `✅ **ISSUE REOPENED**
|
||||
|
||||
Thank you for helping us maintain organized issue reports! 🙏`;
|
||||
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: autoCloseComment.id,
|
||||
body: updatedComment
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '❌ Logs are still detected in the "The problem" section. Please move them to the "Logs" section and try again.'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle initial issue opening
|
||||
if (!hasLogsInProblem) {
|
||||
console.log('No logs detected in problem field');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Logs detected in problem field, closing issue');
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a comment explaining why it was closed
|
||||
const comment = `This issue has been automatically closed because it appears to contain logs, stack traces, or error messages in the "The problem" field.
|
||||
|
||||
⚠️ **Please follow the issue template correctly:**
|
||||
- Use the "The problem" field to **describe** your issue in plain English
|
||||
- Put logs, error messages, and stack traces in the "Logs" section instead
|
||||
|
||||
To reopen this issue:
|
||||
1. Edit your original issue description
|
||||
2. Move any logs/error messages to the appropriate "Logs" section
|
||||
3. Rewrite the "The problem" section with a clear description of what you were trying to do and what went wrong
|
||||
4. Comment exactly \`@esphomebot reopen\` to reassess and automatically reopen if fixed
|
||||
|
||||
Thank you for helping us maintain organized issue reports! 🙏`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add labels
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ['invalid', 'auto-closed']
|
||||
});
|
||||
@@ -301,8 +301,10 @@ class APIConnection : public APIServerConnection {
|
||||
if (entity->has_own_name())
|
||||
msg.name = entity->get_name();
|
||||
|
||||
// Set common EntityBase properties
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = entity->get_icon();
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
#ifdef USE_DEVICES
|
||||
|
||||
@@ -292,9 +292,13 @@ class InfoResponseProtoMessage : public ProtoMessage {
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
bool disabled_by_default{false};
|
||||
#ifdef USE_ENTITY_ICON
|
||||
std::string icon{};
|
||||
#endif
|
||||
enums::EntityCategory entity_category{};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
@@ -303,7 +307,9 @@ class StateResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~StateResponseProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
@@ -312,7 +318,9 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
public:
|
||||
~CommandProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
|
||||
@@ -4,7 +4,13 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
|
||||
from esphome.const import (
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_NUMBER,
|
||||
CONF_PIN,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
from .. import gpio_ns
|
||||
@@ -76,6 +82,18 @@ async def to_code(config):
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
# Check if pin is shared with other components (allow_other_uses)
|
||||
# When a pin is shared, interrupts can interfere with other components
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
||||
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
||||
"The sensor will use polling mode for compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
config[CONF_PIN][CONF_NUMBER],
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
if use_interrupt:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
|
||||
@@ -77,7 +77,6 @@ ALLOWED_CLIMATE_MODES = {
|
||||
}
|
||||
|
||||
ALLOWED_CLIMATE_PRESETS = {
|
||||
"NONE": ClimatePreset.CLIMATE_PRESET_NONE,
|
||||
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
|
||||
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
|
||||
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
|
||||
|
||||
@@ -534,6 +534,8 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
// Draw a pixel at the given coordinates.
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
rotate_coordinates_(x, y);
|
||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||
return;
|
||||
|
||||
@@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
SDL_Rect rect{x, y, 1, 1};
|
||||
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
|
||||
SDL_UpdateTexture(this->texture_, &rect, &data, 2);
|
||||
|
||||
@@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
||||
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
||||
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
|
||||
}
|
||||
#ifdef USE_OTA_STATE_CALLBACK
|
||||
// Report progress - use call_deferred since we're in web server task
|
||||
@@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
||||
|
||||
// Finalize
|
||||
if (final) {
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
|
||||
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
|
||||
this->ota_read_length_, request->contentLength());
|
||||
|
||||
// For Arduino framework, the Update library tracks expected size from firmware header
|
||||
|
||||
@@ -1491,6 +1491,28 @@ def find_common_fields(
|
||||
return common_fields
|
||||
|
||||
|
||||
def get_common_field_ifdef(
|
||||
field_name: str, messages: list[descriptor.DescriptorProto]
|
||||
) -> str | None:
|
||||
"""Get the field_ifdef option if it's consistent across all messages.
|
||||
|
||||
Args:
|
||||
field_name: Name of the field to check
|
||||
messages: List of messages that contain this field
|
||||
|
||||
Returns:
|
||||
The field_ifdef string if all messages have the same value, None otherwise
|
||||
"""
|
||||
field_ifdefs = {
|
||||
get_field_opt(field, pb.field_ifdef)
|
||||
for msg in messages
|
||||
if (field := next((f for f in msg.field if f.name == field_name), None))
|
||||
}
|
||||
|
||||
# Return the ifdef only if all messages agree on the same value
|
||||
return field_ifdefs.pop() if len(field_ifdefs) == 1 else None
|
||||
|
||||
|
||||
def build_base_class(
|
||||
base_class_name: str,
|
||||
common_fields: list[descriptor.FieldDescriptorProto],
|
||||
@@ -1506,9 +1528,14 @@ def build_base_class(
|
||||
for field in common_fields:
|
||||
ti = create_field_type_info(field)
|
||||
|
||||
# Get field_ifdef if it's consistent across all messages
|
||||
field_ifdef = get_common_field_ifdef(field.name, messages)
|
||||
|
||||
# Only add field declarations, not encode/decode logic
|
||||
protected_content.extend(ti.protected_content)
|
||||
public_content.extend(ti.public_content)
|
||||
if ti.protected_content:
|
||||
protected_content.extend(wrap_with_ifdef(ti.protected_content, field_ifdef))
|
||||
if ti.public_content:
|
||||
public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef))
|
||||
|
||||
# Determine if any message using this base class needs decoding
|
||||
needs_decode = any(
|
||||
|
||||
@@ -137,6 +137,10 @@ def should_run_clang_tidy(branch: str | None = None) -> bool:
|
||||
- This ensures all C++ code is checked, including tests, examples, etc.
|
||||
- Examples: esphome/core/component.cpp, tests/custom/my_component.h
|
||||
|
||||
3. The .clang-tidy.hash file itself changed
|
||||
- This indicates the configuration has been updated and clang-tidy should run
|
||||
- Ensures that PRs updating the clang-tidy configuration are properly validated
|
||||
|
||||
If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure
|
||||
code quality is maintained.
|
||||
|
||||
@@ -160,6 +164,12 @@ def should_run_clang_tidy(branch: str | None = None) -> bool:
|
||||
# If hash check fails, run clang-tidy to be safe
|
||||
return True
|
||||
|
||||
# Check if .clang-tidy.hash file itself was changed
|
||||
# This handles the case where the hash was properly updated in the PR
|
||||
files = changed_files(branch)
|
||||
if ".clang-tidy.hash" in files:
|
||||
return True
|
||||
|
||||
return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -262,6 +262,8 @@ def test_should_run_integration_tests_component_dependency() -> None:
|
||||
(0, [], True), # Hash changed - need full scan
|
||||
(1, ["esphome/core.cpp"], True), # C++ file changed
|
||||
(1, ["README.md"], False), # No C++ files changed
|
||||
(1, [".clang-tidy.hash"], True), # Hash file itself changed
|
||||
(1, ["platformio.ini", ".clang-tidy.hash"], True), # Config + hash changed
|
||||
],
|
||||
)
|
||||
def test_should_run_clang_tidy(
|
||||
@@ -277,11 +279,26 @@ def test_should_run_clang_tidy(
|
||||
result = determine_jobs.should_run_clang_tidy()
|
||||
assert result == expected_result
|
||||
|
||||
# Test with hash check failing (exception)
|
||||
if check_returncode != 0:
|
||||
with patch("subprocess.run", side_effect=Exception("Failed")):
|
||||
result = determine_jobs.should_run_clang_tidy()
|
||||
assert result is True # Fail safe - run clang-tidy
|
||||
|
||||
def test_should_run_clang_tidy_hash_check_exception() -> None:
|
||||
"""Test should_run_clang_tidy when hash check fails with exception."""
|
||||
# When hash check fails, clang-tidy should run as a safety measure
|
||||
with (
|
||||
patch.object(determine_jobs, "changed_files", return_value=["README.md"]),
|
||||
patch("subprocess.run", side_effect=Exception("Hash check failed")),
|
||||
):
|
||||
result = determine_jobs.should_run_clang_tidy()
|
||||
assert result is True # Fail safe - run clang-tidy
|
||||
|
||||
# Even with C++ files, exception should trigger clang-tidy
|
||||
with (
|
||||
patch.object(
|
||||
determine_jobs, "changed_files", return_value=["esphome/core.cpp"]
|
||||
),
|
||||
patch("subprocess.run", side_effect=Exception("Hash check failed")),
|
||||
):
|
||||
result = determine_jobs.should_run_clang_tidy()
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_should_run_clang_tidy_with_branch() -> None:
|
||||
@@ -291,7 +308,9 @@ def test_should_run_clang_tidy_with_branch() -> None:
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = Mock(returncode=1) # Hash unchanged
|
||||
determine_jobs.should_run_clang_tidy("release")
|
||||
mock_changed.assert_called_once_with("release")
|
||||
# Changed files is called twice now - once for hash check, once for .clang-tidy.hash check
|
||||
assert mock_changed.call_count == 2
|
||||
mock_changed.assert_has_calls([call("release"), call("release")])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user