sciforge 0.0.3

A comprehensive scientific computing library in pure Rust with zero dependencies
Documentation
use sciforge::benchmark::engine::{BenchmarkMetrics, bench};
use sciforge::benchmark::export::{Entry, export};
use std::fs;
use std::path::Path;
use std::process::Command;

mod acoustics;
mod dispatch;
mod electrodynamics;
mod electromagnetism;
mod electronics;
mod fluid_mechanics;
mod materials;
mod mechanics;
mod nucleosynthesis;
mod optics;
mod particle;
mod quantum;
mod relativity;
mod solid_mechanics;
mod thermodynamics;

fn sanitize_label(name: &str) -> String {
    let mut label = String::with_capacity(name.len());
    for ch in name.chars() {
        if ch.is_ascii_alphanumeric() {
            label.push(ch);
        } else {
            label.push('_');
        }
    }
    while label.contains("__") {
        label = label.replace("__", "_");
    }
    label.trim_matches('_').to_string()
}

fn make_symbol(fname: &str) -> String {
    let parts: Vec<&str> = fname.split('_').filter(|s| !s.is_empty()).collect();
    if parts.len() == 1 {
        let w = parts[0];
        let mut c = w.chars();
        let first = c.next().unwrap_or('?').to_ascii_uppercase();
        let second = c.next().unwrap_or('x').to_ascii_lowercase();
        format!("{first}{second}")
    } else {
        parts
            .iter()
            .take(3)
            .map(|w| w.chars().next().unwrap_or('?').to_ascii_uppercase())
            .collect()
    }
}

fn list_tests() -> Vec<(String, String, String, String, String)> {
    let exe = std::env::current_exe().unwrap();
    let output = Command::new(exe).arg("--list").output().unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    let mut tests = Vec::new();
    for line in stdout.lines() {
        let Some((name, kind)) = line.rsplit_once(": ") else {
            continue;
        };
        if kind.trim() != "test" {
            continue;
        }
        let test_name = name.trim();
        if test_name == "generate_output_files" {
            continue;
        }
        let module = test_name.split("::").next().unwrap_or("other").to_string();
        let label = sanitize_label(test_name);
        let fname = test_name.rsplit("::").next().unwrap_or(test_name);
        let readable = fname.replace('_', " ");
        let display_name = if readable.is_empty() {
            readable
        } else {
            let mut c = readable.chars();
            match c.next() {
                None => String::new(),
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
            }
        };
        let symbol = make_symbol(fname);
        tests.push((test_name.to_string(), module, label, display_name, symbol));
    }
    tests.sort();
    tests
}

fn assert_artifacts(domain: &str, dir: &Path, test_count: usize) {
    assert!(dir.join(format!("{domain}.html")).exists());
    assert!(dir.join(format!("{domain}.md")).exists());
    assert!(dir.join("csv/all.csv").exists());
    assert!(dir.join("json/summary.json").exists());
    assert!(dir.join("yaml/summary.yaml").exists());
    assert!(dir.join("toml/summary.toml").exists());
    let count_files = |p: &Path| -> usize {
        fs::read_dir(p)
            .unwrap()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
            .count()
    };
    let has_entries = test_count > 0;
    assert!(count_files(&dir.join("csv")) >= if has_entries { 2 } else { 1 });
    assert!(count_files(&dir.join("json")) > test_count);
    assert!(count_files(&dir.join("yaml")) >= if has_entries { 2 } else { 1 });
    assert!(count_files(&dir.join("toml")) > test_count);
    assert!(count_files(&dir.join("bmk")) >= 1);
}

fn export_outputs(domain: &'static str, title: &'static str) -> (std::path::PathBuf, usize) {
    let tests = list_tests();
    let test_count = tests.len();
    let exe = std::env::current_exe().unwrap();

    let mut module_order: Vec<String> = Vec::new();
    let mut module_counts: Vec<u8> = Vec::new();
    let mut positions: Vec<(u8, u8)> = Vec::new();
    for (_, module, _, _, _) in &tests {
        let mi = match module_order.iter().position(|m| m == module) {
            Some(p) => p,
            None => {
                module_order.push(module.clone());
                module_counts.push(0);
                module_order.len() - 1
            }
        };
        module_counts[mi] += 1;
        positions.push(((mi as u8) + 1, module_counts[mi]));
    }

    let metrics: Vec<BenchmarkMetrics<'_>> = tests
        .iter()
        .map(|(test_name, _, _, _, _)| {
            bench(test_name, "f64", 1, || {
                let _ = Command::new(&exe)
                    .args(["--exact", test_name, "--test-threads=1", "-q"])
                    .output();
            })
        })
        .collect();
    let entries: Vec<Entry<'_>> = metrics
        .iter()
        .zip(tests.iter())
        .zip(positions.iter())
        .map(
            |((metric, (test_name, module, label, display_name, symbol)), &(row, col))| Entry {
                metrics: metric,
                result: "pass",
                label,
                tags: vec![
                    ("category", module.as_str()),
                    ("symbol", symbol.as_str()),
                    ("name", display_name.as_str()),
                    ("domain", domain),
                    ("test", test_name.as_str()),
                ],
                grid_row: Some(row),
                grid_col: Some(col),
            },
        )
        .collect();
    let dir = Path::new("output/tests").join(domain);
    export(title, &entries, &dir).unwrap();
    (dir, test_count)
}

#[test]
fn generate_output_files() {
    let (dir, test_count) = export_outputs("physics", "SciForge Physics Test");
    assert_artifacts("physics", &dir, test_count);
}