1use serde::Serialize;
10
11use crate::analyzer::stats::Summary;
12
13#[derive(Debug, Clone, Default)]
17pub struct CodeMetrics {
18 pub code_lines: usize,
19 pub cyclomatic_complexity: usize,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct CostConfig {
25 pub average_wage: f64,
27 pub overhead: f64,
29}
30
31impl Default for CostConfig {
32 fn default() -> Self {
33 Self {
34 average_wage: 56_286.0,
35 overhead: 2.4,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
42pub enum ProjectType {
43 #[default]
44 Organic,
45 SemiDetached,
46 Embedded,
47}
48
49impl std::fmt::Display for ProjectType {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 Self::Organic => write!(f, "organic"),
53 Self::SemiDetached => write!(f, "semi-detached"),
54 Self::Embedded => write!(f, "embedded"),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize)]
63pub struct LanguageEstimation {
64 pub language: String,
65 pub code_lines: usize,
66 pub effort_months: f64,
67 pub cost: f64,
68}
69
70#[derive(Debug, Clone, Serialize)]
72pub struct EstimationReport {
73 pub model: String,
74 pub total_sloc: usize,
75 pub effort_months: f64,
76 pub schedule_months: f64,
77 pub people_required: f64,
78 pub estimated_cost: f64,
79 pub by_language: Vec<LanguageEstimation>,
80 pub params: Vec<(String, String)>,
82}
83
84pub trait EstimationModel: Send + Sync {
88 fn name(&self) -> &str;
90
91 fn display_params(&self) -> Vec<(String, String)>;
93
94 fn estimate_effort(&self, metrics: &CodeMetrics) -> f64;
96
97 fn estimate_schedule(&self, effort_months: f64, metrics: &CodeMetrics) -> f64;
99
100 fn estimate_cost(
102 &self,
103 effort_months: f64,
104 metrics: &CodeMetrics,
105 cost_config: &CostConfig,
106 ) -> f64 {
107 let _ = metrics;
108 effort_months * (cost_config.average_wage / 12.0) * cost_config.overhead
109 }
110
111 fn estimate_people(&self, effort_months: f64, schedule_months: f64) -> f64 {
113 if schedule_months > 0.0 {
114 effort_months / schedule_months
115 } else {
116 0.0
117 }
118 }
119}
120
121pub fn estimate(
125 summary: &Summary,
126 model: &dyn EstimationModel,
127 cost_config: &CostConfig,
128) -> EstimationReport {
129 let global_metrics = CodeMetrics {
130 code_lines: summary.lines.code,
131 cyclomatic_complexity: summary.complexity.cyclomatic,
132 };
133
134 let mut by_language: Vec<LanguageEstimation> = summary
136 .by_language
137 .iter()
138 .filter(|(_, ls)| ls.lines.code > 0)
139 .map(|(lang, ls)| {
140 let metrics = CodeMetrics {
141 code_lines: ls.lines.code,
142 cyclomatic_complexity: ls.complexity.cyclomatic,
143 };
144 let effort = model.estimate_effort(&metrics);
145 let cost = model.estimate_cost(effort, &metrics, cost_config);
146 LanguageEstimation {
147 language: lang.clone(),
148 code_lines: ls.lines.code,
149 effort_months: effort,
150 cost,
151 }
152 })
153 .collect();
154
155 by_language.sort_by(|a, b| {
156 b.cost
157 .partial_cmp(&a.cost)
158 .unwrap_or(std::cmp::Ordering::Equal)
159 });
160
161 let effort_months = model.estimate_effort(&global_metrics);
163 let schedule_months = model.estimate_schedule(effort_months, &global_metrics);
164 let people_required = model.estimate_people(effort_months, schedule_months);
165 let estimated_cost = model.estimate_cost(effort_months, &global_metrics, cost_config);
166
167 EstimationReport {
168 model: model.name().to_string(),
169 total_sloc: summary.lines.code,
170 effort_months,
171 schedule_months,
172 people_required,
173 estimated_cost,
174 by_language,
175 params: model.display_params(),
176 }
177}
178
179#[derive(Debug, Clone, Serialize)]
181pub struct EstimationComparison {
182 pub total_sloc: usize,
183 pub reports: Vec<EstimationReport>,
184}
185
186pub fn estimate_all(
188 summary: &Summary,
189 models: &[&dyn EstimationModel],
190 cost_config: &CostConfig,
191) -> EstimationComparison {
192 let reports = models
193 .iter()
194 .map(|m| estimate(summary, *m, cost_config))
195 .collect();
196 EstimationComparison {
197 total_sloc: summary.lines.code,
198 reports,
199 }
200}
201
202pub struct CocomoBasicModel {
213 pub project_type: ProjectType,
214 pub eaf: f64,
215}
216
217impl Default for CocomoBasicModel {
218 fn default() -> Self {
219 Self {
220 project_type: ProjectType::Organic,
221 eaf: 1.0,
222 }
223 }
224}
225
226fn cocomo_basic_coefficients(pt: ProjectType) -> [f64; 4] {
227 match pt {
228 ProjectType::Organic => [2.4, 1.05, 2.5, 0.38],
229 ProjectType::SemiDetached => [3.0, 1.12, 2.5, 0.35],
230 ProjectType::Embedded => [3.6, 1.20, 2.5, 0.32],
231 }
232}
233
234impl EstimationModel for CocomoBasicModel {
235 fn name(&self) -> &str {
236 "COCOMO Basic"
237 }
238
239 fn display_params(&self) -> Vec<(String, String)> {
240 vec![
241 ("Project Type".into(), self.project_type.to_string()),
242 ("EAF".into(), format!("{:.2}", self.eaf)),
243 ]
244 }
245
246 fn estimate_effort(&self, metrics: &CodeMetrics) -> f64 {
247 let [a, b, _, _] = cocomo_basic_coefficients(self.project_type);
248 a * (metrics.code_lines as f64 / 1000.0).powf(b) * self.eaf
249 }
250
251 fn estimate_schedule(&self, effort_months: f64, _metrics: &CodeMetrics) -> f64 {
252 let [_, _, c, d] = cocomo_basic_coefficients(self.project_type);
253 c * effort_months.powf(d)
254 }
255}
256
257pub const COCOMO2_SF_NOMINAL: [f64; 5] = [3.72, 3.04, 4.24, 3.29, 4.68];
263
264pub struct CocomoIIModel {
272 pub a: f64,
274 pub b: f64,
276 pub c: f64,
278 pub d: f64,
280 pub scale_factors: [f64; 5],
282 pub eaf: f64,
284}
285
286impl Default for CocomoIIModel {
287 fn default() -> Self {
288 Self {
289 a: 2.94,
290 b: 0.91,
291 c: 3.67,
292 d: 0.28,
293 scale_factors: COCOMO2_SF_NOMINAL,
294 eaf: 1.0,
295 }
296 }
297}
298
299impl CocomoIIModel {
300 fn exponent_e(&self) -> f64 {
301 self.b + 0.01 * self.scale_factors.iter().sum::<f64>()
302 }
303
304 fn exponent_f(&self) -> f64 {
305 self.d + 0.2 * (self.exponent_e() - self.b)
306 }
307}
308
309impl EstimationModel for CocomoIIModel {
310 fn name(&self) -> &str {
311 "COCOMO II"
312 }
313
314 fn display_params(&self) -> Vec<(String, String)> {
315 let sf_sum: f64 = self.scale_factors.iter().sum();
316 vec![
317 ("A".into(), format!("{:.2}", self.a)),
318 ("E (exponent)".into(), format!("{:.4}", self.exponent_e())),
319 ("SF sum".into(), format!("{:.2}", sf_sum)),
320 ("EAF".into(), format!("{:.2}", self.eaf)),
321 ]
322 }
323
324 fn estimate_effort(&self, metrics: &CodeMetrics) -> f64 {
325 let e = self.exponent_e();
326 self.a * self.eaf * (metrics.code_lines as f64 / 1000.0).powf(e)
327 }
328
329 fn estimate_schedule(&self, effort_months: f64, _metrics: &CodeMetrics) -> f64 {
330 let f = self.exponent_f();
331 self.c * effort_months.powf(f)
332 }
333}
334
335pub struct PutnamModel {
346 pub ck: f64,
349 pub d0: f64,
352}
353
354impl Default for PutnamModel {
355 fn default() -> Self {
356 Self {
357 ck: 8000.0,
358 d0: 15.0,
359 }
360 }
361}
362
363impl EstimationModel for PutnamModel {
364 fn name(&self) -> &str {
365 "Putnam (SLIM)"
366 }
367
368 fn display_params(&self) -> Vec<(String, String)> {
369 vec![
370 ("Ck (productivity)".into(), format!("{:.0}", self.ck)),
371 ("D0 (buildup index)".into(), format!("{:.1}", self.d0)),
372 ]
373 }
374
375 fn estimate_effort(&self, metrics: &CodeMetrics) -> f64 {
376 if metrics.code_lines == 0 {
377 return 0.0;
378 }
379 let size = metrics.code_lines as f64;
380 let t_years = (size / (self.ck * self.d0.powf(1.0 / 3.0))).powf(3.0 / 7.0);
381 let effort_person_years = self.d0 * t_years.powi(3);
382 effort_person_years * 12.0
383 }
384
385 fn estimate_schedule(&self, _effort_months: f64, metrics: &CodeMetrics) -> f64 {
386 if metrics.code_lines == 0 {
387 return 0.0;
388 }
389 let size = metrics.code_lines as f64;
390 let t_years = (size / (self.ck * self.d0.powf(1.0 / 3.0))).powf(3.0 / 7.0);
391 t_years * 12.0
392 }
393}
394
395pub struct LocomoModel {
411 pub tokens_per_line: f64,
412 pub input_per_line: f64,
413 pub complexity_weight: f64,
414 pub base_iterations: f64,
415 pub iteration_weight: f64,
416 pub input_price_per_m: f64,
417 pub output_price_per_m: f64,
418 pub tokens_per_second: f64,
419 pub minutes_per_line: f64,
421}
422
423impl Default for LocomoModel {
424 fn default() -> Self {
425 Self {
426 tokens_per_line: 10.0,
427 input_per_line: 20.0,
428 complexity_weight: 5.0,
429 base_iterations: 1.5,
430 iteration_weight: 2.0,
431 input_price_per_m: 3.0,
432 output_price_per_m: 15.0,
433 tokens_per_second: 50.0,
434 minutes_per_line: 0.1,
435 }
436 }
437}
438
439impl LocomoModel {
440 fn density(metrics: &CodeMetrics) -> f64 {
441 if metrics.code_lines == 0 {
442 return 0.0;
443 }
444 metrics.cyclomatic_complexity as f64 / metrics.code_lines as f64
445 }
446
447 fn token_counts(&self, metrics: &CodeMetrics) -> (f64, f64) {
448 let d = Self::density(metrics);
449 let c_factor = 1.0 + d.sqrt() * self.complexity_weight;
450 let i_factor = self.base_iterations + d.sqrt() * self.iteration_weight;
451 let output_tokens = metrics.code_lines as f64 * self.tokens_per_line * i_factor;
452 let input_tokens = metrics.code_lines as f64 * self.input_per_line * c_factor * i_factor;
453 (input_tokens, output_tokens)
454 }
455}
456
457impl EstimationModel for LocomoModel {
458 fn name(&self) -> &str {
459 "LOCOMO"
460 }
461
462 fn display_params(&self) -> Vec<(String, String)> {
463 vec![
464 (
465 "LLM Pricing".into(),
466 format!(
467 "In ${:.2}/Out ${:.2} per 1M tokens",
468 self.input_price_per_m, self.output_price_per_m,
469 ),
470 ),
471 ("TPS".into(), format!("{:.0}", self.tokens_per_second)),
472 (
473 "Review".into(),
474 format!("{:.2} min/line", self.minutes_per_line),
475 ),
476 ]
477 }
478
479 fn estimate_effort(&self, metrics: &CodeMetrics) -> f64 {
481 let review_hours = metrics.code_lines as f64 * self.minutes_per_line / 60.0;
482 review_hours / 160.0
483 }
484
485 fn estimate_schedule(&self, _effort_months: f64, metrics: &CodeMetrics) -> f64 {
487 let (_, output_tokens) = self.token_counts(metrics);
488 let generation_seconds = output_tokens / self.tokens_per_second;
489 generation_seconds / (730.0 * 3600.0)
491 }
492
493 fn estimate_cost(
495 &self,
496 _effort_months: f64,
497 metrics: &CodeMetrics,
498 _cost_config: &CostConfig,
499 ) -> f64 {
500 let (input_tokens, output_tokens) = self.token_counts(metrics);
501 (input_tokens / 1_000_000.0) * self.input_price_per_m
502 + (output_tokens / 1_000_000.0) * self.output_price_per_m
503 }
504}
505
506#[cfg(test)]
511mod tests {
512 use super::*;
513 use crate::analyzer::stats::{LanguageSummary, LineStats};
514
515 fn make_summary(code: usize, complexity: usize) -> Summary {
516 let mut summary = Summary::default();
517 summary.lines.code = code;
518 summary.complexity.cyclomatic = complexity;
519 summary.by_language.insert(
520 "Rust".to_string(),
521 LanguageSummary {
522 files: 10,
523 lines: LineStats {
524 total: code,
525 code,
526 comment: 0,
527 blank: 0,
528 },
529 size: 0,
530 complexity: crate::analyzer::stats::Complexity {
531 cyclomatic: complexity,
532 functions: 0,
533 max_depth: 0,
534 avg_func_lines: 0.0,
535 },
536 },
537 );
538 summary
539 }
540
541 fn make_multi_lang_summary() -> Summary {
542 let mut summary = Summary::default();
543 summary.lines.code = 12_000;
544 summary.complexity.cyclomatic = 500;
545 summary.by_language.insert(
546 "Rust".to_string(),
547 LanguageSummary {
548 files: 20,
549 lines: LineStats {
550 total: 10_000,
551 code: 10_000,
552 comment: 0,
553 blank: 0,
554 },
555 size: 0,
556 complexity: crate::analyzer::stats::Complexity {
557 cyclomatic: 400,
558 functions: 0,
559 max_depth: 0,
560 avg_func_lines: 0.0,
561 },
562 },
563 );
564 summary.by_language.insert(
565 "Python".to_string(),
566 LanguageSummary {
567 files: 5,
568 lines: LineStats {
569 total: 2_000,
570 code: 2_000,
571 comment: 0,
572 blank: 0,
573 },
574 size: 0,
575 complexity: crate::analyzer::stats::Complexity {
576 cyclomatic: 100,
577 functions: 0,
578 max_depth: 0,
579 avg_func_lines: 0.0,
580 },
581 },
582 );
583 summary
584 }
585
586 #[test]
589 fn test_cocomo_basic_effort_10k() {
590 let m = CocomoBasicModel::default();
591 let metrics = CodeMetrics {
592 code_lines: 10_000,
593 cyclomatic_complexity: 0,
594 };
595 let effort = m.estimate_effort(&metrics);
597 assert!((effort - 26.93).abs() < 0.1, "got {effort}");
598 }
599
600 #[test]
601 fn test_cocomo_basic_schedule() {
602 let m = CocomoBasicModel::default();
603 let metrics = CodeMetrics::default();
604 let sched = m.estimate_schedule(26.93, &metrics);
606 assert!((sched - 8.74).abs() < 0.2, "got {sched}");
607 }
608
609 #[test]
610 fn test_cocomo_basic_cost() {
611 let m = CocomoBasicModel::default();
612 let cost_config = CostConfig::default();
613 let metrics = CodeMetrics::default();
614 let cost = m.estimate_cost(26.93, &metrics, &cost_config);
616 assert!((cost - 303_222.0).abs() < 500.0, "got {cost}");
617 }
618
619 #[test]
620 fn test_cocomo_basic_zero_sloc() {
621 let m = CocomoBasicModel::default();
622 let metrics = CodeMetrics {
623 code_lines: 0,
624 cyclomatic_complexity: 0,
625 };
626 assert!(m.estimate_effort(&metrics).abs() < f64::EPSILON);
627 }
628
629 #[test]
630 fn test_cocomo_basic_eaf_doubles_effort() {
631 let m1 = CocomoBasicModel::default();
632 let m2 = CocomoBasicModel {
633 eaf: 2.0,
634 ..Default::default()
635 };
636 let metrics = CodeMetrics {
637 code_lines: 10_000,
638 cyclomatic_complexity: 0,
639 };
640 let e1 = m1.estimate_effort(&metrics);
641 let e2 = m2.estimate_effort(&metrics);
642 assert!((e2 - e1 * 2.0).abs() < 0.01);
643 }
644
645 #[test]
646 fn test_cocomo_basic_embedded_higher() {
647 let org = CocomoBasicModel::default();
648 let emb = CocomoBasicModel {
649 project_type: ProjectType::Embedded,
650 ..Default::default()
651 };
652 let metrics = CodeMetrics {
653 code_lines: 10_000,
654 cyclomatic_complexity: 0,
655 };
656 assert!(emb.estimate_effort(&metrics) > org.estimate_effort(&metrics));
657 }
658
659 #[test]
662 fn test_cocomo2_exponent_nominal() {
663 let m = CocomoIIModel::default();
664 assert!((m.exponent_e() - 1.0997).abs() < 0.001);
666 }
667
668 #[test]
669 fn test_cocomo2_effort_10k() {
670 let m = CocomoIIModel::default();
671 let metrics = CodeMetrics {
672 code_lines: 10_000,
673 cyclomatic_complexity: 0,
674 };
675 let effort = m.estimate_effort(&metrics);
677 assert!((effort - 37.01).abs() < 0.5, "got {effort}");
678 }
679
680 #[test]
681 fn test_cocomo2_schedule() {
682 let m = CocomoIIModel::default();
683 let metrics = CodeMetrics::default();
684 let sched = m.estimate_schedule(37.01, &metrics);
687 assert!((sched - 11.6).abs() < 0.5, "got {sched}");
688 }
689
690 #[test]
691 fn test_cocomo2_higher_than_basic() {
692 let basic = CocomoBasicModel::default();
693 let cocomo2 = CocomoIIModel::default();
694 let metrics = CodeMetrics {
695 code_lines: 10_000,
696 cyclomatic_complexity: 0,
697 };
698 assert!(cocomo2.estimate_effort(&metrics) > basic.estimate_effort(&metrics));
699 }
700
701 #[test]
704 fn test_putnam_effort_10k() {
705 let m = PutnamModel::default();
706 let metrics = CodeMetrics {
707 code_lines: 10_000,
708 cyclomatic_complexity: 0,
709 };
710 let effort = m.estimate_effort(&metrics);
711 assert!((effort - 75.1).abs() < 1.0, "got {effort}");
712 }
713
714 #[test]
715 fn test_putnam_schedule_10k() {
716 let m = PutnamModel::default();
717 let metrics = CodeMetrics {
718 code_lines: 10_000,
719 cyclomatic_complexity: 0,
720 };
721 let sched = m.estimate_schedule(0.0, &metrics);
722 assert!((sched - 8.82).abs() < 0.2, "got {sched}");
723 }
724
725 #[test]
726 fn test_putnam_zero_sloc() {
727 let m = PutnamModel::default();
728 let metrics = CodeMetrics {
729 code_lines: 0,
730 cyclomatic_complexity: 0,
731 };
732 assert!(m.estimate_effort(&metrics).abs() < f64::EPSILON);
733 assert!(m.estimate_schedule(0.0, &metrics).abs() < f64::EPSILON);
734 }
735
736 #[test]
737 fn test_putnam_higher_ck_less_effort() {
738 let good = PutnamModel {
739 ck: 8000.0,
740 d0: 15.0,
741 };
742 let excellent = PutnamModel {
743 ck: 11000.0,
744 d0: 15.0,
745 };
746 let metrics = CodeMetrics {
747 code_lines: 10_000,
748 cyclomatic_complexity: 0,
749 };
750 assert!(excellent.estimate_effort(&metrics) < good.estimate_effort(&metrics));
751 }
752
753 #[test]
756 fn test_locomo_density() {
757 let m = CodeMetrics {
758 code_lines: 1000,
759 cyclomatic_complexity: 100,
760 };
761 assert!((LocomoModel::density(&m) - 0.1).abs() < f64::EPSILON);
762 }
763
764 #[test]
765 fn test_locomo_cost_medium_preset() {
766 let m = LocomoModel::default();
767 let metrics = CodeMetrics {
768 code_lines: 1000,
769 cyclomatic_complexity: 100,
770 };
771 let cost_config = CostConfig::default();
772 let cost = m.estimate_cost(0.0, &metrics, &cost_config);
773 assert!(cost > 0.0 && cost < 5.0, "got {cost}");
774 }
775
776 #[test]
777 fn test_locomo_effort_is_review_hours() {
778 let m = LocomoModel::default();
779 let metrics = CodeMetrics {
780 code_lines: 10_000,
781 cyclomatic_complexity: 0,
782 };
783 let effort = m.estimate_effort(&metrics);
785 assert!((effort - 0.104).abs() < 0.01, "got {effort}");
786 }
787
788 #[test]
789 fn test_locomo_zero_complexity() {
790 let m = LocomoModel::default();
791 let metrics = CodeMetrics {
792 code_lines: 1000,
793 cyclomatic_complexity: 0,
794 };
795 let cost_config = CostConfig::default();
796 let cost = m.estimate_cost(0.0, &metrics, &cost_config);
797 assert!(cost > 0.0, "got {cost}");
798 }
799
800 #[test]
803 fn test_estimate_report_fields() {
804 let model = CocomoBasicModel::default();
805 let cost_config = CostConfig::default();
806 let summary = make_summary(10_000, 200);
807 let report = estimate(&summary, &model, &cost_config);
808
809 assert_eq!(report.model, "COCOMO Basic");
810 assert_eq!(report.total_sloc, 10_000);
811 assert!(report.effort_months > 0.0);
812 assert!(report.schedule_months > 0.0);
813 assert!(report.people_required > 0.0);
814 assert!(report.estimated_cost > 0.0);
815 assert_eq!(report.by_language.len(), 1);
816 }
817
818 #[test]
819 fn test_estimate_multi_language_sorted() {
820 let model = CocomoBasicModel::default();
821 let cost_config = CostConfig::default();
822 let summary = make_multi_lang_summary();
823 let report = estimate(&summary, &model, &cost_config);
824
825 assert_eq!(report.by_language.len(), 2);
826 assert_eq!(report.by_language[0].language, "Rust");
827 assert!(report.by_language[0].cost > report.by_language[1].cost);
828 }
829
830 #[test]
831 fn test_estimate_empty_summary() {
832 let model = CocomoBasicModel::default();
833 let cost_config = CostConfig::default();
834 let summary = Summary::default();
835 let report = estimate(&summary, &model, &cost_config);
836
837 assert_eq!(report.total_sloc, 0);
838 assert!(report.by_language.is_empty());
839 }
840
841 #[test]
842 fn test_estimate_nonlinear() {
843 let model = CocomoBasicModel::default();
844 let cost_config = CostConfig::default();
845 let summary = make_multi_lang_summary();
846 let report = estimate(&summary, &model, &cost_config);
847
848 let sum_of_parts: f64 = report.by_language.iter().map(|l| l.effort_months).sum();
849 assert!(
850 (report.effort_months - sum_of_parts).abs() > 0.01,
851 "global={} sum={}",
852 report.effort_months,
853 sum_of_parts,
854 );
855 }
856
857 #[test]
858 fn test_all_models_produce_report() {
859 let cost_config = CostConfig::default();
860 let summary = make_summary(10_000, 200);
861
862 let models: Vec<Box<dyn EstimationModel>> = vec![
863 Box::new(CocomoBasicModel::default()),
864 Box::new(CocomoIIModel::default()),
865 Box::new(PutnamModel::default()),
866 Box::new(LocomoModel::default()),
867 ];
868
869 for model in &models {
870 let report = estimate(&summary, model.as_ref(), &cost_config);
871 assert!(!report.model.is_empty());
872 assert_eq!(report.total_sloc, 10_000);
873 assert!(
874 !report.params.is_empty(),
875 "model {} missing params",
876 report.model
877 );
878 }
879 }
880
881 #[test]
882 fn test_project_type_display() {
883 assert_eq!(ProjectType::Organic.to_string(), "organic");
884 assert_eq!(ProjectType::SemiDetached.to_string(), "semi-detached");
885 assert_eq!(ProjectType::Embedded.to_string(), "embedded");
886 }
887
888 #[test]
889 fn test_cost_config_defaults() {
890 let c = CostConfig::default();
891 assert!((c.average_wage - 56_286.0).abs() < f64::EPSILON);
892 assert!((c.overhead - 2.4).abs() < f64::EPSILON);
893 }
894}