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