├── .clj-kondo └── config.edn ├── .github └── workflows │ └── clojure.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── NOTICE ├── README.md ├── RELEASE.md ├── SECURITY.md ├── clj-kondo └── clj-kondo.exports │ └── clara │ └── rules │ ├── config.edn │ └── hooks │ └── clara_rules.clj_kondo ├── package-lock.json ├── package.json ├── project.clj ├── resources └── public │ └── index.html └── src ├── main ├── clojure │ └── clara │ │ ├── macros.clj │ │ ├── rules.cljc │ │ ├── rules │ │ ├── accumulators.cljc │ │ ├── compiler.clj │ │ ├── dsl.clj │ │ ├── durability.clj │ │ ├── durability │ │ │ └── fressian.clj │ │ ├── engine.cljc │ │ ├── java.clj │ │ ├── listener.cljc │ │ ├── memory.cljc │ │ ├── platform.cljc │ │ ├── schema.cljc │ │ ├── test_rules_data.clj │ │ ├── testfacts.cljc │ │ └── update_cache │ │ │ ├── cancelling.clj │ │ │ └── core.cljc │ │ └── tools │ │ ├── fact_graph.cljc │ │ ├── inspect.cljc │ │ ├── internal │ │ └── inspect.cljc │ │ ├── loop_detector.cljc │ │ ├── testing_utils.cljc │ │ └── tracing.cljc └── java │ └── clara │ └── rules │ ├── QueryResult.java │ ├── RuleLoader.java │ ├── WorkingMemory.java │ └── package-info.java └── test ├── clojure └── clara │ ├── durability_rules.clj │ ├── generative │ ├── generators.clj │ ├── test_accum.clj │ └── test_generators.clj │ ├── long_running_tests.clj │ ├── order_ruleset.clj │ ├── other_ruleset.clj │ ├── performance │ └── test_compilation.clj │ ├── sample_ruleset.clj │ ├── sample_ruleset_seq.clj │ ├── test_compiler.clj │ ├── test_durability.clj │ ├── test_fressian.clj │ ├── test_infinite_loops.clj │ ├── test_java.clj │ ├── test_java_facts.clj │ └── test_rules.clj ├── clojurescript └── clara │ ├── test.cljs │ ├── test_complex_negation.cljs │ ├── test_rules.cljs │ └── test_salience.cljs ├── common └── clara │ ├── performance │ └── test_rule_execution.cljc │ ├── rule_defs.cljc │ ├── test_accumulation.cljc │ ├── test_accumulators.cljc │ ├── test_bindings.cljc │ ├── test_clear_ns_productions.cljc │ ├── test_common.cljc │ ├── test_dsl.cljc │ ├── test_exists.cljc │ ├── test_memory.cljc │ ├── test_negation.cljc │ ├── test_node_sharing.cljc │ ├── test_performance_optimizations.cljc │ ├── test_queries.cljc │ ├── test_rhs_retract.cljc │ ├── test_rules_require.cljc │ ├── test_simple_rules.cljc │ ├── test_testing_utils.cljc │ ├── test_truth_maintenance.cljc │ └── tools │ ├── test_fact_graph.cljc │ ├── test_inspect.cljc │ └── test_tracing.cljc ├── html ├── advanced.html └── simple.html ├── java └── clara │ └── test │ └── facts │ └── BeanTestFact.java └── js └── runner.js /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:config-paths ["../clj-kondo/clj-kondo.exports/clara/rules"]} 2 | -------------------------------------------------------------------------------- /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup Java JDK 15 | uses: actions/setup-java@v1.4.4 16 | with: 17 | # The Java version to make available on the path. Takes a whole or semver Java version, or 1.x syntax (e.g. 1.8 => Java 8.x). Early access versions can be specified in the form of e.g. 14-ea, 14.0.0-ea, or 14.0.0-ea.28 18 | java-version: 1.8 19 | - name: Setup Node.js environment 20 | uses: actions/setup-node@v2.5.2 21 | with: 22 | # Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0 23 | node-version: 18.16.0 24 | - name: Install Puppeteer 25 | run: npm install puppeteer 26 | - name: Install Chrome 27 | run: npx puppeteer browsers install chrome 28 | - name: Install dependencies 29 | run: lein deps 30 | - name: Run tests 31 | run: lein test 32 | - name: Run generative tests 33 | run: lein test :generative 34 | - name: Run recent-clj tests 35 | run: lein with-profile dev,recent-clj test 36 | - name: Run clj-kondo linter 37 | run: lein with-profile dev,recent-clj clj-kondo-lint 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | /resources/public/js/* 6 | pom.xml 7 | pom.xml.asc 8 | *.jar 9 | *.class 10 | .lein-deps-sum 11 | .lein-failures 12 | .lein-plugins 13 | .lein-repl-history 14 | figwheel_server.log 15 | *.*~ 16 | .idea/* 17 | *.iml 18 | .clj-kondo 19 | .lsp 20 | node_modules/* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | Help us to make this project better by contributing. Whether it's new features, bug fixes, or simply improving documentation, your contributions are welcome. Please start with logging a [github issue][1] or submit a pull request. 4 | 5 | Before you contribute, please review these guidelines to help ensure a smooth process for everyone. 6 | 7 | Thanks. 8 | 9 | ## Opening issues 10 | 11 | For bugs or enhancement requests, please file a GitHub issue unless it's 12 | security related. When filing a bug remember that the better written the bug is, 13 | the more likely it is to be fixed. If you think you've found a security 14 | vulnerability, do not raise a GitHub issue and follow the instructions in our 15 | [security policy](./SECURITY.md). 16 | 17 | * Please browse our [existing issues][1] before logging new issues. 18 | * Check that the issue has not already been fixed in the `main` branch. 19 | * Open an issue with a descriptive title and a summary. 20 | * Please be as clear and explicit as you can in your description of the problem. 21 | * Please state the version of Clojure and Clara you are using in the description. 22 | * Include any relevant code in the issue summary. 23 | 24 | ## Contributing code 25 | 26 | We welcome your code contributions. Before submitting code via a pull request, 27 | you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and 28 | your commits need to include the following line using the name and e-mail 29 | address you used to sign the OCA: 30 | 31 | ```text 32 | Signed-off-by: Your Name 33 | ``` 34 | 35 | This can be automatically added to pull requests by committing with `--sign-off` 36 | or `-s`, e.g. 37 | 38 | ```text 39 | git commit --signoff 40 | ``` 41 | 42 | Only pull requests from committers that can be verified as having signed the OCA 43 | can be accepted. 44 | 45 | ## Pull request process 46 | 47 | * Read [how to properly contribute to open source projects on Github][2]. 48 | * Fork the project. 49 | * Use a feature branch. 50 | * Write [good commit messages][3]. 51 | * Use the same coding conventions as the rest of the project. 52 | * Commit locally and push to your fork until you are happy with your contribution. 53 | * Make sure to add tests and verify all the tests are passing when merging upstream. 54 | * Add an entry to the [Changelog][4] accordingly. 55 | * Please add your name to the CONTRIBUTORS.md file. Adding your name to the CONTRIBUTORS.md file signifies agreement to all rights and reservations provided by the [License][5]. 56 | * [Squash related commits together][6]. 57 | * Open a [pull request][7]. 58 | * The pull request will be reviewed by the community and merged by the project committers. 59 | 60 | ## Code of conduct 61 | 62 | Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd 63 | like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. 64 | 65 | [1]: https://github.com/cerner/clara-rules/issues 66 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 67 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 68 | [4]: ./CHANGELOG.md 69 | [5]: ./LICENSE 70 | [6]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 71 | [7]: https://help.github.com/articles/using-pull-requests 72 | [OCA]: https://oca.opensource.oracle.com 73 | [COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 74 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Cerner Corporation 2 | 3 | - Ryan Brush [@rbrush] 4 | - Mike Rodriguez [@mrrodriguez] 5 | - William Parker [@WilliamParker] 6 | - Ethan Christian [@EthanEChristian] 7 | - Pushkar Kulkarni [@kulkarnipushkar] 8 | 9 | Community 10 | 11 | - David Goeke [@dgoeke] 12 | - Dave Dixon [@sparkofreason] 13 | - Baptiste Fontaine [@bfontaine] 14 | - Jose Gomez [@k13gomez] 15 | - Imre Kószó [@imrekoszo] 16 | 17 | [@rbrush]: https://github.com/rbrush 18 | [@mrrodriguez]: https://github.com/mrrodriguez 19 | [@WilliamParker]: https://github.com/WilliamParker 20 | [@EthanEChristian]: https://github.com/EthanEChristian 21 | [@kulkarnipushkar]: https://github.com/kulkarnipushkar 22 | [@dgoeke]: https://github.com/dgoeke 23 | [@sparkofreason]: https://github.com/sparkofreason 24 | [@bfontaine]: https://github.com/bfontaine 25 | [@sunilgunisetty]: https://github.com/sunilgunisetty 26 | [@k13gomez]: https://github.com/k13gomez 27 | [@imrekoszo]: https://github.com/imrekoszo 28 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Cerner Innovation, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/cerner/clara-rules/actions/workflows/clojure.yml/badge.svg)](https://github.com/cerner/clara-rules/actions/workflows/clojure.yml) 2 | 3 | # Project name 4 | 5 | clara-rules - it's a forward-chaining rules in Clojure(Script) 6 | 7 | ## _About_ 8 | 9 | Clara is a forward-chaining rules engine written in Clojure(Script) with Java interoperability. It aims to simplify code with a developer-centric approach to expert systems. See [clara-rules.org](http://www.clara-rules.org) for more. 10 | 11 | ## _Usage_ 12 | 13 | Here's a simple example. Complete documentation is at [clara-rules.org](http://www.clara-rules.org/docs/firststeps/). 14 | 15 | ```clj 16 | (ns clara.support-example 17 | (:require [clara.rules :refer :all])) 18 | 19 | (defrecord SupportRequest [client level]) 20 | 21 | (defrecord ClientRepresentative [name client]) 22 | 23 | (defrule is-important 24 | "Find important support requests." 25 | [SupportRequest (= :high level)] 26 | => 27 | (println "High support requested!")) 28 | 29 | (defrule notify-client-rep 30 | "Find the client representative and send a notification of a support request." 31 | [SupportRequest (= ?client client)] 32 | [ClientRepresentative (= ?client client) (= ?name name)] ; Join via the ?client binding. 33 | => 34 | (println "Notify" ?name "that" ?client "has a new support request!")) 35 | 36 | ;; Run the rules! We can just use Clojure's threading macro to wire things up. 37 | (-> (mk-session) 38 | (insert (->ClientRepresentative "Alice" "Acme") 39 | (->SupportRequest "Acme" :high)) 40 | (fire-rules)) 41 | 42 | ;;;; Prints this: 43 | 44 | ;; High support requested! 45 | ;; Notify Alice that Acme has a new support request! 46 | ``` 47 | 48 | ## _Building_ 49 | 50 | Clara is built, tested, and deployed using [Leiningen](http://leiningen.org). 51 | ClojureScript tests are executed via [puppeteer](https://pptr.dev/). 52 | ``` 53 | npm install -g puppeteer 54 | npx puppeteer browsers install chrome 55 | ``` 56 | 57 | ## _Availability_ 58 | 59 | Clara releases are on [Clojars](https://clojars.org/). Simply add the following to your project: 60 | 61 | [![Clojars Project](http://clojars.org/com.cerner/clara-rules/latest-version.svg)](http://clojars.org/com.cerner/clara-rules) 62 | 63 | ## _Communication_ 64 | 65 | Questions can be posted to the [Clara Rules Google Group](https://groups.google.com/forum/?hl=en#!forum/clara-rules) or the [Slack channel](https://clojurians.slack.com/messages/clara/). 66 | 67 | ## Contributing 68 | 69 | This project welcomes contributions from the community. Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md) 70 | 71 | ## Security 72 | 73 | Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process 74 | 75 | ## License 76 | 77 | Copyright (c) 2018, 2025 Oracle and/or its affiliates. 78 | 79 | 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 80 | 81 |     http://www.apache.org/licenses/LICENSE-2.0 82 | 83 | 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. 84 | 85 | 86 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to Release 2 | 3 | This project is hosted on [Clojars][clojars]. You can see it [here][release-site]. 4 | 5 | Releasing the project requires these steps: 6 | 7 | 0. Assert all tests are passing and the project builds : `lein do clean, test` 8 | 1. Make sure CHANGELOG.md is up-to-date for the upcoming release. 9 | 2. Assert you have Github setup with [gpg](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account) 10 | 3. Add gpg key to [sign](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) your commits 11 | * GPG will likely require an additional export to spawn an interactive prompt for signing: 12 | ```export GPG_TTY=$(tty)``` 13 | 4. Create a [Clojars][clojars] Account and [Deploy Token](https://github.com/clojars/clojars-web/wiki/Deploy-Tokens) if you do not already have one. 14 | 5. Create a lein [credentials](https://leiningen.org/deploy.html#gpg) file using the account and token above. 15 | 6. Run `lein release `, where release-type is one of `:patch`,`:minor` and `:major` 16 | 7. Push the new main branch to the repo. 17 | 8. Push the new tag to the repo. 18 | 19 | [clojars]: https://clojars.org 20 | [release-site]: https://clojars.org/com.cerner/clara-rules 21 | 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security vulnerabilities 2 | 3 | Oracle values the independent security research community and believes that 4 | responsible disclosure of security vulnerabilities helps us ensure the security 5 | and privacy of all our users. 6 | 7 | Please do NOT raise a GitHub Issue to report a security vulnerability. If you 8 | believe you have found a security vulnerability, please submit a report to 9 | [secalert_us@oracle.com][1] preferably with a proof of concept. Please review 10 | some additional information on [how to report security vulnerabilities to Oracle][2]. 11 | We encourage people who contact Oracle Security to use email encryption using 12 | [our encryption key][3]. 13 | 14 | We ask that you do not use other channels or contact the project maintainers 15 | directly. 16 | 17 | Non-vulnerability related security issues including ideas for new or improved 18 | security features are welcome on GitHub Issues. 19 | 20 | ## Security updates, alerts and bulletins 21 | 22 | Security updates will be released on a regular cadence. Many of our projects 23 | will typically release security fixes in conjunction with the 24 | Oracle Critical Patch Update program. Additional 25 | information, including past advisories, is available on our [security alerts][4] 26 | page. 27 | 28 | ## Security-related information 29 | 30 | We will provide security related information such as a threat model, considerations 31 | for secure use, or any known security issues in our documentation. Please note 32 | that labs and sample code are intended to demonstrate a concept and may not be 33 | sufficiently hardened for production use. 34 | 35 | [1]: mailto:secalert_us@oracle.com 36 | [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html 37 | [3]: https://www.oracle.com/security-alerts/encryptionkey.html 38 | [4]: https://www.oracle.com/security-alerts/ 39 | -------------------------------------------------------------------------------- /clj-kondo/clj-kondo.exports/clara/rules/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {clara.rules/defsession clojure.core/def 2 | clara.rules.platform/eager-for clojure.core/for} 3 | :hooks {:analyze-call {clara.rules/defquery hooks.clara-rules/analyze-defquery-macro 4 | clara.rules/defrule hooks.clara-rules/analyze-defrule-macro 5 | clara.rules.dsl/parse-query hooks.clara-rules/analyze-parse-query-macro 6 | clara.rules.dsl/parse-rule hooks.clara-rules/analyze-parse-rule-macro 7 | clara.tools.testing-utils/def-rules-test hooks.clara-rules/analyze-def-rules-test-macro}}} 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "puppeteer": "^22.11.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.cerner/clara-rules "0.24.1-SNAPSHOT" 2 | :description "Clara Rules Engine" 3 | :url "https://github.com/cerner/clara-rules" 4 | :license {:name "Apache License Version 2.0" 5 | :url "https://www.apache.org/licenses/LICENSE-2.0"} 6 | :dependencies [[org.clojure/clojure "1.11.2"] 7 | [prismatic/schema "1.1.6"]] 8 | :profiles {:dev {:dependencies [[org.clojure/math.combinatorics "0.1.3"] 9 | [org.clojure/data.fressian "0.2.1"] 10 | [clj-kondo/clj-kondo "2023.04.14"]] 11 | :java-source-paths ["src/test/java"] 12 | :global-vars {*warn-on-reflection* true}} 13 | :provided {:dependencies [[org.clojure/clojurescript "1.11.132"]]} 14 | :recent-clj {:dependencies [^:replace [org.clojure/clojure "1.11.2"] 15 | ^:replace [org.clojure/clojurescript "1.11.132"]]} 16 | :java9 {:jvm-opts ["--add-modules=java.xml.bind"]}} 17 | :plugins [[lein-codox "0.10.3" :exclusions [org.clojure/clojure 18 | org.clojure/clojurescript]] 19 | [lein-javadoc "0.3.0" :exclusions [org.clojure/clojure 20 | org.clojure/clojurescript]] 21 | [lein-cljsbuild "1.1.8" :exclusions [org.clojure/clojure 22 | org.clojure/clojurescript]] 23 | [lein-figwheel "0.5.14" :exclusions [org.clojure/clojure 24 | org.clojure/clojurescript]] 25 | [com.github.clj-kondo/lein-clj-kondo "0.2.4" :exclusions [org.clojure/clojure 26 | org.clojure/clojurescript]]] 27 | :aliases {"clj-kondo-deps" ["clj-kondo" "--copy-configs" "--dependencies" "--parallel" "--lint" "$classpath"] 28 | "clj-kondo-lint" ["do" ["clj-kondo-deps"] ["clj-kondo" "--lint" "src/main:src/test" "--fail-level" "error"]]} 29 | :codox {:namespaces [clara.rules clara.rules.dsl clara.rules.accumulators 30 | clara.rules.listener clara.rules.durability 31 | clara.tools.inspect clara.tools.tracing 32 | clara.tools.fact-graph] 33 | :metadata {:doc/format :markdown}} 34 | :javadoc-opts {:package-names "clara.rules"} 35 | :source-paths ["src/main/clojure"] 36 | :resource-paths ["clj-kondo"] 37 | :test-paths ["src/test/clojure" "src/test/common"] 38 | :java-source-paths ["src/main/java"] 39 | :javac-options ["-target" "1.8" "-source" "1.8"] 40 | :clean-targets ^{:protect false} ["resources/public/js" "target"] 41 | :hooks [leiningen.cljsbuild] 42 | :cljsbuild {:builds [;; Simple mode compilation for tests. 43 | {:id "figwheel" 44 | :source-paths ["src/test/clojurescript" "src/test/common"] 45 | :figwheel true 46 | :compiler {:main "clara.test" 47 | :output-to "resources/public/js/simple.js" 48 | :output-dir "resources/public/js/out" 49 | :asset-path "js/out" 50 | :optimizations :none}} 51 | 52 | {:id "simple" 53 | :source-paths ["src/test/clojurescript" "src/test/common"] 54 | :compiler {:output-to "target/js/simple.js" 55 | :optimizations :whitespace}} 56 | 57 | ;; Advanced mode compilation for tests. 58 | {:id "advanced" 59 | :source-paths ["src/test/clojurescript" "src/test/common"] 60 | :compiler {:output-to "target/js/advanced.js" 61 | :anon-fn-naming-policy :mapped 62 | :optimizations :advanced}}] 63 | 64 | :test-commands {"puppeteer-simple" ["node" 65 | "src/test/js/runner.js" 66 | "src/test/html/simple.html"] 67 | 68 | "puppeteer-advanced" ["node" 69 | "src/test/js/runner.js" 70 | "src/test/html/advanced.html"]}} 71 | 72 | :repl-options {;; The large number of ClojureScript tests is causing long compilation times 73 | ;; to start the REPL. 74 | :timeout 180000} 75 | 76 | ;; Factoring out the duplication of this test selector function causes an error, 77 | ;; perhaps because Leiningen is using this as uneval'ed code. 78 | ;; For now just duplicate the line. 79 | :test-selectors {:default (complement (fn [x] 80 | (let [blacklisted-packages #{"generative" "performance"} 81 | patterns (into [] 82 | (comp 83 | (map #(str "^clara\\." % ".*")) 84 | (interpose "|")) 85 | blacklisted-packages)] 86 | (some->> x :ns ns-name str (re-matches (re-pattern (apply str patterns))))))) 87 | :generative (fn [x] (some->> x :ns ns-name str (re-matches #"^clara\.generative.*"))) 88 | :performance (fn [x] (some->> x :ns ns-name str (re-matches #"^clara\.performance.*")))} 89 | 90 | :scm {:name "git" 91 | :url "https://github.com/cerner/clara-rules"} 92 | :pom-addition [:developers [:developer 93 | [:id "rbrush"] 94 | [:name "Ryan Brush"] 95 | [:url "http://www.clara-rules.org"]]] 96 | :deploy-repositories [["snapshots" {:url "https://oss.sonatype.org/content/repositories/snapshots/" 97 | :creds :gpg}] 98 | ["releases" {:url "https://repo.clojars.org" 99 | :creds :gpg 100 | :sign-releases false}]]) 101 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Figwheel template

11 |

Checkout your developer console.

