fbuild 1.2.8__py3-none-any.whl → 1.2.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fbuild/__init__.py +5 -1
- fbuild/build/configurable_compiler.py +49 -6
- fbuild/build/configurable_linker.py +14 -9
- fbuild/build/orchestrator_esp32.py +6 -3
- fbuild/build/orchestrator_rp2040.py +6 -2
- fbuild/cli.py +300 -5
- fbuild/config/ini_parser.py +13 -1
- fbuild/daemon/__init__.py +11 -0
- fbuild/daemon/async_client.py +5 -4
- fbuild/daemon/async_client_lib.py +1543 -0
- fbuild/daemon/async_protocol.py +825 -0
- fbuild/daemon/async_server.py +2100 -0
- fbuild/daemon/client.py +425 -13
- fbuild/daemon/configuration_lock.py +13 -13
- fbuild/daemon/connection.py +508 -0
- fbuild/daemon/connection_registry.py +579 -0
- fbuild/daemon/daemon.py +517 -164
- fbuild/daemon/daemon_context.py +72 -1
- fbuild/daemon/device_discovery.py +477 -0
- fbuild/daemon/device_manager.py +821 -0
- fbuild/daemon/error_collector.py +263 -263
- fbuild/daemon/file_cache.py +332 -332
- fbuild/daemon/firmware_ledger.py +46 -123
- fbuild/daemon/lock_manager.py +508 -508
- fbuild/daemon/messages.py +431 -0
- fbuild/daemon/operation_registry.py +288 -288
- fbuild/daemon/processors/build_processor.py +34 -1
- fbuild/daemon/processors/deploy_processor.py +1 -3
- fbuild/daemon/processors/locking_processor.py +7 -7
- fbuild/daemon/request_processor.py +457 -457
- fbuild/daemon/shared_serial.py +7 -7
- fbuild/daemon/status_manager.py +238 -238
- fbuild/daemon/subprocess_manager.py +316 -316
- fbuild/deploy/docker_utils.py +182 -2
- fbuild/deploy/monitor.py +1 -1
- fbuild/deploy/qemu_runner.py +71 -13
- fbuild/ledger/board_ledger.py +46 -122
- fbuild/output.py +238 -2
- fbuild/packages/library_compiler.py +15 -5
- fbuild/packages/library_manager.py +12 -6
- fbuild-1.2.15.dist-info/METADATA +569 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
- fbuild-1.2.8.dist-info/METADATA +0 -468
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
fbuild/daemon/error_collector.py
CHANGED
|
@@ -1,263 +1,263 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Error Collector - Structured error collection for async operations.
|
|
3
|
-
|
|
4
|
-
This module provides error collection and aggregation for asynchronous build
|
|
5
|
-
operations, replacing simple exception handling with structured error tracking.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
from dataclasses import dataclass, field
|
|
12
|
-
from enum import Enum
|
|
13
|
-
from typing import Optional
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ErrorSeverity(Enum):
|
|
17
|
-
"""Severity level of a build error."""
|
|
18
|
-
|
|
19
|
-
WARNING = "warning"
|
|
20
|
-
ERROR = "error"
|
|
21
|
-
FATAL = "fatal"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class BuildError:
|
|
26
|
-
"""Single build error."""
|
|
27
|
-
|
|
28
|
-
severity: ErrorSeverity
|
|
29
|
-
phase: str # "download", "compile", "link", "upload"
|
|
30
|
-
file_path: Optional[str]
|
|
31
|
-
error_message: str
|
|
32
|
-
stderr: Optional[str] = None
|
|
33
|
-
stdout: Optional[str] = None
|
|
34
|
-
timestamp: float = field(default_factory=time.time)
|
|
35
|
-
|
|
36
|
-
def format(self) -> str:
|
|
37
|
-
"""Format error as human-readable string.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
Formatted error message
|
|
41
|
-
"""
|
|
42
|
-
lines = [f"[{self.severity.value.upper()}] {self.phase}: {self.error_message}"]
|
|
43
|
-
|
|
44
|
-
if self.file_path:
|
|
45
|
-
lines.append(f" File: {self.file_path}")
|
|
46
|
-
|
|
47
|
-
if self.stderr:
|
|
48
|
-
# Truncate stderr to reasonable length
|
|
49
|
-
stderr_preview = self.stderr[:500]
|
|
50
|
-
if len(self.stderr) > 500:
|
|
51
|
-
stderr_preview += "... (truncated)"
|
|
52
|
-
lines.append(f" stderr: {stderr_preview}")
|
|
53
|
-
|
|
54
|
-
return "\n".join(lines)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class ErrorCollector:
|
|
58
|
-
"""Collects errors during async build operations."""
|
|
59
|
-
|
|
60
|
-
def __init__(self, max_errors: int = 100):
|
|
61
|
-
"""Initialize error collector.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
max_errors: Maximum number of errors to collect
|
|
65
|
-
"""
|
|
66
|
-
self.errors: list[BuildError] = []
|
|
67
|
-
self.lock = threading.Lock()
|
|
68
|
-
self.max_errors = max_errors
|
|
69
|
-
|
|
70
|
-
logging.debug(f"ErrorCollector initialized (max_errors={max_errors})")
|
|
71
|
-
|
|
72
|
-
def add_error(self, error: BuildError) -> None:
|
|
73
|
-
"""Add error to collection.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
error: Build error to add
|
|
77
|
-
"""
|
|
78
|
-
with self.lock:
|
|
79
|
-
if len(self.errors) >= self.max_errors:
|
|
80
|
-
logging.warning(f"ErrorCollector full ({self.max_errors} errors), dropping oldest")
|
|
81
|
-
self.errors.pop(0)
|
|
82
|
-
|
|
83
|
-
self.errors.append(error)
|
|
84
|
-
|
|
85
|
-
def get_errors(self, severity: Optional[ErrorSeverity] = None) -> list[BuildError]:
|
|
86
|
-
"""Get all errors, optionally filtered by severity.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
severity: Filter by severity (None = all errors)
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
List of build errors
|
|
93
|
-
"""
|
|
94
|
-
logging.debug(f"Retrieving errors (severity filter: {severity.value if severity else 'None'})")
|
|
95
|
-
with self.lock:
|
|
96
|
-
if severity:
|
|
97
|
-
filtered = [e for e in self.errors if e.severity == severity]
|
|
98
|
-
logging.debug(f"Filtered {len(filtered)} errors by severity {severity.value} (total: {len(self.errors)})")
|
|
99
|
-
return filtered
|
|
100
|
-
logging.debug(f"Returning all {len(self.errors)} errors")
|
|
101
|
-
return self.errors.copy()
|
|
102
|
-
|
|
103
|
-
def get_errors_by_phase(self, phase: str) -> list[BuildError]:
|
|
104
|
-
"""Get errors for a specific phase.
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
phase: Phase to filter by
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
List of build errors for the phase
|
|
111
|
-
"""
|
|
112
|
-
with self.lock:
|
|
113
|
-
phase_errors = [e for e in self.errors if e.phase == phase]
|
|
114
|
-
logging.debug(f"Found {len(phase_errors)} errors in phase '{phase}' (total: {len(self.errors)})")
|
|
115
|
-
return phase_errors
|
|
116
|
-
|
|
117
|
-
def has_fatal_errors(self) -> bool:
|
|
118
|
-
"""Check if any fatal errors occurred.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
True if fatal errors exist
|
|
122
|
-
"""
|
|
123
|
-
with self.lock:
|
|
124
|
-
has_fatal = any(e.severity == ErrorSeverity.FATAL for e in self.errors)
|
|
125
|
-
fatal_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL)
|
|
126
|
-
logging.debug(f"Fatal error check result: {has_fatal} ({fatal_count} fatal errors)")
|
|
127
|
-
return has_fatal
|
|
128
|
-
|
|
129
|
-
def has_errors(self) -> bool:
|
|
130
|
-
"""Check if any errors (non-warning) occurred.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
True if errors exist
|
|
134
|
-
"""
|
|
135
|
-
logging.debug("Checking for errors (non-warning)")
|
|
136
|
-
with self.lock:
|
|
137
|
-
has_errs = any(e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL) for e in self.errors)
|
|
138
|
-
error_count = sum(1 for e in self.errors if e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL))
|
|
139
|
-
logging.debug(f"Error check result: {has_errs} ({error_count} errors or fatal)")
|
|
140
|
-
return has_errs
|
|
141
|
-
|
|
142
|
-
def has_warnings(self) -> bool:
|
|
143
|
-
"""Check if any warnings occurred.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
True if warnings exist
|
|
147
|
-
"""
|
|
148
|
-
with self.lock:
|
|
149
|
-
has_warn = any(e.severity == ErrorSeverity.WARNING for e in self.errors)
|
|
150
|
-
warning_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING)
|
|
151
|
-
logging.debug(f"Warning check result: {has_warn} ({warning_count} warnings)")
|
|
152
|
-
return has_warn
|
|
153
|
-
|
|
154
|
-
def get_error_count(self) -> dict[str, int]:
|
|
155
|
-
"""Get count of errors by severity.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
Dictionary with counts by severity
|
|
159
|
-
"""
|
|
160
|
-
with self.lock:
|
|
161
|
-
counts = {
|
|
162
|
-
"warnings": sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING),
|
|
163
|
-
"errors": sum(1 for e in self.errors if e.severity == ErrorSeverity.ERROR),
|
|
164
|
-
"fatal": sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL),
|
|
165
|
-
"total": len(self.errors),
|
|
166
|
-
}
|
|
167
|
-
logging.debug(f"Error counts: {counts['total']} total ({counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings)")
|
|
168
|
-
return counts
|
|
169
|
-
|
|
170
|
-
def format_errors(self, max_errors: Optional[int] = None) -> str:
|
|
171
|
-
"""Format all errors as human-readable string.
|
|
172
|
-
|
|
173
|
-
Args:
|
|
174
|
-
max_errors: Maximum number of errors to include (None = all)
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
Formatted error report
|
|
178
|
-
"""
|
|
179
|
-
logging.debug(f"Formatting errors (max_errors: {max_errors})")
|
|
180
|
-
with self.lock:
|
|
181
|
-
if not self.errors:
|
|
182
|
-
return "No errors"
|
|
183
|
-
|
|
184
|
-
errors_to_show = self.errors if max_errors is None else self.errors[:max_errors]
|
|
185
|
-
logging.debug(f"Formatting {len(errors_to_show)} errors (total: {len(self.errors)})")
|
|
186
|
-
lines = []
|
|
187
|
-
|
|
188
|
-
for err in errors_to_show:
|
|
189
|
-
lines.append(err.format())
|
|
190
|
-
|
|
191
|
-
if max_errors and len(self.errors) > max_errors:
|
|
192
|
-
lines.append(f"\n... and {len(self.errors) - max_errors} more errors")
|
|
193
|
-
|
|
194
|
-
# Add summary
|
|
195
|
-
counts = self.get_error_count()
|
|
196
|
-
summary = f"\nSummary: {counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings"
|
|
197
|
-
lines.append(summary)
|
|
198
|
-
|
|
199
|
-
formatted = "\n\n".join(lines)
|
|
200
|
-
logging.debug(f"Error formatting complete: {len(lines)} sections, {len(formatted)} characters")
|
|
201
|
-
return formatted
|
|
202
|
-
|
|
203
|
-
def format_summary(self) -> str:
|
|
204
|
-
"""Format a brief summary of errors.
|
|
205
|
-
|
|
206
|
-
Returns:
|
|
207
|
-
Brief error summary
|
|
208
|
-
"""
|
|
209
|
-
counts = self.get_error_count()
|
|
210
|
-
if counts["total"] == 0:
|
|
211
|
-
return "No errors"
|
|
212
|
-
|
|
213
|
-
parts = []
|
|
214
|
-
if counts["fatal"] > 0:
|
|
215
|
-
parts.append(f"{counts['fatal']} fatal")
|
|
216
|
-
if counts["errors"] > 0:
|
|
217
|
-
parts.append(f"{counts['errors']} errors")
|
|
218
|
-
if counts["warnings"] > 0:
|
|
219
|
-
parts.append(f"{counts['warnings']} warnings")
|
|
220
|
-
|
|
221
|
-
summary = ", ".join(parts)
|
|
222
|
-
return summary
|
|
223
|
-
|
|
224
|
-
def clear(self) -> None:
|
|
225
|
-
"""Clear all collected errors."""
|
|
226
|
-
with self.lock:
|
|
227
|
-
error_count = len(self.errors)
|
|
228
|
-
self.errors.clear()
|
|
229
|
-
|
|
230
|
-
if error_count > 0:
|
|
231
|
-
logging.info(f"Cleared {error_count} errors")
|
|
232
|
-
|
|
233
|
-
def get_first_fatal_error(self) -> Optional[BuildError]:
|
|
234
|
-
"""Get the first fatal error encountered.
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
First fatal error or None
|
|
238
|
-
"""
|
|
239
|
-
with self.lock:
|
|
240
|
-
for error in self.errors:
|
|
241
|
-
if error.severity == ErrorSeverity.FATAL:
|
|
242
|
-
return error
|
|
243
|
-
return None
|
|
244
|
-
|
|
245
|
-
def get_compilation_errors(self) -> list[BuildError]:
|
|
246
|
-
"""Get all compilation-phase errors.
|
|
247
|
-
|
|
248
|
-
Returns:
|
|
249
|
-
List of compilation errors
|
|
250
|
-
"""
|
|
251
|
-
compilation_errors = self.get_errors_by_phase("compile")
|
|
252
|
-
logging.debug(f"Found {len(compilation_errors)} compilation errors")
|
|
253
|
-
return compilation_errors
|
|
254
|
-
|
|
255
|
-
def get_link_errors(self) -> list[BuildError]:
|
|
256
|
-
"""Get all link-phase errors.
|
|
257
|
-
|
|
258
|
-
Returns:
|
|
259
|
-
List of link errors
|
|
260
|
-
"""
|
|
261
|
-
link_errors = self.get_errors_by_phase("link")
|
|
262
|
-
logging.debug(f"Found {len(link_errors)} link errors")
|
|
263
|
-
return link_errors
|
|
1
|
+
"""
|
|
2
|
+
Error Collector - Structured error collection for async operations.
|
|
3
|
+
|
|
4
|
+
This module provides error collection and aggregation for asynchronous build
|
|
5
|
+
operations, replacing simple exception handling with structured error tracking.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ErrorSeverity(Enum):
|
|
17
|
+
"""Severity level of a build error."""
|
|
18
|
+
|
|
19
|
+
WARNING = "warning"
|
|
20
|
+
ERROR = "error"
|
|
21
|
+
FATAL = "fatal"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BuildError:
|
|
26
|
+
"""Single build error."""
|
|
27
|
+
|
|
28
|
+
severity: ErrorSeverity
|
|
29
|
+
phase: str # "download", "compile", "link", "upload"
|
|
30
|
+
file_path: Optional[str]
|
|
31
|
+
error_message: str
|
|
32
|
+
stderr: Optional[str] = None
|
|
33
|
+
stdout: Optional[str] = None
|
|
34
|
+
timestamp: float = field(default_factory=time.time)
|
|
35
|
+
|
|
36
|
+
def format(self) -> str:
|
|
37
|
+
"""Format error as human-readable string.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Formatted error message
|
|
41
|
+
"""
|
|
42
|
+
lines = [f"[{self.severity.value.upper()}] {self.phase}: {self.error_message}"]
|
|
43
|
+
|
|
44
|
+
if self.file_path:
|
|
45
|
+
lines.append(f" File: {self.file_path}")
|
|
46
|
+
|
|
47
|
+
if self.stderr:
|
|
48
|
+
# Truncate stderr to reasonable length
|
|
49
|
+
stderr_preview = self.stderr[:500]
|
|
50
|
+
if len(self.stderr) > 500:
|
|
51
|
+
stderr_preview += "... (truncated)"
|
|
52
|
+
lines.append(f" stderr: {stderr_preview}")
|
|
53
|
+
|
|
54
|
+
return "\n".join(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ErrorCollector:
|
|
58
|
+
"""Collects errors during async build operations."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, max_errors: int = 100):
|
|
61
|
+
"""Initialize error collector.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
max_errors: Maximum number of errors to collect
|
|
65
|
+
"""
|
|
66
|
+
self.errors: list[BuildError] = []
|
|
67
|
+
self.lock = threading.Lock()
|
|
68
|
+
self.max_errors = max_errors
|
|
69
|
+
|
|
70
|
+
logging.debug(f"ErrorCollector initialized (max_errors={max_errors})")
|
|
71
|
+
|
|
72
|
+
def add_error(self, error: BuildError) -> None:
|
|
73
|
+
"""Add error to collection.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
error: Build error to add
|
|
77
|
+
"""
|
|
78
|
+
with self.lock:
|
|
79
|
+
if len(self.errors) >= self.max_errors:
|
|
80
|
+
logging.warning(f"ErrorCollector full ({self.max_errors} errors), dropping oldest")
|
|
81
|
+
self.errors.pop(0)
|
|
82
|
+
|
|
83
|
+
self.errors.append(error)
|
|
84
|
+
|
|
85
|
+
def get_errors(self, severity: Optional[ErrorSeverity] = None) -> list[BuildError]:
|
|
86
|
+
"""Get all errors, optionally filtered by severity.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
severity: Filter by severity (None = all errors)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
List of build errors
|
|
93
|
+
"""
|
|
94
|
+
logging.debug(f"Retrieving errors (severity filter: {severity.value if severity else 'None'})")
|
|
95
|
+
with self.lock:
|
|
96
|
+
if severity:
|
|
97
|
+
filtered = [e for e in self.errors if e.severity == severity]
|
|
98
|
+
logging.debug(f"Filtered {len(filtered)} errors by severity {severity.value} (total: {len(self.errors)})")
|
|
99
|
+
return filtered
|
|
100
|
+
logging.debug(f"Returning all {len(self.errors)} errors")
|
|
101
|
+
return self.errors.copy()
|
|
102
|
+
|
|
103
|
+
def get_errors_by_phase(self, phase: str) -> list[BuildError]:
|
|
104
|
+
"""Get errors for a specific phase.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
phase: Phase to filter by
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of build errors for the phase
|
|
111
|
+
"""
|
|
112
|
+
with self.lock:
|
|
113
|
+
phase_errors = [e for e in self.errors if e.phase == phase]
|
|
114
|
+
logging.debug(f"Found {len(phase_errors)} errors in phase '{phase}' (total: {len(self.errors)})")
|
|
115
|
+
return phase_errors
|
|
116
|
+
|
|
117
|
+
def has_fatal_errors(self) -> bool:
|
|
118
|
+
"""Check if any fatal errors occurred.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if fatal errors exist
|
|
122
|
+
"""
|
|
123
|
+
with self.lock:
|
|
124
|
+
has_fatal = any(e.severity == ErrorSeverity.FATAL for e in self.errors)
|
|
125
|
+
fatal_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL)
|
|
126
|
+
logging.debug(f"Fatal error check result: {has_fatal} ({fatal_count} fatal errors)")
|
|
127
|
+
return has_fatal
|
|
128
|
+
|
|
129
|
+
def has_errors(self) -> bool:
|
|
130
|
+
"""Check if any errors (non-warning) occurred.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if errors exist
|
|
134
|
+
"""
|
|
135
|
+
logging.debug("Checking for errors (non-warning)")
|
|
136
|
+
with self.lock:
|
|
137
|
+
has_errs = any(e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL) for e in self.errors)
|
|
138
|
+
error_count = sum(1 for e in self.errors if e.severity in (ErrorSeverity.ERROR, ErrorSeverity.FATAL))
|
|
139
|
+
logging.debug(f"Error check result: {has_errs} ({error_count} errors or fatal)")
|
|
140
|
+
return has_errs
|
|
141
|
+
|
|
142
|
+
def has_warnings(self) -> bool:
|
|
143
|
+
"""Check if any warnings occurred.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if warnings exist
|
|
147
|
+
"""
|
|
148
|
+
with self.lock:
|
|
149
|
+
has_warn = any(e.severity == ErrorSeverity.WARNING for e in self.errors)
|
|
150
|
+
warning_count = sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING)
|
|
151
|
+
logging.debug(f"Warning check result: {has_warn} ({warning_count} warnings)")
|
|
152
|
+
return has_warn
|
|
153
|
+
|
|
154
|
+
def get_error_count(self) -> dict[str, int]:
|
|
155
|
+
"""Get count of errors by severity.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dictionary with counts by severity
|
|
159
|
+
"""
|
|
160
|
+
with self.lock:
|
|
161
|
+
counts = {
|
|
162
|
+
"warnings": sum(1 for e in self.errors if e.severity == ErrorSeverity.WARNING),
|
|
163
|
+
"errors": sum(1 for e in self.errors if e.severity == ErrorSeverity.ERROR),
|
|
164
|
+
"fatal": sum(1 for e in self.errors if e.severity == ErrorSeverity.FATAL),
|
|
165
|
+
"total": len(self.errors),
|
|
166
|
+
}
|
|
167
|
+
logging.debug(f"Error counts: {counts['total']} total ({counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings)")
|
|
168
|
+
return counts
|
|
169
|
+
|
|
170
|
+
def format_errors(self, max_errors: Optional[int] = None) -> str:
|
|
171
|
+
"""Format all errors as human-readable string.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
max_errors: Maximum number of errors to include (None = all)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Formatted error report
|
|
178
|
+
"""
|
|
179
|
+
logging.debug(f"Formatting errors (max_errors: {max_errors})")
|
|
180
|
+
with self.lock:
|
|
181
|
+
if not self.errors:
|
|
182
|
+
return "No errors"
|
|
183
|
+
|
|
184
|
+
errors_to_show = self.errors if max_errors is None else self.errors[:max_errors]
|
|
185
|
+
logging.debug(f"Formatting {len(errors_to_show)} errors (total: {len(self.errors)})")
|
|
186
|
+
lines = []
|
|
187
|
+
|
|
188
|
+
for err in errors_to_show:
|
|
189
|
+
lines.append(err.format())
|
|
190
|
+
|
|
191
|
+
if max_errors and len(self.errors) > max_errors:
|
|
192
|
+
lines.append(f"\n... and {len(self.errors) - max_errors} more errors")
|
|
193
|
+
|
|
194
|
+
# Add summary
|
|
195
|
+
counts = self.get_error_count()
|
|
196
|
+
summary = f"\nSummary: {counts['fatal']} fatal, {counts['errors']} errors, {counts['warnings']} warnings"
|
|
197
|
+
lines.append(summary)
|
|
198
|
+
|
|
199
|
+
formatted = "\n\n".join(lines)
|
|
200
|
+
logging.debug(f"Error formatting complete: {len(lines)} sections, {len(formatted)} characters")
|
|
201
|
+
return formatted
|
|
202
|
+
|
|
203
|
+
def format_summary(self) -> str:
|
|
204
|
+
"""Format a brief summary of errors.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Brief error summary
|
|
208
|
+
"""
|
|
209
|
+
counts = self.get_error_count()
|
|
210
|
+
if counts["total"] == 0:
|
|
211
|
+
return "No errors"
|
|
212
|
+
|
|
213
|
+
parts = []
|
|
214
|
+
if counts["fatal"] > 0:
|
|
215
|
+
parts.append(f"{counts['fatal']} fatal")
|
|
216
|
+
if counts["errors"] > 0:
|
|
217
|
+
parts.append(f"{counts['errors']} errors")
|
|
218
|
+
if counts["warnings"] > 0:
|
|
219
|
+
parts.append(f"{counts['warnings']} warnings")
|
|
220
|
+
|
|
221
|
+
summary = ", ".join(parts)
|
|
222
|
+
return summary
|
|
223
|
+
|
|
224
|
+
def clear(self) -> None:
|
|
225
|
+
"""Clear all collected errors."""
|
|
226
|
+
with self.lock:
|
|
227
|
+
error_count = len(self.errors)
|
|
228
|
+
self.errors.clear()
|
|
229
|
+
|
|
230
|
+
if error_count > 0:
|
|
231
|
+
logging.info(f"Cleared {error_count} errors")
|
|
232
|
+
|
|
233
|
+
def get_first_fatal_error(self) -> Optional[BuildError]:
|
|
234
|
+
"""Get the first fatal error encountered.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
First fatal error or None
|
|
238
|
+
"""
|
|
239
|
+
with self.lock:
|
|
240
|
+
for error in self.errors:
|
|
241
|
+
if error.severity == ErrorSeverity.FATAL:
|
|
242
|
+
return error
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def get_compilation_errors(self) -> list[BuildError]:
|
|
246
|
+
"""Get all compilation-phase errors.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of compilation errors
|
|
250
|
+
"""
|
|
251
|
+
compilation_errors = self.get_errors_by_phase("compile")
|
|
252
|
+
logging.debug(f"Found {len(compilation_errors)} compilation errors")
|
|
253
|
+
return compilation_errors
|
|
254
|
+
|
|
255
|
+
def get_link_errors(self) -> list[BuildError]:
|
|
256
|
+
"""Get all link-phase errors.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List of link errors
|
|
260
|
+
"""
|
|
261
|
+
link_errors = self.get_errors_by_phase("link")
|
|
262
|
+
logging.debug(f"Found {len(link_errors)} link errors")
|
|
263
|
+
return link_errors
|