1use std::collections::BTreeMap;
14
15use tree_sitter::{Node, Parser};
16
17use crate::coverage_analysis::PointKind;
18use crate::coverage_report::{
19 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum GoInstrumenterError {
24 Parse(String),
25}
26
27impl std::fmt::Display for GoInstrumenterError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 GoInstrumenterError::Parse(detail) => write!(f, "Go parse error: {detail}"),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum GoProbeTarget {
38 Statement {
39 id: String,
40 },
41 Function {
42 id: String,
43 },
44 Alternative {
51 branch: String,
52 alternative: String,
53 },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct GoProbe {
58 pub id: u64,
59 pub target: GoProbeTarget,
60 pub at: usize,
62}
63
64#[derive(Debug, Clone, PartialEq)]
65pub struct GoFileObligations {
66 pub manifest: CoverageManifest,
67 pub probes: BTreeMap<u64, GoProbe>,
68 pub edits: Vec<GoEdit>,
70 pub decision_widths: Vec<u8>,
73}
74
75pub(crate) fn stable_obligation_id(
87 language: &str,
88 file: &str,
89 kind: &str,
90 start: usize,
91 end: usize,
92) -> String {
93 use sha2::{Digest, Sha256};
94 let mut hash = Sha256::new();
95 for value in [file, kind, &start.to_string(), &end.to_string()] {
96 hash.update(value.as_bytes());
97 hash.update([0]);
98 }
99 let digest = hash.finalize();
100 let mut encoded = String::with_capacity(24);
101 for byte in &digest[..12] {
102 use std::fmt::Write as _;
103 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
104 }
105 format!("{language}:{kind}:{encoded}")
106}
107
108pub fn parse(source: &str) -> Result<tree_sitter::Tree, GoInstrumenterError> {
109 let mut parser = Parser::new();
110 parser
111 .set_language(&tree_sitter_go::LANGUAGE.into())
112 .map_err(|error| GoInstrumenterError::Parse(error.to_string()))?;
113 let tree = parser
114 .parse(source, None)
115 .ok_or_else(|| GoInstrumenterError::Parse("parser returned no tree".into()))?;
116 if tree.root_node().has_error() {
117 return Err(GoInstrumenterError::Parse(parse_failure(&tree, source)));
118 }
119 Ok(tree)
120}
121
122pub(crate) fn parse_failure(tree: &tree_sitter::Tree, source: &str) -> String {
130 let mut deepest: Option<(usize, Node)> = None;
131 let mut stack = vec![(0_usize, tree.root_node())];
132 while let Some((depth, node)) = stack.pop() {
133 if (node.is_error() || node.is_missing()) && deepest.is_none_or(|(found, _)| depth > found)
134 {
135 deepest = Some((depth, node));
136 }
137 let mut cursor = node.walk();
141 for child in node.children(&mut cursor) {
142 stack.push((depth + 1, child));
143 }
144 }
145 let Some((_, node)) = deepest else {
146 return "source does not parse".to_owned();
147 };
148 let start = node.start_position();
149 let mut line = source
150 .lines()
151 .nth(start.row)
152 .unwrap_or_default()
153 .trim()
154 .to_owned();
155 if line.chars().count() > 120 {
156 line = line.chars().take(117).collect::<String>() + "...";
157 }
158 let what = if node.is_missing() {
159 "missing syntax"
160 } else {
161 "unexpected syntax"
162 };
163 let end = node.end_position().row + 1;
168 let through = if end > start.row + 1 {
169 format!(" (through line {end})")
170 } else {
171 String::new()
172 };
173 format!(
174 "{what} at line {}, column {}{through}: {line}",
175 start.row + 1,
176 start.column + 1,
177 )
178}
179
180fn is_statement(kind: &str) -> bool {
184 matches!(
185 kind,
186 "assignment_statement"
187 | "break_statement"
188 | "const_declaration"
189 | "continue_statement"
190 | "dec_statement"
191 | "defer_statement"
192 | "expression_statement"
193 | "expression_switch_statement"
194 | "fallthrough_statement"
195 | "for_statement"
196 | "go_statement"
197 | "goto_statement"
198 | "if_statement"
199 | "inc_statement"
200 | "labeled_statement"
201 | "return_statement"
202 | "select_statement"
203 | "send_statement"
204 | "short_var_declaration"
205 | "type_switch_statement"
206 | "var_declaration"
207 )
208}
209
210struct Collector<'a> {
211 file: &'a str,
212 source: &'a str,
213 alias: &'a str,
214 next_probe: &'a mut u64,
215 edits: Vec<GoEdit>,
216 points: Vec<PointMeta>,
217 branches: Vec<BranchMeta>,
218 decisions: Vec<DecisionMeta>,
219 probes: BTreeMap<u64, GoProbe>,
220 limitations: Vec<serde_json::Value>,
221 widths: Vec<u8>,
222 decision_base: u32,
227}
228
229fn branch_wrapper(alias: &str, when_true: u64, when_false: u64, decision: Option<usize>) -> String {
236 match decision {
237 Some(index) => format!("{alias}.BD({when_true}, {when_false}, {index}, "),
238 None => format!("{alias}.B({when_true}, {when_false}, "),
239 }
240}
241
242fn in_statement_position(node: Node) -> bool {
250 node.parent()
251 .is_some_and(|parent| parent.kind() == "statement_list")
252}
253
254fn loop_condition<'t>(node: Node<'t>) -> Option<Node<'t>> {
257 if let Some(condition) = node.child_by_field_name("condition") {
258 return Some(condition);
259 }
260 let mut cursor = node.walk();
261 let clause = node
262 .children(&mut cursor)
263 .find(|child| child.kind() == "for_clause")?;
264 let mut inner = clause.walk();
265 clause
266 .children(&mut inner)
267 .find(|child| child.is_named() && child.kind().ends_with("_expression"))
268}
269
270impl<'a> Collector<'a> {
271 fn id(&mut self, node: Node, kind: &str) -> String {
272 stable_obligation_id("go", self.file, kind, node.start_byte(), node.end_byte())
273 }
274
275 fn limitation_id(&mut self, node: Node, kind: &str) -> String {
280 self.id(node, kind)
281 }
282
283 fn probe(&mut self, target: GoProbeTarget, at: usize) -> u64 {
284 *self.next_probe += 1;
285 let id = *self.next_probe;
286 self.probes.insert(id, GoProbe { id, target, at });
287 id
288 }
289
290 fn edit(&mut self, at: usize, rank: i32, text: String) {
291 self.edits.push(GoEdit { at, rank, text });
292 }
293
294 fn call_before(&mut self, at: usize, call: String) {
297 self.edit(at, 100, format!("{call}; "));
298 }
299
300 fn position(&self, node: Node) -> (usize, usize) {
302 let start = node.start_position();
303 (start.row + 1, start.column + 1)
304 }
305
306 fn text(&self, node: Node) -> String {
307 let raw = &self.source[node.byte_range()];
308 let line = raw.lines().next().unwrap_or("");
309 line.trim().to_owned()
310 }
311
312 fn add_point(&mut self, node: Node, kind: PointKind, label: Option<String>) {
313 let (line, column) = self.position(node);
314 let id = self.id(
315 node,
316 match kind {
317 PointKind::Function => "function",
318 PointKind::Statement => "statement",
319 },
320 );
321 let target = match kind {
322 PointKind::Function => GoProbeTarget::Function { id: id.clone() },
323 PointKind::Statement => GoProbeTarget::Statement { id: id.clone() },
324 };
325 let at = match kind {
328 PointKind::Function => match node.child_by_field_name("body") {
329 Some(body) => body.start_byte() + 1,
330 None => return,
331 },
332 PointKind::Statement => node.start_byte(),
333 };
334 let probe = self.probe(target, at);
335 self.call_before(at, format!("{HITS_VARIABLE}[{probe}] = 2"));
339 self.points.push(PointMeta {
340 id,
341 kind,
342 file: self.file.to_owned(),
343 line,
344 column,
345 source: self.text(node),
346 label,
347 });
348 }
349
350 fn add_branch(&mut self, node: Node, kind: &str, labels: &[&str]) -> Vec<u64> {
351 let (line, column) = self.position(node);
352 let id = self.id(node, "branch");
353 let mut probes = Vec::new();
354 let alternatives = labels
355 .iter()
356 .map(|label| {
357 let alternative = format!("{id}.{label}");
358 probes.push(self.probe(
359 GoProbeTarget::Alternative {
360 branch: id.clone(),
361 alternative: alternative.clone(),
362 },
363 node.start_byte(),
364 ));
365 BranchAlternativeMeta {
366 id: alternative,
367 label: (*label).to_owned(),
368 }
369 })
370 .collect();
371 self.branches.push(BranchMeta {
372 id,
373 kind: kind.to_owned(),
374 file: self.file.to_owned(),
375 line,
376 column,
377 source: self.text(node),
378 alternatives,
379 });
380 probes
381 }
382
383 fn add_decision(&mut self, node: Node, kind: &str) -> Option<usize> {
389 let mut leaves = Vec::new();
390 condition_nodes(node, self.source, &mut leaves);
391 if leaves.len() < 2 {
392 return None;
393 }
394 let conditions = leaves
395 .iter()
396 .map(|leaf| self.source[leaf.byte_range()].trim().to_owned())
397 .collect::<Vec<_>>();
398 let (line, column) = self.position(node);
399 let id = self.id(node, "decision");
400 let index_of_decision = self.decision_base as usize + self.widths.len();
404 self.widths.push(leaves.len().min(64) as u8);
405 let alias = self.alias.to_owned();
406 for (index, leaf) in leaves.iter().enumerate() {
411 self.edit(
415 leaf.start_byte(),
416 20,
417 format!("{alias}.C({index_of_decision}, {index}, "),
418 );
419 self.edit(leaf.end_byte(), 20, ")".to_owned());
420 }
421 self.decisions.push(DecisionMeta {
422 id,
423 file: self.file.to_owned(),
424 line,
425 column,
426 source: self.text(node),
427 conditions,
428 kind: kind.to_owned(),
429 });
430 Some(index_of_decision)
431 }
432
433 fn walk(&mut self, node: Node) {
434 let alias = self.alias.to_owned();
435 match node.kind() {
436 "function_declaration" | "method_declaration" | "func_literal" => {
437 let label = node
438 .child_by_field_name("name")
439 .map(|name| self.source[name.byte_range()].to_owned());
440 self.add_point(node, PointKind::Function, label);
441 }
442 "if_statement" => {
443 if let Some(condition) = node.child_by_field_name("condition") {
444 let probes = self.add_branch(node, "if", &["true", "false"]);
449 let decision = self.add_decision(condition, "if");
450 self.edit(
451 condition.start_byte(),
452 5,
453 branch_wrapper(&alias, probes[0], probes[1], decision),
454 );
455 self.edit(condition.end_byte(), 5, ")".to_owned());
456 }
457 }
458 "for_statement" => {
459 match loop_condition(node) {
464 Some(condition) => {
465 let probes = self.add_branch(node, "loop", &["true", "false"]);
466 let decision = self.add_decision(condition, "loop");
467 self.edit(
468 condition.start_byte(),
469 5,
470 branch_wrapper(&alias, probes[0], probes[1], decision),
471 );
472 self.edit(condition.end_byte(), 5, ")".to_owned());
473 }
474 None => {
475 let (line, column) = self.position(node);
476 let limitation = self.limitation_id(node, "loop-without-condition");
477 self.limitations.push(serde_json::json!({
478 "id": limitation,
479 "kind": "loop-without-condition",
480 "file": self.file,
481 "source": self.text(node),
482 "line": line,
483 "column": column,
484 "reason": "a range or unconditional loop has no condition to observe, so no branch obligation is recorded for it",
485 }));
486 }
487 }
488 }
489 "expression_switch_statement" | "type_switch_statement" | "select_statement" => {
490 let kind = match node.kind() {
491 "expression_switch_statement" => "switch",
492 "type_switch_statement" => "type-switch",
493 _ => "select",
494 };
495 let mut cases = Vec::new();
496 let mut has_default = false;
497 let mut cursor = node.walk();
498 for child in node.children(&mut cursor) {
499 match child.kind() {
500 "expression_case" | "type_case" | "communication_case" => {
501 cases.push((self.text(child), Some(child)))
502 }
503 "default_case" => {
504 has_default = true;
505 cases.push(("default".to_owned(), Some(child)));
506 }
507 _ => {}
508 }
509 }
510 if !has_default && node.kind() != "select_statement" {
525 cases.push(("no case matched".to_owned(), None));
526 }
527 let labels = cases
528 .iter()
529 .map(|(label, _)| label.as_str())
530 .collect::<Vec<_>>();
531 let probes = self.add_branch(node, kind, &labels);
532 for (probe, (_, clause)) in probes.iter().zip(cases.iter()) {
533 match clause {
534 Some(clause) => {
535 let at = clause
536 .children(&mut clause.walk())
537 .find(|child| child.kind() == "statement_list")
538 .map(|body| body.start_byte())
539 .unwrap_or_else(|| clause.end_byte());
540 self.call_before(at, format!("{alias}.A({probe})"));
541 }
542 None => {
543 let at = node.end_byte().saturating_sub(1);
545 self.edit(at, 100, format!("\ndefault:\n{alias}.A({probe})\n"));
546 }
547 }
548 }
549 }
550 kind if is_statement(kind) && in_statement_position(node) => {
551 self.add_point(node, PointKind::Statement, None);
552 }
553 _ => {}
554 }
555 if in_statement_position(node)
558 && matches!(
559 node.kind(),
560 "if_statement"
561 | "for_statement"
562 | "expression_switch_statement"
563 | "type_switch_statement"
564 | "select_statement"
565 )
566 {
567 self.add_point(node, PointKind::Statement, None);
568 }
569 let mut cursor = node.walk();
570 for child in node.children(&mut cursor) {
571 if child.is_named() {
572 self.walk(child);
573 }
574 }
575 }
576}
577
578fn condition_nodes<'t>(node: Node<'t>, source: &str, out: &mut Vec<Node<'t>>) {
584 match node.kind() {
585 "binary_expression" => {
586 let operator = node
587 .child_by_field_name("operator")
588 .map(|op| &source[op.byte_range()])
589 .unwrap_or("");
590 if operator == "&&" || operator == "||" {
591 if let Some(left) = node.child_by_field_name("left") {
592 condition_nodes(left, source, out);
593 }
594 if let Some(right) = node.child_by_field_name("right") {
595 condition_nodes(right, source, out);
596 }
597 return;
598 }
599 out.push(node);
600 }
601 "parenthesized_expression" => {
602 let mut cursor = node.walk();
603 match node.children(&mut cursor).find(|child| child.is_named()) {
604 Some(inner) => condition_nodes(inner, source, out),
605 None => out.push(node),
606 }
607 }
608 _ => out.push(node),
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
619pub struct GoEdit {
620 pub at: usize,
621 pub rank: i32,
622 pub text: String,
623}
624
625pub fn rewrite(source: &str, edits: &[GoEdit]) -> String {
627 let mut ordered = edits.to_vec();
628 ordered.sort_by(|a, b| b.at.cmp(&a.at).then(b.rank.cmp(&a.rank)));
629 let mut out = source.to_owned();
630 for edit in ordered {
631 if edit.at > out.len() {
632 continue;
633 }
634 out.insert_str(edit.at, &edit.text);
635 }
636 out
637}
638
639pub fn import_edit(source: &str, alias: &str, path: &str) -> Option<GoEdit> {
643 let tree = parse(source).ok()?;
644 let mut cursor = tree.root_node().walk();
645 let package = tree
646 .root_node()
647 .children(&mut cursor)
648 .find(|child| child.kind() == "package_clause")?;
649 Some(GoEdit {
650 at: package.end_byte(),
651 rank: 0,
652 text: format!("\nimport {alias} \"{path}\""),
653 })
654}
655
656pub fn build_go_obligations(
657 file: &str,
658 source: &str,
659 next_probe: &mut u64,
660 next_decision: &mut u32,
661) -> Result<GoFileObligations, GoInstrumenterError> {
662 build_go_obligations_with_alias(file, source, next_probe, next_decision, RUNTIME_ALIAS)
663}
664
665pub const RUNTIME_ALIAS: &str = "__supercov";
668
669pub const HITS_VARIABLE: &str = "__supercovHits";
672
673pub const RUNTIME_IMPORT: &str = "github.com/supercorp-ai/supercov/runtime/go/supercov";
675
676pub fn build_go_obligations_with_alias(
677 file: &str,
678 source: &str,
679 next_probe: &mut u64,
680 next_decision: &mut u32,
681 alias: &str,
682) -> Result<GoFileObligations, GoInstrumenterError> {
683 let tree = parse(source)?;
684 let decision_base = *next_decision;
685 let mut collector = Collector {
686 file,
687 source,
688 alias,
689 next_probe,
690 decision_base,
691 edits: Vec::new(),
692 points: Vec::new(),
693 branches: Vec::new(),
694 decisions: Vec::new(),
695 probes: BTreeMap::new(),
696 limitations: Vec::new(),
697 widths: Vec::new(),
698 };
699 let mut cursor = tree.root_node().walk();
700 for child in tree.root_node().children(&mut cursor) {
701 if child.is_named() {
702 collector.walk(child);
703 }
704 }
705 *next_decision += collector.widths.len() as u32;
706 let mut edits = collector.edits;
707 let calls_runtime = edits
711 .iter()
712 .any(|edit| edit.text.contains(&format!("{alias}.")));
713 if calls_runtime && let Some(import) = import_edit(source, alias, RUNTIME_IMPORT) {
714 edits.push(import);
715 }
716 Ok(GoFileObligations {
717 manifest: CoverageManifest {
718 decisions: collector.decisions,
719 points: collector.points,
720 branches: collector.branches,
721 limitations: collector.limitations,
722 unmeasured: Vec::new(),
723 scope: None,
724 },
725 probes: collector.probes,
726 edits,
727 decision_widths: collector.widths,
728 })
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 fn assert_indexable(limitations: &[serde_json::Value]) -> Vec<String> {
744 assert!(!limitations.is_empty(), "nothing to check");
745 for limitation in limitations {
746 for field in ["id", "kind", "file", "source", "reason"] {
747 assert!(
748 limitation.get(field).and_then(|v| v.as_str()).is_some(),
749 "a limitation needs a string {field}: {limitation}"
750 );
751 }
752 for field in ["line", "column"] {
753 assert!(
754 limitation.get(field).and_then(|v| v.as_u64()).is_some(),
755 "a limitation needs a number {field}: {limitation}"
756 );
757 }
758 }
759 let mut kinds = limitations
760 .iter()
761 .filter_map(|limitation| limitation["kind"].as_str().map(str::to_owned))
762 .collect::<Vec<_>>();
763 kinds.sort();
764 kinds.dedup();
765 kinds
766 }
767
768 #[test]
770 fn a_file_that_does_not_parse_says_where() {
771 let broken = "package main\n\nfunc f() int {\n\treturn 1 )\n}\n";
772 let message = parse(broken).expect_err("does not parse").to_string();
773 assert!(message.contains("line 4"), "{message}");
774 assert!(message.contains("return 1"), "{message}");
775 assert!(
776 !message.contains("byte"),
777 "an offset is not somewhere a reader can look: {message}"
778 );
779 }
780
781 #[test]
782 fn every_limitation_carries_what_the_index_stores() {
783 const EVERY: &str = r#"package main
784
785func walk(items []int) int {
786 sum := 0
787 for _, item := range items {
788 sum += item
789 }
790 for {
791 break
792 }
793 return sum
794}
795"#;
796 let mut next = 0;
797 let mut decisions = 0;
798 let obligations =
799 build_go_obligations("walk.go", EVERY, &mut next, &mut decisions).expect("go");
800 assert_eq!(
801 assert_indexable(&obligations.manifest.limitations),
802 ["loop-without-condition"]
803 );
804 }
805
806 const SAMPLE: &str = r#"package main
807
808import "fmt"
809
810func classify(a int, b bool) string {
811 if a > 10 && b {
812 return "big"
813 }
814 for i := 0; i < a; i++ {
815 fmt.Println(i)
816 }
817 switch {
818 case a == 0:
819 return "zero"
820 default:
821 return "small"
822 }
823}
824"#;
825
826 fn obligations(source: &str) -> GoFileObligations {
827 let mut next = 0;
828 let mut decisions = 0;
829 build_go_obligations("main.go", source, &mut next, &mut decisions).expect("obligations")
830 }
831
832 fn rewritten(source: &str) -> String {
835 let mut next = 0;
836 let mut decisions = 0;
837 let go =
838 build_go_obligations("x.go", source, &mut next, &mut decisions).expect("obligations");
839 let out = rewrite(source, &go.edits);
840 parse(&out)
841 .unwrap_or_else(|error| panic!("rewritten source does not parse: {error}\n{out}"));
842 out
843 }
844
845 #[test]
846 fn a_probe_never_lands_where_go_does_not_allow_a_statement() {
847 let out = rewritten(
851 "package main\nfunc f(a int) int {\n\tfor i := 0; i < a; i++ {\n\t\ta++\n\t}\n\tif b := a; b > 1 {\n\t\treturn b\n\t}\n\tswitch c := a; c {\n\tcase 1:\n\t\treturn 1\n\t}\n\treturn 0\n}\n",
852 );
853 assert!(
854 !out.contains("for __supercov"),
855 "probe in a for-clause init:\n{out}"
856 );
857 assert!(
858 !out.contains("; __supercov.P"),
859 "probe in a for-clause post:\n{out}"
860 );
861 assert!(
862 out.contains("if b := a;"),
863 "the if initialiser survived intact:\n{out}"
864 );
865 assert!(
866 out.contains("switch c := a;"),
867 "the switch initialiser survived intact:\n{out}"
868 );
869 }
870
871 #[test]
872 fn wrapping_a_condition_preserves_short_circuit_order() {
873 let out = rewritten(
878 "package main\nfunc f(a int, b bool) bool {\n\tif a > 10 && b {\n\t\treturn true\n\t}\n\treturn false\n}\n",
879 );
880 let condition = out
881 .lines()
882 .find(|line| line.contains("if "))
883 .expect("the if survived");
884 let branch = condition.find(".BD(").expect("branch and decision wrapper");
885 let first = condition.find(".C(").expect("first condition wrapper");
886 assert!(
887 branch < first,
888 "the branch must enclose its conditions: {condition}"
889 );
890 assert_eq!(
891 condition.matches(".C(").count(),
892 2,
893 "one wrapper per condition: {condition}"
894 );
895 assert!(
896 condition.contains("&&"),
897 "the operator itself is untouched: {condition}"
898 );
899 }
900
901 #[test]
902 fn a_single_condition_branch_uses_the_wrapper_that_inlines() {
903 let out = rewritten(
907 "package main\nfunc f(a int) bool {\n\tif a > 10 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
908 );
909 assert!(out.contains(".B("), "{out}");
910 assert!(!out.contains(".BD("), "no decision here to close:\n{out}");
911 }
912
913 #[test]
914 fn a_switch_without_a_default_gains_one_so_matching_nothing_is_observable() {
915 let out = rewritten(
918 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
919 );
920 assert!(out.contains("default:"), "{out}");
921
922 let existing = rewritten(
924 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\tdefault:\n\t\treturn\n\t}\n}\n",
925 );
926 assert_eq!(existing.matches("default:").count(), 1, "{existing}");
927 }
928
929 #[test]
930 fn a_loop_with_no_condition_records_a_limitation_not_an_obligation() {
931 let mut next = 0;
935 let mut decisions = 0;
936 let go = build_go_obligations(
937 "x.go",
938 "package main\nfunc f(xs []int) {\n\tfor _, x := range xs {\n\t\t_ = x\n\t}\n\tfor {\n\t\tbreak\n\t}\n}\n",
939 &mut next,
940 &mut decisions,
941 )
942 .unwrap();
943 assert!(
944 go.manifest.branches.iter().all(|b| b.kind != "loop"),
945 "{:?}",
946 go.manifest.branches
947 );
948 assert_eq!(
949 go.manifest.limitations.len(),
950 2,
951 "{:?}",
952 go.manifest.limitations
953 );
954 assert_eq!(go.manifest.limitations[0]["kind"], "loop-without-condition");
955 }
956
957 #[test]
958 fn only_a_file_that_gained_a_probe_imports_the_runtime() {
959 let out = rewritten("package main\nfunc f(a int) int {\n\treturn a\n}\n");
962 assert!(
963 out.contains(RUNTIME_ALIAS),
964 "a function is itself an obligation:\n{out}"
965 );
966
967 let mut next = 0;
968 let mut decisions = 0;
969 let bare = build_go_obligations(
970 "t.go",
971 "package main\n\ntype T struct{}\n",
972 &mut next,
973 &mut decisions,
974 )
975 .unwrap();
976 assert!(bare.edits.is_empty(), "{:?}", bare.edits);
977 assert_eq!(
978 rewrite("package main\n\ntype T struct{}\n", &bare.edits),
979 "package main\n\ntype T struct{}\n"
980 );
981 }
982
983 #[test]
984 fn a_short_circuiting_operator_splits_a_decision_and_nothing_else_does() {
985 let go = obligations(SAMPLE);
989 let decision = go
990 .manifest
991 .decisions
992 .iter()
993 .find(|d| d.kind == "if")
994 .expect("the if carries a decision");
995 assert_eq!(decision.conditions, ["a > 10", "b"]);
996
997 let simple = obligations(
1000 "package main\nfunc f(a int) bool {\n\tif a > 1 {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1001 );
1002 assert!(
1003 simple.manifest.decisions.is_empty(),
1004 "{:?}",
1005 simple.manifest.decisions
1006 );
1007 }
1008
1009 #[test]
1010 fn negation_and_parentheses_do_not_invent_conditions() {
1011 let go = obligations(
1015 "package main\nfunc f(a int, b bool, c bool) bool {\n\tif (a > 1 || !b) && c {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1016 );
1017 let decision = &go.manifest.decisions[0];
1018 assert_eq!(decision.conditions, ["a > 1", "!b", "c"]);
1019 }
1020
1021 #[test]
1022 fn every_branching_construct_records_the_outcome_that_was_not_taken() {
1023 let go = obligations(SAMPLE);
1027 let by_kind = |kind: &str| {
1028 go.manifest
1029 .branches
1030 .iter()
1031 .find(|b| b.kind == kind)
1032 .map(|b| {
1033 b.alternatives
1034 .iter()
1035 .map(|a| a.label.clone())
1036 .collect::<Vec<_>>()
1037 })
1038 };
1039 assert_eq!(by_kind("if").unwrap(), ["true", "false"]);
1040 assert_eq!(by_kind("loop").unwrap(), ["true", "false"]);
1044 let switch = by_kind("switch").unwrap();
1045 assert!(switch.contains(&"default".to_owned()), "{switch:?}");
1046 assert!(
1047 !switch.contains(&"no case matched".to_owned()),
1048 "a default already covers it"
1049 );
1050
1051 let open = obligations(
1053 "package main\nfunc f(a int) {\n\tswitch a {\n\tcase 1:\n\t\treturn\n\t}\n}\n",
1054 );
1055 let labels = open
1056 .manifest
1057 .branches
1058 .iter()
1059 .find(|b| b.kind == "switch")
1060 .unwrap()
1061 .alternatives
1062 .iter()
1063 .map(|a| a.label.clone())
1064 .collect::<Vec<_>>();
1065 assert!(labels.contains(&"no case matched".to_owned()), "{labels:?}");
1066 }
1067
1068 #[test]
1069 fn functions_and_statements_are_separate_obligations_with_locations() {
1070 let go = obligations(SAMPLE);
1071 let functions = go
1072 .manifest
1073 .points
1074 .iter()
1075 .filter(|p| p.kind == PointKind::Function)
1076 .collect::<Vec<_>>();
1077 assert_eq!(functions.len(), 1);
1078 assert_eq!(functions[0].label.as_deref(), Some("classify"));
1079 assert_eq!(functions[0].line, 5);
1080
1081 let statements = go
1082 .manifest
1083 .points
1084 .iter()
1085 .filter(|p| p.kind == PointKind::Statement)
1086 .count();
1087 assert!(
1088 statements >= 6,
1089 "expected the body's statements, got {statements}"
1090 );
1091 assert!(
1094 go.manifest
1095 .points
1096 .iter()
1097 .all(|p| p.line > 0 && p.column > 0)
1098 );
1099 assert!(
1101 !go.manifest
1102 .points
1103 .iter()
1104 .any(|p| p.source.starts_with("import"))
1105 );
1106 }
1107
1108 #[test]
1109 fn every_obligation_has_a_probe_and_malformed_source_is_refused() {
1110 let go = obligations(SAMPLE);
1111 let expected = go.manifest.points.len()
1112 + go.manifest
1113 .branches
1114 .iter()
1115 .map(|b| b.alternatives.len())
1116 .sum::<usize>();
1117 assert_eq!(go.probes.len(), expected);
1121 assert_eq!(go.decision_widths.len(), go.manifest.decisions.len());
1124
1125 let mut next = 0;
1128 let mut decisions = 0;
1129 assert!(matches!(
1130 build_go_obligations(
1131 "broken.go",
1132 "package main\nfunc f( {",
1133 &mut next,
1134 &mut decisions
1135 ),
1136 Err(GoInstrumenterError::Parse(_))
1137 ));
1138 }
1139
1140 #[test]
1141 fn decisions_are_numbered_across_the_module_not_within_a_file() {
1142 let mut next = 0;
1147 let mut decisions = 0;
1148 let first = build_go_obligations(
1149 "a.go",
1150 "package p\n\nfunc A(x, y bool) bool {\n\tif x && y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1151 &mut next,
1152 &mut decisions,
1153 )
1154 .unwrap();
1155 let second = build_go_obligations(
1156 "b.go",
1157 "package p\n\nfunc B(x, y bool) bool {\n\tif x || y {\n\t\treturn true\n\t}\n\treturn false\n}\n",
1158 &mut next,
1159 &mut decisions,
1160 )
1161 .unwrap();
1162
1163 let referenced = |obligations: &GoFileObligations| {
1164 obligations
1165 .edits
1166 .iter()
1167 .filter_map(|edit| {
1168 let at = edit.text.find(".C(")?;
1169 edit.text[at + 3..]
1170 .split(',')
1171 .next()?
1172 .trim()
1173 .parse::<u32>()
1174 .ok()
1175 })
1176 .collect::<std::collections::BTreeSet<_>>()
1177 };
1178 assert_eq!(referenced(&first), [0].into());
1179 assert_eq!(referenced(&second), [1].into());
1180 assert_eq!(decisions, 2, "the module numbered two decisions in all");
1181 assert_eq!(first.decision_widths, [2]);
1184 assert_eq!(second.decision_widths, [2]);
1185 }
1186}