1use std::collections::{BTreeMap, BTreeSet};
4
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use sha2::{Digest, Sha256};
8
9use crate::{
10 coverage_analysis::PointKind,
11 coverage_report::{
12 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
13 },
14};
15
16const SCHEMA: &str = "supercov-rust-manifest-candidate-v4";
17const MODEL: &str = "rust-source-v1";
18const SOURCE_SNAPSHOT_SCHEMA: &str = "supercov-rust-source-snapshots-v1";
19const SOURCE_FINGERPRINT_DOMAIN: &[u8] = b"supercov-rust-compiler-sources-v1\0";
20
21fn append_fingerprint_field(hash: &mut Sha256, value: &[u8]) {
22 hash.update((value.len() as u64).to_le_bytes());
23 hash.update(value);
24}
25
26fn compiler_source_fingerprint(sources: &BTreeMap<String, RustCompilerSource>) -> (String, usize) {
27 let mut hash = Sha256::new();
28 hash.update(SOURCE_FINGERPRINT_DOMAIN);
29 let mut generated_files = 0;
30 for (key, source) in sources {
31 generated_files += usize::from(key.starts_with("generated:package:"));
32 append_fingerprint_field(&mut hash, key.as_bytes());
33 append_fingerprint_field(&mut hash, source.file.as_bytes());
34 append_fingerprint_field(&mut hash, source.source.as_bytes());
35 }
36 (format!("{:x}", hash.finalize()), generated_files)
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct RustCompilerManifest {
42 pub schema: String,
43 pub model: String,
44 #[serde(rename = "crate")]
45 pub crate_name: String,
46 pub measurement_complete: bool,
47 #[serde(default)]
51 pub bound_bodies: u64,
52 pub points: Vec<RustCompilerPoint>,
53 pub branches: Vec<RustCompilerBranch>,
54 pub decisions: Vec<RustCompilerDecision>,
55 pub selection_groups: Vec<RustCompilerSelectionGroup>,
56 pub limitations: Vec<String>,
57 #[serde(default)]
59 pub unmeasured_obligations: Vec<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase", deny_unknown_fields)]
64pub struct RustCompilerPoint {
65 pub id: String,
66 pub kind: String,
67 pub source_key: String,
68 pub start: u32,
69 pub end: u32,
70 pub provenance: String,
71 pub discriminator: String,
72 pub probe_ordinal: String,
73 pub definitions: Vec<String>,
74 pub canonical: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct RustCompilerBranchAlternative {
80 pub id: String,
81 pub label: String,
82 pub probe_ordinal: String,
83 pub canonical: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase", deny_unknown_fields)]
88pub struct RustCompilerBranch {
89 pub id: String,
90 pub kind: String,
91 pub discriminator: String,
92 pub source_key: String,
93 pub start: u32,
94 pub end: u32,
95 pub provenance: String,
96 pub probe_ordinal: String,
97 pub definitions: Vec<String>,
98 pub alternatives: Vec<RustCompilerBranchAlternative>,
99 pub canonical: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct RustCompilerCondition {
105 pub source_key: String,
106 pub start: u32,
107 pub end: u32,
108 pub source: String,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
112#[serde(rename_all = "camelCase", deny_unknown_fields)]
113pub struct RustCompilerLogicalSelection {
114 pub branch_id: String,
115 pub right_condition_index: usize,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
119#[serde(rename_all = "camelCase", deny_unknown_fields)]
120pub struct RustCompilerDecision {
121 pub id: String,
122 pub kind: String,
123 pub source_key: String,
124 pub start: u32,
125 pub end: u32,
126 pub provenance: String,
127 pub probe_ordinal: String,
128 pub definitions: Vec<String>,
129 pub outcome_branch_id: String,
130 pub loop_branch_id: Option<String>,
131 pub logical_selections: Vec<RustCompilerLogicalSelection>,
132 pub conditions: Vec<RustCompilerCondition>,
133 pub canonical: String,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct RustCompilerMatchArm {
139 pub branch_id: String,
140 pub body_source_key: String,
141 pub body_start: u32,
142 pub body_end: u32,
143 pub guarded: bool,
144 pub guard_decision_id: Option<String>,
145 pub selected_ordinal: String,
146 pub not_selected_ordinal: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase", deny_unknown_fields)]
151pub struct RustCompilerSelectionGroup {
152 pub id: String,
153 pub kind: String,
154 pub source_key: String,
155 pub start: u32,
156 pub end: u32,
157 pub provenance: String,
158 pub probe_ordinal: String,
159 pub definitions: Vec<String>,
160 pub parent_group_id: Option<String>,
161 pub parent_site: Option<String>,
162 pub parent_arm_index: Option<usize>,
163 pub arms: Vec<RustCompilerMatchArm>,
164 pub canonical: String,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct RustCompilerSource {
172 pub file: String,
173 pub source: String,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
177#[serde(rename_all = "camelCase", deny_unknown_fields)]
178pub struct RustCompilerSourceSnapshots {
179 pub schema: String,
180 #[serde(rename = "crate")]
181 pub crate_name: String,
182 pub sources: BTreeMap<String, RustCompilerSource>,
183}
184
185impl RustCompilerSourceSnapshots {
186 pub fn parse(bytes: &[u8]) -> Result<Self, RustCompilerManifestError> {
187 let snapshots: Self = serde_json::from_slice(bytes)
188 .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
189 snapshots.validate()?;
190 Ok(snapshots)
191 }
192
193 pub fn validate(&self) -> Result<(), RustCompilerManifestError> {
194 self.validate_with_pending_doctest(None)
195 }
196
197 pub(crate) fn parse_pending_doctest(
198 bytes: &[u8],
199 group: &str,
200 ) -> Result<Self, RustCompilerManifestError> {
201 let snapshots: Self = serde_json::from_slice(bytes)
202 .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
203 snapshots.validate_with_pending_doctest(Some(group))?;
204 Ok(snapshots)
205 }
206
207 fn validate_with_pending_doctest(
208 &self,
209 pending_group: Option<&str>,
210 ) -> Result<(), RustCompilerManifestError> {
211 if self.schema != SOURCE_SNAPSHOT_SCHEMA
212 || self.crate_name.trim().is_empty()
213 || self.sources.is_empty()
214 || self.sources.iter().any(|(key, source)| {
215 !valid_source_key_for(key, pending_group)
216 || source.file.trim().is_empty()
217 || source.file.chars().any(char::is_control)
218 })
219 {
220 return Err(RustCompilerManifestError::InvalidSource(
221 "malformed compiler source snapshot envelope".into(),
222 ));
223 }
224 if let Some(group) = pending_group {
225 let key = format!("doctest-pending:{group}");
226 if !self.crate_name.starts_with("doctest_bundle_")
227 || self.sources.len() != 1
228 || self
229 .sources
230 .get(&key)
231 .is_none_or(|source| source.file != key)
232 {
233 return Err(RustCompilerManifestError::InvalidSource(
234 "malformed pending merged-doctest source envelope".into(),
235 ));
236 }
237 }
238 Ok(())
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub struct NormalizedRustCompilerManifest {
248 pub manifest: CoverageManifest,
249 pub hit_obligations_by_ordinal: BTreeMap<u64, Vec<String>>,
250 pub internal_ordinals: BTreeSet<u64>,
251 pub decision_outcome_obligations: BTreeMap<String, (String, String)>,
252 pub decision_loop_obligations: BTreeMap<String, (String, String)>,
253 pub decision_logical_selection_obligations:
254 BTreeMap<String, Vec<NormalizedRustLogicalSelection>>,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct NormalizedRustLogicalSelection {
260 pub short_circuited_id: String,
261 pub right_evaluated_id: String,
262 pub right_condition_index: usize,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
266#[serde(rename_all = "camelCase", deny_unknown_fields)]
267pub struct RustCompilerNormalizationRequest {
268 pub manifest: RustCompilerManifest,
269 pub sources: BTreeMap<String, RustCompilerSource>,
270}
271
272impl RustCompilerNormalizationRequest {
273 pub fn parse_and_normalize(
274 bytes: &[u8],
275 ) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
276 let request: Self = serde_json::from_slice(bytes)
277 .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
278 request.manifest.normalize(&request.sources)
279 }
280}
281
282pub fn normalize_rust_compiler_candidates(
283 candidates: Vec<(RustCompilerManifest, RustCompilerSourceSnapshots)>,
284) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
285 if candidates.is_empty() {
286 return Err(RustCompilerManifestError::Invalid(
287 "compiler build emitted no owned Rust denominator".into(),
288 ));
289 }
290 let mut crate_names = BTreeSet::new();
291 let mut points = BTreeMap::<String, RustCompilerPoint>::new();
292 let mut branches = BTreeMap::<String, RustCompilerBranch>::new();
293 let mut decisions = BTreeMap::<String, RustCompilerDecision>::new();
294 let mut groups = BTreeMap::<String, RustCompilerSelectionGroup>::new();
295 let mut limitations = BTreeSet::new();
296 let mut unmeasured_obligations = BTreeSet::new();
297 let mut sources = BTreeMap::<String, RustCompilerSource>::new();
298 let mut bound_bodies = 0_u64;
299 for (manifest, snapshots) in candidates {
300 manifest.validate()?;
301 bound_bodies = bound_bodies.saturating_add(manifest.bound_bodies);
302 snapshots.validate()?;
303 if snapshots.crate_name != manifest.crate_name {
304 return Err(RustCompilerManifestError::InvalidSource(format!(
305 "compiler manifest/source snapshot identity mismatch for {}",
306 manifest.crate_name
307 )));
308 }
309 crate_names.insert(manifest.crate_name);
310 limitations.extend(manifest.limitations);
311 unmeasured_obligations.extend(manifest.unmeasured_obligations);
312 for (key, source) in snapshots.sources {
313 if let Some(existing) = sources.insert(key.clone(), source.clone())
314 && existing != source
315 {
316 return Err(RustCompilerManifestError::InvalidSource(format!(
317 "compiler source {key} changed across build units"
318 )));
319 }
320 }
321 for point in manifest.points {
322 merge_point(&mut points, point)?;
323 }
324 for branch in manifest.branches {
325 merge_branch(&mut branches, branch)?;
326 }
327 for decision in manifest.decisions {
328 merge_decision(&mut decisions, decision)?;
329 }
330 for group in manifest.selection_groups {
331 merge_selection_group(&mut groups, group)?;
332 }
333 }
334 let manifest = RustCompilerManifest {
335 schema: SCHEMA.into(),
336 model: MODEL.into(),
337 crate_name: if crate_names.len() == 1 {
338 crate_names.into_iter().next().expect("one crate")
339 } else {
340 "workspace".into()
341 },
342 measurement_complete: false,
343 bound_bodies,
344 points: points.into_values().collect(),
345 branches: branches.into_values().collect(),
346 decisions: decisions.into_values().collect(),
347 selection_groups: groups.into_values().collect(),
348 limitations: limitations.into_iter().collect(),
349 unmeasured_obligations: unmeasured_obligations.into_iter().collect(),
350 };
351 manifest.normalize(&sources)
352}
353
354fn merge_definitions(destination: &mut Vec<String>, source: Vec<String>) {
355 destination.extend(source);
356 destination.sort();
357 destination.dedup();
358}
359
360fn merge_point(
361 destination: &mut BTreeMap<String, RustCompilerPoint>,
362 point: RustCompilerPoint,
363) -> Result<(), RustCompilerManifestError> {
364 if let Some(existing) = destination.get_mut(&point.id) {
365 let mut left = existing.clone();
366 let mut right = point.clone();
367 left.definitions.clear();
368 right.definitions.clear();
369 if left != right {
370 return Err(RustCompilerManifestError::Invalid(format!(
371 "point {} changed across build units",
372 point.id
373 )));
374 }
375 merge_definitions(&mut existing.definitions, point.definitions);
376 } else {
377 destination.insert(point.id.clone(), point);
378 }
379 Ok(())
380}
381
382fn merge_branch(
383 destination: &mut BTreeMap<String, RustCompilerBranch>,
384 branch: RustCompilerBranch,
385) -> Result<(), RustCompilerManifestError> {
386 if let Some(existing) = destination.get_mut(&branch.id) {
387 let mut left = existing.clone();
388 let mut right = branch.clone();
389 left.definitions.clear();
390 right.definitions.clear();
391 if left != right {
392 return Err(RustCompilerManifestError::Invalid(format!(
393 "branch {} changed across build units",
394 branch.id
395 )));
396 }
397 merge_definitions(&mut existing.definitions, branch.definitions);
398 } else {
399 destination.insert(branch.id.clone(), branch);
400 }
401 Ok(())
402}
403
404fn merge_decision(
405 destination: &mut BTreeMap<String, RustCompilerDecision>,
406 decision: RustCompilerDecision,
407) -> Result<(), RustCompilerManifestError> {
408 if let Some(existing) = destination.get_mut(&decision.id) {
409 let mut left = existing.clone();
410 let mut right = decision.clone();
411 left.definitions.clear();
412 right.definitions.clear();
413 if left != right {
414 return Err(RustCompilerManifestError::Invalid(format!(
415 "decision {} changed across build units",
416 decision.id
417 )));
418 }
419 merge_definitions(&mut existing.definitions, decision.definitions);
420 } else {
421 destination.insert(decision.id.clone(), decision);
422 }
423 Ok(())
424}
425
426fn merge_selection_group(
427 destination: &mut BTreeMap<String, RustCompilerSelectionGroup>,
428 group: RustCompilerSelectionGroup,
429) -> Result<(), RustCompilerManifestError> {
430 if let Some(existing) = destination.get_mut(&group.id) {
431 let mut left = existing.clone();
432 let mut right = group.clone();
433 left.definitions.clear();
434 right.definitions.clear();
435 if left != right {
436 return Err(RustCompilerManifestError::Invalid(format!(
437 "selection group {} changed across build units",
438 group.id
439 )));
440 }
441 merge_definitions(&mut existing.definitions, group.definitions);
442 } else {
443 destination.insert(group.id.clone(), group);
444 }
445 Ok(())
446}
447
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub enum RustCompilerManifestError {
450 Json(String),
451 Invalid(String),
452 MissingSource(String),
453 InvalidSource(String),
454}
455
456impl std::fmt::Display for RustCompilerManifestError {
457 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458 match self {
459 Self::Json(error) => write!(formatter, "invalid Rust compiler manifest JSON: {error}"),
460 Self::Invalid(error) => write!(formatter, "invalid Rust compiler manifest: {error}"),
461 Self::MissingSource(key) => {
462 write!(
463 formatter,
464 "Rust compiler manifest source {key} was not supplied"
465 )
466 }
467 Self::InvalidSource(error) => {
468 write!(formatter, "invalid Rust compiler manifest source: {error}")
469 }
470 }
471 }
472}
473
474impl std::error::Error for RustCompilerManifestError {}
475
476fn sorted_unique_nonempty(values: &[String]) -> bool {
477 !values.is_empty()
478 && values.iter().all(|value| !value.trim().is_empty())
479 && values.windows(2).all(|pair| pair[0] < pair[1])
480}
481
482fn valid_id(id: &str, allowed: &[&str]) -> bool {
483 let mut parts = id.split(':');
484 parts.next() == Some("rs")
485 && parts.next().is_some_and(|kind| allowed.contains(&kind))
486 && parts.next().is_some_and(|digest| {
487 digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
488 })
489 && parts.next().is_none()
490}
491
492fn normalized_relative_path(value: &str, allow_package_root: bool) -> bool {
493 if allow_package_root && value == "." {
494 return true;
495 }
496 !value.is_empty()
497 && !value.starts_with('/')
498 && !value.contains('\\')
499 && value
500 .split('/')
501 .all(|component| !component.is_empty() && !matches!(component, "." | ".."))
502}
503
504fn valid_source_key(key: &str) -> bool {
505 if let Some(path) = key.strip_prefix("source:") {
506 return normalized_relative_path(path, false);
507 }
508 let Some(generated) = key.strip_prefix("generated:package:") else {
509 return false;
510 };
511 let Some((package, output)) = generated.split_once(':') else {
512 return false;
513 };
514 normalized_relative_path(package, true) && normalized_relative_path(output, false)
515}
516
517fn valid_source_key_for(key: &str, pending_group: Option<&str>) -> bool {
518 valid_source_key(key)
519 || pending_group.is_some_and(|group| key == format!("doctest-pending:{group}"))
520}
521
522fn source_range_for(key: &str, start: u32, end: u32, pending_group: Option<&str>) -> bool {
523 valid_source_key_for(key, pending_group) && start < end
524}
525
526fn ordinal(value: &str) -> Option<u64> {
527 value.parse::<u64>().ok().filter(|ordinal| *ordinal != 0)
528}
529
530fn provenance_for(value: &str, pending_group: Option<&str>) -> bool {
531 matches!(
532 value,
533 "authored-source"
534 | "authored-expansion"
535 | "synthetic-expansion"
536 | "generated-source"
537 | "doctest-source"
538 ) || pending_group.is_some() && value == "doctest-pending"
539}
540
541fn insert_identity(
542 ids: &mut BTreeSet<String>,
543 ordinals: &mut BTreeSet<u64>,
544 id: &str,
545 probe_ordinal: &str,
546) -> Result<(), RustCompilerManifestError> {
547 if !ids.insert(id.into()) {
548 return Err(RustCompilerManifestError::Invalid(format!(
549 "duplicate obligation ID {id}"
550 )));
551 }
552 let ordinal = ordinal(probe_ordinal).ok_or_else(|| {
553 RustCompilerManifestError::Invalid(format!(
554 "obligation {id} has invalid probe ordinal {probe_ordinal}"
555 ))
556 })?;
557 if !ordinals.insert(ordinal) {
558 return Err(RustCompilerManifestError::Invalid(format!(
559 "duplicate probe ordinal {ordinal}"
560 )));
561 }
562 Ok(())
563}
564
565impl RustCompilerManifest {
566 pub fn parse(bytes: &[u8]) -> Result<Self, RustCompilerManifestError> {
567 let manifest: Self = serde_json::from_slice(bytes)
568 .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
569 manifest.validate()?;
570 Ok(manifest)
571 }
572
573 pub fn validate(&self) -> Result<(), RustCompilerManifestError> {
574 self.validate_with_pending_doctest(None)
575 }
576
577 pub(crate) fn parse_pending_doctest(
578 bytes: &[u8],
579 group: &str,
580 ) -> Result<Self, RustCompilerManifestError> {
581 let manifest: Self = serde_json::from_slice(bytes)
582 .map_err(|error| RustCompilerManifestError::Json(error.to_string()))?;
583 manifest.validate_with_pending_doctest(Some(group))?;
584 Ok(manifest)
585 }
586
587 fn validate_with_pending_doctest(
588 &self,
589 pending_group: Option<&str>,
590 ) -> Result<(), RustCompilerManifestError> {
591 let invalid = |reason: &str| RustCompilerManifestError::Invalid(reason.into());
592 if self.schema != SCHEMA || self.model != MODEL {
593 return Err(invalid("unsupported schema or source model"));
594 }
595 if self.crate_name.trim().is_empty() || self.measurement_complete {
596 return Err(invalid(
597 "a private candidate needs a crate identity and cannot claim completeness",
598 ));
599 }
600 if let Some(group) = pending_group
601 && (!self.crate_name.starts_with("doctest_bundle_")
602 || group.is_empty()
603 || !group
604 .bytes()
605 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')))
606 {
607 return Err(invalid("malformed pending merged-doctest identity"));
608 }
609 if self.points.is_empty() || !sorted_unique_nonempty(&self.limitations) {
610 return Err(invalid(
611 "a candidate needs points and sorted explicit limitations",
612 ));
613 }
614 if !self.points.windows(2).all(|pair| pair[0].id < pair[1].id)
615 || !self.branches.windows(2).all(|pair| pair[0].id < pair[1].id)
616 || !self
617 .decisions
618 .windows(2)
619 .all(|pair| pair[0].id < pair[1].id)
620 || !self
621 .selection_groups
622 .windows(2)
623 .all(|pair| pair[0].id < pair[1].id)
624 {
625 return Err(invalid("obligation arrays are not in canonical ID order"));
626 }
627 if let Some(group) = pending_group {
628 let key = format!("doctest-pending:{group}");
629 let exact_pending = |source_key: &str, provenance: &str| {
630 source_key == key && provenance == "doctest-pending"
631 };
632 if self
633 .points
634 .iter()
635 .any(|point| !exact_pending(&point.source_key, &point.provenance))
636 || self
637 .branches
638 .iter()
639 .any(|branch| !exact_pending(&branch.source_key, &branch.provenance))
640 || self.decisions.iter().any(|decision| {
641 !exact_pending(&decision.source_key, &decision.provenance)
642 || decision
643 .conditions
644 .iter()
645 .any(|condition| condition.source_key != key)
646 })
647 || self.selection_groups.iter().any(|selection| {
648 !exact_pending(&selection.source_key, &selection.provenance)
649 || selection.arms.iter().any(|arm| arm.body_source_key != key)
650 })
651 {
652 return Err(invalid(
653 "pending merged-doctest manifest mixes final and temporary source identities",
654 ));
655 }
656 }
657
658 let mut ids = BTreeSet::new();
659 let mut ordinals = BTreeSet::new();
660 for point in &self.points {
661 if !valid_id(&point.id, &["statement", "function"])
662 || !matches!(point.kind.as_str(), "statement" | "function")
663 || !source_range_for(&point.source_key, point.start, point.end, pending_group)
664 || !provenance_for(&point.provenance, pending_group)
665 || !sorted_unique_nonempty(&point.definitions)
666 || point.canonical.is_empty()
667 {
668 return Err(invalid("malformed point obligation"));
669 }
670 insert_identity(&mut ids, &mut ordinals, &point.id, &point.probe_ordinal)?;
671 }
672 let mut branch_ids = BTreeSet::new();
673 for branch in &self.branches {
674 if !valid_id(&branch.id, &["branch"])
675 || !matches!(
676 branch.kind.as_str(),
677 "decision-outcome"
678 | "loop-entry"
679 | "match-arm"
680 | "let-else"
681 | "try-operator"
682 | "assertion-outcome"
683 | "logical-selection"
684 )
685 || !source_range_for(&branch.source_key, branch.start, branch.end, pending_group)
686 || !provenance_for(&branch.provenance, pending_group)
687 || !sorted_unique_nonempty(&branch.definitions)
688 || branch.alternatives.len() < 2
689 || (branch.kind == "logical-selection"
690 && !matches!(
691 branch.discriminator.as_str(),
692 "logical-selection:and" | "logical-selection:or"
693 ))
694 || branch.canonical.is_empty()
695 {
696 return Err(invalid("malformed branch obligation"));
697 }
698 insert_identity(&mut ids, &mut ordinals, &branch.id, &branch.probe_ordinal)?;
699 branch_ids.insert(branch.id.as_str());
700 let mut labels = BTreeSet::new();
701 for alternative in &branch.alternatives {
702 if !valid_id(&alternative.id, &["branch-alternative"])
703 || alternative.label.trim().is_empty()
704 || alternative.canonical.is_empty()
705 || !labels.insert(alternative.label.as_str())
706 {
707 return Err(invalid("malformed branch alternative"));
708 }
709 insert_identity(
710 &mut ids,
711 &mut ordinals,
712 &alternative.id,
713 &alternative.probe_ordinal,
714 )?;
715 }
716 }
717 let mut decision_ids = BTreeSet::new();
718 let mut referenced_logical_branch_ids = BTreeSet::new();
719 for decision in &self.decisions {
720 if !valid_id(&decision.id, &["decision"])
721 || !matches!(
722 decision.kind.as_str(),
723 "if" | "if-let"
724 | "while"
725 | "while-let"
726 | "let-chain"
727 | "match-guard"
728 | "assertion"
729 )
730 || !source_range_for(
731 &decision.source_key,
732 decision.start,
733 decision.end,
734 pending_group,
735 )
736 || !provenance_for(&decision.provenance, pending_group)
737 || !sorted_unique_nonempty(&decision.definitions)
738 || decision.conditions.is_empty()
739 || !decision
740 .logical_selections
741 .windows(2)
742 .all(|pair| pair[0].right_condition_index < pair[1].right_condition_index)
743 || decision.canonical.is_empty()
744 || decision.conditions.iter().any(|condition| {
745 !source_range_for(
746 &condition.source_key,
747 condition.start,
748 condition.end,
749 pending_group,
750 ) || condition.source.trim().is_empty()
751 })
752 {
753 return Err(invalid("malformed decision obligation"));
754 }
755 for selection in &decision.logical_selections {
756 if selection.right_condition_index == 0
757 || selection.right_condition_index >= decision.conditions.len()
758 || !referenced_logical_branch_ids.insert(selection.branch_id.as_str())
759 {
760 return Err(invalid("malformed logical-selection relation"));
761 }
762 let Some(branch) = self
763 .branches
764 .iter()
765 .find(|branch| branch.id == selection.branch_id)
766 else {
767 return Err(invalid(
768 "decision references a missing logical-selection branch",
769 ));
770 };
771 if branch.kind != "logical-selection"
772 || branch
773 .alternatives
774 .iter()
775 .map(|alternative| alternative.label.as_str())
776 .collect::<BTreeSet<_>>()
777 != BTreeSet::from(["right operand evaluated", "short-circuited"])
778 {
779 return Err(invalid("decision has a malformed logical-selection branch"));
780 }
781 }
782 let Some(outcome_branch) = self
783 .branches
784 .iter()
785 .find(|branch| branch.id == decision.outcome_branch_id)
786 else {
787 return Err(invalid("decision references a missing outcome branch"));
788 };
789 let expected_kind = if decision.kind == "assertion" {
790 "assertion-outcome"
791 } else {
792 "decision-outcome"
793 };
794 let expected_labels = if decision.kind == "assertion" {
795 BTreeSet::from(["failed", "passed"])
796 } else {
797 BTreeSet::from(["condition false", "condition true"])
798 };
799 if outcome_branch.kind != expected_kind
800 || outcome_branch
801 .alternatives
802 .iter()
803 .map(|alternative| alternative.label.as_str())
804 .collect::<BTreeSet<_>>()
805 != expected_labels
806 {
807 return Err(invalid("decision has a malformed outcome branch"));
808 }
809 match (
810 decision.kind.starts_with("while"),
811 decision.loop_branch_id.as_deref(),
812 ) {
813 (true, Some(loop_branch_id)) => {
814 let Some(loop_branch) = self
815 .branches
816 .iter()
817 .find(|branch| branch.id == loop_branch_id)
818 else {
819 return Err(invalid("decision references a missing loop-entry branch"));
820 };
821 if loop_branch.kind != "loop-entry"
822 || loop_branch
823 .alternatives
824 .iter()
825 .map(|alternative| alternative.label.as_str())
826 .collect::<BTreeSet<_>>()
827 != BTreeSet::from(["entered", "zero iterations"])
828 {
829 return Err(invalid("decision has a malformed loop-entry branch"));
830 }
831 }
832 (true, None) => {
833 return Err(invalid("while decision lacks an exact loop-entry branch"));
834 }
835 (false, Some(_)) => {
836 return Err(invalid("non-while decision references a loop-entry branch"));
837 }
838 (false, None) => {}
839 }
840 insert_identity(
841 &mut ids,
842 &mut ordinals,
843 &decision.id,
844 &decision.probe_ordinal,
845 )?;
846 decision_ids.insert(decision.id.as_str());
847 }
848 let selection_ids = self
849 .selection_groups
850 .iter()
851 .map(|group| group.id.as_str())
852 .collect::<BTreeSet<_>>();
853 if selection_ids.len() != self.selection_groups.len() {
854 return Err(invalid("duplicate match selection group ID"));
855 }
856 let mut grouped_branch_ids = BTreeSet::new();
857 for group in &self.selection_groups {
858 if !valid_id(&group.id, &["match-group"])
859 || group.kind != "match"
860 || !source_range_for(&group.source_key, group.start, group.end, pending_group)
861 || !provenance_for(&group.provenance, pending_group)
862 || !sorted_unique_nonempty(&group.definitions)
863 || group.arms.len() < 2
864 || group.canonical.is_empty()
865 || group
866 .parent_group_id
867 .as_ref()
868 .is_some_and(|parent| !selection_ids.contains(parent.as_str()))
869 || group.parent_site.is_some() != group.parent_group_id.is_some()
870 || group
871 .parent_site
872 .as_deref()
873 .is_some_and(|site| !matches!(site, "scrutinee" | "guard" | "body"))
874 || match group.parent_site.as_deref() {
875 Some("scrutinee") => group.parent_arm_index.is_some(),
876 Some("guard" | "body") => group.parent_arm_index.is_none(),
877 _ => false,
878 }
879 {
880 return Err(RustCompilerManifestError::Invalid(format!(
881 "malformed match selection group {}: id={} kind={} range={} provenance={} definitions={} arms={} canonical={} parent={} site={} arm={}",
882 group.id,
883 valid_id(&group.id, &["match-group"]),
884 group.kind == "match",
885 source_range_for(&group.source_key, group.start, group.end, pending_group,),
886 provenance_for(&group.provenance, pending_group),
887 sorted_unique_nonempty(&group.definitions),
888 group.arms.len(),
889 !group.canonical.is_empty(),
890 group.parent_group_id.is_some(),
891 group.parent_site.is_some(),
892 group.parent_arm_index.is_some(),
893 )));
894 }
895 insert_identity(&mut ids, &mut ordinals, &group.id, &group.probe_ordinal)?;
896 let mut arm_branches = BTreeSet::new();
897 for arm in &group.arms {
898 if !branch_ids.contains(arm.branch_id.as_str())
899 || !arm_branches.insert(arm.branch_id.as_str())
900 || !grouped_branch_ids.insert(arm.branch_id.as_str())
901 || !source_range_for(
902 &arm.body_source_key,
903 arm.body_start,
904 arm.body_end,
905 pending_group,
906 )
907 || arm.guarded != arm.guard_decision_id.is_some()
908 || arm
909 .guard_decision_id
910 .as_ref()
911 .is_some_and(|guard| !decision_ids.contains(guard.as_str()))
912 || ordinal(&arm.selected_ordinal).is_none()
913 || ordinal(&arm.not_selected_ordinal).is_none()
914 {
915 return Err(invalid("malformed match arm mapping"));
916 }
917 let branch = self
918 .branches
919 .iter()
920 .find(|branch| branch.id == arm.branch_id)
921 .expect("validated branch reference");
922 if branch.kind != "match-arm"
923 || branch.alternatives.len() != 2
924 || branch
925 .alternatives
926 .iter()
927 .map(|alternative| alternative.label.as_str())
928 .collect::<BTreeSet<_>>()
929 != BTreeSet::from(["not selected", "selected"])
930 {
931 return Err(invalid("match group references a non-match branch"));
932 }
933 let alternatives = branch
934 .alternatives
935 .iter()
936 .map(|alternative| alternative.probe_ordinal.as_str())
937 .collect::<BTreeSet<_>>();
938 if alternatives
939 != BTreeSet::from([
940 arm.selected_ordinal.as_str(),
941 arm.not_selected_ordinal.as_str(),
942 ])
943 {
944 return Err(invalid("match arm ordinals do not match its branch"));
945 }
946 }
947 }
948 for group in &self.selection_groups {
949 let mut visited = BTreeSet::new();
950 let mut current = group;
951 while let Some(parent_id) = ¤t.parent_group_id {
952 if !visited.insert(current.id.as_str()) {
953 return Err(invalid("cyclic match selection group parentage"));
954 }
955 let parent = self
956 .selection_groups
957 .iter()
958 .find(|candidate| candidate.id == *parent_id)
959 .expect("validated parent group reference");
960 if current
961 .parent_arm_index
962 .is_some_and(|index| index >= parent.arms.len())
963 {
964 return Err(invalid("match selection parent arm is out of range"));
965 }
966 current = parent;
967 }
968 }
969 Ok(())
970 }
971
972 pub fn normalize(
977 &self,
978 sources: &BTreeMap<String, RustCompilerSource>,
979 ) -> Result<NormalizedRustCompilerManifest, RustCompilerManifestError> {
980 self.validate()?;
981 let required_source_keys = self
982 .points
983 .iter()
984 .map(|point| point.source_key.as_str())
985 .chain(
986 self.branches
987 .iter()
988 .map(|branch| branch.source_key.as_str()),
989 )
990 .chain(self.decisions.iter().flat_map(|decision| {
991 std::iter::once(decision.source_key.as_str()).chain(
992 decision
993 .conditions
994 .iter()
995 .map(|condition| condition.source_key.as_str()),
996 )
997 }))
998 .chain(self.selection_groups.iter().flat_map(|group| {
999 std::iter::once(group.source_key.as_str())
1000 .chain(group.arms.iter().map(|arm| arm.body_source_key.as_str()))
1001 }))
1002 .collect::<BTreeSet<_>>();
1003 let supplied_source_keys = sources.keys().map(String::as_str).collect::<BTreeSet<_>>();
1004 if supplied_source_keys != required_source_keys {
1005 let missing = required_source_keys
1006 .difference(&supplied_source_keys)
1007 .copied()
1008 .collect::<Vec<_>>();
1009 let extra = supplied_source_keys
1010 .difference(&required_source_keys)
1011 .copied()
1012 .collect::<Vec<_>>();
1013 return Err(RustCompilerManifestError::InvalidSource(format!(
1014 "source snapshot keys differ from the denominator (missing: {}; extra: {})",
1015 missing.join(", "),
1016 extra.join(", ")
1017 )));
1018 }
1019 let location = |key: &str, start: u32, end: u32| source_location(sources, key, start, end);
1020
1021 let mut hit_obligations_by_ordinal = BTreeMap::<u64, BTreeSet<String>>::new();
1022 let mut internal_ordinals = BTreeSet::new();
1023 let mut points = Vec::with_capacity(self.points.len());
1024 for point in &self.points {
1025 let (file, line, column, source) = location(&point.source_key, point.start, point.end)?;
1026 let point_kind = match point.kind.as_str() {
1027 "statement" => PointKind::Statement,
1028 "function" => PointKind::Function,
1029 _ => unreachable!("validated point kind"),
1030 };
1031 let probe_ordinal = ordinal(&point.probe_ordinal).expect("validated point ordinal");
1032 hit_obligations_by_ordinal
1033 .entry(probe_ordinal)
1034 .or_default()
1035 .insert(point.id.clone());
1036 points.push(PointMeta {
1037 id: point.id.clone(),
1038 kind: point_kind,
1039 file,
1040 line,
1041 column,
1042 source,
1043 label: (!point.discriminator.is_empty()).then(|| point.discriminator.clone()),
1044 });
1045 }
1046
1047 let decision_logical_branch_ids = self
1048 .decisions
1049 .iter()
1050 .flat_map(|decision| {
1051 decision
1052 .logical_selections
1053 .iter()
1054 .map(|selection| selection.branch_id.as_str())
1055 })
1056 .collect::<BTreeSet<_>>();
1057 let mut branches = Vec::with_capacity(self.branches.len());
1058 for branch in &self.branches {
1059 let (file, line, column, source) =
1060 location(&branch.source_key, branch.start, branch.end)?;
1061 internal_ordinals
1062 .insert(ordinal(&branch.probe_ordinal).expect("validated branch group ordinal"));
1063 let alternatives = branch
1064 .alternatives
1065 .iter()
1066 .map(|alternative| {
1067 let probe_ordinal = ordinal(&alternative.probe_ordinal)
1068 .expect("validated branch alternative ordinal");
1069 if decision_logical_branch_ids.contains(branch.id.as_str()) {
1070 internal_ordinals.insert(probe_ordinal);
1071 } else {
1072 hit_obligations_by_ordinal
1073 .entry(probe_ordinal)
1074 .or_default()
1075 .insert(alternative.id.clone());
1076 }
1077 BranchAlternativeMeta {
1078 id: alternative.id.clone(),
1079 label: alternative.label.clone(),
1080 }
1081 })
1082 .collect();
1083 branches.push(BranchMeta {
1084 id: branch.id.clone(),
1085 kind: branch.kind.clone(),
1086 file,
1087 line,
1088 column,
1089 source,
1090 alternatives,
1091 });
1092 }
1093
1094 let mut decisions = Vec::with_capacity(self.decisions.len());
1095 let branches_by_id = self
1096 .branches
1097 .iter()
1098 .map(|branch| (branch.id.as_str(), branch))
1099 .collect::<BTreeMap<_, _>>();
1100 let mut decision_outcome_obligations = BTreeMap::new();
1101 let mut decision_loop_obligations = BTreeMap::new();
1102 let mut decision_logical_selection_obligations = BTreeMap::new();
1103 for decision in &self.decisions {
1104 let (file, line, column, source) =
1105 location(&decision.source_key, decision.start, decision.end)?;
1106 for condition in &decision.conditions {
1107 location(&condition.source_key, condition.start, condition.end)?;
1110 }
1111 internal_ordinals
1112 .insert(ordinal(&decision.probe_ordinal).expect("validated decision ordinal"));
1113 let outcome_branch = branches_by_id[decision.outcome_branch_id.as_str()];
1114 let labels = if decision.kind == "assertion" {
1115 ("failed", "passed")
1116 } else {
1117 ("condition false", "condition true")
1118 };
1119 let alternative = |label: &str| {
1120 outcome_branch
1121 .alternatives
1122 .iter()
1123 .find(|alternative| alternative.label == label)
1124 .expect("validated decision outcome label")
1125 .id
1126 .clone()
1127 };
1128 decision_outcome_obligations.insert(
1129 decision.id.clone(),
1130 (alternative(labels.0), alternative(labels.1)),
1131 );
1132 if let Some(loop_branch_id) = decision.loop_branch_id.as_deref() {
1133 let loop_branch = branches_by_id[loop_branch_id];
1134 let loop_alternative = |label: &str| {
1135 loop_branch
1136 .alternatives
1137 .iter()
1138 .find(|alternative| alternative.label == label)
1139 .expect("validated loop-entry label")
1140 .id
1141 .clone()
1142 };
1143 decision_loop_obligations.insert(
1144 decision.id.clone(),
1145 (
1146 loop_alternative("zero iterations"),
1147 loop_alternative("entered"),
1148 ),
1149 );
1150 }
1151 let logical_selections = decision
1152 .logical_selections
1153 .iter()
1154 .map(|selection| {
1155 let branch = branches_by_id[selection.branch_id.as_str()];
1156 let alternative = |label: &str| {
1157 branch
1158 .alternatives
1159 .iter()
1160 .find(|alternative| alternative.label == label)
1161 .expect("validated logical-selection label")
1162 .id
1163 .clone()
1164 };
1165 NormalizedRustLogicalSelection {
1166 short_circuited_id: alternative("short-circuited"),
1167 right_evaluated_id: alternative("right operand evaluated"),
1168 right_condition_index: selection.right_condition_index,
1169 }
1170 })
1171 .collect::<Vec<_>>();
1172 if !logical_selections.is_empty() {
1173 decision_logical_selection_obligations
1174 .insert(decision.id.clone(), logical_selections);
1175 }
1176 decisions.push(DecisionMeta {
1177 id: decision.id.clone(),
1178 file,
1179 line,
1180 column,
1181 source,
1182 conditions: decision
1183 .conditions
1184 .iter()
1185 .map(|condition| condition.source.clone())
1186 .collect(),
1187 kind: decision.kind.clone(),
1188 });
1189 }
1190
1191 for group in &self.selection_groups {
1192 internal_ordinals
1193 .insert(ordinal(&group.probe_ordinal).expect("validated selection group ordinal"));
1194 for selected in &group.arms {
1195 let selected_ordinal =
1196 ordinal(&selected.selected_ordinal).expect("validated selected ordinal");
1197 let implied = hit_obligations_by_ordinal
1198 .get_mut(&selected_ordinal)
1199 .expect("validated selected alternative ordinal");
1200 for sibling in &group.arms {
1201 if sibling.branch_id == selected.branch_id {
1202 continue;
1203 }
1204 let sibling_branch = branches_by_id
1205 .get(sibling.branch_id.as_str())
1206 .expect("validated match branch");
1207 let not_selected = sibling_branch
1208 .alternatives
1209 .iter()
1210 .find(|alternative| {
1211 alternative.probe_ordinal == sibling.not_selected_ordinal
1212 })
1213 .expect("validated not-selected alternative");
1214 implied.insert(not_selected.id.clone());
1215 }
1216 }
1217 }
1218
1219 let limitation_file = points
1220 .first()
1221 .map(|point| point.file.clone())
1222 .unwrap_or_default();
1223 let limitations = self
1224 .limitations
1225 .iter()
1226 .enumerate()
1227 .map(|(index, limitation)| {
1228 json!({
1229 "id": format!("rust-compiler-candidate:{index}"),
1230 "kind": "rust-compiler-candidate",
1231 "file": limitation_file,
1232 "line": 1,
1233 "column": 0,
1234 "source": "",
1235 "reason": limitation,
1236 })
1237 })
1238 .collect();
1239
1240 let (source_fingerprint, generated_source_files) = compiler_source_fingerprint(sources);
1241 Ok(NormalizedRustCompilerManifest {
1242 manifest: CoverageManifest {
1243 decisions,
1244 points,
1245 branches,
1246 limitations,
1247 unmeasured: self.unmeasured_obligations.clone(),
1248 scope: Some(json!({
1249 "language": "rust",
1250 "model": self.model,
1251 "crate": self.crate_name,
1252 "measurementComplete": self.measurement_complete,
1253 "sourceFingerprint": {
1254 "algorithm": "sha256",
1255 "digest": source_fingerprint,
1256 "files": sources.len(),
1257 "generatedFiles": generated_source_files,
1258 },
1259 })),
1260 },
1261 hit_obligations_by_ordinal: hit_obligations_by_ordinal
1262 .into_iter()
1263 .map(|(ordinal, ids)| (ordinal, ids.into_iter().collect()))
1264 .collect(),
1265 internal_ordinals,
1266 decision_outcome_obligations,
1267 decision_loop_obligations,
1268 decision_logical_selection_obligations,
1269 })
1270 }
1271}
1272
1273fn source_location(
1274 sources: &BTreeMap<String, RustCompilerSource>,
1275 key: &str,
1276 start: u32,
1277 end: u32,
1278) -> Result<(String, usize, usize, String), RustCompilerManifestError> {
1279 let source = sources
1280 .get(key)
1281 .ok_or_else(|| RustCompilerManifestError::MissingSource(key.into()))?;
1282 if source.file.trim().is_empty() {
1283 return Err(RustCompilerManifestError::InvalidSource(format!(
1284 "{key} has an empty display path"
1285 )));
1286 }
1287 let start = usize::try_from(start).expect("u32 always fits supported usize");
1288 let end = usize::try_from(end).expect("u32 always fits supported usize");
1289 if start >= end
1290 || end > source.source.len()
1291 || !source.source.is_char_boundary(start)
1292 || !source.source.is_char_boundary(end)
1293 {
1294 return Err(RustCompilerManifestError::InvalidSource(format!(
1295 "{key} range {start}..{end} is outside UTF-8 source bytes"
1296 )));
1297 }
1298 let line_start = source.source[..start]
1299 .rfind('\n')
1300 .map_or(0, |index| index + 1);
1301 let line = source.source[..start]
1302 .bytes()
1303 .filter(|byte| *byte == b'\n')
1304 .count()
1305 + 1;
1306 Ok((
1307 source.file.clone(),
1308 line,
1309 start - line_start,
1310 source.source[start..end].into(),
1311 ))
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use serde_json::json;
1317
1318 use super::*;
1319
1320 fn valid_manifest() -> serde_json::Value {
1321 json!({
1322 "schema": SCHEMA,
1323 "model": MODEL,
1324 "crate": "fixture",
1325 "measurementComplete": false,
1326 "points": [{
1327 "id": "rs:function:000000000000000000000001",
1328 "kind": "function",
1329 "sourceKey": "source:src/lib.rs",
1330 "start": 0,
1331 "end": 10,
1332 "provenance": "authored-source",
1333 "discriminator": "",
1334 "probeOrdinal": "1",
1335 "definitions": ["fixture::function"],
1336 "canonical": "function"
1337 }],
1338 "branches": [{
1339 "id": "rs:branch:000000000000000000000002",
1340 "kind": "decision-outcome",
1341 "discriminator": "decision-outcome:if",
1342 "sourceKey": "source:src/lib.rs",
1343 "start": 11,
1344 "end": 20,
1345 "provenance": "authored-source",
1346 "probeOrdinal": "2",
1347 "definitions": ["fixture::function"],
1348 "alternatives": [
1349 {"id": "rs:branch-alternative:000000000000000000000003", "label": "condition true", "probeOrdinal": "3", "canonical": "true"},
1350 {"id": "rs:branch-alternative:000000000000000000000004", "label": "condition false", "probeOrdinal": "4", "canonical": "false"}
1351 ],
1352 "canonical": "branch"
1353 }],
1354 "decisions": [{
1355 "id": "rs:decision:000000000000000000000005",
1356 "kind": "if",
1357 "sourceKey": "source:src/lib.rs",
1358 "start": 11,
1359 "end": 15,
1360 "provenance": "authored-source",
1361 "probeOrdinal": "5",
1362 "definitions": ["fixture::function"],
1363 "outcomeBranchId": "rs:branch:000000000000000000000002",
1364 "loopBranchId": null,
1365 "logicalSelections": [],
1366 "conditions": [{"sourceKey": "source:src/lib.rs", "start": 11, "end": 15, "source": "value"}],
1367 "canonical": "decision"
1368 }],
1369 "selectionGroups": [],
1370 "limitations": ["RUST_PRIVATE_CANDIDATE: incomplete"]
1371 })
1372 }
1373
1374 #[test]
1375 fn accepts_only_a_strict_collision_free_private_candidate() {
1376 let manifest =
1377 RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1378 assert_eq!(manifest.crate_name, "fixture");
1379
1380 let mut legacy_schema = valid_manifest();
1381 legacy_schema["schema"] = json!("supercov-rust-manifest-candidate-v1");
1382 assert!(matches!(
1383 RustCompilerManifest::parse(&serde_json::to_vec(&legacy_schema).unwrap()),
1384 Err(RustCompilerManifestError::Invalid(_))
1385 ));
1386 let mut previous_schema = valid_manifest();
1387 previous_schema["schema"] = json!("supercov-rust-manifest-candidate-v2");
1388 assert!(matches!(
1389 RustCompilerManifest::parse(&serde_json::to_vec(&previous_schema).unwrap()),
1390 Err(RustCompilerManifestError::Invalid(_))
1391 ));
1392
1393 let mut missing_alternative_canonical = valid_manifest();
1394 missing_alternative_canonical["branches"][0]["alternatives"][0]
1395 .as_object_mut()
1396 .unwrap()
1397 .remove("canonical");
1398 assert!(matches!(
1399 RustCompilerManifest::parse(
1400 &serde_json::to_vec(&missing_alternative_canonical).unwrap()
1401 ),
1402 Err(RustCompilerManifestError::Json(_))
1403 ));
1404
1405 let mut unknown = valid_manifest();
1406 unknown["unexpected"] = json!(true);
1407 assert!(matches!(
1408 RustCompilerManifest::parse(&serde_json::to_vec(&unknown).unwrap()),
1409 Err(RustCompilerManifestError::Json(_))
1410 ));
1411
1412 let mut collision = valid_manifest();
1413 collision["decisions"][0]["probeOrdinal"] = json!("1");
1414 assert!(matches!(
1415 RustCompilerManifest::parse(&serde_json::to_vec(&collision).unwrap()),
1416 Err(RustCompilerManifestError::Invalid(_))
1417 ));
1418
1419 let mut complete = valid_manifest();
1420 complete["measurementComplete"] = json!(true);
1421 assert!(matches!(
1422 RustCompilerManifest::parse(&serde_json::to_vec(&complete).unwrap()),
1423 Err(RustCompilerManifestError::Invalid(_))
1424 ));
1425
1426 let mut traversal = valid_manifest();
1427 traversal["points"][0]["sourceKey"] = json!("source:../outside.rs");
1428 assert!(matches!(
1429 RustCompilerManifest::parse(&serde_json::to_vec(&traversal).unwrap()),
1430 Err(RustCompilerManifestError::Invalid(_))
1431 ));
1432
1433 let mut missing_outcome = valid_manifest();
1434 missing_outcome["decisions"][0]["outcomeBranchId"] = json!("rs:branch:missing");
1435 assert!(matches!(
1436 RustCompilerManifest::parse(&serde_json::to_vec(&missing_outcome).unwrap()),
1437 Err(RustCompilerManifestError::Invalid(_))
1438 ));
1439
1440 let mut wrong_outcome_kind = valid_manifest();
1441 wrong_outcome_kind["branches"][0]["kind"] = json!("loop-entry");
1442 wrong_outcome_kind["branches"][0]["alternatives"] = json!([
1443 {"id": "rs:branch-alternative:000000000000000000000003", "label": "zero iterations", "probeOrdinal": "3", "canonical": "zero"},
1444 {"id": "rs:branch-alternative:000000000000000000000004", "label": "entered", "probeOrdinal": "4", "canonical": "entered"}
1445 ]);
1446 assert!(matches!(
1447 RustCompilerManifest::parse(&serde_json::to_vec(&wrong_outcome_kind).unwrap()),
1448 Err(RustCompilerManifestError::Invalid(_))
1449 ));
1450
1451 let mut loop_manifest = valid_manifest();
1452 loop_manifest["decisions"][0]["kind"] = json!("while");
1453 loop_manifest["decisions"][0]["loopBranchId"] = json!("rs:branch:000000000000000000000006");
1454 loop_manifest["branches"].as_array_mut().unwrap().push(json!({
1455 "id": "rs:branch:000000000000000000000006",
1456 "kind": "loop-entry",
1457 "discriminator": "loop-entry:while",
1458 "sourceKey": "source:src/lib.rs",
1459 "start": 11,
1460 "end": 20,
1461 "provenance": "authored-source",
1462 "probeOrdinal": "6",
1463 "definitions": ["fixture::function"],
1464 "alternatives": [
1465 {"id": "rs:branch-alternative:000000000000000000000007", "label": "zero iterations", "probeOrdinal": "7", "canonical": "zero"},
1466 {"id": "rs:branch-alternative:000000000000000000000008", "label": "entered", "probeOrdinal": "8", "canonical": "entered"}
1467 ],
1468 "canonical": "loop-entry"
1469 }));
1470 let parsed_loop =
1471 RustCompilerManifest::parse(&serde_json::to_vec(&loop_manifest).unwrap()).unwrap();
1472 assert_eq!(
1473 parsed_loop.decisions[0].loop_branch_id.as_deref(),
1474 Some("rs:branch:000000000000000000000006")
1475 );
1476 let normalized_loop = parsed_loop
1477 .normalize(&BTreeMap::from([(
1478 "source:src/lib.rs".into(),
1479 RustCompilerSource {
1480 file: "src/lib.rs".into(),
1481 source: "0123456789 value and more source bytes".into(),
1482 },
1483 )]))
1484 .unwrap();
1485 assert_eq!(
1486 normalized_loop.decision_loop_obligations["rs:decision:000000000000000000000005"],
1487 (
1488 "rs:branch-alternative:000000000000000000000007".into(),
1489 "rs:branch-alternative:000000000000000000000008".into(),
1490 )
1491 );
1492
1493 let mut missing_loop = loop_manifest.clone();
1494 missing_loop["decisions"][0]["loopBranchId"] = json!(null);
1495 assert!(matches!(
1496 RustCompilerManifest::parse(&serde_json::to_vec(&missing_loop).unwrap()),
1497 Err(RustCompilerManifestError::Invalid(_))
1498 ));
1499
1500 let mut non_loop_relation = loop_manifest;
1501 non_loop_relation["decisions"][0]["kind"] = json!("if");
1502 assert!(matches!(
1503 RustCompilerManifest::parse(&serde_json::to_vec(&non_loop_relation).unwrap()),
1504 Err(RustCompilerManifestError::Invalid(_))
1505 ));
1506 }
1507
1508 #[test]
1509 fn logical_selections_are_strict_relations_projected_from_decision_vectors() {
1510 let mut value = valid_manifest();
1511 value["branches"].as_array_mut().unwrap().push(json!({
1512 "id": "rs:branch:000000000000000000000006",
1513 "kind": "logical-selection",
1514 "discriminator": "logical-selection:and",
1515 "sourceKey": "source:src/lib.rs",
1516 "start": 11,
1517 "end": 20,
1518 "provenance": "authored-source",
1519 "probeOrdinal": "6",
1520 "definitions": ["fixture::function"],
1521 "alternatives": [
1522 {"id": "rs:branch-alternative:000000000000000000000007", "label": "short-circuited", "probeOrdinal": "7", "canonical": "short"},
1523 {"id": "rs:branch-alternative:000000000000000000000008", "label": "right operand evaluated", "probeOrdinal": "8", "canonical": "evaluated"}
1524 ],
1525 "canonical": "logical-selection"
1526 }));
1527 value["decisions"][0]["conditions"] = json!([
1528 {"sourceKey": "source:src/lib.rs", "start": 11, "end": 15, "source": "left"},
1529 {"sourceKey": "source:src/lib.rs", "start": 16, "end": 20, "source": "right"}
1530 ]);
1531 value["decisions"][0]["logicalSelections"] = json!([{
1532 "branchId": "rs:branch:000000000000000000000006",
1533 "rightConditionIndex": 1
1534 }]);
1535
1536 let manifest = RustCompilerManifest::parse(&serde_json::to_vec(&value).unwrap()).unwrap();
1537 let normalized = manifest
1538 .normalize(&BTreeMap::from([(
1539 "source:src/lib.rs".into(),
1540 RustCompilerSource {
1541 file: "src/lib.rs".into(),
1542 source: "0123456789 left && right and more source bytes".into(),
1543 },
1544 )]))
1545 .unwrap();
1546 let relation = &normalized.decision_logical_selection_obligations["rs:decision:000000000000000000000005"]
1547 [0];
1548 assert_eq!(relation.right_condition_index, 1);
1549 assert_eq!(
1550 relation.short_circuited_id,
1551 "rs:branch-alternative:000000000000000000000007"
1552 );
1553 assert_eq!(
1554 relation.right_evaluated_id,
1555 "rs:branch-alternative:000000000000000000000008"
1556 );
1557 assert!(normalized.internal_ordinals.contains(&7));
1558 assert!(normalized.internal_ordinals.contains(&8));
1559 assert!(!normalized.hit_obligations_by_ordinal.contains_key(&7));
1560 assert!(!normalized.hit_obligations_by_ordinal.contains_key(&8));
1561
1562 let mut missing_relation = value.clone();
1563 missing_relation["decisions"][0]["logicalSelections"] = json!([]);
1564 let value_selection =
1565 RustCompilerManifest::parse(&serde_json::to_vec(&missing_relation).unwrap())
1566 .unwrap()
1567 .normalize(&BTreeMap::from([(
1568 "source:src/lib.rs".into(),
1569 RustCompilerSource {
1570 file: "src/lib.rs".into(),
1571 source: "0123456789 left && right and more source bytes".into(),
1572 },
1573 )]))
1574 .unwrap();
1575 assert!(value_selection.hit_obligations_by_ordinal.contains_key(&7));
1576 assert!(value_selection.hit_obligations_by_ordinal.contains_key(&8));
1577
1578 let mut source_ordered = value.clone();
1579 source_ordered["branches"].as_array_mut().unwrap().push(json!({
1580 "id": "rs:branch:000000000000000000000009",
1581 "kind": "logical-selection",
1582 "discriminator": "logical-selection:or",
1583 "sourceKey": "source:src/lib.rs",
1584 "start": 11,
1585 "end": 20,
1586 "provenance": "authored-source",
1587 "probeOrdinal": "9",
1588 "definitions": ["fixture::function"],
1589 "alternatives": [
1590 {"id": "rs:branch-alternative:000000000000000000000010", "label": "short-circuited", "probeOrdinal": "10", "canonical": "short-two"},
1591 {"id": "rs:branch-alternative:000000000000000000000011", "label": "right operand evaluated", "probeOrdinal": "11", "canonical": "evaluated-two"}
1592 ],
1593 "canonical": "logical-selection-two"
1594 }));
1595 source_ordered["decisions"][0]["conditions"] = json!([
1596 {"sourceKey": "source:src/lib.rs", "start": 11, "end": 12, "source": "first"},
1597 {"sourceKey": "source:src/lib.rs", "start": 13, "end": 14, "source": "second"},
1598 {"sourceKey": "source:src/lib.rs", "start": 15, "end": 20, "source": "third"}
1599 ]);
1600 source_ordered["decisions"][0]["logicalSelections"] = json!([
1601 {"branchId": "rs:branch:000000000000000000000009", "rightConditionIndex": 1},
1602 {"branchId": "rs:branch:000000000000000000000006", "rightConditionIndex": 2}
1603 ]);
1604 let parsed_source_ordered =
1605 RustCompilerManifest::parse(&serde_json::to_vec(&source_ordered).unwrap()).unwrap();
1606 assert_eq!(
1607 parsed_source_ordered.decisions[0]
1608 .logical_selections
1609 .iter()
1610 .map(|selection| selection.right_condition_index)
1611 .collect::<Vec<_>>(),
1612 vec![1, 2],
1613 );
1614 source_ordered["decisions"][0]["logicalSelections"]
1615 .as_array_mut()
1616 .unwrap()
1617 .reverse();
1618 assert!(matches!(
1619 RustCompilerManifest::parse(&serde_json::to_vec(&source_ordered).unwrap()),
1620 Err(RustCompilerManifestError::Invalid(_))
1621 ));
1622
1623 let mut invalid_index = value;
1624 invalid_index["decisions"][0]["logicalSelections"][0]["rightConditionIndex"] = json!(2);
1625 assert!(matches!(
1626 RustCompilerManifest::parse(&serde_json::to_vec(&invalid_index).unwrap()),
1627 Err(RustCompilerManifestError::Invalid(_))
1628 ));
1629 let mut invalid_discriminator = valid_manifest();
1630 invalid_discriminator["branches"].as_array_mut().unwrap().push(json!({
1631 "id": "rs:branch:000000000000000000000006",
1632 "kind": "logical-selection",
1633 "discriminator": "logical-selection:unknown",
1634 "sourceKey": "source:src/lib.rs",
1635 "start": 11,
1636 "end": 20,
1637 "provenance": "authored-source",
1638 "probeOrdinal": "6",
1639 "definitions": ["fixture::function"],
1640 "alternatives": [
1641 {"id": "rs:branch-alternative:000000000000000000000007", "label": "short-circuited", "probeOrdinal": "7", "canonical": "short"},
1642 {"id": "rs:branch-alternative:000000000000000000000008", "label": "right operand evaluated", "probeOrdinal": "8", "canonical": "evaluated"}
1643 ],
1644 "canonical": "logical-selection"
1645 }));
1646 assert!(matches!(
1647 RustCompilerManifest::parse(&serde_json::to_vec(&invalid_discriminator).unwrap()),
1648 Err(RustCompilerManifestError::Invalid(_))
1649 ));
1650 }
1651
1652 #[test]
1653 fn accepts_exact_doctest_source_provenance() {
1654 let mut value = valid_manifest();
1655 value["points"][0]["provenance"] = json!("doctest-source");
1656
1657 let manifest = RustCompilerManifest::parse(&serde_json::to_vec(&value).unwrap()).unwrap();
1658 assert_eq!(manifest.points[0].provenance, "doctest-source");
1659 }
1660
1661 #[test]
1662 fn source_snapshot_envelope_is_strict_and_never_resolves_paths() {
1663 let snapshots = json!({
1664 "schema": SOURCE_SNAPSHOT_SCHEMA,
1665 "crate": "fixture",
1666 "sources": {
1667 "source:src/lib.rs": {"file": "src/lib.rs", "source": "fn work() {}\n"},
1668 "generated:package:.:generated.rs": {
1669 "file": "generated:package:.:generated.rs",
1670 "source": "fn generated() {}\n"
1671 }
1672 }
1673 });
1674 let parsed =
1675 RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&snapshots).unwrap()).unwrap();
1676 assert_eq!(parsed.crate_name, "fixture");
1677 assert_eq!(parsed.sources.len(), 2);
1678
1679 let mut unknown = snapshots.clone();
1680 unknown["path"] = json!("/tmp/guess");
1681 assert!(
1682 RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&unknown).unwrap()).is_err()
1683 );
1684
1685 let mut traversal = snapshots;
1686 traversal["sources"]["source:../outside.rs"] =
1687 json!({"file": "outside.rs", "source": "fn hidden() {}"});
1688 assert!(
1689 RustCompilerSourceSnapshots::parse(&serde_json::to_vec(&traversal).unwrap()).is_err()
1690 );
1691 }
1692
1693 #[test]
1694 fn repeated_compiler_units_merge_only_when_identity_and_source_are_exact() {
1695 let first =
1696 RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1697 let mut second = first.clone();
1698 second.points[0].definitions = vec![
1699 "fixture::function".into(),
1700 "fixture::tests::function".into(),
1701 ];
1702 let snapshots = RustCompilerSourceSnapshots::parse(
1703 &serde_json::to_vec(&json!({
1704 "schema": SOURCE_SNAPSHOT_SCHEMA,
1705 "crate": "fixture",
1706 "sources": {
1707 "source:src/lib.rs": {
1708 "file": "src/lib.rs",
1709 "source": "0123456789 value and more source bytes"
1710 }
1711 }
1712 }))
1713 .unwrap(),
1714 )
1715 .unwrap();
1716 let normalized = normalize_rust_compiler_candidates(vec![
1717 (first.clone(), snapshots.clone()),
1718 (second, snapshots.clone()),
1719 ])
1720 .unwrap();
1721 assert_eq!(normalized.manifest.points.len(), 1);
1722 assert_eq!(normalized.manifest.branches.len(), 1);
1723 assert_eq!(normalized.manifest.decisions.len(), 1);
1724
1725 let mut changed = snapshots;
1726 changed.sources.get_mut("source:src/lib.rs").unwrap().source =
1727 "changed source bytes that remain long enough".into();
1728 assert!(normalize_rust_compiler_candidates(vec![
1729 (first.clone(), RustCompilerSourceSnapshots::parse(
1730 &serde_json::to_vec(&json!({
1731 "schema": SOURCE_SNAPSHOT_SCHEMA,
1732 "crate": "fixture",
1733 "sources": {"source:src/lib.rs": {"file": "src/lib.rs", "source": "0123456789 value and more source bytes"}}
1734 })).unwrap(),
1735 ).unwrap()),
1736 (first, changed),
1737 ]).is_err());
1738 }
1739
1740 #[test]
1741 fn normalizes_exact_source_locations_and_runtime_ordinals() {
1742 let manifest =
1743 RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1744 let sources = BTreeMap::from([(
1745 "source:src/lib.rs".into(),
1746 RustCompilerSource {
1747 file: "src/lib.rs".into(),
1748 source: "0123456789\nvalue && more text".into(),
1749 },
1750 )]);
1751 let normalized = manifest.normalize(&sources).unwrap();
1752 assert_eq!(normalized.manifest.points[0].source, "0123456789");
1753 assert_eq!(normalized.manifest.branches[0].line, 2);
1754 assert_eq!(normalized.manifest.branches[0].column, 0);
1755 assert_eq!(normalized.manifest.branches[0].source, "value && ");
1756 assert_eq!(
1757 normalized.hit_obligations_by_ordinal[&1],
1758 ["rs:function:000000000000000000000001"]
1759 );
1760 assert_eq!(
1761 normalized.hit_obligations_by_ordinal[&3],
1762 ["rs:branch-alternative:000000000000000000000003"]
1763 );
1764 assert_eq!(normalized.internal_ordinals, BTreeSet::from([2, 5]));
1765 assert_eq!(
1766 normalized.decision_outcome_obligations["rs:decision:000000000000000000000005"],
1767 (
1768 "rs:branch-alternative:000000000000000000000004".into(),
1769 "rs:branch-alternative:000000000000000000000003".into(),
1770 )
1771 );
1772 assert_eq!(normalized.manifest.limitations.len(), 1);
1773 let fingerprint = &normalized.manifest.scope.as_ref().unwrap()["sourceFingerprint"];
1774 assert_eq!(fingerprint["algorithm"], "sha256");
1775 assert_eq!(fingerprint["digest"].as_str().unwrap().len(), 64);
1776 assert_eq!(fingerprint["files"], 1);
1777 assert_eq!(fingerprint["generatedFiles"], 0);
1778 }
1779
1780 #[test]
1781 fn source_fingerprint_binds_full_generated_bytes_outside_obligation_ranges() {
1782 let mut candidate = valid_manifest();
1783 let generated_key = "generated:package:.:generated.rs";
1784 candidate["points"][0]["sourceKey"] = json!(generated_key);
1785 candidate["branches"][0]["sourceKey"] = json!(generated_key);
1786 candidate["decisions"][0]["sourceKey"] = json!(generated_key);
1787 candidate["decisions"][0]["conditions"][0]["sourceKey"] = json!(generated_key);
1788 let manifest =
1789 RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1790 let baseline_sources = BTreeMap::from([(
1791 generated_key.into(),
1792 RustCompilerSource {
1793 file: generated_key.into(),
1794 source: "0123456789 value and more source bytes // baseline".into(),
1795 },
1796 )]);
1797 let changed_sources = BTreeMap::from([(
1798 generated_key.into(),
1799 RustCompilerSource {
1800 file: generated_key.into(),
1801 source: "0123456789 value and more source bytes // changed!".into(),
1802 },
1803 )]);
1804 let baseline = manifest.normalize(&baseline_sources).unwrap().manifest;
1805 let changed = manifest.normalize(&changed_sources).unwrap().manifest;
1806 assert_eq!(
1807 baseline.scope.as_ref().unwrap()["sourceFingerprint"]["generatedFiles"],
1808 1,
1809 );
1810 assert_ne!(
1811 baseline.scope.as_ref().unwrap()["sourceFingerprint"]["digest"],
1812 changed.scope.as_ref().unwrap()["sourceFingerprint"]["digest"],
1813 );
1814 let mut baseline_without_fingerprint = serde_json::to_value(&baseline).unwrap();
1815 let mut changed_without_fingerprint = serde_json::to_value(&changed).unwrap();
1816 baseline_without_fingerprint["scope"]
1817 .as_object_mut()
1818 .unwrap()
1819 .remove("sourceFingerprint");
1820 changed_without_fingerprint["scope"]
1821 .as_object_mut()
1822 .unwrap()
1823 .remove("sourceFingerprint");
1824 assert_eq!(baseline_without_fingerprint, changed_without_fingerprint);
1825 }
1826
1827 #[test]
1828 fn match_selection_expands_to_sibling_not_selected_obligations() {
1829 let mut candidate = valid_manifest();
1830 let outcome_branch = candidate["branches"][0].clone();
1831 candidate["branches"] = json!([
1832 outcome_branch,
1833 {
1834 "id": "rs:branch:000000000000000000000006",
1835 "kind": "match-arm",
1836 "discriminator": "match-arm:0",
1837 "sourceKey": "source:src/lib.rs",
1838 "start": 11,
1839 "end": 16,
1840 "provenance": "authored-source",
1841 "probeOrdinal": "6",
1842 "definitions": ["fixture::function"],
1843 "alternatives": [
1844 {"id": "rs:branch-alternative:000000000000000000000007", "label": "not selected", "probeOrdinal": "7", "canonical": "not selected"},
1845 {"id": "rs:branch-alternative:000000000000000000000008", "label": "selected", "probeOrdinal": "8", "canonical": "selected"}
1846 ],
1847 "canonical": "first arm"
1848 },
1849 {
1850 "id": "rs:branch:000000000000000000000009",
1851 "kind": "match-arm",
1852 "discriminator": "match-arm:1",
1853 "sourceKey": "source:src/lib.rs",
1854 "start": 17,
1855 "end": 22,
1856 "provenance": "authored-source",
1857 "probeOrdinal": "9",
1858 "definitions": ["fixture::function"],
1859 "alternatives": [
1860 {"id": "rs:branch-alternative:00000000000000000000000a", "label": "not selected", "probeOrdinal": "10", "canonical": "not selected"},
1861 {"id": "rs:branch-alternative:00000000000000000000000b", "label": "selected", "probeOrdinal": "11", "canonical": "selected"}
1862 ],
1863 "canonical": "second arm"
1864 }
1865 ]);
1866 candidate["selectionGroups"] = json!([{
1867 "id": "rs:match-group:00000000000000000000000c",
1868 "kind": "match",
1869 "sourceKey": "source:src/lib.rs",
1870 "start": 11,
1871 "end": 22,
1872 "provenance": "authored-source",
1873 "probeOrdinal": "12",
1874 "definitions": ["fixture::function"],
1875 "parentGroupId": null,
1876 "parentSite": null,
1877 "parentArmIndex": null,
1878 "arms": [
1879 {"branchId": "rs:branch:000000000000000000000006", "bodySourceKey": "source:src/lib.rs", "bodyStart": 11, "bodyEnd": 16, "guarded": false, "guardDecisionId": null, "selectedOrdinal": "8", "notSelectedOrdinal": "7"},
1880 {"branchId": "rs:branch:000000000000000000000009", "bodySourceKey": "source:src/lib.rs", "bodyStart": 17, "bodyEnd": 22, "guarded": false, "guardDecisionId": null, "selectedOrdinal": "11", "notSelectedOrdinal": "10"}
1881 ],
1882 "canonical": "match"
1883 }]);
1884 let manifest =
1885 RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1886 let sources = BTreeMap::from([(
1887 "source:src/lib.rs".into(),
1888 RustCompilerSource {
1889 file: "src/lib.rs".into(),
1890 source: "0123456789\nfirst second trailing".into(),
1891 },
1892 )]);
1893 let normalized = manifest.normalize(&sources).unwrap();
1894 assert_eq!(
1895 normalized.hit_obligations_by_ordinal[&8],
1896 [
1897 "rs:branch-alternative:000000000000000000000008",
1898 "rs:branch-alternative:00000000000000000000000a",
1899 ]
1900 );
1901 assert_eq!(
1902 normalized.hit_obligations_by_ordinal[&11],
1903 [
1904 "rs:branch-alternative:000000000000000000000007",
1905 "rs:branch-alternative:00000000000000000000000b",
1906 ]
1907 );
1908 }
1909
1910 #[test]
1911 fn normalization_fails_closed_on_missing_or_non_utf8_boundary_sources() {
1912 let manifest =
1913 RustCompilerManifest::parse(&serde_json::to_vec(&valid_manifest()).unwrap()).unwrap();
1914 assert!(matches!(
1915 manifest.normalize(&BTreeMap::new()),
1916 Err(RustCompilerManifestError::InvalidSource(_))
1917 ));
1918
1919 let mut candidate = valid_manifest();
1920 candidate["points"][0]["start"] = json!(1);
1921 candidate["points"][0]["end"] = json!(2);
1922 let manifest =
1923 RustCompilerManifest::parse(&serde_json::to_vec(&candidate).unwrap()).unwrap();
1924 let sources = BTreeMap::from([(
1925 "source:src/lib.rs".into(),
1926 RustCompilerSource {
1927 file: "src/lib.rs".into(),
1928 source: "é0123456789\nvalue && more text".into(),
1929 },
1930 )]);
1931 assert!(matches!(
1932 manifest.normalize(&sources),
1933 Err(RustCompilerManifestError::InvalidSource(_))
1934 ));
1935 }
1936}