diff --git a/dev/user.clj b/dev/user.clj index e2133680..38113d27 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -9,6 +9,9 @@ query mk-session clear-ns-vars!]] + [clara.rules.accumulators :as acc] + [clara.rules.engine :as eng] + [clara.tools.inspect :as inspect] [clojure.core.cache.wrapped :as cache])) (comment @@ -26,24 +29,85 @@ (insert! {:type :thing/result :value ?value})) +(defrule return-a-thang + [?thang <- :thang/that [{:keys [value]}] (= value ?value)] + [?thing <- :thing/that [{:keys [value]}] (= value ?value)] + [?zulu <- (acc/all) :from [:zulu/that]] + [:test (and (some? ?thing) + (some? ?thang) + (some? ?zulu))] + => + (insert! {:type :thang/result + :value ?value})) + (defquery query-a-thing [] [?output <- :thing/result]) -(defrule default-data - (insert-all! - [{:type :thing/foo - :value 1} - {:type :thing/bar - :value 2} - {:type :thing/bar - :value 3}])) +(defquery query-a-thang + [] + [?output <- :thang/result]) (comment - (time - (-> (mk-session 'user :fact-type-fn :type) - (fire-rules) - (query query-a-thing)))) + (do + (def facts1 + [{:type :thing/foo + :value 1} + {:type :thing/bar + :value 2} + {:type :thang/that + :value 3} + {:type :zulu/that + :value 4} + {:type :zulu/that + :value 5} + {:type :unmatched-it + :value 100}]) + + (def facts2 + [{:type :thing/that + :value 3}]) + + (def session + (-> (mk-session 'user :fact-type-fn :type) + (insert-all facts1) + (fire-rules) + (insert-all facts2) + (fire-rules))) + + (def components + (eng/components session)) + + (def get-alphas-fn + (-> components :rulebase :get-alphas-fn))) + + (-> components :memory keys) + + (->> components :memory :alpha-memory + vals + (mapcat vals) + (mapcat identity) + (map :fact)) + (->> components :memory :beta-memory + vals + (mapcat vals) + (mapcat identity) + (mapcat :matches) + (map first)) + (->> components :memory :accum-memory + vals + (mapcat vals) + (mapcat vals) + (mapcat first)) + (-> components :memory :production-memory) + + (get-alphas-fn facts1) + (get-alphas-fn facts2) + + (query session query-a-thing) + (query session query-a-thang) + + (inspect/inspect-facts session)) (def session-cache (cache/lru-cache-factory {})) diff --git a/src/main/clojure/clara/rules/compiler.clj b/src/main/clojure/clara/rules/compiler.clj index 6aa73497..d7ed408b 100644 --- a/src/main/clojure/clara/rules/compiler.clj +++ b/src/main/clojure/clara/rules/compiler.clj @@ -1889,9 +1889,8 @@ (defn create-get-alphas-fn "Returns a function that given a sequence of facts, - returns a map associating alpha nodes with the facts they accept." + returns a list of tuples with each containing alpha nodes and the facts they accept." [fact-type-fn ancestors-fn alpha-roots] - (let [;; If a customized fact-type-fn is provided, ;; we must use a specialized grouping function ;; that handles internal control types that may not @@ -1941,13 +1940,18 @@ (.add fact-list fact) fact-list))))] - (fn [facts] + ^{:fact-type-fn wrapped-fact-type-fn + :ancestors-fn wrapped-ancestors-fn} + (fn do-get-alphas + [facts] (let [roots->facts (java.util.LinkedHashMap.)] (doseq [fact facts + ;;; For each fact, find the matching alpha roots based on its type and ancestors. roots-group (fact-type->roots (wrapped-fact-type-fn fact))] + ;;; Update the map of roots to facts (update-roots->facts! roots->facts roots-group fact)) - (let [return-list (hf/mut-list) + (let [matched-alphas (hf/mut-list) entries (.entrySet roots->facts) entries-it (.iterator entries)] ;; We iterate over the LinkedHashMap manually to avoid potential issues described at http://dev.clojure.org/jira/browse/CLJ-1738 @@ -1955,14 +1959,15 @@ ;; for performance but wrap them in unmodifiableList to make it clear that the caller is not expected to mutate these lists. ;; Since after this function returns the only reference to the fact lists will be through the unmodifiedList we can depend elsewhere ;; on these lists not changing. Since the only expected workflow with these lists is to loop through them, not add or remove elements, - ;; we don't gain much from using a transient (which can be efficiently converted to a persistent data structure) rather than a mutable type. + ;; we don't gain much from using a transient (which can be efficiently converted to a persistent data structure) + ;; rather than a mutable type. (loop [] (when (.hasNext entries-it) (let [^java.util.Map$Entry e (.next entries-it)] - (.add return-list [(-> e ^AlphaRootsWrapper (.getKey) (.wrapped)) - (hf/persistent! (.getValue e))]) + (.add matched-alphas [(-> e ^AlphaRootsWrapper (.getKey) (.wrapped)) + (hf/persistent! (.getValue e))]) (recur)))) - (hf/persistent! return-list)))))) + (hf/persistent! matched-alphas)))))) (defn create-ancestors-fn [{:keys [ancestors-fn diff --git a/src/main/clojure/clara/rules/engine.clj b/src/main/clojure/clara/rules/engine.clj index 251749fd..b07b8c50 100644 --- a/src/main/clojure/clara/rules/engine.clj +++ b/src/main/clojure/clara/rules/engine.clj @@ -34,6 +34,11 @@ ;; An activation for the given production and token. (defrecord Activation [node token]) +(defn ->RootElement + "Creates a root element with a root fact and empty bindings." + [fact] + (->Element fact {})) + ;; Token with no bindings, used as the root of beta nodes. (def empty-token (->Token [] {})) @@ -231,12 +236,12 @@ This is similar to the function of the pending-updates in the fire-rules* loop." [get-alphas-fn memory transport listener] (loop [] - (let [retractions (deref *pending-external-retractions*) - ;; We have already obtained a direct reference to the facts to be - ;; retracted in this iteration of the loop outside the cache. Now reset - ;; the cache. The retractions we execute may cause new retractions to be queued - ;; up, in which case the loop will execute again. - _ (reset! *pending-external-retractions* [])] + (let [retractions (deref *pending-external-retractions*)] + ;; We have already obtained a direct reference to the facts to be + ;; retracted in this iteration of the loop outside the cache. Now reset + ;; the cache. The retractions we execute may cause new retractions to be queued + ;; up, in which case the loop will execute again. + (reset! *pending-external-retractions* []) (doseq [[alpha-roots fact-group] (get-alphas-fn retractions) root alpha-roots] (alpha-retract root fact-group memory transport listener)) @@ -251,7 +256,7 @@ (throw (ex-info "session pending updates missing:" {:session current-session :label label}))) (letfn [(flush-all [current-session flushed-items?] - (let [{:keys [rulebase transient-memory transport insertions get-alphas-fn listener]} current-session + (let [{:keys [transient-memory transport get-alphas-fn listener]} current-session pending-updates (-> current-session :pending-updates uc/get-updates-and-reset!)] (if (empty? pending-updates) @@ -294,7 +299,7 @@ This should only be used for facts explicitly retracted in a RHS. It should not be used for retractions that occur as part of automatic truth maintenance." [facts] - (let [{:keys [rulebase transient-memory transport insertions get-alphas-fn listener]} *current-session* + (let [{:keys [transient-memory transport insertions get-alphas-fn listener]} *current-session* {:keys [node token]} *rule-context*] ;; Update the count so the rule engine will know when we have normalized. (swap! insertions + (count facts)) @@ -311,7 +316,7 @@ "Perform the actual fact insertion, optionally making them unconditional. This should only be called once per rule activation for logical insertions." [facts unconditional] - (let [{:keys [rulebase transient-memory transport insertions get-alphas-fn listener]} *current-session* + (let [{:keys [transient-memory insertions listener]} *current-session* {:keys [node token]} *rule-context*] ;; Update the insertion count. @@ -1969,10 +1974,14 @@ (case op-type - :insertion + :insert (do (l/insert-facts! listener nil nil facts) + (when (seq facts) + (mem/add-elements! memory mem/ROOT_NODE {} + (map ->RootElement facts))) + (binding [*pending-external-retractions* (atom [])] ;; Bind the external retractions cache so that any logical retractions as a result ;; of these insertions can be cached and executed as a batch instead of eagerly realizing @@ -1983,10 +1992,14 @@ (alpha-activate root fact-group memory transport listener)) (external-retract-loop get-alphas-fn memory transport listener))) - :retraction + :retract (do (l/retract-facts! listener nil nil facts) + (when (seq facts) + (mem/remove-elements! memory mem/ROOT_NODE {} + (map ->RootElement facts))) + (binding [*pending-external-retractions* (atom facts)] (external-retract-loop get-alphas-fn memory transport listener))))) @@ -1995,27 +2008,37 @@ (let [insertions (sequence (comp (filter (fn [pending-op] (= (:type pending-op) - :insertion))) + :insert))) (mapcat :facts)) pending-operations) retractions (sequence (comp (filter (fn [pending-op] (= (:type pending-op) - :retraction))) + :retract))) (mapcat :facts)) pending-operations)] ;; Insertions should come before retractions so that if we insert and then retract the same ;; fact that is not already in the session the end result will be that the session won't have that fact. ;; If retractions came first then we'd first retract a fact that isn't in the session, which doesn't do anything, ;; and then later we would insert the fact. + + (when (seq insertions) + (mem/add-elements! memory mem/ROOT_NODE {} + (map ->RootElement insertions))) + (doseq [[alpha-roots fact-group] (get-alphas-fn insertions) root alpha-roots] (alpha-activate root fact-group memory transport listener)) + (when (seq retractions) + (mem/remove-elements! memory mem/ROOT_NODE {} + (map ->RootElement retractions))) + (doseq [[alpha-roots fact-group] (get-alphas-fn retractions) root alpha-roots] (alpha-retract root fact-group memory transport listener)) + (fire-rules-handler session opts)))))) (defn- query* @@ -2046,15 +2069,16 @@ ISession (insert [session facts] - (let [new-pending-operations (conj pending-operations (uc/->PendingUpdate :insertion - ;; Preserve the behavior prior to https://github.com/cerner/clara-rules/issues/268 - ;; , particularly for the Java API, where the caller could freely mutate a - ;; collection of facts after passing it to Clara for the constituent - ;; facts to be inserted or retracted. If the caller passes a persistent - ;; Clojure collection don't do any additional work. - (if (coll? facts) - facts - (into [] facts))))] + (let [new-pending-operations (conj pending-operations + (uc/->PendingUpdate :insert + ;; Preserve the behavior prior to https://github.com/cerner/clara-rules/issues/268 + ;; , particularly for the Java API, where the caller could freely mutate a + ;; collection of facts after passing it to Clara for the constituent + ;; facts to be inserted or retracted. If the caller passes a persistent + ;; Clojure collection don't do any additional work. + (if (coll? facts) + facts + (into [] facts))))] (->LocalSession rulebase memory @@ -2065,11 +2089,12 @@ (retract [session facts] - (let [new-pending-operations (conj pending-operations (uc/->PendingUpdate :retraction - ;; As in insert above defend against facts being a mutable collection. - (if (coll? facts) - facts - (into [] facts))))] + (let [new-pending-operations (conj pending-operations + (uc/->PendingUpdate :retract + ;; As in insert above defend against facts being a mutable collection. + (if (coll? facts) + facts + (into [] facts))))] (->LocalSession rulebase memory @@ -2085,11 +2110,10 @@ (fire-rules [session opts] (let [transient-memory (mem/to-transient memory) transient-listener (l/to-transient listener)] - (fire-rules* - rulebase transient-memory transport - transient-listener get-alphas-fn - pending-operations opts - fire-rules!) + (fire-rules* rulebase transient-memory transport + transient-listener get-alphas-fn + pending-operations opts + fire-rules!) (->LocalSession rulebase (mem/to-persistent! transient-memory) transport @@ -2103,11 +2127,10 @@ (async (let [transient-memory (mem/to-transient memory) transient-listener (l/to-transient listener)] - (LocalSession rulebase (mem/to-persistent! transient-memory) transport diff --git a/src/main/clojure/clara/rules/memory.clj b/src/main/clojure/clara/rules/memory.clj index 9857a947..eab66821 100644 --- a/src/main/clojure/clara/rules/memory.clj +++ b/src/main/clojure/clara/rules/memory.clj @@ -114,6 +114,9 @@ ;; Converts the transient memory to persistent form. (to-persistent! [memory])) +(def ROOT_NODE_ID 0) +(def ROOT_NODE {:id ROOT_NODE_ID}) + (defn- coll-empty? "Returns true if the collection is empty. Does not call seq due to avoid overhead that may cause for non-persistent collection types, e.g. @@ -447,7 +450,6 @@ (declare ->PersistentLocalMemory) ;;; Transient local memory implementation. Typically only persistent memory will be visible externally. - (deftype TransientLocalMemory [rulebase activation-group-sort-fn activation-group-fn diff --git a/src/main/clojure/clara/rules/platform.clj b/src/main/clojure/clara/rules/platform.clj index eb4b11bd..f116cf1d 100644 --- a/src/main/clojure/clara/rules/platform.clj +++ b/src/main/clojure/clara/rules/platform.clj @@ -47,10 +47,26 @@ (hashCode [this] hash-code)) (defn jeq-wrap - "wraps the value with a JavaEqualityWrapper" + "Wraps the value with a JavaEqualityWrapper" ^JavaEqualityWrapper [value] (JavaEqualityWrapper. value (hash value))) +;;; This class wraps objects to ensure identity semantics are used for equality +;;; and hash code. This is used in places where we want to distinguish between +;;; different instances of equal objects, such as when comparing facts. +;;; This class also accepts and stores the hash code, since it almost always will be used +;;; once and generally more than once. +(deftype FactIdentityWrapper [wrapped ^int hash-code] + Object + (equals [this other] + (identical? wrapped (.wrapped ^FactIdentityWrapper other))) + (hashCode [this] hash-code)) + +(defn fact-id-wrap + "Wraps the value with a FactIdentityWrapper" + ^FactIdentityWrapper [value] + (FactIdentityWrapper. value (System/identityHashCode value))) + (defn group-by-seq "Groups the items of the given coll by f to each item. Returns a seq of tuples of the form [f-val xs] where xs are items from the coll and f-val is the result of applying f to any of diff --git a/src/main/clojure/clara/tools/fact_graph.clj b/src/main/clojure/clara/tools/fact_graph.clj index dacc7455..65d58ec5 100644 --- a/src/main/clojure/clara/tools/fact_graph.clj +++ b/src/main/clojure/clara/tools/fact_graph.clj @@ -77,7 +77,7 @@ ;; to be accessible without generating the entire session inspection map. However, we want to reuse the functionality ;; here without the performance penalty of generating all of the inspection data for the session. Therefore, for now ;; we break the privacy of the function here. Once issue 286 is completed we should remove this private Var access. - fact->explanations (@#'i/gen-fact->explanations session) + fact->explanations (i/gen-fact->explanations session) ;; Produce tuples of the form [inserted-fact {:rule rule :explanation clara.tools.inspect.Explanation}] insertion-tuples (into [] diff --git a/src/main/clojure/clara/tools/inspect.clj b/src/main/clojure/clara/tools/inspect.clj index b2831863..8c63e3aa 100644 --- a/src/main/clojure/clara/tools/inspect.clj +++ b/src/main/clojure/clara/tools/inspect.clj @@ -7,6 +7,8 @@ (:require [clara.rules.engine :as eng] [clara.rules.schema :as schema] [clara.rules.memory :as mem] + [clara.rules.platform :as platform] + [clojure.set :as set] [clara.tools.internal.inspect :as i] [clojure.main :refer [demunge]] [schema.core :as s] @@ -17,7 +19,8 @@ HashJoinNode ExpressionJoinNode NegationNode - NegationWithJoinFilterNode])) + NegationWithJoinFilterNode] + [clara.rules.platform FactIdentityWrapper])) (s/defschema ConditionMatch "A structure associating a condition with the facts that matched them. The fields are: @@ -55,13 +58,24 @@ bindings :- {s/Keyword s/Any}]) ; Bound variables ;; Schema of an inspected rule session. -(def InspectionSchema +(def RulesInspectionSchema {:rule-matches {schema/Rule [Explanation]} :query-matches {schema/Query [Explanation]} :condition-matches {schema/Condition [s/Any]} - :insertions {schema/Rule [{:explanation Explanation :fact s/Any}]}}) - -(defn- get-condition-matches + :root-facts [s/Any] + :insertions {schema/Rule [{:explanation Explanation :fact s/Any}]} + :fact->explanations {s/Any [{:rule schema/Rule + :explanation Explanation}]} + (s/optional-key :unfiltered-rule-matches) {schema/Rule [Explanation]}}) + +(def FactsInspectionSchema + {:rules {s/Int schema/Rule} + :facts [{:fact s/Any + (s/optional-key :rule-id) s/Int + (s/optional-key :bindings) {s/Keyword s/Any} + :fact-types [s/Any]}]}) + +(defn get-condition-matches "Returns facts matching each condition" [nodes memory] (let [node-class->node-type (fn [node] @@ -98,7 +112,7 @@ {} join-node-ids))) -(defn- to-explanations +(defn to-explanations "Helper function to convert tokens to explanation records." [session tokens] (let [memory (-> session eng/components :memory) @@ -131,7 +145,7 @@ (.startsWith (name k) "?__gen__")) bindings)))))) -(defn ^:private gen-all-rule-matches +(defn gen-all-rule-matches [session] (when-let [activation-info (i/get-activation-info session)] (let [grouped-info (group-by #(-> % :activation :node) activation-info)] @@ -141,11 +155,11 @@ (to-explanations session (map #(-> % :activation :token) v))])) grouped-info)))) -(defn ^:private gen-fact->explanations +(defn gen-fact->explanations [session] (let [{:keys [memory rulebase]} (eng/components session) - {:keys [productions production-nodes query-nodes]} rulebase + {:keys [production-nodes]} rulebase rule-to-rule-node (into {} (for [rule-node production-nodes] [(:production rule-node) rule-node]))] (apply merge-with into @@ -156,6 +170,50 @@ {insertion [{:rule rule :explanation (first (to-explanations session [token]))}]})))) +(defn get-root-facts + "Returns all root facts in the session that were not derived from rules." + [session] + ;;; If there are any root elements at all then attempt to find them in the memory. + ;;; Old sessions may not have any root elements stored in memory when serialized. + (let [{:keys [rulebase memory pending-operations]} (eng/components session) + {:keys [alpha-memory beta-memory accum-memory]} memory + {:keys [production-nodes]} rulebase + pending-facts (->> (group-by :type pending-operations) + (:insert) + (mapcat :facts) + (map platform/fact-id-wrap)) + ;;; Gather facts that were inserted by rules + rule-facts (for [rule-node production-nodes + match-token (keys (mem/get-insertions-all memory rule-node)) + insertion-group (mem/get-insertions memory rule-node match-token) + fact insertion-group] + (platform/fact-id-wrap fact)) + ;;; Gather facts from alpha memory + alpha-facts (->> (vals alpha-memory) + (mapcat vals) + (mapcat identity) + (map :fact) + (map platform/fact-id-wrap)) + ;;; Gather facts from beta memory + beta-facts (->> (vals beta-memory) + (mapcat vals) + (mapcat identity) + (mapcat :matches) + (map first) + (map platform/fact-id-wrap)) + accum-facts (->> (vals accum-memory) + (mapcat vals) + (mapcat vals) + (mapcat first) + (map platform/fact-id-wrap)) + ;;; Combine all gathered facts and remove duplicates, using their identity wrappers + unique-facts (set (concat pending-facts rule-facts alpha-facts beta-facts accum-facts)) + ;;; Root facts are those that are not derived from rules + root-facts (set/difference unique-facts (set rule-facts))] + ;;; Return the unwrapped root facts + (for [^FactIdentityWrapper wrapper root-facts] + (.wrapped wrapper)))) + (def ^{:doc "Return a new session on which information will be gathered for optional inspection keys. This can significantly increase memory consumption since retracted facts cannot be garbage collected as normally."} @@ -165,7 +223,7 @@ This new session will not retain references to any such information previously gathered."} without-full-logging i/without-activation-listening) -(s/defn inspect +(s/defn inspect :- RulesInspectionSchema " Returns a representation of the given rule session useful to understand the state of the underlying rules. @@ -205,45 +263,46 @@ ... The above segment will return matches for the rule in question." - [session] :- InspectionSchema + [session] (let [{:keys [memory rulebase]} (eng/components session) - {:keys [productions production-nodes query-nodes id-to-node]} rulebase - + {:keys [production-nodes query-nodes id-to-node]} rulebase ;; Map of queries to their nodes in the network. - query-to-nodes (into {} (for [[query-name query-node] query-nodes] - [(:query query-node) query-node])) - + query-to-nodes (->> (for [[query-name query-node] query-nodes] + [(:query query-node) query-node]) + (into {})) + query-matches (->> (for [[query query-node] query-to-nodes] + [query (to-explanations session + (mem/get-tokens-all memory query-node))]) + (into {})) ;; Map of rules to their nodes in the network. - rule-to-nodes (into {} (for [rule-node production-nodes] - [(:production rule-node) rule-node])) - - base-info {:rule-matches (into {} - (for [[rule rule-node] rule-to-nodes] - [rule (to-explanations session - (keys (mem/get-insertions-all memory rule-node)))])) - - :query-matches (into {} - (for [[query query-node] query-to-nodes] - [query (to-explanations session - (mem/get-tokens-all memory query-node))])) - - :condition-matches (get-condition-matches (vals id-to-node) memory) - - :insertions (into {} - (for [[rule rule-node] rule-to-nodes] - [rule - (for [token (keys (mem/get-insertions-all memory rule-node)) - insertion-group (get (mem/get-insertions-all memory rule-node) token) - insertion insertion-group] - {:explanation (first (to-explanations session [token])) :fact insertion})])) - - :fact->explanations (gen-fact->explanations session)}] - + rule-to-nodes (->> (for [rule-node production-nodes] + [(:production rule-node) rule-node]) + (into {})) + rule-matches (->> (for [[rule rule-node] rule-to-nodes] + [rule (to-explanations session + (keys (mem/get-insertions-all memory rule-node)))]) + (into {})) + condition-matches (get-condition-matches (vals id-to-node) memory) + root-facts (get-root-facts session) + insertions (->> (for [[rule rule-node] rule-to-nodes] + [rule + (for [token (keys (mem/get-insertions-all memory rule-node)) + insertion-group (get (mem/get-insertions-all memory rule-node) token) + insertion insertion-group] + {:explanation (first (to-explanations session [token])) :fact insertion})]) + (into {})) + fact-explanations (into {} (gen-fact->explanations session)) + base-info {:rule-matches rule-matches + :query-matches query-matches + :condition-matches condition-matches + :root-facts root-facts + :insertions insertions + :fact->explanations fact-explanations}] (if-let [unfiltered-rule-matches (gen-all-rule-matches session)] (assoc base-info :unfiltered-rule-matches unfiltered-rule-matches) base-info))) -(defn- explain-activation +(defn explain-activation "Prints a human-readable explanation of the facts and conditions that created the Rete token." ([explanation] (explain-activation explanation "")) ([explanation prefix] @@ -352,3 +411,39 @@ (throw (ex-info "Unable to determine node from function" {:name node-fn :simple-name simple-fn-name})))))) + +(s/defn inspect-facts :- FactsInspectionSchema + "Returns a map with all rules and their associated facts in the session. + - :rules - a map of rule ids to their production nodes + - :facts - a sequence of maps with the following keys: + - :fact - the fact inserted + - :rule-id (optional) - the id of the rule that inserted the fact, + absent for root facts + - :bindings (optional) - the bindings used to insert the fact, + absent for root facts + - :fact-types - a sequence of fact types associated with the fact, + including ancestors" + [session] + (let [{:keys [memory rulebase get-alphas-fn]} (eng/components session) + {:keys [fact-type-fn + ancestors-fn]} (meta get-alphas-fn) + {:keys [production-nodes]} rulebase + root-facts (for [fact (get-root-facts session) + :let [fact-type (fact-type-fn fact) + ancestors (ancestors-fn fact-type)]] + {:fact fact + :fact-types (cons fact-type ancestors)}) + rule-nodes (for [{:keys [id production]} production-nodes] + [id production]) + rule-facts (for [{:keys [id] :as rule-node} production-nodes + {:keys [bindings] :as token} (keys (mem/get-insertions-all memory rule-node)) + insertion-group (mem/get-insertions memory rule-node token) + fact insertion-group + :let [fact-type (fact-type-fn fact) + ancestors (ancestors-fn fact-type)]] + {:fact fact + :rule-id id + :bindings bindings + :fact-types (cons fact-type ancestors)})] + {:rules (into {} rule-nodes) + :facts (concat root-facts rule-facts)})) diff --git a/src/test/clojure/clara/tools/test_inspect.clj b/src/test/clojure/clara/tools/test_inspect.clj index e245c7df..b5646b18 100644 --- a/src/test/clojure/clara/tools/test_inspect.clj +++ b/src/test/clojure/clara/tools/test_inspect.clj @@ -1,11 +1,13 @@ (ns clara.tools.test-inspect (:require [clara.tools.testing-utils :as tu] - [clara.tools.inspect :refer [inspect map->Explanation + [clara.tools.inspect :refer [inspect inspect-facts + map->Explanation explain-activations with-full-logging without-full-logging node-fn-name->production-name]] [clara.rules :refer [insert fire-rules insert! insert-unconditional! retract query]] + [clara.rules.engine :as eng] [clara.rules.accumulators :as acc] [schema.test :as st] [clojure.walk :as w] @@ -47,8 +49,19 @@ (insert (->Temperature 10 "MCI")) (insert (->Temperature 90 "MCI")) (fire-rules)) - - rule-dump (inspect session)] + session' (-> (eng/components session) + (update-in [:memory :alpha-memory] dissoc 0) + (eng/assemble)) + session-ro (-> (eng/components session) + (eng/assemble-read-only)) + session-ro' (-> (eng/components session) + (update-in [:memory :alpha-memory] dissoc 0) + (eng/assemble-read-only)) + rule-dump (inspect session) + rule-dump' (inspect session') + rule-dump-ro (inspect session-ro) + rule-dump-ro' (inspect session-ro') + rule-facts (inspect-facts session)] ;; Retrieve the tokens matching the cold query. This test validates ;; the tokens contain the expected matching conditions by retrieving @@ -65,6 +78,20 @@ (get-in rule-dump [:rule-matches hot-rule])) "Rule matches test") + (is (= (sort-by :temperature + [(->Temperature 15 "MCI") + (->Temperature 10 "MCI") + (->Temperature 90 "MCI")]) + (sort-by :temperature + (get rule-dump :root-facts)) + (sort-by :temperature + (get rule-dump' :root-facts)) + (sort-by :temperature + (get rule-dump-ro :root-facts)) + (sort-by :temperature + (get rule-dump-ro' :root-facts))) + "Root Facts test") + (is (= [{:explanation hot-rule-90-explanation :fact (map->Hot {:temperature 90})}] (get-in rule-dump [:insertions hot-rule])) @@ -75,6 +102,48 @@ (frequencies (get-in rule-dump [:condition-matches (first (:lhs cold-rule))]))) "Condition matches test") + (is (= [{:fact (->Temperature 10 "MCI") + :rule-id nil + :bindings nil + :fact-type Temperature + :ancestors true} + {:fact (->Temperature 15 "MCI") + :rule-id nil + :bindings nil + :fact-type Temperature + :ancestors true} + {:fact (->Temperature 90 "MCI") + :rule-id nil + :bindings nil + :fact-type Temperature + :ancestors true} + {:fact (->Cold :too-cold) + :rule-id 3 + :bindings {:?t 10} + :fact-type Cold + :ancestors true} + {:fact (->Cold :too-cold) + :rule-id 3 + :bindings {:?t 15} + :fact-type Cold + :ancestors true} + {:fact (->Hot 90) + :rule-id 5 + :bindings {:?t 90} + :fact-type Hot + :ancestors true}] + (->> (for [{:keys [fact + rule-id + bindings + fact-types]} (:facts rule-facts)] + {:fact fact + :rule-id rule-id + :bindings bindings + :fact-type (first fact-types) + :ancestors (boolean (seq (rest fact-types)))}) + (sort-by (comp (juxt :rule-id (comp :temperature :fact)))))) + "Rule facts test") + ;; Test the :fact->explanations key in the inspected session data. (is (= {(map->Cold {:temperature :too-cold}) [{:explanation cold-rule-10-explanation :rule cold-rule} @@ -422,10 +491,9 @@ (defn original-constraints->constraints [condition-matches] (let [rename-fn (fn [form] - (if - (and - (map? form) - (contains? form :original-constraints)) + (if (and + (map? form) + (contains? form :original-constraints)) (-> form (dissoc :original-constraints) (assoc :constraints (:original-constraints form)))