From dc4e748a894a6be22ead0732611c7180b32e9cc8 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 09754157..e3e99011 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 a53728c9..183ca17b 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* @@ -353,6 +354,8 @@ struct: "struct" CNAME "{" unbound_decl* "}" +assertion: "assert" expr + /////////////////////////////////////////////////////////////////////////////////////////////////// // type /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -484,7 +487,7 @@ %ignore WS """ 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 fbe19750..08246bf5 100644 --- a/WDL/_parser.py +++ b/WDL/_parser.py @@ -242,6 +242,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 40b89d2d..bdbffb9f 100644 --- a/WDL/runtime/task.py +++ b/WDL/runtime/task.py @@ -729,6 +729,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 fd3ec0d8..8c8ad9dc 100644 --- a/WDL/runtime/workflow.py +++ b/WDL/runtime/workflow.py @@ -44,7 +44,7 @@ from typing import Optional, List, Set, Tuple, NamedTuple, Dict, Union, Iterable, Callable, Any 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 as download from .._util import ( @@ -353,6 +353,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 8d0c09ed..911bc202 100644 --- a/tests/test_7runner.py +++ b/tests/test_7runner.py @@ -68,3 +68,48 @@ def test_download_input_files(self): self._run(count, {"files": ["https://google.com/robots.txt", "https://raw.githubusercontent.com/chanzuckerberg/miniwdl/master/tests/alyssa_ben.txt"]}) self._run(count, {"files": ["https://google.com/robots.txt", "https://raw.githubusercontent.com/chanzuckerberg/miniwdl/master/nonexistent12345.txt", "https://raw.githubusercontent.com/chanzuckerberg/miniwdl/master/tests/alyssa_ben.txt"]}, expected_exception=WDL.runtime.DownloadFailed) + + +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)