1use 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 pub obligation_ids: BTreeMap<String, String>,
84 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 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 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 pub unfinished_started: Vec<String>,
136 pub unstarted_tests: u64,
139 pub total_seconds: Option<f64>,
140 pub compilation_seconds: Option<f64>,
141}
142
143#[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#[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 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#[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 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 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 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 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
525struct 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 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
623fn 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
1035pub 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
1645pub 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
1865pub 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 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
2211pub 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
2550pub 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
2677pub 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
3121pub 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 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 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 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}