//! Integration tests for the full build pipeline and incremental rebuild behavior.
//!
//! Each test creates a temporary directory with a minimal graph, runs the full
//! pipeline (scan -> parse -> build_graph -> render -> write_output), then
//! simulates changes and rebuilds to verify correctness.
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use optica::config::SiteConfig;
use optica::graph;
use optica::output;
use optica::parser;
use optica::render;
use optica::scanner;
/// Create a minimal test environment: a temp dir with graph/ directory
/// and a SiteConfig pointing to it.
fn setup_test_env() -> (TempDir, SiteConfig) {
let tmp = TempDir::new().expect("failed to create temp dir");
let graph_dir = tmp.path().join("graph");
fs::create_dir_all(&graph_dir).expect("failed to create graph dir");
let mut config = SiteConfig::default();
config.build.input_dir = tmp.path().to_path_buf();
config.build.output_dir = tmp.path().join("build");
config.content.public_only = false; // show all pages in tests
config.content.default_public = true;
config.search.enabled = false; // skip search index generation
config.feeds.enabled = false; // skip RSS feed generation
config.graph.enabled = false; // skip graph data generation
(tmp, config)
}
/// Write a markdown page to graph/<name>.md with optional frontmatter.
fn write_page(base: &Path, name: &str, content: &str) {
let graph_dir = base.join("graph");
// Handle namespaced pages: "ns/page" -> graph/ns/page.md
let page_path = graph_dir.join(format!("{}.md", name));
if let Some(parent) = page_path.parent() {
fs::create_dir_all(parent).expect("failed to create page parent dir");
}
fs::write(&page_path, content).expect("failed to write page");
}
/// Delete a page from graph/<name>.md
fn delete_page(base: &Path, name: &str) {
let page_path = base.join("graph").join(format!("{}.md", name));
if page_path.exists() {
fs::remove_file(&page_path).expect("failed to delete page");
}
// Also remove parent dir if empty (for namespace pages)
if let Some(parent) = page_path.parent() {
let _ = fs::remove_dir(parent); // ignore error if not empty
}
}
/// Run the full build pipeline: scan -> parse -> build_graph -> render -> write_output.
/// Returns the rendered pages for inspection.
fn full_build(config: &SiteConfig) -> Vec<render::RenderedPage> {
let discovered = scanner::scan(&config.build.input_dir, &config.content)
.expect("scan failed");
let parsed_pages = parser::parse_all(&discovered).expect("parse failed");
let page_store = graph::build_graph(parsed_pages).expect("build_graph failed");
let rendered = render::render_all(&page_store, config).expect("render failed");
output::write_output(&rendered, &page_store, config, &discovered)
.expect("write_output failed");
rendered
}
/// Run a full rebuild (simulating incremental by doing a fresh build).
/// This tests the output state after changes without requiring BuildCache.
fn rebuild(config: &SiteConfig) -> Vec<render::RenderedPage> {
let discovered = scanner::scan(&config.build.input_dir, &config.content)
.expect("scan failed");
let parsed_pages = parser::parse_all(&discovered).expect("parse failed");
let page_store = graph::build_graph(parsed_pages).expect("build_graph failed");
let rendered = render::render_all(&page_store, config).expect("render failed");
output::write_incremental(&rendered, &page_store, config, &discovered)
.expect("write_incremental failed");
rendered
}
/// Check if a page output exists at build/<slug>/index.html
fn output_exists(config: &SiteConfig, slug: &str) -> bool {
let index_path = config.build.output_dir.join(slug).join("index.html");
index_path.exists()
}
/// Read the HTML content of a page output at build/<slug>/index.html
fn read_output(config: &SiteConfig, slug: &str) -> String {
let index_path = config.build.output_dir.join(slug).join("index.html");
fs::read_to_string(&index_path)
.unwrap_or_else(|_| panic!("failed to read output at {}", index_path.display()))
}
#[test]
fn test_new_page_appears_in_output() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "test", "---\ntags: cyber\n---\n\nHello world");
let _rendered = full_build(&config);
assert!(
output_exists(&config, "test"),
"build/test/index.html should exist after building graph/test.md"
);
let html = read_output(&config, "test");
assert!(
html.contains("Hello world"),
"output HTML should contain the page content"
);
}
#[test]
fn test_deleted_page_removed_from_output() {
let (tmp, config) = setup_test_env();
// Build with the page present
write_page(tmp.path(), "deleteme", "---\ntags: test\n---\n\nTemporary page");
write_page(tmp.path(), "keeper", "---\ntags: test\n---\n\nPermanent page");
full_build(&config);
assert!(output_exists(&config, "deleteme"), "page should exist after initial build");
assert!(output_exists(&config, "keeper"), "keeper should exist after initial build");
// Delete the page and rebuild
delete_page(tmp.path(), "deleteme");
rebuild(&config);
assert!(
!output_exists(&config, "deleteme"),
"build/deleteme/index.html should be removed after page deletion and rebuild"
);
assert!(
output_exists(&config, "keeper"),
"keeper page should still exist after rebuild"
);
}
#[test]
fn test_moved_page_old_output_removed() {
let (tmp, config) = setup_test_env();
// Build with page at graph/old.md
write_page(tmp.path(), "old", "---\ntags: test\n---\n\nMoving page");
full_build(&config);
assert!(output_exists(&config, "old"), "old page should exist after initial build");
// Move to graph/new.md (delete old, create new)
delete_page(tmp.path(), "old");
write_page(tmp.path(), "new", "---\ntags: test\n---\n\nMoving page");
rebuild(&config);
assert!(
!output_exists(&config, "old"),
"old output should be removed after page move"
);
assert!(
output_exists(&config, "new"),
"new output should exist after page move"
);
}
#[test]
fn test_namespace_move_updates_listing() {
let (tmp, config) = setup_test_env();
// Build with page at root level
write_page(tmp.path(), "mypage", "---\ntags: test\n---\n\nRoot page");
full_build(&config);
assert!(output_exists(&config, "mypage"), "root page should exist");
// Move into a namespace: graph/ns/mypage.md
delete_page(tmp.path(), "mypage");
write_page(tmp.path(), "ns/mypage", "---\ntags: test\n---\n\nNamespaced page");
rebuild(&config);
// Old root-level output should be gone
assert!(
!output_exists(&config, "mypage"),
"root-level output should be removed after namespace move"
);
// Namespaced page should exist with its slugified ID
let ns_slug = parser::slugify_page_name("ns/mypage");
assert!(
output_exists(&config, &ns_slug),
"namespaced page output should exist at build/{}/index.html",
ns_slug
);
// Verify the namespaced page HTML contains the content
let ns_html = read_output(&config, &ns_slug);
assert!(
ns_html.contains("Namespaced page"),
"namespaced page output should contain its content"
);
}
#[test]
fn test_content_edit_reflects_in_output() {
let (tmp, config) = setup_test_env();
// Initial build
write_page(
tmp.path(),
"editable",
"---\ntags: test\n---\n\nOriginal content here",
);
full_build(&config);
let html_before = read_output(&config, "editable");
assert!(
html_before.contains("Original content here"),
"initial build should contain original content"
);
// Edit the page content
write_page(
tmp.path(),
"editable",
"---\ntags: test\n---\n\nUpdated content with new info",
);
rebuild(&config);
let html_after = read_output(&config, "editable");
assert!(
html_after.contains("Updated content with new info"),
"rebuilt output should contain updated content"
);
assert!(
!html_after.contains("Original content here"),
"rebuilt output should not contain old content"
);
}
#[test]
fn test_tag_change_updates_tag_page() {
let (tmp, config) = setup_test_env();
// Build with page having tag "alpha"
write_page(
tmp.path(),
"tagged",
"---\ntags: alpha\n---\n\nA page with a tag",
);
full_build(&config);
// The tags index page should exist
assert!(
output_exists(&config, "tags"),
"tags index page should exist"
);
// Check that tag page for "alpha" includes our page
if output_exists(&config, "tags/alpha") {
let tag_html = read_output(&config, "tags/alpha");
assert!(
tag_html.contains("tagged"),
"tag page for 'alpha' should reference the tagged page"
);
}
// Change the tag to "beta"
write_page(
tmp.path(),
"tagged",
"---\ntags: beta\n---\n\nA page with a different tag",
);
rebuild(&config);
// Tag page for "beta" should now include the page
if output_exists(&config, "tags/beta") {
let tag_html = read_output(&config, "tags/beta");
assert!(
tag_html.contains("tagged"),
"tag page for 'beta' should reference the tagged page after tag change"
);
}
}
#[test]
fn test_multiple_pages_build_correctly() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "page-a", "---\ntags: test\n---\n\nPage A content");
write_page(tmp.path(), "page-b", "---\ntags: test\n---\n\nPage B links to page-a");
write_page(tmp.path(), "page-c", "---\ntags: test\n---\n\nPage C links to page-b");
full_build(&config);
assert!(output_exists(&config, "page-a"), "page-a should exist in output");
assert!(output_exists(&config, "page-b"), "page-b should exist in output");
assert!(output_exists(&config, "page-c"), "page-c should exist in output");
// Verify wikilinks are resolved in output
let html_b = read_output(&config, "page-b");
assert!(
html_b.contains("/page-a"),
"page-b output should contain a link to page-a"
);
}
#[test]
fn test_backlinks_appear_in_output() {
let (tmp, config) = setup_test_env();
write_page(tmp.path(), "target", "---\ntags: test\n---\n\nTarget page");
write_page(
tmp.path(),
"linker",
"---\ntags: test\n---\n\nThis page links to target",
);
full_build(&config);
let target_html = read_output(&config, "target");
// The target page should show a backlink from "linker"
assert!(
target_html.contains("linker"),
"target page should show backlink from linker page"
);
}
#[test]
fn test_namespace_hierarchy_builds() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"project/alpha",
"---\ntags: test\n---\n\nAlpha project",
);
write_page(
tmp.path(),
"project/beta",
"---\ntags: test\n---\n\nBeta project",
);
full_build(&config);
let alpha_slug = parser::slugify_page_name("project/alpha");
let beta_slug = parser::slugify_page_name("project/beta");
assert!(
output_exists(&config, &alpha_slug),
"project/alpha should have output"
);
assert!(
output_exists(&config, &beta_slug),
"project/beta should have output"
);
// Verify the rendered pages have correct content
let alpha_html = read_output(&config, &alpha_slug);
assert!(
alpha_html.contains("Alpha project"),
"alpha page output should contain its content"
);
let beta_html = read_output(&config, &beta_slug);
assert!(
beta_html.contains("Beta project"),
"beta page output should contain its content"
);
}
#[test]
fn test_empty_graph_builds_without_error() {
let (_tmp, config) = setup_test_env();
// Build with no pages at all
full_build(&config);
// Should still produce some output (index, tags page, etc.)
assert!(
config.build.output_dir.exists(),
"output directory should exist even with empty graph"
);
// At minimum, an index page should be generated
let index_path = config.build.output_dir.join("index.html");
assert!(
index_path.exists(),
"index.html should exist even with empty graph"
);
}
#[test]
fn test_page_with_aliases_builds() {
let (tmp, config) = setup_test_env();
write_page(
tmp.path(),
"canonical",
"---\ntags: test\nalias: alt-name, another-alias\n---\n\nPage with aliases",
);
write_page(
tmp.path(),
"referrer",
"---\ntags: test\n---\n\nLinks to alt-name",
);
full_build(&config);
assert!(output_exists(&config, "canonical"), "canonical page should exist");
// The referrer should have a link that resolves (through the alias)
let referrer_html = read_output(&config, "referrer");
assert!(
referrer_html.contains("canonical") || referrer_html.contains("alt-name"),
"referrer should contain a link related to the aliased page"
);
}
#[test]
fn test_rebuild_after_adding_new_page() {
let (tmp, config) = setup_test_env();
// Initial build with one page
write_page(tmp.path(), "first", "---\ntags: test\n---\n\nFirst page");
full_build(&config);
assert!(output_exists(&config, "first"), "first page should exist");
// Add a second page and rebuild
write_page(tmp.path(), "second", "---\ntags: test\n---\n\nSecond page");
rebuild(&config);
assert!(output_exists(&config, "first"), "first page should still exist");
assert!(output_exists(&config, "second"), "second page should appear after rebuild");
}
optica/tests/incremental_rebuild.rs
ฯ 0.0%
//! Integration tests for the full build pipeline and incremental rebuild behavior.
//!
//! Each test creates a temporary directory with a minimal graph, runs the full
//! pipeline (scan -> parse -> build_graph -> render -> write_output), then
//! simulates changes and rebuilds to verify correctness.
use fs;
use Path;
use TempDir;
use SiteConfig;
use graph;
use output;
use parser;
use render;
use scanner;
/// Create a minimal test environment: a temp dir with graph/ directory
/// and a SiteConfig pointing to it.
/// Write a markdown page to graph/<name>.md with optional frontmatter.
/// Delete a page from graph/<name>.md
/// Run the full build pipeline: scan -> parse -> build_graph -> render -> write_output.
/// Returns the rendered pages for inspection.
/// Run a full rebuild (simulating incremental by doing a fresh build).
/// This tests the output state after changes without requiring BuildCache.
/// Check if a page output exists at build/<slug>/index.html
/// Read the HTML content of a page output at build/<slug>/index.html