Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ The root README shows totals for integration failures, validator check errors
(`ERROR !!`), and validator check FAILs (`FAIL`), plus separate per-fabric columns.
It highlights fabrics with check errors and links to the affected checks. Each
fabric's README shows its source pipeline, commit, run date, and result details.
Both summaries include source and target version columns parsed from the log:
the current APIC version and the selected target APIC version. If no firmware
was detected, the target is explicitly not selected. Missing logs show versions
as unavailable; missing or conflicting version headers show unknown values.
`error.txt` is reserved for integration execution or collection failures and
includes this provenance even when a repeated failure has identical diagnostics.
An unavailable or unrecognized validator summary is shown explicitly and is
Expand Down
43 changes: 36 additions & 7 deletions ci/publish_integration_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,23 @@ def validate_source(manifest, environment):
def validator_result(report):
"""Read the validator's summary; keep check errors distinct from FAIL findings."""
result = {"status": "not_available", "error_count": None, "fail_count": None,
"error_checks": [], "fail_checks": []}
"error_checks": [], "fail_checks": [], "source_version": None,
"target_version": None, "target_version_status": "not_available"}
if not report.is_file():
return result
text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", report.read_text(encoding="utf-8", errors="replace"))
# Read the run header, not firmware candidates or versions in check details.
header = re.split(r"^\[Check\s+\d+/\d+\]", text, maxsplit=1, flags=re.MULTILINE)[0]
version = r"(\d+\.\d+\([0-9A-Za-z_.-]+\))"
sources = re.findall(r"^Current APIC Version\.\.\.\s*" + version + r"\s*$", header, re.MULTILINE)
targets = re.findall(r'^You have chosen version "' + version + r'"\s*$', header, re.MULTILINE)
no_firmware = bool(re.search(r"^No Firmware Detected!", header, re.MULTILINE))
result["source_version"] = sources[0] if len(sources) == 1 else None
result["target_version_status"] = "unknown"
if len(targets) == 1 and not no_firmware:
result.update(target_version=targets[0], target_version_status="selected")
elif not targets and no_firmware:
result["target_version_status"] = "not_selected"
lines = text.splitlines()
starts = [index for index, line in enumerate(lines)
if re.match(r"^\[Check\s+\d+/\d+\]", line)]
Expand Down Expand Up @@ -115,15 +128,26 @@ def count_link(result, category, folder=""):
return str(count)


def version_cell(result, category):
value = result[category + "_version"]
if value:
return markdown_label(value)
if result["status"] == "not_available":
return "Not available"
if category == "target" and result["target_version_status"] == "not_selected":
return "Not selected (no firmware)"
return "Unknown"


