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
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ osc-validate --tool <TOOL_NAME> --junitxml validation-results.xml
Enable QC OSI trace checks at test case call sites with:

```bash
osc-validate --tool ESMini --toolpath C:/path/to/esmini/bin/esmini.exe --qc-osi-trace --qc-osi-version 3.7.0
osc-validate --tool ESMini --toolpath C:/path/to/esmini/bin/esmini.exe --assert-osi-compliance --assert-osi-compliance-version 3.7.0
```

Use the Omega Prime ruleset with:

```bash
osc-validate --tool ESMini --toolpath C:/path/to/esmini/bin/esmini.exe --qc-omega-prime
osc-validate --tool ESMini --toolpath C:/path/to/esmini/bin/esmini.exe --assert-osi-compliance-ruleset-preset omega-prime
```

For more information on available command-line options, run:
Expand Down Expand Up @@ -292,21 +292,21 @@ Checker Bundles already integrated:
- [qc-osi-trace - OSI Trace Checker](https://github.com/OpenSimulationInterface/qc-osi-trace)

QC trace checks are opt-in for a validation run.
Test cases call the `assert_osi_trace_compliance` fixture where a generated trace should be checked.
The fixture is a no-op by default and runs qc-osi-trace only when `--qc-osi-trace` is passed.
Test cases call the `assert_osi_compliance` fixture where a generated trace should be checked.
The fixture is a no-op by default and runs qc-osi-trace only when `--assert-osi-compliance` or `--assert-osi-compliance-ruleset-preset omega-prime` is passed.

```python
def test_example(generate_tool_trace, assert_osi_trace_compliance, tmp_path):
def test_example(generate_tool_trace, assert_osi_compliance, tmp_path):
tool_trace_channel_spec = generate_tool_trace(...)
assert_osi_trace_compliance(
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / "qc_result.xqar",
output_config=tmp_path / "qc_config.xml",
)
```

Use `--qc-osi-version` and `--qc-osi-ruleset` to provide default checker settings for the run.
Use `--qc-omega-prime` to use the Omega Prime ruleset as the default ruleset.
Use `--assert-osi-compliance-version` and `--assert-osi-compliance-ruleset` to provide default checker settings for the run.
Use `--assert-osi-compliance-ruleset-preset omega-prime` to use the Omega Prime ruleset as the default ruleset.
A test can override those defaults per call with `osi_version=` and `ruleset=`.

When a QC check fails, the fixture assertion message includes the trace path, checker statuses, issue counts, and the first reported issues.
Expand Down Expand Up @@ -343,7 +343,7 @@ def example_test_case(osi_trace, odr_file, generate_tool_trace, tmp_path, moving
- Integrate pytest fixtures' names in the test case function as function parameters
- Resource fixtures (e.g., `osi_trace`, `odr_file`)
- Tool execution fixture (`generate_tool_trace`)
- Optional QC fixture (`assert_osi_trace_compliance`)
- Optional QC fixture (`assert_osi_compliance`)
- Pytest built-in fixtures (e.g., `tmp_path`, pytest markers such as `moving_object_id`)
- Integrate a reference implementation (e.g., the OSI to OpenSCENARIO XML Converter osi2osc) to generate an OpenSCENARIO file from a baseline OSI reference trace
- Use `generate_tool_trace` callable and the OpenSCENARIO file to generate the corresponding OSI tool trace
Expand Down
6 changes: 3 additions & 3 deletions osc_validation/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def _optional_path(path):
return pathlib.Path(path)


def make_assert_osi_trace_compliance(
def make_assert_osi_compliance(
*,
qc_enabled: bool,
default_osi_version: str | None = None,
Expand All @@ -25,7 +25,7 @@ def make_assert_osi_trace_compliance(
):
default_ruleset = _optional_path(default_ruleset)

def assert_osi_trace_compliance(
def assert_osi_compliance(
channel_spec,
*,
result_file=None,
Expand Down Expand Up @@ -65,4 +65,4 @@ def assert_osi_trace_compliance(
)
assert result, "QC OSI trace check failed."

return assert_osi_trace_compliance
return assert_osi_compliance
56 changes: 35 additions & 21 deletions osc_validation/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,30 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
help="Write a JUnit XML report to PATH.",
)
parser.add_argument(
"--qc-osi-trace",
"--assert-osi-compliance",
action="store_true",
default=False,
help="Enable QC OSI trace checks at test case call sites.",
help="Enable OSI compliance assertions at test case call sites.",
)
parser.add_argument(
"--qc-osi-version",
"--assert-osi-compliance-version",
default=None,
metavar="VERSION",
help="Default OSI version for QC OSI trace checks.",
help="Default OSI version for OSI compliance assertions.",
)
ruleset_group = parser.add_mutually_exclusive_group()
ruleset_group.add_argument(
"--qc-osi-ruleset",
"--assert-osi-compliance-ruleset",
default=None,
metavar="PATH",
help="Default OSI ruleset YAML file for QC OSI trace checks.",
help="Default OSI ruleset YAML file for OSI compliance assertions.",
)
ruleset_group.add_argument(
"--qc-omega-prime",
action="store_true",
default=False,
help="Use the Omega Prime OSI 3.7.0 ruleset for QC OSI trace checks.",
"--assert-osi-compliance-ruleset-preset",
choices=["omega-prime"],
default=None,
metavar="PRESET",
help="Use a named OSI ruleset preset for OSI compliance assertions.",
)
return parser.parse_args(argv)

Expand Down Expand Up @@ -118,24 +119,37 @@ def _pytest_args(args: argparse.Namespace, validation_dir: Path) -> list[str]:
)
if args.junitxml is not None:
pytest_args.append(f"--junitxml={_resolve_from_cwd(args.junitxml)}")
if args.qc_osi_trace:
pytest_args.append("--qc-osi-trace")
if args.qc_osi_version is not None:
pytest_args.extend(["--qc-osi-version", args.qc_osi_version])
if args.qc_osi_ruleset is not None:
pytest_args.extend(["--qc-osi-ruleset", _resolve_from_cwd(args.qc_osi_ruleset)])
if args.qc_omega_prime:
pytest_args.append("--qc-omega-prime")
if args.assert_osi_compliance:
pytest_args.append("--assert-osi-compliance")
if args.assert_osi_compliance_version is not None:
pytest_args.extend(
["--assert-osi-compliance-version", args.assert_osi_compliance_version]
)
if args.assert_osi_compliance_ruleset is not None:
pytest_args.extend(
[
"--assert-osi-compliance-ruleset",
_resolve_from_cwd(args.assert_osi_compliance_ruleset),
]
)
if args.assert_osi_compliance_ruleset_preset is not None:
pytest_args.extend(
[
"--assert-osi-compliance-ruleset-preset",
args.assert_osi_compliance_ruleset_preset,
]
)

return pytest_args


def main(argv: list[str] | None = None) -> int:
args = _parse_args(argv)
validation_dir = _validation_dir()
if not (validation_dir / "pytest.ini").exists() or not (
validation_dir / "scenario"
).exists():
if (
not (validation_dir / "pytest.ini").exists()
or not (validation_dir / "scenario").exists()
):
raise FileNotFoundError(
"Installed validation suite not found at "
f"'{validation_dir}'. "
Expand Down
54 changes: 30 additions & 24 deletions osc_validation/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
``--tool-wrapper-module``, ``--test-profile``).
* Initialise the tool under test and expose it via the ``generate_tool_trace``
session fixture.
* Expose optional QC OSI trace checking via the
``assert_osi_trace_compliance`` session fixture.
* Expose optional OSI compliance assertions via the
``assert_osi_compliance`` session fixture.
* Apply xfail markers from an optional test-profile file.
* Add validation metadata to the pytest report header.

Expand All @@ -33,16 +33,14 @@
import pytest

from osc_validation import __version__ as osc_validation_version
from osc_validation.assertions import make_assert_osi_trace_compliance
from osc_validation.assertions import make_assert_osi_compliance
from osc_validation.dataproviders import DownloadDataProvider
from osc_validation.test_profile import load_test_profile
from osc_validation.tools.esmini import ESMini
from osc_validation.tools.gtgen_cli import GTGen_Simulator
from osc_validation.tools.osc_simulator import OscSimulator

OMEGA_PRIME_OSI_370_RULESET_URL = (
"https://raw.githubusercontent.com/thomassedlmayer/omega-prime/7ed09192ffeaa12bfab04281d17a5b5dc2702197/docs/osirules/omega-prime-osi_3-7-0.yml"
)
OMEGA_PRIME_OSI_370_RULESET_URL = "https://raw.githubusercontent.com/thomassedlmayer/omega-prime/7ed09192ffeaa12bfab04281d17a5b5dc2702197/docs/osirules/omega-prime-osi_3-7-0.yml"


class UnknownToolError(ValueError):
Expand Down Expand Up @@ -175,30 +173,31 @@ def pytest_addoption(parser):
help="Path to a TOML test profile file declaring expected failures for this run",
)
group.addoption(
"--qc-osi-trace",
"--assert-osi-compliance",
action="store_true",
default=False,
help="Enable QC OSI trace checks at assert_osi_trace_compliance fixture call sites",
help="Enable OSI compliance assertions at assert_osi_compliance fixture call sites",
)
group.addoption(
"--qc-osi-version",
"--assert-osi-compliance-version",
action="store",
default=None,
metavar="VERSION",
help="Default OSI version for QC OSI trace checks",
help="Default OSI version for OSI compliance assertions",
)
group.addoption(
"--qc-osi-ruleset",
"--assert-osi-compliance-ruleset",
action="store",
default=None,
metavar="PATH",
help="Default OSI ruleset YAML file for QC OSI trace checks",
help="Default OSI ruleset YAML file for OSI compliance assertions",
)
group.addoption(
"--qc-omega-prime",
action="store_true",
default=False,
help="Use the Omega Prime OSI 3.7.0 ruleset for QC OSI trace checks",
"--assert-osi-compliance-ruleset-preset",
action="store",
default=None,
metavar="PRESET",
help="Use a named OSI ruleset preset for OSI compliance assertions",
)


Expand Down Expand Up @@ -290,18 +289,25 @@ def pytest_collection_modifyitems(config, items):


@pytest.fixture(scope="session")
def assert_osi_trace_compliance(request, tmp_path_factory):
"""Assert OSI trace QC compliance when enabled by a QC option."""
qc_enabled = request.config.getoption("--qc-osi-trace") or request.config.getoption(
"--qc-omega-prime"
def assert_osi_compliance(request, tmp_path_factory):
"""Assert OSI trace compliance when enabled by an assertion option."""
ruleset_preset = request.config.getoption("--assert-osi-compliance-ruleset-preset")
if ruleset_preset not in {None, "omega-prime"}:
raise pytest.UsageError(
"Unsupported OSI compliance ruleset preset: "
f"{ruleset_preset}. Supported presets: omega-prime."
)

qc_enabled = request.config.getoption("--assert-osi-compliance") or (
ruleset_preset is not None
)
default_osi_version = request.config.getoption("--qc-osi-version")
default_ruleset = request.config.getoption("--qc-osi-ruleset")
default_osi_version = request.config.getoption("--assert-osi-compliance-version")
default_ruleset = request.config.getoption("--assert-osi-compliance-ruleset")

if request.config.getoption("--qc-omega-prime"):
if ruleset_preset == "omega-prime":
default_ruleset = _download_omega_prime_ruleset(tmp_path_factory)

return make_assert_osi_trace_compliance(
return make_assert_osi_compliance(
qc_enabled=qc_enabled,
default_osi_version=default_osi_version,
default_ruleset=default_ruleset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def _compute_object_state_metric(
def test_init_teleport_action_places_actor_at_expected_pose(
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
):
"""
Expand Down Expand Up @@ -106,6 +107,10 @@ def test_init_teleport_action_places_actor_at_expected_pose(
rate=rate,
)
assert_no_osc_engine_errors(tool_trace_channel_spec)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / "qc_result_init_teleport_action.xqar",
)

metric_result = _compute_object_state_metric(
reference_channel_spec=case_result.reference_channel_spec,
Expand All @@ -125,6 +130,7 @@ def test_init_teleport_action_places_actor_at_expected_pose(
def test_init_add_entity_action_places_actor_at_expected_pose(
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
):
"""
Expand Down Expand Up @@ -167,6 +173,10 @@ def test_init_add_entity_action_places_actor_at_expected_pose(
rate=rate,
)
assert_no_osc_engine_errors(tool_trace_channel_spec)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / "qc_result_init_add_entity_action.xqar",
)

metric_result = _compute_object_state_metric(
reference_channel_spec=case_result.reference_channel_spec,
Expand All @@ -186,6 +196,7 @@ def test_init_add_entity_action_places_actor_at_expected_pose(
def test_init_speed_action_sets_expected_motion(
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
):
"""
Expand Down Expand Up @@ -229,6 +240,10 @@ def test_init_speed_action_sets_expected_motion(
rate=rate,
)
assert_no_osc_engine_errors(tool_trace_channel_spec)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / "qc_result_init_speed_action.xqar",
)

metric_result = _compute_object_state_metric(
reference_channel_spec=case_result.reference_channel_spec,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_vehicle_dimensions_match_reference(
osi_trace: Path,
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
moving_object_id: int,
tolerance: float = 0.1,
Expand Down Expand Up @@ -67,6 +68,10 @@ def test_vehicle_dimensions_match_reference(
log_path=tmp_path,
rate=0.05,
)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / f"qc_result_vehicle_dimensions_{moving_object_id}.xqar",
)

result = ObjectStateMetric().compute(
reference_channel_spec=reference_trace_channel_spec,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_split_ego_trajectory_preserves_reference_motion(
osi_trace: Path,
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
sequencing_level: str,
entity_ref: str = "Ego",
Expand Down Expand Up @@ -97,6 +98,10 @@ def test_split_ego_trajectory_preserves_reference_motion(
rate=rate,
)
assert_no_osc_engine_errors(tool_trace_channel_spec)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / f"qc_result_split_{sequencing_level}.xqar",
)

metric = TrajectorySimilarityMetric(
name="TrajectorySimilarityMetric", plot_path=tmp_path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def _off_trajectory_init_pose() -> TrajectoryInterpolationVertex:
def test_follow_trajectory_future_time_reference_continues_init_speed_until_first_vertex(
odr_file: Path,
generate_tool_trace: Callable,
assert_osi_compliance: Callable,
tmp_path: Path,
):
"""
Expand Down Expand Up @@ -114,6 +115,10 @@ def test_follow_trajectory_future_time_reference_continues_init_speed_until_firs
rate=rate,
)
assert_no_osc_engine_errors(tool_trace_channel_spec)
assert_osi_compliance(
tool_trace_channel_spec,
result_file=tmp_path / "qc_result_follow_trajectory_future_time_reference.xqar",
)

full_trace_result = ObjectStateMetric().compute(
reference_channel_spec=case_result.reference_channel_spec,
Expand Down
Loading