char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
eae10ac
main
1mod common; 2 3use std:: process::{ Command , Output , Stdio }; 4 5struct Suite { 6dir : tempfile:: TempDir , 7} 8 9impl Suite { 10fn new () ->Self { 11Self { 12dir : tempfile:: tempdir (). unwrap (), 13} 14} 15 16fn file ( & self , name : & str , source : & str ) ->String { 17let path =self . dir . path (). join ( name); 18if let Some ( parent) = path. parent () { 19 std:: fs:: create_dir_all ( parent). unwrap (); 20} 21 std:: fs:: write ( & path, source). unwrap (); 22 path. to_str (). unwrap (). to_owned () 23} 24 25fn test ( & self , args : & [ & str ]) ->Output { 26Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 27. arg ( "test" ) 28. args ( args) 29. current_dir ( self . dir . path ()) 30. stdin ( Stdio :: null ()) 31. stdout ( Stdio :: piped ()) 32. stderr ( Stdio :: piped ()) 33. spawn () 34. map ( common:: wait) 35. unwrap () 36} 37 38fn stdout ( & self , args : & [ & str ]) ->String { 39let output =self . test ( args); 40assert! ( 41 output. status . success (), 42"{args:?}: {}" , 43String :: from_utf8_lossy ( & output. stderr ) 44); 45String :: from_utf8 ( output. stdout ). unwrap () 46} 47 48fn failure ( & self , args : & [ & str ]) ->( String , String ) { 49let output =self . test ( args); 50assert! ( 51 !output. status . success (), 52"{args:?} unexpectedly passed: {}" , 53String :: from_utf8_lossy ( & output. stdout ) 54); 55( 56String :: from_utf8 ( output. stdout ). unwrap (), 57String :: from_utf8 ( output. stderr ). unwrap (), 58) 59} 60} 61 62# [ test ] 63fn test_api_is_in_avril_test_not_avril_std () { 64let output = common:: run ( 65r#" 66import { test } from 'avril:test'; 67const testModule = await import('avril:test'); 68if (testModule.test !== test) throw new Error('Wrong test binding'); 69if (Object.keys(testModule).join(',') !== 'test') throw new Error('Leaked test internals'); 70for (const [fn, name] of [[test, 'test'], [test.only, 'only'], [test.ignore, 'ignore']]) { 71if (fn.name !== name || fn.length !== 2) throw new Error('Changed callable shape'); 72} 73for (const name of ['only', 'ignore']) { 74const descriptor = Object.getOwnPropertyDescriptor(test, name); 75if (!descriptor.writable || !descriptor.enumerable || !descriptor.configurable) { 76throw new Error('Changed test property descriptor'); 77} 78} 79const std = await import('avril:std'); 80if ('test' in std) throw new Error('avril:std still exports test'); 81test('inert under run', () => { throw new Error('must not run'); }); 82console.log('moved'); 83"# , 84& [], 85); 86assert_eq! ( output. stdout , b"moved\n" ); 87} 88 89# [ test ] 90fn runner_captures_date_clock_before_guest_evaluation () { 91let suite =Suite :: new (); 92 suite. file ( 93"poison_clock_test.js" , 94r#" 95import { test } from "avril:test"; 96const originalNow = Date.now; 97Date.now = () => { throw new Error("runner captured the poisoned clock"); }; 98test("restore the clock", () => { Date.now = originalNow; }); 99"# , 100); 101let out = suite. stdout ( & [ "poison_clock_test.js" ]); 102assert! ( out. contains ( "ok - restore the clock" ), "{out}" ); 103} 104 105# [ test ] 106fn registration_uses_captured_array_methods () { 107let suite =Suite :: new (); 108 suite. file ( 109"captured_test.ts" , 110r#" 111import { test } from "avril:test"; 112const push = Array.prototype.push; 113Array.prototype.push = () => { throw new Error("poisoned push"); }; 114try { 115test("filtered", () => { throw new Error("must not run"); }); 116test.only("focused", async (t) => { 117if (!await t.step("child", () => {})) throw new Error("step failed"); 118}); 119test.only({ name: "ignored", ignore: true }); 120} finally { 121Array.prototype.push = push; 122} 123"# , 124); 125let stdout = suite. stdout ( & []); 126assert! ( stdout. contains ( "ok - focused" ), "{stdout}" ); 127assert! ( stdout. contains ( "ok - child" ), "{stdout}" ); 128assert! ( 129 stdout. contains ( "1 passed; 0 failed; 1 ignored; 1 filtered" ), 130"{stdout}" 131); 132} 133 134# [ test ] 135fn passing_suite_reports_each_test_and_a_summary () { 136let suite =Suite :: new (); 137 suite. file ( 138"math_test.ts" , 139r#" 140import { test } from 'avril:test'; 141test('adds', () => { if (1 + 1 !== 2) throw new Error('math broke'); }); 142test('async works', async () => { 143await new Promise((resolve) => setTimeout(resolve, 5)); 144}); 145"# , 146); 147let out = suite. stdout ( & [ "math_test.ts" ]); 148assert! ( out. contains ( "Check ./math_test.ts" ), "{out}" ); 149assert! ( out. contains ( "ok - adds" ), "{out}" ); 150assert! ( out. contains ( "ok - async works" ), "{out}" ); 151assert! ( 152 out. contains ( "ok - ./math_test.ts (2 passed, 0 failed, 0 ignored, 0 filtered)" ), 153"{out}" 154); 155assert! ( 156 out. contains ( "test result: ok. 2 passed; 0 failed; 0 ignored; 0 filtered; 1 file." ), 157"{out}" 158); 159} 160 161# [ test ] 162fn durations_reflect_wall_time () { 163let suite =Suite :: new (); 164 suite. file ( 165"slow_test.js" , 166r#" 167import { test } from 'avril:test'; 168test('slow', async () => { 169await new Promise((resolve) => setTimeout(resolve, 200)); 170}); 171test({ name: 'no deadline', timeout: Infinity, fn() {} }); 172"# , 173); 174let out = suite. stdout ( & [ "slow_test.js" ]); 175let line = out 176. lines () 177. find ( |line| line. starts_with ( "ok - slow (" )) 178. unwrap_or ( "" ); 179let elapsed: u64 = line 180. strip_prefix ( "ok - slow (" ) 181. and_then ( |rest| rest. strip_suffix ( "ms)" )) 182. and_then ( |digits| digits. parse (). ok ()) 183. unwrap_or ( 0 ); 184assert! ( elapsed >=150 , "{out}" ); 185assert! ( out. contains ( "ok - no deadline (" ), "{out}" ); 186} 187 188# [ test ] 189fn spinning_tests_are_terminated_and_fail_their_file () { 190let suite =Suite :: new (); 191 suite. file ( 192"spin_test.js" , 193r#" 194import { test } from 'avril:test'; 195test({ name: 'spins', timeout: 50, fn() { for (;;) {} } }); 196test('never runs', () => {}); 197"# , 198); 199 suite. file ( 200"after_test.js" , 201r#" 202import { test } from 'avril:test'; 203test('unaffected', () => {}); 204"# , 205); 206let ( out, _) = suite. failure ( & [ "spin_test.js" , "after_test.js" ]); 207assert! ( out. contains ( "FAILED - spins (50ms)" ), "{out}" ); 208assert! ( out. contains ( "Test \"spins\" timed out after 50ms" ), "{out}" ); 209assert! ( !out. contains ( "never runs" ), "{out}" ); 210assert! ( out. contains ( "ok - unaffected (" ), "{out}" ); 211assert! ( 212 out. contains ( "test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 filtered; 2 files." ), 213"{out}" 214); 215} 216 217# [ test ] 218fn timeouts_past_the_timer_range_are_rejected () { 219let suite =Suite :: new (); 220 suite. file ( 221"huge_test.js" , 222r#" 223import { test } from 'avril:test'; 224test({ name: 'huge', timeout: 3000000000, fn() {} }); 225"# , 226); 227let ( out, _) = suite. failure ( & [ "huge_test.js" ]); 228assert! ( out. contains ( "at most 2147483647ms" ), "{out}" ); 229} 230 231# [ test ] 232fn steps_resolve_false_and_fail_parents_without_throwing () { 233let suite =Suite :: new (); 234 suite. file ( 235"semantics_test.js" , 236r#" 237import { test } from 'avril:test'; 238test('concurrent', async (t) => { 239const results = await Promise.all([ 240t.step('victim', () => {}), 241t.step('latecomer', () => {}), 242]); 243if (JSON.stringify(results) !== '[true,false]') throw new Error('bad results'); 244}); 245test('unawaited', (t) => { 246t.step('late failure', async () => { 247await new Promise((resolve) => setTimeout(resolve, 10)); 248throw new Error('late boom'); 249}); 250}); 251test('ignored then real', async (t) => { 252// An ignored step must release the context for the next step. 253if ((await t.step({ name: 'skipped', ignore: true })) !== false) { 254throw new Error('bad ignore result'); 255} 256if ((await t.step('real', () => {})) !== true) throw new Error('real step blocked'); 257}); 258"# , 259); 260let ( out, _) = suite. failure ( & [ "semantics_test.js" ]); 261assert! ( out. contains ( " ok - victim (" ), "{out}" ); 262assert! ( out. contains ( " FAILED - latecomer (" ), "{out}" ); 263assert! ( out. contains ( "while another step was running" ), "{out}" ); 264assert! ( out. contains ( "FAILED - concurrent (" ), "{out}" ); 265assert! ( out. contains ( "due to 1 failed step" ), "{out}" ); 266assert! ( out. contains ( " FAILED - late failure (" ), "{out}" ); 267assert! ( out. contains ( "late boom" ), "{out}" ); 268assert! ( out. contains ( "FAILED - unawaited (" ), "{out}" ); 269} 270 271# [ test ] 272fn stale_contexts_and_late_registrations_fail_loudly () { 273let suite =Suite :: new (); 274 suite. file ( 275"stale_test.js" , 276r#" 277import { test } from 'avril:test'; 278let stolen; 279test('first', (t) => { stolen = t; }); 280test('second', async () => { 281try { 282await stolen.step('late', () => {}); 283} catch (error) { 284if (!(error instanceof TypeError)) throw error; 285if (!error.message.includes('called after "first" settled')) throw error; 286return; 287} 288throw new Error('stale step was allowed'); 289}); 290test.ignore('nameless'); 291"# , 292); 293let out = suite. stdout ( & [ "stale_test.js" ]); 294assert! ( out. contains ( "ignored - nameless" ), "{out}" ); 295 suite. file ( 296"late_test.js" , 297r#" 298import { test } from 'avril:test'; 299test('first', () => { test('late', () => {}); }); 300"# , 301); 302let ( out, _) = suite. failure ( & [ "late_test.js" ]); 303assert! ( 304 out. contains ( "must be called while the test file evaluates" ), 305"{out}" 306); 307} 308 309# [ test ] 310fn unhandled_rejections_fail_the_file_with_detail () { 311let suite =Suite :: new (); 312 suite. file ( 313"stray_test.js" , 314r#" 315import { test } from 'avril:test'; 316test('ok', () => {}); 317test('boom', () => { Promise.reject(new Error('lost rejection')); }); 318"# , 319); 320let ( out, _) = suite. failure ( & [ "stray_test.js" ]); 321assert! ( out. contains ( "ok - ok (" ), "{out}" ); 322assert! ( 323 out. contains ( "Unhandled rejections during the test run:" ), 324"{out}" 325); 326assert! ( out. contains ( "lost rejection" ), "{out}" ); 327} 328 329# [ test ] 330fn step_option_errors_name_steps_and_missing_functions () { 331let suite =Suite :: new (); 332 suite. file ( 333"badopts_test.js" , 334r#" 335import { test } from 'avril:test'; 336test('options', async (t) => { 337try { 338await t.step({ name: 's', fn() {}, bogus: true }); 339} catch (error) { 340if (error.message !== 'Unknown step option: bogus') throw error; 341} 342try { 343await t.step({ name: 's', fn() {}, ignore: 1 }); 344} catch (error) { 345if (error.message !== 'Step option ignore must be a boolean') throw error; 346return; 347} 348throw new Error('bad step options were allowed'); 349}); 350test('missing', async (t) => { 351try { 352await t.step('nofn'); 353} catch (error) { 354if (error.message !== 'Step "nofn" requires a function') throw error; 355return; 356} 357throw new Error('missing step function was allowed'); 358}); 359test({ name: 'sync only', timeout: 0, fn() {} }); 360"# , 361); 362let out = suite. stdout ( & [ "badopts_test.js" ]); 363assert! ( out. contains ( "3 passed" ), "{out}" ); 364} 365 366# [ test ] 367fn discovery_skips_dot_files_and_walks_symlinked_dirs () { 368let suite =Suite :: new (); 369 suite. file ( 370"visible_test.js" , 371"import { test } from 'avril:test'; test('seen', () => {});" , 372); 373 suite. file ( 374".hidden_test.js" , 375"import { test } from 'avril:test'; test('unseen', () => { throw new Error('ran'); });" , 376); 377 suite. file ( 378"real/inner_test.js" , 379"import { test } from 'avril:test'; test('linked', () => {});" , 380); 381# [ cfg ( unix )] 382 std:: os:: unix:: fs:: symlink ( suite. dir . path (). join ( "real" ), suite. dir . path (). join ( "link" )) 383. unwrap (); 384// Walking the directory runs the visible file but never the dot-file. 385let out = suite. stdout ( & [ "." ]); 386assert! ( out. contains ( "ok - seen (" ), "{out}" ); 387assert! ( !out. contains ( "unseen" ), "{out}" ); 388# [ cfg ( unix )] 389{ 390// An explicit symlinked directory walks its target. 391let out = suite. stdout ( & [ "link" ]); 392assert! ( out. contains ( "ok - linked (" ), "{out}" ); 393} 394} 395 396# [ test ] 397fn failing_suite_fails_with_stacks_and_a_summary () { 398let suite =Suite :: new (); 399 suite. file ( 400"fail_test.js" , 401r#" 402import { test } from 'avril:test'; 403test('passes', () => {}); 404test('rejects', async () => { await Promise.resolve(); throw new Error('boom'); }); 405test('sync throws', () => { throw new TypeError('bad'); }); 406"# , 407); 408let ( out, err) = suite. failure ( & [ "fail_test.js" ]); 409assert! ( out. contains ( "ok - passes" ), "{out}" ); 410assert! ( out. contains ( "FAILED - rejects" ), "{out}" ); 411assert! ( out. contains ( "FAILED - sync throws" ), "{out}" ); 412assert! ( out. contains ( "Error: boom" ), "{out}" ); 413assert! ( out. contains ( "TypeError: bad" ), "{out}" ); 414assert! ( out. contains ( "fail_test.js:" ), "{out}" ); 415assert! ( !out. contains ( "ext:test/main.js" ), "{out}" ); 416assert! ( !out. contains ( "ext:test/runner.js" ), "{out}" ); 417assert! ( 418 out. contains ( "FAILED - ./fail_test.js (1 passed, 2 failed, 0 ignored, 0 filtered)" ), 419"{out}" 420); 421assert! ( 422 out. contains ( "test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 filtered; 1 file." ), 423"{out}" 424); 425assert! ( err. contains ( "test failed" ), "{err}" ); 426} 427 428# [ test ] 429fn filter_selects_by_substring_and_counts_the_rest () { 430let suite =Suite :: new (); 431 suite. file ( 432"filter_test.js" , 433r#" 434import { test } from 'avril:test'; 435test('apple', () => {}); 436test('banana', () => {}); 437test('crabapple', () => {}); 438"# , 439); 440let out = suite. stdout ( & [ "--filter=apple" , "filter_test.js" ]); 441assert! ( out. contains ( "ok - apple" ), "{out}" ); 442assert! ( out. contains ( "ok - crabapple" ), "{out}" ); 443assert! ( !out. contains ( "ok - banana" ), "{out}" ); 444assert! ( 445 out. contains ( "ok - ./filter_test.js (2 passed, 0 failed, 0 ignored, 1 filtered)" ), 446"{out}" 447); 448} 449 450# [ test ] 451fn ignore_skips_and_only_focuses () { 452let suite =Suite :: new (); 453 suite. file ( 454"focus_test.js" , 455r#" 456import { test } from 'avril:test'; 457test('a', () => {}); 458test({ name: 'b', ignore: true, fn() { throw new Error('must not run'); } }); 459test.ignore('c'); 460test('d', () => {}); 461"# , 462); 463let out = suite. stdout ( & [ "focus_test.js" ]); 464assert! ( out. contains ( "ignored - b" ), "{out}" ); 465assert! ( out. contains ( "ignored - c" ), "{out}" ); 466assert! ( out. contains ( "ok - a" ), "{out}" ); 467assert! ( out. contains ( "ok - d" ), "{out}" ); 468assert! ( 469 out. contains ( "ok - ./focus_test.js (2 passed, 0 failed, 2 ignored, 0 filtered)" ), 470"{out}" 471); 472 suite. file ( 473"only_test.js" , 474r#" 475import { test } from 'avril:test'; 476test('a', () => { throw new Error('must not run'); }); 477test.only('b', () => {}); 478test({ name: 'c', only: true, fn() {} }); 479"# , 480); 481let out = suite. stdout ( & [ "only_test.js" ]); 482assert! ( out. contains ( "ok - b" ), "{out}" ); 483assert! ( out. contains ( "ok - c" ), "{out}" ); 484assert! ( !out. contains ( "ok - a" ), "{out}" ); 485assert! ( 486 out. contains ( "ok - ./only_test.js (2 passed, 0 failed, 0 ignored, 1 filtered)" ), 487"{out}" 488); 489} 490 491# [ test ] 492fn steps_nest_and_a_failing_step_fails_its_parent () { 493let suite =Suite :: new (); 494 suite. file ( 495"steps_test.js" , 496r#" 497import { test } from 'avril:test'; 498test('nested', async (t) => { 499const first = await t.step('one', () => {}); 500const second = await t.step({ name: 'two', ignore: true, fn() {} }); 501if (first !== true || second !== false) throw new Error('bad step results'); 502await t.step('deep', async (t) => { 503await t.step('deeper', () => {}); 504}); 505}); 506test('step failure', async (t) => { 507await t.step('bad', () => { throw new Error('step boom'); }); 508}); 509"# , 510); 511let ( out, _) = suite. failure ( & [ "steps_test.js" ]); 512assert! ( out. contains ( "ok - nested" ), "{out}" ); 513assert! ( out. contains ( " ok - one" ), "{out}" ); 514assert! ( out. contains ( " ignored - two" ), "{out}" ); 515assert! ( out. contains ( " ok - deep" ), "{out}" ); 516assert! ( out. contains ( " ok - deeper" ), "{out}" ); 517assert! ( out. contains ( "FAILED - step failure" ), "{out}" ); 518assert! ( out. contains ( " FAILED - bad" ), "{out}" ); 519assert! ( out. contains ( "step boom" ), "{out}" ); 520} 521 522# [ test ] 523fn slow_tests_time_out () { 524let suite =Suite :: new (); 525 suite. file ( 526"timeout_test.js" , 527r#" 528import { test } from 'avril:test'; 529test({ name: 'hangs', timeout: 30, fn: () => new Promise(() => {}) }); 530"# , 531); 532let ( out, _) = suite. failure ( & [ "timeout_test.js" ]); 533assert! ( out. contains ( "FAILED - hangs" ), "{out}" ); 534assert! ( out. contains ( "timed out after 30ms" ), "{out}" ); 535} 536 537# [ test ] 538fn files_are_isolated_but_run_in_sorted_order () { 539let suite =Suite :: new (); 540 suite. file ( 541"a_first_test.js" , 542r#" 543import { test } from 'avril:test'; 544test('sets global', () => { globalThis.__leak = 42; }); 545"# , 546); 547 suite. file ( 548"b_second_test.js" , 549r#" 550import { test } from 'avril:test'; 551test('sees no leak', () => { 552if (globalThis.__leak !== undefined) throw new Error('state leaked across files'); 553}); 554test('shares within a file', () => { globalThis.__local = 1; }); 555test('reads within a file', () => { 556if (globalThis.__local !== 1) throw new Error('no sharing within a file'); 557}); 558"# , 559); 560let out = suite. stdout ( & [ "a_first_test.js" , "b_second_test.js" ]); 561let first = out. find ( "Check ./a_first_test.js" ). unwrap (); 562let second = out. find ( "Check ./b_second_test.js" ). unwrap (); 563assert! ( first < second, "{out}" ); 564assert! ( 565 out. contains ( "test result: ok. 4 passed; 0 failed; 0 ignored; 0 filtered; 2 files." ), 566"{out}" 567); 568} 569 570# [ test ] 571fn permission_flags_apply_to_every_file () { 572let suite =Suite :: new (); 573let secret = suite. file ( "secret.txt" , "s3cret" ); 574let grant =format! ( "--allow-read={secret}" ); 575 suite. file ( 576"perm_test.js" , 577& format! ( 578r#" 579import {{ test }} from 'avril:test'; 580import {{ readTextFile }} from 'avril:fs'; 581test('reads the fixture', async () => {{ 582if ((await readTextFile({secret:?})) !== 's3cret') throw new Error('bad fixture'); 583}}); 584"# 585), 586); 587let ( out, _) = suite. failure ( & [ "-0" , "perm_test.js" ]); 588assert! ( out. contains ( "FAILED - reads the fixture" ), "{out}" ); 589assert! ( out. contains ( "PermissionDenied" ), "{out}" ); 590let out = suite. stdout ( & [ "-0" , & grant, "perm_test.js" ]); 591assert! ( out. contains ( "ok - reads the fixture" ), "{out}" ); 592} 593 594# [ test ] 595fn pledges_narrow_only_their_own_file () { 596let suite =Suite :: new (); 597 suite. file ( "fixture.txt" , "data" ); 598 suite. file ( "helper.js" , "export function triple(n) { return n * 3; }" ); 599 suite. file ( 600"a_restrict_test.js" , 601r#" 602import { pledge } from 'avril:std'; 603import { test } from 'avril:test'; 604pledge({ read: false, jsExec: false }); 605test('pure', () => { if (1 + 1 !== 2) throw new Error('bad'); }); 606"# , 607); 608 suite. file ( 609"b_free_test.js" , 610r#" 611import { test } from 'avril:test'; 612import { readTextFile } from 'avril:fs'; 613import { triple } from './helper.js'; 614test('reads and loads', async () => { 615if (triple(14) !== 42) throw new Error('bad helper'); 616if ((await readTextFile('./fixture.txt')) !== 'data') throw new Error('bad read'); 617}); 618"# , 619); 620// The restricted file passes alone, the free file passes alone, and 621// both pass together: narrowing must not leak across files. 622 suite. stdout ( & [ "a_restrict_test.js" ]); 623 suite. stdout ( & [ "b_free_test.js" ]); 624let out = suite. stdout ( & [ "a_restrict_test.js" , "b_free_test.js" ]); 625assert! ( 626 out. contains ( "test result: ok. 2 passed; 0 failed; 0 ignored; 0 filtered; 2 files." ), 627"{out}" 628); 629} 630 631# [ test ] 632fn file_errors_carry_detail_and_fail_the_run () { 633let suite =Suite :: new (); 634 suite. file ( "broken_test.js" , "const = broken\n" ); 635 suite. file ( 636"nofn_test.js" , 637r#" 638import { test } from 'avril:test'; 639test('missing function'); 640"# , 641); 642let ( out, _) = suite. failure ( & [ "broken_test.js" , "nofn_test.js" ]); 643assert! ( out. contains ( "FAILED - ./broken_test.js" ), "{out}" ); 644assert! ( out. contains ( "SyntaxError" ), "{out}" ); 645assert! ( out. contains ( "FAILED - ./nofn_test.js" ), "{out}" ); 646assert! ( out. contains ( "requires a function" ), "{out}" ); 647assert! ( out. contains ( "test result: FAILED." ), "{out}" ); 648} 649 650# [ test ] 651fn shared_option_errors_keep_guest_frames_and_hide_test_internals () { 652let suite =Suite :: new (); 653let registration = suite. file ( 654"invalid_registration_test.js" , 655r#" 656import { test } from "avril:test"; 657test({ name: "invalid", bogus: true }); 658"# , 659); 660let step = suite. file ( 661"invalid_step_test.js" , 662r#" 663import { test } from "avril:test"; 664test("invalid step", (t) => t.step({ name: "invalid", bogus: true })); 665"# , 666); 667let ( out, _) = suite. failure ( & [ & registration, & step]); 668assert! ( out. contains ( "Unknown test option: bogus" ), "{out}" ); 669assert! ( out. contains ( "Unknown step option: bogus" ), "{out}" ); 670assert! ( out. contains ( "invalid_registration_test.js:" ), "{out}" ); 671assert! ( out. contains ( "invalid_step_test.js:" ), "{out}" ); 672for framein [ 673"ext:test/main.js" , 674"ext:test/runner.js" , 675"ext:test/shared.js" , 676] { 677assert! ( !out. contains ( frame), "{out}" ); 678} 679} 680 681# [ test ] 682fn no_run_lists_tests_without_running_them () { 683let suite =Suite :: new (); 684 suite. file ( 685"list_test.js" , 686r#" 687import { test } from 'avril:test'; 688test('would run', () => { throw new Error('must not run'); }); 689test({ name: 'would skip', ignore: true, fn() {} }); 690"# , 691); 692let out = suite. stdout ( & [ "--no-run" , "list_test.js" ]); 693assert! ( out. contains ( "test - would run" ), "{out}" ); 694assert! ( out. contains ( "ignored - would skip" ), "{out}" ); 695assert! ( !out. contains ( "must not run" ), "{out}" ); 696assert! ( 697 out. contains ( "found - ./list_test.js (1 tests, not run)" ), 698"{out}" 699); 700assert! ( 701 out. contains ( "test result: ok. found 1 tests in 1 file (not run)." ), 702"{out}" 703); 704} 705 706# [ test ] 707fn discovery_walks_directories_and_ignores_vendor_trees () { 708let suite =Suite :: new (); 709 suite. file ( 710"sub/good_test.js" , 711"import { test } from 'avril:test'; test('good', () => {});" , 712); 713 suite. file ( 714"sub/plain.js" , 715"import { test } from 'avril:test'; test('explicit', () => {});" , 716); 717 suite. file ( 718"node_modules/evil_test.js" , 719"import { test } from 'avril:test'; test('evil', () => { throw new Error('must not run'); });" , 720); 721 suite. file ( 722"vendor/evil_test.js" , 723"import { test } from 'avril:test'; test('evil', () => { throw new Error('must not run'); });" , 724); 725// Walking finds only the test-shaped file; the explicit plain.js runs anyway. 726let out = suite. stdout ( & [ "sub" ]); 727assert! ( out. contains ( "Check ./sub/good_test.js" ), "{out}" ); 728assert! ( !out. contains ( "explicit" ), "{out}" ); 729let out = suite. stdout ( & [ "sub/plain.js" ]); 730assert! ( out. contains ( "ok - explicit" ), "{out}" ); 731let out = suite. stdout ( & []); 732assert! ( out. contains ( "Check ./sub/good_test.js" ), "{out}" ); 733assert! ( !out. contains ( "evil" ), "{out}" ); 734} 735 736# [ test ] 737fn test_help_and_option_validation_follow_command_conventions () { 738let suite =Suite :: new (); 739let help =String :: from_utf8 ( 740Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 741. arg ( "test" ) 742. arg ( "--help" ) 743. output () 744. unwrap () 745. stdout , 746) 747. unwrap (); 748assert! ( 749 help. contains ( "Usage: avril test [OPTIONS] [files...]" ), 750"{help}" 751); 752assert! ( help. contains ( "--filter=PATTERN" ), "{help}" ); 753assert! ( help. contains ( "--no-run" ), "{help}" ); 754for ( args, message) in [ 755( & [ "--bogus" , "x" ] as & [ & str ], "Unknown option: --bogus" ), 756( & [ "--pre" , "x" ], "--pre is not an option of `avril test`" ), 757( 758& [ "--port=0" , "x" ], 759"--port is not an option of `avril test`" , 760), 761( & [ "--filter" ], "Expected a pattern for --filter" ), 762( & [ "--no-run=yes" ], "--no-run does not take a value" ), 763] { 764let output = suite. test ( args); 765assert! ( !output. status . success (), "{args:?}" ); 766let error =String :: from_utf8 ( output. stderr ). unwrap (); 767assert! ( error. contains ( message), "{args:?}: {error}" ); 768} 769}