use std::path::{Path, PathBuf};
use std::process::Command;
fn rustango_crate() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/")
.join("rustango")
}
fn in_repo() -> bool {
rustango_crate().join("Cargo.toml").is_file()
}
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"rustango-scaffold-{}-{}-{tag}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
));
std::fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
fn generate(template: &str, flags: &[&str], tag: &str) -> (PathBuf, PathBuf) {
let work = scratch(tag);
let out = Command::new(env!("CARGO_BIN_EXE_cargo-rustango"))
.current_dir(&work)
.args(["rustango", "new", "probe", "--template", template])
.args(flags)
.arg("--rustango-path")
.arg(rustango_crate())
.output()
.expect("run the scaffolder");
assert!(
out.status.success(),
"scaffolding {template} {flags:?} failed:\n{}",
String::from_utf8_lossy(&out.stderr),
);
let root = work.join("probe");
(work, root)
}
fn scaffold_target() -> PathBuf {
std::env::temp_dir().join("rustango-scaffold-target")
}
fn check(root: &Path, extra: &[&str]) -> (bool, usize, String) {
let out = Command::new(env!("CARGO"))
.current_dir(root)
.args(["check", "--message-format=json-diagnostic-rendered-ansi"])
.args(extra)
.env("CARGO_TARGET_DIR", scaffold_target())
.env("CARGO_INCREMENTAL", "0")
.env("CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS", "fallback")
.output()
.expect("run cargo check");
let stdout = String::from_utf8_lossy(&out.stdout);
let mut warnings = Vec::new();
for line in stdout.lines() {
if !line.contains(r#""reason":"compiler-message""#) {
continue;
}
if !line.contains("probe") {
continue; }
if line.contains(r#""level":"warning""#) {
warnings.push(line.to_owned());
}
}
let log = format!("{stdout}{}", String::from_utf8_lossy(&out.stderr));
(out.status.success(), warnings.len(), log)
}
fn assert_compiles(template: &str, extra: &[&str], label: &str) {
assert_compiles_with(template, &[], extra, label);
}
fn assert_compiles_with(template: &str, scaffold: &[&str], extra: &[&str], label: &str) {
if !in_repo() {
eprintln!("skipping: crates/rustango not alongside — not a repo checkout");
return;
}
let (work, root) = generate(template, scaffold, label);
let (ok, warnings, log) = check(&root, extra);
let _ = std::fs::remove_dir_all(&work);
assert!(ok, "`{template}` {label} did not compile:\n{log}");
assert_eq!(
warnings, 0,
"`{template}` {label} compiled but emitted {warnings} warning(s) — a \
freshly generated project must be clean (#1210):\n{log}"
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn api_template_compiles() {
assert_compiles("api", &[], "default");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn fullstack_template_compiles() {
assert_compiles("fullstack", &[], "default");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn tenant_template_compiles() {
assert_compiles("tenant", &[], "default");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn api_template_compiles_on_sqlite() {
assert_compiles(
"api",
&["--no-default-features", "--features", "sqlite"],
"sqlite",
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn api_template_compiles_on_mysql() {
assert_compiles(
"api",
&["--no-default-features", "--features", "mysql"],
"mysql",
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn fullstack_template_compiles_on_sqlite() {
assert_compiles(
"fullstack",
&["--no-default-features", "--features", "sqlite"],
"sqlite",
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn tenant_template_compiles_on_sqlite() {
assert_compiles(
"tenant",
&["--no-default-features", "--features", "sqlite"],
"sqlite",
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn backend_sqlite_compiles_as_generated() {
assert_compiles_with("fullstack", &["--backend", "sqlite"], &[], "backend-sqlite");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn backend_mysql_compiles_as_generated() {
assert_compiles_with("fullstack", &["--backend", "mysql"], &[], "backend-mysql");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn tenant_on_sqlite_backend_compiles_as_generated() {
assert_compiles_with("tenant", &["--backend", "sqlite"], &[], "tenant-sqlite");
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn selected_features_compile() {
assert_compiles_with(
"fullstack",
&["--features", "csrf,cache-page,testkit"],
&[],
"features",
);
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn tenancy_feature_on_fullstack_compiles() {
assert_compiles_with(
"fullstack",
&["--features", "tenancy"],
&[],
"features-tenancy",
);
}
fn get_head(port: u16, path: &str) -> String {
use std::io::{Read, Write};
let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect");
write!(
s,
"GET {path} HTTP/1.1\r\nHost: localhost:{port}\r\nAccept: text/html\r\nConnection: close\r\n\r\n"
)
.expect("send");
let mut resp = String::new();
let _ = s.read_to_string(&mut resp);
resp.split("\r\n\r\n").next().unwrap_or_default().to_owned()
}
#[test]
#[ignore = "compiles rustango; run with --ignored"]
fn tenant_project_mounts_the_provisioning_pages() {
if !in_repo() {
eprintln!("skipping: crates/rustango not alongside — not a repo checkout");
return;
}
let (work, root) = generate("tenant", &["--backend", "sqlite"], "tenant-run");
let built = Command::new(env!("CARGO"))
.current_dir(&root)
.arg("build")
.env("CARGO_TARGET_DIR", scaffold_target())
.env("CARGO_INCREMENTAL", "0")
.env("CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS", "fallback")
.status()
.expect("run cargo build");
assert!(built.success(), "the tenant project did not build");
let failed = Command::new(scaffold_target().join("debug").join("probe"))
.current_dir(&root)
.env("DATABASE_URL", "sqlite://./probe_dev.db?mode=rwc")
.args(["purge-tenant", "ghost", "--confirm", "ghost"])
.output()
.expect("run a failing verb");
let err = String::from_utf8_lossy(&failed.stderr);
assert!(
!failed.status.success(),
"purging a missing tenant succeeded:\n{err}"
);
let msg = err
.lines()
.find_map(|l| l.strip_prefix("Error: "))
.unwrap_or_default();
let variant: String = msg
.chars()
.take_while(char::is_ascii_alphanumeric)
.collect();
let debug = variant.starts_with(char::is_uppercase) && msg[variant.len()..].starts_with('(');
assert!(
!msg.is_empty() && !debug,
"error printed with Debug:\n{err}"
);
let port = std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.expect("free port")
.port();
let log = work.join("server.log");
let mut server = Command::new(scaffold_target().join("debug").join("probe"))
.current_dir(&root)
.env("DATABASE_URL", "sqlite://./probe_dev.db?mode=rwc")
.env("RUSTANGO_BIND", format!("127.0.0.1:{port}"))
.env("RUSTANGO_APEX_DOMAIN", "localhost")
.stdout(std::fs::File::create(&log).expect("log file"))
.stderr(std::fs::File::create(work.join("server.err")).expect("log file"))
.spawn()
.expect("start the server");
let up = (0..600).any(|_| {
if std::net::TcpStream::connect(("127.0.0.1", port)).is_ok() {
return true;
}
if server.try_wait().ok().flatten().is_some() {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(200));
false
});
let heads: Vec<_> = if up {
["/orgs/new", "/db-servers"]
.iter()
.map(|p| (*p, get_head(port, p)))
.collect()
} else {
Vec::new()
};
let _ = server.kill();
let _ = server.wait();
let logs = format!(
"{}{}",
std::fs::read_to_string(&log).unwrap_or_default(),
std::fs::read_to_string(work.join("server.err")).unwrap_or_default()
);
let _ = std::fs::remove_dir_all(&work);
assert!(up, "the server did not come up:\n{logs}");
for (path, head) in heads {
let status = head.lines().next().unwrap_or_default();
assert!(
status.contains(" 302 ") || status.contains(" 303 "),
"{path} is not mounted: {head}\n{logs}"
);
assert!(
head.to_ascii_lowercase().contains("login"),
"{path}: {head}"
);
}
}