fix(sonar): generic test execution duration must be positive for SQ 26.x
CI / Rust Format (pull_request) Successful in 23s
CI / TypeScript Lint + Typecheck (pull_request) Successful in 26s
CI / TypeScript Tests + Coverage (pull_request) Successful in 38s
CI / Rust Tests + Coverage (pull_request) Successful in 42s
CI / Dependency-Track (BOM) (pull_request) Successful in 22s
CI / Clippy (SARIF) (pull_request) Successful in 1m21s
CI / SonarQube (pull_request) Failing after 31s
CI / Electron Release Build (pull_request) Successful in 3m20s
CI / E2E Tests (Playwright + Electron) (pull_request) Successful in 6m43s

Gitea Actions reaches SonarQube fine; the scanner failed parsing our
converted reports because many testCase elements had duration="0".
Clamp to ≥1 ms and emit a standard XML declaration. Restore Rust +
Vitest paths in sonar.testExecutionReportPaths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-09 01:12:04 -04:00
parent 11dba0d5b7
commit bb153b0e3d
2 changed files with 15 additions and 6 deletions
+12 -2
View File
@@ -143,6 +143,11 @@ def collect_vitest(junit_path: Path, repo: Path) -> ByFile:
return by_file
def _sonar_duration_ms(ms: int) -> int:
"""SonarQube 26.x generic test execution rejects duration=0 (expects positive ms)."""
return max(ms, 1)
def by_file_to_element(by_file: ByFile) -> ET.Element:
out = ET.Element("testExecutions")
out.set("version", "1")
@@ -153,7 +158,7 @@ def by_file_to_element(by_file: ByFile) -> ET.Element:
for display_name, ms, status in sorted(by_file[path], key=lambda x: x[0]):
te = ET.SubElement(fe, "testCase")
te.set("name", display_name)
te.set("duration", str(ms))
te.set("duration", str(_sonar_duration_ms(ms)))
if status == "skipped":
ET.SubElement(te, "skipped")
elif status == "failure":
@@ -190,12 +195,17 @@ def _write_xml(elem: ET.Element, out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
tree = ET.ElementTree(elem)
ET.indent(tree, space=" ")
# Avoid Python's single-quoted XML declaration; keep UTF-8 header aligned with JUnit producers.
tree.write(
out_path,
encoding="utf-8",
xml_declaration=True,
xml_declaration=False,
default_namespace=None,
)
raw = out_path.read_bytes()
out_path.write_bytes(
b'<?xml version="1.0" encoding="UTF-8"?>\n' + raw.lstrip()
)
def main() -> int:
+3 -4
View File
@@ -14,10 +14,9 @@ sonar.sarifReportPaths=target/clippy-report.sarif
# Rust: CI converts target/lcov.info → target/sonar-rust-coverage.xml (Generic Coverage).
# TypeScript still uses sonar.javascript.lcov.reportPaths below.
sonar.coverageReportPaths=target/sonar-rust-coverage.xml
# Generic Test Execution: Vitest JUnit converted in CI (see scripts/junit_to_sonar_test_execution.py).
# SonarQube 26.4+ rejects our Rust report: paths map to library sources (inline #[cfg(test)]), not dedicated test files.
# Rust coverage still comes from sonar.coverageReportPaths; nextest conversion remains in CI for debugging/ future use.
sonar.testExecutionReportPaths=ui-reports/test-results/sonar-test-execution.xml
# Generic Test Execution: nextest + Vitest JUnit → Sonar XML in CI (scripts/junit_to_sonar_test_execution.py).
# SonarQube 26.x rejects duration="0"; the script clamps to ≥1 ms. Paths match artifact layout in the sonarqube job.
sonar.testExecutionReportPaths=target/nextest/ci/sonar-test-execution.xml,ui-reports/test-results/sonar-test-execution.xml
sonar.scm.provider=git