#[allow(unused_imports)]
use anyhow::{Result, anyhow};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq)]
pub enum TestStatus {
Pending,
Running,
Passed,
Failed(String),
Skipped(String),
}
#[derive(Debug, Clone)]
pub struct TestCase {
pub name: String,
pub file: PathBuf,
pub status: TestStatus,
pub duration: Option<Duration>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TestSuite {
pub file: PathBuf,
pub tests: Vec<TestCase>,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub total_duration: Duration,
}
#[derive(Debug, Clone)]
pub struct TestRunnerConfig {
pub pattern: Option<String>,
pub verbose: bool,
pub test_timeout: Duration,
pub max_workers: usize,
}
impl Default for TestRunnerConfig {
fn default() -> Self {
Self {
pattern: None,
verbose: false,
test_timeout: Duration::from_secs(30),
max_workers: num_cpus::get(),
}
}
}
pub struct TestRunner {
config: TestRunnerConfig,
runtime: crate::Runtime,
}
impl TestRunner {
pub fn new(config: TestRunnerConfig) -> Result<Self> {
#[cfg(test)]
{
if !crate::is_v8_available() {
return Err(anyhow!("V8 engine is not available (Once instance is poisoned). Tests cannot run in parallel."));
}
}
let runtime: _ = crate::Runtime::new(
67108864, 1073741824, config.verbose, , false)?;
Ok(Self { config, runtime })
}
pub fn run_file(&self, file: &Path) -> Result<TestSuite> {
let start_time: _ = Instant::now();
if self.config.verbose {
println!("Running tests in: {}", file.display());
}
let result: _ = self.runtime.execute_file(&file.to_path_buf())?;
let tests: _ = self.parse_test_results(&result)?;
let passed: _ = tests
.iter()
.filter(|t| matches!(t.status, TestStatus::Passed))
.count();
let failed: _ = tests
.iter()
.filter(|t| matches!(t.status, TestStatus::Failed(_))
.count();
let skipped: _ = tests
.iter()
.filter(|t| matches!(t.status, TestStatus::Skipped(_))
.count();
let suite: _ = TestSuite {
file: file.to_path_buf(),
tests,
passed,
failed,
skipped,
total_duration: start_time.elapsed(),
};
if self.config.verbose {
println!(
"Tests completed: {} passed, {} failed, {} skipped",
suite.passed, suite.failed, suite.skipped
);
}
Ok(suite)
}
pub fn run_pattern(&self, pattern: &str) -> Result<Vec<TestSuite> {
let mut suites = Vec::new();
let test_files: _ = self.find_test_files(pattern)?;
for file in test_files {
match self.run_file(&file) {
Ok(suite) => suites.push(suite),
Err(e) => {
if self.config.verbose {
eprintln!("Failed to run tests in {}: {}", file.display(), e);
}
}
}
}
Ok(suites)
}
fn find_test_files(&self, _pattern: &str) -> Result<Vec<PathBuf> {
let mut files = Vec::new();
let patterns: _ = vec![
"**/*.test.js",
"**/*.spec.js",
"**/test/**/*.js",
"**/tests/**/*.js",
];
for _pattern in patterns {
if let Ok(entries) = std::fs::read_dir(".") {
for entry in entries {
if let Ok(entry) = entry {
let path: _ = entry.path();
if path.is_file() {
let file_name: _ = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name.contains("test") || file_name.contains("spec") {
if file_name.ends_with(".js") {
files.push(path);
}
}
}
}
}
}
}
Ok(files)
}
fn parse_test_results(&self, _output: &str) -> Result<Vec<TestCase> {
Ok(vec![TestCase {
name: "Sample Test".to_string(),
file: PathBuf::from("sample.js"),
status: TestStatus::Passed,
duration: Some(Duration::from_millis(10)),
error: None,
}])
}
pub fn get_stats(&self) -> TestStats {
TestStats {
total_suites: 0,
total_tests: 0,
passed: 0,
failed: 0,
skipped: 0,
total_duration: Duration::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TestStats {
pub total_suites: usize,
pub total_tests: usize,
pub passed: usize,
pub failed: usize,
pub skipped: usize,
pub total_duration: Duration,
}
impl TestStats {
pub fn format_summary(&self) -> String {
format!(
"Test Results: {} tests, {} passed, {} failed, {} skipped in {:.2}s",
self.total_tests,
self.passed,
self.failed,
self.skipped,
self.total_duration.as_secs_f64()
)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_runner_creation() {
require_v8!();
let config: _ = TestRunnerConfig::default();
let runner: _ = TestRunner::new(config);
assert!(runner.is_ok());
}
#[test]
fn test_run_simple_file() {
require_v8!();
let config: _ = TestRunnerConfig::default();
let runner: _ = TestRunner::new(config).unwrap();
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "console.log('test output');").unwrap();
let result: _ = runner.run_file(file.path());
assert!(result.is_ok());
}
}