├── .github └── workflows │ ├── ci.yml │ ├── publish.yml │ └── zulip_notifier.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── lib ├── Cargo.lock ├── Cargo.toml ├── src │ ├── assert │ │ ├── guidance.rs │ │ ├── macros.rs │ │ └── mod.rs │ ├── internal │ │ ├── local_handler.rs │ │ ├── mod.rs │ │ ├── noop_handler.rs │ │ └── voidstar_handler.rs │ ├── lib.rs │ ├── lifecycle.rs │ ├── prelude.rs │ └── random.rs └── tests │ ├── assert_always_with_details.rs │ ├── assert_guidance.rs │ ├── common │ ├── env.rs │ └── mod.rs │ ├── sdk_info.rs │ ├── send_event.rs │ ├── setup_complete_with_details.rs │ └── setup_complete_without_details.rs ├── shell.nix └── simple ├── Cargo.toml └── src └── main.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | min-version: [true, false] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: cachix/install-nix-action@v26 18 | with: 19 | nix_path: nixpkgs=channel:nixos-unstable 20 | - if: ${{ matrix.min-version }} 21 | run: nix develop . -c cargo update -Z direct-minimal-versions 22 | - run: nix flake check --keep-going --print-build-logs 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Crate 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | dry-run: 7 | description: 'Is this a dry-run?' 8 | required: true 9 | default: true 10 | type: boolean 11 | 12 | jobs: 13 | publish: 14 | name: Publish 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 20 | with: 21 | toolchain: stable 22 | 23 | - name: Cargo Publish (Dry Run) 24 | if: ${{ inputs.dry-run == true || inputs.dry-run == 'true' }} 25 | run: cargo publish --dry-run --token ${CRATES_TOKEN} -p antithesis_sdk 26 | env: 27 | CRATES_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 28 | 29 | - name: Cargo Publish 30 | if: ${{ inputs.dry-run != true && inputs.dry-run != 'true' }} 31 | run: cargo publish --token ${CRATES_TOKEN} -p antithesis_sdk 32 | env: 33 | CRATES_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/zulip_notifier.yml: -------------------------------------------------------------------------------- 1 | name: Zulip Notification Bot 2 | 3 | on: 4 | push: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | post-to-zulip: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/github-script@v7 16 | id: generate-msg 17 | with: 18 | script: | 19 | let message = `- **${context.actor}** \`${context.ref}\` | ${context.sha.substring(0,7)} | [${context.payload.head_commit.message?.split('\n')[0]}](${context.payload.compare})` 20 | let topic = context.repo.repo 21 | core.setOutput("topic", topic); 22 | core.setOutput("msg", message); 23 | 24 | - name: Send a stream message 25 | uses: zulip/github-actions-zulip/send-message@v1 26 | with: 27 | api-key: ${{ secrets.ZULIP_API_KEY }} 28 | email: ${{ secrets.ZULIP_BOT_EMAIL }} 29 | organization-url: ${{ secrets.ZULIP_ORG_URL }} 30 | to: "Commits" 31 | type: "stream" 32 | topic: ${{ steps.generate-msg.outputs.topic }} 33 | content: ${{ steps.generate-msg.outputs.msg }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .envrc 3 | .direnv/ 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "antithesis_sdk" 7 | version = "0.2.5" 8 | dependencies = [ 9 | "libc", 10 | "libloading", 11 | "linkme", 12 | "once_cell", 13 | "rand", 14 | "rustc_version_runtime", 15 | "serde", 16 | "serde_json", 17 | ] 18 | 19 | [[package]] 20 | name = "cfg-if" 21 | version = "1.0.0" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 24 | 25 | [[package]] 26 | name = "getrandom" 27 | version = "0.2.12" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 30 | dependencies = [ 31 | "cfg-if", 32 | "libc", 33 | "wasi", 34 | ] 35 | 36 | [[package]] 37 | name = "itoa" 38 | version = "1.0.11" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 41 | 42 | [[package]] 43 | name = "libc" 44 | version = "0.2.153" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 47 | 48 | [[package]] 49 | name = "libloading" 50 | version = "0.8.3" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 53 | dependencies = [ 54 | "cfg-if", 55 | "windows-targets", 56 | ] 57 | 58 | [[package]] 59 | name = "linkme" 60 | version = "0.3.25" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "bb2cfee0de9bd869589fb9a015e155946d1be5ff415cb844c2caccc6cc4b5db9" 63 | dependencies = [ 64 | "linkme-impl", 65 | ] 66 | 67 | [[package]] 68 | name = "linkme-impl" 69 | version = "0.3.25" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "adf157a4dc5a29b7b464aa8fe7edeff30076e07e13646a1c3874f58477dc99f8" 72 | dependencies = [ 73 | "proc-macro2", 74 | "quote", 75 | "syn", 76 | ] 77 | 78 | [[package]] 79 | name = "once_cell" 80 | version = "1.19.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 83 | 84 | [[package]] 85 | name = "ppv-lite86" 86 | version = "0.2.17" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 89 | 90 | [[package]] 91 | name = "proc-macro2" 92 | version = "1.0.79" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" 95 | dependencies = [ 96 | "unicode-ident", 97 | ] 98 | 99 | [[package]] 100 | name = "quote" 101 | version = "1.0.35" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 104 | dependencies = [ 105 | "proc-macro2", 106 | ] 107 | 108 | [[package]] 109 | name = "rand" 110 | version = "0.8.5" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 113 | dependencies = [ 114 | "libc", 115 | "rand_chacha", 116 | "rand_core", 117 | ] 118 | 119 | [[package]] 120 | name = "rand_chacha" 121 | version = "0.3.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 124 | dependencies = [ 125 | "ppv-lite86", 126 | "rand_core", 127 | ] 128 | 129 | [[package]] 130 | name = "rand_core" 131 | version = "0.6.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 134 | dependencies = [ 135 | "getrandom", 136 | ] 137 | 138 | [[package]] 139 | name = "rustc_version" 140 | version = "0.4.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 143 | dependencies = [ 144 | "semver", 145 | ] 146 | 147 | [[package]] 148 | name = "rustc_version_runtime" 149 | version = "0.3.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" 152 | dependencies = [ 153 | "rustc_version", 154 | "semver", 155 | ] 156 | 157 | [[package]] 158 | name = "ryu" 159 | version = "1.0.17" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 162 | 163 | [[package]] 164 | name = "semver" 165 | version = "1.0.22" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 168 | 169 | [[package]] 170 | name = "serde" 171 | version = "1.0.200" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" 174 | dependencies = [ 175 | "serde_derive", 176 | ] 177 | 178 | [[package]] 179 | name = "serde_derive" 180 | version = "1.0.200" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" 183 | dependencies = [ 184 | "proc-macro2", 185 | "quote", 186 | "syn", 187 | ] 188 | 189 | [[package]] 190 | name = "serde_json" 191 | version = "1.0.115" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" 194 | dependencies = [ 195 | "itoa", 196 | "ryu", 197 | "serde", 198 | ] 199 | 200 | [[package]] 201 | name = "simple" 202 | version = "0.1.0" 203 | dependencies = [ 204 | "antithesis_sdk", 205 | "serde_json", 206 | ] 207 | 208 | [[package]] 209 | name = "syn" 210 | version = "2.0.57" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" 213 | dependencies = [ 214 | "proc-macro2", 215 | "quote", 216 | "unicode-ident", 217 | ] 218 | 219 | [[package]] 220 | name = "unicode-ident" 221 | version = "1.0.12" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 224 | 225 | [[package]] 226 | name = "wasi" 227 | version = "0.11.0+wasi-snapshot-preview1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 230 | 231 | [[package]] 232 | name = "windows-targets" 233 | version = "0.52.4" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 236 | dependencies = [ 237 | "windows_aarch64_gnullvm", 238 | "windows_aarch64_msvc", 239 | "windows_i686_gnu", 240 | "windows_i686_msvc", 241 | "windows_x86_64_gnu", 242 | "windows_x86_64_gnullvm", 243 | "windows_x86_64_msvc", 244 | ] 245 | 246 | [[package]] 247 | name = "windows_aarch64_gnullvm" 248 | version = "0.52.4" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 251 | 252 | [[package]] 253 | name = "windows_aarch64_msvc" 254 | version = "0.52.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 257 | 258 | [[package]] 259 | name = "windows_i686_gnu" 260 | version = "0.52.4" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 263 | 264 | [[package]] 265 | name = "windows_i686_msvc" 266 | version = "0.52.4" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 269 | 270 | [[package]] 271 | name = "windows_x86_64_gnu" 272 | version = "0.52.4" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 275 | 276 | [[package]] 277 | name = "windows_x86_64_gnullvm" 278 | version = "0.52.4" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 281 | 282 | [[package]] 283 | name = "windows_x86_64_msvc" 284 | version = "0.52.4" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 287 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["simple", "lib"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Antithesis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Antithesis Rust SDK 2 | 3 | This library provides methods for Rust programs to configure the [Antithesis](https://antithesis.com) platform. It contains three kinds of functionality: 4 | * Assertion macros that allow you to define test properties about your software or workload. 5 | * Randomness functions for requesting both structured and unstructured randomness from the Antithesis platform. 6 | * Lifecycle functions that inform the Antithesis environment that particular test phases or milestones have been reached. 7 | 8 | For general usage guidance see the [Antithesis Rust SDK Documentation](https://antithesis.com/docs/using_antithesis/sdk/rust/) 9 | 10 | ### Notes 11 | 12 | To disable assertions disable `default-features` for this crate. 13 | 14 | When assertions are disabled, the `condition` and `detail` arguments specified 15 | for assertions will be evaluated, but no assertions will be emitted, or otherwise processed. 16 | 17 | In this case, the assert macros will expand to 18 | nothing (other than the evaluation of `condition` and `details`). 19 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).defaultNix 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1714536327, 11 | "narHash": "sha256-zu4+LcygJwdyFHunTMeDFltBZ9+hoWvR/1A7IEy7ChA=", 12 | "owner": "ipetkov", 13 | "repo": "crane", 14 | "rev": "3124551aebd8db15d4560716d4f903bd44c64e4a", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "ipetkov", 19 | "repo": "crane", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-compat": { 24 | "locked": { 25 | "lastModified": 1696426674, 26 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 27 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 28 | "revCount": 57, 29 | "type": "tarball", 30 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" 31 | }, 32 | "original": { 33 | "type": "tarball", 34 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" 35 | } 36 | }, 37 | "flake-utils": { 38 | "inputs": { 39 | "systems": "systems" 40 | }, 41 | "locked": { 42 | "lastModified": 1705309234, 43 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "flakelight": { 56 | "inputs": { 57 | "nixpkgs": "nixpkgs" 58 | }, 59 | "locked": { 60 | "lastModified": 1714393927, 61 | "narHash": "sha256-iypUnyPbTkRSTF2g17I36sI3X7eGThfQ8QnQ7WQxnjw=", 62 | "owner": "nix-community", 63 | "repo": "flakelight", 64 | "rev": "7a838aff6f7eba0c0d1a58250d8df8e83b25bb8d", 65 | "type": "github" 66 | }, 67 | "original": { 68 | "owner": "nix-community", 69 | "repo": "flakelight", 70 | "type": "github" 71 | } 72 | }, 73 | "nixpkgs": { 74 | "locked": { 75 | "lastModified": 1714253743, 76 | "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=", 77 | "owner": "NixOS", 78 | "repo": "nixpkgs", 79 | "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994", 80 | "type": "github" 81 | }, 82 | "original": { 83 | "id": "nixpkgs", 84 | "ref": "nixos-unstable", 85 | "type": "indirect" 86 | } 87 | }, 88 | "nixpkgs_2": { 89 | "locked": { 90 | "lastModified": 1714562304, 91 | "narHash": "sha256-Mr3U37Rh6tH0FbaDFu0aZDwk9mPAe7ASaqDOGgLqqLU=", 92 | "owner": "NixOS", 93 | "repo": "nixpkgs", 94 | "rev": "bcd44e224fd68ce7d269b4f44d24c2220fd821e7", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "NixOS", 99 | "ref": "nixpkgs-unstable", 100 | "repo": "nixpkgs", 101 | "type": "github" 102 | } 103 | }, 104 | "nixpkgs_3": { 105 | "locked": { 106 | "lastModified": 1706487304, 107 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 108 | "owner": "NixOS", 109 | "repo": "nixpkgs", 110 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 111 | "type": "github" 112 | }, 113 | "original": { 114 | "owner": "NixOS", 115 | "ref": "nixpkgs-unstable", 116 | "repo": "nixpkgs", 117 | "type": "github" 118 | } 119 | }, 120 | "root": { 121 | "inputs": { 122 | "crane": "crane", 123 | "flake-compat": "flake-compat", 124 | "flakelight": "flakelight", 125 | "nixpkgs": "nixpkgs_2", 126 | "rust-overlay": "rust-overlay" 127 | } 128 | }, 129 | "rust-overlay": { 130 | "inputs": { 131 | "flake-utils": "flake-utils", 132 | "nixpkgs": "nixpkgs_3" 133 | }, 134 | "locked": { 135 | "lastModified": 1714616033, 136 | "narHash": "sha256-JcWAjIDl3h0bE/pII0emeHwokTeBl+SWrzwrjoRu7a0=", 137 | "owner": "oxalica", 138 | "repo": "rust-overlay", 139 | "rev": "3e416d5067ba31ff8ac31eeb763e4388bdf45089", 140 | "type": "github" 141 | }, 142 | "original": { 143 | "owner": "oxalica", 144 | "repo": "rust-overlay", 145 | "type": "github" 146 | } 147 | }, 148 | "systems": { 149 | "locked": { 150 | "lastModified": 1681028828, 151 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 152 | "owner": "nix-systems", 153 | "repo": "default", 154 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 155 | "type": "github" 156 | }, 157 | "original": { 158 | "owner": "nix-systems", 159 | "repo": "default", 160 | "type": "github" 161 | } 162 | } 163 | }, 164 | "root": "root", 165 | "version": 7 166 | } 167 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Development environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; 7 | flakelight.url = "github:nix-community/flakelight"; 8 | crane = { 9 | url = "github:ipetkov/crane"; 10 | inputs.nixpkgs.follows = "nixpkgs"; 11 | }; 12 | rust-overlay.url = "github:oxalica/rust-overlay"; 13 | }; 14 | 15 | outputs = { flakelight, ... } @ inputs: flakelight ./. { 16 | inherit inputs; 17 | withOverlays = [ 18 | inputs.rust-overlay.overlays.default 19 | (final: { inputs', lib, rust-bin, ... }: 20 | let 21 | # version: 22 | # "latest" => latest stable 23 | # "nightly" => latest nightly 24 | # "1.61.0" => specific stable version 25 | craneLib = version: inputs'.crane.lib.overrideToolchain (if version == "nightly" then rust-bin.nightly.latest.default else rust-bin.stable.${version}.default); 26 | commonArgs = { 27 | src = ./.; 28 | pname = "antithesis-sdk-rust-workspace"; 29 | version = "0.0.0"; 30 | }; 31 | workspaceDeps = version: (craneLib version).buildDepsOnly commonArgs; 32 | workspace = version: (craneLib version).buildPackage (commonArgs // { 33 | cargoArtifacts = workspaceDeps version; 34 | }); 35 | workspaceEmptyFeature = version: (craneLib version).buildPackage (commonArgs // { 36 | cargoArtifacts = workspaceDeps version; 37 | cargoExtraArgs = "--no-default-features"; # Disable the default `full` feature for builds. 38 | cargoTestExtraArgs = "-F full"; # But enable the `full` feature when running `cargo test`. 39 | }); 40 | clippy = version: (craneLib version).cargoClippy (commonArgs // { 41 | cargoArtifacts = workspaceDeps version; 42 | cargoClippyExtraArgs = "--all-targets -- -D warnings"; 43 | }); 44 | test = version: (craneLib version).cargoTest (commonArgs // { 45 | cargoArtifacts = workspaceDeps version; 46 | }); 47 | doc = version: (craneLib version).cargoDoc (commonArgs // { 48 | cargoArtifacts = workspaceDeps version; 49 | }); 50 | in 51 | { 52 | inherit craneLib workspaceDeps; 53 | antithesis-sdk-rust = { 54 | workspace = workspace "nightly"; 55 | workspaceEmptyFeature = workspaceEmptyFeature "nightly"; 56 | workspaceMSRV = workspace (lib.importTOML ./lib/Cargo.toml).package.rust-version; 57 | clippy = clippy "nightly"; 58 | test = test "nightly"; 59 | doc = doc "nightly"; 60 | }; 61 | }) 62 | ]; 63 | 64 | packages = rec { 65 | default = workspace; 66 | workspace = { antithesis-sdk-rust }: antithesis-sdk-rust.workspace; 67 | doc = { antithesis-sdk-rust }: antithesis-sdk-rust.doc; 68 | }; 69 | 70 | apps = rec { 71 | default = simple; 72 | simple = pkgs: "${pkgs.antithesis-sdk-rust-workspace}/bin/simple"; 73 | }; 74 | 75 | devShells.default = pkgs: { 76 | inputsFrom = with pkgs; [ antithesis-sdk-rust.workspace ]; 77 | packages = with pkgs; [ rust-analyzer cargo-msrv ]; 78 | }; 79 | 80 | # TODO: Perform semver check. 81 | checks = { antithesis-sdk-rust, ... }: { 82 | inherit (antithesis-sdk-rust) workspaceMSRV workspaceEmptyFeature clippy test; 83 | }; 84 | 85 | # TODO: Decide whether we want auto formatting. 86 | # formatters = pkgs: { 87 | # "*.rs" = "${pkgs.rustfmt}/bin/rustfmt"; 88 | # "*.nix" = "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt"; 89 | # }; 90 | 91 | flakelight.builtinFormatters = false; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /lib/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "antithesis_sdk_rust" 7 | version = "0.1.5" 8 | dependencies = [ 9 | "lazy_static", 10 | "libc", 11 | "libloading", 12 | "linkme", 13 | "paste", 14 | "rand", 15 | "rustc_version_runtime", 16 | "serde", 17 | "serde_json", 18 | ] 19 | 20 | [[package]] 21 | name = "cfg-if" 22 | version = "1.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 25 | 26 | [[package]] 27 | name = "getrandom" 28 | version = "0.2.14" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 31 | dependencies = [ 32 | "cfg-if", 33 | "libc", 34 | "wasi", 35 | ] 36 | 37 | [[package]] 38 | name = "itoa" 39 | version = "1.0.11" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 42 | 43 | [[package]] 44 | name = "lazy_static" 45 | version = "1.4.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 48 | 49 | [[package]] 50 | name = "libc" 51 | version = "0.2.153" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 54 | 55 | [[package]] 56 | name = "libloading" 57 | version = "0.8.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" 60 | dependencies = [ 61 | "cfg-if", 62 | "windows-targets", 63 | ] 64 | 65 | [[package]] 66 | name = "linkme" 67 | version = "0.3.25" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "bb2cfee0de9bd869589fb9a015e155946d1be5ff415cb844c2caccc6cc4b5db9" 70 | dependencies = [ 71 | "linkme-impl", 72 | ] 73 | 74 | [[package]] 75 | name = "linkme-impl" 76 | version = "0.3.25" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "adf157a4dc5a29b7b464aa8fe7edeff30076e07e13646a1c3874f58477dc99f8" 79 | dependencies = [ 80 | "proc-macro2", 81 | "quote", 82 | "syn 2.0.59", 83 | ] 84 | 85 | [[package]] 86 | name = "paste" 87 | version = "1.0.14" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 90 | 91 | [[package]] 92 | name = "ppv-lite86" 93 | version = "0.2.17" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 96 | 97 | [[package]] 98 | name = "proc-macro2" 99 | version = "1.0.80" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" 102 | dependencies = [ 103 | "unicode-ident", 104 | ] 105 | 106 | [[package]] 107 | name = "quote" 108 | version = "1.0.36" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 111 | dependencies = [ 112 | "proc-macro2", 113 | ] 114 | 115 | [[package]] 116 | name = "rand" 117 | version = "0.8.5" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 120 | dependencies = [ 121 | "libc", 122 | "rand_chacha", 123 | "rand_core", 124 | ] 125 | 126 | [[package]] 127 | name = "rand_chacha" 128 | version = "0.3.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 131 | dependencies = [ 132 | "ppv-lite86", 133 | "rand_core", 134 | ] 135 | 136 | [[package]] 137 | name = "rand_core" 138 | version = "0.6.4" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 141 | dependencies = [ 142 | "getrandom", 143 | ] 144 | 145 | [[package]] 146 | name = "rustc_version" 147 | version = "0.4.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 150 | dependencies = [ 151 | "semver", 152 | ] 153 | 154 | [[package]] 155 | name = "rustc_version_runtime" 156 | version = "0.3.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" 159 | dependencies = [ 160 | "rustc_version", 161 | "semver", 162 | ] 163 | 164 | [[package]] 165 | name = "ryu" 166 | version = "1.0.17" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 169 | 170 | [[package]] 171 | name = "semver" 172 | version = "1.0.22" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 175 | 176 | [[package]] 177 | name = "serde" 178 | version = "1.0.197" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 181 | dependencies = [ 182 | "serde_derive", 183 | ] 184 | 185 | [[package]] 186 | name = "serde_derive" 187 | version = "1.0.197" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 190 | dependencies = [ 191 | "proc-macro2", 192 | "quote", 193 | "syn 2.0.59", 194 | ] 195 | 196 | [[package]] 197 | name = "serde_json" 198 | version = "1.0.116" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" 201 | dependencies = [ 202 | "itoa", 203 | "ryu", 204 | "serde", 205 | ] 206 | 207 | [[package]] 208 | name = "syn" 209 | version = "1.0.109" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 212 | dependencies = [ 213 | "proc-macro2", 214 | "quote", 215 | "unicode-ident", 216 | ] 217 | 218 | [[package]] 219 | name = "syn" 220 | version = "2.0.59" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" 223 | dependencies = [ 224 | "proc-macro2", 225 | "quote", 226 | "unicode-ident", 227 | ] 228 | 229 | [[package]] 230 | name = "unicode-ident" 231 | version = "1.0.12" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 234 | 235 | [[package]] 236 | name = "wasi" 237 | version = "0.11.0+wasi-snapshot-preview1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 240 | 241 | [[package]] 242 | name = "windows-targets" 243 | version = "0.52.5" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 246 | dependencies = [ 247 | "windows_aarch64_gnullvm", 248 | "windows_aarch64_msvc", 249 | "windows_i686_gnu", 250 | "windows_i686_gnullvm", 251 | "windows_i686_msvc", 252 | "windows_x86_64_gnu", 253 | "windows_x86_64_gnullvm", 254 | "windows_x86_64_msvc", 255 | ] 256 | 257 | [[package]] 258 | name = "windows_aarch64_gnullvm" 259 | version = "0.52.5" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 262 | 263 | [[package]] 264 | name = "windows_aarch64_msvc" 265 | version = "0.52.5" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 268 | 269 | [[package]] 270 | name = "windows_i686_gnu" 271 | version = "0.52.5" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 274 | 275 | [[package]] 276 | name = "windows_i686_gnullvm" 277 | version = "0.52.5" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 280 | 281 | [[package]] 282 | name = "windows_i686_msvc" 283 | version = "0.52.5" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 286 | 287 | [[package]] 288 | name = "windows_x86_64_gnu" 289 | version = "0.52.5" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 292 | 293 | [[package]] 294 | name = "windows_x86_64_gnullvm" 295 | version = "0.52.5" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 298 | 299 | [[package]] 300 | name = "windows_x86_64_msvc" 301 | version = "0.52.5" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 304 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "antithesis_sdk" 3 | version = "0.2.5" 4 | edition = "2021" 5 | rust-version = "1.62.1" 6 | license = "MIT" 7 | readme = "../README.md" 8 | keywords = ["antithesis", "testing", "fuzzing", "quality"] 9 | categories= ["development-tools::testing"] # https://crates.io/category_slugs 10 | repository = "https://github.com/antithesishq/antithesis-sdk-rust" 11 | homepage = "https://github.com/antithesishq/antithesis-sdk-rust" 12 | documentation = "https://antithesis.com/docs/using_antithesis/sdk/rust/overview/" 13 | description = """ 14 | Rust SDK for the Antithesis autonomous software testing platform. 15 | """ 16 | 17 | [dependencies] 18 | serde = { version = "1.0.113", features = ["derive"] } 19 | serde_json = "1.0.25" 20 | rand = "0.8" 21 | 22 | # needed only if full feature is set 23 | rustc_version_runtime = {version = "0.3", optional = true} 24 | once_cell = {version = "1", optional = true} 25 | libloading = {version = "0.8", optional = true} 26 | libc = {version = "0.2.64", optional = true} 27 | linkme = {version = "0.3.17", optional = true} 28 | 29 | 30 | [features] 31 | default = ["full"] 32 | full = ["dep:libloading", "dep:libc", "dep:linkme", "dep:once_cell", "dep:rustc_version_runtime"] -------------------------------------------------------------------------------- /lib/src/assert/guidance.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{self, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32, AtomicU64, AtomicU8, AtomicUsize}; 2 | 3 | use once_cell::sync::Lazy; 4 | use serde::Serialize; 5 | use serde_json::{json, Value}; 6 | 7 | use crate::internal; 8 | 9 | use super::AntithesisLocationInfo; 10 | 11 | // Types and traits that model the SDK filtering of numerical guidance reporting. 12 | // For assertions like "always (x < y)", we would like to only report the most extreme 13 | // violations seen so far, which is implemented by having a `Guard` that keep a maximizing 14 | // watermark on the difference (x - y). 15 | // The `AtomicMinMax` trait requirement allows multiple concurrent update to the watermark. 16 | 17 | // NOTE: The structures setup in this modules allow `Guard` to be generic over the numeric 18 | // type (or even any partially ordered type). 19 | // But due to some limitation of stable Rust, we are only instanciating `Guard` by 20 | // converting the result of all `x - y` into `f64`. 21 | // See the impl `numeric_guidance_helper` for more details on the limitation. 22 | // Once that is lifted, some implementations of `Diff` can be changed to truly take advantage 23 | // of the zero-cost polymorphism that `Guard` provides. 24 | pub struct Guard { 25 | mark: T::Atomic, 26 | } 27 | 28 | pub trait Extremal { 29 | const MIN: Self; 30 | const MAX: Self; 31 | } 32 | 33 | impl Guard 34 | where T::Atomic: Extremal { 35 | pub const fn init() -> Self { 36 | let mark = if MAX { T::Atomic::MIN } else { T::Atomic::MAX }; 37 | Self { mark } 38 | } 39 | } 40 | 41 | pub trait AtomicMinMax { 42 | type Atomic; 43 | fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; 44 | fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self; 45 | } 46 | 47 | impl Guard { 48 | pub fn should_emit(&self, new: T) -> bool { 49 | use std::cmp::Ordering::*; 50 | if MAX { 51 | let max = T::fetch_max(&self.mark, new, atomic::Ordering::SeqCst); 52 | matches!(max.partial_cmp(&new), None | Some(Less | Equal)) 53 | } else { 54 | let min = T::fetch_min(&self.mark, new, atomic::Ordering::SeqCst); 55 | matches!(min.partial_cmp(&new), None | Some(Greater | Equal)) 56 | } 57 | } 58 | } 59 | 60 | pub trait Diff { 61 | type Output; 62 | 63 | fn diff(&self, other: Self) -> Self::Output; 64 | } 65 | 66 | macro_rules! impl_extremal { 67 | ($($t:ty)*) => {$( 68 | impl Extremal for $t { 69 | const MIN: $t = <$t>::MIN; 70 | const MAX: $t = <$t>::MAX; 71 | } 72 | )*} 73 | } 74 | 75 | impl_extremal! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f32 f64 } 76 | 77 | macro_rules! impl_extremal_atomic { 78 | ($(($t:ty, $raw_t:ty))*) => {$( 79 | #[allow(clippy::declare_interior_mutable_const)] 80 | impl Extremal for $t { 81 | const MIN: $t = <$t>::new(<$raw_t>::MIN); 82 | const MAX: $t = <$t>::new(<$raw_t>::MAX); 83 | } 84 | )*} 85 | } 86 | 87 | impl_extremal_atomic! { (AtomicUsize, usize) (AtomicU8, u8) (AtomicU16, u16) (AtomicU32, u32) (AtomicU64, u64) (AtomicIsize, isize) (AtomicI8, i8) (AtomicI16, i16) (AtomicI32, i32) (AtomicI64, i64) } 88 | 89 | // For atomic floats, their minimal/maximal elements are `-inf` and `+inf` respectively. 90 | 91 | #[allow(clippy::declare_interior_mutable_const)] 92 | impl Extremal for AtomicF32 { 93 | const MIN: Self = AtomicF32(AtomicU32::new(0xff800000)); 94 | const MAX: Self = AtomicF32(AtomicU32::new(0x7f800000)); 95 | } 96 | 97 | #[allow(clippy::declare_interior_mutable_const)] 98 | impl Extremal for AtomicF64 { 99 | const MIN: Self = AtomicF64(AtomicU64::new(0xfff0000000000000)); 100 | const MAX: Self = AtomicF64(AtomicU64::new(0x7ff0000000000000)); 101 | } 102 | 103 | macro_rules! impl_atomic_min_max { 104 | ($(($t:ty, $atomic_t:ty))*) => {$( 105 | impl AtomicMinMax for $t { 106 | type Atomic = $atomic_t; 107 | 108 | fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { 109 | current.fetch_min(other, ordering) 110 | } 111 | 112 | fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { 113 | current.fetch_max(other, ordering) 114 | } 115 | } 116 | )*}; 117 | } 118 | 119 | impl_atomic_min_max! { (usize, AtomicUsize) (u8, AtomicU8) (u16, AtomicU16) (u32, AtomicU32) (u64, AtomicU64) (isize, AtomicIsize) (i8, AtomicI8) (i16, AtomicI16) (i32, AtomicI32) (i64, AtomicI64) } 120 | 121 | macro_rules! impl_atomic_min_max_float { 122 | ($(($t:ty, $atomic_t:ident, $store_t:ty))*) => {$( 123 | pub struct $atomic_t($store_t); 124 | 125 | impl AtomicMinMax for $t { 126 | type Atomic = $atomic_t; 127 | 128 | // TODO: Check the atomic orderings are used properly in general. 129 | // Right now we are always passing SeqCst, which should be fine. 130 | fn fetch_min(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { 131 | <$t>::from_bits(current.0.fetch_update(ordering, ordering, |x| Some(<$t>::from_bits(x).min(other).to_bits())).unwrap()) 132 | } 133 | 134 | fn fetch_max(current: &Self::Atomic, other: Self, ordering: atomic::Ordering) -> Self { 135 | <$t>::from_bits(current.0.fetch_update(ordering, ordering, |x| Some(<$t>::from_bits(x).max(other).to_bits())).unwrap()) 136 | } 137 | } 138 | )*}; 139 | } 140 | 141 | impl_atomic_min_max_float! { (f32, AtomicF32, AtomicU32) (f64, AtomicF64, AtomicU64)} 142 | 143 | macro_rules! impl_diff_unsigned { 144 | ($($t:ty)*) => {$( 145 | impl Diff for $t { 146 | type Output = f64; 147 | 148 | fn diff(&self, other: Self) -> Self::Output { 149 | if *self < other { 150 | -((other - self) as f64) 151 | } else { 152 | (self - other) as f64 153 | } 154 | } 155 | } 156 | )*}; 157 | } 158 | 159 | impl_diff_unsigned! { usize u8 u16 u32 u64 u128 } 160 | 161 | macro_rules! impl_diff_signed { 162 | ($(($t:ty, $unsigned_t:ty))*) => {$( 163 | impl Diff for $t { 164 | type Output = f64; 165 | 166 | fn diff(&self, other: Self) -> Self::Output { 167 | if *self < other { 168 | // For correctness, see 169 | // https://github.com/rust-lang/rust/blob/11e760b7f4e4aaa11bf51a64d4bb7f1171f6e466/library/core/src/num/int_macros.rs#L3443-L3456 170 | -((other as $unsigned_t).wrapping_sub(*self as $unsigned_t) as f64) 171 | } else { 172 | (*self as $unsigned_t).wrapping_sub(other as $unsigned_t) as f64 173 | } 174 | } 175 | } 176 | )*}; 177 | } 178 | 179 | impl_diff_signed! { (isize, usize) (i8, u8) (i16, u16) (i32, u32) (i64, u64) (i128, u128) } 180 | 181 | macro_rules! impl_diff_float { 182 | ($($t:ty)*) => {$( 183 | impl Diff for $t { 184 | type Output = f64; 185 | 186 | fn diff(&self, other: Self) -> Self::Output { 187 | (self - other) as f64 188 | } 189 | } 190 | )*}; 191 | } 192 | 193 | impl_diff_float! { f32 f64 } 194 | 195 | #[derive(Copy, Clone, Serialize)] 196 | #[serde(rename_all(serialize = "lowercase"))] 197 | pub enum GuidanceType { 198 | Numeric, 199 | Boolean, 200 | Json, 201 | } 202 | 203 | #[derive(Serialize)] 204 | struct GuidanceInfo { 205 | guidance_type: GuidanceType, 206 | message: String, 207 | id: String, 208 | location: AntithesisLocationInfo, 209 | maximize: bool, 210 | guidance_data: Value, 211 | hit: bool, 212 | } 213 | 214 | pub struct GuidanceCatalogInfo { 215 | pub guidance_type: GuidanceType, 216 | pub message: &'static str, 217 | pub id: &'static str, 218 | pub class: &'static str, 219 | pub function: &'static Lazy<&'static str>, 220 | pub file: &'static str, 221 | pub begin_line: u32, 222 | pub begin_column: u32, 223 | pub maximize: bool, 224 | } 225 | 226 | #[allow(clippy::too_many_arguments)] 227 | pub fn guidance_impl( 228 | guidance_type: GuidanceType, 229 | message: String, 230 | id: String, 231 | class: String, 232 | function: String, 233 | file: String, 234 | begin_line: u32, 235 | begin_column: u32, 236 | maximize: bool, 237 | guidance_data: Value, 238 | hit: bool, 239 | ) { 240 | let location = AntithesisLocationInfo { class, function, file, begin_line, begin_column }; 241 | let guidance = GuidanceInfo { 242 | guidance_type, message, id, location, maximize, guidance_data, hit 243 | }; 244 | 245 | internal::dispatch_output(&json!({ "antithesis_guidance": guidance })); 246 | } 247 | -------------------------------------------------------------------------------- /lib/src/assert/macros.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "full")] 2 | #[doc(hidden)] 3 | #[macro_export] 4 | macro_rules! function { 5 | ($static:ident) => { 6 | // Define a do-nothing function `'_f()'` within the context of 7 | // the function invoking an assertion. Then the ``type_name`` of 8 | // this do-nothing will be something like: 9 | // 10 | // bincrate::binmod::do_stuff::_f 11 | // 12 | // After trimming off the last three chars ``::_f`` what remains is 13 | // the full path to the name of the function invoking the assertion 14 | // 15 | // The result will be stored as a lazily initialized statics in 16 | // `$static`, so that it can be available at 17 | // assertion catalog registration time. 18 | use $crate::once_cell::sync::Lazy; 19 | fn _f() {} 20 | static $static: $crate::once_cell::sync::Lazy<&'static str> = 21 | $crate::once_cell::sync::Lazy::new(|| { 22 | fn type_name_of(_: T) -> &'static str { 23 | ::std::any::type_name::() 24 | } 25 | let name = type_name_of(_f); 26 | &name[..name.len() - 4] 27 | }); 28 | }; 29 | } 30 | 31 | /// Common handling used by all the assertion-related macros 32 | #[cfg(feature = "full")] 33 | #[doc(hidden)] 34 | #[macro_export] 35 | macro_rules! assert_helper { 36 | // The handling of this pattern-arm of assert_helper 37 | // is wrapped in a block {} to avoid name collisions 38 | (condition = $condition:expr, $message:literal, $(details = $details:expr)?, $assert_type:path, $display_type:literal, must_hit = $must_hit:literal) => {{ 39 | // Force evaluation of expressions. 40 | let condition = $condition; 41 | let details = &$crate::serde_json::json!({}); 42 | $(let details = $details;)? 43 | 44 | $crate::function!(FUN_NAME); 45 | 46 | use $crate::assert::AssertionCatalogInfo; 47 | #[$crate::linkme::distributed_slice($crate::assert::ANTITHESIS_CATALOG)] 48 | #[linkme(crate = $crate::linkme)] // Refer to our re-exported linkme. 49 | static ALWAYS_CATALOG_ITEM: AssertionCatalogInfo = AssertionCatalogInfo { 50 | assert_type: $assert_type, 51 | display_type: $display_type, 52 | condition: false, 53 | message: $message, 54 | class: ::std::module_path!(), 55 | function: &FUN_NAME, /* function: &Lazy<&str> */ 56 | file: ::std::file!(), 57 | begin_line: ::std::line!(), 58 | begin_column: ::std::column!(), 59 | must_hit: $must_hit, 60 | id: $message, 61 | }; 62 | 63 | let ptr_function = Lazy::force(&FUN_NAME); 64 | 65 | $crate::assert::assert_impl( 66 | $assert_type, /* assert_type */ 67 | $display_type.to_owned(), /* display_type */ 68 | condition, /* condition */ 69 | $message.to_owned(), /* message */ 70 | ::std::module_path!().to_owned(), /* class */ 71 | String::from(*ptr_function), /* function */ 72 | ::std::file!().to_owned(), /* file */ 73 | ::std::line!(), /* line */ 74 | ::std::column!(), /* column */ 75 | true, /* hit */ 76 | $must_hit, /* must-hit */ 77 | $message.to_owned(), /* id */ 78 | details, /* details */ 79 | ) 80 | }}; // end pattern-arm block 81 | } 82 | 83 | #[cfg(not(feature = "full"))] 84 | #[doc(hidden)] 85 | #[macro_export] 86 | macro_rules! assert_helper { 87 | (condition = $condition:expr, $message:literal, $(details = $details:expr)?, $assert_type:path, $display_type:literal, must_hit = $must_hit:literal) => {{ 88 | // Force evaluation of expressions, ensuring that 89 | // any side effects of these expressions will always be 90 | // evaluated at runtime - even if the assertion itself 91 | // is supressed by the `no-antithesis-sdk` feature 92 | let condition = $condition; 93 | $(let details = $details;)? 94 | }}; 95 | } 96 | 97 | /// Assert that ``condition`` is true every time this function is called, **and** that it is 98 | /// called at least once. The corresponding test property will be viewable in the ``Antithesis SDK: Always`` group of your triage report. 99 | /// 100 | /// # Example 101 | /// 102 | /// ``` 103 | /// use serde_json::{json}; 104 | /// use antithesis_sdk::{assert_always, random}; 105 | /// 106 | /// const MAX_ALLOWED: u64 = 100; 107 | /// let actual = random::get_random() % 100u64; 108 | /// let details = json!({"max_allowed": MAX_ALLOWED, "actual": actual}); 109 | /// antithesis_sdk::assert_always!(actual < MAX_ALLOWED, "Value in range", &details); 110 | /// ``` 111 | #[macro_export] 112 | macro_rules! assert_always { 113 | ($condition:expr, $message:literal$(, $details:expr)?) => { 114 | $crate::assert_helper!( 115 | condition = $condition, 116 | $message, 117 | $(details = $details)?, 118 | $crate::assert::AssertType::Always, 119 | "Always", 120 | must_hit = true 121 | ) 122 | }; 123 | ($($rest:tt)*) => { 124 | ::std::compile_error!( 125 | r#"Invalid syntax when calling macro `assert_always`. 126 | Example usage: 127 | `assert_always!(condition_expr, "assertion message (static literal)", &details_json_value_expr)` 128 | "# 129 | ); 130 | }; 131 | } 132 | 133 | /// Assert that ``condition`` is true every time this function is called. The corresponding test property will pass even if the assertion is never encountered. 134 | /// This test property will be viewable in the ``Antithesis SDK: Always`` group of your triage report. 135 | /// 136 | /// # Example 137 | /// 138 | /// ``` 139 | /// use serde_json::{json}; 140 | /// use antithesis_sdk::{assert_always_or_unreachable, random}; 141 | /// 142 | /// const MAX_ALLOWED: u64 = 100; 143 | /// let actual = random::get_random() % 100u64; 144 | /// let details = json!({"max_allowed": MAX_ALLOWED, "actual": actual}); 145 | /// antithesis_sdk::assert_always_or_unreachable!(actual < MAX_ALLOWED, "Value in range", &details); 146 | /// ``` 147 | #[macro_export] 148 | macro_rules! assert_always_or_unreachable { 149 | ($condition:expr, $message:literal$(, $details:expr)?) => { 150 | $crate::assert_helper!( 151 | condition = $condition, 152 | $message, 153 | $(details = $details)?, 154 | $crate::assert::AssertType::Always, 155 | "AlwaysOrUnreachable", 156 | must_hit = false 157 | ) 158 | }; 159 | ($($rest:tt)*) => { 160 | ::std::compile_error!( 161 | r#"Invalid syntax when calling macro `assert_always_or_unreachable`. 162 | Example usage: 163 | `assert_always_or_unreachable!(condition_expr, "assertion message (static literal)", &details_json_value_expr)` 164 | "# 165 | ); 166 | }; 167 | } 168 | 169 | /// Assert that ``condition`` is true at least one time that this function was called. 170 | /// (If the assertion is never encountered, the test property will therefore fail.) 171 | /// This test property will be viewable in the ``Antithesis SDK: Sometimes`` group. 172 | /// 173 | /// # Example 174 | /// 175 | /// ``` 176 | /// use serde_json::{json}; 177 | /// use antithesis_sdk::{assert_sometimes, random}; 178 | /// 179 | /// const MAX_ALLOWED: u64 = 100; 180 | /// let actual = random::get_random() % 120u64; 181 | /// let details = json!({"max_allowed": MAX_ALLOWED, "actual": actual}); 182 | /// antithesis_sdk::assert_sometimes!(actual > MAX_ALLOWED, "Value in range", &details); 183 | /// ``` 184 | #[macro_export] 185 | macro_rules! assert_sometimes { 186 | ($condition:expr, $message:literal$(, $details:expr)?) => { 187 | $crate::assert_helper!( 188 | condition = $condition, 189 | $message, 190 | $(details = $details)?, 191 | $crate::assert::AssertType::Sometimes, 192 | "Sometimes", 193 | must_hit = true 194 | ) 195 | }; 196 | ($($rest:tt)*) => { 197 | ::std::compile_error!( 198 | r#"Invalid syntax when calling macro `assert_sometimes`. 199 | Example usage: 200 | `assert_sometimes!(condition_expr, "assertion message (static literal)", &details_json_value_expr)` 201 | "# 202 | ); 203 | }; 204 | } 205 | 206 | /// Assert that a line of code is reached at least once. 207 | /// The corresponding test property will pass if this macro is ever called. (If it is never called the test property will therefore fail.) 208 | /// This test property will be viewable in the ``Antithesis SDK: Reachablity assertions`` group. 209 | /// 210 | /// # Example 211 | /// 212 | /// ``` 213 | /// use serde_json::{json}; 214 | /// use antithesis_sdk::{assert_reachable, random}; 215 | /// 216 | /// const MAX_ALLOWED: u64 = 100; 217 | /// let actual = random::get_random() % 120u64; 218 | /// let details = json!({"max_allowed": MAX_ALLOWED, "actual": actual}); 219 | /// if (actual > MAX_ALLOWED) { 220 | /// antithesis_sdk::assert_reachable!("Value in range", &details); 221 | /// } 222 | /// ``` 223 | #[macro_export] 224 | macro_rules! assert_reachable { 225 | ($message:literal$(, $details:expr)?) => { 226 | $crate::assert_helper!( 227 | condition = true, 228 | $message, 229 | $(details = $details)?, 230 | $crate::assert::AssertType::Reachability, 231 | "Reachable", 232 | must_hit = true 233 | ) 234 | }; 235 | ($($rest:tt)*) => { 236 | ::std::compile_error!( 237 | r#"Invalid syntax when calling macro `assert_reachable`. 238 | Example usage: 239 | `assert_reachable!("assertion message (static literal)", &details_json_value_expr)` 240 | "# 241 | ); 242 | }; 243 | } 244 | 245 | /// Assert that a line of code is never reached. 246 | /// The corresponding test property will fail if this macro is ever called. 247 | /// (If it is never called the test property will therefore pass.) 248 | /// This test property will be viewable in the ``Antithesis SDK: Reachablity assertions`` group. 249 | /// 250 | /// # Example 251 | /// 252 | /// ``` 253 | /// use serde_json::{json}; 254 | /// use antithesis_sdk::{assert_unreachable, random}; 255 | /// 256 | /// const MAX_ALLOWED: u64 = 100; 257 | /// let actual = random::get_random() % 120u64; 258 | /// let details = json!({"max_allowed": MAX_ALLOWED, "actual": actual}); 259 | /// if (actual > 120u64) { 260 | /// antithesis_sdk::assert_unreachable!("Value is above range", &details); 261 | /// } 262 | /// ``` 263 | #[macro_export] 264 | macro_rules! assert_unreachable { 265 | ($message:literal$(, $details:expr)?) => { 266 | $crate::assert_helper!( 267 | condition = false, 268 | $message, 269 | $(details = $details)?, 270 | $crate::assert::AssertType::Reachability, 271 | "Unreachable", 272 | must_hit = false 273 | ) 274 | }; 275 | ($($rest:tt)*) => { 276 | ::std::compile_error!( 277 | r#"Invalid syntax when calling macro `assert_unreachable`. 278 | Example usage: 279 | `assert_unreachable!("assertion message (static literal)", &details_json_value_expr)` 280 | "# 281 | ); 282 | }; 283 | } 284 | 285 | #[cfg(feature = "full")] 286 | #[doc(hidden)] 287 | #[macro_export] 288 | macro_rules! guidance_helper { 289 | ($guidance_type:expr, $message:literal, $maximize:literal, $guidance_data:expr) => { 290 | $crate::function!(FUN_NAME); 291 | 292 | use $crate::assert::guidance::{GuidanceCatalogInfo, GuidanceType}; 293 | #[$crate::linkme::distributed_slice($crate::assert::ANTITHESIS_GUIDANCE_CATALOG)] 294 | #[linkme(crate = $crate::linkme)] // Refer to our re-exported linkme. 295 | static GUIDANCE_CATALOG_ITEM: GuidanceCatalogInfo = GuidanceCatalogInfo { 296 | guidance_type: $guidance_type, 297 | message: $message, 298 | id: $message, 299 | class: ::std::module_path!(), 300 | function: &FUN_NAME, 301 | file: ::std::file!(), 302 | begin_line: ::std::line!(), 303 | begin_column: ::std::column!(), 304 | maximize: $maximize, 305 | }; 306 | 307 | $crate::assert::guidance::guidance_impl( 308 | $guidance_type, 309 | $message.to_owned(), 310 | $message.to_owned(), 311 | ::std::module_path!().to_owned(), 312 | Lazy::force(&FUN_NAME).to_string(), 313 | ::std::file!().to_owned(), 314 | ::std::line!(), 315 | ::std::column!(), 316 | $maximize, 317 | $guidance_data, 318 | true, 319 | ) 320 | }; 321 | } 322 | 323 | #[cfg(feature = "full")] 324 | #[doc(hidden)] 325 | #[macro_export] 326 | macro_rules! numeric_guidance_helper { 327 | ($assert:path, $op:tt, $maximize:literal, $left:expr, $right:expr, $message:literal$(, $details:expr)?) => {{ 328 | let left = $left; 329 | let right = $right; 330 | let details = &$crate::serde_json::json!({}); 331 | $(let details = $details;)? 332 | let mut details = details.clone(); 333 | details["left"] = left.into(); 334 | details["right"] = right.into(); 335 | $assert!(left $op right, $message, &details); 336 | 337 | let guidance_data = $crate::serde_json::json!({ 338 | "left": left, 339 | "right": right, 340 | }); 341 | // TODO: Right now it seems to be impossible for this macro to use the returned 342 | // type of `diff` to instanciate the `T` in `Guard`, which has to be 343 | // explicitly provided for the static variable `GUARD`. 344 | // Instead, we currently fix `T` to be `f64`, and ensure all implementations of `Diff` returns `f64`. 345 | // Here are some related language limitations: 346 | // - Although `typeof` is a reserved keyword in Rust, it is never implemented. See . 347 | // - Rust does not, and explicitly would not (see https://doc.rust-lang.org/reference/items/static-items.html#statics--generics), support generic static variable. 348 | // - Type inference is not performed for static variable, i.e. `Guard<_>` is not allowed. 349 | // - Some form of existential type can help, but that's only available in nightly Rust under feature `type_alias_impl_trait`. 350 | // 351 | // Other approaches I can think of either requires dynamic type tagging that has 352 | // runtime overhead, or requires the user of the macro to explicitly provide the type, 353 | // which is really not ergonomic and deviate from the APIs from other SDKs. 354 | let diff = $crate::assert::guidance::Diff::diff(&left, right); 355 | type Guard = $crate::assert::guidance::Guard<$maximize, T>; 356 | // TODO: Waiting for [type_alias_impl_trait](https://github.com/rust-lang/rust/issues/63063) to stabilize... 357 | // type Distance = impl Minimal; 358 | type Distance = f64; 359 | static GUARD: Guard = Guard::init(); 360 | if GUARD.should_emit(diff) { 361 | $crate::guidance_helper!($crate::assert::guidance::GuidanceType::Numeric, $message, $maximize, guidance_data); 362 | } 363 | }}; 364 | } 365 | 366 | #[cfg(not(feature = "full"))] 367 | #[doc(hidden)] 368 | #[macro_export] 369 | macro_rules! numeric_guidance_helper { 370 | ($assert:path, $op:tt, $maximize:literal, $left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 371 | assert!($left $op $right, $message$(, $details)?); 372 | }; 373 | } 374 | 375 | #[cfg(feature = "full")] 376 | #[doc(hidden)] 377 | #[macro_export] 378 | macro_rules! boolean_guidance_helper { 379 | ($assert:path, $all:literal, {$($name:ident: $cond:expr),*}, $message:literal$(, $details:expr)?) => {{ 380 | let details = &$crate::serde_json::json!({}); 381 | $(let details = $details;)? 382 | let mut details = details.clone(); 383 | let (cond, guidance_data) = { 384 | $(let $name = $cond;)* 385 | $(details[::std::stringify!($name)] = $name.into();)* 386 | ( 387 | if $all { true $(&& $name)* } else { false $(|| $name)* }, 388 | $crate::serde_json::json!({$(::std::stringify!($name): $name),*}) 389 | ) 390 | }; 391 | $assert!(cond, $message, &details); 392 | $crate::guidance_helper!($crate::assert::guidance::GuidanceType::Boolean, $message, $all, guidance_data); 393 | }}; 394 | } 395 | 396 | #[cfg(not(feature = "full"))] 397 | #[doc(hidden)] 398 | #[macro_export] 399 | macro_rules! boolean_guidance_helper { 400 | ($assert:path, $all:literal, {$($name:ident: $cond:expr),*}, $message:literal$(, $details:expr)?) => {{ 401 | let cond = { 402 | $(let $name = $cond;)* 403 | if $all { true $(&& $name)* } else { false $(|| $name)* } 404 | }; 405 | $assert!(cond, $message$(, $details)?); 406 | }}; 407 | } 408 | 409 | /// `assert_always_greater_than(x, y, ...)` is mostly equivalent to `assert_always!(x > y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 410 | #[macro_export] 411 | macro_rules! assert_always_greater_than { 412 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 413 | $crate::numeric_guidance_helper!($crate::assert_always, >, false, $left, $right, $message$(, $details)?) 414 | }; 415 | ($($rest:tt)*) => { 416 | ::std::compile_error!( 417 | r#"Invalid syntax when calling macro `assert_always_greater_than`. 418 | Example usage: 419 | `assert_always_greater_than!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 420 | "# 421 | ); 422 | }; 423 | } 424 | 425 | /// `assert_always_greater_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_always!(x >= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 426 | #[macro_export] 427 | macro_rules! assert_always_greater_than_or_equal_to { 428 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 429 | $crate::numeric_guidance_helper!($crate::assert_always, >=, false, $left, $right, $message, $details) 430 | }; 431 | ($($rest:tt)*) => { 432 | ::std::compile_error!( 433 | r#"Invalid syntax when calling macro `assert_always_greater_than_or_equal_to`. 434 | Example usage: 435 | `assert_always_greater_than_or_equal_to!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 436 | "# 437 | ); 438 | }; 439 | } 440 | 441 | /// `assert_always_less_than(x, y, ...)` is mostly equivalent to `assert_always!(x < y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 442 | #[macro_export] 443 | macro_rules! assert_always_less_than { 444 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 445 | $crate::numeric_guidance_helper!($crate::assert_always, <, true, $left, $right, $message, $details) 446 | }; 447 | ($($rest:tt)*) => { 448 | ::std::compile_error!( 449 | r#"Invalid syntax when calling macro `assert_always_less_than`. 450 | Example usage: 451 | `assert_always_less_than!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 452 | "# 453 | ); 454 | }; 455 | } 456 | 457 | /// `assert_always_less_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_always!(x <= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 458 | #[macro_export] 459 | macro_rules! assert_always_less_than_or_equal_to { 460 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 461 | $crate::numeric_guidance_helper!($crate::assert_always, <=, true, $left, $right, $message, $details) 462 | }; 463 | ($($rest:tt)*) => { 464 | ::std::compile_error!( 465 | r#"Invalid syntax when calling macro `assert_always_less_than_or_equal_to`. 466 | Example usage: 467 | `assert_always_less_than_or_equal_to!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 468 | "# 469 | ); 470 | }; 471 | } 472 | 473 | /// `assert_sometimes_greater_than(x, y, ...)` is mostly equivalent to `assert_sometimes!(x > y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 474 | #[macro_export] 475 | macro_rules! assert_sometimes_greater_than { 476 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 477 | $crate::numeric_guidance_helper!($crate::assert_sometimes, >, true, $left, $right, $message, $details) 478 | }; 479 | ($($rest:tt)*) => { 480 | ::std::compile_error!( 481 | r#"Invalid syntax when calling macro `assert_sometimes_greater_than`. 482 | Example usage: 483 | `assert_sometimes_greater_than!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 484 | "# 485 | ); 486 | }; 487 | } 488 | 489 | /// `assert_sometimes_greater_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_sometimes!(x >= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 490 | #[macro_export] 491 | macro_rules! assert_sometimes_greater_than_or_equal_to { 492 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 493 | $crate::numeric_guidance_helper!($crate::assert_sometimes, >=, true, $left, $right, $message, $details) 494 | }; 495 | ($($rest:tt)*) => { 496 | ::std::compile_error!( 497 | r#"Invalid syntax when calling macro `assert_sometimes_greater_than_or_equal_to`. 498 | Example usage: 499 | `assert_sometimes_greater_than_or_equal_to!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 500 | "# 501 | ); 502 | }; 503 | } 504 | 505 | /// `assert_sometimes_less_than(x, y, ...)` is mostly equivalent to `assert_sometimes!(x < y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 506 | #[macro_export] 507 | macro_rules! assert_sometimes_less_than { 508 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 509 | $crate::numeric_guidance_helper!($crate::assert_sometimes, <, false, $left, $right, $message, $details) 510 | }; 511 | ($($rest:tt)*) => { 512 | ::std::compile_error!( 513 | r#"Invalid syntax when calling macro `assert_sometimes_less_than`. 514 | Example usage: 515 | `assert_sometimes_less_than!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 516 | "# 517 | ); 518 | }; 519 | } 520 | 521 | /// `assert_sometimes_less_than_or_equal_to(x, y, ...)` is mostly equivalent to `assert_sometimes!(x <= y, ...)`, except Antithesis has more visibility to the value of `x` and `y`, and the assertion details would be merged with `{"left": x, "right": y}`. 522 | #[macro_export] 523 | macro_rules! assert_sometimes_less_than_or_equal_to { 524 | ($left:expr, $right:expr, $message:literal$(, $details:expr)?) => { 525 | $crate::numeric_guidance_helper!($crate::assert_sometimes, <=, false, $left, $right, $message, $details) 526 | }; 527 | ($($rest:tt)*) => { 528 | ::std::compile_error!( 529 | r#"Invalid syntax when calling macro `assert_sometimes_less_than_or_equal_to`. 530 | Example usage: 531 | `assert_sometimes_less_than_or_equal_to!(left_expr, right_expr, "assertion message (static literal)", &details_json_value_expr)` 532 | "# 533 | ); 534 | }; 535 | } 536 | 537 | /// `assert_always_some({a: x, b: y, ...})` is similar to `assert_always(x || y || ...)`, except: 538 | /// - Antithesis has more visibility to the individual propositions. 539 | /// - There is no short-circuiting, so all of `x`, `y`, ... would be evaluated. 540 | /// - The assertion details would be merged with `{"a": x, "b": y, ...}`. 541 | #[macro_export] 542 | macro_rules! assert_always_some { 543 | ({$($($name:ident: $cond:expr),+ $(,)?)?}, $message:literal$(, $details:expr)?) => { 544 | $crate::boolean_guidance_helper!($crate::assert_always, false, {$($($name: $cond),+)?}, $message$(, $details)?); 545 | }; 546 | ($($rest:tt)*) => { 547 | ::std::compile_error!( 548 | r#"Invalid syntax when calling macro `assert_always_some`. 549 | Example usage: 550 | `assert_always_some!({field1: cond1, field2: cond2, ...}, "assertion message (static literal)", &details_json_value_expr)` 551 | "# 552 | ); 553 | }; 554 | } 555 | 556 | /// `assert_sometimes_all({a: x, b: y, ...})` is similar to `assert_sometimes(x && y && ...)`, except: 557 | /// - Antithesis has more visibility to the individual propositions. 558 | /// - There is no short-circuiting, so all of `x`, `y`, ... would be evaluated. 559 | /// - The assertion details would be merged with `{"a": x, "b": y, ...}`. 560 | #[macro_export] 561 | macro_rules! assert_sometimes_all { 562 | ({$($($name:ident: $cond:expr),+ $(,)?)?}, $message:literal$(, $details:expr)?) => { 563 | $crate::boolean_guidance_helper!($crate::assert_sometimes, true, {$($($name: $cond),+)?}, $message$(, $details)?); 564 | }; 565 | ($($rest:tt)*) => { 566 | ::std::compile_error!( 567 | r#"Invalid syntax when calling macro `assert_sometimes_all`. 568 | Example usage: 569 | `assert_sometimes_all!({field1: cond1, field2: cond2, ...}, "assertion message (static literal)", &details_json_value_expr)` 570 | "# 571 | ); 572 | }; 573 | } 574 | -------------------------------------------------------------------------------- /lib/src/assert/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "full")] 2 | use crate::internal; 3 | #[cfg(feature = "full")] 4 | use linkme::distributed_slice; 5 | #[cfg(feature = "full")] 6 | use once_cell::sync::Lazy; 7 | use serde::Serialize; 8 | use serde_json::Value; 9 | #[cfg(feature = "full")] 10 | use serde_json::json; 11 | 12 | #[cfg(feature = "full")] 13 | use std::collections::HashMap; 14 | #[cfg(feature = "full")] 15 | use std::sync::Mutex; 16 | 17 | mod macros; 18 | #[doc(hidden)] 19 | #[cfg(feature = "full")] 20 | pub mod guidance; 21 | 22 | /// Catalog of all antithesis assertions provided 23 | #[doc(hidden)] 24 | #[distributed_slice] 25 | #[cfg(feature = "full")] 26 | pub static ANTITHESIS_CATALOG: [AssertionCatalogInfo]; 27 | 28 | /// Catalog of all antithesis guidances provided 29 | #[doc(hidden)] 30 | #[distributed_slice] 31 | #[cfg(feature = "full")] 32 | pub static ANTITHESIS_GUIDANCE_CATALOG: [self::guidance::GuidanceCatalogInfo]; 33 | 34 | // Only need an ASSET_TRACKER if there are actually assertions 'hit' 35 | // (i.e. encountered and invoked at runtime). 36 | // 37 | // Typically runtime assertions use the macros ``always!``, ``sometimes!``, etc. 38 | // or, a client is using the 'raw' interface ``assert_raw`` at runtime. 39 | // 40 | #[cfg(feature = "full")] 41 | pub(crate) static ASSERT_TRACKER: Lazy>> = 42 | Lazy::new(|| Mutex::new(HashMap::new())); 43 | 44 | #[cfg(feature = "full")] 45 | pub(crate) static INIT_CATALOG: Lazy<()> = Lazy::new(|| { 46 | for info in ANTITHESIS_CATALOG.iter() { 47 | let f_name: &str = info.function.as_ref(); 48 | assert_impl( 49 | info.assert_type, 50 | info.display_type.to_owned(), 51 | info.condition, 52 | info.message.to_owned(), 53 | info.class.to_owned(), 54 | f_name.to_owned(), 55 | info.file.to_owned(), 56 | info.begin_line, 57 | info.begin_column, 58 | false, /* hit */ 59 | info.must_hit, 60 | info.id.to_owned(), 61 | &json!(null), 62 | ); 63 | } 64 | for info in ANTITHESIS_GUIDANCE_CATALOG.iter() { 65 | guidance::guidance_impl( 66 | info.guidance_type, 67 | info.message.to_owned(), 68 | info.id.to_owned(), 69 | info.class.to_owned(), 70 | Lazy::force(info.function).to_string(), 71 | info.file.to_owned(), 72 | info.begin_line, 73 | info.begin_column, 74 | info.maximize, 75 | json!(null), 76 | false, 77 | ) 78 | } 79 | }); 80 | 81 | #[cfg(feature = "full")] 82 | pub(crate) struct TrackingInfo { 83 | pub pass_count: u64, 84 | pub fail_count: u64, 85 | } 86 | 87 | #[cfg(feature = "full")] 88 | impl Default for TrackingInfo { 89 | fn default() -> Self { 90 | Self::new() 91 | } 92 | } 93 | 94 | #[cfg(feature = "full")] 95 | impl TrackingInfo { 96 | pub fn new() -> Self { 97 | TrackingInfo { 98 | pass_count: 0, 99 | fail_count: 0, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Copy, Clone, PartialEq, Debug, Serialize)] 105 | #[serde(rename_all(serialize = "lowercase"))] 106 | pub enum AssertType { 107 | Always, 108 | Sometimes, 109 | Reachability, 110 | } 111 | 112 | #[derive(Serialize, Debug)] 113 | struct AntithesisLocationInfo { 114 | class: String, 115 | function: String, 116 | file: String, 117 | begin_line: u32, 118 | begin_column: u32, 119 | } 120 | 121 | /// Internal representation for assertion catalog 122 | #[doc(hidden)] 123 | #[derive(Debug)] 124 | #[cfg(feature = "full")] 125 | pub struct AssertionCatalogInfo { 126 | pub assert_type: AssertType, 127 | pub display_type: &'static str, 128 | pub condition: bool, 129 | pub message: &'static str, 130 | pub class: &'static str, 131 | pub function: &'static Lazy<&'static str>, 132 | pub file: &'static str, 133 | pub begin_line: u32, 134 | pub begin_column: u32, 135 | pub must_hit: bool, 136 | pub id: &'static str, 137 | } 138 | 139 | #[derive(Serialize, Debug)] 140 | struct AssertionInfo { 141 | assert_type: AssertType, 142 | display_type: String, 143 | condition: bool, 144 | message: String, 145 | location: AntithesisLocationInfo, 146 | hit: bool, 147 | must_hit: bool, 148 | id: String, 149 | details: Value, 150 | } 151 | 152 | impl AssertionInfo { 153 | #[allow(clippy::too_many_arguments)] 154 | pub fn new( 155 | assert_type: AssertType, 156 | display_type: String, 157 | condition: bool, 158 | message: String, 159 | class: String, 160 | function: String, 161 | file: String, 162 | begin_line: u32, 163 | begin_column: u32, 164 | hit: bool, 165 | must_hit: bool, 166 | id: String, 167 | details: &Value, 168 | ) -> Self { 169 | let location = AntithesisLocationInfo { 170 | class, 171 | function, 172 | file, 173 | begin_line, 174 | begin_column, 175 | }; 176 | 177 | AssertionInfo { 178 | assert_type, 179 | display_type, 180 | condition, 181 | message, 182 | location, 183 | hit, 184 | must_hit, 185 | id, 186 | details: details.clone(), 187 | } 188 | } 189 | } 190 | 191 | #[cfg(feature = "full")] 192 | impl AssertionInfo { 193 | // AssertionInfo::track_entry() determines if the assertion should 194 | // actually be emitted: 195 | // 196 | // [X] If this is an assertion catalog 197 | // registration (assertion.hit == false) then it is emitted. 198 | // 199 | // [X] if `condition` is true increment the tracker_entry.pass_count, 200 | // otherwise increment the tracker_entry.fail_count. 201 | // 202 | // [X] if `condition` is true and tracker_entry_pass_count == 1 then 203 | // actually emit the assertion. 204 | // 205 | // [X] if `condition` is false and tracker_entry_fail_count == 1 then 206 | // actually emit the assertion. 207 | 208 | // Verify that the TrackingInfo for self in 209 | // ASSERT_TRACKER has been updated according to self.condition 210 | fn track_entry(&self) { 211 | // Requirement: Catalog entries must always will emit() 212 | if !self.hit { 213 | self.emit(); 214 | return; 215 | } 216 | 217 | // Establish TrackingInfo for this trackingKey when needed 218 | let mut tracker = ASSERT_TRACKER.lock().unwrap(); 219 | let info = tracker.entry(self.id.clone()).or_default(); 220 | // Record the condition in the associated TrackingInfo entry, 221 | // and emit the assertion when first seeing a condition 222 | let emitting = if self.condition { 223 | info.pass_count += 1; 224 | info.pass_count == 1 225 | } else { 226 | info.fail_count += 1; 227 | info.fail_count == 1 228 | }; 229 | drop(tracker); // release the lock asap 230 | if emitting { 231 | Lazy::force(&INIT_CATALOG); 232 | self.emit(); 233 | } 234 | } 235 | 236 | fn emit(&self) { 237 | let json_event = json!({ "antithesis_assert": &self }); 238 | internal::dispatch_output(&json_event) 239 | } 240 | } 241 | 242 | #[cfg(not(feature = "full"))] 243 | impl AssertionInfo { 244 | fn track_entry(&self) { 245 | return 246 | } 247 | } 248 | 249 | 250 | /// This is a low-level method designed to be used by third-party frameworks. 251 | /// Regular users of the assert package should not call it. 252 | /// 253 | /// This is primarily intended for use by adapters from other 254 | /// diagnostic tools that intend to output Antithesis-style 255 | /// assertions. 256 | /// 257 | /// Be certain to provide an assertion catalog entry 258 | /// for each assertion issued with ``assert_raw()``. Assertion catalog 259 | /// entries are also created using ``assert_raw()``, by setting the value 260 | /// of the ``hit`` parameter to false. 261 | /// 262 | /// Please refer to the general Antithesis documentation regarding the 263 | /// use of the [Fallback SDK](https://antithesis.com/docs/using_antithesis/sdk/fallback/assert/) 264 | /// for additional information. 265 | /// 266 | /// 267 | /// 268 | /// # Example 269 | /// 270 | /// ``` 271 | /// use serde_json::{json}; 272 | /// use antithesis_sdk::{assert, random}; 273 | /// 274 | /// struct Votes { 275 | /// num_voters: u32, 276 | /// candidate_1: u32, 277 | /// candidate_2: u32, 278 | /// } 279 | /// 280 | /// fn main() { 281 | /// establish_catalog(); 282 | /// 283 | /// let mut all_votes = Votes { 284 | /// num_voters: 0, 285 | /// candidate_1: 0, 286 | /// candidate_2: 0, 287 | /// }; 288 | /// 289 | /// for _voter in 0..100 { 290 | /// tally_vote(&mut all_votes, random_bool(), random_bool()); 291 | /// } 292 | /// } 293 | /// 294 | /// fn random_bool() -> bool { 295 | /// let v1 = random::get_random() % 2; 296 | /// v1 == 1 297 | /// } 298 | /// 299 | /// fn establish_catalog() { 300 | /// assert::assert_raw( 301 | /// false, /* condition */ 302 | /// "Never extra votes".to_owned(), /* message */ 303 | /// &json!({}), /* details */ 304 | /// "mycrate::stuff".to_owned(), /* class */ 305 | /// "mycrate::tally_vote".to_owned(), /* function */ 306 | /// "src/voting.rs".to_owned(), /* file */ 307 | /// 20, /* line */ 308 | /// 3, /* column */ 309 | /// false, /* hit */ 310 | /// true, /* must_hit */ 311 | /// assert::AssertType::Always, /* assert_type */ 312 | /// "Always".to_owned(), /* display_type */ 313 | /// "42-1005".to_owned() /* id */ 314 | /// ); 315 | /// } 316 | /// 317 | /// fn tally_vote(votes: &mut Votes, candidate_1: bool, candidate_2: bool) { 318 | /// if candidate_1 || candidate_2 { 319 | /// votes.num_voters += 1; 320 | /// } 321 | /// if candidate_1 { 322 | /// votes.candidate_1 += 1; 323 | /// }; 324 | /// if candidate_2 { 325 | /// votes.candidate_2 += 1; 326 | /// }; 327 | /// 328 | /// let num_votes = votes.candidate_1 + votes.candidate_2; 329 | /// assert::assert_raw( 330 | /// num_votes == votes.num_voters, /* condition */ 331 | /// "Never extra votes".to_owned(), /* message */ 332 | /// &json!({ /* details */ 333 | /// "votes": num_votes, 334 | /// "voters": votes.num_voters 335 | /// }), 336 | /// "mycrate::stuff".to_owned(), /* class */ 337 | /// "mycrate::tally_vote".to_owned(), /* function */ 338 | /// "src/voting.rs".to_owned(), /* file */ 339 | /// 20, /* line */ 340 | /// 3, /* column */ 341 | /// true, /* hit */ 342 | /// true, /* must_hit */ 343 | /// assert::AssertType::Always, /* assert_type */ 344 | /// "Always".to_owned(), /* display_type */ 345 | /// "42-1005".to_owned() /* id */ 346 | /// ); 347 | /// } 348 | /// 349 | /// // Run example with output to /tmp/x7.json 350 | /// // ANTITHESIS_SDK_LOCAL_OUTPUT=/tmp/x7.json cargo test --doc 351 | /// // 352 | /// // Example output from /tmp/x7.json 353 | /// // Contents may vary due to use of random::get_random() 354 | /// // 355 | /// // {"antithesis_sdk":{"language":{"name":"Rust","version":"1.69.0"},"sdk_version":"0.1.2","protocol_version":"1.0.0"}} 356 | /// // {"assert_type":"always","display_type":"Always","condition":false,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":false,"must_hit":true,"id":"42-1005","details":{}} 357 | /// // {"assert_type":"always","display_type":"Always","condition":true,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":true,"must_hit":true,"id":"42-1005","details":{"voters":1,"votes":1}} 358 | /// // {"assert_type":"always","display_type":"Always","condition":false,"message":"Never extra votes","location":{"class":"mycrate::stuff","function":"mycrate::tally_vote","file":"src/voting.rs","begin_line":20,"begin_column":3},"hit":true,"must_hit":true,"id":"42-1005","details":{"voters":3,"votes":4}} 359 | /// ``` 360 | #[allow(clippy::too_many_arguments)] 361 | pub fn assert_raw( 362 | condition: bool, 363 | message: String, 364 | details: &Value, 365 | class: String, 366 | function: String, 367 | file: String, 368 | begin_line: u32, 369 | begin_column: u32, 370 | hit: bool, 371 | must_hit: bool, 372 | assert_type: AssertType, 373 | display_type: String, 374 | id: String, 375 | ) { 376 | assert_impl( 377 | assert_type, 378 | display_type, 379 | condition, 380 | message, 381 | class, 382 | function, 383 | file, 384 | begin_line, 385 | begin_column, 386 | hit, 387 | must_hit, 388 | id, 389 | details, 390 | ) 391 | } 392 | 393 | #[doc(hidden)] 394 | #[allow(clippy::too_many_arguments)] 395 | pub fn assert_impl( 396 | assert_type: AssertType, 397 | display_type: String, 398 | condition: bool, 399 | message: String, 400 | class: String, 401 | function: String, 402 | file: String, 403 | begin_line: u32, 404 | begin_column: u32, 405 | hit: bool, 406 | must_hit: bool, 407 | id: String, 408 | details: &Value, 409 | ) { 410 | let assertion = AssertionInfo::new( 411 | assert_type, 412 | display_type, 413 | condition, 414 | message, 415 | class, 416 | function, 417 | file, 418 | begin_line, 419 | begin_column, 420 | hit, 421 | must_hit, 422 | id, 423 | details, 424 | ); 425 | 426 | let _ = &assertion.track_entry(); 427 | } 428 | 429 | #[cfg(test)] 430 | mod tests { 431 | use super::*; 432 | 433 | //-------------------------------------------------------------------------------- 434 | // Tests for TrackingInfo 435 | //-------------------------------------------------------------------------------- 436 | #[test] 437 | fn new_tracking_info() { 438 | let ti = TrackingInfo::new(); 439 | assert_eq!(ti.pass_count, 0); 440 | assert_eq!(ti.fail_count, 0); 441 | } 442 | 443 | #[test] 444 | fn default_tracking_info() { 445 | let ti: TrackingInfo = Default::default(); 446 | assert_eq!(ti.pass_count, 0); 447 | assert_eq!(ti.fail_count, 0); 448 | } 449 | 450 | //-------------------------------------------------------------------------------- 451 | // Tests for AssertionInfo 452 | //-------------------------------------------------------------------------------- 453 | 454 | #[test] 455 | fn new_assertion_info_always() { 456 | let this_assert_type = AssertType::Always; 457 | let this_display_type = "Always"; 458 | let this_condition = true; 459 | let this_message = "Always message"; 460 | let this_class = "binary::always"; 461 | let this_function = "binary::always::always_function"; 462 | let this_file = "/home/user/binary/src/always_binary.rs"; 463 | let this_begin_line = 10; 464 | let this_begin_column = 5; 465 | let this_hit = true; 466 | let this_must_hit = true; 467 | let this_id = "ID Always message"; 468 | let this_details = json!({ 469 | "color": "always red", 470 | "extent": 15, 471 | }); 472 | 473 | let ai = AssertionInfo::new( 474 | this_assert_type, 475 | this_display_type.to_owned(), 476 | this_condition, 477 | this_message.to_owned(), 478 | this_class.to_owned(), 479 | this_function.to_owned(), 480 | this_file.to_owned(), 481 | this_begin_line, 482 | this_begin_column, 483 | this_hit, 484 | this_must_hit, 485 | this_id.to_owned(), 486 | &this_details, 487 | ); 488 | assert_eq!(ai.display_type.as_str(), this_display_type); 489 | assert_eq!(ai.condition, this_condition); 490 | assert_eq!(ai.message.as_str(), this_message); 491 | assert_eq!(ai.location.class.as_str(), this_class); 492 | assert_eq!(ai.location.function.as_str(), this_function); 493 | assert_eq!(ai.location.file.as_str(), this_file); 494 | assert_eq!(ai.location.begin_line, this_begin_line); 495 | assert_eq!(ai.location.begin_column, this_begin_column); 496 | assert_eq!(ai.hit, this_hit); 497 | assert_eq!(ai.must_hit, this_must_hit); 498 | assert_eq!(ai.id.as_str(), this_id); 499 | assert_eq!(ai.details, this_details); 500 | } 501 | 502 | #[test] 503 | fn new_assertion_info_sometimes() { 504 | let this_assert_type = AssertType::Sometimes; 505 | let this_display_type = "Sometimes"; 506 | let this_condition = true; 507 | let this_message = "Sometimes message"; 508 | let this_class = "binary::sometimes"; 509 | let this_function = "binary::sometimes::sometimes_function"; 510 | let this_file = "/home/user/binary/src/sometimes_binary.rs"; 511 | let this_begin_line = 11; 512 | let this_begin_column = 6; 513 | let this_hit = true; 514 | let this_must_hit = true; 515 | let this_id = "ID Sometimes message"; 516 | let this_details = json!({ 517 | "color": "sometimes red", 518 | "extent": 17, 519 | }); 520 | 521 | let ai = AssertionInfo::new( 522 | this_assert_type, 523 | this_display_type.to_owned(), 524 | this_condition, 525 | this_message.to_owned(), 526 | this_class.to_owned(), 527 | this_function.to_owned(), 528 | this_file.to_owned(), 529 | this_begin_line, 530 | this_begin_column, 531 | this_hit, 532 | this_must_hit, 533 | this_id.to_owned(), 534 | &this_details, 535 | ); 536 | assert_eq!(ai.display_type.as_str(), this_display_type); 537 | assert_eq!(ai.condition, this_condition); 538 | assert_eq!(ai.message.as_str(), this_message); 539 | assert_eq!(ai.location.class.as_str(), this_class); 540 | assert_eq!(ai.location.function.as_str(), this_function); 541 | assert_eq!(ai.location.file.as_str(), this_file); 542 | assert_eq!(ai.location.begin_line, this_begin_line); 543 | assert_eq!(ai.location.begin_column, this_begin_column); 544 | assert_eq!(ai.hit, this_hit); 545 | assert_eq!(ai.must_hit, this_must_hit); 546 | assert_eq!(ai.id.as_str(), this_id); 547 | assert_eq!(ai.details, this_details); 548 | } 549 | 550 | #[test] 551 | fn new_assertion_info_reachable() { 552 | let this_assert_type = AssertType::Reachability; 553 | let this_display_type = "Reachable"; 554 | let this_condition = true; 555 | let this_message = "Reachable message"; 556 | let this_class = "binary::reachable"; 557 | let this_function = "binary::reachable::reachable_function"; 558 | let this_file = "/home/user/binary/src/reachable_binary.rs"; 559 | let this_begin_line = 12; 560 | let this_begin_column = 7; 561 | let this_hit = true; 562 | let this_must_hit = true; 563 | let this_id = "ID Reachable message"; 564 | let this_details = json!({ 565 | "color": "reachable red", 566 | "extent": 19, 567 | }); 568 | 569 | let ai = AssertionInfo::new( 570 | this_assert_type, 571 | this_display_type.to_owned(), 572 | this_condition, 573 | this_message.to_owned(), 574 | this_class.to_owned(), 575 | this_function.to_owned(), 576 | this_file.to_owned(), 577 | this_begin_line, 578 | this_begin_column, 579 | this_hit, 580 | this_must_hit, 581 | this_id.to_owned(), 582 | &this_details, 583 | ); 584 | assert_eq!(ai.display_type.as_str(), this_display_type); 585 | assert_eq!(ai.condition, this_condition); 586 | assert_eq!(ai.message.as_str(), this_message); 587 | assert_eq!(ai.location.class.as_str(), this_class); 588 | assert_eq!(ai.location.function.as_str(), this_function); 589 | assert_eq!(ai.location.file.as_str(), this_file); 590 | assert_eq!(ai.location.begin_line, this_begin_line); 591 | assert_eq!(ai.location.begin_column, this_begin_column); 592 | assert_eq!(ai.hit, this_hit); 593 | assert_eq!(ai.must_hit, this_must_hit); 594 | assert_eq!(ai.id.as_str(), this_id); 595 | assert_eq!(ai.details, this_details); 596 | } 597 | 598 | #[test] 599 | fn assert_impl_pass() { 600 | let this_assert_type = AssertType::Always; 601 | let this_display_type = "Always"; 602 | let this_condition = true; 603 | let this_message = "Always message 2"; 604 | let this_class = "binary::always"; 605 | let this_function = "binary::always::always_function"; 606 | let this_file = "/home/user/binary/src/always_binary.rs"; 607 | let this_begin_line = 10; 608 | let this_begin_column = 5; 609 | let this_hit = true; 610 | let this_must_hit = true; 611 | let this_id = "ID Always message 2"; 612 | let this_details = json!({ 613 | "color": "always red", 614 | "extent": 15, 615 | }); 616 | 617 | let before_tracker = tracking_info_for_key(this_id); 618 | 619 | assert_impl( 620 | this_assert_type, 621 | this_display_type.to_owned(), 622 | this_condition, 623 | this_message.to_owned(), 624 | this_class.to_owned(), 625 | this_function.to_owned(), 626 | this_file.to_owned(), 627 | this_begin_line, 628 | this_begin_column, 629 | this_hit, 630 | this_must_hit, 631 | this_id.to_owned(), 632 | &this_details, 633 | ); 634 | 635 | let after_tracker = tracking_info_for_key(this_id); 636 | 637 | if this_condition { 638 | assert_eq!(before_tracker.pass_count + 1, after_tracker.pass_count); 639 | assert_eq!(before_tracker.fail_count, after_tracker.fail_count); 640 | } else { 641 | assert_eq!(before_tracker.fail_count + 1, after_tracker.fail_count); 642 | assert_eq!(before_tracker.pass_count, after_tracker.pass_count); 643 | }; 644 | } 645 | 646 | #[test] 647 | fn assert_impl_fail() { 648 | let this_assert_type = AssertType::Always; 649 | let this_display_type = "Always"; 650 | let this_condition = false; 651 | let this_message = "Always message 3"; 652 | let this_class = "binary::always"; 653 | let this_function = "binary::always::always_function"; 654 | let this_file = "/home/user/binary/src/always_binary.rs"; 655 | let this_begin_line = 10; 656 | let this_begin_column = 5; 657 | let this_hit = true; 658 | let this_must_hit = true; 659 | let this_id = "ID Always message 3"; 660 | let this_details = json!({ 661 | "color": "always red", 662 | "extent": 15, 663 | }); 664 | 665 | let before_tracker = tracking_info_for_key(this_id); 666 | 667 | assert_impl( 668 | this_assert_type, 669 | this_display_type.to_owned(), 670 | this_condition, 671 | this_message.to_owned(), 672 | this_class.to_owned(), 673 | this_function.to_owned(), 674 | this_file.to_owned(), 675 | this_begin_line, 676 | this_begin_column, 677 | this_hit, 678 | this_must_hit, 679 | this_id.to_owned(), 680 | &this_details, 681 | ); 682 | 683 | let after_tracker = tracking_info_for_key(this_id); 684 | 685 | if this_condition { 686 | assert_eq!(before_tracker.pass_count + 1, after_tracker.pass_count); 687 | assert_eq!(before_tracker.fail_count, after_tracker.fail_count); 688 | } else { 689 | assert_eq!(before_tracker.fail_count + 1, after_tracker.fail_count); 690 | assert_eq!(before_tracker.pass_count, after_tracker.pass_count); 691 | }; 692 | } 693 | 694 | fn tracking_info_for_key(key: &str) -> TrackingInfo { 695 | // Establish TrackingInfo for this trackingKey when needed 696 | let mut tracking_data = TrackingInfo::new(); 697 | 698 | let tracking_key: String = key.to_owned(); 699 | match ASSERT_TRACKER.lock().unwrap().get(&tracking_key) { 700 | None => tracking_data, 701 | Some(ti) => { 702 | tracking_data.pass_count = ti.pass_count; 703 | tracking_data.fail_count = ti.fail_count; 704 | tracking_data 705 | } 706 | } 707 | } 708 | } 709 | -------------------------------------------------------------------------------- /lib/src/internal/local_handler.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::{Error, Write}; 4 | 5 | use crate::internal::{LibHandler, LOCAL_OUTPUT}; 6 | 7 | pub struct LocalHandler { 8 | writer: File, 9 | } 10 | 11 | impl LocalHandler { 12 | pub fn new() -> Option { 13 | let filename = env::var(LOCAL_OUTPUT).ok()?; 14 | 15 | let create_result = File::create(&filename); 16 | if let Ok(writer) = create_result { 17 | Some(LocalHandler { writer }) 18 | } else { 19 | eprintln!( 20 | "Unable to write to '{}' - {}", 21 | filename.as_str(), 22 | create_result.unwrap_err() 23 | ); 24 | None 25 | } 26 | } 27 | } 28 | 29 | impl LibHandler for LocalHandler { 30 | fn output(&self, value: &str) -> Result<(), Error> { 31 | // The compact Display impl (selected using `{}`) of `serde_json::Value` contains no newlines, 32 | // hence we are outputing valid JSONL format here. 33 | // Using the `{:#}` format specifier may results in extra newlines and indentation. 34 | // See https://docs.rs/serde_json/latest/serde_json/enum.Value.html#impl-Display-for-Value. 35 | let mut writer_mut = &self.writer; 36 | writeln!(writer_mut, "{}", value)?; 37 | writer_mut.flush()?; 38 | Ok(()) 39 | } 40 | 41 | fn random(&self) -> u64 { 42 | rand::random::() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/internal/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use std::io::Error; 3 | 4 | use noop_handler::NoOpHandler; 5 | 6 | #[cfg(feature = "full")] 7 | use rustc_version_runtime::version; 8 | 9 | #[cfg(feature = "full")] 10 | use voidstar_handler::VoidstarHandler; 11 | #[cfg(feature = "full")] 12 | use local_handler::LocalHandler; 13 | 14 | #[cfg(feature = "full")] 15 | use once_cell::sync::Lazy; 16 | 17 | 18 | mod noop_handler; 19 | #[cfg(feature = "full")] 20 | mod voidstar_handler; 21 | 22 | #[cfg(feature = "full")] 23 | mod local_handler; 24 | 25 | 26 | #[derive(Serialize, Debug)] 27 | struct AntithesisLanguageInfo { 28 | name: &'static str, 29 | version: String, 30 | } 31 | 32 | #[derive(Serialize, Debug)] 33 | struct AntithesisVersionInfo { 34 | language: AntithesisLanguageInfo, 35 | sdk_version: &'static str, 36 | protocol_version: &'static str, 37 | } 38 | 39 | #[derive(Serialize, Debug)] 40 | struct AntithesisSDKInfo { 41 | antithesis_sdk: AntithesisVersionInfo, 42 | } 43 | 44 | // Hardly ever changes, refers to the underlying JSON representation 45 | #[allow(dead_code)] 46 | const PROTOCOL_VERSION: &str = "1.1.0"; 47 | 48 | // Tracks SDK releases 49 | #[allow(dead_code)] 50 | const SDK_VERSION: &str = env!("CARGO_PKG_VERSION"); 51 | 52 | pub const LOCAL_OUTPUT: &str = "ANTITHESIS_SDK_LOCAL_OUTPUT"; 53 | 54 | #[cfg(feature = "full")] 55 | fn get_handler() -> Box { 56 | match VoidstarHandler::try_load() { 57 | Ok(handler) => Box::new(handler), 58 | Err(_) => match LocalHandler::new() { 59 | Some(h) => Box::new(h), 60 | None => Box::new(NoOpHandler::new()), 61 | }, 62 | } 63 | } 64 | 65 | #[cfg(not(feature = "full"))] 66 | #[allow(dead_code)] 67 | fn get_handler() -> Box { 68 | Box::new(NoOpHandler::new()) 69 | } 70 | 71 | #[cfg(feature = "full")] 72 | pub(crate) static LIB_HANDLER: Lazy> = Lazy::new(|| { 73 | let handler = get_handler(); 74 | let s = serde_json::to_string(&sdk_info()).unwrap_or("{}".to_owned()); 75 | let _ = handler.output(s.as_str()); 76 | handler 77 | }); 78 | 79 | 80 | #[cfg(not(feature = "full"))] 81 | pub(crate) static LIB_HANDLER: NoOpHandler = NoOpHandler{}; 82 | 83 | pub(crate) trait LibHandler { 84 | fn output(&self, value: &str) -> Result<(), Error>; 85 | fn random(&self) -> u64; 86 | } 87 | 88 | // Made public so it can be invoked from the antithesis_sdk::random module 89 | pub(crate) fn dispatch_random() -> u64 { 90 | LIB_HANDLER.random() 91 | } 92 | 93 | // Ignore any and all errors - either the output is completed, 94 | // or it fails silently. 95 | // 96 | // For a Voidstar handler, there is no indication that something failed 97 | // 98 | // For a Local handler, either: 99 | // - Output was not requested (so not really an error) 100 | // - Output was requested and attempted, but an io::Error was detected 101 | // in this case the io::Error is silently ignored. 102 | // 103 | // It would be possible to distinguish between these two cases 104 | // and report detected io:Error's but there is no requirement 105 | // to implement this. 106 | // 107 | // Made public so it can be invoked from the antithesis_sdk::lifecycle 108 | // and antithesis_sdk::assert module 109 | pub fn dispatch_output(json_data: &T) { 110 | let s = serde_json::to_string(json_data).unwrap_or("{}".to_owned()); 111 | let _ = LIB_HANDLER.output(s.as_str()); 112 | } 113 | 114 | #[cfg(feature = "full")] 115 | fn sdk_info() -> AntithesisSDKInfo { 116 | let language_data = AntithesisLanguageInfo { 117 | name: "Rust", 118 | version: version().to_string(), 119 | }; 120 | 121 | let version_data = AntithesisVersionInfo { 122 | language: language_data, 123 | sdk_version: SDK_VERSION, 124 | protocol_version: PROTOCOL_VERSION, 125 | }; 126 | 127 | AntithesisSDKInfo { 128 | antithesis_sdk: version_data, 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/internal/noop_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::internal::LibHandler; 2 | use std::io::Error; 3 | 4 | pub struct NoOpHandler {} 5 | 6 | impl NoOpHandler { 7 | #[allow(dead_code)] 8 | pub fn new() -> Self { 9 | NoOpHandler {} 10 | } 11 | } 12 | 13 | impl LibHandler for NoOpHandler { 14 | fn output(&self, _value: &str) -> Result<(), Error> { 15 | Ok(()) 16 | } 17 | 18 | fn random(&self) -> u64 { 19 | rand::random::() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/internal/voidstar_handler.rs: -------------------------------------------------------------------------------- 1 | use libc::{c_char, size_t}; 2 | use libloading::Library; 3 | use std::io::Error; 4 | 5 | use crate::internal::LibHandler; 6 | 7 | const LIB_NAME: &str = "/usr/lib/libvoidstar.so"; 8 | 9 | 10 | pub struct VoidstarHandler { 11 | // Not used directly but exists to ensure the library is loaded 12 | // and all the following function pointers points to valid memory. 13 | _lib: Library, 14 | // SAFETY: The memory pointed by `s` must be valid up to `l` bytes. 15 | fuzz_json_data: unsafe fn(s: *const c_char, l: size_t), 16 | fuzz_get_random: fn() -> u64, 17 | fuzz_flush: fn(), 18 | } 19 | 20 | impl VoidstarHandler { 21 | pub fn try_load() -> Result { 22 | // SAFETY: 23 | // - The `libvoidstar`/`libmockstar `libraries that we intended to load 24 | // should not have initalization procedures that requires special arrangments at loading time. 25 | // Otherwise, loading an arbitrary library that happens to be at `LIB_NAME` is an unsupported case. 26 | // - Similarly, we load symbols by names and assume they have the expected signatures, 27 | // and loading arbitrary symbols that happen to take those names are unsupported. 28 | // - `fuzz_json_data` and `fuzz_get_random` copy the function pointers, 29 | // but they would be valid as we bind their lifetime to the library they are from 30 | // by storing all of them in the `VoidstarHandler` struct. 31 | unsafe { 32 | let lib = Library::new(LIB_NAME)?; 33 | let fuzz_json_data = *lib.get(b"fuzz_json_data\0")?; 34 | let fuzz_get_random = *lib.get(b"fuzz_get_random\0")?; 35 | let fuzz_flush = *lib.get(b"fuzz_flush\0")?; 36 | Ok(VoidstarHandler { 37 | _lib: lib, 38 | fuzz_json_data, 39 | fuzz_get_random, 40 | fuzz_flush, 41 | }) 42 | } 43 | } 44 | } 45 | 46 | impl LibHandler for VoidstarHandler { 47 | fn output(&self, value: &str) -> Result<(), Error> { 48 | // SAFETY: The data pointer and length passed into `fuzz_json_data` points to valid memory 49 | // that we just initialized above. 50 | unsafe { 51 | (self.fuzz_json_data)(value.as_bytes().as_ptr() as *const c_char, value.len()); 52 | (self.fuzz_flush)(); 53 | } 54 | Ok(()) 55 | } 56 | 57 | fn random(&self) -> u64 { 58 | (self.fuzz_get_random)() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// The assert module enables defining [test properties](https://antithesis.com/docs/using_antithesis/properties/) 2 | /// about your program or [workload](https://antithesis.com/docs/getting_started/first_test/). 3 | /// 4 | /// The constant [const@LOCAL_OUTPUT] is associated with local logging, which is one of the [local execution](https://antithesis.com/docs/using_antithesis/sdk/rust/overview/#sdk-runtime-behavior) modes. 5 | /// 6 | /// Each macro/function in this module takes a parameter called ``message``, which is 7 | /// a string literal identifier used to aggregate assertions. 8 | /// Antithesis generates one test property per unique ``message`` This test property will be named ``message`` in the [triage report](https://antithesis.com/docs/reports/triage/). 9 | /// 10 | /// Each macro/function also takes a parameter called ``details``, which is a key-value map of optional additional information provided by the user to add context for assertion failures. 11 | /// The information that is logged will appear in the ``logs`` section of a [triage report](https://antithesis.com/docs/reports/triage/). 12 | /// Normally the values in ``details`` are evaluated at runtime. 13 | pub mod assert; 14 | 15 | // External crates used in assertion macros 16 | #[doc(hidden)] 17 | #[cfg(feature = "full")] 18 | pub use linkme; 19 | #[doc(hidden)] 20 | #[cfg(feature = "full")] 21 | pub use once_cell; 22 | #[doc(hidden)] 23 | #[cfg(feature = "full")] 24 | pub use serde_json; 25 | 26 | /// The lifecycle module contains functions which inform the Antithesis 27 | /// environment that particular test phases or milestones have been reached. 28 | /// 29 | /// The constant [const@LOCAL_OUTPUT] is associated with local logging, which is one of the [local execution](https://antithesis.com/docs/using_antithesis/sdk/rust/overview/#sdk-runtime-behavior) modes. 30 | pub mod lifecycle; 31 | 32 | /// The random module provides functions that request both structured and unstructured randomness from the Antithesis environment. 33 | /// 34 | /// These functions should not be used to seed a conventional PRNG, and should not have their return values stored and used to make a decision at a later time. 35 | /// Doing either of these things makes it much harder for the Antithesis platform to control the history of your program's execution, and also makes it harder for Antithesis to learn which inputs provided at which times are most fruitful. 36 | /// Instead, you should call a function from the random package every time your program or [workload](https://antithesis.com/docs/getting_started/first_test/) needs to make a decision, at the moment that you need to make the decision. 37 | /// 38 | /// These functions are also safe to call outside the Antithesis environment, where 39 | /// they will fall back on the rust std library implementation. 40 | /// 41 | pub mod random; 42 | 43 | mod internal; 44 | 45 | /// Convenience to import all macros and functions 46 | pub mod prelude; 47 | 48 | /// Global initialization logic. Performs registration of the 49 | /// Antithesis assertion catalog. This should be invoked as early as 50 | /// possible during program execution. It is recommended to call it immediately in ``main``. 51 | /// 52 | /// If called more than once, only the first call will result 53 | /// in the assertion catalog being registered. If never called, 54 | /// the assertion catalog will be registered when it encounters the first assertion at runtime. 55 | /// 56 | /// Example: 57 | /// 58 | /// ``` 59 | /// use std::env; 60 | /// use serde_json::{json}; 61 | /// use antithesis_sdk::{antithesis_init, assert_unreachable}; 62 | /// 63 | /// fn main() { 64 | /// if (env::args_os().len() == 1888999778899) { 65 | /// assert_unreachable!("Unable to provide trillions of arguments", &json!({})); 66 | /// } 67 | /// 68 | /// // if antithesis_init() is omitted, the above unreachable will 69 | /// // not be reported 70 | /// antithesis_init(); 71 | /// } 72 | /// ``` 73 | #[allow(clippy::needless_doctest_main)] 74 | pub fn antithesis_init() { 75 | init(); 76 | } 77 | 78 | #[cfg(feature = "full")] 79 | fn init() { 80 | Lazy::force(&internal::LIB_HANDLER); 81 | Lazy::force(&assert::INIT_CATALOG); 82 | } 83 | 84 | #[cfg(not(feature = "full"))] 85 | fn init() {} 86 | 87 | #[cfg(feature = "full")] 88 | use once_cell::sync::Lazy; 89 | 90 | /// A constant provided by the SDK to report the location of logged output when run locally. 91 | /// This constant is the name of an environment variable ``ANTITHESIS_SDK_LOCAL_OUTPUT``. 92 | /// ``ANTITHESIS_SDK_LOCAL_OUTPUT`` is a path to a file 93 | /// that can be created and written to when running locally. If this environment variable is not present at 94 | /// runtime, then no assertion and lifecycle output will be attempted. 95 | /// 96 | /// This allows you to make use of the Antithesis assertions module 97 | /// in your regular testing, or even in production. In particular, 98 | /// very few assertions frameworks offer a convenient way to define 99 | /// [Sometimes assertions](https://antithesis.com/docs/best_practices/sometimes_assertions/), but they can be quite useful even outside 100 | /// Antithesis. 101 | /// 102 | /// See also the documentation for [local execution](https://antithesis.com/docs/using_antithesis/sdk/rust/overview/#sdk-runtime-behavior). 103 | pub use crate::internal::LOCAL_OUTPUT; 104 | -------------------------------------------------------------------------------- /lib/src/lifecycle.rs: -------------------------------------------------------------------------------- 1 | use crate::internal; 2 | use serde::Serialize; 3 | use serde_json::{json, Value}; 4 | 5 | #[derive(Serialize, Debug)] 6 | struct AntithesisSetupData<'a, 'b> { 7 | status: &'a str, 8 | details: &'b Value, 9 | } 10 | 11 | #[derive(Serialize, Debug)] 12 | struct SetupCompleteData<'a> { 13 | antithesis_setup: AntithesisSetupData<'a, 'a>, 14 | } 15 | 16 | /// Indicates to Antithesis that setup has completed. Call this function when your system and workload are fully initialized. 17 | /// After this function is called, Antithesis will take a snapshot of your system and begin [injecting faults]( https://antithesis.com/docs/applications/reliability/fault_injection/). 18 | /// 19 | /// Calling this function multiple times or from multiple processes will have no effect. 20 | /// Antithesis will treat the first time any process called this function as the moment that the setup was completed. 21 | /// 22 | /// # Example 23 | /// 24 | /// ``` 25 | /// use serde_json::{json, Value}; 26 | /// use antithesis_sdk::lifecycle; 27 | /// 28 | /// let (num_nodes, main_id) = (10, "n-001"); 29 | /// 30 | /// let startup_data: Value = json!({ 31 | /// "num_nodes": num_nodes, 32 | /// "main_node_id": main_id, 33 | /// }); 34 | /// 35 | /// lifecycle::setup_complete(&startup_data); 36 | /// ``` 37 | pub fn setup_complete(details: &Value) { 38 | let status = "complete"; 39 | let antithesis_setup = AntithesisSetupData::<'_, '_> { status, details }; 40 | 41 | let setup_complete_data = SetupCompleteData { antithesis_setup }; 42 | 43 | internal::dispatch_output(&setup_complete_data) 44 | } 45 | 46 | /// Indicates to Antithesis that a certain event has been reached. It sends a structured log message to Antithesis that you may later use to aid debugging. 47 | /// 48 | /// In addition to ``details``, you also provide ``name``, which is the name of the event that you are logging. 49 | /// 50 | /// # Example 51 | /// 52 | /// ``` 53 | /// use serde_json::{json, Value}; 54 | /// use antithesis_sdk::lifecycle; 55 | /// 56 | /// let info_value: Value = json!({ 57 | /// "month": "July", 58 | /// "day": 17 59 | /// }); 60 | /// 61 | /// lifecycle::send_event("start_day", &info_value); 62 | /// ``` 63 | pub fn send_event(name: &str, details: &Value) { 64 | let trimmed_name = name.trim(); 65 | let owned_name: String = if trimmed_name.is_empty() { 66 | "anonymous".to_owned() 67 | } else { 68 | trimmed_name.to_owned() 69 | }; 70 | let json_event = json!({ owned_name: details }); 71 | internal::dispatch_output(&json_event) 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn setup_complete_without_details() { 80 | eprintln!("setup_complete"); 81 | let details: Value = json!({}); 82 | setup_complete(&details); 83 | } 84 | 85 | #[test] 86 | fn setup_complete_with_details() { 87 | let details: Value = json!({ 88 | "name": "Meow Cat", 89 | "age": 11, 90 | "phones": [ 91 | "+1 2126581356", 92 | "+1 2126581384" 93 | ] 94 | }); 95 | setup_complete(&details); 96 | } 97 | 98 | #[test] 99 | fn send_event_without_details() { 100 | let details: Value = json!({}); 101 | send_event("my event", &details); 102 | } 103 | 104 | #[test] 105 | fn send_event_with_details() { 106 | let details: Value = json!({ 107 | "name": "Tweety Bird", 108 | "age": 4, 109 | "phones": [ 110 | "+1 9734970340" 111 | ] 112 | }); 113 | send_event("my event 2", &details); 114 | } 115 | 116 | #[test] 117 | fn send_event_unnamed_without_details() { 118 | let details: Value = json!({}); 119 | send_event("", &details); 120 | } 121 | 122 | #[test] 123 | fn send_event_unnamed_with_details() { 124 | let details: Value = json!({ 125 | "color": "red" 126 | }); 127 | send_event(" ", &details); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::assert::{assert_raw, AssertType}; 2 | pub use crate::assert_always; 3 | pub use crate::assert_always_or_unreachable; 4 | pub use crate::assert_reachable; 5 | pub use crate::assert_sometimes; 6 | pub use crate::assert_unreachable; 7 | pub use crate::assert_always_greater_than; 8 | pub use crate::assert_always_greater_than_or_equal_to; 9 | pub use crate::assert_always_less_than; 10 | pub use crate::assert_always_less_than_or_equal_to; 11 | pub use crate::assert_sometimes_greater_than; 12 | pub use crate::assert_sometimes_greater_than_or_equal_to; 13 | pub use crate::assert_sometimes_less_than; 14 | pub use crate::assert_sometimes_less_than_or_equal_to; 15 | pub use crate::assert_always_some; 16 | pub use crate::assert_sometimes_all; 17 | pub use crate::{antithesis_init, lifecycle, random}; 18 | -------------------------------------------------------------------------------- /lib/src/random.rs: -------------------------------------------------------------------------------- 1 | use rand::{Error, RngCore}; 2 | use crate::internal; 3 | 4 | /// Returns a u64 value chosen by Antithesis. You should not 5 | /// store this value or use it to seed a PRNG, but should use it 6 | /// immediately. 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// use antithesis_sdk::random; 12 | /// 13 | /// let value = random::get_random(); 14 | /// println!("Random value(u64): {value}"); 15 | /// ``` 16 | pub fn get_random() -> u64 { 17 | internal::dispatch_random() 18 | } 19 | 20 | /// Returns a randomly chosen item from a list of options. You 21 | /// should not store this value, but should use it immediately. 22 | /// 23 | /// This function is not purely for convenience. Signaling to 24 | /// the Antithesis platform that you intend to use a random value 25 | /// in a structured way enables it to provide more interesting 26 | /// choices over time. 27 | /// 28 | /// # Example 29 | /// 30 | /// ``` 31 | /// use antithesis_sdk::random; 32 | /// 33 | /// let choices: Vec<&str> = vec!["abc", "def", "xyz", "qrs"]; 34 | /// if let Some(s) = random::random_choice(choices.as_slice()) { 35 | /// println!("Choice: '{s}'"); 36 | /// }; 37 | /// ``` 38 | pub fn random_choice(slice: &[T]) -> Option<&T> { 39 | match slice { 40 | [] => None, 41 | [x] => Some(x), 42 | _ => { 43 | let idx: usize = (get_random() as usize) % slice.len(); 44 | Some(&slice[idx]) 45 | } 46 | } 47 | } 48 | 49 | /// A random number generator that uses Antithesis's random number generation. 50 | /// 51 | /// This implements the `RngCore` trait from the `rand` crate, allowing it to be used 52 | /// with any code that expects a random number generator from that ecosystem. 53 | /// 54 | /// # Example 55 | /// 56 | /// ``` 57 | /// use antithesis_sdk::random::AntithesisRng; 58 | /// use rand::{Rng, RngCore}; 59 | /// 60 | /// let mut rng = AntithesisRng; 61 | /// let random_u32: u32 = rng.gen(); 62 | /// let random_u64: u64 = rng.gen(); 63 | /// let random_char: char = rng.gen(); 64 | /// 65 | /// let mut bytes = [0u8; 16]; 66 | /// rng.fill_bytes(&mut bytes); 67 | /// ``` 68 | pub struct AntithesisRng; 69 | 70 | impl RngCore for AntithesisRng { 71 | fn next_u32(&mut self) -> u32 { 72 | get_random() as u32 73 | } 74 | 75 | fn next_u64(&mut self) -> u64 { 76 | get_random() 77 | } 78 | 79 | fn fill_bytes(&mut self, dest: &mut [u8]) { 80 | // Split the destination buffer into chunks of 8 bytes each 81 | // (since we'll fill each chunk with a u64/8 bytes of random data) 82 | let mut chunks = dest.chunks_exact_mut(8); 83 | 84 | // Fill each complete 8-byte chunk with random bytes 85 | for chunk in chunks.by_ref() { 86 | // Generate 8 random bytes from a u64 in native endian order 87 | let random_bytes = self.next_u64().to_ne_bytes(); 88 | // Copy those random bytes into this chunk 89 | chunk.copy_from_slice(&random_bytes); 90 | } 91 | 92 | // Get any remaining bytes that didn't fit in a complete 8-byte chunk 93 | let remainder = chunks.into_remainder(); 94 | 95 | if !remainder.is_empty() { 96 | // Generate 8 more random bytes 97 | let random_bytes = self.next_u64().to_ne_bytes(); 98 | // Copy just enough random bytes to fill the remainder 99 | remainder.copy_from_slice(&random_bytes[..remainder.len()]); 100 | } 101 | } 102 | 103 | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { 104 | self.fill_bytes(dest); 105 | Ok(()) 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | use std::collections::{HashMap, HashSet}; 113 | use rand::Rng; 114 | use rand::seq::SliceRandom; 115 | 116 | #[test] 117 | fn random_choice_no_choices() { 118 | let array = [""; 0]; 119 | assert_eq!(0, array.len()); 120 | assert_eq!(None, random_choice(&array)) 121 | } 122 | 123 | #[test] 124 | fn random_choice_one_choice() { 125 | let array = ["ABc"; 1]; 126 | assert_eq!(1, array.len()); 127 | assert_eq!(Some(&"ABc"), random_choice(&array)) 128 | } 129 | 130 | #[test] 131 | fn random_choice_few_choices() { 132 | // For each map key, the value is the count of the number of 133 | // random_choice responses received matching that key 134 | let mut counted_items: HashMap<&str, i64> = HashMap::new(); 135 | counted_items.insert("a", 0); 136 | counted_items.insert("b", 0); 137 | counted_items.insert("c", 0); 138 | 139 | let all_keys: Vec<&str> = counted_items.keys().cloned().collect(); 140 | assert_eq!(counted_items.len(), all_keys.len()); 141 | for _i in 0..15 { 142 | let rc = random_choice(all_keys.as_slice()); 143 | if let Some(choice) = rc { 144 | if let Some(x) = counted_items.get_mut(choice) { 145 | *x += 1; 146 | } 147 | } 148 | } 149 | for (key, val) in counted_items.iter() { 150 | assert_ne!(*val, 0, "Did not produce the choice: {}", key); 151 | } 152 | } 153 | 154 | #[test] 155 | fn get_random_100k() { 156 | let mut random_numbers: HashSet = HashSet::new(); 157 | for _i in 0..100000 { 158 | let rn = get_random(); 159 | assert!(!random_numbers.contains(&rn)); 160 | random_numbers.insert(rn); 161 | } 162 | } 163 | 164 | #[test] 165 | fn rng_no_choices() { 166 | let mut rng = AntithesisRng; 167 | let array = [""; 0]; 168 | assert_eq!(0, array.len()); 169 | assert_eq!(None, array.choose(&mut rng)); 170 | } 171 | 172 | #[test] 173 | fn rng_one_choice() { 174 | let mut rng = AntithesisRng; 175 | let array = ["ABc"; 1]; 176 | assert_eq!(1, array.len()); 177 | assert_eq!(Some(&"ABc"), array.choose(&mut rng)); 178 | } 179 | 180 | #[test] 181 | fn rng_few_choices() { 182 | let mut rng = AntithesisRng; 183 | // For each map key, the value is the count of the number of 184 | // random_choice responses received matching that key 185 | let mut counted_items: HashMap<&str, i64> = HashMap::new(); 186 | counted_items.insert("a", 0); 187 | counted_items.insert("b", 0); 188 | counted_items.insert("c", 0); 189 | 190 | let all_keys: Vec<&str> = counted_items.keys().cloned().collect(); 191 | assert_eq!(counted_items.len(), all_keys.len()); 192 | for _i in 0..15 { 193 | let rc = all_keys.choose(&mut rng); 194 | if let Some(choice) = rc { 195 | if let Some(x) = counted_items.get_mut(choice) { 196 | *x += 1; 197 | } 198 | } 199 | } 200 | for (key, val) in counted_items.iter() { 201 | assert_ne!(*val, 0, "Did not produce the choice: {}", key); 202 | } 203 | } 204 | 205 | #[test] 206 | fn rng_100k() { 207 | let mut rng = AntithesisRng; 208 | let mut random_numbers: HashSet = HashSet::new(); 209 | for _i in 0..100000 { 210 | let rn: u64 = rng.gen(); 211 | assert!(!random_numbers.contains(&rn)); 212 | random_numbers.insert(rn); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/tests/assert_always_with_details.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{antithesis_init, assert_always, LOCAL_OUTPUT}; 2 | use serde_json::{json, Value}; 3 | 4 | mod common; 5 | use common::{AntithesisAssert, AssertType, SDKInput}; 6 | 7 | // Expected Output in /tmp/antithesis-assert-always-with-details.json 8 | // Note: Actual version info in antithesis_sdk can vary 9 | // 10 | // { 11 | // "antithesis_sdk": { 12 | // "language": { 13 | // "name": "Rust", 14 | // "version": "1.69.0" 15 | // }, 16 | // "sdk_version": "0.1.2", 17 | // "protocol_version": "1.0.0" 18 | // } 19 | // } 20 | // { 21 | // "antithesis_assert": { 22 | // "assert_type": "always", 23 | // "condition": false, 24 | // "details": {}, 25 | // "display_type": "Always", 26 | // "hit": false, 27 | // "id": "Waterproof Red", 28 | // "location": { 29 | // "begin_column": 5, 30 | // "begin_line": 23, 31 | // "class": "assert_always_with_details", 32 | // "file": "lib/tests/assert_always_with_details.rs", 33 | // "function": "assert_always_with_details::assert_always_with_details" 34 | // }, 35 | // "message": "Waterproof Red", 36 | // "must_hit": true 37 | // } 38 | // } 39 | // { 40 | // "antithesis_assert": { 41 | // "assert_type": "always", 42 | // "condition": true, 43 | // "details": { 44 | // "color": "red", 45 | // "labels": [ 46 | // "outdoor", 47 | // "waterproof" 48 | // ], 49 | // "width": 4 50 | // }, 51 | // "display_type": "Always", 52 | // "hit": true, 53 | // "id": "Waterproof Red", 54 | // "location": { 55 | // "begin_column": 5, 56 | // "begin_line": 23, 57 | // "class": "assert_always_with_details", 58 | // "file": "lib/tests/assert_always_with_details.rs", 59 | // "function": "assert_always_with_details::assert_always_with_details" 60 | // }, 61 | // "message": "Waterproof Red", 62 | // "must_hit": true 63 | // } 64 | // } 65 | 66 | #[test] 67 | fn assert_always_with_details() { 68 | let output_file = "/tmp/antithesis-assert-always-with-details.json"; 69 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 70 | antithesis_init(); 71 | let clothing_details: Value = json!({ 72 | "color": "red", 73 | "width": 4, 74 | "labels": [ 75 | "outdoor", 76 | "waterproof" 77 | ] 78 | }); 79 | let is_waterproof = true; 80 | assert_always!(is_waterproof, "Waterproof Red", &clothing_details); 81 | 82 | // verify the output has landed in the expected file 83 | match common::read_jsonl_tags(output_file) { 84 | Ok(x) => { 85 | let mut did_register = false; 86 | let mut did_hit = false; 87 | for obj in x.iter() { 88 | if let SDKInput::AntithesisAssert(AntithesisAssert { 89 | assert_type, 90 | condition, 91 | display_type, 92 | hit, 93 | must_hit, 94 | id, 95 | message, 96 | location, 97 | details, 98 | }) = obj 99 | { 100 | if *hit { 101 | did_hit = true; 102 | assert_eq!(*condition, is_waterproof); 103 | assert_eq!(details, &clothing_details); 104 | } else { 105 | did_register = true; 106 | }; 107 | assert_eq!(*assert_type, AssertType::Always); 108 | assert_eq!(*display_type, "Always"); 109 | assert!(*must_hit); 110 | assert_eq!(message, "Waterproof Red"); 111 | assert_eq!(id, message); 112 | assert!(location.begin_line > 0); 113 | assert!(location.begin_column >= 0); 114 | assert_eq!(location.class, "assert_always_with_details"); 115 | assert!(location.function.ends_with("::assert_always_with_details")); 116 | assert!(location 117 | .file 118 | .ends_with("/tests/assert_always_with_details.rs")); 119 | } 120 | println!("{:?}", obj); 121 | } 122 | assert!(did_register); 123 | assert!(did_hit); 124 | } 125 | Err(e) => println!("{}", e), 126 | } 127 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 128 | } 129 | -------------------------------------------------------------------------------- /lib/tests/assert_guidance.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{antithesis_init, assert_always_greater_than, LOCAL_OUTPUT}; 2 | use serde_json::json; 3 | 4 | mod common; 5 | use common::SDKInput; 6 | 7 | use crate::common::{AntithesisGuidance, GuidanceType}; 8 | 9 | #[test] 10 | fn assert_guidance() { 11 | let output_file = "/tmp/antithesis-assert-guidance.json"; 12 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 13 | antithesis_init(); 14 | 15 | for i in 0..10 { 16 | let x = if i % 2 == 0 { i } else { -i }; 17 | assert_always_greater_than!(x, 0, "Positive x", &json!({"x": x})); 18 | } 19 | 20 | match common::read_jsonl_tags(output_file) { 21 | Ok(x) => { 22 | let mut did_register = false; 23 | let mut did_hit = false; 24 | for obj in x.iter() { 25 | if let SDKInput::AntithesisGuidance(AntithesisGuidance { 26 | guidance_type, 27 | hit, 28 | id, 29 | message, 30 | location, 31 | .. 32 | }) = obj 33 | { 34 | if *hit { 35 | did_hit = true; 36 | } else { 37 | did_register = true; 38 | }; 39 | assert_eq!(*guidance_type, GuidanceType::Numeric); 40 | assert_eq!(message, "Positive x"); 41 | assert_eq!(id, message); 42 | assert!(location.begin_line > 0); 43 | assert!(location.begin_column >= 0); 44 | assert_eq!(location.class, "assert_guidance"); 45 | assert!(location.function.ends_with("::assert_guidance")); 46 | assert!(location 47 | .file 48 | .ends_with("/tests/assert_guidance.rs")); 49 | } 50 | println!("{:?}", obj); 51 | } 52 | assert!(did_register); 53 | assert!(did_hit); 54 | } 55 | Err(e) => println!("{}", e), 56 | } 57 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 58 | } 59 | -------------------------------------------------------------------------------- /lib/tests/common/env.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | pub fn set_var(k: &str, v: &str) -> Option { 4 | let prev_v = env::var(k); 5 | env::set_var(k, v); 6 | match prev_v { 7 | Ok(s) => Some(s), 8 | Err(_) => None, 9 | } 10 | } 11 | 12 | pub fn restore_var(k: &str, maybe_v: Option) { 13 | match maybe_v { 14 | None => env::remove_var(k), 15 | Some(prev_v) => env::set_var(k, prev_v.as_str()), 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /lib/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | pub mod env; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::{json, Value}; 6 | use std::fs; 7 | 8 | #[derive(Deserialize, Debug)] 9 | pub struct RustLanguage { 10 | pub name: String, 11 | pub version: String, 12 | } 13 | 14 | #[derive(Deserialize, Debug)] 15 | pub struct AntithesisSdk { 16 | pub language: RustLanguage, 17 | pub protocol_version: String, 18 | #[allow(dead_code)] 19 | pub sdk_version: String, 20 | } 21 | 22 | #[derive(Deserialize, Debug)] 23 | pub struct AntithesisSetup { 24 | pub status: String, 25 | pub details: Value, 26 | } 27 | 28 | #[derive(Deserialize, Serialize, Debug)] 29 | pub struct Location { 30 | pub begin_column: i32, 31 | pub begin_line: i32, 32 | pub class: String, 33 | pub file: String, 34 | pub function: String, 35 | } 36 | 37 | #[derive(Deserialize, Debug)] 38 | pub struct AntithesisAssert { 39 | pub assert_type: AssertType, 40 | pub condition: bool, 41 | pub display_type: String, 42 | pub hit: bool, 43 | pub must_hit: bool, 44 | pub id: String, 45 | pub message: String, 46 | pub location: Location, 47 | pub details: Value, 48 | } 49 | 50 | #[derive(Deserialize, Debug)] 51 | pub struct AntithesisGuidance { 52 | pub guidance_type: GuidanceType, 53 | pub message: String, 54 | pub id: String, 55 | pub location: Location, 56 | pub maximize: bool, 57 | pub guidance_data: Value, 58 | pub hit: bool, 59 | } 60 | 61 | #[derive(Deserialize, Debug)] 62 | #[serde(rename_all = "snake_case")] 63 | pub enum SDKInput { 64 | AntithesisSdk(AntithesisSdk), 65 | AntithesisAssert(AntithesisAssert), 66 | AntithesisGuidance(AntithesisGuidance), 67 | AntithesisSetup(AntithesisSetup), 68 | SendEvent { event_name: String, details: Value }, 69 | } 70 | 71 | #[derive(Deserialize, Debug, PartialEq)] 72 | #[serde(rename_all = "snake_case")] 73 | pub enum AssertType { 74 | Always, 75 | Sometimes, 76 | Reachability, 77 | } 78 | 79 | #[derive(Deserialize, Debug, PartialEq)] 80 | #[serde(rename_all = "snake_case")] 81 | pub enum GuidanceType { 82 | Numeric, 83 | Boolean, 84 | Json, 85 | } 86 | 87 | fn parse_lines(lines: Vec<&str>) -> Result, Box> { 88 | let mut result = Vec::new(); 89 | 90 | let default_event = "abc".to_owned(); 91 | for line in lines { 92 | if line.is_empty() { 93 | continue; 94 | } 95 | let parsed: SDKInput = match serde_json::from_str(line) { 96 | Ok(x) => x, 97 | Err(_e) => { 98 | // println!("{}", line); 99 | // println!("PARSING: {:?}", e); 100 | let temp: Value = serde_json::from_str(line)?; 101 | // should be Object(Map) 102 | // in this case the Map has just one entry (top-level name used by SendEvent()) 103 | match temp { 104 | Value::Object(user_data) => { 105 | // let mut result = None; 106 | let mut res = Some(SDKInput::SendEvent { 107 | event_name: default_event.clone(), 108 | details: json!({}), 109 | }); 110 | if let Some((event_name, details)) = user_data.into_iter().next() { 111 | res = Some(SDKInput::SendEvent { 112 | event_name, 113 | details, 114 | }); 115 | } 116 | match res { 117 | Some(x) => x, 118 | None => SDKInput::SendEvent { 119 | event_name: default_event.clone(), 120 | details: json!({}), 121 | }, 122 | } 123 | } 124 | _ => SDKInput::SendEvent { 125 | event_name: default_event.clone(), 126 | details: json!({}), 127 | }, 128 | } 129 | } 130 | }; 131 | result.push(parsed); 132 | } 133 | Ok(result) 134 | } 135 | 136 | pub fn read_jsonl_tags(jsonl_file: &str) -> Result, Box> { 137 | let contents = fs::read_to_string(jsonl_file)?; 138 | // .expect("Should have been able to read the file"); 139 | 140 | let lines = contents.split('\n'); 141 | let parsed = parse_lines(lines.collect())?; 142 | Ok(parsed) 143 | } 144 | -------------------------------------------------------------------------------- /lib/tests/sdk_info.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{lifecycle, LOCAL_OUTPUT}; 2 | use serde_json::json; 3 | 4 | mod common; 5 | use common::SDKInput; 6 | 7 | // Expected output in /tmp/antithesis-sdk-info.json 8 | // Note: Actual version info in antithesis_sdk can vary 9 | // 10 | // { 11 | // "antithesis_sdk": { 12 | // "language": { 13 | // "name": "Rust", 14 | // "version": "1.69.0" 15 | // }, 16 | // "sdk_version": "0.1.2", 17 | // "protocol_version": "1.0.0" 18 | // } 19 | // } 20 | // { 21 | // "antithesis_setup": { 22 | // "status": "complete", 23 | // "details": {} 24 | // } 25 | // } 26 | 27 | #[test] 28 | fn sdk_info() { 29 | let output_file = "/tmp/antithesis-sdk-info.json"; 30 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 31 | let no_details = json!({}); 32 | 33 | // only added to force the antithesis_sdk info to be generated 34 | lifecycle::setup_complete(&no_details); 35 | 36 | // verify the output has landed in the expected file 37 | match common::read_jsonl_tags(output_file) { 38 | Ok(x) => { 39 | for obj in x.iter() { 40 | if let SDKInput::AntithesisSdk(sdk) = obj { 41 | assert_eq!(sdk.protocol_version, "1.1.0"); 42 | assert_eq!(sdk.language.name, "Rust") 43 | } 44 | } 45 | } 46 | Err(e) => println!("{}", e), 47 | } 48 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 49 | } 50 | -------------------------------------------------------------------------------- /lib/tests/send_event.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{lifecycle, LOCAL_OUTPUT}; 2 | use serde_json::json; 3 | 4 | mod common; 5 | use common::SDKInput; 6 | 7 | // Expected output in /tmp/antithesis-send-event.json 8 | // Note: Actual version info in antithesis_sdk can vary 9 | // 10 | // { 11 | // "antithesis_sdk": { 12 | // "language": { 13 | // "name": "Rust", 14 | // "version": "1.69.0" 15 | // }, 16 | // "sdk_version": "0.1.2", 17 | // "protocol_version": "1.0.0" 18 | // } 19 | // } 20 | // { 21 | // "logging": { 22 | // "tag": "last value", 23 | // "x": 100 24 | // } 25 | // } 26 | 27 | #[test] 28 | fn send_event() { 29 | let output_file = "/tmp/antithesis-send-event.json"; 30 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 31 | let details = json!({ 32 | "x": 100, 33 | "tag": "last value" 34 | }); 35 | 36 | // only added to force the antithesis_sdk info to be generated 37 | lifecycle::send_event("logging", &details); 38 | 39 | // verify the output has landed in the expected file 40 | match common::read_jsonl_tags(output_file) { 41 | Ok(x) => { 42 | for obj in x.iter() { 43 | if let SDKInput::SendEvent { 44 | event_name, 45 | details, 46 | } = obj 47 | { 48 | assert_eq!(event_name, "logging"); 49 | assert_eq!(&details["x"], 100); 50 | assert_eq!(&details["tag"], "last value"); 51 | } 52 | } 53 | } 54 | Err(e) => println!("{}", e), 55 | } 56 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 57 | } 58 | -------------------------------------------------------------------------------- /lib/tests/setup_complete_with_details.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{lifecycle, LOCAL_OUTPUT}; 2 | use serde_json::{json, Value}; 3 | 4 | mod common; 5 | use common::{AntithesisSetup, SDKInput}; 6 | 7 | // Expected output in /tmp/antithesis-lifecycle-with-details.json 8 | // Note: Actual version info in antithesis_sdk can vary 9 | // 10 | // { 11 | // "antithesis_sdk": { 12 | // "language": { 13 | // "name": "Rust", 14 | // "version": "1.69.0" 15 | // }, 16 | // "sdk_version": "0.1.2", 17 | // "protocol_version": "1.0.0" 18 | // } 19 | // } 20 | // { 21 | // "antithesis_setup": { 22 | // "status": "complete", 23 | // "details": { 24 | // "age": 4, 25 | // "name": "Tweety Bird", 26 | // "phones": [ 27 | // "+1 9374970340" 28 | // ] 29 | // } 30 | // } 31 | // } 32 | 33 | #[test] 34 | fn setup_complete_with_details() { 35 | let output_file = "/tmp/antithesis-lifecycle-with-details.json"; 36 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 37 | let bird_value: Value = json!({ 38 | "name": "Tweety Bird", 39 | "age": 4, 40 | "phones": [ 41 | "+1 9374970340" 42 | ] 43 | }); 44 | lifecycle::setup_complete(&bird_value); 45 | 46 | // verify the output has landed in the expected file 47 | match common::read_jsonl_tags(output_file) { 48 | Ok(x) => { 49 | for obj in x.iter() { 50 | if let SDKInput::AntithesisSetup(AntithesisSetup { status, details }) = obj { 51 | assert_eq!(status, "complete"); 52 | assert_eq!(details, &bird_value) 53 | } 54 | println!("{:?}", obj) 55 | } 56 | } 57 | Err(e) => println!("{}", e), 58 | } 59 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 60 | } 61 | -------------------------------------------------------------------------------- /lib/tests/setup_complete_without_details.rs: -------------------------------------------------------------------------------- 1 | use antithesis_sdk::{lifecycle, LOCAL_OUTPUT}; 2 | use serde_json::json; 3 | 4 | mod common; 5 | use common::{AntithesisSetup, SDKInput}; 6 | 7 | // Expected output in /tmp/antithesis-setup-complete-without-details.json 8 | // Note: Actual version info in antithesis_sdk can vary 9 | // 10 | // { 11 | // "antithesis_sdk": { 12 | // "language": { 13 | // "name": "Rust", 14 | // "version": "1.69.0" 15 | // }, 16 | // "sdk_version": "0.1.2", 17 | // "protocol_version": "1.0.0" 18 | // } 19 | // } 20 | // { 21 | // "antithesis_setup": { 22 | // "status": "complete", 23 | // "details": {} 24 | // } 25 | // } 26 | 27 | #[test] 28 | fn setup_complete_without_details() { 29 | let output_file = "/tmp/antithesis-setup-complete-without-details.json"; 30 | let prev_v = common::env::set_var(LOCAL_OUTPUT, output_file); 31 | let no_details = json!({}); 32 | 33 | lifecycle::setup_complete(&no_details); 34 | 35 | // verify the output has landed in the expected file 36 | match common::read_jsonl_tags(output_file) { 37 | Ok(x) => { 38 | for obj in x.iter() { 39 | if let SDKInput::AntithesisSetup(AntithesisSetup { status, details }) = obj { 40 | assert_eq!(status, "complete"); 41 | assert_eq!(details, &no_details) 42 | } 43 | } 44 | } 45 | Err(e) => println!("{}", e), 46 | } 47 | common::env::restore_var(LOCAL_OUTPUT, prev_v); 48 | } 49 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 | fetchTarball { 5 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 | sha256 = lock.nodes.flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; } 10 | ).shellNix 11 | -------------------------------------------------------------------------------- /simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | antithesis_sdk = { path = "../lib", default-features = false } 8 | serde_json = "1.0.25" 9 | 10 | 11 | [features] 12 | default = ["antithesis_sdk/full"] 13 | -------------------------------------------------------------------------------- /simple/src/main.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Value}; 2 | 3 | use antithesis_sdk::prelude::*; 4 | 5 | #[allow(dead_code)] 6 | fn random_demo() { 7 | // random::get_random() 8 | println!("fuzz_get_random() => {}", random::get_random()); 9 | 10 | // random::random_choices() 11 | let choices: Vec<&str> = vec!["abc", "def", "xyz", "qrs"]; 12 | let nchoices = 10; 13 | print!("{nchoices} Choices: "); 14 | for n in 0..nchoices { 15 | let z = random::random_choice(choices.as_slice()); 16 | if n > 0 { 17 | print!(" ,"); 18 | } 19 | match z { 20 | Some(s) => print!("'{s}'"), 21 | None => print!("()"), 22 | }; 23 | } 24 | println!(); 25 | } 26 | 27 | #[allow(dead_code)] 28 | fn lifecycle_demo() { 29 | // lifecycle::setup_complete 30 | let bird_value: Value = json!({ 31 | "name": "Tweety Bird", 32 | "age": 4, 33 | "phones": [ 34 | "+1 9734970340" 35 | ] 36 | }); 37 | let cat_value: Value = json!({ 38 | "name": "Meow Cat", 39 | "age": 11, 40 | "phones": [ 41 | "+1 2126581356", 42 | "+1 2126581384" 43 | ] 44 | }); 45 | 46 | let tiger: Value = json!(2457); 47 | lifecycle::setup_complete(&tiger); 48 | lifecycle::setup_complete(&bird_value); 49 | lifecycle::setup_complete(&cat_value); 50 | 51 | // lifecycle::send_event 52 | let info_value: Value = json!({ 53 | "month": "January", 54 | "day": 32 55 | }); 56 | lifecycle::send_event("user_info", &info_value); 57 | } 58 | 59 | fn assert_demo() { 60 | // always 61 | let details = json!({"things": 13}); 62 | assert_always!(true, "Things 777 look good", &details); 63 | 64 | // alwaysOrUnreachable 65 | let details = json!({"more things": "red and blue"}); 66 | assert_always_or_unreachable!(true, "A few colors", &details); 67 | 68 | // sometimes 69 | let details = json!({"notes": [1,2,3,4,5]}); 70 | assert_sometimes!(false, "Notes have small values", &details); 71 | 72 | // reachable 73 | for i in 0..4 { 74 | let details = 75 | json!({"got here": {"name": "somewhere", "scores": [i*10,(i+1)*10,(i+2)*10]}}); 76 | assert_reachable!("Someplace we need to be", &details); 77 | } 78 | 79 | // ant_unreachable 80 | let details = json!({"impossible!": {"name": "trouble", "weights": [100,200,300]}}); 81 | assert_unreachable!("Impossible to get here", &details); 82 | 83 | assert_always_greater_than!(3, 100, "not right"); 84 | 85 | assert_sometimes_all!({a: true, b: false}, "not all right"); 86 | } 87 | 88 | pub fn main() { 89 | antithesis_init(); 90 | antithesis_init(); 91 | antithesis_init(); 92 | 93 | random_demo(); 94 | 95 | lifecycle_demo(); 96 | 97 | assert_demo(); 98 | } 99 | --------------------------------------------------------------------------------