mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2026-05-18 05:05:35 +02:00
refactor: refactored all the test files
This commit is contained in:
90
.github/test_organization.md
vendored
Normal file
90
.github/test_organization.md
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
# Test Organization for wrkflw
|
||||
|
||||
Following Rust best practices, we have reorganized the tests in this project to improve maintainability and clarity.
|
||||
|
||||
## Test Structure
|
||||
|
||||
Tests are now organized as follows:
|
||||
|
||||
### 1. Unit Tests
|
||||
|
||||
Unit tests remain in the source files using the `#[cfg(test)]` attribute. These tests are designed to test individual functions and small units of code in isolation.
|
||||
|
||||
Example:
|
||||
```rust
|
||||
// In src/matrix.rs
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_function() {
|
||||
// Test code here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Integration Tests
|
||||
|
||||
Integration tests have been moved to the `tests/` directory. These tests import and test the public API of the crate, ensuring that different components work together correctly.
|
||||
|
||||
- `tests/matrix_test.rs` - Tests for matrix expansion functionality
|
||||
- `tests/reusable_workflow_test.rs` - Tests for reusable workflow validation
|
||||
|
||||
### 3. End-to-End Tests
|
||||
|
||||
End-to-end tests are also located in the `tests/` directory. These tests simulate real-world usage scenarios and often involve external dependencies like Docker.
|
||||
|
||||
- `tests/cleanup_test.rs` - Tests for cleanup functionality with Docker containers, networks, etc.
|
||||
|
||||
## Running Tests
|
||||
|
||||
You can run all tests using:
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
To run only unit tests:
|
||||
```bash
|
||||
cargo test --lib
|
||||
```
|
||||
|
||||
To run only integration tests:
|
||||
```bash
|
||||
cargo test --test matrix_test --test reusable_workflow_test
|
||||
```
|
||||
|
||||
To run only end-to-end tests:
|
||||
```bash
|
||||
cargo test --test cleanup_test
|
||||
```
|
||||
|
||||
To run a specific test:
|
||||
```bash
|
||||
cargo test test_name
|
||||
```
|
||||
|
||||
## CI Configuration
|
||||
|
||||
Our CI workflow has been updated to run all types of tests separately, allowing for better isolation and clearer failure reporting:
|
||||
|
||||
```yaml
|
||||
- name: Run unit tests
|
||||
run: cargo test --lib --verbose
|
||||
|
||||
- name: Run integration tests
|
||||
run: cargo test --test matrix_test --test reusable_workflow_test --verbose
|
||||
|
||||
- name: Run e2e tests (if Docker available)
|
||||
run: cargo test --test cleanup_test --verbose -- --skip docker --skip processes
|
||||
```
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
When adding new tests:
|
||||
|
||||
1. For unit tests, add them to the relevant source file using `#[cfg(test)]`
|
||||
2. For integration tests, add them to the `tests/` directory with a descriptive name like `feature_name_test.rs`
|
||||
3. For end-to-end tests, also add them to the `tests/` directory with a descriptive name
|
||||
|
||||
Follow the existing patterns to ensure consistency.
|
||||
28
.github/workflows/rust.yml
vendored
28
.github/workflows/rust.yml
vendored
@@ -12,12 +12,32 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
test-unit:
|
||||
needs: [build]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run unit tests
|
||||
run: cargo test --lib --verbose
|
||||
|
||||
test-integration:
|
||||
needs: [build]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run integration tests
|
||||
run: cargo test --test matrix_test --test reusable_workflow_test --verbose
|
||||
|
||||
test-e2e:
|
||||
needs: [build]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run e2e tests (if Docker available)
|
||||
run: cargo test --test cleanup_test --verbose -- --skip docker --skip processes
|
||||
@@ -1,380 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod cleanup_tests {
|
||||
use crate::{
|
||||
cleanup_on_exit,
|
||||
executor::docker,
|
||||
runtime::emulation::{self, EmulationRuntime},
|
||||
};
|
||||
use bollard::Docker;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_docker_container_cleanup() {
|
||||
// Skip if running in CI environment for Linux
|
||||
if cfg!(target_os = "linux") && std::env::var("CI").is_ok() {
|
||||
println!("Skipping Docker container cleanup test in Linux CI environment");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if Docker is not available
|
||||
if !docker::is_available() {
|
||||
println!("Docker not available, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Docker
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => client,
|
||||
Err(_) => {
|
||||
println!("Could not connect to Docker, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a test container by tracking it
|
||||
let container_id = format!("test-container-{}", uuid::Uuid::new_v4());
|
||||
docker::track_container(&container_id);
|
||||
|
||||
// Verify container is tracked
|
||||
let containers = docker::get_tracked_containers();
|
||||
let is_tracked = containers.contains(&container_id);
|
||||
|
||||
assert!(is_tracked, "Container should be tracked for cleanup");
|
||||
|
||||
// Run cleanup with timeout
|
||||
match tokio::time::timeout(Duration::from_secs(10), docker::cleanup_containers(&docker))
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Cleanup completed within timeout
|
||||
// Verify container is no longer tracked
|
||||
let containers = docker::get_tracked_containers();
|
||||
let still_tracked = containers.contains(&container_id);
|
||||
|
||||
assert!(
|
||||
!still_tracked,
|
||||
"Container should be removed from tracking after cleanup"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
// Cleanup timed out
|
||||
println!("Container cleanup timed out after 10 seconds");
|
||||
// Manually untrack to clean up test state
|
||||
docker::untrack_container(&container_id);
|
||||
// Skip assertion as cleanup didn't complete within timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_docker_network_cleanup() {
|
||||
// Skip if running in CI environment for Linux
|
||||
if cfg!(target_os = "linux") && std::env::var("CI").is_ok() {
|
||||
println!("Skipping Docker network cleanup test in Linux CI environment");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if Docker is not available
|
||||
if !docker::is_available() {
|
||||
println!("Docker not available, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Docker
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => client,
|
||||
Err(_) => {
|
||||
println!("Could not connect to Docker, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a test network with timeout
|
||||
let network_id = match tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
docker::create_job_network(&docker),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => match result {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
println!("Could not create test network, skipping test");
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
println!("Network creation timed out after 10 seconds, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify network is tracked
|
||||
let networks = docker::get_tracked_networks();
|
||||
let is_tracked = networks.contains(&network_id);
|
||||
|
||||
assert!(is_tracked, "Network should be tracked for cleanup");
|
||||
|
||||
// Run cleanup with timeout
|
||||
match tokio::time::timeout(Duration::from_secs(10), docker::cleanup_networks(&docker)).await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Cleanup completed within timeout
|
||||
// Verify network is no longer tracked
|
||||
let networks = docker::get_tracked_networks();
|
||||
let still_tracked = networks.contains(&network_id);
|
||||
|
||||
assert!(
|
||||
!still_tracked,
|
||||
"Network should be removed from tracking after cleanup"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
// Cleanup timed out
|
||||
println!("Network cleanup timed out after 10 seconds");
|
||||
// Manually untrack to clean up test state
|
||||
docker::untrack_network(&network_id);
|
||||
// Skip assertion as cleanup didn't complete within timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_emulation_workspace_cleanup() {
|
||||
// Create an emulation runtime instance
|
||||
let _runtime = EmulationRuntime::new();
|
||||
|
||||
// Get the workspace path
|
||||
let workspaces = emulation::get_tracked_workspaces();
|
||||
if workspaces.is_empty() {
|
||||
println!("No workspace was tracked, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
let workspace_path = &workspaces[0];
|
||||
|
||||
// Verify workspace exists
|
||||
assert!(
|
||||
workspace_path.exists(),
|
||||
"Workspace should exist before cleanup"
|
||||
);
|
||||
|
||||
// Run cleanup
|
||||
emulation::cleanup_resources().await;
|
||||
|
||||
// Verify workspace is removed from tracking
|
||||
let workspaces = emulation::get_tracked_workspaces();
|
||||
let still_tracked = workspaces.iter().any(|w| w == workspace_path);
|
||||
|
||||
assert!(
|
||||
!still_tracked,
|
||||
"Workspace should be removed from tracking after cleanup"
|
||||
);
|
||||
|
||||
// Verify workspace directory is deleted
|
||||
assert!(
|
||||
!workspace_path.exists(),
|
||||
"Workspace directory should be deleted after cleanup"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_emulation_process_cleanup() {
|
||||
// Skip tests on CI or environments where spawning processes might be restricted
|
||||
if std::env::var("CI").is_ok() {
|
||||
println!("Running in CI environment, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a process for testing
|
||||
let process_id = if cfg!(unix) {
|
||||
// Use sleep on Unix but DO NOT use & to background
|
||||
// Instead run it directly and track the actual process
|
||||
let child = Command::new("sleep")
|
||||
.arg("10") // Shorter sleep time
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Ok(child) => {
|
||||
// Get the PID and track it
|
||||
let pid = child.id();
|
||||
emulation::track_process(pid);
|
||||
Some(pid)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
} else if cfg!(windows) {
|
||||
// Use timeout on Windows (equivalent to sleep)
|
||||
let child = Command::new("cmd")
|
||||
.arg("/C")
|
||||
.arg("timeout /t 10") // Shorter timeout
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Ok(child) => {
|
||||
// Get the PID and track it
|
||||
let pid = child.id();
|
||||
emulation::track_process(pid);
|
||||
Some(pid)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Skip if we couldn't create a process
|
||||
let process_id = match process_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
println!("Could not create test process, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Verify process is tracked
|
||||
let processes = emulation::get_tracked_processes();
|
||||
let is_tracked = processes.contains(&process_id);
|
||||
|
||||
assert!(is_tracked, "Process should be tracked for cleanup");
|
||||
|
||||
// Run cleanup
|
||||
emulation::cleanup_resources().await;
|
||||
|
||||
// Verify process is removed from tracking
|
||||
let processes = emulation::get_tracked_processes();
|
||||
let still_tracked = processes.contains(&process_id);
|
||||
|
||||
assert!(
|
||||
!still_tracked,
|
||||
"Process should be removed from tracking after cleanup"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleanup_on_exit_function() {
|
||||
// Skip test for Linux in CI environment
|
||||
if cfg!(target_os = "linux") && std::env::var("CI").is_ok() {
|
||||
println!("Skipping cleanup on exit test in Linux CI environment");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip on macOS as Docker operations may take longer
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("Skipping cleanup on exit test on macOS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Docker resources if available
|
||||
let docker_client = if docker::is_available() {
|
||||
match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => {
|
||||
// Create a network with timeout
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
docker::create_job_network(&client),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Some(client),
|
||||
Err(_) => {
|
||||
println!("Network creation timed out after 10 seconds, skipping Docker part of test");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
println!("Docker not available, skipping Docker part of test");
|
||||
None
|
||||
};
|
||||
|
||||
// Create an emulation runtime to track a workspace
|
||||
let _runtime = EmulationRuntime::new();
|
||||
|
||||
// Create a process to track in emulation mode
|
||||
if cfg!(unix) {
|
||||
let child = Command::new("sh").arg("-c").arg("sleep 10 &").spawn();
|
||||
|
||||
if let Ok(child) = child {
|
||||
emulation::track_process(child.id());
|
||||
}
|
||||
}
|
||||
|
||||
// Count initial resource tracking
|
||||
let docker_resources = if docker_client.is_some() {
|
||||
let containers = docker::get_tracked_containers().len();
|
||||
let networks = docker::get_tracked_networks().len();
|
||||
containers + networks
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let emulation_resources = {
|
||||
let processes = emulation::get_tracked_processes().len();
|
||||
let workspaces = emulation::get_tracked_workspaces().len();
|
||||
processes + workspaces
|
||||
};
|
||||
|
||||
// Skip if no resources were created
|
||||
if docker_resources == 0 && emulation_resources == 0 {
|
||||
println!("No resources were created, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Run cleanup with timeout - increased to 30 seconds for macOS compatibility
|
||||
match tokio::time::timeout(Duration::from_secs(30), cleanup_on_exit()).await {
|
||||
Ok(_) => {
|
||||
// Verify Docker resources are cleaned up
|
||||
if docker_client.is_some() {
|
||||
let remaining_containers = docker::get_tracked_containers().len();
|
||||
let remaining_networks = docker::get_tracked_networks().len();
|
||||
|
||||
assert_eq!(
|
||||
remaining_containers, 0,
|
||||
"All Docker containers should be cleaned up"
|
||||
);
|
||||
assert_eq!(
|
||||
remaining_networks, 0,
|
||||
"All Docker networks should be cleaned up"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify emulation resources are cleaned up
|
||||
let remaining_processes = emulation::get_tracked_processes().len();
|
||||
let remaining_workspaces = emulation::get_tracked_workspaces().len();
|
||||
|
||||
assert_eq!(
|
||||
remaining_processes, 0,
|
||||
"All emulation processes should be cleaned up"
|
||||
);
|
||||
assert_eq!(
|
||||
remaining_workspaces, 0,
|
||||
"All emulation workspaces should be cleaned up"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Cleanup timed out after 30 seconds");
|
||||
// Clean up any tracked resources to not affect other tests
|
||||
if docker_client.is_some() {
|
||||
for container_id in docker::get_tracked_containers() {
|
||||
docker::untrack_container(&container_id);
|
||||
}
|
||||
for network_id in docker::get_tracked_networks() {
|
||||
docker::untrack_network(&network_id);
|
||||
}
|
||||
}
|
||||
|
||||
for process_id in emulation::get_tracked_processes() {
|
||||
emulation::untrack_process(process_id);
|
||||
}
|
||||
for workspace_path in emulation::get_tracked_workspaces() {
|
||||
emulation::untrack_workspace(&workspace_path);
|
||||
}
|
||||
// Skip assertions since cleanup didn't complete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/lib.rs
Normal file
52
src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
pub mod evaluator;
|
||||
pub mod executor;
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
pub mod logging;
|
||||
pub mod matrix;
|
||||
pub mod models;
|
||||
pub mod parser;
|
||||
pub mod runtime;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
pub mod validators;
|
||||
|
||||
use bollard::Docker;
|
||||
|
||||
/// Clean up all resources when exiting the application
|
||||
/// This is used by both main.rs and in tests
|
||||
pub async fn cleanup_on_exit() {
|
||||
// Clean up Docker resources if available, but don't let it block indefinitely
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), async {
|
||||
match Docker::connect_with_local_defaults() {
|
||||
Ok(docker) => {
|
||||
let _ = executor::docker::cleanup_containers(&docker).await;
|
||||
let _ = executor::docker::cleanup_networks(&docker).await;
|
||||
}
|
||||
Err(_) => {
|
||||
// Docker not available
|
||||
logging::info("Docker not available, skipping Docker cleanup");
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => logging::debug("Docker cleanup completed successfully"),
|
||||
Err(_) => {
|
||||
logging::warning("Docker cleanup timed out after 3 seconds, continuing with shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
// Always clean up emulation resources
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
runtime::emulation::cleanup_resources(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => logging::debug("Emulation cleanup completed successfully"),
|
||||
Err(_) => logging::warning("Emulation cleanup timed out, continuing with shutdown"),
|
||||
}
|
||||
|
||||
logging::info("Resource cleanup completed");
|
||||
}
|
||||
17
src/main.rs
17
src/main.rs
@@ -1,18 +1,5 @@
|
||||
mod cleanup_test;
|
||||
mod evaluator;
|
||||
mod executor;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod logging;
|
||||
mod matrix;
|
||||
mod matrix_test;
|
||||
mod models;
|
||||
mod parser;
|
||||
mod reusable_workflow_test;
|
||||
mod runtime;
|
||||
mod ui;
|
||||
mod utils;
|
||||
mod validators;
|
||||
// Import public modules from lib.rs
|
||||
use wrkflw::*;
|
||||
|
||||
use bollard::Docker;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::matrix::{self, MatrixCombination, MatrixConfig};
|
||||
use indexmap::IndexMap;
|
||||
use serde_yaml::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn create_test_matrix() -> MatrixConfig {
|
||||
let mut matrix = MatrixConfig::default();
|
||||
|
||||
// Add basic parameters
|
||||
let mut params = IndexMap::new();
|
||||
|
||||
// Add 'os' parameter with array values
|
||||
let os_array = vec![
|
||||
Value::String("ubuntu".to_string()),
|
||||
Value::String("windows".to_string()),
|
||||
Value::String("macos".to_string()),
|
||||
];
|
||||
params.insert("os".to_string(), Value::Sequence(os_array));
|
||||
|
||||
// Add 'node' parameter with array values
|
||||
let node_array = vec![
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
Value::Number(serde_yaml::Number::from(16)),
|
||||
];
|
||||
params.insert("node".to_string(), Value::Sequence(node_array));
|
||||
|
||||
matrix.parameters = params;
|
||||
|
||||
// Add exclude pattern
|
||||
let mut exclude_item = HashMap::new();
|
||||
exclude_item.insert("os".to_string(), Value::String("windows".to_string()));
|
||||
exclude_item.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
);
|
||||
matrix.exclude = vec![exclude_item];
|
||||
|
||||
// Add include pattern
|
||||
let mut include_item = HashMap::new();
|
||||
include_item.insert("os".to_string(), Value::String("ubuntu".to_string()));
|
||||
include_item.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(18)),
|
||||
);
|
||||
include_item.insert("experimental".to_string(), Value::Bool(true));
|
||||
matrix.include = vec![include_item];
|
||||
|
||||
// Set max-parallel
|
||||
matrix.max_parallel = Some(2);
|
||||
|
||||
// Set fail-fast
|
||||
matrix.fail_fast = Some(true);
|
||||
|
||||
matrix
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matrix_expansion() {
|
||||
let matrix = create_test_matrix();
|
||||
|
||||
// Expand the matrix
|
||||
let combinations = matrix::expand_matrix(&matrix).unwrap();
|
||||
|
||||
// We should have 6 combinations:
|
||||
// 3 OS x 2 Node versions = 6 base combinations
|
||||
// - 1 excluded (windows + node 14)
|
||||
// + 1 included (ubuntu + node 18 + experimental)
|
||||
// = 6 total combinations
|
||||
assert_eq!(combinations.len(), 6);
|
||||
|
||||
// Check that the excluded combination is not present
|
||||
let excluded =
|
||||
combinations
|
||||
.iter()
|
||||
.find(|c| match (c.values.get("os"), c.values.get("node")) {
|
||||
(Some(Value::String(os)), Some(Value::Number(node))) => {
|
||||
os == "windows" && node.as_u64() == Some(14)
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
assert!(
|
||||
excluded.is_none(),
|
||||
"Excluded combination should not be present"
|
||||
);
|
||||
|
||||
// Check that the included combination is present
|
||||
let included = combinations.iter().find(|c| {
|
||||
match (
|
||||
c.values.get("os"),
|
||||
c.values.get("node"),
|
||||
c.values.get("experimental"),
|
||||
) {
|
||||
(Some(Value::String(os)), Some(Value::Number(node)), Some(Value::Bool(exp))) => {
|
||||
os == "ubuntu" && node.as_u64() == Some(18) && *exp
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
assert!(included.is_some(), "Included combination should be present");
|
||||
assert!(
|
||||
included.unwrap().is_included,
|
||||
"Combination should be marked as included"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_combination_name() {
|
||||
let mut values = HashMap::new();
|
||||
values.insert("os".to_string(), Value::String("ubuntu".to_string()));
|
||||
values.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
);
|
||||
|
||||
let combination = MatrixCombination {
|
||||
values,
|
||||
is_included: false,
|
||||
};
|
||||
|
||||
let formatted = matrix::format_combination_name("test-job", &combination);
|
||||
|
||||
// Should format as "test-job (os: ubuntu, node: 14)" or similar
|
||||
assert!(formatted.contains("test-job"));
|
||||
assert!(formatted.contains("os: ubuntu"));
|
||||
assert!(formatted.contains("node: 14"));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@ pub struct ValidationResult {
|
||||
pub issues: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ValidationResult {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
pub fn new() -> Self {
|
||||
ValidationResult {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::evaluator::evaluate_workflow_file;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_reusable_workflow_validation() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let workflow_path = temp_dir.path().join("test-workflow.yml");
|
||||
|
||||
// Create a workflow file that uses reusable workflows
|
||||
let content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-1-in-local-repo:
|
||||
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||
call-workflow-2-in-local-repo:
|
||||
uses: ./path/to/workflow.yml
|
||||
with:
|
||||
username: mona
|
||||
secrets:
|
||||
token: ${{ secrets.TOKEN }}
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be valid since we've fixed the validation to handle reusable workflows
|
||||
assert!(
|
||||
result.is_valid,
|
||||
"Workflow should be valid, but got issues: {:?}",
|
||||
result.issues
|
||||
);
|
||||
assert!(result.issues.is_empty());
|
||||
|
||||
// Create an invalid reusable workflow (bad format for 'uses')
|
||||
let invalid_content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-invalid:
|
||||
uses: invalid-format
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, invalid_content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be invalid due to the bad format
|
||||
assert!(!result.is_valid);
|
||||
assert!(result
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.contains("Invalid reusable workflow reference format")));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ pub struct EmulationRuntime {
|
||||
workspace: TempDir,
|
||||
}
|
||||
|
||||
impl Default for EmulationRuntime {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl EmulationRuntime {
|
||||
pub fn new() -> Self {
|
||||
// Create a temporary workspace to simulate container isolation
|
||||
|
||||
50
tests/README.md
Normal file
50
tests/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Testing Strategy
|
||||
|
||||
This directory contains integration tests for the `wrkflw` project. We follow the Rust testing best practices by organizing tests as follows:
|
||||
|
||||
## Test Organization
|
||||
|
||||
- **Unit Tests**: Located alongside the source files in `src/` using `#[cfg(test)]` modules
|
||||
- **Integration Tests**: Located directly in this `tests/` directory
|
||||
- `matrix_test.rs` - Tests for matrix expansion functionality
|
||||
- `reusable_workflow_test.rs` - Tests for reusable workflow validation
|
||||
- **End-to-End Tests**: Also located in this `tests/` directory
|
||||
- `cleanup_test.rs` - Tests for cleanup functionality with Docker resources
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run all tests:
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
To run only unit tests:
|
||||
```bash
|
||||
cargo test --lib
|
||||
```
|
||||
|
||||
To run only integration tests:
|
||||
```bash
|
||||
cargo test --test matrix_test --test reusable_workflow_test
|
||||
```
|
||||
|
||||
To run only end-to-end tests:
|
||||
```bash
|
||||
cargo test --test cleanup_test
|
||||
```
|
||||
|
||||
To run a specific test:
|
||||
```bash
|
||||
cargo test test_name
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Please follow these guidelines when writing tests:
|
||||
|
||||
1. Use meaningful test names that describe what is being tested
|
||||
2. Group related tests together in modules
|
||||
3. Use helper functions to reduce duplication
|
||||
4. Test both success and failure cases
|
||||
5. Use `#[should_panic]` for tests that expect a panic
|
||||
6. Avoid test interdependencies
|
||||
236
tests/cleanup_test.rs
Normal file
236
tests/cleanup_test.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use bollard::Docker;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use uuid::Uuid;
|
||||
use wrkflw::{
|
||||
cleanup_on_exit,
|
||||
executor::docker,
|
||||
runtime::emulation::{self, EmulationRuntime},
|
||||
};
|
||||
|
||||
// Skip the tests when running cargo test with --skip docker
|
||||
fn should_skip_docker_tests() -> bool {
|
||||
std::env::var("TEST_SKIP_DOCKER").is_ok() || !docker::is_available()
|
||||
}
|
||||
|
||||
// Skip the tests when running cargo test with --skip processes
|
||||
fn should_skip_process_tests() -> bool {
|
||||
std::env::var("TEST_SKIP_PROCESSES").is_ok() || std::env::var("CI").is_ok()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_docker_container_cleanup() {
|
||||
// Skip test based on flags or environment
|
||||
if should_skip_docker_tests() {
|
||||
println!("Skipping Docker container cleanup test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if running in CI environment for Linux
|
||||
if cfg!(target_os = "linux") && std::env::var("CI").is_ok() {
|
||||
println!("Skipping Docker container cleanup test in Linux CI environment");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Docker
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => client,
|
||||
Err(_) => {
|
||||
println!("Could not connect to Docker, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a test container by manually tracking it
|
||||
// In a real test, we would create an actual container, but we're just simulating that here
|
||||
let container_id = format!("test-container-{}", Uuid::new_v4());
|
||||
docker::track_container(&container_id);
|
||||
|
||||
// Run cleanup
|
||||
let _ = docker::cleanup_containers(&docker).await;
|
||||
|
||||
// Since we can't directly check the tracking status,
|
||||
// we'll use cleanup_on_exit and check for any errors
|
||||
match cleanup_on_exit().await {
|
||||
() => println!("Cleanup completed successfully"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_docker_network_cleanup() {
|
||||
// Skip test based on flags or environment
|
||||
if should_skip_docker_tests() {
|
||||
println!("Skipping Docker network cleanup test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if running in CI environment for Linux
|
||||
if cfg!(target_os = "linux") && std::env::var("CI").is_ok() {
|
||||
println!("Skipping Docker network cleanup test in Linux CI environment");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Docker
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => client,
|
||||
Err(_) => {
|
||||
println!("Could not connect to Docker, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a test network
|
||||
let network_id = match docker::create_job_network(&docker).await {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
println!("Could not create test network, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Run cleanup
|
||||
match docker::cleanup_networks(&docker).await {
|
||||
Ok(_) => println!("Network cleanup completed successfully"),
|
||||
Err(e) => println!("Network cleanup error: {}", e),
|
||||
}
|
||||
|
||||
// Attempt to remove the network again - this should fail if cleanup worked
|
||||
match docker.remove_network(&network_id).await {
|
||||
Ok(_) => println!("Network still exists, cleanup may not have worked"),
|
||||
Err(_) => println!("Network was properly cleaned up"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_emulation_workspace_cleanup() {
|
||||
// Create an emulation runtime instance
|
||||
let _runtime = EmulationRuntime::new();
|
||||
|
||||
// Run cleanup
|
||||
emulation::cleanup_resources().await;
|
||||
|
||||
// We can only verify that the cleanup operation doesn't crash
|
||||
// since we can't access the private tracking collections
|
||||
println!("Emulation workspace cleanup completed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // This test uses process manipulation which can be problematic
|
||||
async fn test_emulation_process_cleanup() {
|
||||
// Skip tests on CI or environments where spawning processes might be restricted
|
||||
if should_skip_process_tests() {
|
||||
println!("Skipping process cleanup test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a process for testing
|
||||
let process_id = if cfg!(unix) {
|
||||
// Use sleep on Unix but DO NOT use & to background
|
||||
// Instead run it directly and track the actual process
|
||||
let child = Command::new("sleep")
|
||||
.arg("10") // Shorter sleep time
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Ok(child) => {
|
||||
let pid = child.id();
|
||||
// Track the process for cleanup
|
||||
emulation::track_process(pid as u32);
|
||||
pid as u32
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Could not create test process, skipping test");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if cfg!(windows) {
|
||||
// On Windows, use a different long-running command
|
||||
let child = Command::new("timeout")
|
||||
.args(&["/t", "10", "/nobreak"])
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Ok(child) => {
|
||||
let pid = child.id();
|
||||
// Track the process for cleanup
|
||||
emulation::track_process(pid as u32);
|
||||
pid as u32
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Could not create test process, skipping test");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Unsupported platform, skipping test");
|
||||
return;
|
||||
};
|
||||
|
||||
// Run cleanup resources which includes process cleanup
|
||||
emulation::cleanup_resources().await;
|
||||
|
||||
// On Unix, verify process is no longer running
|
||||
if cfg!(unix) {
|
||||
// Allow a short delay for process termination
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Check if process exists
|
||||
let process_exists = unsafe {
|
||||
libc::kill(process_id as i32, 0) == 0
|
||||
|| std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
|
||||
};
|
||||
|
||||
assert!(
|
||||
!process_exists,
|
||||
"Process should be terminated after cleanup"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleanup_on_exit_function() {
|
||||
// Skip if Docker is not available
|
||||
if should_skip_docker_tests() {
|
||||
println!("Docker not available, skipping test");
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to Docker
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(client) => client,
|
||||
Err(_) => {
|
||||
println!("Could not connect to Docker, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create some resources for cleanup
|
||||
|
||||
// Track a container
|
||||
let container_id = format!("test-container-{}", Uuid::new_v4());
|
||||
docker::track_container(&container_id);
|
||||
|
||||
// Create a network
|
||||
let _ = match docker::create_job_network(&docker).await {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
println!("Could not create test network, skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Create an emulation workspace
|
||||
let _runtime = EmulationRuntime::new();
|
||||
|
||||
// Run cleanup function
|
||||
match tokio::time::timeout(Duration::from_secs(15), cleanup_on_exit()).await {
|
||||
Ok(_) => println!("Cleanup completed successfully"),
|
||||
Err(_) => {
|
||||
println!("Cleanup timed out after 15 seconds");
|
||||
// Attempt manual cleanup
|
||||
let _ = docker::cleanup_containers(&docker).await;
|
||||
let _ = docker::cleanup_networks(&docker).await;
|
||||
emulation::cleanup_resources().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
tests/matrix_test.rs
Normal file
125
tests/matrix_test.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use indexmap::IndexMap;
|
||||
use serde_yaml::Value;
|
||||
use std::collections::HashMap;
|
||||
use wrkflw::matrix::{self, MatrixCombination, MatrixConfig};
|
||||
|
||||
fn create_test_matrix() -> MatrixConfig {
|
||||
let mut matrix = MatrixConfig::default();
|
||||
|
||||
// Add basic parameters
|
||||
let mut params = IndexMap::new();
|
||||
|
||||
// Add 'os' parameter with array values
|
||||
let os_array = vec![
|
||||
Value::String("ubuntu".to_string()),
|
||||
Value::String("windows".to_string()),
|
||||
Value::String("macos".to_string()),
|
||||
];
|
||||
params.insert("os".to_string(), Value::Sequence(os_array));
|
||||
|
||||
// Add 'node' parameter with array values
|
||||
let node_array = vec![
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
Value::Number(serde_yaml::Number::from(16)),
|
||||
];
|
||||
params.insert("node".to_string(), Value::Sequence(node_array));
|
||||
|
||||
matrix.parameters = params;
|
||||
|
||||
// Add exclude pattern
|
||||
let mut exclude_item = HashMap::new();
|
||||
exclude_item.insert("os".to_string(), Value::String("windows".to_string()));
|
||||
exclude_item.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
);
|
||||
matrix.exclude = vec![exclude_item];
|
||||
|
||||
// Add include pattern
|
||||
let mut include_item = HashMap::new();
|
||||
include_item.insert("os".to_string(), Value::String("ubuntu".to_string()));
|
||||
include_item.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(18)),
|
||||
);
|
||||
include_item.insert("experimental".to_string(), Value::Bool(true));
|
||||
matrix.include = vec![include_item];
|
||||
|
||||
// Set max-parallel
|
||||
matrix.max_parallel = Some(2);
|
||||
|
||||
// Set fail-fast
|
||||
matrix.fail_fast = Some(true);
|
||||
|
||||
matrix
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matrix_expansion() {
|
||||
let matrix = create_test_matrix();
|
||||
|
||||
// Expand the matrix
|
||||
let combinations = matrix::expand_matrix(&matrix).unwrap();
|
||||
|
||||
// We should have 6 combinations:
|
||||
// 3 OS x 2 Node versions = 6 base combinations
|
||||
// - 1 excluded (windows + node 14)
|
||||
// + 1 included (ubuntu + node 18 + experimental)
|
||||
// = 6 total combinations
|
||||
assert_eq!(combinations.len(), 6);
|
||||
|
||||
// Check that the excluded combination is not present
|
||||
let excluded = combinations
|
||||
.iter()
|
||||
.find(|c| match (c.values.get("os"), c.values.get("node")) {
|
||||
(Some(Value::String(os)), Some(Value::Number(node))) => {
|
||||
os == "windows" && node.as_u64() == Some(14)
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
assert!(
|
||||
excluded.is_none(),
|
||||
"Excluded combination should not be present"
|
||||
);
|
||||
|
||||
// Check that the included combination is present
|
||||
let included = combinations.iter().find(|c| {
|
||||
match (
|
||||
c.values.get("os"),
|
||||
c.values.get("node"),
|
||||
c.values.get("experimental"),
|
||||
) {
|
||||
(Some(Value::String(os)), Some(Value::Number(node)), Some(Value::Bool(exp))) => {
|
||||
os == "ubuntu" && node.as_u64() == Some(18) && *exp
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
assert!(included.is_some(), "Included combination should be present");
|
||||
assert!(
|
||||
included.unwrap().is_included,
|
||||
"Combination should be marked as included"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_combination_name() {
|
||||
let mut values = HashMap::new();
|
||||
values.insert("os".to_string(), Value::String("ubuntu".to_string()));
|
||||
values.insert(
|
||||
"node".to_string(),
|
||||
Value::Number(serde_yaml::Number::from(14)),
|
||||
);
|
||||
|
||||
let combination = MatrixCombination {
|
||||
values,
|
||||
is_included: false,
|
||||
};
|
||||
|
||||
let formatted = matrix::format_combination_name("test-job", &combination);
|
||||
|
||||
// Should format as "test-job (os: ubuntu, node: 14)" or similar
|
||||
assert!(formatted.contains("test-job"));
|
||||
assert!(formatted.contains("os: ubuntu"));
|
||||
assert!(formatted.contains("node: 14"));
|
||||
}
|
||||
64
tests/reusable_workflow_test.rs
Normal file
64
tests/reusable_workflow_test.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
use wrkflw::evaluator::evaluate_workflow_file;
|
||||
|
||||
#[test]
|
||||
fn test_reusable_workflow_validation() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let workflow_path = temp_dir.path().join("test-workflow.yml");
|
||||
|
||||
// Create a workflow file that uses reusable workflows
|
||||
let content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-1-in-local-repo:
|
||||
uses: octo-org/this-repo/.github/workflows/workflow-1.yml@172239021f7ba04fe7327647b213799853a9eb89
|
||||
call-workflow-2-in-local-repo:
|
||||
uses: ./path/to/workflow.yml
|
||||
with:
|
||||
username: mona
|
||||
secrets:
|
||||
token: ${{ secrets.TOKEN }}
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be valid since we've fixed the validation to handle reusable workflows
|
||||
assert!(
|
||||
result.is_valid,
|
||||
"Workflow should be valid, but got issues: {:?}",
|
||||
result.issues
|
||||
);
|
||||
assert!(result.issues.is_empty());
|
||||
|
||||
// Create an invalid reusable workflow (bad format for 'uses')
|
||||
let invalid_content = r#"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
call-workflow-invalid:
|
||||
uses: invalid-format
|
||||
"#;
|
||||
|
||||
fs::write(&workflow_path, invalid_content).unwrap();
|
||||
|
||||
// Validate the workflow
|
||||
let result = evaluate_workflow_file(&workflow_path, false).unwrap();
|
||||
|
||||
// Should be invalid due to the bad format
|
||||
assert!(!result.is_valid);
|
||||
assert!(result
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.contains("Invalid reusable workflow reference format")));
|
||||
}
|
||||
Reference in New Issue
Block a user