projd-core 0.6.0

Core project scanning data model and analysis helpers for Projd.
Documentation
use std::fs;
use std::path::Path;

use projd_core::{
    DiscoverOptions, render_html, render_multi_html, scan_path, scan_paths_recursive,
};
use tempfile::tempdir;

fn write_minimal_cargo_project(dir: &Path, name: &str) {
    fs::create_dir_all(dir.join("src")).unwrap();
    fs::write(
        dir.join("Cargo.toml"),
        format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\n"),
    )
    .unwrap();
    fs::write(dir.join("src/lib.rs"), "pub fn hello() {}\n").unwrap();
    fs::write(dir.join("README.md"), format!("# {name}\n")).unwrap();
    fs::write(dir.join("LICENSE"), "MIT\n").unwrap();
}

#[test]
fn render_html_has_doctype_and_charset() {
    let tmp = tempdir().unwrap();
    write_minimal_cargo_project(tmp.path(), "demo");
    let scan = scan_path(tmp.path()).unwrap();
    let html = render_html(&scan);
    assert!(html.starts_with("<!DOCTYPE html>"), "{}", &html[..80]);
    assert!(html.contains("<meta charset=\"utf-8\">"));
    assert!(html.contains("<title>"));
}

#[test]
fn render_html_includes_project_name() {
    let tmp = tempdir().unwrap();
    write_minimal_cargo_project(tmp.path(), "demo-html");
    let scan = scan_path(tmp.path()).unwrap();
    let html = render_html(&scan);
    assert!(
        html.contains("demo-html"),
        "expected project name in HTML output"
    );
}

#[test]
fn render_html_escapes_user_input() {
    let tmp = tempdir().unwrap();
    // Use a tricky directory name that will flow into project_name fallback.
    let weird = tmp.path().join("with<script>name");
    fs::create_dir_all(&weird).unwrap();
    fs::write(weird.join("README.md"), "# nope\n").unwrap();
    let scan = scan_path(&weird).unwrap();
    let html = render_html(&scan);
    assert!(
        html.contains("with&lt;script&gt;name"),
        "name should be HTML-escaped"
    );
    assert!(
        !html.contains("with<script>name"),
        "raw <script> must not appear unescaped"
    );
}

#[test]
fn render_html_includes_health_score() {
    let tmp = tempdir().unwrap();
    write_minimal_cargo_project(tmp.path(), "demo");
    let scan = scan_path(tmp.path()).unwrap();
    let html = render_html(&scan);
    let needle = format!(">{}</text>", scan.health.score);
    assert!(
        html.contains(&needle),
        "expected health score `{}` in SVG ring",
        scan.health.score
    );
}

#[test]
fn render_multi_html_lists_all_roots() {
    let tmp = tempdir().unwrap();
    for name in ["alpha", "beta"] {
        write_minimal_cargo_project(&tmp.path().join(name), name);
    }
    let multi = scan_paths_recursive(tmp.path(), &DiscoverOptions::default()).unwrap();
    assert_eq!(multi.roots.len(), 2);
    let html = render_multi_html(&multi);
    assert!(html.contains("alpha"), "expected 'alpha' in output");
    assert!(html.contains("beta"), "expected 'beta' in output");
}

#[test]
fn render_multi_html_uses_details_collapse() {
    let tmp = tempdir().unwrap();
    write_minimal_cargo_project(&tmp.path().join("alpha"), "alpha");
    let multi = scan_paths_recursive(tmp.path(), &DiscoverOptions::default()).unwrap();
    let html = render_multi_html(&multi);
    assert!(
        html.contains("<details>"),
        "must use <details> for collapse"
    );
    assert!(html.contains("<summary>"), "must use <summary>");
    assert!(
        !html.contains("<details open"),
        "details must default to collapsed (no `open` attribute)"
    );
}