From e1cbce710f67adc0e6b3daf24db0b8a3ff68a8de Mon Sep 17 00:00:00 2001 From: GlassOfWhiskey Date: Mon, 10 Nov 2025 21:08:41 +0100 Subject: [PATCH 01/10] Discard undeclared fields from `cwl.output.json` This commit discards all fields that are present in a `cwl.output.json` file but have not been declared in the `output` object of the related `CommandLineTool`, implementing common-workflow-language/cwl-v1.3#80. --- cwltool/command_line_tool.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index 54913b47f..9df922e40 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -26,7 +26,7 @@ from mypy_extensions import mypyc_attr from ruamel.yaml.comments import CommentedMap, CommentedSeq -from schema_salad.avro.schema import Schema +from schema_salad.avro.schema import RecordSchema from schema_salad.exceptions import ValidationException from schema_salad.ref_resolver import file_uri, uri_file_path from schema_salad.sourceline import SourceLine @@ -1257,7 +1257,17 @@ def collect_output_ports( if compute_checksum: adjustFileObjs(ret, partial(compute_checksums, fs_access)) - expected_schema = cast(Schema, self.names.get_name("outputs_record_schema", None)) + expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) + for k in list(ret.keys()): + found = False + for field in expected_schema.fields: + if k == field.name: + found = True + break + if not found: + if _logger.isEnabledFor(logging.WARNING): + _logger.warning(f"DIscarded undeclared `{k}` output from {custom_output}") + ret.pop(k) validate_ex( expected_schema, ret, From 72b37ad785dda5b4933bda2e6770b9bbd363bffb Mon Sep 17 00:00:00 2001 From: Iacopo Colonnelli Date: Tue, 11 Nov 2025 13:41:40 +0100 Subject: [PATCH 02/10] Update cwltool/command_line_tool.py Co-authored-by: Michael R. Crusoe <1330696+mr-c@users.noreply.github.com> --- cwltool/command_line_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index 9df922e40..f89e1f8ef 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -1258,7 +1258,7 @@ def collect_output_ports( if compute_checksum: adjustFileObjs(ret, partial(compute_checksums, fs_access)) expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) - for k in list(ret.keys()): + for k in ret: found = False for field in expected_schema.fields: if k == field.name: From 18da237c0174ff646724fea13b35dc5705c0590d Mon Sep 17 00:00:00 2001 From: Iacopo Colonnelli Date: Tue, 11 Nov 2025 13:41:52 +0100 Subject: [PATCH 03/10] Update cwltool/command_line_tool.py Co-authored-by: Michael R. Crusoe <1330696+mr-c@users.noreply.github.com> --- cwltool/command_line_tool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index f89e1f8ef..db820e7f6 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -1265,8 +1265,9 @@ def collect_output_ports( found = True break if not found: - if _logger.isEnabledFor(logging.WARNING): - _logger.warning(f"DIscarded undeclared `{k}` output from {custom_output}") + _logger.warning( + f"Discarded undeclared output named {k!r} from {custom_output}." + ) ret.pop(k) validate_ex( expected_schema, From cf87713fa1180dcd281b83ab86cf148944786974 Mon Sep 17 00:00:00 2001 From: "Michael R. Crusoe" Date: Tue, 11 Nov 2025 14:59:24 +0100 Subject: [PATCH 04/10] make the loop iterable static so we can freely edit the dict --- cwltool/command_line_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index db820e7f6..e814e4610 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -1258,7 +1258,7 @@ def collect_output_ports( if compute_checksum: adjustFileObjs(ret, partial(compute_checksums, fs_access)) expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) - for k in ret: + for k in list(ret.keys()): # so we don't edit the loops iterable later found = False for field in expected_schema.fields: if k == field.name: From 8d0236ebb2a0beee3ce9ae88d52181becedac7e3 Mon Sep 17 00:00:00 2001 From: GlassOfWhiskey Date: Tue, 11 Nov 2025 15:22:46 +0100 Subject: [PATCH 05/10] Added tests --- cwltool/command_line_tool.py | 25 ++++++++++++++----------- tests/test-cwl-out-v1.0.cwl | 20 ++++++++++++++++++++ tests/test-cwl-out-v1.1.cwl | 20 ++++++++++++++++++++ tests/test-cwl-out-v1.2.cwl | 20 ++++++++++++++++++++ tests/test_cwl_output_json.py | 24 ++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 tests/test-cwl-out-v1.0.cwl create mode 100644 tests/test-cwl-out-v1.1.cwl create mode 100644 tests/test-cwl-out-v1.2.cwl create mode 100644 tests/test_cwl_output_json.py diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index e814e4610..8dbfae947 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -1258,17 +1258,20 @@ def collect_output_ports( if compute_checksum: adjustFileObjs(ret, partial(compute_checksums, fs_access)) expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) - for k in list(ret.keys()): # so we don't edit the loops iterable later - found = False - for field in expected_schema.fields: - if k == field.name: - found = True - break - if not found: - _logger.warning( - f"Discarded undeclared output named {k!r} from {custom_output}." - ) - ret.pop(k) + if ORDERED_VERSIONS.index(cast(str, cwl_version)) >= ORDERED_VERSIONS.index( + "v1.3.0-dev1" + ): + for k in list(ret): + found = False + for field in expected_schema.fields: + if k == field.name: + found = True + break + if not found: + _logger.warning( + f"Discarded undeclared output named {k!r} from {custom_output}." + ) + ret.pop(k) validate_ex( expected_schema, ret, diff --git a/tests/test-cwl-out-v1.0.cwl b/tests/test-cwl-out-v1.0.cwl new file mode 100644 index 000000000..4ba6ca66b --- /dev/null +++ b/tests/test-cwl-out-v1.0.cwl @@ -0,0 +1,20 @@ +class: CommandLineTool +cwlVersion: v1.0 +requirements: + - class: ShellCommandRequirement +hints: + DockerRequirement: + dockerPull: docker.io/debian:stable-slim + +inputs: [] + +baseCommand: sh + +arguments: + - -c + - | + echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + +stdout: cwl.output.json + +outputs: {} \ No newline at end of file diff --git a/tests/test-cwl-out-v1.1.cwl b/tests/test-cwl-out-v1.1.cwl new file mode 100644 index 000000000..3f3528655 --- /dev/null +++ b/tests/test-cwl-out-v1.1.cwl @@ -0,0 +1,20 @@ +class: CommandLineTool +cwlVersion: v1.1 +requirements: + - class: ShellCommandRequirement +hints: + DockerRequirement: + dockerPull: docker.io/debian:stable-slim + +inputs: [] + +baseCommand: sh + +arguments: + - -c + - | + echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + +stdout: cwl.output.json + +outputs: {} \ No newline at end of file diff --git a/tests/test-cwl-out-v1.2.cwl b/tests/test-cwl-out-v1.2.cwl new file mode 100644 index 000000000..54aaf2ad7 --- /dev/null +++ b/tests/test-cwl-out-v1.2.cwl @@ -0,0 +1,20 @@ +class: CommandLineTool +cwlVersion: v1.2 +requirements: + - class: ShellCommandRequirement +hints: + DockerRequirement: + dockerPull: docker.io/debian:stable-slim + +inputs: [] + +baseCommand: sh + +arguments: + - -c + - | + echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + +stdout: cwl.output.json + +outputs: {} \ No newline at end of file diff --git a/tests/test_cwl_output_json.py b/tests/test_cwl_output_json.py new file mode 100644 index 000000000..0a2d6548e --- /dev/null +++ b/tests/test_cwl_output_json.py @@ -0,0 +1,24 @@ +import json + +from .util import get_data, get_main_output + + +def test_cwl_outpu_json_missing_field_v1_0() -> None: + """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.0.""" + err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.0.cwl")]) + assert err_code == 0 + assert "foo" in json.loads(stdout) + + +def test_cwl_outpu_json_missing_field_v1_1() -> None: + """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.1.""" + err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.1.cwl")]) + assert err_code == 0 + assert "foo" in json.loads(stdout) + + +def test_cwl_outpu_json_missing_field_v1_2() -> None: + """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.2.""" + err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.2.cwl")]) + assert err_code == 0 + assert "foo" in json.loads(stdout) From 67c1428f3e1705ee3c1b442d5f7923c9d6f06801 Mon Sep 17 00:00:00 2001 From: Iacopo Colonnelli Date: Tue, 11 Nov 2025 15:46:10 +0100 Subject: [PATCH 06/10] Update tests/test-cwl-out-v1.2.cwl Co-authored-by: Michael R. Crusoe <1330696+mr-c@users.noreply.github.com> --- tests/test-cwl-out-v1.2.cwl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test-cwl-out-v1.2.cwl b/tests/test-cwl-out-v1.2.cwl index 54aaf2ad7..ad4c97622 100644 --- a/tests/test-cwl-out-v1.2.cwl +++ b/tests/test-cwl-out-v1.2.cwl @@ -1,10 +1,5 @@ class: CommandLineTool cwlVersion: v1.2 -requirements: - - class: ShellCommandRequirement -hints: - DockerRequirement: - dockerPull: docker.io/debian:stable-slim inputs: [] From dc5c2a5d626a3436a5c00f2dd745d494755ac466 Mon Sep 17 00:00:00 2001 From: Iacopo Colonnelli Date: Tue, 11 Nov 2025 15:46:18 +0100 Subject: [PATCH 07/10] Update tests/test-cwl-out-v1.1.cwl Co-authored-by: Michael R. Crusoe <1330696+mr-c@users.noreply.github.com> --- tests/test-cwl-out-v1.1.cwl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test-cwl-out-v1.1.cwl b/tests/test-cwl-out-v1.1.cwl index 3f3528655..d998e961d 100644 --- a/tests/test-cwl-out-v1.1.cwl +++ b/tests/test-cwl-out-v1.1.cwl @@ -1,10 +1,5 @@ class: CommandLineTool cwlVersion: v1.1 -requirements: - - class: ShellCommandRequirement -hints: - DockerRequirement: - dockerPull: docker.io/debian:stable-slim inputs: [] From 0ffcd88cc20d9c34378facc65559d3cc520fb521 Mon Sep 17 00:00:00 2001 From: Iacopo Colonnelli Date: Tue, 11 Nov 2025 15:46:27 +0100 Subject: [PATCH 08/10] Update tests/test-cwl-out-v1.0.cwl Co-authored-by: Michael R. Crusoe <1330696+mr-c@users.noreply.github.com> --- tests/test-cwl-out-v1.0.cwl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test-cwl-out-v1.0.cwl b/tests/test-cwl-out-v1.0.cwl index 4ba6ca66b..b18bea43c 100644 --- a/tests/test-cwl-out-v1.0.cwl +++ b/tests/test-cwl-out-v1.0.cwl @@ -1,10 +1,5 @@ class: CommandLineTool cwlVersion: v1.0 -requirements: - - class: ShellCommandRequirement -hints: - DockerRequirement: - dockerPull: docker.io/debian:stable-slim inputs: [] From 13f843706dba5aa312ed329cfc5f9c0f96fe0aa2 Mon Sep 17 00:00:00 2001 From: GlassOfWhiskey Date: Tue, 11 Nov 2025 15:48:48 +0100 Subject: [PATCH 09/10] Refactor code --- cwltool/command_line_tool.py | 30 +++++++++++++++--------------- tests/test-cwl-out-v1.0.cwl | 2 +- tests/test-cwl-out-v1.1.cwl | 2 +- tests/test-cwl-out-v1.2.cwl | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cwltool/command_line_tool.py b/cwltool/command_line_tool.py index 8dbfae947..a9d5e57e2 100644 --- a/cwltool/command_line_tool.py +++ b/cwltool/command_line_tool.py @@ -1216,6 +1216,7 @@ def collect_output_ports( if cwl_version != "v1.0": builder.resources["exitCode"] = rcode try: + expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) fs_access = builder.make_fs_access(outdir) custom_output = fs_access.join(outdir, "cwl.output.json") if fs_access.exists(custom_output): @@ -1227,6 +1228,20 @@ def collect_output_ports( custom_output, json_dumps(ret, indent=4), ) + if ORDERED_VERSIONS.index(cast(str, cwl_version)) >= ORDERED_VERSIONS.index( + "v1.3.0-dev1" + ): + for k in list(ret): + found = False + for field in expected_schema.fields: + if k == field.name: + found = True + break + if not found: + _logger.warning( + f"Discarded undeclared output named {k!r} from {custom_output}." + ) + ret.pop(k) else: for i, port in enumerate(ports): with SourceLine( @@ -1257,21 +1272,6 @@ def collect_output_ports( if compute_checksum: adjustFileObjs(ret, partial(compute_checksums, fs_access)) - expected_schema = cast(RecordSchema, self.names.get_name("outputs_record_schema", None)) - if ORDERED_VERSIONS.index(cast(str, cwl_version)) >= ORDERED_VERSIONS.index( - "v1.3.0-dev1" - ): - for k in list(ret): - found = False - for field in expected_schema.fields: - if k == field.name: - found = True - break - if not found: - _logger.warning( - f"Discarded undeclared output named {k!r} from {custom_output}." - ) - ret.pop(k) validate_ex( expected_schema, ret, diff --git a/tests/test-cwl-out-v1.0.cwl b/tests/test-cwl-out-v1.0.cwl index b18bea43c..f3a2c5f51 100644 --- a/tests/test-cwl-out-v1.0.cwl +++ b/tests/test-cwl-out-v1.0.cwl @@ -8,7 +8,7 @@ baseCommand: sh arguments: - -c - | - echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + echo '{"foo": 5 }' stdout: cwl.output.json diff --git a/tests/test-cwl-out-v1.1.cwl b/tests/test-cwl-out-v1.1.cwl index d998e961d..20447ed0e 100644 --- a/tests/test-cwl-out-v1.1.cwl +++ b/tests/test-cwl-out-v1.1.cwl @@ -8,7 +8,7 @@ baseCommand: sh arguments: - -c - | - echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + echo '{"foo": 5 }' stdout: cwl.output.json diff --git a/tests/test-cwl-out-v1.2.cwl b/tests/test-cwl-out-v1.2.cwl index ad4c97622..95acf4940 100644 --- a/tests/test-cwl-out-v1.2.cwl +++ b/tests/test-cwl-out-v1.2.cwl @@ -8,7 +8,7 @@ baseCommand: sh arguments: - -c - | - echo foo > foo && echo '{"foo": {"location": "foo", "class": "File"} }' + echo '{"foo": 5 }' stdout: cwl.output.json From dbfbfabffee53737329a7738c0f7934a4a9fc657 Mon Sep 17 00:00:00 2001 From: "Michael R. Crusoe" Date: Tue, 11 Nov 2025 16:29:40 +0100 Subject: [PATCH 10/10] improve test coverage --- tests/test-cwl-out-v1.3.cwl | 15 +++++++++++++++ tests/test_cwl_output_json.py | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/test-cwl-out-v1.3.cwl diff --git a/tests/test-cwl-out-v1.3.cwl b/tests/test-cwl-out-v1.3.cwl new file mode 100644 index 000000000..b67ea780c --- /dev/null +++ b/tests/test-cwl-out-v1.3.cwl @@ -0,0 +1,15 @@ +class: CommandLineTool +cwlVersion: v1.3.0-dev1 + +inputs: [] + +baseCommand: sh + +arguments: + - -c + - | + echo '{"foo": 5 }' + +stdout: cwl.output.json + +outputs: {} diff --git a/tests/test_cwl_output_json.py b/tests/test_cwl_output_json.py index 0a2d6548e..ce2787aa8 100644 --- a/tests/test_cwl_output_json.py +++ b/tests/test_cwl_output_json.py @@ -3,22 +3,32 @@ from .util import get_data, get_main_output -def test_cwl_outpu_json_missing_field_v1_0() -> None: +def test_cwl_output_json_missing_field_v1_0() -> None: """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.0.""" err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.0.cwl")]) assert err_code == 0 assert "foo" in json.loads(stdout) -def test_cwl_outpu_json_missing_field_v1_1() -> None: +def test_cwl_output_json_missing_field_v1_1() -> None: """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.1.""" err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.1.cwl")]) assert err_code == 0 assert "foo" in json.loads(stdout) -def test_cwl_outpu_json_missing_field_v1_2() -> None: +def test_cwl_output_json_missing_field_v1_2() -> None: """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.2.""" err_code, stdout, _ = get_main_output([get_data("tests/test-cwl-out-v1.2.cwl")]) assert err_code == 0 assert "foo" in json.loads(stdout) + + +def test_cwl_output_json_missing_field_v1_3() -> None: + """Confirm that unknown outputs are propagated from cwl.output.json in CWL v1.3.""" + err_code, stdout, stderr = get_main_output( + ["--enable-dev", get_data("tests/test-cwl-out-v1.3.cwl")] + ) + assert err_code == 0 + assert "foo" not in json.loads(stdout) + assert "Discarded undeclared output named 'foo' from" in stderr