1use fallow_engine::session::AnalysisSession;
2use fallow_types::duplicates::DuplicationReport;
3use rustc_hash::FxHashSet;
4
5use crate::{
6 DependencyUsageQuery, ProgrammaticAnalysisContext, ProgrammaticError, TraceCloneOptions,
7 TraceCloneProgrammaticOutput, TraceCloneTarget, TraceDependencyOptions,
8 TraceDependencyProgrammaticOutput, TraceErrorOptions, TraceErrorProgrammaticOutput,
9 TraceExportOptions, TraceExportProgrammaticOutput, TraceExportTargetOutput, TraceFileOptions,
10 TraceFileProgrammaticOutput, TraceImportPathOptions, TraceImportPathProgrammaticOutput,
11};
12
13use super::{ProgrammaticResult, duplication, resolve_programmatic_analysis_context};
14use crate::analysis_context::validate_workspace_selection;
15
16struct TraceArtifacts {
17 graph: fallow_engine::module_graph::RetainedModuleGraph,
18 script_used_packages: FxHashSet<String>,
19 trace_provenance: fallow_engine::trace::TraceProvenance,
20 results: fallow_types::results::AnalysisResults,
23}
24
25pub fn run_trace_export(
35 options: &TraceExportOptions,
36) -> ProgrammaticResult<TraceExportProgrammaticOutput> {
37 validate_non_empty("file", &options.file)?;
38 validate_non_empty("export_name", &options.export_name)?;
39 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
40 super::type_aware_config::reject_type_aware_trace(&options.analysis.type_aware, &resolved)?;
41 resolved.install(|| {
42 let session = load_trace_session(&resolved)?;
43 let artifacts = trace_artifacts(&session)?;
44 let output = if let Some(mut export) = fallow_engine::trace::trace_export(
49 &artifacts.graph,
50 session.root(),
51 &options.file,
52 &options.export_name,
53 ) {
54 export.apply_plugin_credits(&artifacts.trace_provenance);
55 TraceExportTargetOutput::Export(export)
56 } else if let Some(member) = fallow_engine::trace::trace_class_member(
57 &artifacts.graph,
58 session.root(),
59 &options.file,
60 &options.export_name,
61 ) {
62 TraceExportTargetOutput::Member(member)
63 } else {
64 return Err(ProgrammaticError::new(
65 format!(
66 "export or member '{}' not found in '{}'",
67 options.export_name, options.file
68 ),
69 2,
70 )
71 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
72 .with_help(
73 "The name is neither a top-level export nor a class / enum / store member of this \
74 file. Run trace_file on the file to list its exports, or project_info for the \
75 project symbol set; confirm the file path is project-relative.",
76 )
77 .with_context("trace_export"));
78 };
79 Ok(TraceExportProgrammaticOutput { output })
80 })
81}
82
83pub fn run_trace_file(
90 options: &TraceFileOptions,
91) -> ProgrammaticResult<TraceFileProgrammaticOutput> {
92 validate_non_empty("file", &options.file)?;
93 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
94 resolved.install(|| {
95 let session = load_trace_session(&resolved)?;
96 let artifacts = trace_artifacts(&session)?;
97 let mut output =
98 fallow_engine::trace::trace_file(&artifacts.graph, session.root(), &options.file)
99 .ok_or_else(|| {
100 ProgrammaticError::new(
101 format!("file '{}' not found in module graph", options.file),
102 2,
103 )
104 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
105 .with_help(
106 "The file is not in the analyzed module graph. Run project_info to list \
107 discovered files; the path must be project-relative and not excluded by \
108 ignore patterns or outside the analyzed roots.",
109 )
110 .with_context("trace_file")
111 })?;
112 output.sources = artifacts.trace_provenance.file_sources(&output.file);
113 output.apply_plugin_credits(&artifacts.trace_provenance);
114 Ok(TraceFileProgrammaticOutput { output })
115 })
116}
117
118pub fn run_trace_import_path(
128 options: &TraceImportPathOptions,
129) -> ProgrammaticResult<TraceImportPathProgrammaticOutput> {
130 validate_non_empty("from", &options.from)?;
131 validate_non_empty("to", &options.to)?;
132 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
133 resolved.install(|| {
134 let session = load_trace_session(&resolved)?;
135 let artifacts = trace_artifacts(&session)?;
136 let trace = if options.eager_only {
137 fallow_engine::trace::trace_eager_import_path
138 } else {
139 fallow_engine::trace::trace_import_path
140 };
141 let output = trace(
142 &artifacts.graph,
143 session.root(),
144 &options.from,
145 &options.to,
146 )
147 .map_err(|endpoint| {
148 let label = endpoint.label();
149 let value = match endpoint {
150 fallow_engine::trace::ImportPathEndpoint::From
151 | fallow_engine::trace::ImportPathEndpoint::AmbiguousFrom => &options.from,
152 fallow_engine::trace::ImportPathEndpoint::To
153 | fallow_engine::trace::ImportPathEndpoint::AmbiguousTo => &options.to,
154 };
155 if endpoint.is_ambiguous() {
156 return ProgrammaticError::new(format!("'{value}' ({label}) matches multiple modules"), 2)
157 .with_code("FALLOW_TRACE_TARGET_AMBIGUOUS")
158 .with_help("Use the full project-relative path; run project_info to list discovered files.")
159 .with_context("trace_import_path");
160 }
161 ProgrammaticError::new(format!("'{value}' ({label}) not found in module graph"), 2)
162 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
163 .with_help(
164 "The module is not in the analyzed module graph. Run project_info to list \
165 discovered files; both paths must be project-relative and not excluded by \
166 ignore patterns or outside the analyzed roots.",
167 )
168 .with_context("trace_import_path")
169 })?;
170 Ok(TraceImportPathProgrammaticOutput { output })
171 })
172}
173
174pub fn run_trace_error(
186 options: &TraceErrorOptions,
187) -> ProgrammaticResult<TraceErrorProgrammaticOutput> {
188 if options.trace.trim().is_empty() {
189 return Err(ProgrammaticError::new("trace must not be empty", 2)
193 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
194 .with_help(
195 "Paste the stack trace text into `trace`, as your runtime printed it. \
196 trace_error resolves frames against the project graph, so it has nothing \
197 to resolve without them.",
198 )
199 .with_context("trace_error"));
200 }
201 if options.trace.len() as u64 > fallow_engine::trace_error::MAX_STACK_TRACE_BYTES {
202 let limit = fallow_engine::trace_error::MAX_STACK_TRACE_BYTES;
203 return Err(ProgrammaticError::new(
204 format!("stack trace exceeds the {limit}-byte limit"),
205 2,
206 )
207 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
208 .with_help(
209 "A stack trace is a handful of kilobytes. Pass the trace itself rather than a \
210 redirected log file, and cut it to the frames that matter.",
211 )
212 .with_context("trace_error"));
213 }
214 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
215 resolved.install(|| {
216 let session = load_trace_session(&resolved)?;
217 let output = fallow_engine::trace_error::trace_error_with_session(
218 &session,
219 &options.trace,
220 options.source.clone(),
221 )
222 .map_err(|err| {
223 ProgrammaticError::new(format!("stack-trace resolution failed: {err}"), 2)
224 .with_code("FALLOW_ANALYSIS_FAILED")
225 .with_context("trace_error")
226 })?;
227 Ok(TraceErrorProgrammaticOutput { output })
228 })
229}
230
231pub fn run_trace_dependency(
238 options: &TraceDependencyOptions,
239) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
240 validate_non_empty("package_name", &options.package_name)?;
241 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
242 resolved.install(|| {
243 let session = load_trace_session(&resolved)?;
244 if let Some(query) = &options.usage {
245 return trace_dependency_usage(&session, &options.package_name, query);
246 }
247 let artifacts = trace_artifacts(&session)?;
248 let workspace_roots: Vec<&std::path::Path> = session
249 .workspaces()
250 .iter()
251 .map(|ws| ws.root.as_path())
252 .collect();
253 let output = fallow_engine::trace::build_dependency_trace(
254 &fallow_engine::trace::DependencyTraceInputs {
255 graph: &artifacts.graph,
256 root: session.root(),
257 workspace_roots: &workspace_roots,
258 ignore_patterns: &session.config().ignore_patterns,
259 script_used_packages: &artifacts.script_used_packages,
260 provenance: &artifacts.trace_provenance,
261 results: &artifacts.results,
262 },
263 &options.package_name,
264 );
265 Ok(TraceDependencyProgrammaticOutput { output })
266 })
267}
268
269fn trace_dependency_usage(
271 session: &AnalysisSession,
272 package_name: &str,
273 query: &DependencyUsageQuery,
274) -> ProgrammaticResult<TraceDependencyProgrammaticOutput> {
275 let result = fallow_engine::trace::trace_dependency_with_session(session, package_name, query)
276 .map_err(|err| {
277 super::dead_code::map_engine_error(
278 &err,
279 "trace analysis failed",
280 "FALLOW_TRACE_FAILED",
281 "trace",
282 )
283 })?;
284 result
285 .map(|output| TraceDependencyProgrammaticOutput { output })
286 .map_err(|err| {
287 ProgrammaticError::new(err.to_string(), 2)
288 .with_code("FALLOW_TRACE_INVALID_CURSOR")
289 .with_context("cursor")
290 })
291}
292
293pub fn run_trace_clone(
300 options: &TraceCloneOptions,
301) -> ProgrammaticResult<TraceCloneProgrammaticOutput> {
302 validate_trace_clone_target(&options.target)?;
303 let resolved = resolve_programmatic_analysis_context(&options.duplication.analysis)?;
304 resolved.install(|| {
305 resolved.ensure_not_cancelled("config load and file discovery")?;
306 let session = duplication::load_duplication_session(&options.duplication, &resolved)?;
307 validate_workspace_selection(&resolved, session.workspaces())?;
308 resolved.ensure_not_cancelled("duplication detection")?;
309 let dupes_config =
310 duplication::build_dupes_config(&options.duplication, &session.config().duplicates);
311 let cache_dir = (!resolved.no_cache).then_some(session.config().cache_dir.as_path());
312 let report = session
313 .find_duplicates_with_defaults(&dupes_config, cache_dir)
314 .report;
315 resolved.ensure_not_cancelled("the clone trace")?;
318 let (trace, not_found) = match &options.target {
319 TraceCloneTarget::Location { file, line } => (
320 fallow_engine::trace::trace_clone(&report, session.root(), file, *line),
321 format!("no clone found at {file}:{line}"),
322 ),
323 TraceCloneTarget::Fingerprint(fingerprint) => (
324 fallow_engine::trace::trace_clone_by_fingerprint(
325 &report,
326 session.root(),
327 fingerprint,
328 ),
329 format!("no clone group with fingerprint {fingerprint}"),
330 ),
331 };
332 if trace.matched_instance.is_none() {
333 return Err(ProgrammaticError::new(not_found, 2)
334 .with_code("FALLOW_TRACE_TARGET_NOT_FOUND")
335 .with_help(
336 "No clone matched. Run find_dupes to list clone groups and their fingerprints; \
337 a location must fall inside a reported clone instance, and a fingerprint must \
338 be a find_dupes clone_groups[].fingerprint (a dup:<id> value).",
339 )
340 .with_context("trace_clone"));
341 }
342 Ok(TraceCloneProgrammaticOutput { output: trace })
343 })
344}
345
346#[doc(hidden)]
354#[allow(
355 clippy::implicit_hasher,
356 reason = "the engine trace boundary intentionally accepts the workspace-standard FxHashSet"
357)]
358pub fn benchmark_trace_graph_family_compact_json(
359 graph: &fallow_engine::module_graph::RetainedModuleGraph,
360 root: &std::path::Path,
361 script_used_packages: &FxHashSet<String>,
362) -> ProgrammaticResult<(usize, usize, usize, usize, usize)> {
363 let export =
364 fallow_engine::trace::trace_export(graph, root, "src/000-shared.ts", "sharedValue")
365 .ok_or_else(|| benchmark_trace_target_missing("src/000-shared.ts:sharedValue"))?;
366 let export_reference_count = export.direct_references.len();
367 let export_json =
368 crate::serialize_trace_export_programmatic_json(TraceExportProgrammaticOutput {
369 output: TraceExportTargetOutput::Export(export),
370 })?;
371
372 let file = fallow_engine::trace::trace_file(graph, root, "src/000-shared.ts")
373 .ok_or_else(|| benchmark_trace_target_missing("src/000-shared.ts"))?;
374 let file_export_count = file.exports.len();
375 let file_imported_by_count = file.imported_by.len();
376 let file_json = crate::serialize_trace_file_programmatic_json(TraceFileProgrammaticOutput {
377 output: file,
378 })?;
379
380 let dependency = fallow_engine::trace::trace_dependency(
381 graph,
382 root,
383 &[],
384 &fallow_config::IgnorePatternSet::empty(),
385 "trace-package",
386 script_used_packages,
387 );
388 let dependency_import_count = dependency.import_count;
389 let dependency_json =
390 crate::serialize_trace_dependency_programmatic_json(TraceDependencyProgrammaticOutput {
391 output: dependency,
392 })?;
393
394 let rendered_bytes = compact_json_len(&[export_json, file_json, dependency_json])?;
395 Ok((
396 export_reference_count,
397 file_export_count,
398 file_imported_by_count,
399 dependency_import_count,
400 rendered_bytes,
401 ))
402}
403
404#[doc(hidden)]
406#[derive(Debug, PartialEq, Eq)]
407pub struct TraceCloneBenchmarkResult {
408 pub location_file: std::path::PathBuf,
410 pub location_line: usize,
412 pub location_fingerprint: String,
414 pub fingerprint_fingerprint: String,
416 pub location_group_count: usize,
418 pub fingerprint_group_count: usize,
420 pub location_instance_count: usize,
422 pub fingerprint_instance_count: usize,
424 pub rendered_bytes: usize,
426}
427
428#[doc(hidden)]
435pub fn benchmark_trace_clone_compact_json(
436 report: &DuplicationReport,
437 root: &std::path::Path,
438 file: &str,
439 line: usize,
440 fingerprint: &str,
441) -> ProgrammaticResult<TraceCloneBenchmarkResult> {
442 let location_trace = fallow_engine::trace::trace_clone(report, root, file, line);
443 let matched_location = location_trace
444 .matched_instance
445 .as_ref()
446 .ok_or_else(|| benchmark_trace_target_missing(&format!("{file}:{line}")))?;
447 let location_file = matched_location.file.clone();
448 let location_line = matched_location.start_line;
449 let location_fingerprint = location_trace
450 .clone_groups
451 .first()
452 .map(|group| group.fingerprint.clone())
453 .ok_or_else(|| benchmark_trace_target_missing(&format!("{file}:{line}")))?;
454 let location_group_count = location_trace.clone_groups.len();
455 let location_instance_count = location_trace
456 .clone_groups
457 .iter()
458 .map(|group| group.instances.len())
459 .sum();
460 let location_json =
461 crate::serialize_trace_clone_programmatic_json(TraceCloneProgrammaticOutput {
462 output: location_trace,
463 })?;
464
465 let fingerprint_trace =
466 fallow_engine::trace::trace_clone_by_fingerprint(report, root, fingerprint);
467 if fingerprint_trace.matched_instance.is_none() {
468 return Err(benchmark_trace_target_missing(fingerprint));
469 }
470 let fingerprint_fingerprint = fingerprint_trace
471 .clone_groups
472 .first()
473 .map(|group| group.fingerprint.clone())
474 .ok_or_else(|| benchmark_trace_target_missing(fingerprint))?;
475 let fingerprint_group_count = fingerprint_trace.clone_groups.len();
476 let fingerprint_instance_count = fingerprint_trace
477 .clone_groups
478 .iter()
479 .map(|group| group.instances.len())
480 .sum();
481 let fingerprint_json =
482 crate::serialize_trace_clone_programmatic_json(TraceCloneProgrammaticOutput {
483 output: fingerprint_trace,
484 })?;
485
486 let rendered_bytes = compact_json_len(&[location_json, fingerprint_json])?;
487 Ok(TraceCloneBenchmarkResult {
488 location_file,
489 location_line,
490 location_fingerprint,
491 fingerprint_fingerprint,
492 location_group_count,
493 fingerprint_group_count,
494 location_instance_count,
495 fingerprint_instance_count,
496 rendered_bytes,
497 })
498}
499
500fn compact_json_len(values: &[serde_json::Value]) -> ProgrammaticResult<usize> {
501 serde_json::to_vec(values)
502 .map(|json| json.len())
503 .map_err(|err| {
504 ProgrammaticError::new(
505 format!("failed to serialize benchmark trace JSON: {err}"),
506 2,
507 )
508 .with_code("FALLOW_SERIALIZE_BENCHMARK_TRACE")
509 .with_context("benchmark_trace")
510 })
511}
512
513fn benchmark_trace_target_missing(target: &str) -> ProgrammaticError {
514 ProgrammaticError::new(format!("benchmark trace target not found: {target}"), 2)
515 .with_code("FALLOW_BENCHMARK_TRACE_TARGET_NOT_FOUND")
516 .with_context("benchmark_trace")
517}
518
519fn validate_non_empty(field: &str, value: &str) -> ProgrammaticResult<()> {
520 if value.trim().is_empty() {
521 return Err(
522 ProgrammaticError::new(format!("{field} must not be empty"), 2)
523 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
524 .with_context(field.to_string()),
525 );
526 }
527 Ok(())
528}
529
530fn validate_trace_clone_target(target: &TraceCloneTarget) -> ProgrammaticResult<()> {
531 match target {
532 TraceCloneTarget::Location { file, line } => {
533 validate_non_empty("file", file)?;
534 if *line == 0 {
535 return Err(ProgrammaticError::new("line must be greater than 0", 2)
536 .with_code("FALLOW_INVALID_TRACE_OPTIONS")
537 .with_context("trace_clone.line"));
538 }
539 }
540 TraceCloneTarget::Fingerprint(fingerprint) => {
541 validate_non_empty("fingerprint", fingerprint)?;
542 }
543 }
544 Ok(())
545}
546
547fn load_trace_session(
548 resolved: &ProgrammaticAnalysisContext,
549) -> ProgrammaticResult<AnalysisSession> {
550 let session = super::dead_code::load_dead_code_session(
551 &super::dead_code::default_dead_code_options_for_context(resolved),
552 resolved,
553 )?;
554 validate_workspace_selection(resolved, session.workspaces())?;
555 Ok(session)
556}
557
558fn trace_artifacts(session: &AnalysisSession) -> ProgrammaticResult<TraceArtifacts> {
559 let artifacts = session
560 .analyze_dead_code_with_session_artifacts(false, true, None)
561 .map_err(|err| {
562 super::dead_code::map_engine_error(
563 &err,
564 "trace analysis failed",
565 "FALLOW_TRACE_FAILED",
566 "trace",
567 )
568 })?;
569 let graph = artifacts.analysis.graph.ok_or_else(|| {
570 ProgrammaticError::new("trace requires a retained module graph", 2)
571 .with_code("FALLOW_TRACE_GRAPH_UNAVAILABLE")
572 .with_context("trace.graph")
573 })?;
574 Ok(TraceArtifacts {
575 graph,
576 script_used_packages: artifacts.analysis.script_used_packages,
577 trace_provenance: artifacts.analysis.trace_provenance,
578 results: artifacts.analysis.results,
579 })
580}
581
582#[cfg(test)]
583mod benchmark_tests {
584 use std::fmt::Write as _;
585 use std::fs;
586
587 use fallow_engine::duplicates::CloneFingerprintSet;
588
589 use super::*;
590
591 const FIXTURE_SIZE: usize = 4;
592
593 fn write_file(root: &std::path::Path, path: &str, source: impl AsRef<str>) {
594 let path = root.join(path);
595 fs::create_dir_all(path.parent().expect("fixture file has parent"))
596 .expect("fixture directory is created");
597 fs::write(path, source.as_ref()).expect("fixture file is written");
598 }
599
600 #[test]
601 fn graph_family_benchmark_boundary_uses_only_retained_artifacts() {
602 let temp_dir = tempfile::TempDir::new().expect("temporary project is created");
603 let root = temp_dir.path().to_path_buf();
604 write_file(
605 &root,
606 "package.json",
607 r#"{"name":"trace-test","type":"module","main":"src/index.ts"}"#,
608 );
609 write_file(
610 &root,
611 "src/000-shared.ts",
612 "export const sharedValue = 42;\n",
613 );
614
615 let mut index_source = String::new();
616 for index in 0..FIXTURE_SIZE {
617 write_file(
618 &root,
619 &format!("src/consumer{index}.ts"),
620 format!(
621 "import {{ sharedValue }} from './000-shared';\nimport {{ traceHelper }} from 'trace-package';\nexport const value{index} = traceHelper(sharedValue + {index});\n"
622 ),
623 );
624 writeln!(
625 index_source,
626 "import {{ value{index} }} from './consumer{index}';\nconsole.log(value{index});"
627 )
628 .expect("index source is built");
629 }
630 write_file(&root, "src/index.ts", index_source);
631
632 let session = AnalysisSession::load(&root, None).expect("trace session loads");
633 let target = session
634 .files()
635 .iter()
636 .find(|file| file.path.ends_with("src/000-shared.ts"))
637 .expect("trace target is discovered");
638 assert_eq!(
639 target.id.0, 0,
640 "the retained trace target must precede every non-matching importer"
641 );
642 let trace_root = session.root().to_path_buf();
643 let artifacts = session
644 .analyze_dead_code_with_artifacts(false, true)
645 .expect("trace graph analysis succeeds");
646 drop(session);
647 temp_dir.close().expect("temporary project is removed");
648
649 let result = benchmark_trace_graph_family_compact_json(
650 artifacts.graph.as_ref().expect("trace graph is retained"),
651 &trace_root,
652 &artifacts.script_used_packages,
653 )
654 .expect("retained trace graph serializes without project IO");
655 assert_eq!(result.0, FIXTURE_SIZE);
656 assert_eq!(result.1, 1);
657 assert_eq!(result.2, FIXTURE_SIZE);
658 assert_eq!(result.3, FIXTURE_SIZE);
659 assert!(result.4 > 0);
660 }
661
662 #[test]
663 fn clone_benchmark_boundary_uses_only_the_retained_report() {
664 let temp_dir = tempfile::TempDir::new().expect("temporary project is created");
665 let root = temp_dir.path().to_path_buf();
666 write_file(
667 &root,
668 "package.json",
669 r#"{"name":"trace-clone-test","type":"module"}"#,
670 );
671 for index in 0..FIXTURE_SIZE {
672 write_file(
673 &root,
674 &format!("src/clone{index}.ts"),
675 format!(
676 "export function normalizeRecords(records: Array<{{ active: boolean; value: number }}>) {{\n const active = records.filter((record) => record.active);\n const values = active.map((record) => record.value);\n const total = values.reduce((sum, value) => sum + value, 0);\n const average = values.length === 0 ? 0 : total / values.length;\n const maximum = values.reduce((current, value) => Math.max(current, value), 0);\n return {{ total, average, maximum, count: values.length }};\n}}\n\nexport const cloneId = {index};\n"
677 ),
678 );
679 }
680
681 let session = AnalysisSession::load(&root, None).expect("clone session loads");
682 let trace_root = session.root().to_path_buf();
683 let mut config = session.config().duplicates.clone();
684 config.min_tokens = 35;
685 config.min_lines = 5;
686 config.min_occurrences = FIXTURE_SIZE;
687 let report = session.find_duplicates_with_defaults(&config, None).report;
688 let group = report
689 .clone_groups
690 .iter()
691 .max_by_key(|group| group.instances.len())
692 .expect("clone group exists");
693 let target = group.instances.last().expect("clone instance exists");
694 let target_file = target
695 .file
696 .strip_prefix(&trace_root)
697 .expect("clone path is project-relative")
698 .to_string_lossy()
699 .replace('\\', "/");
700 let target_line = target.start_line;
701 let expected_fingerprint =
702 CloneFingerprintSet::from_groups(&report.clone_groups).fingerprint_for_group(group);
703 drop(session);
704 temp_dir.close().expect("temporary project is removed");
705
706 let result = benchmark_trace_clone_compact_json(
707 &report,
708 &trace_root,
709 &target_file,
710 target_line,
711 &expected_fingerprint,
712 )
713 .expect("retained clone report serializes without project IO");
714 assert_eq!(result.location_file, std::path::PathBuf::from(&target_file));
715 assert_eq!(result.location_line, target_line);
716 assert_eq!(result.location_fingerprint, expected_fingerprint);
717 assert_eq!(result.fingerprint_fingerprint, expected_fingerprint);
718 assert_eq!(result.location_group_count, 1);
719 assert_eq!(result.fingerprint_group_count, 1);
720 assert_eq!(result.location_instance_count, FIXTURE_SIZE);
721 assert_eq!(result.fingerprint_instance_count, FIXTURE_SIZE);
722 assert!(result.rendered_bytes > 0);
723 }
724}