def fabric_readme(fabric, manifest, errors, result, links):
lines = [
"# " + markdown_label(fabric["name"]), "",
"- GitLab pipeline: {}".format(manifest["source_pipeline_id"]),
"- Script commit: `{}`".format(manifest["source_commit"]),
"- Run date: {}".format(manifest.get("created_at", "")), "",
"| Integration / collection | Validator check errors | Validator check FAILs |",
"| --- | --- | --- |",
"| {} | {} | {} |".format("Failed" if errors else "Completed", count_link(result, "error"), count_link(result, "fail")), "",
"| Source version | Target version | Integration / collection | Validator check errors | Validator check FAILs |",
"| --- | --- | --- | --- | --- |",
"| {} | {} | {} | {} | {} |".format(version_cell(result, "source"), version_cell(result, "target"), "Failed" if errors else "Completed", count_link(result, "error"), count_link(result, "fail")), "",
"Files: " + " · ".join(links), "",
]
if errors:
Expand Down Expand Up @@ -186,8 +210,9 @@ def prepare_snapshot(source, destination, environment):
(output / "README.md").write_text(fabric_readme(fabric, manifest, errors, result, local_links), encoding="utf-8")
links.insert(0, "[summary]({}/README.md)".format(quote(folder)))
rows.append(
"| [{}]({}/README.md) | {} | {} | {} | {} |".format(
"| [{}]({}/README.md) | {} | {} | {} | {} | {} | {} |".format(
markdown_label(fabric["name"]), quote(folder),
version_cell(result, "source"), version_cell(result, "target"),
"Failed" if errors else "Completed",
count_link(result, "error", folder), count_link(result, "fail", folder),
" · ".join(links),
Expand Down Expand Up @@ -223,6 +248,10 @@ def prepare_snapshot(source, destination, environment):
"Validator check errors (`ERROR !!`) and validator check FAILs (`FAIL`) are shown separately; "
"completed does not mean upgrade-ready. Missing or unrecognized summaries are not counted as zero.",
"",
"Source version is the current APIC version recorded in the validator log. "
"Target version is the selected APIC version recorded in that same run; "
"no firmware means no target was selected. Versions are unavailable when no log was collected.",
"",
]
if validator_errors:
readme.extend(["### Fabrics with validator check errors", ""])
Expand All @@ -232,8 +261,8 @@ def prepare_snapshot(source, destination, environment):
for fabric in validator_errors)
readme.append("")
readme.extend([
"| Fabric | Integration / collection | Validator check errors | Validator check FAILs | Files |",
"| --- | --- | --- | --- | --- |",
"| Fabric | Source version | Target version | Integration / collection | Validator check errors | Validator check FAILs | Files |",
"| --- | --- | --- | --- | --- | --- | --- |",
])
readme.extend(rows)
readme.extend(
Expand Down
44 changes: 44 additions & 0 deletions ci/tests/test_publish_integration_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,50 @@ def test_unknown_validator_summary_does_not_claim_zero_errors(self):
snapshot = self.prepare()
self.assertIn("Unknown", (snapshot / "fabric-a/README.md").read_text())

def test_versions_use_current_apic_and_selected_target_from_run_header(self):
report = self.source / "fabric-a/results.log"
original = report.read_bytes()
report.write_bytes(
b"Script Version v4.3.0\n"
b"\x1b[32mCurrent APIC Version...6.1(5e)\x1b[0m\n"
b"Lowest Switch Version...6.0(7e)\n"
b"[1]: aci-apic-dk9.4.2.7u.bin\n"
b'You have chosen version "5.3(2a)"\n' + original
)
snapshot = self.prepare()
result = json.loads((snapshot / "manifest.json").read_text())["fabrics"][0]["validator_result"]
self.assertEqual(result["source_version"], "6.1(5e)")
self.assertEqual(result["target_version"], "5.3(2a)")
self.assertEqual(result["target_version_status"], "selected")
for readme in (snapshot / "README.md", snapshot / "fabric-a/README.md"):
self.assertIn("| 6.1(5e) | 5.3(2a) | Completed |", readme.read_text())
self.assertEqual((snapshot / "fabric-a/results.log").read_bytes(), report.read_bytes())

def test_versions_distinguish_no_firmware_from_missing_or_conflicting_headers(self):
report = self.source / "fabric-a/results.log"
summary = report.read_text()
report.write_text("Current APIC Version...1.2(3.456)\n"
"No Firmware Detected! Please Upload APIC Firmware and re-run the script.\n" + summary)
snapshot = self.prepare()
result = publisher.validator_result(report)
self.assertEqual(result["source_version"], "1.2(3.456)")
self.assertIsNone(result["target_version"])
self.assertEqual(result["target_version_status"], "not_selected")
self.assertIn("Not selected (no firmware)", (snapshot / "README.md").read_text())
for header in (
"",
'You have chosen version "6.0(9e)"\nYou have chosen version "6.1(6g)"\n',
'You have chosen version "6.0(9e)"\nNo Firmware Detected!\n',
):
report.write_text(header + summary)
result = publisher.validator_result(report)
self.assertEqual(result["target_version_status"], "unknown")
self.assertEqual(publisher.version_cell(result, "target"), "Unknown")
report.unlink()
result = publisher.validator_result(report)
self.assertEqual(publisher.version_cell(result, "source"), "Not available")
self.assertEqual(publisher.version_cell(result, "target"), "Not available")

def test_identical_connection_failure_has_new_per_fabric_provenance(self):
for filename in publisher.FILES:
(self.source / "fabric-a" / filename).unlink()
Expand Down
Loading