From 2bb6a55b33098f4319ed94665367e08a7d71d2d4 Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Fri, 27 Dec 2019 22:58:27 -1000 Subject: [PATCH] codelab: adding an assertion construct to WDL --- WDL/Tree.py | 8 ++++++++ WDL/_grammar.py | 9 ++++++--- WDL/_parser.py | 3 +++ WDL/runtime/task.py | 2 ++ WDL/runtime/workflow.py | 4 +++- tests/test_7runner.py | 45 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/WDL/Tree.py b/WDL/Tree.py index ae3708c2..a44fb4e0 100644 --- a/WDL/Tree.py +++ b/WDL/Tree.py @@ -230,6 +230,14 @@ def _workflow_node_dependencies(self) -> Iterable[str]: yield from _expr_workflow_node_dependencies(self.expr) +class Assertion(Decl): + message: str + + def __init__(self, pos: SourcePosition, expr: Expr.Base) -> None: + super().__init__(pos, Type.Boolean(), f"_assert_L{pos.line}C{pos.column}", expr) + self.message = f"assertion failed: {str(expr)} ({pos.uri} Ln {pos.line} Col {pos.column})" + + class Task(SourceNode): """ WDL Task diff --git a/WDL/_grammar.py b/WDL/_grammar.py index caf63589..e21b9c75 100644 --- a/WDL/_grammar.py +++ b/WDL/_grammar.py @@ -289,11 +289,11 @@ /////////////////////////////////////////////////////////////////////////////////////////////////// workflow: "workflow" CNAME "{" workflow_element* "}" -?workflow_element: input_decls | any_decl | call | scatter | conditional | workflow_outputs | meta_section +?workflow_element: input_decls | any_decl | call | scatter | conditional | workflow_outputs | meta_section | assertion scatter: "scatter" "(" CNAME "in" expr ")" "{" inner_workflow_element* "}" conditional: "if" "(" expr ")" "{" inner_workflow_element* "}" -?inner_workflow_element: any_decl | call | scatter | conditional +?inner_workflow_element: any_decl | call | scatter | conditional | assertion call: "call" namespaced_ident call_body? -> call | "call" namespaced_ident "as" CNAME call_body? -> call_as @@ -314,6 +314,7 @@ | meta_section | runtime_section | any_decl -> noninput_decl + | assertion -> noninput_decl tasks: task* @@ -352,6 +353,8 @@ struct: "struct" CNAME "{" unbound_decl* "}" +assertion: "assert" expr + /////////////////////////////////////////////////////////////////////////////////////////////////// // type /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -484,7 +487,7 @@ %ignore COMMENT """ keywords["development"] = set( - "Array Float Int Map None Pair String alias as call command else false if import input left meta object output parameter_meta right runtime scatter struct task then true workflow".split( + "Array Float Int Map None Pair String alias as assert call command else false if import input left meta object output parameter_meta right runtime scatter struct task then true workflow".split( " " ) ) diff --git a/WDL/_parser.py b/WDL/_parser.py index daf05a7a..1602ecb6 100644 --- a/WDL/_parser.py +++ b/WDL/_parser.py @@ -268,6 +268,9 @@ def decl(self, items, meta): self._sp(meta), items[0], items[1].value, (items[2] if len(items) > 2 else None) ) + def assertion(self, items, meta): + return Tree.Assertion(self._sp(meta), items[0]) + def input_decls(self, items, meta): return {"inputs": items} diff --git a/WDL/runtime/task.py b/WDL/runtime/task.py index 7e0514da..a87e96a6 100644 --- a/WDL/runtime/task.py +++ b/WDL/runtime/task.py @@ -1035,6 +1035,8 @@ def map_files(v: Value.Base) -> Value.Base: vj = json.dumps(v.json) logger.info(_("eval", name=decl.name, value=(v.json if len(vj) < 4096 else "(((large)))"))) container_env = container_env.bind(decl.name, v) + if isinstance(decl, Tree.Assertion) and not v.value: + raise Error.RuntimeError(decl.message) return container_env diff --git a/WDL/runtime/workflow.py b/WDL/runtime/workflow.py index 92597c4f..828af550 100644 --- a/WDL/runtime/workflow.py +++ b/WDL/runtime/workflow.py @@ -45,7 +45,7 @@ from contextlib import ExitStack import importlib_metadata from .. import Env, Type, Value, Tree, StdLib -from ..Error import InputError +from ..Error import InputError, RuntimeError from .task import run_local_task, _filenames, link_outputs from .download import able as downloadable, run_cached as download from .._util import ( @@ -354,6 +354,8 @@ def _do_job( else: assert job.node.type.optional v = Value.Null() + if isinstance(job.node, Tree.Assertion) and not v.value: + raise RuntimeError(job.node.message) return Env.Bindings(Env.Binding(job.node.name, v)) if isinstance(job.node, WorkflowOutputs): diff --git a/tests/test_7runner.py b/tests/test_7runner.py index 979ca53c..93a8cee9 100644 --- a/tests/test_7runner.py +++ b/tests/test_7runner.py @@ -167,3 +167,48 @@ def test_download_cache4(self): line = json.loads(line) if "downloaded input files" in line["message"]: self.assertEqual(line["downloaded"], 0) + + +class TestAssert(RunnerTestCase): + task1 = R""" + version development + task div { + input { + Int numerator + Int denominator + } + assert denominator != 0 + command { + expr ~{numerator} / ~{denominator} + } + output { + Int quotient = read_int(stdout()) + } + } + """ + + def test_positive(self): + outputs = self._run(self.task1, {"numerator": 7, "denominator": 2}) + self.assertEqual(outputs["quotient"], 3) + + def test_negative(self): + self._run(self.task1, {"numerator": 7, "denominator": 0}, expected_exception=WDL.Error.RuntimeError) + + wf1 = R""" + version development + workflow div { + input { + Int numerator + Int denominator + } + assert denominator != 0 + output { + Int quotient = numerator / denominator + } + } + """ + + def test_workflow(self): + outputs = self._run(self.wf1, {"numerator": 7, "denominator": 2}) + self.assertEqual(outputs["quotient"], 3) + self._run(self.wf1, {"numerator": 7, "denominator": 0}, expected_exception=WDL.Error.RuntimeError)