aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/test_vm.ml134
1 files changed, 131 insertions, 3 deletions
diff --git a/test/test_vm.ml b/test/test_vm.ml
index 3631ee5..e564a83 100644
--- a/test/test_vm.ml
+++ b/test/test_vm.ml
@@ -3,11 +3,11 @@
let load source =
match Sol.Front.parse_string source with
| Error message -> Alcotest.failf "parse error: %s" message
- | Ok statements -> Sol.World.Standalone.load statements
+ | Ok statements -> Sol.Program.load statements
let run source =
match load source with
- | Error error -> Alcotest.failf "load error: %a" Sol.World.pp_error error
+ | Error error -> Alcotest.failf "load error: %a" Sol.Program.pp_error error
| Ok (world, queries) ->
List.map
(fun query ->
@@ -133,7 +133,7 @@ let test_mutual_recursion_rejected () =
let test_unbounded_membership_is_non_total_plan () =
match load {| Query expression: [Integer value: X] |} with
- | Error error -> Alcotest.failf "load error: %a" Sol.World.pp_error error
+ | Error error -> Alcotest.failf "load error: %a" Sol.Program.pp_error error
| Ok (world, [ query ]) -> (
match Sol.Vm.Standalone.solve world query with
| Error (Sol.Vm.Non_total_plan _) -> ()
@@ -145,6 +145,53 @@ let test_domain_constrains_when_ground () =
{| Query expression: [Between value: X lower: -2 upper: 2] :and [Natural value: X] |}
"X=0; X=1; X=2"
+(* Negation: decidable denial over ground goals. *)
+
+let test_negation () =
+ check_answers "people who are not adults"
+ (people
+ ^ {| Query expression: ([Person name: N age: A]) :not [Adult name: N] |})
+ {|A=12,N="Ana"|}
+
+let test_negation_defers_until_ground () =
+ check_answers "denial waits for its variables"
+ (people
+ ^ {| Query expression: ([Adult name: N] :not [Equal left: N right: "Rui"]) :and [Person name: N age: A] |})
+ {|N="Marcos",A=30|}
+
+(* Quantifiers. *)
+
+let test_forall_holds () =
+ check_answers "every person is at least 10"
+ (people
+ ^ {| Query expression: [ForAll variable: P quantifier: [Person name: P age: B] body: [GreaterEqual left: B right: 10]] |})
+ ""
+
+let test_forall_fails () =
+ match
+ run
+ (people
+ ^ {| Query expression: [ForAll variable: P quantifier: [Person name: P age: B] body: [GreaterEqual left: B right: 18]] |})
+ with
+ | [ [] ] -> ()
+ | [ _ :: _ ] -> Alcotest.fail "the universal must fail: Ana is 12"
+ | _ -> Alcotest.fail "expected one query"
+
+let test_forall_over_infinite_rejected () =
+ match load {| Query expression: [ForAll variable: X quantifier: [Integer value: X] body: [True]] |} with
+ | Error error -> Alcotest.failf "load error: %a" Sol.Program.pp_error error
+ | Ok (world, [ query ]) -> (
+ match Sol.Vm.Standalone.solve world query with
+ | Error (Sol.Vm.Non_total_plan _) -> ()
+ | Ok _ -> Alcotest.fail "a universal over an infinite domain must be rejected")
+ | Ok _ -> Alcotest.fail "expected one query"
+
+let test_exists () =
+ check_answers "existence lowers to conjunction"
+ (people
+ ^ {| Query expression: [Exists variable: P quantifier: [Person name: P age: B] body: [GreaterThan left: B right: 60]] |})
+ "B=70"
+
(* Integrity control at loading: facts must satisfy their schema. *)
let test_fact_domain_violation () =
@@ -158,6 +205,63 @@ let test_fact_domain_violation () =
| Error _ -> ()
| Ok _ -> Alcotest.fail "a fact violating its domain must be rejected"
+(* Membership criteria as domains: refinements and foreign keys. *)
+
+let departments =
+ {|
+ Relation named: Department schema: #( [id domain: Integer] )
+ [Department id: 1]
+ [Department id: 2]
+ Relation named: Employee schema: #(
+ [name domain: String]
+ [dept domain: ([Department id: D])]
+ [score domain: ([Natural value: S] :and [LessThanEqual left: S right: 100])]
+ )
+ |}
+
+let test_criterion_admits () =
+ check_answers "a valid employee is admitted"
+ (departments
+ ^ {|
+ [Employee name: "Marcos" dept: 1 score: 90]
+ Query expression: [Employee name: N dept: D score: S]
+ |})
+ {|D=1,N="Marcos",S=90|}
+
+let test_foreign_key_rejected () =
+ match
+ load (departments ^ {| [Employee name: "Ghost" dept: 9 score: 50] |})
+ with
+ | Error (Sol.Program.Integrity { relation = "Employee"; label = "dept" }) -> ()
+ | Error error -> Alcotest.failf "wrong error: %a" Sol.Program.pp_error error
+ | Ok _ -> Alcotest.fail "a dangling foreign key must be rejected"
+
+let test_refinement_rejected () =
+ match
+ load (departments ^ {| [Employee name: "Over" dept: 1 score: 101] |})
+ with
+ | Error (Sol.Program.Integrity { relation = "Employee"; label = "score" }) -> ()
+ | Error error -> Alcotest.failf "wrong error: %a" Sol.Program.pp_error error
+ | Ok _ -> Alcotest.fail "a score beyond the refinement must be rejected"
+
+let test_fact_order_is_irrelevant () =
+ (* The employee arrives before its department; integrity is judged
+ against the complete extension. *)
+ match
+ load
+ {|
+ Relation named: Department schema: #( [id domain: Integer] )
+ Relation named: Employee schema: #(
+ [name domain: String]
+ [dept domain: ([Department id: D])]
+ )
+ [Employee name: "Early" dept: 7]
+ [Department id: 7]
+ |}
+ with
+ | Ok _ -> ()
+ | Error error -> Alcotest.failf "load error: %a" Sol.Program.pp_error error
+
let () =
Alcotest.run "sol-vm"
[
@@ -197,4 +301,28 @@ let () =
Alcotest.test_case "fact domain violation" `Quick
test_fact_domain_violation;
] );
+ ( "negation",
+ [
+ Alcotest.test_case "not adults" `Quick test_negation;
+ Alcotest.test_case "denial defers until ground" `Quick
+ test_negation_defers_until_ground;
+ ] );
+ ( "quantifiers",
+ [
+ Alcotest.test_case "forall holds" `Quick test_forall_holds;
+ Alcotest.test_case "forall fails" `Quick test_forall_fails;
+ Alcotest.test_case "forall over infinite rejected" `Quick
+ test_forall_over_infinite_rejected;
+ Alcotest.test_case "exists" `Quick test_exists;
+ ] );
+ ( "criteria",
+ [
+ Alcotest.test_case "criterion admits" `Quick test_criterion_admits;
+ Alcotest.test_case "foreign key rejected" `Quick
+ test_foreign_key_rejected;
+ Alcotest.test_case "refinement rejected" `Quick
+ test_refinement_rejected;
+ Alcotest.test_case "fact order irrelevant" `Quick
+ test_fact_order_is_irrelevant;
+ ] );
]