1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub struct GoTestFileName(String);
112
113impl GoTestFileName {
114 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 #[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#[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 #[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#[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
254pub enum GoTestPackageMode {
255 Package,
256 ExternalPackage,
257}
258
259impl GoTestPackageMode {
260 #[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}