421 lines
12 KiB
Rust
421 lines
12 KiB
Rust
use std::{
|
|
ffi::OsStr,
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
#[test]
|
|
fn doc_file_renders_public_api_with_signatures_and_shapes() {
|
|
let source = r#"(module api (export Point Status make))
|
|
|
|
(struct Point
|
|
(x i32)
|
|
(label string))
|
|
|
|
(enum Status Ready (Blocked i32))
|
|
|
|
(fn helper ((value i32)) -> i32
|
|
value)
|
|
|
|
(fn make ((x i32) (label string)) -> Point
|
|
(Point (x x) (label label)))
|
|
|
|
(test "make is documented"
|
|
true)
|
|
"#;
|
|
let file = write_file("file-api", source);
|
|
let docs = unique_path("file-api-docs");
|
|
|
|
let output = run_glagol([
|
|
OsStr::new("doc"),
|
|
file.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
|
|
assert_success("doc file", &output);
|
|
let index = read_index(&docs);
|
|
assert!(index.contains("## Module api"));
|
|
assert!(index.contains("### Imports\n\nNone.\n\n"));
|
|
assert!(index.contains("- `Point`"));
|
|
assert!(index.contains("- `make(x i32, label string) -> Point`"));
|
|
assert!(index.contains("- `make is documented`"));
|
|
|
|
let api = public_api_for_module(&index, "api");
|
|
assert!(api.contains("- `fn make(x: i32, label: string) -> Point`"));
|
|
assert!(api.contains("- `struct Point`"));
|
|
assert!(api.contains(" - `x: i32`"));
|
|
assert!(api.contains(" - `label: string`"));
|
|
assert!(api.contains("- `enum Status`"));
|
|
assert!(api.contains(" - `Ready`"));
|
|
assert!(api.contains(" - `Blocked(i32)`"));
|
|
assert!(
|
|
!api.contains("helper"),
|
|
"non-exported helper leaked into public API:\n{}",
|
|
api
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doc_project_renders_package_and_module_public_api() {
|
|
let project = write_project(
|
|
"project-api",
|
|
&[(
|
|
"math",
|
|
r#"(module math (export add Pair))
|
|
|
|
(struct Pair
|
|
(left i32)
|
|
(right i32))
|
|
|
|
(fn add ((left i32) (right i32)) -> i32
|
|
(+ left right))
|
|
|
|
(fn private_double ((value i32)) -> i32
|
|
(+ value value))
|
|
"#,
|
|
)],
|
|
"(module main)\n\n(import math (add Pair))\n\n(fn main () -> i32\n (add 1 2))\n",
|
|
);
|
|
let docs = unique_path("project-api-docs");
|
|
|
|
let output = run_glagol([
|
|
OsStr::new("doc"),
|
|
project.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
|
|
assert_success("doc project", &output);
|
|
let index = read_index(&docs);
|
|
assert!(index.contains("# Project project-api"));
|
|
assert!(index.contains("## Package API project-api"));
|
|
assert!(index.contains("## Module math"));
|
|
assert!(index.contains("## Module main"));
|
|
assert!(index.contains("- `math`"));
|
|
assert!(index.contains("- `add`"));
|
|
|
|
let package_api = package_api(&index, "project-api");
|
|
assert!(package_api.contains("### Module math"));
|
|
assert!(package_api.contains("- `fn add(left: i32, right: i32) -> i32`"));
|
|
assert!(package_api.contains("- `struct Pair`"));
|
|
assert!(
|
|
!package_api.contains("private_double"),
|
|
"non-exported function leaked into package API:\n{}",
|
|
package_api
|
|
);
|
|
|
|
let math_api = public_api_for_module(&index, "math");
|
|
assert!(math_api.contains("- `fn add(left: i32, right: i32) -> i32`"));
|
|
}
|
|
|
|
#[test]
|
|
fn doc_workspace_renders_each_package_api_deterministically() {
|
|
let workspace = unique_path("workspace-api");
|
|
let scaffold = run_glagol([
|
|
OsStr::new("new"),
|
|
workspace.as_os_str(),
|
|
OsStr::new("--template"),
|
|
OsStr::new("workspace"),
|
|
]);
|
|
assert_success("workspace scaffold", &scaffold);
|
|
|
|
let docs = unique_path("workspace-api-docs");
|
|
let output = run_glagol([
|
|
OsStr::new("doc"),
|
|
workspace.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
|
|
assert_success("doc workspace", &output);
|
|
let index = read_index(&docs);
|
|
assert!(index.contains("## Workspace"));
|
|
assert!(index.contains("- `packages/app`"));
|
|
assert!(index.contains("- `packages/libutil`"));
|
|
assert!(index.contains("## Package API app 0.1.0"));
|
|
assert!(index.contains("## Package API libutil 0.1.0"));
|
|
|
|
let app_api = package_api(&index, "app 0.1.0");
|
|
assert!(app_api.contains("None."));
|
|
|
|
let lib_api = package_api(&index, "libutil 0.1.0");
|
|
assert!(lib_api.contains("### Module libutil"));
|
|
assert!(lib_api.contains("- `fn answer() -> i32`"));
|
|
assert!(lib_api.contains("- `fn label() -> string`"));
|
|
}
|
|
|
|
#[test]
|
|
fn doc_workspace_package_api_excludes_loaded_std_modules() {
|
|
let workspace = write_workspace_with_std_import("workspace-std-api");
|
|
let docs = unique_path("workspace-std-api-docs");
|
|
|
|
let output = run_glagol([
|
|
OsStr::new("doc"),
|
|
workspace.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
|
|
assert_success("doc workspace std import", &output);
|
|
let index = read_index(&docs);
|
|
assert!(
|
|
index.contains("## Module option"),
|
|
"module summaries should still include loaded std module docs:\n{}",
|
|
index
|
|
);
|
|
|
|
let app_api = package_api(&index, "app 0.1.0");
|
|
assert!(app_api.contains("### Module main"));
|
|
assert!(app_api.contains("- `fn local_some(value: i32) -> (option i32)`"));
|
|
assert!(
|
|
!app_api.contains("Module std.option") && !app_api.contains("Module option"),
|
|
"loaded std module leaked into package API:\n{}",
|
|
app_api
|
|
);
|
|
assert!(
|
|
!app_api.contains("some_i32"),
|
|
"loaded std helper leaked into package API:\n{}",
|
|
app_api
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn public_api_normalizes_local_aliases_and_omits_alias_exports() {
|
|
let source = r#"(module aliases (export Count Score Status measure))
|
|
|
|
(type Count i32)
|
|
(type MaybeCount (option Count))
|
|
|
|
(struct Score
|
|
(value Count)
|
|
(maybe MaybeCount))
|
|
|
|
(enum Status Ready (Blocked Count) (Maybe MaybeCount))
|
|
|
|
(fn hidden ((value Count)) -> Count
|
|
value)
|
|
|
|
(fn measure ((value Count) (maybe MaybeCount)) -> Count
|
|
value)
|
|
"#;
|
|
let file = write_file("alias-api", source);
|
|
let docs = unique_path("alias-api-docs");
|
|
|
|
let output = run_glagol([
|
|
OsStr::new("doc"),
|
|
file.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
|
|
assert_success("doc aliases", &output);
|
|
let index = read_index(&docs);
|
|
assert!(
|
|
index.contains("- `Count`"),
|
|
"exports summary should retain the alias name"
|
|
);
|
|
assert!(
|
|
index.contains("- `hidden(value Count) -> Count`"),
|
|
"function summary should retain non-public declarations"
|
|
);
|
|
|
|
let api = public_api_for_module(&index, "aliases");
|
|
assert!(api.contains("- `fn measure(value: i32, maybe: (option i32)) -> i32`"));
|
|
assert!(api.contains(" - `value: i32`"));
|
|
assert!(api.contains(" - `maybe: (option i32)`"));
|
|
assert!(api.contains(" - `Blocked(i32)`"));
|
|
assert!(api.contains(" - `Maybe((option i32))`"));
|
|
assert!(
|
|
!api.contains("Count"),
|
|
"alias names leaked into public API:\n{}",
|
|
api
|
|
);
|
|
assert!(
|
|
!api.contains("hidden"),
|
|
"non-exported function leaked into public API:\n{}",
|
|
api
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_doc_generation_is_byte_identical() {
|
|
let source = r#"(module stable (export value))
|
|
|
|
(fn value () -> i32
|
|
42)
|
|
"#;
|
|
let file = write_file("stable-api", source);
|
|
let docs = unique_path("stable-api-docs");
|
|
|
|
let first = run_glagol([
|
|
OsStr::new("doc"),
|
|
file.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
assert_success("first doc", &first);
|
|
let first_bytes = fs::read(docs.join("index.md")).expect("read first docs");
|
|
|
|
let second = run_glagol([
|
|
OsStr::new("doc"),
|
|
file.as_os_str(),
|
|
OsStr::new("-o"),
|
|
docs.as_os_str(),
|
|
]);
|
|
assert_success("second doc", &second);
|
|
let second_bytes = fs::read(docs.join("index.md")).expect("read second docs");
|
|
|
|
assert_eq!(first_bytes, second_bytes);
|
|
}
|
|
|
|
fn write_project(name: &str, modules: &[(&str, &str)], main: &str) -> PathBuf {
|
|
let project = unique_path(name);
|
|
fs::create_dir_all(project.join("src")).expect("create project src");
|
|
fs::write(
|
|
project.join("slovo.toml"),
|
|
format!(
|
|
"[project]\nname = \"{}\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
name
|
|
),
|
|
)
|
|
.expect("write manifest");
|
|
for (module, source) in modules {
|
|
fs::write(project.join("src").join(format!("{}.slo", module)), source)
|
|
.expect("write module");
|
|
}
|
|
fs::write(project.join("src/main.slo"), main).expect("write main");
|
|
project
|
|
}
|
|
|
|
fn write_workspace_with_std_import(name: &str) -> PathBuf {
|
|
let workspace = unique_path(name);
|
|
let package = workspace.join("packages/app");
|
|
fs::create_dir_all(package.join("src")).expect("create workspace package src");
|
|
fs::write(
|
|
workspace.join("slovo.toml"),
|
|
"[workspace]\nmembers = [\"packages/app\"]\ndefault_package = \"app\"\n",
|
|
)
|
|
.expect("write workspace manifest");
|
|
fs::write(
|
|
package.join("slovo.toml"),
|
|
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nsource_root = \"src\"\nentry = \"main\"\n",
|
|
)
|
|
.expect("write package manifest");
|
|
fs::write(
|
|
package.join("src/main.slo"),
|
|
r#"(module main (export local_some))
|
|
|
|
(import std.option (some_i32))
|
|
|
|
(fn local_some ((value i32)) -> (option i32)
|
|
(some_i32 value))
|
|
"#,
|
|
)
|
|
.expect("write package main");
|
|
workspace
|
|
}
|
|
|
|
fn write_file(name: &str, source: &str) -> PathBuf {
|
|
let path = unique_path(name).with_extension("slo");
|
|
fs::write(&path, source).expect("write fixture");
|
|
path
|
|
}
|
|
|
|
fn read_index(docs: &Path) -> String {
|
|
fs::read_to_string(docs.join("index.md")).expect("read generated docs")
|
|
}
|
|
|
|
fn package_api<'a>(docs: &'a str, package: &str) -> &'a str {
|
|
let heading = format!("## Package API {}", package);
|
|
let start = docs
|
|
.find(&heading)
|
|
.unwrap_or_else(|| panic!("missing package API heading `{}`\n{}", heading, docs));
|
|
let rest = &docs[start..];
|
|
let end = rest
|
|
.find("\n## Package API ")
|
|
.or_else(|| rest.find("\n## Module "))
|
|
.unwrap_or(rest.len());
|
|
&rest[..end]
|
|
}
|
|
|
|
fn public_api_for_module<'a>(docs: &'a str, module: &str) -> &'a str {
|
|
let heading = format!("## Module {}", module);
|
|
let module_start = if docs.starts_with(&heading) {
|
|
0
|
|
} else {
|
|
let marker = format!("\n{}", heading);
|
|
docs.find(&marker)
|
|
.map(|index| index + 1)
|
|
.unwrap_or_else(|| panic!("missing module heading `{}`\n{}", heading, docs))
|
|
};
|
|
let module_docs = &docs[module_start..];
|
|
let module_end = module_docs
|
|
.find("\n## Module ")
|
|
.unwrap_or(module_docs.len());
|
|
let module_docs = &module_docs[..module_end];
|
|
let public_start = module_docs.find("### Public API").unwrap_or_else(|| {
|
|
panic!(
|
|
"missing public API for module `{}`\n{}",
|
|
module, module_docs
|
|
)
|
|
});
|
|
let public_docs = &module_docs[public_start..];
|
|
let public_end = public_docs
|
|
.find("\n### Structs")
|
|
.unwrap_or(public_docs.len());
|
|
&public_docs[..public_end]
|
|
}
|
|
|
|
fn unique_path(name: &str) -> PathBuf {
|
|
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
|
|
let nanos = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("system clock before UNIX_EPOCH")
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!(
|
|
"glagol-doc-api-beta11-{}-{}-{}-{}",
|
|
std::process::id(),
|
|
nanos,
|
|
id,
|
|
name
|
|
))
|
|
}
|
|
|
|
fn run_glagol<I, S>(args: I) -> Output
|
|
where
|
|
I: IntoIterator<Item = S>,
|
|
S: AsRef<OsStr>,
|
|
{
|
|
Command::new(env!("CARGO_BIN_EXE_glagol"))
|
|
.args(args)
|
|
.output()
|
|
.expect("run glagol")
|
|
}
|
|
|
|
fn assert_success(context: &str, output: &Output) {
|
|
assert!(
|
|
output.status.success(),
|
|
"{} failed\nstdout:\n{}\nstderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
assert!(
|
|
output.stdout.is_empty(),
|
|
"{} wrote stdout:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stdout)
|
|
);
|
|
assert!(
|
|
output.stderr.is_empty(),
|
|
"{} wrote stderr:\n{}",
|
|
context,
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|