use std::path::{Path, PathBuf};
use std::process::Command;
use onetaskgraph_core::{Config, serve};
use serde_json::{Value, json};
use tempfile::TempDir;
const NETWORK_MARKERS: [&str; 5] = [
"onetaskgraph-github-projects",
"onetaskgraph-linear",
"reqwest",
"hyper",
"native-tls",
];
fn packages_reached_with(features: &[&str]) -> Vec<String> {
let engine = Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace = engine
.parent()
.and_then(Path::parent)
.expect("the engine sits two levels under the workspace root");
let dependent = TempDir::new().expect("a scratch directory");
let features = features
.iter()
.map(|feature| format!("{feature:?}"))
.collect::<Vec<_>>()
.join(", ");
let manifest = format!(
"[package]\nname = \"local-md-host\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\
publish = false\n\n[workspace]\n\n[dependencies]\n\
onetaskgraph-core = {{ path = {:?}, features = [{features}] }}\n",
engine.display().to_string()
);
std::fs::write(dependent.path().join("Cargo.toml"), manifest).expect("a manifest");
std::fs::create_dir(dependent.path().join("src")).expect("a source directory");
std::fs::write(dependent.path().join("src/lib.rs"), "").expect("a library root");
std::fs::copy(
workspace.join("Cargo.lock"),
dependent.path().join("Cargo.lock"),
)
.expect("the workspace lockfile");
let cargo = std::env::var_os("CARGO").map_or_else(|| PathBuf::from("cargo"), PathBuf::from);
let output = Command::new(cargo)
.current_dir(dependent.path())
.args([
"tree",
"--offline",
"--edges",
"normal",
"--prefix",
"none",
"--format",
"{p}",
])
.output()
.expect("cargo runs");
assert!(
output.status.success(),
"cargo tree could not resolve a dependent of the engine:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let mut packages: Vec<String> = String::from_utf8(output.stdout)
.expect("cargo tree prints UTF-8")
.lines()
.filter_map(|line| line.split_whitespace().next())
.map(str::to_owned)
.collect();
packages.sort();
packages.dedup();
packages
}
#[test]
fn a_dependent_taking_the_default_features_links_none_of_the_network_markers() {
let packages = packages_reached_with(&[]);
assert!(
packages
.iter()
.any(|package| package == "onetaskgraph-local-md"),
"the local store is part of every build: {packages:?}"
);
let linked: Vec<&str> = NETWORK_MARKERS
.into_iter()
.filter(|name| packages.iter().any(|package| package == name))
.collect();
assert!(
linked.is_empty(),
"a default-features dependent of onetaskgraph-core links {linked:?}; each network \
plugin must stay behind its own optional feature"
);
}
#[test]
fn a_dependent_enabling_both_network_features_links_every_network_marker() {
let packages = packages_reached_with(&["github-projects", "linear"]);
let missing: Vec<&str> = NETWORK_MARKERS
.into_iter()
.filter(|name| !packages.iter().any(|package| package == name))
.collect();
assert!(
missing.is_empty(),
"enabling both features did not link {missing:?}, so the default-features \
assertion above could not have seen them either"
);
}
fn naming(plugin: &str) -> Value {
json!({"sources": {"work": {"plugin": plugin, "config": {}}}})
}
fn served_handshake(kind: &str) -> Value {
let request = json!({
"id": "0",
"method": "initialize",
"params": {
"protocol_version": 2,
"engine": {"name": "onetaskgraph", "version": "0.1.0"},
"source_name": "work",
"config": {"kind": kind, "config": {}},
"secrets": {}
}
});
let input = format!("{request}\n");
let mut output: Vec<u8> = Vec::new();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a runtime")
.block_on(serve(input.as_bytes(), &mut output))
.expect("the streams are in memory and cannot fail");
serde_json::from_slice(&output).expect("one JSON answer")
}
#[cfg(not(feature = "github-projects"))]
#[test]
fn a_build_without_github_projects_refuses_it_by_name_with_the_feature_to_enable() {
let error = Config::from_document(naming("github-projects"))
.expect_err("this build did not compile the plugin")
.to_string();
assert!(error.contains("sources.work.plugin"), "{error}");
assert!(
error.contains("\"github-projects\" plugin is not compiled into this binary"),
"{error}"
);
assert!(error.contains("`github-projects` feature"), "{error}");
let answer = served_handshake("github-projects");
let message = answer["error"]["message"].as_str().expect("a refusal");
assert!(
message.contains("enable the `github-projects` feature of onetaskgraph-core"),
"{answer}"
);
}
#[cfg(not(feature = "linear"))]
#[test]
fn a_build_without_linear_refuses_it_by_name_with_the_feature_to_enable() {
let error = Config::from_document(naming("linear"))
.expect_err("this build did not compile the plugin")
.to_string();
assert!(error.contains("sources.work.plugin"), "{error}");
assert!(
error.contains("\"linear\" plugin is not compiled into this binary"),
"{error}"
);
assert!(error.contains("`linear` feature"), "{error}");
let answer = served_handshake("linear");
let message = answer["error"]["message"].as_str().expect("a refusal");
assert!(
message.contains("enable the `linear` feature of onetaskgraph-core"),
"{answer}"
);
}
#[cfg(all(feature = "github-projects", feature = "linear"))]
#[test]
fn a_build_with_both_features_registers_both_network_plugins() {
let kinds = onetaskgraph_core::plugin_kinds();
assert!(kinds.contains(&"github-projects"), "{kinds:?}");
assert!(kinds.contains(&"linear"), "{kinds:?}");
Config::from_document(naming("linear")).expect("linear is compiled into this build");
Config::from_document(naming("github-projects"))
.expect("github-projects is compiled into this build");
let answer = served_handshake("linear");
let message = answer["error"]["message"].as_str().expect("a refusal");
assert!(message.contains("LINEAR_API_KEY"), "{answer}");
}
#[test]
fn a_name_no_feature_answers_to_is_still_refused_as_unknown() {
let error = Config::from_document(naming("jira"))
.expect_err("nothing is called jira")
.to_string();
assert!(error.contains("no plugin named \"jira\""), "{error}");
assert!(!error.contains("feature"), "{error}");
}