Skip to main content

use_go_test/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned by Go testing metadata constructors.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum GoTestError {
10    EmptyName,
11    InvalidTestName,
12    InvalidBenchmarkName,
13    InvalidFuzzTestName,
14    InvalidExampleName,
15    EmptyFileName,
16    InvalidTestFileName,
17    UnknownLabel,
18}
19
20impl fmt::Display for GoTestError {
21    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::EmptyName => formatter.write_str("Go test name cannot be empty"),
24            Self::InvalidTestName => formatter.write_str("Go test name must start with `Test`"),
25            Self::InvalidBenchmarkName => {
26                formatter.write_str("Go benchmark name must start with `Benchmark`")
27            }
28            Self::InvalidFuzzTestName => {
29                formatter.write_str("Go fuzz test name must start with `Fuzz`")
30            }
31            Self::InvalidExampleName => {
32                formatter.write_str("Go example name must start with `Example`")
33            }
34            Self::EmptyFileName => formatter.write_str("Go test file name cannot be empty"),
35            Self::InvalidTestFileName => {
36                formatter.write_str("Go test file name should end in `_test.go`")
37            }
38            Self::UnknownLabel => formatter.write_str("unknown Go test metadata label"),
39        }
40    }
41}
42
43impl Error for GoTestError {}
44
45macro_rules! prefixed_name_type {
46    ($name:ident, $prefix:literal, $error:ident) => {
47        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48        pub struct $name(String);
49
50        impl $name {
51            /// Creates Go testing name metadata.
52            ///
53            /// # Errors
54            ///
55            /// Returns [`GoTestError`] when the name is empty or has the wrong Go testing prefix.
56            pub fn new(value: impl AsRef<str>) -> Result<Self, GoTestError> {
57                let trimmed = value.as_ref().trim();
58                if trimmed.is_empty() {
59                    return Err(GoTestError::EmptyName);
60                }
61                if !trimmed.starts_with($prefix) {
62                    return Err(GoTestError::$error);
63                }
64                Ok(Self(trimmed.to_string()))
65            }
66
67            /// Returns the testing name.
68            #[must_use]
69            pub fn as_str(&self) -> &str {
70                &self.0
71            }
72        }
73
74        impl AsRef<str> for $name {
75            fn as_ref(&self) -> &str {
76                self.as_str()
77            }
78        }
79
80        impl fmt::Display for $name {
81            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82                formatter.write_str(self.as_str())
83            }
84        }
85
86        impl FromStr for $name {
87            type Err = GoTestError;
88
89            fn from_str(value: &str) -> Result<Self, Self::Err> {
90                Self::new(value)
91            }
92        }
93
94        impl TryFrom<&str> for $name {
95            type Error = GoTestError;
96
97            fn try_from(value: &str) -> Result<Self, Self::Error> {
98                Self::new(value)
99            }
100        }
101    };
102}
103
104prefixed_name_type!(GoTestName, "Test", InvalidTestName);
105prefixed_name_type!(GoBenchmarkName, "Benchmark", InvalidBenchmarkName);
106prefixed_name_type!(GoFuzzTestName, "Fuzz", InvalidFuzzTestName);
107prefixed_name_type!(GoExampleName, "Example", InvalidExampleName);
108
109/// Validated Go test file-name metadata.
110#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub struct GoTestFileName(String);
112
113impl GoTestFileName {
114    /// Creates Go test file-name metadata.
115    ///
116    /// # Errors
117    ///
118    /// Returns [`GoTestError`] when the file name is empty or does not end in `_test.go`.
119    pub fn new(value: impl AsRef<str>) -> Result<Self, GoTestError> {
120        let trimmed = value.as_ref().trim();
121        if trimmed.is_empty() {
122            return Err(GoTestError::EmptyFileName);
123        }
124        if !trimmed.ends_with("_test.go") {
125            return Err(GoTestError::InvalidTestFileName);
126        }
127        Ok(Self(trimmed.to_string()))
128    }
129
130    /// Returns the file name.
131    #[must_use]
132    pub fn as_str(&self) -> &str {
133        &self.0
134    }
135}
136
137impl AsRef<str> for GoTestFileName {
138    fn as_ref(&self) -> &str {
139        self.as_str()
140    }
141}
142
143impl fmt::Display for GoTestFileName {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        formatter.write_str(self.as_str())
146    }
147}
148
149impl FromStr for GoTestFileName {
150    type Err = GoTestError;
151
152    fn from_str(value: &str) -> Result<Self, Self::Err> {
153        Self::new(value)
154    }
155}
156
157impl TryFrom<&str> for GoTestFileName {
158    type Error = GoTestError;
159
160    fn try_from(value: &str) -> Result<Self, Self::Error> {
161        Self::new(value)
162    }
163}
164
165/// Go test outcome metadata.
166#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub enum GoTestOutcome {
168    Passed,
169    Failed,
170    Skipped,
171    Panicked,
172    TimedOut,
173}
174
175impl GoTestOutcome {
176    /// Returns the outcome label.
177    #[must_use]
178    pub const fn as_str(self) -> &'static str {
179        match self {
180            Self::Passed => "passed",
181            Self::Failed => "failed",
182            Self::Skipped => "skipped",
183            Self::Panicked => "panicked",
184            Self::TimedOut => "timed-out",
185        }
186    }
187}
188
189impl fmt::Display for GoTestOutcome {
190    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
191        formatter.write_str(self.as_str())
192    }
193}
194
195impl FromStr for GoTestOutcome {
196    type Err = GoTestError;
197
198    fn from_str(value: &str) -> Result<Self, Self::Err> {
199        match normalized_label(value)?.as_str() {
200            "passed" | "pass" => Ok(Self::Passed),
201            "failed" | "fail" => Ok(Self::Failed),
202            "skipped" | "skip" => Ok(Self::Skipped),
203            "panicked" | "panic" => Ok(Self::Panicked),
204            "timed-out" | "timed_out" | "timed out" | "timeout" => Ok(Self::TimedOut),
205            _ => Err(GoTestError::UnknownLabel),
206        }
207    }
208}
209
210/// Go test kind metadata.
211#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
212pub enum GoTestKind {
213    Test,
214    Benchmark,
215    Fuzz,
216    Example,
217}
218
219impl GoTestKind {
220    /// Returns the kind label.
221    #[must_use]
222    pub const fn as_str(self) -> &'static str {
223        match self {
224            Self::Test => "test",
225            Self::Benchmark => "benchmark",
226            Self::Fuzz => "fuzz",
227            Self::Example => "example",
228        }
229    }
230}
231
232impl fmt::Display for GoTestKind {
233    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234        formatter.write_str(self.as_str())
235    }
236}
237
238impl FromStr for GoTestKind {
239    type Err = GoTestError;
240
241    fn from_str(value: &str) -> Result<Self, Self::Err> {
242        match normalized_label(value)?.as_str() {
243            "test" => Ok(Self::Test),
244            "benchmark" | "bench" => Ok(Self::Benchmark),
245            "fuzz" => Ok(Self::Fuzz),
246            "example" => Ok(Self::Example),
247            _ => Err(GoTestError::UnknownLabel),
248        }
249    }
250}
251
252/// Go test package mode metadata.
253#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
254pub enum GoTestPackageMode {
255    Package,
256    ExternalPackage,
257}
258
259impl GoTestPackageMode {
260    /// Returns the package mode label.
261    #[must_use]
262    pub const fn as_str(self) -> &'static str {
263        match self {
264            Self::Package => "package",
265            Self::ExternalPackage => "external-package",
266        }
267    }
268}
269
270impl fmt::Display for GoTestPackageMode {
271    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272        formatter.write_str(self.as_str())
273    }
274}
275
276impl FromStr for GoTestPackageMode {
277    type Err = GoTestError;
278
279    fn from_str(value: &str) -> Result<Self, Self::Err> {
280        match normalized_label(value)?.as_str() {
281            "package" => Ok(Self::Package),
282            "external-package" | "external_package" | "external package" => {
283                Ok(Self::ExternalPackage)
284            }
285            _ => Err(GoTestError::UnknownLabel),
286        }
287    }
288}
289
290fn normalized_label(value: &str) -> Result<String, GoTestError> {
291    let trimmed = value.trim();
292    if trimmed.is_empty() {
293        Err(GoTestError::UnknownLabel)
294    } else {
295        Ok(trimmed.to_ascii_lowercase())
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::{
302        GoBenchmarkName, GoExampleName, GoFuzzTestName, GoTestError, GoTestFileName, GoTestKind,
303        GoTestName, GoTestOutcome, GoTestPackageMode,
304    };
305
306    #[test]
307    fn validates_test_names() -> Result<(), GoTestError> {
308        assert_eq!(GoTestName::new("TestHandler")?.as_str(), "TestHandler");
309        assert_eq!(
310            GoBenchmarkName::new("BenchmarkServe")?.as_str(),
311            "BenchmarkServe"
312        );
313        assert_eq!(GoFuzzTestName::new("FuzzParser")?.as_str(), "FuzzParser");
314        assert_eq!(
315            GoExampleName::new("ExampleClient")?.as_str(),
316            "ExampleClient"
317        );
318        assert_eq!(
319            GoTestName::new("Handler"),
320            Err(GoTestError::InvalidTestName)
321        );
322        assert_eq!(
323            GoBenchmarkName::new("BenchServe"),
324            Err(GoTestError::InvalidBenchmarkName)
325        );
326        Ok(())
327    }
328
329    #[test]
330    fn validates_test_file_names() -> Result<(), GoTestError> {
331        let file = GoTestFileName::new("handler_test.go")?;
332        assert_eq!(file.as_str(), "handler_test.go");
333        assert_eq!(GoTestFileName::new(""), Err(GoTestError::EmptyFileName));
334        assert_eq!(
335            GoTestFileName::new("handler.go"),
336            Err(GoTestError::InvalidTestFileName)
337        );
338        Ok(())
339    }
340
341    #[test]
342    fn parses_test_enums() -> Result<(), GoTestError> {
343        assert_eq!("timeout".parse::<GoTestOutcome>()?, GoTestOutcome::TimedOut);
344        assert_eq!("bench".parse::<GoTestKind>()?, GoTestKind::Benchmark);
345        assert_eq!(
346            "external package".parse::<GoTestPackageMode>()?,
347            GoTestPackageMode::ExternalPackage
348        );
349        assert_eq!(GoTestKind::Test.to_string(), "test");
350        Ok(())
351    }
352}