tytanic_core/test/annotation.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
//! Test annotations are used to add information to a test for `tytanic` to pick
//! up on.
//!
//! Annotations may be placed on a leading doc comment block (indicated by
//! `///`), such a doc comment block can be placed after initial empty or
//! regular comment lines, but must come before any content. All annotations in
//! such a block must be at the start, once non-annotation content is
//! encountered parsing stops.
//!
//! ```typst
//! // SPDX-License-Identifier: MIT
//!
//! /// [skip]
//! ///
//! /// Synopsis:
//! /// ...
//!
//! #set page("a4")
//! ...
//! ```
use std::str::FromStr;
use ecow::{EcoString, EcoVec};
use thiserror::Error;
/// An error which may occur while parsing an annotation.
#[derive(Debug, Error)]
pub enum ParseAnnotationError {
/// The delimiter were missing or unclosed.
#[error("the annotation had only one or no delimiter")]
MissingDelimiter,
/// The annotation identifier is unknown, invalid or empty.
#[error("unknown or invalid annotation identifier: {0:?}")]
Unknown(EcoString),
/// The annotation was otherwise malformed.
#[error("the annotation was malformed")]
Other,
}
/// A test annotation used to configure test specific behavior.
///
/// Test annotations are placed on doc comments at the top of a test's source
/// file:
///
/// Each annotation is on it's own line.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Annotation {
/// The ignored annotation, this can be used to exclude a test by virtue of
/// the `ignored` test set.
Skip,
}
impl Annotation {
/// Collects all annotations found within a test's source code.
pub fn collect(source: &str) -> Result<EcoVec<Self>, ParseAnnotationError> {
// skip regular comments and leading empty lines
let lines = source.lines().skip_while(|line| {
line.strip_prefix("//")
.is_some_and(|rest| !rest.starts_with('/'))
|| line.trim().is_empty()
});
// then collect all consecutive doc comment lines
let lines = lines.map_while(|line| line.strip_prefix("///").map(str::trim));
// ignore empty ones
let lines = lines.filter(|line| !line.is_empty());
// take only those which start with an annotation deimiter
let lines = lines.take_while(|line| line.starts_with('['));
lines.map(str::parse).collect()
}
}
impl FromStr for Annotation {
type Err = ParseAnnotationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some(rest) = s.strip_prefix('[') else {
return Err(ParseAnnotationError::MissingDelimiter);
};
let Some(rest) = rest.strip_suffix(']') else {
return Err(ParseAnnotationError::MissingDelimiter);
};
let id = rest.trim();
match id {
"skip" => Ok(Annotation::Skip),
_ => Err(ParseAnnotationError::Unknown(id.into())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_annotation_from_str() {
assert_eq!(Annotation::from_str("[skip]").unwrap(), Annotation::Skip);
assert_eq!(Annotation::from_str("[ skip ]").unwrap(), Annotation::Skip);
assert!(Annotation::from_str("[ skip ").is_err());
assert!(Annotation::from_str("[unknown]").is_err());
}
#[test]
fn test_collect_book_example() {
let source = "\
/// [skip] \n\
/// \n\
/// Synopsis: \n\
/// ... \n\
\n\
#import \"/src/internal.typ\": foo \n\
...";
assert_eq!(Annotation::collect(source).unwrap(), [Annotation::Skip]);
}
#[test]
fn test_collect_issue_109() {
assert_eq!(
Annotation::collect("///[skip]").unwrap(),
[Annotation::Skip]
);
assert_eq!(Annotation::collect("///").unwrap(), []);
assert_eq!(
Annotation::collect("/// [skip]").unwrap(),
[Annotation::Skip]
);
assert_eq!(
Annotation::collect("///[skip]\n///").unwrap(),
[Annotation::Skip]
);
}
}