diff --git a/Cargo.lock b/Cargo.lock index 5eb3dc68..00aeaa21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -696,9 +696,9 @@ dependencies = [ [[package]] name = "openjd-model" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdfffa82b1dbb94865d10c935cd547b4319932280f37b87db475dca961b7ef91" +checksum = "ca2537f12cbe572cb0f0a36bca44c858e8e943a30603c583c959aa8453a2c62d" dependencies = [ "indexmap", "openjd-expr", @@ -728,9 +728,9 @@ dependencies = [ [[package]] name = "openjd-sessions" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877446585bd38cff6a40c68293f62b2147e5fa4f0997a83a39c5d2af38585449" +checksum = "9dc782dfcc53e6850422c30526e94fb80657650c7123152f05166b9873e20e49" dependencies = [ "bitflags", "futures-util", diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt index 3b23a560..d2851ffa 100644 --- a/THIRD-PARTY-LICENSES.txt +++ b/THIRD-PARTY-LICENSES.txt @@ -2535,8 +2535,8 @@ limitations under the License. ** libc; version 0.2.189 -- https://crates.io/crates/libc ** manyhow-macros; version 0.11.4 -- https://crates.io/crates/manyhow-macros ** openjd-expr; version 0.7.0 -- https://crates.io/crates/openjd-expr -** openjd-model; version 0.7.0 -- https://crates.io/crates/openjd-model -** openjd-sessions; version 0.5.7 -- https://crates.io/crates/openjd-sessions +** openjd-model; version 0.7.1 -- https://crates.io/crates/openjd-model +** openjd-sessions; version 0.5.8 -- https://crates.io/crates/openjd-sessions ** pin-project-lite; version 0.2.17 -- https://crates.io/crates/pin-project-lite ** portable-atomic; version 1.15.0 -- https://crates.io/crates/portable-atomic ** proc-macro2; version 1.0.107 -- https://crates.io/crates/proc-macro2 diff --git a/rust-bindings/Cargo.toml b/rust-bindings/Cargo.toml index 235bcc7c..ab2b966b 100644 --- a/rust-bindings/Cargo.toml +++ b/rust-bindings/Cargo.toml @@ -13,8 +13,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] openjd-expr = "0.7.0" -openjd-model = "0.7.0" -openjd-sessions = "0.5.7" +openjd-model = "0.7.1" +openjd-sessions = "0.5.8" tokio = { version = "1", features = ["rt-multi-thread"] } uuid = { version = "1", features = ["v4"] } serde_json = "1" diff --git a/test/openjd/model_v1/test_parse.py b/test/openjd/model_v1/test_parse.py index 35267aef..13bee176 100644 --- a/test/openjd/model_v1/test_parse.py +++ b/test/openjd/model_v1/test_parse.py @@ -405,3 +405,69 @@ def test_json_explicit(self) -> None: def test_invalid_yaml_raises(self) -> None: with pytest.raises(DecodeValidationError): decode_environment_template_str(": not a mapping") + + +class TestStepEnvironmentNameScope(object): + """A Step Environment's ``name`` is scoped to the Step that defines it (Template + Schemas §3 StepTemplate, §4 Environment): unique within that Step's list, and + distinct from every Job Environment. Different Steps may reuse a name. + + openjd-model 0.7.1 (openjd-rs#381) relaxed an over-strict check that held every + environment name in the template in one set, so the second Step to declare + ``StepEnv`` was rejected. The v0 path always accepted this; this is the v1 path, + which had no coverage. + """ + + @staticmethod + def _environment(name: str) -> dict[str, Any]: + return { + "name": name, + "script": {"actions": {"onEnter": {"command": "echo", "args": [name]}}}, + } + + @classmethod + def _step(cls, name: str, environment_names: list[str]) -> dict[str, Any]: + return { + "name": name, + "stepEnvironments": [cls._environment(n) for n in environment_names], + "script": {"actions": {"onRun": {"command": "echo", "args": [name]}}}, + } + + @classmethod + def _template(cls, steps: list[dict[str, Any]]) -> dict[str, Any]: + return { + "specificationVersion": "jobtemplate-2023-09", + "name": "T", + "jobEnvironments": [cls._environment("JobEnv")], + "steps": steps, + } + + def test_same_name_across_steps_is_accepted(self) -> None: + """Four Steps each declare ``StepEnv``. Only one Step's environments are ever + active in a Session, so these names never collide.""" + template = self._template([self._step(f"Step{i}", ["StepEnv"]) for i in range(4)]) + job_template = decode_job_template(template=template, supported_extensions=[]) + names = [[e.name for e in (s.step_environments or [])] for s in job_template.steps] + assert names == [["StepEnv"]] * 4 + + def test_duplicate_within_one_step_is_rejected(self) -> None: + """Control for §3 rule 1: the per-Step uniqueness check must survive the relaxation.""" + template = self._template( + [self._step("Step0", ["StepEnv"]), self._step("Step1", ["StepEnv", "StepEnv"])] + ) + with pytest.raises(ModelValidationError) as excinfo: + decode_job_template(template=template, supported_extensions=[]) + message = str(excinfo.value) + assert "steps[1] -> stepEnvironments[1]" in message + assert "duplicate environment name: 'StepEnv'" in message + + def test_step_env_named_like_job_env_is_rejected(self) -> None: + """Control for §3 rule 2: a Step Environment may not reuse a Job Environment name.""" + template = self._template( + [self._step("Step0", ["StepEnv"]), self._step("Step1", ["JobEnv"])] + ) + with pytest.raises(ModelValidationError) as excinfo: + decode_job_template(template=template, supported_extensions=[]) + message = str(excinfo.value) + assert "steps[1] -> stepEnvironments[0]" in message + assert "duplicate environment name: 'JobEnv'" in message