use std::collections::HashMap;
use std::path::{Path, PathBuf};
use badness::file_discovery::FileKind;
use badness::incremental::{IncrementalDatabase, QueryKind, QueryLogEntry, SourceFile};
use badness::project::{
IncludeKind, Project, ProjectMember, project_graph, resolved_citations, resolved_labels,
};
fn count_by_kind(entries: &[QueryLogEntry]) -> HashMap<QueryKind, usize> {
let mut counts = HashMap::new();
for entry in entries {
*counts.entry(entry.kind).or_insert(0) += 1;
}
counts
}
fn fpath(db: &IncrementalDatabase, file: SourceFile) -> PathBuf {
db.file_path(file).to_path_buf()
}
fn project_main_part<'db>(
db: &'db IncrementalDatabase,
main: SourceFile,
part: SourceFile,
) -> Project<'db> {
let mut members = vec![
ProjectMember {
file: main,
path: fpath(db, main),
kind: FileKind::Tex,
},
ProjectMember {
file: part,
path: fpath(db, part),
kind: FileKind::Tex,
},
];
members.sort_by(|a, b| a.path.cmp(&b.path));
Project::new(db, members)
}
fn project_main_bib<'db>(
db: &'db IncrementalDatabase,
main: SourceFile,
bib: SourceFile,
) -> Project<'db> {
let mut members = vec![
ProjectMember {
file: main,
path: fpath(db, main),
kind: FileKind::Tex,
},
ProjectMember {
file: bib,
path: fpath(db, bib),
kind: FileKind::Bib,
},
];
members.sort_by(|a, b| a.path.cmp(&b.path));
Project::new(db, members)
}
fn main_bib(main_text: &str, bib_text: &str) -> (IncrementalDatabase, SourceFile, SourceFile) {
let mut db = IncrementalDatabase::default();
let main = db.upsert_file(Path::new("/proj/main.tex"), main_text.to_string());
let bib = db.upsert_file(Path::new("/proj/refs.bib"), bib_text.to_string());
(db, main, bib)
}
fn main_part(main_text: &str, part_text: &str) -> (IncrementalDatabase, SourceFile, SourceFile) {
let mut db = IncrementalDatabase::default();
let main = db.upsert_file(Path::new("/proj/main.tex"), main_text.to_string());
let part = db.upsert_file(Path::new("/proj/part.tex"), part_text.to_string());
(db, main, part)
}
#[test]
fn graph_resolves_an_input_edge() {
let (db, main, part) = main_part("\\input{part}\n", "hello\n");
let graph = project_graph(&db, project_main_part(&db, main, part));
let out = graph.outgoing(&fpath(&db, main));
assert_eq!(out.len(), 1);
assert_eq!(out[0].to, fpath(&db, part));
assert_eq!(out[0].kind, IncludeKind::Input);
assert_eq!(graph.included_by(&fpath(&db, part)), &[fpath(&db, main)]);
assert!(graph.unresolved().is_empty());
}
#[test]
fn body_edit_does_not_rebuild_graph() {
let (mut db, main, part) = main_part("\\input{part}\n", "hello\n");
let _ = project_graph(&db, project_main_part(&db, main, part));
db.clear_query_log();
db.set_file_text(part, "hello world\n");
let _ = project_graph(&db, project_main_part(&db, main, part));
let counts = count_by_kind(&db.query_log());
assert_eq!(counts.get(&QueryKind::IncludeEdges), Some(&1));
assert_eq!(
counts.get(&QueryKind::ProjectGraph),
None,
"project graph must not rebuild on a body edit"
);
}
#[test]
fn edge_change_rebuilds_graph() {
let (mut db, main, part) = main_part("\\input{part}\n", "hello\n");
let _ = project_graph(&db, project_main_part(&db, main, part));
db.clear_query_log();
db.set_file_text(main, "\\input{part}\n\\input{extra}\n");
let graph = project_graph(&db, project_main_part(&db, main, part));
let counts = count_by_kind(&db.query_log());
assert_eq!(
counts.get(&QueryKind::ProjectGraph),
Some(&1),
"project graph must rebuild when an edge changes"
);
assert_eq!(graph.unresolved().len(), 1);
assert_eq!(graph.unresolved()[0].from, fpath(&db, main));
}
#[test]
fn resolved_labels_unions_across_the_include_graph() {
let (db, main, part) = main_part(
"\\documentclass{article}\n\\input{part}\n\\ref{a}\n",
"\\label{a}\n",
);
let resolved = resolved_labels(&db, project_main_part(&db, main, part));
assert!(resolved.is_defined(&fpath(&db, main), "a"));
assert!(!resolved.is_defined(&fpath(&db, main), "missing"));
assert!(resolved.is_closed(&fpath(&db, main)));
assert!(resolved.is_root_component(&fpath(&db, part)));
}
#[test]
fn label_set_preserving_edit_does_not_rebuild_resolved_labels() {
let (mut db, main, part) = main_part(
"\\documentclass{article}\n\\input{part}\n\\ref{a}\n",
"\\label{a}\n",
);
let _ = resolved_labels(&db, project_main_part(&db, main, part));
db.clear_query_log();
db.set_file_text(part, "\\label{a}\\ref{a}\n");
let _ = resolved_labels(&db, project_main_part(&db, main, part));
let counts = count_by_kind(&db.query_log());
assert_eq!(counts.get(&QueryKind::FileLabels), Some(&1));
assert_eq!(
counts.get(&QueryKind::ResolvedLabels),
None,
"resolved labels must not rebuild when no label set changed"
);
}
#[test]
fn label_change_rebuilds_resolved_labels() {
let (mut db, main, part) = main_part(
"\\documentclass{article}\n\\input{part}\n\\ref{a}\n",
"\\label{a}\n",
);
let _ = resolved_labels(&db, project_main_part(&db, main, part));
db.clear_query_log();
db.set_file_text(part, "\\label{a}\\label{b}\n");
let resolved = resolved_labels(&db, project_main_part(&db, main, part));
let counts = count_by_kind(&db.query_log());
assert_eq!(
counts.get(&QueryKind::ResolvedLabels),
Some(&1),
"resolved labels must rebuild when a label set changes"
);
assert!(resolved.is_defined(&fpath(&db, main), "b"));
}
#[test]
fn resolved_citations_unions_referenced_bib_keys() {
let (db, main, bib) = main_bib(
"\\documentclass{article}\n\\addbibresource{refs.bib}\n\\cite{knuth}\n",
"@article{knuth, title={x}}\n",
);
let resolved = resolved_citations(&db, project_main_bib(&db, main, bib));
assert!(resolved.is_defined(&fpath(&db, main), "knuth"));
assert!(!resolved.is_defined(&fpath(&db, main), "missing"));
assert!(resolved.is_closed(&fpath(&db, main)));
assert!(resolved.is_root_component(&fpath(&db, main)));
}
#[test]
fn cite_set_preserving_edit_does_not_rebuild_resolved_citations() {
let (mut db, main, bib) = main_bib(
"\\documentclass{article}\n\\addbibresource{refs.bib}\n\\cite{knuth}\n",
"@article{knuth, title={x}}\n",
);
let _ = resolved_citations(&db, project_main_bib(&db, main, bib));
db.clear_query_log();
db.set_file_text(bib, "@article{knuth, title={x}}\n@string{foo = \"bar\"}\n");
let _ = resolved_citations(&db, project_main_bib(&db, main, bib));
let counts = count_by_kind(&db.query_log());
assert_eq!(counts.get(&QueryKind::FileCiteNames), Some(&1));
assert_eq!(
counts.get(&QueryKind::ResolvedCitations),
None,
"resolved citations must not rebuild when no cite-key set changed"
);
}
#[test]
fn cite_key_change_rebuilds_resolved_citations() {
let (mut db, main, bib) = main_bib(
"\\documentclass{article}\n\\addbibresource{refs.bib}\n\\cite{knuth}\n",
"@article{knuth, title={x}}\n",
);
let _ = resolved_citations(&db, project_main_bib(&db, main, bib));
db.clear_query_log();
db.set_file_text(
bib,
"@article{knuth, title={x}}\n@article{lamport, title={y}}\n",
);
let resolved = resolved_citations(&db, project_main_bib(&db, main, bib));
let counts = count_by_kind(&db.query_log());
assert_eq!(
counts.get(&QueryKind::ResolvedCitations),
Some(&1),
"resolved citations must rebuild when a cite-key set changes"
);
assert!(resolved.is_defined(&fpath(&db, main), "lamport"));
}
#[test]
fn reinterning_same_membership_reuses_graph_memo() {
let (db, main, part) = main_part("\\input{part}\n", "hello\n");
let project = project_main_part(&db, main, part);
let _ = project_graph(&db, project);
db.clear_query_log();
let project2 = project_main_part(&db, main, part);
assert!(
project == project2,
"same membership should re-intern to the same id"
);
let _ = project_graph(&db, project2);
assert_eq!(
count_by_kind(&db.query_log()).get(&QueryKind::ProjectGraph),
None,
"an unchanged membership must not rebuild the graph"
);
}