diff --git a/nodescraper/models/taskresult.py b/nodescraper/models/taskresult.py index be406cfa..79c5a860 100644 --- a/nodescraper/models/taskresult.py +++ b/nodescraper/models/taskresult.py @@ -235,40 +235,52 @@ def _event_occurrence_count(event: Event) -> int: return 1 return max(1, n) + @staticmethod + def _format_limited_descriptions(msg_counts: dict[str, int], prefix: str) -> str: + """Format the 3 most frequent descriptions, noting how many were left out.""" + items = sorted(msg_counts.items(), key=lambda kv: kv[1], reverse=True) + details = [f"{msg} (x{count})" if count > 1 else msg for msg, count in items[:3]] + summary = f"{prefix}: {', '.join(details)}" + omitted = len(items) - 3 + if omitted > 0: + summary += f" (omitted {omitted} descriptions)" + return summary + def _get_event_summary(self) -> str: """Get summary string for events Returns: - str: event summary with counts and descriptions + str: warning count, error count with the 3 most frequent error + descriptions, and the 3 most frequent critical descriptions """ + total_warnings = 0 + total_errors = 0 error_msg_counts: dict[str, int] = {} - warning_msg_counts: dict[str, int] = {} - + critical_msg_counts: dict[str, int] = {} for event in self.events: n = self._event_occurrence_count(event) - if event.priority == EventPriority.WARNING: - warning_msg_counts[event.description] = ( - warning_msg_counts.get(event.description, 0) + n + if event.priority == EventPriority.CRITICAL: + critical_msg_counts[event.description] = ( + critical_msg_counts.get(event.description, 0) + n ) - elif event.priority >= EventPriority.ERROR: + elif event.priority == EventPriority.ERROR: error_msg_counts[event.description] = error_msg_counts.get(event.description, 0) + n + if event.priority == EventPriority.WARNING: + total_warnings += n + elif event.priority >= EventPriority.ERROR: + total_errors += n summary_parts = [] - - if warning_msg_counts: - total_warnings = sum(warning_msg_counts.values()) - warning_details = [ - f"{msg} (x{count})" if count > 1 else msg - for msg, count in warning_msg_counts.items() - ] - summary_parts.append(f"{total_warnings} warnings: {', '.join(warning_details)}") - - if error_msg_counts: - total_errors = sum(error_msg_counts.values()) - error_details = [ - f"{msg} (x{count})" if count > 1 else msg for msg, count in error_msg_counts.items() - ] - summary_parts.append(f"{total_errors} errors: {', '.join(error_details)}") + if total_warnings: + summary_parts.append(f"{total_warnings} warnings") + if total_errors: + error_summary = f"{total_errors} errors" + if error_msg_counts: + error_summary = self._format_limited_descriptions(error_msg_counts, error_summary) + summary_parts.append(error_summary) + + if critical_msg_counts: + summary_parts.append(self._format_limited_descriptions(critical_msg_counts, "critical")) return "; ".join(summary_parts) diff --git a/test/unit/framework/test_dataanalyzer.py b/test/unit/framework/test_dataanalyzer.py index 78a1c4b0..b5436911 100644 --- a/test/unit/framework/test_dataanalyzer.py +++ b/test/unit/framework/test_dataanalyzer.py @@ -23,9 +23,13 @@ # SOFTWARE. # ############################################################################### +from typing import Optional + from nodescraper.enums.eventpriority import EventPriority from nodescraper.enums.executionstatus import ExecutionStatus from nodescraper.interfaces.dataanalyzertask import analyze_decorator +from nodescraper.models.event import Event +from nodescraper.models.taskresult import TaskResult def test_invalid_data(mock_analyzer, dummy_data_model, system_info): @@ -117,3 +121,65 @@ def test_analyzer_subclass_without_data_model_raises_type_error(): class MissingModelAnalyzer(DataAnalyzer): def analyze_data(self, data, args=None): return self.result + + +def _event(priority: EventPriority, description: str, count: Optional[int] = None) -> Event: + data = {"count": count} if count is not None else {} + return Event(category="OS", description=description, priority=priority, data=data) + + +def test_event_summary_empty_when_no_events(): + assert TaskResult()._get_event_summary() == "" + + +def test_event_summary_counts_warnings_and_errors(): + result = TaskResult( + events=[ + _event(EventPriority.WARNING, "w1"), + _event(EventPriority.WARNING, "w2", count=2), + _event(EventPriority.ERROR, "e1"), + ] + ) + assert result._get_event_summary() == "3 warnings; 1 errors: e1" + + +def test_event_summary_top_three_errors_by_count_with_omission_note(): + result = TaskResult( + events=[ + _event(EventPriority.ERROR, "e1"), + _event(EventPriority.ERROR, "e2", count=4), + _event(EventPriority.ERROR, "e1"), + _event(EventPriority.ERROR, "e3"), + _event(EventPriority.ERROR, "e4"), + ] + ) + assert result._get_event_summary() == ( + "8 errors: e2 (x4), e1 (x2), e3 (omitted 1 descriptions)" + ) + + +def test_event_summary_critical_counted_as_error_and_listed(): + result = TaskResult(events=[_event(EventPriority.CRITICAL, "c1")]) + assert result._get_event_summary() == "1 errors; critical: c1" + + +def test_event_summary_three_critical_no_omission_note(): + result = TaskResult(events=[_event(EventPriority.CRITICAL, f"c{i}") for i in range(3)]) + summary = result._get_event_summary() + assert summary == "3 errors; critical: c0, c1, c2" + assert "omitted" not in summary + + +def test_event_summary_top_three_critical_by_count_with_omission_note(): + result = TaskResult( + events=[ + _event(EventPriority.CRITICAL, "rare"), + _event(EventPriority.CRITICAL, "common", count=5), + _event(EventPriority.CRITICAL, "mid", count=3), + _event(EventPriority.CRITICAL, "also_rare"), + _event(EventPriority.CRITICAL, "second", count=4), + ] + ) + assert result._get_event_summary() == ( + "14 errors; critical: common (x5), second (x4), mid (x3) (omitted 2 descriptions)" + ) diff --git a/test/unit/plugin/test_package_analyzer.py b/test/unit/plugin/test_package_analyzer.py index 2c16e516..8bb5d15e 100644 --- a/test/unit/plugin/test_package_analyzer.py +++ b/test/unit/plugin/test_package_analyzer.py @@ -107,7 +107,7 @@ def test_data_version_regex(package_analyzer, default_data_lib): def test_data_multiple_errors_regex(package_analyzer, default_data_lib): - """Test that detailed error messages are shown for multiple package errors""" + """Test that the first package error descriptions are included in the summary""" args = PackageAnalyzerArgs( exp_package_ver={ "missing-package": None, @@ -118,8 +118,16 @@ def test_data_multiple_errors_regex(package_analyzer, default_data_lib): ) res = package_analyzer.analyze_data(default_data_lib, args=args) assert res.status == ExecutionStatus.ERROR - assert "missing-package" in res.message - assert "another-missing" in res.message + assert res.message == ( + "Package version mismatch (3 errors: " + "Package missing-package not found in the package list, " + r"Package test-ubuntu-package\.x86_64 Version Mismatch, " + r"Expected 2\.\d+ but found 1.11-1.xx11, " + "Package another-missing not found in the package list)" + ) + descriptions = " ".join(event.description for event in res.events) + assert "missing-package" in descriptions + assert "another-missing" in descriptions assert len(res.events) == 3 diff --git a/test/unit/plugin/test_regex_search_analyzer.py b/test/unit/plugin/test_regex_search_analyzer.py index e93b93da..66257fbb 100644 --- a/test/unit/plugin/test_regex_search_analyzer.py +++ b/test/unit/plugin/test_regex_search_analyzer.py @@ -78,8 +78,7 @@ def test_regex_search_analyzer_match(system_info): ) result = analyzer.analyze_data(data, args) assert result.status == ExecutionStatus.ERROR - assert "task detected errors" in result.message - assert "fatal seen" in result.message + assert result.message == "task detected errors (1 errors: fatal seen)" assert len(result.events) == 1 assert result.events[0].description == "fatal seen" @@ -167,8 +166,10 @@ def test_regex_search_plugin_analyzer_only(system_info, logger): }, ) assert out.status == ExecutionStatus.ERROR - assert "Analysis error:" in out.message - assert "found" in out.message + assert out.message.startswith( + "Analysis error: task detected errors (1 errors: found [file: " + ) + assert out.message.endswith("])") assert out.result_data.analysis_result.status == ExecutionStatus.ERROR assert len(out.result_data.analysis_result.events) == 1 desc = out.result_data.analysis_result.events[0].description diff --git a/test/unit/plugin/test_sysctl_analyzer.py b/test/unit/plugin/test_sysctl_analyzer.py index dacca202..c96bd6c8 100644 --- a/test/unit/plugin/test_sysctl_analyzer.py +++ b/test/unit/plugin/test_sysctl_analyzer.py @@ -63,6 +63,7 @@ def test_analyzer_mismatch(analyzer, correct_data): args = SysctlAnalyzerArgs(exp_vm_swappiness=3, exp_vm_numa_balancing=4) result = analyzer.analyze_data(correct_data, args) assert result.status == ExecutionStatus.ERROR - assert "2 sysctl parameter(s) mismatched." in result.message - assert "1 errors" in result.message - assert "Sysctl mismatch detected" in result.message + assert result.message == ( + "2 sysctl parameter(s) mismatched. (1 errors: Sysctl mismatch detected)" + ) + assert result.events[0].description == "Sysctl mismatch detected"