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