Skip to main content

get_skipper/
lib.rs

1//! Skipper for Rust: enable or disable tests from a Google Spreadsheet.
2//!
3//! ```ignore
4//! use get_skipper::skipper;
5//!
6//! #[skipper]
7//! #[test]
8//! fn can_log_in() { /* … */ }
9//! ```
10//!
11//! Configure with environment variables (`SKIPPER_SPREADSHEET_ID`,
12//! `GOOGLE_CREDS_B64` or `SKIPPER_CREDENTIALS_FILE`, `SKIPPER_SHEET_NAME`),
13//! e.g. in `.cargo/config.toml` under `[env]`. Without them every test runs.
14//! Run through `cargo skipper test` (crate `cargo-skipper`) to fetch the sheet
15//! once per run, get the quarantine report and use `SKIPPER_MODE=sync`.
16
17use std::io::Write;
18use std::path::Path;
19use std::sync::OnceLock;
20
21pub use skipper_core::*;
22pub use skipper_macros::skipper;
23
24/// Skip the current test if it is disabled. For code where the `#[skipper]`
25/// attribute cannot go; must be called directly in the test function body.
26/// Expands to an early `return` (use `skip_if_disabled!(Ok(()))` in tests
27/// returning `Result`).
28#[macro_export]
29macro_rules! skip_if_disabled {
30    () => {
31        $crate::skip_if_disabled!(())
32    };
33    ($ret:expr) => {{
34        fn __skipper_here() {}
35        let name = $crate::__fn_name(::core::any::type_name_of_val(&__skipper_here));
36        if $crate::__should_skip(::core::file!(), ::core::module_path!(), name) {
37            return $ret;
38        }
39    }};
40}
41
42#[doc(hidden)]
43pub fn __fn_name(type_name: &str) -> &str {
44    // "krate::module::test_fn::__skipper_here" (maybe with "::{{closure}}" for async).
45    type_name
46        .split("::")
47        .filter(|s| *s != "__skipper_here" && !s.starts_with("{{"))
48        .last()
49        .unwrap_or(type_name)
50}
51
52fn resolver() -> Option<&'static SkipperResolver> {
53    static RESOLVER: OnceLock<Option<SkipperResolver>> = OnceLock::new();
54    RESOLVER
55        .get_or_init(|| match SkipperResolver::from_env() {
56            Ok(r) => r,
57            // Only reached with SKIPPER_FAIL_OPEN=false: make the test fail loudly.
58            Err(e) => panic!("[skipper] cannot load spreadsheet: {e}"),
59        })
60        .as_ref()
61}
62
63/// Test ID used by `#[skipper]`: `file > module::path > fn` (crate name dropped).
64pub fn test_id(file: &str, module_path: &str, name: &str) -> String {
65    let modules = module_path.split_once("::").map(|(_, rest)| rest);
66    match modules {
67        Some(m) if !m.is_empty() => build_test_id(file, &[m, name]),
68        _ => build_test_id(file, &[name]),
69    }
70}
71
72#[doc(hidden)]
73pub fn __should_skip(file: &str, module_path: &str, name: &str) -> bool {
74    let Some(resolver) = resolver() else {
75        return false;
76    };
77    let id = test_id(file, module_path, name);
78    if let Some(dir) = std::env::var_os("SKIPPER_DISCOVERED_DIR") {
79        if let Err(e) =
80            CacheManager::write_discovered_ids(Path::new(&dir), std::slice::from_ref(&id))
81        {
82            warn(format!("cannot record discovered test {id}: {e}"));
83        }
84    }
85    if resolver.is_test_enabled(&id) {
86        return false;
87    }
88    // Straight to the stderr handle: libtest captures eprintln! of passing tests.
89    let _ = writeln!(std::io::stderr(), "{}", resolver.skip_message(&id));
90    true
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_id_drops_crate_name() {
99        assert_eq!(
100            test_id("tests/auth.rs", "auth", "login"),
101            "tests/auth.rs > login"
102        );
103        assert_eq!(
104            test_id("src/lib.rs", "my_crate::a::tests", "works"),
105            "src/lib.rs > a::tests > works"
106        );
107    }
108
109    #[test]
110    fn fn_name_from_type_name() {
111        assert_eq!(__fn_name("k::m::my_test::__skipper_here"), "my_test");
112        assert_eq!(
113            __fn_name("k::my_test::{{closure}}::__skipper_here"),
114            "my_test"
115        );
116    }
117}