Skip to content

Commit

Permalink
codelab: adding an assertion construct to WDL
Browse files Browse the repository at this point in the history
  • Loading branch information
mlin committed Jan 13, 2021
1 parent 9b10917 commit 1d7873b
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 4 deletions.
8 changes: 8 additions & 0 deletions WDL/Tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,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
Expand Down
9 changes: 6 additions & 3 deletions WDL/_grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ("after" CNAME)* call_body? -> call
| "call" namespaced_ident "as" CNAME ("after" CNAME)* call_body? -> call_as
Expand All @@ -314,6 +314,7 @@
| meta_section
| runtime_section
| any_decl -> noninput_decl
| assertion -> noninput_decl
tasks: task*
Expand Down Expand Up @@ -347,6 +348,8 @@
struct: "struct" CNAME "{" unbound_decl* "}"
assertion: "assert" expr
///////////////////////////////////////////////////////////////////////////////////////////////////
// type
///////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -479,7 +482,7 @@
%ignore COMMENT
"""
keywords["development"] = set(
"Array Directory File 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 Directory File 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(
" "
)
)
Expand Down
3 changes: 3 additions & 0 deletions WDL/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,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}

Expand Down
2 changes: 2 additions & 0 deletions WDL/runtime/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ def map_paths(fn: Union[Value.File, Value.Directory]) -> str:
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

Expand Down
4 changes: 3 additions & 1 deletion WDL/runtime/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from typing import Optional, List, Set, Tuple, NamedTuple, Dict, Union, Iterable, Callable, Any
from contextlib import ExitStack
from .. import Env, Type, Value, Tree, StdLib
from ..Error import InputError
from ..Error import InputError, RuntimeError
from .task import run_local_task, _fspaths, link_outputs, _add_downloadable_defaults
from .download import able as downloadable, run_cached as download
from .._util import (
Expand Down Expand Up @@ -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):
Expand Down
45 changes: 45 additions & 0 deletions tests/test_7runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,3 +897,48 @@ def test_docker(self):
assert sum("18.04" in msg for msg in outputs["results"]) == 2
outputs = self._run(caller, {"message": "hello", "docker": "ubuntu:focal"})
assert sum("20.04" in msg for msg in outputs["results"]) == 2


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)

0 comments on commit 1d7873b

Please sign in to comment.