Files
banderole/tests/integration_test.rs
T
2025-07-25 04:23:49 +04:00

908 lines
27 KiB
Rust

use std::process::Command;
use std::time::Duration;
use tempfile::TempDir;
#[tokio::test]
async fn test_bundle_and_run() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let test_app_path = temp_dir.path().join("test-app");
// Create a simple test app
std::fs::create_dir_all(&test_app_path)?;
let package_json = r#"{
"name": "integration-test-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}"#;
let index_js = r#"
const fs = require('fs');
const path = require('path');
console.log("Hello from integration test!");
console.log("Node version:", process.version);
console.log("Platform:", process.platform);
console.log("Architecture:", process.arch);
// Test file system access
const testFile = path.join(__dirname, 'test.txt');
fs.writeFileSync(testFile, 'test content');
const content = fs.readFileSync(testFile, 'utf8');
console.log("File content:", content);
// Test environment variables
console.log("Test env var:", process.env.TEST_VAR || 'not set');
// Test process arguments
console.log("Process args:", process.argv.slice(2));
// Test module resolution
try {
const uuid = require('uuid');
console.log("UUID:", uuid.v4());
} catch (e) {
console.error("Failed to require uuid:", e.message);
}
process.exit(0);"#;
// Write test files
std::fs::write(test_app_path.join("package.json"), package_json)?;
std::fs::write(test_app_path.join("index.js"), index_js)?;
// Install test dependency
let npm_install = if cfg!(windows) {
Command::new("cmd")
.args(["/C", "npm", "install", "uuid"])
.current_dir(&test_app_path)
.output()?
} else {
Command::new("sh")
.args(["-c", "npm install uuid"])
.current_dir(&test_app_path)
.output()?
};
if !npm_install.status.success() {
eprintln!("npm install failed: {}", String::from_utf8_lossy(&npm_install.stderr));
}
// Build banderole if not already built
println!("Building banderole...");
let target_dir = std::env::current_dir()?.join("target");
let banderole_path = target_dir.join("debug/banderole");
if !banderole_path.exists() {
let output = Command::new("cargo")
.args(["build"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Bundle the test app
println!("Bundling test app...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = bundle_cmd.output()?;
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
return Err("Bundle command failed".into());
}
// Find the created executable
let executable_path = temp_dir.path().join(if cfg!(windows) {
"integration-test-app.exe"
} else {
"integration-test-app"
});
if !executable_path.exists() {
let entries: Vec<_> = std::fs::read_dir(temp_dir.path())
.unwrap()
.filter_map(Result::ok)
.map(|e| e.file_name())
.collect();
panic!(
"Executable was not created at {}. Directory contents: {:?}",
executable_path.display(),
entries
);
}
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&executable_path)?.permissions();
let mut perms = perms.clone();
perms.set_mode(0o755);
std::fs::set_permissions(&executable_path, perms)?;
}
// Test 1: Run the executable directly
println!("Running test 1: Direct execution");
let output = Command::new(&executable_path)
.env("TEST_VAR", "test_value")
.args(["--test-arg1", "value1"])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!("Test 1 - Exit status: {}", output.status);
println!("Test 1 - Stdout: {}", stdout);
println!("Test 1 - Stderr: {}", stderr);
// Check for expected output
assert!(output.status.success(), "First run failed");
assert!(
stdout.contains("Hello from integration test!"),
"Expected greeting not found in output"
);
assert!(
stdout.contains("File content: test content"),
"File operations test failed"
);
assert!(
stdout.contains("Test env var: test_value"),
"Environment variable test failed"
);
assert!(
stdout.contains("--test-arg1"),
"Command line arguments test failed"
);
assert!(
stdout.contains("UUID: "),
"Module resolution test failed"
);
// Test 2: Run again to test cached execution
println!("Running test 2: Cached execution");
let output = Command::new(&executable_path)
.env("TEST_VAR", "cached_run")
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Test 2 - Exit status: {}", output.status);
println!("Test 2 - Output: {}", stdout);
assert!(output.status.success(), "Cached run failed");
assert!(
stdout.contains("Hello from integration test!"),
"Cached run output incorrect"
);
assert!(
stdout.contains("Test env var: cached_run"),
"Cached run env var not set correctly"
);
Ok(())
}
#[test]
fn test_node_version_detection() {
let temp_dir = TempDir::new().unwrap();
let test_app_path = temp_dir.path().join("test-app");
// Create a simple test app with .nvmrc
std::fs::create_dir_all(&test_app_path).unwrap();
let package_json = r#"{
"name": "nvmrc-test-app",
"version": "1.0.0",
"main": "index.js"
}"#;
let index_js = r#"console.log("Node version:", process.version);
console.log("Platform:", process.platform);
process.exit(0);"#;
let nvmrc = "20.18.1";
std::fs::write(test_app_path.join("package.json"), package_json).unwrap();
std::fs::write(test_app_path.join("index.js"), index_js).unwrap();
std::fs::write(test_app_path.join(".nvmrc"), nvmrc).unwrap();
// Build banderole
println!("Building banderole for Node version test...");
let output = Command::new("cargo")
.args(&["build", "--release"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
let banderole_path = std::env::current_dir()
.unwrap()
.join("target/release/banderole");
// Bundle the test app
println!("Bundling test app with .nvmrc...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(&["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = bundle_cmd.output()
.expect("Failed to bundle test app");
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
}
assert!(
bundle_output.status.success(),
"Bundle command failed: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
// Check that the output mentions the correct Node.js version
let stdout = String::from_utf8_lossy(&bundle_output.stdout);
let stderr = String::from_utf8_lossy(&bundle_output.stderr);
let combined_output = format!("{}{}", stdout, stderr);
println!("Bundle output: {}", combined_output);
assert!(
combined_output.contains("Node.js v20.18.1"),
"Expected Node.js version not found in output: {}",
combined_output
);
// Find and run the created executable to verify it uses the correct Node version
let executable_name = if cfg!(target_os = "windows") {
"nvmrc-test-app.exe"
} else {
"nvmrc-test-app"
};
let executable_path = temp_dir.path().join(executable_name);
assert!(
executable_path.exists(),
"Executable was not created: {}. Directory contents: {:?}",
executable_path.display(),
std::fs::read_dir(temp_dir.path())
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect::<Vec<_>>()
);
// Run the executable and check Node version
println!(
"Running executable to check Node version: {}",
executable_path.display()
);
let mut cmd = Command::new(&executable_path);
cmd.env("DEBUG", "1"); // Enable debug mode
// Use simple output instead of timeout to capture output correctly
let run_output = cmd.output()
.expect("Failed to run executable");
if !run_output.status.success() {
println!(
"Executable stdout: {}",
String::from_utf8_lossy(&run_output.stdout)
);
println!(
"Executable stderr: {}",
String::from_utf8_lossy(&run_output.stderr)
);
println!("Exit code: {:?}", run_output.status.code());
}
assert!(
run_output.status.success(),
"Executable failed with exit code {:?}. Stderr: {}",
run_output.status.code(),
String::from_utf8_lossy(&run_output.stderr)
);
let stdout = String::from_utf8_lossy(&run_output.stdout);
println!("Executable output: {}", stdout);
assert!(
stdout.contains("v20.18.1"),
"Expected Node.js version not found in executable output: {}",
stdout
);
}
#[tokio::test]
async fn test_output_path_collision_handling() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let test_app_path = temp_dir.path().join("collision-test-app");
// Create a simple test app
std::fs::create_dir_all(&test_app_path)?;
let package_json = r#"{
"name": "collision-test-app",
"version": "1.0.0",
"main": "index.js"
}"#;
let index_js = r#"console.log("Hello from collision test app!");"#;
std::fs::write(test_app_path.join("package.json"), package_json)?;
std::fs::write(test_app_path.join("index.js"), index_js)?;
// Create a directory with the same name as the app to cause collision
std::fs::create_dir_all(temp_dir.path().join("collision-test-app"))?;
// Build banderole
let target_dir = std::env::current_dir()?.join("target");
let banderole_path = target_dir.join("debug/banderole");
if !banderole_path.exists() {
let output = Command::new("cargo")
.args(["build"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Bundle the test app
println!("Testing output path collision handling...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = run_with_timeout(&mut bundle_cmd, Duration::from_secs(300))?;
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
return Err("Bundle command failed".into());
}
// Verify that a bundle was created with collision-avoided name
let expected_executable = temp_dir.path().join(if cfg!(windows) {
"collision-test-app-bundle.exe"
} else {
"collision-test-app-bundle"
});
assert!(
expected_executable.exists(),
"Executable was not created with collision-avoided name: {}. Directory contents: {:?}",
expected_executable.display(),
std::fs::read_dir(temp_dir.path())
.unwrap()
.filter_map(Result::ok)
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
// Test that the executable works
let output = Command::new(&expected_executable)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Collision test output: {}", stdout);
assert!(output.status.success(), "Collision test executable failed");
assert!(
stdout.contains("Hello from collision test app!"),
"Expected greeting not found in collision test output"
);
Ok(())
}
#[tokio::test]
async fn test_typescript_project_with_dist() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let test_app_path = temp_dir.path().join("ts-test-app");
// Create a TypeScript project structure
std::fs::create_dir_all(&test_app_path)?;
std::fs::create_dir_all(test_app_path.join("dist"))?;
let package_json = r#"{
"name": "ts-test-app",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc"
}
}"#;
let tsconfig_json = r#"{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}"#;
let src_index_ts = r#"console.log("Hello from TypeScript app!");
console.log("Node version:", process.version);
console.log("This should come from the dist directory");"#;
let dist_index_js = r#"console.log("Hello from TypeScript app!");
console.log("Node version:", process.version);
console.log("This should come from the dist directory");
try {
const marker = require('./marker.js');
console.log("Marker file found:", marker.source);
} catch (e) {
console.log("Marker file not found");
}"#;
// Create a distinctive file in dist to verify it was used as source
let dist_marker = r#"module.exports = { source: "dist" };"#;
// Write project files
std::fs::write(test_app_path.join("package.json"), package_json)?;
std::fs::write(test_app_path.join("tsconfig.json"), tsconfig_json)?;
std::fs::create_dir_all(test_app_path.join("src"))?;
std::fs::write(test_app_path.join("src/index.ts"), src_index_ts)?;
std::fs::write(test_app_path.join("dist/index.js"), dist_index_js)?;
std::fs::write(test_app_path.join("dist/marker.js"), dist_marker)?;
// Build banderole
let target_dir = std::env::current_dir()?.join("target");
let banderole_path = target_dir.join("debug/banderole");
if !banderole_path.exists() {
let output = Command::new("cargo")
.args(["build"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Bundle the TypeScript project
println!("Testing TypeScript project bundling...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = run_with_timeout(&mut bundle_cmd, Duration::from_secs(300))?;
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
return Err("TypeScript bundle command failed".into());
}
// We'll verify that the dist directory was used by running the executable
// and checking that it includes our marker file
// Find the created executable (may have collision avoidance suffix)
let mut executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-test-app.exe"
} else {
"ts-test-app"
});
// Check if collision avoidance was used (need to check if it's a file, not just exists)
if !executable_path.exists() || !executable_path.is_file() {
executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-test-app-bundle.exe"
} else {
"ts-test-app-bundle"
});
}
assert!(
executable_path.exists(),
"TypeScript executable was not created: {}. Found files: {:?}",
executable_path.display(),
std::fs::read_dir(temp_dir.path()).unwrap()
.filter_map(Result::ok)
.map(|e| e.file_name())
.collect::<Vec<_>>()
);
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&executable_path)?.permissions();
let mut perms = perms.clone();
perms.set_mode(0o755);
std::fs::set_permissions(&executable_path, perms)?;
}
// Test the executable
let output = Command::new(&executable_path)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
println!("TypeScript test output: {}", stdout);
assert!(output.status.success(), "TypeScript executable failed");
assert!(
stdout.contains("Hello from TypeScript app!"),
"Expected greeting not found in TypeScript output"
);
assert!(
stdout.contains("This should come from the dist directory"),
"Should be running from dist directory"
);
assert!(
stdout.contains("Marker file found: dist"),
"Should have included marker file from dist directory, indicating dist was used as source"
);
Ok(())
}
#[tokio::test]
async fn test_typescript_project_with_tsconfig_outdir() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let test_app_path = temp_dir.path().join("ts-outdir-test");
// Create a TypeScript project with custom outDir
std::fs::create_dir_all(&test_app_path)?;
std::fs::create_dir_all(test_app_path.join("build"))?;
let package_json = r#"{
"name": "ts-outdir-test",
"version": "1.0.0",
"main": "index.js"
}"#;
let tsconfig_json = r#"{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./build",
"rootDir": "./src"
}
}"#;
let build_index_js = r#"console.log("Hello from custom outDir!");
console.log("This comes from the build directory");
try {
const marker = require('./marker.js');
console.log("Marker file found:", marker.source);
} catch (e) {
console.log("Marker file not found");
}"#;
let build_marker = r#"module.exports = { source: "build" };"#;
// Write project files
std::fs::write(test_app_path.join("package.json"), package_json)?;
std::fs::write(test_app_path.join("tsconfig.json"), tsconfig_json)?;
std::fs::write(test_app_path.join("build/index.js"), build_index_js)?;
std::fs::write(test_app_path.join("build/marker.js"), build_marker)?;
// Build banderole
let target_dir = std::env::current_dir()?.join("target");
let banderole_path = target_dir.join("debug/banderole");
if !banderole_path.exists() {
let output = Command::new("cargo")
.args(["build"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Bundle the project
println!("Testing TypeScript project with custom outDir...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = bundle_cmd.output()?;
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
return Err("Custom outDir bundle command failed".into());
}
// We'll verify that the build directory was used by checking the marker file
// Find the created executable (may have collision avoidance suffix)
let mut executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-outdir-test.exe"
} else {
"ts-outdir-test"
});
// Check if collision avoidance was used (need to check if it's a file, not just exists)
if !executable_path.exists() || !executable_path.is_file() {
executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-outdir-test-bundle.exe"
} else {
"ts-outdir-test-bundle"
});
}
assert!(executable_path.exists(), "Custom outDir executable was not created");
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&executable_path)?.permissions();
let mut perms = perms.clone();
perms.set_mode(0o755);
std::fs::set_permissions(&executable_path, perms)?;
}
let output = Command::new(&executable_path)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Custom outDir test output: {}", stdout);
assert!(output.status.success(), "Custom outDir executable failed");
assert!(
stdout.contains("Hello from custom outDir!"),
"Expected greeting not found"
);
assert!(
stdout.contains("This comes from the build directory"),
"Should be running from build directory"
);
assert!(
stdout.contains("Marker file found: build"),
"Should have included marker file from build directory, indicating build was used as source"
);
Ok(())
}
#[tokio::test]
async fn test_typescript_project_with_extends() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let test_app_path = temp_dir.path().join("ts-extends-test");
// Create a TypeScript project with extends configuration
std::fs::create_dir_all(&test_app_path)?;
std::fs::create_dir_all(test_app_path.join("lib"))?;
let package_json = r#"{
"name": "ts-extends-test",
"version": "1.0.0",
"main": "index.js"
}"#;
let base_tsconfig = r#"{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./lib"
}
}"#;
let tsconfig_json = r#"{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"strict": true
}
}"#;
let lib_index_js = r#"console.log("Hello from extended tsconfig!");
console.log("This comes from the lib directory via extends");
try {
const marker = require('./marker.js');
console.log("Marker file found:", marker.source);
} catch (e) {
console.log("Marker file not found");
}"#;
let lib_marker = r#"module.exports = { source: "lib" };"#;
// Write project files
std::fs::write(test_app_path.join("package.json"), package_json)?;
std::fs::write(test_app_path.join("tsconfig.base.json"), base_tsconfig)?;
std::fs::write(test_app_path.join("tsconfig.json"), tsconfig_json)?;
std::fs::write(test_app_path.join("lib/index.js"), lib_index_js)?;
std::fs::write(test_app_path.join("lib/marker.js"), lib_marker)?;
// Build banderole
let target_dir = std::env::current_dir()?.join("target");
let banderole_path = target_dir.join("debug/banderole");
if !banderole_path.exists() {
let output = Command::new("cargo")
.args(["build"])
.output()
.expect("Failed to build banderole");
assert!(
output.status.success(),
"Failed to build banderole: {}",
String::from_utf8_lossy(&output.stderr)
);
}
// Bundle the project
println!("Testing TypeScript project with extends...");
let mut bundle_cmd = Command::new(&banderole_path);
bundle_cmd
.args(["bundle", test_app_path.to_str().unwrap()])
.current_dir(temp_dir.path());
let bundle_output = bundle_cmd.output()?;
if !bundle_output.status.success() {
println!(
"Bundle stdout: {}",
String::from_utf8_lossy(&bundle_output.stdout)
);
println!(
"Bundle stderr: {}",
String::from_utf8_lossy(&bundle_output.stderr)
);
return Err("Extends tsconfig bundle command failed".into());
}
// We'll verify that the lib directory was used by checking the marker file
// Find the created executable (may have collision avoidance suffix)
let mut executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-extends-test.exe"
} else {
"ts-extends-test"
});
// Check if collision avoidance was used (need to check if it's a file, not just exists)
if !executable_path.exists() || !executable_path.is_file() {
executable_path = temp_dir.path().join(if cfg!(windows) {
"ts-extends-test-bundle.exe"
} else {
"ts-extends-test-bundle"
});
}
assert!(executable_path.exists(), "Extends tsconfig executable was not created");
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::metadata(&executable_path)?.permissions();
let mut perms = perms.clone();
perms.set_mode(0o755);
std::fs::set_permissions(&executable_path, perms)?;
}
let output = Command::new(&executable_path)
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Extends tsconfig test output: {}", stdout);
assert!(output.status.success(), "Extends tsconfig executable failed");
assert!(
stdout.contains("Hello from extended tsconfig!"),
"Expected greeting not found"
);
assert!(
stdout.contains("This comes from the lib directory via extends"),
"Should be running from lib directory"
);
assert!(
stdout.contains("Marker file found: lib"),
"Should have included marker file from lib directory, indicating lib was used as source"
);
Ok(())
}
fn run_with_timeout(cmd: &mut Command, timeout: Duration) -> std::io::Result<std::process::Output> {
use std::sync::mpsc;
use std::thread;
let child = cmd.spawn().expect("Failed to spawn process");
let (tx, rx) = mpsc::channel();
// Spawn a thread to wait for the process
let child_id = child.id();
thread::spawn(move || {
let result = child.wait_with_output();
let _ = tx.send(result);
});
// Wait for either completion or timeout
match rx.recv_timeout(timeout) {
Ok(result) => result,
Err(_) => {
// Timeout occurred, kill the process
#[cfg(unix)]
{
let _ = std::process::Command::new("kill")
.args(&["-9", &child_id.to_string()])
.output();
}
#[cfg(windows)]
{
let _ = std::process::Command::new("taskkill")
.args(&["/F", "/PID", &child_id.to_string()])
.output();
}
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Process timed out",
))
}
}
}