Skip to main content

supercov_engine/
rust_doctest.rs

1//! Strict deferred source joining for rustdoc's merged doctest mode.
2//!
3//! rustdoc compiles an extracted bundle before it compiles the runner that
4//! carries each `__doctest_N` module's original path and line. The bundle must
5//! therefore publish temporary, run-local identities. This module validates
6//! the later runner map and resolves extracted byte ranges back to immutable
7//! authored source before the ordinary compiler-manifest parser sees them.
8
9use std::{
10    collections::{BTreeMap, BTreeSet},
11    fs::{self, OpenOptions},
12    io::Write,
13    path::{Path, PathBuf},
14};
15
16use ra_ap_syntax::{
17    AstNode, Edition, SourceFile,
18    ast::{self, HasName},
19};
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22
23use crate::rust_compiler_manifest::{
24    RustCompilerManifest, RustCompilerSource, RustCompilerSourceSnapshots,
25};
26use crate::{
27    rust_probe_transport::{
28        DEFAULT_DESCRIPTOR_CAPACITY, DEFAULT_PAYLOAD_CAPACITY, RustPhaseContext, RustThreadPhase,
29        RustTransportRead, create_rust_transport, read_rust_transport, rust_assertion_context_id,
30        rust_thread_context_id, validate_rust_phase_contexts,
31    },
32    rust_runtime::RustProbeObservation,
33    rust_test_context::rust_test_context_id,
34};
35
36const MAP_SCHEMA: &str = "supercov-rustdoc-merged-map-v2";
37const OUTCOME_SCHEMA: &str = "supercov-rustdoc-outcome-unit-v4";
38const RUSTDOC_CATALOG_FORMAT_VERSION: u32 = 2;
39const MAX_OUTCOME_UNIT_BYTES: u64 = 16 * 1024 * 1024;
40const SOURCE_MODEL: &str = "rust-source-v1";
41const RUSTDOC_TRANSPORT_TOKEN_BYTES: usize = supercov_contracts::RUST_PROBE_TRANSPORT_TOKEN_SIZE;
42
43#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
44#[serde(rename_all = "camelCase", deny_unknown_fields)]
45pub struct RustdocMergedMap {
46    pub schema: String,
47    pub group: String,
48    pub entries: Vec<RustdocMergedEntry>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct RustdocMergedEntry {
54    pub module: String,
55    pub display_name: String,
56    pub path: String,
57    pub line: u64,
58    pub ignored: bool,
59    pub no_run: bool,
60    pub should_panic: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct RustdocMappedRange {
65    pub source_key: String,
66    pub start: u32,
67    pub end: u32,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct RustSourceIdentity {
72    pub id: String,
73    pub canonical: String,
74    pub probe_ordinal: u64,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct RustdocMergedJoin {
80    pub manifest: RustCompilerManifest,
81    pub sources: RustCompilerSourceSnapshots,
82    /// Every temporary bundle identity translated to its final authored ID.
83    pub obligation_ids: BTreeMap<String, String>,
84    /// Every temporary runtime ordinal translated to its final authored ordinal.
85    pub probe_ordinals: BTreeMap<String, String>,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct RustdocMergedUnit {
91    pub map: RustdocMergedMap,
92    /// A map can describe a doctest with no executable source obligations.
93    /// Such a test still participates in outcome attribution but needs no
94    /// identity or runtime translation.
95    pub join: Option<RustdocMergedJoin>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct RustdocResolvedCandidates {
100    pub candidates: Vec<(RustCompilerManifest, RustCompilerSourceSnapshots)>,
101    pub merged_units: Vec<RustdocMergedUnit>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
105#[serde(rename_all = "lowercase", deny_unknown_fields)]
106pub enum RustdocOutcomeStatus {
107    Passed,
108    Failed,
109    Ignored,
110}
111
112#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
113#[serde(rename_all = "camelCase", deny_unknown_fields)]
114pub struct RustdocTestOutcome {
115    pub display_name: String,
116    pub status: RustdocOutcomeStatus,
117    pub execution_seconds: Option<f64>,
118    pub stdout: Option<String>,
119    pub message: Option<String>,
120    pub reason: Option<String>,
121    /// Libtest's `timeout` event is a long-running-test notification. It does
122    /// not itself determine the eventual result.
123    pub timeout_warning: bool,
124}
125
126#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128pub struct RustdocOutcomeReport {
129    pub outcomes: Vec<RustdocTestOutcome>,
130    pub suites: usize,
131    pub planned_tests: u64,
132    pub filtered_out: u64,
133    /// Tests that emitted `started` but no terminal event before a failed
134    /// fail-fast suite ended.
135    pub unfinished_started: Vec<String>,
136    /// Planned tests for which libtest emitted neither a start nor a terminal
137    /// event because a failed suite stopped early.
138    pub unstarted_tests: u64,
139    pub total_seconds: Option<f64>,
140    pub compilation_seconds: Option<f64>,
141}
142
143/// The exact JSON catalog emitted by the pinned rustdoc implementation with
144/// `-Zunstable-options --output-format=doctest`.
145///
146/// This is compiler output, not a Supercov reconstruction. Keeping the full
147/// versioned record lets the outcome join identify merged, standalone,
148/// compile-fail, ignored, no-run and syntax-error doctests without deriving
149/// names or execution attributes from human-readable output.
150#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
151#[serde(deny_unknown_fields)]
152pub struct RustdocExtractedCatalog {
153    pub format_version: u32,
154    pub doctests: Vec<RustdocExtractedDoctest>,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
158#[serde(deny_unknown_fields)]
159pub struct RustdocExtractedDoctest {
160    pub file: String,
161    pub line: u64,
162    pub doctest_attributes: RustdocDoctestAttributes,
163    pub original_code: String,
164    pub doctest_code: Option<RustdocDoctestCode>,
165    pub name: String,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
169#[serde(deny_unknown_fields)]
170pub struct RustdocDoctestAttributes {
171    pub original: String,
172    pub should_panic: bool,
173    pub no_run: bool,
174    pub ignore: RustdocDoctestIgnore,
175    pub rust: bool,
176    pub test_harness: bool,
177    pub compile_fail: bool,
178    pub standalone_crate: bool,
179    pub error_codes: Vec<String>,
180    pub edition: Option<String>,
181    pub added_css_classes: Vec<String>,
182    pub unknown: Vec<String>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
186pub enum RustdocDoctestIgnore {
187    All,
188    None,
189    Some(Vec<String>),
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
193#[serde(deny_unknown_fields)]
194pub struct RustdocDoctestCode {
195    pub crate_level: String,
196    pub code: String,
197    pub wrapper: Option<RustdocDoctestWrapper>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
201#[serde(deny_unknown_fields)]
202pub struct RustdocDoctestWrapper {
203    pub before: String,
204    pub after: String,
205    pub returns_result: bool,
206}
207
208#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
209#[serde(rename_all = "camelCase", deny_unknown_fields)]
210pub struct RustdocOutcomeUnit {
211    pub schema: String,
212    pub invocation_id: String,
213    pub group: String,
214    pub companion_build_id: String,
215    pub raw_catalog_sha256: String,
216    pub raw_events_sha256: String,
217    pub transport_sha256: String,
218    pub catalog: RustdocExtractedCatalog,
219    pub report: RustdocOutcomeReport,
220    pub transport: RustTransportRead,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct RustdocTransportReservation {
225    pub path: PathBuf,
226    pub token: [u8; RUSTDOC_TRANSPORT_TOKEN_BYTES],
227}
228
229/// The exact execution state for one compiler-described merged doctest.
230///
231/// A fail-fast libtest suite can stop after announcing its total test count.
232/// `Unstarted` is therefore distinct from `Ignored`: it has no terminal
233/// outcome and must never be treated as skipped or passing.
234#[derive(Debug, Clone, PartialEq, Serialize)]
235#[serde(rename_all = "camelCase", tag = "state")]
236pub enum RustdocJoinedOutcomeState {
237    Completed {
238        outcome: RustdocTestOutcome,
239    },
240    UnfinishedStarted,
241    Unstarted,
242    FilteredOut,
243    /// Libtest reports only aggregate filtered and fail-fast-unstarted counts.
244    /// If both are non-zero, assigning either state to a particular catalog
245    /// identity would be invented attribution.
246    NotRunAmbiguous,
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize)]
250#[serde(rename_all = "camelCase")]
251pub struct RustdocJoinedOutcome {
252    pub catalog_index: u64,
253    pub catalog: RustdocExtractedDoctest,
254    pub merged_entry: Option<RustdocMergedEntry>,
255    pub state: RustdocJoinedOutcomeState,
256}
257
258/// Lossless join of one authenticated rustdoc invocation to every test in its
259/// compiler catalog. The optional merged descriptor exists only for tests
260/// whose temporary bundle identities need source/probe translation.
261#[derive(Debug, Clone, PartialEq, Serialize)]
262#[serde(rename_all = "camelCase")]
263pub struct RustdocOutcomeGroupJoin {
264    pub invocation_id: String,
265    pub group: String,
266    pub companion_build_id: String,
267    pub raw_catalog_sha256: String,
268    pub raw_events_sha256: String,
269    pub transport_sha256: String,
270    /// Identity/ordinal translation for the merged bundle. `None` is valid
271    /// only when every mapped doctest has zero executable obligations.
272    pub join: Option<RustdocMergedJoin>,
273    pub transport: RustTransportRead,
274    pub entries: Vec<RustdocJoinedOutcome>,
275    pub ambiguous_filtered_out: u64,
276    pub ambiguous_unstarted_tests: u64,
277}
278
279impl RustdocOutcomeGroupJoin {
280    pub fn has_ambiguous_outcomes(&self) -> bool {
281        self.ambiguous_filtered_out != 0 || self.ambiguous_unstarted_tests != 0
282    }
283
284    fn canonical_test_name(&self, entry: &RustdocJoinedOutcome) -> String {
285        format!(
286            "rustdoc:{}:{}:{}",
287            self.group, entry.catalog.file, entry.catalog.line
288        )
289    }
290
291    fn merged_test_name(&self, entry: &RustdocJoinedOutcome) -> Option<String> {
292        entry
293            .merged_entry
294            .as_ref()
295            .map(|merged| format!("rustdoc:{}:{}", self.group, merged.module))
296    }
297
298    pub fn attributed_transport(
299        &self,
300        entry: &RustdocJoinedOutcome,
301    ) -> Result<(u64, RustTransportRead), RustdocOutcomeError> {
302        let base = rust_test_context_id(&self.canonical_test_name(entry))
303            .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
304        let mut read = transport_for_root(&self.transport, base)?;
305        if let Some(merged_name) = self.merged_test_name(entry) {
306            let merged_base = rust_test_context_id(&merged_name)
307                .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
308            if merged_base == base {
309                return Err(RustdocOutcomeError::Invalid(format!(
310                    "rustdoc test context collision between {} and {merged_name}",
311                    self.canonical_test_name(entry)
312                )));
313            }
314            let mut merged = transport_for_root(&self.transport, merged_base)?;
315            if merged.committed != 0 {
316                merged = self
317                    .join
318                    .as_ref()
319                    .ok_or_else(|| {
320                        RustdocOutcomeError::Invalid(format!(
321                            "merged doctest {} has runtime evidence but no identity translation",
322                            entry.catalog.name
323                        ))
324                    })?
325                    .translate_transport(merged_base, &merged)
326                    .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
327                merged = rebase_transport_root(&merged, merged_base, base)?;
328                merge_transport(&mut read, merged)?;
329            }
330        }
331        Ok((base, read))
332    }
333
334    /// Background evidence: context-zero records plus every record whose
335    /// chain contains a thread phase that escaped its root test. Quarantined
336    /// records keep their real contexts; projection must flatten them.
337    pub fn background_transport(&self) -> Result<RustTransportRead, RustdocOutcomeError> {
338        let parents = phase_parent_map(&self.transport)?;
339        let scope = RustdocThreadScope::from_transport(&self.transport)?;
340        let quarantined = |context: u64| -> Result<bool, RustdocOutcomeError> {
341            let root = root_context(context, &parents)?;
342            Ok(!scope.escaped_threads(context, root, &parents)?.is_empty())
343        };
344        let mut observations = Vec::new();
345        for record in &self.transport.observations {
346            if record.context_id == 0 || quarantined(record.context_id)? {
347                observations.push(record.clone());
348            }
349        }
350        let mut ordinal_hits = Vec::new();
351        for record in &self.transport.ordinal_hits {
352            if record.context_id == 0 || quarantined(record.context_id)? {
353                ordinal_hits.push(*record);
354            }
355        }
356        let mut phases = Vec::new();
357        for phase in &self.transport.phases {
358            if quarantined(phase.child_context_id)? {
359                phases.push(phase.clone());
360            }
361        }
362        let mut thread_phases = Vec::new();
363        for phase in &self.transport.thread_phases {
364            if quarantined(phase.child_context_id)? {
365                thread_phases.push(*phase);
366            }
367        }
368        let mut thread_ends = Vec::new();
369        for end in &self.transport.thread_ends {
370            if quarantined(end.context_id)? {
371                thread_ends.push(*end);
372            }
373        }
374        let committed = u64::try_from(
375            observations.len()
376                + ordinal_hits.len()
377                + phases.len()
378                + thread_phases.len()
379                + thread_ends.len(),
380        )
381        .map_err(|_| {
382            RustdocOutcomeError::Invalid("rustdoc background transport count exceeds u64".into())
383        })?;
384        Ok(RustTransportRead {
385            committed,
386            observations,
387            ordinal_hits,
388            phases,
389            thread_phases,
390            thread_ends,
391            test_boundaries: Vec::new(),
392            incomplete: self.transport.incomplete,
393            dropped: self.transport.dropped,
394            attachments: self.transport.attachments,
395        })
396    }
397
398    /// One deterministic note per thread phase whose lifetime escaped its
399    /// root test in this invocation's transport.
400    pub fn thread_scope_limitations(&self) -> Result<BTreeSet<String>, RustdocOutcomeError> {
401        let parents = phase_parent_map(&self.transport)?;
402        let scope = RustdocThreadScope::from_transport(&self.transport)?;
403        let mut limitations = BTreeSet::new();
404        for phase in &self.transport.thread_phases {
405            let root = root_context(phase.child_context_id, &parents)?;
406            for escaped in scope.escaped_threads(phase.child_context_id, root, &parents)? {
407                if escaped == phase.child_context_id {
408                    limitations.insert(format!(
409                        "RUST_THREAD_OUTLIVED_TEST: thread phase {escaped:016x} escaped test {root:016x}"
410                    ));
411                }
412            }
413        }
414        Ok(limitations)
415    }
416
417    fn validate_transport_ownership(&self) -> Result<(), RustdocOutcomeError> {
418        let mut expected = BTreeMap::new();
419        for entry in &self.entries {
420            let names = std::iter::once(self.canonical_test_name(entry))
421                .chain(self.merged_test_name(entry));
422            for name in names {
423                let context = rust_test_context_id(&name)
424                    .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
425                if let Some(other) = expected.insert(context, name.clone()) {
426                    return Err(RustdocOutcomeError::Invalid(format!(
427                        "rustdoc test context {context:016x} collides between {other} and {name}"
428                    )));
429                }
430            }
431        }
432        let phase_parents = phase_parent_map(&self.transport)?;
433        RustdocThreadScope::from_transport(&self.transport)?;
434        for boundary in &self.transport.test_boundaries {
435            if !expected.contains_key(&boundary.context_id) {
436                return Err(RustdocOutcomeError::Invalid(format!(
437                    "rustdoc test boundary {:016x} does not identify a known test",
438                    boundary.context_id
439                )));
440            }
441        }
442        for context in self
443            .transport
444            .observations
445            .iter()
446            .map(|record| record.context_id)
447            .chain(
448                self.transport
449                    .ordinal_hits
450                    .iter()
451                    .map(|record| record.context_id),
452            )
453            .chain(
454                self.transport
455                    .phases
456                    .iter()
457                    .map(|phase| phase.child_context_id),
458            )
459            .chain(
460                self.transport
461                    .thread_phases
462                    .iter()
463                    .map(|phase| phase.child_context_id),
464            )
465            .chain(self.transport.thread_ends.iter().map(|end| end.context_id))
466            .filter(|context| *context != 0)
467        {
468            let root = root_context(context, &phase_parents)?;
469            if !expected.contains_key(&root) {
470                return Err(RustdocOutcomeError::Invalid(format!(
471                    "rustdoc transport context {context:016x} has unknown test root {root:016x}; expected {}",
472                    expected
473                        .iter()
474                        .map(|(context, name)| format!("{context:016x}={name}"))
475                        .collect::<Vec<_>>()
476                        .join(", ")
477                )));
478            }
479        }
480        Ok(())
481    }
482}
483
484#[derive(Debug, Clone, PartialEq, Serialize)]
485#[serde(rename_all = "camelCase")]
486pub struct RustdocOutcomeResolution {
487    pub groups: Vec<RustdocOutcomeGroupJoin>,
488    /// Runner maps for which no authenticated terminal outcome unit exists.
489    pub unmatched_maps: Vec<RustdocMergedUnit>,
490}
491
492impl RustdocOutcomeResolution {
493    pub fn is_fully_catalogued(&self) -> bool {
494        self.unmatched_maps.is_empty()
495    }
496
497    pub fn has_ambiguous_outcomes(&self) -> bool {
498        self.groups
499            .iter()
500            .any(RustdocOutcomeGroupJoin::has_ambiguous_outcomes)
501    }
502}
503
504fn phase_parent_map(read: &RustTransportRead) -> Result<BTreeMap<u64, u64>, RustdocOutcomeError> {
505    let mut parents = BTreeMap::new();
506    for (child, parent) in read
507        .phases
508        .iter()
509        .map(|phase| (phase.child_context_id, phase.parent_context_id))
510        .chain(
511            read.thread_phases
512                .iter()
513                .map(|phase| (phase.child_context_id, phase.parent_context_id)),
514        )
515    {
516        if parents.insert(child, parent).is_some() {
517            return Err(RustdocOutcomeError::Invalid(format!(
518                "rustdoc transport repeats phase context {child:016x}"
519            )));
520        }
521    }
522    Ok(parents)
523}
524
525/// The join-bounded thread acceptance state for one rustdoc invocation's
526/// shared transport: which thread phases exist, when each ended, and when
527/// each test context committed its boundary.
528struct RustdocThreadScope {
529    thread_children: BTreeSet<u64>,
530    thread_end_index: BTreeMap<u64, u64>,
531    boundary_index: BTreeMap<u64, u64>,
532}
533
534impl RustdocThreadScope {
535    fn from_transport(read: &RustTransportRead) -> Result<Self, RustdocOutcomeError> {
536        let thread_children = read
537            .thread_phases
538            .iter()
539            .map(|phase| phase.child_context_id)
540            .collect::<BTreeSet<_>>();
541        let mut thread_end_index = BTreeMap::new();
542        for end in &read.thread_ends {
543            if !thread_children.contains(&end.context_id)
544                || thread_end_index
545                    .insert(end.context_id, end.commit_index)
546                    .is_some()
547            {
548                return Err(RustdocOutcomeError::Invalid(format!(
549                    "rustdoc thread end {:016x} has no unique thread-phase definition",
550                    end.context_id
551                )));
552            }
553        }
554        let mut boundary_index = BTreeMap::new();
555        for boundary in &read.test_boundaries {
556            if boundary_index
557                .insert(boundary.context_id, boundary.commit_index)
558                .is_some()
559            {
560                return Err(RustdocOutcomeError::Invalid(format!(
561                    "rustdoc test boundary {:016x} is repeated",
562                    boundary.context_id
563                )));
564            }
565        }
566        Ok(Self {
567            thread_children,
568            thread_end_index,
569            boundary_index,
570        })
571    }
572
573    /// Thread phases on `context`'s chain whose lifetimes escaped `root`:
574    /// every record under such a chain is deterministic background evidence.
575    fn escaped_threads(
576        &self,
577        mut context: u64,
578        root: u64,
579        parents: &BTreeMap<u64, u64>,
580    ) -> Result<Vec<u64>, RustdocOutcomeError> {
581        let boundary = self.boundary_index.get(&root).copied();
582        let mut escaped = Vec::new();
583        let mut seen = BTreeSet::new();
584        loop {
585            if self.thread_children.contains(&context)
586                && !matches!(
587                    (self.thread_end_index.get(&context), boundary),
588                    (Some(end), Some(boundary)) if *end < boundary
589                )
590            {
591                escaped.push(context);
592            }
593            let Some(parent) = parents.get(&context) else {
594                break;
595            };
596            if !seen.insert(context) {
597                return Err(RustdocOutcomeError::Invalid(format!(
598                    "rustdoc transport phase cycle at {context:016x}"
599                )));
600            }
601            context = *parent;
602        }
603        Ok(escaped)
604    }
605}
606
607fn root_context(
608    mut context: u64,
609    parents: &BTreeMap<u64, u64>,
610) -> Result<u64, RustdocOutcomeError> {
611    let mut seen = BTreeSet::new();
612    while let Some(parent) = parents.get(&context) {
613        if !seen.insert(context) {
614            return Err(RustdocOutcomeError::Invalid(format!(
615                "rustdoc transport phase cycle at {context:016x}"
616            )));
617        }
618        context = *parent;
619    }
620    Ok(context)
621}
622
623/// Keep exactly the records whose chain resolves to `expected_root` and whose
624/// thread phases all ended before the root's boundary. Records under an
625/// escaped thread phase are excluded here and belong to background.
626fn transport_for_root(
627    read: &RustTransportRead,
628    expected_root: u64,
629) -> Result<RustTransportRead, RustdocOutcomeError> {
630    let parents = phase_parent_map(read)?;
631    let scope = RustdocThreadScope::from_transport(read)?;
632    let accepts = |context: u64| -> Result<bool, RustdocOutcomeError> {
633        if root_context(context, &parents)? != expected_root {
634            return Ok(false);
635        }
636        Ok(scope
637            .escaped_threads(context, expected_root, &parents)?
638            .is_empty())
639    };
640    let mut observations = Vec::new();
641    for record in &read.observations {
642        if record.context_id != 0 && accepts(record.context_id)? {
643            observations.push(record.clone());
644        }
645    }
646    let mut ordinal_hits = Vec::new();
647    for record in &read.ordinal_hits {
648        if record.context_id != 0 && accepts(record.context_id)? {
649            ordinal_hits.push(*record);
650        }
651    }
652    let mut phases = Vec::new();
653    for phase in &read.phases {
654        if accepts(phase.child_context_id)? {
655            phases.push(phase.clone());
656        }
657    }
658    let mut thread_phases = Vec::new();
659    for phase in &read.thread_phases {
660        if accepts(phase.child_context_id)? {
661            thread_phases.push(*phase);
662        }
663    }
664    let mut thread_ends = Vec::new();
665    for end in &read.thread_ends {
666        if accepts(end.context_id)? {
667            thread_ends.push(*end);
668        }
669    }
670    let mut test_boundaries = Vec::new();
671    for boundary in &read.test_boundaries {
672        if boundary.context_id == expected_root {
673            test_boundaries.push(*boundary);
674        }
675    }
676    let committed = u64::try_from(
677        observations.len()
678            + ordinal_hits.len()
679            + phases.len()
680            + thread_phases.len()
681            + thread_ends.len()
682            + test_boundaries.len(),
683    )
684    .map_err(|_| RustdocOutcomeError::Invalid("rustdoc transport count exceeds u64".into()))?;
685    Ok(RustTransportRead {
686        observations,
687        ordinal_hits,
688        phases,
689        thread_phases,
690        thread_ends,
691        test_boundaries,
692        committed,
693        incomplete: 0,
694        dropped: 0,
695        attachments: 0,
696    })
697}
698
699enum RustdocPhaseDefinition<'read> {
700    Assertion(&'read RustPhaseContext),
701    Thread(&'read RustThreadPhase),
702}
703
704impl RustdocPhaseDefinition<'_> {
705    fn parent_context_id(&self) -> u64 {
706        match self {
707            Self::Assertion(phase) => phase.parent_context_id,
708            Self::Thread(phase) => phase.parent_context_id,
709        }
710    }
711}
712
713fn rustdoc_phase_definitions(
714    read: &RustTransportRead,
715) -> Result<BTreeMap<u64, RustdocPhaseDefinition<'_>>, RustdocOutcomeError> {
716    let mut definitions = BTreeMap::new();
717    for phase in &read.phases {
718        if definitions
719            .insert(
720                phase.child_context_id,
721                RustdocPhaseDefinition::Assertion(phase),
722            )
723            .is_some()
724        {
725            return Err(RustdocOutcomeError::Invalid(
726                "rustdoc transport has duplicate assertion contexts".into(),
727            ));
728        }
729    }
730    for phase in &read.thread_phases {
731        if definitions
732            .insert(
733                phase.child_context_id,
734                RustdocPhaseDefinition::Thread(phase),
735            )
736            .is_some()
737        {
738            return Err(RustdocOutcomeError::Invalid(
739                "rustdoc transport has duplicate phase contexts".into(),
740            ));
741        }
742    }
743    Ok(definitions)
744}
745
746fn rebase_transport_root(
747    read: &RustTransportRead,
748    old_base: u64,
749    new_base: u64,
750) -> Result<RustTransportRead, RustdocOutcomeError> {
751    validate_rust_phase_contexts(old_base, read)
752        .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
753    let definitions = rustdoc_phase_definitions(read)?;
754    fn translate(
755        context: u64,
756        old_base: u64,
757        new_base: u64,
758        definitions: &BTreeMap<u64, RustdocPhaseDefinition<'_>>,
759        translated: &mut BTreeMap<u64, u64>,
760        visiting: &mut BTreeSet<u64>,
761    ) -> Result<u64, RustdocOutcomeError> {
762        if context == old_base {
763            return Ok(new_base);
764        }
765        if let Some(context) = translated.get(&context) {
766            return Ok(*context);
767        }
768        if !visiting.insert(context) {
769            return Err(RustdocOutcomeError::Invalid(format!(
770                "rustdoc assertion context cycle at {context:016x}"
771            )));
772        }
773        let phase = definitions.get(&context).ok_or_else(|| {
774            RustdocOutcomeError::Invalid(format!(
775                "rustdoc context {context:016x} has no phase definition"
776            ))
777        })?;
778        let parent = translate(
779            phase.parent_context_id(),
780            old_base,
781            new_base,
782            definitions,
783            translated,
784            visiting,
785        )?;
786        let rebased = match phase {
787            RustdocPhaseDefinition::Assertion(phase) => {
788                rust_assertion_context_id(parent, &phase.decision_id, phase.invocation_nonce)
789                    .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?
790            }
791            RustdocPhaseDefinition::Thread(phase) => {
792                rust_thread_context_id(parent, phase.invocation_nonce)
793            }
794        };
795        visiting.remove(&context);
796        translated.insert(context, rebased);
797        Ok(rebased)
798    }
799    let mut translated = BTreeMap::from([(old_base, new_base)]);
800    let mut visiting = BTreeSet::new();
801    for context in definitions.keys() {
802        translate(
803            *context,
804            old_base,
805            new_base,
806            &definitions,
807            &mut translated,
808            &mut visiting,
809        )?;
810    }
811    let map_context = |context: u64| {
812        translated.get(&context).copied().ok_or_else(|| {
813            RustdocOutcomeError::Invalid(format!(
814                "rustdoc record context {context:016x} was not rebased"
815            ))
816        })
817    };
818    let mut rebased = read.clone();
819    for observation in &mut rebased.observations {
820        observation.context_id = map_context(observation.context_id)?;
821    }
822    for hit in &mut rebased.ordinal_hits {
823        hit.context_id = map_context(hit.context_id)?;
824    }
825    for phase in &mut rebased.phases {
826        phase.child_context_id = map_context(phase.child_context_id)?;
827        phase.parent_context_id = map_context(phase.parent_context_id)?;
828    }
829    for phase in &mut rebased.thread_phases {
830        phase.child_context_id = map_context(phase.child_context_id)?;
831        phase.parent_context_id = map_context(phase.parent_context_id)?;
832    }
833    for end in &mut rebased.thread_ends {
834        end.context_id = map_context(end.context_id)?;
835    }
836    for boundary in &mut rebased.test_boundaries {
837        boundary.context_id = map_context(boundary.context_id)?;
838    }
839    validate_rust_phase_contexts(new_base, &rebased)
840        .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
841    Ok(rebased)
842}
843
844fn merge_transport(
845    destination: &mut RustTransportRead,
846    source: RustTransportRead,
847) -> Result<(), RustdocOutcomeError> {
848    let mut phase_ids = destination
849        .phases
850        .iter()
851        .map(|phase| phase.child_context_id)
852        .chain(
853            destination
854                .thread_phases
855                .iter()
856                .map(|phase| phase.child_context_id),
857        )
858        .collect::<BTreeSet<_>>();
859    if source
860        .phases
861        .iter()
862        .map(|phase| phase.child_context_id)
863        .chain(
864            source
865                .thread_phases
866                .iter()
867                .map(|phase| phase.child_context_id),
868        )
869        .any(|child| !phase_ids.insert(child))
870    {
871        return Err(RustdocOutcomeError::Invalid(
872            "rustdoc standalone and merged assertion contexts collide".into(),
873        ));
874    }
875    let mut end_ids = destination
876        .thread_ends
877        .iter()
878        .map(|end| end.context_id)
879        .collect::<BTreeSet<_>>();
880    if source
881        .thread_ends
882        .iter()
883        .any(|end| !end_ids.insert(end.context_id))
884    {
885        return Err(RustdocOutcomeError::Invalid(
886            "rustdoc standalone and merged thread ends collide".into(),
887        ));
888    }
889    let mut boundary_ids = destination
890        .test_boundaries
891        .iter()
892        .map(|boundary| boundary.context_id)
893        .collect::<BTreeSet<_>>();
894    if source
895        .test_boundaries
896        .iter()
897        .any(|boundary| !boundary_ids.insert(boundary.context_id))
898    {
899        return Err(RustdocOutcomeError::Invalid(
900            "rustdoc standalone and merged test boundaries collide".into(),
901        ));
902    }
903    destination.committed = destination
904        .committed
905        .checked_add(source.committed)
906        .ok_or_else(|| RustdocOutcomeError::Invalid("rustdoc committed count overflow".into()))?;
907    destination.observations.extend(source.observations);
908    destination.ordinal_hits.extend(source.ordinal_hits);
909    destination.phases.extend(source.phases);
910    destination.thread_phases.extend(source.thread_phases);
911    destination.thread_ends.extend(source.thread_ends);
912    destination.test_boundaries.extend(source.test_boundaries);
913    Ok(())
914}
915
916#[derive(Debug, Clone, PartialEq, Eq)]
917pub enum RustdocOutcomeError {
918    Io { path: PathBuf, reason: String },
919    Json(String),
920    Invalid(String),
921}
922
923impl std::fmt::Display for RustdocOutcomeError {
924    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925        match self {
926            Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
927            Self::Json(reason) => write!(formatter, "invalid rustdoc outcome JSON: {reason}"),
928            Self::Invalid(reason) => write!(formatter, "invalid rustdoc outcome: {reason}"),
929        }
930    }
931}
932
933impl std::error::Error for RustdocOutcomeError {}
934
935#[derive(Debug, Clone, Default, PartialEq)]
936enum StrictField<T> {
937    #[default]
938    Missing,
939    Value(T),
940}
941
942impl<'de, T: Deserialize<'de>> Deserialize<'de> for StrictField<T> {
943    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
944        T::deserialize(deserializer).map(Self::Value)
945    }
946}
947
948impl<T> StrictField<T> {
949    fn take(self) -> Option<T> {
950        match self {
951            Self::Missing => None,
952            Self::Value(value) => Some(value),
953        }
954    }
955
956    fn is_missing(&self) -> bool {
957        matches!(self, Self::Missing)
958    }
959}
960
961#[derive(Debug, Deserialize)]
962#[serde(deny_unknown_fields)]
963struct RawLibtestEvent {
964    #[serde(rename = "type")]
965    kind: String,
966    #[serde(default)]
967    event: StrictField<String>,
968    #[serde(default)]
969    name: StrictField<String>,
970    #[serde(default)]
971    test_count: StrictField<u64>,
972    #[serde(default)]
973    shuffle_seed: StrictField<u64>,
974    #[serde(default)]
975    passed: StrictField<u64>,
976    #[serde(default)]
977    failed: StrictField<u64>,
978    #[serde(default)]
979    ignored: StrictField<u64>,
980    #[serde(default)]
981    measured: StrictField<u64>,
982    #[serde(default)]
983    filtered_out: StrictField<u64>,
984    #[serde(default)]
985    exec_time: StrictField<f64>,
986    #[serde(default)]
987    stdout: StrictField<String>,
988    #[serde(default)]
989    message: StrictField<String>,
990    #[serde(default)]
991    reason: StrictField<String>,
992    #[serde(default)]
993    total_time: StrictField<f64>,
994    #[serde(default)]
995    compilation_time: StrictField<f64>,
996}
997
998impl RawLibtestEvent {
999    fn has_only(&self, fields: &[&str]) -> bool {
1000        let present = [
1001            ("event", !self.event.is_missing()),
1002            ("name", !self.name.is_missing()),
1003            ("test_count", !self.test_count.is_missing()),
1004            ("shuffle_seed", !self.shuffle_seed.is_missing()),
1005            ("passed", !self.passed.is_missing()),
1006            ("failed", !self.failed.is_missing()),
1007            ("ignored", !self.ignored.is_missing()),
1008            ("measured", !self.measured.is_missing()),
1009            ("filtered_out", !self.filtered_out.is_missing()),
1010            ("exec_time", !self.exec_time.is_missing()),
1011            ("stdout", !self.stdout.is_missing()),
1012            ("message", !self.message.is_missing()),
1013            ("reason", !self.reason.is_missing()),
1014            ("total_time", !self.total_time.is_missing()),
1015            ("compilation_time", !self.compilation_time.is_missing()),
1016        ];
1017        present
1018            .into_iter()
1019            .all(|(field, present)| !present || fields.contains(&field))
1020    }
1021}
1022
1023#[derive(Debug)]
1024struct ActiveSuite {
1025    expected: u64,
1026    outcomes: BTreeMap<String, RustdocOutcomeStatus>,
1027    started: BTreeSet<String>,
1028    timed_out: BTreeSet<String>,
1029}
1030
1031fn nonnegative_finite(value: Option<f64>) -> bool {
1032    value.is_none_or(|value| value.is_finite() && value >= 0.0)
1033}
1034
1035/// Parse the exact JSON event stream emitted by the pinned Rust libtest
1036/// formatter. The parser validates field shapes, event ordering, terminal
1037/// uniqueness and suite arithmetic; a truncated or future-incompatible stream
1038/// cannot silently become passing coverage.
1039pub fn parse_rustdoc_libtest_json(bytes: &[u8]) -> Result<RustdocOutcomeReport, RustdocJoinError> {
1040    let source =
1041        std::str::from_utf8(bytes).map_err(|error| RustdocJoinError::Json(error.to_string()))?;
1042    let mut active: Option<ActiveSuite> = None;
1043    let mut outcomes = BTreeMap::<String, RustdocTestOutcome>::new();
1044    let mut suites = 0_usize;
1045    let mut planned_tests = 0_u64;
1046    let mut filtered_out = 0_u64;
1047    let mut unfinished_started = BTreeSet::new();
1048    let mut unstarted_tests = 0_u64;
1049    let mut report = None;
1050    for (index, line) in source.lines().enumerate() {
1051        if line.trim().is_empty() {
1052            return Err(RustdocJoinError::Json(format!(
1053                "libtest event {} is empty",
1054                index + 1
1055            )));
1056        }
1057        let raw: RawLibtestEvent = serde_json::from_str(line).map_err(|error| {
1058            RustdocJoinError::Json(format!("libtest event {}: {error}", index + 1))
1059        })?;
1060        let event = raw.event.clone().take();
1061        match (raw.kind.as_str(), event.as_deref()) {
1062            ("suite", Some("started")) => {
1063                if active.is_some()
1064                    || report.is_some()
1065                    || !raw.has_only(&["event", "test_count", "shuffle_seed"])
1066                {
1067                    return Err(RustdocJoinError::Invalid(format!(
1068                        "libtest suite start {} is out of order or malformed",
1069                        index + 1
1070                    )));
1071                }
1072                let expected = raw.test_count.take().ok_or_else(|| {
1073                    RustdocJoinError::Invalid("libtest suite start has no test count".into())
1074                })?;
1075                active = Some(ActiveSuite {
1076                    expected,
1077                    outcomes: BTreeMap::new(),
1078                    started: BTreeSet::new(),
1079                    timed_out: BTreeSet::new(),
1080                });
1081            }
1082            ("test", Some("started")) => {
1083                if !raw.has_only(&["event", "name"]) {
1084                    return Err(RustdocJoinError::Invalid(
1085                        "libtest test start has unexpected fields".into(),
1086                    ));
1087                }
1088                let name = raw
1089                    .name
1090                    .take()
1091                    .filter(|name| !name.is_empty())
1092                    .ok_or_else(|| {
1093                        RustdocJoinError::Invalid("libtest test start has no name".into())
1094                    })?;
1095                let suite = active.as_mut().ok_or_else(|| {
1096                    RustdocJoinError::Invalid("libtest test started outside a suite".into())
1097                })?;
1098                if !suite.started.insert(name.clone()) || suite.outcomes.contains_key(&name) {
1099                    return Err(RustdocJoinError::Invalid(format!(
1100                        "libtest test {name} started more than once"
1101                    )));
1102                }
1103            }
1104            ("test", Some("timeout")) => {
1105                if !raw.has_only(&["event", "name"]) {
1106                    return Err(RustdocJoinError::Invalid(
1107                        "libtest timeout has unexpected fields".into(),
1108                    ));
1109                }
1110                let name = raw
1111                    .name
1112                    .take()
1113                    .filter(|name| !name.is_empty())
1114                    .ok_or_else(|| {
1115                        RustdocJoinError::Invalid("libtest timeout has no name".into())
1116                    })?;
1117                let suite = active.as_mut().ok_or_else(|| {
1118                    RustdocJoinError::Invalid("libtest timeout is outside a suite".into())
1119                })?;
1120                if !suite.started.contains(&name) || !suite.timed_out.insert(name.clone()) {
1121                    return Err(RustdocJoinError::Invalid(format!(
1122                        "libtest timeout for {name} has no unique start"
1123                    )));
1124                }
1125            }
1126            ("test", Some(status @ ("ok" | "failed" | "ignored"))) => {
1127                if !raw.has_only(&["event", "name", "exec_time", "stdout", "message", "reason"]) {
1128                    return Err(RustdocJoinError::Invalid(
1129                        "libtest terminal test event has unexpected fields".into(),
1130                    ));
1131                }
1132                let name = raw
1133                    .name
1134                    .take()
1135                    .filter(|name| !name.is_empty())
1136                    .ok_or_else(|| {
1137                        RustdocJoinError::Invalid("libtest terminal event has no name".into())
1138                    })?;
1139                let execution_seconds = raw.exec_time.take();
1140                if !nonnegative_finite(execution_seconds) {
1141                    return Err(RustdocJoinError::Invalid(format!(
1142                        "libtest test {name} has invalid execution time"
1143                    )));
1144                }
1145                let message = raw.message.take();
1146                let reason = raw.reason.take();
1147                if (status == "ok" && (message.is_some() || reason.is_some()))
1148                    || (status == "ignored" && reason.is_some())
1149                    || (status == "failed" && message.is_some() && reason.is_some())
1150                {
1151                    return Err(RustdocJoinError::Invalid(format!(
1152                        "libtest test {name} has impossible terminal details"
1153                    )));
1154                }
1155                let status = match status {
1156                    "ok" => RustdocOutcomeStatus::Passed,
1157                    "failed" => RustdocOutcomeStatus::Failed,
1158                    "ignored" => RustdocOutcomeStatus::Ignored,
1159                    _ => unreachable!(),
1160                };
1161                let suite = active.as_mut().ok_or_else(|| {
1162                    RustdocJoinError::Invalid("libtest result is outside a suite".into())
1163                })?;
1164                if !suite.started.contains(&name) {
1165                    return Err(RustdocJoinError::Invalid(format!(
1166                        "libtest test {name} has a terminal result without a start"
1167                    )));
1168                }
1169                if suite.outcomes.insert(name.clone(), status).is_some()
1170                    || outcomes.contains_key(&name)
1171                {
1172                    return Err(RustdocJoinError::Invalid(format!(
1173                        "libtest test {name} has more than one terminal result"
1174                    )));
1175                }
1176                if reason
1177                    .as_deref()
1178                    .is_some_and(|reason| reason != "time limit exceeded")
1179                {
1180                    return Err(RustdocJoinError::Invalid(format!(
1181                        "libtest test {name} has an unknown failure reason"
1182                    )));
1183                }
1184                let timeout_warning = suite.timed_out.contains(&name);
1185                let stdout = raw.stdout.take();
1186                if status == RustdocOutcomeStatus::Ignored
1187                    && (execution_seconds.is_some() || stdout.is_some())
1188                {
1189                    return Err(RustdocJoinError::Invalid(format!(
1190                        "ignored libtest {name} has impossible execution details"
1191                    )));
1192                }
1193                outcomes.insert(
1194                    name.clone(),
1195                    RustdocTestOutcome {
1196                        display_name: name,
1197                        status,
1198                        execution_seconds,
1199                        stdout,
1200                        message,
1201                        reason,
1202                        timeout_warning,
1203                    },
1204                );
1205            }
1206            ("suite", Some(status @ ("ok" | "failed"))) => {
1207                if !raw.has_only(&[
1208                    "event",
1209                    "passed",
1210                    "failed",
1211                    "ignored",
1212                    "measured",
1213                    "filtered_out",
1214                    "exec_time",
1215                ]) {
1216                    return Err(RustdocJoinError::Invalid(
1217                        "libtest suite result has unexpected fields".into(),
1218                    ));
1219                }
1220                let suite = active.take().ok_or_else(|| {
1221                    RustdocJoinError::Invalid("libtest suite ended without a start".into())
1222                })?;
1223                let passed = raw.passed.take();
1224                let failed = raw.failed.take();
1225                let ignored = raw.ignored.take();
1226                let measured = raw.measured.take();
1227                let filtered = raw.filtered_out.take();
1228                let execution = raw.exec_time.take();
1229                if passed.is_none()
1230                    || failed.is_none()
1231                    || ignored.is_none()
1232                    || measured.is_none()
1233                    || filtered.is_none()
1234                    || !nonnegative_finite(execution)
1235                {
1236                    return Err(RustdocJoinError::Invalid(
1237                        "libtest suite result is incomplete".into(),
1238                    ));
1239                }
1240                let actual_passed = suite
1241                    .outcomes
1242                    .values()
1243                    .filter(|outcome| **outcome == RustdocOutcomeStatus::Passed)
1244                    .count() as u64;
1245                let actual_failed = suite
1246                    .outcomes
1247                    .values()
1248                    .filter(|outcome| **outcome == RustdocOutcomeStatus::Failed)
1249                    .count() as u64;
1250                let actual_ignored = suite
1251                    .outcomes
1252                    .values()
1253                    .filter(|outcome| **outcome == RustdocOutcomeStatus::Ignored)
1254                    .count() as u64;
1255                let actual_completed = actual_passed + actual_failed + actual_ignored;
1256                let suite_unfinished = suite
1257                    .started
1258                    .iter()
1259                    .filter(|name| !suite.outcomes.contains_key(*name))
1260                    .cloned()
1261                    .collect::<BTreeSet<_>>();
1262                if suite.expected < suite.started.len() as u64
1263                    || suite.expected < actual_completed
1264                    || actual_completed != suite.outcomes.len() as u64
1265                {
1266                    return Err(RustdocJoinError::Invalid(
1267                        "libtest suite contains more events than planned tests".into(),
1268                    ));
1269                }
1270                let stopped_early = actual_completed != suite.expected;
1271                if passed != Some(actual_passed)
1272                    || failed != Some(actual_failed)
1273                    || ignored != Some(actual_ignored)
1274                    || measured != Some(0)
1275                    || (status == "ok") != (actual_failed == 0)
1276                    || (stopped_early && actual_failed == 0)
1277                {
1278                    return Err(RustdocJoinError::Invalid(
1279                        "libtest suite arithmetic does not match terminal events".into(),
1280                    ));
1281                }
1282                planned_tests = planned_tests.checked_add(suite.expected).ok_or_else(|| {
1283                    RustdocJoinError::Invalid("libtest planned-test count overflow".into())
1284                })?;
1285                filtered_out = filtered_out
1286                    .checked_add(filtered.expect("validated above"))
1287                    .ok_or_else(|| {
1288                        RustdocJoinError::Invalid("libtest filtered-test count overflow".into())
1289                    })?;
1290                unstarted_tests = unstarted_tests
1291                    .checked_add(
1292                        suite
1293                            .expected
1294                            .checked_sub(suite.started.len() as u64)
1295                            .expect("event count validated above"),
1296                    )
1297                    .ok_or_else(|| {
1298                        RustdocJoinError::Invalid("libtest unstarted-test count overflow".into())
1299                    })?;
1300                for name in suite_unfinished {
1301                    if !unfinished_started.insert(name.clone()) {
1302                        return Err(RustdocJoinError::Invalid(format!(
1303                            "libtest unfinished test {name} appeared in more than one suite"
1304                        )));
1305                    }
1306                }
1307                suites += 1;
1308            }
1309            ("report", None) => {
1310                if active.is_some()
1311                    || suites == 0
1312                    || report.is_some()
1313                    || !raw.has_only(&["total_time", "compilation_time"])
1314                {
1315                    return Err(RustdocJoinError::Invalid(
1316                        "libtest merged report is out of order or malformed".into(),
1317                    ));
1318                }
1319                let total = raw.total_time.take();
1320                let compilation = raw.compilation_time.take();
1321                if total.is_none()
1322                    || compilation.is_none()
1323                    || !nonnegative_finite(total)
1324                    || !nonnegative_finite(compilation)
1325                    || compilation > total
1326                {
1327                    return Err(RustdocJoinError::Invalid(
1328                        "libtest merged report has invalid timings".into(),
1329                    ));
1330                }
1331                report = Some((total, compilation));
1332            }
1333            _ => {
1334                return Err(RustdocJoinError::Invalid(format!(
1335                    "unsupported libtest event {} ({:?})",
1336                    raw.kind, event
1337                )));
1338            }
1339        }
1340    }
1341    if active.is_some() || suites == 0 {
1342        return Err(RustdocJoinError::Invalid(
1343            "libtest event stream is truncated or contains no suite".into(),
1344        ));
1345    }
1346    let (total_seconds, compilation_seconds) = report.unwrap_or((None, None));
1347    let report = RustdocOutcomeReport {
1348        outcomes: outcomes.into_values().collect(),
1349        suites,
1350        planned_tests,
1351        filtered_out,
1352        unfinished_started: unfinished_started.into_iter().collect(),
1353        unstarted_tests,
1354        total_seconds,
1355        compilation_seconds,
1356    };
1357    report
1358        .validate()
1359        .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?;
1360    Ok(report)
1361}
1362
1363fn canonical_sha256(value: &str) -> bool {
1364    value.len() == 64
1365        && value
1366            .bytes()
1367            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1368}
1369
1370impl RustdocOutcomeReport {
1371    pub fn validate(&self) -> Result<(), RustdocOutcomeError> {
1372        if self.suites == 0
1373            || !nonnegative_finite(self.total_seconds)
1374            || !nonnegative_finite(self.compilation_seconds)
1375            || self.total_seconds.is_some() != self.compilation_seconds.is_some()
1376            || self.compilation_seconds > self.total_seconds
1377        {
1378            return Err(RustdocOutcomeError::Invalid(
1379                "report has invalid suite or timing metadata".into(),
1380            ));
1381        }
1382        let mut previous = None;
1383        let mut completed = BTreeSet::new();
1384        let mut has_failure = false;
1385        for outcome in &self.outcomes {
1386            if outcome.display_name.trim().is_empty()
1387                || outcome.display_name.chars().any(char::is_control)
1388                || previous.is_some_and(|previous| previous >= outcome.display_name.as_str())
1389                || !completed.insert(outcome.display_name.as_str())
1390                || !nonnegative_finite(outcome.execution_seconds)
1391            {
1392                return Err(RustdocOutcomeError::Invalid(
1393                    "report outcomes are malformed, duplicated or unsorted".into(),
1394                ));
1395            }
1396            previous = Some(outcome.display_name.as_str());
1397            match outcome.status {
1398                RustdocOutcomeStatus::Passed
1399                    if outcome.message.is_some() || outcome.reason.is_some() =>
1400                {
1401                    return Err(RustdocOutcomeError::Invalid(format!(
1402                        "passed doctest {} has failure details",
1403                        outcome.display_name
1404                    )));
1405                }
1406                RustdocOutcomeStatus::Failed => {
1407                    has_failure = true;
1408                    if (outcome.message.is_some() && outcome.reason.is_some())
1409                        || outcome
1410                            .reason
1411                            .as_deref()
1412                            .is_some_and(|reason| reason != "time limit exceeded")
1413                    {
1414                        return Err(RustdocOutcomeError::Invalid(format!(
1415                            "failed doctest {} has incompatible details",
1416                            outcome.display_name
1417                        )));
1418                    }
1419                }
1420                RustdocOutcomeStatus::Ignored
1421                    if outcome.execution_seconds.is_some()
1422                        || outcome.stdout.is_some()
1423                        || outcome.reason.is_some() =>
1424                {
1425                    return Err(RustdocOutcomeError::Invalid(format!(
1426                        "ignored doctest {} has execution details",
1427                        outcome.display_name
1428                    )));
1429                }
1430                _ => {}
1431            }
1432        }
1433        let mut previous: Option<&str> = None;
1434        for name in &self.unfinished_started {
1435            if name.trim().is_empty()
1436                || name.chars().any(char::is_control)
1437                || previous.is_some_and(|previous| previous >= name.as_str())
1438                || completed.contains(name.as_str())
1439            {
1440                return Err(RustdocOutcomeError::Invalid(
1441                    "unfinished doctest identities are malformed, duplicated or completed".into(),
1442                ));
1443            }
1444            previous = Some(name.as_str());
1445        }
1446        let completed = u64::try_from(self.outcomes.len()).map_err(|_| {
1447            RustdocOutcomeError::Invalid("completed doctest count exceeds u64".into())
1448        })?;
1449        let unfinished = u64::try_from(self.unfinished_started.len()).map_err(|_| {
1450            RustdocOutcomeError::Invalid("unfinished doctest count exceeds u64".into())
1451        })?;
1452        let accounted = completed
1453            .checked_add(unfinished)
1454            .and_then(|count| count.checked_add(self.unstarted_tests))
1455            .ok_or_else(|| RustdocOutcomeError::Invalid("doctest count overflow".into()))?;
1456        if accounted != self.planned_tests
1457            || ((unfinished != 0 || self.unstarted_tests != 0) && !has_failure)
1458        {
1459            return Err(RustdocOutcomeError::Invalid(
1460                "planned, completed and fail-fast doctest counts disagree".into(),
1461            ));
1462        }
1463        Ok(())
1464    }
1465}
1466
1467fn clean_catalog_text(value: &str) -> bool {
1468    !value.is_empty() && !value.chars().any(char::is_control)
1469}
1470
1471fn clean_catalog_values(values: &[String]) -> bool {
1472    values.iter().all(|value| clean_catalog_text(value))
1473}
1474
1475impl RustdocExtractedCatalog {
1476    pub fn parse(bytes: &[u8]) -> Result<Self, RustdocOutcomeError> {
1477        let catalog: Self = serde_json::from_slice(bytes)
1478            .map_err(|error| RustdocOutcomeError::Json(format!("rustdoc catalog: {error}")))?;
1479        catalog.validate()?;
1480        Ok(catalog)
1481    }
1482
1483    pub fn validate(&self) -> Result<(), RustdocOutcomeError> {
1484        if self.format_version != RUSTDOC_CATALOG_FORMAT_VERSION {
1485            return Err(RustdocOutcomeError::Invalid(format!(
1486                "unsupported rustdoc doctest catalog format {}",
1487                self.format_version
1488            )));
1489        }
1490        let mut names = BTreeSet::new();
1491        let mut source_sites = BTreeSet::new();
1492        for doctest in &self.doctests {
1493            doctest.validate()?;
1494            if !names.insert(doctest.name.as_str()) {
1495                return Err(RustdocOutcomeError::Invalid(format!(
1496                    "rustdoc catalog contains duplicate doctest name {}",
1497                    doctest.name
1498                )));
1499            }
1500            if !source_sites.insert((doctest.file.as_str(), doctest.line)) {
1501                return Err(RustdocOutcomeError::Invalid(format!(
1502                    "rustdoc catalog contains duplicate source site {}:{}",
1503                    doctest.file, doctest.line
1504                )));
1505            }
1506        }
1507        Ok(())
1508    }
1509}
1510
1511impl RustdocExtractedDoctest {
1512    fn validate(&self) -> Result<(), RustdocOutcomeError> {
1513        if !clean_catalog_text(&self.file) || self.line == 0 || !clean_catalog_text(&self.name) {
1514            return Err(RustdocOutcomeError::Invalid(
1515                "rustdoc catalog contains an invalid file, line or name".into(),
1516            ));
1517        }
1518        let prefix = format!("{} - ", self.file);
1519        let suffix = format!("(line {})", self.line);
1520        if !self.name.starts_with(&prefix) || !self.name.ends_with(&suffix) {
1521            return Err(RustdocOutcomeError::Invalid(format!(
1522                "rustdoc catalog name {} does not bind to {}:{}",
1523                self.name, self.file, self.line
1524            )));
1525        }
1526        let attributes = &self.doctest_attributes;
1527        if !matches!(
1528            attributes.edition.as_deref(),
1529            None | Some("2015" | "2018" | "2021" | "2024")
1530        ) || !clean_catalog_values(&attributes.error_codes)
1531            || !clean_catalog_values(&attributes.added_css_classes)
1532            || !clean_catalog_values(&attributes.unknown)
1533            || matches!(&attributes.ignore, RustdocDoctestIgnore::Some(targets) if targets.is_empty() || !clean_catalog_values(targets))
1534        {
1535            return Err(RustdocOutcomeError::Invalid(format!(
1536                "rustdoc catalog attributes are invalid for {}",
1537                self.name
1538            )));
1539        }
1540        Ok(())
1541    }
1542
1543    fn ignored(&self) -> bool {
1544        !matches!(self.doctest_attributes.ignore, RustdocDoctestIgnore::None)
1545    }
1546}
1547
1548impl RustdocOutcomeUnit {
1549    pub fn validate(&self) -> Result<(), RustdocOutcomeError> {
1550        if self.schema != OUTCOME_SCHEMA
1551            || !canonical_sha256(&self.invocation_id)
1552            || !safe_group(&self.group)
1553            || !canonical_sha256(&self.companion_build_id)
1554            || !canonical_sha256(&self.raw_catalog_sha256)
1555            || !canonical_sha256(&self.raw_events_sha256)
1556            || !canonical_sha256(&self.transport_sha256)
1557        {
1558            return Err(RustdocOutcomeError::Invalid(
1559                "outcome unit has an unsupported schema or invalid identity binding".into(),
1560            ));
1561        }
1562        self.catalog.validate()?;
1563        self.report.validate()?;
1564        let transport_bytes = serde_json::to_vec(&self.transport)
1565            .map_err(|error| RustdocOutcomeError::Json(error.to_string()))?;
1566        if self.transport_sha256 != format!("{:x}", Sha256::digest(&transport_bytes)) {
1567            return Err(RustdocOutcomeError::Invalid(
1568                "rustdoc transport digest does not match its authenticated snapshot".into(),
1569            ));
1570        }
1571        let committed = u64::try_from(
1572            self.transport.observations.len()
1573                + self.transport.ordinal_hits.len()
1574                + self.transport.phases.len()
1575                + self.transport.thread_phases.len()
1576                + self.transport.thread_ends.len()
1577                + self.transport.test_boundaries.len(),
1578        )
1579        .map_err(|_| RustdocOutcomeError::Invalid("rustdoc transport count exceeds u64".into()))?;
1580        if committed != self.transport.committed {
1581            return Err(RustdocOutcomeError::Invalid(
1582                "rustdoc transport committed count disagrees with its records".into(),
1583            ));
1584        }
1585        let mut children = BTreeSet::new();
1586        for phase in &self.transport.phases {
1587            if !children.insert(phase.child_context_id)
1588                || rust_assertion_context_id(
1589                    phase.parent_context_id,
1590                    &phase.decision_id,
1591                    phase.invocation_nonce,
1592                )
1593                .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?
1594                    != phase.child_context_id
1595            {
1596                return Err(RustdocOutcomeError::Invalid(
1597                    "rustdoc transport contains an invalid or duplicate phase context".into(),
1598                ));
1599            }
1600        }
1601        for phase in &self.transport.thread_phases {
1602            if !children.insert(phase.child_context_id)
1603                || rust_thread_context_id(phase.parent_context_id, phase.invocation_nonce)
1604                    != phase.child_context_id
1605            {
1606                return Err(RustdocOutcomeError::Invalid(
1607                    "rustdoc transport contains an invalid or duplicate thread phase context"
1608                        .into(),
1609                ));
1610            }
1611        }
1612        Ok(())
1613    }
1614}
1615
1616pub fn rustdoc_outcome_unit_from_libtest(
1617    invocation_id: String,
1618    group: String,
1619    companion_build_id: String,
1620    raw_catalog: &[u8],
1621    raw_events: &[u8],
1622    transport: RustTransportRead,
1623) -> Result<RustdocOutcomeUnit, RustdocOutcomeError> {
1624    let catalog = RustdocExtractedCatalog::parse(raw_catalog)?;
1625    let report = parse_rustdoc_libtest_json(raw_events)
1626        .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
1627    let transport_bytes = serde_json::to_vec(&transport)
1628        .map_err(|error| RustdocOutcomeError::Json(error.to_string()))?;
1629    let unit = RustdocOutcomeUnit {
1630        schema: OUTCOME_SCHEMA.into(),
1631        invocation_id,
1632        group,
1633        companion_build_id,
1634        raw_catalog_sha256: format!("{:x}", Sha256::digest(raw_catalog)),
1635        raw_events_sha256: format!("{:x}", Sha256::digest(raw_events)),
1636        transport_sha256: format!("{:x}", Sha256::digest(&transport_bytes)),
1637        catalog,
1638        report,
1639        transport,
1640    };
1641    unit.validate()?;
1642    Ok(unit)
1643}
1644
1645/// Decode the private launcher-to-engine frame: an eight-byte big-endian
1646/// catalog length, the exact catalog bytes, then the exact libtest JSONL
1647/// bytes. The hashes in the published unit bind both compiler outputs.
1648pub fn rustdoc_outcome_unit_from_framed_input(
1649    invocation_id: String,
1650    group: String,
1651    companion_build_id: String,
1652    input: &[u8],
1653    transport: RustTransportRead,
1654) -> Result<RustdocOutcomeUnit, RustdocOutcomeError> {
1655    let length = input.get(..8).ok_or_else(|| {
1656        RustdocOutcomeError::Invalid("rustdoc outcome input has no catalog frame".into())
1657    })?;
1658    let catalog_length = usize::try_from(u64::from_be_bytes(
1659        length
1660            .try_into()
1661            .expect("checked eight-byte catalog length"),
1662    ))
1663    .map_err(|_| {
1664        RustdocOutcomeError::Invalid("rustdoc outcome catalog length exceeds this platform".into())
1665    })?;
1666    let catalog_end = 8usize.checked_add(catalog_length).ok_or_else(|| {
1667        RustdocOutcomeError::Invalid("rustdoc outcome catalog frame length overflow".into())
1668    })?;
1669    let catalog = input.get(8..catalog_end).ok_or_else(|| {
1670        RustdocOutcomeError::Invalid("rustdoc outcome catalog frame is truncated".into())
1671    })?;
1672    let events = input.get(catalog_end..).ok_or_else(|| {
1673        RustdocOutcomeError::Invalid("rustdoc outcome event frame is missing".into())
1674    })?;
1675    if catalog.is_empty() || events.is_empty() {
1676        return Err(RustdocOutcomeError::Invalid(
1677            "rustdoc outcome catalog and event frames must both be non-empty".into(),
1678        ));
1679    }
1680    rustdoc_outcome_unit_from_libtest(
1681        invocation_id,
1682        group,
1683        companion_build_id,
1684        catalog,
1685        events,
1686        transport,
1687    )
1688}
1689
1690fn outcome_io(path: &Path, error: impl std::fmt::Display) -> RustdocOutcomeError {
1691    RustdocOutcomeError::Io {
1692        path: path.to_path_buf(),
1693        reason: error.to_string(),
1694    }
1695}
1696
1697fn validate_outcome_directory(directory: &Path) -> Result<(), RustdocOutcomeError> {
1698    let metadata = fs::symlink_metadata(directory).map_err(|error| outcome_io(directory, error))?;
1699    if !metadata.file_type().is_dir() {
1700        return Err(outcome_io(
1701            directory,
1702            "rustdoc outcome destination is not a non-symlink directory",
1703        ));
1704    }
1705    Ok(())
1706}
1707
1708pub fn reserve_rustdoc_transport(
1709    directory: &Path,
1710    invocation_id: &str,
1711) -> Result<RustdocTransportReservation, RustdocOutcomeError> {
1712    validate_outcome_directory(directory)?;
1713    if !canonical_sha256(invocation_id) {
1714        return Err(RustdocOutcomeError::Invalid(
1715            "rustdoc transport invocation identity is invalid".into(),
1716        ));
1717    }
1718    let path = directory.join(format!("doctest-transport-{invocation_id}.mmap"));
1719    let mut token = [0_u8; RUSTDOC_TRANSPORT_TOKEN_BYTES];
1720    getrandom::fill(&mut token).map_err(|error| RustdocOutcomeError::Io {
1721        path: path.clone(),
1722        reason: error.to_string(),
1723    })?;
1724    create_rust_transport(
1725        &path,
1726        token,
1727        DEFAULT_DESCRIPTOR_CAPACITY,
1728        DEFAULT_PAYLOAD_CAPACITY,
1729    )
1730    .map_err(|error| outcome_io(&path, error))?;
1731    Ok(RustdocTransportReservation { path, token })
1732}
1733
1734pub fn rustdoc_transport_token_hex(token: &[u8; RUSTDOC_TRANSPORT_TOKEN_BYTES]) -> String {
1735    token.iter().map(|byte| format!("{byte:02x}")).collect()
1736}
1737
1738pub fn read_reserved_rustdoc_transport(
1739    path: &Path,
1740    token_hex: &str,
1741) -> Result<RustTransportRead, RustdocOutcomeError> {
1742    if token_hex.len() != RUSTDOC_TRANSPORT_TOKEN_BYTES * 2
1743        || !token_hex.bytes().all(|byte| byte.is_ascii_hexdigit())
1744    {
1745        return Err(outcome_io(path, "rustdoc transport token is invalid"));
1746    }
1747    let mut token = [0_u8; RUSTDOC_TRANSPORT_TOKEN_BYTES];
1748    for (index, byte) in token.iter_mut().enumerate() {
1749        *byte = u8::from_str_radix(&token_hex[index * 2..index * 2 + 2], 16)
1750            .map_err(|error| outcome_io(path, error))?;
1751    }
1752    read_rust_transport(path, &token).map_err(|error| outcome_io(path, error))
1753}
1754
1755pub fn publish_rustdoc_outcome_unit(
1756    directory: &Path,
1757    unit: &RustdocOutcomeUnit,
1758) -> Result<PathBuf, RustdocOutcomeError> {
1759    unit.validate()?;
1760    validate_outcome_directory(directory)?;
1761    let name = format!("doctest-outcome-{}.json", unit.invocation_id);
1762    let destination = directory.join(&name);
1763    let partial = directory.join(format!(".{name}.partial"));
1764    let bytes =
1765        serde_json::to_vec(unit).map_err(|error| RustdocOutcomeError::Json(error.to_string()))?;
1766    let publication = (|| {
1767        if fs::symlink_metadata(&destination).is_ok() {
1768            return Err(outcome_io(&destination, "outcome unit already exists"));
1769        }
1770        let mut options = OpenOptions::new();
1771        options.create_new(true).write(true);
1772        #[cfg(unix)]
1773        {
1774            use std::os::unix::fs::OpenOptionsExt as _;
1775            options.mode(0o600);
1776        }
1777        let mut output = options
1778            .open(&partial)
1779            .map_err(|error| outcome_io(&partial, error))?;
1780        output
1781            .write_all(&bytes)
1782            .and_then(|()| output.sync_all())
1783            .map_err(|error| outcome_io(&partial, error))?;
1784        if fs::symlink_metadata(&destination).is_ok() {
1785            return Err(outcome_io(
1786                &destination,
1787                "outcome unit appeared during publication",
1788            ));
1789        }
1790        fs::rename(&partial, &destination).map_err(|error| outcome_io(&destination, error))?;
1791        crate::lifecycle::sync_directory_handle(directory)
1792            .map_err(|error| outcome_io(directory, error))?;
1793        Ok(destination.clone())
1794    })();
1795    if publication.is_err() {
1796        let _ = fs::remove_file(&partial);
1797    }
1798    publication
1799}
1800
1801pub fn read_rustdoc_outcome_units(
1802    directory: &Path,
1803) -> Result<Vec<RustdocOutcomeUnit>, RustdocOutcomeError> {
1804    validate_outcome_directory(directory)?;
1805    let mut units = BTreeMap::new();
1806    for entry in fs::read_dir(directory)
1807        .map_err(|error| outcome_io(directory, error))?
1808        .collect::<Result<Vec<_>, _>>()
1809        .map_err(|error| outcome_io(directory, error))?
1810    {
1811        let path = entry.path();
1812        let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
1813            return Err(RustdocOutcomeError::Invalid(
1814                "rustdoc outcome directory contains a non-UTF-8 name".into(),
1815            ));
1816        };
1817        let relevant =
1818            name.starts_with("doctest-outcome-") || name.starts_with(".doctest-outcome-");
1819        if !relevant {
1820            continue;
1821        }
1822        let file_type = entry
1823            .file_type()
1824            .map_err(|error| outcome_io(&path, error))?;
1825        if !file_type.is_file() {
1826            return Err(outcome_io(
1827                &path,
1828                "rustdoc outcome artifact is not a regular file",
1829            ));
1830        }
1831        let Some(invocation_id) = name
1832            .strip_prefix("doctest-outcome-")
1833            .and_then(|name| name.strip_suffix(".json"))
1834            .filter(|identity| canonical_sha256(identity))
1835        else {
1836            return Err(RustdocOutcomeError::Invalid(format!(
1837                "unrecognized or incomplete rustdoc outcome artifact {name}"
1838            )));
1839        };
1840        let metadata = entry.metadata().map_err(|error| outcome_io(&path, error))?;
1841        if metadata.len() == 0 || metadata.len() > MAX_OUTCOME_UNIT_BYTES {
1842            return Err(RustdocOutcomeError::Invalid(format!(
1843                "rustdoc outcome artifact {name} has invalid size"
1844            )));
1845        }
1846        let bytes = fs::read(&path).map_err(|error| outcome_io(&path, error))?;
1847        let unit: RustdocOutcomeUnit = serde_json::from_slice(&bytes)
1848            .map_err(|error| RustdocOutcomeError::Json(format!("{name}: {error}")))?;
1849        unit.validate()?;
1850        if unit.invocation_id != invocation_id {
1851            return Err(RustdocOutcomeError::Invalid(format!(
1852                "rustdoc outcome filename does not match {}",
1853                unit.invocation_id
1854            )));
1855        }
1856        if units.insert(invocation_id.to_owned(), unit).is_some() {
1857            return Err(RustdocOutcomeError::Invalid(format!(
1858                "duplicate rustdoc outcome invocation {invocation_id}"
1859            )));
1860        }
1861    }
1862    Ok(units.into_values().collect())
1863}
1864
1865/// Join rustdoc's authoritative extracted catalog, compiler-described merged
1866/// doctests and authenticated libtest outcomes without guessing identities.
1867///
1868/// The extracted catalog names every merged, standalone and compile-fail
1869/// doctest. The compiler map is required only for merged source/probe identity
1870/// translation. Libtest's filtered and fail-fast-unstarted counts are
1871/// aggregate-only; when both are non-zero the affected catalog entries remain
1872/// explicitly ambiguous instead of receiving an invented status.
1873pub fn join_rustdoc_outcomes(
1874    merged_units: Vec<RustdocMergedUnit>,
1875    outcome_units: Vec<RustdocOutcomeUnit>,
1876) -> Result<RustdocOutcomeResolution, RustdocOutcomeError> {
1877    let mut maps = BTreeMap::new();
1878    for unit in merged_units {
1879        unit.map
1880            .validate()
1881            .map_err(|error| RustdocOutcomeError::Invalid(error.to_string()))?;
1882        let group = unit.map.group.clone();
1883        if maps.insert(group.clone(), unit).is_some() {
1884            return Err(RustdocOutcomeError::Invalid(format!(
1885                "duplicate merged rustdoc outcome group {group}"
1886            )));
1887        }
1888    }
1889
1890    let mut outcomes = BTreeMap::new();
1891    for unit in outcome_units {
1892        unit.validate()?;
1893        let group = unit.group.clone();
1894        if outcomes.insert(group.clone(), unit).is_some() {
1895            return Err(RustdocOutcomeError::Invalid(format!(
1896                "more than one rustdoc outcome invocation uses group {group}"
1897            )));
1898        }
1899    }
1900
1901    let mut groups = Vec::new();
1902    for (group, outcome_unit) in outcomes {
1903        let map_unit = maps.remove(&group);
1904        let catalog_count = u64::try_from(outcome_unit.catalog.doctests.len()).map_err(|_| {
1905            RustdocOutcomeError::Invalid(format!(
1906                "rustdoc catalog for {group} exceeds the supported test count"
1907            ))
1908        })?;
1909        let reported_count = outcome_unit
1910            .report
1911            .planned_tests
1912            .checked_add(outcome_unit.report.filtered_out)
1913            .ok_or_else(|| {
1914                RustdocOutcomeError::Invalid(format!(
1915                    "rustdoc catalog arithmetic overflow for {group}"
1916                ))
1917            })?;
1918        if catalog_count != reported_count {
1919            return Err(RustdocOutcomeError::Invalid(format!(
1920                "rustdoc catalog for {group} has {catalog_count} tests but libtest accounted for {reported_count}"
1921            )));
1922        }
1923
1924        let mut merged_entries = BTreeMap::new();
1925        let join = if let Some(map_unit) = map_unit {
1926            for entry in &map_unit.map.entries {
1927                let catalog = outcome_unit
1928                    .catalog
1929                    .doctests
1930                    .iter()
1931                    .find(|candidate| candidate.name == entry.display_name)
1932                    .ok_or_else(|| {
1933                        RustdocOutcomeError::Invalid(format!(
1934                            "merged doctest {} is absent from rustdoc's catalog",
1935                            entry.display_name
1936                        ))
1937                    })?;
1938                if catalog.file != entry.path
1939                    || catalog.line != entry.line
1940                    || catalog.ignored() != entry.ignored
1941                    || catalog.doctest_attributes.no_run != entry.no_run
1942                    || catalog.doctest_attributes.should_panic != entry.should_panic
1943                    || catalog.doctest_attributes.compile_fail
1944                    || catalog.doctest_attributes.standalone_crate
1945                {
1946                    return Err(RustdocOutcomeError::Invalid(format!(
1947                        "merged compiler descriptor disagrees with rustdoc's catalog for {}",
1948                        entry.display_name
1949                    )));
1950                }
1951                if merged_entries
1952                    .insert(entry.display_name.clone(), entry.clone())
1953                    .is_some()
1954                {
1955                    return Err(RustdocOutcomeError::Invalid(format!(
1956                        "duplicate merged catalog binding for {}",
1957                        entry.display_name
1958                    )));
1959                }
1960            }
1961            map_unit.join
1962        } else {
1963            None
1964        };
1965
1966        let mut terminal = outcome_unit
1967            .report
1968            .outcomes
1969            .iter()
1970            .cloned()
1971            .map(|outcome| (outcome.display_name.clone(), outcome))
1972            .collect::<BTreeMap<_, _>>();
1973        let mut unfinished = outcome_unit
1974            .report
1975            .unfinished_started
1976            .iter()
1977            .cloned()
1978            .collect::<BTreeSet<_>>();
1979        let mut entries = Vec::with_capacity(outcome_unit.catalog.doctests.len());
1980        for (catalog_index, catalog) in outcome_unit.catalog.doctests.into_iter().enumerate() {
1981            let state = if let Some(outcome) = terminal.remove(&catalog.name) {
1982                RustdocJoinedOutcomeState::Completed { outcome }
1983            } else if unfinished.remove(&catalog.name) {
1984                RustdocJoinedOutcomeState::UnfinishedStarted
1985            } else if outcome_unit.report.filtered_out == 0 {
1986                RustdocJoinedOutcomeState::Unstarted
1987            } else if outcome_unit.report.unstarted_tests == 0 {
1988                RustdocJoinedOutcomeState::FilteredOut
1989            } else {
1990                RustdocJoinedOutcomeState::NotRunAmbiguous
1991            };
1992            let catalog_index = u64::try_from(catalog_index).map_err(|_| {
1993                RustdocOutcomeError::Invalid(format!(
1994                    "rustdoc catalog index exceeds u64 for {}",
1995                    catalog.name
1996                ))
1997            })?;
1998            let merged_entry = merged_entries.remove(&catalog.name);
1999            entries.push(RustdocJoinedOutcome {
2000                catalog_index,
2001                catalog,
2002                merged_entry,
2003                state,
2004            });
2005        }
2006        if !terminal.is_empty() || !unfinished.is_empty() || !merged_entries.is_empty() {
2007            return Err(RustdocOutcomeError::Invalid(format!(
2008                "rustdoc outcomes or compiler descriptors for {group} contain identities absent from the authoritative catalog"
2009            )));
2010        }
2011        let unnamed_count = entries
2012            .iter()
2013            .filter(|entry| {
2014                matches!(
2015                    entry.state,
2016                    RustdocJoinedOutcomeState::Unstarted
2017                        | RustdocJoinedOutcomeState::FilteredOut
2018                        | RustdocJoinedOutcomeState::NotRunAmbiguous
2019                )
2020            })
2021            .count();
2022        let expected_unnamed = outcome_unit
2023            .report
2024            .filtered_out
2025            .checked_add(outcome_unit.report.unstarted_tests)
2026            .and_then(|count| usize::try_from(count).ok())
2027            .ok_or_else(|| {
2028                RustdocOutcomeError::Invalid(format!(
2029                    "rustdoc unresolved outcome count exceeds this platform for {group}"
2030                ))
2031            })?;
2032        if unnamed_count != expected_unnamed {
2033            return Err(RustdocOutcomeError::Invalid(format!(
2034                "rustdoc unresolved catalog count disagrees with libtest for {group}"
2035            )));
2036        }
2037        let joined_group = RustdocOutcomeGroupJoin {
2038            invocation_id: outcome_unit.invocation_id,
2039            group,
2040            companion_build_id: outcome_unit.companion_build_id,
2041            raw_catalog_sha256: outcome_unit.raw_catalog_sha256,
2042            raw_events_sha256: outcome_unit.raw_events_sha256,
2043            transport_sha256: outcome_unit.transport_sha256,
2044            join,
2045            transport: outcome_unit.transport,
2046            entries,
2047            ambiguous_filtered_out: outcome_unit.report.filtered_out
2048                * u64::from(outcome_unit.report.unstarted_tests != 0),
2049            ambiguous_unstarted_tests: outcome_unit.report.unstarted_tests
2050                * u64::from(outcome_unit.report.filtered_out != 0),
2051        };
2052        joined_group.validate_transport_ownership()?;
2053        groups.push(joined_group);
2054    }
2055
2056    let unmatched_maps = maps.into_values().collect();
2057
2058    Ok(RustdocOutcomeResolution {
2059        groups,
2060        unmatched_maps,
2061    })
2062}
2063
2064impl RustdocMergedJoin {
2065    /// Translate transport records emitted before rustdoc's merged runner made
2066    /// final authored identities available. Assertion context IDs are derived
2067    /// from decision IDs, so translating a decision also requires rebuilding
2068    /// its complete nested phase chain and every record that refers to it.
2069    pub fn translate_transport(
2070        &self,
2071        base_context_id: u64,
2072        read: &RustTransportRead,
2073    ) -> Result<RustTransportRead, RustdocJoinError> {
2074        validate_rust_phase_contexts(base_context_id, read)
2075            .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?;
2076
2077        let definitions = rustdoc_phase_definitions(read)
2078            .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?;
2079        let mut translated_contexts = BTreeMap::from([(base_context_id, base_context_id)]);
2080        let mut visiting = BTreeSet::new();
2081        fn translate_context(
2082            context: u64,
2083            base: u64,
2084            definitions: &BTreeMap<u64, RustdocPhaseDefinition<'_>>,
2085            obligation_ids: &BTreeMap<String, String>,
2086            translated: &mut BTreeMap<u64, u64>,
2087            visiting: &mut BTreeSet<u64>,
2088        ) -> Result<u64, RustdocJoinError> {
2089            if context == 0 || context == base {
2090                return Ok(context);
2091            }
2092            if let Some(translated) = translated.get(&context) {
2093                return Ok(*translated);
2094            }
2095            if !visiting.insert(context) {
2096                return Err(RustdocJoinError::Invalid(format!(
2097                    "merged doctest assertion context cycle at {context:016x}"
2098                )));
2099            }
2100            let phase = definitions.get(&context).ok_or_else(|| {
2101                RustdocJoinError::Invalid(format!(
2102                    "merged doctest context {context:016x} has no phase definition"
2103                ))
2104            })?;
2105            let parent = translate_context(
2106                phase.parent_context_id(),
2107                base,
2108                definitions,
2109                obligation_ids,
2110                translated,
2111                visiting,
2112            )?;
2113            let final_context = match phase {
2114                RustdocPhaseDefinition::Assertion(phase) => {
2115                    let decision = obligation_ids
2116                        .get(&phase.decision_id)
2117                        .map_or(phase.decision_id.as_str(), String::as_str);
2118                    rust_assertion_context_id(parent, decision, phase.invocation_nonce)
2119                        .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?
2120                }
2121                RustdocPhaseDefinition::Thread(phase) => {
2122                    rust_thread_context_id(parent, phase.invocation_nonce)
2123                }
2124            };
2125            visiting.remove(&context);
2126            translated.insert(context, final_context);
2127            Ok(final_context)
2128        }
2129        for context in definitions.keys() {
2130            translate_context(
2131                *context,
2132                base_context_id,
2133                &definitions,
2134                &self.obligation_ids,
2135                &mut translated_contexts,
2136                &mut visiting,
2137            )?;
2138        }
2139
2140        let translate_record_context = |context: u64| {
2141            if context == 0 {
2142                Ok(0)
2143            } else {
2144                translated_contexts.get(&context).copied().ok_or_else(|| {
2145                    RustdocJoinError::Invalid(format!(
2146                        "merged doctest record context {context:016x} was not translated"
2147                    ))
2148                })
2149            }
2150        };
2151        let mut translated = read.clone();
2152        for observation in &mut translated.observations {
2153            observation.context_id = translate_record_context(observation.context_id)?;
2154            let id = match &mut observation.observation {
2155                RustProbeObservation::Hit { id, .. }
2156                | RustProbeObservation::Decision { id, .. }
2157                | RustProbeObservation::Assertion { id, .. } => id,
2158            };
2159            if let Some(final_id) = self.obligation_ids.get(id) {
2160                *id = final_id.clone();
2161            }
2162        }
2163        for hit in &mut translated.ordinal_hits {
2164            hit.context_id = translate_record_context(hit.context_id)?;
2165            let old = hit.ordinal.to_string();
2166            if let Some(final_ordinal) = self.probe_ordinals.get(&old) {
2167                hit.ordinal = final_ordinal.parse::<u64>().map_err(|_| {
2168                    RustdocJoinError::Invalid(format!(
2169                        "merged doctest final probe ordinal {final_ordinal} is invalid"
2170                    ))
2171                })?;
2172            }
2173        }
2174        for phase in &mut translated.phases {
2175            phase.child_context_id = translate_record_context(phase.child_context_id)?;
2176            phase.parent_context_id = translate_record_context(phase.parent_context_id)?;
2177            if let Some(final_id) = self.obligation_ids.get(&phase.decision_id) {
2178                phase.decision_id = final_id.clone();
2179            }
2180        }
2181        for phase in &mut translated.thread_phases {
2182            phase.child_context_id = translate_record_context(phase.child_context_id)?;
2183            phase.parent_context_id = translate_record_context(phase.parent_context_id)?;
2184        }
2185        for end in &mut translated.thread_ends {
2186            end.context_id = translate_record_context(end.context_id)?;
2187        }
2188        for boundary in &mut translated.test_boundaries {
2189            boundary.context_id = translate_record_context(boundary.context_id)?;
2190        }
2191        let unique_phases = translated
2192            .phases
2193            .iter()
2194            .map(|phase| phase.child_context_id)
2195            .chain(
2196                translated
2197                    .thread_phases
2198                    .iter()
2199                    .map(|phase| phase.child_context_id),
2200            )
2201            .collect::<BTreeSet<_>>();
2202        if unique_phases.len() != translated.phases.len() + translated.thread_phases.len() {
2203            return Err(RustdocJoinError::Invalid(
2204                "merged doctest assertion contexts collided after identity translation".into(),
2205            ));
2206        }
2207        validate_rust_phase_contexts(base_context_id, &translated)
2208            .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?;
2209        Ok(translated)
2210    }
2211}
2212
2213/// Parse an entire compiler-output generation and resolve every deferred
2214/// merged-doctest candidate before ordinary workspace normalization. Normal
2215/// candidates provide immutable authored source snapshots; a pending bundle
2216/// must match exactly one runner map, while a map without a pending bundle is
2217/// retained because the represented test may have no source obligations.
2218pub fn resolve_merged_doctest_candidates(
2219    raw_pairs: Vec<(Vec<u8>, Vec<u8>)>,
2220    raw_maps: Vec<Vec<u8>>,
2221) -> Result<RustdocResolvedCandidates, RustdocJoinError> {
2222    let mut maps = BTreeMap::<String, RustdocMergedMap>::new();
2223    for raw in raw_maps {
2224        let map = RustdocMergedMap::parse(&raw)?;
2225        if maps.insert(map.group.clone(), map).is_some() {
2226            return Err(RustdocJoinError::Invalid(
2227                "compiler output contains duplicate merged-doctest groups".into(),
2228            ));
2229        }
2230    }
2231
2232    struct Pending {
2233        group: String,
2234        manifest: Vec<u8>,
2235        sources: Vec<u8>,
2236    }
2237    let mut candidates = Vec::new();
2238    let mut pending = Vec::new();
2239    let mut authored_sources = BTreeMap::<String, RustCompilerSource>::new();
2240    for (manifest_bytes, source_bytes) in raw_pairs {
2241        let ordinary_manifest = RustCompilerManifest::parse(&manifest_bytes);
2242        let ordinary_sources = RustCompilerSourceSnapshots::parse(&source_bytes);
2243        if let (Ok(manifest), Ok(sources)) = (ordinary_manifest, ordinary_sources) {
2244            if manifest.crate_name != sources.crate_name {
2245                return Err(RustdocJoinError::Manifest(format!(
2246                    "compiler manifest/source identity differs for {}",
2247                    manifest.crate_name
2248                )));
2249            }
2250            for (key, source) in &sources.sources {
2251                if authored_sources
2252                    .insert(key.clone(), source.clone())
2253                    .is_some_and(|existing| existing != *source)
2254                {
2255                    return Err(RustdocJoinError::Invalid(format!(
2256                        "authored compiler source {key} changed across units"
2257                    )));
2258                }
2259            }
2260            candidates.push((manifest, sources));
2261            continue;
2262        }
2263
2264        let matching = maps
2265            .keys()
2266            .filter(|group| {
2267                RustCompilerManifest::parse_pending_doctest(&manifest_bytes, group).is_ok()
2268                    && RustCompilerSourceSnapshots::parse_pending_doctest(&source_bytes, group)
2269                        .is_ok()
2270            })
2271            .cloned()
2272            .collect::<Vec<_>>();
2273        let [group] = matching.as_slice() else {
2274            return Err(RustdocJoinError::Invalid(format!(
2275                "compiler candidate matches {} merged-doctest maps instead of exactly one",
2276                matching.len()
2277            )));
2278        };
2279        if pending
2280            .iter()
2281            .any(|candidate: &Pending| candidate.group == *group)
2282        {
2283            return Err(RustdocJoinError::Invalid(format!(
2284                "merged-doctest group {group} has more than one pending bundle"
2285            )));
2286        }
2287        pending.push(Pending {
2288            group: group.clone(),
2289            manifest: manifest_bytes,
2290            sources: source_bytes,
2291        });
2292    }
2293
2294    let mut joined_by_group = BTreeMap::new();
2295    for pending in pending {
2296        let map = maps
2297            .get(&pending.group)
2298            .expect("pending group was selected from parsed maps");
2299        let encoded_map =
2300            serde_json::to_vec(map).map_err(|error| RustdocJoinError::Json(error.to_string()))?;
2301        let joined = join_merged_doctest(
2302            &pending.manifest,
2303            &pending.sources,
2304            &encoded_map,
2305            &authored_sources,
2306        )?;
2307        candidates.push((joined.manifest.clone(), joined.sources.clone()));
2308        joined_by_group.insert(pending.group, joined);
2309    }
2310    candidates.sort_by(|left, right| {
2311        left.0.crate_name.cmp(&right.0.crate_name).then_with(|| {
2312            left.0
2313                .points
2314                .first()
2315                .map(|point| &point.id)
2316                .cmp(&right.0.points.first().map(|point| &point.id))
2317        })
2318    });
2319    let merged_units = maps
2320        .into_iter()
2321        .map(|(group, map)| RustdocMergedUnit {
2322            map,
2323            join: joined_by_group.remove(&group),
2324        })
2325        .collect();
2326    Ok(RustdocResolvedCandidates {
2327        candidates,
2328        merged_units,
2329    })
2330}
2331
2332#[derive(Debug, Clone, PartialEq, Eq)]
2333pub enum RustdocJoinError {
2334    Json(String),
2335    Manifest(String),
2336    Invalid(String),
2337}
2338
2339impl std::fmt::Display for RustdocJoinError {
2340    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2341        match self {
2342            Self::Json(reason) => write!(formatter, "invalid merged rustdoc map JSON: {reason}"),
2343            Self::Manifest(reason) => {
2344                write!(
2345                    formatter,
2346                    "invalid merged rustdoc compiler manifest: {reason}"
2347                )
2348            }
2349            Self::Invalid(reason) => write!(formatter, "invalid merged rustdoc join: {reason}"),
2350        }
2351    }
2352}
2353
2354impl std::error::Error for RustdocJoinError {}
2355
2356fn safe_group(value: &str) -> bool {
2357    !value.is_empty()
2358        && value
2359            .bytes()
2360            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
2361}
2362
2363fn module_index(value: &str) -> Option<u64> {
2364    value.strip_prefix("__doctest_")?.parse::<u64>().ok()
2365}
2366
2367fn normalized_relative_path(value: &str) -> bool {
2368    !value.is_empty()
2369        && !value.starts_with('/')
2370        && !value.contains('\\')
2371        && value
2372            .split('/')
2373            .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
2374}
2375
2376impl RustdocMergedMap {
2377    pub fn parse(bytes: &[u8]) -> Result<Self, RustdocJoinError> {
2378        let map: Self = serde_json::from_slice(bytes)
2379            .map_err(|error| RustdocJoinError::Json(error.to_string()))?;
2380        map.validate()?;
2381        Ok(map)
2382    }
2383
2384    pub fn validate(&self) -> Result<(), RustdocJoinError> {
2385        if self.schema != MAP_SCHEMA || !safe_group(&self.group) || self.entries.is_empty() {
2386            return Err(RustdocJoinError::Invalid(
2387                "schema, group and at least one entry are required".into(),
2388            ));
2389        }
2390        let mut modules = BTreeSet::new();
2391        let mut display_names = BTreeSet::new();
2392        let mut source_sites = BTreeSet::new();
2393        let mut previous = None;
2394        for entry in &self.entries {
2395            let Some(index) = module_index(&entry.module) else {
2396                return Err(RustdocJoinError::Invalid(format!(
2397                    "invalid merged doctest module {}",
2398                    entry.module
2399                )));
2400            };
2401            if previous.is_some_and(|previous| previous >= index) {
2402                return Err(RustdocJoinError::Invalid(
2403                    "merged doctest entries are not in numeric module order".into(),
2404                ));
2405            }
2406            previous = Some(index);
2407            if !modules.insert(entry.module.as_str())
2408                || !display_names.insert(entry.display_name.as_str())
2409                || !source_sites.insert((entry.path.as_str(), entry.line))
2410                || !normalized_relative_path(&entry.path)
2411                || entry.line == 0
2412                || entry.display_name.trim().is_empty()
2413                || entry.display_name.chars().any(char::is_control)
2414            {
2415                return Err(RustdocJoinError::Invalid(format!(
2416                    "malformed merged doctest entry {}",
2417                    entry.module
2418                )));
2419            }
2420        }
2421        Ok(())
2422    }
2423
2424    pub fn entry(&self, module: &str) -> Result<&RustdocMergedEntry, RustdocJoinError> {
2425        self.entries
2426            .iter()
2427            .find(|entry| entry.module == module)
2428            .ok_or_else(|| {
2429                RustdocJoinError::Invalid(format!(
2430                    "pending bundle module {module} has no runner descriptor"
2431                ))
2432            })
2433    }
2434
2435    fn next_line_for(&self, entry: &RustdocMergedEntry) -> Option<u64> {
2436        self.entries
2437            .iter()
2438            .filter(|candidate| candidate.path == entry.path && candidate.line > entry.line)
2439            .map(|candidate| candidate.line)
2440            .min()
2441    }
2442}
2443
2444fn source_lines(source: &str) -> Vec<(u64, usize, &str)> {
2445    let mut offset = 0;
2446    source
2447        .split_inclusive('\n')
2448        .enumerate()
2449        .map(|(index, line)| {
2450            let record = (index as u64 + 1, offset, line);
2451            offset += line.len();
2452            record
2453        })
2454        .collect()
2455}
2456
2457#[derive(Clone, Copy)]
2458struct ExtractedLine<'a> {
2459    start: usize,
2460    end: usize,
2461    source: &'a str,
2462}
2463
2464fn extracted_module_lines<'a>(
2465    bundle_source: &'a str,
2466    module: &str,
2467) -> Result<Vec<ExtractedLine<'a>>, RustdocJoinError> {
2468    let parsed = SourceFile::parse(bundle_source, Edition::CURRENT);
2469    if !parsed.errors().is_empty() {
2470        return Err(RustdocJoinError::Invalid(format!(
2471            "merged bundle does not parse as Rust: {}",
2472            parsed
2473                .errors()
2474                .iter()
2475                .map(ToString::to_string)
2476                .collect::<Vec<_>>()
2477                .join("; ")
2478        )));
2479    }
2480    let tree = parsed.tree();
2481    let modules = tree
2482        .syntax()
2483        .descendants()
2484        .filter_map(ast::Module::cast)
2485        .filter(|candidate| candidate.name().is_some_and(|name| name.text() == module))
2486        .collect::<Vec<_>>();
2487    let [module_node] = modules.as_slice() else {
2488        return Err(RustdocJoinError::Invalid(format!(
2489            "merged bundle contains {} modules named {module}",
2490            modules.len()
2491        )));
2492    };
2493    let functions = module_node
2494        .syntax()
2495        .descendants()
2496        .filter_map(ast::Fn::cast)
2497        .filter(|function| {
2498            function.name().is_some_and(|name| name.text() == "main")
2499                && function
2500                    .syntax()
2501                    .ancestors()
2502                    .skip(1)
2503                    .find_map(ast::Module::cast)
2504                    .as_ref()
2505                    == Some(module_node)
2506        })
2507        .collect::<Vec<_>>();
2508    let [function] = functions.as_slice() else {
2509        return Err(RustdocJoinError::Invalid(format!(
2510            "merged module {module} contains {} direct main functions",
2511            functions.len()
2512        )));
2513    };
2514    let body = function.body().ok_or_else(|| {
2515        RustdocJoinError::Invalid(format!("merged module {module} main has no body"))
2516    })?;
2517    let range = body.syntax().text_range();
2518    let body_start = usize::from(range.start());
2519    let body_end = usize::from(range.end());
2520    if body_end <= body_start + 1
2521        || bundle_source.as_bytes().get(body_start) != Some(&b'{')
2522        || bundle_source.as_bytes().get(body_end - 1) != Some(&b'}')
2523    {
2524        return Err(RustdocJoinError::Invalid(format!(
2525            "merged module {module} main has an invalid syntax range"
2526        )));
2527    }
2528    let content_start = body_start + 1;
2529    let content = &bundle_source[content_start..body_end - 1];
2530    let mut offset = content_start;
2531    let lines = content
2532        .split_inclusive('\n')
2533        .filter_map(|line| {
2534            let source = line.strip_suffix('\n').unwrap_or(line);
2535            let record = (!source.trim().is_empty()).then_some(ExtractedLine {
2536                start: offset,
2537                end: offset + source.len(),
2538                source,
2539            });
2540            offset += line.len();
2541            record
2542        })
2543        .collect::<Vec<_>>();
2544    if lines.is_empty() {
2545        return Err(RustdocJoinError::Invalid(format!(
2546            "merged module {module} main has no extracted source lines"
2547        )));
2548    }
2549    Ok(lines)
2550}
2551
2552/// Map one exact extracted range to its authored source. Its nonblank lines
2553/// must have exactly one complete, ordered mapping inside that doctest's
2554/// runner-bounded source interval. Repeated fragments are valid when their
2555/// sequence identifies one mapping; genuinely ambiguous sequences fail closed.
2556pub fn map_merged_range(
2557    map: &RustdocMergedMap,
2558    module: &str,
2559    bundle_source: &str,
2560    pending_start: u32,
2561    pending_end: u32,
2562    authored_source: &str,
2563) -> Result<RustdocMappedRange, RustdocJoinError> {
2564    map.validate()?;
2565    let entry = map.entry(module)?;
2566    let start = pending_start as usize;
2567    let end = pending_end as usize;
2568    if start >= end
2569        || end > bundle_source.len()
2570        || !bundle_source.is_char_boundary(start)
2571        || !bundle_source.is_char_boundary(end)
2572    {
2573        return Err(RustdocJoinError::Invalid(format!(
2574            "pending range {pending_start}..{pending_end} is outside UTF-8 bundle bytes"
2575        )));
2576    }
2577    if bundle_source[start..end].contains('\r') {
2578        return Err(RustdocJoinError::Invalid(
2579            "carriage-return extracted source is unsupported".into(),
2580        ));
2581    }
2582    let next_line = map.next_line_for(entry).unwrap_or(u64::MAX);
2583    let authored_lines = source_lines(authored_source);
2584    let extracted_lines = extracted_module_lines(bundle_source, module)?;
2585    let candidates = extracted_lines
2586        .iter()
2587        .map(|extracted| {
2588            authored_lines
2589                .iter()
2590                .filter(|(line, _, _)| *line >= entry.line && *line < next_line)
2591                .flat_map(|(line, offset, authored_line)| {
2592                    authored_line
2593                        .match_indices(extracted.source)
2594                        .map(move |(column, _)| (*line, *offset + column, extracted.source.len()))
2595                })
2596                .collect::<Vec<_>>()
2597        })
2598        .collect::<Vec<_>>();
2599    if candidates.iter().any(Vec::is_empty) {
2600        return Err(RustdocJoinError::Invalid(format!(
2601            "merged fragment has no authored match in {}:{}",
2602            entry.path, entry.line
2603        )));
2604    }
2605    fn ordered_sequences(
2606        candidates: &[Vec<(u64, usize, usize)>],
2607        index: usize,
2608        previous_line: Option<u64>,
2609        current: &mut Vec<(u64, usize, usize)>,
2610        solutions: &mut Vec<Vec<(u64, usize, usize)>>,
2611    ) {
2612        if solutions.len() > 1 {
2613            return;
2614        }
2615        if index == candidates.len() {
2616            solutions.push(current.clone());
2617            return;
2618        }
2619        for candidate in &candidates[index] {
2620            if previous_line.is_some_and(|previous| previous >= candidate.0) {
2621                continue;
2622            }
2623            current.push(*candidate);
2624            ordered_sequences(candidates, index + 1, Some(candidate.0), current, solutions);
2625            current.pop();
2626            if solutions.len() > 1 {
2627                return;
2628            }
2629        }
2630    }
2631    let mut solutions = Vec::new();
2632    ordered_sequences(&candidates, 0, None, &mut Vec::new(), &mut solutions);
2633    let [anchors] = solutions.as_slice() else {
2634        return Err(RustdocJoinError::Invalid(format!(
2635            "merged fragments have {} ordered authored mappings in {}:{}",
2636            solutions.len(),
2637            entry.path,
2638            entry.line
2639        )));
2640    };
2641    let start_line = extracted_lines
2642        .iter()
2643        .position(|line| start >= line.start && start < line.end)
2644        .ok_or_else(|| {
2645            RustdocJoinError::Invalid(format!(
2646                "pending range start {pending_start} is outside extracted source lines"
2647            ))
2648        })?;
2649    let end_line = extracted_lines
2650        .iter()
2651        .position(|line| end > line.start && end <= line.end)
2652        .ok_or_else(|| {
2653            RustdocJoinError::Invalid(format!(
2654                "pending range end {pending_end} is outside extracted source lines"
2655            ))
2656        })?;
2657    if start_line > end_line {
2658        return Err(RustdocJoinError::Invalid(
2659            "pending range crosses extracted lines in reverse order".into(),
2660        ));
2661    }
2662    let authored_start = anchors[start_line]
2663        .1
2664        .checked_add(start - extracted_lines[start_line].start)
2665        .ok_or_else(|| RustdocJoinError::Invalid("authored source offset overflow".into()))?;
2666    let authored_end = anchors[end_line]
2667        .1
2668        .checked_add(end - extracted_lines[end_line].start)
2669        .ok_or_else(|| RustdocJoinError::Invalid("authored source offset overflow".into()))?;
2670    Ok(RustdocMappedRange {
2671        source_key: format!("source:{}", entry.path),
2672        start: u32::try_from(authored_start)
2673            .map_err(|_| RustdocJoinError::Invalid("authored start exceeds u32".into()))?,
2674        end: u32::try_from(authored_end)
2675            .map_err(|_| RustdocJoinError::Invalid("authored end exceeds u32".into()))?,
2676    })
2677}
2678
2679/// Produce the frozen identity for a non-synthetic authored/doctest
2680/// obligation after deferred source mapping.
2681pub fn rust_source_identity(
2682    kind: &str,
2683    source: &RustdocMappedRange,
2684    discriminator: &str,
2685) -> Result<RustSourceIdentity, RustdocJoinError> {
2686    if !matches!(
2687        kind,
2688        "statement" | "function" | "branch" | "branch-alternative" | "decision" | "match-group"
2689    ) || !source.source_key.starts_with("source:")
2690        || !normalized_relative_path(&source.source_key["source:".len()..])
2691        || source.start >= source.end
2692    {
2693        return Err(RustdocJoinError::Invalid(
2694            "invalid final Rust source identity input".into(),
2695        ));
2696    }
2697    identity_for_range(kind, source, discriminator)
2698}
2699
2700fn identity_for_range(
2701    kind: &str,
2702    source: &RustdocMappedRange,
2703    discriminator: &str,
2704) -> Result<RustSourceIdentity, RustdocJoinError> {
2705    if !matches!(
2706        kind,
2707        "statement" | "function" | "branch" | "branch-alternative" | "decision" | "match-group"
2708    ) || source.start >= source.end
2709        || source.source_key.chars().any(char::is_control)
2710        || discriminator.chars().any(char::is_control)
2711    {
2712        return Err(RustdocJoinError::Invalid(
2713            "invalid Rust source identity components".into(),
2714        ));
2715    }
2716    let canonical = format!(
2717        "{SOURCE_MODEL}\0{kind}\0{}\0{}\0{}\0{discriminator}\0",
2718        source.source_key, source.start, source.end
2719    );
2720    identity_from_canonical(kind, canonical)
2721}
2722
2723fn identity_from_canonical(
2724    kind: &str,
2725    canonical: String,
2726) -> Result<RustSourceIdentity, RustdocJoinError> {
2727    let digest = Sha256::digest(canonical.as_bytes());
2728    let encoded = digest[..12]
2729        .iter()
2730        .map(|byte| format!("{byte:02x}"))
2731        .collect::<String>();
2732    let probe_ordinal = u64::from_be_bytes(
2733        digest[..8]
2734            .try_into()
2735            .expect("a SHA-256 digest always has eight prefix bytes"),
2736    );
2737    Ok(RustSourceIdentity {
2738        id: format!("rs:{kind}:{encoded}"),
2739        canonical,
2740        probe_ordinal,
2741    })
2742}
2743
2744#[derive(Debug)]
2745struct SyntheticExpansionFrame {
2746    description: String,
2747    source: RustdocMappedRange,
2748    definition: String,
2749}
2750
2751#[derive(Debug)]
2752struct SyntheticCanonical {
2753    frames: Vec<SyntheticExpansionFrame>,
2754    definition: String,
2755    owner_ordinal: u64,
2756}
2757
2758fn canonical_u32(value: &str, field: &str) -> Result<u32, RustdocJoinError> {
2759    let parsed = value.parse::<u32>().map_err(|_| {
2760        RustdocJoinError::Invalid(format!("synthetic canonical has invalid {field}"))
2761    })?;
2762    if value != parsed.to_string() {
2763        return Err(RustdocJoinError::Invalid(format!(
2764            "synthetic canonical has non-canonical {field}"
2765        )));
2766    }
2767    Ok(parsed)
2768}
2769
2770fn canonical_u64(value: &str, field: &str) -> Result<u64, RustdocJoinError> {
2771    let parsed = value.parse::<u64>().map_err(|_| {
2772        RustdocJoinError::Invalid(format!("synthetic canonical has invalid {field}"))
2773    })?;
2774    if value != parsed.to_string() {
2775        return Err(RustdocJoinError::Invalid(format!(
2776            "synthetic canonical has non-canonical {field}"
2777        )));
2778    }
2779    Ok(parsed)
2780}
2781
2782fn parse_synthetic_canonical(
2783    canonical: &str,
2784    kind: &str,
2785    source_key: &str,
2786    start: u32,
2787    end: u32,
2788    discriminator: &str,
2789) -> Result<Option<SyntheticCanonical>, RustdocJoinError> {
2790    let parts = canonical.split('\0').collect::<Vec<_>>();
2791    if parts.get(6) != Some(&"synthetic-expansion") {
2792        return Ok(None);
2793    }
2794    if parts.last() != Some(&"")
2795        || parts.len() < 15
2796        || (parts.len() - 10) % 5 != 0
2797        || parts[0] != SOURCE_MODEL
2798        || parts[1] != kind
2799        || parts[2] != source_key
2800        || canonical_u32(parts[3], "source start")? != start
2801        || canonical_u32(parts[4], "source end")? != end
2802        || parts[5] != discriminator
2803    {
2804        return Err(RustdocJoinError::Invalid(format!(
2805            "malformed synthetic canonical for {kind}"
2806        )));
2807    }
2808    let frame_count = (parts.len() - 10) / 5;
2809    let mut frames = Vec::with_capacity(frame_count);
2810    for frame in parts[7..7 + frame_count * 5].chunks_exact(5) {
2811        if frame[0].is_empty() || frame[1].is_empty() || frame[4].is_empty() {
2812            return Err(RustdocJoinError::Invalid(
2813                "synthetic expansion frame has an empty identity component".into(),
2814            ));
2815        }
2816        frames.push(SyntheticExpansionFrame {
2817            description: frame[0].into(),
2818            source: RustdocMappedRange {
2819                source_key: frame[1].into(),
2820                start: canonical_u32(frame[2], "frame start")?,
2821                end: canonical_u32(frame[3], "frame end")?,
2822            },
2823            definition: frame[4].into(),
2824        });
2825    }
2826    let definition_index = 7 + frame_count * 5;
2827    let definition = parts[definition_index];
2828    let owner_ordinal = canonical_u64(parts[definition_index + 1], "owner ordinal")?;
2829    if definition.is_empty() {
2830        return Err(RustdocJoinError::Invalid(
2831            "synthetic canonical has an empty owner definition".into(),
2832        ));
2833    }
2834    Ok(Some(SyntheticCanonical {
2835        frames,
2836        definition: definition.into(),
2837        owner_ordinal,
2838    }))
2839}
2840
2841fn stable_definition(
2842    entry: &RustdocMergedEntry,
2843    definition: &str,
2844) -> Result<String, RustdocJoinError> {
2845    let main = format!("{}::main", entry.module);
2846    if let Some(suffix) = definition.strip_prefix(&main) {
2847        return Ok(format!("doctest:{}:{}{suffix}", entry.path, entry.line));
2848    }
2849    if definition.is_empty()
2850        || definition.chars().any(char::is_control)
2851        || definition.contains("doctest_bundle_")
2852        || definition.contains("__doctest_")
2853    {
2854        return Err(RustdocJoinError::Invalid(format!(
2855            "synthetic expansion definition {definition} is not stable"
2856        )));
2857    }
2858    Ok(definition.into())
2859}
2860
2861struct RebasedIdentity {
2862    identity: RustSourceIdentity,
2863    source: RustdocMappedRange,
2864    provenance: &'static str,
2865}
2866
2867#[allow(clippy::too_many_arguments)]
2868fn rebase_identity(
2869    map: &RustdocMergedMap,
2870    entry: &RustdocMergedEntry,
2871    bundle_source: &str,
2872    authored_sources: &BTreeMap<String, RustCompilerSource>,
2873    kind: &str,
2874    source_key: &str,
2875    start: u32,
2876    end: u32,
2877    old_discriminator: &str,
2878    new_discriminator: &str,
2879    id: &str,
2880    canonical: &str,
2881    probe_ordinal: &str,
2882) -> Result<RebasedIdentity, RustdocJoinError> {
2883    let source = map_obligation_range(map, entry, bundle_source, start, end, authored_sources)?;
2884    if let Some(synthetic) =
2885        parse_synthetic_canonical(canonical, kind, source_key, start, end, old_discriminator)?
2886    {
2887        let old = identity_from_canonical(kind, canonical.into())?;
2888        verify_pending_identity(&old, id, Some(canonical), probe_ordinal)?;
2889        let pending_key = format!("doctest-pending:{}", map.group);
2890        let mut frame_canonical = String::new();
2891        for frame in synthetic.frames {
2892            if frame.source.source_key != pending_key {
2893                return Err(RustdocJoinError::Invalid(format!(
2894                    "synthetic expansion frame escaped pending source {}",
2895                    frame.source.source_key
2896                )));
2897            }
2898            let mapped = map_obligation_range(
2899                map,
2900                entry,
2901                bundle_source,
2902                frame.source.start,
2903                frame.source.end,
2904                authored_sources,
2905            )?;
2906            frame_canonical.push_str(&format!(
2907                "{}\0{}\0{}\0{}\0{}\0",
2908                frame.description,
2909                mapped.source_key,
2910                mapped.start,
2911                mapped.end,
2912                stable_definition(entry, &frame.definition)?,
2913            ));
2914        }
2915        let canonical = format!(
2916            "{SOURCE_MODEL}\0{kind}\0{}\0{}\0{}\0{new_discriminator}\0synthetic-expansion\0{}{}\0{}\0",
2917            source.source_key,
2918            source.start,
2919            source.end,
2920            frame_canonical,
2921            stable_definition(entry, &synthetic.definition)?,
2922            synthetic.owner_ordinal,
2923        );
2924        return Ok(RebasedIdentity {
2925            identity: identity_from_canonical(kind, canonical)?,
2926            source,
2927            provenance: "synthetic-expansion",
2928        });
2929    }
2930    let old = pending_identity(kind, source_key, start, end, old_discriminator)?;
2931    verify_pending_identity(&old, id, Some(canonical), probe_ordinal)?;
2932    Ok(RebasedIdentity {
2933        identity: rust_source_identity(kind, &source, new_discriminator)?,
2934        source,
2935        provenance: "doctest-source",
2936    })
2937}
2938
2939fn pending_identity(
2940    kind: &str,
2941    source_key: &str,
2942    start: u32,
2943    end: u32,
2944    discriminator: &str,
2945) -> Result<RustSourceIdentity, RustdocJoinError> {
2946    identity_for_range(
2947        kind,
2948        &RustdocMappedRange {
2949            source_key: source_key.into(),
2950            start,
2951            end,
2952        },
2953        discriminator,
2954    )
2955}
2956
2957fn verify_pending_identity(
2958    identity: &RustSourceIdentity,
2959    id: &str,
2960    canonical: Option<&str>,
2961    probe_ordinal: &str,
2962) -> Result<(), RustdocJoinError> {
2963    if identity.probe_ordinal == 0
2964        || id != identity.id
2965        || canonical.is_some_and(|canonical| canonical != identity.canonical)
2966        || probe_ordinal != identity.probe_ordinal.to_string()
2967    {
2968        return Err(RustdocJoinError::Invalid(format!(
2969            "temporary merged-doctest identity {id} does not match its frozen canonical form"
2970        )));
2971    }
2972    Ok(())
2973}
2974
2975fn insert_translation(
2976    ids: &mut BTreeMap<String, String>,
2977    ordinals: &mut BTreeMap<String, String>,
2978    old_id: &str,
2979    old_ordinal: &str,
2980    new_identity: &RustSourceIdentity,
2981) -> Result<(), RustdocJoinError> {
2982    let parsed_ordinal = old_ordinal.parse::<u64>().map_err(|_| {
2983        RustdocJoinError::Invalid(format!(
2984            "temporary obligation {old_id} has an invalid ordinal"
2985        ))
2986    })?;
2987    if parsed_ordinal == 0 || old_ordinal != parsed_ordinal.to_string() {
2988        return Err(RustdocJoinError::Invalid(format!(
2989            "temporary obligation {old_id} has a non-canonical ordinal"
2990        )));
2991    }
2992    if ids.insert(old_id.into(), new_identity.id.clone()).is_some()
2993        || ordinals
2994            .insert(old_ordinal.into(), new_identity.probe_ordinal.to_string())
2995            .is_some()
2996    {
2997        return Err(RustdocJoinError::Invalid(format!(
2998            "duplicate temporary merged-doctest identity {old_id}"
2999        )));
3000    }
3001    Ok(())
3002}
3003
3004fn definition_module<'a>(
3005    map: &'a RustdocMergedMap,
3006    definitions: &[String],
3007) -> Result<&'a RustdocMergedEntry, RustdocJoinError> {
3008    let mut matches = BTreeSet::new();
3009    for definition in definitions {
3010        for entry in &map.entries {
3011            let main = format!("{}::main", entry.module);
3012            if definition == &main || definition.starts_with(&format!("{main}::")) {
3013                matches.insert(entry.module.as_str());
3014            }
3015        }
3016    }
3017    if matches.len() != 1 {
3018        return Err(RustdocJoinError::Invalid(format!(
3019            "obligation definitions do not resolve to exactly one merged doctest module: {}",
3020            definitions.join(", ")
3021        )));
3022    }
3023    let module = matches.into_iter().next().expect("exactly one module");
3024    map.entry(module)
3025}
3026
3027fn stable_definitions(
3028    entry: &RustdocMergedEntry,
3029    definitions: &[String],
3030) -> Result<Vec<String>, RustdocJoinError> {
3031    let main = format!("{}::main", entry.module);
3032    let root = format!("doctest:{}:{}", entry.path, entry.line);
3033    let mut stable = definitions
3034        .iter()
3035        .map(|definition| {
3036            definition
3037                .strip_prefix(&main)
3038                .map(|suffix| format!("{root}{suffix}"))
3039        })
3040        .collect::<Option<Vec<_>>>()
3041        .ok_or_else(|| {
3042            RustdocJoinError::Invalid(format!(
3043                "definition escaped merged doctest module {}",
3044                entry.module
3045            ))
3046        })?;
3047    stable.sort();
3048    stable.dedup();
3049    if stable.is_empty() {
3050        return Err(RustdocJoinError::Invalid(
3051            "merged doctest obligation has no stable definitions".into(),
3052        ));
3053    }
3054    Ok(stable)
3055}
3056
3057fn authored_source<'a>(
3058    sources: &'a BTreeMap<String, RustCompilerSource>,
3059    entry: &RustdocMergedEntry,
3060) -> Result<&'a RustCompilerSource, RustdocJoinError> {
3061    let key = format!("source:{}", entry.path);
3062    let source = sources.get(&key).ok_or_else(|| {
3063        RustdocJoinError::Invalid(format!("authored source snapshot {key} was not supplied"))
3064    })?;
3065    if source.file != entry.path {
3066        return Err(RustdocJoinError::Invalid(format!(
3067            "authored source snapshot {key} has display path {}",
3068            source.file
3069        )));
3070    }
3071    Ok(source)
3072}
3073
3074fn map_obligation_range(
3075    map: &RustdocMergedMap,
3076    entry: &RustdocMergedEntry,
3077    bundle_source: &str,
3078    start: u32,
3079    end: u32,
3080    authored_sources: &BTreeMap<String, RustCompilerSource>,
3081) -> Result<RustdocMappedRange, RustdocJoinError> {
3082    let source = authored_source(authored_sources, entry)?;
3083    map_merged_range(
3084        map,
3085        &entry.module,
3086        bundle_source,
3087        start,
3088        end,
3089        &source.source,
3090    )
3091}
3092
3093fn alternative_discriminator(
3094    discriminator: &str,
3095    kind: &str,
3096    label: &str,
3097) -> Result<String, RustdocJoinError> {
3098    let token = match (kind, label) {
3099        ("decision-outcome", "condition false") => "false",
3100        ("decision-outcome", "condition true") => "true",
3101        ("assertion-outcome", "failed") => "failed",
3102        ("assertion-outcome", "passed") => "passed",
3103        ("loop-entry", "zero iterations") => "zero",
3104        ("loop-entry", "entered") => "entered",
3105        ("match-arm", "not selected") => "not-selected",
3106        ("match-arm", "selected") => "selected",
3107        ("let-else", "matched") => "matched",
3108        ("let-else", "else") => "else",
3109        ("try-operator", "continued") => "continued",
3110        ("try-operator", "early return") => "returned",
3111        ("logical-selection", "short-circuited") => "short-circuit",
3112        ("logical-selection", "right operand evaluated") => "evaluated",
3113        _ => {
3114            return Err(RustdocJoinError::Invalid(format!(
3115                "unknown {} alternative label {label}",
3116                kind
3117            )));
3118        }
3119    };
3120    Ok(format!("{discriminator}:{token}"))
3121}
3122
3123/// Resolve a merged rustdoc bundle only after its runner map and immutable
3124/// authored source snapshots are available. The returned ID/ordinal maps are
3125/// required to translate already-emitted bundle observations; accepting the
3126/// final manifest without translating those observations would silently lose
3127/// coverage.
3128pub fn join_merged_doctest(
3129    pending_manifest_bytes: &[u8],
3130    pending_source_bytes: &[u8],
3131    map_bytes: &[u8],
3132    authored_sources: &BTreeMap<String, RustCompilerSource>,
3133) -> Result<RustdocMergedJoin, RustdocJoinError> {
3134    let map = RustdocMergedMap::parse(map_bytes)?;
3135    let mut manifest =
3136        RustCompilerManifest::parse_pending_doctest(pending_manifest_bytes, &map.group)
3137            .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3138    let pending_sources =
3139        RustCompilerSourceSnapshots::parse_pending_doctest(pending_source_bytes, &map.group)
3140            .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3141    if pending_sources.crate_name != manifest.crate_name {
3142        return Err(RustdocJoinError::Invalid(
3143            "pending merged-doctest manifest/source crate mismatch".into(),
3144        ));
3145    }
3146    let pending_key = format!("doctest-pending:{}", map.group);
3147    let bundle_source = &pending_sources
3148        .sources
3149        .get(&pending_key)
3150        .expect("pending source parser requires the exact key")
3151        .source;
3152    let mut ids = BTreeMap::new();
3153    let mut ordinals = BTreeMap::new();
3154
3155    for point in &mut manifest.points {
3156        let entry = definition_module(&map, &point.definitions)?;
3157        let rebased = rebase_identity(
3158            &map,
3159            entry,
3160            bundle_source,
3161            authored_sources,
3162            &point.kind,
3163            &point.source_key,
3164            point.start,
3165            point.end,
3166            &point.discriminator,
3167            &point.discriminator,
3168            &point.id,
3169            &point.canonical,
3170            &point.probe_ordinal,
3171        )?;
3172        insert_translation(
3173            &mut ids,
3174            &mut ordinals,
3175            &point.id,
3176            &point.probe_ordinal,
3177            &rebased.identity,
3178        )?;
3179        point.id = rebased.identity.id;
3180        point.canonical = rebased.identity.canonical;
3181        point.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3182        point.source_key = rebased.source.source_key;
3183        point.start = rebased.source.start;
3184        point.end = rebased.source.end;
3185        point.provenance = rebased.provenance.into();
3186        point.definitions = stable_definitions(entry, &point.definitions)?;
3187    }
3188
3189    let mut group_ids = BTreeMap::new();
3190    for group in &mut manifest.selection_groups {
3191        let entry = definition_module(&map, &group.definitions)?;
3192        let rebased = rebase_identity(
3193            &map,
3194            entry,
3195            bundle_source,
3196            authored_sources,
3197            "match-group",
3198            &group.source_key,
3199            group.start,
3200            group.end,
3201            "match",
3202            "match",
3203            &group.id,
3204            &group.canonical,
3205            &group.probe_ordinal,
3206        )?;
3207        insert_translation(
3208            &mut ids,
3209            &mut ordinals,
3210            &group.id,
3211            &group.probe_ordinal,
3212            &rebased.identity,
3213        )?;
3214        group_ids.insert(group.id.clone(), rebased.identity.id.clone());
3215        group.id = rebased.identity.id;
3216        group.canonical = rebased.identity.canonical;
3217        group.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3218        group.source_key = rebased.source.source_key;
3219        group.start = rebased.source.start;
3220        group.end = rebased.source.end;
3221        group.provenance = rebased.provenance.into();
3222        group.definitions = stable_definitions(entry, &group.definitions)?;
3223        for arm in &mut group.arms {
3224            let source = map_obligation_range(
3225                &map,
3226                entry,
3227                bundle_source,
3228                arm.body_start,
3229                arm.body_end,
3230                authored_sources,
3231            )?;
3232            arm.body_source_key = source.source_key;
3233            arm.body_start = source.start;
3234            arm.body_end = source.end;
3235        }
3236    }
3237
3238    let mut branch_ids = BTreeMap::new();
3239    for branch in &mut manifest.branches {
3240        let old_discriminator = branch.discriminator.clone();
3241        let old_source_key = branch.source_key.clone();
3242        let old_start = branch.start;
3243        let old_end = branch.end;
3244        let entry = definition_module(&map, &branch.definitions)?;
3245        let discriminator = if branch.kind == "match-arm" {
3246            let mut translated = old_discriminator.clone();
3247            for (old_group, new_group) in &group_ids {
3248                translated = translated.replace(old_group, new_group);
3249            }
3250            if translated == old_discriminator {
3251                return Err(RustdocJoinError::Invalid(format!(
3252                    "match-arm discriminator {} has no translated parent group",
3253                    old_discriminator
3254                )));
3255            }
3256            translated
3257        } else {
3258            old_discriminator.clone()
3259        };
3260        let rebased = rebase_identity(
3261            &map,
3262            entry,
3263            bundle_source,
3264            authored_sources,
3265            "branch",
3266            &old_source_key,
3267            old_start,
3268            old_end,
3269            &old_discriminator,
3270            &discriminator,
3271            &branch.id,
3272            &branch.canonical,
3273            &branch.probe_ordinal,
3274        )?;
3275        insert_translation(
3276            &mut ids,
3277            &mut ordinals,
3278            &branch.id,
3279            &branch.probe_ordinal,
3280            &rebased.identity,
3281        )?;
3282        branch_ids.insert(branch.id.clone(), rebased.identity.id.clone());
3283        branch.id = rebased.identity.id;
3284        branch.canonical = rebased.identity.canonical;
3285        branch.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3286        branch.source_key = rebased.source.source_key.clone();
3287        branch.start = rebased.source.start;
3288        branch.end = rebased.source.end;
3289        branch.provenance = rebased.provenance.into();
3290        branch.definitions = stable_definitions(entry, &branch.definitions)?;
3291        branch.discriminator = discriminator.clone();
3292        for alternative in &mut branch.alternatives {
3293            let old_alternative_discriminator =
3294                alternative_discriminator(&old_discriminator, &branch.kind, &alternative.label)?;
3295            let new_discriminator =
3296                alternative_discriminator(&discriminator, &branch.kind, &alternative.label)?;
3297            let rebased = rebase_identity(
3298                &map,
3299                entry,
3300                bundle_source,
3301                authored_sources,
3302                "branch-alternative",
3303                &old_source_key,
3304                old_start,
3305                old_end,
3306                &old_alternative_discriminator,
3307                &new_discriminator,
3308                &alternative.id,
3309                &alternative.canonical,
3310                &alternative.probe_ordinal,
3311            )?;
3312            insert_translation(
3313                &mut ids,
3314                &mut ordinals,
3315                &alternative.id,
3316                &alternative.probe_ordinal,
3317                &rebased.identity,
3318            )?;
3319            alternative.id = rebased.identity.id;
3320            alternative.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3321            alternative.canonical = rebased.identity.canonical;
3322        }
3323    }
3324
3325    let mut decision_ids = BTreeMap::new();
3326    for decision in &mut manifest.decisions {
3327        let entry = definition_module(&map, &decision.definitions)?;
3328        let rebased = rebase_identity(
3329            &map,
3330            entry,
3331            bundle_source,
3332            authored_sources,
3333            "decision",
3334            &decision.source_key,
3335            decision.start,
3336            decision.end,
3337            &decision.kind,
3338            &decision.kind,
3339            &decision.id,
3340            &decision.canonical,
3341            &decision.probe_ordinal,
3342        )?;
3343        insert_translation(
3344            &mut ids,
3345            &mut ordinals,
3346            &decision.id,
3347            &decision.probe_ordinal,
3348            &rebased.identity,
3349        )?;
3350        decision_ids.insert(decision.id.clone(), rebased.identity.id.clone());
3351        decision.id = rebased.identity.id;
3352        decision.canonical = rebased.identity.canonical;
3353        decision.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3354        decision.source_key = rebased.source.source_key;
3355        decision.start = rebased.source.start;
3356        decision.end = rebased.source.end;
3357        decision.provenance = rebased.provenance.into();
3358        decision.definitions = stable_definitions(entry, &decision.definitions)?;
3359        decision.outcome_branch_id = branch_ids
3360            .get(&decision.outcome_branch_id)
3361            .cloned()
3362            .ok_or_else(|| {
3363                RustdocJoinError::Invalid("decision outcome branch was not rebased".into())
3364            })?;
3365        decision.loop_branch_id = decision
3366            .loop_branch_id
3367            .as_ref()
3368            .map(|id| {
3369                branch_ids.get(id).cloned().ok_or_else(|| {
3370                    RustdocJoinError::Invalid("decision loop branch was not rebased".into())
3371                })
3372            })
3373            .transpose()?;
3374        for selection in &mut decision.logical_selections {
3375            selection.branch_id =
3376                branch_ids
3377                    .get(&selection.branch_id)
3378                    .cloned()
3379                    .ok_or_else(|| {
3380                        RustdocJoinError::Invalid(
3381                            "decision logical-selection branch was not rebased".into(),
3382                        )
3383                    })?;
3384        }
3385        decision
3386            .logical_selections
3387            .sort_by(|left, right| left.branch_id.cmp(&right.branch_id));
3388        for condition in &mut decision.conditions {
3389            let source = map_obligation_range(
3390                &map,
3391                entry,
3392                bundle_source,
3393                condition.start,
3394                condition.end,
3395                authored_sources,
3396            )?;
3397            condition.source_key = source.source_key;
3398            condition.start = source.start;
3399            condition.end = source.end;
3400        }
3401    }
3402
3403    for group in &mut manifest.selection_groups {
3404        group.parent_group_id = group
3405            .parent_group_id
3406            .as_ref()
3407            .map(|id| {
3408                group_ids.get(id).cloned().ok_or_else(|| {
3409                    RustdocJoinError::Invalid("match parent group was not rebased".into())
3410                })
3411            })
3412            .transpose()?;
3413        for arm in &mut group.arms {
3414            arm.branch_id = branch_ids.get(&arm.branch_id).cloned().ok_or_else(|| {
3415                RustdocJoinError::Invalid("match arm branch was not rebased".into())
3416            })?;
3417            arm.guard_decision_id = arm
3418                .guard_decision_id
3419                .as_ref()
3420                .map(|id| {
3421                    decision_ids.get(id).cloned().ok_or_else(|| {
3422                        RustdocJoinError::Invalid("match guard decision was not rebased".into())
3423                    })
3424                })
3425                .transpose()?;
3426            arm.selected_ordinal = ordinals
3427                .get(&arm.selected_ordinal)
3428                .ok_or_else(|| {
3429                    RustdocJoinError::Invalid("match selected ordinal was not rebased".into())
3430                })?
3431                .clone();
3432            arm.not_selected_ordinal = ordinals
3433                .get(&arm.not_selected_ordinal)
3434                .ok_or_else(|| {
3435                    RustdocJoinError::Invalid("match not-selected ordinal was not rebased".into())
3436                })?
3437                .clone();
3438        }
3439    }
3440
3441    manifest
3442        .points
3443        .sort_by(|left, right| left.id.cmp(&right.id));
3444    manifest
3445        .branches
3446        .sort_by(|left, right| left.id.cmp(&right.id));
3447    manifest
3448        .decisions
3449        .sort_by(|left, right| left.id.cmp(&right.id));
3450    manifest
3451        .selection_groups
3452        .sort_by(|left, right| left.id.cmp(&right.id));
3453
3454    let required_keys = manifest
3455        .points
3456        .iter()
3457        .map(|point| point.source_key.as_str())
3458        .chain(
3459            manifest
3460                .branches
3461                .iter()
3462                .map(|branch| branch.source_key.as_str()),
3463        )
3464        .chain(manifest.decisions.iter().flat_map(|decision| {
3465            std::iter::once(decision.source_key.as_str()).chain(
3466                decision
3467                    .conditions
3468                    .iter()
3469                    .map(|condition| condition.source_key.as_str()),
3470            )
3471        }))
3472        .chain(manifest.selection_groups.iter().flat_map(|group| {
3473            std::iter::once(group.source_key.as_str())
3474                .chain(group.arms.iter().map(|arm| arm.body_source_key.as_str()))
3475        }))
3476        .collect::<BTreeSet<_>>();
3477    let sources = RustCompilerSourceSnapshots {
3478        schema: pending_sources.schema,
3479        crate_name: manifest.crate_name.clone(),
3480        sources: required_keys
3481            .into_iter()
3482            .map(|key| {
3483                authored_sources
3484                    .get(key)
3485                    .cloned()
3486                    .map(|source| (key.into(), source))
3487                    .ok_or_else(|| {
3488                        RustdocJoinError::Invalid(format!(
3489                            "final authored source snapshot {key} was not supplied"
3490                        ))
3491                    })
3492            })
3493            .collect::<Result<_, _>>()?,
3494    };
3495    manifest
3496        .validate()
3497        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3498    sources
3499        .validate()
3500        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3501    manifest
3502        .normalize(&sources.sources)
3503        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3504    Ok(RustdocMergedJoin {
3505        manifest,
3506        sources,
3507        obligation_ids: ids,
3508        probe_ordinals: ordinals,
3509    })
3510}
3511
3512#[cfg(test)]
3513mod tests {
3514    use super::*;
3515    use crate::rust_compiler_manifest::{
3516        RustCompilerBranch, RustCompilerBranchAlternative, RustCompilerCondition,
3517        RustCompilerDecision, RustCompilerManifest, RustCompilerMatchArm, RustCompilerPoint,
3518        RustCompilerSelectionGroup, RustCompilerSourceSnapshots,
3519        normalize_rust_compiler_candidates,
3520    };
3521    use crate::rust_probe_transport::{
3522        RustOrdinalHit, RustTestBoundary, RustThreadEnd, RustTransportObservation,
3523        rust_assertion_context_id,
3524    };
3525
3526    fn map() -> RustdocMergedMap {
3527        RustdocMergedMap::parse(
3528            br#"{
3529                "schema":"supercov-rustdoc-merged-map-v2",
3530                "group":"fixture",
3531                "entries":[
3532                    {"module":"__doctest_0","displayName":"src/lib.rs - (line 3)","path":"src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false},
3533                    {"module":"__doctest_1","displayName":"src/lib.rs - (line 10)","path":"src/lib.rs","line":10,"ignored":false,"noRun":true,"shouldPanic":true}
3534                ]
3535            }"#,
3536        )
3537        .expect("valid map")
3538    }
3539
3540    fn merged_bundle(body: &str) -> String {
3541        format!(
3542            "\n#![allow(unused)]\npub mod __doctest_0 {{\nfn main() {{\n{body}\n}}\npub fn __main_fn() -> impl std::process::Termination {{ main() }}\n}}\n"
3543        )
3544    }
3545
3546    fn libtest_stream(lines: &[&str]) -> Vec<u8> {
3547        lines.join("\n").into_bytes()
3548    }
3549
3550    fn empty_transport() -> RustTransportRead {
3551        RustTransportRead::empty()
3552    }
3553
3554    fn transport_sha256(transport: &RustTransportRead) -> String {
3555        format!(
3556            "{:x}",
3557            Sha256::digest(serde_json::to_vec(transport).unwrap())
3558        )
3559    }
3560
3561    fn catalog_doctest(
3562        name: &str,
3563        line: u64,
3564        ignored: bool,
3565        no_run: bool,
3566        should_panic: bool,
3567        compile_fail: bool,
3568        standalone_crate: bool,
3569    ) -> RustdocExtractedDoctest {
3570        RustdocExtractedDoctest {
3571            file: "src/lib.rs".into(),
3572            line,
3573            doctest_attributes: RustdocDoctestAttributes {
3574                original: String::new(),
3575                should_panic,
3576                no_run,
3577                ignore: if ignored {
3578                    RustdocDoctestIgnore::All
3579                } else {
3580                    RustdocDoctestIgnore::None
3581                },
3582                rust: true,
3583                test_harness: false,
3584                compile_fail,
3585                standalone_crate,
3586                error_codes: Vec::new(),
3587                edition: None,
3588                added_css_classes: Vec::new(),
3589                unknown: Vec::new(),
3590            },
3591            original_code: "assert!(true);".into(),
3592            doctest_code: Some(RustdocDoctestCode {
3593                crate_level: "#![allow(unused)]\n".into(),
3594                code: "assert!(true);".into(),
3595                wrapper: Some(RustdocDoctestWrapper {
3596                    before: "fn main() {\n".into(),
3597                    after: "\n}".into(),
3598                    returns_result: false,
3599                }),
3600            }),
3601            name: name.into(),
3602        }
3603    }
3604
3605    struct OutcomeDirectory(PathBuf);
3606
3607    impl OutcomeDirectory {
3608        fn new() -> Self {
3609            use std::sync::atomic::{AtomicU64, Ordering};
3610            static NEXT: AtomicU64 = AtomicU64::new(0);
3611            let path = std::env::temp_dir().join(format!(
3612                "supercov-rustdoc-outcome-{}-{}",
3613                std::process::id(),
3614                NEXT.fetch_add(1, Ordering::Relaxed)
3615            ));
3616            fs::create_dir(&path).expect("create outcome test directory");
3617            Self(path)
3618        }
3619    }
3620
3621    impl Drop for OutcomeDirectory {
3622        fn drop(&mut self) {
3623            let _ = fs::remove_dir_all(&self.0);
3624        }
3625    }
3626
3627    fn passing_outcome_unit() -> RustdocOutcomeUnit {
3628        let transport = empty_transport();
3629        RustdocOutcomeUnit {
3630            schema: OUTCOME_SCHEMA.into(),
3631            invocation_id: "1".repeat(64),
3632            group: "fixture".into(),
3633            companion_build_id: "2".repeat(64),
3634            raw_catalog_sha256: "4".repeat(64),
3635            raw_events_sha256: "3".repeat(64),
3636            transport_sha256: transport_sha256(&transport),
3637            catalog: RustdocExtractedCatalog {
3638                format_version: RUSTDOC_CATALOG_FORMAT_VERSION,
3639                doctests: vec![catalog_doctest(
3640                    "src/lib.rs - (line 3)",
3641                    3,
3642                    false,
3643                    false,
3644                    false,
3645                    false,
3646                    false,
3647                )],
3648            },
3649            report: RustdocOutcomeReport {
3650                outcomes: vec![RustdocTestOutcome {
3651                    display_name: "src/lib.rs - (line 3)".into(),
3652                    status: RustdocOutcomeStatus::Passed,
3653                    execution_seconds: Some(0.25),
3654                    stdout: None,
3655                    message: None,
3656                    reason: None,
3657                    timeout_warning: false,
3658                }],
3659                suites: 1,
3660                planned_tests: 1,
3661                filtered_out: 0,
3662                unfinished_started: Vec::new(),
3663                unstarted_tests: 0,
3664                total_seconds: None,
3665                compilation_seconds: None,
3666            },
3667            transport,
3668        }
3669    }
3670
3671    fn merged_unit() -> RustdocMergedUnit {
3672        RustdocMergedUnit {
3673            map: map(),
3674            join: None,
3675        }
3676    }
3677
3678    fn outcome(display_name: &str, status: RustdocOutcomeStatus) -> RustdocTestOutcome {
3679        RustdocTestOutcome {
3680            display_name: display_name.into(),
3681            status,
3682            execution_seconds: (status != RustdocOutcomeStatus::Ignored).then_some(0.25),
3683            stdout: None,
3684            message: None,
3685            reason: None,
3686            timeout_warning: false,
3687        }
3688    }
3689
3690    #[test]
3691    fn parses_the_exact_pinned_rustdoc_catalog_format() {
3692        let raw = br##"{
3693            "format_version": 2,
3694            "doctests": [
3695                {
3696                    "file": "src/lib.rs",
3697                    "line": 3,
3698                    "doctest_attributes": {
3699                        "original": "ignore-x86_64,edition2024",
3700                        "should_panic": false,
3701                        "no_run": false,
3702                        "ignore": {"Some": ["x86_64"]},
3703                        "rust": true,
3704                        "test_harness": false,
3705                        "compile_fail": false,
3706                        "standalone_crate": false,
3707                        "error_codes": [],
3708                        "edition": "2024",
3709                        "added_css_classes": [],
3710                        "unknown": []
3711                    },
3712                    "original_code": "assert!(true);",
3713                    "doctest_code": {
3714                        "crate_level": "#![allow(unused)]\n",
3715                        "code": "assert!(true);",
3716                        "wrapper": null
3717                    },
3718                    "name": "src/lib.rs - example (line 3)"
3719                },
3720                {
3721                    "file": "src/lib.rs",
3722                    "line": 10,
3723                    "doctest_attributes": {
3724                        "original": "compile_fail,E0308",
3725                        "should_panic": false,
3726                        "no_run": true,
3727                        "ignore": "None",
3728                        "rust": true,
3729                        "test_harness": false,
3730                        "compile_fail": true,
3731                        "standalone_crate": false,
3732                        "error_codes": ["E0308"],
3733                        "edition": null,
3734                        "added_css_classes": [],
3735                        "unknown": []
3736                    },
3737                    "original_code": "let _: u8 = true;",
3738                    "doctest_code": null,
3739                    "name": "src/lib.rs - compile_error (line 10)"
3740                }
3741            ]
3742        }"##;
3743        let catalog = RustdocExtractedCatalog::parse(raw).expect("pinned catalog v2");
3744        assert_eq!(catalog.doctests.len(), 2);
3745        assert!(matches!(
3746            &catalog.doctests[0].doctest_attributes.ignore,
3747            RustdocDoctestIgnore::Some(targets)
3748                if targets.len() == 1 && targets[0] == "x86_64"
3749        ));
3750        assert!(catalog.doctests[1].doctest_code.is_none());
3751        assert!(catalog.doctests[1].doctest_attributes.compile_fail);
3752    }
3753
3754    #[test]
3755    fn rejects_unknown_malformed_or_ambiguous_rustdoc_catalogs() {
3756        let valid = serde_json::to_value(&passing_outcome_unit().catalog).unwrap();
3757        let mut cases = Vec::new();
3758
3759        let mut value = valid.clone();
3760        value["format_version"] = serde_json::json!(3);
3761        cases.push(value);
3762
3763        let mut value = valid.clone();
3764        value["extra"] = serde_json::json!(true);
3765        cases.push(value);
3766
3767        let mut value = valid.clone();
3768        value["doctests"][0]["line"] = serde_json::json!(0);
3769        cases.push(value);
3770
3771        let mut value = valid.clone();
3772        value["doctests"][0]["name"] = serde_json::json!("a guessed name");
3773        cases.push(value);
3774
3775        let mut value = valid.clone();
3776        let duplicate = value["doctests"][0].clone();
3777        value["doctests"].as_array_mut().unwrap().push(duplicate);
3778        cases.push(value);
3779
3780        let mut value = valid;
3781        value["doctests"][0]["doctest_attributes"]["edition"] = serde_json::json!("2099");
3782        cases.push(value);
3783
3784        for value in cases {
3785            let bytes = serde_json::to_vec(&value).unwrap();
3786            assert!(
3787                RustdocExtractedCatalog::parse(&bytes).is_err(),
3788                "accepted invalid rustdoc catalog: {value}"
3789            );
3790        }
3791    }
3792
3793    #[test]
3794    fn parses_exact_libtest_outcomes_across_merged_suites() {
3795        let report = parse_rustdoc_libtest_json(&libtest_stream(&[
3796            r#"{"type":"suite","event":"started","test_count":3,"shuffle_seed":17}"#,
3797            r#"{"type":"test","event":"started","name":"alpha"}"#,
3798            r#"{"type":"test","event":"timeout","name":"alpha"}"#,
3799            r#"{"type":"test","name":"alpha","event":"ok","exec_time":1.25,"stdout":"visible\noutput"}"#,
3800            r#"{"type":"test","event":"started","name":"beta"}"#,
3801            r#"{"type":"test","name":"beta","event":"failed","exec_time":0.5,"stdout":"failure","reason":"time limit exceeded"}"#,
3802            r#"{"type":"test","event":"started","name":"ignored"}"#,
3803            r#"{"type":"test","name":"ignored","event":"ignored","message":"platform"}"#,
3804            r#"{"type":"suite","event":"failed","passed":1,"failed":1,"ignored":1,"measured":0,"filtered_out":2,"exec_time":1.75}"#,
3805            r#"{"type":"suite","event":"started","test_count":0}"#,
3806            r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":3}"#,
3807            r#"{"type":"report","total_time":2.5,"compilation_time":0.75}"#,
3808        ]))
3809        .expect("pinned libtest stream");
3810
3811        assert_eq!(report.suites, 2);
3812        assert_eq!(report.planned_tests, 3);
3813        assert_eq!(report.filtered_out, 5);
3814        assert!(report.unfinished_started.is_empty());
3815        assert_eq!(report.unstarted_tests, 0);
3816        assert_eq!(report.total_seconds, Some(2.5));
3817        assert_eq!(report.compilation_seconds, Some(0.75));
3818        assert_eq!(
3819            report
3820                .outcomes
3821                .iter()
3822                .map(|outcome| (
3823                    outcome.display_name.as_str(),
3824                    outcome.status,
3825                    outcome.timeout_warning,
3826                    outcome.message.as_deref(),
3827                    outcome.reason.as_deref(),
3828                ))
3829                .collect::<Vec<_>>(),
3830            vec![
3831                ("alpha", RustdocOutcomeStatus::Passed, true, None, None),
3832                (
3833                    "beta",
3834                    RustdocOutcomeStatus::Failed,
3835                    false,
3836                    None,
3837                    Some("time limit exceeded"),
3838                ),
3839                (
3840                    "ignored",
3841                    RustdocOutcomeStatus::Ignored,
3842                    false,
3843                    Some("platform"),
3844                    None,
3845                ),
3846            ]
3847        );
3848        assert_eq!(report.outcomes[0].execution_seconds, Some(1.25));
3849        assert_eq!(
3850            report.outcomes[0].stdout.as_deref(),
3851            Some("visible\noutput")
3852        );
3853    }
3854
3855    #[test]
3856    fn represents_failed_fail_fast_suites_without_inventing_outcomes() {
3857        let report = parse_rustdoc_libtest_json(&libtest_stream(&[
3858            r#"{"type":"suite","event":"started","test_count":4}"#,
3859            r#"{"type":"test","event":"started","name":"failing"}"#,
3860            r#"{"type":"test","event":"started","name":"still-running"}"#,
3861            r#"{"type":"test","name":"failing","event":"failed","message":"boom"}"#,
3862            r#"{"type":"suite","event":"failed","passed":0,"failed":1,"ignored":0,"measured":0,"filtered_out":7}"#,
3863        ]))
3864        .expect("valid fail-fast stream");
3865
3866        assert_eq!(report.planned_tests, 4);
3867        assert_eq!(report.filtered_out, 7);
3868        assert_eq!(report.unfinished_started, ["still-running"]);
3869        assert_eq!(report.unstarted_tests, 2);
3870        assert_eq!(report.outcomes.len(), 1);
3871        assert_eq!(report.outcomes[0].status, RustdocOutcomeStatus::Failed);
3872        assert_eq!(report.outcomes[0].message.as_deref(), Some("boom"));
3873        assert_eq!(report.total_seconds, None);
3874    }
3875
3876    #[test]
3877    fn rejects_malformed_truncated_or_semantically_impossible_libtest_streams() {
3878        let cases = [
3879            vec![],
3880            vec![r#"{"type":"suite","event":"started","test_count":0,"extra":true}"#],
3881            vec![r#"{"type":"suite","event":null,"test_count":0}"#],
3882            vec![r#"{"type":"suite","event":"started","event":"started","test_count":0}"#],
3883            vec![
3884                r#"{"type":"suite","event":"started","test_count":1}"#,
3885                r#"{"type":"test","name":"missing-start","event":"ok"}"#,
3886                r#"{"type":"suite","event":"ok","passed":1,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3887            ],
3888            vec![
3889                r#"{"type":"suite","event":"started","test_count":1}"#,
3890                r#"{"type":"test","event":"started","name":"duplicate"}"#,
3891                r#"{"type":"test","event":"started","name":"duplicate"}"#,
3892            ],
3893            vec![
3894                r#"{"type":"suite","event":"started","test_count":1}"#,
3895                r#"{"type":"test","event":"timeout","name":"missing-start"}"#,
3896            ],
3897            vec![
3898                r#"{"type":"suite","event":"started","test_count":1}"#,
3899                r#"{"type":"test","event":"started","name":"unknown-reason"}"#,
3900                r#"{"type":"test","name":"unknown-reason","event":"failed","reason":"new reason"}"#,
3901                r#"{"type":"suite","event":"failed","passed":0,"failed":1,"ignored":0,"measured":0,"filtered_out":0}"#,
3902            ],
3903            vec![
3904                r#"{"type":"suite","event":"started","test_count":1}"#,
3905                r#"{"type":"test","event":"started","name":"ignored"}"#,
3906                r#"{"type":"test","name":"ignored","event":"ignored","stdout":"impossible"}"#,
3907                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":1,"measured":0,"filtered_out":0}"#,
3908            ],
3909            vec![
3910                r#"{"type":"suite","event":"started","test_count":1}"#,
3911                r#"{"type":"test","event":"started","name":"unfinished"}"#,
3912                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3913            ],
3914            vec![
3915                r#"{"type":"suite","event":"started","test_count":1}"#,
3916                r#"{"type":"test","event":"started","name":"wrong-count"}"#,
3917                r#"{"type":"test","name":"wrong-count","event":"ok"}"#,
3918                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3919            ],
3920            vec![
3921                r#"{"type":"suite","event":"started","test_count":0}"#,
3922                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":1,"filtered_out":0}"#,
3923            ],
3924            vec![
3925                r#"{"type":"suite","event":"started","test_count":0}"#,
3926                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3927                r#"{"type":"report","total_time":1.0,"compilation_time":2.0}"#,
3928            ],
3929            vec![r#"{"type":"suite","event":"discovery"}"#],
3930            vec![r#"{"type":"suite","event":"started","test_count":0}"#],
3931        ];
3932        for lines in cases {
3933            assert!(
3934                parse_rustdoc_libtest_json(&libtest_stream(&lines)).is_err(),
3935                "accepted invalid libtest stream: {lines:?}"
3936            );
3937        }
3938    }
3939
3940    #[test]
3941    fn publishes_and_reads_exact_atomic_rustdoc_outcome_units() {
3942        let directory = OutcomeDirectory::new();
3943        let unit = passing_outcome_unit();
3944        let path = publish_rustdoc_outcome_unit(&directory.0, &unit).expect("publish outcome");
3945        assert_eq!(
3946            path.file_name().and_then(|name| name.to_str()),
3947            Some(format!("doctest-outcome-{}.json", unit.invocation_id).as_str())
3948        );
3949        assert_eq!(
3950            read_rustdoc_outcome_units(&directory.0).expect("read outcome"),
3951            std::slice::from_ref(&unit)
3952        );
3953        assert!(publish_rustdoc_outcome_unit(&directory.0, &unit).is_err());
3954        assert_eq!(
3955            read_rustdoc_outcome_units(&directory.0).expect("published outcome stayed intact"),
3956            [unit]
3957        );
3958    }
3959
3960    #[test]
3961    fn reserves_each_rustdoc_transport_once_and_authenticates_reads() {
3962        let directory = OutcomeDirectory::new();
3963        let invocation = "a".repeat(64);
3964        let reservation = reserve_rustdoc_transport(&directory.0, &invocation)
3965            .expect("reserve rustdoc transport");
3966        assert!(reservation.path.is_file());
3967        let token = rustdoc_transport_token_hex(&reservation.token);
3968        assert_eq!(
3969            read_reserved_rustdoc_transport(&reservation.path, &token)
3970                .expect("authenticated empty transport"),
3971            empty_transport()
3972        );
3973        assert!(reserve_rustdoc_transport(&directory.0, &invocation).is_err());
3974        assert!(read_reserved_rustdoc_transport(&reservation.path, &"0".repeat(32)).is_err());
3975    }
3976
3977    #[test]
3978    fn decodes_and_hashes_the_exact_catalog_and_event_frame() {
3979        let catalog = serde_json::to_vec(&passing_outcome_unit().catalog).unwrap();
3980        let events = libtest_stream(&[
3981            r#"{"type":"suite","event":"started","test_count":1}"#,
3982            r#"{"type":"test","event":"started","name":"src/lib.rs - (line 3)"}"#,
3983            r#"{"type":"test","name":"src/lib.rs - (line 3)","event":"ok"}"#,
3984            r#"{"type":"suite","event":"ok","passed":1,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3985        ]);
3986        let mut framed = u64::try_from(catalog.len()).unwrap().to_be_bytes().to_vec();
3987        framed.extend_from_slice(&catalog);
3988        framed.extend_from_slice(&events);
3989        let unit = rustdoc_outcome_unit_from_framed_input(
3990            "1".repeat(64),
3991            "fixture".into(),
3992            "2".repeat(64),
3993            &framed,
3994            empty_transport(),
3995        )
3996        .expect("exact framed rustdoc outputs");
3997        assert_eq!(
3998            unit.raw_catalog_sha256,
3999            format!("{:x}", Sha256::digest(&catalog))
4000        );
4001        assert_eq!(
4002            unit.raw_events_sha256,
4003            format!("{:x}", Sha256::digest(&events))
4004        );
4005        for invalid in [
4006            Vec::new(),
4007            1u64.to_be_bytes().to_vec(),
4008            1000u64.to_be_bytes().into_iter().chain([b'{']).collect(),
4009            {
4010                let mut only_catalog = u64::try_from(catalog.len()).unwrap().to_be_bytes().to_vec();
4011                only_catalog.extend_from_slice(&catalog);
4012                only_catalog
4013            },
4014        ] {
4015            assert!(
4016                rustdoc_outcome_unit_from_framed_input(
4017                    "1".repeat(64),
4018                    "fixture".into(),
4019                    "2".repeat(64),
4020                    &invalid,
4021                    empty_transport(),
4022                )
4023                .is_err()
4024            );
4025        }
4026    }
4027
4028    #[test]
4029    fn joins_merged_standalone_compile_fail_and_fail_fast_state_from_catalog() {
4030        let mut unit = passing_outcome_unit();
4031        unit.catalog.doctests = vec![
4032            catalog_doctest(
4033                "src/lib.rs - (line 3)",
4034                3,
4035                false,
4036                false,
4037                false,
4038                false,
4039                false,
4040            ),
4041            catalog_doctest(
4042                "src/lib.rs - (line 10)",
4043                10,
4044                false,
4045                true,
4046                true,
4047                false,
4048                false,
4049            ),
4050            catalog_doctest(
4051                "src/lib.rs - standalone (line 20)",
4052                20,
4053                false,
4054                false,
4055                false,
4056                false,
4057                true,
4058            ),
4059            catalog_doctest(
4060                "src/lib.rs - compile_fail (line 30)",
4061                30,
4062                false,
4063                true,
4064                false,
4065                true,
4066                false,
4067            ),
4068            catalog_doctest(
4069                "src/lib.rs - later (line 40)",
4070                40,
4071                false,
4072                false,
4073                false,
4074                false,
4075                false,
4076            ),
4077        ];
4078        unit.report = RustdocOutcomeReport {
4079            outcomes: vec![
4080                outcome("src/lib.rs - (line 10)", RustdocOutcomeStatus::Ignored),
4081                outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
4082                outcome(
4083                    "src/lib.rs - standalone (line 20)",
4084                    RustdocOutcomeStatus::Failed,
4085                ),
4086            ],
4087            suites: 1,
4088            planned_tests: 5,
4089            filtered_out: 0,
4090            unfinished_started: vec!["src/lib.rs - compile_fail (line 30)".into()],
4091            unstarted_tests: 1,
4092            total_seconds: None,
4093            compilation_seconds: None,
4094        };
4095        unit.validate().expect("valid mixed rustdoc outcome unit");
4096
4097        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4098            .expect("lossless merged outcome join");
4099        assert!(resolution.is_fully_catalogued());
4100        assert!(resolution.unmatched_maps.is_empty());
4101        let [group] = resolution.groups.as_slice() else {
4102            panic!("expected one joined rustdoc group")
4103        };
4104        assert_eq!(group.entries.len(), 5);
4105        assert!(matches!(
4106            &group.entries[0].state,
4107            RustdocJoinedOutcomeState::Completed { outcome }
4108                if outcome.status == RustdocOutcomeStatus::Passed
4109        ));
4110        assert!(matches!(
4111            &group.entries[1].state,
4112            RustdocJoinedOutcomeState::Completed { outcome }
4113                if outcome.status == RustdocOutcomeStatus::Ignored
4114        ));
4115        assert!(matches!(
4116            &group.entries[2].state,
4117            RustdocJoinedOutcomeState::Completed { outcome }
4118                if outcome.status == RustdocOutcomeStatus::Failed
4119        ));
4120        assert!(group.entries[2].merged_entry.is_none());
4121        assert!(matches!(
4122            group.entries[3].state,
4123            RustdocJoinedOutcomeState::UnfinishedStarted
4124        ));
4125        assert!(group.entries[3].catalog.doctest_attributes.compile_fail);
4126        assert!(matches!(
4127            group.entries[4].state,
4128            RustdocJoinedOutcomeState::Unstarted
4129        ));
4130        assert_eq!(
4131            group.raw_catalog_sha256,
4132            "4".repeat(64),
4133            "catalog binding must survive the join"
4134        );
4135        assert!(!group.has_ambiguous_outcomes());
4136    }
4137
4138    #[test]
4139    fn joins_named_fail_fast_states_without_inventing_terminal_outcomes() {
4140        let mut unit = passing_outcome_unit();
4141        unit.catalog.doctests.push(catalog_doctest(
4142            "src/lib.rs - (line 10)",
4143            10,
4144            false,
4145            true,
4146            true,
4147            false,
4148            false,
4149        ));
4150        unit.report = RustdocOutcomeReport {
4151            outcomes: vec![outcome(
4152                "src/lib.rs - (line 3)",
4153                RustdocOutcomeStatus::Failed,
4154            )],
4155            suites: 1,
4156            planned_tests: 2,
4157            filtered_out: 0,
4158            unfinished_started: vec!["src/lib.rs - (line 10)".into()],
4159            unstarted_tests: 0,
4160            total_seconds: None,
4161            compilation_seconds: None,
4162        };
4163        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4164            .expect("join fail-fast identities");
4165        assert!(resolution.is_fully_catalogued());
4166        assert!(matches!(
4167            resolution.groups[0].entries[1].state,
4168            RustdocJoinedOutcomeState::UnfinishedStarted
4169        ));
4170
4171        let mut unit = passing_outcome_unit();
4172        unit.catalog.doctests.push(catalog_doctest(
4173            "src/lib.rs - (line 10)",
4174            10,
4175            false,
4176            true,
4177            true,
4178            false,
4179            false,
4180        ));
4181        unit.report = RustdocOutcomeReport {
4182            outcomes: vec![outcome(
4183                "src/lib.rs - (line 3)",
4184                RustdocOutcomeStatus::Failed,
4185            )],
4186            suites: 1,
4187            planned_tests: 2,
4188            filtered_out: 0,
4189            unfinished_started: Vec::new(),
4190            unstarted_tests: 1,
4191            total_seconds: None,
4192            compilation_seconds: None,
4193        };
4194        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4195            .expect("join unstarted identity");
4196        assert!(resolution.is_fully_catalogued());
4197        assert!(matches!(
4198            resolution.groups[0].entries[1].state,
4199            RustdocJoinedOutcomeState::Unstarted
4200        ));
4201    }
4202
4203    #[test]
4204    fn preserves_filter_and_fail_fast_identity_ambiguity_instead_of_guessing() {
4205        let mut unit = passing_outcome_unit();
4206        unit.catalog.doctests.extend([
4207            catalog_doctest(
4208                "src/lib.rs - filtered-or-unstarted-a (line 20)",
4209                20,
4210                false,
4211                false,
4212                false,
4213                false,
4214                true,
4215            ),
4216            catalog_doctest(
4217                "src/lib.rs - filtered-or-unstarted-b (line 30)",
4218                30,
4219                false,
4220                true,
4221                false,
4222                true,
4223                false,
4224            ),
4225        ]);
4226        unit.report = RustdocOutcomeReport {
4227            outcomes: vec![outcome(
4228                "src/lib.rs - (line 3)",
4229                RustdocOutcomeStatus::Failed,
4230            )],
4231            suites: 1,
4232            planned_tests: 2,
4233            filtered_out: 1,
4234            unfinished_started: Vec::new(),
4235            unstarted_tests: 1,
4236            total_seconds: None,
4237            compilation_seconds: None,
4238        };
4239        let resolution =
4240            join_rustdoc_outcomes(Vec::new(), vec![unit]).expect("lossless ambiguous outcome join");
4241        assert!(resolution.is_fully_catalogued());
4242        assert!(resolution.has_ambiguous_outcomes());
4243        assert_eq!(resolution.groups[0].ambiguous_filtered_out, 1);
4244        assert_eq!(resolution.groups[0].ambiguous_unstarted_tests, 1);
4245        assert!(
4246            resolution.groups[0].entries[1..]
4247                .iter()
4248                .all(|entry| { matches!(entry.state, RustdocJoinedOutcomeState::NotRunAmbiguous) })
4249        );
4250    }
4251
4252    #[test]
4253    fn outcome_join_rejects_ambiguous_groups_and_impossible_missing_entries() {
4254        let unit = passing_outcome_unit();
4255        assert!(
4256            join_rustdoc_outcomes(vec![merged_unit(), merged_unit()], vec![unit.clone()]).is_err()
4257        );
4258        assert!(join_rustdoc_outcomes(vec![merged_unit()], vec![unit.clone(), unit]).is_err());
4259
4260        let mut incomplete = passing_outcome_unit();
4261        incomplete.report.outcomes[0].display_name = "src/lib.rs - (line 3)".into();
4262        assert!(join_rustdoc_outcomes(vec![merged_unit()], vec![incomplete]).is_err());
4263    }
4264
4265    #[test]
4266    fn outcome_join_retains_maps_without_outcomes_and_catalogs_units_without_maps() {
4267        let maps_only = join_rustdoc_outcomes(vec![merged_unit()], Vec::new()).unwrap();
4268        assert_eq!(maps_only.unmatched_maps.len(), 1);
4269        assert!(!maps_only.is_fully_catalogued());
4270
4271        let units_only = join_rustdoc_outcomes(Vec::new(), vec![passing_outcome_unit()]).unwrap();
4272        assert_eq!(units_only.groups.len(), 1);
4273        assert!(units_only.groups[0].entries[0].merged_entry.is_none());
4274        assert!(units_only.is_fully_catalogued());
4275    }
4276
4277    #[test]
4278    fn rejects_incomplete_tampered_or_inconsistent_rustdoc_outcome_units() {
4279        let mut invalid = Vec::new();
4280
4281        let mut unit = passing_outcome_unit();
4282        unit.schema = "supercov-rustdoc-outcome-unit-v0".into();
4283        invalid.push(unit);
4284
4285        let mut unit = passing_outcome_unit();
4286        unit.invocation_id = "A".repeat(64);
4287        invalid.push(unit);
4288
4289        let mut unit = passing_outcome_unit();
4290        unit.report.planned_tests = 2;
4291        invalid.push(unit);
4292
4293        let mut unit = passing_outcome_unit();
4294        unit.report.total_seconds = Some(1.0);
4295        invalid.push(unit);
4296
4297        let mut unit = passing_outcome_unit();
4298        unit.report.outcomes[0].status = RustdocOutcomeStatus::Ignored;
4299        invalid.push(unit);
4300
4301        let mut unit = passing_outcome_unit();
4302        unit.transport.attachments = 1;
4303        invalid.push(unit);
4304
4305        for unit in invalid {
4306            assert!(unit.validate().is_err(), "accepted invalid unit: {unit:?}");
4307        }
4308
4309        let directory = OutcomeDirectory::new();
4310        let unit = passing_outcome_unit();
4311        fs::write(
4312            directory.0.join(format!(
4313                ".doctest-outcome-{}.json.partial",
4314                unit.invocation_id
4315            )),
4316            b"partial",
4317        )
4318        .unwrap();
4319        assert!(read_rustdoc_outcome_units(&directory.0).is_err());
4320    }
4321
4322    #[test]
4323    fn outcome_join_rejects_transport_owned_by_an_unknown_test() {
4324        let mut unit = passing_outcome_unit();
4325        unit.transport.observations.push(RustTransportObservation {
4326            process_id: 1,
4327            context_id: 7,
4328            observation: RustProbeObservation::Hit {
4329                id: "rs:function:111111111111111111111111".into(),
4330            },
4331        });
4332        unit.transport.committed = 1;
4333        unit.transport_sha256 = transport_sha256(&unit.transport);
4334        assert!(unit.validate().is_ok());
4335        assert!(join_rustdoc_outcomes(Vec::new(), vec![unit]).is_err());
4336    }
4337
4338    #[test]
4339    fn map_is_strict_sorted_and_path_safe() {
4340        let valid = map();
4341        assert_eq!(valid.entry("__doctest_1").expect("entry").line, 10);
4342        assert!(valid.entry("__doctest_1").expect("entry").no_run);
4343        assert!(valid.entry("__doctest_1").expect("entry").should_panic);
4344        for invalid in [
4345            br#"{"schema":"wrong","group":"fixture","entries":[]}"#.as_slice(),
4346            br#"{"schema":"supercov-rustdoc-merged-map-v1","group":"fixture","entries":[{"module":"__doctest_0","displayName":"old","path":"src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false}]}"#.as_slice(),
4347            br#"{"schema":"supercov-rustdoc-merged-map-v2","group":"fixture","entries":[{"module":"__doctest_1","displayName":"one","path":"src/lib.rs","line":10,"ignored":false,"noRun":false,"shouldPanic":false},{"module":"__doctest_0","displayName":"zero","path":"src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false}]}"#.as_slice(),
4348            br#"{"schema":"supercov-rustdoc-merged-map-v2","group":"fixture","entries":[{"module":"__doctest_0","displayName":"duplicate","path":"src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false},{"module":"__doctest_1","displayName":"duplicate","path":"src/lib.rs","line":10,"ignored":false,"noRun":false,"shouldPanic":false}]}"#.as_slice(),
4349            br#"{"schema":"supercov-rustdoc-merged-map-v2","group":"fixture","entries":[{"module":"__doctest_0","displayName":"bad","path":"../src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false}]}"#.as_slice(),
4350            br#"{"schema":"supercov-rustdoc-merged-map-v2","group":"fixture","entries":[{"module":"__doctest_0","displayName":"bad","path":"src/lib.rs","line":0,"ignored":false,"noRun":false,"shouldPanic":false,"extra":true}]}"#.as_slice(),
4351        ] {
4352            assert!(RustdocMergedMap::parse(invalid).is_err());
4353        }
4354    }
4355
4356    #[test]
4357    fn maps_hidden_multiline_and_duplicate_later_doctests_exactly() {
4358        let map = map();
4359        let snippet = "let value = hidden\n    + 2;";
4360        let bundle = merged_bundle(snippet);
4361        let start = bundle.find(snippet).unwrap() as u32;
4362        let end = start + snippet.len() as u32;
4363        let authored = concat!(
4364            "//! docs\n",
4365            "//! ```\n",
4366            "//! # let hidden = 20;\n",
4367            "//! let value = hidden\n",
4368            "//!     + 2;\n",
4369            "//! assert_eq!(value, 22);\n",
4370            "//! ```\n",
4371            "//! more\n",
4372            "//! ```\n",
4373            "//! let value = hidden\n",
4374            "//!     + 2;\n",
4375            "//! ```\n",
4376        );
4377        let mapped = map_merged_range(&map, "__doctest_0", &bundle, start, end, authored)
4378            .expect("exact range");
4379        assert_eq!(mapped.source_key, "source:src/lib.rs");
4380        assert_eq!(
4381            &authored[mapped.start as usize..mapped.end as usize],
4382            "let value = hidden\n//!     + 2;"
4383        );
4384    }
4385
4386    #[test]
4387    fn rejects_ambiguous_or_unmapped_bundle_ranges() {
4388        let map = map();
4389        let bundle = merged_bundle("same();");
4390        let start = bundle.find("same();").unwrap() as u32;
4391        let end = start + 7;
4392        let ambiguous = "//! docs\n//! ```\n//! same();\n//! same();\n//! ```\n";
4393        assert!(map_merged_range(&map, "__doctest_0", &bundle, start, end, ambiguous).is_err());
4394        assert!(map_merged_range(&map, "__doctest_9", &bundle, start, end, ambiguous).is_err());
4395        assert!(map_merged_range(&map, "__doctest_0", &bundle, end, start, ambiguous).is_err());
4396    }
4397
4398    #[test]
4399    fn maps_repeated_fragments_when_the_full_sequence_is_unique() {
4400        let map = map();
4401        let snippet = "same();\nsame();";
4402        let bundle = merged_bundle(snippet);
4403        let start = bundle.find(snippet).unwrap() as u32;
4404        let end = start + snippet.len() as u32;
4405        let authored = concat!(
4406            "//! docs\n",
4407            "//! ```\n",
4408            "//! same();\n",
4409            "//! same();\n",
4410            "//! ```\n",
4411        );
4412        let mapped = map_merged_range(&map, "__doctest_0", &bundle, start, end, authored)
4413            .expect("one ordered mapping");
4414        assert_eq!(
4415            &authored[mapped.start as usize..mapped.end as usize],
4416            "same();\n//! same();"
4417        );
4418    }
4419
4420    #[test]
4421    fn maps_repeated_subexpressions_through_their_extracted_line_context() {
4422        let map = map();
4423        let snippet = concat!(
4424            "let flag = true;\n",
4425            "if flag { yes(); } else { no(); }\n",
4426            "match flag { true => yes(), false => no() };",
4427        );
4428        let bundle = merged_bundle(snippet);
4429        let if_line = "if flag { yes(); } else { no(); }";
4430        let start = bundle.find(if_line).unwrap() + "if ".len();
4431        let end = start + "flag".len();
4432        let authored = concat!(
4433            "//! docs\n",
4434            "//! ```\n",
4435            "//! let flag = true;\n",
4436            "//! if flag { yes(); } else { no(); }\n",
4437            "//! match flag { true => yes(), false => no() };\n",
4438            "//! ```\n",
4439        );
4440        let mapped = map_merged_range(
4441            &map,
4442            "__doctest_0",
4443            &bundle,
4444            start as u32,
4445            end as u32,
4446            authored,
4447        )
4448        .expect("full extracted-line context disambiguates flag");
4449        assert_eq!(
4450            &authored[mapped.start as usize..mapped.end as usize],
4451            "flag"
4452        );
4453        assert_eq!(
4454            authored[..mapped.start as usize]
4455                .bytes()
4456                .filter(|byte| *byte == b'\n')
4457                .count(),
4458            3,
4459            "the mapped flag must come from the if line"
4460        );
4461    }
4462
4463    #[test]
4464    fn final_identity_matches_the_frozen_rust_source_model() {
4465        let source = RustdocMappedRange {
4466            source_key: "source:src/lib.rs".into(),
4467            start: 42,
4468            end: 57,
4469        };
4470        let identity =
4471            rust_source_identity("statement", &source, "expression").expect("valid identity");
4472        assert_eq!(
4473            identity.canonical,
4474            "rust-source-v1\0statement\0source:src/lib.rs\x0042\x0057\0expression\0"
4475        );
4476        assert_eq!(identity.id, "rs:statement:8446ba638fcb36ffc76b4293");
4477        assert_eq!(identity.probe_ordinal, 9531510598153221887);
4478    }
4479
4480    fn pending_assertion_candidate() -> (
4481        Vec<u8>,
4482        Vec<u8>,
4483        Vec<u8>,
4484        BTreeMap<String, RustCompilerSource>,
4485    ) {
4486        let group = "fixture";
4487        let key = format!("doctest-pending:{group}");
4488        let snippet = "assert_eq!(fixture::authored(true), 1)";
4489        let bundle = format!(
4490            "\n#![allow(unused)]\npub mod __doctest_0 {{\nfn main() {{\n{snippet};\n}}\n}}\n"
4491        );
4492        let start = u32::try_from(bundle.find(snippet).expect("snippet")).unwrap();
4493        let end = start + u32::try_from(snippet.len()).unwrap();
4494        let definition = vec!["__doctest_0::main".into()];
4495        let point_identity = pending_identity("statement", &key, start, end, "expression").unwrap();
4496        let branch_identity =
4497            pending_identity("branch", &key, start, end, "assertion-outcome:assertion").unwrap();
4498        let passed_identity = pending_identity(
4499            "branch-alternative",
4500            &key,
4501            start,
4502            end,
4503            "assertion-outcome:assertion:passed",
4504        )
4505        .unwrap();
4506        let failed_identity = pending_identity(
4507            "branch-alternative",
4508            &key,
4509            start,
4510            end,
4511            "assertion-outcome:assertion:failed",
4512        )
4513        .unwrap();
4514        let decision_identity =
4515            pending_identity("decision", &key, start, end, "assertion").unwrap();
4516        let manifest = RustCompilerManifest {
4517            unmeasured_obligations: Vec::new(),
4518            schema: "supercov-rust-manifest-candidate-v4".into(),
4519            model: "rust-source-v1".into(),
4520            crate_name: "doctest_bundle_2024".into(),
4521            measurement_complete: false,
4522            bound_bodies: 1,
4523            points: vec![RustCompilerPoint {
4524                id: point_identity.id,
4525                kind: "statement".into(),
4526                source_key: key.clone(),
4527                start,
4528                end,
4529                provenance: "doctest-pending".into(),
4530                discriminator: "expression".into(),
4531                probe_ordinal: point_identity.probe_ordinal.to_string(),
4532                definitions: definition.clone(),
4533                canonical: point_identity.canonical,
4534            }],
4535            branches: vec![RustCompilerBranch {
4536                id: branch_identity.id.clone(),
4537                kind: "assertion-outcome".into(),
4538                discriminator: "assertion-outcome:assertion".into(),
4539                source_key: key.clone(),
4540                start,
4541                end,
4542                provenance: "doctest-pending".into(),
4543                probe_ordinal: branch_identity.probe_ordinal.to_string(),
4544                definitions: definition.clone(),
4545                alternatives: vec![
4546                    RustCompilerBranchAlternative {
4547                        id: passed_identity.id,
4548                        label: "passed".into(),
4549                        probe_ordinal: passed_identity.probe_ordinal.to_string(),
4550                        canonical: passed_identity.canonical,
4551                    },
4552                    RustCompilerBranchAlternative {
4553                        id: failed_identity.id,
4554                        label: "failed".into(),
4555                        probe_ordinal: failed_identity.probe_ordinal.to_string(),
4556                        canonical: failed_identity.canonical,
4557                    },
4558                ],
4559                canonical: branch_identity.canonical,
4560            }],
4561            decisions: vec![RustCompilerDecision {
4562                id: decision_identity.id,
4563                kind: "assertion".into(),
4564                source_key: key.clone(),
4565                start,
4566                end,
4567                provenance: "doctest-pending".into(),
4568                probe_ordinal: decision_identity.probe_ordinal.to_string(),
4569                definitions: definition,
4570                outcome_branch_id: branch_identity.id,
4571                loop_branch_id: None,
4572                logical_selections: Vec::new(),
4573                conditions: vec![RustCompilerCondition {
4574                    source_key: key.clone(),
4575                    start,
4576                    end,
4577                    source: snippet.into(),
4578                }],
4579                canonical: decision_identity.canonical,
4580            }],
4581            selection_groups: Vec::new(),
4582            limitations: vec!["RUST_DOCTEST_MAPPING_PENDING".into()],
4583        };
4584        let snapshots = RustCompilerSourceSnapshots {
4585            schema: "supercov-rust-source-snapshots-v1".into(),
4586            crate_name: manifest.crate_name.clone(),
4587            sources: BTreeMap::from([(
4588                key.clone(),
4589                RustCompilerSource {
4590                    file: key,
4591                    source: bundle,
4592                },
4593            )]),
4594        };
4595        let map = br#"{
4596            "schema":"supercov-rustdoc-merged-map-v2",
4597            "group":"fixture",
4598            "entries":[{
4599                "module":"__doctest_0",
4600                "displayName":"src/lib.rs - (line 3)",
4601                "path":"src/lib.rs",
4602                "line":3,
4603                "ignored":false,
4604                "noRun":false,
4605                "shouldPanic":false
4606            }]
4607        }"#
4608        .to_vec();
4609        let authored = concat!(
4610            "//! docs\n",
4611            "//! ```\n",
4612            "//! assert_eq!(fixture::authored(true), 1);\n",
4613            "//! ```\n",
4614        );
4615        (
4616            serde_json::to_vec(&manifest).unwrap(),
4617            serde_json::to_vec(&snapshots).unwrap(),
4618            map,
4619            BTreeMap::from([(
4620                "source:src/lib.rs".into(),
4621                RustCompilerSource {
4622                    file: "src/lib.rs".into(),
4623                    source: authored.into(),
4624                },
4625            )]),
4626        )
4627    }
4628
4629    fn pending_branch(
4630        key: &str,
4631        start: u32,
4632        end: u32,
4633        kind: &str,
4634        discriminator: &str,
4635        alternatives: [(&str, &str); 2],
4636    ) -> RustCompilerBranch {
4637        let identity = pending_identity("branch", key, start, end, discriminator).unwrap();
4638        RustCompilerBranch {
4639            id: identity.id,
4640            kind: kind.into(),
4641            discriminator: discriminator.into(),
4642            source_key: key.into(),
4643            start,
4644            end,
4645            provenance: "doctest-pending".into(),
4646            probe_ordinal: identity.probe_ordinal.to_string(),
4647            definitions: vec!["__doctest_0::main".into()],
4648            alternatives: alternatives
4649                .into_iter()
4650                .map(|(token, label)| {
4651                    let identity = pending_identity(
4652                        "branch-alternative",
4653                        key,
4654                        start,
4655                        end,
4656                        &format!("{discriminator}:{token}"),
4657                    )
4658                    .unwrap();
4659                    RustCompilerBranchAlternative {
4660                        id: identity.id,
4661                        label: label.into(),
4662                        probe_ordinal: identity.probe_ordinal.to_string(),
4663                        canonical: identity.canonical,
4664                    }
4665                })
4666                .collect(),
4667            canonical: identity.canonical,
4668        }
4669    }
4670
4671    fn synthetic_pending_identity(
4672        kind: &str,
4673        key: &str,
4674        start: u32,
4675        end: u32,
4676        discriminator: &str,
4677        owner_ordinal: u64,
4678    ) -> RustSourceIdentity {
4679        identity_from_canonical(
4680            kind,
4681            format!(
4682                concat!(
4683                    "rust-source-v1\0{}\0{}\0{}\0{}\0{}\0",
4684                    "synthetic-expansion\0proc-macro\0{}\0{}\0{}\0probe_macros::generated\0",
4685                    "__doctest_0::main\0{}\0"
4686                ),
4687                kind, key, start, end, discriminator, key, start, end, owner_ordinal,
4688            ),
4689        )
4690        .unwrap()
4691    }
4692
4693    #[test]
4694    fn joins_pending_bundle_manifest_into_final_authored_identities() {
4695        let (manifest, sources, map, authored) = pending_assertion_candidate();
4696        assert!(RustCompilerManifest::parse(&manifest).is_err());
4697        assert!(RustCompilerSourceSnapshots::parse(&sources).is_err());
4698
4699        let joined =
4700            join_merged_doctest(&manifest, &sources, &map, &authored).expect("strict merged join");
4701        assert_eq!(joined.manifest.points.len(), 1);
4702        assert_eq!(joined.manifest.branches.len(), 1);
4703        assert_eq!(joined.manifest.decisions.len(), 1);
4704        assert_eq!(joined.obligation_ids.len(), 5);
4705        assert_eq!(joined.probe_ordinals.len(), 5);
4706        assert_eq!(joined.manifest.points[0].source_key, "source:src/lib.rs");
4707        assert_eq!(joined.manifest.branches[0].source_key, "source:src/lib.rs");
4708        assert_eq!(joined.manifest.decisions[0].source_key, "source:src/lib.rs");
4709        let point = &joined.manifest.points[0];
4710        let source = &authored["source:src/lib.rs"].source;
4711        assert_eq!(
4712            &source[point.start as usize..point.end as usize],
4713            "assert_eq!(fixture::authored(true), 1)"
4714        );
4715        assert_eq!(point.provenance, "doctest-source");
4716        assert_eq!(point.definitions, ["doctest:src/lib.rs:3"]);
4717        assert_eq!(joined.sources.sources.len(), 1);
4718        joined
4719            .manifest
4720            .normalize(&joined.sources.sources)
4721            .expect("final manifest normalizes through the production path");
4722    }
4723
4724    #[test]
4725    fn merged_join_rejects_tampering_missing_sources_and_malformed_synthetic_expansion() {
4726        let (manifest, sources, map, authored) = pending_assertion_candidate();
4727        let mut tampered: serde_json::Value = serde_json::from_slice(&manifest).unwrap();
4728        tampered["points"][0]["id"] =
4729            serde_json::Value::String("rs:statement:000000000000000000000000".into());
4730        assert!(
4731            join_merged_doctest(
4732                &serde_json::to_vec(&tampered).unwrap(),
4733                &sources,
4734                &map,
4735                &authored,
4736            )
4737            .is_err()
4738        );
4739
4740        assert!(join_merged_doctest(&manifest, &sources, &map, &BTreeMap::new(),).is_err());
4741
4742        let mut synthetic: serde_json::Value = serde_json::from_slice(&manifest).unwrap();
4743        synthetic["points"][0]["canonical"] = serde_json::Value::String(
4744            concat!(
4745                "rust-source-v1\0statement\0doctest-pending:fixture\0",
4746                "1\0",
4747                "2\0expression\0synthetic-expansion\0"
4748            )
4749            .into(),
4750        );
4751        assert!(
4752            join_merged_doctest(
4753                &serde_json::to_vec(&synthetic).unwrap(),
4754                &sources,
4755                &map,
4756                &authored,
4757            )
4758            .is_err()
4759        );
4760    }
4761
4762    #[test]
4763    fn rebases_decision_match_cross_references_and_runtime_ordinals() {
4764        let key = "doctest-pending:fixture";
4765        let body = concat!(
4766            "let flag = true;\n",
4767            "if flag { yes(); } else { no(); }\n",
4768            "match flag { true => yes(), false => no() };",
4769        );
4770        let bundle = merged_bundle(body);
4771        let range = |fragment: &str| {
4772            let start = bundle.find(fragment).unwrap() as u32;
4773            (start, start + fragment.len() as u32)
4774        };
4775        let point_range = range("let flag = true;");
4776        let if_range = range("if flag { yes(); } else { no(); }");
4777        let if_flag_start = if_range.0 + "if ".len() as u32;
4778        let if_flag_end = if_flag_start + "flag".len() as u32;
4779        let match_range = range("match flag { true => yes(), false => no() }");
4780        let first_arm_range = range("true => yes()");
4781        let second_arm_range = range("false => no()");
4782        let match_start = match_range.0 as usize;
4783        let first_body_start = match_start + bundle[match_start..].find("yes()").unwrap();
4784        let second_body_start = match_start + bundle[match_start..].find("no()").unwrap();
4785        let first_body_range = (
4786            first_body_start as u32,
4787            (first_body_start + "yes()".len()) as u32,
4788        );
4789        let second_body_range = (
4790            second_body_start as u32,
4791            (second_body_start + "no()".len()) as u32,
4792        );
4793
4794        let point_identity =
4795            pending_identity("statement", key, point_range.0, point_range.1, "let").unwrap();
4796        let decision_identity =
4797            pending_identity("decision", key, if_flag_start, if_flag_end, "if").unwrap();
4798        let outcome = pending_branch(
4799            key,
4800            if_range.0,
4801            if_range.1,
4802            "decision-outcome",
4803            "decision-outcome:if",
4804            [("true", "condition true"), ("false", "condition false")],
4805        );
4806        let group_identity =
4807            pending_identity("match-group", key, match_range.0, match_range.1, "match").unwrap();
4808        let first_arm = pending_branch(
4809            key,
4810            first_arm_range.0,
4811            first_arm_range.1,
4812            "match-arm",
4813            &format!("match-arm:{}:0", group_identity.id),
4814            [("not-selected", "not selected"), ("selected", "selected")],
4815        );
4816        let second_arm = pending_branch(
4817            key,
4818            second_arm_range.0,
4819            second_arm_range.1,
4820            "match-arm",
4821            &format!("match-arm:{}:1", group_identity.id),
4822            [("not-selected", "not selected"), ("selected", "selected")],
4823        );
4824        let arm_ordinals = |branch: &RustCompilerBranch| {
4825            let ordinal = |label: &str| {
4826                branch
4827                    .alternatives
4828                    .iter()
4829                    .find(|alternative| alternative.label == label)
4830                    .unwrap()
4831                    .probe_ordinal
4832                    .clone()
4833            };
4834            (ordinal("selected"), ordinal("not selected"))
4835        };
4836        let first_ordinals = arm_ordinals(&first_arm);
4837        let second_ordinals = arm_ordinals(&second_arm);
4838        let mut branches = vec![outcome.clone(), first_arm.clone(), second_arm.clone()];
4839        branches.sort_by(|left, right| left.id.cmp(&right.id));
4840        let manifest = RustCompilerManifest {
4841            unmeasured_obligations: Vec::new(),
4842            schema: "supercov-rust-manifest-candidate-v4".into(),
4843            model: "rust-source-v1".into(),
4844            crate_name: "doctest_bundle_2024".into(),
4845            measurement_complete: false,
4846            bound_bodies: 1,
4847            points: vec![RustCompilerPoint {
4848                id: point_identity.id,
4849                kind: "statement".into(),
4850                source_key: key.into(),
4851                start: point_range.0,
4852                end: point_range.1,
4853                provenance: "doctest-pending".into(),
4854                discriminator: "let".into(),
4855                probe_ordinal: point_identity.probe_ordinal.to_string(),
4856                definitions: vec!["__doctest_0::main".into()],
4857                canonical: point_identity.canonical,
4858            }],
4859            branches,
4860            decisions: vec![RustCompilerDecision {
4861                id: decision_identity.id,
4862                kind: "if".into(),
4863                source_key: key.into(),
4864                start: if_flag_start,
4865                end: if_flag_end,
4866                provenance: "doctest-pending".into(),
4867                probe_ordinal: decision_identity.probe_ordinal.to_string(),
4868                definitions: vec!["__doctest_0::main".into()],
4869                outcome_branch_id: outcome.id,
4870                loop_branch_id: None,
4871                logical_selections: Vec::new(),
4872                conditions: vec![RustCompilerCondition {
4873                    source_key: key.into(),
4874                    start: if_flag_start,
4875                    end: if_flag_end,
4876                    source: "flag".into(),
4877                }],
4878                canonical: decision_identity.canonical,
4879            }],
4880            selection_groups: vec![RustCompilerSelectionGroup {
4881                id: group_identity.id,
4882                kind: "match".into(),
4883                source_key: key.into(),
4884                start: match_range.0,
4885                end: match_range.1,
4886                provenance: "doctest-pending".into(),
4887                probe_ordinal: group_identity.probe_ordinal.to_string(),
4888                definitions: vec!["__doctest_0::main".into()],
4889                parent_group_id: None,
4890                parent_site: None,
4891                parent_arm_index: None,
4892                arms: vec![
4893                    RustCompilerMatchArm {
4894                        branch_id: first_arm.id,
4895                        body_source_key: key.into(),
4896                        body_start: first_body_range.0,
4897                        body_end: first_body_range.1,
4898                        guarded: false,
4899                        guard_decision_id: None,
4900                        selected_ordinal: first_ordinals.0,
4901                        not_selected_ordinal: first_ordinals.1,
4902                    },
4903                    RustCompilerMatchArm {
4904                        branch_id: second_arm.id,
4905                        body_source_key: key.into(),
4906                        body_start: second_body_range.0,
4907                        body_end: second_body_range.1,
4908                        guarded: false,
4909                        guard_decision_id: None,
4910                        selected_ordinal: second_ordinals.0,
4911                        not_selected_ordinal: second_ordinals.1,
4912                    },
4913                ],
4914                canonical: group_identity.canonical,
4915            }],
4916            limitations: vec!["RUST_DOCTEST_MAPPING_PENDING".into()],
4917        };
4918        let snapshots = RustCompilerSourceSnapshots {
4919            schema: "supercov-rust-source-snapshots-v1".into(),
4920            crate_name: manifest.crate_name.clone(),
4921            sources: BTreeMap::from([(
4922                key.into(),
4923                RustCompilerSource {
4924                    file: key.into(),
4925                    source: bundle,
4926                },
4927            )]),
4928        };
4929        let authored = concat!(
4930            "//! docs\n",
4931            "//! ```\n",
4932            "//! let flag = true;\n",
4933            "//! if flag { yes(); } else { no(); }\n",
4934            "//! match flag { true => yes(), false => no() };\n",
4935            "//! ```\n",
4936        );
4937        let map = br#"{
4938            "schema":"supercov-rustdoc-merged-map-v2",
4939            "group":"fixture",
4940            "entries":[{
4941                "module":"__doctest_0",
4942                "displayName":"src/lib.rs - (line 3)",
4943                "path":"src/lib.rs",
4944                "line":3,
4945                "ignored":false,
4946                "noRun":false,
4947                "shouldPanic":false
4948            }]
4949        }"#;
4950        let joined = join_merged_doctest(
4951            &serde_json::to_vec(&manifest).unwrap(),
4952            &serde_json::to_vec(&snapshots).unwrap(),
4953            map,
4954            &BTreeMap::from([(
4955                "source:src/lib.rs".into(),
4956                RustCompilerSource {
4957                    file: "src/lib.rs".into(),
4958                    source: authored.into(),
4959                },
4960            )]),
4961        )
4962        .expect("decision and match join");
4963
4964        let decision = &joined.manifest.decisions[0];
4965        assert!(
4966            joined
4967                .manifest
4968                .branches
4969                .iter()
4970                .any(|branch| branch.id == decision.outcome_branch_id)
4971        );
4972        let group = &joined.manifest.selection_groups[0];
4973        assert!(group.id.starts_with("rs:match-group:"));
4974        for arm in &group.arms {
4975            let branch = joined
4976                .manifest
4977                .branches
4978                .iter()
4979                .find(|branch| branch.id == arm.branch_id)
4980                .unwrap();
4981            assert!(branch.discriminator.contains(&group.id));
4982            assert!(
4983                branch
4984                    .alternatives
4985                    .iter()
4986                    .any(|alternative| alternative.probe_ordinal == arm.selected_ordinal)
4987            );
4988            assert!(
4989                branch
4990                    .alternatives
4991                    .iter()
4992                    .any(|alternative| alternative.probe_ordinal == arm.not_selected_ordinal)
4993            );
4994        }
4995        assert!(
4996            joined.manifest.decisions[0]
4997                .conditions
4998                .iter()
4999                .all(|condition| condition.source_key == "source:src/lib.rs")
5000        );
5001        assert_eq!(joined.obligation_ids.len(), 12);
5002        assert_eq!(joined.probe_ordinals.len(), 12);
5003    }
5004
5005    #[test]
5006    fn rebases_complete_synthetic_expansion_canonicals_without_guessing() {
5007        let (manifest, sources, map, authored) = pending_assertion_candidate();
5008        let mut manifest = RustCompilerManifest::parse_pending_doctest(&manifest, "fixture")
5009            .expect("pending candidate");
5010        let key = "doctest-pending:fixture";
5011        let mut owner_ordinal = 1;
5012        let mut replace = |kind: &str,
5013                           start: u32,
5014                           end: u32,
5015                           discriminator: &str,
5016                           id: &mut String,
5017                           canonical: &mut String,
5018                           ordinal: &mut String| {
5019            let identity =
5020                synthetic_pending_identity(kind, key, start, end, discriminator, owner_ordinal);
5021            owner_ordinal += 1;
5022            *id = identity.id;
5023            *canonical = identity.canonical;
5024            *ordinal = identity.probe_ordinal.to_string();
5025        };
5026        for point in &mut manifest.points {
5027            replace(
5028                &point.kind,
5029                point.start,
5030                point.end,
5031                &point.discriminator,
5032                &mut point.id,
5033                &mut point.canonical,
5034                &mut point.probe_ordinal,
5035            );
5036        }
5037        for branch in &mut manifest.branches {
5038            replace(
5039                "branch",
5040                branch.start,
5041                branch.end,
5042                &branch.discriminator,
5043                &mut branch.id,
5044                &mut branch.canonical,
5045                &mut branch.probe_ordinal,
5046            );
5047            for alternative in &mut branch.alternatives {
5048                let discriminator = alternative_discriminator(
5049                    &branch.discriminator,
5050                    &branch.kind,
5051                    &alternative.label,
5052                )
5053                .unwrap();
5054                replace(
5055                    "branch-alternative",
5056                    branch.start,
5057                    branch.end,
5058                    &discriminator,
5059                    &mut alternative.id,
5060                    &mut alternative.canonical,
5061                    &mut alternative.probe_ordinal,
5062                );
5063            }
5064        }
5065        let branch_id = manifest.branches[0].id.clone();
5066        for decision in &mut manifest.decisions {
5067            replace(
5068                "decision",
5069                decision.start,
5070                decision.end,
5071                &decision.kind,
5072                &mut decision.id,
5073                &mut decision.canonical,
5074                &mut decision.probe_ordinal,
5075            );
5076            decision.outcome_branch_id = branch_id.clone();
5077        }
5078        manifest
5079            .points
5080            .sort_by(|left, right| left.id.cmp(&right.id));
5081        manifest
5082            .branches
5083            .sort_by(|left, right| left.id.cmp(&right.id));
5084        manifest
5085            .decisions
5086            .sort_by(|left, right| left.id.cmp(&right.id));
5087
5088        let joined = join_merged_doctest(
5089            &serde_json::to_vec(&manifest).unwrap(),
5090            &sources,
5091            &map,
5092            &authored,
5093        )
5094        .expect("synthetic expansion join");
5095        assert!(
5096            joined
5097                .manifest
5098                .points
5099                .iter()
5100                .all(|point| point.provenance == "synthetic-expansion")
5101        );
5102        assert!(
5103            joined
5104                .manifest
5105                .branches
5106                .iter()
5107                .all(|branch| branch.provenance == "synthetic-expansion"
5108                    && branch.alternatives.iter().all(|alternative| {
5109                        alternative.canonical.contains("source:src/lib.rs")
5110                            && !alternative.canonical.contains("doctest-pending:")
5111                    }))
5112        );
5113        assert!(
5114            joined
5115                .manifest
5116                .decisions
5117                .iter()
5118                .all(|decision| decision.provenance == "synthetic-expansion")
5119        );
5120        for canonical in joined
5121            .manifest
5122            .points
5123            .iter()
5124            .map(|point| &point.canonical)
5125            .chain(joined.manifest.branches.iter().flat_map(|branch| {
5126                std::iter::once(&branch.canonical).chain(
5127                    branch
5128                        .alternatives
5129                        .iter()
5130                        .map(|alternative| &alternative.canonical),
5131                )
5132            }))
5133            .chain(
5134                joined
5135                    .manifest
5136                    .decisions
5137                    .iter()
5138                    .map(|decision| &decision.canonical),
5139            )
5140        {
5141            assert!(canonical.contains("doctest:src/lib.rs:3"));
5142            assert!(!canonical.contains("__doctest_0"));
5143            assert!(!canonical.contains("doctest-pending:"));
5144        }
5145        assert_eq!(joined.obligation_ids.len(), 5);
5146        assert_eq!(joined.probe_ordinals.len(), 5);
5147    }
5148
5149    #[test]
5150    fn translates_deferred_runtime_ids_ordinals_and_nested_assertion_contexts() {
5151        let (pending_manifest, sources, map, authored) = pending_assertion_candidate();
5152        let pending = RustCompilerManifest::parse_pending_doctest(&pending_manifest, "fixture")
5153            .expect("pending candidate");
5154        let mut joined = join_merged_doctest(&pending_manifest, &sources, &map, &authored)
5155            .expect("strict merged join");
5156        let old_point = &pending.points[0];
5157        let old_outer = &pending.decisions[0].id;
5158        let final_outer = joined.obligation_ids[old_outer].clone();
5159        let old_inner = "rs:decision:111111111111111111111111".to_owned();
5160        let final_inner = "rs:decision:222222222222222222222222".to_owned();
5161        joined
5162            .obligation_ids
5163            .insert(old_inner.clone(), final_inner.clone());
5164
5165        let base = 42;
5166        let outer_nonce = 7;
5167        let inner_nonce = 8;
5168        let old_outer_context =
5169            rust_assertion_context_id(base, old_outer, outer_nonce).expect("old outer context");
5170        let old_inner_context =
5171            rust_assertion_context_id(old_outer_context, &old_inner, inner_nonce)
5172                .expect("old inner context");
5173        let final_outer_context =
5174            rust_assertion_context_id(base, &final_outer, outer_nonce).expect("final outer");
5175        let final_inner_context =
5176            rust_assertion_context_id(final_outer_context, &final_inner, inner_nonce)
5177                .expect("final inner");
5178        let dependency = "rs:function:333333333333333333333333";
5179        let read = RustTransportRead {
5180            observations: vec![
5181                RustTransportObservation {
5182                    process_id: 10,
5183                    context_id: old_outer_context,
5184                    observation: RustProbeObservation::Hit {
5185                        id: old_point.id.clone(),
5186                    },
5187                },
5188                RustTransportObservation {
5189                    process_id: 10,
5190                    context_id: old_inner_context,
5191                    observation: RustProbeObservation::Decision {
5192                        id: old_inner.clone(),
5193                        values: vec![Some(true)],
5194                        outcome: true,
5195                    },
5196                },
5197                RustTransportObservation {
5198                    process_id: 10,
5199                    context_id: 0,
5200                    observation: RustProbeObservation::Hit {
5201                        id: dependency.into(),
5202                    },
5203                },
5204            ],
5205            ordinal_hits: vec![RustOrdinalHit {
5206                process_id: 10,
5207                context_id: old_outer_context,
5208                ordinal: old_point.probe_ordinal.parse().unwrap(),
5209            }],
5210            // Deliberately child-first: transport descriptor order is not a
5211            // topological guarantee and the rewriter must not depend on it.
5212            phases: vec![
5213                RustPhaseContext {
5214                    process_id: 10,
5215                    child_context_id: old_inner_context,
5216                    parent_context_id: old_outer_context,
5217                    invocation_nonce: inner_nonce,
5218                    decision_id: old_inner,
5219                },
5220                RustPhaseContext {
5221                    process_id: 10,
5222                    child_context_id: old_outer_context,
5223                    parent_context_id: base,
5224                    invocation_nonce: outer_nonce,
5225                    decision_id: old_outer.clone(),
5226                },
5227            ],
5228            committed: 6,
5229            incomplete: 1,
5230            dropped: 0,
5231            attachments: 2,
5232            ..RustTransportRead::empty()
5233        };
5234
5235        let translated = joined
5236            .translate_transport(base, &read)
5237            .expect("exact transport translation");
5238        assert_eq!(translated.committed, read.committed);
5239        assert_eq!(translated.incomplete, read.incomplete);
5240        assert_eq!(translated.attachments, read.attachments);
5241        assert_eq!(translated.observations[0].context_id, final_outer_context);
5242        assert_eq!(translated.observations[1].context_id, final_inner_context);
5243        assert_eq!(translated.observations[2], read.observations[2]);
5244        assert!(matches!(
5245            &translated.observations[0].observation,
5246            RustProbeObservation::Hit { id }
5247                if id == &joined.obligation_ids[&old_point.id]
5248        ));
5249        assert!(matches!(
5250            &translated.observations[1].observation,
5251            RustProbeObservation::Decision { id, .. } if id == &final_inner
5252        ));
5253        assert_eq!(translated.ordinal_hits[0].context_id, final_outer_context);
5254        assert_eq!(
5255            translated.ordinal_hits[0].ordinal.to_string(),
5256            joined.probe_ordinals[&old_point.probe_ordinal]
5257        );
5258        assert_eq!(translated.phases[0].child_context_id, final_inner_context);
5259        assert_eq!(translated.phases[0].parent_context_id, final_outer_context);
5260        assert_eq!(translated.phases[0].decision_id, final_inner);
5261        assert_eq!(translated.phases[1].child_context_id, final_outer_context);
5262        assert_eq!(translated.phases[1].parent_context_id, base);
5263        assert_eq!(translated.phases[1].decision_id, final_outer);
5264    }
5265
5266    #[test]
5267    fn combines_canonical_and_merged_runtime_roots_into_one_exact_doctest() {
5268        let (pending_manifest, sources, map_bytes, authored) = pending_assertion_candidate();
5269        let pending = RustCompilerManifest::parse_pending_doctest(&pending_manifest, "fixture")
5270            .expect("pending candidate");
5271        let joined = join_merged_doctest(&pending_manifest, &sources, &map_bytes, &authored)
5272            .expect("strict merged join");
5273        let final_point = joined.obligation_ids[&pending.points[0].id].clone();
5274        let catalog = catalog_doctest(
5275            "src/lib.rs - (line 3)",
5276            3,
5277            false,
5278            false,
5279            false,
5280            false,
5281            false,
5282        );
5283        let merged_entry = map().entries[0].clone();
5284        let canonical_name = "rustdoc:fixture:src/lib.rs:3";
5285        let merged_name = "rustdoc:fixture:__doctest_0";
5286        let canonical = rust_test_context_id(canonical_name).expect("canonical context");
5287        let merged = rust_test_context_id(merged_name).expect("merged context");
5288        assert_ne!(canonical, merged);
5289        let entry = RustdocJoinedOutcome {
5290            catalog_index: 0,
5291            catalog,
5292            merged_entry: Some(merged_entry),
5293            state: RustdocJoinedOutcomeState::Completed {
5294                outcome: outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
5295            },
5296        };
5297        let group = RustdocOutcomeGroupJoin {
5298            invocation_id: "1".repeat(64),
5299            group: "fixture".into(),
5300            companion_build_id: "2".repeat(64),
5301            raw_catalog_sha256: "3".repeat(64),
5302            raw_events_sha256: "4".repeat(64),
5303            transport_sha256: "5".repeat(64),
5304            join: Some(joined),
5305            transport: RustTransportRead {
5306                observations: vec![
5307                    RustTransportObservation {
5308                        process_id: 10,
5309                        context_id: canonical,
5310                        observation: RustProbeObservation::Hit {
5311                            id: final_point.clone(),
5312                        },
5313                    },
5314                    RustTransportObservation {
5315                        process_id: 11,
5316                        context_id: merged,
5317                        observation: RustProbeObservation::Hit {
5318                            id: pending.points[0].id.clone(),
5319                        },
5320                    },
5321                ],
5322                ordinal_hits: Vec::new(),
5323                phases: Vec::new(),
5324                committed: 2,
5325                incomplete: 0,
5326                dropped: 0,
5327                attachments: 2,
5328                ..RustTransportRead::empty()
5329            },
5330            entries: vec![entry.clone()],
5331            ambiguous_filtered_out: 0,
5332            ambiguous_unstarted_tests: 0,
5333        };
5334
5335        let (base, combined) = group
5336            .attributed_transport(&entry)
5337            .expect("canonical plus merged transport");
5338        assert_eq!(base, canonical);
5339        assert_eq!(combined.committed, 2);
5340        assert_eq!(combined.observations.len(), 2);
5341        assert!(
5342            combined
5343                .observations
5344                .iter()
5345                .all(|observation| observation.context_id == canonical)
5346        );
5347        assert!(combined.observations.iter().all(|observation| {
5348            matches!(
5349                &observation.observation,
5350                RustProbeObservation::Hit { id } if id == &final_point
5351            )
5352        }));
5353    }
5354
5355    #[test]
5356    fn doctest_thread_phases_are_join_bounded_and_escapes_become_background() {
5357        let canonical_name = "rustdoc:fixture:src/lib.rs:3";
5358        let canonical = rust_test_context_id(canonical_name).expect("canonical context");
5359        let joined_thread = rust_thread_context_id(canonical, 0);
5360        let escaped_thread = rust_thread_context_id(canonical, 1);
5361        let hit = |context_id: u64| RustTransportObservation {
5362            process_id: 10,
5363            context_id,
5364            observation: RustProbeObservation::Hit {
5365                id: "rs:statement:0123456789abcdef01234567".into(),
5366            },
5367        };
5368        let entry = RustdocJoinedOutcome {
5369            catalog_index: 0,
5370            catalog: catalog_doctest(
5371                "src/lib.rs - (line 3)",
5372                3,
5373                false,
5374                false,
5375                false,
5376                false,
5377                false,
5378            ),
5379            merged_entry: None,
5380            state: RustdocJoinedOutcomeState::Completed {
5381                outcome: outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
5382            },
5383        };
5384        let group = RustdocOutcomeGroupJoin {
5385            invocation_id: "1".repeat(64),
5386            group: "fixture".into(),
5387            companion_build_id: "2".repeat(64),
5388            raw_catalog_sha256: "3".repeat(64),
5389            raw_events_sha256: "4".repeat(64),
5390            transport_sha256: "5".repeat(64),
5391            join: None,
5392            transport: RustTransportRead {
5393                observations: vec![hit(joined_thread), hit(escaped_thread), hit(0)],
5394                thread_phases: vec![
5395                    RustThreadPhase {
5396                        process_id: 10,
5397                        child_context_id: joined_thread,
5398                        parent_context_id: canonical,
5399                        invocation_nonce: 0,
5400                        commit_index: 0,
5401                    },
5402                    RustThreadPhase {
5403                        process_id: 10,
5404                        child_context_id: escaped_thread,
5405                        parent_context_id: canonical,
5406                        invocation_nonce: 1,
5407                        commit_index: 1,
5408                    },
5409                ],
5410                thread_ends: vec![RustThreadEnd {
5411                    process_id: 10,
5412                    context_id: joined_thread,
5413                    commit_index: 4,
5414                }],
5415                test_boundaries: vec![RustTestBoundary {
5416                    process_id: 10,
5417                    context_id: canonical,
5418                    commit_index: 5,
5419                }],
5420                committed: 7,
5421                attachments: 1,
5422                ..RustTransportRead::empty()
5423            },
5424            entries: vec![entry.clone()],
5425            ambiguous_filtered_out: 0,
5426            ambiguous_unstarted_tests: 0,
5427        };
5428        group
5429            .validate_transport_ownership()
5430            .expect("thread-kind records stay within known doctest roots");
5431
5432        let (base, attributed) = group
5433            .attributed_transport(&entry)
5434            .expect("join-bounded canonical transport");
5435        assert_eq!(base, canonical);
5436        // Joined thread phase, its end, its hit and the boundary are exact.
5437        assert_eq!(attributed.committed, 4);
5438        assert_eq!(attributed.observations, vec![hit(joined_thread)]);
5439        assert_eq!(attributed.thread_phases.len(), 1);
5440        assert_eq!(attributed.thread_ends.len(), 1);
5441        assert_eq!(attributed.test_boundaries.len(), 1);
5442
5443        let background = group.background_transport().expect("background transport");
5444        assert_eq!(background.committed, 3);
5445        assert_eq!(background.observations, vec![hit(escaped_thread), hit(0)]);
5446        assert_eq!(background.thread_phases.len(), 1);
5447        assert_eq!(background.thread_phases[0].child_context_id, escaped_thread);
5448
5449        assert_eq!(
5450            group.thread_scope_limitations().expect("limitations"),
5451            std::collections::BTreeSet::from([format!(
5452                "RUST_THREAD_OUTLIVED_TEST: thread phase {escaped_thread:016x} escaped test {canonical:016x}"
5453            )])
5454        );
5455
5456        // A boundary for an unknown context fails ownership validation.
5457        let mut unknown_boundary = group;
5458        unknown_boundary
5459            .transport
5460            .test_boundaries
5461            .push(RustTestBoundary {
5462                process_id: 10,
5463                context_id: 99,
5464                commit_index: 6,
5465            });
5466        unknown_boundary.transport.committed = 8;
5467        assert!(unknown_boundary.validate_transport_ownership().is_err());
5468    }
5469
5470    #[test]
5471    fn resolves_a_complete_compiler_generation_before_normalization() {
5472        let (pending_manifest, pending_sources, map, authored) = pending_assertion_candidate();
5473        let direct =
5474            join_merged_doctest(&pending_manifest, &pending_sources, &map, &authored).unwrap();
5475        let ordinary_manifest = serde_json::to_vec(&direct.manifest).unwrap();
5476        let ordinary_sources = serde_json::to_vec(&direct.sources).unwrap();
5477
5478        let resolved = resolve_merged_doctest_candidates(
5479            vec![
5480                (pending_manifest.clone(), pending_sources.clone()),
5481                (ordinary_manifest.clone(), ordinary_sources.clone()),
5482            ],
5483            vec![map.clone()],
5484        )
5485        .expect("generation join");
5486        assert_eq!(resolved.candidates.len(), 2);
5487        assert_eq!(resolved.merged_units.len(), 1);
5488        assert_eq!(resolved.merged_units[0].join.as_ref().unwrap(), &direct);
5489        normalize_rust_compiler_candidates(resolved.candidates).unwrap();
5490
5491        let no_obligations = resolve_merged_doctest_candidates(
5492            vec![(ordinary_manifest, ordinary_sources)],
5493            vec![map.clone()],
5494        )
5495        .expect("map-only test remains attributable");
5496        assert!(no_obligations.merged_units[0].join.is_none());
5497
5498        assert!(
5499            resolve_merged_doctest_candidates(
5500                vec![(pending_manifest.clone(), pending_sources.clone())],
5501                Vec::new(),
5502            )
5503            .is_err()
5504        );
5505        assert!(
5506            resolve_merged_doctest_candidates(
5507                vec![(pending_manifest, pending_sources)],
5508                vec![map.clone(), map],
5509            )
5510            .is_err()
5511        );
5512    }
5513}