├── .changelog ├── config.toml ├── unreleased │ ├── .gitkeep │ ├── Bug-fixes │ │ └── Rust │ │ │ └── 173-function-parsing.md │ ├── Fixes │ │ └── Rust │ │ │ └── 169-tla-function-parser.md │ ├── Improvement │ │ └── 166-monorepo.md │ └── Improvements │ │ └── Rust │ │ └── 154-jar-manager.md ├── v0.1.0 │ └── summary.md ├── v0.2.0 │ └── summary.md ├── v0.2.1 │ └── summary.md ├── v0.3.0 │ └── summary.md ├── v0.3.2 │ └── summary.md ├── v0.4.0 │ ├── features │ │ ├── Go │ │ │ ├── 2-intro.md │ │ │ └── 3-step-runner.md │ │ └── Rust │ │ │ ├── 4-event-stream.md │ │ │ └── 5-parallel.md │ ├── improvements │ │ └── Rust │ │ │ ├── 6-refactoring.md │ │ │ ├── 7-parsers.md │ │ │ └── 8-temp-dir.md │ ├── summary.md │ └── test │ │ ├── 1-improved-ci.md │ │ └── Rust │ │ └── 9-integration-tests.md ├── v0.4.1 │ ├── bug-fixes │ │ └── Rust │ │ │ ├── 147-unexpected-jars.md │ │ │ └── 152-148-fix-concurrent-tlc.md │ ├── improvements │ │ ├── Go │ │ │ └── 146-smoother-go-build.md │ │ └── Rust │ │ │ └── 135-bump-apalache.md │ ├── notes │ │ └── Rust │ │ │ └── 137-jar-dir.md │ └── summary.md └── v0.4.2 │ ├── rust │ ├── 157-support-ranged-set.md │ └── 162-clap-v3.md │ └── summary.md ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── release_template.md ├── pull_request_template.md └── workflows │ ├── cli.yml │ ├── ghpages.yml │ ├── prepare-release.yml │ ├── project.yml │ ├── python.yml │ ├── release.yml │ └── semver-release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── INSTALLATION.md ├── LICENSE ├── Modelator.md ├── ModelatorShell.md ├── README.md ├── README.rst ├── docs ├── .gitignore ├── book.toml ├── images │ └── modelator_cli.png └── src │ ├── SUMMARY.md │ ├── inner-workings │ └── index.md │ └── user-manual │ └── index.md ├── examples └── hanoi │ ├── README.md │ ├── hanoi │ └── __init__.py │ ├── model │ └── hanoi.tla │ ├── pyproject.toml │ └── tests │ └── test_hanoi.py ├── jekyll ├── .gitignore ├── Gemfile ├── README.md ├── _config.yml ├── _includes │ ├── footer_custom.html │ ├── images │ │ └── model-based-testing.svg │ └── title.html ├── _sass │ └── custom │ │ └── custom.scss ├── docs │ ├── MBT-Architecture.svg │ ├── model.md │ ├── modelator.md │ └── tla_basics_tutorials │ │ ├── .gitignore │ │ ├── apalache_vs_tlc.md │ │ ├── drawings │ │ ├── HelloWorld.excalidraw │ │ ├── HelloWorld.png │ │ └── HelloWorldGraph.png │ │ ├── ecosystem.md │ │ ├── ethereum.md │ │ ├── generating_traces.md │ │ ├── hello_world.md │ │ ├── index.md │ │ ├── models │ │ ├── .gitignore │ │ ├── erc20 │ │ │ ├── ERC20.tla │ │ │ ├── erc20-steps.md │ │ │ └── typedefs.tla │ │ ├── hello_world │ │ │ ├── hello_world.cfg │ │ │ └── hello_world.tla │ │ ├── hello_world_typed │ │ │ ├── hello_world_typed.cfg │ │ │ └── hello_world_typed.tla │ │ └── multiple_traces │ │ │ ├── multiple_traces.tla │ │ │ └── typedefs.tla │ │ ├── tla+cheatsheet.md │ │ ├── tutorial.md │ │ └── typechecking.md ├── favicon.ico └── index.md ├── modelator ├── .python-version ├── Model.py ├── ModelMonitor.py ├── ModelResult.py ├── ModelShell.py ├── __init__.py ├── __main__.py ├── _version.py ├── checker │ ├── CheckResult.py │ ├── check.py │ └── simulate.py ├── cli │ ├── __init__.py │ ├── model_config_file.py │ └── model_file.py ├── const_values.py ├── itf.py ├── modelator_shell.py ├── monitors │ ├── content.py │ ├── html_monitor.py │ ├── html_writer.py │ ├── markdown_monitor.py │ ├── markdown_writer.py │ └── templates │ │ ├── html_monitor.html │ │ ├── html_section.html │ │ ├── html_section_entry.html │ │ ├── html_table.html │ │ └── html_trace.html ├── parse.py ├── pytest │ ├── __init__.py │ └── decorators.py ├── samples │ ├── AlarmClock.tla │ ├── Hello.cfg │ ├── Hello.tla │ ├── HelloFlawed.tla │ ├── HelloFlawedType.tla │ ├── HelloFull.config.toml │ ├── HelloFull.tla │ ├── HelloFull1.itf.json │ ├── HelloInv.tla │ ├── HelloWorld.tla │ ├── HourClock.tla │ ├── HourClockTraits.tla │ ├── helloConfig.json │ └── helloModel.json ├── typecheck.py └── utils │ ├── ErrorMessage.py │ ├── apalache_helpers.py │ ├── apalache_jar.py │ ├── helpers.py │ ├── model_exceptions.py │ ├── modelator_helpers.py │ ├── tla_helpers.py │ └── tlc_helpers.py ├── modelator_api.py.example ├── pylama.ini ├── pyproject.toml ├── scripts ├── generate_version.sh ├── github_release.sh └── prepare_release.sh └── tests ├── __init__.py ├── __snapshots__ └── test_itf.ambr ├── cli ├── .gitignore ├── model │ ├── Test1.config.toml │ ├── Test1.tla │ ├── Test2.config.toml │ ├── Test2.tla │ ├── Test3.tla │ ├── errors │ │ ├── TestError1.tla │ │ └── TestError2.tla │ └── transferLegacy.tla ├── run-tests.sh ├── simulation_traces_last_generated.sh ├── simulation_traces_num_generated.sh ├── test_basic.md ├── test_check.md ├── test_constants.md ├── test_extra_args.md ├── test_load_with_config.md ├── test_load_without_config.md ├── test_parse.md ├── test_sample.md ├── test_simulate.md ├── test_typecheck.md ├── trace_check.sh ├── traces_last_generated.sh ├── traces_length.sh └── traces_num_generated.sh ├── models ├── bingame.tla └── collatz.tla ├── sampleFiles ├── check │ ├── Hello.tla │ ├── correct │ │ └── dir1 │ │ │ ├── Hello.cfg │ │ │ ├── Hello.tla │ │ │ └── Inits.tla │ └── flawed │ │ └── dir1 │ │ ├── Hello.cfg │ │ └── Hello.tla ├── correct │ ├── Hello.cfg │ └── Hello_Hi.tla ├── parse │ ├── correct │ │ └── dir1 │ │ │ └── Hello_Hi.tla │ └── flawed │ │ ├── dir1 │ │ └── HelloFlawed1.tla │ │ └── dir2 │ │ └── HelloFlawed2.tla └── typecheck │ ├── correct │ └── dir1 │ │ └── Hello_Hi.tla │ └── flawed │ ├── dir1 │ └── Hello1.tla │ └── dir2 │ └── Hello2.tla ├── test_apalache_jar.py ├── test_hellofull.py ├── test_itf.py ├── test_model_check.py ├── test_model_parse.py ├── test_model_parse_file.py ├── test_model_typecheck.py ├── test_pytest_bingame.py ├── test_pytest_collatz.py ├── test_release.py ├── test_utils.py └── traces └── itf ├── IBCTransferAcknowledgePacketInv_counterexample1.itf.json ├── IBCTransferAcknowledgePacketInv_counterexample2.itf.json ├── IBCTransferTimeoutPacketInv_counterexample1.itf.json ├── IBCTransferTimeoutPacketInv_counterexample2.itf.json ├── LocalTransferInv_counterexample1.itf.json ├── LocalTransferInv_counterexample2.itf.json └── TupleAndBigint.itf.json /.changelog/config.toml: -------------------------------------------------------------------------------- 1 | project_url = 'https://github.com/informalsystems/modelator' 2 | -------------------------------------------------------------------------------- /.changelog/unreleased/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/.changelog/unreleased/.gitkeep -------------------------------------------------------------------------------- /.changelog/unreleased/Bug-fixes/Rust/173-function-parsing.md: -------------------------------------------------------------------------------- 1 | - Fixes function parsing (#173) 2 | -------------------------------------------------------------------------------- /.changelog/unreleased/Fixes/Rust/169-tla-function-parser.md: -------------------------------------------------------------------------------- 1 | - fixes function parsing for TLA+ states (#170) 2 | -------------------------------------------------------------------------------- /.changelog/unreleased/Improvement/166-monorepo.md: -------------------------------------------------------------------------------- 1 | - Mono repo. (#166) 2 | -------------------------------------------------------------------------------- /.changelog/unreleased/Improvements/Rust/154-jar-manager.md: -------------------------------------------------------------------------------- 1 | - Improved JAR management (#154). 2 | -------------------------------------------------------------------------------- /.changelog/v0.1.0/summary.md: -------------------------------------------------------------------------------- 1 | This is the first public version; please use the crate at https://crates.io/crates/modelator/0.1.0 2 | 3 | Cargo dependency: `modelator = "0.1.0"` 4 | -------------------------------------------------------------------------------- /.changelog/v0.2.0/summary.md: -------------------------------------------------------------------------------- 1 | * provide two top-level functions to test a system using execution traces coming from TLA+ (see #44) 2 | - `run_tla_steps` will run anything that implements a `StepRunner` trait: this is suitable for small specs and simple cases 3 | - `run_tla_events` will run an implementation of `EventRunner`, which expects that a TLA+ state is structured, and contains besides state, also the `action` to execute, as well as the `actionOutcome` to expect. 4 | * make Apalache the default model checker 5 | * execute model checkers in a temporary directory (see #48) 6 | -------------------------------------------------------------------------------- /.changelog/v0.2.1/summary.md: -------------------------------------------------------------------------------- 1 | This is a bug-fixing release: 2 | - fixed #57 related to clap beta release 3 | -------------------------------------------------------------------------------- /.changelog/v0.3.0/summary.md: -------------------------------------------------------------------------------- 1 | This is the massive refactoring release: all internal and external interfaces has been changed; also the tool stability has been greatly improved. 2 | 3 | Improvements: 4 | - Refactor error handling (#53) 5 | - Reliable extraction of counterexample traces (#58) 6 | - Reintroduce generic artifacts (#61) 7 | - Rework Apalache module (#62) 8 | - Rework TLC module (#63) 9 | 10 | Bug fixes: 11 | - Confusing "No test trace found" error state (#52) 12 | - Running binary with only the argument tla causes panic instead of giving warning to user (#55) 13 | - Translate.tla counterexample using modelator tla tla-trace-to-json-trace results in parsing error (#56) 14 | -------------------------------------------------------------------------------- /.changelog/v0.3.2/summary.md: -------------------------------------------------------------------------------- 1 | This is a bug-fixing release: 2 | - fixed #112 related to clap beta release 3 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/features/Go/2-intro.md: -------------------------------------------------------------------------------- 1 | - Modelator-go for Golang. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/features/Go/3-step-runner.md: -------------------------------------------------------------------------------- 1 | - Implemented step runner. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/features/Rust/4-event-stream.md: -------------------------------------------------------------------------------- 1 | - Event stream API. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/features/Rust/5-parallel.md: -------------------------------------------------------------------------------- 1 | - Support for parallel tests. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/improvements/Rust/6-refactoring.md: -------------------------------------------------------------------------------- 1 | - Huge rework on modelator-rs API and CLI. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/improvements/Rust/7-parsers.md: -------------------------------------------------------------------------------- 1 | - Better parsers for TLA+ traces. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/improvements/Rust/8-temp-dir.md: -------------------------------------------------------------------------------- 1 | - Execute model checkers in temporary directories to avoid clutters. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/summary.md: -------------------------------------------------------------------------------- 1 | Like the last minor release, this is another massive refactoring release. 2 | - Reworked interfaces for friendlier usage. 3 | - Better parsers for improved handling of model checker outputs. 4 | - Golang bindings. 5 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/test/1-improved-ci.md: -------------------------------------------------------------------------------- 1 | - CI Workflow matrix for Windows, MacOS, and Linux. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.0/test/Rust/9-integration-tests.md: -------------------------------------------------------------------------------- 1 | - Large integration test. 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/bug-fixes/Rust/147-unexpected-jars.md: -------------------------------------------------------------------------------- 1 | - Fix panics at unexpected jars. (#151) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/bug-fixes/Rust/152-148-fix-concurrent-tlc.md: -------------------------------------------------------------------------------- 1 | - Fix concurrent TLC execution. (#152) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/improvements/Go/146-smoother-go-build.md: -------------------------------------------------------------------------------- 1 | - Smoother Go build. (#146) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/improvements/Rust/135-bump-apalache.md: -------------------------------------------------------------------------------- 1 | - Update Apalache to `v0.17.5`. (#135) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/notes/Rust/137-jar-dir.md: -------------------------------------------------------------------------------- 1 | - A unique directory to store model-checker jars. (#137) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.1/summary.md: -------------------------------------------------------------------------------- 1 | Various fixes and improvements. -------------------------------------------------------------------------------- /.changelog/v0.4.2/rust/157-support-ranged-set.md: -------------------------------------------------------------------------------- 1 | - Parse ranged sets from TLA states. (#157) 2 | -------------------------------------------------------------------------------- /.changelog/v0.4.2/rust/162-clap-v3.md: -------------------------------------------------------------------------------- 1 | - `clap` v3 (#162) -------------------------------------------------------------------------------- /.changelog/v0.4.2/summary.md: -------------------------------------------------------------------------------- 1 | Few fixes and dependency improvements. 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.jar filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to our project. 4 | 5 | ## Branches 6 | 7 | All significant changes must be done on a new branch and creating PRs. 8 | The `main` branch is modified only through PRs. 9 | 10 | ## Forking 11 | 12 | If you want to contribute, but do not have the write permission, the contribution must be made through a fork on Github. 13 | 14 | ## Reporting bugs 15 | 16 | When reporting bugs, search among existing issues first - even among closed issues. 17 | If you found one existing issue, do comment with the details of your own bug. 18 | So that we are aware of the number of people affected by that bug. 19 | 20 | If you can not find an issue, thank you for finding a new issue. 21 | Please go ahead open a new issue with details. 22 | 23 | ## Pull request 24 | 25 | Every pull request must correspond to an issue. 26 | 27 | Before opening a pull request, open an issue describing what is wrong and what is your solution. 28 | Then, reference it in your PR with the changes for the solution. 29 | 30 | Do not forget to include `CHANGELOG.md` changes if applicable. 31 | 32 | ## Changelog 33 | 34 | We maintain our changelog using [`unclog`](https://github.com/informalsystems/unclog). 35 | 36 | Every non-trivial PR must add an entry(ies) to `.changelog` directory using `unclog add`. 37 | This will add the entries to `unreleased` changes. 38 | 39 | When a release is prepared, the unreleased changes must be moved to `release` using `unclog release`. 40 | 41 | ## Release 42 | 43 | ### Checklist 44 | - The versions **should not be bumped** from the current version. The versions will be bumped by the workflow. 45 | - The changes are added to `.changelog` via `unclog add`. 46 | - Write a summary of the release at `.changelog/unreleased/summary.md`. 47 | - Perform `unclog build -u` to check the latest changelog. Commit changes if something is wrong. 48 | - This will be used as release notes in PR and Github releases. 49 | 50 | ### Workflow 51 | - When the codebase is ready for a release, trigger the [`Prepare Release` workflow](actions/workflows/prepare-release.yml) from project Actions. 52 | - We follow [_Semantic Versioning_](http://semver.org). The workflow takes an input - the component of the semantic version to be updated. 53 | - `patch` (v1.4.2 becomes v1.4.3) _(default)_ 54 | - `minor` (v1.4.2 becomes v1.5.0) 55 | - `major` (v1.4.2 becomes v2.0.0) 56 | - The workflow will create a branch `release/vX.Y.Z` from `main`. 57 | - And commit necessary changes to it for the release. 58 | - This includes bumping version numbers in projects. 59 | - Preparing changelog. 60 | - Publish projects with `--dry-run` to make sure everything is fine locally. 61 | - Then, create a PR with the title `[RELEASE] vX.Y.Z` to `main` branch. 62 | - We review the PR. If something went wrong in the PR, we push changes to the branch to correct them. 63 | - When the PR is ready, we simply merge it to `main`, which triggers the [`Release` workflow](actions/workflows/release.yml). 64 | - It will tag the merge commit with `vX.Y.Z`. 65 | - Publish the projects to package registries. 66 | - Create a Github release with changelog. 67 | 68 | ## License 69 | 70 | All the contributions must be under `Apache-2.0` license. 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Expected behavior 18 | 19 | 20 | 21 | ## Output/log 22 | 23 | 24 | 25 | ## System information 26 | 27 | - OS [e.g. Ubuntu Linux or Mac OS, and the current version]: 28 | 29 | ## Additional context 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Release Request 3 | about: Create a proposal to track the release of a new version of modelator 4 | --- 5 | 6 | 12 | 13 | # Release modelator v.X.Y.Z 14 | 15 | - [ ] Create a new release in the changelog, using [`unclog`](https://github.com/informalsystems/unclog). 16 | - [ ] Bump all crate versions to the new version. 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | Closes: #XXX 9 | 10 | ## Description 11 | 12 | 15 | 16 | ______ 17 | 18 | For contributor use: 19 | 20 | - [ ] If applicable, unit tests are written and added to CI. 21 | - [ ] Ran `go fmt`, `cargo fmt` and etc. (or had formatting run automatically on all files edited) 22 | - [ ] Updated relevant documentation (`docs/`) and code comments. 23 | - [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. 24 | - [ ] Re-reviewed `Files changed` in the Github PR explorer. 25 | - [ ] Added a changelog entry, using [`unclog`](https://github.com/informalsystems/unclog). 26 | -------------------------------------------------------------------------------- /.github/workflows/cli.yml: -------------------------------------------------------------------------------- 1 | name: CLI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - .github/workflows/cli.yml 8 | - modelator/** 9 | - tests/cli/*.md 10 | - tests/cli/*.sh 11 | - pyproject.toml 12 | - poetry.lock 13 | 14 | jobs: 15 | mdx: 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | os: ["ubuntu-latest", "macos-latest"] 20 | python-version: ["3.10"] 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - name: Check out repository 24 | uses: actions/checkout@v3 25 | - name: Set up python ${{ matrix.python-version }} 26 | id: setup-python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install Poetry 31 | uses: snok/install-poetry@v1 32 | with: 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | - name: Install java 36 | uses: actions/setup-java@v3 37 | with: 38 | distribution: "temurin" 39 | java-version: 17 40 | - name: Load cached venv 41 | id: cached-poetry-dependencies 42 | uses: actions/cache@v3 43 | with: 44 | path: .venv 45 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ hashFiles('**/pyproject.lock') }} 46 | - name: Install dependencies 47 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 48 | run: poetry install --no-interaction --no-root 49 | - name: Set up OCaml 50 | uses: ocaml/setup-ocaml@v2 51 | with: 52 | ocaml-compiler: 4.13.x 53 | - name: Install MDX 54 | run: opam install mdx 55 | - name: Install and setup Modelator 56 | run: | 57 | source .venv/bin/activate 58 | poetry install --no-interaction 59 | modelator apalache get 60 | - name: Run tests 61 | run: | 62 | source .venv/bin/activate 63 | cd tests/cli 64 | eval $(opam env) 65 | ./run-tests.sh 66 | - name: Setup tmate session 67 | if: ${{ failure() }} 68 | uses: mxschmitt/action-tmate@v3 69 | timeout-minutes: 15 70 | -------------------------------------------------------------------------------- /.github/workflows/ghpages.yml: -------------------------------------------------------------------------------- 1 | name: Jekyll publication on GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - "dev" 7 | paths: 8 | - "jekyll/**" 9 | - ".github/workflows/ghpages.yml" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | jekyll: 14 | environment: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/cache@v4 20 | with: 21 | path: vendor/bundle 22 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-gems- 25 | 26 | - uses: jeffreytse/jekyll-deploy-action@v0.6.0 27 | with: 28 | provider: "github" 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | repository: "informalsystems/modelator" 31 | branch: "gh-pages" 32 | jekyll_src: "./jekyll" 33 | jekyll_cfg: "_config.yml" 34 | bundler_ver: ">=0" 35 | cname: "mbt.informal.systems" 36 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | update_component: 7 | description: "Release component (eg. major, minor, patch)" 8 | required: false 9 | default: patch 10 | 11 | jobs: 12 | prepare-release: 13 | environment: Release 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Verify and generate version 21 | env: 22 | UPDATE_COMPONENT: ${{ github.event.inputs.update_component }} 23 | run: | 24 | ./scripts/generate_version.sh "${UPDATE_COMPONENT}" 25 | 26 | - name: Configure Git 27 | run: | 28 | git config --global user.name "$GITHUB_ACTOR" 29 | git config --global user.email "github@actions.ci" 30 | 31 | - name: Install `quickinstall` 32 | uses: actions-rs/cargo@v1 33 | with: 34 | command: install 35 | args: cargo-quickinstall 36 | 37 | - name: Install `unclog` 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: quickinstall 41 | args: unclog 42 | 43 | - name: Prepare release branch 44 | run: | 45 | ./scripts/prepare_release.sh 46 | 47 | - name: Setup Python 48 | uses: actions/setup-python@v1 49 | 50 | - name: Setup Poetry 51 | uses: Gr1N/setup-poetry@v7 52 | 53 | - name: Poetry publish 54 | run: | 55 | poetry publish --username u --password p --build --dry-run 56 | -------------------------------------------------------------------------------- /.github/workflows/project.yml: -------------------------------------------------------------------------------- 1 | name: Check project rules 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - .github/workflows/project.yml 8 | - pyproject.toml 9 | 10 | jobs: 11 | dependency-version-check: 12 | runs-on: "ubuntu-latest" 13 | container: "archlinux" 14 | steps: 15 | - name: Install dependencies 16 | run: | 17 | pacman -Syu --needed --noconfirm git yq 18 | - name: Check out repository 19 | uses: actions/checkout@v3 20 | - name: Check for git version 21 | run: | 22 | cat pyproject.toml | tomlq -r '.tool.poetry.dependencies[].git?' | (! grep --invert-match null) 23 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - .github/workflows/python.yml 8 | - modelator/** 9 | - tests/** 10 | - pyproject.toml 11 | - poetry.lock 12 | 13 | jobs: 14 | linting: 15 | strategy: 16 | matrix: 17 | os: ["ubuntu-latest"] 18 | python-version: ["3.8", "3.10"] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - uses: actions/cache@v3 26 | id: pip-cache 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ runner.os }}-pip 30 | - name: Install dependencies 31 | run: python -m pip install pyflakes==2.4.0 black pylama[all] 32 | - name: Run linters 33 | run: | 34 | black . --check 35 | pylama -l pyflakes,pycodestyle,isort 36 | 37 | compile: 38 | strategy: 39 | matrix: 40 | os: ["ubuntu-latest"] 41 | python-version: ["3.8", "3.10"] 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - name: Check out repository 45 | uses: actions/checkout@v3 46 | - name: Set up python ${{ matrix.python-version }} 47 | id: setup-python 48 | uses: actions/setup-python@v4 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | - name: Compile python source code 52 | run: python -m compileall modelator tests 53 | 54 | test: 55 | strategy: 56 | fail-fast: true 57 | matrix: 58 | os: ["ubuntu-latest", "macos-latest"] 59 | python-version: ["3.8", "3.10"] 60 | runs-on: ${{ matrix.os }} 61 | steps: 62 | - name: Check out repository 63 | uses: actions/checkout@v3 64 | - name: Set up python ${{ matrix.python-version }} 65 | id: setup-python 66 | uses: actions/setup-python@v4 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - name: Install Poetry 70 | uses: snok/install-poetry@v1 71 | with: 72 | virtualenvs-create: true 73 | virtualenvs-in-project: true 74 | - name: Install java 75 | uses: actions/setup-java@v3 76 | with: 77 | distribution: "temurin" 78 | java-version: 17 79 | - name: Load cached venv 80 | id: cached-poetry-dependencies 81 | uses: actions/cache@v3 82 | with: 83 | path: .venv 84 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ hashFiles('**/pyproject.toml') }} 85 | - name: Install dependencies 86 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 87 | run: poetry install --no-interaction --no-root 88 | - name: Install Apalache 89 | run: poetry run modelator apalache get 90 | - name: Run tests 91 | run: | 92 | source .venv/bin/activate 93 | pytest 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | branches: [dev] 6 | types: [closed] 7 | 8 | jobs: 9 | github-release: 10 | environment: Release 11 | runs-on: ubuntu-latest 12 | if: startsWith(github.event.pull_request.title, '[RELEASE] ') && 13 | github.event.pull_request.merged == true 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Configure Git 20 | run: | 21 | git config --global user.name "$GITHUB_ACTOR" 22 | git config --global user.email "github@actions.ci" 23 | 24 | - name: Install `quickinstall` 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: install 28 | args: cargo-quickinstall 29 | 30 | - name: Install `unclog` 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: quickinstall 34 | args: unclog 35 | 36 | - name: Parse release version 37 | env: 38 | TITLE: ${{ github.event.pull_request.title }} 39 | run: | 40 | echo "RELEASE_VERSION=`sed 's/^\[RELEASE\] v//g' <<< "${TITLE}"`" >> $GITHUB_ENV 41 | 42 | - name: Tag and make a Github release 43 | run: ./scripts/github_release.sh 44 | 45 | - name: Setup Python 46 | uses: actions/setup-python@v1 47 | 48 | - name: Setup Poetry 49 | uses: Gr1N/setup-poetry@v7 50 | 51 | - name: Poetry publish 52 | run: | 53 | # poetry publish --build 54 | echo "Skipping Python package" 55 | -------------------------------------------------------------------------------- /.github/workflows/semver-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | release: 7 | environment: Release 8 | runs-on: ubuntu-latest 9 | concurrency: release 10 | if: github.repository_owner == 'informalsystems' 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Python Semantic Release 17 | uses: relekang/python-semantic-release@master 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | repository_username: __token__ 21 | repository_password: ${{ secrets.PYPI_TOKEN }} 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | - id: trailing-whitespace 8 | exclude: "tests/__snapshots__/" 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | - id: check-added-large-files 12 | - repo: https://github.com/psf/black 13 | rev: 22.10.0 14 | hooks: 15 | - id: black 16 | - repo: https://github.com/pre-commit/mirrors-prettier 17 | rev: v2.7.1 18 | hooks: 19 | - id: prettier 20 | - repo: https://github.com/rnbguy/pylama-pre-commit 21 | rev: 0.2.0 22 | hooks: 23 | - id: pylama 24 | args: ["-l", "pyflakes,pycodestyle,isort"] 25 | pass_filenames: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.4.2 4 | 5 | Few fixes and dependency improvements. 6 | 7 | ### RUST 8 | 9 | - Parse ranged sets from TLA states. (#157) 10 | - `clap` v3 (#162) 11 | 12 | ## v0.4.1 13 | 14 | Various fixes and improvements. 15 | 16 | ### BUG FIXES 17 | 18 | - Rust 19 | - Fix panics at unexpected jars. (#151) 20 | - Fix concurrent TLC execution. (#152) 21 | 22 | ### IMPROVEMENTS 23 | 24 | - Go 25 | - Smoother Go build. (#146) 26 | - Rust 27 | - Update Apalache to `v0.17.5`. (#135) 28 | 29 | ### NOTES 30 | 31 | - Rust 32 | - A unique directory to store model-checker jars. (#137) 33 | 34 | ## v0.4.0 35 | 36 | Like the last minor release, this is another massive refactoring release. 37 | - Reworked interfaces for friendlier usage. 38 | - Better parsers for improved handling of model checker outputs. 39 | - Golang bindings. 40 | 41 | ### FEATURES 42 | 43 | - Go 44 | - Modelator-go for Golang. 45 | - Implemented step runner. 46 | - Rust 47 | - Event stream API. 48 | - Support for parallel tests. 49 | 50 | ### IMPROVEMENTS 51 | 52 | - Rust 53 | - Huge rework on modelator-rs API and CLI. 54 | - Better parsers for TLA+ traces. 55 | - Execute model checkers in temporary directories to avoid clutters. 56 | 57 | ### TEST 58 | 59 | - General 60 | - CI Workflow matrix for Windows, MacOS, and Linux. 61 | - Rust 62 | - Large integration test. 63 | 64 | ## v0.3.2 65 | 66 | This is a bug-fixing release: 67 | - fixed #112 related to clap beta release 68 | 69 | ## v0.3.0 70 | 71 | This is the massive refactoring release: all internal and external interfaces has been changed; also the tool stability has been greatly improved. 72 | 73 | Improvements: 74 | - Refactor error handling (#53) 75 | - Reliable extraction of counterexample traces (#58) 76 | - Reintroduce generic artifacts (#61) 77 | - Rework Apalache module (#62) 78 | - Rework TLC module (#63) 79 | 80 | Bug fixes: 81 | - Confusing "No test trace found" error state (#52) 82 | - Running binary with only the argument tla causes panic instead of giving warning to user (#55) 83 | - Translate.tla counterexample using modelator tla tla-trace-to-json-trace results in parsing error (#56) 84 | 85 | ## v0.2.1 86 | 87 | This is a bug-fixing release: 88 | - fixed #57 related to clap beta release 89 | 90 | ## v0.2.0 91 | 92 | * provide two top-level functions to test a system using execution traces coming from TLA+ (see #44) 93 | - `run_tla_steps` will run anything that implements a `StepRunner` trait: this is suitable for small specs and simple cases 94 | - `run_tla_events` will run an implementation of `EventRunner`, which expects that a TLA+ state is structured, and contains besides state, also the `action` to execute, as well as the `actionOutcome` to expect. 95 | * make Apalache the default model checker 96 | * execute model checkers in a temporary directory (see #48) 97 | 98 | ## v0.1.0 99 | 100 | This is the first public version; please use the crate at https://crates.io/crates/modelator/0.1.0 101 | 102 | Cargo dependency: `modelator = "0.1.0"` 103 | 104 | -------------------------------------------------------------------------------- /INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To install and use Modelator, make sure your system has 4 | 5 | - `Python 3.8` or newer 6 | - `Java 17` or newer 7 | 8 | ## Python 9 | 10 | Installing Modelator requires [Python](https://www.python.org) 3.8 or newer, together with its package manager [pip](https://pip.pypa.io/en/stable/installation/). 11 | (They usually come pre-installed on Linux and MacOS.) 12 | 13 | A good tool for managing different python versions is [pyenv](<(https://github.com/pyenv/pyenv)>). 14 | For example: 15 | 16 | ``` 17 | pyenv install 3.10.6 18 | pyenv global 3.10.6 19 | ``` 20 | 21 | These two commands set the python version to 3.10.6. If you want to go back to your old Python version, run `pyenv global system`. 22 | (For the Python version to be changed only in a current working directory, use the analogous `pyenv local...` command.) 23 | 24 | Once you have the correct version of Python, run `pip install modelator`. 25 | That's it! Please verify that the tool works by executing `modelator` command on the terminal. 26 | You should see something like this: 27 | 28 | ![Modelator CLI](docs/images/modelator_cli.png) 29 | 30 | ## Java 31 | 32 | Modelator uses our in-house Apalache model checker to generate test scenarios from TLA+ models. 33 | 34 | Modelator allows you to download and manage Apalache releases automatically, so you don't need to worry about that. The only prerequisite for using Apalache is to have Java installed on your system. We recommend version 17 builds of OpenJDK, for example [Eclipse Temurin](https://adoptium.net/) or [Zulu](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#download-openjdk). In case you don't have Java installed already, please download and install the package suitable for your system. 35 | -------------------------------------------------------------------------------- /ModelatorShell.md: -------------------------------------------------------------------------------- 1 | # Modelator Shell 2 | 3 | Shell is giving users an interactive way to experiment with Modelator. 4 | 5 | ## Starting the shell 6 | 7 | Run `python -i modelator_shell.py`. 8 | 9 | ## Usage examples 10 | 11 | Run `m = ModelatorShell()`. 12 | Variable `m` now holds an instance of modelator. 13 | We get the basic info about the instance by typing into the shell `m` (more detailed) or `print(m)` (prettier). 14 | By typing `help(m)` or `help(ModelatorShell)`, we get the info on how to use the `ModelatorShell` class. 15 | 16 | The function `load` loads the model file (get all the info by typing `help(m.load)`). 17 | For instance, type `m.load('modelator/samples/Hello.tla')`. 18 | With the file loaded, `ModelatorShell` will make sure to reload it before calling any other action. 19 | (This is the default behavior, can be changed by setting `m.autoload` to `False`.) 20 | 21 | Calling `m.parse()` will parse the currently loaded file. 22 | In our case it will return `File modelator/samples/Hello.tla successfully parsed.`. 23 | Feel free to modify the file `Hello.tla`. 24 | For instance, by adding a comma after the declaration of variable `x`. 25 | In this case, `m.parse()` should return 26 | 27 | ``` 28 | Parse error at modelator/samples/Hello.tla:11: 29 | Was expecting "Identifier" 30 | Encountered "Beginning of definition" at line 11, column 1 and token "," 31 | ``` 32 | 33 | If using the shell inside VSCode editor, you can click on the specific location of the error 34 | and it will open the place where the problem is detected. 35 | 36 | You can use the command `m.typecheck()` the same way. 37 | 38 | The last command to use is `m.check()`. 39 | If run without arguments, it will assume the default arguments: 40 | 41 | - that the initial state predicate is 'Init', 42 | - that the next state predicate is 'Next', 43 | - that the list of invariants to check is ['Inv'], 44 | - and that the checker is `apalache` 45 | 46 | We could suply each of these arguments individually, for instance 47 | `m.check(invariants=['Inv', 'Inv2'], checker='apalache')`. 48 | The output is 49 | 50 | ``` 51 | Check error at modelator/samples/Hello.tla: 52 | Invariant Inv violated. 53 | Counterexample is [{'x': 'hello', 'y': 22}, {'x': 'world', 'y': 20}] 54 | ``` 55 | 56 | We could also give the name of the config file as an argument, as in 57 | `m.check(config_file_name='Hello.cfg')`. 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modelator 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 4 | [![Release](https://img.shields.io/pypi/v/modelator)](https://pypi.org/project/modelator) 5 | [![Build Status](https://github.com/informalsystems/modelator/actions/workflows/python.yml/badge.svg)](https://github.com/informalsystems/modelator/actions/workflows/python.yml) 6 | 7 | Modelator is a tool that enables automatic generation of tests from models. 8 | Modelator takes [TLA+ models](https://mbt.informal.systems/docs/tla_basics_tutorials/tutorial.html) as its input and generates tests that can be executed against an implementation of the model. 9 | 10 | Under the hood, Modelator uses [Apalache](https://apalache.informal.systems), our in-house model checker, to generate tests from models. 11 | 12 | Modelator is used by [Atomkraft](https://github.com/informalsystems/atomkraft), a testing framework for the [Cosmos blockchains network](https://cosmos.network). 13 | 14 | # Installing Modelator 15 | 16 | First, make sure your system has 17 | 18 | - `Python 3.8` or newer 19 | - `Java 17` or newer 20 | 21 | To install Modelator, simply run `pip install modelator`. 22 | That's it! Please verify that the tool is working by writing `modelator` on the command line. 23 | You should see something like this: 24 | 25 | ![Modelator CLI](docs/images/modelator_cli.png) 26 | 27 | For detailed installation instructions and clarifications of dependencies, check [INSTALLATION.md](INSTALLATION.md) 28 | 29 | # Using Modelator 30 | 31 | For a full example of running Modelator together with the system implementation and the corresponding TLA+ model, see the [Towers of Hanoi example](examples/hanoi). 32 | 33 | To see all commands of the Modelator CLI, run `modelator --help`. 34 | 35 | The command `apalache` provides the info about the current version of Apalache installed and enables you to download different versions. 36 | (In order to check the usage of this command, run `modelator apalache --help`. 37 | Do the same for details of all other commands.) 38 | 39 | Commands `load`, `info`, and `reset` are used to manipulate the default value for a TLA+ model. 40 | (A default version is not strictly necessary since a model can always be given as an argument.) 41 | 42 | The most useful commands are `simulate`, `sample`, and `check`. 43 | Command `simulate` will generate a number of (randomly chosen) behaviors described by the model. 44 | If you would like to obtain a particular model behavior, use command `sample`: it will generate behaviors described by a TLA+ predicate. 45 | Finally, to check if a TLA+ predicate is an invariant of the model, use `check`. 46 | To see all the options of these commands, run `modelator simulate --help`, `modelator sample --help`, or `modelator check --help`. 47 | 48 | ## Contributing 49 | 50 | If you encounter a bug or have a an idea for a new feature of Modelator, please submit [an issue](https://github.com/informalsystems/modelator/issues). 51 | 52 | If you wish to contribute code to Modelator, set up the repository as follows: 53 | 54 | ``` 55 | git clone git@github.com/informalsystems/modelator 56 | cd modelator 57 | poetry install 58 | poetry shell 59 | ``` 60 | 61 | ## License 62 | 63 | Copyright © 2021-2022 Informal Systems Inc. and modelator authors. 64 | 65 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use the files in this repository except in compliance with the License. You may obtain a copy of the License at 66 | 67 | https://www.apache.org/licenses/LICENSE-2.0 68 | 69 | 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. 70 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/README.rst -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Andrey Kupriyanov", "Ranadeep Biswas", "Daniel Tisdall"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "modelator documentation" 7 | 8 | [output.html] 9 | mathjax-support = true 10 | git-repository-url = "https://github.com/informalsystems/modelator" 11 | 12 | [build] 13 | # Don't create empty files when a chapter doesn't exist 14 | create-missing = false 15 | # Don't rename README.md to index.md 16 | use-default-preprocessors = false 17 | 18 | # Do allow for file inclusion via {{ #include }} 19 | [preprocessor.links] 20 | -------------------------------------------------------------------------------- /docs/images/modelator_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/docs/images/modelator_cli.png -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This book will soon contain a user manual as well as documentation on design and inner workings. 4 | 5 | 1. [User Manual](./user-manual/index.md) 6 | 2. [Inner Workings](./inner-workings/index.md) 7 | -------------------------------------------------------------------------------- /docs/src/inner-workings/index.md: -------------------------------------------------------------------------------- 1 | # Modelator Inner Workings 2 | 3 | Coming soon! -------------------------------------------------------------------------------- /docs/src/user-manual/index.md: -------------------------------------------------------------------------------- 1 | # Modelator User Manual 2 | 3 | Coming soon! -------------------------------------------------------------------------------- /examples/hanoi/README.md: -------------------------------------------------------------------------------- 1 | # Tower of Hanoi in Modelator 2 | 3 | [About Tower of Hanoi](https://en.wikipedia.org/wiki/Tower_of_Hanoi) 4 | 5 | ## Directory structure 6 | 7 | ### `/hanoi` 8 | 9 | Contains the python code for SUT. 10 | 11 | ### `/model` 12 | 13 | Contains the reference TLA+ model of the SUT. 14 | 15 | ### `/tests` 16 | 17 | Tests written using `modelator` library. 18 | 19 | The example test should give an idea of how to use modelator as a model-based-testing or MBT library. 20 | 21 | ### `/traces` 22 | 23 | Contains generated traces from TLA+ model via `modelator` executable. 24 | 25 | ## Code instruction 26 | 27 | Generate traces from TLA+ model using `modelator` executable. 28 | 29 | ``` 30 | modelator sample --model-path model/hanoi.tla --tests Test --traces-dir traces 31 | ``` 32 | 33 | Execute the test using `pytest`. 34 | 35 | ``` 36 | python -m pytest -s 37 | ``` 38 | -------------------------------------------------------------------------------- /examples/hanoi/hanoi/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | 4 | class HanoiTower: 5 | state: Tuple[List[int], List[int], List[int]] 6 | 7 | def __init__(self, n_disc: Optional[int] = None): 8 | self.n_disc = n_disc or 3 9 | self.state = (list(range(self.n_disc, 0, -1)), [], []) 10 | 11 | def move(self, source: int, target: int): 12 | if len(self.state[source - 1]) == 0: 13 | raise RuntimeError("Source tower is empty") 14 | 15 | if ( 16 | len(self.state[target - 1]) > 0 17 | and self.state[target - 1][-1] < self.state[source - 1][-1] 18 | ): 19 | raise RuntimeError("target has smaller disc than source") 20 | 21 | self.state[target - 1].append(self.state[source - 1].pop()) 22 | 23 | def is_valid(self) -> bool: 24 | return all(sorted(e, reverse=True) == e for e in self.state) 25 | 26 | def is_solved(self) -> bool: 27 | return self.state == ([], [], list(range(self.n_disc, 0, -1))) 28 | -------------------------------------------------------------------------------- /examples/hanoi/model/hanoi.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE hanoi ---- 2 | EXTENDS Apalache, Integers, Sequences, SequencesExt 3 | 4 | VARIABLES 5 | \* @type: Seq(Seq(Int)); 6 | hanoi, 7 | \* @type: {tag: Str, source: Int, target: Int}; 8 | action 9 | 10 | N_DISC == 3 11 | 12 | INIT_TOWER == 13 | LET 14 | \* @type: (Seq(Int), Int) => Seq(Int); 15 | NotLambda(s,i) == <> \o s 16 | IN ApaFoldSet(NotLambda, <<>>, 1..N_DISC) 17 | 18 | Init == 19 | /\ hanoi = << INIT_TOWER, <<>>, <<>> >> 20 | /\ action = [tag |-> "init", source |-> 0, target |-> 0] 21 | 22 | 23 | \* @type: (Int, Int) => Bool; 24 | IsValidMove(source, target) == 25 | /\ Len(hanoi[source]) > 0 26 | /\ (Len(hanoi[target]) = 0 \/ Last(hanoi[source]) < Last(hanoi[target])) 27 | 28 | 29 | \* @type: (Int, Int) => Seq(Seq(Int)); 30 | Move(source, target) == 31 | [ 32 | hanoi EXCEPT 33 | ![source] = Front(@), 34 | ![target] = Append(@, Last(hanoi[source])) 35 | ] 36 | 37 | 38 | Next == 39 | \E source \in 1..3, target \in 1..3: 40 | /\ source /= target 41 | /\ IsValidMove(source, target) 42 | /\ hanoi' = Move(source, target) 43 | /\ action' = [tag |-> "move", source |-> source, target |-> target] 44 | 45 | 46 | Test == 47 | hanoi = << <<>>, <<>>, INIT_TOWER >> 48 | 49 | ==== 50 | -------------------------------------------------------------------------------- /examples/hanoi/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "hanoi" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Ranadeep Biswas "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | munch = "^2.5.0" 11 | pytest = "^7.2.0" 12 | modelator = "^0.6.4" 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /examples/hanoi/tests/test_hanoi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from hanoi import HanoiTower 3 | from munch import Munch 4 | 5 | from modelator.pytest.decorators import itf, step 6 | 7 | N_DISC = 3 8 | 9 | 10 | # pytest.fixture allows a variable to be persistent through out the test. 11 | # we want the SUT state to persist during the test. 12 | # ref: https://docs.pytest.org/en/6.2.x/fixture.html 13 | @pytest.fixture 14 | def sut(): 15 | return Munch() 16 | 17 | 18 | # you have to create hooks for each actions/steps from TLA+ trace steep. 19 | # this is done by annotating with `@step(TAG)`. 20 | @step("init") 21 | def init(sut, action): 22 | print(f"N_DISC: {N_DISC}") 23 | sut.hanoi = HanoiTower(N_DISC) 24 | 25 | 26 | # note how `action`` variable is automatically available from ITF trace. 27 | @step("move") 28 | def move(sut, action): 29 | print(f"MOVE: {sut.hanoi.state} | {action.source} -> {action.target}") 30 | sut.hanoi.move(action.source, action.target) 31 | assert sut.hanoi.is_valid() 32 | 33 | 34 | # `@itf(ITF_TRACE_FILE)` annotates a pytest method to use drive the test using that itf trace. 35 | # `keypath` params denotes a consistent path to a string variable in each ITF trace state 36 | # it is used to match against tags for each step. 37 | @itf("traces/Test/violation1.itf.json", keypath="action.tag") 38 | def test_hanoi(sut): 39 | assert sut.hanoi.is_solved() 40 | print(f"FINAL: {sut.hanoi.state}") 41 | -------------------------------------------------------------------------------- /jekyll/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .jekyll-metadata 3 | *-cache/ 4 | vendor/ 5 | Gemfile.lock 6 | .bundle 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /jekyll/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jekyll" 4 | 5 | # gem "github-pages", group: :jekyll_plugins 6 | 7 | group :jekyll_plugins do 8 | gem "jekyll-remote-theme" 9 | gem "jekyll-spaceship" 10 | gem "jekyll-seo-tag" 11 | gem "jekyll-pdf-embed" 12 | gem "rake" 13 | gem "kramdown-parser-gfm" 14 | gem "webrick" 15 | gem "just-the-docs" 16 | end 17 | 18 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] 19 | -------------------------------------------------------------------------------- /jekyll/_config.yml: -------------------------------------------------------------------------------- 1 | title: Model Based Techniques 2 | description: Part of Informal Systems 3 | baseurl: "" 4 | url: "https://mbt.informal.systems" 5 | 6 | remote_theme: pmarsceill/just-the-docs 7 | 8 | plugins: 9 | - jekyll-remote-theme 10 | - jekyll-spaceship 11 | - jekyll-pdf-embed 12 | - jekyll-seo-tag 13 | 14 | repository: "informalsystems/modelator" 15 | 16 | nav_sort: case_sensitive 17 | 18 | back_to_top: true 19 | back_to_top_text: "Back to top" 20 | 21 | gh_edit_link: true 22 | gh_edit_link_text: "Edit this page on GitHub" 23 | gh_edit_repository: "https://github.com/informalsystems/modelator" 24 | gh_edit_branch: main 25 | gh_edit_source: jekyll 26 | gh_edit_view_mode: tree 27 | 28 | markdown: kramdown 29 | 30 | kramdown: 31 | syntax_highlighter_opts: 32 | block: 33 | line_numbers: false 34 | 35 | jekyll-spaceship: 36 | mathjax-processor: 37 | optimize: 38 | enabled: false 39 | 40 | logo_svg: "/images/model-based-testing.svg" 41 | short_title: "MBT" 42 | -------------------------------------------------------------------------------- /jekyll/_includes/footer_custom.html: -------------------------------------------------------------------------------- 1 | Maintained by Informal Systems, a workers' cooperative 2 | specializing in protocol design, formal verification, and security audits. 3 | -------------------------------------------------------------------------------- /jekyll/_includes/images/model-based-testing.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /jekyll/_includes/title.html: -------------------------------------------------------------------------------- 1 | {% if site.short_title %} 2 |
{{ site.short_title }}
3 | {% if site.logo_svg and site.short_title %} 4 | {% include {{ site.logo_svg }} %} 5 | {% endif %} 6 | {% else %} 7 | {{ site.title }} 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /jekyll/_sass/custom/custom.scss: -------------------------------------------------------------------------------- 1 | 2 | #mbtlogo { 3 | width: auto; 4 | margin-left: 1em; 5 | margin-right: 1em; 6 | } 7 | 8 | #mbtlogo > path { 9 | fill: $purple-000; 10 | } 11 | 12 | @import url('https://fonts.googleapis.com/css2?family=Mukta+Mahee&display=swap'); 13 | #minititle { 14 | margin-left: auto; 15 | font-family: 'Mukta Mahee', sans-serif; 16 | vertical-align: middle; 17 | font-size: xx-large; 18 | color: $grey-dk-200; 19 | } 20 | 21 | code.language-tla { 22 | display: block; 23 | overflow: auto; 24 | } 25 | -------------------------------------------------------------------------------- /jekyll/docs/model.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | - [Leslie Lamport's Specifying Systems](https://lamport.azurewebsites.net/tla/book-02-08-08.pdf) 10 | - Case Studies -------------------------------------------------------------------------------- /jekyll/docs/modelator.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modelator 3 | description: Tool to model based testing from Informal Systems 4 | layout: default 5 | parent: Model Based Techniques 6 | nav_order: 3 7 | --- 8 | 9 | [Modelator](https://github.com/informalsystems/modelator) is a tool that facilitates generation and execution of tests. Besides other features, it provides: 10 | - easy selection of a model checker to execute (Apalache or TLC) 11 | - automatic enumeration of tests in TLA+ files 12 | - generation of multiple test executions from the same test assertion 13 | - interfaces for execution of generated tests in target languages (currently Rust and Go) 14 | 15 | ![MBT Architecture](MBT-Architecture.svg) -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/_apalache-out/ -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/apalache_vs_tlc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Apalache vs TLC 3 | description: Comparing model checkers 4 | layout: default 5 | parent: TLA+ Basics Tutorials 6 | nav_order: 5 7 | --- 8 | 9 | # Apalache vs TLC 10 | 11 | In 'Hello World' we used TLC to model check a simple model. Both model checkers have advantages and disadvantages. 12 | 13 | ## Apalache 14 | 15 | Apalache is a _bounded symbolic model checker_. Apalache will [transform](https://apalache.informal.systems/docs/apalache/theory.html) your Init and Next boolean functions into a logical formula which can be solved using an SMT solver. The formula is a conjunction of smaller formulas, one for each step of the system. This means Apalache requires a parameter _k_ specifying how many steps it should explore, starting from Init. 16 | 17 | The advantage of Apalache's approach is that it can deal with some infinite state spaces. For example it can solve the constraint problem (x is integer) /\ (0 <= x) /\ (x < 2^32) very easily - providing concrete values of x that satisfy the constraints. Can you see how this may be useful for modelling financial transaction software? 18 | 19 | The disadvantage of Apalache's approach is that it can not easily check executions which take many steps from Init. This is because the formula grows for each step in the execution, becoming progressively more difficult to solve with an SMT solver. In practice 6-12 steps may be achievable in a reasonable time, depending on the model. 20 | 21 | ## TLC 22 | 23 | TLC is an _explicit state enumeration model checker_. TLC will perform a breadth first search of the state space starting from the Init state. Each explored state is fingerprinted and the fingerprint is stored. When a state is processed from the queue (BFS style) TLC will only explore its successor states if its fingerprint has not been seen before. 24 | 25 | The advantage of TLC's approach is that it can check unbounded length executions. In particular, if there are finitely many possible system states, TLC can enumerate all of them. In practice it is possible to check billions of states, the only limit is storage (to store the BFS queue and the fingerprints) and time. In practice TLC is fast when it can use only RAM but will become extremely slow if it runs out of memory and has to store data on disk. 26 | 27 | The disadvantage of TLC's approach is that it must enumerate states explicitly and cannot solve symbolic constraints. For example if x can feasibly take values in [1, 2^32] then TLC will have to check a state for each value. How many states will TLC have to check if (x, y) can both take values in [1, 2^32]? 28 | 29 | ## Feature asymmetry 30 | 31 | There are features that TLC has and Apalache does't and vice versa. 32 | 33 | [Coming soon! TODO: link to an Apalache vs TLC page with a detailed discussion] 34 | 35 | In this tutorial we focus on Apalache, and particularly three features: 36 | 37 | 1. [The type checker](https://apalache.informal.systems/docs/apalache/typechecker-snowcat.html)\ 38 | Apalache comes with a type checker for TLA+ which helps you to develop models without creating bugs in the model itself 39 | 2. [Trace invariants](https://apalache.informal.systems/docs/apalache/principles/invariants.html?highlight=invariant#trace-invariants)\ 40 | Apalache lets you define boolean functions over the entire sequence of states in an execution. This lets you detect system behavior that single state boolean functions would not be able to detect 41 | 3. [Enumerating counterexamples](https://apalache.informal.systems/docs/apalache/principles/enumeration.html?highlight=enumer#enumerating-counterexamples)\ 42 | Apalache can generate multiple traces for a given behavior. This enables generating thorough tests for a real system. 43 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/drawings/HelloWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/jekyll/docs/tla_basics_tutorials/drawings/HelloWorld.png -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/drawings/HelloWorldGraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/jekyll/docs/tla_basics_tutorials/drawings/HelloWorldGraph.png -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/ecosystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: (Bonus) Ecosystem 3 | description: A glance at the TLA+ ecosystem 4 | layout: default 5 | parent: TLA+ Basics Tutorials 6 | nav_order: 8 7 | --- 8 | 9 | # The TLA+ ecosystem 10 | 11 | There are a number of resources in the ecosystem. The most important are 12 | 13 | 1. The language itself 14 | 2. [Apalache](https://github.com/informalsystems/apalache) - a symbolic bounded model checker 15 | 3. [TLC](https://github.com/tlaplus/tlaplus) - an explicit state model checker 16 | 17 | The Apalache and TLC model checkers each have pros and cons that make them suited for evaluating different models. 18 | 19 | There are common names you will see repeatedly as you learn TLA+. We use the following 20 | 21 | 1. [VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=alygin.vscode-tlaplus) - highlights and shows syntax errors, runs TLC\ 22 | We recommend using this. 23 | 2. [TLA+ standard modules](https://github.com/tlaplus/tlaplus/tree/master/tlatools/org.lamport.tlatools/src/tla2sany/StandardModules) - the standard library\ 24 | These modules are automatically found by Apalache and TLC when you include them. 25 | 3. [Google Group](https://groups.google.com/g/tlaplus) - the official discussion forum for TLA+\ 26 | It's worth searching here if you're stuck. 27 | 4. [tlaplus repository issues](https://github.com/tlaplus/tlaplus/issues) - the issues for the TLA+ components maintained by Microsoft\ 28 | It's worth searching here if you're stuck. 29 | 30 | Additional keywords you might see, but that we don't use in the basic tutorials: 31 | 32 | 1. [Modelator](https://modelator.informal.systems/) - a tool for model-based testing using TLA+ models\ 33 | modelator can generate hundreds of tests from a model and run them against your real system. 34 | 2. [SANY](https://github.com/tlaplus/tlaplus) - the canonical TLA+ parser used by TLC and Apalache\ 35 | You don't need to worry about using SANY on its own. 36 | 3. [Toolbox](https://github.com/tlaplus/tlaplus) - a bespoke IDE for writing TLA+ and running TLC.\ 37 | The toolbox has unique features useful in niche circumstances. We recommend trying the toolbox only after getting used to TLA+, if you need to. 38 | 4. [TLA+ community modules](https://github.com/tlaplus/CommunityModules) - additional modules contributed by the community\ 39 | Using them may require downloading and providing the path for the package. 40 | 5. [Pluscal](https://learntla.com/pluscal/a-simple-spec/) - another language which translates to TLA+\ 41 | Pluscal is less expressive than TLA+ and uses a different syntax. There are Pascal and C-like flavours. You have to translate it to TLA+ using a transpiler before checking a model written in it. We recommend trying it only after getting used to using regular TLA+. 42 | 6. [Specifying Systems](https://lamport.azurewebsites.net/tla/book-02-08-08.pdf) - a book on TLA+ written by Leslie Lamport, original creator of TLA+\ 43 | It contains useful information on niche features of TLC and TLA+. 44 | 45 | ## Footnote 46 | 47 | Please note there are many more tools and works in the ecosystem. This page contains a basic subset. -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TLA+ Basics Tutorials 3 | description: TLA+ basic tutorials overview 4 | layout: default 5 | has_children: true 6 | --- 7 | 8 | # TLA+ 9 | 10 | These is our basic set of tutorials for TLA+. Please use the cheatsheet too! 11 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/.gitignore: -------------------------------------------------------------------------------- 1 | apalache-pkg-0.17.4-full.jar 2 | tla2tools.jar 3 | **/states/ 4 | **/*.out -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/erc20/erc20-steps.md: -------------------------------------------------------------------------------- 1 | # Typing the specification and checking it 2 | 3 | ## Inspecting a complete spec 4 | 5 | 1. Read [EIP-20](https://eips.ethereum.org/EIPS/eip-20) 6 | and try to figure how it is working. 7 | 1. Read the description of the 8 | [attack scenario on EIP-20](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/) 9 | 1. Open [ERC20.tla](../examples/erc20-approve-attack/ERC20.tla) 10 | and [MC_ERC20.tla](../examples/erc20-approve-attack/MC_ERC20.tla). 11 | 1. Check the trace invariant `NoTransferAboveApproved`: 12 | 13 | ```sh 14 | $ apalache-mc check --inv=NoTransferAboveApproved MC_ERC20.tla 15 | ``` 16 | 1. The tool reports an invariant violation. 17 | 1. Open the counterexample and see, 18 | whether it matches the above attack scenario. 19 | 20 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/erc20/typedefs.tla: -------------------------------------------------------------------------------- 1 | ------------------------ MODULE typedefs -------------------------------- 2 | (* 3 | Type definitions for the module ERC20. 4 | 5 | An account address, in our case, simply an uninterpreted string: 6 | @typeAlias: ADDR = Str; 7 | 8 | A transaction (a la discriminated union but all fields are packed together): 9 | @typeAlias: TX = [ 10 | tag: Str, 11 | id: Int, 12 | fail: Bool, 13 | sender: ADDR, 14 | spender: ADDR, 15 | fromAddr: ADDR, 16 | toAddr: ADDR, 17 | value: Int 18 | ]; 19 | 20 | A state of the state machine: 21 | @typeAlias: STATE = [ 22 | balanceOf: ADDR -> Int, 23 | allowance: <> -> Int, 24 | pendingTransactions: Set(TX), 25 | lastTx: TX, 26 | nextTxId: Int 27 | ]; 28 | 29 | Below is a dummy definition to introduce the above type aliases. 30 | *) 31 | ERC20_typedefs == TRUE 32 | =============================================================================== 33 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/hello_world/hello_world.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | 4 | INVARIANTS 5 | NotBobIsHappy 6 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/hello_world/hello_world.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE hello_world ---- 2 | 3 | EXTENDS Sequences \* Import Sequences module from the standard library 4 | 5 | VARIABLES 6 | alices_outbox, 7 | network, 8 | bobs_mood, 9 | bobs_inbox 10 | 11 | Init == 12 | /\ alices_outbox = {} \* Alice's memory of what she sent is the empty set 13 | /\ network = {} \* AND so is the network 14 | /\ bobs_mood = "neutral" \* AND Bob's mood is neutral 15 | /\ bobs_inbox = <<>> \* AND Bob'b inbox is an empty Sequence (list) 16 | 17 | AliceSend(m) == 18 | /\ m \notin alices_outbox 19 | /\ alices_outbox' = alices_outbox \union {m} 20 | /\ network' = network \union {m} 21 | /\ UNCHANGED <> 22 | 23 | NetworkLoss == 24 | /\ \E e \in network: network' = network \ {e} 25 | /\ UNCHANGED <> 26 | 27 | NetworkDeliver == 28 | /\ \E e \in network: 29 | /\ bobs_inbox' = bobs_inbox \o <> 30 | /\ network' = network \ {e} 31 | /\ UNCHANGED <> 32 | 33 | BobCheckInbox == 34 | /\ bobs_mood' = IF bobs_inbox = <<"hello", "world">> THEN "happy" ELSE "neutral" 35 | /\ UNCHANGED <> 36 | 37 | Next == 38 | \/ AliceSend("hello") 39 | \/ AliceSend("world") 40 | \/ NetworkLoss 41 | \/ NetworkDeliver 42 | \/ BobCheckInbox 43 | 44 | NothingUnexpectedInNetwork == \A e \in network: e \in alices_outbox 45 | 46 | NotBobIsHappy == 47 | LET BobIsHappy == bobs_mood = "happy" 48 | IN ~BobIsHappy 49 | 50 | ==== -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/hello_world_typed/hello_world_typed.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | 4 | INVARIANTS 5 | NotBobIsHappy -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/hello_world_typed/hello_world_typed.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE hello_world_typed ---- 2 | 3 | EXTENDS Sequences \* Import Sequences module from the standard library 4 | 5 | VARIABLES 6 | \* @type: Set(Str); 7 | alices_outbox, 8 | \* @type: Set(Str); 9 | network, 10 | \* @type: Str; 11 | bobs_mood, 12 | \* @type: Seq(Str); 13 | bobs_inbox 14 | 15 | \* @type: () => Bool; 16 | Init == 17 | /\ alices_outbox = {} \* Alice's memory of what she sent is the empty set 18 | /\ network = {} \* AND so is the network 19 | /\ bobs_mood = "neutral" \* AND Bob's mood is neutral 20 | /\ bobs_inbox = <<>> \* AND Bob'b inbox is an empty Sequence (list) 21 | 22 | \* @type: (Str) => Bool; 23 | AliceSend(m) == 24 | /\ m \notin alices_outbox 25 | /\ alices_outbox' = alices_outbox \union {m} 26 | /\ network' = network \union {m} 27 | /\ UNCHANGED <> 28 | 29 | \* @type: () => Bool; 30 | NetworkLoss == 31 | /\ \E e \in network: network' = network \ {e} 32 | /\ UNCHANGED <> 33 | 34 | \* @type: () => Bool; 35 | NetworkDeliver == 36 | /\ \E e \in network: 37 | /\ bobs_inbox' = bobs_inbox \o <> 38 | /\ network' = network \ {e} 39 | /\ UNCHANGED <> 40 | 41 | \* @type: () => Bool; 42 | BobCheckInbox == 43 | /\ bobs_mood' = IF bobs_inbox = <<"hello", "world">> THEN "happy" ELSE "neutral" 44 | /\ UNCHANGED <> 45 | 46 | \* @type: () => Bool; 47 | Next == 48 | \/ AliceSend("hello") 49 | \/ AliceSend("world") 50 | \/ NetworkLoss 51 | \/ NetworkDeliver 52 | \/ BobCheckInbox 53 | 54 | \* @type: () => Bool; 55 | NothingUnexpectedInNetwork == \A e \in network: e \in alices_outbox 56 | 57 | \* @type: () => Bool; 58 | NotBobIsHappy == 59 | LET BobIsHappy == bobs_mood = "happy" 60 | IN ~BobIsHappy 61 | 62 | ==== -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/multiple_traces/multiple_traces.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE multiple_traces ---- 2 | 3 | EXTENDS Integers, Sequences, Apalache, typedefs 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | auxiliary_str, 8 | \* @type: Int; 9 | important_int 10 | 11 | Init == 12 | /\ auxiliary_str = "foo" 13 | /\ important_int = 0 14 | 15 | ChangeAuxiliaryStr == 16 | /\ auxiliary_str' \in {"foo", "bar", "wiz"} 17 | /\ UNCHANGED important_int 18 | 19 | AddToImportantInt == 20 | /\ UNCHANGED auxiliary_str 21 | /\ \E x \in 1..4 : important_int' = important_int + x 22 | 23 | Next == 24 | \/ ChangeAuxiliaryStr 25 | \/ AddToImportantInt 26 | 27 | \* @type: () => Bool; 28 | ImportantIntIs6 == 29 | LET Behavior == important_int = 6 30 | IN ~Behavior 31 | 32 | \* @type: Seq(STATE) => Bool; 33 | ImportantIntIsOddUntil6(trace) == 34 | LET Behavior == 35 | /\ trace[Len(trace)].important_int = 6 36 | /\ \A i \in DOMAIN trace : 37 | \/ (i = 1 \/ i = Len(trace)) 38 | \/ trace[i].important_int % 2 = 1 39 | IN ~Behavior 40 | 41 | View == important_int 42 | 43 | ==== -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/models/multiple_traces/typedefs.tla: -------------------------------------------------------------------------------- 1 | ------------------------ MODULE typedefs -------------------------------- 2 | 3 | (* 4 | @typeAlias: STATE = [ 5 | auxiliary_str : Str, 6 | important_int : Int 7 | ]; 8 | *) 9 | 10 | include_typedefs == TRUE 11 | =============================================================================== 12 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/tutorial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: How to make them most of the tutorials 4 | layout: default 5 | parent: TLA+ Basics Tutorials 6 | nav_order: 2 7 | --- 8 | 9 | # TLA+ Basics Tutorial 10 | 11 | This is a straightforward introduction to TLA+. By the end you should be able to write your own models of distributed systems and concurrent algorithms. You should also be able to check properties of the models, and generate execution traces which can be used in automatic testing pipelines. The target audience is software engineers who are fluent in a mainstream programming language and understand computer science. 12 | 13 | This document contains prose. The [cheatsheet](./cheatsheet) is useful as a reference. 14 | 15 | ## TLA+ capabilities 16 | 17 | TLA+ is a language for writing models of distributed systems and concurrent algorithms. It doesn't execute on your machine like a typical programming language: you run it through a model checker. A model checker is a program that explores all possible executions of a system. You can specify properties and behaviors, and the model checker will tell you if they hold or not. The model checker can also give you examples of behaviors. 18 | 19 | TLA+ has been used to model a wide variety of things including [locks and allocators in the linux kernel](https://git.kernel.org/pub/scm/linux/kernel/git/cmarinas/kernel-tla.git/), the [Docker SwarmKit container orchestrator](https://github.com/docker/swarmkit/tree/master/design/tla), [Paxos consensus](https://github.com/tlaplus/DrTLAPlus/blob/master/Paxos/Paxos.tla), the [Raft replicated state machine](https://github.com/ongardie/raft.tla/blob/master/raft.tla) and more. 20 | 21 | All TLA+ models are structured as a state machine. You specify an initial state and a collection of transitions. Additionally you can specify boolean functions (invariants) over the state. The model checker will check for boolean function violations. 22 | 23 | To give examples: you could model a concurrent garbage collector algorithm and check that no memory leak is possible. You could also model the API for a financial transfers system, and check that it is not possible to steal funds. 24 | 25 | ## Getting set up 26 | 27 | For these tutorials we require the TLC and Apalache model checkers. 28 | 29 | TLC can be downloaded with 30 | 31 | ``` 32 | tlaurl=https://github.com/tlaplus/tlaplus/releases/download/v1.7.1/tla2tools.jar; 33 | curl -LO $tlaurl; 34 | ``` 35 | 36 | Apalache can be downloaded with 37 | 38 | ``` 39 | apalacheurl=https://github.com/informalsystems/apalache/releases/download/v0.17.5/apalache-v0.17.5.zip; 40 | curl -LO $apalacheurl; 41 | ``` 42 | 43 | You will need to unzip Apalache and move the jar from `mod-distribution/target/apalache-pkg-0.17.5-full.jar` to your working directory. 44 | 45 | We recommend using the Visual Studio Code [TLA+ extension](https://marketplace.visualstudio.com/items?itemName=alygin.vscode-tlaplus) to work on your models. It provides syntax highlighting, parsing and model checking through TLC. Model checking, parsing and other features are accessed through the VSCode context menu (cmd + shift + p on OSX). 46 | 47 | There are more resources that we won't in this set of tutorials but that might be useful to know about. Please see [The TLA+ ecosystem](./ecosystem). 48 | 49 | **The .tla and other files referenced in these tutorials are included [here](https://github.com/informalsystems/modelator/tree/main/jekyll/docs/tla_basics_tutorials/models).** 50 | 51 | ## Let's get started 52 | 53 | We have 5 mini tutorials giving you increasing power. 54 | 55 | 1. ['Hello world' using TLC](./hello_world) 56 | 2. [Typechecking your models](./typechecking) 57 | 3. [Apalache vs TLC](./apalache_vs_tlc) 58 | 4. [Finding an Ethereum exploit using Apalache](./ethereum) 59 | 5. [Generating traces for automated testing using Apalache](./generating_traces) 60 | 61 | That's it for the basic tutorials; congratulations! 62 | 63 | These tutorials make up the basics of using TLA+, please see [advanced tutorials](TODO: link)! 64 | 65 | ## Footnote: what the tutorials do not include 66 | 67 | TLA+ and its tools includes many features. We ignored the following in these basic tutorials 68 | 69 | 1. Formal proof using [TLAPS](https://apalache.informal.systems/docs/apalache/theory.html) 70 | 2. [Inductive invariants](https://apalache.informal.systems/docs/apalache/running.html?highlight=inductive#checking-an-inductive-invariant) using Apalache 71 | 3. Verifying temporal (liveness) properties [[1](https://pron.github.io/posts/tlaplus_part3), [2](https://learntla.com/temporal-logic/usage/)] 72 | 4. TLC's [symmetry sets and model values](https://tla.msr-inria.inria.fr/tlatoolbox/doc/model/model-values.html) 73 | 5. Apalache's [uninterpreted types](https://apalache.informal.systems/docs/HOWTOs/uninterpretedTypes.html) 74 | 75 | These features are useful in some circumstances. We may add sections in the future. 76 | -------------------------------------------------------------------------------- /jekyll/docs/tla_basics_tutorials/typechecking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typechecking 3 | description: How to type check your models 4 | layout: default 5 | parent: TLA+ Basics Tutorials 6 | nav_order: 4 7 | --- 8 | 9 | # Typechecking 10 | 11 | **The .tla and other referenced files are included [here](https://github.com/informalsystems/modelator/tree/main/jekyll/docs/tla_basics_tutorials/models).** 12 | 13 | As a model grows it becomes difficult to ensure that the TLA+ code in the models is doing what you think it is. There are techniques to help ensure there are no bugs in your model. The best way to make sure your model is high quality is to use types and the Apalache type checker. 14 | 15 | Apalache comes with a type checker. The [docs](https://apalache.informal.systems/docs/HOWTOs/howto-write-type-annotations.html) contain all the details. In this tutorial we will type the model of Alice and Bob's interactions in hello_world.tla. We will use a subset of the built in types. The full list of builtin types can be found [here](https://apalache.informal.systems/docs/adr/002adr-types.html?highlight=types#11-type-grammar-type-system-1-or-ts1). 16 | 17 | 18 | ## Typechecking 19 | 20 | Our model of Alice and Bob's interaction is simple: the state variables are simple data structures. 21 | 22 | We should type the variables with a particular data structure. 23 | 24 | ```tla 25 | VARIABLES 26 | \* @type: Set(Str); 27 | alices_outbox, 28 | \* @type: Set(Str); 29 | network, 30 | \* @type: Str; 31 | bobs_mood, 32 | \* @type: Seq(Str); 33 | bobs_inbox 34 | ``` 35 | 36 | The Apalache type system works by annotating lines of code with special TLA+ comments 37 | 38 | ``` 39 | \* @type: ... 40 | ``` 41 | 42 | We have specified that 43 | 44 | 1. _alices_outbox_ is a set of strings 45 | 2. _network_ is a set of strings 46 | 3. _bobs_mood_ is a string 47 | 4. _bobs_inbox_ is a sequence of strings 48 | 49 | We can also specify the type of operators. For example we can annotate AliceSend(m) 50 | 51 | ```tla 52 | \* @type: (Str) => Bool; 53 | AliceSend(m) == 54 | /\ m \notin alices_outbox 55 | /\ alices_outbox' = alices_outbox \union {m} 56 | /\ network' = network \union {m} 57 | /\ UNCHANGED < 58 | ``` 59 | 60 | The annotation says that AliceSend is an operator taking strings and returning booleans. 61 | (Note that very often the typechecker can infer annotations for operators automatically. It is able to do so for the operator AliceSend, too. You can try typechecking with the manual annotation left out.) 62 | 63 | Finally we can typecheck the model 64 | 65 | ```bash 66 | java -jar apalache-pkg-0.17.5-full.jar typecheck hello_world_typed.tla 67 | 68 | # Apalache output: 69 | # ... 70 | # Type checker [OK] 71 | ``` 72 | -------------------------------------------------------------------------------- /jekyll/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/jekyll/favicon.ico -------------------------------------------------------------------------------- /jekyll/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Model Based Techniques 4 | nav_order: 1 5 | description: Model Based Techniques at Informal Systems 6 | has_children: true 7 | --- 8 | 9 | # Model Based Techniques for Software Correctness 10 | Building a model of our software gives us a couple of elegant and efficient ways to increase confidence in its correctness. 11 | Modeling languages (such as [TLA+](https://lamport.azurewebsites.net/tla/tla.html) and [Quint](https://github.com/informalsystems/quint)) are supported by _model checkers_, which enable us to reason about the model's properties. 12 | We can specify desired properties and verify that the model satisfies them. We can also generate a large number of tests directly from the model and run them against the implementation. 13 | 14 | A model can be written even before the development starts. 15 | This enables finding problems early on, in the development phase. 16 | 17 | Besides being a tool for finding difficult-to-spot problems, models serve as high-level yet precise and executable specifications. 18 | 19 | 20 | # Model Based Techniques @ Informal Systems 21 | 22 | At [Informal Systems](https://informal.systems), we use model-based techniques both in our development practice and as a part of our security audit services. 23 | As a premier partner in protocol design and cross-chain infrastructure, we develop and maintain the following tools that make model-based techniques easy 24 | to incorporate into the development and auditing practice: 25 | 26 | - [Quint](https://github.com/informalsystems/quint), a modern modeling language 27 | - [Apalache](https://apalache.informal.systems/), a symbolic model checker 28 | - [Modelator](https://github.com/informalsystems/modelator), a tool that enables automatic generation of tests from models 29 | - [cosmwasm-to-quint](https://github.com/informalsystems/cosmwasm-to-quint), a tool for generating Quint model stubs and accompanying tests directly from [CosmWasm](https://cosmwasm.com/) contracts 30 | 31 | ## Learn More About Informal Systems 32 | 33 | [Informal Systems](https://informal.systems) was founded to build trust in software and monetary systems, specializing in secure, interoperable, and fault-tolerant networks. 34 | We bring rigorous protocol design, formal verification, and a dedication to sustainability, empowering teams to create systems people can fully rely on. 35 | 36 | Our team is growing! Check out [our careers page](https://informal.systems/careers) to join our team of engineers, researchers, and security experts. 37 | -------------------------------------------------------------------------------- /modelator/.python-version: -------------------------------------------------------------------------------- 1 | 3.10.0 2 | -------------------------------------------------------------------------------- /modelator/ModelMonitor.py: -------------------------------------------------------------------------------- 1 | from modelator.ModelResult import ModelResult 2 | 3 | 4 | class ModelMonitor: 5 | """ 6 | Monitor for actions done with the model 7 | """ 8 | 9 | def on_parse_start(self, res: ModelResult): 10 | pass 11 | 12 | def on_parse_finish(self, res: ModelResult): 13 | pass 14 | 15 | def on_sample_start(self, res: ModelResult): 16 | pass 17 | 18 | def on_sample_update(self, res: ModelResult): 19 | pass 20 | 21 | def on_sample_finish(self, res: ModelResult): 22 | pass 23 | 24 | def on_check_start(self, res: ModelResult): 25 | pass 26 | 27 | def on_check_update(self, res: ModelResult): 28 | pass 29 | 30 | def on_check_finish(self, res: ModelResult): 31 | pass 32 | 33 | def on_simulate_start(self, res: ModelResult): 34 | pass 35 | 36 | def on_simulate_update(self, res: ModelResult): 37 | pass 38 | 39 | def on_simulate_finish(self, res: ModelResult): 40 | pass 41 | -------------------------------------------------------------------------------- /modelator/ModelResult.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from io import StringIO 3 | from threading import Lock 4 | from typing import List, Optional 5 | 6 | 7 | class ModelResult: 8 | """ 9 | A result of running some action on a set of model operators 10 | Different actions can have different outcomes: 11 | - example sampling is successful when a trace satisfying it can be produced. 12 | - invariant checking is successful when a trace violating it can't be produced. 13 | """ 14 | 15 | def __init__( 16 | self, 17 | model, 18 | all_operators=None, 19 | parsing_error: Optional[str] = None, 20 | typing_error: Optional[str] = None, 21 | ) -> None: 22 | self._model = model 23 | self._time = datetime.now() 24 | self._in_progress_operators = ( 25 | list(all_operators) if all_operators is not None else [] 26 | ) 27 | self._finished_operators = [] 28 | self._successful = [] 29 | self._unsuccessful = [] 30 | self._traces = {} 31 | # unlike traces in self._traces and self._trace_paths, these are not bound to any predicate/invariant 32 | self._simulation_traces = set() 33 | self._simulation_traces_paths = set() 34 | self._trace_paths = {} 35 | self.lock = Lock() 36 | self.parsing_error = parsing_error 37 | self.typing_error = typing_error 38 | self.operator_errors = {} 39 | 40 | def model(self): 41 | """ 42 | returns the model on which the action was executed 43 | """ 44 | return self._model 45 | 46 | def time(self): 47 | return self._time 48 | 49 | def inprogress(self): 50 | """ 51 | Returns the list of operators for which the action has not completed yet 52 | """ 53 | return sorted(self._in_progress_operators) 54 | 55 | def successful(self): 56 | """ 57 | Returns the list of operators for which the action was successful 58 | """ 59 | return sorted(self._successful) 60 | 61 | def unsuccessful(self): 62 | """ 63 | Returns the list of operators for which the action was unsuccessful 64 | """ 65 | return sorted(self._unsuccessful) 66 | 67 | def traces(self, operator): 68 | """ 69 | Traces associated with the result of applying an action to the operator, if available. 70 | Availability depends on action type, and its success for the operator. 71 | If available, at least one trace is guaranteed to exist. 72 | """ 73 | return self._traces[operator] if operator in self._traces else None 74 | 75 | def all_traces(self): 76 | return self._traces 77 | 78 | def trace_paths(self, operator) -> List[str]: 79 | """ 80 | The list of trace files associated with an operator as a result of running the checker. 81 | """ 82 | return self._trace_paths[operator] if operator in self._trace_paths else [] 83 | 84 | def add_trace_paths(self, operator: str, trace_paths: List[str]): 85 | self._trace_paths[operator] = trace_paths 86 | 87 | def progress(self, operator): 88 | """ 89 | returns a progress measure between 0 and 1 (inclusive) 90 | for any operator on which the action is executed. 91 | """ 92 | if operator in self._finished_operators: 93 | return 1 94 | else: 95 | return 0 96 | 97 | def _write_traces(self, s, indent, op): 98 | trace = self.traces(op) 99 | if trace: 100 | s.write(f"{indent}Trace: {trace}\n") 101 | 102 | trace_paths = self.trace_paths(op) 103 | if trace_paths: 104 | s.write(f"{indent}Trace files: {' '.join(trace_paths)}\n") 105 | 106 | def __str__(self): 107 | indent = " " * 4 108 | s = StringIO() 109 | 110 | if self.parsing_error: 111 | s.write("Parsing error 💥\n") 112 | s.write(f"{indent}{self.parsing_error}\n") 113 | elif self.typing_error: 114 | s.write("Type checking error 💥\n") 115 | s.write(f"{indent}{self.typing_error}\n") 116 | else: 117 | for op in self.inprogress(): 118 | s.write(f"- {op} ⏳\n") 119 | 120 | for op in self.successful(): 121 | s.write(f"- {op} OK ✅\n") 122 | 123 | self._write_traces(s, indent, op) 124 | 125 | for op in self.unsuccessful(): 126 | s.write(f"- {op} FAILED ❌\n") 127 | 128 | if op in self.operator_errors and self.operator_errors[op]: 129 | error = str(self.operator_errors[op]).replace("\n", f"{indent}\n") 130 | s.write(f"{indent}{error}\n") 131 | 132 | self._write_traces(s, indent, op) 133 | 134 | s.write("Simulation completed✅\n") 135 | if len(self._simulation_traces_paths) > 0: 136 | s.write( 137 | f"{indent}Trace files: {' '.join(self._simulation_traces_paths)}\n" 138 | ) 139 | else: 140 | for trace_path in self._simulation_traces_paths: 141 | s.write(f"{indent}Trace: {trace_path}\n") 142 | 143 | string = s.getvalue() 144 | s.close() 145 | return string 146 | -------------------------------------------------------------------------------- /modelator/ModelShell.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from typing import Any, Dict, List, Optional, Union 4 | 5 | from typing_extensions import Self 6 | from watchdog.events import FileSystemEventHandler 7 | from watchdog.observers import Observer 8 | 9 | from modelator import ModelResult, const_values 10 | from modelator.Model import Model 11 | from modelator.utils import tla_helpers 12 | from modelator.utils.model_exceptions import ModelError 13 | 14 | 15 | class ModelShell(Model): 16 | @classmethod 17 | def parse_file( 18 | cls, file_name: str, init: str = "Init", next: str = "Next" 19 | ) -> Union[Self, ModelError]: 20 | 21 | auxiliary_files = tla_helpers.get_auxiliary_tla_files(file_name) 22 | 23 | # TODO: how to handle .cfg files? They are actually necessary for TLC 24 | 25 | m = ModelShell( 26 | tla_file_path=file_name, 27 | init_predicate=init, 28 | next_predicate=next, 29 | files_contents=auxiliary_files, 30 | ) 31 | # a member function which will raise a ModelParsingError exception in case of problems 32 | try: 33 | m.parse() 34 | except ModelError as e: 35 | print(e) 36 | except Exception as e: 37 | logging.error("Unexpected exception: {}".format(e)) 38 | else: 39 | return m 40 | 41 | def typecheck(self) -> Optional[ModelError]: 42 | try: 43 | super().typecheck() 44 | except ModelError as e: 45 | print(e) 46 | except Exception as e: 47 | self.logger.error("Unexpected exception: {}".format(e)) 48 | 49 | def check( 50 | self, 51 | invariants: List[str] = None, 52 | model_constants: Dict = None, 53 | checker: str = const_values.APALACHE, 54 | checker_params: Dict = None, 55 | ) -> ModelResult: 56 | try: 57 | return super().check(invariants, model_constants, checker, checker_params) 58 | except ModelError as e: 59 | print(e) 60 | except Exception as e: 61 | self.logger.error("Unexpected exception: {}".format(e)) 62 | 63 | def sample( 64 | self, 65 | examples: List[str] = None, 66 | model_constants: Dict = None, 67 | checker: str = const_values.APALACHE, 68 | checker_params: Dict = None, 69 | ) -> ModelResult: 70 | 71 | try: 72 | return super().sample(examples, model_constants, checker, checker_params) 73 | except ModelError as e: 74 | print(e) 75 | except Exception as e: 76 | self.logger.error("Unexpected exception: {}".format(e)) 77 | 78 | def __init__( 79 | self, 80 | tla_file_path: str, 81 | init_predicate: str, 82 | next_predicate: str, 83 | files_contents: Dict[str, str] = None, 84 | constants: Dict[str, Any] = None, 85 | ) -> None: 86 | 87 | super().__init__( 88 | tla_file_path, init_predicate, next_predicate, files_contents, constants 89 | ) 90 | 91 | self.autoload = True 92 | self.auto_parse_file() 93 | # self.load_observer = Observer() 94 | # self.autoloadhandler = FileSystemEventHandler() 95 | # self.autoloadhandler.on_modified = self._load_on_modified 96 | # self.old = 0 97 | # self.load_observer.schedule( 98 | # self.autoloadhandler, os.path.abspath(self.tla_file_path), recursive=True 99 | # ) 100 | # self.load_observer.start() 101 | 102 | def _load_on_modified(self, event): 103 | if event.is_directory: 104 | return None 105 | 106 | statbuf = os.stat(event.src_path) 107 | self.new = statbuf.st_mtime 108 | if (self.new - self.old) > 0.5: 109 | # this is a real event 110 | self.files_contents[self.tla_file_path] = open(event.src_path).read() 111 | 112 | self.old = self.new 113 | 114 | def auto_parse_file(self): 115 | self.autoparse = True 116 | self.observer = Observer() 117 | self.autohandler = FileSystemEventHandler() 118 | self.autohandler.on_modified = self._parse_on_modified 119 | self.old = 0 120 | self.observer.schedule( 121 | self.autohandler, os.path.abspath(self.tla_file_path), recursive=True 122 | ) 123 | self.observer.start() 124 | 125 | def _parse_on_modified(self, event): 126 | if event.is_directory: 127 | return None 128 | 129 | statbuf = os.stat(event.src_path) 130 | self.new = statbuf.st_mtime 131 | if (self.new - self.old) > 0.5: 132 | # this is a real event 133 | self.files_contents[self.tla_file_path] = open(event.src_path).read() 134 | try: 135 | self.parse() 136 | except ModelError as e: 137 | self.parsable = False 138 | print(e) 139 | finally: 140 | self.old = self.new 141 | 142 | def stop_auto_parse(self): 143 | self.observer.stop() 144 | self.observer.join() 145 | -------------------------------------------------------------------------------- /modelator/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | from .cli import app 3 | from .Model import Model 4 | 5 | __all__ = ["__version__", "app", "Model"] 6 | -------------------------------------------------------------------------------- /modelator/__main__.py: -------------------------------------------------------------------------------- 1 | from cli import app 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /modelator/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.6" 2 | -------------------------------------------------------------------------------- /modelator/checker/CheckResult.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from modelator.utils.ErrorMessage import ErrorMessage 4 | 5 | 6 | class CheckResult: 7 | def __init__( 8 | self, 9 | is_ok: bool, 10 | error_msg: Optional[ErrorMessage] = None, 11 | traces: List[str] = [], 12 | trace_paths: List[str] = [], 13 | ) -> None: 14 | self.is_ok = is_ok 15 | self.error_msg = error_msg 16 | self.traces = traces 17 | self.trace_paths = trace_paths 18 | 19 | def __repr__(self) -> str: 20 | if self.is_ok: 21 | return f"CheckResult(success, {self.traces}, {self.trace_paths})" 22 | else: 23 | return f"CheckResult(failed, {self.error_msg}, {self.traces}, {self.trace_paths})" 24 | -------------------------------------------------------------------------------- /modelator/checker/check.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | from modelator_py.apalache.pure import apalache_pure 4 | from modelator_py.tlc.pure import tlc_pure 5 | from modelator_py.util.tlc import tlc_itf 6 | 7 | from modelator import const_values 8 | from modelator.checker.CheckResult import CheckResult 9 | from modelator.const_values import APALACHE_STDOUT 10 | from modelator.utils import apalache_helpers, tlc_helpers 11 | from modelator.utils.modelator_helpers import ( 12 | create_logger, 13 | extract_line_with, 14 | wrap_command, 15 | ) 16 | 17 | from ..itf import ITF 18 | from ..parse import parse 19 | from ..typecheck import typecheck 20 | from ..utils.ErrorMessage import ErrorMessage 21 | 22 | check_logger = create_logger(logger_name=__file__, loglevel="error") 23 | 24 | 25 | def check_tlc( 26 | tla_file_name: str, 27 | files: Dict[str, str], 28 | args: Dict = {}, 29 | do_parse: bool = True, 30 | traces_dir: Optional[str] = None, 31 | ) -> CheckResult: 32 | 33 | if do_parse is True: 34 | parse(tla_file_name=tla_file_name, files=files, args=args) 35 | 36 | json_command = wrap_command( 37 | cmd=const_values.CHECK_CMD, 38 | checker=const_values.TLC, 39 | tla_file_name=tla_file_name, 40 | files=files, 41 | args=args, 42 | ) 43 | 44 | result = tlc_pure(json=json_command) 45 | if result["return_code"] == 0: 46 | return CheckResult(True) 47 | 48 | itf_trace_objects = tlc_itf( 49 | json={"stdout": result["stdout"], "lists": True, "records": True} 50 | ) 51 | 52 | counterexample = itf_trace_objects[0]["states"] 53 | inv_violated = tlc_helpers.invariant_from_stdout(result["stdout"]) 54 | 55 | trace = [ITF(state) for state in counterexample] 56 | error_msg = ErrorMessage( 57 | problem_description=f"Invariant {inv_violated} violated.\nCounterexample is {trace}", 58 | error_category=const_values.CHECK, 59 | full_error_msg=result["stdout"], 60 | ) 61 | return CheckResult(False, error_msg, trace) 62 | 63 | 64 | def check_apalache( 65 | tla_file_name: str, 66 | files: Dict[str, str], 67 | args: Dict = {}, 68 | do_parse: bool = True, 69 | do_typecheck: bool = True, 70 | traces_dir: Optional[str] = None, 71 | ) -> CheckResult: 72 | check_logger.debug("# check_apalache") 73 | check_logger.debug(f"- tla_file_name: {tla_file_name}") 74 | check_logger.debug(f"- files: {list(files.keys())}") 75 | check_logger.debug(f"- args: {args}") 76 | check_logger.debug(f"- traces_dir: {traces_dir}") 77 | 78 | if do_parse is True: 79 | parse(tla_file_name, files, args) 80 | 81 | if do_typecheck is True: 82 | typecheck(tla_file_name, files, args) 83 | 84 | json_command = wrap_command( 85 | cmd=const_values.CHECK_CMD, 86 | tla_file_name=tla_file_name, 87 | files=files, 88 | args=args, 89 | ) 90 | check_logger.debug(f"command jar: {json_command['jar']}") 91 | check_logger.debug(f"command args: {json_command['args']}") 92 | check_logger.debug(f"command files: {list(json_command['files'].keys())}") 93 | if json_command["args"][const_values.CONFIG] in json_command["files"]: 94 | check_logger.debug( 95 | f"command config: {json_command['files'][json_command['args'][const_values.CONFIG]]}" 96 | ) 97 | 98 | result = apalache_pure(json=json_command) 99 | check_logger.debug(f"result return_code: {result['return_code']}") 100 | check_logger.debug(f"result shell_cmd: {result['shell_cmd']}") 101 | check_logger.debug(f"result files: {list(result['files'].keys())}") 102 | check_logger.debug(f"result stdout: {result['stdout']}") 103 | 104 | if traces_dir: 105 | trace_paths = apalache_helpers.write_trace_files_to(result, traces_dir) 106 | for trace_path in trace_paths: 107 | check_logger.info(f"Wrote trace file to {trace_path}") 108 | else: 109 | trace_paths = [] 110 | 111 | if result["return_code"] == 0: 112 | return CheckResult(True, trace_paths=trace_paths) 113 | 114 | if APALACHE_STDOUT["CONSTANTS_NOT_INITIALIZED"] in result["stdout"]: 115 | return CheckResult( 116 | False, ErrorMessage("A constant in the model is not initialized") 117 | ) 118 | 119 | config_error = extract_line_with(APALACHE_STDOUT["CONFIG_ERROR"], result["stdout"]) 120 | if config_error: 121 | return CheckResult( 122 | False, ErrorMessage(config_error, error_category="Configuration") 123 | ) 124 | 125 | try: 126 | inv_violated, counterexample = apalache_helpers.extract_counterexample( 127 | result["files"] 128 | ) 129 | except Exception: 130 | check_logger.error( 131 | f"Could not extract counterexample from Apalache output: {result['stdout']}" 132 | ) 133 | raise 134 | 135 | trace = [ITF(state) for state in counterexample] 136 | error_msg = ErrorMessage( 137 | problem_description=f"Invariant {inv_violated} violated", 138 | error_category=const_values.CHECK, 139 | full_error_msg=result["stdout"], 140 | ) 141 | return CheckResult(False, error_msg, trace, trace_paths) 142 | -------------------------------------------------------------------------------- /modelator/checker/simulate.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | from modelator_py.apalache.pure import apalache_pure 4 | 5 | from modelator import const_values 6 | from modelator.utils import apalache_helpers 7 | from modelator.utils.modelator_helpers import create_logger, wrap_command 8 | 9 | from ..itf import ITF 10 | from ..parse import parse 11 | from ..typecheck import typecheck 12 | 13 | simulate_logger = create_logger(logger_name=__file__, loglevel="error") 14 | 15 | 16 | def simulate_apalache( 17 | tla_file_name: str, 18 | files: Dict[str, str], 19 | args: Dict = {}, 20 | do_parse: bool = True, 21 | do_typecheck: bool = True, 22 | traces_dir: Optional[str] = None, 23 | cmd=const_values.SIMULATE_CMD, 24 | ): 25 | simulate_logger.debug("# SIMULATE_apalache") 26 | simulate_logger.debug(f"- tla_file_name: {tla_file_name}") 27 | simulate_logger.debug(f"- files: {list(files.keys())}") 28 | simulate_logger.debug(f"- args: {args}") 29 | simulate_logger.debug(f"- traces_dir: {traces_dir}") 30 | 31 | if do_parse is True: 32 | parse(tla_file_name, files, args) 33 | 34 | if do_typecheck is True: 35 | typecheck(tla_file_name, files, args) 36 | 37 | json_command = wrap_command( 38 | cmd=cmd, 39 | tla_file_name=tla_file_name, 40 | files=files, 41 | args=args, 42 | ) 43 | simulate_logger.debug(f"command jar: {json_command['jar']}") 44 | simulate_logger.debug(f"command args: {json_command['args']}") 45 | simulate_logger.debug(f"command files: {list(json_command['files'].keys())}") 46 | if json_command["args"][const_values.CONFIG] in json_command["files"]: 47 | simulate_logger.debug( 48 | f"command config: {json_command['files'][json_command['args'][const_values.CONFIG]]}" 49 | ) 50 | 51 | result = apalache_pure(json=json_command) 52 | simulate_logger.debug(f"result return_code: {result['return_code']}") 53 | simulate_logger.debug(f"result shell_cmd: {result['shell_cmd']}") 54 | simulate_logger.debug(f"result files: {list(result['files'].keys())}") 55 | simulate_logger.debug(f"result stdout: {result['stdout']}") 56 | 57 | traces_paths = [] 58 | if traces_dir: 59 | trace_paths = apalache_helpers.write_trace_files_to( 60 | result, traces_dir, simulate=True 61 | ) 62 | for trace_path in trace_paths: 63 | simulate_logger.info(f"Wrote trace file to {trace_path}") 64 | traces_paths.append(trace_path) 65 | 66 | simulation_tests = apalache_helpers.extract_simulations(trace_paths=trace_paths) 67 | traces = [[ITF(state) for state in trace] for trace in simulation_tests] 68 | return traces, traces_paths 69 | -------------------------------------------------------------------------------- /modelator/cli/model_config_file.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import toml 4 | 5 | 6 | def load_config_file(config_path): 7 | """ 8 | Load model and model checker configuration from `config_path`, or return a 9 | configuration with default values. 10 | """ 11 | config = {} 12 | if config_path: 13 | if not Path(config_path).is_file(): 14 | raise FileNotFoundError("Config file not found.") 15 | 16 | try: 17 | config = toml.load(config_path) 18 | except Exception as e: 19 | print(f"Error while parsing toml file: {e}") 20 | raise e 21 | 22 | config = _set_default_values(config) 23 | 24 | # use the same key name as in the CLI commands 25 | config["params"] = config["Apalache"] 26 | del config["Apalache"] 27 | 28 | config = _flatten(config) 29 | 30 | return config 31 | 32 | 33 | def _supported_apalache_parameters(): 34 | return [ 35 | # the name of an operator that initializes CONSTANTS, default: None 36 | "cinit", 37 | # configuration file in TLC format 38 | "config", 39 | # maximal number of Next steps; default: 10 40 | "length", 41 | # do not stop on first error, but produce up to a given number of counterexamples (fine tune with --view), default: 1 42 | "max_error", 43 | # do not check for deadlocks; default: false 44 | "no_deadlock", 45 | # save an example trace for each simulated run, default: false 46 | # not supported by modelator-py 47 | # "save_runs", 48 | # the state view to use with --max-error=n, default: transition index 49 | "view", 50 | ] 51 | 52 | 53 | def _set_default_values(config): 54 | """ 55 | Set default values for missing keys in the configuration. 56 | """ 57 | config = {"Model": {}, "Constants": {}, "Config": {}, "Apalache": {}, **config} 58 | 59 | config["Model"] = { 60 | "init": "Init", 61 | "next": "Next", 62 | "invariants": [], 63 | "tests": [], 64 | "config_file_path": None, 65 | **config["Model"], 66 | } 67 | 68 | config["Config"] = { 69 | "traces_dir": None, 70 | **config["Config"], 71 | } 72 | 73 | default_apalache_params = {p: None for p in _supported_apalache_parameters()} 74 | config["Apalache"] = {**default_apalache_params, **config["Apalache"]} 75 | 76 | return config 77 | 78 | 79 | def _flatten(config): 80 | """ 81 | Flatten nested dictionaries. 82 | """ 83 | config = {**config, **config["Model"]} 84 | del config["Model"] 85 | 86 | config["constants"] = config["Constants"] 87 | del config["Constants"] 88 | 89 | config = {**config, **config["Config"]} 90 | del config["Config"] 91 | 92 | return config 93 | -------------------------------------------------------------------------------- /modelator/cli/model_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | from pathlib import Path 4 | from typing import Dict, Optional, Tuple 5 | 6 | from modelator.Model import Model 7 | 8 | # File that stores the serialized object of the current model. 9 | MODEL_FILE_NAME = ".model.pickle" 10 | 11 | 12 | class ModelFile: 13 | """ 14 | Ser/Deserialize a model's instance and optionally a config file into a 15 | pickle file. 16 | """ 17 | 18 | @staticmethod 19 | def load( 20 | log_level: Optional[str] = None, 21 | ) -> Tuple[Optional[Model], Optional[Dict], Optional[str]]: 22 | 23 | if not ModelFile.exists(): 24 | return None, None, None 25 | 26 | with open(MODEL_FILE_NAME, "rb") as f: 27 | data = pickle.load(f) 28 | 29 | try: 30 | model = data["model"] 31 | config = data["config"] 32 | config_path = data["config_path"] 33 | except (KeyError, TypeError) as e: 34 | print(f"Error in saved file: {e}") 35 | return None, None, None 36 | 37 | if log_level: 38 | model.set_log_level(log_level) 39 | 40 | return model, config, config_path 41 | 42 | @staticmethod 43 | def save(model, config=None, config_path=None): 44 | """ 45 | Save serialized model object to a file 46 | """ 47 | data = {"model": model, "config": config, "config_path": config_path} 48 | with open(MODEL_FILE_NAME, "wb") as f: 49 | pickle.dump(data, f) 50 | 51 | @staticmethod 52 | def exists(): 53 | return Path(MODEL_FILE_NAME).is_file() 54 | 55 | @staticmethod 56 | def clean() -> bool: 57 | try: 58 | os.remove(MODEL_FILE_NAME) 59 | return True 60 | except OSError: 61 | return False 62 | -------------------------------------------------------------------------------- /modelator/const_values.py: -------------------------------------------------------------------------------- 1 | # model checkers 2 | import os 3 | 4 | import appdirs 5 | 6 | APALACHE = "apalache" 7 | TLC = "tlc" 8 | 9 | DEFAULT_APALACHE_VERSION = "0.30.1" 10 | DEFAULT_CHECKERS_LOCATION = os.path.join(appdirs.user_data_dir(__package__), "checkers") 11 | DEFAULT_APALACHE_LOCATION = os.path.join(DEFAULT_CHECKERS_LOCATION, "apalache") 12 | 13 | DEFAULT_APALACHE_JAR_FILENAME = f"apalache-{DEFAULT_APALACHE_VERSION}.jar" 14 | DEFAULT_APALACHE_JAR = os.path.join( 15 | DEFAULT_APALACHE_LOCATION, 16 | "lib", 17 | DEFAULT_APALACHE_JAR_FILENAME, 18 | ) 19 | APALACHE_SHA_CHECKSUMS = { 20 | "0.25.0": "41a60d1e2d5ab0f4d523b5821f61e0907d03b00f4d22edb997779722a08800f7", 21 | "0.25.1": "1746f04311e36dfce4289f12ba8fc0a314e1e56ecf83b461e3f1b18114eea5c6", 22 | "0.25.10": "1844511c579891b413377dde18a1d6ac30304a5859d4c3631a8ef02313a2e08d", 23 | } 24 | 25 | APALACHE_DEFAULTS = { 26 | "result_violation_tla_file": "violation.tla", 27 | "result_violation_itf_file": "violation.itf.json", 28 | "trace_name": "violation", 29 | } 30 | 31 | APALACHE_STDOUT = { 32 | "CONSTANTS_NOT_INITIALIZED": "CONSTANTS are not initialized", 33 | "CONFIG_ERROR": "Configuration error", 34 | "INVARIANT_VIOLATION": "InvariantViolation == ", 35 | } 36 | 37 | 38 | def apalache_release_url(expected_version): 39 | return f"https://github.com/informalsystems/apalache/releases/download/v{expected_version}/apalache.zip" 40 | 41 | 42 | def apalache_checksum_url(expected_version: str): 43 | return f"https://github.com/informalsystems/apalache/releases/download/v{expected_version}/sha256sum.txt" 44 | 45 | 46 | DEFAULT_TLC_JAR = "jars/tla2tools-v1.8.0.jar" 47 | 48 | PARSE = "parse" 49 | TYPECHECK = "typecheck" 50 | CHECK = "check" 51 | 52 | PARSE_CMD = "parse" 53 | CHECK_CMD = "check" 54 | TYPECHECK_CMD = "typecheck" 55 | SIMULATE_CMD = "simulate" 56 | 57 | GLOBAL_ARGS = ["features", "out_dir"] 58 | 59 | PARSE_CMD_ARGS = ["output"] 60 | TYPECHECK_CMD_ARGS = ["infer_poly", "output"] 61 | 62 | DEFAULT_INVARIANTS = ["Inv"] 63 | DEFAULT_INIT = "Init" 64 | DEFAULT_NEXT = "Next" 65 | DEFAULT_TRACES_DIR = "./traces/" 66 | 67 | INIT = "init" 68 | NEXT = "next" 69 | INVARIANT = "inv" 70 | APALACHE_NUM_STEPS = "length" 71 | CONFIG = "config" 72 | 73 | SHELL_ACTIVE = False 74 | 75 | 76 | CHECKER_TIMEOUT = 60 77 | -------------------------------------------------------------------------------- /modelator/modelator_shell.py: -------------------------------------------------------------------------------- 1 | # from inspect import trace 2 | # import os 3 | # from typing import List 4 | # from modelator.parse import parse 5 | # from modelator.typecheck import typecheck 6 | # from modelator.check import check_apalache, check_tlc 7 | # from modelator.utils import shell_helpers, tla_helpers 8 | # import traceback 9 | 10 | 11 | from modelator import const_values 12 | from modelator.Model import Model 13 | 14 | const_values.SHELL_ACTIVE = True 15 | 16 | __all__ = ["Model"] 17 | -------------------------------------------------------------------------------- /modelator/monitors/content.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | 4 | from typing_extensions import Self 5 | 6 | from modelator.ModelResult import ModelResult 7 | 8 | 9 | class Status(Enum): 10 | success = "success" 11 | failure = "failure" 12 | inprogress = "inprogress" 13 | unknown = "unknown" 14 | 15 | def __str__(self): 16 | if self == Status.success: 17 | return "✅" 18 | if self == Status.failure: 19 | return "❌" 20 | if self == Status.inprogress: 21 | return "⏳" 22 | return "❓" 23 | 24 | def html_color(self): 25 | if self == Status.success: 26 | return "green" 27 | if self == Status.failure: 28 | return "red" 29 | if self == Status.inprogress: 30 | return "" 31 | return "yellow" 32 | 33 | 34 | class MonitorEntry: 35 | """ 36 | An entry in a section. 37 | """ 38 | 39 | status_position: int = None 40 | 41 | def __init__(self, name: str, status: Status = None, trace=None): 42 | self.name = name 43 | self.status = status 44 | self.trace = trace 45 | 46 | def set_status_position(self, position): 47 | self.status_position = position 48 | 49 | 50 | class MonitorSection: 51 | """ 52 | A section is either a Sample or an Invariant. 53 | """ 54 | 55 | def __init__(self, name: str, entries: list[MonitorEntry], time: datetime): 56 | self.name = name 57 | self.entries = entries 58 | self.start_time = time 59 | self.update_time = None 60 | 61 | @staticmethod 62 | def all_entries_from(res: ModelResult): 63 | inprogress = [MonitorEntry(op, Status.inprogress) for op in res.inprogress()] 64 | successful = [ 65 | MonitorEntry(op, Status.success, trace=res.traces(op)) 66 | for op in res.successful() 67 | ] 68 | unsuccessful = [ 69 | MonitorEntry(op, Status.failure, trace=res.traces(op)) 70 | for op in res.unsuccessful() 71 | ] 72 | entries = inprogress + successful + unsuccessful 73 | return sorted(entries, key=lambda e: e.name) 74 | 75 | def create_from(name: str, res: ModelResult) -> Self: 76 | entries = MonitorSection.all_entries_from(res) 77 | return MonitorSection(name, entries=entries, time=res.time()) 78 | 79 | def update_with(self, res: ModelResult) -> Self: 80 | self.entries = MonitorSection.all_entries_from(res) 81 | self.update_time = res.time() 82 | return self 83 | -------------------------------------------------------------------------------- /modelator/monitors/html_monitor.py: -------------------------------------------------------------------------------- 1 | from modelator.itf import ITF 2 | from modelator.ModelMonitor import ModelMonitor 3 | from modelator.ModelResult import ModelResult 4 | from modelator.monitors.content import MonitorSection 5 | from modelator.monitors.html_writer import HtmlWriter 6 | 7 | 8 | class HtmlModelMonitor(ModelMonitor): 9 | """ 10 | A monitor that writes all model action updates to an HTML file. 11 | """ 12 | 13 | def __init__(self, filename): 14 | super().__init__() 15 | self.writer = HtmlWriter(filename) 16 | 17 | self.title: str = None 18 | self.sections: list[MonitorSection] = [] 19 | 20 | def __enter__(self): 21 | return self 22 | 23 | def __exit__(self, exc_type, exc_value, trace): 24 | self.writer.close() 25 | 26 | def on_parse_start(self, res: ModelResult): 27 | self.title = f"⏳ Parsing {res.model().tla_file_path}..." 28 | self.write_file() 29 | 30 | def on_parse_finish(self, res: ModelResult): 31 | self._update_title(res) 32 | self.write_file() 33 | 34 | def on_sample_start(self, res: ModelResult): 35 | self._update_title(res) 36 | self._add_section(MonitorSection.create_from("Tests", res)) 37 | self.write_file() 38 | 39 | def on_sample_update(self, res: ModelResult): 40 | last_section = self.sections[-1] 41 | self._set_last_section(last_section.update_with(res)) 42 | self.write_file() 43 | 44 | def on_sample_finish(self, res: ModelResult): 45 | last_section = self.sections[-1] 46 | self._set_last_section(last_section.update_with(res)) 47 | self.write_file() 48 | 49 | def on_check_start(self, res: ModelResult): 50 | self._update_title(res) 51 | self._add_section(MonitorSection.create_from("Invariants", res)) 52 | self.write_file() 53 | 54 | def on_check_update(self, res: ModelResult): 55 | last_section = self.sections[-1] 56 | self._set_last_section(last_section.update_with(res)) 57 | self.write_file() 58 | 59 | def on_check_finish(self, res: ModelResult): 60 | last_section = self.sections[-1] 61 | self._set_last_section(last_section.update_with(res)) 62 | self.write_file() 63 | 64 | def write_file(self): 65 | self.writer.write_content(self.title, self.sections) 66 | 67 | def _update_title(self, res): 68 | self.title = res.model().module_name 69 | if res.parsing_error: 70 | self.title = self.title + ": parsing problem!" 71 | 72 | def _add_section(self, section): 73 | self.sections.append(section) 74 | 75 | def _set_last_section(self, section): 76 | index = len(self.sections) - 1 77 | self.sections[index] = section 78 | 79 | 80 | test_traces = { 81 | "Inv2": [ 82 | ITF({"x": 0, "y": 1, "f": '["foo" |-> "spam"]'}), 83 | ITF({"x": 1, "y": 1, "f": '["foo" |-> "spam spam"]'}), 84 | ITF({"x": 2, "y": 100, "f": '["foo" |-> "spam spam eggs"]'}), 85 | ] 86 | } 87 | 88 | 89 | class Model: 90 | tla_file_name = "foo/bar/ModuleName.tla" 91 | module_name = "ModuleName" 92 | 93 | 94 | monitor = None 95 | 96 | 97 | def test_add_section(): 98 | input("Press key to on_check_start") 99 | res = ModelResult(model=Model()) 100 | res._in_progress_operators = ["Inv1", "Inv2", "Inv3"] 101 | monitor.on_check_start(res) 102 | 103 | input("Press key to on_check_update") 104 | res = ModelResult(model=Model()) 105 | res._in_progress_operators = ["Inv1"] 106 | res._successful = ["Inv3"] 107 | res._unsuccessful = ["Inv2"] 108 | res._traces = test_traces 109 | monitor.on_check_update(res) 110 | 111 | input("Press key to on_check_finish") 112 | res = ModelResult(model=monitor) 113 | res._in_progress_operators = [] 114 | res._successful = ["Inv1", "Inv3"] 115 | res._unsuccessful = ["Inv2"] 116 | res._traces = test_traces 117 | monitor.on_check_finish(res) 118 | 119 | 120 | def test(filename): 121 | global monitor 122 | monitor = HtmlModelMonitor(filename) 123 | 124 | input("Press key to on_parse_start") 125 | monitor.on_parse_start(ModelResult(model=Model())) 126 | 127 | input("Press key to on_parse_finish") 128 | monitor.on_parse_finish(ModelResult(model=Model())) 129 | 130 | test_add_section() 131 | test_add_section() 132 | -------------------------------------------------------------------------------- /modelator/monitors/html_writer.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | from typing import Any 3 | 4 | from modelator.itf import ITF 5 | 6 | TEMPLATES_DIR = "modelator/monitors/templates" 7 | TRACE_COLUMNS = ["Variable", "Value", "Next value"] 8 | 9 | 10 | class HtmlWriter: 11 | def __init__(self, filename): 12 | self.filename = filename 13 | self.fd = open(self.filename, "w+", 1) 14 | self.fd.truncate(0) 15 | 16 | def close(self): 17 | self.fd.close() 18 | 19 | def write_content(self, title, sections=[]): 20 | with open(f"{TEMPLATES_DIR}/html_monitor.html", "r") as f: 21 | template = Template(f.read()) 22 | html = template.substitute( 23 | {"title": title, "sections": self._make_html_for(sections)} 24 | ) 25 | self.fd.truncate(0) 26 | self.fd.seek(0) 27 | self.fd.write(html) 28 | self.fd.flush() 29 | 30 | def _make_html_for(self, sections): 31 | with open(f"{TEMPLATES_DIR}/html_section.html", "r") as f: 32 | template = Template(f.read()) 33 | time_format = "%Y-%m-%d %H:%M:%S" 34 | html_sections = [] 35 | is_first_section = True 36 | for section in reversed(sections): 37 | if is_first_section: 38 | section_status = 'open="open"' 39 | is_first_section = False 40 | else: 41 | section_status = "" 42 | html_section = template.substitute( 43 | { 44 | "name": section.name, 45 | "section_status": section_status, 46 | "startTime": section.start_time.strftime(time_format), 47 | "lastUpdate": section.update_time.strftime(time_format) 48 | if section.update_time 49 | else "-", 50 | "sectionEntries": self._make_html_entries_for(section), 51 | } 52 | ) 53 | html_sections.append(html_section) 54 | return "\n".join(html_sections) 55 | 56 | def _make_html_entries_for(self, section): 57 | with open(f"{TEMPLATES_DIR}/html_section_entry.html", "r") as f: 58 | template = Template(f.read()) 59 | entries = [] 60 | for entry in section.entries: 61 | status_color = entry.status.html_color() 62 | status_color_str = "color: " + status_color if status_color else "" 63 | if entry.trace is not None: 64 | html_trace = self._make_html_trace(entry.trace, columns=TRACE_COLUMNS) 65 | else: 66 | html_trace = "" 67 | entries.append( 68 | template.substitute( 69 | { 70 | "status": entry.status, 71 | "status_color": status_color_str, 72 | "name": entry.name, 73 | "trace": html_trace, 74 | } 75 | ) 76 | ) 77 | return "\n".join(entries) 78 | 79 | def _make_html_trace(self, trace: list["ITF"], columns: list[str]) -> str: 80 | with open(f"{TEMPLATES_DIR}/html_trace.html", "r") as f: 81 | template = Template(f.read()) 82 | trace_txs = [] 83 | for ix, step in enumerate(ITF.diff(trace)): 84 | trace_txs.append( 85 | template.substitute( 86 | { 87 | "transition_name": f"State {ix} -> State {ix + 1}", 88 | "transition_variables": self._make_table(step, columns), 89 | } 90 | ) 91 | ) 92 | return "\n".join(trace_txs) 93 | 94 | def _make_table(self, transition: list[tuple[str, Any, Any]], columns: list[str]): 95 | # build header 96 | template_header_cell = Template("$cell") 97 | header = "\n".join( 98 | template_header_cell.substitute({"cell": col}) for col in columns 99 | ) 100 | header = "" + header + "" 101 | 102 | # build body 103 | template_body_cell = Template("$cell") 104 | rows = [] 105 | for (varname, value1, value2) in transition: 106 | row = "\n".join( 107 | [ 108 | template_body_cell.substitute({"cell": varname}), 109 | template_body_cell.substitute( 110 | {"cell": HtmlWriter._replace_special_characters(value1)} 111 | ), 112 | template_body_cell.substitute( 113 | {"cell": HtmlWriter._replace_special_characters(value2)} 114 | ), 115 | ] 116 | ) 117 | rows.append("" + row + "") 118 | rows = "\n".join(rows) 119 | 120 | with open(f"{TEMPLATES_DIR}/html_table.html", "r") as f: 121 | template = Template(f.read()) 122 | return template.substitute({"header": header, "body": rows}) 123 | 124 | @staticmethod 125 | def _replace_special_characters(s: Any) -> Any: 126 | if isinstance(s, str): 127 | return s.replace("|->", "↦").replace("<", "<").replace(">", ">") 128 | else: 129 | return s 130 | -------------------------------------------------------------------------------- /modelator/monitors/markdown_monitor.py: -------------------------------------------------------------------------------- 1 | from modelator.itf import ITF 2 | from modelator.ModelMonitor import ModelMonitor 3 | from modelator.ModelResult import ModelResult 4 | from modelator.monitors.content import MonitorSection 5 | from modelator.monitors.markdown_writer import MarkdownWriter 6 | 7 | 8 | class MarkdownModelMonitor(ModelMonitor): 9 | """ 10 | A monitor that writes all model action updates to a Markdown file. 11 | """ 12 | 13 | def __init__(self, filename): 14 | super().__init__() 15 | self.writer = MarkdownWriter(filename) 16 | 17 | self.title: str = None 18 | self.sections: list[MonitorSection] = [] 19 | 20 | def __enter__(self): 21 | return self 22 | 23 | def __exit__(self, exc_type, exc_value, trace): 24 | self.writer.close() 25 | 26 | def on_parse_start(self, res: ModelResult): 27 | self.title = f"⏳ Parsing {res.model().tla_file_path}..." 28 | self.write_file() 29 | 30 | def on_parse_finish(self, res: ModelResult): 31 | self._update_title(res) 32 | self.write_file() 33 | 34 | def on_sample_start(self, res: ModelResult): 35 | self._update_title(res) 36 | self._add_section(MonitorSection.create_from("Tests", res)) 37 | self.write_file() 38 | 39 | def on_sample_update(self, res: ModelResult): 40 | last_section = self.sections[-1] 41 | self._set_last_section(last_section.update_with(res)) 42 | self.write_file() 43 | 44 | def on_sample_finish(self, res: ModelResult): 45 | last_section = self.sections[-1] 46 | self._set_last_section(last_section.update_with(res)) 47 | self.write_file() 48 | 49 | def on_check_start(self, res: ModelResult): 50 | self._update_title(res) 51 | self._add_section(MonitorSection.create_from("Invariants", res)) 52 | self.write_file() 53 | 54 | def on_check_update(self, res: ModelResult): 55 | last_section = self.sections[-1] 56 | self._set_last_section(last_section.update_with(res)) 57 | self.write_file() 58 | 59 | def on_check_finish(self, res: ModelResult): 60 | last_section = self.sections[-1] 61 | self._set_last_section(last_section.update_with(res)) 62 | self.write_file() 63 | 64 | def write_file(self): 65 | self.writer.write_content(self.title, self.sections) 66 | 67 | def _update_title(self, res): 68 | self.title = res.model().module_name 69 | if res.parsing_error: 70 | self.title = self.title + ": parsing problem!" 71 | 72 | def _add_section(self, section): 73 | self.sections.append(section) 74 | 75 | def _set_last_section(self, section): 76 | index = len(self.sections) - 1 77 | self.sections[index] = section 78 | 79 | 80 | test_traces = { 81 | "Inv2": [ 82 | ITF({"x": 0, "y": 1, "f": '["foo" |-> "spam"]'}), 83 | ITF({"x": 1, "y": 1, "f": '["foo" |-> "spam spam"]'}), 84 | ITF({"x": 2, "y": 100, "f": '["foo" |-> "spam spam eggs"]'}), 85 | ] 86 | } 87 | 88 | 89 | class Model: 90 | tla_file_name = "foo/bar/ModuleName.tla" 91 | module_name = "ModuleName" 92 | 93 | 94 | monitor = None 95 | 96 | 97 | def test_add_section(): 98 | input("Press key to on_check_start") 99 | res = ModelResult(model=Model()) 100 | res._in_progress_operators = ["Inv1", "Inv2", "Inv3"] 101 | monitor.on_check_start(res) 102 | 103 | input("Press key to on_check_update") 104 | res = ModelResult(model=Model()) 105 | res._in_progress_operators = ["Inv1"] 106 | res._successful = ["Inv3"] 107 | res._unsuccessful = ["Inv2"] 108 | res._traces = test_traces 109 | monitor.on_check_update(res) 110 | 111 | input("Press key to on_check_finish") 112 | res = ModelResult(model=monitor) 113 | res._in_progress_operators = [] 114 | res._successful = ["Inv1", "Inv3"] 115 | res._unsuccessful = ["Inv2"] 116 | res._traces = test_traces 117 | monitor.on_check_finish(res) 118 | 119 | 120 | def test(filename): 121 | global monitor 122 | monitor = MarkdownModelMonitor(filename) 123 | 124 | input("Press key to on_parse_start") 125 | monitor.on_parse_start(ModelResult(model=Model())) 126 | 127 | input("Press key to on_parse_finish") 128 | monitor.on_parse_finish(ModelResult(model=Model())) 129 | 130 | test_add_section() 131 | test_add_section() 132 | -------------------------------------------------------------------------------- /modelator/monitors/markdown_writer.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from modelator.itf import ITF 4 | 5 | TRACE_COLUMNS = ["Variable", "Value", "Next value"] 6 | 7 | 8 | class MarkdownWriter: 9 | def __init__(self, filename): 10 | self.filename = filename 11 | self.fd = open(self.filename, "w+", 1) 12 | self.fd.truncate(0) 13 | 14 | def close(self): 15 | self.fd.close() 16 | 17 | def write_content(self, title, sections=[]): 18 | self.fd.truncate(0) 19 | self.fd.seek(0) 20 | 21 | self.fd.write(f"# {title}\n\n") 22 | self.fd.flush() 23 | 24 | for section in reversed(sections): 25 | self._write_section(section) 26 | self.fd.flush() 27 | 28 | def _write_section(self, section): 29 | time_format = "%Y-%m-%d %H:%M:%S" 30 | self.fd.write("---\n") 31 | self.fd.write(f"## {section.name}\n\n") 32 | self.fd.write(f"- Start time: {section.start_time.strftime(time_format)}\n") 33 | update_time = ( 34 | section.update_time.strftime(time_format) if section.update_time else "-" 35 | ) 36 | self.fd.write(f"- Last update: {update_time}\n") 37 | self.fd.write("\n") 38 | 39 | for entry in section.entries: 40 | self.fd.write("### ") 41 | entry.status_position = self.fd.tell() 42 | self.fd.write(f"{str(entry.status)} {entry.name}\n\n") 43 | if entry.trace is not None: 44 | self._write_trace(entry.trace, columns=TRACE_COLUMNS) 45 | 46 | def _write_trace(self, trace, columns): 47 | for ix, step in enumerate(ITF.diff(trace)): 48 | self.fd.write(f"State {ix} -> State {ix + 1}\n\n") 49 | for row in self._make_table(step, columns): 50 | self.fd.write(row) 51 | self.fd.write("\n\n") 52 | 53 | def _make_table( 54 | self, 55 | transition: list[tuple[str, Any, Any]], 56 | columns: list[str], 57 | column_width: int = 25, 58 | ): 59 | rows = [] 60 | 61 | # build header 62 | column_names = "|".join(s.center(column_width) for s in columns) 63 | separator = "|".join("-" * column_width for _ in range(len(columns))) 64 | rows.append(f"|{column_names}|\n") 65 | rows.append(f"|{separator}|\n") 66 | 67 | # build body 68 | for (varname, value1, value2) in transition: 69 | row = "|".join( 70 | [ 71 | varname.center(column_width), 72 | MarkdownWriter._replace_special_characters(value1).center( 73 | column_width 74 | ), 75 | MarkdownWriter._replace_special_characters(value2).center( 76 | column_width 77 | ), 78 | ] 79 | ) 80 | rows.append(f"|{row}|\n") 81 | 82 | return rows 83 | 84 | @staticmethod 85 | def _replace_special_characters(s: Any) -> str: 86 | if isinstance(s, str): 87 | return s.replace("|->", "↦") 88 | else: 89 | return str(s) 90 | -------------------------------------------------------------------------------- /modelator/monitors/templates/html_monitor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $title 5 | 6 | 7 |

$title

8 |
9 | 10 | $sections 11 | 12 | 13 | -------------------------------------------------------------------------------- /modelator/monitors/templates/html_section.html: -------------------------------------------------------------------------------- 1 |
2 | $name 3 |
    4 |
  • Start time: $startTime
  • 5 |
  • Last update: $lastUpdate
  • 6 |
7 | $sectionEntries 8 |
9 |
10 | -------------------------------------------------------------------------------- /modelator/monitors/templates/html_section_entry.html: -------------------------------------------------------------------------------- 1 |

$status $name

2 | $trace 3 | -------------------------------------------------------------------------------- /modelator/monitors/templates/html_table.html: -------------------------------------------------------------------------------- 1 | 2 | $header $body 3 |
4 | -------------------------------------------------------------------------------- /modelator/monitors/templates/html_trace.html: -------------------------------------------------------------------------------- 1 |

$transition_name

2 |

$transition_variables

3 | -------------------------------------------------------------------------------- /modelator/parse.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | from modelator_py.apalache.pure import apalache_pure 5 | 6 | from modelator.utils.apalache_helpers import extract_parse_error 7 | from modelator.utils.model_exceptions import ModelParsingError 8 | from modelator.utils.modelator_helpers import wrap_command 9 | 10 | 11 | def parse(tla_file_name: str, files: Dict[str, str], args: Dict[str, str]): 12 | """ 13 | Call Apalache's parser. Return nothing if ok, otherwise raise a 14 | ModelParsingError. 15 | """ 16 | 17 | json_command = wrap_command("parse", tla_file_name, files, args=args) 18 | result = apalache_pure(json=json_command) 19 | 20 | if result["return_code"] != 0: 21 | try: 22 | error, error_file, line_number = extract_parse_error(result["stdout"]) 23 | except Exception as e: 24 | error = f"Unknown error:\n{e}" 25 | error_file = None 26 | line_number = None 27 | 28 | if error_file: 29 | dir = os.path.dirname(tla_file_name) 30 | error_file_name = os.path.join(dir, error_file) 31 | else: 32 | error_file_name = tla_file_name 33 | 34 | raise ModelParsingError( 35 | problem_description=error, 36 | location=line_number, 37 | full_error_msg=result["stdout"], 38 | file_path=os.path.abspath(error_file_name), 39 | ) 40 | -------------------------------------------------------------------------------- /modelator/pytest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/modelator/pytest/__init__.py -------------------------------------------------------------------------------- /modelator/samples/AlarmClock.tla: -------------------------------------------------------------------------------- 1 | ----- MODULE AlarmClock ----- 2 | 3 | EXTENDS Naturals, Sequences 4 | 5 | VARIABLES 6 | \* @typeAlias: state = { hr: Int, alarmHr: Int, alarmOn: Bool }; 7 | \* @type: Int; 8 | hr, 9 | \* @type: Int; 10 | alarmHr, 11 | \* @type: Bool; 12 | alarmOn 13 | 14 | Init == 15 | /\ hr \in (1 .. 12) 16 | /\ alarmHr \in (1..12) 17 | /\ alarmOn = FALSE 18 | AdvanceHour == 19 | /\ hr' = IF hr /= 12 THEN hr + 1 ELSE 1 20 | /\ UNCHANGED <> 21 | SetAlarm == 22 | /\ alarmHr' \in (1..12) 23 | \* Oops, forgot to set alarmOn' = TRUE 24 | /\ UNCHANGED <> 25 | Ring == 26 | /\ alarmOn \* Oops, alarmOn is always FALSE 27 | /\ hr = alarmHr 28 | /\ alarmOn' = FALSE 29 | /\ UNCHANGED <> 30 | 31 | Next == 32 | \/ AdvanceHour 33 | \/ SetAlarm 34 | \/ Ring 35 | 36 | ============================ 37 | -------------------------------------------------------------------------------- /modelator/samples/Hello.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | INVARIANT Inv2 4 | -------------------------------------------------------------------------------- /modelator/samples/Hello.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences, HelloInv 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 22 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = y-2 18 | 19 | 20 | 21 | =========================================== 22 | -------------------------------------------------------------------------------- /modelator/samples/HelloFlawed.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloFlawed ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y, 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /modelator/samples/HelloFlawedType.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloFlawedType ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Str; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /modelator/samples/HelloFull.config.toml: -------------------------------------------------------------------------------- 1 | [Model] 2 | init = "Init" 3 | # next = "Next" 4 | invariants = ["Inv", "AlwaysEvenInvariant"] 5 | tests = ["ExTest", "ExFail"] 6 | 7 | [Config] 8 | # location for the generated trace files 9 | traces_dir = "modelator/samples/traces/HelloFull" 10 | 11 | [Apalache] 12 | # the name of an operator that initializes CONSTANTS, default: None 13 | # cinit = "CInit" 14 | 15 | # configuration file in TLC format 16 | # config = "path/to/config.cfg" 17 | 18 | # maximal number of Next steps; default: 10 19 | length = 100 20 | 21 | # do not stop on first error, but produce up to a given number of counterexamples (fine tune with --view), default: 1 22 | # max_error = 5 23 | 24 | # do not check for deadlocks; default: false 25 | # no_deadlock = false 26 | 27 | # the state view to use with --max-error=n, default: transition index 28 | view = "View" 29 | -------------------------------------------------------------------------------- /modelator/samples/HelloFull.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloFull ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Int; 7 | x, 8 | \* @type: Str; 9 | y 10 | 11 | Init == 12 | /\ x = 1400 13 | /\ y = "hello" 14 | 15 | Next == 16 | /\ x' = x-2 17 | /\ y' = "world" 18 | 19 | 20 | Inv == 21 | x /= 1396 22 | 23 | AlwaysEvenInvariant == 24 | x % 2 = 0 25 | 26 | 27 | 28 | ExTest == 29 | x = 1394 30 | 31 | 32 | 33 | =========================================== 34 | -------------------------------------------------------------------------------- /modelator/samples/HelloFull1.itf.json: -------------------------------------------------------------------------------- 1 | { 2 | "#meta": { 3 | "format": "ITF", 4 | "format-description": "https://apalache.informal.systems/docs/adr/015adr-trace.html", 5 | "description": "Created by Apalache on Tue Jun 14 22:36:01 CEST 2022" 6 | }, 7 | "vars": ["x", "y"], 8 | "states": [ 9 | { 10 | "#meta": { 11 | "index": 0 12 | }, 13 | "x": 1400, 14 | "y": "hello" 15 | }, 16 | { 17 | "#meta": { 18 | "index": 1 19 | }, 20 | "x": 1398, 21 | "y": "world" 22 | }, 23 | { 24 | "#meta": { 25 | "index": 2 26 | }, 27 | "x": 1396, 28 | "y": "world" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /modelator/samples/HelloInv.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloInv ------------- 2 | VARIABLES 3 | \* @type: Str; 4 | x, 5 | \* @type: Str; 6 | y 7 | 8 | 9 | Inv == 10 | ~ 11 | ( 12 | /\ x = "world" 13 | /\ y = 20 14 | ) 15 | 16 | Inv2 == 17 | y = 10 18 | 19 | 20 | ========= 21 | -------------------------------------------------------------------------------- /modelator/samples/HelloWorld.tla: -------------------------------------------------------------------------------- 1 | ----- MODULE HelloWorld ----- 2 | 3 | EXTENDS Naturals 4 | CONSTANTS 5 | \* @type: Int; 6 | MAX 7 | VARIABLES 8 | \* @type: Int; 9 | x 10 | Init == x = 0 11 | Next == x <= MAX /\ x' = x + 1 12 | Inv == x >= 0 /\ x<= MAX 13 | 14 | ============================= 15 | -------------------------------------------------------------------------------- /modelator/samples/HourClock.tla: -------------------------------------------------------------------------------- 1 | ----- MODULE HourClock ----- 2 | 3 | EXTENDS Naturals, Sequences 4 | VARIABLES 5 | \* @typeAlias: state = { hr: Int }; 6 | \* @type: Int; 7 | hr 8 | Init == hr \in (1..12) 9 | Next == hr' = IF hr /= 12 THEN hr + 1 ELSE 1 10 | 11 | ============================ 12 | -------------------------------------------------------------------------------- /modelator/samples/HourClockTraits.tla: -------------------------------------------------------------------------------- 1 | ----- MODULE HourClockTraits ----- 2 | 3 | EXTENDS HourClock 4 | 5 | ExThreeHours == hr = 3 6 | 7 | ExHourDecrease == hr' < hr 8 | 9 | \* @type: Seq($state) => Bool; 10 | ExFullRun(trace) == 11 | /\ Len(trace) = 12 12 | /\ \A s1, s2 \in DOMAIN trace: 13 | s1 /= s2 => trace[s1].hr /= trace[s2].hr 14 | 15 | 16 | InvNoOverflow == hr <= 12 17 | InvNoUnderflow == hr >= 1 18 | 19 | InvExThreeHours == ~ExThreeHours 20 | InvExHourDecrease == ~ExHourDecrease 21 | 22 | \* @type: Seq($state) => Bool; 23 | InvExFullRun(trace) == ~ExFullRun(trace) 24 | 25 | ================================== 26 | -------------------------------------------------------------------------------- /modelator/samples/helloConfig.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/modelator/samples/helloConfig.json -------------------------------------------------------------------------------- /modelator/samples/helloModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileName": "Hello.tla", 3 | "fileContent": "------------ MODULE Hello -------------\n\nEXTENDS Naturals, FiniteSets, Sequences\n\nVARIABLES\n \\* @type: Str;\n x,\n \\* @type: Int;\n y\n\nInit ==\n /\\ x = \"hello\"\n /\\ y = 42\n\nNext ==\n /\\ x' = IF x = \"hello\" THEN \"world\" ELSE \"hello\"\n /\\ y' = 42-y\n\nInv ==\n ~\n (\n /\\ x = \"world\"\n /\\ y = 0\n )\n\n===========================================\n" 4 | } 5 | -------------------------------------------------------------------------------- /modelator/typecheck.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | from modelator_py.apalache.pure import apalache_pure 5 | 6 | from modelator import const_values 7 | from modelator.utils.apalache_helpers import extract_typecheck_error 8 | from modelator.utils.model_exceptions import ModelTypecheckingError 9 | from modelator.utils.modelator_helpers import wrap_command 10 | 11 | 12 | def typecheck(tla_file_name: str, files: Dict[str, str], args: Dict[str, str]): 13 | """ 14 | Call Apalache's typechecker. Return nothing if ok, otherwise raise a 15 | ModelParsingError. 16 | """ 17 | json_command = wrap_command( 18 | const_values.TYPECHECK_CMD, tla_file_name, files, args=args 19 | ) 20 | result = apalache_pure(json=json_command) 21 | 22 | if result["return_code"] != 0: 23 | try: 24 | error, error_file, line_number = extract_typecheck_error(result["stdout"]) 25 | except Exception as e: 26 | error = f"Unknown error:\n{e}" 27 | error_file = None 28 | line_number = None 29 | 30 | if error_file: 31 | files_dir = os.path.dirname(tla_file_name) 32 | error_file_name = os.path.join(files_dir, error_file) 33 | else: 34 | error_file_name = tla_file_name 35 | 36 | raise ModelTypecheckingError( 37 | problem_description=error, 38 | location=line_number, 39 | full_error_msg=result["stdout"], 40 | file_path=os.path.abspath(error_file_name), 41 | ) 42 | -------------------------------------------------------------------------------- /modelator/utils/ErrorMessage.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class ErrorMessage: 7 | problem_description: str 8 | location: Optional[int] = None 9 | full_error_msg: str = "" 10 | file_path: str = "" 11 | error_category: str = "" 12 | 13 | def __str__(self) -> str: 14 | if self.location: 15 | locationInfo = ":" + str(self.location) 16 | else: 17 | locationInfo = "" 18 | 19 | kind = self.error_category.capitalize() 20 | location = f"{self.file_path}{locationInfo}" 21 | 22 | s = "" 23 | if kind: 24 | s += f"{kind} error" 25 | if location: 26 | if not kind: 27 | s += "error" 28 | s += f" at {location}" 29 | if s: 30 | s += ":\n" 31 | s += self.problem_description 32 | 33 | return s 34 | -------------------------------------------------------------------------------- /modelator/utils/apalache_helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | from pathlib import Path 5 | from typing import Dict, List 6 | 7 | from modelator.const_values import APALACHE_DEFAULTS, APALACHE_STDOUT 8 | from modelator.utils.modelator_helpers import extract_line_with 9 | 10 | 11 | def extract_tla_module_name(tla_file_content: str): 12 | match = re.search(r"-+[ ]*MODULE[ ]*(?P\w+)[ ]*-+", tla_file_content) 13 | if match is None: 14 | return None 15 | return match.group("moduleName") 16 | 17 | 18 | def write_trace_files_to( 19 | apalache_result: Dict, traces_dir: str, simulate: bool = False 20 | ) -> List[str]: 21 | # create directory if it does not exist 22 | Path(traces_dir).mkdir(parents=True, exist_ok=True) 23 | 24 | itfs_filenames_pattern = re.compile(r"\d\.itf\.json$") 25 | itfs_filenames = [ 26 | f for f in apalache_result["files"].keys() if itfs_filenames_pattern.search(f) 27 | ] 28 | itfs_filenames.sort() 29 | if simulate is True and len(itfs_filenames) > 0: 30 | # have to filter out the "example0.itf.json" because in the simulation mode and older versions 31 | # of Apalache (e.g., current Modelator's default, 0.25.10), an additional examples under the name 32 | # "example0.itf.json" is generated. 33 | itfs_filenames = [f for f in itfs_filenames if not f == "example0.itf.json"] 34 | 35 | trace_paths = [] 36 | for filename in itfs_filenames: 37 | 38 | path = os.path.join(traces_dir, filename) 39 | 40 | if Path(path).is_file(): 41 | print(f"WARNING: existing file will be overwritten: {path}") 42 | 43 | with open(path, "w+") as f: 44 | f.write(apalache_result["files"][filename]) 45 | trace_paths.append(path) 46 | 47 | return trace_paths 48 | 49 | 50 | def extract_simulations(trace_paths: List[str]): 51 | itf_tests = [] 52 | for path in trace_paths: 53 | with open(path) as simulation_content: 54 | simulation_json = json.load(simulation_content)["states"] 55 | itf_tests.append(simulation_json) 56 | 57 | return itf_tests 58 | 59 | 60 | def extract_counterexample( 61 | files: Dict[str, str], trace_name=APALACHE_DEFAULTS["trace_name"] 62 | ): 63 | 64 | tla_file_name = trace_name + ".tla" 65 | cex_tla_content = files[tla_file_name] 66 | 67 | violated_invariant = extract_line_with( 68 | APALACHE_STDOUT["INVARIANT_VIOLATION"], cex_tla_content 69 | ) 70 | 71 | itf_file_name = trace_name + ".itf.json" 72 | itf_file_content = files[itf_file_name] 73 | counterexample = json.loads(itf_file_content)["states"] 74 | 75 | return (violated_invariant, counterexample) 76 | 77 | 78 | def extract_parse_error(parser_output: str): 79 | report = [] 80 | reportActive = False 81 | line_number = None 82 | file_name = None 83 | for line in parser_output.splitlines(): 84 | # this will trigger for every parsed file, but the last update will be before the error happens 85 | if line.startswith("Parsing file "): 86 | # taking only the file name, because apalache runs in a temporary directory, 87 | # so all the info about absolute paths is useless 88 | file_name = line[len("Parsing file ") :].split("/")[-1] 89 | if line == "Residual stack trace follows:": 90 | break 91 | 92 | if reportActive is True: 93 | report.append(line) 94 | match = re.search(r"at line (?P\d+)", line) 95 | if match is not None: 96 | line_number = int(match.group("lineNumber")) 97 | 98 | if line == "***Parse Error***": 99 | reportActive = True 100 | 101 | if len(report) == 0: 102 | return (None, None, None) 103 | else: 104 | return ("\n".join(report), file_name, line_number) 105 | 106 | 107 | def extract_typecheck_error(parser_output: str): 108 | report = [] 109 | reportActive = False 110 | line_number = None 111 | file_name = None 112 | for line in parser_output.splitlines(): 113 | 114 | if reportActive is True and ( 115 | "Snowcat asks you to fix the types" in line or "It took me" in line 116 | ): 117 | break 118 | 119 | if reportActive is True: 120 | # find error reports which point to exact locations 121 | match_exact_loc = re.search( 122 | r"\[(?P\w+\.tla):(?P\d+):.+\]: (?P.+) E@.+", 123 | line, 124 | ) 125 | if match_exact_loc is not None: 126 | info = match_exact_loc["info"] 127 | file_name = match_exact_loc["fileName"] 128 | if line_number is None: 129 | line_number = match_exact_loc["lineNumber"] 130 | 131 | else: 132 | match_general_typing_error = re.search( 133 | "Typing input error:(?P.+) E@.+", line 134 | ) 135 | info = match_general_typing_error["info"].strip() 136 | line_number = "" 137 | 138 | report.append(info) 139 | if "Running Snowcat" in line: 140 | reportActive = True 141 | 142 | if len(report) == 0: 143 | return None, None, None 144 | else: 145 | return ("\n".join(report), file_name, line_number) 146 | -------------------------------------------------------------------------------- /modelator/utils/apalache_jar.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import io 3 | import logging 4 | import os 5 | import subprocess 6 | import zipfile 7 | from pathlib import Path 8 | from urllib.error import HTTPError 9 | from urllib.request import urlopen 10 | 11 | from .. import const_values 12 | 13 | 14 | def apalache_jar_build_path(checkers_dir: str, version: str) -> str: 15 | return os.path.join( 16 | checkers_dir, 17 | "apalache", 18 | "lib", 19 | f"apalache-{version}.jar", 20 | ) 21 | 22 | 23 | def apalache_jar_version(jar_path=const_values.DEFAULT_APALACHE_JAR) -> str: 24 | """ 25 | Return the version of the `jar_path`. 26 | """ 27 | try: 28 | version = subprocess.check_output( 29 | ["java", "-jar", jar_path, "version"], text=True, stderr=subprocess.STDOUT 30 | ).strip() 31 | return version 32 | except subprocess.CalledProcessError: 33 | logging.warning("Error while checking the existence of the jar file") 34 | return None 35 | 36 | 37 | def apalache_jar_exists(jar_path: str, expected_version: str) -> bool: 38 | """ 39 | Check for existence of the `expected_version` of the Apalache uber jar at 40 | the location `jar_path`. 41 | """ 42 | 43 | logging.debug(f"Checking for jar file at {jar_path}") 44 | if not Path(jar_path).is_file(): 45 | return False 46 | 47 | version = apalache_jar_version(jar_path) 48 | if version is None: 49 | return False 50 | 51 | if version != expected_version: 52 | logging.debug( 53 | f"Current existing version is {version} and we are looking for {expected_version}" 54 | ) 55 | return False 56 | 57 | return True 58 | 59 | 60 | def apalache_jar_download( 61 | download_location=const_values.DEFAULT_CHECKERS_LOCATION, 62 | expected_version=const_values.DEFAULT_APALACHE_VERSION, 63 | sha256_checksum=None, 64 | ): 65 | """ 66 | Download the `expected_version` of Apalache's uber jar release file to `download_location`. 67 | Raise an exception if the checksum for the expected version is missing. 68 | """ 69 | if sha256_checksum is None: 70 | try: 71 | sha256_checksum = const_values.APALACHE_SHA_CHECKSUMS[expected_version] 72 | except KeyError as k_err: 73 | checksum_url = const_values.apalache_checksum_url(expected_version) 74 | try: 75 | with urlopen(checksum_url) as zip_response: 76 | for line in zip_response.readlines(): 77 | if b"apalache.zip" in line: 78 | sha256_checksum, _ = line.decode().split() 79 | if sha256_checksum is None: 80 | raise ValueError( 81 | "SHA Checksum is missing from Apalache release. Check Apalache repository to debug." 82 | ) from k_err 83 | except HTTPError as h_err: 84 | if h_err.code == 404: 85 | raise ValueError( 86 | "SHA Checksum is missing. Add it manually to `const_values.APALACHE_SHA_CHECKSUMS`" 87 | ) from h_err 88 | raise ValueError( 89 | f"Error while fetching the checksum: ({h_err.code}) {h_err.reason}" 90 | ) from h_err 91 | 92 | release_url = const_values.apalache_release_url(expected_version) 93 | 94 | logging.debug(f"Downloading {release_url} to {download_location}...") 95 | with urlopen(release_url) as zip_response: 96 | data = zip_response.read() 97 | assert sha256_checksum == hashlib.sha256(data).hexdigest() 98 | 99 | with zipfile.ZipFile(io.BytesIO(data)) as zip_file: 100 | jar_relative_path = "apalache/lib/apalache.jar" 101 | zip_file.extract(member=jar_relative_path, path=download_location) 102 | extracted_jar_path = f"{download_location}/{jar_relative_path}" 103 | 104 | final_jar_path = apalache_jar_build_path( 105 | download_location, expected_version 106 | ) 107 | os.rename(extracted_jar_path, final_jar_path) 108 | logging.debug(f"Downloaded version {expected_version} to {final_jar_path}") 109 | -------------------------------------------------------------------------------- /modelator/utils/helpers.py: -------------------------------------------------------------------------------- 1 | # removes the prefix `prefix` from `text`. 2 | # Implemented here as a helper fucntion in order to support Python 3.8 3 | def remove_prefix(text, prefix): 4 | if text.startswith(prefix): 5 | return text[len(prefix) :] 6 | return text 7 | -------------------------------------------------------------------------------- /modelator/utils/model_exceptions.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from .. import const_values 4 | 5 | 6 | @dataclass 7 | class ModelError(Exception): 8 | problem_description: str 9 | location: int = None 10 | full_error_msg: str = "" 11 | file_path: str = "" 12 | error_category: str = "" 13 | 14 | def __str__(self) -> str: 15 | if self.location is not None: 16 | locationInfo = ":" + str(self.location) 17 | else: 18 | locationInfo = "" 19 | error_message = "{kind} error at {path}{lineNum}:\n{errorContent}".format( 20 | kind=self.error_category.capitalize(), 21 | path=self.file_path, 22 | lineNum=locationInfo, 23 | errorContent=self.problem_description, 24 | ) 25 | 26 | return error_message 27 | 28 | 29 | class ModelCheckingError(ModelError): 30 | def __init__(self, exc): 31 | raise exc 32 | 33 | 34 | class ModelParsingError(ModelError): 35 | def __init__(self, problem_description, location, full_error_msg, file_path): 36 | super().__init__( 37 | problem_description, 38 | location, 39 | full_error_msg, 40 | file_path, 41 | error_category=const_values.PARSE, 42 | ) 43 | 44 | 45 | class ModelTypecheckingError(ModelError): 46 | def __init__(self, problem_description, location, full_error_msg, file_path): 47 | super().__init__( 48 | problem_description, 49 | location, 50 | full_error_msg, 51 | file_path, 52 | error_category=const_values.TYPECHECK, 53 | ) 54 | -------------------------------------------------------------------------------- /modelator/utils/modelator_helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | from typing import Dict, Optional 5 | 6 | from .. import const_values 7 | 8 | 9 | def create_logger(logger_name, loglevel): 10 | logger = logging.getLogger(logger_name) 11 | numeric_level = getattr(logging, loglevel.upper(), None) 12 | if not isinstance(numeric_level, int): 13 | raise ValueError("Invalid log level: %s" % loglevel) 14 | logger.setLevel(numeric_level) 15 | 16 | # create console handler and set level to debug 17 | ch = logging.StreamHandler() 18 | ch.setLevel(logging.DEBUG) 19 | 20 | # create formatter 21 | formatter = logging.Formatter( 22 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S" 23 | ) 24 | 25 | # add formatter to ch 26 | ch.setFormatter(formatter) 27 | 28 | # add ch to logger 29 | # TODO: clarify why adding this line adds double logger 30 | logger.addHandler(ch) 31 | 32 | return logger 33 | 34 | 35 | def wrap_command( 36 | cmd: str, 37 | tla_file_name: str, 38 | files: Dict[str, str], 39 | checker: str = const_values.APALACHE, 40 | args: Dict = None, 41 | num_workers: int = 4, 42 | ): 43 | 44 | json_command = {} 45 | json_command["args"] = {} 46 | 47 | # TODO: come up with a more systematic way of setting defaults when they would make more sense for an end user 48 | # (such as here, where Apalache default for nworkers is 1) --> maybe inside shell, at the very frontend? 49 | 50 | # this is necessary: sending an invalid argument to apalache commands will result 51 | # in an exception 52 | if cmd == const_values.CHECK_CMD: 53 | if checker == const_values.APALACHE: 54 | json_command["args"]["nworkers"] = num_workers 55 | 56 | else: 57 | json_command["args"]["workers"] = "auto" 58 | 59 | if cmd == const_values.CHECK_CMD: 60 | tla_module_name = tla_file_name.split(".")[0] 61 | config_file_name = tla_module_name + ".cfg" 62 | if config_file_name in files: 63 | json_command["args"][const_values.CONFIG] = config_file_name 64 | 65 | if args is not None: 66 | for arg in args: 67 | json_command["args"][arg] = args[arg] 68 | 69 | if cmd == const_values.PARSE_CMD: 70 | not_accepted_args = [ 71 | a 72 | for a in json_command["args"] 73 | if a not in const_values.PARSE_CMD_ARGS 74 | and a not in const_values.GLOBAL_ARGS 75 | ] 76 | for e in not_accepted_args: 77 | json_command["args"].pop(e) 78 | 79 | if cmd == const_values.TYPECHECK_CMD: 80 | not_accepted_args = [ 81 | a 82 | for a in json_command["args"] 83 | if a not in const_values.TYPECHECK_CMD_ARGS 84 | and a not in const_values.GLOBAL_ARGS 85 | ] 86 | for e in not_accepted_args: 87 | json_command["args"].pop(e) 88 | 89 | json_command["args"]["file"] = os.path.basename(tla_file_name) 90 | 91 | # send only basenames of files to modelator-py 92 | json_command["files"] = {os.path.basename(f): files[f] for f in files} 93 | 94 | if checker == const_values.TLC: 95 | json_command["jar"] = os.path.abspath(const_values.DEFAULT_TLC_JAR) 96 | else: 97 | json_command["jar"] = os.path.abspath(const_values.DEFAULT_APALACHE_JAR) 98 | 99 | if checker == const_values.APALACHE: 100 | json_command["args"]["cmd"] = cmd 101 | 102 | return json_command 103 | 104 | 105 | def extract_line_with(prefix, text) -> Optional[str]: 106 | """ 107 | Extract from text the line that starts with the given prefix, without 108 | including the prefix. 109 | """ 110 | try: 111 | return re.search(f"^{prefix}(.*)$", text, re.MULTILINE).group(1) 112 | except AttributeError: 113 | return None 114 | -------------------------------------------------------------------------------- /modelator/utils/tla_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, List, Tuple 3 | 4 | import modelator_py.util.tla as tla_parsing 5 | 6 | 7 | def get_auxiliary_tla_files(model_name: str) -> Dict[str, str]: 8 | 9 | dir = os.path.dirname(os.path.abspath(model_name)) 10 | tla_files = [ 11 | os.path.join(dir, f) 12 | for f in os.listdir(dir) 13 | if os.path.isfile(os.path.join(dir, f)) and f.split(".")[-1] in ["tla", "cfg"] 14 | ] 15 | 16 | files = {} 17 | for file_name in tla_files: 18 | files[file_name] = open(file_name).read() 19 | 20 | return files 21 | 22 | 23 | def _default_example_criteria(operator_name: str): 24 | op = operator_name.lower() 25 | return op.startswith("ex") or op.endswith("ex") or op.endswith("example") 26 | 27 | 28 | def _default_invariant_criteria(operator_name: str): 29 | op = operator_name.lower() 30 | return op.startswith("inv") or op.endswith("inv") or op.endswith("invariant") 31 | 32 | 33 | def build_config_file_content( 34 | init: str, next: str, invariants: List[str], constants: Dict[str, str] 35 | ): 36 | invariants_string = " \n".join(invariants) 37 | config_string = f"INIT {init}\nNEXT {next}\nINVARIANTS {invariants_string}" 38 | if constants: 39 | constants_string = " \n".join([f"{k} <- {v}" for k, v in constants.items()]) 40 | config_string += f"\nCONSTANTS {constants_string}" 41 | 42 | return config_string 43 | 44 | 45 | def _negated_predicate(predicate_name: str): 46 | return predicate_name + "_negated" 47 | 48 | 49 | def tla_file_with_negated_predicates( 50 | module_name: str, predicates: List[str] 51 | ) -> Tuple[str, str, str]: 52 | negated_module_name = module_name + "__negated" 53 | negated_file_name = negated_module_name + ".tla" 54 | negated_predicates = [] 55 | header = "------------ MODULE {} -------------\n EXTENDS {}\n".format( 56 | negated_module_name, module_name 57 | ) 58 | body = [] 59 | for predicate in predicates: 60 | 61 | neg_predicate = _negated_predicate(predicate) 62 | negated_predicates.append(neg_predicate) 63 | body.append("{} == ~{}".format(neg_predicate, predicate)) 64 | footer = "====" 65 | negated_file_content = header + "\n".join(body) + "\n" + footer 66 | 67 | return negated_file_name, negated_file_content, negated_predicates 68 | 69 | 70 | def get_model_elements(model_name: str) -> Tuple[List[str], List[str]]: 71 | """ 72 | TODO: this only works when the model is in a single file (it will not get all the 73 | operators from all extendees) 74 | 75 | """ 76 | 77 | with open(model_name, "r") as f: 78 | tla_spec = f.read() 79 | tree = tla_parsing.parser.parse(tla_spec) 80 | variables = [] 81 | operators = [] 82 | if tree is None: 83 | raise ValueError("Expecting this file to be parsable") 84 | else: 85 | for body_element in tree.body: 86 | if isinstance(body_element, tla_parsing.ast.Nodes.Variables): 87 | variables = [str(d) for d in body_element.declarations] 88 | 89 | if isinstance(body_element, tla_parsing.ast.Nodes.Definition): 90 | operators.append(body_element.definition.name) 91 | 92 | return variables, operators, tree.name 93 | 94 | 95 | def create_file(module, extends, content): 96 | return f""" 97 | ------------ MODULE {module} ------------- 98 | 99 | EXTENDS {", ".join(extends)} 100 | 101 | {content} 102 | 103 | =========================================== 104 | """ 105 | -------------------------------------------------------------------------------- /modelator/utils/tlc_helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def invariant_from_stdout(stdout: str) -> str: 5 | match = re.search(r"Invariant (?P\w+) is violated.", stdout) 6 | return match["invName"] 7 | -------------------------------------------------------------------------------- /pylama.ini: -------------------------------------------------------------------------------- 1 | [pylama] 2 | ignore = E501,C901,E203 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "modelator" 3 | version = "0.6.6" 4 | description = "Framework for Model Based Testing" 5 | authors = [ 6 | "Andrey Kuprianov ", 7 | "Hernán Vanzetto ", 8 | "Ivan Gavran ", 9 | "Ranadeep Biswas ", 10 | ] 11 | keywords = [ 12 | "testing", 13 | "model based testing", 14 | "formal method", 15 | "model checker", 16 | "tla", 17 | "apalache", 18 | "tlc", 19 | ] 20 | readme = "README.md" 21 | license = "Apache-2.0" 22 | repository = "https://github.com/informalsystems/atomkraft" 23 | include = ["LICENSE"] 24 | classifiers = [ 25 | "Topic :: Software Development :: Testing", 26 | "Programming Language :: Python :: 3 :: Only", 27 | ] 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.8" 31 | deepdiff = "^6.2.1" 32 | tabulate = "^0.9.0" 33 | modelator-py = "^0.2.6" 34 | watchdog = "^2.1.9" 35 | typing-extensions = "^4.4.0" 36 | wget = "^3.2" 37 | appdirs = "^1.4.4" 38 | toml = "^0.10.2" 39 | typer = {extras = ["all"], version = "^0.7.0"} 40 | rich = "^12.6.0" 41 | pyrsistent = "^0.19.2" 42 | munch = "^2.5.0" 43 | semver = "^2.13.0" 44 | 45 | [tool.poetry.dev-dependencies] 46 | fire = "^0.4.0" 47 | pytest = "^7.1.2" 48 | syrupy = "^2.3.0" 49 | prospector = { extras = ["with_mypy", "with_bandit"], version = "^1.7.7" } 50 | 51 | [build-system] 52 | requires = ["poetry-core>=1.0.0", "setuptools"] 53 | build-backend = "poetry.core.masonry.api" 54 | 55 | [tool.poetry.scripts] 56 | modelator = "modelator.cli:app" 57 | 58 | [tool.poetry.plugins] 59 | pytest11 = { pytest-modelator = "modelator.pytest" } 60 | 61 | [tool.pytest.ini_options] 62 | markers = [ 63 | "network: marks test as running over network (deselect with '-m \"not network\"')", 64 | ] 65 | 66 | [tool.semantic_release] 67 | version_variable = ["modelator/_version.py:__version__"] 68 | version_toml = ["pyproject.toml:tool.poetry.version"] 69 | major_on_zero = true 70 | branch = "dev" 71 | upload_to_release = true 72 | upload_to_repository = true 73 | build_command = "pip install poetry && poetry build" 74 | 75 | [tool.isort] 76 | profile = "black" 77 | -------------------------------------------------------------------------------- /scripts/generate_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | set -o xtrace 5 | 6 | [ -z $1 ] && echo "No update component provided." && exit 1 7 | 8 | # get python-version 9 | LAST_PYTHON_VERSION=$(grep -i '^version = ' "pyproject.toml" | sed 's|^version = "\([^"]\+\)"|\1|g') 10 | 11 | # get last release tag 12 | LAST_RELEASE_VERSION=$(grep '^## v' CHANGELOG.md | head -1 | sed 's|^## v||g') 13 | 14 | echo "Python version $LAST_PYTHON_VERSION" 15 | echo "Release version $LAST_RELEASE_VERSION" 16 | 17 | if [[ "$LAST_RELEASE_VERSION" != "$LAST_PYTHON_VERSION" ]]; then 18 | echo "Versions did not the match. Exiting." 19 | exit 2 20 | fi 21 | 22 | function semver_update { 23 | [ -z $1 ] && [ -z $2 ] && exit 2 24 | patch_version=$(cut -d'.' -f3 <<< $2) 25 | minor_version=$(cut -d'.' -f2 <<< $2) 26 | major_version=$(cut -d'.' -f1 <<< $2) 27 | case $1 in 28 | patch) 29 | echo $major_version.$minor_version.$(( patch_version + 1 )) 30 | ;; 31 | minor) 32 | echo $major_version.$(( minor_version + 1 )).0 33 | ;; 34 | major) 35 | echo $(( major_version + 1 )).0.0 36 | ;; 37 | esac 38 | } 39 | 40 | echo "RELEASE_VERSION=$(semver_update $1 $LAST_RELEASE_VERSION)" >> $GITHUB_ENV 41 | -------------------------------------------------------------------------------- /scripts/github_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | set -o xtrace 5 | 6 | # the `[release] vX.Y.Z` is merged to `main` now 7 | 8 | # create a tag `vX.Y.Z` 9 | 10 | TAG_NAME="v${RELEASE_VERSION}" 11 | 12 | rm -rf ".changelog/unreleased" 13 | mv ".changelog/v${RELEASE_VERSION}" ".changelog/unreleased" 14 | 15 | RELEASE_NOTES_FILE="current_changelog" 16 | 17 | unclog build -u | tail -n +3 > "$RELEASE_NOTES_FILE" 18 | 19 | git tag \ 20 | --annotate "$TAG_NAME" \ 21 | --message "Release version $TAG_NAME" 22 | git push --tags 23 | 24 | # create a github release with change log 25 | 26 | gh release create \ 27 | --title "$TAG_NAME" \ 28 | --notes-file "$RELEASE_NOTES_FILE" \ 29 | --prerelease \ 30 | "$TAG_NAME" 31 | # ./dist/*.tar.gz 32 | # ./dist/*.zip 33 | 34 | # publish to crates.io, pypi.org and go registry 35 | # done on GH action workflow 36 | -------------------------------------------------------------------------------- /scripts/prepare_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | set -o xtrace 5 | 6 | # create a new branch `[release] vX.Y.Z` with ${RELEASE_VERSION} 7 | 8 | git checkout -B "release/v${RELEASE_VERSION}" 9 | 10 | # update the version on Python module 11 | sed -i "s|^version = \"[^\"]\+\"|version = \"${RELEASE_VERSION}\"|g" "pyproject.toml" 12 | 13 | BODY_FILE="current_changelog" 14 | 15 | unclog build --unreleased | sed "s/## Unreleased/## v${RELEASE_VERSION}/g" > "$BODY_FILE" 16 | 17 | # unclog hack until https://github.com/informalsystems/unclog/issues/22 closes 18 | cat > fake_editor < CHANGELOG.md 28 | 29 | git add ".changelog/v${RELEASE_VERSION}" 30 | git add --update 31 | 32 | COMMIT_MSG="[RELEASE] v${RELEASE_VERSION}" 33 | 34 | git commit --message "$COMMIT_MSG" 35 | git push --force origin "release/v${RELEASE_VERSION}" 36 | 37 | # create the pull request from `[release] vX.Y.Z` to `main` 38 | 39 | gh pr create \ 40 | --title "$COMMIT_MSG" \ 41 | --body-file "$BODY_FILE" \ 42 | --assignee "@me" 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/informalsystems/modelator/39ef36cc65dbcbdc347503399596a02733da3c1a/tests/__init__.py -------------------------------------------------------------------------------- /tests/cli/.gitignore: -------------------------------------------------------------------------------- 1 | traces/ 2 | -------------------------------------------------------------------------------- /tests/cli/model/Test1.config.toml: -------------------------------------------------------------------------------- 1 | [Model] 2 | init = "Init" 3 | # next = "Next" 4 | invariants = ["Inv"] 5 | # tests = ["ExTest", "ExFail"] 6 | 7 | [Config] 8 | # location for the generated trace files 9 | traces_dir = "traces/Test1" 10 | 11 | [Apalache] 12 | # the name of an operator that initializes CONSTANTS, default: None 13 | # cinit = "CInit" 14 | 15 | # configuration file in TLC format 16 | # config = "path/to/config.cfg" 17 | 18 | # maximal number of Next steps; default: 10 19 | length = 5 20 | 21 | # do not stop on first error, but produce up to a given number of counterexamples (fine tune with --view), default: 1 22 | # max_error = 5 23 | 24 | # do not check for deadlocks; default: false 25 | no_deadlock = true 26 | 27 | # the state view to use with --max-error=n, default: transition index 28 | # view = "View" 29 | -------------------------------------------------------------------------------- /tests/cli/model/Test1.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Test1 ---- 2 | VARIABLE 3 | \* @type: Str; 4 | x 5 | Init == x = "a" 6 | InitB == x = "b" 7 | Next == UNCHANGED x 8 | Inv == x = "a" 9 | InvB == x = "b" 10 | InvC == x # "b" 11 | ====================== 12 | -------------------------------------------------------------------------------- /tests/cli/model/Test2.config.toml: -------------------------------------------------------------------------------- 1 | [Model] 2 | # init = "Init" 3 | # next = "Next" 4 | invariants = ["Inv"] 5 | 6 | [Config] 7 | # location for the generated trace files 8 | traces_dir = "traces/Test2" 9 | 10 | [Apalache] 11 | # the name of an operator that initializes CONSTANTS, default: None 12 | cinit = "ConstInit" 13 | 14 | # configuration file in TLC format 15 | # config = "path/to/config.cfg" 16 | 17 | # maximal number of Next steps; default: 10 18 | length = 5 19 | 20 | # do not stop on first error, but produce up to a given number of counterexamples (fine tune with --view), default: 1 21 | # max_error = 5 22 | 23 | # do not check for deadlocks; default: false 24 | # no_deadlock = true 25 | 26 | # the state view to use with --max-error=n, default: transition index 27 | # view = "View" 28 | -------------------------------------------------------------------------------- /tests/cli/model/Test2.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Test2 ---- 2 | CONSTANT 3 | \* @type: Set(Str); 4 | X 5 | VARIABLE 6 | \* @type: Str; 7 | x 8 | Init == x \in X 9 | Next == UNCHANGED x 10 | Inv == x \in X 11 | ---------------------- 12 | InstanceX == {"a", "b"} 13 | ConstInit == X = InstanceX 14 | ====================== 15 | -------------------------------------------------------------------------------- /tests/cli/model/Test3.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Test3 ---- 2 | EXTENDS Integers 3 | VARIABLE 4 | \* @type: Int; 5 | x 6 | Init == x = 0 7 | Next == x' = x + 1 8 | Inv == x \in Nat 9 | Inv2 == x' > x 10 | ThreeSteps == x = 3 11 | ====================== 12 | -------------------------------------------------------------------------------- /tests/cli/model/errors/TestError1.tla: -------------------------------------------------------------------------------- 1 | Syntactically invalid TLA+: there is an extra comma after the variable declaration. 2 | ---- MODULE TestError1 ---- 3 | VARIABLE x, 4 | Init == x \in {"a"} 5 | Next == UNCHANGED x 6 | Inv == x \in {"a"} 7 | =========================== 8 | -------------------------------------------------------------------------------- /tests/cli/model/errors/TestError2.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE TestError2 ---- 2 | \* Variable with incorrect Apalache type (should be Str). 3 | VARIABLE 4 | \* @type: String; 5 | x 6 | Init == x \in {"a"} 7 | Next == UNCHANGED x 8 | Inv == x \in {"a"} 9 | =========================== 10 | -------------------------------------------------------------------------------- /tests/cli/model/transferLegacy.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE transferLegacy ---- 2 | EXTENDS Apalache, Integers, FiniteSets 3 | 4 | VARIABLES 5 | \* @type: Str -> Int; 6 | balances, 7 | \* @type: [tag: Str, value: [wallets: Set(Str), sender: Str, receiver: Str, amount: Int]]; 8 | action, 9 | \* @type: Int; 10 | step 11 | 12 | WALLETS == {"Alice", "Bob"} 13 | 14 | Init == 15 | /\ balances = [wallet \in WALLETS |-> 100] 16 | /\ action = [tag |-> "Init", value |-> [wallets |-> WALLETS]] 17 | /\ step = 0 18 | 19 | Next == 20 | \E sender \in WALLETS: 21 | \E receiver \in WALLETS: 22 | \E amount \in 1..balances[sender]: 23 | /\ sender /= receiver 24 | /\ balances' = [ 25 | balances EXCEPT 26 | ![sender] = @ - amount, 27 | ![receiver] = @ + amount 28 | ] 29 | /\ action' = [tag |-> "Transfer", value |-> [sender |-> sender, receiver |-> receiver, amount |-> amount]] 30 | /\ step' = step + 1 31 | 32 | View == 33 | IF action.tag = "Transfer" 34 | THEN action.value 35 | ELSE [sender |-> "", receiver |-> "", amount |-> 0] 36 | 37 | TestAliceZero == balances["Alice"] = 0 38 | 39 | ==== 40 | -------------------------------------------------------------------------------- /tests/cli/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | shopt -s failglob 6 | 7 | MDX=ocaml-mdx 8 | TRACES_DIR="./traces/" 9 | MD_FILES="${1:-*.md}" 10 | 11 | if ! command -v $MDX &> /dev/null; then 12 | echo "$MDX could not be found" 13 | echo "Please install MDX (https://github.com/realworldocaml/mdx)" 14 | exit 1 15 | fi 16 | 17 | eval $(opam env) 18 | 19 | SCRIPT_EXIT_CODE=0 20 | 21 | test_file() { 22 | echo "▶️ Testing file $1..." 23 | $MDX test -v $1 24 | if [ -f "$1.corrected" ]; then 25 | diff -u "${1}" "${1}.corrected" || echo "❌ FAILED: see $1.corrected" 26 | SCRIPT_EXIT_CODE=1 27 | else 28 | echo "✅ OK" 29 | fi 30 | } 31 | 32 | for MD_FILE in $MD_FILES; do 33 | test_file $MD_FILE 34 | done 35 | 36 | rm -rdf $TRACES_DIR 37 | 38 | if (( $SCRIPT_EXIT_CODE != 0 )); then 39 | echo "Some tests failed" 40 | exit $SCRIPT_EXIT_CODE 41 | fi 42 | -------------------------------------------------------------------------------- /tests/cli/simulation_traces_last_generated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | [ $# -eq 0 ] && echo "Usage: $0 " && exit 1 7 | 8 | 9 | TRACES_DIR="$1" 10 | 11 | DIR=$TRACES_DIR 12 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 13 | 14 | 15 | # last file in the directory by time 16 | LAST_FILE=$(ls -rt $DIR | grep .itf.json | tail -1) 17 | 18 | # Return last generated trace file 19 | echo $DIR/$LAST_FILE 20 | -------------------------------------------------------------------------------- /tests/cli/simulation_traces_num_generated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | [ $# -eq 0 ] && echo "Usage: $0 " && exit 1 7 | 8 | TRACES_DIR="$1" 9 | 10 | DIR="$TRACES_DIR" 11 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 12 | 13 | 14 | # Return the number of files in the directory 15 | # - xargs for trimming whitespace 16 | NUM_TRACE_FILES=$(ls -rt $DIR | grep .itf.json | tail -n+1 | wc -l | xargs) 17 | echo $NUM_TRACE_FILES 18 | -------------------------------------------------------------------------------- /tests/cli/test_basic.md: -------------------------------------------------------------------------------- 1 | First make sure that there is no model loaded: 2 | 3 | ```sh 4 | $ modelator reset 5 | ... 6 | ``` 7 | 8 | Commands that should print a message when there is no model loaded: 9 | 10 | ```sh 11 | $ modelator info 12 | Model file does not exist 13 | $ modelator typecheck 14 | Model file does not exist 15 | $ modelator reload 16 | ERROR: model not loaded; run `modelator load` first 17 | ``` 18 | 19 | Running `load` without passing a path will exit with an error code: 20 | 21 | ```sh 22 | $ modelator load 23 | ... 24 | [2] 25 | ``` 26 | -------------------------------------------------------------------------------- /tests/cli/test_check.md: -------------------------------------------------------------------------------- 1 | # Test the `check` command 2 | 3 | Running `check` without a loaded model and without specifying any model path 4 | should fail: 5 | 6 | ```sh 7 | $ modelator check 8 | ... 9 | [1] 10 | ``` 11 | 12 | Running `check` with a non existing config file should fail: 13 | 14 | ```sh 15 | $ modelator check --config-path non-existing-file 16 | ERROR: config file not found 17 | [4] 18 | ``` 19 | 20 | Running `check` trying to prove a property that is an invariant should succeed: 21 | 22 | ```sh 23 | $ modelator check --model-path model/Test1.tla --invariants=Inv 24 | ... 25 | - Inv OK ✅ 26 | ... 27 | ``` 28 | 29 | Check that the number and length of the generated trace files are as expected: 30 | 31 | ```sh 32 | $ ./traces_num_generated.sh Inv 33 | [1] 34 | ``` 35 | 36 | Running `check` trying to prove a property that is not an invariant should fail: 37 | 38 | ```sh 39 | $ modelator check --model-path model/Test1.tla --invariants=InvB 40 | ... 41 | - InvB FAILED ❌ 42 | Check error: 43 | ... 44 | Trace: [(x = "a")] 45 | ... 46 | ``` 47 | 48 | Check that the number and length of the generated trace files are as expected: 49 | 50 | ```sh 51 | $ ./traces_num_generated.sh InvB 52 | 1 53 | $ ./traces_last_generated.sh InvB | xargs -I {} ./traces_length.sh {} 54 | 0 55 | ``` 56 | 57 | Running `check` trying to prove two properties: 58 | 59 | ```sh 60 | $ modelator check --model-path model/Test1.tla --invariants Inv,InvC 61 | ... 62 | - Inv OK ✅ 63 | ... 64 | - InvC OK ✅ 65 | ... 66 | ``` 67 | 68 | Running `check` trying to prove two properties: 69 | 70 | ```sh 71 | $ modelator check --model-path model/Test1.tla --invariants Inv,InvB 72 | ... 73 | - Inv OK ✅ 74 | ... 75 | - InvB FAILED ❌ 76 | Check error: 77 | ... 78 | Trace: [(x = "a")] 79 | ... 80 | ``` 81 | 82 | Check that the number and length of the generated trace files are as expected: 83 | 84 | ```sh 85 | $ ./traces_num_generated.sh Inv 86 | [1] 87 | $ ./traces_num_generated.sh InvB 88 | 1 89 | $ ./traces_last_generated.sh InvB | xargs -I {} ./traces_length.sh {} 90 | 0 91 | ``` 92 | 93 | Running `check` on a property that is not defined in the model should fail: 94 | 95 | ```sh 96 | $ modelator check --model-path model/Test1.tla --invariants=NonExistingProperty 97 | ... 98 | ERROR: NonExistingProperty not defined in the model 99 | ... 100 | [3] 101 | ``` 102 | -------------------------------------------------------------------------------- /tests/cli/test_constants.md: -------------------------------------------------------------------------------- 1 | # Tests on instantiation of model constants 2 | 3 | First, make sure that there is no model loaded: 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | ``` 9 | 10 | Load a model that declares some constant: 11 | 12 | ```sh 13 | $ modelator load model/Test2.tla 14 | ... 15 | Loading OK ✅ 16 | ``` 17 | 18 | Running `check` on the loaded model, without initializing constants, should fail 19 | with an error message: 20 | 21 | ```sh 22 | $ modelator check --invariants Inv 23 | ... 24 | - Inv FAILED ❌ 25 | A constant in the model is not initialized 26 | ... 27 | ``` 28 | 29 | Running `check` on the loaded model passing `invariants` and valid `constants` 30 | should succeed: 31 | 32 | ```sh 33 | $ modelator check --invariants Inv --constants X=InstanceX 34 | ... 35 | - Inv OK ✅ 36 | ... 37 | ``` 38 | 39 | Running `check` on the loaded model passing `invariants` and invalid `constants` 40 | should fail: 41 | 42 | ```sh 43 | $ modelator check --invariants Inv --constants X=AnUndefinedIdentifier 44 | ... 45 | - Inv FAILED ❌ 46 | Configuration error: 47 | ... 48 | ``` 49 | 50 | Finally, clean the generated files after the test: 51 | 52 | ```sh 53 | $ modelator reset 54 | Model file removed 55 | ``` 56 | -------------------------------------------------------------------------------- /tests/cli/test_extra_args.md: -------------------------------------------------------------------------------- 1 | # Tests on passing extra arguments to the model checker 2 | 3 | First make sure that there is no model loaded: 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | ``` 9 | 10 | Load a model: 11 | 12 | ```sh 13 | $ modelator load model/Test2.tla 14 | ... 15 | Loading OK ✅ 16 | ``` 17 | 18 | Running `check` on the loaded model, without initializing constants, should fail: 19 | 20 | ```sh 21 | $ modelator check --invariants Inv 22 | ... 23 | - Inv FAILED ❌ 24 | A constant in the model is not initialized 25 | ... 26 | ``` 27 | 28 | Running `check` on the loaded model, while reading `invariants` and `cinit` from 29 | the config file, should succeed: 30 | 31 | ```sh 32 | $ modelator check --config-path model/Test2.config.toml 33 | ... 34 | - Inv OK ✅ 35 | ... 36 | ``` 37 | 38 | Running `check` on the loaded model overriding the property to check and passing 39 | some setting to the checker should succeed: 40 | 41 | ```sh 42 | $ modelator check --invariants Inv --cinit=ConstInit --length=2 43 | ... 44 | - Inv OK ✅ 45 | ... 46 | ``` 47 | 48 | Finally, clean the generated files after the test: 49 | 50 | ```sh 51 | $ modelator reset 52 | Model file removed 53 | ``` 54 | -------------------------------------------------------------------------------- /tests/cli/test_load_with_config.md: -------------------------------------------------------------------------------- 1 | # Tests on loading a model from a TLA+ file and a configuration from a TOML file 2 | 3 | First make sure that there is no model loaded: 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | ``` 9 | 10 | Load a model and a configuration from a toml file: 11 | 12 | ```sh 13 | $ modelator load model/Test1.tla --config model/Test1.config.toml 14 | ... 15 | Loading OK ✅ 16 | $ modelator typecheck 17 | Type checking OK ✅ 18 | ``` 19 | 20 | Check the output of `info`: 21 | 22 | ```sh 23 | $ modelator info 24 | Model: 25 | - constants: {} 26 | ... 27 | - init: Init 28 | - model_path: model/Test1.tla 29 | - module_name: Test1 30 | - monitors: [] 31 | - next: Next 32 | - operators: ['Init', 'InitB', 'Next', 'Inv', 'InvB', 'InvC'] 33 | - variables: ['x'] 34 | Config at model/Test1.config.toml: 35 | - config_file_path: None 36 | - constants: {} 37 | - init: Init 38 | - invariants: ['Inv'] 39 | - next: Next 40 | - params: {'cinit': None, 'config': None, 'length': 5, 'max_error': None, 'no_deadlock': True, 'view': None} 41 | - tests: [] 42 | - traces_dir: traces/Test1 43 | ``` 44 | 45 | Running `check` on the loaded model should succeed: 46 | 47 | ```sh 48 | $ modelator check 49 | ... 50 | - Inv OK ✅ 51 | ... 52 | ``` 53 | 54 | Running `check` on the loaded model, while overriding the property to check and 55 | passing some setting to the checker, should succeed: 56 | 57 | ```sh 58 | $ modelator check --invariants InvB --init=InitB --length=3 59 | ... 60 | - InvB OK ✅ 61 | ... 62 | ``` 63 | 64 | Running `check` on a property that is not defined in the model should fail: 65 | 66 | ```sh 67 | $ modelator check --invariants=NonExistingProperty 68 | ... 69 | ERROR: NonExistingProperty not defined in the model 70 | ... 71 | [3] 72 | ``` 73 | 74 | Finally, clean the generated files after the test: 75 | 76 | ```sh 77 | $ modelator reset 78 | Model file removed 79 | ``` 80 | -------------------------------------------------------------------------------- /tests/cli/test_load_without_config.md: -------------------------------------------------------------------------------- 1 | # Tests on loading a model from a TLA+ file 2 | 3 | First make sure that there is no model loaded: 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | ``` 9 | 10 | Load model from a TLA+ file: 11 | 12 | ```sh 13 | $ modelator load model/Test1.tla 14 | ... 15 | Loading OK ✅ 16 | $ modelator typecheck 17 | Type checking OK ✅ 18 | ``` 19 | 20 | ```sh 21 | $ modelator info 22 | Model: 23 | - constants: {} 24 | ... 25 | - init: Init 26 | - model_path: model/Test1.tla 27 | - module_name: Test1 28 | - monitors: [] 29 | - next: Next 30 | - operators: ['Init', 'InitB', 'Next', 'Inv', 'InvB', 'InvC'] 31 | - variables: ['x'] 32 | ``` 33 | 34 | Running `check` on the loaded model without specifying any property to check 35 | should fail: 36 | 37 | ```sh 38 | $ modelator check 39 | ... 40 | [2] 41 | ``` 42 | 43 | Running `check` with a non existing config file should fail: 44 | 45 | ```sh 46 | $ modelator check --config-path non-existing-file 47 | ERROR: config file not found 48 | [4] 49 | ``` 50 | 51 | Running `check` trying to prove a property that is an invariant should succeed: 52 | 53 | ```sh 54 | $ modelator check --invariants=Inv 55 | ... 56 | - Inv OK ✅ 57 | ... 58 | ``` 59 | 60 | Running `check` trying to prove a property that is not an invariant should fail: 61 | 62 | ```sh 63 | $ modelator check --invariants=InvB 64 | ... 65 | - InvB FAILED ❌ 66 | Check error: 67 | ... 68 | ``` 69 | 70 | Running `check` trying to prove two properties: 71 | 72 | ```sh 73 | $ modelator check --invariants Inv,InvC 74 | ... 75 | - Inv OK ✅ 76 | ... 77 | - InvC OK ✅ 78 | ... 79 | ``` 80 | 81 | Running `check` trying to prove two properties: 82 | 83 | ```sh 84 | $ modelator check --invariants Inv,InvB 85 | ... 86 | - Inv OK ✅ 87 | ... 88 | - InvB FAILED ❌ 89 | Check error: 90 | ... 91 | ``` 92 | 93 | Running `check` on a property that is not defined in the model should fail: 94 | 95 | ```sh 96 | $ modelator check --invariants=NonExistingProperty 97 | ... 98 | ERROR: NonExistingProperty not defined in the model 99 | ... 100 | [3] 101 | ``` 102 | 103 | Finally, clean the generated files after the test: 104 | 105 | ```sh 106 | $ modelator reset 107 | Model file removed 108 | ``` 109 | -------------------------------------------------------------------------------- /tests/cli/test_parse.md: -------------------------------------------------------------------------------- 1 | # Test parsing errors 2 | 3 | ## Run `load` and `typecheck` on a model is not syntactically valid 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | $ modelator load model/errors/TestError1.tla 9 | ... 10 | Parsing error 💥 11 | ... 12 | [5] 13 | ``` 14 | 15 | ## Run `check` and `sample` on a model that is not syntactically valid 16 | 17 | ```sh 18 | $ modelator check --model-path model/errors/TestError1.tla --invariants Inv 19 | ... 20 | Parsing error 💥 21 | ... 22 | [5] 23 | ``` 24 | 25 | ```sh 26 | $ modelator sample --model-path model/errors/TestError1.tla --tests Inv 27 | ... 28 | Parsing error 💥 29 | ... 30 | [5] 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/cli/test_sample.md: -------------------------------------------------------------------------------- 1 | # Test the `sample` command 2 | 3 | Running `sample` without specifying a model or a property to check should fail: 4 | 5 | ```sh 6 | $ modelator sample 7 | ... 8 | [1] 9 | ``` 10 | 11 | Running `sample` without specifying a model should fail: 12 | 13 | ```sh 14 | $ modelator sample --tests=ThreeSteps 15 | ... 16 | [1] 17 | ``` 18 | 19 | Running `sample` with a non existing config file should fail: 20 | 21 | ```sh 22 | $ modelator sample --config-path non-existing-file 23 | ERROR: config file not found 24 | [4] 25 | ``` 26 | 27 | Running `sample` with a model and a property to sample should succeed: 28 | 29 | ```sh 30 | $ modelator sample --model-path model/Test3.tla --tests ThreeSteps 31 | ... 32 | - ThreeSteps OK ✅ 33 | ... 34 | ``` 35 | 36 | Check that the number and length of the generated trace files are as expected: 37 | 38 | ```sh 39 | $ ./traces_num_generated.sh ThreeSteps 40 | 1 41 | $ ./traces_last_generated.sh ThreeSteps | xargs -I {} ./traces_length.sh {} 42 | 3 43 | ``` 44 | 45 | Running `sample` with a model and a property to sample should succeed and generate 3 trace files: 46 | 47 | ```sh 48 | $ modelator sample --model-path model/Test3.tla --tests ThreeSteps --max_error=3 --view=ThreeSteps 49 | ... 50 | - ThreeSteps OK ✅ 51 | ... 52 | ``` 53 | 54 | Check that the number and length of the generated trace files are as expected: 55 | 56 | ```sh 57 | $ ./traces_num_generated.sh ThreeSteps 58 | 1 59 | $ ./traces_last_generated.sh ThreeSteps | xargs -I {} ./traces_length.sh {} 60 | 3 61 | ``` 62 | 63 | Running `sample` on a property that is not defined in the model should fail: 64 | 65 | ```sh 66 | $ modelator sample --model-path model/Test3.tla --tests=NonExistingProperty 67 | ... 68 | ERROR: NonExistingProperty not defined in the model 69 | ... 70 | [3] 71 | ``` 72 | -------------------------------------------------------------------------------- /tests/cli/test_simulate.md: -------------------------------------------------------------------------------- 1 | # Test the `simulate` command 2 | 3 | First, reset the model. 4 | 5 | ```sh 6 | $ modelator reset 7 | ... 8 | ``` 9 | 10 | Running `simulate` without a loaded model and without specifying any model path 11 | should fail: 12 | 13 | ```sh 14 | $ modelator simulate 15 | ... 16 | [1] 17 | ``` 18 | 19 | Running `simulate` with a non existing config file should fail: 20 | 21 | ```sh 22 | $ modelator simulate --config-path non-existing-file 23 | ERROR: config file not found 24 | [4] 25 | ``` 26 | 27 | Running `simulate` with an argument of a file should succeed. 28 | 29 | ```sh 30 | $ modelator simulate --model-path model/Test1.tla 31 | Simulating... 32 | ... 33 | ``` 34 | 35 | Running `simulate` with arguments specifying the number of simulations and their length. 36 | 37 | ```sh 38 | $ rm -r test_tracesXX || true 39 | ... 40 | ``` 41 | 42 | ```sh 43 | $ modelator simulate --model-path model/Test1.tla --max-trace 4 --length 3 --traces-dir test_tracesXX 44 | Simulating... 45 | ... 46 | ``` 47 | 48 | Check that the number and length of the generated trace files are as expected: 49 | 50 | ```sh 51 | $ ./simulation_traces_num_generated.sh test_tracesXX 52 | 4 53 | $ ./simulation_traces_last_generated.sh test_tracesXX | xargs -I {} ./traces_length.sh {} 54 | 3 55 | ``` 56 | -------------------------------------------------------------------------------- /tests/cli/test_typecheck.md: -------------------------------------------------------------------------------- 1 | # Test type annotations in models 2 | 3 | ## Run `load` and `typecheck` on a model with incorrect type annotations 4 | 5 | Loading a model that is syntactically correct but has incorrect type annotations 6 | should succeed. 7 | 8 | ```sh 9 | $ modelator reset 10 | ... 11 | $ modelator load model/errors/TestError2.tla 12 | ... 13 | Loading OK ✅ 14 | ... 15 | ``` 16 | 17 | Then, the `typecheck` command should show an error message and fail. 18 | 19 | ```sh 20 | $ modelator typecheck 21 | Type checking error 💥 22 | ... 23 | [6] 24 | ``` 25 | 26 | Clean the generated files after the test: 27 | 28 | ```sh 29 | $ modelator reset 30 | ... 31 | ``` 32 | 33 | ## Run `typecheck` on the model with type old-style type annotations (prior to Apalache 0.29) 34 | 35 | ```sh 36 | $ modelator load model/transferLegacy.tla 37 | ... 38 | ``` 39 | 40 | First, run typecheck with no extra options. 41 | 42 | ``` 43 | $ modelator typecheck 44 | Type checking error 💥 45 | ... 46 | [6] 47 | ``` 48 | 49 | Then, run typecheck but with an extra option to enable legacy types. 50 | 51 | ``` 52 | $ modelator typecheck --features=no-rows 53 | Type checking OK ✅ 54 | ``` 55 | 56 | Clean the generated files after the test: 57 | 58 | ```sh 59 | $ modelator reset 60 | ... 61 | ``` 62 | 63 | ## Run `check` and `sample` on a model with incorrect type annotations 64 | 65 | ```sh 66 | $ modelator check --model-path model/errors/TestError2.tla --invariants Inv 67 | ... 68 | Type checking error 💥 69 | ... 70 | ``` 71 | 72 | ```sh 73 | $ modelator sample --model-path model/errors/TestError2.tla --tests Inv 74 | ... 75 | Type checking error 💥 76 | ... 77 | ``` 78 | 79 | Clean the generated files after the test: 80 | 81 | ```sh 82 | $ modelator reset 83 | ... 84 | ``` 85 | -------------------------------------------------------------------------------- /tests/cli/trace_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | PREFIX="${1:-violation}" 7 | TRACES_DIR="${2:-traces}" 8 | 9 | TRACE_FILE=$(./trace_find.sh $PREFIX $TRACES_DIR) 10 | echo "Trace file: $TRACE_FILE" 11 | 12 | TRACE_LENGTH=$(./trace_length.sh $TRACE_FILE) 13 | echo "Trace length: $TRACE_LENGTH" 14 | -------------------------------------------------------------------------------- /tests/cli/traces_last_generated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | [ $# -eq 0 ] && echo "Usage: $0 []" && exit 1 7 | 8 | PROPERTY_NAME="$1" 9 | TRACES_DIR="${2:-traces}" 10 | 11 | DIR=$TRACES_DIR 12 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 13 | 14 | TIMESTAMP_SUBDIR=$(ls -rt $TRACES_DIR | tail -1) 15 | DIR=$DIR/$TIMESTAMP_SUBDIR 16 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 17 | 18 | DIR=$DIR/$PROPERTY_NAME 19 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 20 | 21 | # last file in the directory by time 22 | LAST_FILE=$(ls -rt $DIR | grep .itf.json | tail -1) 23 | 24 | # Return last generated trace file 25 | echo $DIR/$LAST_FILE 26 | -------------------------------------------------------------------------------- /tests/cli/traces_length.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | [ $# -eq 0 ] && echo "Usage: $0 []" && exit 1 7 | 8 | TRACE_FILE="$1" 9 | 10 | [ ! -f "$TRACE_FILE" ] && echo -e "ERROR: file $TRACE_FILE does not exists" && exit 1 11 | 12 | TRACE_LENGTH=$(cat $TRACE_FILE | jq '.states[]."#meta".index' | tail -1) 13 | echo $TRACE_LENGTH 14 | -------------------------------------------------------------------------------- /tests/cli/traces_num_generated.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See https://sipb.mit.edu/doc/safe-shell/ 4 | set -eu -o pipefail 5 | 6 | [ $# -eq 0 ] && echo "Usage: $0 []" && exit 1 7 | 8 | PROPERTY_NAME="$1" 9 | TRACES_DIR="${2:-traces}" 10 | 11 | DIR="$TRACES_DIR" 12 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 13 | 14 | TIMESTAMP_SUBDIR=$(ls -rt $TRACES_DIR | tail -1) 15 | DIR="$DIR/$TIMESTAMP_SUBDIR/$PROPERTY_NAME" 16 | [ ! -d "$DIR" ] && echo "Directory $DIR does not exist" && exit 1 17 | 18 | # Return the number of files in the directory 19 | # - xargs for trimming whitespace 20 | NUM_TRACE_FILES=$(ls -rt $DIR | grep .itf.json | tail -n+1 | wc -l | xargs) 21 | echo $NUM_TRACE_FILES 22 | -------------------------------------------------------------------------------- /tests/models/bingame.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE bingame ---- 2 | EXTENDS Integers 3 | 4 | VARIABLES 5 | \* @type: Int; 6 | number, 7 | \* @type: Str; 8 | action 9 | 10 | 11 | Init == 12 | /\ number = 0 13 | /\ action = "Zero" 14 | 15 | Next == 16 | /\ action' \in {"AddOne", "MultiplyTwo"} 17 | /\ number' = 18 | IF action' = "AddOne" THEN number + 1 ELSE 19 | IF action' = "MultiplyTwo" THEN number * 2 ELSE 20 | -1 21 | 22 | Inv == 23 | number /= 30 24 | 25 | ==== 26 | -------------------------------------------------------------------------------- /tests/models/collatz.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE collatz ---- 2 | EXTENDS Naturals 3 | 4 | VARIABLE 5 | \* @type: Int; 6 | x 7 | 8 | Init == 9 | x \in 1..100 10 | 11 | Next == 12 | x' = IF (x % 2 = 0) THEN (x \div 2) ELSE (x * 3 + 1) 13 | 14 | Inv == 15 | x /= 1 16 | 17 | ==== 18 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/Hello.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | InitA == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | InitB == 16 | /\ x = "hi" 17 | /\ y = 42 18 | 19 | InitC == 20 | /\ x = "hello" 21 | /\ y = 0 22 | 23 | Next == 24 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 25 | /\ y' = 42-y 26 | 27 | Inv == 28 | ~ 29 | ( 30 | /\ x = "world" 31 | /\ y = 0 32 | ) 33 | 34 | 35 | =========================================== 36 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/correct/dir1/Hello.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | INVARIANT Inv 4 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/correct/dir1/Hello.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences, Inits 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/correct/dir1/Inits.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Inits ---- 2 | 3 | VARIABLES 4 | \* @type: Str; 5 | x, 6 | \* @type: Int; 7 | y 8 | 9 | InitA == 10 | /\ x = "hello" 11 | /\ y = 32 12 | 13 | InitB == 14 | /\ x = "ho" 15 | /\ y = 0 16 | 17 | ==== 18 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/flawed/dir1/Hello.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | INVARIANT Inv 4 | -------------------------------------------------------------------------------- /tests/sampleFiles/check/flawed/dir1/Hello.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/correct/Hello.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | INVARIANT Inv 4 | -------------------------------------------------------------------------------- /tests/sampleFiles/correct/Hello_Hi.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Hello_Hi-------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/parse/correct/dir1/Hello_Hi.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Hello_Hi-------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/parse/flawed/dir1/HelloFlawed1.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloFlawed1 ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | \/ x = 2 25 | /\ y = 3 26 | ) 27 | 28 | 29 | =========================================== 30 | -------------------------------------------------------------------------------- /tests/sampleFiles/parse/flawed/dir2/HelloFlawed2.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE HelloFlawed2 ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y, 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/typecheck/correct/dir1/Hello_Hi.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE Hello_Hi-------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Int; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/typecheck/flawed/dir1/Hello1.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello1 ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | \* @type: Str; 9 | y 10 | 11 | Init == 12 | /\ x = "hello" 13 | /\ y = 42 14 | 15 | Next == 16 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 17 | /\ y' = 42-y 18 | 19 | Inv == 20 | ~ 21 | ( 22 | /\ x = "world" 23 | /\ y = 0 24 | ) 25 | 26 | 27 | =========================================== 28 | -------------------------------------------------------------------------------- /tests/sampleFiles/typecheck/flawed/dir2/Hello2.tla: -------------------------------------------------------------------------------- 1 | ------------ MODULE Hello2 ------------- 2 | 3 | EXTENDS Naturals, FiniteSets, Sequences 4 | 5 | VARIABLES 6 | \* @type: Str; 7 | x, 8 | y 9 | 10 | Init == 11 | /\ x = "hello" 12 | /\ y = 42 13 | 14 | Next == 15 | /\ x' = IF x = "hello" THEN "world" ELSE "hello" 16 | /\ y' = 42-y 17 | 18 | Inv == 19 | ~ 20 | ( 21 | /\ x = "world" 22 | /\ y = 0 23 | ) 24 | 25 | 26 | =========================================== 27 | -------------------------------------------------------------------------------- /tests/test_apalache_jar.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from modelator import const_values 6 | from modelator.utils.apalache_jar import ( 7 | apalache_jar_download, 8 | apalache_jar_exists, 9 | apalache_jar_version, 10 | ) 11 | 12 | 13 | def _setup_temp_dir(tmp_path, expected_version: str): 14 | """ 15 | Create a temporary directory for downloading the jar files. The directory 16 | will be automatically removed at the end of the test. Return the directory 17 | and the expected location of the jar file. 18 | """ 19 | test_download_dir = tmp_path / "checkers" 20 | jar_path = os.path.join( 21 | test_download_dir, "apalache", "lib", f"apalache-{expected_version}.jar" 22 | ) 23 | 24 | return test_download_dir, jar_path 25 | 26 | 27 | def test_version_non_existing_jar_file(tmp_path): 28 | _, jar_path = _setup_temp_dir(tmp_path, "0.25.10") 29 | 30 | version = apalache_jar_version(jar_path) 31 | assert version is None 32 | 33 | 34 | @pytest.mark.network 35 | def test_download_ok(tmp_path): 36 | expected_version = "0.25.10" 37 | test_download_dir, jar_path = _setup_temp_dir(tmp_path, expected_version) 38 | assert not apalache_jar_exists(jar_path, expected_version) 39 | 40 | apalache_jar_download(test_download_dir, expected_version, sha256_checksum=None) 41 | assert apalache_jar_exists(jar_path, expected_version) 42 | 43 | version = apalache_jar_version(jar_path) 44 | assert version == expected_version 45 | 46 | 47 | @pytest.mark.network 48 | def test_download_wrong_checksum(tmp_path): 49 | expected_version = "0.25.1" 50 | test_download_dir, jar_path = _setup_temp_dir(tmp_path, expected_version) 51 | wrong_expected_checksum = const_values.APALACHE_SHA_CHECKSUMS["0.25.0"] 52 | 53 | with pytest.raises(AssertionError): 54 | apalache_jar_download( 55 | test_download_dir, expected_version, wrong_expected_checksum 56 | ) 57 | assert not apalache_jar_exists(jar_path, expected_version) 58 | 59 | 60 | @pytest.mark.network 61 | def test_download_different_version(tmp_path): 62 | expected_version = "0.25.1" 63 | test_download_dir, jar_path = _setup_temp_dir(tmp_path, expected_version) 64 | correct_expected_checksum = const_values.APALACHE_SHA_CHECKSUMS[expected_version] 65 | 66 | apalache_jar_download( 67 | test_download_dir, expected_version, correct_expected_checksum 68 | ) 69 | assert apalache_jar_exists(jar_path, expected_version) 70 | 71 | version = apalache_jar_version(jar_path) 72 | assert version == expected_version 73 | 74 | 75 | def test_download_non_existing_version(tmp_path): 76 | non_existing_version = "25.0" 77 | test_download_dir, jar_path = _setup_temp_dir(tmp_path, non_existing_version) 78 | 79 | with pytest.raises(ValueError): 80 | apalache_jar_download( 81 | test_download_dir, non_existing_version, sha256_checksum=None 82 | ) 83 | assert not apalache_jar_exists(jar_path, non_existing_version) 84 | -------------------------------------------------------------------------------- /tests/test_hellofull.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | 5 | from modelator.pytest.decorators import itf, mbt, step 6 | 7 | 8 | @dataclass 9 | class State: 10 | x: int 11 | y: str 12 | 13 | 14 | @pytest.fixture 15 | def state() -> State: 16 | return State(1400, "hello") 17 | 18 | 19 | @step() 20 | def hellostep(state: State, x, y): 21 | assert state.x == x 22 | assert state.y == y 23 | 24 | print(f"At step: {state}") 25 | 26 | state.x -= 2 27 | state.y = "world" 28 | 29 | 30 | @mbt("modelator/samples/HelloFull.tla") 31 | def test_hellofull(): 32 | print("end of tla test") 33 | 34 | 35 | @itf("modelator/samples/HelloFull1.itf.json") 36 | def test_hellofull_itf(): 37 | print("end of itf test") 38 | -------------------------------------------------------------------------------- /tests/test_itf.py: -------------------------------------------------------------------------------- 1 | import glob 2 | 3 | import pytest 4 | from pyrsistent import pmap, pset, pvector 5 | 6 | from modelator.itf import ITF 7 | from modelator.pytest.decorators import sanitize_itf 8 | 9 | 10 | @pytest.mark.parametrize("itf_json_file", glob.glob("tests/traces/itf/*.itf.json")) 11 | def test_itf_parse_and_diff(snapshot, itf_json_file): 12 | trace = ITF.from_itf_json(itf_json_file) 13 | diff = ITF.diff(trace) 14 | assert (trace, diff) == snapshot 15 | 16 | 17 | @pytest.mark.parametrize("itf_json_file", glob.glob("tests/traces/itf/*.itf.json")) 18 | def test_itf_diff_print(capfd, snapshot, itf_json_file): 19 | trace = ITF.from_itf_json(itf_json_file) 20 | ITF.print_diff(trace) 21 | out, _ = capfd.readouterr() 22 | assert out == snapshot 23 | 24 | 25 | # Apalache docs on ITF Informal Trace Format 26 | # https://apalache.informal.systems/docs/adr/015adr-trace.html 27 | 28 | sanitize_pairs = [ 29 | ( 30 | { 31 | "#meta": 1, 32 | "#info": "test", 33 | "states": [ 34 | { 35 | "keys": "list", 36 | "func": {"#map": [(["1", "2"], ["1and2"])]}, 37 | "set": {"#set": [(["1", "2"], ["2", "3"])]}, 38 | }, 39 | { 40 | "keys": "set", 41 | "func": {"#map": [({"#set": ["1", "2"]}, ["1and2"])]}, 42 | "set": {"#map": [({"#set": ["1", "2"]}, {"#set": ["2", "3"]})]}, 43 | }, 44 | { 45 | "keys": "map", 46 | "func": {"#map": [({"#map": [("1", "2")]}, ["1and2"])]}, 47 | "set": {"#map": [({"#map": [("1", "2")]}, {"#map": [("2", "3")]})]}, 48 | }, 49 | ], 50 | }, 51 | { 52 | "states": [ 53 | { 54 | "func": {pvector(["1", "2"]): ["1and2"]}, 55 | "keys": "list", 56 | "set": {(pvector(["1", "2"]), pvector(["2", "3"]))}, 57 | }, 58 | { 59 | "func": {pset(["2", "1"]): ["1and2"]}, 60 | "keys": "set", 61 | "set": {pset(["2", "1"]): {"2", "3"}}, 62 | }, 63 | { 64 | "func": {pmap({"1": "2"}): ["1and2"]}, 65 | "keys": "map", 66 | "set": {pmap({"1": "2"}): {"2": "3"}}, 67 | }, 68 | ], 69 | }, 70 | ), 71 | ( 72 | { 73 | "states": [ 74 | { 75 | "var_bigint": {"#bigint": "1000000000000000000000000000001"}, 76 | "var_tuple": {"#tup": ["1", "2", "3"]}, 77 | } 78 | ] 79 | }, 80 | { 81 | "states": [ 82 | { 83 | "var_bigint": 1000000000000000000000000000001, 84 | "var_tuple": pvector(["1", "2", "3"]), 85 | } 86 | ] 87 | }, 88 | ), 89 | ] 90 | 91 | 92 | @pytest.mark.parametrize("a,b", sanitize_pairs) 93 | def test_sanitize_itf(a, b): 94 | assert sanitize_itf(a) == b 95 | -------------------------------------------------------------------------------- /tests/test_model_check.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from typing import Dict, Optional 4 | 5 | from modelator import const_values 6 | from modelator.checker.check import check_apalache 7 | from modelator.utils import tla_helpers 8 | 9 | 10 | def _matching_check_value( 11 | test_dir: str, 12 | tla_file_name: str, 13 | expected_result: bool, 14 | config_file_name: Optional[str] = None, 15 | args: Dict = {}, 16 | check_function=check_apalache, 17 | ): 18 | traces_dir = f"{test_dir}/traces" 19 | 20 | tla_file_path = os.path.join(test_dir, tla_file_name) 21 | files = tla_helpers.get_auxiliary_tla_files(tla_file_path) 22 | if config_file_name: 23 | config_file_path = os.path.join(test_dir, config_file_name) 24 | with open(config_file_path, "r") as f: 25 | args[const_values.CONFIG] = config_file_name 26 | files[config_file_name] = f.read() 27 | 28 | check_result = check_function( 29 | files=files, 30 | tla_file_name=tla_file_name, 31 | args=args, 32 | traces_dir=traces_dir, 33 | ) 34 | 35 | assert check_result.is_ok == expected_result 36 | assert (check_result.error_msg is None) == check_result.is_ok 37 | 38 | trace_filenames = [os.path.basename(p) for p in check_result.trace_paths] 39 | if check_result.is_ok: 40 | assert len(check_result.trace_paths) == 0 41 | assert all([f.startswith("example") for f in trace_filenames]) 42 | else: 43 | assert len(check_result.trace_paths) == 1 44 | assert all([f.startswith("violation") for f in trace_filenames]) 45 | 46 | # clean traces 47 | [f.unlink() for f in Path(traces_dir).glob("*")] 48 | Path(traces_dir).rmdir() 49 | 50 | 51 | def test_check_apalache_invariant_holds(): 52 | _matching_check_value( 53 | test_dir=os.path.abspath("tests/sampleFiles/check/correct/dir1"), 54 | tla_file_name="Hello.tla", 55 | config_file_name="Hello.cfg", 56 | expected_result=True, 57 | check_function=check_apalache, 58 | ) 59 | 60 | 61 | def test_check_apalache_invariant_does_not_hold(): 62 | _matching_check_value( 63 | test_dir=os.path.abspath("tests/sampleFiles/check/flawed/dir1"), 64 | tla_file_name="Hello.tla", 65 | config_file_name="Hello.cfg", 66 | expected_result=False, 67 | check_function=check_apalache, 68 | ) 69 | 70 | 71 | # def test_check_tlc_invariant_holds(): 72 | # _matching_check_value( 73 | # test_dir=os.path.abspath("tests/sampleFiles/check/correct/dir1"), 74 | # tla_file_name="Hello.tla", 75 | # config_file_name="Hello.cfg", 76 | # expected_result=True, 77 | # check_function=check_tlc, 78 | # ) 79 | -------------------------------------------------------------------------------- /tests/test_model_parse.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from modelator.Model import Model 6 | from modelator.utils.model_exceptions import ModelParsingError 7 | 8 | 9 | def test_parse_success(): 10 | file_path = "tests/sampleFiles/parse/correct/dir1/Hello_Hi.tla" 11 | files_contents = {os.path.basename(file_path): open(file_path).read()} 12 | m = Model(file_path, "Init", "Next", files_contents) 13 | m.parse() 14 | assert isinstance(m, Model) 15 | 16 | 17 | def test_parse_fail_indentation(): 18 | file_path = "tests/sampleFiles/parse/flawed/dir1/HelloFlawed1.tla" 19 | files_contents = {os.path.basename(file_path): open(file_path).read()} 20 | m = Model(file_path, "Init", "Next", files_contents) 21 | with pytest.raises(ModelParsingError): 22 | m.parse() 23 | 24 | 25 | def test_parse_fail_extra_comma(): 26 | file_path = "tests/sampleFiles/parse/flawed/dir2/HelloFlawed2.tla" 27 | files_contents = {os.path.basename(file_path): open(file_path).read()} 28 | m = Model(file_path, "Init", "Next", files_contents) 29 | with pytest.raises(ModelParsingError): 30 | m.parse() 31 | -------------------------------------------------------------------------------- /tests/test_model_parse_file.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from modelator.Model import Model 4 | from modelator.utils.model_exceptions import ModelParsingError 5 | 6 | 7 | def test_parse_file_success(): 8 | file_path = "tests/sampleFiles/parse/correct/dir1/Hello_Hi.tla" 9 | m = Model.parse_file(file_path) 10 | assert isinstance(m, Model) 11 | 12 | 13 | def test_parse_file_fail_indentation(): 14 | file_path = "tests/sampleFiles/parse/flawed/dir1/HelloFlawed1.tla" 15 | with pytest.raises(ModelParsingError): 16 | Model.parse_file(file_path) 17 | 18 | 19 | def test_parse_file_fail_extra_comma(): 20 | file_path = "tests/sampleFiles/parse/flawed/dir2/HelloFlawed2.tla" 21 | with pytest.raises(ModelParsingError): 22 | Model.parse_file(file_path) 23 | -------------------------------------------------------------------------------- /tests/test_model_typecheck.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from modelator.Model import Model 6 | from modelator.utils.model_exceptions import ModelTypecheckingError 7 | 8 | 9 | def test_typecheck_success(): 10 | file_path = "tests/sampleFiles/typecheck/correct/dir1/Hello_Hi.tla" 11 | m = Model.parse_file(os.path.abspath(file_path)) 12 | m.typecheck() 13 | 14 | 15 | def test_typecheck_fail_wrong_type_annotation(): 16 | file_path = "tests/sampleFiles/typecheck/flawed/dir1/Hello1.tla" 17 | m = Model.parse_file(os.path.abspath(file_path)) 18 | with pytest.raises(ModelTypecheckingError): 19 | m.typecheck() 20 | 21 | 22 | def test_typecheck_fail_missing_type_annotation(): 23 | file_path = "tests/sampleFiles/typecheck/flawed/dir2/Hello2.tla" 24 | m = Model.parse_file(os.path.abspath(file_path)) 25 | with pytest.raises(ModelTypecheckingError): 26 | m.typecheck() 27 | -------------------------------------------------------------------------------- /tests/test_pytest_bingame.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from modelator.pytest.decorators import mbt, step 4 | 5 | 6 | @pytest.fixture 7 | def state(): 8 | return {} 9 | 10 | 11 | @step("Zero") 12 | def zero(state, number): 13 | state["number"] = 0 14 | assert state["number"] == number 15 | 16 | 17 | @step("AddOne") 18 | def add_one(state, number): 19 | state["number"] += 1 20 | assert state["number"] == number 21 | 22 | 23 | @step("MultiplyTwo") 24 | def multiply_two(state, number): 25 | state["number"] *= 2 26 | assert state["number"] == number 27 | 28 | 29 | @mbt("tests/models/bingame.tla", keypath="action") 30 | def test_collatz(state): 31 | assert state["number"] == 30 32 | -------------------------------------------------------------------------------- /tests/test_pytest_collatz.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | import pytest 5 | 6 | from modelator.pytest.decorators import mbt, step 7 | 8 | 9 | @dataclass 10 | class Number: 11 | val: Optional[int] 12 | 13 | 14 | @pytest.fixture 15 | def state() -> Number: 16 | return Number(None) 17 | 18 | 19 | @step() 20 | def collatz(state: Number, x): 21 | if state.val is None: 22 | state.val = x 23 | else: 24 | n = state.val 25 | if n % 2 == 0: 26 | next_val = n // 2 27 | else: 28 | next_val = 1 + n * 3 29 | assert next_val == x 30 | state.val = next_val 31 | 32 | 33 | @mbt("tests/models/collatz.tla") 34 | def test_collatz(): 35 | print("pass test") 36 | -------------------------------------------------------------------------------- /tests/test_release.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import modelator 4 | 5 | 6 | def test_version(): 7 | poetry_version = ( 8 | subprocess.check_output(["poetry", "version", "-s"]).decode().strip() 9 | ) 10 | src_version = modelator.__version__ 11 | assert poetry_version == src_version 12 | -------------------------------------------------------------------------------- /tests/traces/itf/TupleAndBigint.itf.json: -------------------------------------------------------------------------------- 1 | { 2 | "#meta": { 3 | "format": "ITF", 4 | "format-description": "https://apalache.informal.systems/docs/adr/015adr-trace.html", 5 | "description": "Created by Apalache on Tue Sep 06 09:50:50 CEST 2022" 6 | }, 7 | "vars": ["balances", "action", "step"], 8 | "states": [ 9 | { 10 | "#meta": { 11 | "index": 0 12 | }, 13 | "action": { 14 | "tag": "Init", 15 | "value": { 16 | "wallets": { 17 | "#tup": ["Alice", "Bob"] 18 | } 19 | } 20 | }, 21 | "balances": { 22 | "#map": [ 23 | [ 24 | "Alice", 25 | { 26 | "#bigint": "1000000000000000000000000000001" 27 | } 28 | ], 29 | [ 30 | "Bob", 31 | { 32 | "#bigint": "1000000000000000000000000000001" 33 | } 34 | ] 35 | ] 36 | }, 37 | "step": 0 38 | }, 39 | { 40 | "#meta": { 41 | "index": 1 42 | }, 43 | "action": { 44 | "tag": "Transfer", 45 | "value": { 46 | "amount": 3, 47 | "receiver": "Alice", 48 | "sender": "Bob" 49 | } 50 | }, 51 | "balances": { 52 | "#map": [ 53 | [ 54 | "Alice", 55 | { 56 | "#bigint": "1000000000000000000000000000004" 57 | } 58 | ], 59 | [ 60 | "Bob", 61 | { 62 | "#bigint": "999999999999999999999999999998" 63 | } 64 | ] 65 | ] 66 | }, 67 | "step": 1 68 | }, 69 | { 70 | "#meta": { 71 | "index": 2 72 | }, 73 | "action": { 74 | "tag": "Transfer", 75 | "value": { 76 | "amount": 1, 77 | "receiver": "Bob", 78 | "sender": "Alice" 79 | } 80 | }, 81 | "balances": { 82 | "#map": [ 83 | [ 84 | "Alice", 85 | { 86 | "#bigint": "1000000000000000000000000000003" 87 | } 88 | ], 89 | [ 90 | "Bob", 91 | { 92 | "#bigint": "999999999999999999999999999999" 93 | } 94 | ] 95 | ] 96 | }, 97 | "step": 2 98 | }, 99 | { 100 | "#meta": { 101 | "index": 3 102 | }, 103 | "action": { 104 | "tag": "Transfer", 105 | "value": { 106 | "amount": 1, 107 | "receiver": "Bob", 108 | "sender": "Alice" 109 | } 110 | }, 111 | "balances": { 112 | "#map": [ 113 | [ 114 | "Alice", 115 | { 116 | "#bigint": "1000000000000000000000000000002" 117 | } 118 | ], 119 | [ 120 | "Bob", 121 | { 122 | "#bigint": "1000000000000000000000000000000" 123 | } 124 | ] 125 | ] 126 | }, 127 | "step": 3 128 | }, 129 | { 130 | "#meta": { 131 | "index": 4 132 | }, 133 | "action": { 134 | "tag": "Transfer", 135 | "value": { 136 | "amount": 1, 137 | "receiver": "Bob", 138 | "sender": "Alice" 139 | } 140 | }, 141 | "balances": { 142 | "#map": [ 143 | [ 144 | "Alice", 145 | { 146 | "#bigint": "1000000000000000000000000000001" 147 | } 148 | ], 149 | [ 150 | "Bob", 151 | { 152 | "#bigint": "1000000000000000000000000000001" 153 | } 154 | ] 155 | ] 156 | }, 157 | "step": 4 158 | } 159 | ] 160 | } 161 | --------------------------------------------------------------------------------