├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── basic.rs └── lyrics.txt ├── rustfmt.toml ├── screenshots └── expectorate.png ├── src ├── feature_predicates.rs └── lib.rs └── tests ├── data_a.txt ├── data_a2.txt ├── data_b.txt └── test_basic.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Dependabot configuration file 3 | # 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for GitHub-based CI, based on the stock GitHub Rust config. 3 | # 4 | name: Rust 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | check-style: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v3.5.0 17 | - name: Check style 18 | run: cargo fmt -- --check 19 | 20 | build-and-test: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [ubuntu-22.04, windows-2022, macos-latest] 25 | # 1.75 is the MSRV 26 | toolchain: ["1.75", stable] 27 | features: [all, default] 28 | include: 29 | - features: all 30 | feature_flags: --all-features 31 | steps: 32 | - uses: actions/checkout@v3.5.0 33 | - uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: ${{ matrix.toolchain }} 36 | - name: Build 37 | run: cargo build --tests --verbose ${{ matrix.feature_flags }} 38 | - name: Run tests 39 | run: cargo test --verbose ${{ matrix.feature_flags }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.rustfmt.overrideCommand": [ 3 | "rustup", 4 | "run", 5 | "nightly", 6 | "rustfmt" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstyle" 16 | version = "1.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 19 | 20 | [[package]] 21 | name = "atomicwrites" 22 | version = "0.4.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3" 25 | dependencies = [ 26 | "rustix 0.38.44", 27 | "tempfile", 28 | "windows-sys 0.52.0", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "2.9.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 42 | 43 | [[package]] 44 | name = "cfg-if" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 48 | 49 | [[package]] 50 | name = "console" 51 | version = "0.15.7" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" 54 | dependencies = [ 55 | "encode_unicode", 56 | "lazy_static", 57 | "libc", 58 | "unicode-width", 59 | "windows-sys 0.45.0", 60 | ] 61 | 62 | [[package]] 63 | name = "difflib" 64 | version = "0.4.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 67 | 68 | [[package]] 69 | name = "either" 70 | version = "1.8.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 73 | 74 | [[package]] 75 | name = "encode_unicode" 76 | version = "0.3.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 79 | 80 | [[package]] 81 | name = "errno" 82 | version = "0.3.11" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 85 | dependencies = [ 86 | "libc", 87 | "windows-sys 0.59.0", 88 | ] 89 | 90 | [[package]] 91 | name = "expectorate" 92 | version = "1.2.0" 93 | dependencies = [ 94 | "atomicwrites", 95 | "console", 96 | "filetime", 97 | "newline-converter", 98 | "predicates", 99 | "similar", 100 | "tempfile", 101 | ] 102 | 103 | [[package]] 104 | name = "fastrand" 105 | version = "2.3.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 108 | 109 | [[package]] 110 | name = "filetime" 111 | version = "0.2.25" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 114 | dependencies = [ 115 | "cfg-if", 116 | "libc", 117 | "libredox", 118 | "windows-sys 0.59.0", 119 | ] 120 | 121 | [[package]] 122 | name = "float-cmp" 123 | version = "0.9.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" 126 | dependencies = [ 127 | "num-traits", 128 | ] 129 | 130 | [[package]] 131 | name = "getrandom" 132 | version = "0.3.2" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 135 | dependencies = [ 136 | "cfg-if", 137 | "libc", 138 | "r-efi", 139 | "wasi", 140 | ] 141 | 142 | [[package]] 143 | name = "itertools" 144 | version = "0.11.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 147 | dependencies = [ 148 | "either", 149 | ] 150 | 151 | [[package]] 152 | name = "lazy_static" 153 | version = "1.4.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 156 | 157 | [[package]] 158 | name = "libc" 159 | version = "0.2.172" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 162 | 163 | [[package]] 164 | name = "libredox" 165 | version = "0.1.3" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 168 | dependencies = [ 169 | "bitflags", 170 | "libc", 171 | "redox_syscall", 172 | ] 173 | 174 | [[package]] 175 | name = "linux-raw-sys" 176 | version = "0.4.15" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 179 | 180 | [[package]] 181 | name = "linux-raw-sys" 182 | version = "0.9.4" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 185 | 186 | [[package]] 187 | name = "memchr" 188 | version = "2.5.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 191 | 192 | [[package]] 193 | name = "newline-converter" 194 | version = "0.3.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" 197 | dependencies = [ 198 | "unicode-segmentation", 199 | ] 200 | 201 | [[package]] 202 | name = "normalize-line-endings" 203 | version = "0.3.0" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 206 | 207 | [[package]] 208 | name = "num-traits" 209 | version = "0.2.15" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 212 | dependencies = [ 213 | "autocfg", 214 | ] 215 | 216 | [[package]] 217 | name = "once_cell" 218 | version = "1.21.3" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 221 | 222 | [[package]] 223 | name = "predicates" 224 | version = "3.0.4" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" 227 | dependencies = [ 228 | "anstyle", 229 | "difflib", 230 | "float-cmp", 231 | "itertools", 232 | "normalize-line-endings", 233 | "predicates-core", 234 | "regex", 235 | ] 236 | 237 | [[package]] 238 | name = "predicates-core" 239 | version = "1.0.6" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 242 | 243 | [[package]] 244 | name = "r-efi" 245 | version = "5.2.0" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 248 | 249 | [[package]] 250 | name = "redox_syscall" 251 | version = "0.5.12" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 254 | dependencies = [ 255 | "bitflags", 256 | ] 257 | 258 | [[package]] 259 | name = "regex" 260 | version = "1.8.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" 263 | dependencies = [ 264 | "aho-corasick", 265 | "memchr", 266 | "regex-syntax", 267 | ] 268 | 269 | [[package]] 270 | name = "regex-syntax" 271 | version = "0.7.1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" 274 | 275 | [[package]] 276 | name = "rustix" 277 | version = "0.38.44" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 280 | dependencies = [ 281 | "bitflags", 282 | "errno", 283 | "libc", 284 | "linux-raw-sys 0.4.15", 285 | "windows-sys 0.59.0", 286 | ] 287 | 288 | [[package]] 289 | name = "rustix" 290 | version = "1.0.7" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 293 | dependencies = [ 294 | "bitflags", 295 | "errno", 296 | "libc", 297 | "linux-raw-sys 0.9.4", 298 | "windows-sys 0.59.0", 299 | ] 300 | 301 | [[package]] 302 | name = "similar" 303 | version = "2.2.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" 306 | 307 | [[package]] 308 | name = "tempfile" 309 | version = "3.19.1" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 312 | dependencies = [ 313 | "fastrand", 314 | "getrandom", 315 | "once_cell", 316 | "rustix 1.0.7", 317 | "windows-sys 0.59.0", 318 | ] 319 | 320 | [[package]] 321 | name = "unicode-segmentation" 322 | version = "1.10.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" 325 | 326 | [[package]] 327 | name = "unicode-width" 328 | version = "0.1.10" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 331 | 332 | [[package]] 333 | name = "wasi" 334 | version = "0.14.2+wasi-0.2.4" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 337 | dependencies = [ 338 | "wit-bindgen-rt", 339 | ] 340 | 341 | [[package]] 342 | name = "windows-sys" 343 | version = "0.45.0" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 346 | dependencies = [ 347 | "windows-targets 0.42.2", 348 | ] 349 | 350 | [[package]] 351 | name = "windows-sys" 352 | version = "0.52.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 355 | dependencies = [ 356 | "windows-targets 0.52.6", 357 | ] 358 | 359 | [[package]] 360 | name = "windows-sys" 361 | version = "0.59.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 364 | dependencies = [ 365 | "windows-targets 0.52.6", 366 | ] 367 | 368 | [[package]] 369 | name = "windows-targets" 370 | version = "0.42.2" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 373 | dependencies = [ 374 | "windows_aarch64_gnullvm 0.42.2", 375 | "windows_aarch64_msvc 0.42.2", 376 | "windows_i686_gnu 0.42.2", 377 | "windows_i686_msvc 0.42.2", 378 | "windows_x86_64_gnu 0.42.2", 379 | "windows_x86_64_gnullvm 0.42.2", 380 | "windows_x86_64_msvc 0.42.2", 381 | ] 382 | 383 | [[package]] 384 | name = "windows-targets" 385 | version = "0.52.6" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 388 | dependencies = [ 389 | "windows_aarch64_gnullvm 0.52.6", 390 | "windows_aarch64_msvc 0.52.6", 391 | "windows_i686_gnu 0.52.6", 392 | "windows_i686_gnullvm", 393 | "windows_i686_msvc 0.52.6", 394 | "windows_x86_64_gnu 0.52.6", 395 | "windows_x86_64_gnullvm 0.52.6", 396 | "windows_x86_64_msvc 0.52.6", 397 | ] 398 | 399 | [[package]] 400 | name = "windows_aarch64_gnullvm" 401 | version = "0.42.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 404 | 405 | [[package]] 406 | name = "windows_aarch64_gnullvm" 407 | version = "0.52.6" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 410 | 411 | [[package]] 412 | name = "windows_aarch64_msvc" 413 | version = "0.42.2" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 416 | 417 | [[package]] 418 | name = "windows_aarch64_msvc" 419 | version = "0.52.6" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 422 | 423 | [[package]] 424 | name = "windows_i686_gnu" 425 | version = "0.42.2" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 428 | 429 | [[package]] 430 | name = "windows_i686_gnu" 431 | version = "0.52.6" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 434 | 435 | [[package]] 436 | name = "windows_i686_gnullvm" 437 | version = "0.52.6" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 440 | 441 | [[package]] 442 | name = "windows_i686_msvc" 443 | version = "0.42.2" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 446 | 447 | [[package]] 448 | name = "windows_i686_msvc" 449 | version = "0.52.6" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 452 | 453 | [[package]] 454 | name = "windows_x86_64_gnu" 455 | version = "0.42.2" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 458 | 459 | [[package]] 460 | name = "windows_x86_64_gnu" 461 | version = "0.52.6" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 464 | 465 | [[package]] 466 | name = "windows_x86_64_gnullvm" 467 | version = "0.42.2" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 470 | 471 | [[package]] 472 | name = "windows_x86_64_gnullvm" 473 | version = "0.52.6" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 476 | 477 | [[package]] 478 | name = "windows_x86_64_msvc" 479 | version = "0.42.2" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 482 | 483 | [[package]] 484 | name = "windows_x86_64_msvc" 485 | version = "0.52.6" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 488 | 489 | [[package]] 490 | name = "wit-bindgen-rt" 491 | version = "0.39.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 494 | dependencies = [ 495 | "bitflags", 496 | ] 497 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "expectorate" 3 | version = "1.2.0" 4 | edition = "2021" 5 | rust-version = "1.75" 6 | license = "Apache-2.0" 7 | description = "Library for comparing output to file contents with simple updating" 8 | repository = "https://github.com/oxidecomputer/expectorate" 9 | readme = "README.md" 10 | keywords = ["test", "fixture"] 11 | categories = ["development-tools::testing"] 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | rustdoc-args = ["--cfg", "docsrs"] 16 | 17 | [features] 18 | predicates = ["dep:predicates"] 19 | 20 | [dependencies] 21 | atomicwrites = "0.4.4" 22 | console = "0.15.7" 23 | newline-converter = "0.3.0" 24 | predicates = { version = "3.0.4", optional = true } 25 | similar = "2.2.1" 26 | 27 | [dev-dependencies] 28 | filetime = "0.2.25" 29 | tempfile = "3.19.1" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expectorate 2 | 3 | This is a little library to validate expected output saved in files. It makes it easy to update that content when it should be updated to match the new results. 4 | 5 | ## Validating Output 6 | 7 | Say you have a function `compose()` that produces a string and you have a file named `lyrics.txt` that contain the expected output. You can compare the actual output like this: 8 | 9 | ```rust 10 | let actual: &str = compose(); 11 | assert_content("lyrics.txt", actual); 12 | ``` 13 | 14 | If the output doesn't match, you'll see output like this: 15 | 16 | ![](screenshots/expectorate.png) 17 | 18 | White means that the content matches. Red means that content from the file was missing. Green means that content not in the file was added. 19 | 20 | If we want to accept the changes from `compose()` we'd simply run with `EXPECTORATE=overwrite`. Assuming `lyrics.txt` is checked in, `git diff` will show you something like this: 21 | 22 | ```diff 23 | diff --git a/examples/lyrics.txt b/examples/lyrics.txt 24 | index e4104c1..ea6beaf 100644 25 | --- a/examples/lyrics.txt 26 | +++ b/examples/lyrics.txt 27 | @@ -1,5 +1,2 @@ 28 | -No one hits like Gaston 29 | -Matches wits like Gaston 30 | -In a spitting match nobody spits like Gaston 31 | +In a testing match nobody tests like Gaston 32 | I'm especially good at expectorating 33 | -Ten points for Gaston 34 | ``` 35 | 36 | ## Predicates (feature: predicates) 37 | 38 | Expectorate can be used in places where you might use the [`predicates` 39 | crate](https://crates.io/crates/predicates). If you're using 40 | `predicates::path::eq_file` you can instead use `expectorate::eq_file` or 41 | `expectorate::eq_file_or_panic`. Populate or update the specified file as 42 | above. -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Oxide Computer Company 2 | 3 | use expectorate::assert_contents; 4 | 5 | fn main() { 6 | let actual = compose(); 7 | assert_contents("examples/lyrics.txt", actual); 8 | } 9 | 10 | fn compose() -> &'static str { 11 | "In a testing match nobody tests like Gaston\nI'm especially good at \ 12 | expectorating\n" 13 | } 14 | -------------------------------------------------------------------------------- /examples/lyrics.txt: -------------------------------------------------------------------------------- 1 | No one hits like Gaston 2 | Matches wits like Gaston 3 | In a spitting match nobody spits like Gaston 4 | I'm especially good at expectorating 5 | Ten points for Gaston 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /screenshots/expectorate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxidecomputer/expectorate/20fe36cc339827d3d2d7aa9b1b54150e82733e10/screenshots/expectorate.png -------------------------------------------------------------------------------- /src/feature_predicates.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Oxide Computer Company 2 | 3 | use std::{fmt::Display, path::PathBuf}; 4 | 5 | use predicates::{reflection::PredicateReflection, Predicate}; 6 | 7 | /// Creates a new predicate that ensures equality with the given file. 8 | /// 9 | /// To accept changes to the file, run with `EXPECTORATE=overwrite`. 10 | #[cfg_attr(docsrs, doc(cfg(feature = "predicates")))] 11 | pub fn eq_file(path: impl Into) -> FilePredicate { 12 | let path = path.into(); 13 | FilePredicate { path, panic: false } 14 | } 15 | 16 | /// Creates a new predicate that ensures equality with the given file and 17 | /// panics if there's a mismatch. 18 | /// 19 | /// To accept changes to the file, run with `EXPECTORATE=overwrite`. 20 | #[cfg_attr(docsrs, doc(cfg(feature = "predicates")))] 21 | pub fn eq_file_or_panic(path: impl Into) -> FilePredicate { 22 | let path = path.into(); 23 | FilePredicate { path, panic: true } 24 | } 25 | 26 | pub struct FilePredicate { 27 | path: PathBuf, 28 | panic: bool, 29 | } 30 | 31 | impl Display for FilePredicate { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | write!(f, "{} {}", self.path.display(), self.panic) 34 | } 35 | } 36 | 37 | impl Predicate for FilePredicate { 38 | fn eval(&self, actual: &str) -> bool { 39 | match crate::assert_contents_impl( 40 | &self.path, 41 | actual, 42 | crate::OverwriteMode::from_env(), 43 | ) { 44 | Err(e) if self.panic => { 45 | panic!("assertion failed: {e}") 46 | } 47 | Err(e) => { 48 | println!("{e}"); 49 | false 50 | } 51 | Ok(_) => true, 52 | } 53 | } 54 | 55 | fn find_case<'a>( 56 | &'a self, 57 | expected: bool, 58 | variable: &str, 59 | ) -> Option> { 60 | let actual = self.eval(variable); 61 | if expected == actual { 62 | Some(predicates::reflection::Case::new(None, actual)) 63 | } else { 64 | None 65 | } 66 | } 67 | } 68 | 69 | impl PredicateReflection for FilePredicate {} 70 | 71 | #[cfg(test)] 72 | mod test { 73 | use predicates::Predicate; 74 | 75 | use crate::eq_file; 76 | 77 | #[test] 78 | fn predicates_good() { 79 | let actual = include_str!("../tests/data_a.txt"); 80 | assert!(eq_file("tests/data_a.txt").eval(actual)); 81 | } 82 | 83 | #[test] 84 | #[should_panic] 85 | fn predicates_bad() { 86 | let actual = include_str!("../tests/data_a.txt"); 87 | assert!(eq_file("tests/data_b.txt").eval(actual)); 88 | } 89 | 90 | #[test] 91 | #[should_panic] 92 | fn predicates_one_line_change() { 93 | let actual = include_str!("../tests/data_a.txt"); 94 | assert!(eq_file("tests/data_a2.txt").eval(actual)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Oxide Computer Company 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | 5 | //! This library is for comparing multi-line output to data stored in version 6 | //! controlled files. It makes it easy to update the contents when should be 7 | //! updated to match the new results. 8 | //! 9 | //! Use it like this: 10 | //! 11 | //! ```rust 12 | //! # fn compose() -> &'static str { "" } 13 | //! let actual: &str = compose(); 14 | //! expectorate::assert_contents("lyrics.txt", actual); 15 | //! ``` 16 | //! 17 | //! If the output doesn't match, the program will panic! and emit the 18 | //! color-coded diffs. 19 | //! 20 | //! To accept the changes from `compose()`, run with `EXPECTORATE=overwrite`. 21 | //! Assuming `lyrics.txt` is checked in, `git diff` will show you something 22 | //! like this: 23 | //! 24 | //! ```diff 25 | //! diff --git a/examples/lyrics.txt b/examples/lyrics.txt 26 | //! index e4104c1..ea6beaf 100644 27 | //! --- a/examples/lyrics.txt 28 | //! +++ b/examples/lyrics.txt 29 | //! @@ -1,5 +1,2 @@ 30 | //! -No one hits like Gaston 31 | //! -Matches wits like Gaston 32 | //! -In a spitting match nobody spits like Gaston 33 | //! +In a testing match nobody tests like Gaston 34 | //! I'm especially good at expectorating 35 | //! -Ten points for Gaston 36 | //! ``` 37 | //! 38 | //! # `predicates` feature 39 | //! 40 | //! Enable the `predicates` feature for compatibility with `predicates` via 41 | //! [`eq_file`] and [`eq_file_or_panic`]. 42 | //! # Predicates (feature: predicates) 43 | 44 | //! Expectorate can be used in places where you might use the [`predicates` 45 | //! crate](https://crates.io/crates/predicates). If you're using 46 | //! `predicates::path::eq_file` you can instead use `expectorate::eq_file` or 47 | //! `expectorate::eq_file_or_panic`. Populate or update the specified file as 48 | //! above. 49 | 50 | #[cfg(feature = "predicates")] 51 | mod feature_predicates; 52 | #[cfg(feature = "predicates")] 53 | pub use feature_predicates::*; 54 | 55 | use atomicwrites::{AtomicFile, OverwriteBehavior}; 56 | use console::Style; 57 | use newline_converter::dos2unix; 58 | use similar::{Algorithm, ChangeTag, TextDiff}; 59 | use std::{env, ffi::OsStr, fs, io::Write, path::Path}; 60 | 61 | /// Compare the contents of the file to the string provided 62 | #[track_caller] 63 | pub fn assert_contents>(path: P, actual: &str) { 64 | if let Err(e) = 65 | assert_contents_impl(path, actual, OverwriteMode::from_env()) 66 | { 67 | panic!("assertion failed: {e}") 68 | } 69 | } 70 | 71 | #[derive(Clone, Copy, Debug)] 72 | pub(crate) enum OverwriteMode { 73 | Check, 74 | Overwrite, 75 | } 76 | 77 | impl OverwriteMode { 78 | pub(crate) fn from_env() -> Self { 79 | let var = env::var_os("EXPECTORATE"); 80 | if var.as_deref().and_then(OsStr::to_str) == Some("overwrite") { 81 | OverwriteMode::Overwrite 82 | } else { 83 | OverwriteMode::Check 84 | } 85 | } 86 | } 87 | 88 | pub(crate) fn assert_contents_impl>( 89 | path: P, 90 | actual: &str, 91 | mode: OverwriteMode, 92 | ) -> Result<(), String> { 93 | let path = path.as_ref(); 94 | let actual = dos2unix(actual); 95 | 96 | let current = match fs::read_to_string(path) { 97 | Ok(s) => Some(s), 98 | Err(e) => match e.kind() { 99 | std::io::ErrorKind::NotFound => None, 100 | _ => panic!("unable to read contents of {}: {}", path.display(), e), 101 | }, 102 | }; 103 | 104 | match mode { 105 | OverwriteMode::Overwrite => { 106 | // Don't write the file if it's the same contents. This avoids mtime 107 | // invalidation. 108 | if current.as_deref() != Some(&actual) { 109 | // There's no way to do a compare-and-set kind of operation on 110 | // filesystems where you can say "only overwrite this file if the 111 | // inode matches what was just read". The closest approximation is 112 | // to disallow overwrites if the file doesn't exist. 113 | let behavior = if current.is_some() { 114 | OverwriteBehavior::AllowOverwrite 115 | } else { 116 | OverwriteBehavior::DisallowOverwrite 117 | }; 118 | let f = AtomicFile::new(path, behavior); 119 | let res = f.write(|f| { 120 | // We're writing the contents out in one call, so there's no 121 | // need to have a BufWriter wrapper. 122 | f.write(actual.as_bytes()) 123 | }); 124 | if let Err(e) = res { 125 | panic!("unable to write to {}: {}", path.display(), e); 126 | } 127 | } 128 | } 129 | OverwriteMode::Check => { 130 | // Treat a nonexistent file like an empty file. 131 | let expected_s = current.unwrap_or_default(); 132 | let expected = dos2unix(&expected_s); 133 | 134 | if expected != actual { 135 | for hunk in TextDiff::configure() 136 | .algorithm(Algorithm::Myers) 137 | .diff_lines(&expected, &actual) 138 | .unified_diff() 139 | .context_radius(5) 140 | .iter_hunks() 141 | { 142 | println!("{}", hunk.header()); 143 | for change in hunk.iter_changes() { 144 | let (marker, style) = match change.tag() { 145 | ChangeTag::Delete => ('-', Style::new().red()), 146 | ChangeTag::Insert => ('+', Style::new().green()), 147 | ChangeTag::Equal => (' ', Style::new()), 148 | }; 149 | print!("{}", style.apply_to(marker).bold()); 150 | print!("{}", style.apply_to(change)); 151 | if change.missing_newline() { 152 | println!(); 153 | } 154 | } 155 | } 156 | println!(); 157 | return Err(format!( 158 | r#"string doesn't match the contents of file: "{}" see diffset above 159 | set EXPECTORATE=overwrite if these changes are intentional"#, 160 | path.display() 161 | )); 162 | } 163 | } 164 | } 165 | Ok(()) 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | use super::*; 171 | use filetime::{set_file_mtime, FileTime}; 172 | use tempfile::TempDir; 173 | 174 | /// If EXPECTORATE=overwrite is set and the file is unchanged, ensure that 175 | /// the mtime stays the same. 176 | #[test] 177 | fn overwite_same_mtime_doesnt_change() { 178 | static CONTENTS: &str = "foo"; 179 | // Setting the mtime to 1970-01-01 doesn't appear to work on Windows. 180 | // Instead, set it to 2000-01-01. The exact time doesn't really matter 181 | // here as much as having a fixed value that's in the past.` 182 | const MTIME: FileTime = FileTime::from_unix_time(946684800, 0); 183 | 184 | let dir = TempDir::with_prefix("expectorate-").unwrap(); 185 | let path = dir.path().join("my-file.txt"); 186 | fs::write(&path, CONTENTS).unwrap(); 187 | 188 | // Set the mtime to a fixed value. 189 | set_file_mtime(&path, MTIME).unwrap(); 190 | 191 | // Overwrite the contents with the same value. 192 | assert_contents_impl(&path, CONTENTS, OverwriteMode::Overwrite) 193 | .unwrap(); 194 | 195 | let meta = fs::metadata(&path).unwrap(); 196 | let mtime2 = FileTime::from_last_modification_time(&meta); 197 | 198 | assert_eq!(mtime2, MTIME, "mtime is zero"); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tests/data_a.txt: -------------------------------------------------------------------------------- 1 | A eh? 2 | a a A 3 | a aye 4 | eh. 5 | 6 | A eh? 7 | a a A 8 | a aye 9 | eh. 10 | 11 | A eh? 12 | a a A 13 | a aye 14 | eh. 15 | 16 | A eh? 17 | a a A 18 | a aye 19 | eh. 20 | 21 | A eh? 22 | a a A 23 | a aye 24 | eh. -------------------------------------------------------------------------------- /tests/data_a2.txt: -------------------------------------------------------------------------------- 1 | A eh? 2 | a a A 3 | a aye 4 | eh. 5 | 6 | A eh? 7 | a a A 8 | a aye 9 | eh. 10 | 11 | A eh? 12 | a a A 13 | a aye 14 | eh. 15 | 16 | A eh? 17 | this line changed 18 | a aye 19 | eh. 20 | 21 | A eh? 22 | a a A 23 | a aye 24 | eh. -------------------------------------------------------------------------------- /tests/data_b.txt: -------------------------------------------------------------------------------- 1 | b B b bee be b 2 | b bbb 3 | Be be 4 | 5 | bee 6 | -------------------------------------------------------------------------------- /tests/test_basic.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Oxide Computer Company 2 | 3 | use expectorate::assert_contents; 4 | 5 | #[test] 6 | fn good() { 7 | let actual = include_str!("data_a.txt"); 8 | assert_contents("tests/data_a.txt", actual); 9 | } 10 | 11 | #[test] 12 | #[should_panic] 13 | fn bad() { 14 | let actual = include_str!("data_a.txt"); 15 | assert_contents("tests/data_b.txt", actual); 16 | } 17 | 18 | #[test] 19 | #[should_panic] 20 | fn one_line_change() { 21 | let actual = include_str!("data_a.txt"); 22 | assert_contents("tests/data_a2.txt", actual); 23 | } 24 | --------------------------------------------------------------------------------