From 272711b2cb498a173326db58ce00157bda32ea1d Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Mon, 22 Jun 2026 14:58:27 -0400 Subject: [PATCH 1/5] ci: upload omni-github test-result artifacts from CI jobs --- .github/actions/install-ci-run/action.yml | 8 ++ .github/actions/run-package-tests/action.yml | 1 + .github/actions/run-tests/action.yml | 12 ++ .../action.yml | 98 +++++++++++++ .../junit_to_omni_github_results.py | 130 ++++++++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 .github/actions/upload-omni-github-test-results/action.yml create mode 100644 .github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py diff --git a/.github/actions/install-ci-run/action.yml b/.github/actions/install-ci-run/action.yml index e099be41e561..75d2231dbbf2 100644 --- a/.github/actions/install-ci-run/action.yml +++ b/.github/actions/install-ci-run/action.yml @@ -54,3 +54,11 @@ runs: path: ${{ github.workspace }}/results/results.xml if-no-files-found: ignore retention-days: 7 + + - name: Upload omni-github test results + if: always() + uses: ./.github/actions/upload-omni-github-test-results + with: + junit-file: ${{ github.workspace }}/results/results.xml + artifact-prefix: pytest-results-${{ github.job }}-${{ runner.arch }} + repository-id: '567038244' diff --git a/.github/actions/run-package-tests/action.yml b/.github/actions/run-package-tests/action.yml index 264e01a158b8..4f4df2487166 100644 --- a/.github/actions/run-package-tests/action.yml +++ b/.github/actions/run-package-tests/action.yml @@ -150,6 +150,7 @@ runs: include-files: ${{ inputs.include-files }} volume-mount-source: ${{ github.workspace }} extra-pip-packages: ${{ inputs.extra-pip-packages }} + omni-github-artifact-prefix: pytest-results-${{ github.job }}-${{ inputs.container-name }} - name: Check Test Results if: always() diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index ceea3a1c4e65..2068b75da334 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -69,6 +69,10 @@ inputs: description: 'Space-separated pip packages to install inside the Docker container before pytest starts' default: '' required: false + omni-github-artifact-prefix: + description: 'Optional human-readable prefix for the omni-github test-result artifact' + default: '' + required: false runs: using: composite @@ -437,6 +441,14 @@ runs: if-no-files-found: ignore retention-days: 7 + - name: Upload omni-github test results + if: always() + uses: ./.github/actions/upload-omni-github-test-results + with: + junit-file: ${{ inputs.reports-dir }}/${{ inputs.result-file }} + artifact-prefix: ${{ inputs.omni-github-artifact-prefix || format('pytest-results-{0}-{1}', github.job, inputs.container-name) }} + repository-id: '567038244' + - name: Clean up Docker container if: always() && !cancelled() shell: bash diff --git a/.github/actions/upload-omni-github-test-results/action.yml b/.github/actions/upload-omni-github-test-results/action.yml new file mode 100644 index 000000000000..4213df02529d --- /dev/null +++ b/.github/actions/upload-omni-github-test-results/action.yml @@ -0,0 +1,98 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +name: 'Upload omni-github test results' +description: > + Converts a JUnit XML report into the omni-github test-result artifact + contract and uploads it with the registered IsaacLab repository identity. + +inputs: + junit-file: + description: 'Path to the JUnit XML report to convert' + required: true + artifact-prefix: + description: 'Human-readable artifact prefix; normalized before upload' + required: true + repository-id: + description: 'Registered GitHub repository ID used in the omni-github artifact identity' + default: '567038244' + required: false + test-tool-id: + description: 'Identifier for the test runner that produced the JUnit report' + default: 'pytest' + required: false + test-type: + description: 'Test type stored on each converted test row' + default: 'pytest' + required: false + app-platform: + description: 'omni-github app.platform value; defaults from runner OS and architecture' + default: '' + required: false + app-config: + description: 'omni-github app.config value; defaults to the GitHub job id' + default: '' + required: false + retention-days: + description: 'GitHub artifact retention in days' + default: '7' + required: false + +runs: + using: composite + steps: + - name: Convert JUnit XML to omni-github results + id: convert + shell: bash + env: + APP_CONFIG: ${{ inputs.app-config }} + APP_PLATFORM: ${{ inputs.app-platform }} + ARTIFACT_PREFIX: ${{ inputs.artifact-prefix }} + JUNIT_FILE: ${{ inputs.junit-file }} + TEST_TOOL_ID: ${{ inputs.test-tool-id }} + TEST_TYPE: ${{ inputs.test-type }} + run: | + set -euo pipefail + + if [ ! -f "$JUNIT_FILE" ]; then + echo "::warning::Skipping omni-github upload because JUnit report was not found: $JUNIT_FILE" + echo "upload=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + artifact_prefix="$( + printf '%s' "$ARTIFACT_PREFIX" \ + | tr '[:upper:]' '[:lower:]' \ + | sed -E 's/[^a-z0-9_]+/-/g; s/-+/-/g; s/^-+//; s/-+$//' + )" + if [ -z "$artifact_prefix" ]; then + artifact_prefix="pytest-results" + fi + + artifact_dir="${RUNNER_TEMP}/omni-github-test-results/${artifact_prefix}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}" + rm -rf "$artifact_dir" + mkdir -p "$artifact_dir" + + python3 "$GITHUB_ACTION_PATH/junit_to_omni_github_results.py" \ + --junit-file "$JUNIT_FILE" \ + --output-dir "$artifact_dir" \ + --test-tool-id "$TEST_TOOL_ID" \ + --test-type "$TEST_TYPE" \ + --app-platform "$APP_PLATFORM" \ + --app-config "$APP_CONFIG" + + echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" + echo "artifact_prefix=$artifact_prefix" >> "$GITHUB_OUTPUT" + echo "upload=true" >> "$GITHUB_OUTPUT" + + - name: Upload omni-github test results + if: always() && steps.convert.outputs.upload == 'true' + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.convert.outputs.artifact_prefix }}--v1-${{ inputs.repository-id }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ job.check_run_id }} + path: ${{ steps.convert.outputs.artifact_dir }} + if-no-files-found: error + retention-days: ${{ inputs.retention-days }} + compression-level: 9 diff --git a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py new file mode 100644 index 000000000000..da5e7edc65c3 --- /dev/null +++ b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py @@ -0,0 +1,130 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Convert a JUnit XML report into the omni-github test-result artifact format.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import xml.etree.ElementTree as ET + + +_MANIFEST_NAME = "omni-github-test-results-upload.json" +_RESULT_PATH = "_testoutput/test_results.json" + + +def _local_name(tag: str) -> str: + """Return an XML tag name without its namespace.""" + return tag.rsplit("}", maxsplit=1)[-1] + + +def _iter_testcases(root: ET.Element) -> list[ET.Element]: + """Return all JUnit testcase elements from the parsed XML tree.""" + return [element for element in root.iter() if _local_name(element.tag) == "testcase"] + + +def _has_child(testcase: ET.Element, names: set[str]) -> bool: + """Return whether a testcase has a direct child with one of the given names.""" + return any(_local_name(child.tag) in names for child in testcase) + + +def _duration_seconds(testcase: ET.Element) -> float: + """Return the testcase duration in seconds.""" + try: + duration = float(testcase.attrib.get("time", "0")) + except ValueError: + duration = 0.0 + return max(duration, 0.0) + + +def _test_id(testcase: ET.Element) -> str: + """Return a stable test identifier from JUnit classname and name fields.""" + name = testcase.attrib.get("name", "").strip() + classname = testcase.attrib.get("classname", "").strip() + if classname and name: + return f"{classname}::{name}" + return name or classname or "unknown-testcase" + + +def _convert_testcase(testcase: ET.Element, test_type: str) -> dict[str, object]: + """Convert one JUnit testcase element into an omni-github test row.""" + return { + "test_id": _test_id(testcase), + "passed": not _has_child(testcase, {"error", "failure"}), + "duration": _duration_seconds(testcase), + "test_type": test_type, + } + + +def convert_junit( + junit_file: Path, + output_dir: Path, + test_tool_id: str, + test_type: str, + app_platform: str, + app_config: str, +) -> None: + """Convert a JUnit XML report and write the omni-github artifact directory. + + Args: + junit_file: Path to the source JUnit XML report. + output_dir: Directory where the artifact root should be written. + test_tool_id: Identifier for the test tool that produced the report. + test_type: Test type to store on each converted test row. + app_platform: Platform label for the result app metadata. + app_config: Configuration label for the result app metadata. + """ + root = ET.parse(junit_file).getroot() + tests = [_convert_testcase(testcase, test_type) for testcase in _iter_testcases(root)] + + result: dict[str, object] = { + "result_schema_version": 1, + "test_tool_id": test_tool_id, + "app": { + "platform": app_platform, + "config": app_config, + }, + "tests": tests, + } + manifest = { + "schema_version": 1, + "result_paths": [_RESULT_PATH], + } + + result_path = output_dir / _RESULT_PATH + result_path.parent.mkdir(parents=True, exist_ok=True) + result_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + (output_dir / _MANIFEST_NAME).write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--junit-file", type=Path, required=True, help="Path to the JUnit XML report.") + parser.add_argument("--output-dir", type=Path, required=True, help="Artifact root directory to write.") + parser.add_argument("--test-tool-id", required=True, help="Identifier for the test tool.") + parser.add_argument("--test-type", required=True, help="Test type for each test row.") + parser.add_argument("--app-platform", required=True, help="App platform label.") + parser.add_argument("--app-config", required=True, help="App configuration label.") + return parser.parse_args() + + +def main() -> None: + """Run the converter.""" + args = parse_args() + convert_junit( + junit_file=args.junit_file, + output_dir=args.output_dir, + test_tool_id=args.test_tool_id, + test_type=args.test_type, + app_platform=args.app_platform, + app_config=args.app_config, + ) + + +if __name__ == "__main__": + main() From 1b0d459a81c5d413fcf22c15043e3467b0d0200c Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Mon, 22 Jun 2026 15:07:10 -0400 Subject: [PATCH 2/5] update --- .../action.yml | 22 +++++++++++++++++-- .../junit_to_omni_github_results.py | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/actions/upload-omni-github-test-results/action.yml b/.github/actions/upload-omni-github-test-results/action.yml index 4213df02529d..120d85ae83fd 100644 --- a/.github/actions/upload-omni-github-test-results/action.yml +++ b/.github/actions/upload-omni-github-test-results/action.yml @@ -71,6 +71,24 @@ runs: artifact_prefix="pytest-results" fi + app_platform="$APP_PLATFORM" + if [ -z "$app_platform" ]; then + case "${RUNNER_OS:-unknown}-${RUNNER_ARCH:-unknown}" in + Linux-X64) app_platform="linux-x86_64" ;; + Linux-ARM64) app_platform="linux-aarch64" ;; + Windows-X64) app_platform="windows-x86_64" ;; + Windows-ARM64) app_platform="windows-aarch64" ;; + macOS-X64) app_platform="macos-x86_64" ;; + macOS-ARM64) app_platform="macos-aarch64" ;; + *) app_platform="$(printf '%s-%s' "${RUNNER_OS:-unknown}" "${RUNNER_ARCH:-unknown}" | tr '[:upper:]' '[:lower:]')" ;; + esac + fi + + app_config="$APP_CONFIG" + if [ -z "$app_config" ]; then + app_config="${GITHUB_JOB:-github-job}" + fi + artifact_dir="${RUNNER_TEMP}/omni-github-test-results/${artifact_prefix}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}" rm -rf "$artifact_dir" mkdir -p "$artifact_dir" @@ -80,8 +98,8 @@ runs: --output-dir "$artifact_dir" \ --test-tool-id "$TEST_TOOL_ID" \ --test-type "$TEST_TYPE" \ - --app-platform "$APP_PLATFORM" \ - --app-config "$APP_CONFIG" + --app-platform "$app_platform" \ + --app-config "$app_config" echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" echo "artifact_prefix=$artifact_prefix" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py index da5e7edc65c3..2e24a85cfed6 100644 --- a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py +++ b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py @@ -54,7 +54,7 @@ def _convert_testcase(testcase: ET.Element, test_type: str) -> dict[str, object] """Convert one JUnit testcase element into an omni-github test row.""" return { "test_id": _test_id(testcase), - "passed": not _has_child(testcase, {"error", "failure"}), + "passed": not _has_child(testcase, {"error", "failure", "skipped"}), "duration": _duration_seconds(testcase), "test_type": test_type, } From 0f2c4e45178648d96c1b29a1093496319162767f Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Mon, 22 Jun 2026 15:56:13 -0400 Subject: [PATCH 3/5] format --- .../junit_to_omni_github_results.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py index 2e24a85cfed6..0ed7a4eb8bf3 100644 --- a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py +++ b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py @@ -9,9 +9,8 @@ import argparse import json -from pathlib import Path import xml.etree.ElementTree as ET - +from pathlib import Path _MANIFEST_NAME = "omni-github-test-results-upload.json" _RESULT_PATH = "_testoutput/test_results.json" From 245ebd1db3eb78c9129999515328991517881642 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Mon, 22 Jun 2026 16:22:46 -0400 Subject: [PATCH 4/5] fix: use github repository id --- .github/actions/install-ci-run/action.yml | 1 - .github/actions/run-tests/action.yml | 1 - .../actions/upload-omni-github-test-results/action.yml | 8 ++------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/actions/install-ci-run/action.yml b/.github/actions/install-ci-run/action.yml index 75d2231dbbf2..c7b13c23fbfd 100644 --- a/.github/actions/install-ci-run/action.yml +++ b/.github/actions/install-ci-run/action.yml @@ -61,4 +61,3 @@ runs: with: junit-file: ${{ github.workspace }}/results/results.xml artifact-prefix: pytest-results-${{ github.job }}-${{ runner.arch }} - repository-id: '567038244' diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index 2068b75da334..925045f3f251 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -447,7 +447,6 @@ runs: with: junit-file: ${{ inputs.reports-dir }}/${{ inputs.result-file }} artifact-prefix: ${{ inputs.omni-github-artifact-prefix || format('pytest-results-{0}-{1}', github.job, inputs.container-name) }} - repository-id: '567038244' - name: Clean up Docker container if: always() && !cancelled() diff --git a/.github/actions/upload-omni-github-test-results/action.yml b/.github/actions/upload-omni-github-test-results/action.yml index 120d85ae83fd..2add46d0d85b 100644 --- a/.github/actions/upload-omni-github-test-results/action.yml +++ b/.github/actions/upload-omni-github-test-results/action.yml @@ -6,7 +6,7 @@ name: 'Upload omni-github test results' description: > Converts a JUnit XML report into the omni-github test-result artifact - contract and uploads it with the registered IsaacLab repository identity. + contract and uploads it with the current GitHub repository identity. inputs: junit-file: @@ -15,10 +15,6 @@ inputs: artifact-prefix: description: 'Human-readable artifact prefix; normalized before upload' required: true - repository-id: - description: 'Registered GitHub repository ID used in the omni-github artifact identity' - default: '567038244' - required: false test-tool-id: description: 'Identifier for the test runner that produced the JUnit report' default: 'pytest' @@ -109,7 +105,7 @@ runs: if: always() && steps.convert.outputs.upload == 'true' uses: actions/upload-artifact@v7 with: - name: ${{ steps.convert.outputs.artifact_prefix }}--v1-${{ inputs.repository-id }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ job.check_run_id }} + name: ${{ steps.convert.outputs.artifact_prefix }}--v1-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ job.check_run_id }} path: ${{ steps.convert.outputs.artifact_dir }} if-no-files-found: error retention-days: ${{ inputs.retention-days }} From ba6ba0ecf30137b055066f68e0974d67d6c4c18b Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Tue, 23 Jun 2026 00:09:41 -0400 Subject: [PATCH 5/5] update --- .../action.yml | 8 ++ .../junit_to_omni_github_results.py | 39 +++++- .../test_junit_to_omni_github_results.py | 118 ++++++++++++++++++ 3 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 .github/actions/upload-omni-github-test-results/test_junit_to_omni_github_results.py diff --git a/.github/actions/upload-omni-github-test-results/action.yml b/.github/actions/upload-omni-github-test-results/action.yml index 2add46d0d85b..d00071829483 100644 --- a/.github/actions/upload-omni-github-test-results/action.yml +++ b/.github/actions/upload-omni-github-test-results/action.yml @@ -97,6 +97,14 @@ runs: --app-platform "$app_platform" \ --app-config "$app_config" + manifest_path="${artifact_dir}/omni-github-test-results-upload.json" + result_path="${artifact_dir}/_testoutput/test_results.json" + if [ ! -f "$manifest_path" ] || [ ! -f "$result_path" ]; then + echo "::notice::Skipping omni-github upload because no schema-valid result artifact was generated for JUnit report: $JUNIT_FILE" + echo "upload=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "artifact_dir=$artifact_dir" >> "$GITHUB_OUTPUT" echo "artifact_prefix=$artifact_prefix" >> "$GITHUB_OUTPUT" echo "upload=true" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py index 0ed7a4eb8bf3..da0cf601f0df 100644 --- a/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py +++ b/.github/actions/upload-omni-github-test-results/junit_to_omni_github_results.py @@ -31,6 +31,14 @@ def _has_child(testcase: ET.Element, names: set[str]) -> bool: return any(_local_name(child.tag) in names for child in testcase) +def _find_child(testcase: ET.Element, names: set[str]) -> ET.Element | None: + """Return the first direct testcase child with one of the given names.""" + for child in testcase: + if _local_name(child.tag) in names: + return child + return None + + def _duration_seconds(testcase: ET.Element) -> float: """Return the testcase duration in seconds.""" try: @@ -49,14 +57,29 @@ def _test_id(testcase: ET.Element) -> str: return name or classname or "unknown-testcase" +def _message(element: ET.Element | None) -> str | None: + """Return a bounded JUnit child message, if present.""" + if element is None: + return None + message = element.attrib.get("message", "").strip() + return message[:4096] if message else None + + def _convert_testcase(testcase: ET.Element, test_type: str) -> dict[str, object]: """Convert one JUnit testcase element into an omni-github test row.""" - return { + skipped = _find_child(testcase, {"skipped"}) + failed_or_error = _has_child(testcase, {"error", "failure"}) + row: dict[str, object] = { "test_id": _test_id(testcase), - "passed": not _has_child(testcase, {"error", "failure", "skipped"}), + "passed": not failed_or_error and skipped is None, "duration": _duration_seconds(testcase), "test_type": test_type, } + if skipped is not None: + row["skipped"] = True + if skip_reason := _message(skipped): + row["skip_reason"] = skip_reason + return row def convert_junit( @@ -66,7 +89,7 @@ def convert_junit( test_type: str, app_platform: str, app_config: str, -) -> None: +) -> int: """Convert a JUnit XML report and write the omni-github artifact directory. Args: @@ -76,9 +99,14 @@ def convert_junit( test_type: Test type to store on each converted test row. app_platform: Platform label for the result app metadata. app_config: Configuration label for the result app metadata. + + Returns: + Number of test cases converted. """ root = ET.parse(junit_file).getroot() tests = [_convert_testcase(testcase, test_type) for testcase in _iter_testcases(root)] + if not tests: + return 0 result: dict[str, object] = { "result_schema_version": 1, @@ -98,6 +126,7 @@ def convert_junit( result_path.parent.mkdir(parents=True, exist_ok=True) result_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") (output_dir / _MANIFEST_NAME).write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + return len(tests) def parse_args() -> argparse.Namespace: @@ -115,7 +144,7 @@ def parse_args() -> argparse.Namespace: def main() -> None: """Run the converter.""" args = parse_args() - convert_junit( + test_count = convert_junit( junit_file=args.junit_file, output_dir=args.output_dir, test_tool_id=args.test_tool_id, @@ -123,6 +152,8 @@ def main() -> None: app_platform=args.app_platform, app_config=args.app_config, ) + if test_count == 0: + print("No JUnit test cases found; skipping omni-github artifact generation.") if __name__ == "__main__": diff --git a/.github/actions/upload-omni-github-test-results/test_junit_to_omni_github_results.py b/.github/actions/upload-omni-github-test-results/test_junit_to_omni_github_results.py new file mode 100644 index 000000000000..9b03f57d30df --- /dev/null +++ b/.github/actions/upload-omni-github-test-results/test_junit_to_omni_github_results.py @@ -0,0 +1,118 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for converting JUnit XML into omni-github test-result artifacts.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from types import ModuleType + +_CONVERTER_PATH = Path(__file__).with_name("junit_to_omni_github_results.py") +_MANIFEST_NAME = "omni-github-test-results-upload.json" +_RESULT_PATH = Path("_testoutput/test_results.json") + + +def _load_converter() -> ModuleType: + """Load the converter module from its action-local script path.""" + spec = importlib.util.spec_from_file_location("junit_to_omni_github_results", _CONVERTER_PATH) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_convert_junit_writes_schema_compatible_artifact(tmp_path: Path) -> None: + """Test that JUnit test cases are converted into manifest-listed result rows.""" + junit_file = tmp_path / "junit.xml" + junit_file.write_text( + """ + + + + + + + + + +""", + encoding="utf-8", + ) + output_dir = tmp_path / "artifact" + + test_count = _load_converter().convert_junit( + junit_file=junit_file, + output_dir=output_dir, + test_tool_id="pytest", + test_type="unit", + app_platform="linux-x86_64", + app_config="test-job", + ) + + assert test_count == 3 + manifest = json.loads((output_dir / _MANIFEST_NAME).read_text(encoding="utf-8")) + result = json.loads((output_dir / _RESULT_PATH).read_text(encoding="utf-8")) + assert manifest == { + "schema_version": 1, + "result_paths": [_RESULT_PATH.as_posix()], + } + assert result["result_schema_version"] == 1 + assert result["test_tool_id"] == "pytest" + assert result["app"] == { + "platform": "linux-x86_64", + "config": "test-job", + } + assert result["tests"] == [ + { + "duration": 0.125, + "passed": True, + "test_id": "tests.test_demo::test_passes", + "test_type": "unit", + }, + { + "duration": 1.25, + "passed": False, + "test_id": "tests.test_demo::test_fails", + "test_type": "unit", + }, + { + "duration": 0.0, + "passed": False, + "skip_reason": "not applicable", + "skipped": True, + "test_id": "tests.test_demo::test_skips", + "test_type": "unit", + }, + ] + + +def test_convert_junit_skips_artifact_when_no_testcases(tmp_path: Path) -> None: + """Test that an empty JUnit report does not create a schema-invalid artifact.""" + junit_file = tmp_path / "junit.xml" + junit_file.write_text( + """ + +""", + encoding="utf-8", + ) + output_dir = tmp_path / "artifact" + output_dir.mkdir() + + test_count = _load_converter().convert_junit( + junit_file=junit_file, + output_dir=output_dir, + test_tool_id="pytest", + test_type="unit", + app_platform="linux-x86_64", + app_config="test-job", + ) + + assert test_count == 0 + assert not (output_dir / _MANIFEST_NAME).exists() + assert not (output_dir / _RESULT_PATH).exists()