Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/flow_runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ defmodule FlowRunner do
)
end

@doc """
Evaluates a block or condition expression.

Runtime arithmetic in an expression can fail even when the operands are valid
numbers, division by zero being the canonical case, and
`Expression.evaluate_block!/4` surfaces that as an `ArithmeticError`. Rather than
let it crash the calling process, we treat it like any other unusable condition
value: the caller (`FlowRunner.Spec.Exit.evaluate/2`,
`FlowRunner.Spec.Blocks.Case.evaluate_outgoing/5`) falls through to the block's
default exit.
"""
@impl FlowRunner.Contract
def evaluate_expression_block(expression, %Context{} = context) do
Expression.evaluate_block!(
Expand All @@ -134,6 +145,9 @@ defmodule FlowRunner do
rescue
e in Expression.Error ->
{:error, e.type, e.message}

e in ArithmeticError ->
{:error, :arithmetic, Exception.message(e)}
end

defdelegate fetch_resource_by_uuid(container, uuid), to: FlowRunner.Spec.Container
Expand Down
8 changes: 4 additions & 4 deletions lib/flow_runner/spec/exit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ defmodule FlowRunner.Spec.Exit do
test = exit.test || ""

case FlowRunner.evaluate_expression_block(test, context) |> extract_value() do
other when not is_boolean(other) ->
{:error, reason, bad_parts} ->
Logger.info(
"Expression '#{exit.test}' returned #{inspect(other)} when expecting a boolean last_block_uuid=#{context.last_block_uuid}"
"Expression '#{exit.test}' returned parsing error #{inspect(reason)} when hit #{inspect(bad_parts)} last_block_uuid=#{context.last_block_uuid}"
)

false

{:error, reason, bad_parts} ->
other when not is_boolean(other) ->
Logger.info(
"Expression '#{exit.test}' returned parsing error #{inspect(reason)} when hit #{inspect(bad_parts)} last_block_uuid=#{context.last_block_uuid}"
"Expression '#{exit.test}' returned #{inspect(other)} when expecting a boolean last_block_uuid=#{context.last_block_uuid}"
)

false
Expand Down
22 changes: 22 additions & 0 deletions test/block_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,28 @@ defmodule BlockTest do
Block.evaluate_exits(block, context)
end

test "an exit whose test raises a runtime arithmetic error falls through to the default exit" do
context = %Context{
vars: %{"score" => 10, "total" => 0}
}

block = %Block{
exits: [
%Exit{
uuid: "b586afa7-0097-4805-9951-f6d3156c08db",
test: "score / total > 1"
},
%Exit{
uuid: "cbf4382f-0ce0-426b-9a3c-40cbe84dd081",
default: true
}
]
}

assert {:ok, %Exit{uuid: "cbf4382f-0ce0-426b-9a3c-40cbe84dd081"}} =
Block.evaluate_exits(block, context)
end

test "fetch default block" do
# 1. With NO default exit
context = %Context{
Expand Down
8 changes: 8 additions & 0 deletions test/flow_runner/expression_mode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ defmodule FlowRunner.ExpressionModeTest do
assert FlowRunner.evaluate_expression_block("age", context(%{"age" => "30"}, :v3)) ==
"30"
end

test "a runtime arithmetic failure returns an :arithmetic error instead of raising" do
assert {:error, :arithmetic, _message} =
FlowRunner.evaluate_expression_block(
"score / total",
context(%{"score" => 10, "total" => 0}, :v2)
)
end
end

describe "evaluate_expression_as_string!/2" do
Expand Down
Loading