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
11 changes: 11 additions & 0 deletions .github/workflows/test_and_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ jobs:
# figures of the platform verification report.
run: |
bazel test --lockfile_mode=error --config=linux-x86_64 //platform_integration_tests:pit
- name: Execute Module Integration Tests
# Driven from quality_runners.py (checked out from the PR head) rather
# than hardcoded here: this job runs via pull_request_target, which reads
# this YAML from the base branch, so a module upgrade PR could not adjust
# the test targets/flags otherwise. Runs before the docs build so the
# source code linker turns the tests' Verifies properties into `testlink`s
# on the lifecycle, logging and persistency requirement needs. Mirrors the
# unit-test step's cache gating: only tag runs re-execute from scratch.
run: |
python ./scripts/quality_runners.py --integration-tests \
${{ github.ref_type != 'tag' && '--trust-cache' || '' }}
- name: Install uv (required for SBOM license scanning)
uses: astral-sh/setup-uv@v5
with:
Expand Down
2 changes: 2 additions & 0 deletions bazel_common/score_modules_target_sw.MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ git_override(
patch_strip = 1,
patches = [
"//patches/lifecycle:001-migrate-baselibs-rust-targets.patch",
"//patches/lifecycle:002-undev-integration-test-deps.patch",
],
remote = "https://github.com/eclipse-score/lifecycle.git",
)
Expand All @@ -78,6 +79,7 @@ git_override(
patch_strip = 1,
patches = [
"//patches/logging:001-migrate-baselibs-targets.patch",
"//patches/logging:002-undev-component-test-deps.patch",
],
remote = "https://github.com/eclipse-score/logging.git",
)
Expand Down
24 changes: 21 additions & 3 deletions known_good.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@
"//score/kvs/tests/integration_test_scenarios/...",
"//score/kvs/tests/test_cases/..."
],
"rust_coverage_config": "ferrocene-coverage-per"
"rust_coverage_config": "ferrocene-coverage-per",
"integration_test_targets": [
"//score/kvs/tests/test_cases:cit"
]
},
"hash": "323950c42431956ce76bf48c407a1709f114876d"
},
Expand All @@ -96,21 +99,30 @@
"score_lifecycle": {
"repo": "https://github.com/eclipse-score/lifecycle.git",
"bazel_patches": [
"//patches/lifecycle:001-migrate-baselibs-rust-targets.patch"
"//patches/lifecycle:001-migrate-baselibs-rust-targets.patch",
"//patches/lifecycle:002-undev-integration-test-deps.patch"
],
"metadata": {
"code_root_path": "//score/...",
"extra_test_config": [],
"exclude_test_targets": [
"//score/health_monitor/src/rust:miri_tests"
],
"integration_test_targets": [
"//tests/integration/..."
],
"integration_test_config": [
"flaky_test_attempts=3",
"@score_lifecycle//config:integration_mode=docker"
]
},
"hash": "85da2168328d17a3cf53791ebf93534e5ed921fc"
},
"score_logging": {
"repo": "https://github.com/eclipse-score/logging.git",
"bazel_patches": [
"//patches/logging:001-migrate-baselibs-targets.patch"
"//patches/logging:001-migrate-baselibs-targets.patch",
"//patches/logging:002-undev-component-test-deps.patch"
],
"metadata": {
"code_root_path": "//score/mw/log/...",
Expand All @@ -122,6 +134,12 @@
],
"exclude_test_targets": [
"//score/mw/log/legacy_non_verbose_api:unit_test"
],
"integration_test_targets": [
"//score/test/component/..."
],
"integration_test_config": [
"flaky_test_attempts=3"
]
},
"hash": "b4f272b0a036d46fc7ff2e9a051fdec95fd23b4f"
Expand Down
16 changes: 16 additions & 0 deletions patches/lifecycle/002-undev-integration-test-deps.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/MODULE.bazel b/MODULE.bazel
index 721aacc9e..f7726c220 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -174,9 +174,9 @@ pip.parse(
)
use_repo(pip, "score_lifecycle_pip")

-bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True)
+bazel_dep(name = "score_itf", version = "0.5.0")

-oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True)
+oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
oci.pull(
name = "debian-test-runtime",
digest = "sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a",
44 changes: 44 additions & 0 deletions patches/logging/002-undev-component-test-deps.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
diff --git a/MODULE.bazel b/MODULE.bazel
index 45a6aafd..afd6a2aa 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -42,15 +42,15 @@ bazel_dep(name = "score_rust_policies", version = "0.0.5")
bazel_dep(name = "score_process_description", version = "2.1.2")
bazel_dep(name = "score_platform", version = "0.7.2")

-bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True)
+bazel_dep(name = "score_itf", version = "0.5.0")

# OCI / Docker image rules for integration tests
-bazel_dep(name = "rules_oci", version = "2.2.7", dev_dependency = True)
+bazel_dep(name = "rules_oci", version = "2.2.7")

bazel_dep(name = "rules_pkg", version = "1.2.0")

# QNX image filesystem rules for QNX integration tests
-bazel_dep(name = "score_rules_imagefs", version = "0.1.0", dev_dependency = True)
+bazel_dep(name = "score_rules_imagefs", version = "0.1.0")

imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True)
imagefs.toolchain(
@@ -75,7 +75,7 @@ use_repo(
"score_qnx_x86_64_ifs_toolchain",
)

-oci = use_extension("@rules_oci//oci:extensions.bzl", "oci", dev_dependency = True)
+oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
oci.pull(
name = "ubuntu_24_04",
digest = "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30",
diff --git a/score/test/component/defs.bzl b/score/test/component/defs.bzl
index 665f891a..0177a698 100644
--- a/score/test/component/defs.bzl
+++ b/score/test/component/defs.bzl
@@ -66,7 +66,6 @@ def py_logging_itf_test(name, srcs, filesystem, filesystem_pkg, extra_oci_tars =
filesystem_pkg,
],
build_file = "{}:init_build".format(_ENV),
- extra_build_files = ["{}:system_build".format(_ENV)],
target_compatible_with = [
"@platforms//cpu:x86_64",
"@platforms//os:qnx",
Comment on lines +33 to +44
11 changes: 11 additions & 0 deletions scripts/known_good/models/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,20 @@ class Metadata:
extra_test_config: List of extra test configuration flags
exclude_test_targets: List of test targets to exclude
langs: List of languages supported (e.g., ["cpp", "rust"])
integration_test_targets: Module-relative labels of integration/component
test suites to run (e.g. ["//tests/integration/..."]). Empty means
the module has no integration tests to run from the integration.
integration_test_config: Extra bazel flags (without the leading ``--``)
applied only to the integration test run (e.g. ["flaky_test_attempts=3"]).
"""

code_root_path: str = "//score/..."
extra_test_config: list[str] = field(default_factory=lambda: [])
exclude_test_targets: list[str] = field(default_factory=lambda: [])
langs: list[str] = field(default_factory=lambda: ["cpp", "rust"])
rust_coverage_config: str | None = "ferrocene-coverage" # Optional field for Rust coverage configuration
integration_test_targets: list[str] = field(default_factory=lambda: [])
integration_test_config: list[str] = field(default_factory=lambda: [])

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Metadata:
Expand All @@ -127,6 +134,8 @@ def from_dict(cls, data: Dict[str, Any]) -> Metadata:
exclude_test_targets=data.get("exclude_test_targets", []),
langs=data.get("langs", ["cpp", "rust"]),
rust_coverage_config=data.get("rust_coverage_config", "ferrocene-coverage"),
integration_test_targets=data.get("integration_test_targets", []),
integration_test_config=data.get("integration_test_config", []),
)

def to_dict(self) -> Dict[str, Any]:
Expand All @@ -141,6 +150,8 @@ def to_dict(self) -> Dict[str, Any]:
"exclude_test_targets": self.exclude_test_targets,
"langs": self.langs,
"rust_coverage_config": self.rust_coverage_config,
"integration_test_targets": self.integration_test_targets,
"integration_test_config": self.integration_test_config,
}


Expand Down
71 changes: 64 additions & 7 deletions scripts/quality_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,45 @@ def run_command(command: list[str], **kwargs) -> ProcessResult:
return ProcessResult(stdout="".join(stdout_data), stderr="".join(stderr_data), exit_code=exit_code)


def run_integration_tests(known, modules_to_test: list[str], trust_cache: bool = False) -> bool:
"""Run each module's integration test suite, mirroring the unit-test loop.

Iterates over the target_sw modules and runs ``integration_test_targets`` for
every module that declares them in known_good.json. Returns True when at least
one suite failed, matching ``main``'s convention so the process exits non-zero.

Driven from known_good.json (checked out from the PR head) rather than the
workflow YAML on purpose: the Code Quality & Documentation job runs via
``pull_request_target``, which reads the YAML from the base branch, so a module
upgrade PR could not adjust the targets or flags in lockstep. This can.
"""
summary = {}
for module in known.modules["target_sw"].values():
if modules_to_test and module.name not in modules_to_test:
continue
if not module.metadata.integration_test_targets:
continue
print_centered(f"QR: Running integration tests for {module.name}")
call = (
["bazel", "test", "--lockfile_mode=error", "--config=linux-x86_64"]
+ ([] if trust_cache else ["--nocache_test_results"])
+ [f"--{flag}" for flag in module.metadata.integration_test_config]
+ [f"@{module.name}{target}" for target in module.metadata.integration_test_targets]
)
summary[module.name] = {"exit_code": run_command(call).exit_code}

failed = sorted(name for name, result in summary.items() if result["exit_code"] != 0)
for name in failed:
print(f"::error title=Integration tests failed::{name}: bazel exited with {summary[name]['exit_code']}")

print_centered("QR: INTEGRATION TEST EXECUTION SUMMARY", fillchar="=")
for name, result in sorted(summary.items()):
print(f" {'pass' if result['exit_code'] == 0 else 'FAILED':<7} {name}")
if failed:
print_centered(f"QR: {len(failed)} of {len(summary)} MODULES FAILED: {', '.join(failed)}", fillchar="=")
return bool(failed)


def parse_arguments() -> argparse.Namespace:
import argparse

Expand All @@ -378,6 +417,15 @@ def parse_arguments() -> argparse.Namespace:
default=[],
help="List of modules to test",
)
parser.add_argument(
"--integration-tests",
action="store_true",
help="Run each module's integration test suite (its known_good.json "
"integration_test_targets) instead of the unit tests and coverage. Kept "
"here rather than in the workflow YAML because that YAML is read from the "
"base branch under pull_request_target and could not be changed by a "
"module upgrade PR.",
)
parser.add_argument(
"--trust-cache",
action="store_true",
Expand All @@ -390,20 +438,19 @@ def parse_arguments() -> argparse.Namespace:
return parser.parse_args()


def main() -> bool:
args = parse_arguments()
def run_unit_tests(known, args: argparse.Namespace) -> bool:
"""Run unit tests and coverage for every module, mirroring run_integration_tests.

Returns True when any test or coverage extraction failed, so the process
exits non-zero.
"""
configure_aslr_for_sanitizers()
args.coverage_output_dir.mkdir(parents=True, exist_ok=True)
path_to_docs = Path(__file__).parent.parent / "docs/verification_report"
path_to_docs.mkdir(parents=True, exist_ok=True)

known = load_known_good(args.known_good_path.resolve())

unit_tests_summary, coverage_summary = {}, {}

if args.modules_to_test:
print_centered(f"QR: User requested tests only for specified modules: {', '.join(args.modules_to_test)}")

for module in known.modules["target_sw"].values():
if args.modules_to_test and module.name not in args.modules_to_test:
print_centered(f"QR: Skipping module {module.name}")
Expand Down Expand Up @@ -460,6 +507,16 @@ def main() -> bool:
return any(r["exit_code"] != 0 for r in {**unit_tests_summary, **coverage_summary}.values())


def main() -> bool:
args = parse_arguments()
known = load_known_good(args.known_good_path.resolve())
if args.modules_to_test:
print_centered(f"QR: User requested tests only for specified modules: {', '.join(args.modules_to_test)}")
if args.integration_tests:
return run_integration_tests(known, args.modules_to_test, args.trust_cache)
return run_unit_tests(known, args)


if __name__ == "__main__":
sys.stdout.reconfigure(line_buffering=True)
sys.exit(main())
Loading