kcode-web-libs 0.2.0

Managed plain HTML, CSS, and JavaScript libraries with browser checks and immutable publication
Documentation
use kcode_web_libs::{File, Lib, create, docs, open};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;

fn source_root(root: &TempDir) -> PathBuf {
    root.path().join("source")
}

fn publications_root(root: &TempDir) -> PathBuf {
    root.path().join("publications")
}

fn contents_mut<'a>(library: &'a mut Lib, path: &str) -> &'a mut String {
    &mut library
        .files
        .iter_mut()
        .find(|file| file.path == path)
        .unwrap()
        .contents
}

#[test]
fn create_open_and_docs_return_the_complete_sorted_source() {
    let root = TempDir::new().unwrap();
    let created = create(
        source_root(&root),
        publications_root(&root),
        "Example_lib-2",
    )
    .unwrap();
    assert_eq!(
        created
            .files
            .iter()
            .map(|file| file.path.as_str())
            .collect::<Vec<_>>(),
        ["Documentation.md", "index.js", "kcode-web.json", "tests.js"]
    );

    let opened = open(
        source_root(&root),
        publications_root(&root),
        "Example_lib-2",
    )
    .unwrap();
    assert_eq!(opened.files, created.files);
    let (version, documentation) = docs(source_root(&root), "Example_lib-2").unwrap();
    assert_eq!(version, "0.1.0");
    assert!(documentation.starts_with("# Example_lib-2\n"));
}

#[test]
fn source_and_publication_roots_must_be_disjoint() {
    let root = TempDir::new().unwrap();
    let source = source_root(&root);
    let nested_publications = source.join(".published");
    let error = create(&source, &nested_publications, "nested").unwrap_err();
    assert_eq!(
        error.to_string(),
        "invalid_publication_storage: source and publication roots must be disjoint"
    );

    let shared = root.path().join("shared");
    let error = create(&shared, &shared, "shared").unwrap_err();
    assert_eq!(
        error.to_string(),
        "invalid_publication_storage: source and publication roots must be disjoint"
    );
}

#[test]
fn complete_replacement_removes_omitted_files_and_old_generations() {
    let root = TempDir::new().unwrap();
    let mut library = create(source_root(&root), publications_root(&root), "demo").unwrap();
    library.files.push(File {
        path: "temporary.js".to_owned(),
        contents: "export {};".to_owned(),
    });
    library.write().unwrap();
    library.files.retain(|file| file.path != "temporary.js");
    *contents_mut(&mut library, "Documentation.md") = "second write\n".to_owned();
    library.write().unwrap();

    let reopened = open(source_root(&root), publications_root(&root), "demo").unwrap();
    assert!(
        reopened
            .files
            .iter()
            .all(|file| file.path != "temporary.js")
    );
    assert_eq!(
        reopened
            .files
            .iter()
            .find(|file| file.path == "Documentation.md")
            .unwrap()
            .contents,
        "second write\n"
    );
    assert_eq!(
        fs::read_dir(source_root(&root).join("demo/generations"))
            .unwrap()
            .count(),
        1
    );
}

#[test]
fn two_writers_from_one_snapshot_cannot_both_commit() {
    let root = TempDir::new().unwrap();
    create(source_root(&root), publications_root(&root), "demo").unwrap();
    let mut first = open(source_root(&root), publications_root(&root), "demo").unwrap();
    let mut second = open(source_root(&root), publications_root(&root), "demo").unwrap();
    *contents_mut(&mut first, "Documentation.md") = "first\n".to_owned();
    *contents_mut(&mut second, "Documentation.md") = "second\n".to_owned();

    first.write().unwrap();
    let error = second.write().unwrap_err();
    assert!(error.to_string().starts_with("stale_snapshot:"));
    assert_eq!(docs(source_root(&root), "demo").unwrap().1, "first\n");
}

