1use scribe_core::Result;
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ComplexityMetrics {
38 pub cyclomatic_complexity: usize,
40
41 pub max_nesting_depth: usize,
43
44 pub function_count: usize,
46
47 pub logical_lines: usize,
49
50 pub comment_lines: usize,
52
53 pub blank_lines: usize,
55
56 pub total_lines: usize,
58
59 pub cognitive_complexity: usize,
61
62 pub maintainability_index: f64,
64
65 pub average_function_length: f64,
67
68 pub code_density: f64,
70
71 pub comment_ratio: f64,
73
74 pub language_metrics: LanguageSpecificMetrics,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct LanguageSpecificMetrics {
81 pub language: String,
83
84 pub complexity_factors: HashMap<String, f64>,
86
87 pub import_count: usize,
89
90 pub export_count: usize,
92
93 pub api_surface_area: usize,
95}
96
97#[derive(Debug)]
99pub struct ComplexityAnalyzer {
100 config: ComplexityConfig,
102}
103
104#[derive(Debug, Clone)]
106pub struct ComplexityConfig {
107 pub enable_cognitive_complexity: bool,
109
110 pub enable_maintainability_index: bool,
112
113 pub enable_language_specific: bool,
115
116 pub thresholds: ComplexityThresholds,
118}
119
120#[derive(Debug, Clone)]
122pub struct ComplexityThresholds {
123 pub cyclomatic_warning: usize,
125
126 pub nesting_warning: usize,
128
129 pub function_length_warning: usize,
131
132 pub maintainability_warning: f64,
134}
135
136impl Default for ComplexityConfig {
137 fn default() -> Self {
138 Self {
139 enable_cognitive_complexity: true,
140 enable_maintainability_index: true,
141 enable_language_specific: true,
142 thresholds: ComplexityThresholds::default(),
143 }
144 }
145}
146
147impl Default for ComplexityThresholds {
148 fn default() -> Self {
149 Self {
150 cyclomatic_warning: 10, nesting_warning: 4, function_length_warning: 50, maintainability_warning: 20.0, }
155 }
156}
157
158impl ComplexityAnalyzer {
159 pub fn new() -> Self {
161 Self {
162 config: ComplexityConfig::default(),
163 }
164 }
165
166 pub fn with_config(config: ComplexityConfig) -> Self {
168 Self { config }
169 }
170
171 pub fn analyze_content(&self, content: &str, language: &str) -> Result<ComplexityMetrics> {
173 let line_metrics = self.analyze_lines(content);
174 let cyclomatic_complexity = self.calculate_cyclomatic_complexity(content, language);
175 let max_nesting_depth = self.calculate_max_nesting_depth(content, language);
176 let function_count = self.count_functions(content, language);
177
178 let cognitive_complexity = if self.config.enable_cognitive_complexity {
179 self.calculate_cognitive_complexity(content, language)
180 } else {
181 0
182 };
183
184 let maintainability_index = if self.config.enable_maintainability_index {
185 self.calculate_maintainability_index(
186 &line_metrics,
187 cyclomatic_complexity,
188 function_count,
189 )
190 } else {
191 0.0
192 };
193
194 let average_function_length = if function_count > 0 {
195 line_metrics.logical_lines as f64 / function_count as f64
196 } else {
197 0.0
198 };
199
200 let code_density = if line_metrics.total_lines > 0 {
201 line_metrics.logical_lines as f64 / line_metrics.total_lines as f64
202 } else {
203 0.0
204 };
205
206 let comment_ratio = if line_metrics.logical_lines > 0 {
207 line_metrics.comment_lines as f64 / line_metrics.logical_lines as f64
208 } else {
209 0.0
210 };
211
212 let language_metrics = if self.config.enable_language_specific {
213 self.analyze_language_specific(content, language)
214 } else {
215 LanguageSpecificMetrics::default(language)
216 };
217
218 Ok(ComplexityMetrics {
219 cyclomatic_complexity,
220 max_nesting_depth,
221 function_count,
222 logical_lines: line_metrics.logical_lines,
223 comment_lines: line_metrics.comment_lines,
224 blank_lines: line_metrics.blank_lines,
225 total_lines: line_metrics.total_lines,
226 cognitive_complexity,
227 maintainability_index,
228 average_function_length,
229 code_density,
230 comment_ratio,
231 language_metrics,
232 })
233 }
234
235 fn analyze_lines(&self, content: &str) -> LineMetrics {
237 let mut logical_lines = 0;
238 let mut comment_lines = 0;
239 let mut blank_lines = 0;
240 let total_lines = content.lines().count();
241
242 for line in content.lines() {
243 let trimmed = line.trim();
244
245 if trimmed.is_empty() {
246 blank_lines += 1;
247 } else if self.is_comment_line(trimmed) {
248 comment_lines += 1;
249 } else {
250 logical_lines += 1;
251 }
252 }
253
254 LineMetrics {
255 logical_lines,
256 comment_lines,
257 blank_lines,
258 total_lines,
259 }
260 }
261
262 fn is_comment_line(&self, line: &str) -> bool {
264 let trimmed = line.trim();
265
266 trimmed.starts_with("//")
268 || trimmed.starts_with("#")
269 || trimmed.starts_with("/*")
270 || trimmed.starts_with("*")
271 || trimmed.starts_with("*/")
272 || trimmed.starts_with("<!--")
273 || trimmed.starts_with("--")
274 || trimmed.starts_with("%")
275 || trimmed.starts_with(";")
276 }
277
278 fn calculate_cyclomatic_complexity(&self, content: &str, language: &str) -> usize {
280 let mut complexity = 1; match language.to_lowercase().as_str() {
284 "rust" => {
285 complexity += content.matches(" if ").count();
287 complexity += content.matches(" else ").count();
288 complexity += content.matches(" match ").count();
289 complexity += content.matches(" while ").count();
290 complexity += content.matches(" for ").count();
291 complexity += content.matches("?").count(); complexity += content.matches("&&").count();
293 complexity += content.matches("||").count();
294 }
295 "python" => {
296 complexity += content.matches(" if ").count();
297 complexity += content.matches(" elif ").count();
298 complexity += content.matches(" while ").count();
299 complexity += content.matches(" for ").count();
300 complexity += content.matches(" except ").count();
301 complexity += content.matches(" and ").count();
302 complexity += content.matches(" or ").count();
303 }
304 "javascript" | "typescript" => {
305 complexity += content.matches(" if ").count();
306 complexity += content.matches(" while ").count();
307 complexity += content.matches(" for ").count();
308 complexity += content.matches(" catch ").count();
309 complexity += content.matches("&&").count();
310 complexity += content.matches("||").count();
311 complexity += content.matches("?").count();
312 }
313 _ => {
314 complexity += content.matches(" if ").count();
316 complexity += content.matches(" while ").count();
317 complexity += content.matches(" for ").count();
318 }
319 }
320
321 complexity.max(1) }
323
324 fn get_complexity_keywords(&self, language: &str) -> Vec<&'static str> {
326 match language.to_lowercase().as_str() {
327 "rust" => vec![
328 "if", "else if", "match", "while", "for", "loop", "catch", "?", "&&", "||",
329 "break", "continue",
330 ],
331 "python" => vec![
332 "if", "elif", "while", "for", "except", "and", "or", "break", "continue", "return",
333 "yield",
334 ],
335 "javascript" | "typescript" => vec![
336 "if", "else if", "while", "for", "catch", "case", "&&", "||", "?", "break",
337 "continue", "return",
338 ],
339 "java" | "c#" => vec![
340 "if", "else if", "while", "for", "foreach", "catch", "case", "&&", "||", "?",
341 "break", "continue", "return",
342 ],
343 "go" => vec![
344 "if", "else if", "for", "switch", "case", "select", "&&", "||", "break",
345 "continue", "return",
346 ],
347 "c" | "cpp" | "c++" => vec![
348 "if", "else if", "while", "for", "switch", "case", "&&", "||", "?", "break",
349 "continue", "return",
350 ],
351 _ => vec![
352 "if", "else", "while", "for", "switch", "case", "&&", "||", "?", "break",
353 "continue", "return",
354 ],
355 }
356 }
357
358 fn calculate_max_nesting_depth(&self, content: &str, _language: &str) -> usize {
360 let mut max_depth: usize = 0;
361 let mut current_depth: usize = 0;
362
363 for line in content.lines() {
365 let opens = line.matches('{').count();
367 let closes = line.matches('}').count();
368
369 current_depth += opens;
370 max_depth = max_depth.max(current_depth);
371 current_depth = current_depth.saturating_sub(closes);
372 }
373
374 max_depth
375 }
376
377 fn get_nesting_chars(&self, language: &str) -> (Vec<char>, Vec<char>) {
379 match language.to_lowercase().as_str() {
380 "python" => {
381 (vec!['{', '[', '('], vec!['}', ']', ')'])
383 }
384 _ => {
385 (vec!['{', '[', '('], vec!['}', ']', ')'])
387 }
388 }
389 }
390
391 fn count_functions(&self, content: &str, language: &str) -> usize {
393 let function_keywords = self.get_function_keywords(language);
394 let mut count = 0;
395
396 for line in content.lines() {
397 let line = line.trim();
398
399 for keyword in &function_keywords {
400 if line.starts_with(keyword) || line.contains(&format!(" {}", keyword)) {
401 count += 1;
402 break; }
404 }
405 }
406
407 count
408 }
409
410 fn get_function_keywords(&self, language: &str) -> Vec<&'static str> {
412 match language.to_lowercase().as_str() {
413 "rust" => vec!["fn ", "pub fn ", "async fn ", "pub async fn "],
414 "python" => vec!["def ", "async def ", "class "],
415 "javascript" | "typescript" => {
416 vec!["function ", "const ", "let ", "var ", "async function "]
417 }
418 "java" => vec!["public ", "private ", "protected ", "static "],
419 "c#" => vec!["public ", "private ", "protected ", "internal ", "static "],
420 "go" => vec!["func "],
421 "c" | "cpp" | "c++" => vec!["int ", "void ", "char ", "float ", "double ", "static "],
422 _ => vec!["function ", "def ", "fn "],
423 }
424 }
425
426 fn calculate_cognitive_complexity(&self, content: &str, language: &str) -> usize {
428 let mut complexity: usize = 0;
429 let mut nesting_level: usize = 0;
430
431 for line in content.lines() {
432 let line = line.trim().to_lowercase();
433
434 if line.contains('{') {
436 nesting_level += 1;
437 }
438 if line.contains('}') {
439 nesting_level = nesting_level.saturating_sub(1);
440 }
441
442 if line.contains("if") || line.contains("while") || line.contains("for") {
444 complexity += 1 + nesting_level; }
446
447 if line.contains("else if") || line.contains("elif") {
448 complexity += 1;
449 }
450
451 if line.contains("catch") || line.contains("except") {
452 complexity += 1 + nesting_level;
453 }
454
455 if line.contains("switch") || line.contains("match") {
456 complexity += 1 + nesting_level;
457 }
458
459 if self.has_recursive_call(&line, language) {
461 complexity += 1;
462 }
463 }
464
465 complexity
466 }
467
468 fn has_recursive_call(&self, line: &str, _language: &str) -> bool {
470 line.contains("self.")
472 || line.contains("this.")
473 || line.contains("recursive")
474 || line.contains("recurse")
475 }
476
477 fn calculate_maintainability_index(
479 &self,
480 line_metrics: &LineMetrics,
481 cyclomatic: usize,
482 functions: usize,
483 ) -> f64 {
484 let volume = (line_metrics.logical_lines as f64).ln();
488 let complexity = cyclomatic as f64;
489 let lloc = line_metrics.logical_lines as f64;
490
491 let mi = 171.0 - 5.2 * volume - 0.23 * complexity - 16.2 * lloc.ln();
493
494 mi.max(0.0).min(100.0)
496 }
497
498 fn analyze_language_specific(&self, content: &str, language: &str) -> LanguageSpecificMetrics {
500 let mut complexity_factors = HashMap::new();
501 let import_count = self.count_imports(content, language);
502 let export_count = self.count_exports(content, language);
503 let api_surface_area = self.estimate_api_surface_area(content, language);
504
505 match language.to_lowercase().as_str() {
507 "rust" => {
508 complexity_factors.insert(
509 "ownership_complexity".to_string(),
510 self.calculate_ownership_complexity(content),
511 );
512 complexity_factors.insert(
513 "trait_complexity".to_string(),
514 self.count_trait_usage(content) as f64,
515 );
516 complexity_factors.insert(
517 "macro_complexity".to_string(),
518 self.count_macro_usage(content) as f64,
519 );
520 }
521 "python" => {
522 complexity_factors.insert(
523 "decorator_complexity".to_string(),
524 self.count_decorators(content) as f64,
525 );
526 complexity_factors.insert(
527 "comprehension_complexity".to_string(),
528 self.count_comprehensions(content) as f64,
529 );
530 }
531 "javascript" | "typescript" => {
532 complexity_factors.insert(
533 "closure_complexity".to_string(),
534 self.count_closures(content) as f64,
535 );
536 complexity_factors.insert(
537 "promise_complexity".to_string(),
538 self.count_async_patterns(content) as f64,
539 );
540 }
541 _ => {
542 complexity_factors.insert("generic_complexity".to_string(), 1.0);
544 }
545 }
546
547 LanguageSpecificMetrics {
548 language: language.to_string(),
549 complexity_factors,
550 import_count,
551 export_count,
552 api_surface_area,
553 }
554 }
555
556 fn count_imports(&self, content: &str, language: &str) -> usize {
558 let import_patterns = match language.to_lowercase().as_str() {
559 "rust" => vec!["use ", "extern crate "],
560 "python" => vec!["import ", "from "],
561 "javascript" | "typescript" => vec!["import ", "require(", "const ", "let "],
562 "java" => vec!["import "],
563 "go" => vec!["import "],
564 _ => vec!["import ", "include ", "use "],
565 };
566
567 content
568 .lines()
569 .filter(|line| {
570 let trimmed = line.trim();
571 import_patterns
572 .iter()
573 .any(|pattern| trimmed.starts_with(pattern))
574 })
575 .count()
576 }
577
578 fn count_exports(&self, content: &str, language: &str) -> usize {
580 let export_patterns = match language.to_lowercase().as_str() {
581 "rust" => vec!["pub fn ", "pub struct ", "pub enum ", "pub trait "],
582 "python" => vec!["def ", "class "], "javascript" | "typescript" => vec!["export ", "module.exports"],
584 "java" => vec!["public class ", "public interface ", "public enum "],
585 _ => vec!["public ", "export "],
586 };
587
588 content
589 .lines()
590 .filter(|line| {
591 let trimmed = line.trim();
592 export_patterns
593 .iter()
594 .any(|pattern| trimmed.contains(pattern))
595 })
596 .count()
597 }
598
599 fn estimate_api_surface_area(&self, content: &str, language: &str) -> usize {
601 let public_items = self.count_exports(content, language);
603 let function_count = self.count_functions(content, language);
604
605 public_items.min(function_count)
607 }
608
609 fn calculate_ownership_complexity(&self, content: &str) -> f64 {
611 let ownership_keywords = ["&", "&mut", "Box<", "Rc<", "Arc<", "RefCell<", "Mutex<"];
612 let mut complexity = 0.0;
613
614 for line in content.lines() {
615 for keyword in &ownership_keywords {
616 complexity += line.matches(keyword).count() as f64 * 0.5;
617 }
618 }
619
620 complexity
621 }
622
623 fn count_trait_usage(&self, content: &str) -> usize {
624 content
625 .lines()
626 .filter(|line| line.contains("trait ") || line.contains("impl "))
627 .count()
628 }
629
630 fn count_macro_usage(&self, content: &str) -> usize {
631 content
632 .lines()
633 .filter(|line| line.contains("macro_rules!") || line.contains("!"))
634 .count()
635 }
636
637 fn count_decorators(&self, content: &str) -> usize {
638 content
639 .lines()
640 .filter(|line| line.trim().starts_with("@"))
641 .count()
642 }
643
644 fn count_comprehensions(&self, content: &str) -> usize {
645 content
646 .lines()
647 .filter(|line| {
648 line.contains("[") && line.contains("for ") && line.contains("in ")
649 || line.contains("{") && line.contains("for ") && line.contains("in ")
650 })
651 .count()
652 }
653
654 fn count_closures(&self, content: &str) -> usize {
655 content
656 .lines()
657 .filter(|line| line.contains("=>") || line.contains("function("))
658 .count()
659 }
660
661 fn count_async_patterns(&self, content: &str) -> usize {
662 content
663 .lines()
664 .filter(|line| {
665 line.contains("async")
666 || line.contains("await")
667 || line.contains("Promise")
668 || line.contains(".then(")
669 })
670 .count()
671 }
672}
673
674#[derive(Debug)]
676struct LineMetrics {
677 logical_lines: usize,
678 comment_lines: usize,
679 blank_lines: usize,
680 total_lines: usize,
681}
682
683impl Default for LanguageSpecificMetrics {
684 fn default() -> Self {
685 Self::default("unknown")
686 }
687}
688
689impl LanguageSpecificMetrics {
690 fn default(language: &str) -> Self {
691 Self {
692 language: language.to_string(),
693 complexity_factors: HashMap::new(),
694 import_count: 0,
695 export_count: 0,
696 api_surface_area: 0,
697 }
698 }
699}
700
701impl ComplexityMetrics {
702 pub fn complexity_score(&self) -> f64 {
704 let cyclomatic_score = (self.cyclomatic_complexity as f64 / 20.0).min(1.0);
706 let nesting_score = (self.max_nesting_depth as f64 / 8.0).min(1.0);
707 let cognitive_score = (self.cognitive_complexity as f64 / 15.0).min(1.0);
708 let maintainability_score = (100.0 - self.maintainability_index) / 100.0;
709
710 (cyclomatic_score * 0.3
712 + nesting_score * 0.2
713 + cognitive_score * 0.3
714 + maintainability_score * 0.2)
715 .min(1.0)
716 }
717
718 pub fn exceeds_thresholds(&self, thresholds: &ComplexityThresholds) -> Vec<String> {
720 let mut warnings = Vec::new();
721
722 if self.cyclomatic_complexity > thresholds.cyclomatic_warning {
723 warnings.push(format!(
724 "High cyclomatic complexity: {}",
725 self.cyclomatic_complexity
726 ));
727 }
728
729 if self.max_nesting_depth > thresholds.nesting_warning {
730 warnings.push(format!("Deep nesting: {}", self.max_nesting_depth));
731 }
732
733 if self.average_function_length > thresholds.function_length_warning as f64 {
734 warnings.push(format!(
735 "Long functions: avg {:.1} lines",
736 self.average_function_length
737 ));
738 }
739
740 if self.maintainability_index < thresholds.maintainability_warning {
741 warnings.push(format!(
742 "Low maintainability: {:.1}",
743 self.maintainability_index
744 ));
745 }
746
747 warnings
748 }
749
750 pub fn summary(&self) -> String {
752 format!(
753 "Complexity: CC={}, Depth={}, Functions={}, MI={:.1}, Cognitive={}",
754 self.cyclomatic_complexity,
755 self.max_nesting_depth,
756 self.function_count,
757 self.maintainability_index,
758 self.cognitive_complexity
759 )
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766
767 #[test]
768 fn test_analyzer_creation() {
769 let analyzer = ComplexityAnalyzer::new();
770 assert!(analyzer.config.enable_cognitive_complexity);
771 assert!(analyzer.config.enable_maintainability_index);
772 }
773
774 #[test]
775 fn test_simple_rust_analysis() {
776 let analyzer = ComplexityAnalyzer::new();
777 let content = r#"
778fn main() {
779 if x > 0 {
780 println!("positive");
781 } else {
782 println!("negative");
783 }
784}
785"#;
786
787 let metrics = analyzer.analyze_content(content, "rust").unwrap();
788
789 assert!(metrics.cyclomatic_complexity >= 2); assert!(metrics.function_count >= 1);
791 assert!(metrics.max_nesting_depth >= 1);
792 assert!(metrics.total_lines > 0);
793 }
794
795 #[test]
796 fn test_complex_code_analysis() {
797 let analyzer = ComplexityAnalyzer::new();
798 let content = r#"
799fn complex_function() {
800 for i in 0..10 {
801 if i % 2 == 0 {
802 while some_condition() {
803 if another_condition() {
804 match value {
805 1 => do_something(),
806 2 => do_something_else(),
807 _ => default_action(),
808 }
809 }
810 }
811 } else {
812 continue;
813 }
814 }
815}
816"#;
817
818 let metrics = analyzer.analyze_content(content, "rust").unwrap();
819
820 assert!(metrics.cyclomatic_complexity > 5); assert!(metrics.max_nesting_depth > 3); assert!(metrics.cognitive_complexity > metrics.cyclomatic_complexity); }
824
825 #[test]
826 fn test_line_analysis() {
827 let analyzer = ComplexityAnalyzer::new();
828 let content = r#"
829// This is a comment
830fn test() {
831 // Another comment
832 let x = 5;
833
834 // More comments
835 println!("Hello");
836}
837"#;
838
839 let metrics = analyzer.analyze_content(content, "rust").unwrap();
840
841 assert!(metrics.comment_lines > 0);
842 assert!(metrics.blank_lines > 0);
843 assert!(metrics.logical_lines > 0);
844 assert!(metrics.comment_ratio > 0.0);
845 assert!(metrics.code_density > 0.0);
846 }
847
848 #[test]
849 fn test_language_specific_analysis() {
850 let analyzer = ComplexityAnalyzer::new();
851
852 let rust_content = r#"
854use std::collections::HashMap;
855pub fn test() -> Result<(), Box<dyn Error>> {
856 let mut data: Vec<&str> = vec![];
857 Ok(())
858}
859"#;
860
861 let rust_metrics = analyzer.analyze_content(rust_content, "rust").unwrap();
862 assert_eq!(rust_metrics.language_metrics.language, "rust");
863 assert!(rust_metrics.language_metrics.import_count > 0);
864
865 let python_content = r#"
867import os
868from typing import List
869
870@decorator
871def test_function():
872 result = [x for x in range(10) if x % 2 == 0]
873 return result
874"#;
875
876 let python_metrics = analyzer.analyze_content(python_content, "python").unwrap();
877 assert_eq!(python_metrics.language_metrics.language, "python");
878 assert!(python_metrics.language_metrics.import_count > 0);
879 }
880
881 #[test]
882 fn test_complexity_score() {
883 let analyzer = ComplexityAnalyzer::new();
884
885 let simple_content = "fn main() { println!(\"hello\"); }";
887 let simple_metrics = analyzer.analyze_content(simple_content, "rust").unwrap();
888 let simple_score = simple_metrics.complexity_score();
889
890 let complex_content = r#"
892fn complex() {
893 for i in 0..100 {
894 if i % 2 == 0 {
895 while condition() {
896 match value {
897 1 => { if nested() { deep(); } },
898 2 => { if more_nested() { deeper(); } },
899 _ => { if even_more() { deepest(); } },
900 }
901 }
902 }
903 }
904}
905"#;
906 let complex_metrics = analyzer.analyze_content(complex_content, "rust").unwrap();
907 let complex_score = complex_metrics.complexity_score();
908
909 assert!(complex_score > simple_score);
910 assert!(simple_score >= 0.0 && simple_score <= 1.0);
911 assert!(complex_score >= 0.0 && complex_score <= 1.0);
912 }
913
914 #[test]
915 fn test_threshold_warnings() {
916 let analyzer = ComplexityAnalyzer::new();
917 let thresholds = ComplexityThresholds {
918 cyclomatic_warning: 5,
919 nesting_warning: 2,
920 function_length_warning: 10,
921 maintainability_warning: 50.0,
922 };
923
924 let complex_content = r#"
925fn complex_function() {
926 for i in 0..10 {
927 if i % 2 == 0 {
928 while some_condition() {
929 if another_condition() {
930 if yet_another() {
931 do_something();
932 }
933 }
934 }
935 }
936 }
937}
938"#;
939
940 let metrics = analyzer.analyze_content(complex_content, "rust").unwrap();
941 let warnings = metrics.exceeds_thresholds(&thresholds);
942
943 assert!(!warnings.is_empty());
944 assert!(warnings.iter().any(|w| w.contains("complexity")));
945 }
946
947 #[test]
948 fn test_metrics_summary() {
949 let analyzer = ComplexityAnalyzer::new();
950 let content = "fn test() { if x > 0 { return 1; } else { return 0; } }";
951 let metrics = analyzer.analyze_content(content, "rust").unwrap();
952
953 let summary = metrics.summary();
954 assert!(summary.contains("CC="));
955 assert!(summary.contains("Depth="));
956 assert!(summary.contains("Functions="));
957 assert!(summary.contains("MI="));
958 assert!(summary.contains("Cognitive="));
959 }
960}