use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
struct E2ETestEnvironment {
temp_dir: TempDir,
config_dir: PathBuf,
}
impl E2ETestEnvironment {
fn new() -> Self {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let config_dir = temp_dir.path().join(".pmdaemon");
fs::create_dir_all(&config_dir).expect("Failed to create config directory");
Self {
temp_dir,
config_dir,
}
}
fn cmd(&self) -> Command {
let mut cmd = Command::cargo_bin("pmdaemon").expect("Failed to find binary");
cmd.env("PMDAEMON_HOME", &self.config_dir);
cmd.env("NO_COLOR", "1");
cmd.env("RUST_LOG", "error");
cmd
}
fn temp_path(&self) -> &std::path::Path {
self.temp_dir.path()
}
fn unique_name(&self, base: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{}-{}", base, timestamp % 1000000)
}
}
fn create_script(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
#[cfg(windows)]
{
let script_path = dir.join(format!("{}.bat", name));
let batch_content = convert_bash_to_batch(content);
fs::write(&script_path, batch_content).expect("Failed to write test script");
script_path
}
#[cfg(not(windows))]
{
let script_path = dir.join(format!("{}.sh", name));
fs::write(&script_path, content).expect("Failed to write test script");
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script_path, perms).unwrap();
script_path
}
}
#[cfg(windows)]
fn convert_bash_to_batch(bash_content: &str) -> String {
let mut batch_content = String::from("@echo off\n");
for line in bash_content.lines() {
if line.starts_with("#!/") {
continue;
} else if line.starts_with("echo ") {
batch_content.push_str(&line.replace("echo ", "echo "));
batch_content.push('\n');
} else if line.starts_with("sleep ") {
let sleep_time = line
.replace("sleep ", "")
.trim()
.parse::<u32>()
.unwrap_or(1);
batch_content.push_str(&format!("timeout /t {} /nobreak >nul\n", sleep_time));
} else if line.contains("for i in {") && line.contains("}; do") {
if line.contains("{1..10}") {
batch_content.push_str("for /l %%i in (1,1,10) do (\n");
} else if line.contains("{1..5}") {
batch_content.push_str("for /l %%i in (1,1,5) do (\n");
}
} else if line.trim() == "done" {
batch_content.push_str(")\n");
} else if line.contains("exit ") {
batch_content.push_str(line);
batch_content.push('\n');
} else if !line.trim().is_empty() && !line.contains("$") {
batch_content.push_str(line);
batch_content.push('\n');
}
}
batch_content
}
fn create_python_script(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
let script_path = dir.join(format!("{}.py", name));
fs::write(&script_path, content).expect("Failed to write Python script");
script_path
}
fn create_node_script(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
let script_path = dir.join(format!("{}.js", name));
fs::write(&script_path, content).expect("Failed to write Node.js script");
script_path
}
#[test]
#[cfg(not(windows))]
fn test_simple_shell_script() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("shell-app");
let script = create_script(
env.temp_path(),
"simple_shell",
r#"#!/bin/bash
echo "Shell script starting..."
for i in {1..10}; do
echo "Iteration $i"
sleep 1
done
echo "Shell script completed"
"#,
);
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(500));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name))
.stdout(predicate::str::contains("online"));
env.cmd().args(["stop", &process_name]).assert().success();
env.cmd().args(["delete", &process_name]).assert().success();
}
#[test]
#[cfg(not(windows))]
fn test_python_script() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("python-app");
let script = create_python_script(
env.temp_path(),
"python_app",
r#"#!/usr/bin/env python3
import time
import sys
print("Python application starting...")
for i in range(5):
print(f"Python iteration {i+1}")
time.sleep(1)
print("Python application completed")
"#,
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script, perms).unwrap();
}
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(500));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name));
env.cmd().args(["delete", &process_name]).assert().success();
}
#[test]
#[cfg(not(windows))]
fn test_node_script() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("node-app");
let script = create_node_script(
env.temp_path(),
"node_app",
r#"#!/usr/bin/env node
console.log('Node.js application starting...');
let counter = 0;
const interval = setInterval(() => {
counter++;
console.log(`Node.js iteration ${counter}`);
if (counter >= 5) {
console.log('Node.js application completed');
clearInterval(interval);
process.exit(0);
}
}, 1000);
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully');
clearInterval(interval);
process.exit(0);
});
"#,
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script, perms).unwrap();
}
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(500));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name));
env.cmd().args(["delete", &process_name]).assert().success();
}
#[test]
#[cfg(not(windows))]
fn test_clustering_mode() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("cluster-app");
let script = create_script(
env.temp_path(),
"cluster_app",
r#"#!/bin/bash
echo "Cluster instance starting with PID $$"
echo "Instance ID: ${PM2_INSTANCE_ID:-0}"
sleep 5
echo "Cluster instance completed"
"#,
);
env.cmd()
.args([
"start",
script.to_str().unwrap(),
"--name",
&process_name,
"--instances",
"2",
])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(1000));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name));
let _ = env.cmd().args(["delete", &process_name]).assert();
}
#[test]
#[cfg(not(windows))]
fn test_port_management() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("port-app");
let script = create_script(
env.temp_path(),
"port_server",
r#"#!/bin/bash
echo "Server starting on port ${PORT:-8080}"
echo "Process PID: $$"
sleep 5
echo "Server shutting down"
"#,
);
env.cmd()
.args([
"start",
script.to_str().unwrap(),
"--name",
&process_name,
"--port",
"9000",
])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(500));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name))
.stdout(predicate::str::contains("9000"));
env.cmd().args(["delete", &process_name]).assert().success();
}
#[test]
#[cfg(not(windows))]
fn test_auto_restart() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("restart-app");
let script = create_script(
env.temp_path(),
"quick_exit",
r#"#!/bin/bash
echo "Process starting..."
sleep 1
echo "Process exiting..."
exit 1
"#,
);
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(2000));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name));
let _ = env.cmd().args(["delete", &process_name]).assert();
}
#[test]
#[cfg(not(windows))]
fn test_graceful_shutdown() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("graceful-app");
let script = create_script(
env.temp_path(),
"graceful_shutdown",
r#"#!/bin/bash
cleanup() {
echo "Received signal, cleaning up..."
sleep 1
echo "Cleanup completed, exiting gracefully"
exit 0
}
trap cleanup SIGTERM SIGINT
echo "Process starting with PID $$"
echo "Waiting for signal..."
# Run indefinitely until signal received
while true; do
sleep 1
done
"#,
);
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(500));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name))
.stdout(predicate::str::contains("online"));
env.cmd()
.args(["stop", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("Stopped"));
env.cmd().args(["delete", &process_name]).assert().success();
}
#[test]
#[cfg(not(windows))]
fn test_resource_monitoring() {
let env = E2ETestEnvironment::new();
let process_name = env.unique_name("monitor-app");
let script = create_script(
env.temp_path(),
"resource_app",
r#"#!/bin/bash
echo "Resource-intensive process starting..."
# Simulate some CPU and memory usage
for i in {1..5}; do
echo "Working... iteration $i"
# Create some temporary data
data=$(seq 1 1000)
sleep 1
done
echo "Resource process completed"
"#,
);
env.cmd()
.args(["start", script.to_str().unwrap(), "--name", &process_name])
.assert()
.success()
.stdout(predicate::str::contains("started"));
thread::sleep(Duration::from_millis(1000));
env.cmd()
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(&process_name));
env.cmd().args(["delete", &process_name]).assert().success();
}