use std::process::Command; use std::time::Duration; use tempfile::TempDir; #[tokio::test] async fn test_bundle_and_run() -> Result<(), Box> { 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::>() ); // 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> { 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::>() ); // 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> { 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::>() ); // 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> { 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> { 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 { 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", )) } } }