(**************************************************************************)
(*  Copyright 2014, 2015, Sebastien Mondet <seb@mondet.org>               *) (*                                                                        *) (*  Licensed under the Apache License, Version 2.0 (the "License");       *) (*  you may not use this file except in compliance with the License.      *) (*  You may obtain a copy of the License at                               *) (*                                                                        *) (*      http://www.apache.org/licenses/LICENSE-2.0                        *) (*                                                                        *) (*  Unless required by applicable law or agreed to in writing, software   *) (*  distributed under the License is distributed on an "AS IS" BASIS,     *) (*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or       *) (*  implied.  See the License for the specific language governing         *) (*  permissions and limitations under the License.                        *)
(**************************************************************************)
open Nonstd open Pvem_lwt_unix open Pvem_lwt_unix.Deferred_result module String = Sosa.Native_string  let (//) = Filename.concat let say fmt = ksprintf (printf "%s\n%!") fmt module Test = struct   exception Tests_failed   let max_failures =      try Sys.getenv "MAX_FAILURES" |> int_of_string with _ -> 2_000_000   let failed_tests = ref []   let fail s =     failed_tests := s :: !failed_tests;     if List.length !failed_tests > max_failures then (       List.iter !failed_tests ~f:(fun t ->           eprintf "Failed test: %S\n%!" t         );       raise Tests_failed      ) else ()   let new_tmp_dir () =     let db_file = Filename.temp_file  "trakeva_tmp_test" ".d" in     Sys.command (sprintf "rm -rf %s" db_file) |> ignore;     Sys.command (sprintf "mkdir -p %s" db_file) |> ignore;     db_file   let run_monad name f =     Lwt_main.run (f ())     |> function     | `Ok () -> ()     | `Error (`Database e) ->       ksprintf fail "%S ends with error: %s" name         (Trakeva.Error.to_string e)   let check names c =     if c then ()     else ksprintf fail "test.assert failed: %s" (String.concat ~sep:" → " names)   let now () = Unix.gettimeofday () end    module type TEST_DATABASE = sig   val test_name: string   module DBTrakeva.KEY_VALUE_STORE   val debug_mode : bool -> unit end module In_memory : TEST_DATABASE = struct   let test_name = "in-mem"   let debug_mode _ = ()   module DB = struct     type t = {       nocol: (string, string) Hashtbl.t;       cols: (string, (string, string) Hashtbl.t) Hashtbl.t;     }     let load _ = return {nocol = Hashtbl.create 42; cols = Hashtbl.create 42}      let close _ = return ()     let get_collection t = function       | None -> t.nocol       | Some s ->         begin try Hashtbl.find t.cols s         with _ ->           let newone = Hashtbl.create 42 in           Hashtbl.add t.cols s newone;           newone         end     let get ?collection t ~key =       let col = get_collection t collection in       begin try Some (Hashtbl.find col key) |> return        with _ -> return None       end     let get_all t ~collection =       let col = get_collection t (Some collection) in       let l = ref [] in       Hashtbl.iter (fun k v -> l := k :: !l) col;       return !l     let iterator t ~collection =       let allref = ref None in       begin fun () ->         begin match !allref with         | None ->           get_all t collection           >>= fun all ->           let a = ref all in           allref := Some a;           return a         | Some l -> return l         end         >>= fun all ->         match !all with         | [] -> return None         | h :: t -> all := t; return (Some h)       end     let act t ~action =       let open Trakeva.Action in       let open Trakeva.Key_in_collection in       let rec go = function       | Set ({key; collection}, v) ->         let col = get_collection t collection in         Hashtbl.replace col key v;         true       | Unset {key; collection} ->         let col = get_collection t collection in         Hashtbl.remove col key;         true       | Check _ -> true       | Sequence l ->         List.fold l ~init:true ~f:(fun prev act -> prev && go act)       in       match go action with       | true -> return `Done       | false -> return `Not_done   end end let open_close_test (module Test_db : TEST_DATABASE) uri_string () =   let open Test_db in   DB.load uri_string   >>= fun db ->   DB.close db let basic_test (module Test_db : TEST_DATABASE) uri_string () =   let open Test_db in   let open Trakeva.Action in   let local_assert name c =     Test.check (name :: test_name :: uri_string :: []) c in   DB.load uri_string   >>= fun db ->   let test_get ?(handle=db) ?collection k f =     DB.get handle ?collection ~key:k     >>= fun opt ->     local_assert (sprintf "get %s/%s" (Option.value collection ~default:"") k)       (f opt);     return () in   let is_none = ((=) Nonein   test_get "k" is_none >>= fun () ->   test_get ~collection:"c" "k" is_none >>= fun () ->   DB.get_all db ~collection:"c"   >>= fun list ->   local_assert "all c" (list = []);   let test_actions res actions =     let action = seq actions in     DB.act db ~action     >>= function     | r when r = res -> return ()     | `Done ->       ksprintf Test.fail "Action %s should be Not_done" (to_string action);       return ()     | `Not_done ->       ksprintf Test.fail "Action %s should be Done" (to_string action);       return ()   in   test_actions `Done [set ~key:"k" "v0"] >>= fun () ->   test_get  "k" ((=) (Some "v0")) >>= fun () ->   test_actions `Done [set ~key:"k" "v"] >>= fun () ->   test_get  "k" ((=) (Some "v")) >>= fun () ->   test_actions `Done [     contains ~key:"k" "v";     unset "k";     set ~key:"k1" ~collection:"c" "V";   ] >>= fun () ->   test_actions `Done [     set ~key:"thekey" ~collection:"c1" "v1";     set ~key:"thekey" ~collection:"c2" "v2";   ]   >>= fun () ->   test_get ?collection:None "thekey" ((=) None) >>= fun () ->   test_get ~collection:"c1" "thekey" ((=) (Some "v1")) >>= fun () ->   test_get ~collection:"c2" "thekey" ((=) (Some "v2")) >>= fun () ->   test_actions `Not_done [     contains ~key:"k" "v";     set ~key:"k1" ~collection:"c" "V";   ] >>= fun () ->   test_actions `Done [     is_not_set "k";     set ~key:"k2" ~collection:"c" "V2";     set ~key:"k3" ~collection:"c" "V3";     set ~key:"k4" ~collection:"c" "V4";     set ~key:"k5" ~collection:"c" "V5";   ] >>= fun () ->   DB.get_all db ~collection:"c"   >>= fun list ->   local_assert "full collection 'c'"     (List.sort ~cmp:String.compare list = ["k1""k2""k3""k4""k5"]);   let key = "K" in   let insane =     let buf = Bytes.make 256 '\000' in     for i = 0 to 255 do Bytes.set buf i (char_of_int i) done;     Bytes.to_string buf in   test_actions `Done [     set ~key "\"";     set ~key "\\\"";     set ~key "\"'\"";     set ~key:insane insane;   ]   >>= fun () ->   test_get ?collection:None insane ((=) (Some insane))   >>= fun () ->   let to_list res_stream =     let rec go acc =       res_stream ()       >>= function       | None -> return acc       | Some v -> go (v :: acc) in     go []   in   let list_equal l1 l2 =     let prep = List.sort ~cmp:Pervasives.compare in     match prep l1 = prep l2 with     | true -> true     | false ->       say "[%s] ≠ [%s]"         (String.concat  ~sep:", " l1)         (String.concat  ~sep:", " l2);       false   in   let check_iterator_like_get_all ~collection =     let stream = DB.iterator db ~collection in     to_list stream     >>= fun all ->     DB.get_all db ~collection     >>= fun all_too ->     local_assert (sprintf "iter %s" collection) (list_equal all all_too);     return ()   in   check_iterator_like_get_all "c" >>= fun () ->   check_iterator_like_get_all "cc" >>= fun () ->   (*      We now test going through a collection with `iterator` while      modifying the values of the collection.   *)   let make_self_collection ~collection =     let keyvalues = List.init 10 ~f:(sprintf "kv%d"in     let actions = List.map keyvalues ~f:(fun kv -> set ~collection ~key:kv kv) in     test_actions `Done actions     >>= fun () ->     return keyvalues   in   let iterate_and_set ~collection =     let stream = DB.iterator db ~collection in     let rec go_through () =       stream () >>= function       | Some kv ->         (* say "%s got some %S in the stream" Test_db.test_name kv; *)         test_actions `Done [set ~collection ~key:kv ("SET" ^ kv)]         >>= fun () ->         go_through ()       | None -> return ()     in     go_through ()   in   let test_rw_interleave collection =     make_self_collection ~collection     >>= fun keyvalues ->     iterate_and_set ~collection     >>= fun () ->     check_iterator_like_get_all collection     >>= fun () ->     DB.get_all db ~collection     >>= fun allnew ->     (* let modified = List.map keyvalues (fun v -> "SET" ^ v) in *)     local_assert (sprintf "test_rw_interleave %s" collection)       (list_equal keyvalues allnew);     return ()   in   (* Test_db.debug_mode true; *)   test_rw_interleave "ccc"   >>= fun () ->   (*      We now try to delete all values in a collection while iterating on it.   *)   let iterate_and_delete ~collection ~at ~all =     let stream = DB.iterator db ~collection in     let rec go_through remaining =       stream () >>= function       | Some kv when remaining = 0 ->         say "%s got some %S in the stream" Test_db.test_name kv;         test_actions `Done (List.map all ~f:(fun key -> unset ~collection key))         >>= fun () ->         go_through (-1)       | Some kv ->         say "%s got some %S in the stream : remaining: %d" Test_db.test_name kv remaining;         go_through (remaining - 1)       | None -> return ()     in     go_through at   in   let test_delete_iterleave collection =     make_self_collection ~collection     >>= fun keyvalues ->     iterate_and_delete       ~collection ~all:keyvalues ~at:(List.length keyvalues / 2)     >>= fun () ->     DB.get_all db ~collection     >>= fun allnew ->     local_assert ("test_delete_iterleave") (allnew = []);     return ()   in   test_delete_iterleave "aaa"   >>= fun () ->   (* Read and write concurrently: *)   let bunch_of_ints = List.init 100 ~f:(fun i -> i) in   Deferred_list.for_concurrent bunch_of_ints ~f:(function     | n when n mod 2 = 0 ->       let v = sprintf "%d" n in       test_actions `Done [set ~key:v v]     | n ->       DB.get db ~key       >>= fun _ ->       return ()     )   >>= fun ((_ : unit list), errors) ->   begin match errors with   | [] -> return ()   | more ->     let msg =       sprintf "concurrent errors: %s"         (List.map more ~f:(function            | `Database (`Get _, m)            | `Database (`Act _, m) -> m)          |> String.concat ~sep:"; "in     local_assert msg false;     return ()   end   >>= fun () ->   (* Test_db.debug_mode false; *)   DB.close db let benchmark_01 (module Test_db : TEST_DATABASE) uri_string     ?(collection = 200) ?(big_string_kb = 10) () =   let open Test_db in   let open Trakeva.Action in   let benches = ref [] in   let add_bench s t = benches := (s, t) :: !benches in   DB.load uri_string   >>= fun db ->   let bench_function s f =     let b = Test.now () in     f ()     >>= fun x ->     let e = Test.now () in     add_bench s (e -. b);     return x   in   let bench_action ~action s =     bench_function s (fun () -> DB.act db ~action:(seq action)) in   let action =     List.init collection (fun i ->         set ~key:(sprintf "k%d" i) ~collection:"c" "small")   in   bench_action ~action (sprintf "%d small strings" collection)   >>= fun _ ->   let bench_in_collection name =     bench_action       (sprintf "Set %d %d KB strings into %s" collection big_string_kb name)       ~action:(         List.init collection (fun i ->             let value = String.make (big_string_kb * 1_000) 'B' in             set ~key:(sprintf "k%d" i) ~collection:name value))     >>= fun _ ->     bench_function (sprintf "Get all collection %s" name)       (fun () -> DB.get_all db name)     >>= fun cc ->     let l = List.length cc in     Test.check ["bench01""length"; name; Int.to_string l] (l = collection);     bench_function (sprintf "Iterate on all collection %s" name)       (fun () ->          let iter = DB.iterator db ~collection:name in          let rec loop () =            iter ()            >>= function            | Some obj -> loop ()            | None -> return ()          in          loop ())     >>= fun () ->     bench_function (sprintf "Get all %s one by one" name)       (fun () ->          let rec loop n =            if n = 0            then return ()            else (              let key = sprintf "k%d" (n - 1) in              DB.get ~collection:name  db ~key              >>= fun v ->              Test.check ["bench01"; key; "not none"; ] (v <> None);              loop (n - 1)            )          in          loop collection)     >>= fun _ ->     return ()   in   bench_in_collection "C1" >>= fun () ->   bench_in_collection "C2" >>= fun () ->   bench_in_collection "C3" >>= fun () ->   bench_in_collection "C3" >>= fun () ->   DB.close db   >>= fun () ->   return (test_name, List.rev !benches)    module Test_sqlite = struct   let test_name = "Test_sqlite"    module DB = Trakeva_sqlite   let debug_mode v = Trakeva_sqlite.debug := v end module Test_sqlite_with_greedy_cache = struct   let test_name = "Test_sqlite_with_greedy_cache"    module DB = Trakeva_cache.Add(Trakeva_sqlite)   let debug_mode v =     Trakeva_sqlite.debug := v;     Trakeva_cache.debug := v; end module Test_postgresql = struct   let test_name = "Test_postgresql"    module DB = Trakeva_of_uri   let debug_mode v = Trakeva_postgresql.debug := v end let pg_server () =   let cmdf fmt =     ksprintf (fun s ->         let ret = Sys.command s in         if ret = 0 then () else failwith (sprintf "command %S returned %d" s ret)       ) fmt in   let dir =  "/tmp/trakeva_test/" in   let port = try Sys.getenv "POSTGRESQL_PORT" |> int_of_string with _ -> 4242 in   cmdf "rm -fr %s" dir;   cmdf "mkdir -p %s" dir;   cmdf "initdb -D %s" dir;   cmdf "PGPORT=%d pg_ctl start -l %s/log -D %s" port dir dir;   say "Starting postgresql test server on port %d with %s" port dir;   let stop () = cmdf "PGPORT=%d pg_ctl -D %s -m fast stop" port dir in   at_exit stop;   object     method stop = stop ()     method status = cmdf "PGPORT=%d pg_ctl -D %s status" port dir     method port = port     method conninfo = sprintf "postgresql:///template1?port=%d" port   end let independent_pg_test () =   let module PG = Postgresql in   let pg = pg_server () in   let conninfo = pg#conninfo in   Unix.sleep 1;   begin try     let handle = new PG.connection ~conninfo () in     let create_table t =       sprintf "CREATE TABLE IF NOT EXISTS %s (collection BYTEA, key BYTEA, value BYTEA, UNIQUE (collection, key))" t in     let res = handle#exec (create_table "test"in     let show_res name res =       say "%s\n  status: %s | error: %s | tuples: %d × %d"         name (PG.result_status res#status) res#error res#ntuples res#nfields;       for i = 0 to res#ntuples - 1 do         say "     (%s)"           (List.init res#nfields (fun j ->                if res#getisnull i j then "Null"                else sprintf "%S" (PG.unescape_bytea (res#getvalue i j)))            |> String.concat ~sep:", ");       done;     in     show_res "create-table" res;     let res = handle#exec (create_table "test"in     show_res "create-table again" res;     let execl l =       List.iter l ~f:(function         | (sql, args) ->           let res =             let as_array = Array.of_list args in             let params =               Array.map as_array ~f:(function | `Null -> PG.null | `V s -> s) in             let binary_params =               Array.map as_array ~f:(function `Null -> false | `V _ -> truein             handle#exec sql ~params ~binary_params           in           let name = String.split sql ~on:(`Character '\n') |> List.hd_exn in           show_res name res         )     in     let add c k v =       execl [         "BEGIN", [];         "LOCK TABLE test IN ACCESS EXCLUSIVE MODE", [];         "UPDATE test SET value = $3 WHERE collection = $1 AND key = $2", [c; `V k; `V v];         {sql|            INSERT INTO test (collection, key, value)                   SELECT $1, $2, $3                   WHERE NOT EXISTS (SELECT 1 FROM test WHERE collection = $1 AND key = $2);       |sql}, [c; `V k; `V v];         "END", [];       ] in     add (`V "C""K1" "V1";     add (`V "C""K2" "V2";     add (`V "C""K3" "V3\000\001";     add (`V "C""K2" "V4";     execl ["SELECT key, value FROM test WHERE collection = $1", [`V "C"];];     execl [       "DELETE FROM test WHERE key = $2 AND collection = $1", [`V "C"`V "K1"];     ];     execl ["SELECT collection, key, value FROM test WHERE collection = $1", [`V "C"];];     add (`Null"K1" "VVVVVV";     add (`V "null""K2" "VVVvvvvvvvVVV";     execl ["SELECT collection, key, value FROM test WHERE collection = $1", [`Null];];     execl ["SELECT collection, key, value FROM test WHERE collection is null", [];];     execl ["SELECT collection, key, value FROM test WHERE collection = $1 or ($1 is null AND collection is null)", [`Null];];     execl ["SELECT collection, key, value FROM test WHERE collection = $1 or ($1 is null AND collection is null)", [`V "C"];];     begin       Test.run_monad "pgtest" (fun () ->           Trakeva_postgresql.load conninfo           >>= fun trak ->           Trakeva_postgresql.get trak ~key:"K"           >>= begin function           | None -> say "get OK"; return ()           | Some s -> say "what??? %S" s; return ()           end           >>= fun () ->           let open Trakeva.Action in           Trakeva_postgresql.act trak  (seq [               set ~key:"K" "VVV";               set ~collection:"C" ~key:"K" "VVV";               set ~key:"K1" "VVV";               unset "K1";               is_not_set "K1";             ])           >>= fun _ ->           Trakeva_postgresql.get trak ~key:"K"           >>= begin function           | None -> say "2nd get NOT OK"; return ()           | Some s -> say "OK: %S" s; return ()           end           >>= fun () ->           let tab = Trakeva_postgresql.table_name trak in           execl [sprintf "SELECT collection, key, value FROM %s WHERE collection = $1 or ($1 is null AND collection is null)" tab, [`Null];];           Trakeva_postgresql.act trak  (seq [               set ~collection:"C" ~key:"K"  "\000";               set ~collection:"C" ~key:"K1" "\000";               set ~collection:"C" ~key:"K2" "\000";               set ~collection:"C" ~key:"K3" "\000";             ])           >>= fun _ ->           Trakeva_postgresql.get_all trak ~collection:"C"           >>= fun list_of_keys ->           say "list_of_keys: %s" (String.concat ~sep:", " list_of_keys);           execl [sprintf "SELECT collection, key, value FROM %s WHERE collection = $1 or ($1 is null AND collection is null)" tab, [`V "C"];];           let it = Trakeva_postgresql.iterator trak ~collection:"C" in           let rec loop () =             it ()             >>= function             | Some key ->               Trakeva_postgresql.get trak ~collection:"C" ~key               >>= fun v ->               say "%s -> %s" key (Option.value v ~default:"????");               loop ()             | None ->               say "Iterator done";               return ()           in           loop ()           >>= fun () ->           (*           execl ["BEGIN", []];           execl [sprintf "DECLARE cur1 CURSOR FOR (SELECT collection, key, value FROM %s WHERE collection = $1 or ($1 is null AND collection is null))" tab, [`V "C"];];           (* execl ["OPEN cur1", []]; *)           execl ["FETCH NEXT FROM cur1", []];           execl ["FETCH NEXT FROM cur1", []];           execl ["FETCH NEXT FROM cur1", []];           execl ["FETCH NEXT FROM cur1", []];           execl ["FETCH NEXT FROM cur1", []];           execl ["FETCH NEXT FROM cur1", []];           execl ["CLOSE cur1", []];           execl ["END", []];              *)           return ()         )     end;     handle#finish;   with   | PG.Error e -> say "Error: %s" (PG.string_of_error e)   | e -> say "Exn: %s" (Printexc.to_string e)   end;   say "EXITING";   exit 0 (* let () = pg#stop  let () = pg#status *) let has_arg arlg possible =   List.exists arlg ~f:(List.mem ~set:possible) let find_arg argl ~name ~convert =   List.find_map argl (fun arg ->       match String.split ~on:(`Character '=') arg |> List.map ~f:String.strip with       | n :: c :: [] when n = name -> convert c       | _ -> None) let () =   let argl = Sys.argv |> Array.to_list |> List.tl_exn in   (* independent_pg_test (); *)      let sqlite_path = "/tmp/trakeva-sqlite-test" in   ksprintf Sys.command "rm -fr %s" sqlite_path |> ignore;   Test.run_monad "basic/sqlite" (basic_test (module Test_sqlite) sqlite_path);   ksprintf Sys.command "rm -fr %s" sqlite_path |> ignore;   Test.run_monad "basic/sqlite-with-cache" (basic_test (module Test_sqlite_with_greedy_cache) sqlite_path);   begin     let conninfo =       try Sys.getenv "POSTGRESQL_CONNECTION_INFO"       with _ ->         let pg = pg_server () in         Unix.sleep 1;         pg#conninfo     in     Test.run_monad "basic/postgres" (basic_test (module Test_postgresql) conninfo);     (* pg#stop; *)   end;   if has_arg argl ["bench""benchmarks"then (     ksprintf Sys.command "rm -fr %s" sqlite_path |> ignore;     let collection = find_arg argl ~name:"collection" ~convert:Int.of_string in     let big_string_kb = find_arg argl ~name:"kb" ~convert:Int.of_string in     let timeout =       find_arg argl ~name:"timeout" ~convert:Float.of_string       |> Option.value ~default:10.     in     let bench_with_timeout file f =       ksprintf Sys.command "rm -fr %s" sqlite_path |> ignore;       System.with_timeout timeout (fun () -> f file)       >>< begin function       | `Ok (_, res) -> return res       | `Error (`Timeout t) -> return ["Timeout", t]       | `Error (`System (_, `Exn e)) -> raise e       | `Error (`Database _ as other) -> fail other       end     in     Test.run_monad "bench01" (fun () ->         bench_with_timeout "/tmp/trakeva-bench-sqlite" (fun path ->             benchmark_01 ?collection ?big_string_kb               (module Test_sqlite) path ()           )         >>= fun sqlite_bench01 ->         bench_with_timeout "/tmp/trakeva-bench-sqlite-with-cache" (fun path ->             benchmark_01 ?collection ?big_string_kb               (module Test_sqlite_with_greedy_cache) path ()           )         >>= fun sqlite_cached_bench01 ->         bench_with_timeout "" (fun _ ->             benchmark_01 ?collection ?big_string_kb               (module In_memory"" ()           )         >>= fun in_mem_bench01 ->         say "Bench01 sqlite: %F\tsqlite+cache: %F\tin-mem: %F"           (List.fold sqlite_bench01 ~init:0. ~f:(fun p (_, f) -> p +. f))           (List.fold sqlite_cached_bench01 ~init:0. ~f:(fun p (_, f) -> p +. f))           (List.fold in_mem_bench01 ~init:0. ~f:(fun p (_, f) -> p +. f)) ;         List.iter sqlite_bench01 ~f:(fun (n, f) ->             say "sqlite\t%s\t%F s" n f           );         List.iter sqlite_cached_bench01 ~f:(fun (n, f) ->             say "sqliteGC\t%s\t%F s" n f           );         List.iter in_mem_bench01 ~f:(fun (n, f) ->             say "in_mem\t%s\t%F s" n f           );         return ()       );   );   begin match !Test.failed_tests with   | [] ->     say "No tests failed \\o/ (arg-list: [%s])"       (String.concat ~sep:"." (List.map argl ~f:(sprintf "%S")));     exit 0   | some ->     say "Some tests failed (arg-list: [%s]):"       (String.concat ~sep:"." (List.map argl ~f:(sprintf "%S")));     List.iter some ~f:(fun t ->         say "%s" t;         say "%s" (String.make 80 '-');       );     exit 3   end