Skip to content

Commit d8eeed7

Browse files
committed
Merge #47: Update E2E full tests to use CreateCommandHandler and fix preflight cleanup
0f3f76a refactor: [#39] move cleanup_previous_test_data to preflight_cleanup module (Jose Celano) 0e7d5a0 fix: [#37] add data directory cleanup to E2E preflight (copilot-swe-agent[bot]) ce59c6a refactor: [#37] move create_environment_via_command to e2e tasks module (copilot-swe-agent[bot]) e5f2fb0 refactor: [#37] improve error handling in create_environment_via_command (copilot-swe-agent[bot]) 4455c9b feat: [#37] update e2e-tests-full to use CreateCommandHandler (copilot-swe-agent[bot]) c0f248d Initial plan (copilot-swe-agent[bot]) Pull request description: Subissue 5/7 of #34 - The E2E full test suite was creating environments directly via `Environment::new()` instead of exercising the new `CreateCommandHandler`, missing test coverage of the command layer. Additionally, the E2E preflight cleanup was not removing stale environment data from previous test runs, causing test failures. ## Changes - **Created `run_create_command` task module**: New module in `src/testing/e2e/tasks/run_create_command.rs` that encapsulates environment creation using `CreateCommandHandler`, following the established pattern of other E2E tasks like `run_configure_command` - **Replaced direct environment creation**: Main test flow in `e2e_tests_full.rs` now uses the task module instead of `Environment::new()`, exercising the complete command pattern workflow - **Improved error handling**: Task module includes comprehensive error handling with `CreateTaskError` enum and `help()` method for detailed troubleshooting guidance - **Consistent patterns**: Uses `RepositoryFactory` and `Clock` as parameters, and `"data"` as base directory, matching the pattern in `TestContext.create_repository()` - **Added data directory cleanup**: New `cleanup_data_environment()` function in `src/testing/e2e/tasks/preflight_cleanup.rs` that removes `data/{environment_name}` directories from previous test runs, preventing "environment already exists" errors and ensuring proper test isolation ## Module Structure The new task follows the same structure as other E2E tasks: - Located in `src/testing/e2e/tasks/run_create_command.rs` - Exported from `src/testing/e2e/tasks/mod.rs` - Documented as an infrastructure-agnostic task ## Preflight Cleanup Enhancement The preflight cleanup now includes data directory removal in addition to build and templates cleanup: - **Container preflight** (`src/testing/e2e/tasks/container/preflight_cleanup.rs`): Calls `cleanup_data_environment()` before Docker cleanup - **VM preflight** (`src/testing/e2e/tasks/virtual_machine/preflight_cleanup.rs`): Calls `cleanup_data_environment()` before OpenTofu/LXD cleanup This ensures E2E tests always start with a clean slate, preventing failures from stale environment state left by interrupted previous test runs. ## Example ```rust // Before: Direct construction bypassed command layer let environment = Environment::new(environment_name, ssh_credentials, ssh_port); // After: Exercises CreateCommandHandler via task module with full validation and persistence let repository_factory = RepositoryFactory::new(Duration::from_secs(30)); let clock: Arc<dyn Clock> = Arc::new(SystemClock); let environment = run_create_command( &repository_factory, clock, "e2e-full", ssh_private_key_path.to_string_lossy().to_string(), ssh_public_key_path.to_string_lossy().to_string(), "torrust", DEFAULT_SSH_PORT, )?; ``` The E2E test suite now validates the command handler end-to-end through a properly structured task module, ensuring repository persistence and configuration validation work correctly in real deployment scenarios while maintaining consistency with the existing E2E testing architecture. The enhanced preflight cleanup guarantees reliable test execution by removing all artifacts from previous runs. - Fixes #39 <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[Subissue 5/7] Update E2E Full Tests to Use Create Command</issue_title> > <issue_description>**Parent Epic**: #34 - Implement Create Environment Command > **Depends On**: #36 - Application Layer Command > **Estimated Time**: 1-2 hours > > ## Overview > > Update the `src/bin/e2e_tests_full.rs` to use the new create command handler instead of direct environment creation. This ensures the full E2E test exercises the complete create command functionality as part of the comprehensive test suite. > > ## Goals > > - [ ] Add new function to create environment using CreateCommand handler > - [ ] Update e2e_tests_full.rs to use the new create command for environment creation > - [ ] Maintain existing test flow while exercising create command logic > - [ ] Ensure comprehensive test coverage of create command in full E2E context > - [ ] Preserve existing test reliability and performance > > ## Implementation Summary > > - **Location**: `src/bin/e2e_tests_full.rs` > - **Approach**: Add `create_environment_via_command()` function that uses CreateCommand handler > - **Integration**: Replace direct environment creation with command-based creation > - **Testing**: Not black-box like Subissue 4 - this uses the command handler directly > > ## Key Changes > > ```rust > // Add function to create environment using CreateCommand > async fn create_environment_via_command(context: &TestContext) -> Result<Environment<Created>> { > let create_command = CreateCommand::new(context.environment_repository.clone()); > > let config = EnvironmentCreationConfig { > environment_name: context.environment_name.clone(), > ssh_credentials: context.ssh_credentials.clone(), > }; > > create_command.execute(config) > } > > // Replace direct environment creation in main test function > let environment = create_environment_via_command(&context) > .await > .expect("Failed to create environment via command"); > ``` > > ## Acceptance Criteria > > - [ ] E2E full tests use CreateCommand handler for environment creation > - [ ] All existing E2E test functionality remains intact > - [ ] Create command is properly exercised in comprehensive test suite > - [ ] Test execution time and reliability maintained > - [ ] Proper error handling and logging preserved > > For detailed specification, see: [docs/issues/epic-create-environment-command-subissue-5-update-e2e-full-tests.md](https://github.com/torrust/torrust-tracker-deployer/blob/main/docs/issues/epic-create-environment-command-subissue-5-update-e2e-full-tests.md)</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #39 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. ACKs for top commit: josecelano: ACK 0f3f76a Tree-SHA512: 6f4ce20948a363d73fdf63e57193556d8a0ac9bdb06d01a4c7563c4cd51d649b9d8c8a0c1dd582f21c98c0a1c55509b027035551a8083bfdb2070a95c24558b0
2 parents 4ef803c + 0f3f76a commit d8eeed7

File tree

6 files changed

+380
-19
lines changed

6 files changed

+380
-19
lines changed

src/bin/e2e_tests_full.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,20 @@
5555
5656
use anyhow::Result;
5757
use clap::Parser;
58-
use std::time::Instant;
58+
use std::sync::Arc;
59+
use std::time::{Duration, Instant};
5960
use tracing::{error, info};
6061

6162
// Import E2E testing infrastructure
62-
use torrust_tracker_deployer_lib::adapters::ssh::{SshCredentials, DEFAULT_SSH_PORT};
63-
use torrust_tracker_deployer_lib::domain::{Environment, EnvironmentName};
63+
use torrust_tracker_deployer_lib::adapters::ssh::DEFAULT_SSH_PORT;
64+
use torrust_tracker_deployer_lib::infrastructure::persistence::repository_factory::RepositoryFactory;
6465
use torrust_tracker_deployer_lib::logging::{LogFormat, LogOutput, LoggingBuilder};
65-
use torrust_tracker_deployer_lib::shared::Username;
66+
use torrust_tracker_deployer_lib::shared::{Clock, SystemClock};
6667
use torrust_tracker_deployer_lib::testing::e2e::context::{TestContext, TestContextType};
6768
use torrust_tracker_deployer_lib::testing::e2e::tasks::{
69+
preflight_cleanup::cleanup_previous_test_data,
6870
run_configure_command::run_configure_command,
71+
run_create_command::run_create_command,
6972
run_test_command::run_test_command,
7073
virtual_machine::{
7174
preflight_cleanup::preflight_cleanup_previous_resources,
@@ -109,6 +112,7 @@ struct Cli {
109112
///
110113
/// May panic during the match statement if unexpected error combinations occur
111114
/// that are not handled by the current error handling logic.
115+
#[allow(clippy::too_many_lines)]
112116
#[tokio::main]
113117
pub async fn main() -> Result<()> {
114118
let cli = Cli::parse();
@@ -127,29 +131,38 @@ pub async fn main() -> Result<()> {
127131
"Starting E2E tests"
128132
);
129133

130-
// Create environment entity for e2e-full testing
131-
let environment_name = EnvironmentName::new("e2e-full".to_string())?;
132-
133134
// Use absolute paths to project root for SSH keys to ensure they can be found by Ansible
134135
let project_root = std::env::current_dir().expect("Failed to get current directory");
135136
let ssh_private_key_path = project_root.join("fixtures/testing_rsa");
136137
let ssh_public_key_path = project_root.join("fixtures/testing_rsa.pub");
137-
let ssh_user = Username::new("torrust").expect("Valid hardcoded username");
138-
let ssh_credentials = SshCredentials::new(
139-
ssh_private_key_path.clone(),
140-
ssh_public_key_path.clone(),
141-
ssh_user.clone(),
142-
);
143138

144-
let ssh_port = DEFAULT_SSH_PORT;
145-
let environment = Environment::new(environment_name, ssh_credentials, ssh_port);
139+
// Cleanup any artifacts from previous test runs BEFORE creating the environment
140+
// This prevents "environment already exists" errors from stale state
141+
// We do this before CreateCommandHandler because it checks if environment exists in repository
142+
cleanup_previous_test_data("e2e-full").map_err(|e| anyhow::anyhow!("{e}"))?;
143+
144+
// Create repository factory and clock for environment creation
145+
let repository_factory = RepositoryFactory::new(Duration::from_secs(30));
146+
let clock: Arc<dyn Clock> = Arc::new(SystemClock);
147+
148+
// Create environment via CreateCommandHandler
149+
let environment = run_create_command(
150+
&repository_factory,
151+
clock,
152+
"e2e-full",
153+
ssh_private_key_path.to_string_lossy().to_string(),
154+
ssh_public_key_path.to_string_lossy().to_string(),
155+
"torrust",
156+
DEFAULT_SSH_PORT,
157+
)
158+
.map_err(|e| anyhow::anyhow!("{e}"))?;
146159

147160
let mut test_context =
148161
TestContext::from_environment(cli.keep, environment, TestContextType::VirtualMachine)?
149162
.init()?;
150163

151-
// Cleanup any artifacts from previous test runs that may have failed to clean up
152-
// This ensures a clean slate before starting new tests
164+
// Additional preflight cleanup for infrastructure (OpenTofu, LXD resources)
165+
// This handles any lingering infrastructure from interrupted previous runs
153166
preflight_cleanup_previous_resources(&test_context)?;
154167

155168
let test_start = Instant::now();

src/testing/e2e/tasks/container/preflight_cleanup.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
use crate::shared::command::CommandExecutor;
99
use crate::testing::e2e::context::TestContext;
1010
use crate::testing::e2e::tasks::preflight_cleanup::{
11-
cleanup_build_directory, cleanup_templates_directory, PreflightCleanupError,
11+
cleanup_build_directory, cleanup_data_environment, cleanup_templates_directory,
12+
PreflightCleanupError,
1213
};
1314
use tracing::{info, warn};
1415

@@ -44,6 +45,9 @@ pub fn preflight_cleanup_previous_resources(
4445
// Clean the templates directory to ensure fresh embedded template extraction for E2E tests
4546
cleanup_templates_directory(env)?;
4647

48+
// Clean the data directory to ensure fresh environment state for E2E tests
49+
cleanup_data_environment(env)?;
50+
4751
// Clean up any hanging Docker containers from interrupted test runs
4852
cleanup_hanging_docker_containers(env);
4953

src/testing/e2e/tasks/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! The tasks are organized by deployment target:
1010
//!
1111
//! ### Infrastructure-agnostic tasks (can be used with both containers and VMs):
12+
//! - `run_create_command` - Environment creation using `CreateCommandHandler`
1213
//! - `run_configure_command` - Infrastructure configuration via Ansible and playbook execution
1314
//! - `run_configuration_validation` - Configuration validation and testing
1415
//! - `run_test_command` - Deployment validation and testing
@@ -30,5 +31,6 @@ pub mod container;
3031
pub mod preflight_cleanup;
3132
pub mod run_configuration_validation;
3233
pub mod run_configure_command;
34+
pub mod run_create_command;
3335
pub mod run_test_command;
3436
pub mod virtual_machine;

src/testing/e2e/tasks/preflight_cleanup.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,100 @@ impl std::error::Error for PreflightCleanupError {
4848
}
4949
}
5050

51+
// TODO: Refactor TestContext to eliminate the need for this workaround function
52+
//
53+
// Current issue: TestContext requires an Environment, but we need to clean up data
54+
// directories BEFORE creating the Environment (because CreateCommandHandler checks
55+
// if the environment already exists in the repository).
56+
//
57+
// Proposed solutions:
58+
// 1. Make Environment optional in TestContext (TestContext { environment: Option<Environment> })
59+
// 2. Move Environment out of TestContext (preferred - better separation of concerns)
60+
//
61+
// The second option is better because:
62+
// - TestContext should manage test infrastructure (services, config, temp directories)
63+
// - Environment is a domain entity that represents deployment state
64+
// - Separating them provides clearer responsibilities and easier testing
65+
//
66+
// After refactoring, we could eliminate this standalone function and have all cleanup
67+
// go through a single preflight_cleanup_previous_resources() that doesn't require
68+
// a fully initialized TestContext with an Environment.
69+
70+
/// Cleans the data directory for a specific environment name before `TestContext` creation
71+
///
72+
/// This helper function removes the `data/{environment_name}` directory to prevent
73+
/// "environment already exists" errors when `CreateCommandHandler` checks the repository.
74+
/// Unlike `cleanup_data_environment`, this function works without a `TestContext` and is
75+
/// intended to be called early in the test setup before environment creation.
76+
///
77+
/// # Safety
78+
///
79+
/// This function is only intended for E2E test environments and should never
80+
/// be called in production code paths. It's designed to provide test isolation
81+
/// by ensuring environments from previous test runs don't interfere.
82+
///
83+
/// # Arguments
84+
///
85+
/// * `environment_name` - The name of the environment to clean up
86+
///
87+
/// # Returns
88+
///
89+
/// Returns `Ok(())` if cleanup succeeds or if the directory doesn't exist.
90+
///
91+
/// # Errors
92+
///
93+
/// Returns a `PreflightCleanupError::ResourceConflicts` error if the data directory
94+
/// cannot be removed due to permission issues or file locks.
95+
pub fn cleanup_previous_test_data(environment_name: &str) -> Result<(), PreflightCleanupError> {
96+
use std::path::Path;
97+
98+
let data_dir = Path::new("data").join(environment_name);
99+
100+
if !data_dir.exists() {
101+
info!(
102+
operation = "preflight_data_cleanup",
103+
status = "clean",
104+
path = %data_dir.display(),
105+
"No previous data directory found, skipping cleanup"
106+
);
107+
return Ok(());
108+
}
109+
110+
info!(
111+
operation = "preflight_data_cleanup",
112+
path = %data_dir.display(),
113+
"Cleaning data directory from previous test run"
114+
);
115+
116+
match std::fs::remove_dir_all(&data_dir) {
117+
Ok(()) => {
118+
info!(
119+
operation = "preflight_data_cleanup",
120+
status = "success",
121+
path = %data_dir.display(),
122+
"Data directory cleaned successfully"
123+
);
124+
Ok(())
125+
}
126+
Err(e) => {
127+
warn!(
128+
operation = "preflight_data_cleanup",
129+
status = "failed",
130+
path = %data_dir.display(),
131+
error = %e,
132+
"Failed to clean data directory"
133+
);
134+
Err(PreflightCleanupError::ResourceConflicts {
135+
details: format!(
136+
"Failed to clean data directory '{}': {}",
137+
data_dir.display(),
138+
e
139+
),
140+
})
141+
}
142+
}
143+
}
144+
51145
/// Cleans the build directory to ensure fresh template state for E2E tests
52146
///
53147
/// This function removes the build directory if it exists, ensuring that
@@ -192,3 +286,79 @@ pub fn cleanup_templates_directory(
192286
}
193287
}
194288
}
289+
290+
/// Cleans the data directory for the test environment to ensure fresh state for E2E tests
291+
///
292+
/// This function removes the environment's data directory if it exists, ensuring that
293+
/// E2E tests start with a clean state and don't encounter conflicts with stale
294+
/// environment data from previous test runs. This prevents "environment already exists"
295+
/// errors and ensures proper test isolation.
296+
///
297+
/// # Safety
298+
///
299+
/// This function is only intended for E2E test environments and should never
300+
/// be called in production code paths. It's designed to provide test isolation
301+
/// by ensuring fresh environment state for each test run.
302+
///
303+
/// # Arguments
304+
///
305+
/// * `test_context` - The test context containing the environment configuration
306+
///
307+
/// # Returns
308+
///
309+
/// Returns `Ok(())` if cleanup succeeds or if the data directory doesn't exist.
310+
///
311+
/// # Errors
312+
///
313+
/// Returns a `PreflightCleanupError::ResourceConflicts` error if the data directory
314+
/// cannot be removed due to permission issues or file locks.
315+
pub fn cleanup_data_environment(test_context: &TestContext) -> Result<(), PreflightCleanupError> {
316+
use std::path::Path;
317+
318+
// Construct the data directory path: data/{environment_name}
319+
let data_dir = Path::new("data").join(test_context.environment.name().as_str());
320+
321+
if !data_dir.exists() {
322+
info!(
323+
operation = "data_directory_cleanup",
324+
status = "clean",
325+
path = %data_dir.display(),
326+
"Data directory doesn't exist, skipping cleanup"
327+
);
328+
return Ok(());
329+
}
330+
331+
info!(
332+
operation = "data_directory_cleanup",
333+
path = %data_dir.display(),
334+
"Cleaning data directory for previous test environment"
335+
);
336+
337+
match std::fs::remove_dir_all(&data_dir) {
338+
Ok(()) => {
339+
info!(
340+
operation = "data_directory_cleanup",
341+
status = "success",
342+
path = %data_dir.display(),
343+
"Data directory cleaned successfully"
344+
);
345+
Ok(())
346+
}
347+
Err(e) => {
348+
warn!(
349+
operation = "data_directory_cleanup",
350+
status = "failed",
351+
path = %data_dir.display(),
352+
error = %e,
353+
"Failed to clean data directory"
354+
);
355+
Err(PreflightCleanupError::ResourceConflicts {
356+
details: format!(
357+
"Failed to clean data directory '{}': {}",
358+
data_dir.display(),
359+
e
360+
),
361+
})
362+
}
363+
}
364+
}

0 commit comments

Comments
 (0)