379 lines
12 KiB
Elixir
379 lines
12 KiB
Elixir
defmodule RPNCalculatorInspectionTest do
|
|
use ExUnit.Case, async: false
|
|
|
|
defmodule RPNCalculator do
|
|
def unsafe_division(input) do
|
|
[_, a, b] = Regex.run(~r/^(\d*) (\d*) \/$/, input)
|
|
String.to_integer(a) / String.to_integer(b)
|
|
end
|
|
end
|
|
|
|
defp flush_messages() do
|
|
receive do
|
|
_ ->
|
|
flush_messages()
|
|
after
|
|
50 ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
setup_all do
|
|
# turning off the logger to avoid error logs spamming the output
|
|
# when the functions are expected to crash
|
|
Logger.configure(level: :none)
|
|
end
|
|
|
|
setup do
|
|
# just in case, we clear the test process inbox before each test
|
|
flush_messages()
|
|
%{}
|
|
end
|
|
|
|
describe "start_reliability_check" do
|
|
@tag task_id: 1
|
|
test "returns a map with test data" do
|
|
calculator = fn _ -> 0 end
|
|
input = "1 2 +"
|
|
result = RPNCalculatorInspection.start_reliability_check(calculator, input)
|
|
assert is_map(result)
|
|
assert is_pid(result.pid)
|
|
assert result.input == input
|
|
end
|
|
|
|
@tag task_id: 1
|
|
test "starts a linked process" do
|
|
old_value = Process.flag(:trap_exit, true)
|
|
|
|
calculator = fn _ -> :timer.sleep(50) end
|
|
input = "1 2 +"
|
|
|
|
%{pid: pid} = RPNCalculatorInspection.start_reliability_check(calculator, input)
|
|
|
|
assert pid in Keyword.get(Process.info(self()), :links)
|
|
assert_receive {:EXIT, ^pid, :normal}
|
|
|
|
Process.flag(:trap_exit, old_value)
|
|
end
|
|
|
|
@tag task_id: 1
|
|
test "the process runs the calculator function with the given input" do
|
|
caller_process_pid = self()
|
|
|
|
calculator = fn input -> send(caller_process_pid, input) end
|
|
input = "7 3 +"
|
|
|
|
RPNCalculatorInspection.start_reliability_check(calculator, input)
|
|
|
|
assert_receive ^input
|
|
end
|
|
end
|
|
|
|
describe "await_reliability_check_result" do
|
|
@tag task_id: 2
|
|
test "adds `input` => :ok to the results after a normal exit" do
|
|
caller_process_pid = self()
|
|
test_data = %{pid: caller_process_pid, input: "2 3 +"}
|
|
check_results_so_far = %{"2 0 /" => :error}
|
|
expected_result = %{"2 0 /" => :error, "2 3 +" => :ok}
|
|
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, :normal})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "adds `input` => :error to the results after an abnormal exit" do
|
|
caller_process_pid = self()
|
|
test_data = %{pid: caller_process_pid, input: "3 0 /"}
|
|
check_results_so_far = %{"1 1 +" => :ok}
|
|
expected_result = %{"1 1 +" => :ok, "3 0 /" => :error}
|
|
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, {%ArithmeticError{}, []}})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "adds `input` => :timeout to the results if no message arrives in 100ms" do
|
|
caller_process_pid = self()
|
|
test_data = %{pid: caller_process_pid, input: "24 12 /"}
|
|
check_results_so_far = %{"3 1 +" => :ok}
|
|
expected_result = %{"3 1 +" => :ok, "24 12 /" => :timeout}
|
|
|
|
task =
|
|
Task.async(fn ->
|
|
:timer.sleep(200)
|
|
# this message should arrive too late
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, {%ArithmeticError{}, []}})
|
|
end)
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
Task.await(task)
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "normal exit messages from processes whose pids don't match stay in the inbox" do
|
|
caller_process_pid = self()
|
|
other_process_pid = spawn(fn -> nil end)
|
|
test_data = %{pid: caller_process_pid, input: "5 0 /"}
|
|
check_results_so_far = %{"5 0 +" => :ok}
|
|
expected_result = %{"5 0 +" => :ok, "5 0 /" => :error}
|
|
|
|
send(caller_process_pid, {:EXIT, other_process_pid, :normal})
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, {%ArithmeticError{}, []}})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
assert Keyword.get(Process.info(self()), :message_queue_len) == 1
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "abnormal exit messages from processes whose pids don't match stay in the inbox" do
|
|
caller_process_pid = self()
|
|
other_process_pid = spawn(fn -> nil end)
|
|
test_data = %{pid: caller_process_pid, input: "2 2 +"}
|
|
check_results_so_far = %{"0 0 /" => :error}
|
|
expected_result = %{"0 0 /" => :error, "2 2 +" => :ok}
|
|
|
|
send(caller_process_pid, {:EXIT, other_process_pid, {%ArithmeticError{}, []}})
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, :normal})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
assert Keyword.get(Process.info(self()), :message_queue_len) == 1
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "any other messages stay in the inbox" do
|
|
caller_process_pid = self()
|
|
test_data = %{pid: caller_process_pid, input: "4 2 /"}
|
|
check_results_so_far = %{"4 0 /" => :error}
|
|
expected_result = %{"4 0 /" => :error, "4 2 /" => :ok}
|
|
|
|
send(caller_process_pid, {:exit, caller_process_pid, {%ArithmeticError{}, []}})
|
|
send(caller_process_pid, {:something_else, caller_process_pid, {%ArithmeticError{}, []}})
|
|
send(caller_process_pid, :something_else)
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, :normal})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
assert Keyword.get(Process.info(self()), :message_queue_len) == 3
|
|
end
|
|
|
|
@tag task_id: 2
|
|
test "doesn't change the trap_exit flag of the caller process" do
|
|
caller_process_pid = self()
|
|
Process.flag(:trap_exit, false)
|
|
|
|
test_data = %{pid: caller_process_pid, input: "30 3 /"}
|
|
check_results_so_far = %{}
|
|
expected_result = %{"30 3 /" => :ok}
|
|
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, :normal})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
assert Keyword.get(Process.info(self()), :trap_exit) == false
|
|
Process.flag(:trap_exit, true)
|
|
|
|
send(caller_process_pid, {:EXIT, caller_process_pid, :normal})
|
|
|
|
assert RPNCalculatorInspection.await_reliability_check_result(
|
|
test_data,
|
|
check_results_so_far
|
|
) ==
|
|
expected_result
|
|
|
|
assert Keyword.get(Process.info(self()), :trap_exit) == true
|
|
end
|
|
end
|
|
|
|
describe "reliability_check" do
|
|
@tag task_id: 3
|
|
test "returns an empty map when input list empty" do
|
|
inputs = []
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = %{}
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "returns a map with inputs as keys and :ok as values" do
|
|
inputs = ["4 2 /", "8 2 /", "6 3 /"]
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
|
|
outputs = %{
|
|
"4 2 /" => :ok,
|
|
"8 2 /" => :ok,
|
|
"6 3 /" => :ok
|
|
}
|
|
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "returns a map when input list has 1000 elements" do
|
|
inputs = Enum.map(1..1000, &"#{2 * &1} #{&1} /")
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = 1..1000 |> Enum.map(&{"#{2 * &1} #{&1} /", :ok}) |> Enum.into(%{})
|
|
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "returns a map when input list has 1000 elements and the calculator takes 50ms for each calculation" do
|
|
inputs = Enum.map(1..1000, &"#{2 * &1} #{&1} /")
|
|
parent_pid = self()
|
|
calculator = fn input -> :timer.sleep(50) && RPNCalculator.unsafe_division(input) end
|
|
|
|
Task.start_link(fn ->
|
|
outputs = RPNCalculatorInspection.reliability_check(calculator, inputs)
|
|
send(parent_pid, {:outputs, outputs})
|
|
end)
|
|
|
|
expected = 1..1000 |> Enum.map(&{"#{2 * &1} #{&1} /", :ok}) |> Enum.into(%{})
|
|
|
|
assert_receive(
|
|
{:outputs, ^expected},
|
|
5000,
|
|
"This test shouldn't take this long to complete. Make sure to start all tasks first before awaiting them."
|
|
)
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "returns :error values for inputs that cause the calculator to crash" do
|
|
inputs = ["3 0 /", "22 11 /", "4 0 /"]
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
|
|
outputs = %{
|
|
"3 0 /" => :error,
|
|
"22 11 /" => :ok,
|
|
"4 0 /" => :error
|
|
}
|
|
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "returns a map when input list has 1000 elements and all of them crash" do
|
|
inputs = Enum.map(1..1000, &"#{2 * &1} 0 /")
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = 1..1000 |> Enum.map(&{"#{2 * &1} 0 /", :error}) |> Enum.into(%{})
|
|
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 3
|
|
test "restores the original value of the trap_exit flag" do
|
|
inputs = ["3 0 /", "22 11 /", "4 0 /"]
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
|
|
outputs = %{
|
|
"3 0 /" => :error,
|
|
"22 11 /" => :ok,
|
|
"4 0 /" => :error
|
|
}
|
|
|
|
Process.flag(:trap_exit, false)
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
assert Keyword.get(Process.info(self()), :trap_exit) == false
|
|
|
|
Process.flag(:trap_exit, true)
|
|
assert RPNCalculatorInspection.reliability_check(calculator, inputs) == outputs
|
|
assert Keyword.get(Process.info(self()), :trap_exit) == true
|
|
end
|
|
end
|
|
|
|
describe "correctness_check" do
|
|
@tag task_id: 4
|
|
test "returns an empty list when input list empty" do
|
|
inputs = []
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = []
|
|
assert RPNCalculatorInspection.correctness_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 4
|
|
test "returns a list of results" do
|
|
inputs = ["3 2 /", "4 2 /", "5 2 /"]
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = [1.5, 2, 2.5]
|
|
assert RPNCalculatorInspection.correctness_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 4
|
|
test "returns a list of results when input list has 1000 elements" do
|
|
inputs = Enum.map(1..1000, &"100 #{&1} /")
|
|
calculator = &RPNCalculator.unsafe_division/1
|
|
outputs = Enum.map(1..1000, &(100 / &1))
|
|
assert RPNCalculatorInspection.correctness_check(calculator, inputs) == outputs
|
|
end
|
|
|
|
@tag task_id: 4
|
|
test "returns a list of results when input list has 1000 elements and the calculator takes 50ms for each calculation" do
|
|
inputs = Enum.map(1..1000, &"100 #{&1} /")
|
|
parent_pid = self()
|
|
calculator = fn input -> :timer.sleep(50) && RPNCalculator.unsafe_division(input) end
|
|
|
|
Task.start_link(fn ->
|
|
outputs = RPNCalculatorInspection.correctness_check(calculator, inputs)
|
|
send(parent_pid, {:outputs, outputs})
|
|
end)
|
|
|
|
expected = Enum.map(1..1000, &(100 / &1))
|
|
|
|
assert_receive(
|
|
{:outputs, ^expected},
|
|
5000,
|
|
"This test shouldn't take this long to complete. Make sure to start all tasks first before awaiting them."
|
|
)
|
|
end
|
|
|
|
@tag task_id: 4
|
|
test "awaits a single task for 100ms" do
|
|
inputs = ["1 1 /1"]
|
|
calculator = fn _ -> :timer.sleep(500) end
|
|
|
|
Process.flag(:trap_exit, true)
|
|
pid = spawn_link(fn -> RPNCalculatorInspection.correctness_check(calculator, inputs) end)
|
|
|
|
assert_receive {:EXIT, ^pid, {:timeout, {Task, task_fn, [_task, 100]}}}
|
|
when task_fn in [:await, :await_many],
|
|
150,
|
|
"expected to receive a timemout message from Task.await/2 or Task.await_many/2"
|
|
|
|
Process.flag(:trap_exit, false)
|
|
end
|
|
end
|
|
end
|