tesults-test 1.0.0

Rust test integration for Tesults — enhanced test reporting with #[tesults_test::test]
Documentation
pub use tesults_test_macros::test;

use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};

thread_local! {
    static PENDING_DESC:   RefCell<String>                  = RefCell::new(String::new());
    static PENDING_STEPS:  RefCell<Vec<tesults::Step>>      = RefCell::new(Vec::new());
    static PENDING_CUSTOM: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
    static PENDING_FILES:  RefCell<Vec<String>>             = RefCell::new(Vec::new());
}

static RESULTS: Mutex<Vec<tesults::Case>> = Mutex::new(Vec::new());

/// Set a description for the currently-running test.
pub fn description(desc: &str) {
    PENDING_DESC.with(|d| *d.borrow_mut() = desc.to_string());
}

/// Attach a custom key-value field to the currently-running test.
pub fn custom(key: &str, value: &str) {
    PENDING_CUSTOM.with(|c| {
        c.borrow_mut().insert(key.to_string(), value.to_string());
    });
}

/// Record a step for the currently-running test.
/// `result` should be `"pass"` or `"fail"`.
pub fn step(name: &str, result: &str, desc: &str, reason: &str) {
    PENDING_STEPS.with(|s| {
        s.borrow_mut().push(tesults::Step {
            name: name.to_string(),
            result: result.to_string(),
            desc: desc.to_string(),
            reason: reason.to_string(),
        });
    });
}

/// Attach a file path to the currently-running test (uploaded to Tesults).
pub fn file(path: &str) {
    PENDING_FILES.with(|f| f.borrow_mut().push(path.to_string()));
}

#[ctor::dtor]
fn upload_on_exit() {
    let cases: Vec<tesults::Case> = {
        let mut guard = match RESULTS.lock() {
            Ok(g) => g,
            Err(e) => e.into_inner(),
        };
        std::mem::take(&mut *guard)
    };

    if cases.is_empty() {
        return;
    }

    let target_raw = match std::env::var("TESULTS_TARGET") {
        Ok(v) if !v.is_empty() => v,
        _ => return,
    };

    let target = lookup_config(&target_raw);

    let data = tesults::Data {
        target,
        cases,
        integration_name: String::from("tesults-test"),
        integration_version: String::from(env!("CARGO_PKG_VERSION")),
        test_framework: String::from("rust"),
    };

    eprintln!("tesults-test: uploading results...");
    let response = tesults::upload(data);
    eprintln!("tesults-test: success: {}", response.success);
    eprintln!("tesults-test: message: {}", response.message);
    eprintln!("tesults-test: warnings: {:?}", response.warnings);
    eprintln!("tesults-test: errors: {:?}", response.errors);
}

fn lookup_config(key: &str) -> String {
    if let Ok(config_path) = std::env::var("TESULTS_CONFIG") {
        if let Ok(content) = std::fs::read_to_string(&config_path) {
            for line in content.lines() {
                let line = line.trim();
                if line.starts_with('#') || line.is_empty() {
                    continue;
                }
                if let Some((k, v)) = line.split_once('=') {
                    if k.trim() == key {
                        return v.trim().to_string();
                    }
                }
            }
        }
    }
    key.to_string()
}

pub mod __private {
    use super::*;

    pub fn now_ms() -> i64 {
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis() as i64)
            .unwrap_or(0)
    }

    pub fn record_result(
        name: &str,
        module: &str,
        passed: bool,
        reason: String,
        start: i64,
        end: i64,
    ) {
        let desc = PENDING_DESC.with(|d| d.borrow().clone());
        let steps = PENDING_STEPS.with(|s| s.borrow().clone());
        let custom = PENDING_CUSTOM.with(|c| c.borrow().clone());
        let files = PENDING_FILES.with(|f| f.borrow().clone());

        PENDING_DESC.with(|d| d.borrow_mut().clear());
        PENDING_STEPS.with(|s| s.borrow_mut().clear());
        PENDING_CUSTOM.with(|c| c.borrow_mut().clear());
        PENDING_FILES.with(|f| f.borrow_mut().clear());

        let suite = module.split("::").last().unwrap_or(module).to_string();

        let case = tesults::Case {
            name: name.to_string(),
            result: if passed {
                String::from("pass")
            } else {
                String::from("fail")
            },
            suite,
            desc,
            reason,
            start,
            end,
            files,
            custom,
            steps,
            ..Default::default()
        };

        if let Ok(mut guard) = RESULTS.lock() {
            guard.push(case);
        }
    }
}