diff --git a/src/openjd/model/v2023_09/_model.py b/src/openjd/model/v2023_09/_model.py index 8d7ae71a..763fb053 100644 --- a/src/openjd/model/v2023_09/_model.py +++ b/src/openjd/model/v2023_09/_model.py @@ -758,15 +758,6 @@ def _requires_oneof(cls, values: dict[str, Any], info: ValidationInfo) -> dict[s on_enter = values.get("onEnter") on_exit = values.get("onExit") - # Base 2023-09 (§3.5) requires onEnter whenever a script is present; - # RFC 0008 relaxes this to "at least one action" when the - # WRAP_ACTIONS extension is declared. The strict base rule is only - # applied at template decode (context present) — job-instantiation - # re-validation has no parsing context, matching the other extension - # gates in this module. - if context is not None and "WRAP_ACTIONS" not in extensions: - if on_enter is None: - raise ValueError("onEnter is required.") if on_enter is None and on_exit is None: raise ValueError("Must define one of: onEnter or onExit") return values diff --git a/test/openjd/model_v0/v2023_09/test_action.py b/test/openjd/model_v0/v2023_09/test_action.py index d415f4a0..a8b86d3c 100644 --- a/test/openjd/model_v0/v2023_09/test_action.py +++ b/test/openjd/model_v0/v2023_09/test_action.py @@ -164,6 +164,7 @@ class TestEnvironmentActions: "data", ( pytest.param({"onEnter": {"command": "foo"}}, id="has onEnter"), + pytest.param({"onExit": {"command": "foo"}}, id="has onExit"), # For making sure our pre-validator logic is correct pytest.param( { @@ -187,26 +188,35 @@ def test_parse_success(self, data: dict[str, Any]) -> None: # THEN # no exception was raised. - def test_parse_onexit_only_with_wrap_actions_extension(self) -> None: - # RFC 0008: with the WRAP_ACTIONS extension declared, an environment - # may define any single action without a standalone onEnter. + @pytest.mark.parametrize( + "supported_extensions", + ( + pytest.param([], id="no extensions"), + pytest.param(["EXPR", "WRAP_ACTIONS"], id="EXPR and WRAP_ACTIONS"), + ), + ) + def test_parse_onexit_only(self, supported_extensions: list[str]) -> None: + # An environment may define onExit without onEnter; either ordinary + # action alone satisfies the one-of requirement, with or without an + # extension being declared. # GIVEN - context = ModelParsingContext(supported_extensions=["EXPR", "WRAP_ACTIONS"]) + context = ModelParsingContext(supported_extensions=supported_extensions) # WHEN - _parse_model(model=EnvironmentActions, obj={"onExit": {"command": "foo"}}, context=context) + actions = _parse_model( + model=EnvironmentActions, obj={"onExit": {"command": "foo"}}, context=context + ) # THEN - # no exception was raised. + assert actions.onEnter is None + assert actions.onExit is not None + assert actions.onExit.command == "foo" @pytest.mark.parametrize( "data", ( pytest.param({}, id="empty object"), - # §3.5: base 2023-09 requires onEnter whenever a script is - # present (the WRAP_ACTIONS extension relaxes this). - pytest.param({"onExit": {"command": "foo"}}, id="onExit only"), pytest.param({"onEnter": {"command": "foo"}, "onUnknown": "blah"}, id="unknown field"), ), ) @@ -219,3 +229,15 @@ def test_parse_fails(self, data: dict[str, Any]) -> None: # THEN assert len(excinfo.value.errors()) > 0 + + def test_parse_fails_no_actions_message(self) -> None: + # An actions object with neither ordinary action must be rejected with + # the one-of message; this is the only remaining constraint on which + # ordinary actions an environment defines. + + # WHEN + with pytest.raises(ValidationError) as excinfo: + _parse_model(model=EnvironmentActions, obj={}) + + # THEN + assert "Must define one of: onEnter or onExit" in str(excinfo.value) diff --git a/test/openjd/model_v0/v2023_09/test_environment_template.py b/test/openjd/model_v0/v2023_09/test_environment_template.py index 864e1345..ed0ca37a 100644 --- a/test/openjd/model_v0/v2023_09/test_environment_template.py +++ b/test/openjd/model_v0/v2023_09/test_environment_template.py @@ -21,6 +21,16 @@ class TestEnvironmentTemplate: {"specificationVersion": "environment-2023-09", "environment": ENVIRONMENT}, id="minimum required", ), + pytest.param( + { + "specificationVersion": "environment-2023-09", + "environment": { + "name": "Foo", + "script": {"actions": {"onExit": {"command": "foo"}}}, + }, + }, + id="script with onExit only", + ), pytest.param( { "specificationVersion": "environment-2023-09", diff --git a/test/openjd/model_v0/v2023_09/test_job_template.py b/test/openjd/model_v0/v2023_09/test_job_template.py index d58d4baa..8a96a89c 100644 --- a/test/openjd/model_v0/v2023_09/test_job_template.py +++ b/test/openjd/model_v0/v2023_09/test_job_template.py @@ -73,6 +73,20 @@ class TestJobTemplate: }, id="with least environments", ), + pytest.param( + { + "specificationVersion": "jobtemplate-2023-09", + "name": "Foo", + "steps": [STEP_TEMPLATE], + "jobEnvironments": [ + { + "name": "Foo", + "script": {"actions": {"onExit": {"command": "foo"}}}, + } + ], + }, + id="with environment defining onExit only", + ), pytest.param( { "specificationVersion": "jobtemplate-2023-09",