Skip to main content

supercov_engine/
assertion_coverage.rs

1//! Assertions coverage: which executed statements a test would catch breaking.
2//!
3//! A statement counts as asserted when changing it makes at least one passing
4//! test that executes it fail. The change follows the statement: an `if` has its
5//! condition inverted (each direction), `return x` returns undefined, a
6//! declaration's value becomes undefined, an expression point evaluates to
7//! undefined, and any other statement is skipped. Imports, declarations without
8//! a value, and function, class and type declarations are not assessed.
9//!
10//! Supercov does not decide this itself. For each test it renders what the test
11//! ran (its code, its helpers, and the source it executed) and asks Jev, one
12//! yes/no question per statement, whether the test would fail. This module owns
13//! the population, the change rules and the rendering; the CLI owns the requests,
14//! the answer cache and the output. What Jev sees for one statement and one test
15//! is also the cache key, so an answer is reused only when the question would be
16//! asked with exactly the same text.
17use crate::coverage_analysis::PointKind;
18use crate::coverage_report::CoverageReport;
19use oxc_allocator::Allocator;
20use oxc_ast::ast::{
21    Declaration, ExportDefaultDeclarationKind, Expression, Statement as JsStatement,
22};
23use oxc_ast_visit::{Visit, walk};
24use oxc_parser::Parser;
25use oxc_span::{GetSpan, SourceType};
26use serde::Serialize;
27use serde_json::{Value, json};
28use sha2::{Digest, Sha256};
29use std::collections::{BTreeMap, BTreeSet};
30use std::path::{Path, PathBuf};
31
32mod go;
33mod jvm;
34mod ruby;
35mod rust_source;
36
37/// Statements starting on each line: their text and change (`None`: not
38/// assessed).
39type Starts = BTreeMap<usize, Vec<(String, Option<Change>)>>;
40
41pub const MODEL: &str = "jev-1.13.0";
42/// Tests asked about a statement before it may be called unchecked, then the
43/// wider set asked before it is (measured: more tests remove false "unchecked"
44/// flags, and 25 tests was no better than 15).
45pub const FIRST_TESTS: usize = 5;
46pub const MORE_TESTS: usize = 15;
47/// Asked last, when none of those is judged to fail: the tests most likely
48/// written for the statement.
49pub const LAST_TESTS: usize = 3;
50/// The most tests one statement is asked of.
51pub const MAX_TESTS: usize = MORE_TESTS + LAST_TESTS;
52pub const MAX_QUESTIONS: usize = 150;
53/// A statement is asserted when some test's answer is at least this.
54pub const THRESHOLD: f64 = 0.5;
55const HEAD_LINES: usize = 10;
56const HELPER_CHARS: usize = 3000;
57/// The budget for data files a test names (Ruby spec support fixtures),
58/// beside the helpers': the cases a data-driven test checks are its asserts.
59const FIXTURE_CHARS: usize = 12000;
60const CONTEXT_LINES: usize = 0;
61const WINDOW_LINES: usize = 4;
62/// How far either side of a statement that is in no function its file is
63/// shown.
64const OUTSIDE_A_FUNCTION: usize = 25;
65const CODE_CHARS: usize = 10000;
66const BODY_LINES: usize = 150;
67
68pub const INSTRUCTIONS: &str = "Each question describes exactly one change to the program, made alone. The test passes on the code as written and executes the changed line. Question: with that change, does this test fail (an assertion fails, test or helper code throws, the test times out, or the process crashes)? Follow what the changed line computes to what the test checks; do not guess from names. In `code_run`, lines marked ▶ start statements this test ran.";
69
70fn runtime(runner: &str) -> &'static str {
71    match runner {
72        "node:test" => {
73            "node:test; a test past its timeout fails; after hooks still run. The tests spawn the program as child processes; an unhandled promise rejection ends a Node process."
74        }
75        "jest" => {
76            "Jest; a test past its timeout (default 5000 ms) fails; an unhandled error during the run fails it; jest.fn() returns undefined unless given an implementation."
77        }
78        "ava" => {
79            "AVA; a failed t assertion, an uncaught error or a rejected promise the test returns fails it; a test past its timeout (default 10 s) fails; a test with no assertion fails unless failWithoutAssertions is off; a file's tests run concurrently unless declared with test.serial."
80        }
81        "tap" => {
82            "node-tap; a failed t assertion, an uncaught error or a rejected promise a subtest returns fails it; a subtest past its timeout (default 30 s) fails; a parent fails when a subtest does; each test file runs as a process of its own."
83        }
84        "mocha" => {
85            "Mocha; a test past its timeout (default 2000 ms) fails; an uncaught error, a rejected promise it returns or an error passed to done fails it; beforeEach hooks run before each test."
86        }
87        "pytest" => {
88            "pytest; a failing assert or an uncaught exception fails the test; there is no timeout unless pytest-timeout is configured; fixtures from conftest.py run before the test; a Mock or MagicMock returns another mock unless given a return value."
89        }
90        "rspec" => {
91            "RSpec; a failed expectation or an uncaught exception fails the example; there is no timeout; before hooks and let blocks run for each example; a double raises on messages it was not told to expect, and allow(...).to receive returns nil unless given a value."
92        }
93        "minitest" => {
94            "Minitest; a failed assertion or an uncaught exception fails the test; there is no timeout; setup runs before each test; a stubbed method returns the value it was given."
95        }
96        "test-unit" => {
97            "test-unit; a failed assertion or an uncaught exception fails the test; there is no timeout; setup runs before each test."
98        }
99        "cucumber" => {
100            "Cucumber; a step that raises (a failed expectation included) fails the scenario; a step with no definition leaves it undefined, not passed; Before hooks run for each scenario."
101        }
102        "go-test" => {
103            "go test; t.Error, t.Fatal and a panic fail the test; a test past the -timeout (default 10 minutes) fails; subtests run inside their parent; a nil pointer dereference panics. A test catches a changed value only where one of its checks compares it, directly or through what it feeds: a byte, field or result that no check reads can change while the test still passes."
104        }
105        "rust-libtest" | "rust-nextest" | "nextest" => {
106            "Rust tests; a panic fails the test (a failed assert!, assert_eq!, unwrap or expect included) unless it is marked #[should_panic], which then fails when nothing panics; there is no timeout."
107        }
108        "rustdoc" => {
109            "Rust doctests; the code block is compiled and run as its own program, and a panic fails it; a block marked no_run is only compiled and should_panic fails when nothing panics."
110        }
111        "junit-platform" => {
112            "JUnit; a failed assertion or an uncaught exception fails the test (an expected one inside assertThrows aside); @BeforeEach runs before each test; there is no timeout unless one is set; a Mockito mock returns null, 0, false or an empty collection unless stubbed."
113        }
114        "testng" => {
115            "TestNG; a failed assertion or an uncaught exception fails the test; @BeforeMethod runs before each test; there is no timeout unless one is set; a Mockito mock returns null, 0, false or an empty collection unless stubbed."
116        }
117        "unittest" => {
118            "unittest; a failing assert* method or an uncaught exception fails the test; there is no timeout; setUp runs before each test; a Mock or MagicMock returns another mock unless given a return value."
119        }
120        _ => {
121            "Vitest; a test past its timeout (default 5000 ms) fails; an unhandled error during the run fails it; vi.fn() returns undefined unless given an implementation."
122        }
123    }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub enum Change {
129    /// `if`: the condition inverted, each direction asked separately.
130    Invert,
131    /// `return x`: returns undefined without evaluating `x`.
132    ReturnUndefined,
133    /// A declaration with a value: the value becomes undefined.
134    ValueUndefined,
135    /// An expression point that is not a statement (a JSX expression).
136    ExpressionUndefined,
137    /// Anything else: the statement is skipped.
138    Skip,
139    /// A boolean literal returned or declared: it becomes the other one.
140    /// `false` becoming undefined changes nothing a truthiness check reads,
141    /// so that change could never be caught.
142    Flip,
143    /// A TypeScript `throw`: it throws a bare `new Error()` instead. Deleting
144    /// it seldom compiles: a throw usually guards the narrowing the next line
145    /// relies on (`if ('error' in result) throw ...; return result.result`
146    /// is TS2339 without it). Keeping the throw keeps the types, and asks
147    /// whether any test checks which error comes out.
148    BareError,
149}
150
151/// The languages whose statements and tests this module can read.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Language {
154    JavaScript,
155    Python,
156    Ruby,
157    Go,
158    Rust,
159    /// Java and Kotlin, told apart by file extension.
160    Jvm,
161}
162
163impl Language {
164    /// Whether a request shows, of the code the test ran, the function each
165    /// asked statement is in and nothing else, and leaves the test's helper
166    /// files out. Measured for these two on eight projects against the
167    /// outcome of applying each change (2026-10-13): 28 to 81% of the tokens,
168    /// and 451 wrong verdicts where the fuller request gave 457. The other
169    /// languages have not been measured and are asked as before.
170    fn asks_within_the_function(self) -> bool {
171        matches!(self, Language::JavaScript | Language::Python)
172    }
173
174    /// The run manifest's language name, when it is one of these.
175    pub fn from_manifest(name: &str) -> Option<Self> {
176        match name {
177            "javascript" => Some(Self::JavaScript),
178            "python" => Some(Self::Python),
179            "ruby" => Some(Self::Ruby),
180            "go" => Some(Self::Go),
181            "rust" => Some(Self::Rust),
182            "jvm" => Some(Self::Jvm),
183            _ => None,
184        }
185    }
186    fn comment(self, line: &str) -> bool {
187        let t = line.trim_start();
188        match self {
189            Self::Python | Self::Ruby => t.starts_with('#'),
190            Self::JavaScript | Self::Go | Self::Rust | Self::Jvm => {
191                t.starts_with("//")
192                    || t.starts_with("/*")
193                    || t.starts_with("*/")
194                    || t == "*"
195                    || t.starts_with("* ")
196                    || t.starts_with("*\t")
197            }
198        }
199    }
200    /// The value a removed value becomes, as the questions name it.
201    fn nothing(self) -> &'static str {
202        match self {
203            Self::JavaScript => "undefined",
204            Self::Python => "None",
205            Self::Ruby => "nil",
206            Self::Go => "the zero value",
207            Self::Rust => "`Default::default()`",
208            Self::Jvm => "the default value (null, or 0 or false for a primitive)",
209        }
210    }
211}
212
213/// What separates a test's name from the groups it sits in.
214fn separator(language: Language, runner: &str) -> &'static str {
215    match (language, runner) {
216        (Language::JavaScript, _) => " > ",
217        (Language::Python, _) => "::",
218        (_, "rspec") => ":",
219        (_, "minitest" | "test-unit" | "junit-platform" | "testng") => "#",
220        (_, "go-test") => "/",
221        (_, "rustdoc") => " - ",
222        _ => "::",
223    }
224}
225
226#[derive(Debug, Clone)]
227pub struct Statement {
228    pub file: String,
229    pub line: usize,
230    pub text: String,
231    pub change: Change,
232    /// The value that replaces the statement's where the language chooses one
233    /// by type (Rust), rather than a stand-in for nothing.
234    pub replacement: Option<String>,
235    /// Indexes into `Population::tests`: the passing tests that executed it.
236    pub tests: Vec<usize>,
237}
238
239#[derive(Debug, Clone)]
240pub struct TestRef {
241    pub id: String,
242    pub file: String,
243    pub name: String,
244    pub runner: String,
245}
246
247pub struct Population {
248    pub root: PathBuf,
249    pub language: Language,
250    pub statements: Vec<Statement>,
251    pub tests: Vec<TestRef>,
252    /// Per test: the statement lines it ran, by file.
253    ran: Vec<BTreeMap<String, BTreeSet<usize>>>,
254    /// Files holding assessed statements; a test's own helpers are the files
255    /// it imports that are not among these.
256    source_files: BTreeSet<String>,
257    /// The project's Java and Kotlin files, for JVM tests and their imports.
258    jvm_files: Vec<String>,
259    /// Files read once: every request and every cache key reads the same text.
260    files: std::sync::Mutex<BTreeMap<String, Option<std::sync::Arc<str>>>>,
261    /// Whether a test file contains a text fragment, by (file, fragment).
262    quotes: std::sync::Mutex<std::collections::HashMap<(String, String), bool>>,
263}
264
265/// The executed statements of a run and the tests that ran them. `sources` are
266/// the run's source files, verified against the run.
267/// What assessing a run reads from its analysis: the passing view's tests,
268/// and its measured statements and executed decisions with the tests that ran
269/// them. Publication stores it, so an assessment does not analyse the run's
270/// evidence again (4.4 s of supergateway's 7.6 s of planning).
271#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
272pub struct AssessmentInput {
273    pub tests: Vec<InputTest>,
274    pub points: Vec<InputPoint>,
275    pub decisions: Vec<InputPoint>,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
279pub struct InputTest {
280    pub id: String,
281    pub file: Option<String>,
282    pub name: String,
283    pub role: String,
284    pub runner: String,
285}
286
287/// A statement or decision, with the tests that ran it as indexes into
288/// `AssessmentInput::tests`.
289#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
290pub struct InputPoint {
291    pub file: String,
292    pub line: usize,
293    pub source: String,
294    pub tests: Vec<u32>,
295}
296
297pub fn assessment_input(report: &CoverageReport) -> AssessmentInput {
298    let view = &report.filters.passed;
299    let tests = view
300        .tests
301        .iter()
302        .map(|test| InputTest {
303            id: test.id.clone(),
304            file: test.file.clone(),
305            name: test.name.clone(),
306            role: test.role.clone(),
307            runner: test.provenance.runner.clone(),
308        })
309        .collect::<Vec<_>>();
310    let index = tests
311        .iter()
312        .enumerate()
313        .map(|(i, test)| (test.id.as_str(), i as u32))
314        .collect::<BTreeMap<_, _>>();
315    let indexes = |ids: &[crate::interned::Id]| {
316        ids.iter()
317            .filter_map(|id| index.get(id.as_str()).copied())
318            .collect::<Vec<_>>()
319    };
320    let points = view
321        .points
322        .iter()
323        .filter(|p| p.measured && p.meta.kind == PointKind::Statement)
324        .map(|p| InputPoint {
325            file: p.meta.file.clone(),
326            line: p.meta.line,
327            source: p.meta.source.clone(),
328            tests: indexes(&p.tests),
329        })
330        .collect();
331    let decisions = view
332        .decisions
333        .iter()
334        .filter(|d| d.executed)
335        .map(|d| InputPoint {
336            file: d.meta.file.clone(),
337            line: d.meta.line,
338            source: d.meta.source.clone(),
339            tests: indexes(&d.tests),
340        })
341        .collect();
342    AssessmentInput {
343        tests,
344        points,
345        decisions,
346    }
347}
348
349pub fn population(
350    root: &Path,
351    report: &CoverageReport,
352    sources: &BTreeMap<String, String>,
353    language: Language,
354) -> Result<Population, String> {
355    population_from(root, &assessment_input(report), sources, language)
356}
357
358pub fn population_from(
359    root: &Path,
360    input: &AssessmentInput,
361    sources: &BTreeMap<String, String>,
362    language: Language,
363) -> Result<Population, String> {
364    let mut tests = Vec::new();
365    // JVM tests are named by class; their file is found by its path.
366    let jvm_files = if language == Language::Jvm {
367        project_files(root, &["java", "kt"])
368    } else {
369        Vec::new()
370    };
371    // Input test index -> population test index.
372    let mut index = BTreeMap::<u32, usize>::new();
373    for (position, test) in input.tests.iter().enumerate() {
374        let file = match test.file.as_ref().filter(|f| !f.is_empty()) {
375            Some(file) => file.clone(),
376            None if language == Language::Jvm => {
377                let found = jvm::class_paths(&test.name)
378                    .iter()
379                    .find_map(|suffix| jvm_files.iter().find(|f| ends_with_path(f, suffix)));
380                match found {
381                    Some(file) => file.clone(),
382                    None => continue,
383                }
384            }
385            None => continue,
386        };
387        if test.role != "test" {
388            continue;
389        }
390        index.insert(position as u32, tests.len());
391        tests.push(TestRef {
392            id: test.id.clone(),
393            file,
394            name: test.name.clone(),
395            runner: test.runner.clone(),
396        });
397    }
398    let mut changes = BTreeMap::<String, BTreeMap<usize, Vec<(String, Option<Change>)>>>::new();
399    let mut replacements = BTreeMap::<String, rust_source::Replacements>::new();
400    let signatures = if language == Language::Rust {
401        rust_source::signatures(
402            sources
403                .iter()
404                .filter(|(file, _)| file.ends_with(".rs"))
405                .map(|(file, source)| (file.as_str(), source.as_str())),
406        )
407    } else {
408        rust_source::Signatures::default()
409    };
410    let mut statements = Vec::new();
411    let mut seen = BTreeSet::new();
412    // Python and Ruby record an `elif`/`elsif`, and Go, Rust and the JVM
413    // languages may record an `else if`, as a decision only: its condition is
414    // taken after every statement, where no statement starts on its line.
415    let executed = input
416        .points
417        .iter()
418        .map(|p| (&p.file, p.line, &p.source, &p.tests, false));
419    let elifs = input
420        .decisions
421        .iter()
422        .filter(|_| language != Language::JavaScript)
423        .map(|d| (&d.file, d.line, &d.source, &d.tests, true));
424    for (file, line, text, point_tests, decision) in executed.chain(elifs) {
425        let key = (file.clone(), line);
426        if !seen.insert(key) {
427            continue; // the first statement on a line, as the audit reads it
428        }
429        let ids = point_tests
430            .iter()
431            .filter_map(|position| index.get(position).copied())
432            .collect::<Vec<_>>();
433        if ids.is_empty() {
434            continue;
435        }
436        if language == Language::Rust && rust_source::test_path(file) {
437            continue;
438        }
439        let Some(source) = sources.get(file) else {
440            continue;
441        };
442        let lines = changes
443            .entry(file.clone())
444            .or_insert_with(|| match language {
445                Language::JavaScript => statement_starts(file, source),
446                Language::Python => python_statement_starts(source),
447                Language::Ruby => ruby::statement_starts(source),
448                Language::Go => go::statement_starts(source),
449                Language::Rust => rust_source::statement_starts(source),
450                Language::Jvm => jvm::statement_starts(file, source),
451            });
452        let Some(change) = change_at(lines, line, text, language) else {
453            continue;
454        };
455        if decision && change != Change::Invert {
456            continue;
457        }
458        let valued = matches!(change, Change::ReturnUndefined | Change::ValueUndefined);
459        let replacement = match language {
460            Language::Rust if valued => {
461                let first = prefix(text.lines().next().unwrap_or("").trim(), 30);
462                let first = first.trim_end_matches(';');
463                replacements
464                    .entry(file.clone())
465                    .or_insert_with(|| rust_source::replacements(file, source, &signatures))
466                    .get(&line)
467                    .and_then(|all| all.iter().find(|(t, _)| t.starts_with(first)))
468                    .map(|(_, r)| r.clone())
469            }
470            // Go: the line the change turns it into, zero values written out
471            // from the result types. (Writing the JVM's `null` out the same
472            // way measured no better on commons-cli and moved the share away
473            // from the truth, so the JVM keeps its description.)
474            Language::Go if valued => go::rewrite(source, line, change),
475            _ => None,
476        };
477        // A replacement that writes the statement out as it already is --
478        // `return 0, nil` with zero values, `count := 0` -- is no change.
479        if replacement
480            .as_deref()
481            .is_some_and(|written| same_code(written, text))
482        {
483            continue;
484        }
485        statements.push(Statement {
486            file: file.clone(),
487            line,
488            text: text.clone(),
489            change,
490            replacement,
491            tests: ids,
492        });
493    }
494    let mut ran = vec![BTreeMap::<String, BTreeSet<usize>>::new(); tests.len()];
495    for s in &statements {
496        for &t in &s.tests {
497            ran[t].entry(s.file.clone()).or_default().insert(s.line);
498        }
499    }
500    let source_files = statements.iter().map(|s| s.file.clone()).collect();
501    Ok(Population {
502        root: root.to_path_buf(),
503        language,
504        statements,
505        tests,
506        ran,
507        source_files,
508        jvm_files,
509        files: Default::default(),
510        quotes: Default::default(),
511    })
512}
513
514/// Whether two lines of code are the same but for spacing and a final `;`.
515fn same_code(a: &str, b: &str) -> bool {
516    let squash = |code: &str| {
517        code.trim()
518            .trim_end_matches(';')
519            .chars()
520            .filter(|c| !c.is_whitespace())
521            .collect::<String>()
522    };
523    squash(a) == squash(b)
524}
525
526/// The words of a name or a line of code: runs of letters and digits split at
527/// camelCase and at anything else, lowercased, short and filler words left out.
528fn name_words(text: &str) -> BTreeSet<String> {
529    const FILLER: [&str; 13] = [
530        "test", "tests", "the", "and", "for", "with", "get", "set", "self", "from", "not", "does",
531        "should",
532    ];
533    let mut words = BTreeSet::new();
534    let chars = text.chars().collect::<Vec<_>>();
535    let mut word = String::new();
536    let mut flush = |word: &mut String| {
537        let w = word.to_lowercase();
538        if w.len() > 2 && !FILLER.contains(&w.as_str()) {
539            words.insert(w);
540        }
541        word.clear();
542    };
543    for (i, &c) in chars.iter().enumerate() {
544        if !c.is_ascii_alphanumeric() {
545            flush(&mut word);
546            continue;
547        }
548        let previous = i.checked_sub(1).map(|p| chars[p]);
549        let next = chars.get(i + 1).copied();
550        // `getHTTPResponse`: a word starts at a capital after a lowercase
551        // letter, and at the last capital of a run before a lowercase one.
552        let starts = c.is_ascii_uppercase()
553            && (previous.is_some_and(|p| p.is_ascii_lowercase() || p.is_ascii_digit())
554                || (previous.is_some_and(|p| p.is_ascii_uppercase())
555                    && next.is_some_and(|n| n.is_ascii_lowercase())));
556        if starts {
557            flush(&mut word);
558        }
559        word.push(c);
560    }
561    flush(&mut word);
562    words
563}
564
565/// Every statement starting on each line, outermost first, with its text and
566/// the change the audit applies to it (`None`: not assessed).
567fn statement_starts(file: &str, source: &str) -> BTreeMap<usize, Vec<(String, Option<Change>)>> {
568    let allocator = Allocator::default();
569    let source_type = SourceType::from_path(file).unwrap_or_else(|_| SourceType::tsx());
570    let parsed = Parser::new(&allocator, source, source_type).parse();
571    let starts = line_starts(source);
572    struct Collector<'s> {
573        source: &'s str,
574        starts: &'s [usize],
575        typescript: bool,
576        out: BTreeMap<usize, Vec<(String, Option<Change>)>>,
577    }
578    impl<'a> Visit<'a> for Collector<'_> {
579        fn visit_statement(&mut self, statement: &JsStatement<'a>) {
580            let span = statement.span();
581            let line = line_of(self.starts, span.start as usize);
582            let text = self
583                .source
584                .get(span.start as usize..span.end as usize)
585                .unwrap_or("")
586                .to_owned();
587            self.out.entry(line).or_default().push((
588                text,
589                match statement {
590                    JsStatement::ThrowStatement(_) if self.typescript => Some(Change::BareError),
591                    _ => change_of(statement),
592                },
593            ));
594            walk::walk_statement(self, statement);
595        }
596    }
597    let mut collector = Collector {
598        source,
599        starts: &starts,
600        typescript: source_type.is_typescript(),
601        out: BTreeMap::new(),
602    };
603    collector.visit_program(&parsed.program);
604    collector.out
605}
606
607/// `undefined` or `void 0`: returning undefined instead is the same code.
608/// What TypeScript wraps a value in to give it a type (`undefined as T`)
609/// leaves it the same value.
610fn is_undefined(expression: &Expression) -> bool {
611    match expression {
612        Expression::Identifier(identifier) => identifier.name == "undefined",
613        Expression::UnaryExpression(unary) => {
614            unary.operator == oxc_syntax::operator::UnaryOperator::Void
615        }
616        Expression::TSAsExpression(cast) => is_undefined(&cast.expression),
617        Expression::TSSatisfiesExpression(cast) => is_undefined(&cast.expression),
618        Expression::TSTypeAssertion(cast) => is_undefined(&cast.expression),
619        Expression::TSNonNullExpression(cast) => is_undefined(&cast.expression),
620        Expression::ParenthesizedExpression(inner) => is_undefined(&inner.expression),
621        _ => false,
622    }
623}
624
625fn change_of(statement: &JsStatement) -> Option<Change> {
626    Some(match statement {
627        JsStatement::IfStatement(_) => Change::Invert,
628        // `return undefined` returning undefined is the same code; skipping
629        // the return is the change that can show.
630        JsStatement::ReturnStatement(r) => match &r.argument {
631            Some(argument) if is_undefined(argument) => Change::Skip,
632            Some(Expression::BooleanLiteral(_)) => Change::Flip,
633            Some(_) => Change::ReturnUndefined,
634            None => Change::Skip,
635        },
636        JsStatement::VariableDeclaration(d) => {
637            if let [only] = d.declarations.as_slice()
638                && matches!(only.init, Some(Expression::BooleanLiteral(_)))
639            {
640                Change::Flip
641            } else if d.declarations.iter().any(|d| d.init.is_some()) {
642                Change::ValueUndefined
643            } else {
644                return None;
645            }
646        }
647        // A constructor cannot skip `super(...)`: the result does not compile
648        // (TS2377), and in JavaScript it throws before anything is observed.
649        JsStatement::ExpressionStatement(e)
650            if matches!(&e.expression, Expression::CallExpression(call)
651                if matches!(call.callee, Expression::Super(_))) =>
652        {
653            return None;
654        }
655        JsStatement::ExportNamedDeclaration(e) => match &e.declaration {
656            Some(Declaration::VariableDeclaration(d))
657                if d.declarations.iter().any(|d| d.init.is_some()) =>
658            {
659                Change::ValueUndefined
660            }
661            _ => return None,
662        },
663        JsStatement::ExportDefaultDeclaration(e) => match &e.declaration {
664            ExportDefaultDeclarationKind::FunctionDeclaration(_)
665            | ExportDefaultDeclarationKind::ClassDeclaration(_)
666            | ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => return None,
667            _ => Change::Skip,
668        },
669        JsStatement::ImportDeclaration(_)
670        | JsStatement::ExportAllDeclaration(_)
671        | JsStatement::FunctionDeclaration(_)
672        | JsStatement::ClassDeclaration(_)
673        | JsStatement::TSTypeAliasDeclaration(_)
674        | JsStatement::TSInterfaceDeclaration(_)
675        | JsStatement::TSEnumDeclaration(_)
676        | JsStatement::TSModuleDeclaration(_)
677        | JsStatement::TSGlobalDeclaration(_)
678        | JsStatement::TSImportEqualsDeclaration(_)
679        | JsStatement::TSExportAssignment(_)
680        | JsStatement::TSNamespaceExportDeclaration(_) => return None,
681        _ => Change::Skip,
682    })
683}
684
685/// The change for the measured statement on `line` whose text is `text`: the
686/// first statement starting there with that text, or an expression point when
687/// no statement starts there (a JSX expression is measured but not a statement).
688fn change_at(
689    starts: &BTreeMap<usize, Vec<(String, Option<Change>)>>,
690    line: usize,
691    text: &str,
692    language: Language,
693) -> Option<Change> {
694    let first = prefix(text.lines().next().unwrap_or("").trim(), 30);
695    let found = starts.get(&line).and_then(|all| {
696        all.iter().find(|(t, _)| {
697            let bare = t
698                .strip_prefix("export ")
699                .map(|t| t.strip_prefix("default ").unwrap_or(t))
700                .unwrap_or(t);
701            bare.starts_with(first) || t.starts_with(first)
702        })
703    });
704    match (found, language) {
705        (Some((_, change)), _) => *change,
706        (None, Language::JavaScript) => Some(Change::ExpressionUndefined),
707        // Elsewhere every statement is its own point; one no statement
708        // matches is not assessed rather than guessed at.
709        (None, _) => None,
710    }
711}
712
713/// Every Python statement starting on each line, outermost first, with its
714/// text and change: `if` inverted, `return x` returning None, an assignment's
715/// value becoming None, anything else skipped; imports, definitions, `pass`,
716/// `global`/`nonlocal` and bare strings (docstrings) are not assessed.
717fn python_statement_starts(source: &str) -> BTreeMap<usize, Vec<(String, Option<Change>)>> {
718    use ruff_python_ast::{
719        Expr, Stmt,
720        visitor::{Visitor, walk_stmt},
721    };
722    use ruff_text_size::Ranged;
723    struct Collector<'s> {
724        source: &'s str,
725        starts: Vec<usize>,
726        out: BTreeMap<usize, Vec<(String, Option<Change>)>>,
727    }
728    impl<'a> Visitor<'a> for Collector<'_> {
729        fn visit_stmt(&mut self, stmt: &'a Stmt) {
730            let change = match stmt {
731                Stmt::If(_) => Some(Change::Invert),
732                Stmt::Return(r) => Some(match r.value.as_deref() {
733                    // `return None` returning None is the same code.
734                    Some(Expr::NoneLiteral(_)) | None => Change::Skip,
735                    Some(Expr::BooleanLiteral(_)) => Change::Flip,
736                    Some(_) => Change::ReturnUndefined,
737                }),
738                Stmt::Assign(a)
739                    if a.targets.len() == 1
740                        && matches!(a.value.as_ref(), Expr::BooleanLiteral(_)) =>
741                {
742                    Some(Change::Flip)
743                }
744                Stmt::Assign(_) => Some(Change::ValueUndefined),
745                Stmt::AnnAssign(a)
746                    if matches!(a.value.as_deref(), Some(Expr::BooleanLiteral(_))) =>
747                {
748                    Some(Change::Flip)
749                }
750                Stmt::AnnAssign(a) => a.value.as_ref().map(|_| Change::ValueUndefined),
751                Stmt::Import(_)
752                | Stmt::ImportFrom(_)
753                | Stmt::FunctionDef(_)
754                | Stmt::ClassDef(_)
755                | Stmt::Pass(_)
756                | Stmt::Global(_)
757                | Stmt::Nonlocal(_)
758                | Stmt::TypeAlias(_)
759                | Stmt::IpyEscapeCommand(_) => None,
760                Stmt::Expr(e) if matches!(e.value.as_ref(), Expr::StringLiteral(_)) => None,
761                _ => Some(Change::Skip),
762            };
763            let range = stmt.range();
764            let (start, end) = (range.start().to_usize(), range.end().to_usize());
765            let line = line_of(&self.starts, start);
766            let text = self.source.get(start..end).unwrap_or("").to_owned();
767            self.out.entry(line).or_default().push((text, change));
768            // An `elif` is an `if` of its own, as JavaScript's `else if` is.
769            if let Stmt::If(s) = stmt {
770                // Its point is its condition, as coverage records it.
771                for test in s.elif_else_clauses.iter().filter_map(|c| c.test.as_ref()) {
772                    let start = test.range().start().to_usize();
773                    let text = self.source.get(start..test.range().end().to_usize());
774                    self.out
775                        .entry(line_of(&self.starts, start))
776                        .or_default()
777                        .push((text.unwrap_or("").to_owned(), Some(Change::Invert)));
778                }
779            }
780            walk_stmt(self, stmt);
781        }
782    }
783    let Ok(parsed) = ruff_python_parser::parse_module(source) else {
784        return BTreeMap::new();
785    };
786    let mut collector = Collector {
787        source,
788        starts: line_starts(source),
789        out: BTreeMap::new(),
790    };
791    for stmt in &parsed.syntax().body {
792        collector.visit_stmt(stmt);
793    }
794    collector.out
795}
796
797/// The line a JavaScript test is declared on. Each group in its name
798/// (`cart > totals`) narrows the search to that group's block, since groups
799/// often hold tests of the same title; a title written as a string is
800/// preferred over the same words anywhere else.
801fn js_test_line(lines: &[&str], name: &str) -> Option<usize> {
802    let parts = name.split(" > ").collect::<Vec<_>>();
803    let (title, groups) = parts.split_last()?;
804    let quoted = |line: &str, text: &str| {
805        ['\'', '"', '`']
806            .iter()
807            .any(|quote| line.contains(&format!("{quote}{text}")))
808    };
809    let (mut from, mut to) = (0, lines.len());
810    for group in groups {
811        let needle = prefix(group, 40);
812        if needle.is_empty() {
813            continue;
814        }
815        if let Some(at) = (from..to).find(|&i| quoted(lines[i], needle)) {
816            (from, to) = (at, (block_end(lines, at) + 1).min(lines.len()));
817        }
818    }
819    let needle = prefix(title, 40);
820    if needle.is_empty() {
821        return None;
822    }
823    (from..to)
824        .find(|&i| quoted(lines[i], needle))
825        .or_else(|| (from..to).find(|&i| lines[i].contains(needle)))
826        .or_else(|| (0..lines.len()).find(|&i| lines[i].contains(needle)))
827}
828
829fn prefix(text: &str, chars: usize) -> &str {
830    match text.char_indices().nth(chars) {
831        Some((i, _)) => &text[..i],
832        None => text,
833    }
834}
835
836/// The lines of each function that lies in no other, with the statement that
837/// declares it (`export const seal = (key) => {`) and its decorators. A
838/// callback written inside a function belongs to it. First and last line,
839/// from one.
840fn outermost_functions(language: Language, file: &str, source: &str) -> Vec<(usize, usize)> {
841    let starts = line_starts(source);
842    let lines = |from: usize, to: usize| {
843        (
844            line_of(&starts, from),
845            line_of(&starts, to.saturating_sub(1).max(from)),
846        )
847    };
848    let mut spans = match language {
849        Language::JavaScript => {
850            let allocator = Allocator::default();
851            let source_type = SourceType::from_path(file).unwrap_or_else(|_| SourceType::tsx());
852            let parsed = Parser::new(&allocator, source, source_type).parse();
853            #[derive(Default)]
854            struct Functions {
855                // The statement, or the member of a class, a function is
856                // written in: what stands directly in the file, a block or a
857                // class body.
858                within: Option<(u32, u32)>,
859                found: Vec<(u32, u32)>,
860            }
861            impl Functions {
862                fn inside(&mut self, span: (u32, u32), walk: impl FnOnce(&mut Self)) {
863                    let outer = self.within.replace(span);
864                    walk(self);
865                    self.within = outer;
866                }
867                fn function(&mut self, own: oxc_span::Span) {
868                    self.found.push(self.within.unwrap_or((own.start, own.end)));
869                }
870            }
871            fn from_decorators(
872                span: oxc_span::Span,
873                decorators: &[oxc_ast::ast::Decorator<'_>],
874            ) -> (u32, u32) {
875                (
876                    decorators
877                        .first()
878                        .map_or(span.start, |first| first.span.start.min(span.start)),
879                    span.end,
880                )
881            }
882            impl<'a> Visit<'a> for Functions {
883                fn visit_program(&mut self, program: &oxc_ast::ast::Program<'a>) {
884                    for statement in &program.body {
885                        let span = statement.span();
886                        self.inside((span.start, span.end), |this| {
887                            this.visit_statement(statement)
888                        });
889                    }
890                }
891                fn visit_block_statement(&mut self, block: &oxc_ast::ast::BlockStatement<'a>) {
892                    for statement in &block.body {
893                        let span = statement.span();
894                        self.inside((span.start, span.end), |this| {
895                            this.visit_statement(statement)
896                        });
897                    }
898                }
899                fn visit_ts_module_block(&mut self, block: &oxc_ast::ast::TSModuleBlock<'a>) {
900                    for statement in &block.body {
901                        let span = statement.span();
902                        self.inside((span.start, span.end), |this| {
903                            this.visit_statement(statement)
904                        });
905                    }
906                }
907                fn visit_method_definition(&mut self, it: &oxc_ast::ast::MethodDefinition<'a>) {
908                    self.inside(from_decorators(it.span, &it.decorators), |this| {
909                        walk::walk_method_definition(this, it)
910                    });
911                }
912                fn visit_property_definition(&mut self, it: &oxc_ast::ast::PropertyDefinition<'a>) {
913                    self.inside(from_decorators(it.span, &it.decorators), |this| {
914                        walk::walk_property_definition(this, it)
915                    });
916                }
917                fn visit_accessor_property(&mut self, it: &oxc_ast::ast::AccessorProperty<'a>) {
918                    self.inside(from_decorators(it.span, &it.decorators), |this| {
919                        walk::walk_accessor_property(this, it)
920                    });
921                }
922                fn visit_static_block(&mut self, it: &oxc_ast::ast::StaticBlock<'a>) {
923                    self.inside((it.span.start, it.span.end), |this| {
924                        walk::walk_static_block(this, it)
925                    });
926                }
927                // Not walked into: what is inside a function is the function's.
928                fn visit_function(
929                    &mut self,
930                    it: &oxc_ast::ast::Function<'a>,
931                    _flags: oxc_syntax::scope::ScopeFlags,
932                ) {
933                    if it.body.is_some() {
934                        self.function(it.span);
935                    }
936                }
937                fn visit_arrow_function_expression(
938                    &mut self,
939                    it: &oxc_ast::ast::ArrowFunctionExpression<'a>,
940                ) {
941                    self.function(it.span);
942                }
943            }
944            let mut functions = Functions::default();
945            functions.visit_program(&parsed.program);
946            functions
947                .found
948                .into_iter()
949                .map(|(from, to)| lines(from as usize, to as usize))
950                .collect::<Vec<_>>()
951        }
952        Language::Python => {
953            use ruff_python_ast::{
954                Stmt,
955                visitor::{Visitor, walk_stmt},
956            };
957            use ruff_text_size::Ranged;
958            struct Functions(Vec<(usize, usize)>);
959            impl<'a> Visitor<'a> for Functions {
960                fn visit_stmt(&mut self, stmt: &'a Stmt) {
961                    match stmt {
962                        Stmt::FunctionDef(function) => {
963                            let from = function
964                                .decorator_list
965                                .first()
966                                .map_or(function.start(), |first| {
967                                    first.start().min(function.start())
968                                });
969                            self.0.push((from.to_usize(), function.end().to_usize()));
970                        }
971                        _ => walk_stmt(self, stmt),
972                    }
973                }
974            }
975            let Ok(parsed) = ruff_python_parser::parse_module(source) else {
976                return Vec::new();
977            };
978            let mut functions = Functions(Vec::new());
979            for stmt in &parsed.syntax().body {
980                functions.visit_stmt(stmt);
981            }
982            functions
983                .0
984                .into_iter()
985                .map(|(from, to)| lines(from, to))
986                .collect()
987        }
988        _ => Vec::new(),
989    };
990    spans.sort_unstable();
991    spans.dedup();
992    spans
993}
994
995/// How many lines a deleted statement is named by, when its question names
996/// all of them ("all 4 lines of it (lines 133 to 136)").
997fn whole_statement(statement: &Statement) -> Option<usize> {
998    let lines = statement.text.lines().count();
999    let word = statement
1000        .text
1001        .lines()
1002        .next()
1003        .unwrap_or("")
1004        .trim()
1005        .trim_end_matches(';')
1006        .trim();
1007    (statement.change == Change::Skip
1008        && lines > 1
1009        && !matches!(word, "break" | "continue" | "next"))
1010    .then_some(lines)
1011}
1012
1013fn line_starts(source: &str) -> Vec<usize> {
1014    std::iter::once(0)
1015        .chain(source.match_indices('\n').map(|(i, _)| i + 1))
1016        .collect()
1017}
1018
1019fn line_of(starts: &[usize], offset: usize) -> usize {
1020    starts.partition_point(|&s| s <= offset)
1021}
1022
1023fn numbered(lines: &[&str]) -> String {
1024    lines
1025        .iter()
1026        .enumerate()
1027        .map(|(i, l)| format!("{:5} {l}", i + 1))
1028        .collect::<Vec<_>>()
1029        .join("\n")
1030}
1031
1032/// The text a statement writes out: the fixed parts of its string literals,
1033/// six characters or longer with a letter in them (`- Headers:` from
1034/// `` `  - Headers: ${describe(headers)}` ``). A test that quotes one is the
1035/// likeliest to assert what the statement produces.
1036fn quoted_fragments(text: &str) -> Vec<String> {
1037    let mut out = Vec::new();
1038    let mut chars = text.char_indices().peekable();
1039    while let Some((start, quote)) = chars.next() {
1040        if !matches!(quote, '\'' | '"' | '`') {
1041            continue;
1042        }
1043        let mut end = None;
1044        while let Some((at, c)) = chars.next() {
1045            if c == '\\' {
1046                chars.next();
1047            } else if c == quote {
1048                end = Some(at);
1049                break;
1050            }
1051        }
1052        let Some(end) = end else { break };
1053        let body = &text[start + quote.len_utf8()..end];
1054        // The fixed text only: what sits inside `${...}`, `#{...}` or an
1055        // f-string's `{...}` is an expression, not output.
1056        let mut pieces = vec![String::new()];
1057        let mut depth = 0usize;
1058        for c in body.chars() {
1059            match c {
1060                '{' => {
1061                    depth += 1;
1062                    pieces.push(String::new());
1063                }
1064                '}' if depth > 0 => depth -= 1,
1065                '$' | '#' | '%' if depth == 0 => pieces.push(String::new()),
1066                _ if depth == 0 => pieces.last_mut().expect("one piece").push(c),
1067                _ => {}
1068            }
1069        }
1070        for piece in pieces {
1071            let piece = piece.trim();
1072            if piece.len() >= 6 && piece.chars().filter(|c| c.is_alphabetic()).count() >= 3 {
1073                out.push(piece.to_owned());
1074            }
1075        }
1076    }
1077    out.sort();
1078    out.dedup();
1079    out
1080}
1081
1082fn stem(path: &str) -> String {
1083    Path::new(path)
1084        .file_name()
1085        .and_then(|n| n.to_str())
1086        .unwrap_or("")
1087        .split('.')
1088        .next()
1089        .unwrap_or("")
1090        .to_lowercase()
1091}
1092
1093fn describe(name: &str, separator: &str) -> String {
1094    let parts = name.split(separator).collect::<Vec<_>>();
1095    parts[..parts.len().saturating_sub(1)].join(separator)
1096}
1097
1098impl Population {
1099    /// Read these files from here rather than the checkout: a run's sources
1100    /// as they were assessed, once the checkout has moved on.
1101    pub fn with_files(self, files: BTreeMap<String, String>) -> Self {
1102        {
1103            let mut cache = self
1104                .files
1105                .lock()
1106                .expect("the file cache is only read and filled");
1107            for (file, text) in files {
1108                cache.insert(file, Some(text.into()));
1109            }
1110        }
1111        self
1112    }
1113
1114    /// Every file read so far, as it was read.
1115    pub fn files_read(&self) -> BTreeMap<String, String> {
1116        self.files
1117            .lock()
1118            .expect("the file cache is only read and filled")
1119            .iter()
1120            .filter_map(|(file, text)| text.as_ref().map(|t| (file.clone(), t.to_string())))
1121            .collect()
1122    }
1123
1124    fn read(&self, file: &str) -> Option<std::sync::Arc<str>> {
1125        let mut files = self
1126            .files
1127            .lock()
1128            .expect("the file cache is only read and filled");
1129        files
1130            .entry(file.to_owned())
1131            .or_insert_with(|| {
1132                std::fs::read_to_string(self.root.join(file))
1133                    .ok()
1134                    .map(Into::into)
1135            })
1136            .clone()
1137    }
1138
1139    /// Whether `test`'s file contains one of these fragments.
1140    fn quotes(&self, test: usize, fragments: &[String]) -> bool {
1141        let file = &self.tests[test].file;
1142        fragments.iter().any(|fragment| {
1143            let key = (file.clone(), fragment.clone());
1144            if let Some(&known) = self
1145                .quotes
1146                .lock()
1147                .expect("the quote cache is only read and filled")
1148                .get(&key)
1149            {
1150                return known;
1151            }
1152            let found = self
1153                .read(file)
1154                .is_some_and(|text| text.contains(fragment.as_str()));
1155            self.quotes
1156                .lock()
1157                .expect("the quote cache is only read and filled")
1158                .insert(key, found);
1159            found
1160        })
1161    }
1162
1163    /// The tests to ask about a statement, in order: tests whose file quotes
1164    /// text the statement produces first, one per file; then tests in a file
1165    /// named like the source; then one per describe block, round-robin over
1166    /// files. Without the first rule, the alphabetically first files filled
1167    /// the sample: on supergateway 15 of 179 tests were asked about a log
1168    /// line, none of them the test that compares the whole startup log.
1169    pub fn order(&self, statement: usize, k: usize) -> Vec<usize> {
1170        let s = &self.statements[statement];
1171        let base = stem(&s.file);
1172        let key = |&t: &usize| {
1173            let test = &self.tests[t];
1174            (
1175                !Path::new(&test.file)
1176                    .file_name()
1177                    .and_then(|n| n.to_str())
1178                    .unwrap_or("")
1179                    .to_lowercase()
1180                    .contains(&base),
1181                test.file.clone(),
1182                test.name.clone(),
1183            )
1184        };
1185        let mut all = s.tests.clone();
1186        all.sort_by_key(key);
1187        if all.len() <= k {
1188            return all;
1189        }
1190        let mut groups = Vec::<((String, String), Vec<usize>)>::new();
1191        for &t in &all {
1192            let g = (
1193                self.tests[t].file.clone(),
1194                describe(
1195                    &self.tests[t].name,
1196                    separator(self.language, &self.tests[t].runner),
1197                ),
1198            );
1199            match groups.iter_mut().find(|(k, _)| *k == g) {
1200                Some((_, v)) => v.push(t),
1201                None => groups.push((g, vec![t])),
1202            }
1203        }
1204        let mut by_file = Vec::<(String, Vec<Vec<usize>>)>::new();
1205        for ((file, _), members) in groups {
1206            match by_file.iter_mut().find(|(f, _)| *f == file) {
1207                Some((_, v)) => v.push(members),
1208                None => by_file.push((file, vec![members])),
1209            }
1210        }
1211        by_file.sort_by_key(|(f, _)| {
1212            (
1213                !Path::new(f)
1214                    .file_name()
1215                    .and_then(|n| n.to_str())
1216                    .unwrap_or("")
1217                    .to_lowercase()
1218                    .contains(&base),
1219                f.clone(),
1220            )
1221        });
1222        let mut picked = Vec::new();
1223        let fragments = quoted_fragments(&s.text);
1224        if !fragments.is_empty() {
1225            let quoting = all
1226                .iter()
1227                .copied()
1228                .filter(|&t| self.quotes(t, &fragments))
1229                .collect::<Vec<_>>();
1230            let mut files = BTreeSet::new();
1231            let (first, rest): (Vec<usize>, Vec<usize>) = quoting
1232                .iter()
1233                .partition(|&&t| files.insert(self.tests[t].file.clone()));
1234            for t in first.into_iter().chain(rest) {
1235                if picked.len() < k {
1236                    picked.push(t);
1237                }
1238            }
1239        }
1240        let mut depth = 0;
1241        while picked.len() < k && by_file.iter().any(|(_, g)| g.len() > depth) {
1242            for (_, g) in &by_file {
1243                if depth < g.len() && picked.len() < k && !picked.contains(&g[depth][0]) {
1244                    picked.push(g[depth][0]);
1245                }
1246            }
1247            depth += 1;
1248        }
1249        for t in all {
1250            if picked.len() >= k {
1251                break;
1252            }
1253            if !picked.contains(&t) {
1254                picked.push(t);
1255            }
1256        }
1257        picked
1258    }
1259
1260    /// The tests to ask last, when none of `asked` was judged to fail: of the
1261    /// rest, those whose name shares the most words with the statement's
1262    /// line, and among those the tests that ran the least code.
1263    ///
1264    /// A statement hundreds of tests run is mostly run in passing, by
1265    /// end-to-end tests on their way somewhere else. The tests written for it
1266    /// tend to say so in their name (`corsOrigin: a false value` for
1267    /// `return corsOrigin(...)`) and to run little besides it. Asking a few
1268    /// more in the usual order found few of them; replacing some of the usual
1269    /// tests with these lost statements the usual tests catch.
1270    pub fn last_order(&self, statement: usize, asked: &[usize], k: usize) -> Vec<usize> {
1271        let spread = self.order(statement, MORE_TESTS);
1272        let position = |t: &usize| spread.iter().position(|x| x == t).unwrap_or(usize::MAX);
1273        let s = &self.statements[statement];
1274        let line = name_words(s.text.lines().next().unwrap_or(""));
1275        let shared = |t: usize| {
1276            let name = &self.tests[t].name;
1277            let name = name.rsplit("::").next().unwrap_or(name);
1278            name_words(name).intersection(&line).count()
1279        };
1280        let mut rest = s
1281            .tests
1282            .iter()
1283            .copied()
1284            .filter(|t| !asked.contains(t))
1285            .collect::<Vec<_>>();
1286        rest.sort_by_cached_key(|t| {
1287            (
1288                std::cmp::Reverse(shared(*t)),
1289                self.breadth(*t),
1290                position(t),
1291                *t,
1292            )
1293        });
1294        rest.truncate(k);
1295        rest
1296    }
1297
1298    /// How many statement lines a test ran.
1299    fn breadth(&self, test: usize) -> usize {
1300        self.ran[test].values().map(BTreeSet::len).sum()
1301    }
1302
1303    /// The test's code: the file's first lines and the test's own body, found
1304    /// by its title, comment lines left out.
1305    pub fn test_code(&self, test: usize) -> String {
1306        match self.language {
1307            Language::Python => return self.python_test_code(test),
1308            Language::JavaScript => {}
1309            _ => return self.located_test_code(test),
1310        }
1311        let t = &self.tests[test];
1312        let text = self.read(&t.file).unwrap_or_default();
1313        let lines = text.split('\n').collect::<Vec<_>>();
1314        let start = js_test_line(&lines, &t.name);
1315        let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1316        let head = (0..lines.len().min(HEAD_LINES))
1317            .filter(|&i| !self.language.comment(lines[i]))
1318            .map(fmt)
1319            .collect::<Vec<_>>();
1320        let Some(start) = start else {
1321            let rest = (30.min(lines.len())..230.min(lines.len())).map(fmt);
1322            return head
1323                .into_iter()
1324                .chain(std::iter::once("  ...".to_owned()))
1325                .chain(rest)
1326                .collect::<Vec<_>>()
1327                .join("\n");
1328        };
1329        let indent = lines[start].len() - lines[start].trim_start().len();
1330        let mut body = Vec::new();
1331        for (i, l) in lines
1332            .iter()
1333            .copied()
1334            .enumerate()
1335            .take(start + BODY_LINES)
1336            .skip(start)
1337        {
1338            if i > start && is_test_call(l) && l.len() - l.trim_start().len() <= indent {
1339                break;
1340            }
1341            if !self.language.comment(l) {
1342                body.push(fmt(i));
1343            }
1344        }
1345        let mut out = head;
1346        if start > HEAD_LINES {
1347            out.push("  ...".into());
1348        }
1349        out.extend(body);
1350        out.join("\n")
1351    }
1352
1353    /// A Ruby, Go, Rust or JVM test's code: the file's first lines, the lines
1354    /// that frame it (its class or groups, its annotations), and its body.
1355    fn located_test_code(&self, test: usize) -> String {
1356        let t = &self.tests[test];
1357        let text = self.read(&t.file).unwrap_or_default();
1358        let lines = text.split('\n').collect::<Vec<_>>();
1359        let located = match self.language {
1360            Language::Ruby => ruby::locate(&text, &lines, &t.runner, &t.name),
1361            Language::Go => go::locate(&lines, &t.name),
1362            Language::Rust => rust_source::locate(&lines, &t.name),
1363            _ => jvm::locate(&lines, &t.name),
1364        };
1365        let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1366        // Lines inside `/* ... */` (a license header, say) are comments too.
1367        let mut open = false;
1368        let commented = lines
1369            .iter()
1370            .map(|l| {
1371                let t = l.trim();
1372                let was = open;
1373                if self.language != Language::Ruby {
1374                    if !open && t.starts_with("/*") {
1375                        open = true;
1376                    }
1377                    if open && t.contains("*/") {
1378                        open = false;
1379                        return true;
1380                    }
1381                }
1382                was || open || self.language.comment(l)
1383            })
1384            .collect::<Vec<_>>();
1385        // The head: the file's first code lines (its package and imports).
1386        let head = (0..lines.len())
1387            .filter(|&i| !commented[i])
1388            .take(HEAD_LINES)
1389            .collect::<Vec<_>>();
1390        let head_end = head.last().map_or(0, |&i| i + 1);
1391        let mut out = head.iter().map(|&i| fmt(i)).collect::<Vec<_>>();
1392        let Some(located) = located else {
1393            out.push("  ...".into());
1394            out.extend((head_end..(head_end + 200).min(lines.len())).map(fmt));
1395            return out.join("\n");
1396        };
1397        let end = located
1398            .end
1399            .min(located.start + BODY_LINES)
1400            .min(lines.len().saturating_sub(1));
1401        let mut last = head_end;
1402        let mut shown = located.context.iter().copied().collect::<BTreeSet<_>>();
1403        shown.extend(located.start..=end);
1404        // What the test uses from its own file: a table it loops over, a
1405        // helper it calls, a `let` it reads.
1406        shown.extend(used_definitions(&lines, self.language, located.start, end));
1407        for i in shown {
1408            // A doctest lives in comments: its lines are the test.
1409            if i < last || (commented[i] && self.language != Language::Rust) {
1410                continue;
1411            }
1412            if i > last {
1413                out.push("  ...".into());
1414            }
1415            out.push(fmt(i));
1416            last = i + 1;
1417        }
1418        out.join("\n")
1419    }
1420
1421    /// A Python test's code: the file's first lines, the class holding the
1422    /// test, its decorators, and its body, found by the test function's name
1423    /// and ended by indentation.
1424    fn python_test_code(&self, test: usize) -> String {
1425        let t = &self.tests[test];
1426        let text = self.read(&t.file).unwrap_or_default();
1427        let lines = text.split('\n').collect::<Vec<_>>();
1428        let title = python_title(&t.name);
1429        let indent_of = |l: &str| l.len() - l.trim_start().len();
1430        // Test classes in one file often share method names, so the method
1431        // is looked for inside the class its id names, then anywhere.
1432        let (from, to, body) = python_class_lines(&lines, &python_classes(&t.name));
1433        let defines = |i: &usize| {
1434            let t = lines[*i].trim_start();
1435            t.starts_with(&format!("def {title}(")) || t.starts_with(&format!("async def {title}("))
1436        };
1437        // In the class's own body, not a class nested in it.
1438        let own = |i: &usize| defines(i) && body.is_none_or(|body| indent_of(lines[*i]) == body);
1439        let start = (!title.is_empty())
1440            .then(|| {
1441                (from..to)
1442                    .find(own)
1443                    .or_else(|| (0..lines.len()).find(defines))
1444            })
1445            .flatten();
1446        let fmt = |i: usize| format!("{:5} {}", i + 1, lines[i]);
1447        let mut out = (0..lines.len().min(HEAD_LINES))
1448            .filter(|&i| !self.language.comment(lines[i]))
1449            .map(fmt)
1450            .collect::<Vec<_>>();
1451        let Some(start) = start else {
1452            out.push("  ...".into());
1453            out.extend((HEAD_LINES.min(lines.len())..(HEAD_LINES + 200).min(lines.len())).map(fmt));
1454            return out.join("\n");
1455        };
1456        let indent = indent_of(lines[start]);
1457        let mut first = start;
1458        while first > 0
1459            && lines[first - 1].trim_start().starts_with('@')
1460            && indent_of(lines[first - 1]) == indent
1461        {
1462            first -= 1;
1463        }
1464        let class = (indent > 0)
1465            .then(|| {
1466                (0..first).rev().find(|&i| {
1467                    lines[i].trim_start().starts_with("class ") && indent_of(lines[i]) < indent
1468                })
1469            })
1470            .flatten();
1471        // `...` where lines are left out between the ones shown.
1472        let mut last = HEAD_LINES.min(lines.len()).checked_sub(1);
1473        let mut show = |out: &mut Vec<String>, i: usize| {
1474            if i >= HEAD_LINES {
1475                if last.is_some_and(|l| i > l + 1) {
1476                    out.push("  ...".into());
1477                }
1478                out.push(fmt(i));
1479                last = Some(i);
1480            }
1481        };
1482        for i in class.into_iter().chain(first..=start) {
1483            show(&mut out, i);
1484        }
1485        for (i, &l) in lines
1486            .iter()
1487            .enumerate()
1488            .skip(start + 1)
1489            .take(BODY_LINES - 1)
1490        {
1491            if !l.trim().is_empty() && indent_of(l) <= indent {
1492                break;
1493            }
1494            // A comment in the body is left out without a mark.
1495            if !self.language.comment(l) && i >= HEAD_LINES {
1496                out.push(fmt(i));
1497            }
1498        }
1499        out.join("\n")
1500    }
1501
1502    /// Test-side files the test imports (helpers, fixtures), cut to a budget
1503    /// that grows with the number of questions. None for JavaScript,
1504    /// TypeScript and Python (see `asks_within_the_function`): on
1505    /// supergateway they were a sixth of what was sent and changed 4 verdicts
1506    /// in 101, two each way.
1507    pub fn helpers(&self, test: usize, questions: usize) -> BTreeMap<String, String> {
1508        if self.language.asks_within_the_function() {
1509            return BTreeMap::new();
1510        }
1511        let cap = HELPER_CHARS.min(1500 + 400 * questions);
1512        let t = &self.tests[test];
1513        let text = self.read(&t.file).unwrap_or_default();
1514        let mut out = BTreeMap::new();
1515        let mut size = 0;
1516        let files = match self.language {
1517            Language::JavaScript | Language::Python => Vec::new(),
1518            Language::Ruby => ruby::helpers(&self.root, &t.file, &text),
1519            Language::Go => go::helpers(&self.root, &t.file, &text),
1520            Language::Rust => rust_source::helpers(&self.root, &t.file, &text),
1521            Language::Jvm => jvm::imports(&text)
1522                .iter()
1523                .filter_map(|import| {
1524                    jvm::class_paths(&import.replace('/', "."))
1525                        .iter()
1526                        .find_map(|suffix| {
1527                            self.jvm_files.iter().find(|f| ends_with_path(f, suffix))
1528                        })
1529                        .cloned()
1530                })
1531                .collect(),
1532        };
1533        for file in files {
1534            if out.contains_key(&file) {
1535                continue;
1536            }
1537            if self.source_files.contains(&file) || file.contains("node_modules") {
1538                continue;
1539            }
1540            let mut body = self.read(&file).map(|b| b.to_string()).unwrap_or_default();
1541            if size + body.len() > cap {
1542                let keep = cap.saturating_sub(size);
1543                let cut = body
1544                    .char_indices()
1545                    .map(|(i, _)| i)
1546                    .take_while(|&i| i <= keep)
1547                    .last()
1548                    .unwrap_or(0);
1549                body = format!("{}\n... (cut)", &body[..cut]);
1550            }
1551            size += body.len();
1552            out.insert(file, numbered(&body.split('\n').collect::<Vec<_>>()));
1553            if size >= cap {
1554                break;
1555            }
1556        }
1557        if self.language == Language::Ruby {
1558            let mut used = 0;
1559            for file in ruby::support_files(&self.root, &text) {
1560                if out.contains_key(&file) || used >= FIXTURE_CHARS {
1561                    continue;
1562                }
1563                let mut body = self.read(&file).map(|b| b.to_string()).unwrap_or_default();
1564                if used + body.len() > FIXTURE_CHARS {
1565                    let keep = FIXTURE_CHARS - used;
1566                    let cut = body
1567                        .char_indices()
1568                        .map(|(i, _)| i)
1569                        .take_while(|&i| i <= keep)
1570                        .last()
1571                        .unwrap_or(0);
1572                    body = format!("{}\n... (cut)", &body[..cut]);
1573                }
1574                used += body.len();
1575                out.insert(file, numbered(&body.split('\n').collect::<Vec<_>>()));
1576            }
1577        }
1578        out
1579    }
1580
1581    /// The source this test ran: every statement it executed marked ▶, four
1582    /// code lines around each asked statement, comment lines left out, within
1583    /// a budget that grows with the number of questions. Two lines around
1584    /// every executed statement and eight around each asked one, in a budget
1585    /// four times this, cost 28% more over 3,532 checked statements in 14
1586    /// projects and were no more accurate.
1587    ///
1588    /// In JavaScript, TypeScript and Python that is cut to the function each
1589    /// asked statement is in (see `asks_within_the_function`): what the test
1590    /// ran elsewhere in the file, and in other files, is left out.
1591    pub fn code_run(&self, test: usize, asked: &[usize]) -> BTreeMap<String, String> {
1592        let budget = CODE_CHARS.min(1000 + 400 * asked.len());
1593        let within = self.language.asks_within_the_function();
1594        let ran = &self.ran[test];
1595        let mut want = BTreeMap::<String, BTreeSet<usize>>::new();
1596        for &a in asked {
1597            want.entry(self.statements[a].file.clone())
1598                .or_default()
1599                .insert(self.statements[a].line);
1600        }
1601        let mut files = want.keys().cloned().collect::<Vec<_>>();
1602        let mut others = ran
1603            .iter()
1604            .filter(|(f, _)| !within && !want.contains_key(*f))
1605            .collect::<Vec<_>>();
1606        others.sort_by_key(|(f, lines)| (std::cmp::Reverse(lines.len()), (*f).clone()));
1607        files.extend(others.into_iter().map(|(f, _)| f.clone()));
1608        let empty = BTreeSet::new();
1609        let mut out = BTreeMap::new();
1610        let mut total = 0;
1611        for f in files {
1612            let Some(source) = self.read(&f) else {
1613                continue;
1614            };
1615            let text = source.split('\n').collect::<Vec<_>>();
1616            let r = ran.get(&f).unwrap_or(&empty);
1617            let code = (1..=text.len())
1618                .filter(|&i| !self.language.comment(text[i - 1]))
1619                .collect::<Vec<_>>();
1620            let pos = code
1621                .iter()
1622                .enumerate()
1623                .map(|(n, &i)| (i, n))
1624                .collect::<BTreeMap<_, _>>();
1625            let around = |l: usize, w: usize| -> Vec<usize> {
1626                match pos.get(&l) {
1627                    Some(&n) => code[n.saturating_sub(w)..(n + w + 1).min(code.len())].to_vec(),
1628                    None => (l.saturating_sub(w).max(1)..=(l + w).min(text.len())).collect(),
1629                }
1630            };
1631            let mut show = BTreeSet::new();
1632            for &l in r {
1633                show.extend(around(l, CONTEXT_LINES));
1634            }
1635            for &l in want.get(&f).unwrap_or(&empty) {
1636                show.extend(around(l, WINDOW_LINES));
1637            }
1638            // How far the file was going to be shown, for the line that says
1639            // how much of it is not.
1640            let end = show
1641                .iter()
1642                .rev()
1643                .copied()
1644                .find(|&i| i != 0 && i <= text.len() && !self.language.comment(text[i - 1]))
1645                .unwrap_or(0);
1646            if within {
1647                let functions = outermost_functions(self.language, &f, &source);
1648                let around =
1649                    |l: usize| (l.saturating_sub(OUTSIDE_A_FUNCTION), l + OUTSIDE_A_FUNCTION);
1650                let function = |l: usize| {
1651                    functions
1652                        .iter()
1653                        .copied()
1654                        .find(|&(from, to)| from <= l && l <= to)
1655                };
1656                // The lines an assessed statement of this file begins on.
1657                let begins = self
1658                    .statements
1659                    .iter()
1660                    .filter(|s| s.file == f)
1661                    .map(|s| s.line)
1662                    .collect::<BTreeSet<_>>();
1663                let mut kept = Vec::new();
1664                for statement in asked.iter().map(|&a| &self.statements[a]) {
1665                    if statement.file != f {
1666                        continue;
1667                    }
1668                    kept.push(function(statement.line).unwrap_or(around(statement.line)));
1669                    // A statement its question names by all its lines also
1670                    // keeps what lies near each line it goes on to, as the
1671                    // requests this was measured with did.
1672                    for l in (statement.line + 1)
1673                        ..statement.line + whole_statement(statement).unwrap_or(1)
1674                    {
1675                        kept.push(
1676                            begins
1677                                .contains(&l)
1678                                .then(|| function(l))
1679                                .flatten()
1680                                .unwrap_or(around(l)),
1681                        );
1682                    }
1683                }
1684                show.retain(|i| kept.iter().any(|&(from, to)| from <= *i && *i <= to));
1685            }
1686            let mut lines = Vec::new();
1687            let mut last = 0;
1688            for i in show {
1689                if i == 0 || i > text.len() || self.language.comment(text[i - 1]) {
1690                    continue;
1691                }
1692                if i != last + 1 {
1693                    lines.push(format!("        ... {} lines not shown", i - last - 1));
1694                }
1695                lines.push(format!(
1696                    "{}{:5} {}",
1697                    if r.contains(&i) { '▶' } else { ' ' },
1698                    i,
1699                    text[i - 1]
1700                ));
1701                last = i;
1702            }
1703            if within && last != 0 && end > last {
1704                lines.push(format!("        ... {} lines not shown", end - last));
1705            }
1706            let body = lines.join("\n");
1707            if !want.contains_key(&f) && total + body.len() > budget {
1708                continue;
1709            }
1710            total += body.len();
1711            out.insert(f, body);
1712        }
1713        out
1714    }
1715
1716    /// The questions for one statement: one per change (two for an `if`).
1717    pub fn questions(&self, statement: usize) -> Vec<String> {
1718        let s = &self.statements[statement];
1719        let line = s.text.lines().next().unwrap_or("").trim();
1720        let changes = match (s.change, &s.replacement) {
1721            (Change::ReturnUndefined | Change::ValueUndefined, Some(r))
1722                if self.language == Language::Rust =>
1723            {
1724                vec![rust_question(line, s.change, r)]
1725            }
1726            // The value is replaced after it is computed, as Go's mutation
1727            // testers do.
1728            (Change::ReturnUndefined, Some(r)) if self.language == Language::Go => {
1729                vec![format!(
1730                    "`{line}` becomes `{r}` (the original expression is still evaluated first)"
1731                )]
1732            }
1733            (Change::ValueUndefined, Some(r)) if self.language == Language::Go => {
1734                vec![format!(
1735                    "`{line}` becomes `{r}` (the original value is still computed first)"
1736                )]
1737            }
1738            _ => self.default_questions(s, line),
1739        };
1740        changes
1741            .into_iter()
1742            .map(|c| format!("{}:{}: {c}.", s.file, s.line))
1743            .collect()
1744    }
1745
1746    fn default_questions(&self, s: &Statement, line: &str) -> Vec<String> {
1747        match s.change {
1748            // Go and the JVM languages replace a value after it is computed,
1749            // as their mutation testers do: the expression still runs.
1750            Change::ReturnUndefined if self.language == Language::Go => vec![format!(
1751                "`{line}` returns the zero value of each result instead: nil for an error, pointer, slice, map or interface, 0, \"\" or false, or an empty struct (its expressions still run)"
1752            )],
1753            // A `when`, `switch` or `try` ending a function returns from its
1754            // branches.
1755            Change::ReturnUndefined
1756                if self.language == Language::Jvm
1757                    && s.text.contains("return")
1758                    && ["when", "switch", "try"].iter().any(|k| {
1759                        line.strip_prefix(k)
1760                            .is_some_and(|rest| rest.starts_with([' ', '(', '{']))
1761                    }) =>
1762            {
1763                vec![format!(
1764                    "every value `{line}` returns becomes {} instead (the returned expressions still run)",
1765                    self.language.nothing()
1766                )]
1767            }
1768            Change::ReturnUndefined if self.language == Language::Jvm => vec![format!(
1769                "`{line}` returns {} instead (its expression still runs)",
1770                self.language.nothing()
1771            )],
1772            Change::ReturnUndefined if matches!(self.language, Language::Ruby | Language::Rust) => {
1773                let nothing = match self.language {
1774                    Language::Rust
1775                        if ["Ok(", "Err(", "return Ok(", "return Err("]
1776                            .iter()
1777                            .any(|p| line.starts_with(p)) =>
1778                    {
1779                        "`Ok(Default::default())`"
1780                    }
1781                    other => other.nothing(),
1782                };
1783                vec![if line.starts_with("return") {
1784                    format!("`{line}` returns {nothing} instead (the expression is not evaluated)")
1785                } else {
1786                    // Often a call made for what it does, not what it gives
1787                    // back: saying only "the value becomes nil" hides that.
1788                    format!(
1789                        "`{line}` becomes {nothing}: it is not evaluated, so nothing it does happens (no call it makes, no value it stores), and the enclosing block or method produces {nothing}"
1790                    )
1791                }]
1792            }
1793            Change::ValueUndefined if self.language == Language::Go => vec![format!(
1794                "`{line}`: after it runs, the variables it assigns hold the zero value of their type instead"
1795            )],
1796            Change::ValueUndefined if self.language == Language::Jvm => vec![format!(
1797                "`{line}`: after it runs, the variable it assigns holds {} instead",
1798                self.language.nothing()
1799            )],
1800            Change::ValueUndefined if matches!(self.language, Language::Ruby | Language::Rust) => {
1801                let nothing = self.language.nothing();
1802                vec![match split_initializer(line) {
1803                    Some(head) => format!(
1804                        "`{line}`: the value after `{head}` becomes {nothing} (it is not evaluated)"
1805                    ),
1806                    None => format!("`{line}`: the assigned value becomes {nothing}"),
1807                }]
1808            }
1809            Change::ReturnUndefined if self.language == Language::Python => vec![format!(
1810                "`{line}` becomes `return None` (the expression is not evaluated)"
1811            )],
1812            Change::ReturnUndefined => vec![format!(
1813                "`{line}` becomes `return undefined;` (the expression is not evaluated)"
1814            )],
1815            Change::ValueUndefined if self.language == Language::Python => {
1816                vec![match split_initializer(line) {
1817                    Some(head) => {
1818                        format!("`{line}` becomes `{head} None` (the value is not evaluated)")
1819                    }
1820                    None => format!("`{line}`: the assigned value becomes None"),
1821                }]
1822            }
1823            Change::ValueUndefined => vec![match split_initializer(line) {
1824                Some(head) => format!(
1825                    "`{line}` becomes `{head} undefined` (the initializer is not evaluated)"
1826                ),
1827                None => format!("`{line}`: the declared value becomes undefined"),
1828            }],
1829            Change::Skip => {
1830                let word = line.trim_end_matches(';').trim();
1831                vec![match word {
1832                    "break" => format!(
1833                        "`{line}` is deleted: execution does not leave the enclosing loop or switch here, and carries on with what follows it"
1834                    ),
1835                    "continue" | "next" => format!(
1836                        "`{line}` is deleted: the rest of the loop body runs in this iteration"
1837                    ),
1838                    // A statement of several lines is shown by its first.
1839                    // Read as "`for x in items:` is deleted", only the header
1840                    // seemed to go: 22 of 100 such verdicts were wrong on six
1841                    // projects, 9 with all of it named.
1842                    _ if whole_statement(s).is_some() => {
1843                        let lines = s.text.lines().count();
1844                        format!(
1845                            "the whole statement starting `{line}` is deleted, all {lines} lines of it (lines {} to {}): none of it runs",
1846                            s.line,
1847                            s.line + lines - 1
1848                        )
1849                    }
1850                    // Python: what deleting a return or a raise lets happen.
1851                    // Of the skipped returns and raises of five projects, 61
1852                    // of 371 verdicts were wrong as "is deleted" and 44 so.
1853                    // (TypeScript returns were judged worse this way, 21 of
1854                    // 82 against 18, and keep the plain wording.)
1855                    _ if self.language == Language::Python
1856                        && (word == "return" || word.starts_with("return ")) =>
1857                    {
1858                        format!(
1859                            "`{line}` is deleted: the function does not return here, and execution carries on with whatever follows it"
1860                        )
1861                    }
1862                    _ if self.language == Language::Python
1863                        && (word == "raise" || word.starts_with("raise ")) =>
1864                    {
1865                        format!(
1866                            "`{line}` is deleted: no error is raised here, and execution carries on with whatever follows it"
1867                        )
1868                    }
1869                    _ => format!("`{line}` is deleted (it never runs)"),
1870                }]
1871            }
1872            Change::ExpressionUndefined => vec![format!(
1873                "the expression `{line}` evaluates to undefined instead (it is not evaluated)"
1874            )],
1875            Change::Flip => vec![format!("`{line}` becomes `{}`", flipped(line))],
1876            Change::BareError => vec![format!(
1877                "`{line}` throws `new Error()` instead: execution still stops here, but the error has no message and is not its own type"
1878            )],
1879            Change::Invert => {
1880                let c = match self.language {
1881                    Language::Python => python_condition(line),
1882                    Language::JavaScript => condition(line),
1883                    Language::Ruby => ruby::condition(line),
1884                    Language::Go => go::condition(line),
1885                    Language::Rust => rust_source::condition(line),
1886                    Language::Jvm => jvm::condition(line),
1887                };
1888                // A pattern match (`if let`) can be made to fail, not to
1889                // succeed: its bindings would have no value.
1890                if c.starts_with("let ") {
1891                    return vec![format!(
1892                        "`{line}`: whenever the pattern `{c}` matches it is treated as not matching (the branch it would enter is skipped)"
1893                    )];
1894                }
1895                vec![
1896                    format!(
1897                        "`{line}`: whenever the condition `{c}` is true it is treated as false (the branch it would enter is skipped)"
1898                    ),
1899                    format!(
1900                        "`{line}`: whenever the condition `{c}` is false it is treated as true (the branch is entered)"
1901                    ),
1902                ]
1903            }
1904        }
1905    }
1906
1907    /// The request for one test and the statements asked about it.
1908    pub fn request(&self, test: usize, asked: &[usize], model: &str) -> Value {
1909        let t = &self.tests[test];
1910        let mut questions = serde_json::Map::new();
1911        for (n, &a) in asked.iter().enumerate() {
1912            for (j, q) in self.questions(a).into_iter().enumerate() {
1913                questions.insert(
1914                    format!("q{n}_{j}"),
1915                    json!({"type": "noul", "instructions": {"task": q}, "criteria": {"true": "The test fails.", "false": "The test still passes."}}),
1916                );
1917            }
1918        }
1919        let mut state = json!({
1920            "instructions": INSTRUCTIONS, "runtime": runtime(&t.runner),
1921            "test": {"file": t.file, "name": t.name},
1922            "test_code": self.test_code(test),
1923            "code_run": self.code_run(test, asked)});
1924        if !self.language.asks_within_the_function() {
1925            state["helpers"] = json!(self.helpers(test, asked.len()));
1926        }
1927        json!({"model": model, "state": state, "questions": questions})
1928    }
1929
1930    /// What Jev sees for one statement asked of one test, line numbers aside:
1931    /// the same code renumbered is the same question. Equal keys, equal question.
1932    /// `salt` carries what changes every answer at once: the run's dependency
1933    /// and configuration fingerprints.
1934    pub fn pair_key(&self, statement: usize, test: usize, model: &str, salt: &str) -> String {
1935        let t = &self.tests[test];
1936        let parts = json!([
1937            model,
1938            salt,
1939            INSTRUCTIONS,
1940            runtime(&t.runner),
1941            true,
1942            &t.file,
1943            &t.name,
1944            strip_numbers(&self.test_code(test)),
1945            self.helpers(test, 1)
1946                .into_iter()
1947                .map(|(f, b)| (f, strip_numbers(&b)))
1948                .collect::<BTreeMap<_, _>>(),
1949            self.code_run(test, &[statement])
1950                .into_iter()
1951                .map(|(f, b)| (f, strip_numbers(&b)))
1952                .collect::<BTreeMap<_, _>>(),
1953            self.questions(statement)
1954                .iter()
1955                .map(|q| strip_location(q))
1956                .collect::<Vec<_>>(),
1957        ]);
1958        format!(
1959            "{:x}",
1960            Sha256::digest(serde_json::to_vec(&parts).expect("serializable key"))
1961        )
1962    }
1963}
1964
1965/// The line with its boolean literal turned into the other one: the last
1966/// `true`/`false` (`True`/`False` in Python) that stands as a word.
1967fn flipped(line: &str) -> String {
1968    // A trailing comment is not code: `let ready = false // true once open`
1969    // flipped the comment's `true`.
1970    let cut = comment_start(line);
1971    let (code, comment) = line.split_at(cut);
1972    format!("{}{comment}", flipped_code(code))
1973}
1974
1975/// Where a trailing `//` or `# ` comment starts, outside string literals; the
1976/// line's length when it has none. A bare `#` is code (a private field).
1977fn comment_start(line: &str) -> usize {
1978    let bytes = line.as_bytes();
1979    let mut quote = None;
1980    let mut i = 0;
1981    while i < bytes.len() {
1982        let c = bytes[i];
1983        match quote {
1984            Some(_) if c == b'\\' => i += 1,
1985            Some(q) if c == q => quote = None,
1986            Some(_) => {}
1987            None if matches!(c, b'\'' | b'"' | b'`') => quote = Some(c),
1988            None if c == b'/' && bytes.get(i + 1) == Some(&b'/') => return i,
1989            None if c == b'#' && bytes.get(i + 1).is_none_or(|n| *n == b' ') => return i,
1990            None => {}
1991        }
1992        i += 1;
1993    }
1994    line.len()
1995}
1996
1997fn flipped_code(line: &str) -> String {
1998    let words = [
1999        ("true", "false"),
2000        ("false", "true"),
2001        ("True", "False"),
2002        ("False", "True"),
2003    ];
2004    let found = words
2005        .iter()
2006        .filter_map(|(from, to)| {
2007            line.match_indices(from)
2008                .filter(|(at, _)| {
2009                    let before = line[..*at].chars().next_back();
2010                    let after = line[at + from.len()..].chars().next();
2011                    let word = |c: Option<char>| c.is_some_and(|c| c.is_alphanumeric() || c == '_');
2012                    !word(before) && !word(after)
2013                })
2014                .last()
2015                .map(|(at, _)| (at, *from, *to))
2016        })
2017        .max_by_key(|(at, _, _)| *at);
2018    match found {
2019        Some((at, from, to)) => format!("{}{to}{}", &line[..at], &line[at + from.len()..]),
2020        None => line.to_owned(),
2021    }
2022}
2023
2024/// A Rust return or assignment's change, as the line it becomes.
2025fn rust_question(line: &str, change: Change, replacement: &str) -> String {
2026    let repl = replacement.split_whitespace().collect::<Vec<_>>().join(" ");
2027    match change {
2028        Change::ReturnUndefined if line.starts_with("return") => {
2029            format!("`{line}` becomes `return {repl};`")
2030        }
2031        Change::ReturnUndefined => {
2032            format!("`{line}` becomes `{repl}` (it is the value the enclosing block produces)")
2033        }
2034        _ => match split_initializer(line) {
2035            Some(head) => format!("`{line}` becomes `{head} {repl};`"),
2036            None => format!("`{line}`: the value it assigns becomes `{repl}`"),
2037        },
2038    }
2039}
2040
2041/// Write files' text, compressed, to `path`.
2042pub fn save_snapshot(path: &Path, files: &BTreeMap<String, String>) -> Result<(), String> {
2043    use std::io::Write;
2044    let bytes = serde_json::to_vec(files).map_err(|e| e.to_string())?;
2045    let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2046    encoder.write_all(&bytes).map_err(|e| e.to_string())?;
2047    let compressed = encoder.finish().map_err(|e| e.to_string())?;
2048    let temp = path.with_extension("tmp");
2049    std::fs::write(&temp, compressed).map_err(|e| e.to_string())?;
2050    std::fs::rename(&temp, path).map_err(|e| e.to_string())
2051}
2052
2053/// Read files written by [`save_snapshot`].
2054pub fn load_snapshot(path: &Path) -> Option<BTreeMap<String, String>> {
2055    use std::io::Read;
2056    let compressed = std::fs::read(path).ok()?;
2057    let mut text = Vec::new();
2058    flate2::read::GzDecoder::new(compressed.as_slice())
2059        .read_to_end(&mut text)
2060        .ok()?;
2061    serde_json::from_slice(&text).ok()
2062}
2063
2064/// Where a test sits in its file: its first line, its last, and lines that
2065/// frame it (its class, groups or annotations), all zero-based.
2066struct Located {
2067    context: Vec<usize>,
2068    start: usize,
2069    end: usize,
2070}
2071
2072fn indent(line: &str) -> usize {
2073    line.len() - line.trim_start().len()
2074}
2075
2076/// The line closing the braces opened from `start` on (strings, characters
2077/// and line comments aside).
2078fn brace_end(lines: &[&str], start: usize) -> usize {
2079    brace_end_within(lines, start, BODY_LINES)
2080}
2081
2082/// Where the block opened at `start` closes, however long it is: the region
2083/// a class, module or group holds, which is searched rather than shown.
2084fn block_end(lines: &[&str], start: usize) -> usize {
2085    brace_end_within(lines, start, lines.len())
2086}
2087
2088fn brace_end_within(lines: &[&str], start: usize, limit: usize) -> usize {
2089    let mut depth = 0i64;
2090    let mut opened = false;
2091    for (i, line) in lines.iter().enumerate().skip(start).take(limit) {
2092        let mut chars = line.chars().peekable();
2093        let mut quote: Option<char> = None;
2094        while let Some(c) = chars.next() {
2095            match (quote, c) {
2096                (Some(_), '\\') => {
2097                    chars.next();
2098                }
2099                (Some(q), c) if c == q => quote = None,
2100                (Some(_), _) => {}
2101                (None, '"' | '`') => quote = Some(c),
2102                (None, '/') if chars.peek() == Some(&'/') => break,
2103                (None, '{') => {
2104                    depth += 1;
2105                    opened = true;
2106                }
2107                (None, '}') => depth -= 1,
2108                _ => {}
2109            }
2110        }
2111        if opened && depth <= 0 {
2112            return i;
2113        }
2114    }
2115    start
2116        .saturating_add(limit)
2117        .min(lines.len())
2118        .saturating_sub(1)
2119}
2120
2121/// The `end` (or `}`) closing a Ruby block or method opened on `start`.
2122fn keyword_end(lines: &[&str], start: usize) -> usize {
2123    let first = lines[start].trim_end();
2124    if first.ends_with(" end") || first.ends_with('}') {
2125        return start;
2126    }
2127    let depth = indent(lines[start]);
2128    (start + 1..lines.len().min(start + BODY_LINES))
2129        .find(|&i| {
2130            let t = lines[i].trim_start();
2131            indent(lines[i]) == depth && (t == "end" || t.starts_with("end ") || t.starts_with('}'))
2132        })
2133        .unwrap_or((start + BODY_LINES).min(lines.len()).saturating_sub(1))
2134}
2135
2136const DEFINITION_LINES: usize = 120;
2137
2138/// The lines of the test file's own definitions that the test at
2139/// `start..=end` names, and those they name in turn, up to a budget.
2140fn used_definitions(lines: &[&str], language: Language, start: usize, end: usize) -> Vec<usize> {
2141    let words = |range: std::ops::RangeInclusive<usize>| {
2142        let mut out = BTreeSet::new();
2143        for i in range {
2144            let line = lines.get(i).copied().unwrap_or("");
2145            out.extend(
2146                line.split(|c: char| !(c.is_alphanumeric() || c == '_'))
2147                    .filter(|w| !w.is_empty())
2148                    .map(str::to_owned),
2149            );
2150        }
2151        out
2152    };
2153    let definitions = definitions(lines, language);
2154    let mut wanted = words(start..=end);
2155    let mut picked = BTreeSet::new();
2156    let mut budget = DEFINITION_LINES;
2157    for _ in 0..2 {
2158        let mut next = BTreeSet::new();
2159        for (name, a, b) in &definitions {
2160            let (a, b) = (*a, *b);
2161            if picked.contains(&a) || (a >= start && a <= end) || !wanted.contains(name) {
2162                continue;
2163            }
2164            let size = b - a + 1;
2165            if size > budget {
2166                continue;
2167            }
2168            budget -= size;
2169            picked.insert(a);
2170            next.extend(words(a..=b));
2171            picked.extend(a..=b);
2172        }
2173        wanted = next;
2174    }
2175    picked.into_iter().collect()
2176}
2177
2178/// A file's named definitions (functions, methods, table variables, RSpec
2179/// `let`/`subject`) with their first and last lines.
2180fn definitions(lines: &[&str], language: Language) -> Vec<(String, usize, usize)> {
2181    let ident = |s: &str| {
2182        s.chars()
2183            .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '?' || *c == '!')
2184            .collect::<String>()
2185    };
2186    let mut out = Vec::new();
2187    for (i, line) in lines.iter().enumerate() {
2188        let t = line.trim_start();
2189        let name = match language {
2190            Language::Go => t
2191                .strip_prefix("func ")
2192                .map(|r| match r.strip_prefix('(') {
2193                    Some(r) => r.split_once(')').map_or("", |(_, r)| r.trim_start()),
2194                    None => r,
2195                })
2196                .or_else(|| t.strip_prefix("var "))
2197                .or_else(|| t.strip_prefix("type "))
2198                .filter(|_| indent(line) == 0)
2199                .map(ident),
2200            Language::Rust => {
2201                let r = t.strip_prefix("pub ").unwrap_or(t);
2202                let r = r.strip_prefix("pub(crate) ").unwrap_or(r);
2203                ["fn ", "const ", "static ", "struct ", "enum "]
2204                    .iter()
2205                    .find_map(|k| r.strip_prefix(k))
2206                    .map(ident)
2207            }
2208            Language::Ruby => t
2209                .strip_prefix("def ")
2210                .map(|r| ident(r.strip_prefix("self.").unwrap_or(r)))
2211                .or_else(|| {
2212                    ["let(:", "let!(:", "subject(:"]
2213                        .iter()
2214                        .find_map(|k| t.strip_prefix(k))
2215                        .map(ident)
2216                })
2217                .or_else(|| {
2218                    (t.starts_with("subject ") || t.starts_with("subject{"))
2219                        .then(|| "subject".into())
2220                }),
2221            Language::Jvm => {
2222                if let Some(r) = t.split_once("fun ").filter(|(head, _)| !head.contains('(')) {
2223                    Some(ident(r.1.trim_start_matches('`')))
2224                } else if t.contains('(')
2225                    && !t.ends_with(';')
2226                    && !t.starts_with(['@', '}', '.', '/', '*', '+', '"'])
2227                    && ![
2228                        "return", "new ", "if", "for", "while", "switch", "catch", "else", "try",
2229                        "throw", "assert", "super", "this",
2230                    ]
2231                    .iter()
2232                    .any(|k| t.starts_with(k))
2233                {
2234                    let head = t.split('(').next().unwrap_or("");
2235                    let mut parts = head.split_whitespace().rev();
2236                    let name = parts.next().unwrap_or("");
2237                    (parts.next().is_some() && !head.contains('=')).then(|| ident(name))
2238                } else {
2239                    None
2240                }
2241            }
2242            _ => None,
2243        };
2244        let Some(name) = name.filter(|n| !n.is_empty()) else {
2245            continue;
2246        };
2247        let end = match language {
2248            Language::Ruby => keyword_end(lines, i),
2249            _ if line.contains('{')
2250                || lines
2251                    .get(i + 1)
2252                    .is_some_and(|n| n.trim_start().starts_with('{')) =>
2253            {
2254                brace_end(lines, i)
2255            }
2256            _ => i,
2257        };
2258        out.push((name, i, end));
2259    }
2260    out
2261}
2262
2263/// Whether `path` is `suffix` or ends with `/suffix`.
2264fn ends_with_path(path: &str, suffix: &str) -> bool {
2265    path.strip_suffix(suffix)
2266        .is_some_and(|head| head.is_empty() || head.ends_with('/'))
2267}
2268
2269/// The project's files with these extensions, as project-relative paths
2270/// (ignored files, such as build output, left out).
2271fn project_files(root: &Path, extensions: &[&str]) -> Vec<String> {
2272    let mut out = ignore::WalkBuilder::new(root)
2273        .build()
2274        .filter_map(Result::ok)
2275        .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
2276        .filter(|e| {
2277            e.path()
2278                .extension()
2279                .and_then(|x| x.to_str())
2280                .is_some_and(|x| extensions.contains(&x))
2281        })
2282        .filter_map(|e| {
2283            e.path()
2284                .strip_prefix(root)
2285                .ok()
2286                .map(|p| p.to_string_lossy().replace('\\', "/"))
2287        })
2288        .filter(|p| !p.starts_with(".supercov/"))
2289        .collect::<Vec<_>>();
2290    out.sort();
2291    out
2292}
2293
2294fn is_test_call(line: &str) -> bool {
2295    let t = line.trim_start();
2296    let rest = if let Some(r) = t.strip_prefix("it") {
2297        r
2298    } else if let Some(r) = t.strip_prefix("test") {
2299        r
2300    } else {
2301        return false;
2302    };
2303    let rest = if let Some(r) = rest.strip_prefix('.') {
2304        r.trim_start_matches(|c: char| c.is_alphanumeric() || c == '_')
2305    } else {
2306        rest
2307    };
2308    rest.trim_start().starts_with('(')
2309}
2310
2311/// `const x = value` -> `const x =`; `None` when there is no plain `=`.
2312fn split_initializer(line: &str) -> Option<String> {
2313    let b = line.as_bytes();
2314    for i in 1..b.len() {
2315        if b[i] == b'='
2316            && !matches!(b[i - 1], b'=' | b'!' | b'<' | b'>')
2317            && !matches!(b.get(i + 1), Some(b'=') | Some(b'>'))
2318        {
2319            return Some(line[..=i].to_owned());
2320        }
2321    }
2322    None
2323}
2324
2325/// `if c:` -> `c`
2326/// A Python test's function name: pytest `file::Class::test[param]`,
2327/// unittest `module.Class.test`.
2328fn python_title(name: &str) -> &str {
2329    let last = name.rsplit("::").next().unwrap_or(name);
2330    let last = last.split('[').next().unwrap_or(last);
2331    last.rsplit('.').next().unwrap_or(last)
2332}
2333
2334/// The classes a Python test id names around its function: pytest's
2335/// `file::TestA::TestB::test[param]`, unittest's `module.TestA.test`.
2336fn python_classes(name: &str) -> Vec<&str> {
2337    let name = name.split('[').next().unwrap_or(name);
2338    if name.contains("::") {
2339        let parts = name.split("::").collect::<Vec<_>>();
2340        return parts[1..parts.len().saturating_sub(1)].to_vec();
2341    }
2342    let parts = name.split('.').collect::<Vec<_>>();
2343    parts
2344        .len()
2345        .checked_sub(2)
2346        .map(|at| parts[at])
2347        .filter(|class| class.starts_with(|c: char| c.is_ascii_uppercase()))
2348        .into_iter()
2349        .collect()
2350}
2351
2352/// The lines of the innermost of `classes`, each found inside the one before
2353/// and ended by indentation, with the indentation of that class's own body;
2354/// the whole file when one is not found.
2355fn python_class_lines(lines: &[&str], classes: &[&str]) -> (usize, usize, Option<usize>) {
2356    let indent_of = |l: &str| l.len() - l.trim_start().len();
2357    let (mut from, mut to, mut body) = (0, lines.len(), None);
2358    for class in classes {
2359        let declared = (from..to).find(|&i| {
2360            lines[i]
2361                .trim_start()
2362                .strip_prefix("class ")
2363                .and_then(|rest| rest.strip_prefix(class))
2364                .is_some_and(|rest| rest.starts_with(['(', ':']))
2365        });
2366        let Some(at) = declared else {
2367            return (0, lines.len(), None);
2368        };
2369        let indent = indent_of(lines[at]);
2370        let end = (at + 1..to)
2371            .find(|&i| !lines[i].trim().is_empty() && indent_of(lines[i]) <= indent)
2372            .unwrap_or(to);
2373        body = (at + 1..end)
2374            .find(|&i| !lines[i].trim().is_empty())
2375            .map(|i| indent_of(lines[i]));
2376        (from, to) = (at, end);
2377    }
2378    (from, to, body)
2379}
2380
2381fn python_condition(line: &str) -> String {
2382    let t = line.trim();
2383    let t = t
2384        .strip_prefix("elif")
2385        .or_else(|| t.strip_prefix("if"))
2386        .map(str::trim_start)
2387        .unwrap_or(t);
2388    t.strip_suffix(':')
2389        .map(str::trim_end)
2390        .unwrap_or(t)
2391        .to_owned()
2392}
2393
2394/// `if (c) {` -> `c`
2395fn condition(line: &str) -> String {
2396    let mut t = line.trim();
2397    if let Some(r) = t.strip_prefix('}') {
2398        let r = r.trim_start();
2399        if let Some(r) = r.strip_prefix("else") {
2400            t = r.trim_start();
2401        }
2402    }
2403    let Some(r) = t.strip_prefix("if") else {
2404        return line.to_owned();
2405    };
2406    let r = r.trim_start();
2407    let Some(r) = r.strip_prefix('(') else {
2408        return line.to_owned();
2409    };
2410    let r = r.trim_end();
2411    let r = r.strip_suffix('{').map(str::trim_end).unwrap_or(r);
2412    match r.strip_suffix(')') {
2413        Some(c) => c.to_owned(),
2414        None => line.to_owned(),
2415    }
2416}
2417
2418fn strip_numbers(text: &str) -> String {
2419    text.split('\n')
2420        .map(|l| {
2421            let (mark, rest) = match l.chars().next() {
2422                Some(c @ ('▶' | ' ')) => (Some(c), &l[c.len_utf8()..]),
2423                _ => (None, l),
2424            };
2425            let trimmed = rest.trim_start();
2426            let digits = trimmed.len()
2427                - trimmed
2428                    .trim_start_matches(|c: char| c.is_ascii_digit())
2429                    .len();
2430            if digits > 0 && trimmed[digits..].starts_with(' ') {
2431                format!(
2432                    "{}{}",
2433                    mark.map(String::from).unwrap_or_default(),
2434                    &trimmed[digits + 1..]
2435                )
2436            } else if let Some(n) = l
2437                .trim_start()
2438                .strip_prefix("... ")
2439                .and_then(|r| r.split_once(" lines not shown"))
2440            {
2441                let _ = n;
2442                "...".to_owned()
2443            } else {
2444                l.to_owned()
2445            }
2446        })
2447        .collect::<Vec<_>>()
2448        .join("\n")
2449}
2450
2451fn strip_location(question: &str) -> String {
2452    // `file:12: change` -> `file:: change`
2453    match question.split_once(": ") {
2454        Some((loc, rest)) => match loc.rsplit_once(':') {
2455            Some((file, line)) if line.chars().all(|c| c.is_ascii_digit()) => {
2456                format!("{file}:: {rest}")
2457            }
2458            _ => question.to_owned(),
2459        },
2460        None => question.to_owned(),
2461    }
2462}
2463
2464/// The project-relative name of `path`, `..` and `.` resolved, with `/`
2465/// between parts on every platform: a run names its files that way, and on
2466/// Windows a joined path had `\\` and named a helper no run had.
2467fn normalize(path: &Path) -> String {
2468    let mut out = Vec::new();
2469    for c in path.components() {
2470        match c {
2471            std::path::Component::ParentDir => {
2472                out.pop();
2473            }
2474            std::path::Component::CurDir => {}
2475            other => out.push(other.as_os_str().to_string_lossy().into_owned()),
2476        }
2477    }
2478    out.join("/")
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483    #[test]
2484    fn a_replacement_that_is_the_statement_is_no_change() {
2485        assert!(super::same_code("return 0, nil", "return 0,  nil"));
2486        assert!(super::same_code("count := 0", "count := 0;"));
2487        assert!(!super::same_code("return 0, nil", "return n, nil"));
2488        let source = "package p\n\nfunc f(n int) (int, error) {\n\tif n > 0 {\n\t\treturn n, nil\n\t}\n\treturn 0, nil\n}\n";
2489        assert_eq!(
2490            super::go::rewrite(source, 7, Change::ReturnUndefined).as_deref(),
2491            Some("return 0, nil")
2492        );
2493    }
2494
2495    use super::*;
2496
2497    #[test]
2498    fn a_statement_many_tests_run_is_asked_of_the_tests_that_ran_little_else() {
2499        // Seventeen end-to-end tests pass through `total` and nine other
2500        // statements; three unit tests, in the file that sorts last, run
2501        // `total` alone. One test per file in file order never reaches the
2502        // unit tests within fifteen.
2503        let source = (0..10)
2504            .map(|n| format!("const v{n} = compute({n});"))
2505            .collect::<Vec<_>>()
2506            .join("\n");
2507        let test = |file: &str, name: &str| InputTest {
2508            id: format!("{file}::{name}"),
2509            file: Some(file.to_owned()),
2510            name: name.to_owned(),
2511            role: "test".into(),
2512            runner: "node:test".into(),
2513        };
2514        let mut tests = (0..17)
2515            .map(|n| {
2516                let name = if n == 16 {
2517                    "calls compute on start"
2518                } else {
2519                    "serves a request"
2520                };
2521                test(&format!("tests/e2e{n:02}.test.js"), name)
2522            })
2523            .collect::<Vec<_>>();
2524        tests.extend((0..3).map(|n| test("tests/zz_unit.test.js", &format!("total case {n}"))));
2525        let everyone = (0..20).collect::<Vec<u32>>();
2526        let end_to_end = (0..17).collect::<Vec<u32>>();
2527        let points = (0..10)
2528            .map(|n| InputPoint {
2529                file: "src/sum.js".into(),
2530                line: n + 1,
2531                source: format!("const v{n} = compute({n});"),
2532                tests: if n == 0 {
2533                    everyone.clone()
2534                } else {
2535                    end_to_end.clone()
2536                },
2537            })
2538            .collect();
2539        let input = AssessmentInput {
2540            tests,
2541            points,
2542            decisions: Vec::new(),
2543        };
2544        let sources = BTreeMap::from([("src/sum.js".to_owned(), source)]);
2545        let population = population_from(
2546            Path::new("/nonexistent"),
2547            &input,
2548            &sources,
2549            Language::JavaScript,
2550        )
2551        .unwrap();
2552        let target = population
2553            .statements
2554            .iter()
2555            .position(|s| s.line == 1)
2556            .unwrap();
2557        let unit = |order: &[usize]| {
2558            order
2559                .iter()
2560                .filter(|&&t| population.tests[t].file == "tests/zz_unit.test.js")
2561                .count()
2562        };
2563        let spread = population.order(target, MORE_TESTS);
2564        assert_eq!(
2565            unit(&spread),
2566            0,
2567            "file order alone leaves the unit tests out"
2568        );
2569        let last = population.last_order(target, &spread, LAST_TESTS);
2570        assert_eq!(last.len(), LAST_TESTS);
2571        assert!(last.iter().all(|t| !spread.contains(t)), "{last:?}");
2572        // The test naming what the line calls comes first, though it ran as
2573        // much as any; then the tests that ran the least.
2574        assert_eq!(population.tests[last[0]].name, "calls compute on start");
2575        assert_eq!(unit(&last[1..]), 2, "{last:?}");
2576        assert_eq!(
2577            name_words("const hostCount = getHTTPResponse2(send_data)"),
2578            [
2579                "const",
2580                "count",
2581                "data",
2582                "host",
2583                "http",
2584                "response2",
2585                "send"
2586            ]
2587            .map(String::from)
2588            .into()
2589        );
2590    }
2591
2592    #[test]
2593    fn a_statement_is_shown_with_the_function_it_is_in() {
2594        // The outermost one, with the statement that declares it and what
2595        // decorates it; a callback written inside it is part of it.
2596        let typescript = "import { log } from './log'\n\nexport const seal = (key: string) => {\n  const parts = key.split('-').map((part) => {\n    return part.trim()\n  })\n  return parts\n}\n\nclass Vault {\n  size = 0\n  @logged()\n  open(door: string) {\n    return () => door\n  }\n}\n\nserver.on('close', function () {\n  log('closed')\n})\n\nif (flag) {\n  function inner() {\n    return 1\n  }\n}\nconst limit = 4\n";
2597        assert_eq!(
2598            outermost_functions(Language::JavaScript, "src/vault.ts", typescript),
2599            [(3, 8), (12, 15), (18, 20), (23, 25)]
2600        );
2601        let python = "import os\n\nLIMIT = 4\n\n@cached\ndef seal(key):\n    def trim(part):\n        return part.strip()\n    return [trim(part) for part in key.split('-')]\n\nclass Vault:\n    size = 0\n\n    async def open(self, door):\n        return lambda: door\n\nif os.name == 'nt':\n    def separator():\n        return ';'\n";
2602        assert_eq!(
2603            outermost_functions(Language::Python, "vault.py", python),
2604            [(5, 9), (14, 15), (18, 19)]
2605        );
2606        // The other languages are asked as they were.
2607        assert!(outermost_functions(Language::Go, "vault.go", "package vault\n").is_empty());
2608        assert!(Language::Python.asks_within_the_function());
2609        assert!(!Language::Ruby.asks_within_the_function());
2610    }
2611
2612    #[test]
2613    fn changes_follow_the_audit() {
2614        let source = "import x from 'y'\nexport const a = 1\nlet b: number\nfunction f() {\n  if (a) return 2\n  return;\n}\nconst el = <div>{a && b}</div>\n";
2615        let starts = statement_starts("x.tsx", source);
2616        let js = Language::JavaScript;
2617        assert_eq!(change_at(&starts, 1, "import x from 'y'", js), None);
2618        assert_eq!(
2619            change_at(&starts, 2, "const a = 1", js),
2620            Some(Change::ValueUndefined)
2621        );
2622        assert_eq!(change_at(&starts, 3, "let b: number", js), None);
2623        assert_eq!(
2624            change_at(&starts, 5, "if (a) return 2", js),
2625            Some(Change::Invert)
2626        );
2627        assert_eq!(change_at(&starts, 6, "return;", js), Some(Change::Skip));
2628        assert_eq!(
2629            change_at(&starts, 8, "a && b", js),
2630            Some(Change::ExpressionUndefined)
2631        );
2632    }
2633
2634    #[test]
2635    fn a_change_that_cannot_change_anything_is_not_asked() {
2636        // supergateway's audit: `return undefined` "returning undefined" and
2637        // `let answered = false` "becoming undefined" (read only as `!answered`)
2638        // could never be caught, and skipping `super(...)` does not compile.
2639        let source = "class H extends Map {\n  constructor() {\n    super([])\n  }\n}\nfunction f(a) {\n  if (a) return undefined\n  if (!a) return void 0\n  let answered = false\n  const done = true, more = 1\n  return false\n}\n";
2640        let starts = statement_starts("x.ts", source);
2641        let js = Language::JavaScript;
2642        assert_eq!(change_at(&starts, 3, "super([])", js), None);
2643        assert_eq!(
2644            change_at(&starts, 7, "return undefined", js),
2645            Some(Change::Skip)
2646        );
2647        assert_eq!(
2648            change_at(&starts, 8, "return void 0", js),
2649            Some(Change::Skip)
2650        );
2651        // destr: `return undefined as T` was asked as "returns undefined", which no
2652        // test can catch, and read as the one statement not asserted.
2653        let typed = "function f<T>(a: number): T {\n  if (a === 1) return undefined as T\n  if (a === 2) return (undefined as unknown) as T\n  if (a === 3) return <T>undefined\n  return undefined!\n}\n";
2654        let starts = statement_starts("x.ts", typed);
2655        for (line, text) in [
2656            (2, "return undefined as T"),
2657            (3, "return (undefined as unknown) as T"),
2658            (4, "return <T>undefined"),
2659            (5, "return undefined!"),
2660        ] {
2661            assert_eq!(
2662                change_at(&starts, line, text, js),
2663                Some(Change::Skip),
2664                "{text}"
2665            );
2666        }
2667        let starts = statement_starts("x.ts", source);
2668        assert_eq!(
2669            change_at(&starts, 9, "let answered = false", js),
2670            Some(Change::Flip)
2671        );
2672        // Two declarators: the value change still applies to both.
2673        assert_eq!(
2674            change_at(&starts, 10, "const done = true, more = 1", js),
2675            Some(Change::ValueUndefined)
2676        );
2677        assert_eq!(
2678            change_at(&starts, 11, "return false", js),
2679            Some(Change::Flip)
2680        );
2681        let python = python_statement_starts(
2682            "def f(a):\n    if a:\n        return None\n    ready = False\n    return True\n",
2683        );
2684        let py = Language::Python;
2685        assert_eq!(change_at(&python, 3, "return None", py), Some(Change::Skip));
2686        assert_eq!(
2687            change_at(&python, 4, "ready = False", py),
2688            Some(Change::Flip)
2689        );
2690        assert_eq!(change_at(&python, 5, "return True", py), Some(Change::Flip));
2691        assert_eq!(flipped("let answered = false"), "let answered = true");
2692        assert_eq!(flipped("return isTrue && false;"), "return isTrue && true;");
2693        assert_eq!(flipped("ready = False"), "ready = True");
2694        assert_eq!(
2695            flipped("let ready = false // true once open"),
2696            "let ready = true // true once open"
2697        );
2698        assert_eq!(
2699            flipped("ready = False  # True later"),
2700            "ready = True  # True later"
2701        );
2702        assert_eq!(flipped("this.#open = false"), "this.#open = true");
2703        assert_eq!(
2704            flipped("const s = 'a // b', on = false"),
2705            "const s = 'a // b', on = true"
2706        );
2707    }
2708
2709    #[test]
2710    fn a_typescript_throw_keeps_its_types() {
2711        // supergateway's modernHttp.ts:294: deleting the guard's throw made
2712        // `result.result` fail to type-check on the next line (TS2339).
2713        let source = "async function list(reply) {\n  const result = await reply\n  if ('error' in result)\n    throw new Error(`tools/list failed: ${result.error.message}`)\n  return result.result\n}\n";
2714        let typed = statement_starts("x.ts", source);
2715        let js = Language::JavaScript;
2716        assert_eq!(
2717            change_at(
2718                &typed,
2719                4,
2720                "throw new Error(`tools/list failed: ${result.error.message}`)",
2721                js
2722            ),
2723            Some(Change::BareError)
2724        );
2725        let plain = statement_starts("x.js", source);
2726        assert_eq!(
2727            change_at(
2728                &plain,
2729                4,
2730                "throw new Error(`tools/list failed: ${result.error.message}`)",
2731                js
2732            ),
2733            Some(Change::Skip)
2734        );
2735    }
2736
2737    #[test]
2738    fn a_deleted_statement_says_what_goes_and_what_then_runs() {
2739        let source = "def f(items, n):\n    if n < 0:\n        raise ValueError(\"negative\")\n    if not items:\n        return None\n    for item in items:\n        use(item)\n    log(n)\n    return n\n";
2740        let point = |line: usize, text: &str| InputPoint {
2741            file: "pkg/f.py".into(),
2742            line,
2743            source: text.into(),
2744            tests: vec![0],
2745        };
2746        let input = AssessmentInput {
2747            tests: vec![InputTest {
2748                id: "tests/test_f.py::test_f".into(),
2749                file: Some("tests/test_f.py".into()),
2750                name: "tests/test_f.py::test_f".into(),
2751                role: "test".into(),
2752                runner: "pytest".into(),
2753            }],
2754            points: vec![
2755                point(3, "raise ValueError(\"negative\")"),
2756                point(5, "return None"),
2757                point(6, "for item in items:\n        use(item)"),
2758                point(8, "log(n)"),
2759            ],
2760            decisions: Vec::new(),
2761        };
2762        let sources = BTreeMap::from([("pkg/f.py".to_owned(), source.to_owned())]);
2763        let python = population_from(
2764            Path::new("/nonexistent"),
2765            &input,
2766            &sources,
2767            Language::Python,
2768        )
2769        .unwrap();
2770        let question = |population: &Population, line: usize| {
2771            let at = population
2772                .statements
2773                .iter()
2774                .position(|s| s.line == line)
2775                .unwrap();
2776            population.questions(at).join(" | ")
2777        };
2778        assert_eq!(
2779            question(&python, 3),
2780            "pkg/f.py:3: `raise ValueError(\"negative\")` is deleted: no error is raised here, and execution carries on with whatever follows it."
2781        );
2782        assert_eq!(
2783            question(&python, 5),
2784            "pkg/f.py:5: `return None` is deleted: the function does not return here, and execution carries on with whatever follows it."
2785        );
2786        assert_eq!(
2787            question(&python, 6),
2788            "pkg/f.py:6: the whole statement starting `for item in items:` is deleted, all 2 lines of it (lines 6 to 7): none of it runs."
2789        );
2790        assert_eq!(
2791            question(&python, 8),
2792            "pkg/f.py:8: `log(n)` is deleted (it never runs)."
2793        );
2794        // A JavaScript return keeps the plain wording: it was judged worse
2795        // the other way.
2796        let js = AssessmentInput {
2797            tests: vec![InputTest {
2798                id: "t".into(),
2799                file: Some("test/g.test.js".into()),
2800                name: "g".into(),
2801                role: "test".into(),
2802                runner: "node:test".into(),
2803            }],
2804            points: vec![InputPoint {
2805                file: "src/g.js".into(),
2806                line: 3,
2807                source: "return;".into(),
2808                tests: vec![0],
2809            }],
2810            decisions: Vec::new(),
2811        };
2812        let sources = BTreeMap::from([(
2813            "src/g.js".to_owned(),
2814            "function g(a) {\n  if (a) {\n    return;\n  }\n  use(a);\n}\n".to_owned(),
2815        )]);
2816        let javascript = population_from(
2817            Path::new("/nonexistent"),
2818            &js,
2819            &sources,
2820            Language::JavaScript,
2821        )
2822        .unwrap();
2823        assert_eq!(
2824            question(&javascript, 3),
2825            "src/g.js:3: `return;` is deleted (it never runs)."
2826        );
2827    }
2828
2829    #[test]
2830    fn a_statement_s_text_is_what_a_test_would_quote() {
2831        assert_eq!(
2832            quoted_fragments("logger.info(`  - Headers: ${describeHeaders(headers)}`)"),
2833            vec!["- Headers:"]
2834        );
2835        assert_eq!(
2836            quoted_fragments("logger.error(`Child exited: code=${code}, signal=${signal}`)"),
2837            vec![", signal=", "Child exited: code="]
2838        );
2839        assert_eq!(
2840            quoted_fragments("log(f\"Serving {path} on {port}\")"),
2841            vec!["Serving"]
2842        );
2843        // Short or letterless pieces say nothing about a test.
2844        assert!(quoted_fragments("x = 'abc' + \"1234567\"").is_empty());
2845        assert!(quoted_fragments("return a + b").is_empty());
2846    }
2847
2848    /// A pytest method far into its file: the file's first lines, then the
2849    /// class around it, its decorators and its body, each once.
2850    #[test]
2851    fn a_python_test_deep_in_its_file_is_shown_with_its_class_and_decorators() {
2852        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2853        let root = std::env::temp_dir().join(format!(
2854            "supercov-python-test-code-{}-{}",
2855            std::process::id(),
2856            COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2857        ));
2858        std::fs::create_dir_all(root.join("tests")).unwrap();
2859        let mut file =
2860            String::from("# The shop's tests.\nimport pytest\n\nfrom shop import total\n");
2861        for i in 0..10 {
2862            file.push_str(&format!("\n\ndef helper_{i}():\n    return {i}\n"));
2863        }
2864        file.push_str("\n\nclass TestTotal:\n    def test_first(self):\n        assert total([]) == 0\n\n    @pytest.mark.slow\n    @pytest.mark.parametrize(\"n\", [1])\n    def test_deep(self, n):\n        # one item\n        assert total([n]) == n\n\n    def test_after(self):\n        pass\n");
2865        std::fs::write(root.join("tests/test_total.py"), &file).unwrap();
2866        let input = AssessmentInput {
2867            tests: vec![InputTest {
2868                id: "tests/test_total.py::TestTotal::test_deep[1]".into(),
2869                file: Some("tests/test_total.py".into()),
2870                name: "tests/test_total.py::TestTotal::test_deep[1]".into(),
2871                role: "test".into(),
2872                runner: "pytest".into(),
2873            }],
2874            points: vec![InputPoint {
2875                file: "shop.py".into(),
2876                line: 1,
2877                source: "return 0".into(),
2878                tests: vec![0],
2879            }],
2880            decisions: Vec::new(),
2881        };
2882        let sources = BTreeMap::from([("shop.py".to_owned(), "return 0\n".to_owned())]);
2883        let population = population_from(&root, &input, &sources, Language::Python).unwrap();
2884        let code = population.test_code(0);
2885        std::fs::remove_dir_all(&root).unwrap();
2886        let shown = code
2887            .lines()
2888            .map(|line| {
2889                let line = line.trim_start();
2890                line.split_once(' ').map_or(line, |(_, l)| l.trim_start())
2891            })
2892            .collect::<Vec<_>>();
2893        assert_eq!(
2894            shown,
2895            [
2896                "import pytest",
2897                "",
2898                "from shop import total",
2899                "",
2900                "",
2901                "def helper_0():",
2902                "return 0",
2903                "",
2904                "",
2905                "...",
2906                "class TestTotal:",
2907                "...",
2908                "@pytest.mark.slow",
2909                "@pytest.mark.parametrize(\"n\", [1])",
2910                "def test_deep(self, n):",
2911                "assert total([n]) == n",
2912                "",
2913            ],
2914            "{code}"
2915        );
2916    }
2917
2918    /// The small readers a question is built with: a statement's condition,
2919    /// the part of a line before its value, a test call, and the numbers and
2920    /// locations a cache key leaves out.
2921    #[test]
2922    fn statement_text_is_read_the_way_each_language_writes_it() {
2923        assert_eq!(condition("if (a && b) {"), "a && b");
2924        assert_eq!(condition("} else if (ready) {"), "ready");
2925        assert_eq!(condition("if (a) return;"), "if (a) return;");
2926        assert_eq!(condition("while (a) {"), "while (a) {");
2927        assert_eq!(python_condition("elif x is None:"), "x is None");
2928        assert_eq!(python_condition("if ready:"), "ready");
2929        assert_eq!(
2930            split_initializer("const x = f(1);").as_deref(),
2931            Some("const x =")
2932        );
2933        assert_eq!(
2934            split_initializer("if (a <= b && c != d) total = 1").as_deref(),
2935            Some("if (a <= b && c != d) total =")
2936        );
2937        assert_eq!(split_initializer("a === b"), None);
2938        assert_eq!(split_initializer("items.map((x) => x)"), None);
2939        assert!(is_test_call("  it('works', () => {"));
2940        assert!(is_test_call("test.only('works', () => {"));
2941        assert!(is_test_call("test ('spaced', () => {"));
2942        assert!(!is_test_call("items.push(1)"));
2943        assert!(!is_test_call("testing(1)"));
2944        assert_eq!(
2945            strip_numbers("▶   12 total(1)\n    13 next\n  ... 4 lines not shown\nplain"),
2946            "▶total(1)\n next\n...\nplain"
2947        );
2948        assert_eq!(
2949            strip_location("src/a.js:12: `x` is deleted"),
2950            "src/a.js:: `x` is deleted"
2951        );
2952        assert_eq!(strip_location("src/a.js:main: kept"), "src/a.js:main: kept");
2953        assert_eq!(strip_location("no location"), "no location");
2954    }
2955
2956    /// A helper file's named definitions, so a test is sent only the helpers
2957    /// it names: methods with their bodies (a brace on its own line too),
2958    /// Kotlin functions, RSpec `let` and `subject`, Rust items.
2959    #[test]
2960    fn helper_definitions_are_read_the_way_each_language_declares_them() {
2961        let names = |language: Language, text: &str| {
2962            let lines = text.split('\n').collect::<Vec<_>>();
2963            definitions(&lines, language)
2964                .into_iter()
2965                .map(|(name, start, end)| format!("{name} {}-{}", start + 1, end + 1))
2966                .collect::<Vec<_>>()
2967        };
2968        assert_eq!(
2969            names(
2970                Language::Jvm,
2971                "class Helpers {\n  static int total(int a) {\n    return a;\n  }\n  private static final Map<String, Integer> PRICES = Map.of(\"a\", 1);\n  public <T> List<T> wrap(T value)\n  {\n    return List.of(value);\n  }\n  void call() { helper(1); }\n}\n"
2972            ),
2973            ["total 2-4", "wrap 6-9", "call 10-10"]
2974        );
2975        assert_eq!(
2976            names(
2977                Language::Jvm,
2978                "object Fixtures {\n    private fun price(x: Int) = x * 2\n    val list = listOf(1).map { it }\n    suspend fun later() {\n    }\n}\n"
2979            ),
2980            ["price 2-2", "later 4-5"]
2981        );
2982        assert_eq!(
2983            names(
2984                Language::Ruby,
2985                "module Helpers\n  def self.make(x)\n    x\n  end\n  let(:order) { 1 }\n  let!(:user) { 2 }\n  subject(:cart) { 3 }\n  subject { 4 }\n  def valid?\n    true\n  end\nend\n"
2986            ),
2987            [
2988                "make 2-4",
2989                "order 5-5",
2990                "user 6-6",
2991                "cart 7-7",
2992                "subject 8-8",
2993                "valid? 9-11"
2994            ]
2995        );
2996        assert_eq!(
2997            names(
2998                Language::Rust,
2999                "pub fn make() -> u8 {\n    1\n}\npub(crate) const LIMIT: u8 = 2;\nstruct Point {\n    x: u8,\n}\n"
3000            ),
3001            ["make 1-3", "LIMIT 4-4", "Point 5-7"]
3002        );
3003    }
3004
3005    #[test]
3006    fn a_python_test_is_found_in_the_class_its_id_names() {
3007        let text = "import pytest\n\n\nclass TestCart:\n    def test_total(self):\n        assert cart() == 0\n\n\nclass TestOrder:\n    class TestRefund:\n        def test_total(self):\n            assert refund() == 1\n\n    def test_total(self):\n        assert order() == 2\n";
3008        let lines = text.split('\n').collect::<Vec<_>>();
3009        let at = |name: &str| {
3010            let (from, to, body) = python_class_lines(&lines, &python_classes(name));
3011            (from..to)
3012                .find(|&i| {
3013                    lines[i].trim_start().starts_with("def test_total(")
3014                        && body
3015                            .is_none_or(|body| lines[i].len() - lines[i].trim_start().len() == body)
3016                })
3017                .map(|i| i + 1)
3018        };
3019        assert_eq!(at("tests/test_a.py::TestCart::test_total"), Some(5));
3020        assert_eq!(
3021            at("tests/test_a.py::TestOrder::TestRefund::test_total[1]"),
3022            Some(11)
3023        );
3024        // The class's own method, not the one of a class nested in it.
3025        assert_eq!(at("tests/test_a.py::TestOrder::test_total"), Some(14));
3026        assert_eq!(at("tests.test_a.TestCart.test_total"), Some(5));
3027        assert_eq!(
3028            python_classes("tests/test_a.py::test_plain"),
3029            Vec::<&str>::new()
3030        );
3031        assert_eq!(
3032            python_classes("tests.test_a.test_plain"),
3033            Vec::<&str>::new()
3034        );
3035    }
3036
3037    #[test]
3038    fn a_javascript_test_is_found_in_the_group_its_name_gives() {
3039        let text = "import { totals } from '../src/cart.js';\n\ndescribe('cart', () => {\n  it('totals', () => {\n    expect(totals([])).toBe(0);\n  });\n});\n\ndescribe('order', () => {\n  describe('refund', () => {\n    it('totals', () => {});\n  });\n  it('totals', () => {\n    expect(1).toBe(1);\n  });\n});\n";
3040        let lines = text.split('\n').collect::<Vec<_>>();
3041        let line = |name: &str| js_test_line(&lines, name).map(|i| i + 1);
3042        assert_eq!(
3043            line("cart > totals"),
3044            Some(4),
3045            "not the import that names it"
3046        );
3047        assert_eq!(line("order > refund > totals"), Some(11));
3048        assert_eq!(line("totals"), Some(4));
3049        // A group longer than a body is shown is still searched to its end.
3050        let long = format!(
3051            "describe('a', () => {{\n  it('x', () => {{}});\n}});\ndescribe('b', () => {{\n{}  it('x', () => {{}});\n}});\n",
3052            "  const y = 1;\n".repeat(200)
3053        );
3054        let lines = long.split('\n').collect::<Vec<_>>();
3055        assert_eq!(js_test_line(&lines, "b > x").map(|i| i + 1), Some(205));
3056    }
3057
3058    #[test]
3059    fn python_changes_follow_the_audit() {
3060        let source = "import os\n\n\ndef f(x):\n    \"\"\"Docs.\"\"\"\n    y = x + 1\n    if y > 2:\n        return y\n    z: int\n    count += 1\n    return\ndef g(v):\n    if v:\n        pass\n    elif v is None:\n        pass\n";
3061        let starts = python_statement_starts(source);
3062        let py = Language::Python;
3063        assert_eq!(change_at(&starts, 1, "import os", py), None);
3064        assert_eq!(change_at(&starts, 4, "def f(x):", py), None);
3065        assert_eq!(change_at(&starts, 5, "\"\"\"Docs.\"\"\"", py), None);
3066        assert_eq!(
3067            change_at(&starts, 6, "y = x + 1", py),
3068            Some(Change::ValueUndefined)
3069        );
3070        assert_eq!(change_at(&starts, 7, "if y > 2:", py), Some(Change::Invert));
3071        assert_eq!(
3072            change_at(&starts, 8, "return y", py),
3073            Some(Change::ReturnUndefined)
3074        );
3075        assert_eq!(change_at(&starts, 9, "z: int", py), None);
3076        assert_eq!(change_at(&starts, 10, "count += 1", py), Some(Change::Skip));
3077        assert_eq!(change_at(&starts, 11, "return", py), Some(Change::Skip));
3078        assert_eq!(
3079            change_at(&starts, 15, "v is None", py),
3080            Some(Change::Invert)
3081        );
3082        assert_eq!(
3083            python_title("tests/test_a.py::TestA::test_b[1.5-x]"),
3084            "test_b"
3085        );
3086        assert_eq!(python_title("tests.test_a.TestA.test_b"), "test_b");
3087        assert_eq!(python_condition("if y > 2:"), "y > 2");
3088        assert_eq!(python_condition("elif v is None:"), "v is None");
3089    }
3090
3091    #[test]
3092    fn change_texts() {
3093        assert_eq!(
3094            split_initializer("const x = a === b").as_deref(),
3095            Some("const x =")
3096        );
3097        assert_eq!(
3098            split_initializer("const f = () => 1").as_deref(),
3099            Some("const f =")
3100        );
3101        assert_eq!(condition("} else if (x > 1) {"), "x > 1");
3102        assert_eq!(
3103            condition("if (!body) return null"),
3104            "if (!body) return null"
3105        );
3106        assert!(is_test_call("  it(\"works\", () => {"));
3107        assert!(is_test_call("test.each([1])('x', () => {"));
3108        assert!(!is_test_call("items.push(1)"));
3109    }
3110
3111    #[test]
3112    fn keys_ignore_line_numbers() {
3113        assert_eq!(
3114            strip_numbers("▶   12   return x\n        ... 3 lines not shown\n    15 }"),
3115            "▶  return x\n...\n }"
3116        );
3117        assert_eq!(
3118            strip_location("src/a.ts:41: `x` is deleted."),
3119            "src/a.ts:: `x` is deleted."
3120        );
3121    }
3122}