diff --git a/lib/flow_runner.ex b/lib/flow_runner.ex index 5e137e1..4124953 100644 --- a/lib/flow_runner.ex +++ b/lib/flow_runner.ex @@ -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!( @@ -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 diff --git a/lib/flow_runner/spec/exit.ex b/lib/flow_runner/spec/exit.ex index a8a6d78..04ce636 100644 --- a/lib/flow_runner/spec/exit.ex +++ b/lib/flow_runner/spec/exit.ex @@ -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 diff --git a/test/block_test.exs b/test/block_test.exs index f889e56..dc33718 100644 --- a/test/block_test.exs +++ b/test/block_test.exs @@ -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{ diff --git a/test/flow_runner/expression_mode_test.exs b/test/flow_runner/expression_mode_test.exs index 0ffcbdf..232dff7 100644 --- a/test/flow_runner/expression_mode_test.exs +++ b/test/flow_runner/expression_mode_test.exs @@ -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