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