module QuicQuid.BrokerTest(t) where import Control.Concurrent import qualified Data.Map as M import Data.Maybe import Control.Monad(forever) import Control.Monad.Trans (MonadIO, liftIO) import System.IO import QuicQuid.Log import QuicQuid.Test import QuicQuid.Term import QuicQuid.Logic import QuicQuid.Router import QuicQuid.Agent.And import QuicQuid.Agent.DBAgent import QuicQuid.Agent.Curl import QuicQuid.Agent.Random import QuicQuid.Broker -- Naive implementation. import qualified QuicQuid.BrokerCoreNaive as N -- High Performance implementation. import qualified QuicQuid.BrokerCoreSage as S -- Tests the broker and the broker core implementations. {- TODO: * Test invariants: ** Every possible term is an acceptable pattern to answer or ask. ** For every possible ask term, an answer term obtained by substituting variables to subtrees of the ask term matches the ask term (might also verify that the returned bindings are correct). ** Adding a to an existing repository, a new answer or a new ask generates a set of matches that is proper superset of the original repository match sets. ** Any permutation of a sequence of ask and answers will produce the same matches (except for their order). ** Compare the new implementation with the currently deployed one and see that differences are as expected. -} tp = perf 5 t = do -- Choose a core implementation core <- N.core setup [broker core,andAgent,db1,db2] -- ,andAgent -- And run the tests -- Test queries vs the db1+db2 databases testQuery -- Now that we have some queries, test also the answers matches (bad idea, should clean up first). -- testAnswer setup agents = do -- Setup logging -- set to DEBUG to see logs, to ERROR to hide them h <- verboseStreamHandler stderr DEBUG updateGlobalLogger rootLoggerName (setHandlers [h]) updateGlobalLogger rootLoggerName (setLevel ERROR) updateGlobalLogger "Router.readCh" (setLevel WARNING) updateGlobalLogger "QuicQuid.Agent.And" (setLevel ERROR) -- Run the broker + the DB agent mapM_ forkIO $ agents -- Wait for the agents to be running threadDelay $ 10 * 1000 -- simpleTest = ask "random ?X" ,answer "?q" answer "?X ?Y" answer "random ?Z" testQuery = testQ "Query" newQuery id [ t2 "random 7777" [] -- Failed query, no answer. ,t2 "random 1234" ["{}"] -- Successful query, with empty bindings. ,t2 "random ABC" ["{}"] ,t2 "random ?R " ["{R:ABC}","{R:1234}"] ,t2 "integer {min:0,max:3} 1" ["{}"] ,t2 "and[random ?R,integer ?R]" ["{R:1234}"] ,t2 "and[match[?X,IBM],type[?X,?T]]" ["{T:\"/en/company\",X:\"/en/ibm\"}","{T:\"/en/thing\",X:\"/en/ibm\"}"] -- TODO: fix test, add second possibility. ,t2 "and[match[?X,IBM],type[?X,?T],name[?T,?TN],name[?X,?N]]" ["{N:\"Inter Bus Mach\",T:\"/en/company\",TN:\"Company\",X:\"/en/ibm\"}","{N:\"Inter Bus Mach\",T:\"/en/thing\",TN:\"A Thing\",X:\"/en/ibm\"}"] -- don't care variables -- ,t2 "random ?_R" ["{}","{}"] -- ,t2 "random ?_" ["{}","{}"] -- ask for an answer from a specific provider. ,t2 "context[\"/dns/org/quicquid/db1\",resource{name:?N,publisher:?P}]" ["{N:QQ,P:john}"] -- ask for all answers -- ,t2 "context[\"{address:?adr,answer:?answer}]" ["{...}"] ] testAnswer = testQ "Answer" newAnswer (map (\(Arr [b,_])->b)) [ t2 "random 7777" ["{}"] ,t2 "random ?R" ["{R:?R}","{R:?R}","{R:ABC}","{R:1234}","{R:7777}"] -- We also need to match more then one predicate: ,t2 "and[random ?V,integer ?V]" ["{V:?R}"] ] testQ name op modActual arr = runTestTT $ tests name chk arr where chk (query,expected) = do actual <- withTimeout op query 100000 -- Warn: if the query is slow to answer it will seem to fail. assertEqual (name ++ ": " ++ query) (map (fromJust.parse) expected) (modActual actual) -- An agent that can answer a few predefined queries. -- WARN: this should create a set of agents, each satisfying just one predicate -- as db has itself complexity O(numPredicates) and skews the test results. db1 = db "db1" ["match[\"/en/ibm\",IBM]" ,"type[\"/en/ibm\",\"/en/company\"]" ,"name[\"/en/ibm\",\"Inter Bus Mach\"]" ,"name[\"/en/company\",Company]" ,"type[\"/en/ibm\",\"/en/thing\"]" ,"name[\"/en/thing\",\"A Thing\"]" ,"random ABC " ,"random 1234" ,"integer 1234" ,"hash abcd" ,"hash dcba" ,"integer {min:0,max:3} 1" ,"resource{name:QQ,publisher:john}" ] db2 = db "db2" ["resource{name:QQ,publisher:bip}"] db name = dbAgent name . map (fromJust . parse) -- |One off query with timeout in microseconds. -- TODO: might move it to Broker.lhs queryWithTimeout :: String -> Int -> IO [Term] queryWithTimeout = withTimeout newQuery withTimeout op queryS timeout = do qch <- op queryS threadDelay timeout msgs <- readMessages qch return $ map (\(adr,bind)->bind) msgs -- A performance test. -- TODO: fix, should expect two results per query -- t0 = test liftIO $ tst 1 perf n = do let numReaders = n core <- N.core ch <- newChan mapM_ forkIO $ [broker core,db1] ++ (take numReaders $ repeat (c0 ch)) readN ch $ numReaders*1 debugM "Agents.tst" "I received the right number of answers." where readN ch 0 = return () readN ch n = do bs <- readChan ch readN ch (n-1) c0 och = do qch <- newQuery "and[match[?X,IBM],type[?X,?T],name[?T,?TN],name[?X,?N]]" p qch p qch where p ch = do bs <- readBody ch debugM "Agents.c0" $ "ANSWER:" ++ show bs writeChan och bs --- Convenience function for quick informal testing. m = do -- Choose a core implementation core <- N.core setup [broker core,curlAgent,randomAgent] -- And run the tests q "random (integer ?random)" where q query = queryWithTimeout query 1000 >>= debugM ("Reply to '" ++ query ++ "' -> ") . show . map showCanonical {- root Branch App [Branch (StrTerm,VarTerm) [Leaf (App (Str "random") (Var "R")]]] Answer: random ?r random random App data .. = AppTerm | StrTem ["App","random","1234"] -> ["Arr","App","random","1234"] random 1234 -> ["App","random","1234"] App Str "random" Var (R) -> adr1 [R=abc,q] Str "abc" -> adr2 [q] "resource" Query: random "abc" random ?g ?z 342 ?x ?y -}