From 1c4f49a948d9d05160087cdf671cbd9f5641954d Mon Sep 17 00:00:00 2001
From: sandeeplocharla
The API error above is often only a symptom. The cause is " + f"usually in the CloudStack logs: {links}
") + for number, failure in enumerate(failures, 1): + names = failure["names"] or (f"unmapped-{number}",) + anchors = "".join( + f'' for name in names) + title = ", ".join(failure["names"]) or failure["description"] + sections.append( + f'{anchors}'
+ f'{html.escape(chr(10).join(failure["lines"]))}')
+ return "'
+ "\n".join(rendered)
+ ""
@@ -175,6 +286,12 @@ def generate_log_artifacts(results, debian_log):
results / "phase2" / "configure-cloudstack.log",
results / "phase2" / "health-check.log",
]
+ runtime = results / "phase2" / "runtime-logs"
+ related_logs = (
+ ("Management server log",
+ runtime / "var-log-cloudstack-management" / "management-server.log"),
+ ("Agent log", runtime / "var-log-cloudstack-agent" / "agent.log"),
+ )
marvin = results / "phase2" / "marvin"
candidates.extend(marvin.glob("*.log"))
candidates.extend(
@@ -183,7 +300,15 @@ def generate_log_artifacts(results, debian_log):
for source in sorted(set(candidates)):
if not source.is_file():
continue
- log_to_html(source, Path(f"{source}.html"))
+ failures = (
+ parse_suite_failures(source.with_name("results.txt"))
+ if source.name == "stdout.log" else ())
+ destination = Path(f"{source}.html")
+ related = tuple(
+ (label, os.path.relpath(path, destination.parent))
+ for label, path in related_logs if path.is_file()
+ ) if failures else ()
+ log_to_html(source, destination, failures, related)
def parse_stage_events(stage_events, results):
@@ -339,13 +464,26 @@ def test_rows(tests, results, build_url):
for test in tests:
status = display_test_status(test.get("status"))
log_path = test_log_for_summary(test["_summary"], test)
+ failure = None
+ if status not in ("PASS", "SKIP"):
+ failure = failure_for_test(test["_summary"], test)
if log_path:
relative = log_path.relative_to(results.parent)
- link = artifact_url(
- build_url, relative, f"test-{slug(test.get('name', 'test'))}")
+ name = test.get("name", "test")
+ # The failure section sits at the top of the suite page, so a
+ # failed test links straight to its exception.
+ fragment = (
+ failure_anchor(name)
+ if failure and log_path.name == "stdout.log.html"
+ else f"test-{slug(name)}")
+ link = artifact_url(build_url, relative, fragment)
log_cell = f'HTML log'
else:
log_cell = "—"
+ error_cell = (
+ f''
+ f'{html.escape(failure["error"])}'
+ if failure and failure["error"] else "")
suite = str(test.get("label") or test.get("tag") or "")
rows.append(
"{html.escape(str(test.get('name', '')))}Failures are listed first. Select a log link to jump to the " "test.
" - + table(["Protocol", "Suite", "Test", "Status", "Details"], tests_html) + + table(["Protocol", "Suite", "Test", "Status", "Error", "Details"], + tests_html) if tests else "No Marvin test summary was available.
") body = ( diff --git a/private-cicd/scripts/test_generate_presubmit_report.py b/private-cicd/scripts/test_generate_presubmit_report.py new file mode 100644 index 000000000000..02dd29c95aa9 --- /dev/null +++ b/private-cicd/scripts/test_generate_presubmit_report.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SPEC = importlib.util.spec_from_file_location( + "generate_presubmit_report", + Path(__file__).with_name("generate-presubmit-report.py")) +report = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(report) + +SEP = "=" * 70 +DASH = "-" * 70 +RESULTS = f"""Start the VM. ... === TestName: test_06_start | Status : EXCEPTION === +ERROR +Check the pool. ... === TestName: test_07_pool | Status : FAIL === +FAIL +Detach it. ... === TestName: test_08_detach | Status : SUCCESS === +ok + +{SEP} +ERROR: Start the VM. +{DASH} +Traceback (most recent call last): + File "test.py", line 750, in test_06_start + self.apiClient.startVirtualMachine(cmd) +Exception: Job failed: {{errortext : 'Unable to orchestrate the start of VM'}} +-------------------- >> begin captured logging << -------------------- +captured line that must not be shown +--------------------- >> end captured logging << --------------------- + +{SEP} +FAIL: Check the pool. +{DASH} +Traceback (most recent call last): + File "test.py", line 90, in test_07_pool + self.assertEqual(a, b) +AssertionError: 1 != 2 +{DASH} +Ran 3 tests in 1.0s + +FAILED (errors=1, failures=1) +""" +SETUP_ERROR = f"""{SEP} +ERROR: setUpClass (suite.Class) +{DASH} +Traceback (most recent call last): +RuntimeError: pool create failed +{DASH} +Ran 0 tests in 0.1s +""" + + +class ParseSuiteFailuresTest(unittest.TestCase): + def parse(self, text): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "results.txt" + path.write_text(text, encoding="utf-8") + return report.parse_suite_failures(path) + + def test_error_and_fail_blocks_are_mapped_to_test_names(self): + failures = self.parse(RESULTS) + self.assertEqual([f["kind"] for f in failures], ["ERROR", "FAIL"]) + self.assertEqual(failures[0]["names"], ("test_06_start",)) + self.assertEqual(failures[1]["names"], ("test_07_pool",)) + + def test_error_is_the_exception_line_without_captured_logging(self): + failures = self.parse(RESULTS) + self.assertTrue(failures[0]["error"].startswith("Exception: Job failed")) + self.assertIn("Unable to orchestrate", failures[0]["error"]) + self.assertEqual(failures[1]["error"], "AssertionError: 1 != 2") + self.assertNotIn( + "must not be shown", "\n".join(failures[0]["lines"])) + + def test_cloudstack_job_failure_leads_with_its_message(self): + line = ("Exception: Job failed: {accountid : 'a', account : 'admin', " + "jobresult : {errorcode : 530, errortext : 'Unable to start " + "the VM'}}") + text = f"{SEP}\nERROR: d\n{DASH}\n{line}\n" + self.assertEqual( + self.parse(text)[0]["error"], + "Job failed (code 530): Unable to start the VM") + + def test_unmapped_block_is_kept_without_names(self): + failures = self.parse(SETUP_ERROR) + self.assertEqual(len(failures), 1) + self.assertEqual(failures[0]["names"], ()) + self.assertEqual(failures[0]["error"], "RuntimeError: pool create failed") + + def test_missing_file_or_clean_run_gives_no_failures(self): + self.assertEqual( + report.parse_suite_failures(Path("/nonexistent/results.txt")), ()) + self.assertEqual(self.parse("Detach. ... === TestName: t | Status : SUCCESS ===\nok\n"), ()) + + def test_long_error_is_truncated(self): + long_line = "Exception: " + "x" * 2000 + text = f"{SEP}\nERROR: d\n{DASH}\n{long_line}\n" + error = self.parse(text)[0]["error"] + self.assertEqual(len(error), report.MAX_ERROR_CHARS + 1) + + +class FailurePageTest(unittest.TestCase): + def test_suite_page_shows_failure_before_the_log(self): + with tempfile.TemporaryDirectory() as directory: + suite = (Path(directory) / "phase2" / "marvin" / "ontap-results" + / "run-iscsi" / "suites" / "vm") + suite.mkdir(parents=True) + (suite / "results.txt").write_text(RESULTS, encoding="utf-8") + log = suite / "stdout.log" + log.write_text( + "=== TestName: test_06_start | Status : EXCEPTION ===\n" + "=== TestName: test_08_detach | Status : SUCCESS ===\n", + encoding="utf-8") + management = (Path(directory) / "phase2" / "runtime-logs" + / "var-log-cloudstack-management") + management.mkdir(parents=True) + (management / "management-server.log").write_text("log\n") + report.generate_log_artifacts(Path(directory), None) + page = (suite / "stdout.log.html").read_text(encoding="utf-8") + self.assertIn('id="fail-test_06_start"', page) + self.assertIn("Unable to orchestrate", page) + self.assertIn('id="test-test_08_detach"', page) + self.assertIn("Management server log", page) + self.assertIn( + "../../../../../runtime-logs/var-log-cloudstack-management/" + "management-server.log", page) + self.assertNotIn("Agent log", page) + self.assertLess(page.index("Failure details"), page.index("Suite log")) + self.assertLess(page.index("Suite log"), page.index("TestName")) + + def test_clean_suite_page_has_no_failure_section(self): + with tempfile.TemporaryDirectory() as directory: + log = Path(directory) / "stdout.log" + log.write_text("=== TestName: t | Status : SUCCESS ===\n", + encoding="utf-8") + destination = Path(directory) / "stdout.log.html" + report.log_to_html(log, destination) + self.assertNotIn( + "Failure details", destination.read_text(encoding="utf-8")) + + +if __name__ == "__main__": + unittest.main() From 4400b629cf0640dd33b70904b98f4cf51714fead Mon Sep 17 00:00:00 2001 From: sandeeplocharla${htmlEscape(env.RESOLVED_SOURCE_SHA)}")
- rows += presubmitInfoRow('Started', htmlEscape(env.PRESUBMIT_STARTED_AT))
+ rows += presubmitInfoRow(
+ 'Started', htmlEscape(env.PRESUBMIT_STARTED_AT_IST ?: env.PRESUBMIT_STARTED_AT))
def consoleUrl = "${env.BUILD_URL}console"
return """