#[test]
fn invalid_paths_and_metadata_fail_before_commit() {
    let root = TempDir::new().unwrap();
    let mut library = create(source_root(&root), publications_root(&root), "demo").unwrap();
    let original = library.files.clone();

    library.files.push(File {
        path: "../escape".to_owned(),
        contents: "bad".to_owned(),
    });
    assert!(library.write().is_err());
    library.files = original;

    *contents_mut(&mut library, "kcode-web.json") =
        r#"{"name":"demo","version":"1.2.3-beta","entry":"index.js","tests":"tests.js"}"#
            .to_owned();
    assert!(library.write().is_err());
    assert_eq!(docs(source_root(&root), "demo").unwrap().0, "0.1.0");
}

#[test]
fn check_uses_current_in_memory_source_without_requiring_a_write() {
    let root = TempDir::new().unwrap();
    let mut library = create(source_root(&root), publications_root(&root), "demo").unwrap();
    library.files.push(File {
        path: "../not-committed".to_owned(),
        contents: "bad".to_owned(),
    });

    let error = library.check().unwrap_err();
    assert!(error.to_string().starts_with("invalid_source:"));
}

#[test]
fn publish_rechecks_the_current_in_memory_source() {
    let root = TempDir::new().unwrap();
    let mut library = create(source_root(&root), publications_root(&root), "demo").unwrap();
    *contents_mut(&mut library, "kcode-web.json") =
        r#"{"name":"demo","version":"1.2.3-beta","entry":"index.js","tests":"tests.js"}"#
            .to_owned();

    let error = library.publish().unwrap_err();
    assert!(error.to_string().starts_with("invalid_source:"));
    assert!(!publications_root(&root).join("module").exists());
}

#[test]
#[ignore = "temporary diagnostic isolation"]
fn chromium_smoke_check_and_publish_a_browser_module() {
    let root = TempDir::new().unwrap();
    let mut library = create(
        source_root(&root),
        publications_root(&root),
        "browser-smoke",
    )
    .unwrap();
    *contents_mut(&mut library, "index.js") = r#"
export function mount(root) {
  const heading = document.createElement("h1");
  heading.textContent = "Hello, world!";
  root.replaceChildren(heading);
}
"#
    .trim_start()
    .to_owned();
    *contents_mut(&mut library, "tests.js") = r#"
import { mount } from "./index.js";

export async function runTests() {
  await Promise.resolve();
  const root = document.createElement("main");
  mount(root);
  if (root.textContent !== "Hello, world!") {
    throw new Error(`unexpected rendered text: ${root.textContent}`);
  }
}
"#
    .trim_start()
    .to_owned();
    let expected_index = contents_mut(&mut library, "index.js").clone();
    let expected_tests = contents_mut(&mut library, "tests.js").clone();

    library.publish().unwrap();

    let published = publications_root(&root).join("module/browser-smoke/v0.1.0");
    assert_eq!(
        fs::read_to_string(published.join("index.js")).unwrap(),
        expected_index
    );
    assert_eq!(
        fs::read_to_string(published.join("tests.js")).unwrap(),
        expected_tests
    );
}

#[test]
#[ignore = "temporary diagnostic isolation"]
fn chromium_smoke_reports_a_browser_test_failure() {
    let root = TempDir::new().unwrap();
    let mut library = create(
        source_root(&root),
        publications_root(&root),
        "failing-browser-smoke",
    )
    .unwrap();
    *contents_mut(&mut library, "tests.js") = r#"
export function runTests() {
  throw new Error("deliberate Chromium smoke failure");
}
"#
    .trim_start()
    .to_owned();

    let error = library.check().unwrap_err().to_string();

    assert!(error.starts_with("check.test:"));
    assert!(error.contains("deliberate Chromium smoke failure"));
}

#[cfg(unix)]
#[test]
fn docs_does_not_traverse_unrelated_source_but_open_rejects_symlinks() {
    use std::os::unix::fs::symlink;

    let root = TempDir::new().unwrap();
    create(source_root(&root), publications_root(&root), "demo").unwrap();
    let repository = source_root(&root).join("demo");
    let generation = fs::read_to_string(repository.join("HEAD"))
        .unwrap()
        .trim()
        .to_owned();
    symlink(
        root.path(),
        repository
            .join("generations")
            .join(generation)
            .join("unrelated-link"),
    )
    .unwrap();

    assert_eq!(docs(source_root(&root), "demo").unwrap().0, "0.1.0");
    assert!(open(source_root(&root), publications_root(&root), "demo").is_err());
}