Skip to main content

stern4rust/finding/model/
test_marker.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5// The phase or phases one marker comment names. Plain data: a marker is read,
6// never asked anything.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum MarkerPhase {
9    Arrange,
10    Act,
11    Assert,
12}
13
14// One AAA marker comment, read as the phases it names.
15//
16// Two properties do the work, and both were taken from what the family already
17// writes rather than from the standard as stated.
18//
19// A marker may carry **trailing prose** -- `// Arrange -- four nodes`,
20// `// Act: heal the partition`, `// Assert. every node commits`. All three
21// punctuations appear across the repositories, and demanding a bare marker would
22// report the tests that took the trouble to explain themselves.
23//
24// A marker ends on a **word boundary**. Without it `// Actually this needs
25// explaining` is an Act, and the sequence a test reads as becomes nonsense.
26pub struct TestMarker {
27    pub line: usize,
28    pub label: String,
29    pub phases: Vec<MarkerPhase>,
30}
31
32impl TestMarker {
33    // Longest first, so `Arrange & Act` is never read as a bare `Arrange`.
34    pub const FORMS: [&'static str; 6] = [
35        "Arrange & Act & Assert",
36        "Arrange & Act",
37        "Act & Assert",
38        "Arrange",
39        "Assert",
40        "Act",
41    ];
42
43    pub fn parse(text: &str, line: usize) -> Option<Self> {
44        let comment = text.trim_start().strip_prefix("//")?.trim_start();
45        let form = Self::form_of(comment)?;
46        Some(Self {
47            line,
48            label: format!("// {form}"),
49            phases: Self::phases_of(form),
50        })
51    }
52
53    fn form_of(comment: &str) -> Option<&'static str> {
54        Self::FORMS
55            .into_iter()
56            .find(|form| Self::is_form(comment, form))
57    }
58
59    // The rest may be nothing, or anything that does not continue the word.
60    fn is_form(comment: &str, form: &str) -> bool {
61        comment.strip_prefix(form).is_some_and(|rest| {
62            rest.chars()
63                .next()
64                .is_none_or(|next| !next.is_alphanumeric() && next != '_')
65        })
66    }
67
68    fn phases_of(form: &str) -> Vec<MarkerPhase> {
69        match form {
70            "Arrange & Act & Assert" => {
71                vec![MarkerPhase::Arrange, MarkerPhase::Act, MarkerPhase::Assert]
72            }
73            "Arrange & Act" => vec![MarkerPhase::Arrange, MarkerPhase::Act],
74            "Act & Assert" => vec![MarkerPhase::Act, MarkerPhase::Assert],
75            "Arrange" => vec![MarkerPhase::Arrange],
76            "Assert" => vec![MarkerPhase::Assert],
77            _ => vec![MarkerPhase::Act],
78        }
79    }
80}