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 } | RustProbeObservation::Decision { id, .. } => id,
2156            };
2157            if let Some(final_id) = self.obligation_ids.get(id) {
2158                *id = final_id.clone();
2159            }
2160        }
2161        for hit in &mut translated.ordinal_hits {
2162            hit.context_id = translate_record_context(hit.context_id)?;
2163            let old = hit.ordinal.to_string();
2164            if let Some(final_ordinal) = self.probe_ordinals.get(&old) {
2165                hit.ordinal = final_ordinal.parse::<u64>().map_err(|_| {
2166                    RustdocJoinError::Invalid(format!(
2167                        "merged doctest final probe ordinal {final_ordinal} is invalid"
2168                    ))
2169                })?;
2170            }
2171        }
2172        for phase in &mut translated.phases {
2173            phase.child_context_id = translate_record_context(phase.child_context_id)?;
2174            phase.parent_context_id = translate_record_context(phase.parent_context_id)?;
2175            if let Some(final_id) = self.obligation_ids.get(&phase.decision_id) {
2176                phase.decision_id = final_id.clone();
2177            }
2178        }
2179        for phase in &mut translated.thread_phases {
2180            phase.child_context_id = translate_record_context(phase.child_context_id)?;
2181            phase.parent_context_id = translate_record_context(phase.parent_context_id)?;
2182        }
2183        for end in &mut translated.thread_ends {
2184            end.context_id = translate_record_context(end.context_id)?;
2185        }
2186        for boundary in &mut translated.test_boundaries {
2187            boundary.context_id = translate_record_context(boundary.context_id)?;
2188        }
2189        let unique_phases = translated
2190            .phases
2191            .iter()
2192            .map(|phase| phase.child_context_id)
2193            .chain(
2194                translated
2195                    .thread_phases
2196                    .iter()
2197                    .map(|phase| phase.child_context_id),
2198            )
2199            .collect::<BTreeSet<_>>();
2200        if unique_phases.len() != translated.phases.len() + translated.thread_phases.len() {
2201            return Err(RustdocJoinError::Invalid(
2202                "merged doctest assertion contexts collided after identity translation".into(),
2203            ));
2204        }
2205        validate_rust_phase_contexts(base_context_id, &translated)
2206            .map_err(|error| RustdocJoinError::Invalid(error.to_string()))?;
2207        Ok(translated)
2208    }
2209}
2210
2211/// Parse an entire compiler-output generation and resolve every deferred
2212/// merged-doctest candidate before ordinary workspace normalization. Normal
2213/// candidates provide immutable authored source snapshots; a pending bundle
2214/// must match exactly one runner map, while a map without a pending bundle is
2215/// retained because the represented test may have no source obligations.
2216pub fn resolve_merged_doctest_candidates(
2217    raw_pairs: Vec<(Vec<u8>, Vec<u8>)>,
2218    raw_maps: Vec<Vec<u8>>,
2219) -> Result<RustdocResolvedCandidates, RustdocJoinError> {
2220    let mut maps = BTreeMap::<String, RustdocMergedMap>::new();
2221    for raw in raw_maps {
2222        let map = RustdocMergedMap::parse(&raw)?;
2223        if maps.insert(map.group.clone(), map).is_some() {
2224            return Err(RustdocJoinError::Invalid(
2225                "compiler output contains duplicate merged-doctest groups".into(),
2226            ));
2227        }
2228    }
2229
2230    struct Pending {
2231        group: String,
2232        manifest: Vec<u8>,
2233        sources: Vec<u8>,
2234    }
2235    let mut candidates = Vec::new();
2236    let mut pending = Vec::new();
2237    let mut authored_sources = BTreeMap::<String, RustCompilerSource>::new();
2238    for (manifest_bytes, source_bytes) in raw_pairs {
2239        let ordinary_manifest = RustCompilerManifest::parse(&manifest_bytes);
2240        let ordinary_sources = RustCompilerSourceSnapshots::parse(&source_bytes);
2241        if let (Ok(manifest), Ok(sources)) = (ordinary_manifest, ordinary_sources) {
2242            if manifest.crate_name != sources.crate_name {
2243                return Err(RustdocJoinError::Manifest(format!(
2244                    "compiler manifest/source identity differs for {}",
2245                    manifest.crate_name
2246                )));
2247            }
2248            for (key, source) in &sources.sources {
2249                if authored_sources
2250                    .insert(key.clone(), source.clone())
2251                    .is_some_and(|existing| existing != *source)
2252                {
2253                    return Err(RustdocJoinError::Invalid(format!(
2254                        "authored compiler source {key} changed across units"
2255                    )));
2256                }
2257            }
2258            candidates.push((manifest, sources));
2259            continue;
2260        }
2261
2262        let matching = maps
2263            .keys()
2264            .filter(|group| {
2265                RustCompilerManifest::parse_pending_doctest(&manifest_bytes, group).is_ok()
2266                    && RustCompilerSourceSnapshots::parse_pending_doctest(&source_bytes, group)
2267                        .is_ok()
2268            })
2269            .cloned()
2270            .collect::<Vec<_>>();
2271        let [group] = matching.as_slice() else {
2272            return Err(RustdocJoinError::Invalid(format!(
2273                "compiler candidate matches {} merged-doctest maps instead of exactly one",
2274                matching.len()
2275            )));
2276        };
2277        if pending
2278            .iter()
2279            .any(|candidate: &Pending| candidate.group == *group)
2280        {
2281            return Err(RustdocJoinError::Invalid(format!(
2282                "merged-doctest group {group} has more than one pending bundle"
2283            )));
2284        }
2285        pending.push(Pending {
2286            group: group.clone(),
2287            manifest: manifest_bytes,
2288            sources: source_bytes,
2289        });
2290    }
2291
2292    let mut joined_by_group = BTreeMap::new();
2293    for pending in pending {
2294        let map = maps
2295            .get(&pending.group)
2296            .expect("pending group was selected from parsed maps");
2297        let encoded_map =
2298            serde_json::to_vec(map).map_err(|error| RustdocJoinError::Json(error.to_string()))?;
2299        let joined = join_merged_doctest(
2300            &pending.manifest,
2301            &pending.sources,
2302            &encoded_map,
2303            &authored_sources,
2304        )?;
2305        candidates.push((joined.manifest.clone(), joined.sources.clone()));
2306        joined_by_group.insert(pending.group, joined);
2307    }
2308    candidates.sort_by(|left, right| {
2309        left.0.crate_name.cmp(&right.0.crate_name).then_with(|| {
2310            left.0
2311                .points
2312                .first()
2313                .map(|point| &point.id)
2314                .cmp(&right.0.points.first().map(|point| &point.id))
2315        })
2316    });
2317    let merged_units = maps
2318        .into_iter()
2319        .map(|(group, map)| RustdocMergedUnit {
2320            map,
2321            join: joined_by_group.remove(&group),
2322        })
2323        .collect();
2324    Ok(RustdocResolvedCandidates {
2325        candidates,
2326        merged_units,
2327    })
2328}
2329
2330#[derive(Debug, Clone, PartialEq, Eq)]
2331pub enum RustdocJoinError {
2332    Json(String),
2333    Manifest(String),
2334    Invalid(String),
2335}
2336
2337impl std::fmt::Display for RustdocJoinError {
2338    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2339        match self {
2340            Self::Json(reason) => write!(formatter, "invalid merged rustdoc map JSON: {reason}"),
2341            Self::Manifest(reason) => {
2342                write!(
2343                    formatter,
2344                    "invalid merged rustdoc compiler manifest: {reason}"
2345                )
2346            }
2347            Self::Invalid(reason) => write!(formatter, "invalid merged rustdoc join: {reason}"),
2348        }
2349    }
2350}
2351
2352impl std::error::Error for RustdocJoinError {}
2353
2354fn safe_group(value: &str) -> bool {
2355    !value.is_empty()
2356        && value
2357            .bytes()
2358            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
2359}
2360
2361fn module_index(value: &str) -> Option<u64> {
2362    value.strip_prefix("__doctest_")?.parse::<u64>().ok()
2363}
2364
2365fn normalized_relative_path(value: &str) -> bool {
2366    !value.is_empty()
2367        && !value.starts_with('/')
2368        && !value.contains('\\')
2369        && value
2370            .split('/')
2371            .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
2372}
2373
2374impl RustdocMergedMap {
2375    pub fn parse(bytes: &[u8]) -> Result<Self, RustdocJoinError> {
2376        let map: Self = serde_json::from_slice(bytes)
2377            .map_err(|error| RustdocJoinError::Json(error.to_string()))?;
2378        map.validate()?;
2379        Ok(map)
2380    }
2381
2382    pub fn validate(&self) -> Result<(), RustdocJoinError> {
2383        if self.schema != MAP_SCHEMA || !safe_group(&self.group) || self.entries.is_empty() {
2384            return Err(RustdocJoinError::Invalid(
2385                "schema, group and at least one entry are required".into(),
2386            ));
2387        }
2388        let mut modules = BTreeSet::new();
2389        let mut display_names = BTreeSet::new();
2390        let mut source_sites = BTreeSet::new();
2391        let mut previous = None;
2392        for entry in &self.entries {
2393            let Some(index) = module_index(&entry.module) else {
2394                return Err(RustdocJoinError::Invalid(format!(
2395                    "invalid merged doctest module {}",
2396                    entry.module
2397                )));
2398            };
2399            if previous.is_some_and(|previous| previous >= index) {
2400                return Err(RustdocJoinError::Invalid(
2401                    "merged doctest entries are not in numeric module order".into(),
2402                ));
2403            }
2404            previous = Some(index);
2405            if !modules.insert(entry.module.as_str())
2406                || !display_names.insert(entry.display_name.as_str())
2407                || !source_sites.insert((entry.path.as_str(), entry.line))
2408                || !normalized_relative_path(&entry.path)
2409                || entry.line == 0
2410                || entry.display_name.trim().is_empty()
2411                || entry.display_name.chars().any(char::is_control)
2412            {
2413                return Err(RustdocJoinError::Invalid(format!(
2414                    "malformed merged doctest entry {}",
2415                    entry.module
2416                )));
2417            }
2418        }
2419        Ok(())
2420    }
2421
2422    pub fn entry(&self, module: &str) -> Result<&RustdocMergedEntry, RustdocJoinError> {
2423        self.entries
2424            .iter()
2425            .find(|entry| entry.module == module)
2426            .ok_or_else(|| {
2427                RustdocJoinError::Invalid(format!(
2428                    "pending bundle module {module} has no runner descriptor"
2429                ))
2430            })
2431    }
2432
2433    fn next_line_for(&self, entry: &RustdocMergedEntry) -> Option<u64> {
2434        self.entries
2435            .iter()
2436            .filter(|candidate| candidate.path == entry.path && candidate.line > entry.line)
2437            .map(|candidate| candidate.line)
2438            .min()
2439    }
2440}
2441
2442fn source_lines(source: &str) -> Vec<(u64, usize, &str)> {
2443    let mut offset = 0;
2444    source
2445        .split_inclusive('\n')
2446        .enumerate()
2447        .map(|(index, line)| {
2448            let record = (index as u64 + 1, offset, line);
2449            offset += line.len();
2450            record
2451        })
2452        .collect()
2453}
2454
2455#[derive(Clone, Copy)]
2456struct ExtractedLine<'a> {
2457    start: usize,
2458    end: usize,
2459    source: &'a str,
2460}
2461
2462fn extracted_module_lines<'a>(
2463    bundle_source: &'a str,
2464    module: &str,
2465) -> Result<Vec<ExtractedLine<'a>>, RustdocJoinError> {
2466    let parsed = SourceFile::parse(bundle_source, Edition::CURRENT);
2467    if !parsed.errors().is_empty() {
2468        return Err(RustdocJoinError::Invalid(format!(
2469            "merged bundle does not parse as Rust: {}",
2470            parsed
2471                .errors()
2472                .iter()
2473                .map(ToString::to_string)
2474                .collect::<Vec<_>>()
2475                .join("; ")
2476        )));
2477    }
2478    let tree = parsed.tree();
2479    let modules = tree
2480        .syntax()
2481        .descendants()
2482        .filter_map(ast::Module::cast)
2483        .filter(|candidate| candidate.name().is_some_and(|name| name.text() == module))
2484        .collect::<Vec<_>>();
2485    let [module_node] = modules.as_slice() else {
2486        return Err(RustdocJoinError::Invalid(format!(
2487            "merged bundle contains {} modules named {module}",
2488            modules.len()
2489        )));
2490    };
2491    let functions = module_node
2492        .syntax()
2493        .descendants()
2494        .filter_map(ast::Fn::cast)
2495        .filter(|function| {
2496            function.name().is_some_and(|name| name.text() == "main")
2497                && function
2498                    .syntax()
2499                    .ancestors()
2500                    .skip(1)
2501                    .find_map(ast::Module::cast)
2502                    .as_ref()
2503                    == Some(module_node)
2504        })
2505        .collect::<Vec<_>>();
2506    let [function] = functions.as_slice() else {
2507        return Err(RustdocJoinError::Invalid(format!(
2508            "merged module {module} contains {} direct main functions",
2509            functions.len()
2510        )));
2511    };
2512    let body = function.body().ok_or_else(|| {
2513        RustdocJoinError::Invalid(format!("merged module {module} main has no body"))
2514    })?;
2515    let range = body.syntax().text_range();
2516    let body_start = usize::from(range.start());
2517    let body_end = usize::from(range.end());
2518    if body_end <= body_start + 1
2519        || bundle_source.as_bytes().get(body_start) != Some(&b'{')
2520        || bundle_source.as_bytes().get(body_end - 1) != Some(&b'}')
2521    {
2522        return Err(RustdocJoinError::Invalid(format!(
2523            "merged module {module} main has an invalid syntax range"
2524        )));
2525    }
2526    let content_start = body_start + 1;
2527    let content = &bundle_source[content_start..body_end - 1];
2528    let mut offset = content_start;
2529    let lines = content
2530        .split_inclusive('\n')
2531        .filter_map(|line| {
2532            let source = line.strip_suffix('\n').unwrap_or(line);
2533            let record = (!source.trim().is_empty()).then_some(ExtractedLine {
2534                start: offset,
2535                end: offset + source.len(),
2536                source,
2537            });
2538            offset += line.len();
2539            record
2540        })
2541        .collect::<Vec<_>>();
2542    if lines.is_empty() {
2543        return Err(RustdocJoinError::Invalid(format!(
2544            "merged module {module} main has no extracted source lines"
2545        )));
2546    }
2547    Ok(lines)
2548}
2549
2550/// Map one exact extracted range to its authored source. Its nonblank lines
2551/// must have exactly one complete, ordered mapping inside that doctest's
2552/// runner-bounded source interval. Repeated fragments are valid when their
2553/// sequence identifies one mapping; genuinely ambiguous sequences fail closed.
2554pub fn map_merged_range(
2555    map: &RustdocMergedMap,
2556    module: &str,
2557    bundle_source: &str,
2558    pending_start: u32,
2559    pending_end: u32,
2560    authored_source: &str,
2561) -> Result<RustdocMappedRange, RustdocJoinError> {
2562    map.validate()?;
2563    let entry = map.entry(module)?;
2564    let start = pending_start as usize;
2565    let end = pending_end as usize;
2566    if start >= end
2567        || end > bundle_source.len()
2568        || !bundle_source.is_char_boundary(start)
2569        || !bundle_source.is_char_boundary(end)
2570    {
2571        return Err(RustdocJoinError::Invalid(format!(
2572            "pending range {pending_start}..{pending_end} is outside UTF-8 bundle bytes"
2573        )));
2574    }
2575    if bundle_source[start..end].contains('\r') {
2576        return Err(RustdocJoinError::Invalid(
2577            "carriage-return extracted source is unsupported".into(),
2578        ));
2579    }
2580    let next_line = map.next_line_for(entry).unwrap_or(u64::MAX);
2581    let authored_lines = source_lines(authored_source);
2582    let extracted_lines = extracted_module_lines(bundle_source, module)?;
2583    let candidates = extracted_lines
2584        .iter()
2585        .map(|extracted| {
2586            authored_lines
2587                .iter()
2588                .filter(|(line, _, _)| *line >= entry.line && *line < next_line)
2589                .flat_map(|(line, offset, authored_line)| {
2590                    authored_line
2591                        .match_indices(extracted.source)
2592                        .map(move |(column, _)| (*line, *offset + column, extracted.source.len()))
2593                })
2594                .collect::<Vec<_>>()
2595        })
2596        .collect::<Vec<_>>();
2597    if candidates.iter().any(Vec::is_empty) {
2598        return Err(RustdocJoinError::Invalid(format!(
2599            "merged fragment has no authored match in {}:{}",
2600            entry.path, entry.line
2601        )));
2602    }
2603    fn ordered_sequences(
2604        candidates: &[Vec<(u64, usize, usize)>],
2605        index: usize,
2606        previous_line: Option<u64>,
2607        current: &mut Vec<(u64, usize, usize)>,
2608        solutions: &mut Vec<Vec<(u64, usize, usize)>>,
2609    ) {
2610        if solutions.len() > 1 {
2611            return;
2612        }
2613        if index == candidates.len() {
2614            solutions.push(current.clone());
2615            return;
2616        }
2617        for candidate in &candidates[index] {
2618            if previous_line.is_some_and(|previous| previous >= candidate.0) {
2619                continue;
2620            }
2621            current.push(*candidate);
2622            ordered_sequences(candidates, index + 1, Some(candidate.0), current, solutions);
2623            current.pop();
2624            if solutions.len() > 1 {
2625                return;
2626            }
2627        }
2628    }
2629    let mut solutions = Vec::new();
2630    ordered_sequences(&candidates, 0, None, &mut Vec::new(), &mut solutions);
2631    let [anchors] = solutions.as_slice() else {
2632        return Err(RustdocJoinError::Invalid(format!(
2633            "merged fragments have {} ordered authored mappings in {}:{}",
2634            solutions.len(),
2635            entry.path,
2636            entry.line
2637        )));
2638    };
2639    let start_line = extracted_lines
2640        .iter()
2641        .position(|line| start >= line.start && start < line.end)
2642        .ok_or_else(|| {
2643            RustdocJoinError::Invalid(format!(
2644                "pending range start {pending_start} is outside extracted source lines"
2645            ))
2646        })?;
2647    let end_line = extracted_lines
2648        .iter()
2649        .position(|line| end > line.start && end <= line.end)
2650        .ok_or_else(|| {
2651            RustdocJoinError::Invalid(format!(
2652                "pending range end {pending_end} is outside extracted source lines"
2653            ))
2654        })?;
2655    if start_line > end_line {
2656        return Err(RustdocJoinError::Invalid(
2657            "pending range crosses extracted lines in reverse order".into(),
2658        ));
2659    }
2660    let authored_start = anchors[start_line]
2661        .1
2662        .checked_add(start - extracted_lines[start_line].start)
2663        .ok_or_else(|| RustdocJoinError::Invalid("authored source offset overflow".into()))?;
2664    let authored_end = anchors[end_line]
2665        .1
2666        .checked_add(end - extracted_lines[end_line].start)
2667        .ok_or_else(|| RustdocJoinError::Invalid("authored source offset overflow".into()))?;
2668    Ok(RustdocMappedRange {
2669        source_key: format!("source:{}", entry.path),
2670        start: u32::try_from(authored_start)
2671            .map_err(|_| RustdocJoinError::Invalid("authored start exceeds u32".into()))?,
2672        end: u32::try_from(authored_end)
2673            .map_err(|_| RustdocJoinError::Invalid("authored end exceeds u32".into()))?,
2674    })
2675}
2676
2677/// Produce the frozen identity for a non-synthetic authored/doctest
2678/// obligation after deferred source mapping.
2679pub fn rust_source_identity(
2680    kind: &str,
2681    source: &RustdocMappedRange,
2682    discriminator: &str,
2683) -> Result<RustSourceIdentity, RustdocJoinError> {
2684    if !matches!(
2685        kind,
2686        "statement" | "function" | "branch" | "branch-alternative" | "decision" | "match-group"
2687    ) || !source.source_key.starts_with("source:")
2688        || !normalized_relative_path(&source.source_key["source:".len()..])
2689        || source.start >= source.end
2690    {
2691        return Err(RustdocJoinError::Invalid(
2692            "invalid final Rust source identity input".into(),
2693        ));
2694    }
2695    identity_for_range(kind, source, discriminator)
2696}
2697
2698fn identity_for_range(
2699    kind: &str,
2700    source: &RustdocMappedRange,
2701    discriminator: &str,
2702) -> Result<RustSourceIdentity, RustdocJoinError> {
2703    if !matches!(
2704        kind,
2705        "statement" | "function" | "branch" | "branch-alternative" | "decision" | "match-group"
2706    ) || source.start >= source.end
2707        || source.source_key.chars().any(char::is_control)
2708        || discriminator.chars().any(char::is_control)
2709    {
2710        return Err(RustdocJoinError::Invalid(
2711            "invalid Rust source identity components".into(),
2712        ));
2713    }
2714    let canonical = format!(
2715        "{SOURCE_MODEL}\0{kind}\0{}\0{}\0{}\0{discriminator}\0",
2716        source.source_key, source.start, source.end
2717    );
2718    identity_from_canonical(kind, canonical)
2719}
2720
2721fn identity_from_canonical(
2722    kind: &str,
2723    canonical: String,
2724) -> Result<RustSourceIdentity, RustdocJoinError> {
2725    let digest = Sha256::digest(canonical.as_bytes());
2726    let encoded = digest[..12]
2727        .iter()
2728        .map(|byte| format!("{byte:02x}"))
2729        .collect::<String>();
2730    let probe_ordinal = u64::from_be_bytes(
2731        digest[..8]
2732            .try_into()
2733            .expect("a SHA-256 digest always has eight prefix bytes"),
2734    );
2735    Ok(RustSourceIdentity {
2736        id: format!("rs:{kind}:{encoded}"),
2737        canonical,
2738        probe_ordinal,
2739    })
2740}
2741
2742#[derive(Debug)]
2743struct SyntheticExpansionFrame {
2744    description: String,
2745    source: RustdocMappedRange,
2746    definition: String,
2747}
2748
2749#[derive(Debug)]
2750struct SyntheticCanonical {
2751    frames: Vec<SyntheticExpansionFrame>,
2752    definition: String,
2753    owner_ordinal: u64,
2754}
2755
2756fn canonical_u32(value: &str, field: &str) -> Result<u32, RustdocJoinError> {
2757    let parsed = value.parse::<u32>().map_err(|_| {
2758        RustdocJoinError::Invalid(format!("synthetic canonical has invalid {field}"))
2759    })?;
2760    if value != parsed.to_string() {
2761        return Err(RustdocJoinError::Invalid(format!(
2762            "synthetic canonical has non-canonical {field}"
2763        )));
2764    }
2765    Ok(parsed)
2766}
2767
2768fn canonical_u64(value: &str, field: &str) -> Result<u64, RustdocJoinError> {
2769    let parsed = value.parse::<u64>().map_err(|_| {
2770        RustdocJoinError::Invalid(format!("synthetic canonical has invalid {field}"))
2771    })?;
2772    if value != parsed.to_string() {
2773        return Err(RustdocJoinError::Invalid(format!(
2774            "synthetic canonical has non-canonical {field}"
2775        )));
2776    }
2777    Ok(parsed)
2778}
2779
2780fn parse_synthetic_canonical(
2781    canonical: &str,
2782    kind: &str,
2783    source_key: &str,
2784    start: u32,
2785    end: u32,
2786    discriminator: &str,
2787) -> Result<Option<SyntheticCanonical>, RustdocJoinError> {
2788    let parts = canonical.split('\0').collect::<Vec<_>>();
2789    if parts.get(6) != Some(&"synthetic-expansion") {
2790        return Ok(None);
2791    }
2792    if parts.last() != Some(&"")
2793        || parts.len() < 15
2794        || (parts.len() - 10) % 5 != 0
2795        || parts[0] != SOURCE_MODEL
2796        || parts[1] != kind
2797        || parts[2] != source_key
2798        || canonical_u32(parts[3], "source start")? != start
2799        || canonical_u32(parts[4], "source end")? != end
2800        || parts[5] != discriminator
2801    {
2802        return Err(RustdocJoinError::Invalid(format!(
2803            "malformed synthetic canonical for {kind}"
2804        )));
2805    }
2806    let frame_count = (parts.len() - 10) / 5;
2807    let mut frames = Vec::with_capacity(frame_count);
2808    for frame in parts[7..7 + frame_count * 5].chunks_exact(5) {
2809        if frame[0].is_empty() || frame[1].is_empty() || frame[4].is_empty() {
2810            return Err(RustdocJoinError::Invalid(
2811                "synthetic expansion frame has an empty identity component".into(),
2812            ));
2813        }
2814        frames.push(SyntheticExpansionFrame {
2815            description: frame[0].into(),
2816            source: RustdocMappedRange {
2817                source_key: frame[1].into(),
2818                start: canonical_u32(frame[2], "frame start")?,
2819                end: canonical_u32(frame[3], "frame end")?,
2820            },
2821            definition: frame[4].into(),
2822        });
2823    }
2824    let definition_index = 7 + frame_count * 5;
2825    let definition = parts[definition_index];
2826    let owner_ordinal = canonical_u64(parts[definition_index + 1], "owner ordinal")?;
2827    if definition.is_empty() {
2828        return Err(RustdocJoinError::Invalid(
2829            "synthetic canonical has an empty owner definition".into(),
2830        ));
2831    }
2832    Ok(Some(SyntheticCanonical {
2833        frames,
2834        definition: definition.into(),
2835        owner_ordinal,
2836    }))
2837}
2838
2839fn stable_definition(
2840    entry: &RustdocMergedEntry,
2841    definition: &str,
2842) -> Result<String, RustdocJoinError> {
2843    let main = format!("{}::main", entry.module);
2844    if let Some(suffix) = definition.strip_prefix(&main) {
2845        return Ok(format!("doctest:{}:{}{suffix}", entry.path, entry.line));
2846    }
2847    if definition.is_empty()
2848        || definition.chars().any(char::is_control)
2849        || definition.contains("doctest_bundle_")
2850        || definition.contains("__doctest_")
2851    {
2852        return Err(RustdocJoinError::Invalid(format!(
2853            "synthetic expansion definition {definition} is not stable"
2854        )));
2855    }
2856    Ok(definition.into())
2857}
2858
2859struct RebasedIdentity {
2860    identity: RustSourceIdentity,
2861    source: RustdocMappedRange,
2862    provenance: &'static str,
2863}
2864
2865#[allow(clippy::too_many_arguments)]
2866fn rebase_identity(
2867    map: &RustdocMergedMap,
2868    entry: &RustdocMergedEntry,
2869    bundle_source: &str,
2870    authored_sources: &BTreeMap<String, RustCompilerSource>,
2871    kind: &str,
2872    source_key: &str,
2873    start: u32,
2874    end: u32,
2875    old_discriminator: &str,
2876    new_discriminator: &str,
2877    id: &str,
2878    canonical: &str,
2879    probe_ordinal: &str,
2880) -> Result<RebasedIdentity, RustdocJoinError> {
2881    let source = map_obligation_range(map, entry, bundle_source, start, end, authored_sources)?;
2882    if let Some(synthetic) =
2883        parse_synthetic_canonical(canonical, kind, source_key, start, end, old_discriminator)?
2884    {
2885        let old = identity_from_canonical(kind, canonical.into())?;
2886        verify_pending_identity(&old, id, Some(canonical), probe_ordinal)?;
2887        let pending_key = format!("doctest-pending:{}", map.group);
2888        let mut frame_canonical = String::new();
2889        for frame in synthetic.frames {
2890            if frame.source.source_key != pending_key {
2891                return Err(RustdocJoinError::Invalid(format!(
2892                    "synthetic expansion frame escaped pending source {}",
2893                    frame.source.source_key
2894                )));
2895            }
2896            let mapped = map_obligation_range(
2897                map,
2898                entry,
2899                bundle_source,
2900                frame.source.start,
2901                frame.source.end,
2902                authored_sources,
2903            )?;
2904            frame_canonical.push_str(&format!(
2905                "{}\0{}\0{}\0{}\0{}\0",
2906                frame.description,
2907                mapped.source_key,
2908                mapped.start,
2909                mapped.end,
2910                stable_definition(entry, &frame.definition)?,
2911            ));
2912        }
2913        let canonical = format!(
2914            "{SOURCE_MODEL}\0{kind}\0{}\0{}\0{}\0{new_discriminator}\0synthetic-expansion\0{}{}\0{}\0",
2915            source.source_key,
2916            source.start,
2917            source.end,
2918            frame_canonical,
2919            stable_definition(entry, &synthetic.definition)?,
2920            synthetic.owner_ordinal,
2921        );
2922        return Ok(RebasedIdentity {
2923            identity: identity_from_canonical(kind, canonical)?,
2924            source,
2925            provenance: "synthetic-expansion",
2926        });
2927    }
2928    let old = pending_identity(kind, source_key, start, end, old_discriminator)?;
2929    verify_pending_identity(&old, id, Some(canonical), probe_ordinal)?;
2930    Ok(RebasedIdentity {
2931        identity: rust_source_identity(kind, &source, new_discriminator)?,
2932        source,
2933        provenance: "doctest-source",
2934    })
2935}
2936
2937fn pending_identity(
2938    kind: &str,
2939    source_key: &str,
2940    start: u32,
2941    end: u32,
2942    discriminator: &str,
2943) -> Result<RustSourceIdentity, RustdocJoinError> {
2944    identity_for_range(
2945        kind,
2946        &RustdocMappedRange {
2947            source_key: source_key.into(),
2948            start,
2949            end,
2950        },
2951        discriminator,
2952    )
2953}
2954
2955fn verify_pending_identity(
2956    identity: &RustSourceIdentity,
2957    id: &str,
2958    canonical: Option<&str>,
2959    probe_ordinal: &str,
2960) -> Result<(), RustdocJoinError> {
2961    if identity.probe_ordinal == 0
2962        || id != identity.id
2963        || canonical.is_some_and(|canonical| canonical != identity.canonical)
2964        || probe_ordinal != identity.probe_ordinal.to_string()
2965    {
2966        return Err(RustdocJoinError::Invalid(format!(
2967            "temporary merged-doctest identity {id} does not match its frozen canonical form"
2968        )));
2969    }
2970    Ok(())
2971}
2972
2973fn insert_translation(
2974    ids: &mut BTreeMap<String, String>,
2975    ordinals: &mut BTreeMap<String, String>,
2976    old_id: &str,
2977    old_ordinal: &str,
2978    new_identity: &RustSourceIdentity,
2979) -> Result<(), RustdocJoinError> {
2980    let parsed_ordinal = old_ordinal.parse::<u64>().map_err(|_| {
2981        RustdocJoinError::Invalid(format!(
2982            "temporary obligation {old_id} has an invalid ordinal"
2983        ))
2984    })?;
2985    if parsed_ordinal == 0 || old_ordinal != parsed_ordinal.to_string() {
2986        return Err(RustdocJoinError::Invalid(format!(
2987            "temporary obligation {old_id} has a non-canonical ordinal"
2988        )));
2989    }
2990    if ids.insert(old_id.into(), new_identity.id.clone()).is_some()
2991        || ordinals
2992            .insert(old_ordinal.into(), new_identity.probe_ordinal.to_string())
2993            .is_some()
2994    {
2995        return Err(RustdocJoinError::Invalid(format!(
2996            "duplicate temporary merged-doctest identity {old_id}"
2997        )));
2998    }
2999    Ok(())
3000}
3001
3002fn definition_module<'a>(
3003    map: &'a RustdocMergedMap,
3004    definitions: &[String],
3005) -> Result<&'a RustdocMergedEntry, RustdocJoinError> {
3006    let mut matches = BTreeSet::new();
3007    for definition in definitions {
3008        for entry in &map.entries {
3009            let main = format!("{}::main", entry.module);
3010            if definition == &main || definition.starts_with(&format!("{main}::")) {
3011                matches.insert(entry.module.as_str());
3012            }
3013        }
3014    }
3015    if matches.len() != 1 {
3016        return Err(RustdocJoinError::Invalid(format!(
3017            "obligation definitions do not resolve to exactly one merged doctest module: {}",
3018            definitions.join(", ")
3019        )));
3020    }
3021    let module = matches.into_iter().next().expect("exactly one module");
3022    map.entry(module)
3023}
3024
3025fn stable_definitions(
3026    entry: &RustdocMergedEntry,
3027    definitions: &[String],
3028) -> Result<Vec<String>, RustdocJoinError> {
3029    let main = format!("{}::main", entry.module);
3030    let root = format!("doctest:{}:{}", entry.path, entry.line);
3031    let mut stable = definitions
3032        .iter()
3033        .map(|definition| {
3034            definition
3035                .strip_prefix(&main)
3036                .map(|suffix| format!("{root}{suffix}"))
3037        })
3038        .collect::<Option<Vec<_>>>()
3039        .ok_or_else(|| {
3040            RustdocJoinError::Invalid(format!(
3041                "definition escaped merged doctest module {}",
3042                entry.module
3043            ))
3044        })?;
3045    stable.sort();
3046    stable.dedup();
3047    if stable.is_empty() {
3048        return Err(RustdocJoinError::Invalid(
3049            "merged doctest obligation has no stable definitions".into(),
3050        ));
3051    }
3052    Ok(stable)
3053}
3054
3055fn authored_source<'a>(
3056    sources: &'a BTreeMap<String, RustCompilerSource>,
3057    entry: &RustdocMergedEntry,
3058) -> Result<&'a RustCompilerSource, RustdocJoinError> {
3059    let key = format!("source:{}", entry.path);
3060    let source = sources.get(&key).ok_or_else(|| {
3061        RustdocJoinError::Invalid(format!("authored source snapshot {key} was not supplied"))
3062    })?;
3063    if source.file != entry.path {
3064        return Err(RustdocJoinError::Invalid(format!(
3065            "authored source snapshot {key} has display path {}",
3066            source.file
3067        )));
3068    }
3069    Ok(source)
3070}
3071
3072fn map_obligation_range(
3073    map: &RustdocMergedMap,
3074    entry: &RustdocMergedEntry,
3075    bundle_source: &str,
3076    start: u32,
3077    end: u32,
3078    authored_sources: &BTreeMap<String, RustCompilerSource>,
3079) -> Result<RustdocMappedRange, RustdocJoinError> {
3080    let source = authored_source(authored_sources, entry)?;
3081    map_merged_range(
3082        map,
3083        &entry.module,
3084        bundle_source,
3085        start,
3086        end,
3087        &source.source,
3088    )
3089}
3090
3091fn alternative_discriminator(
3092    discriminator: &str,
3093    kind: &str,
3094    label: &str,
3095) -> Result<String, RustdocJoinError> {
3096    let token = match (kind, label) {
3097        ("decision-outcome", "condition false") => "false",
3098        ("decision-outcome", "condition true") => "true",
3099        ("assertion-outcome", "failed") => "failed",
3100        ("assertion-outcome", "passed") => "passed",
3101        ("loop-entry", "zero iterations") => "zero",
3102        ("loop-entry", "entered") => "entered",
3103        ("match-arm", "not selected") => "not-selected",
3104        ("match-arm", "selected") => "selected",
3105        ("let-else", "matched") => "matched",
3106        ("let-else", "else") => "else",
3107        ("try-operator", "continued") => "continued",
3108        ("try-operator", "early return") => "returned",
3109        ("logical-selection", "short-circuited") => "short-circuit",
3110        ("logical-selection", "right operand evaluated") => "evaluated",
3111        _ => {
3112            return Err(RustdocJoinError::Invalid(format!(
3113                "unknown {} alternative label {label}",
3114                kind
3115            )));
3116        }
3117    };
3118    Ok(format!("{discriminator}:{token}"))
3119}
3120
3121/// Resolve a merged rustdoc bundle only after its runner map and immutable
3122/// authored source snapshots are available. The returned ID/ordinal maps are
3123/// required to translate already-emitted bundle observations; accepting the
3124/// final manifest without translating those observations would silently lose
3125/// coverage.
3126pub fn join_merged_doctest(
3127    pending_manifest_bytes: &[u8],
3128    pending_source_bytes: &[u8],
3129    map_bytes: &[u8],
3130    authored_sources: &BTreeMap<String, RustCompilerSource>,
3131) -> Result<RustdocMergedJoin, RustdocJoinError> {
3132    let map = RustdocMergedMap::parse(map_bytes)?;
3133    let mut manifest =
3134        RustCompilerManifest::parse_pending_doctest(pending_manifest_bytes, &map.group)
3135            .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3136    let pending_sources =
3137        RustCompilerSourceSnapshots::parse_pending_doctest(pending_source_bytes, &map.group)
3138            .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3139    if pending_sources.crate_name != manifest.crate_name {
3140        return Err(RustdocJoinError::Invalid(
3141            "pending merged-doctest manifest/source crate mismatch".into(),
3142        ));
3143    }
3144    let pending_key = format!("doctest-pending:{}", map.group);
3145    let bundle_source = &pending_sources
3146        .sources
3147        .get(&pending_key)
3148        .expect("pending source parser requires the exact key")
3149        .source;
3150    let mut ids = BTreeMap::new();
3151    let mut ordinals = BTreeMap::new();
3152
3153    for point in &mut manifest.points {
3154        let entry = definition_module(&map, &point.definitions)?;
3155        let rebased = rebase_identity(
3156            &map,
3157            entry,
3158            bundle_source,
3159            authored_sources,
3160            &point.kind,
3161            &point.source_key,
3162            point.start,
3163            point.end,
3164            &point.discriminator,
3165            &point.discriminator,
3166            &point.id,
3167            &point.canonical,
3168            &point.probe_ordinal,
3169        )?;
3170        insert_translation(
3171            &mut ids,
3172            &mut ordinals,
3173            &point.id,
3174            &point.probe_ordinal,
3175            &rebased.identity,
3176        )?;
3177        point.id = rebased.identity.id;
3178        point.canonical = rebased.identity.canonical;
3179        point.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3180        point.source_key = rebased.source.source_key;
3181        point.start = rebased.source.start;
3182        point.end = rebased.source.end;
3183        point.provenance = rebased.provenance.into();
3184        point.definitions = stable_definitions(entry, &point.definitions)?;
3185    }
3186
3187    let mut group_ids = BTreeMap::new();
3188    for group in &mut manifest.selection_groups {
3189        let entry = definition_module(&map, &group.definitions)?;
3190        let rebased = rebase_identity(
3191            &map,
3192            entry,
3193            bundle_source,
3194            authored_sources,
3195            "match-group",
3196            &group.source_key,
3197            group.start,
3198            group.end,
3199            "match",
3200            "match",
3201            &group.id,
3202            &group.canonical,
3203            &group.probe_ordinal,
3204        )?;
3205        insert_translation(
3206            &mut ids,
3207            &mut ordinals,
3208            &group.id,
3209            &group.probe_ordinal,
3210            &rebased.identity,
3211        )?;
3212        group_ids.insert(group.id.clone(), rebased.identity.id.clone());
3213        group.id = rebased.identity.id;
3214        group.canonical = rebased.identity.canonical;
3215        group.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3216        group.source_key = rebased.source.source_key;
3217        group.start = rebased.source.start;
3218        group.end = rebased.source.end;
3219        group.provenance = rebased.provenance.into();
3220        group.definitions = stable_definitions(entry, &group.definitions)?;
3221        for arm in &mut group.arms {
3222            let source = map_obligation_range(
3223                &map,
3224                entry,
3225                bundle_source,
3226                arm.body_start,
3227                arm.body_end,
3228                authored_sources,
3229            )?;
3230            arm.body_source_key = source.source_key;
3231            arm.body_start = source.start;
3232            arm.body_end = source.end;
3233        }
3234    }
3235
3236    let mut branch_ids = BTreeMap::new();
3237    for branch in &mut manifest.branches {
3238        let old_discriminator = branch.discriminator.clone();
3239        let old_source_key = branch.source_key.clone();
3240        let old_start = branch.start;
3241        let old_end = branch.end;
3242        let entry = definition_module(&map, &branch.definitions)?;
3243        let discriminator = if branch.kind == "match-arm" {
3244            let mut translated = old_discriminator.clone();
3245            for (old_group, new_group) in &group_ids {
3246                translated = translated.replace(old_group, new_group);
3247            }
3248            if translated == old_discriminator {
3249                return Err(RustdocJoinError::Invalid(format!(
3250                    "match-arm discriminator {} has no translated parent group",
3251                    old_discriminator
3252                )));
3253            }
3254            translated
3255        } else {
3256            old_discriminator.clone()
3257        };
3258        let rebased = rebase_identity(
3259            &map,
3260            entry,
3261            bundle_source,
3262            authored_sources,
3263            "branch",
3264            &old_source_key,
3265            old_start,
3266            old_end,
3267            &old_discriminator,
3268            &discriminator,
3269            &branch.id,
3270            &branch.canonical,
3271            &branch.probe_ordinal,
3272        )?;
3273        insert_translation(
3274            &mut ids,
3275            &mut ordinals,
3276            &branch.id,
3277            &branch.probe_ordinal,
3278            &rebased.identity,
3279        )?;
3280        branch_ids.insert(branch.id.clone(), rebased.identity.id.clone());
3281        branch.id = rebased.identity.id;
3282        branch.canonical = rebased.identity.canonical;
3283        branch.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3284        branch.source_key = rebased.source.source_key.clone();
3285        branch.start = rebased.source.start;
3286        branch.end = rebased.source.end;
3287        branch.provenance = rebased.provenance.into();
3288        branch.definitions = stable_definitions(entry, &branch.definitions)?;
3289        branch.discriminator = discriminator.clone();
3290        for alternative in &mut branch.alternatives {
3291            let old_alternative_discriminator =
3292                alternative_discriminator(&old_discriminator, &branch.kind, &alternative.label)?;
3293            let new_discriminator =
3294                alternative_discriminator(&discriminator, &branch.kind, &alternative.label)?;
3295            let rebased = rebase_identity(
3296                &map,
3297                entry,
3298                bundle_source,
3299                authored_sources,
3300                "branch-alternative",
3301                &old_source_key,
3302                old_start,
3303                old_end,
3304                &old_alternative_discriminator,
3305                &new_discriminator,
3306                &alternative.id,
3307                &alternative.canonical,
3308                &alternative.probe_ordinal,
3309            )?;
3310            insert_translation(
3311                &mut ids,
3312                &mut ordinals,
3313                &alternative.id,
3314                &alternative.probe_ordinal,
3315                &rebased.identity,
3316            )?;
3317            alternative.id = rebased.identity.id;
3318            alternative.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3319            alternative.canonical = rebased.identity.canonical;
3320        }
3321    }
3322
3323    let mut decision_ids = BTreeMap::new();
3324    for decision in &mut manifest.decisions {
3325        let entry = definition_module(&map, &decision.definitions)?;
3326        let rebased = rebase_identity(
3327            &map,
3328            entry,
3329            bundle_source,
3330            authored_sources,
3331            "decision",
3332            &decision.source_key,
3333            decision.start,
3334            decision.end,
3335            &decision.kind,
3336            &decision.kind,
3337            &decision.id,
3338            &decision.canonical,
3339            &decision.probe_ordinal,
3340        )?;
3341        insert_translation(
3342            &mut ids,
3343            &mut ordinals,
3344            &decision.id,
3345            &decision.probe_ordinal,
3346            &rebased.identity,
3347        )?;
3348        decision_ids.insert(decision.id.clone(), rebased.identity.id.clone());
3349        decision.id = rebased.identity.id;
3350        decision.canonical = rebased.identity.canonical;
3351        decision.probe_ordinal = rebased.identity.probe_ordinal.to_string();
3352        decision.source_key = rebased.source.source_key;
3353        decision.start = rebased.source.start;
3354        decision.end = rebased.source.end;
3355        decision.provenance = rebased.provenance.into();
3356        decision.definitions = stable_definitions(entry, &decision.definitions)?;
3357        decision.outcome_branch_id = branch_ids
3358            .get(&decision.outcome_branch_id)
3359            .cloned()
3360            .ok_or_else(|| {
3361                RustdocJoinError::Invalid("decision outcome branch was not rebased".into())
3362            })?;
3363        decision.loop_branch_id = decision
3364            .loop_branch_id
3365            .as_ref()
3366            .map(|id| {
3367                branch_ids.get(id).cloned().ok_or_else(|| {
3368                    RustdocJoinError::Invalid("decision loop branch was not rebased".into())
3369                })
3370            })
3371            .transpose()?;
3372        for selection in &mut decision.logical_selections {
3373            selection.branch_id =
3374                branch_ids
3375                    .get(&selection.branch_id)
3376                    .cloned()
3377                    .ok_or_else(|| {
3378                        RustdocJoinError::Invalid(
3379                            "decision logical-selection branch was not rebased".into(),
3380                        )
3381                    })?;
3382        }
3383        decision
3384            .logical_selections
3385            .sort_by(|left, right| left.branch_id.cmp(&right.branch_id));
3386        for condition in &mut decision.conditions {
3387            let source = map_obligation_range(
3388                &map,
3389                entry,
3390                bundle_source,
3391                condition.start,
3392                condition.end,
3393                authored_sources,
3394            )?;
3395            condition.source_key = source.source_key;
3396            condition.start = source.start;
3397            condition.end = source.end;
3398        }
3399    }
3400
3401    for group in &mut manifest.selection_groups {
3402        group.parent_group_id = group
3403            .parent_group_id
3404            .as_ref()
3405            .map(|id| {
3406                group_ids.get(id).cloned().ok_or_else(|| {
3407                    RustdocJoinError::Invalid("match parent group was not rebased".into())
3408                })
3409            })
3410            .transpose()?;
3411        for arm in &mut group.arms {
3412            arm.branch_id = branch_ids.get(&arm.branch_id).cloned().ok_or_else(|| {
3413                RustdocJoinError::Invalid("match arm branch was not rebased".into())
3414            })?;
3415            arm.guard_decision_id = arm
3416                .guard_decision_id
3417                .as_ref()
3418                .map(|id| {
3419                    decision_ids.get(id).cloned().ok_or_else(|| {
3420                        RustdocJoinError::Invalid("match guard decision was not rebased".into())
3421                    })
3422                })
3423                .transpose()?;
3424            arm.selected_ordinal = ordinals
3425                .get(&arm.selected_ordinal)
3426                .ok_or_else(|| {
3427                    RustdocJoinError::Invalid("match selected ordinal was not rebased".into())
3428                })?
3429                .clone();
3430            arm.not_selected_ordinal = ordinals
3431                .get(&arm.not_selected_ordinal)
3432                .ok_or_else(|| {
3433                    RustdocJoinError::Invalid("match not-selected ordinal was not rebased".into())
3434                })?
3435                .clone();
3436        }
3437    }
3438
3439    manifest
3440        .points
3441        .sort_by(|left, right| left.id.cmp(&right.id));
3442    manifest
3443        .branches
3444        .sort_by(|left, right| left.id.cmp(&right.id));
3445    manifest
3446        .decisions
3447        .sort_by(|left, right| left.id.cmp(&right.id));
3448    manifest
3449        .selection_groups
3450        .sort_by(|left, right| left.id.cmp(&right.id));
3451
3452    let required_keys = manifest
3453        .points
3454        .iter()
3455        .map(|point| point.source_key.as_str())
3456        .chain(
3457            manifest
3458                .branches
3459                .iter()
3460                .map(|branch| branch.source_key.as_str()),
3461        )
3462        .chain(manifest.decisions.iter().flat_map(|decision| {
3463            std::iter::once(decision.source_key.as_str()).chain(
3464                decision
3465                    .conditions
3466                    .iter()
3467                    .map(|condition| condition.source_key.as_str()),
3468            )
3469        }))
3470        .chain(manifest.selection_groups.iter().flat_map(|group| {
3471            std::iter::once(group.source_key.as_str())
3472                .chain(group.arms.iter().map(|arm| arm.body_source_key.as_str()))
3473        }))
3474        .collect::<BTreeSet<_>>();
3475    let sources = RustCompilerSourceSnapshots {
3476        schema: pending_sources.schema,
3477        crate_name: manifest.crate_name.clone(),
3478        sources: required_keys
3479            .into_iter()
3480            .map(|key| {
3481                authored_sources
3482                    .get(key)
3483                    .cloned()
3484                    .map(|source| (key.into(), source))
3485                    .ok_or_else(|| {
3486                        RustdocJoinError::Invalid(format!(
3487                            "final authored source snapshot {key} was not supplied"
3488                        ))
3489                    })
3490            })
3491            .collect::<Result<_, _>>()?,
3492    };
3493    manifest
3494        .validate()
3495        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3496    sources
3497        .validate()
3498        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3499    manifest
3500        .normalize(&sources.sources)
3501        .map_err(|error| RustdocJoinError::Manifest(error.to_string()))?;
3502    Ok(RustdocMergedJoin {
3503        manifest,
3504        sources,
3505        obligation_ids: ids,
3506        probe_ordinals: ordinals,
3507    })
3508}
3509
3510#[cfg(test)]
3511mod tests {
3512    use super::*;
3513    use crate::rust_compiler_manifest::{
3514        RustCompilerBranch, RustCompilerBranchAlternative, RustCompilerCondition,
3515        RustCompilerDecision, RustCompilerManifest, RustCompilerMatchArm, RustCompilerPoint,
3516        RustCompilerSelectionGroup, RustCompilerSourceSnapshots,
3517        normalize_rust_compiler_candidates,
3518    };
3519    use crate::rust_probe_transport::{
3520        RustOrdinalHit, RustTestBoundary, RustThreadEnd, RustTransportObservation,
3521        rust_assertion_context_id,
3522    };
3523
3524    fn map() -> RustdocMergedMap {
3525        RustdocMergedMap::parse(
3526            br#"{
3527                "schema":"supercov-rustdoc-merged-map-v2",
3528                "group":"fixture",
3529                "entries":[
3530                    {"module":"__doctest_0","displayName":"src/lib.rs - (line 3)","path":"src/lib.rs","line":3,"ignored":false,"noRun":false,"shouldPanic":false},
3531                    {"module":"__doctest_1","displayName":"src/lib.rs - (line 10)","path":"src/lib.rs","line":10,"ignored":false,"noRun":true,"shouldPanic":true}
3532                ]
3533            }"#,
3534        )
3535        .expect("valid map")
3536    }
3537
3538    fn merged_bundle(body: &str) -> String {
3539        format!(
3540            "\n#![allow(unused)]\npub mod __doctest_0 {{\nfn main() {{\n{body}\n}}\npub fn __main_fn() -> impl std::process::Termination {{ main() }}\n}}\n"
3541        )
3542    }
3543
3544    fn libtest_stream(lines: &[&str]) -> Vec<u8> {
3545        lines.join("\n").into_bytes()
3546    }
3547
3548    fn empty_transport() -> RustTransportRead {
3549        RustTransportRead::empty()
3550    }
3551
3552    fn transport_sha256(transport: &RustTransportRead) -> String {
3553        format!(
3554            "{:x}",
3555            Sha256::digest(serde_json::to_vec(transport).unwrap())
3556        )
3557    }
3558
3559    fn catalog_doctest(
3560        name: &str,
3561        line: u64,
3562        ignored: bool,
3563        no_run: bool,
3564        should_panic: bool,
3565        compile_fail: bool,
3566        standalone_crate: bool,
3567    ) -> RustdocExtractedDoctest {
3568        RustdocExtractedDoctest {
3569            file: "src/lib.rs".into(),
3570            line,
3571            doctest_attributes: RustdocDoctestAttributes {
3572                original: String::new(),
3573                should_panic,
3574                no_run,
3575                ignore: if ignored {
3576                    RustdocDoctestIgnore::All
3577                } else {
3578                    RustdocDoctestIgnore::None
3579                },
3580                rust: true,
3581                test_harness: false,
3582                compile_fail,
3583                standalone_crate,
3584                error_codes: Vec::new(),
3585                edition: None,
3586                added_css_classes: Vec::new(),
3587                unknown: Vec::new(),
3588            },
3589            original_code: "assert!(true);".into(),
3590            doctest_code: Some(RustdocDoctestCode {
3591                crate_level: "#![allow(unused)]\n".into(),
3592                code: "assert!(true);".into(),
3593                wrapper: Some(RustdocDoctestWrapper {
3594                    before: "fn main() {\n".into(),
3595                    after: "\n}".into(),
3596                    returns_result: false,
3597                }),
3598            }),
3599            name: name.into(),
3600        }
3601    }
3602
3603    struct OutcomeDirectory(PathBuf);
3604
3605    impl OutcomeDirectory {
3606        fn new() -> Self {
3607            use std::sync::atomic::{AtomicU64, Ordering};
3608            static NEXT: AtomicU64 = AtomicU64::new(0);
3609            let path = std::env::temp_dir().join(format!(
3610                "supercov-rustdoc-outcome-{}-{}",
3611                std::process::id(),
3612                NEXT.fetch_add(1, Ordering::Relaxed)
3613            ));
3614            fs::create_dir(&path).expect("create outcome test directory");
3615            Self(path)
3616        }
3617    }
3618
3619    impl Drop for OutcomeDirectory {
3620        fn drop(&mut self) {
3621            let _ = fs::remove_dir_all(&self.0);
3622        }
3623    }
3624
3625    fn passing_outcome_unit() -> RustdocOutcomeUnit {
3626        let transport = empty_transport();
3627        RustdocOutcomeUnit {
3628            schema: OUTCOME_SCHEMA.into(),
3629            invocation_id: "1".repeat(64),
3630            group: "fixture".into(),
3631            companion_build_id: "2".repeat(64),
3632            raw_catalog_sha256: "4".repeat(64),
3633            raw_events_sha256: "3".repeat(64),
3634            transport_sha256: transport_sha256(&transport),
3635            catalog: RustdocExtractedCatalog {
3636                format_version: RUSTDOC_CATALOG_FORMAT_VERSION,
3637                doctests: vec![catalog_doctest(
3638                    "src/lib.rs - (line 3)",
3639                    3,
3640                    false,
3641                    false,
3642                    false,
3643                    false,
3644                    false,
3645                )],
3646            },
3647            report: RustdocOutcomeReport {
3648                outcomes: vec![RustdocTestOutcome {
3649                    display_name: "src/lib.rs - (line 3)".into(),
3650                    status: RustdocOutcomeStatus::Passed,
3651                    execution_seconds: Some(0.25),
3652                    stdout: None,
3653                    message: None,
3654                    reason: None,
3655                    timeout_warning: false,
3656                }],
3657                suites: 1,
3658                planned_tests: 1,
3659                filtered_out: 0,
3660                unfinished_started: Vec::new(),
3661                unstarted_tests: 0,
3662                total_seconds: None,
3663                compilation_seconds: None,
3664            },
3665            transport,
3666        }
3667    }
3668
3669    fn merged_unit() -> RustdocMergedUnit {
3670        RustdocMergedUnit {
3671            map: map(),
3672            join: None,
3673        }
3674    }
3675
3676    fn outcome(display_name: &str, status: RustdocOutcomeStatus) -> RustdocTestOutcome {
3677        RustdocTestOutcome {
3678            display_name: display_name.into(),
3679            status,
3680            execution_seconds: (status != RustdocOutcomeStatus::Ignored).then_some(0.25),
3681            stdout: None,
3682            message: None,
3683            reason: None,
3684            timeout_warning: false,
3685        }
3686    }
3687
3688    #[test]
3689    fn parses_the_exact_pinned_rustdoc_catalog_format() {
3690        let raw = br##"{
3691            "format_version": 2,
3692            "doctests": [
3693                {
3694                    "file": "src/lib.rs",
3695                    "line": 3,
3696                    "doctest_attributes": {
3697                        "original": "ignore-x86_64,edition2024",
3698                        "should_panic": false,
3699                        "no_run": false,
3700                        "ignore": {"Some": ["x86_64"]},
3701                        "rust": true,
3702                        "test_harness": false,
3703                        "compile_fail": false,
3704                        "standalone_crate": false,
3705                        "error_codes": [],
3706                        "edition": "2024",
3707                        "added_css_classes": [],
3708                        "unknown": []
3709                    },
3710                    "original_code": "assert!(true);",
3711                    "doctest_code": {
3712                        "crate_level": "#![allow(unused)]\n",
3713                        "code": "assert!(true);",
3714                        "wrapper": null
3715                    },
3716                    "name": "src/lib.rs - example (line 3)"
3717                },
3718                {
3719                    "file": "src/lib.rs",
3720                    "line": 10,
3721                    "doctest_attributes": {
3722                        "original": "compile_fail,E0308",
3723                        "should_panic": false,
3724                        "no_run": true,
3725                        "ignore": "None",
3726                        "rust": true,
3727                        "test_harness": false,
3728                        "compile_fail": true,
3729                        "standalone_crate": false,
3730                        "error_codes": ["E0308"],
3731                        "edition": null,
3732                        "added_css_classes": [],
3733                        "unknown": []
3734                    },
3735                    "original_code": "let _: u8 = true;",
3736                    "doctest_code": null,
3737                    "name": "src/lib.rs - compile_error (line 10)"
3738                }
3739            ]
3740        }"##;
3741        let catalog = RustdocExtractedCatalog::parse(raw).expect("pinned catalog v2");
3742        assert_eq!(catalog.doctests.len(), 2);
3743        assert!(matches!(
3744            &catalog.doctests[0].doctest_attributes.ignore,
3745            RustdocDoctestIgnore::Some(targets)
3746                if targets.len() == 1 && targets[0] == "x86_64"
3747        ));
3748        assert!(catalog.doctests[1].doctest_code.is_none());
3749        assert!(catalog.doctests[1].doctest_attributes.compile_fail);
3750    }
3751
3752    #[test]
3753    fn rejects_unknown_malformed_or_ambiguous_rustdoc_catalogs() {
3754        let valid = serde_json::to_value(&passing_outcome_unit().catalog).unwrap();
3755        let mut cases = Vec::new();
3756
3757        let mut value = valid.clone();
3758        value["format_version"] = serde_json::json!(3);
3759        cases.push(value);
3760
3761        let mut value = valid.clone();
3762        value["extra"] = serde_json::json!(true);
3763        cases.push(value);
3764
3765        let mut value = valid.clone();
3766        value["doctests"][0]["line"] = serde_json::json!(0);
3767        cases.push(value);
3768
3769        let mut value = valid.clone();
3770        value["doctests"][0]["name"] = serde_json::json!("a guessed name");
3771        cases.push(value);
3772
3773        let mut value = valid.clone();
3774        let duplicate = value["doctests"][0].clone();
3775        value["doctests"].as_array_mut().unwrap().push(duplicate);
3776        cases.push(value);
3777
3778        let mut value = valid;
3779        value["doctests"][0]["doctest_attributes"]["edition"] = serde_json::json!("2099");
3780        cases.push(value);
3781
3782        for value in cases {
3783            let bytes = serde_json::to_vec(&value).unwrap();
3784            assert!(
3785                RustdocExtractedCatalog::parse(&bytes).is_err(),
3786                "accepted invalid rustdoc catalog: {value}"
3787            );
3788        }
3789    }
3790
3791    #[test]
3792    fn parses_exact_libtest_outcomes_across_merged_suites() {
3793        let report = parse_rustdoc_libtest_json(&libtest_stream(&[
3794            r#"{"type":"suite","event":"started","test_count":3,"shuffle_seed":17}"#,
3795            r#"{"type":"test","event":"started","name":"alpha"}"#,
3796            r#"{"type":"test","event":"timeout","name":"alpha"}"#,
3797            r#"{"type":"test","name":"alpha","event":"ok","exec_time":1.25,"stdout":"visible\noutput"}"#,
3798            r#"{"type":"test","event":"started","name":"beta"}"#,
3799            r#"{"type":"test","name":"beta","event":"failed","exec_time":0.5,"stdout":"failure","reason":"time limit exceeded"}"#,
3800            r#"{"type":"test","event":"started","name":"ignored"}"#,
3801            r#"{"type":"test","name":"ignored","event":"ignored","message":"platform"}"#,
3802            r#"{"type":"suite","event":"failed","passed":1,"failed":1,"ignored":1,"measured":0,"filtered_out":2,"exec_time":1.75}"#,
3803            r#"{"type":"suite","event":"started","test_count":0}"#,
3804            r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":3}"#,
3805            r#"{"type":"report","total_time":2.5,"compilation_time":0.75}"#,
3806        ]))
3807        .expect("pinned libtest stream");
3808
3809        assert_eq!(report.suites, 2);
3810        assert_eq!(report.planned_tests, 3);
3811        assert_eq!(report.filtered_out, 5);
3812        assert!(report.unfinished_started.is_empty());
3813        assert_eq!(report.unstarted_tests, 0);
3814        assert_eq!(report.total_seconds, Some(2.5));
3815        assert_eq!(report.compilation_seconds, Some(0.75));
3816        assert_eq!(
3817            report
3818                .outcomes
3819                .iter()
3820                .map(|outcome| (
3821                    outcome.display_name.as_str(),
3822                    outcome.status,
3823                    outcome.timeout_warning,
3824                    outcome.message.as_deref(),
3825                    outcome.reason.as_deref(),
3826                ))
3827                .collect::<Vec<_>>(),
3828            vec![
3829                ("alpha", RustdocOutcomeStatus::Passed, true, None, None),
3830                (
3831                    "beta",
3832                    RustdocOutcomeStatus::Failed,
3833                    false,
3834                    None,
3835                    Some("time limit exceeded"),
3836                ),
3837                (
3838                    "ignored",
3839                    RustdocOutcomeStatus::Ignored,
3840                    false,
3841                    Some("platform"),
3842                    None,
3843                ),
3844            ]
3845        );
3846        assert_eq!(report.outcomes[0].execution_seconds, Some(1.25));
3847        assert_eq!(
3848            report.outcomes[0].stdout.as_deref(),
3849            Some("visible\noutput")
3850        );
3851    }
3852
3853    #[test]
3854    fn represents_failed_fail_fast_suites_without_inventing_outcomes() {
3855        let report = parse_rustdoc_libtest_json(&libtest_stream(&[
3856            r#"{"type":"suite","event":"started","test_count":4}"#,
3857            r#"{"type":"test","event":"started","name":"failing"}"#,
3858            r#"{"type":"test","event":"started","name":"still-running"}"#,
3859            r#"{"type":"test","name":"failing","event":"failed","message":"boom"}"#,
3860            r#"{"type":"suite","event":"failed","passed":0,"failed":1,"ignored":0,"measured":0,"filtered_out":7}"#,
3861        ]))
3862        .expect("valid fail-fast stream");
3863
3864        assert_eq!(report.planned_tests, 4);
3865        assert_eq!(report.filtered_out, 7);
3866        assert_eq!(report.unfinished_started, ["still-running"]);
3867        assert_eq!(report.unstarted_tests, 2);
3868        assert_eq!(report.outcomes.len(), 1);
3869        assert_eq!(report.outcomes[0].status, RustdocOutcomeStatus::Failed);
3870        assert_eq!(report.outcomes[0].message.as_deref(), Some("boom"));
3871        assert_eq!(report.total_seconds, None);
3872    }
3873
3874    #[test]
3875    fn rejects_malformed_truncated_or_semantically_impossible_libtest_streams() {
3876        let cases = [
3877            vec![],
3878            vec![r#"{"type":"suite","event":"started","test_count":0,"extra":true}"#],
3879            vec![r#"{"type":"suite","event":null,"test_count":0}"#],
3880            vec![r#"{"type":"suite","event":"started","event":"started","test_count":0}"#],
3881            vec![
3882                r#"{"type":"suite","event":"started","test_count":1}"#,
3883                r#"{"type":"test","name":"missing-start","event":"ok"}"#,
3884                r#"{"type":"suite","event":"ok","passed":1,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3885            ],
3886            vec![
3887                r#"{"type":"suite","event":"started","test_count":1}"#,
3888                r#"{"type":"test","event":"started","name":"duplicate"}"#,
3889                r#"{"type":"test","event":"started","name":"duplicate"}"#,
3890            ],
3891            vec![
3892                r#"{"type":"suite","event":"started","test_count":1}"#,
3893                r#"{"type":"test","event":"timeout","name":"missing-start"}"#,
3894            ],
3895            vec![
3896                r#"{"type":"suite","event":"started","test_count":1}"#,
3897                r#"{"type":"test","event":"started","name":"unknown-reason"}"#,
3898                r#"{"type":"test","name":"unknown-reason","event":"failed","reason":"new reason"}"#,
3899                r#"{"type":"suite","event":"failed","passed":0,"failed":1,"ignored":0,"measured":0,"filtered_out":0}"#,
3900            ],
3901            vec![
3902                r#"{"type":"suite","event":"started","test_count":1}"#,
3903                r#"{"type":"test","event":"started","name":"ignored"}"#,
3904                r#"{"type":"test","name":"ignored","event":"ignored","stdout":"impossible"}"#,
3905                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":1,"measured":0,"filtered_out":0}"#,
3906            ],
3907            vec![
3908                r#"{"type":"suite","event":"started","test_count":1}"#,
3909                r#"{"type":"test","event":"started","name":"unfinished"}"#,
3910                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3911            ],
3912            vec![
3913                r#"{"type":"suite","event":"started","test_count":1}"#,
3914                r#"{"type":"test","event":"started","name":"wrong-count"}"#,
3915                r#"{"type":"test","name":"wrong-count","event":"ok"}"#,
3916                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3917            ],
3918            vec![
3919                r#"{"type":"suite","event":"started","test_count":0}"#,
3920                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":1,"filtered_out":0}"#,
3921            ],
3922            vec![
3923                r#"{"type":"suite","event":"started","test_count":0}"#,
3924                r#"{"type":"suite","event":"ok","passed":0,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3925                r#"{"type":"report","total_time":1.0,"compilation_time":2.0}"#,
3926            ],
3927            vec![r#"{"type":"suite","event":"discovery"}"#],
3928            vec![r#"{"type":"suite","event":"started","test_count":0}"#],
3929        ];
3930        for lines in cases {
3931            assert!(
3932                parse_rustdoc_libtest_json(&libtest_stream(&lines)).is_err(),
3933                "accepted invalid libtest stream: {lines:?}"
3934            );
3935        }
3936    }
3937
3938    #[test]
3939    fn publishes_and_reads_exact_atomic_rustdoc_outcome_units() {
3940        let directory = OutcomeDirectory::new();
3941        let unit = passing_outcome_unit();
3942        let path = publish_rustdoc_outcome_unit(&directory.0, &unit).expect("publish outcome");
3943        assert_eq!(
3944            path.file_name().and_then(|name| name.to_str()),
3945            Some(format!("doctest-outcome-{}.json", unit.invocation_id).as_str())
3946        );
3947        assert_eq!(
3948            read_rustdoc_outcome_units(&directory.0).expect("read outcome"),
3949            std::slice::from_ref(&unit)
3950        );
3951        assert!(publish_rustdoc_outcome_unit(&directory.0, &unit).is_err());
3952        assert_eq!(
3953            read_rustdoc_outcome_units(&directory.0).expect("published outcome stayed intact"),
3954            [unit]
3955        );
3956    }
3957
3958    #[test]
3959    fn reserves_each_rustdoc_transport_once_and_authenticates_reads() {
3960        let directory = OutcomeDirectory::new();
3961        let invocation = "a".repeat(64);
3962        let reservation = reserve_rustdoc_transport(&directory.0, &invocation)
3963            .expect("reserve rustdoc transport");
3964        assert!(reservation.path.is_file());
3965        let token = rustdoc_transport_token_hex(&reservation.token);
3966        assert_eq!(
3967            read_reserved_rustdoc_transport(&reservation.path, &token)
3968                .expect("authenticated empty transport"),
3969            empty_transport()
3970        );
3971        assert!(reserve_rustdoc_transport(&directory.0, &invocation).is_err());
3972        assert!(read_reserved_rustdoc_transport(&reservation.path, &"0".repeat(32)).is_err());
3973    }
3974
3975    #[test]
3976    fn decodes_and_hashes_the_exact_catalog_and_event_frame() {
3977        let catalog = serde_json::to_vec(&passing_outcome_unit().catalog).unwrap();
3978        let events = libtest_stream(&[
3979            r#"{"type":"suite","event":"started","test_count":1}"#,
3980            r#"{"type":"test","event":"started","name":"src/lib.rs - (line 3)"}"#,
3981            r#"{"type":"test","name":"src/lib.rs - (line 3)","event":"ok"}"#,
3982            r#"{"type":"suite","event":"ok","passed":1,"failed":0,"ignored":0,"measured":0,"filtered_out":0}"#,
3983        ]);
3984        let mut framed = u64::try_from(catalog.len()).unwrap().to_be_bytes().to_vec();
3985        framed.extend_from_slice(&catalog);
3986        framed.extend_from_slice(&events);
3987        let unit = rustdoc_outcome_unit_from_framed_input(
3988            "1".repeat(64),
3989            "fixture".into(),
3990            "2".repeat(64),
3991            &framed,
3992            empty_transport(),
3993        )
3994        .expect("exact framed rustdoc outputs");
3995        assert_eq!(
3996            unit.raw_catalog_sha256,
3997            format!("{:x}", Sha256::digest(&catalog))
3998        );
3999        assert_eq!(
4000            unit.raw_events_sha256,
4001            format!("{:x}", Sha256::digest(&events))
4002        );
4003        for invalid in [
4004            Vec::new(),
4005            1u64.to_be_bytes().to_vec(),
4006            1000u64.to_be_bytes().into_iter().chain([b'{']).collect(),
4007            {
4008                let mut only_catalog = u64::try_from(catalog.len()).unwrap().to_be_bytes().to_vec();
4009                only_catalog.extend_from_slice(&catalog);
4010                only_catalog
4011            },
4012        ] {
4013            assert!(
4014                rustdoc_outcome_unit_from_framed_input(
4015                    "1".repeat(64),
4016                    "fixture".into(),
4017                    "2".repeat(64),
4018                    &invalid,
4019                    empty_transport(),
4020                )
4021                .is_err()
4022            );
4023        }
4024    }
4025
4026    #[test]
4027    fn joins_merged_standalone_compile_fail_and_fail_fast_state_from_catalog() {
4028        let mut unit = passing_outcome_unit();
4029        unit.catalog.doctests = vec![
4030            catalog_doctest(
4031                "src/lib.rs - (line 3)",
4032                3,
4033                false,
4034                false,
4035                false,
4036                false,
4037                false,
4038            ),
4039            catalog_doctest(
4040                "src/lib.rs - (line 10)",
4041                10,
4042                false,
4043                true,
4044                true,
4045                false,
4046                false,
4047            ),
4048            catalog_doctest(
4049                "src/lib.rs - standalone (line 20)",
4050                20,
4051                false,
4052                false,
4053                false,
4054                false,
4055                true,
4056            ),
4057            catalog_doctest(
4058                "src/lib.rs - compile_fail (line 30)",
4059                30,
4060                false,
4061                true,
4062                false,
4063                true,
4064                false,
4065            ),
4066            catalog_doctest(
4067                "src/lib.rs - later (line 40)",
4068                40,
4069                false,
4070                false,
4071                false,
4072                false,
4073                false,
4074            ),
4075        ];
4076        unit.report = RustdocOutcomeReport {
4077            outcomes: vec![
4078                outcome("src/lib.rs - (line 10)", RustdocOutcomeStatus::Ignored),
4079                outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
4080                outcome(
4081                    "src/lib.rs - standalone (line 20)",
4082                    RustdocOutcomeStatus::Failed,
4083                ),
4084            ],
4085            suites: 1,
4086            planned_tests: 5,
4087            filtered_out: 0,
4088            unfinished_started: vec!["src/lib.rs - compile_fail (line 30)".into()],
4089            unstarted_tests: 1,
4090            total_seconds: None,
4091            compilation_seconds: None,
4092        };
4093        unit.validate().expect("valid mixed rustdoc outcome unit");
4094
4095        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4096            .expect("lossless merged outcome join");
4097        assert!(resolution.is_fully_catalogued());
4098        assert!(resolution.unmatched_maps.is_empty());
4099        let [group] = resolution.groups.as_slice() else {
4100            panic!("expected one joined rustdoc group")
4101        };
4102        assert_eq!(group.entries.len(), 5);
4103        assert!(matches!(
4104            &group.entries[0].state,
4105            RustdocJoinedOutcomeState::Completed { outcome }
4106                if outcome.status == RustdocOutcomeStatus::Passed
4107        ));
4108        assert!(matches!(
4109            &group.entries[1].state,
4110            RustdocJoinedOutcomeState::Completed { outcome }
4111                if outcome.status == RustdocOutcomeStatus::Ignored
4112        ));
4113        assert!(matches!(
4114            &group.entries[2].state,
4115            RustdocJoinedOutcomeState::Completed { outcome }
4116                if outcome.status == RustdocOutcomeStatus::Failed
4117        ));
4118        assert!(group.entries[2].merged_entry.is_none());
4119        assert!(matches!(
4120            group.entries[3].state,
4121            RustdocJoinedOutcomeState::UnfinishedStarted
4122        ));
4123        assert!(group.entries[3].catalog.doctest_attributes.compile_fail);
4124        assert!(matches!(
4125            group.entries[4].state,
4126            RustdocJoinedOutcomeState::Unstarted
4127        ));
4128        assert_eq!(
4129            group.raw_catalog_sha256,
4130            "4".repeat(64),
4131            "catalog binding must survive the join"
4132        );
4133        assert!(!group.has_ambiguous_outcomes());
4134    }
4135
4136    #[test]
4137    fn joins_named_fail_fast_states_without_inventing_terminal_outcomes() {
4138        let mut unit = passing_outcome_unit();
4139        unit.catalog.doctests.push(catalog_doctest(
4140            "src/lib.rs - (line 10)",
4141            10,
4142            false,
4143            true,
4144            true,
4145            false,
4146            false,
4147        ));
4148        unit.report = RustdocOutcomeReport {
4149            outcomes: vec![outcome(
4150                "src/lib.rs - (line 3)",
4151                RustdocOutcomeStatus::Failed,
4152            )],
4153            suites: 1,
4154            planned_tests: 2,
4155            filtered_out: 0,
4156            unfinished_started: vec!["src/lib.rs - (line 10)".into()],
4157            unstarted_tests: 0,
4158            total_seconds: None,
4159            compilation_seconds: None,
4160        };
4161        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4162            .expect("join fail-fast identities");
4163        assert!(resolution.is_fully_catalogued());
4164        assert!(matches!(
4165            resolution.groups[0].entries[1].state,
4166            RustdocJoinedOutcomeState::UnfinishedStarted
4167        ));
4168
4169        let mut unit = passing_outcome_unit();
4170        unit.catalog.doctests.push(catalog_doctest(
4171            "src/lib.rs - (line 10)",
4172            10,
4173            false,
4174            true,
4175            true,
4176            false,
4177            false,
4178        ));
4179        unit.report = RustdocOutcomeReport {
4180            outcomes: vec![outcome(
4181                "src/lib.rs - (line 3)",
4182                RustdocOutcomeStatus::Failed,
4183            )],
4184            suites: 1,
4185            planned_tests: 2,
4186            filtered_out: 0,
4187            unfinished_started: Vec::new(),
4188            unstarted_tests: 1,
4189            total_seconds: None,
4190            compilation_seconds: None,
4191        };
4192        let resolution = join_rustdoc_outcomes(vec![merged_unit()], vec![unit])
4193            .expect("join unstarted identity");
4194        assert!(resolution.is_fully_catalogued());
4195        assert!(matches!(
4196            resolution.groups[0].entries[1].state,
4197            RustdocJoinedOutcomeState::Unstarted
4198        ));
4199    }
4200
4201    #[test]
4202    fn preserves_filter_and_fail_fast_identity_ambiguity_instead_of_guessing() {
4203        let mut unit = passing_outcome_unit();
4204        unit.catalog.doctests.extend([
4205            catalog_doctest(
4206                "src/lib.rs - filtered-or-unstarted-a (line 20)",
4207                20,
4208                false,
4209                false,
4210                false,
4211                false,
4212                true,
4213            ),
4214            catalog_doctest(
4215                "src/lib.rs - filtered-or-unstarted-b (line 30)",
4216                30,
4217                false,
4218                true,
4219                false,
4220                true,
4221                false,
4222            ),
4223        ]);
4224        unit.report = RustdocOutcomeReport {
4225            outcomes: vec![outcome(
4226                "src/lib.rs - (line 3)",
4227                RustdocOutcomeStatus::Failed,
4228            )],
4229            suites: 1,
4230            planned_tests: 2,
4231            filtered_out: 1,
4232            unfinished_started: Vec::new(),
4233            unstarted_tests: 1,
4234            total_seconds: None,
4235            compilation_seconds: None,
4236        };
4237        let resolution =
4238            join_rustdoc_outcomes(Vec::new(), vec![unit]).expect("lossless ambiguous outcome join");
4239        assert!(resolution.is_fully_catalogued());
4240        assert!(resolution.has_ambiguous_outcomes());
4241        assert_eq!(resolution.groups[0].ambiguous_filtered_out, 1);
4242        assert_eq!(resolution.groups[0].ambiguous_unstarted_tests, 1);
4243        assert!(
4244            resolution.groups[0].entries[1..]
4245                .iter()
4246                .all(|entry| { matches!(entry.state, RustdocJoinedOutcomeState::NotRunAmbiguous) })
4247        );
4248    }
4249
4250    #[test]
4251    fn outcome_join_rejects_ambiguous_groups_and_impossible_missing_entries() {
4252        let unit = passing_outcome_unit();
4253        assert!(
4254            join_rustdoc_outcomes(vec![merged_unit(), merged_unit()], vec![unit.clone()]).is_err()
4255        );
4256        assert!(join_rustdoc_outcomes(vec![merged_unit()], vec![unit.clone(), unit]).is_err());
4257
4258        let mut incomplete = passing_outcome_unit();
4259        incomplete.report.outcomes[0].display_name = "src/lib.rs - (line 3)".into();
4260        assert!(join_rustdoc_outcomes(vec![merged_unit()], vec![incomplete]).is_err());
4261    }
4262
4263    #[test]
4264    fn outcome_join_retains_maps_without_outcomes_and_catalogs_units_without_maps() {
4265        let maps_only = join_rustdoc_outcomes(vec![merged_unit()], Vec::new()).unwrap();
4266        assert_eq!(maps_only.unmatched_maps.len(), 1);
4267        assert!(!maps_only.is_fully_catalogued());
4268
4269        let units_only = join_rustdoc_outcomes(Vec::new(), vec![passing_outcome_unit()]).unwrap();
4270        assert_eq!(units_only.groups.len(), 1);
4271        assert!(units_only.groups[0].entries[0].merged_entry.is_none());
4272        assert!(units_only.is_fully_catalogued());
4273    }
4274
4275    #[test]
4276    fn rejects_incomplete_tampered_or_inconsistent_rustdoc_outcome_units() {
4277        let mut invalid = Vec::new();
4278
4279        let mut unit = passing_outcome_unit();
4280        unit.schema = "supercov-rustdoc-outcome-unit-v0".into();
4281        invalid.push(unit);
4282
4283        let mut unit = passing_outcome_unit();
4284        unit.invocation_id = "A".repeat(64);
4285        invalid.push(unit);
4286
4287        let mut unit = passing_outcome_unit();
4288        unit.report.planned_tests = 2;
4289        invalid.push(unit);
4290
4291        let mut unit = passing_outcome_unit();
4292        unit.report.total_seconds = Some(1.0);
4293        invalid.push(unit);
4294
4295        let mut unit = passing_outcome_unit();
4296        unit.report.outcomes[0].status = RustdocOutcomeStatus::Ignored;
4297        invalid.push(unit);
4298
4299        let mut unit = passing_outcome_unit();
4300        unit.transport.attachments = 1;
4301        invalid.push(unit);
4302
4303        for unit in invalid {
4304            assert!(unit.validate().is_err(), "accepted invalid unit: {unit:?}");
4305        }
4306
4307        let directory = OutcomeDirectory::new();
4308        let unit = passing_outcome_unit();
4309        fs::write(
4310            directory.0.join(format!(
4311                ".doctest-outcome-{}.json.partial",
4312                unit.invocation_id
4313            )),
4314            b"partial",
4315        )
4316        .unwrap();
4317        assert!(read_rustdoc_outcome_units(&directory.0).is_err());
4318    }
4319
4320    #[test]
4321    fn outcome_join_rejects_transport_owned_by_an_unknown_test() {
4322        let mut unit = passing_outcome_unit();
4323        unit.transport.observations.push(RustTransportObservation {
4324            process_id: 1,
4325            context_id: 7,
4326            observation: RustProbeObservation::Hit {
4327                id: "rs:function:111111111111111111111111".into(),
4328            },
4329        });
4330        unit.transport.committed = 1;
4331        unit.transport_sha256 = transport_sha256(&unit.transport);
4332        assert!(unit.validate().is_ok());
4333        assert!(join_rustdoc_outcomes(Vec::new(), vec![unit]).is_err());
4334    }
4335
4336    #[test]
4337    fn map_is_strict_sorted_and_path_safe() {
4338        let valid = map();
4339        assert_eq!(valid.entry("__doctest_1").expect("entry").line, 10);
4340        assert!(valid.entry("__doctest_1").expect("entry").no_run);
4341        assert!(valid.entry("__doctest_1").expect("entry").should_panic);
4342        for invalid in [
4343            br#"{"schema":"wrong","group":"fixture","entries":[]}"#.as_slice(),
4344            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(),
4345            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(),
4346            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(),
4347            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(),
4348            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(),
4349        ] {
4350            assert!(RustdocMergedMap::parse(invalid).is_err());
4351        }
4352    }
4353
4354    #[test]
4355    fn maps_hidden_multiline_and_duplicate_later_doctests_exactly() {
4356        let map = map();
4357        let snippet = "let value = hidden\n    + 2;";
4358        let bundle = merged_bundle(snippet);
4359        let start = bundle.find(snippet).unwrap() as u32;
4360        let end = start + snippet.len() as u32;
4361        let authored = concat!(
4362            "//! docs\n",
4363            "//! ```\n",
4364            "//! # let hidden = 20;\n",
4365            "//! let value = hidden\n",
4366            "//!     + 2;\n",
4367            "//! assert_eq!(value, 22);\n",
4368            "//! ```\n",
4369            "//! more\n",
4370            "//! ```\n",
4371            "//! let value = hidden\n",
4372            "//!     + 2;\n",
4373            "//! ```\n",
4374        );
4375        let mapped = map_merged_range(&map, "__doctest_0", &bundle, start, end, authored)
4376            .expect("exact range");
4377        assert_eq!(mapped.source_key, "source:src/lib.rs");
4378        assert_eq!(
4379            &authored[mapped.start as usize..mapped.end as usize],
4380            "let value = hidden\n//!     + 2;"
4381        );
4382    }
4383
4384    #[test]
4385    fn rejects_ambiguous_or_unmapped_bundle_ranges() {
4386        let map = map();
4387        let bundle = merged_bundle("same();");
4388        let start = bundle.find("same();").unwrap() as u32;
4389        let end = start + 7;
4390        let ambiguous = "//! docs\n//! ```\n//! same();\n//! same();\n//! ```\n";
4391        assert!(map_merged_range(&map, "__doctest_0", &bundle, start, end, ambiguous).is_err());
4392        assert!(map_merged_range(&map, "__doctest_9", &bundle, start, end, ambiguous).is_err());
4393        assert!(map_merged_range(&map, "__doctest_0", &bundle, end, start, ambiguous).is_err());
4394    }
4395
4396    #[test]
4397    fn maps_repeated_fragments_when_the_full_sequence_is_unique() {
4398        let map = map();
4399        let snippet = "same();\nsame();";
4400        let bundle = merged_bundle(snippet);
4401        let start = bundle.find(snippet).unwrap() as u32;
4402        let end = start + snippet.len() as u32;
4403        let authored = concat!(
4404            "//! docs\n",
4405            "//! ```\n",
4406            "//! same();\n",
4407            "//! same();\n",
4408            "//! ```\n",
4409        );
4410        let mapped = map_merged_range(&map, "__doctest_0", &bundle, start, end, authored)
4411            .expect("one ordered mapping");
4412        assert_eq!(
4413            &authored[mapped.start as usize..mapped.end as usize],
4414            "same();\n//! same();"
4415        );
4416    }
4417
4418    #[test]
4419    fn maps_repeated_subexpressions_through_their_extracted_line_context() {
4420        let map = map();
4421        let snippet = concat!(
4422            "let flag = true;\n",
4423            "if flag { yes(); } else { no(); }\n",
4424            "match flag { true => yes(), false => no() };",
4425        );
4426        let bundle = merged_bundle(snippet);
4427        let if_line = "if flag { yes(); } else { no(); }";
4428        let start = bundle.find(if_line).unwrap() + "if ".len();
4429        let end = start + "flag".len();
4430        let authored = concat!(
4431            "//! docs\n",
4432            "//! ```\n",
4433            "//! let flag = true;\n",
4434            "//! if flag { yes(); } else { no(); }\n",
4435            "//! match flag { true => yes(), false => no() };\n",
4436            "//! ```\n",
4437        );
4438        let mapped = map_merged_range(
4439            &map,
4440            "__doctest_0",
4441            &bundle,
4442            start as u32,
4443            end as u32,
4444            authored,
4445        )
4446        .expect("full extracted-line context disambiguates flag");
4447        assert_eq!(
4448            &authored[mapped.start as usize..mapped.end as usize],
4449            "flag"
4450        );
4451        assert_eq!(
4452            authored[..mapped.start as usize]
4453                .bytes()
4454                .filter(|byte| *byte == b'\n')
4455                .count(),
4456            3,
4457            "the mapped flag must come from the if line"
4458        );
4459    }
4460
4461    #[test]
4462    fn final_identity_matches_the_frozen_rust_source_model() {
4463        let source = RustdocMappedRange {
4464            source_key: "source:src/lib.rs".into(),
4465            start: 42,
4466            end: 57,
4467        };
4468        let identity =
4469            rust_source_identity("statement", &source, "expression").expect("valid identity");
4470        assert_eq!(
4471            identity.canonical,
4472            "rust-source-v1\0statement\0source:src/lib.rs\x0042\x0057\0expression\0"
4473        );
4474        assert_eq!(identity.id, "rs:statement:8446ba638fcb36ffc76b4293");
4475        assert_eq!(identity.probe_ordinal, 9531510598153221887);
4476    }
4477
4478    fn pending_assertion_candidate() -> (
4479        Vec<u8>,
4480        Vec<u8>,
4481        Vec<u8>,
4482        BTreeMap<String, RustCompilerSource>,
4483    ) {
4484        let group = "fixture";
4485        let key = format!("doctest-pending:{group}");
4486        let snippet = "assert_eq!(fixture::authored(true), 1)";
4487        let bundle = format!(
4488            "\n#![allow(unused)]\npub mod __doctest_0 {{\nfn main() {{\n{snippet};\n}}\n}}\n"
4489        );
4490        let start = u32::try_from(bundle.find(snippet).expect("snippet")).unwrap();
4491        let end = start + u32::try_from(snippet.len()).unwrap();
4492        let definition = vec!["__doctest_0::main".into()];
4493        let point_identity = pending_identity("statement", &key, start, end, "expression").unwrap();
4494        let branch_identity =
4495            pending_identity("branch", &key, start, end, "assertion-outcome:assertion").unwrap();
4496        let passed_identity = pending_identity(
4497            "branch-alternative",
4498            &key,
4499            start,
4500            end,
4501            "assertion-outcome:assertion:passed",
4502        )
4503        .unwrap();
4504        let failed_identity = pending_identity(
4505            "branch-alternative",
4506            &key,
4507            start,
4508            end,
4509            "assertion-outcome:assertion:failed",
4510        )
4511        .unwrap();
4512        let decision_identity =
4513            pending_identity("decision", &key, start, end, "assertion").unwrap();
4514        let manifest = RustCompilerManifest {
4515            unmeasured_obligations: Vec::new(),
4516            schema: "supercov-rust-manifest-candidate-v4".into(),
4517            model: "rust-source-v1".into(),
4518            crate_name: "doctest_bundle_2024".into(),
4519            measurement_complete: false,
4520            bound_bodies: 1,
4521            points: vec![RustCompilerPoint {
4522                id: point_identity.id,
4523                kind: "statement".into(),
4524                source_key: key.clone(),
4525                start,
4526                end,
4527                provenance: "doctest-pending".into(),
4528                discriminator: "expression".into(),
4529                probe_ordinal: point_identity.probe_ordinal.to_string(),
4530                definitions: definition.clone(),
4531                canonical: point_identity.canonical,
4532            }],
4533            branches: vec![RustCompilerBranch {
4534                id: branch_identity.id.clone(),
4535                kind: "assertion-outcome".into(),
4536                discriminator: "assertion-outcome:assertion".into(),
4537                source_key: key.clone(),
4538                start,
4539                end,
4540                provenance: "doctest-pending".into(),
4541                probe_ordinal: branch_identity.probe_ordinal.to_string(),
4542                definitions: definition.clone(),
4543                alternatives: vec![
4544                    RustCompilerBranchAlternative {
4545                        id: passed_identity.id,
4546                        label: "passed".into(),
4547                        probe_ordinal: passed_identity.probe_ordinal.to_string(),
4548                        canonical: passed_identity.canonical,
4549                    },
4550                    RustCompilerBranchAlternative {
4551                        id: failed_identity.id,
4552                        label: "failed".into(),
4553                        probe_ordinal: failed_identity.probe_ordinal.to_string(),
4554                        canonical: failed_identity.canonical,
4555                    },
4556                ],
4557                canonical: branch_identity.canonical,
4558            }],
4559            decisions: vec![RustCompilerDecision {
4560                id: decision_identity.id,
4561                kind: "assertion".into(),
4562                source_key: key.clone(),
4563                start,
4564                end,
4565                provenance: "doctest-pending".into(),
4566                probe_ordinal: decision_identity.probe_ordinal.to_string(),
4567                definitions: definition,
4568                outcome_branch_id: branch_identity.id,
4569                loop_branch_id: None,
4570                logical_selections: Vec::new(),
4571                conditions: vec![RustCompilerCondition {
4572                    source_key: key.clone(),
4573                    start,
4574                    end,
4575                    source: snippet.into(),
4576                }],
4577                canonical: decision_identity.canonical,
4578            }],
4579            selection_groups: Vec::new(),
4580            limitations: vec!["RUST_DOCTEST_MAPPING_PENDING".into()],
4581        };
4582        let snapshots = RustCompilerSourceSnapshots {
4583            schema: "supercov-rust-source-snapshots-v1".into(),
4584            crate_name: manifest.crate_name.clone(),
4585            sources: BTreeMap::from([(
4586                key.clone(),
4587                RustCompilerSource {
4588                    file: key,
4589                    source: bundle,
4590                },
4591            )]),
4592        };
4593        let map = br#"{
4594            "schema":"supercov-rustdoc-merged-map-v2",
4595            "group":"fixture",
4596            "entries":[{
4597                "module":"__doctest_0",
4598                "displayName":"src/lib.rs - (line 3)",
4599                "path":"src/lib.rs",
4600                "line":3,
4601                "ignored":false,
4602                "noRun":false,
4603                "shouldPanic":false
4604            }]
4605        }"#
4606        .to_vec();
4607        let authored = concat!(
4608            "//! docs\n",
4609            "//! ```\n",
4610            "//! assert_eq!(fixture::authored(true), 1);\n",
4611            "//! ```\n",
4612        );
4613        (
4614            serde_json::to_vec(&manifest).unwrap(),
4615            serde_json::to_vec(&snapshots).unwrap(),
4616            map,
4617            BTreeMap::from([(
4618                "source:src/lib.rs".into(),
4619                RustCompilerSource {
4620                    file: "src/lib.rs".into(),
4621                    source: authored.into(),
4622                },
4623            )]),
4624        )
4625    }
4626
4627    fn pending_branch(
4628        key: &str,
4629        start: u32,
4630        end: u32,
4631        kind: &str,
4632        discriminator: &str,
4633        alternatives: [(&str, &str); 2],
4634    ) -> RustCompilerBranch {
4635        let identity = pending_identity("branch", key, start, end, discriminator).unwrap();
4636        RustCompilerBranch {
4637            id: identity.id,
4638            kind: kind.into(),
4639            discriminator: discriminator.into(),
4640            source_key: key.into(),
4641            start,
4642            end,
4643            provenance: "doctest-pending".into(),
4644            probe_ordinal: identity.probe_ordinal.to_string(),
4645            definitions: vec!["__doctest_0::main".into()],
4646            alternatives: alternatives
4647                .into_iter()
4648                .map(|(token, label)| {
4649                    let identity = pending_identity(
4650                        "branch-alternative",
4651                        key,
4652                        start,
4653                        end,
4654                        &format!("{discriminator}:{token}"),
4655                    )
4656                    .unwrap();
4657                    RustCompilerBranchAlternative {
4658                        id: identity.id,
4659                        label: label.into(),
4660                        probe_ordinal: identity.probe_ordinal.to_string(),
4661                        canonical: identity.canonical,
4662                    }
4663                })
4664                .collect(),
4665            canonical: identity.canonical,
4666        }
4667    }
4668
4669    fn synthetic_pending_identity(
4670        kind: &str,
4671        key: &str,
4672        start: u32,
4673        end: u32,
4674        discriminator: &str,
4675        owner_ordinal: u64,
4676    ) -> RustSourceIdentity {
4677        identity_from_canonical(
4678            kind,
4679            format!(
4680                concat!(
4681                    "rust-source-v1\0{}\0{}\0{}\0{}\0{}\0",
4682                    "synthetic-expansion\0proc-macro\0{}\0{}\0{}\0probe_macros::generated\0",
4683                    "__doctest_0::main\0{}\0"
4684                ),
4685                kind, key, start, end, discriminator, key, start, end, owner_ordinal,
4686            ),
4687        )
4688        .unwrap()
4689    }
4690
4691    #[test]
4692    fn joins_pending_bundle_manifest_into_final_authored_identities() {
4693        let (manifest, sources, map, authored) = pending_assertion_candidate();
4694        assert!(RustCompilerManifest::parse(&manifest).is_err());
4695        assert!(RustCompilerSourceSnapshots::parse(&sources).is_err());
4696
4697        let joined =
4698            join_merged_doctest(&manifest, &sources, &map, &authored).expect("strict merged join");
4699        assert_eq!(joined.manifest.points.len(), 1);
4700        assert_eq!(joined.manifest.branches.len(), 1);
4701        assert_eq!(joined.manifest.decisions.len(), 1);
4702        assert_eq!(joined.obligation_ids.len(), 5);
4703        assert_eq!(joined.probe_ordinals.len(), 5);
4704        assert_eq!(joined.manifest.points[0].source_key, "source:src/lib.rs");
4705        assert_eq!(joined.manifest.branches[0].source_key, "source:src/lib.rs");
4706        assert_eq!(joined.manifest.decisions[0].source_key, "source:src/lib.rs");
4707        let point = &joined.manifest.points[0];
4708        let source = &authored["source:src/lib.rs"].source;
4709        assert_eq!(
4710            &source[point.start as usize..point.end as usize],
4711            "assert_eq!(fixture::authored(true), 1)"
4712        );
4713        assert_eq!(point.provenance, "doctest-source");
4714        assert_eq!(point.definitions, ["doctest:src/lib.rs:3"]);
4715        assert_eq!(joined.sources.sources.len(), 1);
4716        joined
4717            .manifest
4718            .normalize(&joined.sources.sources)
4719            .expect("final manifest normalizes through the production path");
4720    }
4721
4722    #[test]
4723    fn merged_join_rejects_tampering_missing_sources_and_malformed_synthetic_expansion() {
4724        let (manifest, sources, map, authored) = pending_assertion_candidate();
4725        let mut tampered: serde_json::Value = serde_json::from_slice(&manifest).unwrap();
4726        tampered["points"][0]["id"] =
4727            serde_json::Value::String("rs:statement:000000000000000000000000".into());
4728        assert!(
4729            join_merged_doctest(
4730                &serde_json::to_vec(&tampered).unwrap(),
4731                &sources,
4732                &map,
4733                &authored,
4734            )
4735            .is_err()
4736        );
4737
4738        assert!(join_merged_doctest(&manifest, &sources, &map, &BTreeMap::new(),).is_err());
4739
4740        let mut synthetic: serde_json::Value = serde_json::from_slice(&manifest).unwrap();
4741        synthetic["points"][0]["canonical"] = serde_json::Value::String(
4742            concat!(
4743                "rust-source-v1\0statement\0doctest-pending:fixture\0",
4744                "1\0",
4745                "2\0expression\0synthetic-expansion\0"
4746            )
4747            .into(),
4748        );
4749        assert!(
4750            join_merged_doctest(
4751                &serde_json::to_vec(&synthetic).unwrap(),
4752                &sources,
4753                &map,
4754                &authored,
4755            )
4756            .is_err()
4757        );
4758    }
4759
4760    #[test]
4761    fn rebases_decision_match_cross_references_and_runtime_ordinals() {
4762        let key = "doctest-pending:fixture";
4763        let body = concat!(
4764            "let flag = true;\n",
4765            "if flag { yes(); } else { no(); }\n",
4766            "match flag { true => yes(), false => no() };",
4767        );
4768        let bundle = merged_bundle(body);
4769        let range = |fragment: &str| {
4770            let start = bundle.find(fragment).unwrap() as u32;
4771            (start, start + fragment.len() as u32)
4772        };
4773        let point_range = range("let flag = true;");
4774        let if_range = range("if flag { yes(); } else { no(); }");
4775        let if_flag_start = if_range.0 + "if ".len() as u32;
4776        let if_flag_end = if_flag_start + "flag".len() as u32;
4777        let match_range = range("match flag { true => yes(), false => no() }");
4778        let first_arm_range = range("true => yes()");
4779        let second_arm_range = range("false => no()");
4780        let match_start = match_range.0 as usize;
4781        let first_body_start = match_start + bundle[match_start..].find("yes()").unwrap();
4782        let second_body_start = match_start + bundle[match_start..].find("no()").unwrap();
4783        let first_body_range = (
4784            first_body_start as u32,
4785            (first_body_start + "yes()".len()) as u32,
4786        );
4787        let second_body_range = (
4788            second_body_start as u32,
4789            (second_body_start + "no()".len()) as u32,
4790        );
4791
4792        let point_identity =
4793            pending_identity("statement", key, point_range.0, point_range.1, "let").unwrap();
4794        let decision_identity =
4795            pending_identity("decision", key, if_flag_start, if_flag_end, "if").unwrap();
4796        let outcome = pending_branch(
4797            key,
4798            if_range.0,
4799            if_range.1,
4800            "decision-outcome",
4801            "decision-outcome:if",
4802            [("true", "condition true"), ("false", "condition false")],
4803        );
4804        let group_identity =
4805            pending_identity("match-group", key, match_range.0, match_range.1, "match").unwrap();
4806        let first_arm = pending_branch(
4807            key,
4808            first_arm_range.0,
4809            first_arm_range.1,
4810            "match-arm",
4811            &format!("match-arm:{}:0", group_identity.id),
4812            [("not-selected", "not selected"), ("selected", "selected")],
4813        );
4814        let second_arm = pending_branch(
4815            key,
4816            second_arm_range.0,
4817            second_arm_range.1,
4818            "match-arm",
4819            &format!("match-arm:{}:1", group_identity.id),
4820            [("not-selected", "not selected"), ("selected", "selected")],
4821        );
4822        let arm_ordinals = |branch: &RustCompilerBranch| {
4823            let ordinal = |label: &str| {
4824                branch
4825                    .alternatives
4826                    .iter()
4827                    .find(|alternative| alternative.label == label)
4828                    .unwrap()
4829                    .probe_ordinal
4830                    .clone()
4831            };
4832            (ordinal("selected"), ordinal("not selected"))
4833        };
4834        let first_ordinals = arm_ordinals(&first_arm);
4835        let second_ordinals = arm_ordinals(&second_arm);
4836        let mut branches = vec![outcome.clone(), first_arm.clone(), second_arm.clone()];
4837        branches.sort_by(|left, right| left.id.cmp(&right.id));
4838        let manifest = RustCompilerManifest {
4839            unmeasured_obligations: Vec::new(),
4840            schema: "supercov-rust-manifest-candidate-v4".into(),
4841            model: "rust-source-v1".into(),
4842            crate_name: "doctest_bundle_2024".into(),
4843            measurement_complete: false,
4844            bound_bodies: 1,
4845            points: vec![RustCompilerPoint {
4846                id: point_identity.id,
4847                kind: "statement".into(),
4848                source_key: key.into(),
4849                start: point_range.0,
4850                end: point_range.1,
4851                provenance: "doctest-pending".into(),
4852                discriminator: "let".into(),
4853                probe_ordinal: point_identity.probe_ordinal.to_string(),
4854                definitions: vec!["__doctest_0::main".into()],
4855                canonical: point_identity.canonical,
4856            }],
4857            branches,
4858            decisions: vec![RustCompilerDecision {
4859                id: decision_identity.id,
4860                kind: "if".into(),
4861                source_key: key.into(),
4862                start: if_flag_start,
4863                end: if_flag_end,
4864                provenance: "doctest-pending".into(),
4865                probe_ordinal: decision_identity.probe_ordinal.to_string(),
4866                definitions: vec!["__doctest_0::main".into()],
4867                outcome_branch_id: outcome.id,
4868                loop_branch_id: None,
4869                logical_selections: Vec::new(),
4870                conditions: vec![RustCompilerCondition {
4871                    source_key: key.into(),
4872                    start: if_flag_start,
4873                    end: if_flag_end,
4874                    source: "flag".into(),
4875                }],
4876                canonical: decision_identity.canonical,
4877            }],
4878            selection_groups: vec![RustCompilerSelectionGroup {
4879                id: group_identity.id,
4880                kind: "match".into(),
4881                source_key: key.into(),
4882                start: match_range.0,
4883                end: match_range.1,
4884                provenance: "doctest-pending".into(),
4885                probe_ordinal: group_identity.probe_ordinal.to_string(),
4886                definitions: vec!["__doctest_0::main".into()],
4887                parent_group_id: None,
4888                parent_site: None,
4889                parent_arm_index: None,
4890                arms: vec![
4891                    RustCompilerMatchArm {
4892                        branch_id: first_arm.id,
4893                        body_source_key: key.into(),
4894                        body_start: first_body_range.0,
4895                        body_end: first_body_range.1,
4896                        guarded: false,
4897                        guard_decision_id: None,
4898                        selected_ordinal: first_ordinals.0,
4899                        not_selected_ordinal: first_ordinals.1,
4900                    },
4901                    RustCompilerMatchArm {
4902                        branch_id: second_arm.id,
4903                        body_source_key: key.into(),
4904                        body_start: second_body_range.0,
4905                        body_end: second_body_range.1,
4906                        guarded: false,
4907                        guard_decision_id: None,
4908                        selected_ordinal: second_ordinals.0,
4909                        not_selected_ordinal: second_ordinals.1,
4910                    },
4911                ],
4912                canonical: group_identity.canonical,
4913            }],
4914            limitations: vec!["RUST_DOCTEST_MAPPING_PENDING".into()],
4915        };
4916        let snapshots = RustCompilerSourceSnapshots {
4917            schema: "supercov-rust-source-snapshots-v1".into(),
4918            crate_name: manifest.crate_name.clone(),
4919            sources: BTreeMap::from([(
4920                key.into(),
4921                RustCompilerSource {
4922                    file: key.into(),
4923                    source: bundle,
4924                },
4925            )]),
4926        };
4927        let authored = concat!(
4928            "//! docs\n",
4929            "//! ```\n",
4930            "//! let flag = true;\n",
4931            "//! if flag { yes(); } else { no(); }\n",
4932            "//! match flag { true => yes(), false => no() };\n",
4933            "//! ```\n",
4934        );
4935        let map = br#"{
4936            "schema":"supercov-rustdoc-merged-map-v2",
4937            "group":"fixture",
4938            "entries":[{
4939                "module":"__doctest_0",
4940                "displayName":"src/lib.rs - (line 3)",
4941                "path":"src/lib.rs",
4942                "line":3,
4943                "ignored":false,
4944                "noRun":false,
4945                "shouldPanic":false
4946            }]
4947        }"#;
4948        let joined = join_merged_doctest(
4949            &serde_json::to_vec(&manifest).unwrap(),
4950            &serde_json::to_vec(&snapshots).unwrap(),
4951            map,
4952            &BTreeMap::from([(
4953                "source:src/lib.rs".into(),
4954                RustCompilerSource {
4955                    file: "src/lib.rs".into(),
4956                    source: authored.into(),
4957                },
4958            )]),
4959        )
4960        .expect("decision and match join");
4961
4962        let decision = &joined.manifest.decisions[0];
4963        assert!(
4964            joined
4965                .manifest
4966                .branches
4967                .iter()
4968                .any(|branch| branch.id == decision.outcome_branch_id)
4969        );
4970        let group = &joined.manifest.selection_groups[0];
4971        assert!(group.id.starts_with("rs:match-group:"));
4972        for arm in &group.arms {
4973            let branch = joined
4974                .manifest
4975                .branches
4976                .iter()
4977                .find(|branch| branch.id == arm.branch_id)
4978                .unwrap();
4979            assert!(branch.discriminator.contains(&group.id));
4980            assert!(
4981                branch
4982                    .alternatives
4983                    .iter()
4984                    .any(|alternative| alternative.probe_ordinal == arm.selected_ordinal)
4985            );
4986            assert!(
4987                branch
4988                    .alternatives
4989                    .iter()
4990                    .any(|alternative| alternative.probe_ordinal == arm.not_selected_ordinal)
4991            );
4992        }
4993        assert!(
4994            joined.manifest.decisions[0]
4995                .conditions
4996                .iter()
4997                .all(|condition| condition.source_key == "source:src/lib.rs")
4998        );
4999        assert_eq!(joined.obligation_ids.len(), 12);
5000        assert_eq!(joined.probe_ordinals.len(), 12);
5001    }
5002
5003    #[test]
5004    fn rebases_complete_synthetic_expansion_canonicals_without_guessing() {
5005        let (manifest, sources, map, authored) = pending_assertion_candidate();
5006        let mut manifest = RustCompilerManifest::parse_pending_doctest(&manifest, "fixture")
5007            .expect("pending candidate");
5008        let key = "doctest-pending:fixture";
5009        let mut owner_ordinal = 1;
5010        let mut replace = |kind: &str,
5011                           start: u32,
5012                           end: u32,
5013                           discriminator: &str,
5014                           id: &mut String,
5015                           canonical: &mut String,
5016                           ordinal: &mut String| {
5017            let identity =
5018                synthetic_pending_identity(kind, key, start, end, discriminator, owner_ordinal);
5019            owner_ordinal += 1;
5020            *id = identity.id;
5021            *canonical = identity.canonical;
5022            *ordinal = identity.probe_ordinal.to_string();
5023        };
5024        for point in &mut manifest.points {
5025            replace(
5026                &point.kind,
5027                point.start,
5028                point.end,
5029                &point.discriminator,
5030                &mut point.id,
5031                &mut point.canonical,
5032                &mut point.probe_ordinal,
5033            );
5034        }
5035        for branch in &mut manifest.branches {
5036            replace(
5037                "branch",
5038                branch.start,
5039                branch.end,
5040                &branch.discriminator,
5041                &mut branch.id,
5042                &mut branch.canonical,
5043                &mut branch.probe_ordinal,
5044            );
5045            for alternative in &mut branch.alternatives {
5046                let discriminator = alternative_discriminator(
5047                    &branch.discriminator,
5048                    &branch.kind,
5049                    &alternative.label,
5050                )
5051                .unwrap();
5052                replace(
5053                    "branch-alternative",
5054                    branch.start,
5055                    branch.end,
5056                    &discriminator,
5057                    &mut alternative.id,
5058                    &mut alternative.canonical,
5059                    &mut alternative.probe_ordinal,
5060                );
5061            }
5062        }
5063        let branch_id = manifest.branches[0].id.clone();
5064        for decision in &mut manifest.decisions {
5065            replace(
5066                "decision",
5067                decision.start,
5068                decision.end,
5069                &decision.kind,
5070                &mut decision.id,
5071                &mut decision.canonical,
5072                &mut decision.probe_ordinal,
5073            );
5074            decision.outcome_branch_id = branch_id.clone();
5075        }
5076        manifest
5077            .points
5078            .sort_by(|left, right| left.id.cmp(&right.id));
5079        manifest
5080            .branches
5081            .sort_by(|left, right| left.id.cmp(&right.id));
5082        manifest
5083            .decisions
5084            .sort_by(|left, right| left.id.cmp(&right.id));
5085
5086        let joined = join_merged_doctest(
5087            &serde_json::to_vec(&manifest).unwrap(),
5088            &sources,
5089            &map,
5090            &authored,
5091        )
5092        .expect("synthetic expansion join");
5093        assert!(
5094            joined
5095                .manifest
5096                .points
5097                .iter()
5098                .all(|point| point.provenance == "synthetic-expansion")
5099        );
5100        assert!(
5101            joined
5102                .manifest
5103                .branches
5104                .iter()
5105                .all(|branch| branch.provenance == "synthetic-expansion"
5106                    && branch.alternatives.iter().all(|alternative| {
5107                        alternative.canonical.contains("source:src/lib.rs")
5108                            && !alternative.canonical.contains("doctest-pending:")
5109                    }))
5110        );
5111        assert!(
5112            joined
5113                .manifest
5114                .decisions
5115                .iter()
5116                .all(|decision| decision.provenance == "synthetic-expansion")
5117        );
5118        for canonical in joined
5119            .manifest
5120            .points
5121            .iter()
5122            .map(|point| &point.canonical)
5123            .chain(joined.manifest.branches.iter().flat_map(|branch| {
5124                std::iter::once(&branch.canonical).chain(
5125                    branch
5126                        .alternatives
5127                        .iter()
5128                        .map(|alternative| &alternative.canonical),
5129                )
5130            }))
5131            .chain(
5132                joined
5133                    .manifest
5134                    .decisions
5135                    .iter()
5136                    .map(|decision| &decision.canonical),
5137            )
5138        {
5139            assert!(canonical.contains("doctest:src/lib.rs:3"));
5140            assert!(!canonical.contains("__doctest_0"));
5141            assert!(!canonical.contains("doctest-pending:"));
5142        }
5143        assert_eq!(joined.obligation_ids.len(), 5);
5144        assert_eq!(joined.probe_ordinals.len(), 5);
5145    }
5146
5147    #[test]
5148    fn translates_deferred_runtime_ids_ordinals_and_nested_assertion_contexts() {
5149        let (pending_manifest, sources, map, authored) = pending_assertion_candidate();
5150        let pending = RustCompilerManifest::parse_pending_doctest(&pending_manifest, "fixture")
5151            .expect("pending candidate");
5152        let mut joined = join_merged_doctest(&pending_manifest, &sources, &map, &authored)
5153            .expect("strict merged join");
5154        let old_point = &pending.points[0];
5155        let old_outer = &pending.decisions[0].id;
5156        let final_outer = joined.obligation_ids[old_outer].clone();
5157        let old_inner = "rs:decision:111111111111111111111111".to_owned();
5158        let final_inner = "rs:decision:222222222222222222222222".to_owned();
5159        joined
5160            .obligation_ids
5161            .insert(old_inner.clone(), final_inner.clone());
5162
5163        let base = 42;
5164        let outer_nonce = 7;
5165        let inner_nonce = 8;
5166        let old_outer_context =
5167            rust_assertion_context_id(base, old_outer, outer_nonce).expect("old outer context");
5168        let old_inner_context =
5169            rust_assertion_context_id(old_outer_context, &old_inner, inner_nonce)
5170                .expect("old inner context");
5171        let final_outer_context =
5172            rust_assertion_context_id(base, &final_outer, outer_nonce).expect("final outer");
5173        let final_inner_context =
5174            rust_assertion_context_id(final_outer_context, &final_inner, inner_nonce)
5175                .expect("final inner");
5176        let dependency = "rs:function:333333333333333333333333";
5177        let read = RustTransportRead {
5178            observations: vec![
5179                RustTransportObservation {
5180                    process_id: 10,
5181                    context_id: old_outer_context,
5182                    observation: RustProbeObservation::Hit {
5183                        id: old_point.id.clone(),
5184                    },
5185                },
5186                RustTransportObservation {
5187                    process_id: 10,
5188                    context_id: old_inner_context,
5189                    observation: RustProbeObservation::Decision {
5190                        id: old_inner.clone(),
5191                        values: vec![Some(true)],
5192                        outcome: true,
5193                    },
5194                },
5195                RustTransportObservation {
5196                    process_id: 10,
5197                    context_id: 0,
5198                    observation: RustProbeObservation::Hit {
5199                        id: dependency.into(),
5200                    },
5201                },
5202            ],
5203            ordinal_hits: vec![RustOrdinalHit {
5204                process_id: 10,
5205                context_id: old_outer_context,
5206                ordinal: old_point.probe_ordinal.parse().unwrap(),
5207            }],
5208            // Deliberately child-first: transport descriptor order is not a
5209            // topological guarantee and the rewriter must not depend on it.
5210            phases: vec![
5211                RustPhaseContext {
5212                    process_id: 10,
5213                    child_context_id: old_inner_context,
5214                    parent_context_id: old_outer_context,
5215                    invocation_nonce: inner_nonce,
5216                    decision_id: old_inner,
5217                },
5218                RustPhaseContext {
5219                    process_id: 10,
5220                    child_context_id: old_outer_context,
5221                    parent_context_id: base,
5222                    invocation_nonce: outer_nonce,
5223                    decision_id: old_outer.clone(),
5224                },
5225            ],
5226            committed: 6,
5227            incomplete: 1,
5228            dropped: 0,
5229            attachments: 2,
5230            ..RustTransportRead::empty()
5231        };
5232
5233        let translated = joined
5234            .translate_transport(base, &read)
5235            .expect("exact transport translation");
5236        assert_eq!(translated.committed, read.committed);
5237        assert_eq!(translated.incomplete, read.incomplete);
5238        assert_eq!(translated.attachments, read.attachments);
5239        assert_eq!(translated.observations[0].context_id, final_outer_context);
5240        assert_eq!(translated.observations[1].context_id, final_inner_context);
5241        assert_eq!(translated.observations[2], read.observations[2]);
5242        assert!(matches!(
5243            &translated.observations[0].observation,
5244            RustProbeObservation::Hit { id }
5245                if id == &joined.obligation_ids[&old_point.id]
5246        ));
5247        assert!(matches!(
5248            &translated.observations[1].observation,
5249            RustProbeObservation::Decision { id, .. } if id == &final_inner
5250        ));
5251        assert_eq!(translated.ordinal_hits[0].context_id, final_outer_context);
5252        assert_eq!(
5253            translated.ordinal_hits[0].ordinal.to_string(),
5254            joined.probe_ordinals[&old_point.probe_ordinal]
5255        );
5256        assert_eq!(translated.phases[0].child_context_id, final_inner_context);
5257        assert_eq!(translated.phases[0].parent_context_id, final_outer_context);
5258        assert_eq!(translated.phases[0].decision_id, final_inner);
5259        assert_eq!(translated.phases[1].child_context_id, final_outer_context);
5260        assert_eq!(translated.phases[1].parent_context_id, base);
5261        assert_eq!(translated.phases[1].decision_id, final_outer);
5262    }
5263
5264    #[test]
5265    fn combines_canonical_and_merged_runtime_roots_into_one_exact_doctest() {
5266        let (pending_manifest, sources, map_bytes, authored) = pending_assertion_candidate();
5267        let pending = RustCompilerManifest::parse_pending_doctest(&pending_manifest, "fixture")
5268            .expect("pending candidate");
5269        let joined = join_merged_doctest(&pending_manifest, &sources, &map_bytes, &authored)
5270            .expect("strict merged join");
5271        let final_point = joined.obligation_ids[&pending.points[0].id].clone();
5272        let catalog = catalog_doctest(
5273            "src/lib.rs - (line 3)",
5274            3,
5275            false,
5276            false,
5277            false,
5278            false,
5279            false,
5280        );
5281        let merged_entry = map().entries[0].clone();
5282        let canonical_name = "rustdoc:fixture:src/lib.rs:3";
5283        let merged_name = "rustdoc:fixture:__doctest_0";
5284        let canonical = rust_test_context_id(canonical_name).expect("canonical context");
5285        let merged = rust_test_context_id(merged_name).expect("merged context");
5286        assert_ne!(canonical, merged);
5287        let entry = RustdocJoinedOutcome {
5288            catalog_index: 0,
5289            catalog,
5290            merged_entry: Some(merged_entry),
5291            state: RustdocJoinedOutcomeState::Completed {
5292                outcome: outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
5293            },
5294        };
5295        let group = RustdocOutcomeGroupJoin {
5296            invocation_id: "1".repeat(64),
5297            group: "fixture".into(),
5298            companion_build_id: "2".repeat(64),
5299            raw_catalog_sha256: "3".repeat(64),
5300            raw_events_sha256: "4".repeat(64),
5301            transport_sha256: "5".repeat(64),
5302            join: Some(joined),
5303            transport: RustTransportRead {
5304                observations: vec![
5305                    RustTransportObservation {
5306                        process_id: 10,
5307                        context_id: canonical,
5308                        observation: RustProbeObservation::Hit {
5309                            id: final_point.clone(),
5310                        },
5311                    },
5312                    RustTransportObservation {
5313                        process_id: 11,
5314                        context_id: merged,
5315                        observation: RustProbeObservation::Hit {
5316                            id: pending.points[0].id.clone(),
5317                        },
5318                    },
5319                ],
5320                ordinal_hits: Vec::new(),
5321                phases: Vec::new(),
5322                committed: 2,
5323                incomplete: 0,
5324                dropped: 0,
5325                attachments: 2,
5326                ..RustTransportRead::empty()
5327            },
5328            entries: vec![entry.clone()],
5329            ambiguous_filtered_out: 0,
5330            ambiguous_unstarted_tests: 0,
5331        };
5332
5333        let (base, combined) = group
5334            .attributed_transport(&entry)
5335            .expect("canonical plus merged transport");
5336        assert_eq!(base, canonical);
5337        assert_eq!(combined.committed, 2);
5338        assert_eq!(combined.observations.len(), 2);
5339        assert!(
5340            combined
5341                .observations
5342                .iter()
5343                .all(|observation| observation.context_id == canonical)
5344        );
5345        assert!(combined.observations.iter().all(|observation| {
5346            matches!(
5347                &observation.observation,
5348                RustProbeObservation::Hit { id } if id == &final_point
5349            )
5350        }));
5351    }
5352
5353    #[test]
5354    fn doctest_thread_phases_are_join_bounded_and_escapes_become_background() {
5355        let canonical_name = "rustdoc:fixture:src/lib.rs:3";
5356        let canonical = rust_test_context_id(canonical_name).expect("canonical context");
5357        let joined_thread = rust_thread_context_id(canonical, 0);
5358        let escaped_thread = rust_thread_context_id(canonical, 1);
5359        let hit = |context_id: u64| RustTransportObservation {
5360            process_id: 10,
5361            context_id,
5362            observation: RustProbeObservation::Hit {
5363                id: "rs:statement:0123456789abcdef01234567".into(),
5364            },
5365        };
5366        let entry = RustdocJoinedOutcome {
5367            catalog_index: 0,
5368            catalog: catalog_doctest(
5369                "src/lib.rs - (line 3)",
5370                3,
5371                false,
5372                false,
5373                false,
5374                false,
5375                false,
5376            ),
5377            merged_entry: None,
5378            state: RustdocJoinedOutcomeState::Completed {
5379                outcome: outcome("src/lib.rs - (line 3)", RustdocOutcomeStatus::Passed),
5380            },
5381        };
5382        let group = RustdocOutcomeGroupJoin {
5383            invocation_id: "1".repeat(64),
5384            group: "fixture".into(),
5385            companion_build_id: "2".repeat(64),
5386            raw_catalog_sha256: "3".repeat(64),
5387            raw_events_sha256: "4".repeat(64),
5388            transport_sha256: "5".repeat(64),
5389            join: None,
5390            transport: RustTransportRead {
5391                observations: vec![hit(joined_thread), hit(escaped_thread), hit(0)],
5392                thread_phases: vec![
5393                    RustThreadPhase {
5394                        process_id: 10,
5395                        child_context_id: joined_thread,
5396                        parent_context_id: canonical,
5397                        invocation_nonce: 0,
5398                        commit_index: 0,
5399                    },
5400                    RustThreadPhase {
5401                        process_id: 10,
5402                        child_context_id: escaped_thread,
5403                        parent_context_id: canonical,
5404                        invocation_nonce: 1,
5405                        commit_index: 1,
5406                    },
5407                ],
5408                thread_ends: vec![RustThreadEnd {
5409                    process_id: 10,
5410                    context_id: joined_thread,
5411                    commit_index: 4,
5412                }],
5413                test_boundaries: vec![RustTestBoundary {
5414                    process_id: 10,
5415                    context_id: canonical,
5416                    commit_index: 5,
5417                }],
5418                committed: 7,
5419                attachments: 1,
5420                ..RustTransportRead::empty()
5421            },
5422            entries: vec![entry.clone()],
5423            ambiguous_filtered_out: 0,
5424            ambiguous_unstarted_tests: 0,
5425        };
5426        group
5427            .validate_transport_ownership()
5428            .expect("thread-kind records stay within known doctest roots");
5429
5430        let (base, attributed) = group
5431            .attributed_transport(&entry)
5432            .expect("join-bounded canonical transport");
5433        assert_eq!(base, canonical);
5434        // Joined thread phase, its end, its hit and the boundary are exact.
5435        assert_eq!(attributed.committed, 4);
5436        assert_eq!(attributed.observations, vec![hit(joined_thread)]);
5437        assert_eq!(attributed.thread_phases.len(), 1);
5438        assert_eq!(attributed.thread_ends.len(), 1);
5439        assert_eq!(attributed.test_boundaries.len(), 1);
5440
5441        let background = group.background_transport().expect("background transport");
5442        assert_eq!(background.committed, 3);
5443        assert_eq!(background.observations, vec![hit(escaped_thread), hit(0)]);
5444        assert_eq!(background.thread_phases.len(), 1);
5445        assert_eq!(background.thread_phases[0].child_context_id, escaped_thread);
5446
5447        assert_eq!(
5448            group.thread_scope_limitations().expect("limitations"),
5449            std::collections::BTreeSet::from([format!(
5450                "RUST_THREAD_OUTLIVED_TEST: thread phase {escaped_thread:016x} escaped test {canonical:016x}"
5451            )])
5452        );
5453
5454        // A boundary for an unknown context fails ownership validation.
5455        let mut unknown_boundary = group;
5456        unknown_boundary
5457            .transport
5458            .test_boundaries
5459            .push(RustTestBoundary {
5460                process_id: 10,
5461                context_id: 99,
5462                commit_index: 6,
5463            });
5464        unknown_boundary.transport.committed = 8;
5465        assert!(unknown_boundary.validate_transport_ownership().is_err());
5466    }
5467
5468    #[test]
5469    fn resolves_a_complete_compiler_generation_before_normalization() {
5470        let (pending_manifest, pending_sources, map, authored) = pending_assertion_candidate();
5471        let direct =
5472            join_merged_doctest(&pending_manifest, &pending_sources, &map, &authored).unwrap();
5473        let ordinary_manifest = serde_json::to_vec(&direct.manifest).unwrap();
5474        let ordinary_sources = serde_json::to_vec(&direct.sources).unwrap();
5475
5476        let resolved = resolve_merged_doctest_candidates(
5477            vec![
5478                (pending_manifest.clone(), pending_sources.clone()),
5479                (ordinary_manifest.clone(), ordinary_sources.clone()),
5480            ],
5481            vec![map.clone()],
5482        )
5483        .expect("generation join");
5484        assert_eq!(resolved.candidates.len(), 2);
5485        assert_eq!(resolved.merged_units.len(), 1);
5486        assert_eq!(resolved.merged_units[0].join.as_ref().unwrap(), &direct);
5487        normalize_rust_compiler_candidates(resolved.candidates).unwrap();
5488
5489        let no_obligations = resolve_merged_doctest_candidates(
5490            vec![(ordinary_manifest, ordinary_sources)],
5491            vec![map.clone()],
5492        )
5493        .expect("map-only test remains attributable");
5494        assert!(no_obligations.merged_units[0].join.is_none());
5495
5496        assert!(
5497            resolve_merged_doctest_candidates(
5498                vec![(pending_manifest.clone(), pending_sources.clone())],
5499                Vec::new(),
5500            )
5501            .is_err()
5502        );
5503        assert!(
5504            resolve_merged_doctest_candidates(
5505                vec![(pending_manifest, pending_sources)],
5506                vec![map.clone(), map],
5507            )
5508            .is_err()
5509        );
5510    }
5511}