use std::path::{Path, PathBuf};
fn laravel_vendor() -> PathBuf {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/work/laravel/vendor");
assert!(
dir.is_dir(),
"fixture laravel missing — run fixtures/make.sh"
);
dir
}
#[test]
fn proxies_match_composer_byte_for_byte() {
let vendor = laravel_vendor();
let installed: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(vendor.join("composer/installed.json")).expect("installed.json"),
)
.expect("json");
let mut checked = 0;
for pkg in installed["packages"].as_array().expect("packages") {
let name = pkg["name"].as_str().expect("name");
let Some(bins) = pkg.get("bin").and_then(|b| b.as_array()) else {
continue;
};
for bin in bins {
let bin = bin.as_str().expect("bin").trim_start_matches("./");
let link_name = bin.rsplit_once('/').map(|(_, f)| f).unwrap_or(bin);
let link = vendor.join("bin").join(link_name);
if !link.exists() {
continue;
}
let expected = std::fs::read_to_string(&link).expect("proxy composer");
let ours = vivacity_core::binproxy::proxy_content(
&vendor,
&link,
&vendor.join(name).join(bin),
)
.expect("proxy vivacity");
assert_eq!(
ours, expected,
"diverging proxy for {link_name} (package {name})"
);
checked += 1;
}
}
assert!(checked >= 5, "too few proxies compared: {checked}");
}
fn scratch_pkg(tag: &str) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) {
let root = std::env::temp_dir().join(format!("vivacity-bat-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let vendor = root.join("vendor");
let pkg = vendor.join("nikic/php-parser");
std::fs::create_dir_all(pkg.join("bin")).unwrap();
std::fs::write(
pkg.join("bin/php-parse"),
"#!/usr/bin/env php\n<?php\nrequire __DIR__.'/../lib/x.php';\n",
)
.unwrap();
(root, vendor, pkg)
}
#[test]
fn bat_proxy_bytes_match_composer_under_full_bin_compat() {
use vivacity_core::binproxy::{install_binaries, windows_proxy_content, BinCompat};
let (root, vendor, pkg) = scratch_pkg("bytes");
install_binaries(&vendor, &pkg, &["bin/php-parse"], BinCompat::Full).expect("install_binaries");
let proxy = vendor.join("bin").join("php-parse");
let bat = vendor.join("bin").join("php-parse.bat");
assert!(proxy.is_file(), "unixy proxy missing");
assert!(bat.is_file(), ".bat proxy missing under bin-compat full");
let expected = "@ECHO OFF\r\n\
setlocal DISABLEDELAYEDEXPANSION\r\n\
SET BIN_TARGET=%~dp0/php-parse\r\n\
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n\
php \"%BIN_TARGET%\" %*\r\n";
let got = std::fs::read_to_string(&bat).expect("read .bat");
assert_eq!(got, expected, ".bat diverges from Composer");
assert_eq!(got.len(), 136, ".bat must be 136 bytes");
let direct = windows_proxy_content(&bat, "php-parse", &pkg.join("bin/php-parse")).unwrap();
assert_eq!(direct, expected);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn bat_follows_resolved_bin_compat() {
use serde_json::json;
use vivacity_core::binproxy::{install_binaries, resolve_bin_compat_with, BinCompat};
let empty = json!({});
let full = json!({"config": {"bin-compat": "full"}});
let proxy = json!({"config": {"bin-compat": "proxy"}});
let g_full = json!("full");
let g_proxy = json!("proxy");
for (env, manifest, global, windows_or_wsl, want) in [
(None, &empty, None, false, BinCompat::Proxy), (None, &empty, None, true, BinCompat::Full), (None, &full, None, false, BinCompat::Full), (None, &proxy, None, true, BinCompat::Proxy), (Some("full"), &proxy, None, false, BinCompat::Full), (Some("symlink"), &empty, None, true, BinCompat::Proxy), (None, &empty, Some(&g_full), false, BinCompat::Full), (None, &proxy, Some(&g_full), true, BinCompat::Proxy), (Some("full"), &empty, Some(&g_proxy), false, BinCompat::Full), (Some(""), &empty, Some(&g_full), false, BinCompat::Full), (Some("0"), &empty, Some(&g_full), false, BinCompat::Full), ] {
assert_eq!(
resolve_bin_compat_with(env, manifest, global, windows_or_wsl).unwrap(),
want,
"env={env:?} manifest={manifest} global={global:?} windows_or_wsl={windows_or_wsl}"
);
}
assert!(
resolve_bin_compat_with(
None,
&json!({"config": {"bin-compat": "nope"}}),
None,
false
)
.is_err(),
"an invalid bin-compat is refused, like Composer"
);
assert!(
resolve_bin_compat_with(None, &empty, Some(&json!("nope")), false).is_err(),
"an invalid global bin-compat is refused too"
);
let (root, vendor, pkg) = scratch_pkg("rule");
install_binaries(&vendor, &pkg, &["bin/php-parse"], BinCompat::Proxy)
.expect("install_binaries");
assert!(
vendor.join("bin/php-parse").is_file(),
"unixy proxy missing"
);
assert!(
!vendor.join("bin/php-parse.bat").exists(),
".bat written under proxy mode — Composer writes it only when \
bin-compat resolves to full"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn bat_proxy_existing_is_skipped_not_overwritten() {
use vivacity_core::binproxy::{install_binaries, BinCompat};
let (root, vendor, pkg) = scratch_pkg("skip");
let bat = vendor.join("bin/php-parse.bat");
std::fs::create_dir_all(vendor.join("bin")).unwrap();
let sentinel = "@ECHO OFF\r\nREM user-managed proxy\r\n";
std::fs::write(&bat, sentinel).unwrap();
install_binaries(&vendor, &pkg, &["bin/php-parse"], BinCompat::Full).expect("install_binaries");
assert!(
vendor.join("bin/php-parse").is_file(),
"unixy proxy missing"
);
assert_eq!(
std::fs::read_to_string(&bat).unwrap(),
sentinel,
"an existing .bat proxy must be skipped, not overwritten"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn bat_caller_matches_determine_binary_caller() {
use std::path::Path;
use vivacity_core::binproxy::{windows_binary_caller, windows_proxy_content};
let content = windows_proxy_content(
Path::new("/work/vendor/bin/tool.bat"),
"tool",
Path::new("/work/vendor/acme/tool/bin/tool.exe"),
)
.unwrap();
assert_eq!(
content,
"@ECHO OFF\r\n\
setlocal DISABLEDELAYEDEXPANSION\r\n\
SET BIN_TARGET=%~dp0/../acme/tool/bin/tool.exe\r\n\
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n\
call \"%BIN_TARGET%\" %*\r\n"
);
let root = std::env::temp_dir().join(format!("vivacity-bat-caller-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let cmd = root.join("run.cmd");
std::fs::write(&cmd, "@echo off\r\necho hi\r\n").unwrap();
assert_eq!(windows_binary_caller(&cmd).unwrap(), "php");
for (shebang, want) in [
(
"#!/usr/bin/env php -dmemory_limit=1G\n",
"php -dmemory_limit=1G",
),
("#!/usr/bin/php -n\r\n", "php -n"),
("#!/bin/sh\n", "sh"),
("#!php\n", "php"), ] {
let f = root.join("target-bin");
std::fs::write(&f, format!("{shebang}<?php\n")).unwrap();
assert_eq!(
windows_binary_caller(&f).unwrap(),
want,
"shebang {shebang:?}"
);
}
let pkg_bin = root.join("vendor/acme/tool/bin");
std::fs::create_dir_all(&pkg_bin).unwrap();
let target = pkg_bin.join("tool");
std::fs::write(&target, "#!/usr/bin/env php -n\n<?php\n").unwrap();
let bat = root.join("vendor/bin/tool.bat");
let content = windows_proxy_content(&bat, "tool", &target).unwrap();
assert_eq!(
content,
"@ECHO OFF\r\n\
setlocal DISABLEDELAYEDEXPANSION\r\n\
SET BIN_TARGET=%~dp0/../acme/tool/bin/tool\r\n\
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0\r\n\
php -n \"%BIN_TARGET%\" %*\r\n"
);
let _ = std::fs::remove_dir_all(&root);
}
#[cfg(windows)]
#[test]
#[ignore = "requires `php` on PATH — run by the Windows CI via --ignored"]
fn bat_proxy_executes_with_a_real_php() {
use vivacity_core::binproxy::{install_binaries, resolve_bin_compat, BinCompat};
let root = std::env::temp_dir().join(format!("vivacity-bat-exec-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let vendor = root.join("vendor");
let pkg = vendor.join("acme/tool");
std::fs::create_dir_all(pkg.join("bin")).unwrap();
std::fs::write(
pkg.join("bin/greet"),
"#!/usr/bin/env php\n<?php\n\
$dir = getenv('COMPOSER_RUNTIME_BIN_DIR') !== false ? 'bin-dir-set' : 'bin-dir-missing';\n\
echo 'vivacity-bat:' . implode(',', array_slice($argv, 1)) . ':' . $dir . \"\n\";\n",
)
.unwrap();
let compat = resolve_bin_compat(&serde_json::json!({})).expect("resolve bin-compat");
assert_eq!(
compat,
BinCompat::Full,
"auto must resolve to full on Windows"
);
install_binaries(&vendor, &pkg, &["bin/greet"], compat).expect("install_binaries");
let bat = vendor.join("bin").join("greet.bat");
let out = std::process::Command::new(&bat)
.args(["alpha", "beta"])
.output()
.expect("launch the .bat via cmd.exe");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
".bat failed (php missing from PATH?)\nstdout: {stdout}\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(stdout.trim_end(), "vivacity-bat:alpha,beta:bin-dir-set");
let _ = std::fs::remove_dir_all(&root);
}