200 lines
9.8 KiB
OCaml
200 lines
9.8 KiB
OCaml
open Test_harness
|
|
|
|
let row customer total = Value.VRecord ("order", [ ("customer", Value.VString customer); ("total", Value.VInt total) ])
|
|
|
|
let input entries = List.mapi (fun index entry -> (index + 1, entry)) entries
|
|
|
|
let run fixture_name entries =
|
|
let program = infer (read_fixture fixture_name) in
|
|
Interpret.program program (input entries)
|
|
|
|
let run_text text entries =
|
|
let program = infer text in
|
|
Interpret.program program (input entries)
|
|
|
|
let cases =
|
|
[
|
|
( "the example query selects and projects rows",
|
|
fun () ->
|
|
let result =
|
|
run "expensive_order.delta"
|
|
[ row "Ada" 1500; row "Bo" 900; row "Lin" 2200 ]
|
|
in
|
|
check_equal_string "result" "(collection (1 (tuple \"Ada\" 300)) (3 (tuple \"Lin\" 440)))"
|
|
(Value.to_string result) );
|
|
( "a filter that keeps nothing yields an empty collection",
|
|
fun () ->
|
|
let result = run "expensive_order.delta" [ row "Bo" 10 ] in
|
|
check_equal_string "empty" "(collection )" (Value.to_string result) );
|
|
( "an empty input yields an empty collection and zero aggregates",
|
|
fun () ->
|
|
check_equal_string "no rows" "(collection )" (Value.to_string (run "expensive_order.delta" []));
|
|
check_equal_int "revenue" 0
|
|
(match run "revenue.delta" [] with Value.VInt total -> total | _ -> -1);
|
|
check_equal_int "count" 0
|
|
(match run "count_large.delta" [] with Value.VInt total -> total | _ -> -1) );
|
|
( "revenue sums the taxed totals",
|
|
fun () ->
|
|
let result = run "revenue.delta" [ row "Ada" 1000; row "Bo" 250 ] in
|
|
check_equal_string "revenue" "250" (Value.to_string result) );
|
|
( "count_large counts the retained rows",
|
|
fun () ->
|
|
let result = run "count_large.delta" [ row "Ada" 1000; row "Bo" 250; row "Lin" 501 ] in
|
|
check_equal_string "count" "2" (Value.to_string result) );
|
|
( "negative values are preserved",
|
|
fun () ->
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> filter (fun r -> r < 0) |> sum\n"
|
|
[ Value.VInt (-5); Value.VInt 3; Value.VInt (-7) ]
|
|
in
|
|
check_equal_string "sum" "-12" (Value.to_string result) );
|
|
( "integer division truncates towards zero",
|
|
fun () ->
|
|
let result = run_text "input rows : collection int\nquery q = rows |> map (fun r -> r / 3) |> sum\n"
|
|
[ Value.VInt 10; Value.VInt (-10) ]
|
|
in
|
|
check_equal_string "sum" "0" (Value.to_string result) );
|
|
( "division by zero is a runtime error",
|
|
fun () ->
|
|
(try
|
|
ignore (run_text "input rows : collection int\nquery q = rows |> map (fun r -> 100 / r) |> sum\n"
|
|
[ Value.VInt 5; Value.VInt 0 ]);
|
|
fail "division" "expected a runtime error"
|
|
with Division_by_zero -> check "raised" true) );
|
|
( "the mapping expression runs only where the filter keeps rows",
|
|
fun () ->
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> filter (fun r -> r > 10) |> map (fun r -> 1000 / r) |> sum\n"
|
|
[ Value.VInt 5; Value.VInt 20 ]
|
|
in
|
|
check_equal_string "sum" "50" (Value.to_string result) );
|
|
( "boolean operators short circuit",
|
|
fun () ->
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> count\n"
|
|
[ Value.VInt 1 ]
|
|
in
|
|
check_equal_string "count" "1" (Value.to_string result);
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> filter (fun r -> false && 1 / r > 0) |> count\n"
|
|
[ Value.VInt 0 ]
|
|
in
|
|
check_equal_string "short circuit and" "0" (Value.to_string result);
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> filter (fun r -> true || 1 / r > 0) |> count\n"
|
|
[ Value.VInt 0 ]
|
|
in
|
|
check_equal_string "short circuit or" "1" (Value.to_string result) );
|
|
( "map preserves keys and filter keeps the original keys",
|
|
fun () ->
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> filter (fun r -> r > 1) |> map (fun r -> r * 10)\n"
|
|
[ Value.VInt 1; Value.VInt 2; Value.VInt 3 ]
|
|
in
|
|
check_equal_string "keys" "(collection (2 20) (3 30))" (Value.to_string result) );
|
|
( "helpers are applied at their call sites",
|
|
fun () ->
|
|
let result =
|
|
run_text
|
|
"input rows : collection int\nlet scale n = n * 3\nlet offset n = scale n + 1\nquery q = rows |> map offset |> sum\n"
|
|
[ Value.VInt 1; Value.VInt 2 ]
|
|
in
|
|
check_equal_string "sum" "11" (Value.to_string result) );
|
|
( "records and tuples are compared structurally",
|
|
fun () ->
|
|
let result =
|
|
run_text
|
|
"type pair = { first : int; second : int }\ninput rows : collection pair\nquery q = rows |> filter (fun r -> r = { first = 1; second = 2 }) |> count\n"
|
|
[ Value.VRecord ("pair", [ ("first", Value.VInt 1); ("second", Value.VInt 2) ]); Value.VRecord ("pair", [ ("first", Value.VInt 2); ("second", Value.VInt 1) ]) ]
|
|
in
|
|
check_equal_string "count" "1" (Value.to_string result) );
|
|
( "a constant query ignores the input",
|
|
fun () ->
|
|
let result = run_text "input rows : collection int\nquery q = 6 * 7\n" [ Value.VInt 1 ] in
|
|
check_equal_string "constant" "42" (Value.to_string result) );
|
|
( "out of range arithmetic follows machine integers",
|
|
fun () ->
|
|
let result =
|
|
run_text "input rows : collection int\nquery q = rows |> sum\n"
|
|
[ Value.VInt max_int; Value.VInt 1 ]
|
|
in
|
|
check_equal_string "wrapped" (string_of_int min_int) (Value.to_string result) );
|
|
]
|
|
|
|
let plan text = Graph.build (Anf.program (Specialize.program (infer text)))
|
|
|
|
let plan_of_fixture name = Graph.build (Anf.program (Specialize.program (infer (read_fixture name))))
|
|
|
|
let graph_cases =
|
|
[
|
|
( "the example plan is a source, a filter and a map",
|
|
fun () ->
|
|
let plan = plan_of_fixture "expensive_order.delta" in
|
|
check_equal_int "three nodes" 3 (List.length plan.Graph.pl_nodes);
|
|
let nodes = plan.Graph.pl_nodes in
|
|
check "source first" (match (List.nth nodes 0).Graph.n_kind with Graph.Source -> true | _ -> false);
|
|
check "filter second"
|
|
(match (List.nth nodes 1).Graph.n_kind with Graph.Filter _ -> true | _ -> false);
|
|
check "map last" (match (List.nth nodes 2).Graph.n_kind with Graph.Map _ -> true | _ -> false);
|
|
check_equal_string "output" "collection (string, int)" (Types.pp plan.Graph.pl_output) );
|
|
( "the revenue plan ends in a sum accumulator",
|
|
fun () ->
|
|
let plan = plan_of_fixture "revenue.delta" in
|
|
let root = Graph.node_of_id plan (match plan.Graph.pl_result with Graph.Result_collection id -> id | _ -> -1) in
|
|
check "sum" (match root.Graph.n_kind with Graph.Sum -> true | _ -> false);
|
|
check "accumulator cache" (root.Graph.n_cache = Graph.Accumulator);
|
|
check_equal_string "linear output" "int" (Types.pp plan.Graph.pl_output) );
|
|
( "the count query counts the retained rows",
|
|
fun () ->
|
|
let plan = plan_of_fixture "count_large.delta" in
|
|
check_equal_int "three nodes" 3 (List.length plan.Graph.pl_nodes);
|
|
let root =
|
|
match plan.Graph.pl_result with
|
|
| Graph.Result_collection id -> Graph.node_of_id plan id
|
|
| Graph.Result_scalar _ ->
|
|
fail "plan" "expected a node result";
|
|
{ Graph.n_id = -1; n_kind = Graph.Source; n_input = None; n_element = Types.TInt; n_cache = Graph.No_cache; n_span = Location.none }
|
|
in
|
|
check "count" (match root.Graph.n_kind with Graph.Count -> true | _ -> false) );
|
|
( "collection nodes record their consumers",
|
|
fun () ->
|
|
let plan = plan_of_fixture "expensive_order.delta" in
|
|
check_equal_string "source consumers" "1" (Util.join "," (List.map string_of_int plan.Graph.pl_consumers.(0)));
|
|
check_equal_string "filter consumers" "2" (Util.join "," (List.map string_of_int plan.Graph.pl_consumers.(1)));
|
|
check_equal_string "map has no consumers" "" (Util.join "," (List.map string_of_int plan.Graph.pl_consumers.(2))) );
|
|
( "an identity query has a single source node",
|
|
fun () ->
|
|
let plan = plan "input rows : collection int\nquery q = rows\n" in
|
|
check_equal_int "one node" 1 (List.length plan.Graph.pl_nodes);
|
|
check "the result is the source"
|
|
(match plan.Graph.pl_result with Graph.Result_collection 0 -> true | _ -> false) );
|
|
( "a plan dump is deterministic",
|
|
fun () ->
|
|
Ident.reset ();
|
|
Types.reset ();
|
|
Anf.reset ();
|
|
let first = Graph.dump (plan_of_fixture "expensive_order.delta") in
|
|
Ident.reset ();
|
|
Types.reset ();
|
|
Anf.reset ();
|
|
let second = Graph.dump (plan_of_fixture "expensive_order.delta") in
|
|
check_equal_string "identical" first second );
|
|
( "an integer query that only uses the input is a plain plan",
|
|
fun () ->
|
|
let plan = plan "input rows : collection int\nquery q = rows |> sum\n" in
|
|
check_equal_int "two nodes" 2 (List.length plan.Graph.pl_nodes);
|
|
check "integer output" (Types.repr plan.Graph.pl_output = Types.TInt) );
|
|
( "a constant integer query produces no collection nodes",
|
|
fun () ->
|
|
let plan = plan "input rows : collection int\nquery q = 40 + 2\n" in
|
|
check_equal_int "no nodes" 0 (List.length plan.Graph.pl_nodes);
|
|
check "scalar result" (match plan.Graph.pl_result with Graph.Result_scalar _ -> true | _ -> false) );
|
|
( "shared collection temporaries are reuse",
|
|
fun () ->
|
|
let plan =
|
|
plan
|
|
"input rows : collection int\nquery q = rows |> filter (fun r -> r > 0) |> map (fun r -> r + 1) |> sum\n"
|
|
in
|
|
check_equal_int "four nodes" 4 (List.length plan.Graph.pl_nodes) );
|
|
]
|