Skip to main content

executable_stories/
collector.rs

1use std::sync::Mutex;
2
3use crate::json_writer;
4use crate::types::{RawCIInfo, RawRun, RawTestCase};
5
6static COLLECTED: Mutex<Vec<RawTestCase>> = Mutex::new(Vec::new());
7static ORDER_SEQ: Mutex<u32> = Mutex::new(0);
8
9/// Record a completed test case into the global collector.
10pub fn record(tc: RawTestCase) {
11    COLLECTED.lock().unwrap().push(tc);
12}
13
14/// Get the next source order value and increment the sequence.
15pub fn next_order() -> u32 {
16    let mut seq = ORDER_SEQ.lock().unwrap();
17    let val = *seq;
18    *seq += 1;
19    val
20}
21
22/// Get a clone of all collected test cases.
23pub fn get_all() -> Vec<RawTestCase> {
24    COLLECTED.lock().unwrap().clone()
25}
26
27fn detect_ci() -> Option<RawCIInfo> {
28    if std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") {
29        let url = match (
30            std::env::var("GITHUB_SERVER_URL"),
31            std::env::var("GITHUB_REPOSITORY"),
32            std::env::var("GITHUB_RUN_ID"),
33        ) {
34            (Ok(s), Ok(r), Ok(id)) => Some(format!("{s}/{r}/actions/runs/{id}")),
35            _ => None,
36        };
37        return Some(RawCIInfo {
38            name: "github".to_string(),
39            build_number: std::env::var("GITHUB_RUN_NUMBER").ok(),
40            url,
41        });
42    }
43    if std::env::var("CIRCLECI").as_deref() == Ok("true") {
44        return Some(RawCIInfo {
45            name: "circleci".to_string(),
46            build_number: std::env::var("CIRCLE_BUILD_NUM").ok(),
47            url: std::env::var("CIRCLE_BUILD_URL").ok(),
48        });
49    }
50    if std::env::var("JENKINS_URL").is_ok() {
51        return Some(RawCIInfo {
52            name: "jenkins".to_string(),
53            build_number: std::env::var("BUILD_NUMBER").ok(),
54            url: std::env::var("BUILD_URL").ok(),
55        });
56    }
57    if std::env::var("TRAVIS").as_deref() == Ok("true") {
58        return Some(RawCIInfo {
59            name: "travis".to_string(),
60            build_number: std::env::var("TRAVIS_BUILD_NUMBER").ok(),
61            url: std::env::var("TRAVIS_BUILD_WEB_URL").ok(),
62        });
63    }
64    if std::env::var("GITLAB_CI").as_deref() == Ok("true") {
65        return Some(RawCIInfo {
66            name: "gitlab".to_string(),
67            build_number: std::env::var("CI_PIPELINE_IID").ok(),
68            url: std::env::var("CI_PIPELINE_URL").ok(),
69        });
70    }
71    if std::env::var("CI").as_deref() == Ok("true") {
72        return Some(RawCIInfo { name: "ci".to_string(), build_number: None, url: None });
73    }
74    None
75}
76
77/// Write all collected results to JSON.
78///
79/// The output path is determined by the `EXECUTABLE_STORIES_OUTPUT` environment variable,
80/// defaulting to `.executable-stories/raw-run.json`.
81///
82/// Call this at the end of your test suite.
83///
84/// # Panics
85///
86/// Panics if writing the JSON file fails (e.g. permission or I/O error).
87#[allow(clippy::missing_panics_doc)]
88pub fn write_results() {
89    let cases = get_all();
90    if cases.is_empty() {
91        return;
92    }
93
94    let output = std::env::var("EXECUTABLE_STORIES_OUTPUT")
95        .unwrap_or_else(|_| ".executable-stories/raw-run.json".to_string());
96
97    let cwd = std::env::current_dir().map(|p| p.to_string_lossy().to_string()).unwrap_or_default();
98
99    let run = RawRun {
100        schema_version: 1,
101        test_cases: cases,
102        project_root: cwd,
103        started_at_ms: None,
104        finished_at_ms: None,
105        ci: detect_ci(),
106    };
107
108    json_writer::write_raw_run(&run, &output).expect("Failed to write raw run JSON");
109}
110
111/// Reset the collector state. Useful for testing.
112#[allow(dead_code)]
113pub fn reset() {
114    COLLECTED.lock().unwrap().clear();
115    *ORDER_SEQ.lock().unwrap() = 0;
116}