12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/accumulators.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.accumulators 2 | "A set of common accumulators usable in Clara rules." 3 | (:require [clara.rules.engine :as eng] 4 | [clojure.set :as set] 5 | [schema.core :as s]) 6 | (:refer-clojure :exclude [min max distinct count])) 7 | 8 | (defn accum 9 | "Creates a new accumulator. Users are encouraged to use a pre-defined 10 | accumulator in this namespace if one fits their needs. (See min, max, all, 11 | distinct, and others in this namespace.) This function 12 | exists for cases where a custom accumulator is necessary. 13 | 14 | The following properties are accepted. 15 | 16 | * An initial-value to be used with the reduced operations. 17 | * A reduce-fn that can be used with the Clojure Reducers library to reduce items. 18 | * An optional combine-fn that can be used with the Clojure Reducers library to combine reduced items. 19 | * An optional retract-fn that can remove a retracted fact from a previously reduced computation. 20 | * An optional convert-return-fn that converts the reduced data into something useful to the caller. 21 | Simply uses identity by default. 22 | " 23 | [{:keys [initial-value reduce-fn combine-fn retract-fn convert-return-fn] :as accum-map}] 24 | 25 | ;; Validate expected arguments are present. 26 | (s/validate {(s/optional-key :initial-value) s/Any 27 | (s/optional-key :combine-fn) s/Any 28 | (s/optional-key :convert-return-fn) s/Any 29 | :reduce-fn s/Any 30 | (s/optional-key :retract-fn) s/Any} 31 | accum-map) 32 | 33 | (eng/map->Accumulator 34 | (merge {;; Default conversion does nothing, so use identity. 35 | :convert-return-fn identity} 36 | accum-map))) 37 | 38 | (defn- drop-one-of 39 | "Removes one instance of the given value from the sequence." 40 | [items value] 41 | (let [pred #(not= value %)] 42 | (into (empty items) 43 | cat 44 | [(take-while pred items) 45 | (rest (drop-while pred items))]))) 46 | 47 | (defn reduce-to-accum 48 | "Creates an accumulator using a given reduce function with optional initial value and 49 | conversion to the final result. 50 | 51 | For example, a a simple function that return a Temperature fact with the highest value: 52 | 53 | (acc/reduce-to-accum 54 | (fn [previous value] 55 | (if previous 56 | (if (> (:temperature value) (:temperature previous)) 57 | value 58 | previous) 59 | value))) 60 | 61 | Note that the above example produces the same result as 62 | (clara.rules.accumulators/max :temperature :returns-fact true), 63 | and users should prefer to use built-in accumulators when possible. This funciton exists to easily 64 | convert arbitrary reduce functions to an accumulator. 65 | 66 | Callers may optionally pass in an initial value (which defaults to nil), 67 | a function to transform the value returned by the reduce (which defaults to identity), 68 | and a function to combine two reduced results (which uses the reduce-fn to add new 69 | items to the same reduced value by default)." 70 | 71 | ([reduce-fn] 72 | (reduce-to-accum reduce-fn nil)) 73 | ([reduce-fn initial-value] 74 | (reduce-to-accum reduce-fn initial-value identity)) 75 | ([reduce-fn initial-value convert-return-fn] 76 | (reduce-to-accum reduce-fn initial-value convert-return-fn nil)) 77 | ([reduce-fn initial-value convert-return-fn combine-fn] 78 | (accum (cond-> {:initial-value initial-value 79 | :reduce-fn reduce-fn 80 | :convert-return-fn convert-return-fn} 81 | combine-fn (assoc :combine-fn combine-fn))))) 82 | 83 | (let [grouping-fn (fnil conj [])] 84 | (defn grouping-by 85 | "Return a generic grouping accumulator. Behaves like clojure.core/group-by. 86 | 87 | * `field` - required - The field of a fact to group by. 88 | * `convert-return-fn` - optional - Converts the resulting grouped 89 | data. Defaults to clojure.core/identity." 90 | ([field] 91 | (grouping-by field identity)) 92 | ([field convert-return-fn] 93 | {:pre [(ifn? convert-return-fn)]} 94 | (reduce-to-accum 95 | (fn [m x] 96 | (let [v (field x)] 97 | (update m v grouping-fn x))) 98 | {} 99 | convert-return-fn)))) 100 | 101 | (defn- comparison-based 102 | "Creates a comparison-based result such as min or max" 103 | [field comparator returns-fact] 104 | (let [reduce-fn (fn [previous value] 105 | (if previous 106 | (if (comparator (field previous) (field value)) 107 | previous 108 | value) 109 | value)) 110 | 111 | convert-return-fn (if returns-fact 112 | identity 113 | field)] 114 | (accum 115 | {:reduce-fn reduce-fn 116 | :convert-return-fn convert-return-fn}))) 117 | 118 | (defn min 119 | "Returns an accumulator that returns the minimum value of a given field. 120 | 121 | The caller may provide the following options: 122 | 123 | * :returns-fact Returns the fact rather than the field value if set to true. Defaults to false." 124 | [field & {:keys [returns-fact]}] 125 | (comparison-based field < returns-fact)) 126 | 127 | (defn max 128 | "Returns an accumulator that returns the maximum value of a given field. 129 | 130 | The caller may provide the following options: 131 | 132 | * :returns-fact Returns the fact rather than the field value if set to true. Defaults to false." 133 | [field & {:keys [returns-fact]}] 134 | (comparison-based field > returns-fact)) 135 | 136 | (defn average 137 | "Returns an accumulator that returns the average value of a given field." 138 | [field] 139 | (accum 140 | {:initial-value [0 0] 141 | :reduce-fn (fn [[value count] item] 142 | [(+ value (field item)) (inc count)]) 143 | :retract-fn (fn [[value count] retracted] 144 | [(- value (field retracted)) (dec count)]) 145 | :combine-fn (fn [[value1 count1] [value2 count2]] 146 | [(+ value1 value2) (+ count1 count2)]) 147 | :convert-return-fn (fn [[value count]] 148 | (if (= 0 count) 149 | nil 150 | (/ value count)))})) 151 | 152 | (defn sum 153 | "Returns an accumulator that returns the sum of values of a given field" 154 | [field] 155 | (accum 156 | {:initial-value 0 157 | :reduce-fn (fn [total item] 158 | (+ total (field item))) 159 | :retract-fn (fn [total item] 160 | (- total (field item))) 161 | :combine-fn +})) 162 | 163 | (defn count 164 | "Returns an accumulator that simply counts the number of matching facts" 165 | [] 166 | (accum 167 | {:initial-value 0 168 | :reduce-fn (fn [count value] (inc count)) 169 | :retract-fn (fn [count retracted] (dec count)) 170 | :combine-fn +})) 171 | 172 | (defn exists 173 | "Returns an accumulator that accumulates to true if at least one fact 174 | exists and nil otherwise, the latter causing the accumulator condition to not match." 175 | [] 176 | (assoc (count) :convert-return-fn (fn [v] 177 | ;; This specifically needs to return nil rather than false if the pos? predicate is false so that 178 | ;; the accumulator condition will fail to match; the accumulator will consider 179 | ;; boolean false a valid match. See https://github.com/cerner/clara-rules/issues/182#issuecomment-217142418 180 | ;; and the following comments for the original discussion around suppressing nil accumulator 181 | ;; return values but propagating boolean false. 182 | (when (pos? v) 183 | true)))) 184 | 185 | (defn distinct 186 | "Returns an accumulator producing a distinct set of facts. 187 | If given a field, returns a distinct set of values for that field." 188 | ([] (distinct identity)) 189 | ([field] 190 | (accum 191 | {:initial-value {} 192 | :reduce-fn (fn [freq-map value] (update freq-map (field value) (fnil inc 0))) 193 | :retract-fn (fn [freq-map retracted-item] 194 | (let [item-field (field retracted-item) 195 | current (get freq-map item-field)] 196 | (if (= 1 current) 197 | (dissoc freq-map item-field) 198 | (update freq-map item-field dec)))) 199 | :convert-return-fn (comp set keys)}))) 200 | 201 | (defn all 202 | "Returns an accumulator that preserves all accumulated items. 203 | If given a field, returns all values in that field." 204 | ([] 205 | (accum 206 | {:initial-value [] 207 | :reduce-fn (fn [items value] (conj items value)) 208 | :retract-fn (fn [items retracted] (drop-one-of items retracted))})) 209 | ([field] 210 | (accum 211 | {:initial-value [] 212 | :reduce-fn (fn [items value] (conj items (field value))) 213 | :retract-fn (fn [items retracted] (drop-one-of items (field retracted)))}))) 214 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/java.clj: -------------------------------------------------------------------------------- 1 | (ns clara.rules.java 2 | "This namespace is for internal use and may move in the future. 3 | Java support. Users should use the Java API, or the clara.rules namespace from Clojure." 4 | (:require [clara.rules :as clara] 5 | [clara.rules.engine :as eng] 6 | [clara.rules.compiler :as com] 7 | [clara.rules.memory :as mem]) 8 | (:refer-clojure :exclude [==]) 9 | (:import [clara.rules.engine LocalTransport] 10 | [clara.rules WorkingMemory QueryResult])) 11 | 12 | (deftype JavaQueryResult [result] 13 | QueryResult 14 | (getResult [_ fieldName] 15 | (get result (keyword fieldName))) 16 | Object 17 | (toString [_] 18 | (.toString result))) 19 | 20 | (defn- run-query [session name args] 21 | (let [query-var (or (resolve (symbol name)) 22 | (throw (IllegalArgumentException. 23 | (str "Unable to resolve symbol to query: " name)))) 24 | 25 | ;; Keywordize string keys from Java. 26 | keyword-args (into {} 27 | (for [[k v] args] 28 | [(keyword k) v])) 29 | results (eng/query session (deref query-var) keyword-args)] 30 | (map #(JavaQueryResult. %) results))) 31 | 32 | (deftype JavaWorkingMemory [session] 33 | WorkingMemory 34 | 35 | (insert [this facts] 36 | (JavaWorkingMemory. (apply clara/insert session facts))) 37 | 38 | (retract [this facts] 39 | (JavaWorkingMemory. (apply clara/retract session facts))) 40 | 41 | (fireRules [this] 42 | (JavaWorkingMemory. (clara/fire-rules session))) 43 | 44 | (query [this name args] 45 | (run-query session name args)) 46 | 47 | (query [this name] 48 | (run-query session name {}))) 49 | 50 | (defn mk-java-session [rulesets] 51 | (JavaWorkingMemory. 52 | (com/mk-session (map symbol rulesets)))) 53 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/listener.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.listener 2 | "Event listeners for analyzing the flow through Clara. This is for primarily for use by 3 | tooling, but advanced users may use this to analyze sessions.") 4 | 5 | (defprotocol IPersistentEventListener 6 | (to-transient [listener])) 7 | 8 | ;; TODO: Handle add-accum-reduced 9 | (defprotocol ITransientEventListener 10 | (left-activate! [listener node tokens]) 11 | (left-retract! [listener node tokens]) 12 | (right-activate! [listener node elements]) 13 | (right-retract! [listener node elements]) 14 | (insert-facts! [listener node token facts]) 15 | (alpha-activate! [listener node facts]) 16 | (insert-facts-logical! [listener node token facts]) 17 | (retract-facts! [listener node token facts]) 18 | (alpha-retract! [listener node facts]) 19 | (retract-facts-logical! [listener node token facts]) 20 | (add-accum-reduced! [listener node join-bindings result fact-bindings]) 21 | (remove-accum-reduced! [listener node join-bindings fact-bindings]) 22 | (add-activations! [listener node activations]) 23 | (remove-activations! [listener node activations]) 24 | (fire-activation! [listener activation resulting-operations]) 25 | (fire-rules! [listener node]) 26 | (activation-group-transition! [listener original-group new-group]) 27 | (to-persistent! [listener])) 28 | 29 | ;; A listener that does nothing. 30 | (deftype NullListener [] 31 | ITransientEventListener 32 | (left-activate! [listener node tokens] 33 | listener) 34 | (left-retract! [listener node tokens] 35 | listener) 36 | (right-activate! [listener node elements] 37 | listener) 38 | (right-retract! [listener node elements] 39 | listener) 40 | (insert-facts! [listener node token facts] 41 | listener) 42 | (alpha-activate! [listener node facts] 43 | listener) 44 | (insert-facts-logical! [listener node token facts] 45 | listener) 46 | (retract-facts! [listener node token facts] 47 | listener) 48 | (alpha-retract! [listener node facts] 49 | listener) 50 | (retract-facts-logical! [listener node token facts] 51 | listener) 52 | (add-accum-reduced! [listener node join-bindings result fact-bindings] 53 | listener) 54 | (remove-accum-reduced! [listener node join-bindings fact-bindings] 55 | listener) 56 | (add-activations! [listener node activations] 57 | listener) 58 | (remove-activations! [listener node activations] 59 | listener) 60 | (fire-activation! [listener activation resulting-operations] 61 | listener) 62 | (fire-rules! [listener node] 63 | listener) 64 | (activation-group-transition! [listener original-group new-group] 65 | listener) 66 | (to-persistent! [listener] 67 | listener) 68 | 69 | IPersistentEventListener 70 | (to-transient [listener] 71 | listener)) 72 | 73 | (declare delegating-listener) 74 | 75 | ;; A listener that simply delegates to others 76 | (deftype DelegatingListener [children] 77 | ITransientEventListener 78 | (left-activate! [listener node tokens] 79 | (doseq [child children] 80 | (left-activate! child node tokens))) 81 | 82 | (left-retract! [listener node tokens] 83 | (doseq [child children] 84 | (left-retract! child node tokens))) 85 | 86 | (right-activate! [listener node elements] 87 | (doseq [child children] 88 | (right-activate! child node elements))) 89 | 90 | (right-retract! [listener node elements] 91 | (doseq [child children] 92 | (right-retract! child node elements))) 93 | 94 | (insert-facts! [listener node token facts] 95 | (doseq [child children] 96 | (insert-facts! child node token facts))) 97 | 98 | (alpha-activate! [listener node facts] 99 | (doseq [child children] 100 | (alpha-activate! child node facts))) 101 | 102 | (insert-facts-logical! [listener node token facts] 103 | (doseq [child children] 104 | (insert-facts-logical! child node token facts))) 105 | 106 | (retract-facts! [listener node token facts] 107 | (doseq [child children] 108 | (retract-facts! child node token facts))) 109 | 110 | (alpha-retract! [listener node facts] 111 | (doseq [child children] 112 | (alpha-retract! child node facts))) 113 | 114 | (retract-facts-logical! [listener node token facts] 115 | (doseq [child children] 116 | (retract-facts-logical! child node token facts))) 117 | 118 | (add-accum-reduced! [listener node join-bindings result fact-bindings] 119 | (doseq [child children] 120 | (add-accum-reduced! child node join-bindings result fact-bindings))) 121 | 122 | (remove-accum-reduced! [listener node join-bindings fact-bindings] 123 | (doseq [child children] 124 | (remove-accum-reduced! child node join-bindings fact-bindings))) 125 | 126 | (add-activations! [listener node activations] 127 | (doseq [child children] 128 | (add-activations! child node activations))) 129 | 130 | (remove-activations! [listener node activations] 131 | (doseq [child children] 132 | (remove-activations! child node activations))) 133 | 134 | (fire-activation! [listener activation resulting-operations] 135 | (doseq [child children] 136 | (fire-activation! child activation resulting-operations))) 137 | 138 | (fire-rules! [listener node] 139 | (doseq [child children] 140 | (fire-rules! child node))) 141 | 142 | (activation-group-transition! [listener original-group new-group] 143 | (doseq [child children] 144 | (activation-group-transition! child original-group new-group))) 145 | 146 | (to-persistent! [listener] 147 | (delegating-listener (map to-persistent! children)))) 148 | 149 | (deftype PersistentDelegatingListener [children] 150 | IPersistentEventListener 151 | (to-transient [listener] 152 | (DelegatingListener. (map to-transient children)))) 153 | 154 | (defn delegating-listener 155 | "Returns a listener that delegates to its children." 156 | [children] 157 | (PersistentDelegatingListener. children)) 158 | 159 | (defn null-listener? 160 | "Returns true if the given listener is the null listener, false otherwise." 161 | [listener] 162 | (instance? NullListener listener)) 163 | 164 | (defn get-children 165 | "Returns the children of a delegating listener." 166 | [^PersistentDelegatingListener listener] 167 | (.-children listener)) 168 | 169 | ;; Default listener. 170 | (def default-listener (NullListener.)) 171 | 172 | (defn ^:internal ^:no-doc flatten-listener 173 | [listener] 174 | (if (null-listener? listener) 175 | [] 176 | (get-children listener))) 177 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/platform.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.platform 2 | "This namespace is for internal use and may move in the future. 3 | Platform unified code Clojure/ClojureScript.") 4 | 5 | (defn throw-error 6 | "Throw an error with the given description string." 7 | [^String description] 8 | (throw #?(:clj (IllegalArgumentException. description) :cljs (js/Error. description)))) 9 | 10 | (defn query-param 11 | "Coerces a query param to a parameter keyword such as :?param, if an unsupported type is 12 | supplied then an exception will be thrown" 13 | [p] 14 | (cond 15 | (keyword? p) p 16 | (symbol? p) (keyword p) 17 | :else 18 | (throw-error (str "Query bindings must be specified as a keyword or symbol: " p)))) 19 | 20 | ;; This class wraps Clojure objects to ensure Clojure's equality and hash 21 | ;; semantics are visible to Java code. This allows these Clojure objects 22 | ;; to be safely used in things like Java Sets or Maps. 23 | ;; This class also accepts and stores the hash code, since it almost always 24 | ;; will be used once and generally more than once. 25 | #?(:clj 26 | (deftype JavaEqualityWrapper [wrapped ^int hash-code] 27 | 28 | Object 29 | (equals [this other] 30 | (and (instance? JavaEqualityWrapper other) 31 | (= wrapped (.wrapped ^JavaEqualityWrapper other)))) 32 | 33 | (hashCode [this] 34 | hash-code))) 35 | 36 | #?(:clj 37 | (defn group-by-seq 38 | "Groups the items of the given coll by f to each item. Returns a seq of tuples of the form 39 | [f-val xs] where xs are items from the coll and f-val is the result of applying f to any of 40 | those xs. Each x in xs has the same value (f x). xs will be in the same order as they were 41 | found in coll. 42 | The behavior is similar to calling `(seq (group-by f coll))` However, the returned seq will 43 | always have consistent ordering from process to process. The ordering is insertion order 44 | as new (f x) values are found traversing the given coll collection in its seq order. The 45 | returned order is made consistent to ensure that relevant places within the rules engine that 46 | use this grouping logic have deterministic behavior across different processes." 47 | [f coll] 48 | (let [^java.util.Map m (reduce (fn [^java.util.Map m x] 49 | (let [k (f x) 50 | ;; Use Java's hashcode for performance reasons as 51 | ;; discussed at https://github.com/cerner/clara-rules/issues/393 52 | wrapper (JavaEqualityWrapper. k 53 | (if (nil? k) 54 | (int 0) 55 | (int (.hashCode ^Object k)))) 56 | xs (or (.get m wrapper) 57 | (transient []))] 58 | (.put m wrapper (conj! xs x))) 59 | m) 60 | (java.util.LinkedHashMap.) 61 | coll) 62 | it (.iterator (.entrySet m))] 63 | ;; Explicitly iterate over a Java iterator in order to avoid running into issues as 64 | ;; discussed in http://dev.clojure.org/jira/browse/CLJ-1738 65 | (loop [coll (transient [])] 66 | (if (.hasNext it) 67 | (let [^java.util.Map$Entry e (.next it)] 68 | (recur (conj! coll [(.wrapped ^JavaEqualityWrapper (.getKey e)) (persistent! (.getValue e))]))) 69 | (persistent! coll))))) 70 | :cljs 71 | (def group-by-seq (comp seq clojure.core/group-by))) 72 | 73 | #?(:clj 74 | (defn tuned-group-by 75 | "Equivalent of the built-in group-by, but tuned for when there are many values per key." 76 | [f coll] 77 | (->> coll 78 | (reduce (fn [map value] 79 | (let [k (f value) 80 | items (or (.get ^java.util.HashMap map k) 81 | (transient []))] 82 | (.put ^java.util.HashMap map k (conj! items value))) 83 | map) 84 | (java.util.HashMap.)) 85 | (reduce (fn [map [key value]] 86 | (assoc! map key (persistent! value))) 87 | (transient {})) 88 | (persistent!))) 89 | :cljs 90 | (def tuned-group-by clojure.core/group-by)) 91 | 92 | #?(:clj 93 | (defmacro thread-local-binding 94 | "Wraps given body in a try block, where it sets each given ThreadLocal binding 95 | and removes it in finally block." 96 | [bindings & body] 97 | (when-not (vector? bindings) 98 | (throw (ex-info "Binding needs to be a vector." 99 | {:bindings bindings}))) 100 | (when-not (even? (count bindings)) 101 | (throw (ex-info "Needs an even number of forms in binding vector" 102 | {:bindings bindings}))) 103 | (let [binding-pairs (partition 2 bindings)] 104 | `(try 105 | ~@(for [[tl v] binding-pairs] 106 | `(.set ~tl ~v)) 107 | ~@body 108 | (finally 109 | ~@(for [[tl] binding-pairs] 110 | `(.remove ~tl))))))) 111 | 112 | (defmacro eager-for 113 | "A for wrapped with a doall to force realisation. Usage is the same as regular for." 114 | [& body] 115 | `(doall (for ~@body))) 116 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/schema.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.schema 2 | "Schema definition of Clara data structures using Prismatic's Schema library. This includes structures for rules and queries, as well as the schema 3 | for the underlying Rete network itself. This can be used by tools or other libraries working with rules." 4 | (:require [schema.core :as s])) 5 | 6 | 7 | (s/defn condition-type :- (s/enum :or :not :and :exists :fact :accumulator :test) 8 | "Returns the type of node in a LHS condition expression." 9 | [condition] 10 | (if (map? condition) ; Leaf nodes are maps, per the schema 11 | 12 | (cond 13 | (:type condition) :fact 14 | (:accumulator condition) :accumulator 15 | :else :test) 16 | 17 | ;; Otherwise the node must a sequential that starts with the boolean operator. 18 | (first condition))) 19 | 20 | 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | ;; Rule and query structure schema. 23 | 24 | (def SExpr 25 | (s/pred seq? "s-expression")) 26 | 27 | (def FactCondition 28 | {:type s/Any ;(s/either s/Keyword (s/pred symbol?)) 29 | :constraints [SExpr] 30 | ;; Original constraints preserved for tooling in case a transformation was applied to the condition. 31 | (s/optional-key :original-constraints) [SExpr] 32 | (s/optional-key :fact-binding) s/Keyword 33 | (s/optional-key :args) s/Any 34 | }) 35 | 36 | (def AccumulatorCondition 37 | {:accumulator s/Any 38 | :from FactCondition 39 | (s/optional-key :result-binding) s/Keyword}) 40 | 41 | (def TestCondition 42 | {:constraints [SExpr]}) 43 | 44 | (def LeafCondition 45 | (s/conditional 46 | :type FactCondition 47 | :accumulator AccumulatorCondition 48 | :else TestCondition)) 49 | 50 | (declare Condition) 51 | 52 | (def BooleanCondition 53 | [(s/one (s/enum :or :not :and :exists) "operator") 54 | (s/recursive #'Condition)]) 55 | 56 | (def Condition 57 | (s/conditional 58 | sequential? BooleanCondition 59 | map? LeafCondition)) 60 | 61 | (def Rule 62 | {;; :ns-name is currently used to eval the :rhs form of a rule in the same 63 | ;; context that it was originally defined in. It is optional and only used 64 | ;; when given. It may be used for other purposes in the future. 65 | (s/optional-key :ns-name) s/Symbol 66 | (s/optional-key :name) (s/cond-pre s/Str s/Keyword) 67 | (s/optional-key :doc) s/Str 68 | (s/optional-key :props) {s/Keyword s/Any} 69 | (s/optional-key :env) {s/Keyword s/Any} 70 | :lhs [Condition] 71 | :rhs s/Any}) 72 | 73 | (def Query 74 | {(s/optional-key :name) (s/cond-pre s/Str s/Keyword) 75 | (s/optional-key :doc) s/Str 76 | (s/optional-key :props) {s/Keyword s/Any} 77 | (s/optional-key :env) {s/Keyword s/Any} 78 | :lhs [Condition] 79 | :params #{s/Keyword}}) 80 | 81 | (def Production 82 | (s/conditional 83 | :rhs Rule 84 | :else Query)) 85 | 86 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 87 | ;; Schema for the Rete network itself. 88 | 89 | (def ConditionNode 90 | {:node-type (s/enum :join :negation :test :accumulator) 91 | :condition LeafCondition 92 | 93 | ;; Captured environment in which the condition was defined, like closed variables. 94 | ;; Most rules (such as those defined by defrule) have no surrounding 95 | ;; environment, but user generated rules might. 96 | (s/optional-key :env) {s/Keyword s/Any} 97 | 98 | ;; Variables used to join to other expressions in the network. 99 | (s/optional-key :join-bindings) #{s/Keyword} 100 | 101 | ;; Variable bindings used by expressions in this node. 102 | :used-bindings #{s/Keyword} 103 | 104 | ;; Variable bindings used in the constraints that are not present in the ancestors of this node. 105 | :new-bindings #{s/Keyword} 106 | 107 | ;; An expression used to filter joined data. 108 | (s/optional-key :join-filter-expressions) LeafCondition 109 | 110 | ;; Bindings used to perform non-hash joins in the join filter expression. 111 | ;; this is a subset of :used-bindings. 112 | (s/optional-key :join-filter-join-bindings) #{s/Keyword} 113 | 114 | ;; The expression to create the accumulator. 115 | (s/optional-key :accumulator) s/Any 116 | 117 | ;; The optional fact or accumulator result binding. 118 | (s/optional-key :result-binding) s/Keyword}) 119 | 120 | (def ProductionNode 121 | {:node-type (s/enum :production :query) 122 | 123 | ;; Rule for rule nodes. 124 | (s/optional-key :production) Rule 125 | 126 | ;; Query for query nodes. 127 | (s/optional-key :query) Query 128 | 129 | ;; Bindings used in the rule right-hand side. 130 | (s/optional-key :bindings) #{s/Keyword}}) 131 | 132 | ;; Alpha network schema. 133 | (def AlphaNode 134 | {:id s/Int 135 | :condition FactCondition 136 | ;; Opional environment for the alpha node. 137 | (s/optional-key :env) {s/Keyword s/Any} 138 | ;; IDs of the beta nodes that are the children. 139 | :beta-children [s/Num]}) 140 | 141 | ;; A graph representing the beta side of the rete network. 142 | (def BetaGraph 143 | {;; Edges from parent to child nodes. 144 | :forward-edges {s/Int #{s/Int}} 145 | 146 | ;; Edges from child to parent nodes. 147 | :backward-edges {s/Int #{s/Int}} 148 | 149 | ;; Map of identifier to condition nodes. 150 | :id-to-condition-node {s/Int (s/cond-pre (s/eq :clara.rules.compiler/root-condition) 151 | ConditionNode)} 152 | 153 | ;; Map of identifier to query or rule nodes. 154 | :id-to-production-node {s/Int ProductionNode} 155 | 156 | ;; Map of identifier to new bindings created by the corresponding node. 157 | :id-to-new-bindings {s/Int #{s/Keyword}}}) 158 | 159 | (defn tuple 160 | "Given `items`, a list of schemas, will generate a schema to validate that a vector contains and is in the order provided 161 | by `items`." 162 | [& items] 163 | (s/constrained [s/Any] 164 | (fn [tuple-vals] 165 | (and (= (count tuple-vals) 166 | (count items)) 167 | (every? nil? (map s/check items tuple-vals)))) 168 | "tuple")) 169 | 170 | (def NodeCompilationValue 171 | (s/constrained {s/Keyword s/Any} 172 | (fn [compilation] 173 | (let [expr-keys #{:alpha-expr :action-expr :join-filter-expr :test-expr :accum-expr}] 174 | (some expr-keys (keys compilation)))) 175 | "node-compilation-value")) 176 | 177 | (def NodeCompilationContext 178 | (s/constrained NodeCompilationValue 179 | (fn [compilation] 180 | (let [xor #(and (or %1 %2) 181 | (not (and %1 %2)))] 182 | (and (contains? compilation :compile-ctx) 183 | (contains? (:compile-ctx compilation) :msg) 184 | (xor (contains? (:compile-ctx compilation) :condition) 185 | (contains? (:compile-ctx compilation) :production))))) 186 | "node-compilation-context")) 187 | 188 | ;; A map of [ ] to SExpression, used in compilation of the rulebase. 189 | (def NodeExprLookup 190 | ;; schema should be NodeCompilationContext in standard compilation, 191 | ;; but during serde it might be either as :compile-ctx is only used for compilation failures 192 | ;; and can be disabled post compilation. 193 | {(tuple s/Int s/Keyword) (tuple SExpr (s/conditional :compile-ctx NodeCompilationContext 194 | :else NodeCompilationValue))}) 195 | 196 | ;; An evaluated version of the schema mentioned above. 197 | (def NodeFnLookup 198 | ;; This schema uses a relaxed version of NodeCompilationContext as once the expressions 199 | ;; have been eval'd there is technically no need for compile-ctx to be maintained except for 200 | ;; deserialization. In such events the compile-ctx would only be valuable when the environment 201 | ;; where the Session is being deserialized doesn't match that of the serialization, ie functions 202 | ;; and symbols cannot be resolved on the deserialization side. 203 | {(tuple s/Int s/Keyword) (tuple (s/pred ifn? "ifn?") NodeCompilationValue)}) -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/test_rules_data.clj: -------------------------------------------------------------------------------- 1 | ;;; This namespace exists for testing purposes only, and is temporarily plac.d under src/main/clojure/clara 2 | ;;; due to issues with the CLJS test environment. Move te test/common/clara when this issue is resolved 3 | ;;; and tests can be compiled and run with this file in that location. 4 | ;;; See issue #288 for further info (https://github.com/cerner/clara-rules/issues/388). 5 | 6 | (ns clara.rules.test-rules-data 7 | (:require [clara.rules] 8 | [clara.rules.testfacts] 9 | [clara.rules.compiler :as com])) 10 | 11 | (def the-rules 12 | [{:doc "Rule to determine whether it is indeed cold and windy." 13 | :name "clara.rules.test-rules-data/is-cold-and-windy-data" 14 | :lhs [{:type (if (com/compiling-cljs?) 'clara.rules.testfacts/Temperature 'clara.rules.testfacts.Temperature) 15 | :constraints '[(< temperature 20) 16 | (== ?t temperature)]} 17 | {:type (if (com/compiling-cljs?) 'clara.rules.testfacts/WindSpeed 'clara.rules.testfacts.WindSpeed) 18 | :constraints '[(> windspeed 30) 19 | (== ?w windspeed)]}] 20 | :rhs '(clara.rules/insert! (clara.rules.testfacts/->ColdAndWindy ?t ?w))} 21 | 22 | {:name "clara.rules.test-rules-data/find-cold-and-windy-data" 23 | :lhs [{:fact-binding :?fact 24 | :type (if (com/compiling-cljs?) 'clara.rules.testfacts/ColdAndWindy 'clara.rules.testfacts.ColdAndWindy) 25 | :constraints []}] 26 | :params #{}}]) 27 | 28 | (defn weather-rules 29 | "Return some weather rules" 30 | [] 31 | the-rules) 32 | 33 | (def the-rules-with-keyword-names (mapv #(update % :name keyword) the-rules)) 34 | 35 | (defn weather-rules-with-keyword-names 36 | "Return some weather rules using keyword names" 37 | [] 38 | the-rules-with-keyword-names) -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/testfacts.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.testfacts 2 | "This namespace exists primary for testing purposes, working around the fact that we cannot AOT compile test classes. This should be moved to the tests once a workaround for this is solved.") 3 | 4 | ;; Reflection against records requires them to be compiled AOT, so we temporarily 5 | ;; place them here as leiningen won't AOT compile test resources. 6 | (defrecord Temperature [temperature location]) 7 | (defrecord WindSpeed [windspeed location]) 8 | (defrecord Cold [temperature]) 9 | (defrecord Hot [temperature]) 10 | (defrecord ColdAndWindy [temperature windspeed]) 11 | (defrecord LousyWeather []) 12 | (defrecord TemperatureHistory [temperatures]) 13 | 14 | 15 | ;; Test facts for chained rules. 16 | (defrecord First []) 17 | (defrecord Second []) 18 | (defrecord Third []) 19 | (defrecord Fourth []) 20 | 21 | ;; Record utilizing clj flexible field names. 22 | (defrecord FlexibleFields [it-works? 23 | a->b 24 | x+y 25 | bang!]) 26 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/update_cache/cancelling.clj: -------------------------------------------------------------------------------- 1 | (ns clara.rules.update-cache.cancelling 2 | (:require [clara.rules.update-cache.core :as uc]) 3 | (:import [java.util 4 | List 5 | Map 6 | LinkedList 7 | LinkedHashMap 8 | Collections])) 9 | 10 | ;;; We need a wrapper to use Clojure equality semantics inside 11 | ;;; a Java collection. Furthermore, since we know we will need to do 12 | ;;; a hash operation for each such wrapper created anyway we can ensure 13 | ;;; that if the hashes of two facts are not equal that the equals implementation 14 | ;;; here will quickly return false. 15 | (deftype FactWrapper [fact ^int fact-hash] 16 | 17 | Object 18 | (equals [this other] 19 | 20 | (cond 21 | 22 | ;; There are some cases where the inserted and retracted facts could be identical, particularly 23 | ;; if user code in the RHS has caches, so we go ahead and check for identity as a first-pass check, 24 | ;; but there are probably aren't enough cases where the facts are identical to make doing a full sweep 25 | ;; on identity first worthwhile, particularly since in practice the hash check will make the vast majority 26 | ;; of .equals calls that return false quite fast. 27 | (identical? fact (.fact ^FactWrapper other)) 28 | true 29 | 30 | (not (== fact-hash (.fact_hash ^FactWrapper other))) 31 | false 32 | 33 | :else (= fact (.fact ^FactWrapper other)))) 34 | 35 | (hashCode [this] fact-hash)) 36 | 37 | 38 | ;;; These functions essentially allow us to use a Java map to create a set that stores 39 | ;;; the frequency of its items. Note that when multiple instances of a fact are added 40 | ;;; we keep both instances as distinct objects. We don't strictly speaking need to do this 41 | ;;; but we expect it to perform better. The memory will retain both distinct references 42 | ;;; and future updates are expected to be faster if these references are maintained since 43 | ;;; memory operations look for matches on identity first in tokens before falling back to matching 44 | ;;; on equality. 45 | (defn inc-fact-count! [^Map m fact] 46 | (let [wrapper (FactWrapper. fact (hash fact)) 47 | ^List current-val (.get m wrapper)] 48 | (if current-val 49 | (.add current-val fact) 50 | (.put m wrapper (LinkedList. [fact]))))) 51 | 52 | (defn dec-fact-count! [^Map m fact] 53 | (let [wrapper (FactWrapper. fact (hash fact)) 54 | ;; Note that we specifically use a LinkedList type hint here since we 55 | ;; use methods from multiple interfaces here, namely List and Deque. 56 | ^LinkedList current-val (.get m wrapper)] 57 | (if current-val 58 | (do 59 | (if (= (.size current-val) 1) 60 | (.remove m wrapper) 61 | ;;; Since as noted above, the facts are equal, we don't actually care which one we remove. 62 | ;;; We remove the first here to avoid any work checking equality and since this is a constant-time 63 | ;;; operation on LinkedList. Since the insertions will be newly inserted facts we probably won't 64 | ;;; have many identical retractions, so doing a sweep for identical facts first probably wouldn't 65 | ;;; have enough hits to be worth the cost. 66 | (.removeFirst current-val)) 67 | true) 68 | false))) 69 | 70 | (defn map->vals-concated 71 | [^Map m] 72 | (let [val-list (java.util.LinkedList.) 73 | it (.iterator (.entrySet m))] 74 | (loop [] 75 | (when (.hasNext it) 76 | (do (let [^java.util.Map$Entry e (.next it) 77 | fact (.fact ^FactWrapper (.getKey e)) 78 | ^Iterable facts-in-val (.getValue e) 79 | fact-iter (.iterator facts-in-val)] 80 | (loop [] 81 | (when (.hasNext fact-iter) 82 | (do 83 | (.add val-list (.next fact-iter)) 84 | (recur))))) 85 | (recur)))) 86 | ;; This list will never be exposed to the user; it is simply iterated over 87 | ;; by the engine and then discarded. This being the case there is no 88 | ;; need to return a persistent data structure rather than an unmodifiable one. 89 | (Collections/unmodifiableList val-list))) 90 | 91 | ;;; This is a pending updates cache that allows 92 | ;; retractions and insertions of equal facts 93 | ;;; to cancel each other out. 94 | ;;; More formally, for i insertions and r retractions 95 | ;;; of a fact f, it will: 96 | ;;; - If i = r, no operations will be performed. 97 | ;;; - If i > r, f will be returned for insertion i - r times. 98 | ;;; - If r > i, f will be returned for retraction r - i times. 99 | (deftype CancellingUpdateCache [^Map ^:unsynchronized-mutable insertions 100 | ^Map ^:unsynchronized-mutable retractions] 101 | 102 | uc/UpdateCache 103 | 104 | (add-insertions! [this facts] 105 | (let [fact-iter (.iterator ^Iterable facts)] 106 | (loop [] 107 | (when (.hasNext fact-iter) 108 | (let [fact (.next fact-iter)] 109 | (when-not (dec-fact-count! retractions fact) 110 | (inc-fact-count! insertions fact)) 111 | (recur)))))) 112 | 113 | (add-retractions! [this facts] 114 | (let [fact-iter (.iterator ^Iterable facts)] 115 | (loop [] 116 | (when (.hasNext fact-iter) 117 | (let [fact (.next fact-iter)] 118 | (when-not (dec-fact-count! insertions fact) 119 | (inc-fact-count! retractions fact)) 120 | (recur)))))) 121 | 122 | (get-updates-and-reset! [this] 123 | (let [retractions-update (when (-> retractions .size pos?) 124 | (uc/->PendingUpdate :retract (map->vals-concated retractions))) 125 | insertions-update (when (-> insertions .size pos?) 126 | (uc/->PendingUpdate :insert (map->vals-concated insertions)))] 127 | (set! insertions (LinkedHashMap.)) 128 | (set! retractions (LinkedHashMap.)) 129 | 130 | (cond 131 | 132 | (and insertions-update retractions-update) 133 | ;; This could be ordered to have insertions before retractions if we ever 134 | ;; found that that performs better on average. Either ordering should 135 | ;; be functionally correct. 136 | [[retractions-update] [insertions-update]] 137 | 138 | insertions-update 139 | [[insertions-update]] 140 | 141 | retractions-update 142 | [[retractions-update]])))) 143 | 144 | ;; We use LinkedHashMap so that the ordering of the pending updates will be deterministic. 145 | (defn get-cancelling-update-cache 146 | [] 147 | (CancellingUpdateCache. (LinkedHashMap.) (LinkedHashMap.))) 148 | -------------------------------------------------------------------------------- /src/main/clojure/clara/rules/update_cache/core.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rules.update-cache.core) 2 | 3 | ;; Record indicating pending insertion or removal of a sequence of facts. 4 | (defrecord PendingUpdate [type facts]) 5 | 6 | ;; This is expected to be used while activating rules in a given salience group 7 | ;; to store updates before propagating those updates to the alpha nodes as a group. 8 | (defprotocol UpdateCache 9 | (add-insertions! [this facts]) 10 | (add-retractions! [this facts]) 11 | (get-updates-and-reset! [this])) 12 | 13 | ;; This cache replicates the behavior prior to https://github.com/cerner/clara-rules/issues/249, 14 | ;; just in a stateful object rather than a persistent data structure. 15 | (deftype OrderedUpdateCache [updates] 16 | 17 | UpdateCache 18 | 19 | (add-insertions! [this facts] 20 | (swap! updates into [(->PendingUpdate :insert facts)])) 21 | 22 | (add-retractions! [this facts] 23 | (swap! updates into [(->PendingUpdate :retract facts)])) 24 | 25 | (get-updates-and-reset! [this] 26 | (let [current-updates @updates] 27 | (reset! updates []) 28 | (partition-by :type current-updates)))) 29 | 30 | (defn get-ordered-update-cache 31 | [] 32 | (OrderedUpdateCache. (atom []))) 33 | -------------------------------------------------------------------------------- /src/main/clojure/clara/tools/fact_graph.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.tools.fact-graph 2 | (:require [clara.tools.inspect :as i] 3 | [schema.core :as sc])) 4 | 5 | ;; This node will have either facts or results of accumulations as its parents. 6 | ;; Its children will be facts that the rule inserted. 7 | (sc/defrecord RuleActivationNode [rule-name :- sc/Str 8 | id :- sc/Int]) 9 | 10 | ;; The parents of this node are facts over which an accumulation was run. 11 | ;; It will have a single child, the result of the accumulation. So, for example, 12 | ;; with the condition [?t <- (acc/min :temperature) :from [Temperature]], if we have 13 | ;; (->Temperature 50 "MCI") and (->Temperature 30 "MCI") the child of this node will be 14 | ;; an AccumulationResult with the :result 30 and the parents will be the two Temperature facts. 15 | (sc/defrecord AccumulationNode [id :- sc/Int]) 16 | 17 | ;; As alluded to above, this node represents the result of an accumulation. Its child will be a 18 | ;; RuleActivationNode. Note that there will be an AccumulationResult for each distinct rules firing. 19 | (sc/defrecord AccumulationResultNode [id :- sc/Int 20 | result :- sc/Any]) 21 | 22 | (def ^:private empty-fact-graph {:forward-edges {} 23 | :backward-edges {}}) 24 | 25 | (defn ^:private add-edge [graph from to] 26 | (-> graph 27 | (update-in [:forward-edges from] (fnil conj #{}) to) 28 | (update-in [:backward-edges to] (fnil conj #{}) from))) 29 | 30 | (defn ^:private add-insertion-to-graph 31 | [original-graph id-counter fact-inserted {:keys [rule-name explanation]}] 32 | (let [facts-direct (sequence 33 | (comp (remove :facts-accumulated) 34 | (map :fact)) 35 | (:matches explanation)) 36 | 37 | activation-node (map->RuleActivationNode {:rule-name rule-name 38 | :id (swap! id-counter inc)}) 39 | 40 | accum-matches (filter :facts-accumulated (:matches explanation))] 41 | 42 | (as-> original-graph graph 43 | (if (seq accum-matches) 44 | (reduce (fn [reduce-graph accum-match] 45 | (let [accum-node (->AccumulationNode (swap! id-counter inc)) 46 | accum-result (->AccumulationResultNode (swap! id-counter inc) (:fact accum-match))] 47 | (as-> reduce-graph g 48 | ;; Add edges from individual facts to an AccumulationResultNode. 49 | (reduce (fn [g accum-element] 50 | (add-edge g accum-element accum-node)) 51 | g (:facts-accumulated accum-match)) 52 | (add-edge g accum-node accum-result) 53 | (add-edge g accum-result activation-node)))) 54 | graph 55 | accum-matches) 56 | graph) 57 | ;; Add edges to the rule activation node from the facts that contributed 58 | ;; to the rule firing that were not in accumulator condition. 59 | (reduce (fn [g f] 60 | (add-edge g f activation-node)) 61 | graph 62 | facts-direct) 63 | (add-edge graph activation-node fact-inserted)))) 64 | 65 | (defn session->fact-graph 66 | "Given a session, return a graph structure connecting all facts to the facts 67 | that they caused to be logically inserted. Note that such connections will not 68 | be made for unconditionally inserted facts." 69 | [session] 70 | (let [id-counter (atom 0) 71 | ;; Use a counter, whose value will be added to internal nodes, to the ensure that 72 | ;; these nodes are not equal to each other. This ensures that the number of the internal 73 | ;; nodes accurately reflects the cardinality of each fact in the session. 74 | 75 | ;; This function generates one of the entries in the map returned by clara.tools.inspect/inspect. 76 | ;; The function is private since it would be confusing for only one of the entries in clara.tools.inspect/inspect 77 | ;; to be accessible without generating the entire session inspection map. However, we want to reuse the functionality 78 | ;; here without the performance penalty of generating all of the inspection data for the session. Therefore, for now 79 | ;; we break the privacy of the function here. Once issue 286 is completed we should remove this private Var access. 80 | fact->explanations (@#'i/gen-fact->explanations session) 81 | 82 | ;; Produce tuples of the form [inserted-fact {:rule rule :explanation clara.tools.inspect.Explanation}] 83 | insertion-tuples (into [] 84 | (comp 85 | (map (fn [[fact v]] 86 | (map (fn [{:keys [rule explanation]}] 87 | [fact {:rule-name (:name rule) 88 | :explanation explanation}]) 89 | v))) 90 | cat) 91 | fact->explanations)] 92 | 93 | (reduce (fn [graph tuple] 94 | (apply add-insertion-to-graph graph id-counter tuple)) 95 | empty-fact-graph 96 | insertion-tuples))) 97 | -------------------------------------------------------------------------------- /src/main/clojure/clara/tools/internal/inspect.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.tools.internal.inspect 2 | "Internal implementation details of session inspection. Nothing in this namespace 3 | should be directly referenced by callers outside of the clara-rules project." 4 | (:require [clara.rules.listener :as l] 5 | [clara.rules.engine :as eng])) 6 | 7 | (declare to-persistent-listener) 8 | 9 | (deftype TransientActivationListener [activations] 10 | l/ITransientEventListener 11 | (fire-activation! [listener activation resulting-operations] 12 | (swap! (.-activations listener) conj {:activation activation 13 | :resulting-operations resulting-operations}) 14 | listener) 15 | (to-persistent! [listener] 16 | (to-persistent-listener @(.-activations listener))) 17 | 18 | ;; The methods below don't do anything; they aren't needed for this functionality. 19 | (left-activate! [listener node tokens] 20 | listener) 21 | (left-retract! [listener node tokens] 22 | listener) 23 | (right-activate! [listener node elements] 24 | listener) 25 | (right-retract! [listener node elements] 26 | listener) 27 | (insert-facts! [listener node token facts] 28 | listener) 29 | (alpha-activate! [listener node facts] 30 | listener) 31 | (insert-facts-logical! [listener node token facts] 32 | listener) 33 | (retract-facts! [listener node token facts] 34 | listener) 35 | (alpha-retract! [listener node facts] 36 | listener) 37 | (retract-facts-logical! [listener node token facts] 38 | listener) 39 | (add-accum-reduced! [listener node join-bindings result fact-bindings] 40 | listener) 41 | (remove-accum-reduced! [listener node join-bindings fact-bindings] 42 | listener) 43 | (add-activations! [listener node activations] 44 | listener) 45 | (remove-activations! [listener node activations] 46 | listener) 47 | (activation-group-transition! [listener previous-group new-group] 48 | listener) 49 | (fire-rules! [listener node] 50 | listener)) 51 | 52 | (deftype PersistentActivationListener [activations] 53 | l/IPersistentEventListener 54 | (to-transient [listener] 55 | (TransientActivationListener. (atom activations)))) 56 | 57 | (defn to-persistent-listener 58 | [activations] 59 | (PersistentActivationListener. activations)) 60 | 61 | (defn with-activation-listening 62 | [session] 63 | (if (empty? (eng/find-listeners session (partial instance? PersistentActivationListener))) 64 | (eng/with-listener session (PersistentActivationListener. [])) 65 | session)) 66 | 67 | (defn without-activation-listening 68 | [session] 69 | (eng/remove-listeners session (partial instance? PersistentActivationListener))) 70 | 71 | (defn get-activation-info 72 | [session] 73 | (let [matching-listeners (eng/find-listeners session (partial instance? PersistentActivationListener))] 74 | (condp = (count matching-listeners) 75 | 0 nil 76 | 1 (-> matching-listeners ^PersistentActivationListener (first) .-activations) 77 | (throw (ex-info "Found more than one PersistentActivationListener on session" 78 | {:session session}))))) 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/clojure/clara/tools/loop_detector.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.tools.loop-detector 2 | (:require [clara.rules.listener :as l] 3 | [clara.rules.engine :as eng] 4 | [clara.tools.tracing :as trace])) 5 | 6 | ;; Although we use a single type here note that the cycles-count and the on-limit-delay fields 7 | ;; will be nil during the persistent state of the listener. 8 | (deftype CyclicalRuleListener [cycles-count max-cycles on-limit-fn on-limit-delay] 9 | l/ITransientEventListener 10 | (left-activate! [listener node tokens] 11 | listener) 12 | (left-retract! [listener node tokens] 13 | listener) 14 | (right-activate! [listener node elements] 15 | listener) 16 | (right-retract! [listener node elements] 17 | listener) 18 | (insert-facts! [listener node token facts] 19 | listener) 20 | (alpha-activate! [listener node facts] 21 | listener) 22 | (insert-facts-logical! [listener node token facts] 23 | listener) 24 | (retract-facts! [listener node token facts] 25 | listener) 26 | (alpha-retract! [listener node facts] 27 | listener) 28 | (retract-facts-logical! [listener node token facts] 29 | listener) 30 | (add-accum-reduced! [listener node join-bindings result fact-bindings] 31 | listener) 32 | (remove-accum-reduced! [listener node join-bindings fact-bindings] 33 | listener) 34 | (add-activations! [listener node activations] 35 | listener) 36 | (remove-activations! [listener node activations] 37 | listener) 38 | (fire-activation! [listener activation resulting-operations] 39 | listener) 40 | (fire-rules! [listener node] 41 | listener) 42 | (activation-group-transition! [listener original-group new-group] 43 | (when (>= @cycles-count max-cycles) 44 | @on-limit-delay) 45 | (swap! cycles-count inc)) 46 | (to-persistent! [listener] 47 | (CyclicalRuleListener. nil max-cycles on-limit-fn nil)) 48 | 49 | l/IPersistentEventListener 50 | (to-transient [listener] 51 | ;; To-transient will be called when a call to fire-rules begins, and to-persistent! will be called when it ends. 52 | ;; The resetting of the cycles-count atom prevents cycles from one call of fire-rules from leaking into the count 53 | ;; for another. Similarly the on-limit-fn should be invoked 1 or 0 times per fire-rules call. We only call 54 | ;; it once, rather than each time the limit is breached, since it may not cause the call to terminate but rather log 55 | ;; something etc., in which case we don't want to spam the user's logs. 56 | (CyclicalRuleListener. (atom 0) max-cycles on-limit-fn (delay (on-limit-fn))))) 57 | 58 | (defn throw-exception-on-max-cycles 59 | [] 60 | (let [trace (trace/listener->trace (l/to-persistent! (:listener eng/*current-session*)))] 61 | (throw (ex-info "Reached maximum activation group transitions threshhold; an infinite loop is suspected" 62 | (cond-> {:clara-rules/infinite-loop-suspected true} 63 | trace (assoc :trace trace)))))) 64 | 65 | (defn ->standard-out-warning 66 | [] 67 | (println "Reached maximum activation group transitions threshhold; an infinite loop is suspected")) 68 | 69 | (defn on-limit-fn-lookup 70 | [fn-or-keyword] 71 | (cond 72 | (= fn-or-keyword :throw-exception) throw-exception-on-max-cycles 73 | (= fn-or-keyword :standard-out-warning) ->standard-out-warning 74 | (ifn? fn-or-keyword) fn-or-keyword 75 | :else (throw (ex-info "The :on-error-fn must be a non-nil function value" {:clara-rules/max-cycles-exceeded-fn fn-or-keyword})))) 76 | 77 | 78 | (defn with-loop-detection 79 | "Detect suspected infinite loops in the session. 80 | 81 | Max-cycles is the maximum 82 | number of transitions permitted between different activation groups (salience levels) 83 | plus the number of times all rules are evaluated and their facts inserted, thus leading 84 | to another cycle of rules activations in the same activation group. 85 | 86 | on-limit-fn is a 0-arg function that is invoked exactly once when this limit is exceeded. It can either be 87 | a user-provided function or a keyword that indicates a built-in function to use. Currently supported keywords are: 88 | 89 | :throw-exception - This throws an exception when the limit is reached. If tracing is enabled, the exception will include 90 | the trace. 91 | 92 | :standard-out-warning - This prints a warning to standard out." 93 | 94 | [session max-cycles on-limit-fn] 95 | 96 | (let [on-limit-fn-normalized (on-limit-fn-lookup on-limit-fn)] 97 | (eng/with-listener 98 | session 99 | (CyclicalRuleListener. 100 | nil 101 | max-cycles 102 | on-limit-fn-normalized 103 | nil)))) 104 | -------------------------------------------------------------------------------- /src/main/clojure/clara/tools/tracing.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.tools.tracing 2 | "Support for tracing state changes in a Clara session." 3 | (:require [clara.rules.listener :as l] 4 | [clara.rules.engine :as eng])) 5 | 6 | (declare to-tracing-listener) 7 | 8 | (deftype PersistentTracingListener [trace] 9 | l/IPersistentEventListener 10 | (to-transient [listener] 11 | (to-tracing-listener listener))) 12 | 13 | (declare append-trace) 14 | 15 | (deftype TracingListener [trace] 16 | l/ITransientEventListener 17 | (left-activate! [listener node tokens] 18 | (append-trace listener {:type :left-activate :node-id (:id node) :tokens tokens})) 19 | 20 | (left-retract! [listener node tokens] 21 | (append-trace listener {:type :left-retract :node-id (:id node) :tokens tokens})) 22 | 23 | (right-activate! [listener node elements] 24 | (append-trace listener {:type :right-activate :node-id (:id node) :elements elements})) 25 | 26 | (right-retract! [listener node elements] 27 | (append-trace listener {:type :right-retract :node-id (:id node) :elements elements})) 28 | 29 | (insert-facts! [listener node token facts] 30 | (append-trace listener {:type :add-facts :node node :token token :facts facts})) 31 | 32 | (alpha-activate! [listener node facts] 33 | (append-trace listener {:type :alpha-activate :facts facts})) 34 | 35 | (insert-facts-logical! [listener node token facts] 36 | (append-trace listener {:type :add-facts-logical :node node :token token :facts facts})) 37 | 38 | (retract-facts! [listener node token facts] 39 | (append-trace listener {:type :retract-facts :node node :token token :facts facts})) 40 | 41 | (alpha-retract! [listener node facts] 42 | (append-trace listener {:type :alpha-retract :facts facts})) 43 | 44 | (retract-facts-logical! [listener node token facts] 45 | (append-trace listener {:type :retract-facts-logical :node node :token token :facts facts})) 46 | 47 | (add-accum-reduced! [listener node join-bindings result fact-bindings] 48 | (append-trace listener {:type :accum-reduced 49 | :node-id (:id node) 50 | :join-bindings join-bindings 51 | :result result 52 | :fact-bindings fact-bindings})) 53 | 54 | (remove-accum-reduced! [listener node join-bindings fact-bindings] 55 | (append-trace listener {:type :remove-accum-reduced 56 | :node-id (:id node) 57 | :join-bindings join-bindings 58 | :fact-bindings fact-bindings})) 59 | 60 | (add-activations! [listener node activations] 61 | (append-trace listener {:type :add-activations :node-id (:id node) :tokens (map :token activations)})) 62 | 63 | (remove-activations! [listener node activations] 64 | (append-trace listener {:type :remove-activations :node-id (:id node) :activations activations})) 65 | 66 | (fire-activation! [listener activation resulting-operations] 67 | (append-trace listener {:type :fire-activation :activation activation :resulting-operations resulting-operations})) 68 | 69 | (fire-rules! [listener node] 70 | (append-trace listener {:type :fire-rules :node-id (:id node)})) 71 | 72 | (activation-group-transition! [listener previous-group new-group] 73 | (append-trace listener {:type :activation-group-transition :new-group new-group :previous-group previous-group})) 74 | 75 | (to-persistent! [listener] 76 | (PersistentTracingListener. @trace))) 77 | 78 | (defn- to-tracing-listener [^PersistentTracingListener listener] 79 | (TracingListener. (atom (.-trace listener)))) 80 | 81 | (defn- append-trace 82 | "Appends a trace event and returns a new listener with it." 83 | [^TracingListener listener event] 84 | (reset! (.-trace listener) (conj @(.-trace listener) event))) 85 | 86 | (defn tracing-listener 87 | "Creates a persistent tracing event listener" 88 | [] 89 | (PersistentTracingListener. [])) 90 | 91 | (defn is-tracing? 92 | "Returns true if the given session has tracing enabled, false otherwise." 93 | [session] 94 | (let [{:keys [listeners]} (eng/components session)] 95 | (boolean (some #(instance? PersistentTracingListener %) listeners)))) 96 | 97 | (defn with-tracing 98 | "Returns a new session identical to the given one, but with tracing enabled. 99 | The given session is returned unmodified if tracing is already enabled." 100 | [session] 101 | (if (is-tracing? session) 102 | session 103 | (eng/with-listener session (PersistentTracingListener. [])))) 104 | 105 | (defn without-tracing 106 | "Returns a new session identical to the given one, but with tracing disabled 107 | The given session is returned unmodified if tracing is already disabled." 108 | [session] 109 | (eng/remove-listeners session (partial instance? PersistentTracingListener))) 110 | 111 | (defn get-trace 112 | "Returns the trace from the given session." 113 | [session] 114 | (if-let [listener (->> (eng/components session) 115 | :listeners 116 | (filter #(instance? PersistentTracingListener %)) 117 | (first))] 118 | (.-trace ^PersistentTracingListener listener) 119 | (throw (ex-info "No tracing listener attached to session." {:session session})))) 120 | 121 | (defn listener->trace 122 | [listener] 123 | (let [tracing-listener (cond 124 | (instance? PersistentTracingListener listener) 125 | listener 126 | 127 | (some (partial instance? PersistentTracingListener) (l/flatten-listener listener)) 128 | (first (filter (partial instance? PersistentTracingListener) (l/flatten-listener listener))))] 129 | (when tracing-listener 130 | (.-trace ^PersistentTracingListener tracing-listener)))) 131 | 132 | 133 | (defn ^:private node-id->productions 134 | "Given a session and a node ID return a list of the rule and query names associated 135 | with the node." 136 | [session id] 137 | (let [node (-> session eng/components :rulebase :id-to-node (get id))] 138 | (into [] 139 | (comp 140 | (map second) 141 | cat 142 | (map second)) 143 | (eng/get-conditions-and-rule-names node)))) 144 | 145 | (defn ranked-productions 146 | "Given a session with tracing enabled, return a map of rule and query names 147 | to a numerical index that represents an approximation of the proportional 148 | amount of times Clara performed processing related to this rule. This 149 | is not intended to have a precise meaning, and is intended solely as a means 150 | to provide a rough guide to which rules and queries should be considered 151 | the first suspects when diagnosing performance problems in rules sessions. 152 | It is possible for a relatively small number of interactions to take a long 153 | time if those interactions are particularly costly. It is expected that 154 | the results may change between different versions when Clara's internals change, 155 | for example to optimize the rules network. Nevertheless, it is anticipated 156 | that this will provide useful information for a first pass at rules 157 | performance problem debugging. This should not be used to drive user logic. 158 | 159 | This currently returns a Clojure array map in order to conveniently have the rules 160 | with the most interactions printed first in the string representation of the map." 161 | [session] 162 | (let [node-ids (->> session 163 | get-trace 164 | (map :node-id)) 165 | 166 | production-names (into [] 167 | (comp 168 | (map (partial node-id->productions session)) 169 | cat) 170 | node-ids) 171 | 172 | production-name->interactions (frequencies production-names) 173 | 174 | ranked-tuples (reverse (sort-by second production-name->interactions))] 175 | 176 | (apply array-map (into [] cat ranked-tuples)))) 177 | -------------------------------------------------------------------------------- /src/main/java/clara/rules/QueryResult.java: -------------------------------------------------------------------------------- 1 | package clara.rules; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Result of a Clara query. This is typically returned in 7 | * a list of results from the working memory. 8 | */ 9 | public interface QueryResult { 10 | 11 | /** 12 | * Returns the object matching the field specified in the query. 13 | */ 14 | public Object getResult(String fieldName); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/clara/rules/RuleLoader.java: -------------------------------------------------------------------------------- 1 | package clara.rules; 2 | 3 | import clojure.lang.IFn; 4 | import clojure.lang.RT; 5 | import clojure.lang.Symbol; 6 | import clojure.lang.Var; 7 | 8 | /** 9 | * Clara rule loader. Produces a {@link WorkingMemory WorkingMemory} given one or more Clojure 10 | * namespaces containing rules. 11 | */ 12 | public class RuleLoader { 13 | 14 | /** 15 | * The clojure require function. 16 | */ 17 | static final IFn require = RT.var("clojure.core", "require"); 18 | 19 | /** 20 | * Function to make a new Clara session. 21 | */ 22 | static final IFn makeSession; 23 | 24 | static { 25 | 26 | require.invoke(Symbol.intern("clara.rules.java")); 27 | 28 | makeSession = RT.var("clara.rules.java", "mk-java-session"); 29 | } 30 | 31 | /** 32 | * Returns a new working memory with rules loaded from the given namespaces. 33 | * 34 | * @param namespaces namespaces from which to load rules 35 | * @return an empty working memory with rules from the given namespaces. 36 | */ 37 | public static WorkingMemory loadRules(String... namespaces) { 38 | 39 | // Ensure requested namespaces are loaded. 40 | for (String namespace: namespaces) 41 | require.invoke(Symbol.intern(namespace)); 42 | 43 | // Create a new working memory. 44 | return (WorkingMemory) makeSession.invoke(namespaces); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/clara/rules/WorkingMemory.java: -------------------------------------------------------------------------------- 1 | package clara.rules; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | /** 7 | * An immutable working memory of Clara rules. 8 | */ 9 | public interface WorkingMemory { 10 | 11 | /** 12 | * Returns a new working memory with the given facts inserted 13 | * @param facts facts to insert into the working memory 14 | * @return a new working memory with the facts inserted 15 | */ 16 | public WorkingMemory insert(Iterable facts); 17 | 18 | /** 19 | * Returns a new working memory with the given facs retracted 20 | * @param facts facts to retract from the working memory 21 | * @return a new working memory with the facts retracted 22 | */ 23 | public WorkingMemory retract(Iterable facts); 24 | 25 | /** 26 | * Fires any pending rules in the working memory, and returns a new 27 | * working memory with the rules in a fired state. 28 | * 29 | * @return a new working memory with the rules in a fired state. 30 | */ 31 | public WorkingMemory fireRules(); 32 | 33 | /** 34 | * Runs the query by the given name against the working memory and returns the matching 35 | * results. Query names are structured as "namespace/name" 36 | * 37 | * @param queryName the name of the query to perform, formatted as "namespace/name". 38 | * @param arguments query arguments 39 | * @return a list of query results 40 | */ 41 | public Iterable query(String queryName, Map arguments); 42 | 43 | /** 44 | * Runs the query by the given name against the working memory and returns the matching 45 | * results. Query names are structured as "namespace/name" 46 | * 47 | * @param queryName the name of the query to perform, formatted as "namespace/name". 48 | * @return a list of query results 49 | */ 50 | public Iterable query(String queryName); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/clara/rules/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The Java API for working Clara rules. It contains three simple pieces: 3 | * 4 | *
    5 | *
  • The {@link clara.rules.RuleLoader RuleLoader}, responsible for loading rules into a new working memory
  • 6 | *
  • The {@link clara.rules.WorkingMemory WorkingMemory}, an immutable instance of a rule session.
  • 7 | *
  • The {@link clara.rules.QueryResult QueryResult}, a container of query results.
  • 8 | *
9 | * 10 | *

11 | * Note this API does not have a separate "knowledge base" class like those of other rules engines. Instead, 12 | * the user can simply create and reuse a single, empty WorkingMemory object for multiple rule instances -- optionally 13 | * sticking the initial empty working memory in a static variable. This type of pattern is efficient and possible 14 | * since the WorkingMemory is immutable, creating a new instance that shares internal state when changes occur. 15 | *

16 | * 17 | * See the Clara Examples 18 | * project for an example of this in action. 19 | */ 20 | package clara.rules; 21 | -------------------------------------------------------------------------------- /src/test/clojure/clara/durability_rules.clj: -------------------------------------------------------------------------------- 1 | (ns clara.durability-rules 2 | (:require [clara.rules :refer :all] 3 | [clara.rules.accumulators :as acc] 4 | [clara.rules.testfacts :refer :all]) 5 | (:import [clara.rules.testfacts 6 | Temperature 7 | WindSpeed 8 | Cold 9 | Hot 10 | TemperatureHistory])) 11 | 12 | (defrecord Threshold [v]) 13 | (defrecord TempsUnderThreshold [ts]) 14 | 15 | (defrule find-cold 16 | "Rule with no joins" 17 | [Temperature (= ?t temperature) (< temperature 30)] 18 | => 19 | (insert! (->Cold ?t))) 20 | 21 | (defrule find-hot 22 | "Rule using NegationWithJoinFilterNode" 23 | [Temperature (= ?t temperature)] 24 | [:not [Cold (>= temperature ?t)]] 25 | => 26 | (insert! (->Hot ?t))) 27 | 28 | (defrecord UnpairedWindSpeed [ws]) 29 | 30 | (defrule find-wind-speeds-without-temp 31 | "Rule using NegationNode" 32 | [?w <- WindSpeed 33 | ;; Empty constraint and a constraint containing an empty list to test serializing an EmptyList, 34 | ;; see Issue 352 for more information 35 | () 36 | (not= this ()) 37 | (= ?loc location)] 38 | [:not [Temperature (= ?loc location)]] 39 | => 40 | (insert! (->UnpairedWindSpeed ?w))) 41 | 42 | (defrule find-temps 43 | "Rule using AccumulateNode and TestNode" 44 | [?ts <- (acc/all) :from [Temperature]] 45 | [:test (not-empty ?ts)] 46 | => 47 | (insert! (->TemperatureHistory (mapv :temperature ?ts)))) 48 | 49 | (defrule find-temps-under-threshold 50 | "Rule using AccumulateWithJoinFilterNode" 51 | [Threshold (= ?v v)] 52 | [?ts <- (acc/all) :from [Temperature (< temperature ?v)]] 53 | => 54 | (insert! (->TempsUnderThreshold ?ts))) 55 | 56 | (defquery unpaired-wind-speed [] 57 | [?ws <- UnpairedWindSpeed]) 58 | 59 | (defquery cold-temp [] 60 | [?c <- Cold]) 61 | 62 | (defquery hot-temp [] 63 | [?h <- Hot]) 64 | 65 | (defquery temp-his [] 66 | [?his <- TemperatureHistory]) 67 | 68 | (defquery temps-under-thresh [] 69 | [?tut <- TempsUnderThreshold]) 70 | -------------------------------------------------------------------------------- /src/test/clojure/clara/generative/generators.clj: -------------------------------------------------------------------------------- 1 | (ns clara.generative.generators 2 | (:require [clojure.math.combinatorics :as combo] 3 | [clara.rules :refer :all] 4 | [schema.core :as s])) 5 | 6 | (s/defschema FactSessionOperation {:type (s/enum :insert :retract) 7 | :facts [s/Any]}) 8 | 9 | (s/defschema FireSessionOperation {:type (s/enum :fire)}) 10 | 11 | (s/defschema SessionOperation (s/conditional 12 | #(= (:type %) :fire) FireSessionOperation 13 | :else FactSessionOperation)) 14 | 15 | (defn session-run-ops 16 | "Run the provided sequence of operations on the provide session and return the final session." 17 | [session ops] 18 | (let [final-session (reduce (fn [session op] 19 | (let [new-session (condp = (:type op) 20 | :insert (insert-all session (:facts op)) 21 | :retract (apply retract session (:facts op)) 22 | :fire (fire-rules session))] 23 | new-session)) 24 | session ops)] 25 | final-session)) 26 | 27 | (defn ^:private retract-before-insertion? 28 | "Given a sequence of operations, determine if the number of retractions of any fact exceeds the number 29 | of times has been inserted at any time." 30 | [ops] 31 | (let [alter-fact-count (fn [alter-fn] 32 | (fn [fc facts] 33 | (reduce (fn [updated-fc f] 34 | (update fc f alter-fn)) 35 | fc facts))) 36 | inc-fact-count (alter-fact-count (fnil inc 0)) 37 | dec-fact-count (alter-fact-count (fnil dec 0)) 38 | 39 | any-count-negative? (fn [fc] 40 | (boolean (some neg? (vals fc))))] 41 | 42 | (= ::premature-retract (reduce (fn [fact-count op] 43 | (let [new-count (condp = (:type op) 44 | :insert (inc-fact-count fact-count (:facts op)) 45 | :retract (dec-fact-count fact-count (:facts op)) 46 | fact-count)] 47 | (if (any-count-negative? new-count) 48 | (reduced ::premature-retract) 49 | new-count))) 50 | {} 51 | ops)))) 52 | 53 | (defn ^:private ops->add-insert-retract 54 | "Takes an operations sequence and returns a sequence of sequences of operations 55 | where every possible combination of inserting and retracting each fact inserted 56 | in the parent sequence up to the number of times set by dup-level. Note that one 57 | of the possibilities returned will be the original operations sequence. This is not 58 | special-cased, but a reflection of the fact that every combination of adding insert/retract pairs 59 | from 0 to dup-level is in the possibilities returned. The number of possibilities returned 60 | will explode rapidly for large dup-level values." 61 | [ops dup-level] 62 | (let [ops->extra (fn [ops] 63 | (map (fn [op] 64 | (when (= (:type op) 65 | :insert) 66 | [{:type :insert 67 | :facts (:facts op)} 68 | {:type :retract 69 | :facts (:facts op)}])) 70 | ops)) 71 | extras-with-dups (apply concat (repeat dup-level 72 | (ops->extra ops))) 73 | 74 | extra-subsets (combo/subsets extras-with-dups)] 75 | 76 | (map (fn [extras] 77 | (into ops cat extras)) 78 | extra-subsets))) 79 | 80 | (s/defn ops->permutations :- [[SessionOperation]] 81 | "Given a sequence of operations, return all permutations of the operations for 82 | which no retraction of a fact that has not yet been inserted occurs. By default 83 | permutations where extra insertions and retractions in equal number of inserted facts 84 | are added will be present. The default number of such pairs allowed to be added per insertion 85 | is 1." 86 | [ops :- [SessionOperation] 87 | {:keys [dup-level] :or {dup-level 0}}] 88 | (let [dup-ops-seqs (ops->add-insert-retract ops dup-level) 89 | permutations (mapcat combo/permutations dup-ops-seqs)] 90 | 91 | ;; The permutation creation allows for a retraction to occur before insertion, which 92 | ;; effectively removes the retraction from the seq of operations since retractions of facts 93 | ;; that are not present do not cause alteration of the session state. The idea of these helpers 94 | ;; is to produce orders of operations that should all have the same outcomes. 95 | ;; The following would have an A when done: 96 | ;; retract A, insert A 97 | ;; while this would not: 98 | ;; insert A, retract A 99 | ;; 100 | ;; For now, we can just find all permutations and remove the ones with invalid ordering. 101 | ;; This is inefficient and there may be a more efficient algorithm or implementation. 102 | (remove retract-before-insertion? permutations))) 103 | -------------------------------------------------------------------------------- /src/test/clojure/clara/generative/test_accum.clj: -------------------------------------------------------------------------------- 1 | (ns clara.generative.test-accum 2 | (:require [clara.rules :refer :all] 3 | [clojure.test :refer :all] 4 | [clara.rules.testfacts :refer :all] 5 | [clara.rules.accumulators :as acc] 6 | [clara.rules.dsl :as dsl] 7 | [schema.test] 8 | [clara.generative.generators :as gen]) 9 | (:import [clara.rules.testfacts 10 | Temperature 11 | WindSpeed 12 | Cold 13 | TemperatureHistory 14 | ColdAndWindy])) 15 | 16 | (use-fixtures :once schema.test/validate-schemas) 17 | 18 | (deftest test-simple-all-condition-binding-groups 19 | (let [r (dsl/parse-rule [[?ts <- (acc/all) :from [Temperature (= ?loc location)]]] 20 | ;; The all accumulator can return facts in different orders, so we sort 21 | ;; the temperatures to make asserting on the output easier. 22 | (insert! (->TemperatureHistory [?loc (sort (map :temperature ?ts))]))) 23 | 24 | q (dsl/parse-query [] [[?history <- TemperatureHistory]]) 25 | 26 | empty-session (mk-session [r q] :cache false)] 27 | 28 | (let [operations [{:type :insert 29 | :facts [(->Temperature 11 "MCI")]} 30 | {:type :insert 31 | :facts [(->Temperature 19 "MCI")]} 32 | {:type :insert 33 | :facts [(->Temperature 1 "ORD")]} 34 | {:type :insert 35 | :facts [(->Temperature 22 "LAX")]} 36 | {:type :retract 37 | :facts [(->Temperature 22 "LAX")]} 38 | ;; Note that a :fire operation is added to the end later. If this is at the end, 39 | ;; then it should be functionally the same as if we only had one at the end. 40 | ;; On the other hand, a fire operation in the middle of the operations could potentially 41 | ;; reveal bugs. 42 | {:type :fire}] 43 | 44 | operation-permutations (gen/ops->permutations operations {}) 45 | 46 | expected-output? (fn [session permutation] 47 | (let [actual-temp-hist (-> session 48 | (query q) 49 | frequencies) 50 | expected-temp-hist (frequencies [{:?history (->TemperatureHistory ["MCI" [11 19]])} 51 | {:?history (->TemperatureHistory ["ORD" [1]])}])] 52 | (= actual-temp-hist expected-temp-hist)))] 53 | 54 | (doseq [permutation (map #(concat % [{:type :fire}]) operation-permutations) 55 | :let [session (gen/session-run-ops empty-session permutation)]] 56 | (is (expected-output? session permutation) 57 | (str "Failure for operation permutation: " 58 | \newline 59 | ;; Put into a vector so that the str implementation shows the elements of the collection, 60 | ;; not just LazySeq. 61 | (into [] permutation) 62 | \newline 63 | "Output was: " 64 | \newline 65 | (into [] (query session q)))))) 66 | 67 | (let [operations (mapcat (fn [fact] 68 | [{:type :insert 69 | :facts [fact]} 70 | {:type :retract 71 | :facts [fact]}]) 72 | [(->Temperature 10 "MCI") 73 | (->Temperature 20 "MCI") 74 | (->Temperature 15 "LGA") 75 | (->Temperature 25 "LGA")]) 76 | 77 | operation-permutations (gen/ops->permutations operations {})] 78 | 79 | (doseq [permutation (map #(concat % [{:type :fire}]) operation-permutations) 80 | :let [session (gen/session-run-ops empty-session permutation) 81 | output (query session q)]] 82 | (is (empty? output) 83 | (str "Non-empty result for operation permutation: " 84 | \newline 85 | (into [] permutation) 86 | "Output was: " 87 | (into [] output))))))) 88 | 89 | (deftest test-min-accum-with-binding-groups 90 | (let [coldest-rule (dsl/parse-rule [[?coldest-temp <- (acc/min :temperature :returns-fact true) 91 | :from [ColdAndWindy (= ?w windspeed)]]] 92 | (insert! (->Cold (:temperature ?coldest-temp)))) 93 | cold-query (dsl/parse-query [] [[?c <- Cold]]) 94 | 95 | empty-session (mk-session [coldest-rule cold-query] :cache false) 96 | 97 | operations [{:type :insert 98 | :facts [(->ColdAndWindy 10 20)]} 99 | {:type :insert 100 | :facts [(->ColdAndWindy 5 20)]} 101 | {:type :retract 102 | :facts [(->ColdAndWindy 5 20)]} 103 | {:type :insert 104 | :facts [(->ColdAndWindy 20 20)]} 105 | {:type :insert 106 | :facts [(->ColdAndWindy 0 30)]} 107 | {:type :fire}] 108 | 109 | operation-permutations (gen/ops->permutations operations {})] 110 | 111 | (doseq [permutation (map #(concat % [{:type :fire}]) operation-permutations) 112 | :let [session (gen/session-run-ops empty-session permutation) 113 | output (query session cold-query)]] 114 | (is (= (frequencies output) 115 | {{:?c (->Cold 0)} 1 116 | {:?c (->Cold 10)} 1}) 117 | (str "The minimum cold temperatures per windspeed are not correct for permutation: " 118 | \newline 119 | (into [] permutation) 120 | \newline 121 | "The output was: " 122 | (into [] output)))))) 123 | 124 | (deftest test-min-accum-without-binding-groups 125 | (let [coldest-rule (dsl/parse-rule [[?coldest <- (acc/min :temperature) :from [Cold]]] 126 | (insert! (->Temperature ?coldest "MCI"))) 127 | temp-query (dsl/parse-query [] [[Temperature (= ?t temperature)]]) 128 | 129 | empty-session (mk-session [coldest-rule temp-query] :cache false)] 130 | 131 | (doseq [temp-1 (range 5) 132 | temp-2 (range 5) 133 | temp-3 (range 5) 134 | 135 | :let [operations [{:type :insert 136 | :facts [(->Cold temp-1)]} 137 | {:type :insert 138 | :facts [(->Cold temp-2)]} 139 | {:type :insert 140 | :facts [(->Cold temp-3)]} 141 | {:type :retract 142 | :facts [(->Cold temp-3)]} 143 | {:type :fire}]] 144 | 145 | permutation (map #(concat % [{:type :fire}]) 146 | (gen/ops->permutations operations {})) 147 | 148 | :let [session (gen/session-run-ops empty-session permutation) 149 | output (query session temp-query)]] 150 | 151 | (is (= output 152 | [{:?t (min temp-1 temp-2)}]) 153 | (str "Did not find the correct minimum temperature for permutation: " 154 | \newline 155 | (into [] permutation) 156 | \newline 157 | "Output was: " 158 | \newline 159 | (into [] output)))))) 160 | -------------------------------------------------------------------------------- /src/test/clojure/clara/generative/test_generators.clj: -------------------------------------------------------------------------------- 1 | (ns clara.generative.test-generators 2 | (:require 3 | [clara.generative.generators :refer :all] 4 | [clojure.test :refer :all] 5 | [schema.test])) 6 | 7 | (use-fixtures :once schema.test/validate-schemas) 8 | 9 | ;; Basic sanity test of the insert/retract/fire permutation generation. 10 | (deftest test-basic-permutations 11 | (let [base-ops [{:type :insert 12 | :facts [:a]}] 13 | permuted-ops (ops->permutations base-ops {:dup-level 1})] 14 | (is (= (set permuted-ops) 15 | #{[{:type :insert, :facts [:a]}] 16 | [{:type :insert, :facts [:a]} 17 | {:type :insert, :facts [:a]} 18 | {:type :retract, :facts [:a]}] 19 | [{:type :insert, :facts [:a]} 20 | {:type :retract, :facts [:a]} 21 | {:type :insert, :facts [:a]}]})))) 22 | -------------------------------------------------------------------------------- /src/test/clojure/clara/long_running_tests.clj: -------------------------------------------------------------------------------- 1 | (ns clara.long-running-tests 2 | (:require [clojure.test :refer :all] 3 | [clara.rules.compiler :as com] 4 | [schema.core :as sc])) 5 | 6 | (deftest test-maximum-forms-per-eval 7 | ;; 5472 to ensure that we have enough forms to create a full batch, see clara.rules.compiler/forms-per-eval-default 8 | ;; or Issue 381 for more info 9 | (let [rules (for [_ (range 5472) 10 | :let [fact-type (keyword (gensym))]] 11 | {:ns-name (ns-name *ns*) 12 | :lhs [{:type fact-type 13 | :constraints []}] 14 | :rhs `(println ~(str fact-type))}) 15 | rules-and-opts (conj (vector rules) :forms-per-eval 5472 :cache false) 16 | 17 | e (try 18 | ;; Not validating schema here because it will be very expensive 19 | (sc/without-fn-validation (com/mk-session rules-and-opts)) 20 | (is false "Max batching size should have been exceeded, and exception thrown") 21 | (catch Exception e e))] 22 | 23 | ;; Validating that there were 5472 forms in the eval call. 24 | (is (= 5472 (count (-> e ex-data :compilation-ctxs)))) 25 | (is (re-find #"method size exceeded" (.getMessage e))) 26 | 27 | ;; Validate that the stated 5471 forms per eval will compile 28 | (is (sc/without-fn-validation (com/mk-session (conj (vector rules) :forms-per-eval 5471 :cache false)))))) -------------------------------------------------------------------------------- /src/test/clojure/clara/order_ruleset.clj: -------------------------------------------------------------------------------- 1 | (ns clara.order-ruleset 2 | (:use clara.rules 3 | clara.rules.testfacts) 4 | (:refer-clojure :exclude [==]) 5 | (:import [clara.rules.testfacts 6 | Temperature 7 | WindSpeed 8 | Cold 9 | ColdAndWindy 10 | LousyWeather])) 11 | 12 | (def ^:dynamic *rule-order-atom* nil) 13 | 14 | (def ^{:dynamic true 15 | :production-seq true} 16 | *rule-seq-prior* []) 17 | 18 | (defrule rule-C 19 | [Cold (constantly true)] 20 | => 21 | (swap! *rule-order-atom* conj :C)) 22 | 23 | (defrule rule-D 24 | [Cold (constantly true)] 25 | => 26 | (swap! *rule-order-atom* conj :D)) 27 | 28 | (def ^{:dynamic true 29 | :production-seq true} 30 | *rule-seq-after* []) 31 | -------------------------------------------------------------------------------- /src/test/clojure/clara/other_ruleset.clj: -------------------------------------------------------------------------------- 1 | (ns clara.other-ruleset 2 | (:use clara.rules 3 | clara.rules.testfacts) 4 | (:refer-clojure :exclude [==]) 5 | (:import [clara.rules.testfacts 6 | Temperature 7 | WindSpeed 8 | Cold 9 | ColdAndWindy 10 | LousyWeather])) 11 | 12 | (defrule is-lousy 13 | (ColdAndWindy (= temperature 15)) 14 | => 15 | (insert! (->LousyWeather))) 16 | 17 | ;;; These rules are used for unit testing loading from a namespace. 18 | (defquery subzero-locations 19 | "Query the subzero locations." 20 | [] 21 | (Temperature (< temperature 0) (== ?loc location))) 22 | 23 | (defquery temp-by-location 24 | "Query temperatures by location." 25 | [:?loc] 26 | (Temperature (== ?temp temperature) 27 | (== ?loc location))) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/test/clojure/clara/performance/test_compilation.clj: -------------------------------------------------------------------------------- 1 | (ns clara.performance.test-compilation 2 | (:require [clojure.test :refer :all] 3 | [clara.rules.compiler :as com] 4 | [clara.rules :as r] 5 | [clara.rules.durability :as dura] 6 | [clara.rules.durability.fressian :as fres] 7 | [clara.rules.accumulators :as acc] 8 | [clara.tools.testing-utils :as utils]) 9 | (:import [java.io ByteArrayOutputStream ByteArrayInputStream])) 10 | 11 | (defn filter-fn 12 | [seed-keyword] 13 | (= seed-keyword (keyword (gensym)))) 14 | 15 | (def base-production 16 | {:ns-name (symbol (str *ns*))}) 17 | 18 | (defn generate-filter-productions 19 | [seed-syms] 20 | (into {} 21 | (for [seed-sym seed-syms 22 | :let [next-fact (symbol (str seed-sym "prime")) 23 | production (assoc base-production 24 | :lhs [{:type (keyword seed-sym) 25 | :constraints [`(= ~'this ~'?binding) `(filter-fn ~'this)]}] 26 | :rhs `(r/insert! (with-meta ~(set (repeatedly 10 #(rand-nth (range 100)))) 27 | {:type ~(keyword next-fact) 28 | :val ~'?binding})))]] 29 | [next-fact production]))) 30 | 31 | (defn generate-compose-productions 32 | [seed-syms] 33 | (let [template (fn template 34 | ([l] (template l l)) 35 | ([l r] (let [next-fact (symbol (str l r "prime")) 36 | production (assoc base-production 37 | :lhs [{:type (keyword l) 38 | :constraints [`(= ~'this ~'?binding-l)]} 39 | {:type (keyword r) 40 | :constraints [`(= ~'?binding-l ~'this)]}] 41 | :rhs `(r/insert! (with-meta ~(set (repeatedly 10 #(rand-nth (range 100)))) 42 | {:type ~(keyword next-fact)})))] 43 | [next-fact production])))] 44 | (into {} 45 | (for [combo (partition-all 2 (shuffle seed-syms))] 46 | (apply template combo))))) 47 | 48 | (defn generate-collection-productions 49 | [seed-syms] 50 | (into {} 51 | (for [seed-sym seed-syms 52 | :let [next-fact (symbol (str seed-sym "prime")) 53 | production (assoc base-production 54 | :lhs [{:accumulator `(acc/all) 55 | :from {:type (keyword seed-sym), 56 | :constraints [`(filter-fn ~'this)]} 57 | :result-binding :?binding}] 58 | :rhs `(r/insert! (with-meta ~'?binding 59 | {:type ~(keyword next-fact)})))]] 60 | [next-fact production]))) 61 | 62 | (defn generate-queries 63 | [seed-syms] 64 | (into {} 65 | (for [seed-sym seed-syms 66 | :let [production (assoc base-production 67 | :lhs [{:type (keyword seed-sym) 68 | :constraints [] 69 | :fact-binding :?binding}] 70 | :params #{})]] 71 | [seed-sym production]))) 72 | 73 | (defn generate-rules-and-opts 74 | [num-starter-facts] 75 | (let [starter-seeds (repeatedly num-starter-facts gensym)] 76 | (-> (reduce (fn [[seeds prods] next-fn] 77 | (let [new-productions-and-facts (next-fn seeds)] 78 | [(keys new-productions-and-facts) 79 | (concat prods 80 | (vals new-productions-and-facts))])) 81 | [starter-seeds []] 82 | [generate-filter-productions 83 | generate-compose-productions 84 | generate-collection-productions 85 | generate-queries]) 86 | second 87 | vector 88 | (conj :cache false)))) 89 | 90 | (deftest compilation-performance-test 91 | (let [rules (generate-rules-and-opts 500)] 92 | (testing "Session creation performance" 93 | (utils/run-performance-test 94 | {:description "Generated Session Compilation" 95 | :func #(com/mk-session rules) 96 | :iterations 50 97 | :mean-assertion (partial > 5000)})) 98 | 99 | (let [session (com/mk-session rules) 100 | os (ByteArrayOutputStream.)] 101 | (testing "Session rulebase serialization performance" 102 | (utils/run-performance-test 103 | {:description "Session rulebase serialization" 104 | :func #(dura/serialize-rulebase 105 | session 106 | (fres/create-session-serializer (ByteArrayOutputStream.))) 107 | :iterations 50 108 | :mean-assertion (partial > 1000)})) 109 | 110 | (testing "Session rulebase deserialization performance" 111 | (dura/serialize-rulebase 112 | session 113 | (fres/create-session-serializer os)) 114 | 115 | (let [session-bytes (.toByteArray os)] 116 | (utils/run-performance-test 117 | {:description "Session rulebase deserialization" 118 | :func #(dura/deserialize-rulebase 119 | (fres/create-session-serializer (ByteArrayInputStream. session-bytes))) 120 | :iterations 50 121 | :mean-assertion (partial > 5000)})))))) 122 | -------------------------------------------------------------------------------- /src/test/clojure/clara/sample_ruleset.clj: -------------------------------------------------------------------------------- 1 | (ns clara.sample-ruleset 2 | (:use clara.rules 3 | clara.rules.testfacts 4 | clojure.pprint 5 | clara.rules.dsl) 6 | (:refer-clojure :exclude [==]) 7 | (:import [clara.rules.testfacts 8 | Temperature 9 | WindSpeed 10 | Cold 11 | ColdAndWindy 12 | LousyWeather])) 13 | 14 | 15 | ;;; These rules are used for unit testing loading from a namespace. 16 | (defquery freezing-locations 17 | "Query the freezing locations." 18 | [] 19 | (Temperature (< temperature 32) (== ?loc location))) 20 | 21 | (defrule is-cold-and-windy 22 | "Rule to determine whether it is indeed cold and windy." 23 | 24 | (Temperature (< temperature 20) (== ?t temperature)) 25 | (WindSpeed (> windspeed 30) (== ?w windspeed)) 26 | => 27 | (insert! (->ColdAndWindy ?t ?w))) 28 | 29 | (defquery find-cold-and-windy 30 | [] 31 | (?fact <- ColdAndWindy)) 32 | 33 | (defquery find-lousy-weather 34 | [] 35 | (?fact <- LousyWeather)) 36 | 37 | (comment 38 | freezing-locations 39 | 40 | ;; Example usage: 41 | (-> (mk-session 'clara.sample-ruleset) 42 | (insert (->Temperature 10 "KC") 43 | (->WindSpeed 40 "KC")) 44 | (fire-rules) 45 | (query find-cold-and-windy))) 46 | -------------------------------------------------------------------------------- /src/test/clojure/clara/sample_ruleset_seq.clj: -------------------------------------------------------------------------------- 1 | (ns clara.sample-ruleset-seq 2 | "This namespace contains all productions in clara.sample-ruleset, but instead of containing them 3 | in individual rule and query structures they are contained in a seq that is marked as containing a sequence 4 | of productions. This namespace exists so that tests can validate the functionality of scanning namespaces for 5 | rule sequences as discussed in https://github.com/cerner/clara-rules/issues/134" 6 | (:require [clara.sample-ruleset])) 7 | 8 | (def ^:production-seq all-rules (->> (ns-interns 'clara.sample-ruleset) 9 | vals 10 | (filter #((some-fn :rule :query) (meta %))) 11 | (map deref) 12 | doall)) 13 | -------------------------------------------------------------------------------- /src/test/clojure/clara/test_compiler.clj: -------------------------------------------------------------------------------- 1 | (ns clara.test-compiler 2 | (:require [clojure.test :refer :all] 3 | [clara.tools.testing-utils :as tu] 4 | [clara.rules :as r] 5 | [clara.rules.engine :as eng] 6 | [clojure.string :as str] 7 | [clara.rules.accumulators :as acc] 8 | [clojure.main :as m]) 9 | (:import [clara.rules.engine 10 | AlphaNode 11 | TestNode 12 | AccumulateNode 13 | AccumulateWithJoinFilterNode 14 | ProductionNode 15 | NegationWithJoinFilterNode 16 | ExpressionJoinNode 17 | RootJoinNode] 18 | [clojure.lang ExceptionInfo])) 19 | 20 | ;; See https://github.com/cerner/clara-rules/pull/451 for more info 21 | (tu/def-rules-test test-nodes-have-named-fns 22 | {:rules [;; covers AlphaNode, ExpressionJoinNode and ProductionNode 23 | expression-node-rule [[[::a [{:keys [some-field]}] (= ?some-field some-field)] 24 | [::b [{:keys [another-field]}] (contains? another-field ?some-field)]] 25 | (r/insert! {:fact-type ::c})] 26 | ;; covers AccumulateNode, TestNode 27 | accum-test-rule [[[?cs <- (acc/all) :from [::c]] 28 | [:test (seq ?cs)]] 29 | (r/insert! {:fact-type ::d :vals ?cs})] 30 | ;; covers AccumulateWithJoinFilterNode 31 | accum-join-filter [[[::a [{:keys [some-field]}] (= ?some-field some-field)] 32 | [?ds <- (acc/all) :from [::d [{:keys [another-field]}] (contains? another-field ?some-field)]]] 33 | (r/insert! {:fact-type ::e :vals ?ds})] 34 | ;; covers NegationWithJoinFilter 35 | negation-join-filter [[[::e [{:keys [some-field]}] (= ?some-field some-field)] 36 | [:not [::d [{:keys [another-field]}] (contains? another-field ?some-field)]]] 37 | (r/insert! {:fact-type ::f})]] 38 | :queries [] 39 | :sessions [base-session [expression-node-rule 40 | accum-test-rule 41 | accum-join-filter 42 | negation-join-filter] {:fact-type-fn :fact-type}]} 43 | (let [get-node-fns (fn [node] 44 | (condp instance? node 45 | AlphaNode [(:activation node)] 46 | TestNode [(:test node)] 47 | AccumulateNode [] 48 | AccumulateWithJoinFilterNode [(:join-filter-fn node)] 49 | ProductionNode [(:rhs node)] 50 | NegationWithJoinFilterNode [(:join-filter-fn node)] 51 | ExpressionJoinNode [(:join-filter-fn node)] 52 | RootJoinNode []))] 53 | (doseq [node (-> base-session eng/components :rulebase :id-to-node vals) 54 | node-fn (get-node-fns node)] 55 | (is (seq (re-find (re-pattern (str (get eng/node-type->abbreviated-type (.getSimpleName (class node))) 56 | "-" 57 | (:id node))) 58 | (-> node-fn str m/demunge (str/split #"/") last))) 59 | (str "For node: " node " and node-fn: " node-fn))))) 60 | 61 | ;; See https://github.com/cerner/clara-rules/issues/454 for more info 62 | (deftest test-query-node-requires-bindings-exist 63 | (let [;; (defquery a-query 64 | ;; [:?b] 65 | ;; [?c <- ::a-fact-type]) 66 | query {:lhs [{:type ::a-fact-type 67 | :constraints [] 68 | :args [] 69 | :fact-binding :?c}] 70 | :params #{:?b} 71 | :name "a-query"}] 72 | (tu/assert-ex-data {:expected-bindings #{:?b} 73 | :available-bindings #{:?c} 74 | :query "a-query"} 75 | (r/mk-session [query])))) 76 | -------------------------------------------------------------------------------- /src/test/clojure/clara/test_fressian.clj: -------------------------------------------------------------------------------- 1 | (ns clara.test-fressian 2 | (:require [clara.rules.durability :as d] 3 | [clara.rules.durability.fressian :as df] 4 | [clojure.data.fressian :as fres] 5 | [clara.rules.platform :as pform] 6 | [clojure.test :refer :all]) 7 | (:import [org.fressian 8 | FressianWriter 9 | FressianReader])) 10 | 11 | (defn custom-comparator [x y] 12 | (> y x)) 13 | 14 | (defrecord Tester [x]) 15 | 16 | (defn serde1 [x] 17 | (with-open [os (java.io.ByteArrayOutputStream.) 18 | ^FressianWriter wtr (fres/create-writer os :handlers df/write-handler-lookup)] 19 | ;; Write 20 | (pform/thread-local-binding [d/node-id->node-cache (volatile! {}) 21 | d/clj-struct-holder (java.util.IdentityHashMap.)] 22 | (fres/write-object wtr x)) 23 | ;; Read 24 | (let [data (.toByteArray os)] 25 | (pform/thread-local-binding [d/clj-struct-holder (java.util.ArrayList.)] 26 | (with-open [is (java.io.ByteArrayInputStream. data) 27 | ^FressianReader rdr (fres/create-reader is :handlers df/read-handler-lookup)] 28 | (fres/read-object rdr)))))) 29 | 30 | (defn serde [x] 31 | ;; Tests all serialization cases in a way that SerDe's 2 times to show that the serialization to 32 | ;; deserialization process does not lose important details for the next time serializing it. 33 | (-> x serde1 serde1)) 34 | 35 | (defn test-serde [expected x] 36 | (is (= expected (serde x)))) 37 | 38 | (defn test-serde-with-meta [expected x] 39 | (let [no-meta (serde x) 40 | test-meta {:test :meta} 41 | x-with-meta (vary-meta x merge test-meta) 42 | ;; In case x already has metadata it needs to be added to the expectation 43 | ;; along with the test metadata added in case it has none to test already. 44 | expected-meta (meta x-with-meta) 45 | has-meta (serde x-with-meta)] 46 | 47 | (is (= expected 48 | no-meta 49 | has-meta)) 50 | (is (= expected-meta 51 | (meta has-meta))))) 52 | 53 | (deftest test-handlers 54 | 55 | (testing "class" 56 | (test-serde String String)) 57 | 58 | (testing "set" 59 | (test-serde-with-meta #{:x :y} #{:x :y})) 60 | 61 | (testing "vec" 62 | (test-serde-with-meta [1 2 3] [1 2 3])) 63 | 64 | (testing "list" 65 | (test-serde-with-meta (list "a" "b") (list "a" "b"))) 66 | 67 | (testing "aseq" 68 | (test-serde-with-meta ['a 'b] (seq ['a 'b]))) 69 | 70 | (testing "lazy seq" 71 | (test-serde-with-meta [2 3 4] (map inc [1 2 3]))) 72 | 73 | (testing "map" 74 | (test-serde-with-meta {:x 1 :y 2} {:x 1 :y 2})) 75 | 76 | (testing "map entry" 77 | (let [e (first {:x 1})] 78 | (test-serde [:x 1] e) 79 | (is (instance? clojure.lang.MapEntry (serde e)) 80 | "preserves map entry type to be sure to still work with `key` and `val`"))) 81 | 82 | (testing "sym" 83 | (test-serde-with-meta 't 't)) 84 | 85 | (testing "record" 86 | (test-serde-with-meta (->Tester 10) (->Tester 10))) 87 | 88 | (testing "sorted collections" 89 | (let [ss (sorted-set 1 10) 90 | ss-custom (with-meta (sorted-set-by custom-comparator 1 10) 91 | {:clara.rules.durability/comparator-name `custom-comparator}) 92 | 93 | sm (sorted-map 1 :x 10 :y) 94 | sm-custom (with-meta (sorted-map-by custom-comparator 1 :x 10 :y) 95 | {:clara.rules.durability/comparator-name `custom-comparator})] 96 | 97 | (testing "set" 98 | (test-serde-with-meta ss ss) 99 | (test-serde-with-meta ss-custom ss-custom) 100 | (is (thrown? Exception 101 | (serde (with-meta ss-custom {}))) 102 | "cannot serialized custom sort comparators without name given in metadata")) 103 | 104 | (testing "map" 105 | (test-serde-with-meta sm sm) 106 | (test-serde-with-meta sm-custom sm-custom) 107 | (is (thrown? Exception 108 | (serde (with-meta sm-custom {}))) 109 | "cannot serialized custom sort comparators without name given in metadata"))))) 110 | 111 | (deftest test-handler-identity 112 | (let [v [1 2 3] 113 | l (list 4 5 6) 114 | ls (map inc [1 2 3]) 115 | m {:a 1 :b 2} 116 | s #{:a :b :c} 117 | sym 'a 118 | os (sorted-set "a" "c" "b") 119 | om (sorted-map "a" 1 "c" 3 "b" 2) 120 | r (serde (->Tester [v v l l ls ls m m s s sym sym os os om om]))] 121 | (doseq [[x y] (partition 2 (:x r))] 122 | (testing (str "Serde preserves identity for " (type x)) 123 | (is (identical? x y) 124 | "preserving object references"))))) 125 | 126 | -------------------------------------------------------------------------------- /src/test/clojure/clara/test_java.clj: -------------------------------------------------------------------------------- 1 | (ns clara.test-java 2 | (:use clojure.test 3 | clara.rules.testfacts) 4 | (:require [clara.sample-ruleset :as sample] 5 | [clara.other-ruleset :as other]) 6 | (:import [clara.rules.testfacts Temperature WindSpeed Cold ColdAndWindy LousyWeather First Second Third Fourth] 7 | [clara.rules QueryResult RuleLoader WorkingMemory])) 8 | 9 | (defn- java-namespace-args 10 | "The java API expects an arra of strings containing namespace names, so create that." 11 | [] 12 | (doto (make-array String 2) 13 | (aset 0 "clara.sample-ruleset") 14 | (aset 1 "clara.other-ruleset"))) 15 | 16 | (deftest simple-rule 17 | 18 | (let [;; Simulate use of a typical Javaland object, the array list. 19 | ;; Normally we'd just use the Clojure shorthand, but this is testing Java interop specifically 20 | facts (doto (java.util.ArrayList.) 21 | (.add (->Temperature 15 "MCI")) 22 | (.add (->Temperature 10 "BOS")) 23 | (.add (->Temperature 50 "SFO")) 24 | (.add (->Temperature -10 "CHI"))) 25 | 26 | ;; Testing Java interop, so session is a clara.rules.WorkingMemory object. 27 | session (-> (RuleLoader/loadRules (java-namespace-args)) 28 | (.insert facts) 29 | (.fireRules)) 30 | 31 | subzero-locs (.query session "clara.other-ruleset/subzero-locations" {}) 32 | freezing-locs (.query session "clara.sample-ruleset/freezing-locations" {})] 33 | 34 | (is (= #{"CHI"} 35 | (set (map #(.getResult % "?loc") subzero-locs)))) 36 | 37 | (is (= #{"CHI" "BOS" "MCI"} 38 | (set (map #(.getResult % "?loc") freezing-locs)))))) 39 | 40 | (deftest query-with-args 41 | (let [session 42 | (-> (RuleLoader/loadRules (java-namespace-args)) 43 | (.insert [(->Temperature 15 "MCI") 44 | (->Temperature 10 "BOS") 45 | (->Temperature 50 "SFO") 46 | (->Temperature -10 "CHI")]) 47 | (.fireRules)) 48 | 49 | ;; Simulate invocation from Java by creating a hashmap of arguments. 50 | java-args (doto (java.util.HashMap.) 51 | (.put "?loc" "CHI")) 52 | 53 | chicago-temp (.query session "clara.other-ruleset/temp-by-location" java-args)] 54 | 55 | (is (= #{-10} 56 | (set (map #(.getResult % "?temp") chicago-temp)))))) 57 | -------------------------------------------------------------------------------- /src/test/clojure/clara/test_java_facts.clj: -------------------------------------------------------------------------------- 1 | (ns clara.test-java-facts 2 | (:require [clara.tools.testing-utils :as tu] 3 | [clara.rules :as rules] 4 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 5 | [clara.rules.compiler :as com]) 6 | (:import [clara.test.facts 7 | BeanTestFact])) 8 | 9 | ;; A test to demonstrate that a Pojo with indexed property accessors can be used as an alpha root in a session, 10 | ;; see https://github.com/cerner/clara-rules/issues/446 11 | (tu/def-rules-test test-basic-rule 12 | {:rules [kansas-rule [[[BeanTestFact 13 | (= ?locs locations) 14 | (some #(= "Kansas" %) ?locs)]] 15 | (rules/insert! "Kansas Exists")]] 16 | :queries [string-query [[] [[?s <- String]]]] 17 | 18 | :sessions [empty-session [kansas-rule string-query] {}]} 19 | 20 | (let [locs (doto (make-array String 2) 21 | (aset 0 "Florida") 22 | (aset 1 "Kansas"))] 23 | (let [session-strings (map :?s 24 | (-> empty-session 25 | (rules/insert (BeanTestFact. locs)) 26 | (rules/fire-rules) 27 | (rules/query string-query)))] 28 | (is (= ["Kansas Exists"] session-strings))))) 29 | 30 | ;; Using an indexed property accessor that doesn't have a standard accessor will throw exception as clara cannot resolve 31 | ;; the usage of the accessor. See https://github.com/cerner/clara-rules/issues/446 32 | (deftest test-indexed-property-accessor 33 | (let [rule-using-unsupported-accessor {:ns-name (ns-name *ns*) 34 | :lhs [{:type BeanTestFact :constraints [`(contains? ~'roadConditions "Slippery")]}] 35 | :rhs `(rules/insert! "doesn't matter") 36 | :name "rule-using-unsupported-accessor"}] 37 | (try 38 | (com/mk-session [[rule-using-unsupported-accessor]]) 39 | (is false "An exception should be thrown") 40 | (catch Exception e 41 | (loop [exc e] 42 | (cond 43 | (re-find #"Failed compiling alpha node" (.getMessage exc)) 44 | :success 45 | 46 | (.getCause exc) 47 | (recur (.getCause exc)) 48 | 49 | :else 50 | (is false "Exception didn't contain a message containing alpha node failure"))))))) -------------------------------------------------------------------------------- /src/test/clojurescript/clara/test.cljs: -------------------------------------------------------------------------------- 1 | (ns clara.test 2 | (:require-macros [cljs.test :as test]) 3 | (:require [clara.test-rules] 4 | [clara.test-rules-require] 5 | [cljs.test] 6 | [clara.test-salience] 7 | [clara.test-complex-negation] 8 | [clara.test-common] 9 | [clara.test-testing-utils] 10 | [clara.test-accumulators] 11 | [clara.test-exists] 12 | [clara.tools.test-tracing] 13 | [clara.tools.test-fact-graph] 14 | [clara.tools.test-inspect] 15 | [clara.test-truth-maintenance] 16 | [clara.test-dsl] 17 | [clara.test-accumulation] 18 | [clara.test-memory] 19 | [clara.test-simple-rules] 20 | [clara.test-rhs-retract] 21 | [clara.test-bindings] 22 | [clara.test-clear-ns-productions] 23 | [clara.test-negation] 24 | [clara.performance.test-rule-execution] 25 | [clara.test-node-sharing] 26 | [clara.test-queries])) 27 | 28 | (enable-console-print!) 29 | 30 | (def ^:dynamic *successful?* nil) 31 | 32 | (defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m] 33 | (if (cljs.test/successful? m) 34 | (do 35 | (println "Success!") 36 | (reset! *successful?* true)) 37 | (do 38 | (println "FAIL") 39 | (reset! *successful?* false)))) 40 | 41 | (defn ^:export run [] 42 | (binding [*successful?* (atom nil)] 43 | (test/run-tests 'clara.test-rules 44 | 'clara.test-rules-require 45 | 'clara.test-common 46 | 'clara.test-salience 47 | 'clara.test-testing-utils 48 | 'clara.test-complex-negation 49 | 'clara.test-accumulators 50 | 'clara.test-exists 51 | 'clara.tools.test-tracing 52 | 'clara.tools.test-fact-graph 53 | 'clara.tools.test-inspect 54 | 'clara.test-truth-maintenance 55 | 'clara.test-dsl 56 | 'clara.test-accumulation 57 | 'clara.test-memory 58 | 'clara.test-simple-rules 59 | 'clara.test-rhs-retract 60 | 'clara.test-bindings 61 | 'clara.test-clear-ns-productions 62 | 'clara.test-negation 63 | 'clara.performance.test-rule-execution 64 | 'clara.test-node-sharing 65 | 'clara.test-queries) 66 | @*successful?*)) 67 | -------------------------------------------------------------------------------- /src/test/clojurescript/clara/test_complex_negation.cljs: -------------------------------------------------------------------------------- 1 | (ns clara.test-complex-negation 2 | "Tests that validate that we wrap the fact-type-fn and ancestors-fn so that Clara's internal 3 | facts, for example NegationResult facts (added to fix issue 149) are not provided to user-provided 4 | custom fact-type-fn or ancestors-fn functions." 5 | (:require [clara.rules 6 | :refer-macros [defquery 7 | defsession] 8 | :refer [query 9 | insert 10 | fire-rules]] 11 | [clara.rules.testfacts :refer [Temperature ->Temperature 12 | WindSpeed ->WindSpeed 13 | Cold ->Cold]] 14 | [cljs.test :as t] 15 | [cljs.test :refer-macros [run-tests 16 | deftest 17 | is] 18 | :include-macros true])) 19 | 20 | (defquery negation-inside-negation-query 21 | [] 22 | [:windspeed (= ?l (:location this))] 23 | [:not [:and 24 | [?t <- :temperature (= ?l (:location this))] 25 | [:not [:cold (= (:temperature this) (:temperature ?t))]]]]) 26 | 27 | ;; Use ancestors of the fact types to ensure that the custom ancestors-fn 28 | ;; is used and that its arguments are the types from the custom fact-type-fn 29 | (defquery negation-inside-negation-ancestors-query 30 | [] 31 | [:windspeed-ancestor (= ?l (:location this))] 32 | [:not [:and 33 | [?t <- :temperature-ancestor (= ?l (:location this))] 34 | [:not [:cold-ancestor (= (:temperature this) (:temperature ?t))]]]]) 35 | 36 | (defn type->keyword 37 | [fact] 38 | (cond 39 | (instance? WindSpeed fact) :windspeed 40 | (instance? Temperature fact) :temperature 41 | (instance? Cold fact) :cold 42 | ;; If we reach the :else case then we are probably calling the user-provided :fact-type-fn 43 | ;; on an internal NegationResult fact which we should not do; see issue 241. 44 | :else (throw (ex-info "A fact that is not a WindSpeed, Temperature, or Cold was provided." 45 | {:fact fact})))) 46 | 47 | (defn keyword->ancestors 48 | [type-key] 49 | (condp = type-key 50 | :windspeed #{:windspeed-ancestor} 51 | :temperature #{:temperature-ancestor} 52 | :cold #{:cold-ancestor} 53 | 54 | (throw (ex-info "A type that is not :windspeed, :temperature, or :cold was provided" 55 | {:type type})))) 56 | 57 | (defsession test-session 'clara.test-complex-negation 58 | :fact-type-fn type->keyword 59 | :ancestors-fn keyword->ancestors) 60 | 61 | (deftest test-complex-negation 62 | (let [different-temps (-> test-session 63 | (insert (->WindSpeed 10 "MCI") 64 | (->Temperature 10 "MCI") 65 | (->Cold 20)) 66 | (fire-rules)) 67 | 68 | same-temps (-> test-session 69 | (insert (->WindSpeed 10 "MCI") 70 | (->Temperature 10 "MCI") 71 | (->Cold 10)) 72 | (fire-rules))] 73 | (is (empty? 74 | (query different-temps negation-inside-negation-query))) 75 | (is (empty? 76 | (query different-temps negation-inside-negation-ancestors-query))) 77 | 78 | (is (= [{:?l "MCI"}] 79 | (query same-temps negation-inside-negation-query) 80 | (query same-temps negation-inside-negation-ancestors-query))))) 81 | -------------------------------------------------------------------------------- /src/test/clojurescript/clara/test_rules.cljs: -------------------------------------------------------------------------------- 1 | (ns clara.test-rules 2 | (:require-macros [cljs.test :refer (is deftest run-tests testing)] 3 | [clara.rules.test-rules-data]) 4 | (:require [cljs.test :as t] 5 | [clara.rules.engine :as eng] 6 | [clara.rules.accumulators :as acc] 7 | [clara.rules :refer [insert retract fire-rules query insert!] 8 | :refer-macros [defrule defsession defquery]] 9 | [clara.rules.testfacts :refer [->Temperature Temperature 10 | ->WindSpeed WindSpeed 11 | ->ColdAndWindy ColdAndWindy]])) 12 | 13 | (comment 14 | ;; Launch browser repl. 15 | (cemerick.piggieback/cljs-repl :repl-env (cemerick.austin/exec-env)) 16 | ) 17 | 18 | (defn- has-fact? [token fact] 19 | (some #{fact} (map first (:matches token)))) 20 | 21 | (def simple-defrule-side-effect (atom nil)) 22 | (def other-defrule-side-effect (atom nil)) 23 | 24 | (defrule test-rule 25 | [Temperature (< temperature 20)] 26 | => 27 | (reset! other-defrule-side-effect ?__token__) 28 | (reset! simple-defrule-side-effect ?__token__)) 29 | 30 | (defquery cold-query 31 | [] 32 | [Temperature (< temperature 20) (== ?t temperature)]) 33 | 34 | ;; Accumulator for getting the lowest temperature. 35 | (def lowest-temp (acc/min :temperature)) 36 | 37 | (defquery coldest-query 38 | [] 39 | [?t <- lowest-temp :from [Temperature]]) 40 | 41 | (defrule is-cold-and-windy 42 | "Rule to determine whether it is indeed cold and windy." 43 | 44 | (Temperature (< temperature 20) (== ?t temperature)) 45 | (WindSpeed (> windspeed 30) (== ?w windspeed)) 46 | => 47 | (insert! (->ColdAndWindy ?t ?w))) 48 | 49 | (defrule is-cold-and-windy-map 50 | "A rule which uses a custom type on a map, to determine whether it 51 | is indeed cold and windy" 52 | 53 | [:temp [{degrees :degrees}] (< degrees 20) (== ?t degrees)] 54 | [:wind [{mph :mph}] (> mph 30) (== ?w mph)] 55 | => 56 | (insert! {:type :cold-and-windy 57 | :temp ?t 58 | :wind ?w})) 59 | 60 | (defrule throw-on-bad-temp 61 | "Rule to test exception flow." 62 | [Temperature (> temperature 10000) (= ?t temperature)] 63 | => 64 | (throw (ex-info "Bad temperature!" {:temp ?t}))) 65 | 66 | (defquery find-cold-and-windy 67 | [] 68 | [?fact <- ColdAndWindy]) 69 | 70 | (defquery find-cold-and-windy-map 71 | [] 72 | [?fact <- :cold-and-windy]) 73 | 74 | (defquery wind-without-temperature 75 | [] 76 | [WindSpeed (== ?w windspeed)] 77 | [:not [Temperature]]) 78 | 79 | (defquery wind-with-temperature 80 | [] 81 | [WindSpeed (== ?w windspeed) (== ?loc location)] 82 | [Temperature (== ?t temperature) (== ?loc location)]) 83 | 84 | ;; The idea here is that Number will resolve to java.lang.Number in a Clojure environment, 85 | ;; so this validates that we correctly handle symbols in a ClojureScript rule that happen 86 | ;; to resolve to something in a Clojure environment. Since ClojureScript's compiler 87 | ;; is in Clojure failing to handle this correctly can cause us to attempt to embed 88 | ;; Java objects in ClojureScript code, which won't work. See issue 300. 89 | (defrecord Number [value]) 90 | 91 | (defquery num-query 92 | [] 93 | [?n <- Number]) 94 | 95 | (defsession my-session 'clara.test-rules) 96 | (defsession my-session-map 'clara.test-rules :fact-type-fn :type) 97 | (defsession my-session-data (clara.rules.test-rules-data/weather-rules)) 98 | 99 | (deftest test-number-query 100 | (is (= (-> my-session 101 | (insert (->Number 5)) 102 | fire-rules 103 | (query num-query)) 104 | [{:?n (->Number 5)}]))) 105 | 106 | (deftest test-simple-defrule 107 | (let [session (insert my-session (->Temperature 10 "MCI"))] 108 | 109 | (fire-rules session) 110 | 111 | (is (has-fact? @simple-defrule-side-effect (->Temperature 10 "MCI"))) 112 | (is (has-fact? @other-defrule-side-effect (->Temperature 10 "MCI"))))) 113 | 114 | (deftest test-simple-query 115 | (let [session (-> my-session 116 | (insert (->Temperature 15 "MCI")) 117 | (insert (->Temperature 10 "MCI")) 118 | (insert (->Temperature 80 "MCI")) 119 | fire-rules)] 120 | 121 | ;; The query should identify all items that wer einserted and matchd the 122 | ;; expected criteria. 123 | (is (= #{{:?t 15} {:?t 10}} 124 | (set (query session cold-query)))))) 125 | 126 | (deftest test-simple-accumulator 127 | (let [session (-> my-session 128 | (insert (->Temperature 15 "MCI")) 129 | (insert (->Temperature 10 "MCI")) 130 | (insert (->Temperature 80 "MCI")) 131 | fire-rules)] 132 | 133 | ;; Accumulator returns the lowest value. 134 | (is (= #{{:?t 10}} 135 | (set (query session coldest-query)))))) 136 | 137 | (deftest test-simple-insert 138 | (let [session (-> my-session 139 | (insert (->Temperature 15 "MCI")) 140 | (insert (->WindSpeed 45 "MCI")) 141 | (fire-rules))] 142 | 143 | (is (= #{{:?fact (->ColdAndWindy 15 45)}} 144 | (set 145 | (query session find-cold-and-windy)))))) 146 | 147 | (deftest test-simple-insert-map 148 | 149 | (let [session (-> my-session-map 150 | (insert {:type :temp :degrees 15}) 151 | (insert {:type :wind :mph 45}) 152 | (fire-rules))] 153 | (is (= #{{:?fact {:type :cold-and-windy :temp 15 :wind 45}}} 154 | (set 155 | (query session find-cold-and-windy-map)))))) 156 | 157 | (deftest test-simple-insert-data 158 | 159 | (let [session (-> my-session-data 160 | (insert (->Temperature 15 "MCI")) 161 | (insert (->WindSpeed 45 "MCI")) 162 | (fire-rules))] 163 | (is (= #{{:?fact (->ColdAndWindy 15 45)}} 164 | (set 165 | (query session "clara.rules.test-rules-data/find-cold-and-windy-data")))))) 166 | 167 | (deftest test-no-temperature 168 | 169 | ;; Test that a temperature cancels the match. 170 | (let [session (-> my-session 171 | (insert (->Temperature 15 "MCI")) 172 | (insert (->WindSpeed 45 "MCI")) 173 | (fire-rules))] 174 | 175 | (is (= #{} 176 | (set 177 | (query session wind-without-temperature))))) 178 | 179 | ;; Now test the no temperature scenario. 180 | (let [session (-> my-session 181 | (insert (->WindSpeed 45 "MCI")) 182 | (fire-rules))] 183 | 184 | (is (= #{{:?w 45}} 185 | (set 186 | (query session wind-without-temperature)))))) 187 | 188 | 189 | (deftest test-simple-join 190 | 191 | (let [session (-> my-session 192 | (insert (->Temperature 15 "MCI")) 193 | (insert (->WindSpeed 45 "MCI")) 194 | (fire-rules))] 195 | 196 | (is (= #{{:?w 45 :?t 15 :?loc "MCI"}} 197 | (set 198 | (query session wind-with-temperature)))))) 199 | 200 | (deftest test-throw-rhs 201 | 202 | (try 203 | (-> my-session 204 | (insert (->Temperature 999999 "MCI")) 205 | (fire-rules)) 206 | (catch :default e 207 | 208 | (is (= {:?t 999999} 209 | (:bindings (ex-data e)))) 210 | (is (= "clara.test-rules/throw-on-bad-temp" 211 | (:name (ex-data e))))))) 212 | 213 | (deftest test-remove-pending-rule-activation 214 | (let [no-activations-session (-> my-session 215 | (insert (->Temperature -10 "ORD") 216 | (->WindSpeed 50 "ORD")) 217 | (retract (->WindSpeed 50 "ORD")) 218 | fire-rules) 219 | 220 | one-activation-session (-> my-session 221 | (insert (->Temperature -10 "ORD") 222 | (->WindSpeed 50 "ORD") 223 | (->WindSpeed 50 "ORD")) 224 | (retract (->WindSpeed 50 "ORD")) 225 | fire-rules)] 226 | 227 | (is (= (query no-activations-session find-cold-and-windy) [])) 228 | (is (= (query one-activation-session find-cold-and-windy) 229 | [{:?fact (->ColdAndWindy -10 50)}])))) 230 | 231 | ;;; Basic test of keyword names 232 | (defsession my-session-data-with-keyword-names (clara.rules.test-rules-data/weather-rules-with-keyword-names)) 233 | (deftest test-simple-insert-data-with-keyword-names 234 | 235 | (let [session (-> my-session-data-with-keyword-names 236 | (insert (->Temperature 15 "MCI")) 237 | (insert (->WindSpeed 45 "MCI")) 238 | (fire-rules))] 239 | (is (= [{:?fact (->ColdAndWindy 15 45)}] 240 | (query session :clara.rules.test-rules-data/find-cold-and-windy-data))))) -------------------------------------------------------------------------------- /src/test/clojurescript/clara/test_salience.cljs: -------------------------------------------------------------------------------- 1 | (ns clara.test-salience 2 | (:require-macros [cljs.test :refer (is deftest run-tests testing)] 3 | [clara.rules.test-rules-data]) 4 | (:require [cljs.test :as t] 5 | [clara.rules.engine :as eng] 6 | [clara.rules.accumulators :as acc] 7 | [clara.rules :refer [insert retract fire-rules query insert!] 8 | :refer-macros [defrule defsession defquery]] 9 | [clara.rules.testfacts :refer [->Temperature Temperature 10 | ->WindSpeed WindSpeed 11 | ->ColdAndWindy ColdAndWindy]])) 12 | 13 | (def salience-rule-output (atom [])) 14 | 15 | (defrule salience-rule1 16 | {:salience 100} 17 | [Temperature] 18 | => 19 | (swap! salience-rule-output conj 100)) 20 | 21 | (defrule salience-rule2 22 | {:salience 50} 23 | [Temperature] 24 | => 25 | (swap! salience-rule-output conj 50)) 26 | 27 | (defrule salience-rule3 28 | {:salience 0} 29 | [Temperature] 30 | => 31 | (swap! salience-rule-output conj 0)) 32 | 33 | (defrule salience-rule4 34 | {:salience -50} 35 | [Temperature] 36 | => 37 | (swap! salience-rule-output conj -50)) 38 | 39 | 40 | (deftest test-salience 41 | (doseq [[sort-fn 42 | group-fn 43 | expected-order] 44 | 45 | [[:default-sort :default-group :forward-order] 46 | [:default-sort :salience-group :forward-order] 47 | [:default-sort :neg-salience-group :backward-order] 48 | 49 | [:numeric-greatest-sort :default-group :forward-order] 50 | [:numeric-greatest-sort :salience-group :forward-order] 51 | [:numeric-greatest-sort :neg-salience-group :backward-order] 52 | 53 | 54 | [:boolean-greatest-sort :default-group :forward-order] 55 | [:boolean-greatest-sort :salience-group :forward-order] 56 | [:boolean-greatest-sort :neg-salience-group :backward-order] 57 | 58 | 59 | [:numeric-least-sort :default-group :backward-order] 60 | [:numeric-least-sort :salience-group :backward-order] 61 | [:numeric-least-sort :neg-salience-group :forward-order] 62 | 63 | [:boolean-least-sort :default-group :backward-order] 64 | [:boolean-least-sort :salience-group :backward-order] 65 | [:boolean-least-sort :neg-salience-group :forward-order]]] 66 | 67 | (let [numeric-greatest-sort (fn [x y] 68 | (cond 69 | (= x y) 0 70 | (> x y) -1 71 | :else 1)) 72 | 73 | numeric-least-sort (fn [x y] 74 | (numeric-greatest-sort y x)) 75 | 76 | salience-group-fn (fn [production] 77 | (or (some-> production :props :salience) 78 | 0)) 79 | 80 | neg-salience-group-fn (fn [p] 81 | (- (salience-group-fn p)))] 82 | 83 | ;; A CLJS macro that behaves like mk-session (creates a session but does not intern a Var) 84 | ;; has been proposed in #292. Internally, this would facilitate session generation for CLJS 85 | ;; tests such as this one, and may be useful if exposed publicly. 86 | 87 | (defsession test-salience-session 'clara.test-salience 88 | :cache false 89 | :activation-group-sort-fn (condp = sort-fn 90 | :default-sort nil 91 | :numeric-greatest-sort numeric-greatest-sort 92 | :numeric-least-sort numeric-least-sort 93 | :boolean-greatest-sort > 94 | :boolean-least-sort <) 95 | :activation-group-fn (condp = group-fn 96 | :default-group nil 97 | :salience-group salience-group-fn 98 | :neg-salience-group neg-salience-group-fn)) 99 | 100 | (reset! salience-rule-output []) 101 | 102 | (-> test-salience-session 103 | (insert (->Temperature 10 "MCI")) 104 | (fire-rules)) 105 | 106 | (let [test-fail-str 107 | (str "Failure with sort-fn: " sort-fn ", group-fn: " group-fn ", and expected order: " expected-order)] 108 | (condp = expected-order 109 | :forward-order 110 | (is (= [100 50 0 -50] @salience-rule-output) 111 | test-fail-str) 112 | 113 | :backward-order 114 | (is (= [-50 0 50 100] @salience-rule-output) 115 | test-fail-str)))))) 116 | -------------------------------------------------------------------------------- /src/test/common/clara/performance/test_rule_execution.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.performance.test-rule-execution 2 | (:require [clara.rules.accumulators :as acc] 3 | [clara.rules :as r] 4 | #?(:clj 5 | [clojure.test :refer :all] 6 | :cljs [cljs.test :refer-macros [is deftest]]) 7 | #?(:clj 8 | [clara.tools.testing-utils :refer [def-rules-test run-performance-test]] 9 | :cljs [clara.tools.testing-utils :refer [run-performance-test]])) 10 | #?(:cljs (:require-macros [clara.tools.testing-utils :refer [def-rules-test]]))) 11 | 12 | (defrecord AFact [id]) 13 | (defrecord BFact [id]) 14 | (defrecord ParentFact [a-id b-id]) 15 | 16 | (def counter (atom {:a-count 0 17 | :b-count 0})) 18 | 19 | (def number-of-facts #?(:clj 1500 :cljs 150)) 20 | 21 | (def-rules-test test-get-in-perf 22 | {:rules [rule [[[?parent <- ParentFact] 23 | [?as <- (acc/all) :from [AFact (= (:a-id ?parent) id)]] 24 | [?bs <- (acc/all) :from [BFact (= (:b-id ?parent) id)]]] 25 | '(do (swap! counter update :a-count inc))]] 26 | :sessions [session [rule] {}]} 27 | (let [parents (for [x (range number-of-facts)] 28 | (->ParentFact x (inc x))) 29 | a-facts (for [id (range number-of-facts)] 30 | (->AFact id)) 31 | b-facts (for [id (range number-of-facts)] 32 | (->BFact id)) 33 | 34 | facts (doall (concat parents 35 | a-facts 36 | b-facts))] 37 | (run-performance-test {:description "Slow get-in perf" 38 | :func #(-> session 39 | (r/insert-all facts) 40 | r/fire-rules) 41 | :iterations 5 42 | :mean-assertion (partial > 10000)}))) -------------------------------------------------------------------------------- /src/test/common/clara/rule_defs.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.rule-defs 2 | (:require [clara.rules.accumulators :as acc] 3 | [clara.rules.testfacts :as tf] 4 | [clara.tools.testing-utils :as tu] 5 | #?(:clj [clara.rules :refer [defrule defquery insert!]]) 6 | #?(:cljs [clara.rules :refer-macros [defrule defquery] :refer [insert!]])) 7 | #?(:clj 8 | (:import [clara.rules.testfacts Temperature WindSpeed ColdAndWindy]))) 9 | 10 | ;; Rule definitions used for tests in clara.test-rules-require. 11 | 12 | (defrule test-rule 13 | [?t <- #?(:clj Temperature :cljs tf/Temperature) (< temperature 20)] 14 | => 15 | (reset! tu/side-effect-holder ?t)) 16 | 17 | (defquery cold-query 18 | [] 19 | [#?(:clj Temperature :cljs tf/Temperature) (< temperature 20) (== ?t temperature)]) 20 | 21 | ;; Accumulator for getting the lowest temperature. 22 | (def lowest-temp (acc/min :temperature)) 23 | 24 | (defquery coldest-query 25 | [] 26 | [?t <- lowest-temp :from [#?(:clj Temperature :cljs tf/Temperature)]]) 27 | 28 | (defrule is-cold-and-windy 29 | "Rule to determine whether it is indeed cold and windy." 30 | 31 | (#?(:clj Temperature :cljs tf/Temperature) (< temperature 20) (== ?t temperature)) 32 | (#?(:clj WindSpeed :cljs tf/WindSpeed) (> windspeed 30) (== ?w windspeed)) 33 | => 34 | (insert! (tf/->ColdAndWindy ?t ?w))) 35 | 36 | (defquery find-cold-and-windy 37 | [] 38 | [?fact <- #?(:clj ColdAndWindy :cljs tf/ColdAndWindy)]) 39 | 40 | -------------------------------------------------------------------------------- /src/test/common/clara/test_clear_ns_productions.cljc: -------------------------------------------------------------------------------- 1 | ;;; Tests that clear-ns-productions! correction clears all vars marked as productions from the namespace. 2 | #?(:clj 3 | (ns clara.test-clear-ns-productions 4 | (:require [clara.tools.testing-utils :as tu] 5 | [clara.rules :refer [fire-rules 6 | insert 7 | insert! 8 | query 9 | defrule 10 | defquery 11 | defsession 12 | clear-ns-productions!]] 13 | [clojure.test :refer [is deftest run-tests testing use-fixtures]])) 14 | :cljs 15 | (ns clara.test-clear-ns-productions 16 | (:require [clara.rules :refer [fire-rules 17 | insert 18 | insert! 19 | query]] 20 | [cljs.test] 21 | [clara.tools.testing-utils :as tu]) 22 | (:require-macros [clara.rules :refer [defrule defquery defsession clear-ns-productions!]] 23 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 24 | 25 | (use-fixtures :each tu/side-effect-holder-fixture) 26 | 27 | (defrule rule-to-be-cleared 28 | [:a] 29 | => 30 | (reset! tu/side-effect-holder :before-clearing) 31 | (insert! :before-clearing)) 32 | 33 | (defquery query-to-be-cleared [] [?f <- :before-clearing]) 34 | 35 | #?(:clj 36 | (def ^:production-seq ns-production-seq-to-be-cleared 37 | [{:doc "Before clearing" 38 | :name "clara.test-clear-ns-productions/production-seq-to-be-cleared" 39 | :lhs '[{:type :a 40 | :constraints []}] 41 | :rhs '(clara.rules/insert! :before-clearing-seq)}])) 42 | 43 | (defsession uncleared-session 'clara.test-clear-ns-productions :fact-type-fn identity) 44 | 45 | (clear-ns-productions!) 46 | 47 | (defrule rule-after-clearing 48 | [:a] 49 | => 50 | (insert! :after-clearing)) 51 | 52 | (defquery query-before-clearing [] [?f <- :before-clearing]) 53 | (defquery query-after-clearing [] [?f <- :after-clearing]) 54 | (defquery query-before-clearing-seq [] [?f <- :before-clearing-seq]) 55 | (defquery query-after-clearing-seq [] [?f <- :after-clearing-seq]) 56 | #?(:clj 57 | (def ^:production-seq production-seq-after-clearing 58 | [{:doc "After clearing" 59 | :name "clara.test-clear-ns-productions/production-seq-after-clearing" 60 | :lhs '[{:type :a 61 | :constraints []}] 62 | :rhs '(clara.rules/insert! :after-clearing-seq)}])) 63 | 64 | (defsession cleared-session 'clara.test-clear-ns-productions :fact-type-fn identity) 65 | 66 | ;;; Then tests validating what productions the respective sessions have. 67 | (deftest cleared? 68 | (let [uncleared (-> uncleared-session (insert :a) (fire-rules))] 69 | (is (= :before-clearing @tu/side-effect-holder)) 70 | (reset! tu/side-effect-holder nil)) 71 | (let [cleared (-> cleared-session (insert :a) (fire-rules))] 72 | (testing "cleared-session should not contain any productions before (clear-ns-productions!)" 73 | (is (= nil @tu/side-effect-holder)) 74 | (is (empty? (query cleared query-before-clearing))) 75 | #?(:clj (is (not-empty (query cleared query-after-clearing))))) 76 | (is (empty? (query cleared query-before-clearing-seq))) 77 | #?(:clj (is (not-empty (query cleared query-after-clearing-seq)))))) 78 | 79 | (deftest query-cleared? 80 | (let [uncleared (-> uncleared-session (insert :a) (fire-rules)) 81 | cleared (-> cleared-session (insert :a) (fire-rules))] 82 | (is (not-empty (query uncleared "clara.test-clear-ns-productions/query-to-be-cleared"))) 83 | (is (thrown-with-msg? #?(:clj IllegalArgumentException :cljs js/Error) #"clara.test-clear-ns-productions/query-to-be-cleared" 84 | (query cleared "clara.test-clear-ns-productions/query-to-be-cleared"))))) -------------------------------------------------------------------------------- /src/test/common/clara/test_common.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.test-common 2 | "Common tests for Clara in Clojure and ClojureScript." 3 | (:require #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [is deftest testing]]) 5 | 6 | #?(:clj [clara.rules :refer [defrule defsession defquery 7 | insert fire-rules query]] 8 | :cljs [clara.rules :refer [insert fire-rules query] 9 | :refer-macros [defrule defsession defquery]]) 10 | 11 | [clara.rules.accumulators :as acc] 12 | 13 | [clara.rules.platform :as platform])) 14 | 15 | (defn- has-fact? [token fact] 16 | (some #{fact} (map first (:matches token)))) 17 | 18 | (def simple-defrule-side-effect (atom nil)) 19 | (def other-defrule-side-effect (atom nil)) 20 | 21 | (defrule test-rule 22 | [:temperature [{temperature :temperature}] (< temperature 20)] 23 | => 24 | (reset! other-defrule-side-effect ?__token__) 25 | (reset! simple-defrule-side-effect ?__token__)) 26 | 27 | (defquery cold-query 28 | [] 29 | [:temperature [{temperature :temperature}] (< temperature 20) (= ?t temperature)]) 30 | 31 | (defsession my-session [test-rule cold-query] :fact-type-fn :type) 32 | 33 | (deftest test-simple-defrule 34 | (let [t {:type :temperature 35 | :temperature 10 36 | :location "MCI"} 37 | session (insert my-session t)] 38 | 39 | (fire-rules session) 40 | 41 | (is (has-fact? @simple-defrule-side-effect t)) 42 | (is (has-fact? @other-defrule-side-effect t)))) 43 | 44 | (deftest test-simple-query 45 | (let [session (-> my-session 46 | (insert {:type :temperature 47 | :temperature 15 48 | :location "MCI"}) 49 | (insert {:type :temperature 50 | :temperature 10 51 | :location "MCI"}) 52 | (insert {:type :temperature 53 | :temperature 80 54 | :location "MCI"}) 55 | fire-rules)] 56 | 57 | ;; The query should identify all items that were inserted and matched the 58 | ;; expected criteria. 59 | (is (= #{{:?t 15} {:?t 10}} 60 | (set (query session cold-query)))))) 61 | 62 | 63 | (defquery temps-below-threshold 64 | [] 65 | [:threshold [{value :value}] (= ?threshold value)] 66 | [?low-temps <- (acc/all) :from [:temperature [{value :value}] (< value ?threshold)]]) 67 | 68 | (defsession accum-with-filter-session [temps-below-threshold] :fact-type-fn :type) 69 | 70 | (deftest test-accum-with-filter 71 | 72 | (is (= [{:?threshold 0, :?low-temps []}] 73 | (-> accum-with-filter-session 74 | (insert {:type :temperature :value 20}) 75 | (insert {:type :threshold :value 0}) 76 | (insert {:type :temperature :value 10}) 77 | fire-rules 78 | (query temps-below-threshold)))) 79 | 80 | (let [results (-> accum-with-filter-session 81 | (insert {:type :temperature :value 20}) 82 | (insert {:type :threshold :value 40}) 83 | (insert {:type :temperature :value 10}) 84 | (insert {:type :temperature :value 60}) 85 | fire-rules 86 | (query temps-below-threshold)) 87 | 88 | [{threshold :?threshold low-temps :?low-temps }] results] 89 | 90 | (is (= 1 (count results))) 91 | 92 | (is (= 40 threshold)) 93 | 94 | (is (= #{{:type :temperature :value 10} {:type :temperature :value 20}} 95 | (set low-temps))))) 96 | 97 | (defquery none-below-threshold 98 | [] 99 | [:threshold [{value :value}] (= ?threshold value)] 100 | [:not [:temperature [{value :value}] (< value ?threshold)]]) 101 | 102 | (defquery temperature-below-value-using-symbol-arg 103 | [?value] 104 | [:temperature [{value :value}] (< value ?value)]) 105 | 106 | (defquery temperature-below-value-using-keyword-arg 107 | [:?value] 108 | [:temperature [{value :value}] (< value ?value)]) 109 | 110 | (deftest test-query-definition-bindings-args 111 | (testing "can define queries using symbol or keyword arguments" 112 | (is (= (dissoc temperature-below-value-using-symbol-arg :name) (dissoc temperature-below-value-using-keyword-arg :name))))) 113 | 114 | (deftest test-query-param-args 115 | (testing "noop query-params using keyword arguments" 116 | (is (= (platform/query-param :?value) :?value))) 117 | (testing "coerce query-params using symbol arguments" 118 | (is (= (platform/query-param '?value) :?value))) 119 | (testing "can not coerce query-params using string arguments" 120 | (try 121 | (platform/query-param "?value") 122 | (is false "Running the rules in this test should cause an exception.") 123 | (catch #?(:clj java.lang.IllegalArgumentException 124 | :cljs js/Error) e 125 | (is (= "Query bindings must be specified as a keyword or symbol: ?value" 126 | #?(:clj (.getMessage e) 127 | :cljs (.-message e)))))))) 128 | 129 | (defsession negation-with-filter-session [none-below-threshold] :fact-type-fn :type) 130 | 131 | (deftest test-negation-with-filter 132 | 133 | (is (= [{:?threshold 0}] 134 | (-> negation-with-filter-session 135 | (insert {:type :temperature :value 20}) 136 | (insert {:type :threshold :value 0}) 137 | (insert {:type :temperature :value 10}) 138 | fire-rules 139 | (query none-below-threshold)))) 140 | 141 | ;; Values below the threshold exist, so we should not match. 142 | (is (empty? 143 | (-> negation-with-filter-session 144 | (insert {:type :temperature :value 20}) 145 | (insert {:type :threshold :value 40}) 146 | (insert {:type :temperature :value 10}) 147 | (insert {:type :temperature :value 60}) 148 | fire-rules 149 | (query none-below-threshold))))) 150 | -------------------------------------------------------------------------------- /src/test/common/clara/test_exists.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.test-exists 3 | (:require [clara.tools.testing-utils :refer [def-rules-test] :as tu] 4 | [clara.rules :refer [fire-rules 5 | insert 6 | insert-all 7 | insert-unconditional! 8 | insert! 9 | retract 10 | query]] 11 | 12 | [clara.rules.testfacts :refer [->Temperature ->Cold ->WindSpeed 13 | ->ColdAndWindy]] 14 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 15 | [clara.rules.accumulators] 16 | [schema.test :as st]) 17 | (:import [clara.rules.testfacts 18 | Temperature 19 | Cold 20 | WindSpeed 21 | ColdAndWindy])) 22 | 23 | :cljs 24 | (ns clara.test-exists 25 | (:require [clara.rules :refer [fire-rules 26 | insert 27 | insert! 28 | insert-all 29 | insert-unconditional! 30 | retract 31 | query]] 32 | [clara.rules.testfacts :refer [->Temperature Temperature 33 | ->Cold Cold 34 | ->WindSpeed WindSpeed 35 | ->ColdAndWindy ColdAndWindy]] 36 | [clara.rules.accumulators] 37 | [cljs.test] 38 | [schema.test :as st]) 39 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 40 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 41 | 42 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 43 | 44 | (def-rules-test test-simple-exists 45 | {:queries [has-windspeed [[] [[:exists [WindSpeed (= ?location location)]]]]] 46 | 47 | :sessions [empty-session [has-windspeed] {}]} 48 | 49 | ;; An empty session should produce no results. 50 | (is (empty? (query empty-session has-windspeed))) 51 | 52 | ;; Should only match one windspeed despite multiple inserts. 53 | (is (= [{:?location "MCI"}] 54 | (-> empty-session 55 | (insert (->WindSpeed 50 "MCI")) 56 | (insert (->WindSpeed 60 "MCI")) 57 | fire-rules 58 | (query has-windspeed)))) 59 | 60 | ;; Retraction should remove exists check. 61 | (is (empty? 62 | (-> empty-session 63 | (insert (->WindSpeed 50 "MCI")) 64 | (insert (->WindSpeed 60 "MCI")) 65 | (retract (->WindSpeed 50 "MCI")) 66 | (retract (->WindSpeed 60 "MCI")) 67 | fire-rules 68 | (query has-windspeed)))) 69 | 70 | ;; There should be one location for each distinct binding. 71 | (is (= #{{:?location "MCI"} {:?location "SFO"} {:?location "ORD"}} 72 | (-> empty-session 73 | (insert (->WindSpeed 50 "MCI")) 74 | (insert (->WindSpeed 60 "MCI")) 75 | (insert (->WindSpeed 60 "SFO")) 76 | (insert (->WindSpeed 80 "SFO")) 77 | (insert (->WindSpeed 80 "ORD")) 78 | (insert (->WindSpeed 90 "ORD")) 79 | fire-rules 80 | (query has-windspeed) 81 | (set))))) 82 | 83 | (def-rules-test test-exists-with-conjunction 84 | {:queries [wind-and-temp [[] [[:exists [Temperature (= ?location location)]] 85 | [:exists [WindSpeed (= ?location location)]]]]] 86 | 87 | :sessions [empty-session [wind-and-temp] {}]} 88 | 89 | ;; An empty session should produce no results. 90 | (is (empty? (query empty-session wind-and-temp))) 91 | 92 | ;; A match of one exist but not the other should yield nothing. 93 | (is (empty? 94 | (-> empty-session 95 | (insert (->WindSpeed 50 "MCI")) 96 | (insert (->WindSpeed 60 "MCI")) 97 | fire-rules 98 | (query wind-and-temp)))) 99 | 100 | ;; Differing locations should not yield a match. 101 | (is (empty? 102 | (-> empty-session 103 | (insert (->WindSpeed 50 "MCI")) 104 | (insert (->Temperature 60 "ORD")) 105 | fire-rules 106 | (query wind-and-temp)))) 107 | 108 | ;; Simple match for both exists. 109 | (is (= [{:?location "MCI"}] 110 | (-> empty-session 111 | (insert (->WindSpeed 50 "MCI")) 112 | (insert (->WindSpeed 60 "MCI")) 113 | (insert (->Temperature 60 "MCI")) 114 | (insert (->Temperature 70 "MCI")) 115 | fire-rules 116 | (query wind-and-temp)))) 117 | 118 | ;; There should be a match for each distinct city. 119 | (is (= #{{:?location "MCI"} {:?location "ORD"}} 120 | (-> empty-session 121 | (insert (->WindSpeed 50 "MCI")) 122 | (insert (->WindSpeed 60 "ORD")) 123 | (insert (->Temperature 60 "MCI")) 124 | (insert (->Temperature 70 "ORD")) 125 | fire-rules 126 | (query wind-and-temp) 127 | (set))))) 128 | 129 | (def-rules-test test-exists-inside-boolean-conjunction-and-disjunction 130 | 131 | {:rules [or-rule [[[:or 132 | [:exists [ColdAndWindy]] 133 | [:exists [Temperature (< temperature 20)]]]] 134 | (insert! (->Cold nil))] 135 | 136 | and-rule [[[:and 137 | [:exists [ColdAndWindy]] 138 | [:exists [Temperature (< temperature 20)]]]] 139 | (insert! (->Cold nil))]] 140 | 141 | :queries [cold-query [[] [[Cold (= ?t temperature)]]]] 142 | 143 | :sessions [or-session [or-rule cold-query] {} 144 | and-session [and-rule cold-query] {}]} 145 | 146 | (is (empty? (-> or-session 147 | fire-rules 148 | (query cold-query))) 149 | "Verify that :exists under an :or does not fire if nothing meeting the :exists is present") 150 | 151 | (is (= (-> or-session 152 | (insert (->ColdAndWindy 10 10)) 153 | fire-rules 154 | (query cold-query)) 155 | [{:?t nil}]) 156 | "Validate that :exists can match under a boolean :or condition.") 157 | 158 | (is (empty? (-> and-session 159 | (insert (->ColdAndWindy 10 10)) 160 | fire-rules 161 | (query cold-query))) 162 | "Validate that :exists under an :and condition without both conditions does not cause the rule to fire.") 163 | 164 | (is (= (-> and-session 165 | (insert (->ColdAndWindy 10 10) (->Temperature 10 "MCI")) 166 | fire-rules 167 | (query cold-query)) 168 | [{:?t nil}]) 169 | "Validate that :exists can match under a boolean :and condition.")) 170 | 171 | ;; Test of the performance optimization in https://github.com/cerner/clara-rules/issues/298 172 | ;; The idea is that if inserting additional items beyond the first causes a retraction and then 173 | ;; rebuilding of the Rete network an unconditional insertion will happen twice. 174 | (def-rules-test test-additional-item-noop 175 | 176 | {:rules [exists-rule [[[:exists [Temperature (< temperature 0)]]] 177 | (insert-unconditional! (->Cold :freezing))]] 178 | 179 | :queries [cold-query [[] [[Cold (= ?t temperature)]]]] 180 | 181 | :sessions [empty-session [exists-rule cold-query] {}]} 182 | 183 | (is (= [{:?t :freezing}] 184 | (-> empty-session 185 | (insert (->Temperature -10 "INV")) 186 | (fire-rules) 187 | (insert (->Temperature -10 "INV")) 188 | fire-rules 189 | (query cold-query))))) 190 | -------------------------------------------------------------------------------- /src/test/common/clara/test_memory.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.test-memory 3 | (:require [clara.tools.testing-utils :refer [def-rules-test] :as tu] 4 | [clara.rules :refer [fire-rules 5 | insert 6 | insert-all 7 | insert! 8 | retract 9 | query]] 10 | [clara.rules.testfacts :refer [->Temperature ->Cold ->WindSpeed ->Hot 11 | ->ColdAndWindy ->First ->Second]] 12 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 13 | [clara.rules.accumulators :as acc] 14 | [schema.test :as st]) 15 | (:import [clara.rules.testfacts 16 | Temperature 17 | Hot 18 | Cold 19 | WindSpeed 20 | ColdAndWindy 21 | First 22 | Second])) 23 | 24 | :cljs 25 | (ns clara.test-memory 26 | (:require [clara.rules :refer [fire-rules 27 | insert 28 | insert! 29 | insert-all 30 | retract 31 | query]] 32 | [clara.rules.testfacts :refer [->Temperature Temperature 33 | ->Cold Cold 34 | ->Hot Hot 35 | ->WindSpeed WindSpeed 36 | ->ColdAndWindy ColdAndWindy 37 | ->First First 38 | ->Second Second]] 39 | [clara.rules.accumulators :as acc] 40 | [clara.tools.testing-utils :as tu] 41 | [cljs.test] 42 | [schema.test :as st]) 43 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 44 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 45 | 46 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 47 | 48 | ;; While the memory is tested through rules and queries, rather than direct unit tests on the memory, 49 | ;; the intent of these tests is to create patterns in the engine that cover edge cases and other paths 50 | ;; of concern in clara.rules.memory. 51 | ;; This test are here to verify https://github.com/cerner/clara-rules/issues/303 52 | (def-rules-test test-negation-complex-join-with-numerous-non-matching-facts-inserted-after-descendant-negation 53 | {:rules [] 54 | :queries [query1 [[] 55 | [[Hot (= ?t temperature)] 56 | [:not [Cold (tu/join-filter-equals ?t temperature)]]]] 57 | query2 [[] 58 | [[Hot (= ?t temperature)] 59 | [:not [Cold (= ?t temperature)]]]]] 60 | :sessions [empty-session-jfe [query1] {} 61 | empty-session-equals [query2] {}]} 62 | (let [lots-of-hot (doall (for [_ (range 100)] 63 | (->Hot 20)))] 64 | (is (= (repeat 100 {:?t 20}) 65 | (-> empty-session-jfe 66 | (insert (->Cold 10)) 67 | fire-rules 68 | (insert-all lots-of-hot) 69 | fire-rules 70 | (query query1)))) 71 | 72 | (is (= (repeat 100 {:?t 20}) 73 | (-> empty-session-equals 74 | (insert (->Cold 10)) 75 | fire-rules 76 | (insert-all lots-of-hot) 77 | fire-rules 78 | (query query2)))))) 79 | 80 | (def-rules-test test-query-for-many-added-elements 81 | {:queries [temp-query [[] [[Temperature (= ?t temperature)]]]] 82 | 83 | :sessions [empty-session [temp-query] {}]} 84 | 85 | (let [n 6000 86 | ;; Do not batch insert to expose any StackOverflowError potential 87 | ;; of stacking lazy evaluations in working memory. 88 | session (reduce insert empty-session 89 | (for [i (range n)] (->Temperature i "MCI"))) 90 | session (fire-rules session)] 91 | 92 | (is (= n 93 | (count (query session temp-query)))))) 94 | 95 | (def-rules-test test-query-for-many-added-tokens 96 | 97 | {:rules [cold-temp [[[Temperature (< temperature 30) (= ?t temperature)]] 98 | (insert! (->Cold ?t))]] 99 | 100 | :queries [cold-query [[] [[Cold (= ?t temperature)]]]] 101 | 102 | :sessions [empty-session [cold-temp cold-query] {}]} 103 | 104 | (let [n 6000 105 | 106 | ;; Do not batch insert to expose any StackOverflowError potential 107 | ;; of stacking lazy evaluations in working memory. 108 | session (reduce insert empty-session 109 | (for [i (range n)] (->Temperature (- i) "MCI"))) 110 | 111 | session (fire-rules session)] 112 | 113 | (is (= n 114 | (count (query session cold-query)))))) 115 | 116 | 117 | (def-rules-test test-many-retract-accumulated-for-same-accumulate-with-join-filter-node 118 | 119 | {:rules [count-cold-temps [[[Cold (= ?cold-temp temperature)] 120 | [?temp-count <- (acc/count) :from [Temperature (some? temperature) (<= temperature ?cold-temp)]]] 121 | (insert! {:count ?temp-count 122 | :type :temp-counter})]] 123 | 124 | :queries [cold-temp-count-query [[] [[:temp-counter [{:keys [count]}] (= ?count count)]]]] 125 | 126 | :sessions [empty-session [count-cold-temps cold-temp-count-query] {:fact-type-fn (fn [f] 127 | (or (:type f) 128 | (type f)))}]} 129 | 130 | (let [n 6000 131 | 132 | session (reduce insert empty-session 133 | ;; Insert all temperatures one at a time to ensure the 134 | ;; accumulate node will continuously re-accumulate via 135 | ;; `right-activate-reduced` to expose any StackOverflowError 136 | ;; potential of stacking lazy evaluations in working memory. 137 | (for [t (range n)] (->Temperature (- t) "MCI"))) 138 | session (-> session 139 | (insert (->Cold 30)) 140 | fire-rules)] 141 | 142 | ;; All temperatures are under the Cold temperature threshold. 143 | (is (= #{{:?count 6000}} (set (query session cold-temp-count-query)))))) 144 | 145 | (def-rules-test test-disjunctions-sharing-production-node 146 | ;; Ensures that when 'sibling' nodes are sharing a common child 147 | ;; production node, that activations are effectively retracted in some 148 | ;; TMS control flows. 149 | ;; See https://github.com/cerner/clara-rules/pull/145 for more context. 150 | 151 | {:rules [r [[[:or 152 | [First] 153 | [Second]] 154 | [?ts <- (acc/all) :from [Temperature]]] 155 | (insert! (with-meta {:ts ?ts} 156 | {:type :holder}))]] 157 | 158 | :queries [q [[] 159 | [[?h <- :holder]]]] 160 | 161 | :sessions [s [r q] {}]} 162 | 163 | (let [;; Vary the insertion order to ensure that the outcomes are the same. 164 | ;; This insertion order will cause retractions to need to be propagated 165 | ;; to the RHS production node that is shared by the nested conditions 166 | ;; of the disjunction. 167 | qres1 (-> s 168 | (insert (->First)) 169 | (insert (->Temperature 1 "MCI")) 170 | (insert (->Second)) 171 | (insert (->Temperature 2 "MCI")) 172 | fire-rules 173 | (query q) 174 | set) 175 | qres2 (-> s 176 | (insert (->First)) 177 | (insert (->Temperature 1 "MCI")) 178 | (insert (->Temperature 2 "MCI")) 179 | (insert (->Second)) 180 | fire-rules 181 | (query q) 182 | set)] 183 | (is (= qres1 qres2)))) 184 | 185 | (def-rules-test test-force-multiple-transient-transitions-activation-memory 186 | ;; The objective of this test is to verify that activation memory works 187 | ;; properly after going through persistent/transient shifts, including shifts 188 | ;; that empty it (i.e. firing the rules.) 189 | 190 | {:rules [rule [[[ColdAndWindy]] 191 | (insert! (->Cold 10))]] 192 | 193 | :queries [cold-query [[] 194 | [[Cold (= ?t temperature)]]]] 195 | 196 | :sessions [empty-session [rule cold-query] {}]} 197 | 198 | (let [windy-fact (->ColdAndWindy 20 20)] 199 | 200 | (is (= (-> empty-session 201 | (insert windy-fact) 202 | (insert windy-fact) 203 | fire-rules 204 | (query cold-query)) 205 | [{:?t 10} {:?t 10}]) 206 | "Make two insert calls forcing the memory to go through a persistent/transient transition 207 | in between insert calls.") 208 | 209 | (is (= (-> empty-session 210 | (insert windy-fact) 211 | (insert windy-fact) 212 | fire-rules 213 | (insert windy-fact) 214 | (insert windy-fact) 215 | fire-rules 216 | (query cold-query)) 217 | (repeat 4 {:?t 10})) 218 | "Validate that we can still go through a persistent/transient transition in the memory 219 | after firing rules causes the activation memory to be emptied."))) 220 | -------------------------------------------------------------------------------- /src/test/common/clara/test_performance_optimizations.cljc: -------------------------------------------------------------------------------- 1 | ;; These tests validate that operations that the rules engine should optimize 2 | ;; away are in fact optimized away. The target here is not the actual execution time, 3 | ;; which will vary per system, but verification that the action operations in question are not performed. 4 | #?(:clj 5 | (ns clara.test-performance-optimizations 6 | (:require [clara.tools.testing-utils :refer [def-rules-test 7 | side-effect-holder] :as tu] 8 | [clara.rules :refer [fire-rules 9 | insert 10 | insert! 11 | query]] 12 | 13 | [clara.rules.testfacts :refer [->Cold ->ColdAndWindy]] 14 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 15 | [clara.rules.accumulators] 16 | [schema.test :as st]) 17 | (:import [clara.rules.testfacts 18 | Cold 19 | ColdAndWindy])) 20 | 21 | :cljs 22 | (ns clara.test-performance-optimizations 23 | (:require [clara.rules :refer [fire-rules 24 | insert 25 | insert! 26 | query]] 27 | [clara.rules.testfacts :refer [->Cold Cold 28 | ->ColdAndWindy ColdAndWindy]] 29 | [clara.rules.accumulators] 30 | [cljs.test] 31 | [schema.test :as st] 32 | [clara.tools.testing-utils :refer [side-effect-holder] :as tu]) 33 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 34 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 35 | 36 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 37 | (use-fixtures :each tu/side-effect-holder-fixture) 38 | 39 | #?(:clj 40 | (defmacro true-if-binding-absent 41 | [] 42 | (not (contains? &env '?unused-binding)))) 43 | 44 | ;; See issue https://github.com/cerner/clara-rules/issues/383 45 | ;; This validates that we don't create let bindings for binding 46 | ;; variables that aren't used. Doing so both imposes runtime costs 47 | ;; and increases the size of the generated code that must be evaluated. 48 | (def-rules-test test-unused-rhs-binding-not-bound 49 | 50 | {:rules [cold-windy-rule [[[ColdAndWindy (= ?used-binding temperature) (= ?unused-binding windspeed)]] 51 | (when (true-if-binding-absent) 52 | (insert! (->Cold ?used-binding)))]] 53 | 54 | :queries [cold-query [[] [[Cold (= ?c temperature)]]]] 55 | 56 | :sessions [empty-session [cold-windy-rule cold-query] {}]} 57 | 58 | (is (= [{:?c 0}] 59 | (-> empty-session 60 | (insert (->ColdAndWindy 0 0)) 61 | fire-rules 62 | (query cold-query))))) 63 | -------------------------------------------------------------------------------- /src/test/common/clara/test_queries.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.test-queries 3 | (:require [clara.tools.testing-utils :refer [def-rules-test 4 | side-effect-holder] :as tu] 5 | [clara.rules :refer [fire-rules 6 | insert 7 | query]] 8 | 9 | [clara.rules.testfacts :refer [->Temperature]] 10 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 11 | [schema.test :as st]) 12 | (:import [clara.rules.testfacts 13 | Temperature])) 14 | 15 | :cljs 16 | (ns clara.test-queries 17 | (:require [clara.rules :refer [fire-rules 18 | insert 19 | query]] 20 | [clara.rules.testfacts :refer [->Temperature Temperature]] 21 | [cljs.test] 22 | [schema.test :as st]) 23 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 24 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 25 | 26 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 27 | 28 | (def-rules-test test-simple-query 29 | 30 | {:queries [cold-query [[] 31 | [[Temperature (< temperature 20) (= ?t temperature)]]]] 32 | 33 | :sessions [empty-session [cold-query] {}]} 34 | 35 | (let [session (-> empty-session 36 | (insert (->Temperature 15 "MCI")) 37 | (insert (->Temperature 10 "MCI")) 38 | (insert (->Temperature 80 "MCI")) 39 | fire-rules)] 40 | 41 | ;; The query should identify all items that were inserted and matchd the 42 | ;; expected criteria. 43 | (is (= #{{:?t 15} {:?t 10}} 44 | (set (query session cold-query)))))) 45 | 46 | (def-rules-test test-param-query 47 | 48 | {:queries [cold-query [[:?l] 49 | [[Temperature (< temperature 50) 50 | (= ?t temperature) 51 | (= ?l location)]]]] 52 | 53 | :sessions [empty-session [cold-query] {}]} 54 | 55 | (let [session (-> empty-session 56 | (insert (->Temperature 15 "MCI")) 57 | (insert (->Temperature 20 "MCI")) ; Test multiple items in result. 58 | (insert (->Temperature 10 "ORD")) 59 | (insert (->Temperature 35 "BOS")) 60 | (insert (->Temperature 80 "BOS")) 61 | fire-rules)] 62 | 63 | ;; Query by location. 64 | (is (= #{{:?l "BOS" :?t 35}} 65 | (set (query session cold-query :?l "BOS")))) 66 | 67 | (is (= #{{:?l "MCI" :?t 15} {:?l "MCI" :?t 20}} 68 | (set (query session cold-query :?l "MCI")))) 69 | 70 | (is (= #{{:?l "ORD" :?t 10}} 71 | (set (query session cold-query :?l "ORD")))))) 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/test/common/clara/test_rhs_retract.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.test-rhs-retract 3 | (:require [clara.tools.testing-utils :refer [def-rules-test] :as tu] 4 | [clara.rules :refer [fire-rules 5 | insert 6 | insert-all 7 | insert! 8 | retract 9 | query 10 | retract!]] 11 | 12 | [clara.rules.testfacts :refer [->Temperature ->Cold]] 13 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 14 | [clara.rules.accumulators] 15 | [schema.test :as st]) 16 | (:import [clara.rules.testfacts 17 | Temperature 18 | Cold])) 19 | 20 | :cljs 21 | (ns clara.test-rhs-retract 22 | (:require [clara.rules :refer [fire-rules 23 | insert 24 | insert! 25 | insert-all 26 | retract 27 | query 28 | retract!]] 29 | [clara.rules.testfacts :refer [->Temperature Temperature 30 | ->Cold Cold]] 31 | [clara.rules.accumulators] 32 | [cljs.test] 33 | [schema.test :as st]) 34 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 35 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 36 | 37 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 38 | 39 | (def-rules-test test-retract! 40 | 41 | {:rules [not-cold-rule [[[Temperature (> temperature 50)]] 42 | (retract! (->Cold 20))]] 43 | 44 | :queries [cold-query [[] 45 | [[Cold (= ?t temperature)]]]] 46 | 47 | :sessions [empty-session [not-cold-rule cold-query] {}]} 48 | 49 | (let [session (-> empty-session 50 | (insert (->Cold 20)) 51 | (fire-rules))] 52 | 53 | ;; The session should contain our initial cold reading. 54 | (is (= #{{:?t 20}} 55 | (set (query session cold-query)))) 56 | 57 | ;; Insert a higher temperature and ensure the cold fact was retracted. 58 | (is (= #{} 59 | (set (query (-> session 60 | (insert (->Temperature 80 "MCI")) 61 | (fire-rules)) 62 | cold-query)))))) 63 | -------------------------------------------------------------------------------- /src/test/common/clara/test_rules_require.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.test-rules-require 2 | (:require [clara.tools.testing-utils :as tu] 3 | [clara.rule-defs :as rd] 4 | [clara.rules.testfacts :as facts] 5 | #?(:clj [clojure.test :refer [is deftest run-tests testing use-fixtures]]) 6 | #?(:cljs [cljs.test :refer-macros [is deftest run-tests testing use-fixtures]]) 7 | #?(:clj [clara.rules :refer [insert fire-rules query defsession]]) 8 | #?(:cljs [clara.rules :refer [insert fire-rules query] :refer-macros [defsession]]))) 9 | 10 | ;; Tests the case where rules/facts are required from a different namespace where the session is defined, 11 | ;; without an explicit :refer. 12 | ;; See https://github.com/cerner/clara-rules/issues/359 13 | 14 | (use-fixtures :each tu/side-effect-holder-fixture) 15 | 16 | (defsession my-session 'clara.rule-defs) 17 | 18 | (deftest test-simple-defrule 19 | (let [session (insert my-session (facts/->Temperature 10 "MCI"))] 20 | (fire-rules session) 21 | (is (= @tu/side-effect-holder (facts/->Temperature 10 "MCI"))))) 22 | 23 | (deftest test-simple-query 24 | (let [session (-> my-session 25 | (insert (facts/->Temperature 15 "MCI")) 26 | (insert (facts/->Temperature 10 "MCI")) 27 | (insert (facts/->Temperature 80 "MCI")) 28 | fire-rules)] 29 | 30 | ;; The query should identify all items that were inserted and matched the 31 | ;; expected criteria. 32 | (is (= #{{:?t 15} {:?t 10}} 33 | (set (query session rd/cold-query)))))) 34 | 35 | (deftest test-simple-accumulator 36 | (let [session (-> my-session 37 | (insert (facts/->Temperature 15 "MCI")) 38 | (insert (facts/->Temperature 10 "MCI")) 39 | (insert (facts/->Temperature 80 "MCI")) 40 | fire-rules)] 41 | 42 | ;; Accumulator returns the lowest value. 43 | (is (= #{{:?t 10}} 44 | (set (query session rd/coldest-query)))))) 45 | 46 | (deftest test-simple-insert 47 | (let [session (-> my-session 48 | (insert (facts/->Temperature 15 "MCI")) 49 | (insert (facts/->WindSpeed 45 "MCI")) 50 | (fire-rules))] 51 | 52 | (is (= #{{:?fact (facts/->ColdAndWindy 15 45)}} 53 | (set 54 | (query session rd/find-cold-and-windy)))))) 55 | 56 | -------------------------------------------------------------------------------- /src/test/common/clara/test_simple_rules.cljc: -------------------------------------------------------------------------------- 1 | ;; These are tests that validate elementary function of the rules engine like 2 | ;; inserting and retracting facts. They don't fit very well with specialized 3 | ;; test namespaces due to this simplicity. This functionality is transitively 4 | ;; tested by numerous other tests, but there is some value in having direct tests 5 | ;; in case the complexity of those tests obscured a simpler issue. 6 | #?(:clj 7 | (ns clara.test-simple-rules 8 | (:require [clara.tools.testing-utils :refer [def-rules-test 9 | side-effect-holder] :as tu] 10 | [clara.rules :refer [fire-rules 11 | insert 12 | insert-all 13 | insert-all! 14 | insert! 15 | retract 16 | query]] 17 | 18 | [clara.rules.testfacts :refer [->Temperature ->Cold ->WindSpeed 19 | ->ColdAndWindy ->LousyWeather]] 20 | [clojure.test :refer [is deftest run-tests testing use-fixtures]] 21 | [clara.rules.accumulators] 22 | [schema.test :as st]) 23 | (:import [clara.rules.testfacts 24 | Temperature 25 | Cold 26 | WindSpeed 27 | ColdAndWindy 28 | LousyWeather])) 29 | 30 | :cljs 31 | (ns clara.test-simple-rules 32 | (:require [clara.rules :refer [fire-rules 33 | insert 34 | insert! 35 | insert-all 36 | insert-all! 37 | retract 38 | query]] 39 | [clara.rules.testfacts :refer [->Temperature Temperature 40 | ->Cold Cold 41 | ->WindSpeed WindSpeed 42 | ->ColdAndWindy ColdAndWindy 43 | ->LousyWeather LousyWeather]] 44 | [clara.rules.accumulators] 45 | [cljs.test] 46 | [schema.test :as st] 47 | [clara.tools.testing-utils :refer [side-effect-holder] :as tu]) 48 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 49 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 50 | 51 | (use-fixtures :once st/validate-schemas #?(:clj tu/opts-fixture)) 52 | (use-fixtures :each tu/side-effect-holder-fixture) 53 | 54 | (defn- has-fact? [token fact] 55 | (some #{fact} (map first (:matches token)))) 56 | 57 | (def-rules-test test-simple-rule 58 | 59 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 60 | (reset! side-effect-holder ?__token__)]] 61 | 62 | :sessions [empty-session [cold-rule] {}]} 63 | 64 | (-> empty-session 65 | (insert (->Temperature 10 "MCI")) 66 | (fire-rules)) 67 | 68 | (is (has-fact? @side-effect-holder (->Temperature 10 "MCI")))) 69 | 70 | (def-rules-test test-simple-insert 71 | 72 | {:rules [cold-rule [[[Temperature (< temperature 20) (= ?t temperature)]] 73 | (insert! (->Cold ?t))]] 74 | 75 | :queries [cold-query [[] [[Cold (= ?c temperature)]]]] 76 | 77 | :sessions [empty-session [cold-rule cold-query] {}]} 78 | 79 | (let [session (-> empty-session 80 | (insert (->Temperature 10 "MCI")) 81 | (fire-rules))] 82 | 83 | (is (= #{{:?c 10}} 84 | (set (query session cold-query)))))) 85 | 86 | (def-rules-test test-simple-insert-all 87 | 88 | {:rules [cold-lousy-rule [[[Temperature (< temperature 20) (= ?t temperature)]] 89 | (insert-all! [(->Cold ?t) (->LousyWeather)])]] 90 | 91 | :queries [cold-lousy-query [[] [[Cold (= ?c temperature)] 92 | [LousyWeather]]]] 93 | 94 | :sessions [empty-session [cold-lousy-rule cold-lousy-query] {}]} 95 | 96 | (let [session (-> empty-session 97 | (insert (->Temperature 10 "MCI")) 98 | (fire-rules))] 99 | 100 | (is (= #{{:?c 10}} 101 | (set (query session cold-lousy-query)))))) 102 | 103 | (def-rules-test test-multiple-condition-rule 104 | 105 | {:rules [cold-windy-rule [[[Temperature (< temperature 20)] 106 | [WindSpeed (> windspeed 25)]] 107 | (reset! side-effect-holder ?__token__)]] 108 | 109 | :sessions [empty-session [cold-windy-rule] {}]} 110 | 111 | (let [session (-> empty-session 112 | (insert (->WindSpeed 30 "MCI")) 113 | (insert (->Temperature 10 "MCI")) 114 | fire-rules)] 115 | 116 | (is (has-fact? @side-effect-holder (->WindSpeed 30 "MCI"))) 117 | (is (has-fact? @side-effect-holder (->Temperature 10 "MCI"))))) 118 | 119 | (def-rules-test test-simple-retraction 120 | 121 | {:queries [cold-query [[] [[Temperature (< temperature 20) (= ?t temperature)]]]] 122 | 123 | :sessions [empty-session [cold-query] {}]} 124 | 125 | (let [temp (->Temperature 10 "MCI") 126 | 127 | session (-> empty-session 128 | (insert temp) 129 | fire-rules) 130 | 131 | retracted-session (-> session 132 | (retract temp) 133 | fire-rules)] 134 | 135 | ;; Ensure the item is there as expected. 136 | (is (= #{{:?t 10}} 137 | (set (query session cold-query)))) 138 | 139 | ;; Ensure the item is retracted as expected. 140 | (is (= #{} 141 | (set (query retracted-session cold-query)))))) 142 | 143 | (def-rules-test test-noop-retraction 144 | 145 | {:queries [cold-query [[] [[Temperature (< temperature 20) (= ?t temperature)]]]] 146 | 147 | :sessions [empty-session [cold-query] {}]} 148 | 149 | (let [session (-> empty-session 150 | (insert (->Temperature 10 "MCI")) 151 | ;; Ensure retracting a nonexistent item has no ill effects. 152 | (retract (->Temperature 15 "MCI")) 153 | fire-rules)] 154 | 155 | (is (= #{{:?t 10}} 156 | (set (query session cold-query)))))) 157 | 158 | (def-rules-test test-multiple-simple-rules 159 | 160 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 161 | (swap! side-effect-holder assoc :cold ?__token__)] 162 | 163 | windy-rule [[[WindSpeed (> windspeed 25)]] 164 | (swap! side-effect-holder assoc :windy ?__token__)]] 165 | 166 | :sessions [empty-session [cold-rule windy-rule] {}]} 167 | 168 | (reset! side-effect-holder {}) 169 | 170 | (let [session (-> empty-session 171 | (insert (->WindSpeed 30 "MCI")) 172 | (insert (->Temperature 10 "MCI")) 173 | fire-rules)] 174 | 175 | ;; Check rule side effects contin the expected token. 176 | (is (has-fact? (:cold @side-effect-holder) (->Temperature 10 "MCI"))) 177 | 178 | (is (has-fact? (:windy @side-effect-holder) (->WindSpeed 30 "MCI"))))) 179 | 180 | (def-rules-test test-multiple-rules-same-fact 181 | 182 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 183 | (swap! side-effect-holder assoc :cold ?__token__)] 184 | 185 | subzero-rule [[[Temperature (< temperature 0)]] 186 | (swap! side-effect-holder assoc :subzero ?__token__)]] 187 | 188 | :sessions [empty-session [cold-rule subzero-rule] {}]} 189 | 190 | (let [session (-> empty-session 191 | (insert (->Temperature -10 "MCI")) 192 | fire-rules)] 193 | 194 | (is (has-fact? (:cold @side-effect-holder) (->Temperature -10 "MCI") )) 195 | 196 | (is (has-fact? (:subzero @side-effect-holder) (->Temperature -10 "MCI"))))) 197 | 198 | (def-rules-test test-query-failure-when-provided-invalid-parameters 199 | 200 | {:queries [temp-query [[:?t] [[Temperature (= ?t temperature)]]]] 201 | 202 | :sessions [empty-session [temp-query] {}]} 203 | 204 | (let [session (-> empty-session 205 | (insert (->Temperature 10 "MCI")) 206 | ;; Ensure retracting a nonexistent item has no ill effects. 207 | (retract (->Temperature 15 "MCI")) 208 | fire-rules) 209 | 210 | expected-msg #"was not provided with the correct parameters"] 211 | 212 | ;; passivity test 213 | (is (= #{{:?t 10}} 214 | (set (query session temp-query :?t 10)))) 215 | 216 | (is (thrown-with-msg? #?(:clj IllegalArgumentException :cljs js/Error) 217 | expected-msg 218 | (query session temp-query :?another-param 42))) 219 | 220 | (is (thrown-with-msg? #?(:clj IllegalArgumentException :cljs js/Error) 221 | expected-msg 222 | (query session temp-query))) 223 | 224 | (is (thrown-with-msg? #?(:clj IllegalArgumentException :cljs js/Error) 225 | expected-msg 226 | (query session temp-query :?t 42 :?another-param 42))))) 227 | -------------------------------------------------------------------------------- /src/test/common/clara/test_testing_utils.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.test-testing-utils 3 | (:require [clara.tools.testing-utils :refer [def-rules-test 4 | run-performance-test]] 5 | [clara.rules :as r] 6 | 7 | [clara.rules.testfacts :refer [->Temperature ->Cold]] 8 | [clojure.test :refer [is deftest run-tests] :as t]) 9 | (:import [clara.rules.testfacts 10 | Temperature 11 | Cold])) 12 | 13 | :cljs 14 | (ns clara.test-testing-utils 15 | (:require [clara.rules :as r] 16 | [clara.rules.testfacts :refer [->Temperature Temperature 17 | ->Cold Cold]] 18 | [cljs.test :as t] 19 | [clara.tools.testing-utils :refer [run-performance-test]]) 20 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 21 | [cljs.test :refer (is deftest run-tests)]))) 22 | 23 | (def test-ran-atom (atom false)) 24 | 25 | ;; This test fixture validates that def-rules-test actually executed the test bodies it 26 | ;; is provided. If the test bodies were not executed test-ran-atom would have a value of false 27 | ;; after test execution. 28 | (t/use-fixtures :once (fn [t] 29 | (reset! test-ran-atom false) 30 | (t) 31 | (is (true? @test-ran-atom)))) 32 | 33 | (def-rules-test basic-tests 34 | {:rules [rule1 [[[?t <- Temperature (< temperature 0)]] 35 | (r/insert! (->Cold (:temperature ?t)))]] 36 | 37 | :queries [query1 [[] 38 | [[Cold (= ?t temperature)]]]] 39 | 40 | :sessions [session1 [rule1 query1] {} 41 | session2 [rule1 query1] {:fact-type-fn (fn [fact] :bogus)}]} 42 | 43 | (reset! test-ran-atom true) 44 | (is (= [{:?t -50}] 45 | (-> session1 46 | (r/insert (->Temperature -50 "MCI")) 47 | r/fire-rules 48 | (r/query query1)))) 49 | 50 | ;; Since we validate later (outside the scope of this test) that the state 51 | ;; change occurred put it in the middle so that it would fail if we took either 52 | ;; the first or last test form, rather than all test forms. 53 | (reset! test-ran-atom true) 54 | 55 | (is (empty? (-> session2 56 | (r/insert (->Temperature -50 "MCI")) 57 | r/fire-rules 58 | (r/query query1))))) 59 | 60 | (def fire-rules-counter (atom 0)) 61 | 62 | (def-rules-test test-performance-test 63 | {:rules [rule1 [[[?t <- Temperature (< temperature 0)]] 64 | (swap! fire-rules-counter inc)]] 65 | :queries [] 66 | :sessions [session1 [rule1] {}]} 67 | (run-performance-test {:description "Simple fire-rules demonstration" 68 | :func #(-> session1 69 | (r/insert (->Temperature -50 "MCI")) 70 | r/fire-rules) 71 | :iterations 5 72 | :mean-assertion (partial > 500)}) 73 | (is (= @fire-rules-counter 5))) 74 | -------------------------------------------------------------------------------- /src/test/common/clara/tools/test_fact_graph.cljc: -------------------------------------------------------------------------------- 1 | (ns clara.tools.test-fact-graph 2 | (:require [clara.tools.testing-utils :as tu] 3 | [clara.tools.fact-graph :as g] 4 | [clara.rules :as cr] 5 | [clara.rules.accumulators :as acc] 6 | [schema.test :as st] 7 | #?(:clj [clojure.test :refer [is deftest run-tests testing use-fixtures]] 8 | :cljs [cljs.test :refer-macros [is deftest run-tests testing use-fixtures]]) 9 | #?(:clj [clara.rules.testfacts :as tf] 10 | :cljs [clara.rules.testfacts :refer [Cold WindSpeed ColdAndWindy] :as tf])) 11 | #?(:clj 12 | (:import [clara.rules.testfacts Cold WindSpeed ColdAndWindy]))) 13 | 14 | (use-fixtures :once st/validate-schemas) 15 | 16 | (tu/def-rules-test test-basic-fact-graph 17 | {:rules [cold-windy-rule [[[Cold (= ?t temperature) (< ?t 0)] 18 | [WindSpeed (= ?w windspeed) (> ?w 30)]] 19 | (cr/insert! (tf/->ColdAndWindy ?t ?w))]] 20 | :sessions [empty-session [cold-windy-rule] {:cache false}]} 21 | (let [actual-graph (-> empty-session 22 | (cr/insert (tf/->Cold -10) 23 | (tf/->WindSpeed 50 "MCI")) 24 | cr/fire-rules 25 | g/session->fact-graph) 26 | 27 | expected-graph (let [act-node (g/map->RuleActivationNode {:rule-name "cold-windy-rule" 28 | :id 1})] 29 | {:forward-edges 30 | {(tf/->Cold -10) #{act-node} 31 | (tf/->WindSpeed 50 "MCI") #{act-node} 32 | act-node #{(tf/->ColdAndWindy -10 50)}} 33 | :backward-edges 34 | {act-node #{(tf/->Cold -10) 35 | (tf/->WindSpeed 50 "MCI")} 36 | (tf/->ColdAndWindy -10 50) #{act-node}}})] 37 | 38 | (is (= expected-graph actual-graph)))) 39 | 40 | (tu/def-rules-test test-accum-rule-fact-graph 41 | {:rules [cold-windy-rule [[[?t <- (acc/min :temperature) :from [Cold]] 42 | [WindSpeed (= ?w windspeed) (> ?w 30)]] 43 | (cr/insert! (tf/->ColdAndWindy ?t ?w))]] 44 | :sessions [empty-session [cold-windy-rule] {:cache false}]} 45 | (let [actual-graph (-> empty-session 46 | (cr/insert (tf/->Cold -10) 47 | (tf/->Cold 10) 48 | (tf/->WindSpeed 50 "MCI")) 49 | cr/fire-rules 50 | g/session->fact-graph) 51 | 52 | expected-graph (let [act-node (g/map->RuleActivationNode {:rule-name "cold-windy-rule" 53 | :id 1}) 54 | accum-node (g/map->AccumulationNode {:id 2}) 55 | accum-result-node (g/map->AccumulationResultNode {:id 3 56 | :result -10})] 57 | {:forward-edges 58 | {(tf/->Cold -10) #{accum-node} 59 | (tf/->Cold 10) #{accum-node} 60 | accum-node #{accum-result-node} 61 | accum-result-node #{act-node} 62 | (tf/->WindSpeed 50 "MCI") #{act-node} 63 | act-node #{(tf/->ColdAndWindy -10 50)}} 64 | :backward-edges 65 | {accum-node #{(tf/->Cold -10) (tf/->Cold 10)} 66 | accum-result-node #{accum-node} 67 | act-node #{(tf/->WindSpeed 50 "MCI") 68 | accum-result-node} 69 | (tf/->ColdAndWindy -10 50) #{act-node}}})] 70 | 71 | (is (= expected-graph actual-graph)))) 72 | 73 | (tu/def-rules-test test-fact-duplicates 74 | {:rules [b-rule [[[?a <- "a"]] 75 | (cr/insert! "b")] 76 | c-rule [[[?b <- "b"]] 77 | (cr/insert! "c")]] 78 | :sessions [empty-session [b-rule c-rule] {:cache false :fact-type-fn identity}]} 79 | (let [actual-graph (-> empty-session 80 | (cr/insert "a" "a") 81 | cr/fire-rules 82 | g/session->fact-graph) 83 | 84 | expected-graph (let [rule1-node1 (g/map->RuleActivationNode {:rule-name "b-rule" 85 | :id 1}) 86 | rule1-node2 (g/map->RuleActivationNode {:rule-name "b-rule" 87 | :id 2}) 88 | rule2-node1 (g/map->RuleActivationNode {:rule-name "c-rule" 89 | :id 3}) 90 | rule2-node2 (g/map->RuleActivationNode {:rule-name "c-rule" 91 | :id 4})] 92 | {:forward-edges {"a" #{rule1-node1 rule1-node2} 93 | rule1-node1 #{"b"} 94 | rule1-node2 #{"b"} 95 | "b" #{rule2-node1 rule2-node2} 96 | rule2-node1 #{"c"} 97 | rule2-node2 #{"c"}} 98 | :backward-edges {rule1-node1 #{"a"} 99 | rule1-node2 #{"a"} 100 | "b" #{rule1-node1 rule1-node2} 101 | rule2-node1 #{"b"} 102 | rule2-node2 #{"b"} 103 | "c" #{rule2-node1 rule2-node2}}})] 104 | 105 | (is (= actual-graph expected-graph)))) 106 | 107 | (tu/def-rules-test test-duplicate-fact-from-different-rules 108 | {:rules [a-rule [[[?a <- "a"]] 109 | (cr/insert! "c") 110 | {:salience 1}] 111 | b-rule [[[?b <- "b"]] 112 | (cr/insert! "c")]] 113 | :sessions [empty-session [a-rule b-rule] {:cache false :fact-type-fn identity}]} 114 | (let [actual-graph (-> empty-session 115 | (cr/insert "a" "b") 116 | cr/fire-rules 117 | g/session->fact-graph) 118 | 119 | expected-graph (let [rule1-node (g/map->RuleActivationNode {:rule-name "a-rule" 120 | :id 1}) 121 | rule2-node (g/map->RuleActivationNode {:rule-name "b-rule" 122 | :id 2})] 123 | 124 | {:forward-edges {"a" #{rule1-node} 125 | "b" #{rule2-node} 126 | rule1-node #{"c"} 127 | rule2-node #{"c"}} 128 | :backward-edges {rule1-node #{"a"} 129 | rule2-node #{"b"} 130 | "c" #{rule1-node rule2-node}}})] 131 | 132 | (is (= expected-graph actual-graph)))) 133 | -------------------------------------------------------------------------------- /src/test/common/clara/tools/test_tracing.cljc: -------------------------------------------------------------------------------- 1 | #?(:clj 2 | (ns clara.tools.test-tracing 3 | (:require [clara.tools.testing-utils :refer [def-rules-test] :as tu] 4 | [clara.rules :refer [fire-rules 5 | insert 6 | insert-all 7 | insert! 8 | retract 9 | retract! 10 | query]] 11 | [clara.tools.tracing :as t] 12 | [clara.rules.accumulators :as acc] 13 | [clara.rules.testfacts :refer :all] 14 | [clojure.test :refer :all]) 15 | (:import [clara.rules.testfacts Temperature WindSpeed Cold Hot TemperatureHistory 16 | ColdAndWindy LousyWeather First Second Third Fourth])) 17 | 18 | :cljs 19 | (ns clara.tools.test-tracing 20 | (:require [clara.rules :refer [fire-rules 21 | insert 22 | insert! 23 | retract! 24 | insert-all 25 | retract 26 | query]] 27 | [clara.tools.tracing :as t] 28 | [clara.rules.accumulators :as acc] 29 | [clara.rules.testfacts :refer [->Temperature Temperature 30 | ->TemperatureHistory TemperatureHistory 31 | ->Hot Hot 32 | ->Cold Cold 33 | ->ColdAndWindy ColdAndWindy 34 | ->WindSpeed WindSpeed]]) 35 | (:require-macros [clara.tools.testing-utils :refer [def-rules-test]] 36 | [cljs.test :refer [is deftest run-tests testing use-fixtures]]))) 37 | 38 | (def-rules-test test-tracing-toggle 39 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 40 | (println "It's cold!")]] 41 | 42 | :sessions [session [cold-rule] {}]} 43 | 44 | (is (= false (t/is-tracing? session))) 45 | 46 | (is (= true (t/is-tracing? (-> session 47 | (t/with-tracing))))) 48 | 49 | (is (= false (t/is-tracing? (-> session 50 | (t/with-tracing) 51 | (t/without-tracing)))))) 52 | 53 | (def rule-output (atom nil)) 54 | (def-rules-test test-simple-trace 55 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 56 | (reset! rule-output ?__token__)]] 57 | :sessions [empty-session [cold-rule] {}]} 58 | 59 | (let [session (-> empty-session 60 | (t/with-tracing) 61 | (insert (->Temperature 10 "MCI")) 62 | (fire-rules))] 63 | 64 | ;; Ensure expected events occur in order. 65 | (is (= [:add-facts :alpha-activate :right-activate :left-activate :add-activations :fire-activation] 66 | (map :type (t/get-trace session)))))) 67 | 68 | (def-rules-test test-rhs-retraction-trace 69 | {:rules [cold-rule [[[Temperature (< temperature 20)]] 70 | (retract! (->Hot :too-hot))]] 71 | :queries [hot-query [[] 72 | [[?hot <- Hot]]]] 73 | :sessions [empty-session [cold-rule hot-query] {}]} 74 | 75 | (let [session (-> empty-session 76 | (t/with-tracing) 77 | (insert (->Hot :too-hot)) 78 | (fire-rules) 79 | (insert (->Temperature 10 "MCI")) 80 | (fire-rules))] 81 | (is (= (map :type (t/get-trace session)) 82 | [:add-facts :alpha-activate :right-activate :left-activate 83 | :add-facts :alpha-activate :right-activate :left-activate 84 | :add-activations :fire-activation :retract-facts 85 | :alpha-retract :right-retract :left-retract]) 86 | "Validate that a retract! call in the RHS side of a rule appears in the trace 87 | before the :right-retract"))) 88 | 89 | (def-rules-test test-accumulate-trace 90 | {:queries [coldest-query [[] 91 | [[?t <- (acc/min :temperature :returns-fact true) from [Temperature]]]]] 92 | :sessions [empty-session [coldest-query] {}]} 93 | 94 | (let [session (-> empty-session 95 | (t/with-tracing) 96 | (insert (->Temperature 15 "MCI")) 97 | (insert (->Temperature 10 "MCI")) 98 | (insert (->Temperature 80 "MCI")) 99 | fire-rules)] 100 | 101 | (is (= [:add-facts :alpha-activate :right-activate :accum-reduced 102 | :left-activate :add-facts :alpha-activate :right-activate 103 | :accum-reduced :left-retract :left-activate :add-facts 104 | :alpha-activate :right-activate :accum-reduced] 105 | 106 | (map :type (t/get-trace session)))))) 107 | 108 | (def-rules-test remove-accum-reduced 109 | {:queries [all-temps [[] 110 | [[?t <- (acc/all) from [Temperature]]]]] 111 | :sessions [empty-session [all-temps] {}]} 112 | 113 | (let [session (-> empty-session 114 | (t/with-tracing) 115 | fire-rules 116 | (insert (->Temperature 15 "MCI")) 117 | fire-rules)] 118 | 119 | (is (= [:add-facts :alpha-activate :right-activate :accum-reduced :left-retract :left-activate] 120 | 121 | (map :type (t/get-trace session)))))) 122 | 123 | (def-rules-test test-insert-trace 124 | {:rules [cold-rule [[[Temperature (= ?temperature temperature) (< temperature 20)]] 125 | (insert! (->Cold ?temperature))]] 126 | :sessions [empty-session [cold-rule] {}]} 127 | 128 | (let [session (-> empty-session 129 | (t/with-tracing) 130 | (insert (->Temperature 10 "MCI")) 131 | (fire-rules))] 132 | 133 | ;; Ensure expected events occur in order. 134 | (is (= [:add-facts :alpha-activate :right-activate :left-activate 135 | :add-activations :fire-activation :add-facts-logical :activation-group-transition] 136 | (map :type (t/get-trace session)))))) 137 | 138 | (def-rules-test test-insert-and-retract-trace 139 | {:rules [cold-rule [[[Temperature (= ?temperature temperature) (< temperature 20)]] 140 | (insert! (->Cold ?temperature))]] 141 | :sessions [empty-session [cold-rule] {:cache false}]} 142 | 143 | (let [session (-> empty-session 144 | (t/with-tracing) 145 | (insert (->Temperature 10 "MCI") 146 | (->Temperature 20 "MCI")) 147 | (fire-rules) 148 | (retract (->Temperature 10 "MCI")) 149 | (fire-rules)) 150 | 151 | session-trace (t/get-trace session)] 152 | 153 | ;; Ensure expected events occur in order. 154 | (is (= [:add-facts :alpha-activate :right-activate :left-activate :add-activations :fire-activation 155 | :add-facts-logical :activation-group-transition :retract-facts :alpha-retract :right-retract 156 | :left-retract :remove-activations :retract-facts-logical] 157 | (map :type session-trace))) 158 | 159 | ;; Ensure only the expected fact was indicated as retracted. 160 | (let [retraction (first (filter #(= :retract-facts-logical (:type %)) session-trace))] 161 | (is (= [(->Cold 10)] (:facts retraction)))))) 162 | 163 | (def-rules-test test-ranked-productions 164 | {:rules [temperature-rule [[[Temperature (= ?temperature temperature) (< temperature 20)]] 165 | (insert! (->Cold ?temperature))] 166 | 167 | cold-and-windy-rule [[[ColdAndWindy (= ?temperature temperature) (< temperature 20)]] 168 | (insert! (->Cold ?temperature))]] 169 | 170 | :sessions [empty-session [temperature-rule cold-and-windy-rule] {}]} 171 | 172 | (let [mostly-temp (-> empty-session 173 | t/with-tracing 174 | (insert (->Temperature 10 "MCI")) 175 | fire-rules 176 | (insert (->Temperature 10 "MCI")) 177 | fire-rules 178 | (insert (->ColdAndWindy 10 10)) 179 | fire-rules) 180 | 181 | mostly-temp-counts (t/ranked-productions mostly-temp) 182 | 183 | mostly-cold-and-windy (-> empty-session 184 | t/with-tracing 185 | (insert (->Temperature 10 "MCI")) 186 | fire-rules 187 | (insert (->ColdAndWindy 10 10)) 188 | fire-rules 189 | (insert (->ColdAndWindy 10 10)) 190 | fire-rules) 191 | 192 | mostly-cold-and-windy-counts (t/ranked-productions mostly-cold-and-windy)] 193 | 194 | (is (= (keys mostly-temp-counts) 195 | ["temperature-rule" "cold-and-windy-rule"])) 196 | 197 | (is (= (keys mostly-cold-and-windy-counts) 198 | ["cold-and-windy-rule" "temperature-rule"])) 199 | 200 | ;; Since the exact number of interactions is subject to change based on Clara's internal implementation details 201 | ;; we instead test the ratio of interaction counts instead, which should be more stable. The pattern above 202 | ;; of inserting and firing a single fact at a time prevents fact batching from impacting these ratios. 203 | (is (= (mostly-temp-counts "temperature-rule") 204 | (* 2 (mostly-temp-counts "cold-and-windy-rule")))) 205 | 206 | (is (= (mostly-cold-and-windy-counts "cold-and-windy-rule") 207 | (* 2 (mostly-cold-and-windy-counts "temperature-rule")))))) 208 | -------------------------------------------------------------------------------- /src/test/html/advanced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/html/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/java/clara/test/facts/BeanTestFact.java: -------------------------------------------------------------------------------- 1 | package clara.test.facts; 2 | 3 | /** 4 | * A Java Pojo for the express purpose of testing the behavior of compilation and execution of rules. 5 | * 6 | * This class should not be included in the released artifact, if it did make it into a released artifact it should be 7 | * ignored and not consumed as it may be moved/removed without warning to consumers. 8 | */ 9 | public class BeanTestFact { 10 | private String[] locations; 11 | private String[] roadConditions; 12 | 13 | public BeanTestFact(String[] locations) { 14 | this.locations = locations; 15 | } 16 | 17 | // Standard and Indexed property accessors 18 | public void setLocations(String[] locations) { 19 | this.locations = locations; 20 | } 21 | public String[] getLocations() { 22 | return locations; 23 | } 24 | public void setLocations(int pos, String location) { 25 | locations[pos] = location; 26 | } 27 | public String getLocations(int pos){ 28 | return locations[pos]; 29 | } 30 | 31 | // Partial Indexed property accessor, ie. no standard accessor 32 | // See https://github.com/cerner/clara-rules/issues/446 for more details 33 | public void setRoadConditions(int pos, String condition) { 34 | roadConditions[pos] = condition; 35 | } 36 | public String getRoadConditions(int pos){ 37 | return roadConditions[pos]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/js/runner.js: -------------------------------------------------------------------------------- 1 | var puppeteer = require('puppeteer'); 2 | 3 | if (process.argv.length !== 3) { 4 | console.log('Expected a target URL parameter.'); 5 | process.exit(1); 6 | } 7 | 8 | (async () => { 9 | const browser = await puppeteer.launch({ headless: true }); // Launch headless Chrome 10 | const page = await browser.newPage(); // Create a new page 11 | 12 | // test html file 13 | var url = 'file://' + process.cwd() + '/' + process.argv[2]; 14 | 15 | await page.goto(url); 16 | 17 | page.on('console', async (msg) => { 18 | const msgArgs = msg.args(); 19 | for (let i = 0; i < msgArgs.length; ++i) { 20 | console.log(await msgArgs[i].jsonValue()); 21 | } 22 | }); 23 | 24 | var success = await page.evaluate(() => { 25 | return clara.test.run(); 26 | }) 27 | 28 | await browser.close(); 29 | 30 | return success; 31 | })().then(success => 32 | { 33 | if (success){ 34 | process.exit(0); 35 | } else { 36 | process.exit(1); 37 | } 38 | }) 39 | 40 | 41 | 42 | 43 | 44 | --------------------------------------------------------------------------------