Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
# Generated by Athena v0.1.0 from MISSING_IMAGE_TEST deployment
# Generated: 2025-09-16 16:56:36 UTC
# Generated by Athena v0.1.0 from test_no_conflicts deployment
# Generated: 2025-09-17 13:08:45 UTC
# Features: Intelligent defaults, optimized networking, enhanced health checks
# DO NOT EDIT MANUALLY - This file is auto-generated

# Services: 1 configured with intelligent defaults
# Services: 3 configured with intelligent defaults

services:
test_service:
build:
context: .
dockerfile: Dockerfile
container_name: missing-image-test-test_service
app2:
image: httpd:alpine
container_name: test-no-conflicts-app2
ports:
- 8080:80
- 8081:8000
restart: unless-stopped
networks:
- missing_image_test_network
- test_no_conflicts_network
pull_policy: missing
labels:
athena.project: MISSING_IMAGE_TEST
athena.project: test_no_conflicts
athena.type: generic
athena.service: test_service
athena.generated: 2025-09-16
athena.service: app2
athena.generated: 2025-09-17

app1:
image: nginx:alpine
container_name: test-no-conflicts-app1
ports:
- 8080:80
restart: always
networks:
- test_no_conflicts_network
pull_policy: missing
labels:
athena.service: app1
athena.type: proxy
athena.project: test_no_conflicts
athena.generated: 2025-09-17

app3:
image: apache:latest
container_name: test-no-conflicts-app3
ports:
- 9000:80
restart: always
networks:
- test_no_conflicts_network
pull_policy: missing
labels:
athena.type: proxy
athena.project: test_no_conflicts
athena.service: app3
athena.generated: 2025-09-17
networks:
missing_image_test_network:
test_no_conflicts_network:
driver: bridge
name: MISSING_IMAGE_TEST
name: test_no_conflicts
57 changes: 54 additions & 3 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,17 @@ cargo test --test integration_tests structural --verbose
- Tests environment variable templating
- Tests port mappings, volume mounts
- Tests health checks and resource limits
- Tests port conflict detection during generation
- Tests successful generation with different ports
- Validates YAML syntax and structure

### 3. Error Handling Tests (`error_handling_test.rs`)
- Tests file not found scenarios
- Tests invalid syntax handling
- Tests circular dependency detection
- Tests port conflict detection with detailed error messages
- Tests port conflict suggestions for resolution
- Tests mixed port mapping scenarios
- Tests malformed configuration errors
- Tests permission and access errors
- Validates error message quality
Expand Down Expand Up @@ -252,10 +257,10 @@ Modular boilerplate tests organized by framework, each verifying:
### Test Performance & Statistics

**Current test suite:**
- **Total tests**: 67 integration tests
- **Total tests**: 69 integration tests
- **CLI tests**: 13 tests (command parsing, help, validation)
- **Docker Compose generation**: 9 tests (YAML generation and validation)
- **Error handling**: 18 tests (comprehensive error scenarios)
- **Docker Compose generation**: 11 tests (YAML generation, validation, port conflict detection)
- **Error handling**: 21 tests (comprehensive error scenarios including port conflicts)
- **Boilerplate generation**: 14 tests (modular by framework)
- **Structural tests**: 13 tests (organized in 6 categories)
- **Execution time**: < 1 second for structural tests
Expand All @@ -277,11 +282,57 @@ Modular boilerplate tests organized by framework, each verifying:
- `go_tests.rs`: 3 tests (Gin, Echo, Fiber frameworks)
- `common_tests.rs`: 3 tests (error handling, validation, help commands)

## Port Conflict Detection Tests

### Overview
As of the latest update, Athena includes comprehensive port conflict detection that prevents Docker Compose generation when multiple services attempt to use the same host port.

### Test Coverage

#### In `docker_compose_generation_test.rs`:
- **`test_port_conflict_prevention_in_generation`**: Verifies that Docker Compose generation fails when multiple services use the same host port (e.g., two services both mapping to port 8080)
- **`test_successful_generation_with_different_ports`**: Confirms that generation succeeds when services use different host ports (e.g., one service uses 8080, another uses 8081)

#### In `error_handling_test.rs`:
- **`test_port_conflict_detection`**: Tests basic port conflict detection using the fixture file
- **`test_port_conflict_suggestions`**: Verifies that the error message includes helpful port suggestions (e.g., "Consider using different ports like: 3000, 3001, 3002")
- **`test_no_port_conflicts_different_ports`**: Ensures no false positives when services use different ports
- **`test_port_conflict_with_mixed_mappings`**: Tests scenarios where services have multiple port mappings with conflicts

### Key Features Tested
1. **Conflict Detection**: Identifies when multiple services use the same host port
2. **Detailed Error Messages**: Provides clear information about which services are conflicting
3. **Port Suggestions**: Automatically generates alternative port suggestions
4. **Mixed Mappings**: Handles services with multiple port mappings correctly
5. **Integration with Generation**: Prevents invalid Docker Compose file creation

### Example Test Scenarios
```rust
// Conflict scenario - should fail
SERVICE service1
PORT-MAPPING 8080 TO 80
END SERVICE

SERVICE service2
PORT-MAPPING 8080 TO 8000 // ❌ Conflict on host port 8080
END SERVICE

// Valid scenario - should succeed
SERVICE service1
PORT-MAPPING 8080 TO 80
END SERVICE

SERVICE service2
PORT-MAPPING 8081 TO 8000 // ✅ Different host port
END SERVICE
```

### Coverage Goals
The test suite aims for >80% coverage on critical code paths:
- CLI argument parsing
- .ath file parsing and validation
- Docker Compose generation
- Port conflict detection and validation
- Error handling and reporting
- Project initialization

Expand Down
80 changes: 80 additions & 0 deletions src/athena/generator/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ fn validate_compose_enhanced(compose: &DockerCompose) -> AthenaResult<()> {
// Fast circular dependency detection
detect_circular_dependencies_optimized(compose)?;

// Detect port conflicts between services
detect_port_conflicts(compose)?;

Ok(())
}

Expand Down Expand Up @@ -266,6 +269,67 @@ fn has_cycle_iterative(
Ok(false)
}

/// Detect port conflicts between services
fn detect_port_conflicts(compose: &DockerCompose) -> AthenaResult<()> {
use std::collections::HashMap;

let mut port_to_services: HashMap<String, Vec<String>> = HashMap::new();

// Collect all host ports from all services
for (service_name, service) in &compose.services {
if let Some(ports) = &service.ports {
for port_mapping in ports {
if let Some(host_port) = extract_host_port(port_mapping) {
port_to_services
.entry(host_port)
.or_insert_with(Vec::new)
.push(service_name.clone());
}
}
}
}

// Check for conflicts
for (port, services) in port_to_services {
if services.len() > 1 {
return Err(AthenaError::ValidationError(
format!(
"Port conflict detected! Host port {} is used by multiple services: {}. \
Each service must use a unique host port. Consider using different ports like: {}",
port,
services.join(", "),
generate_port_suggestions(&port, services.len())
)
));
}
}

Ok(())
}

/// Extract host port from port mapping (e.g., "8080:80" -> "8080")
fn extract_host_port(port_mapping: &str) -> Option<String> {
let parts: Vec<&str> = port_mapping.split(':').collect();
if parts.len() >= 2 {
Some(parts[0].to_string())
} else {
None
}
}

/// Generate port suggestions for conflicts
fn generate_port_suggestions(base_port: &str, count: usize) -> String {
if let Ok(port_num) = base_port.parse::<u16>() {
let mut suggestions = Vec::new();
for i in 0..count {
suggestions.push((port_num + i as u16).to_string());
}
suggestions.join(", ")
} else {
"8080, 8081, 8082".to_string() // fallback suggestions
}
}

/// Improve YAML formatting for better readability by adding blank lines between services
fn improve_yaml_formatting(yaml: String) -> String {
let lines: Vec<&str> = yaml.lines().collect();
Expand Down Expand Up @@ -369,4 +433,20 @@ mod tests {
assert!(yaml.contains("restart: unless-stopped"));
assert!(yaml.contains("container_name: test-project-backend"));
}


#[test]
fn test_extract_host_port() {
assert_eq!(extract_host_port("8080:80"), Some("8080".to_string()));
assert_eq!(extract_host_port("3000:3000/tcp"), Some("3000".to_string()));
assert_eq!(extract_host_port("80"), None);
assert_eq!(extract_host_port(""), None);
}

#[test]
fn test_port_suggestions() {
assert_eq!(generate_port_suggestions("8080", 3), "8080, 8081, 8082");
assert_eq!(generate_port_suggestions("3000", 2), "3000, 3001");
assert_eq!(generate_port_suggestions("invalid", 2), "8080, 8081, 8082");
}
}
13 changes: 13 additions & 0 deletions tests/fixtures/port_conflicts.ath
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
DEPLOYMENT-ID test_project

SERVICES SECTION

SERVICE app1
IMAGE-ID nginx:alpine
PORT-MAPPING 8080 TO 80
END SERVICE

SERVICE app2
IMAGE-ID httpd:alpine
PORT-MAPPING 8080 TO 8000
END SERVICE
76 changes: 76 additions & 0 deletions tests/integration/docker_compose_generation_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,80 @@ END SERVICE"#;
.any(|n| n.as_str() == Some("custom_network_name"));
assert!(has_custom_network, "Service should be connected to custom network");
}
}

#[test]
fn test_port_conflict_prevention_in_generation() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let ath_content = r#"DEPLOYMENT-ID PORT_CONFLICT_TEST

SERVICES SECTION

SERVICE service1
IMAGE-ID nginx:alpine
PORT-MAPPING 8080 TO 80
END SERVICE

SERVICE service2
IMAGE-ID apache:latest
PORT-MAPPING 8080 TO 8000
END SERVICE"#;

let ath_file = create_test_ath_file(&temp_dir, "port_conflict.ath", ath_content);
let output_file = temp_dir.path().join("docker-compose.yml").to_string_lossy().to_string();

// This should fail due to port conflict
let result = run_athena_build(&ath_file, &output_file);
assert!(result.is_err(), "Build should fail due to port conflict");

let error_message = result.unwrap_err().to_string();
assert!(error_message.contains("Port conflict detected"),
"Error should mention port conflict detection");
assert!(error_message.contains("8080"),
"Error should mention the conflicting port");
}

#[test]
fn test_successful_generation_with_different_ports() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let ath_content = r#"DEPLOYMENT-ID NO_PORT_CONFLICT_TEST

SERVICES SECTION

SERVICE service1
IMAGE-ID nginx:alpine
PORT-MAPPING 8080 TO 80
END SERVICE

SERVICE service2
IMAGE-ID apache:latest
PORT-MAPPING 8081 TO 8000
END SERVICE"#;

let ath_file = create_test_ath_file(&temp_dir, "no_conflict.ath", ath_content);
let output_file = temp_dir.path().join("docker-compose.yml").to_string_lossy().to_string();

// This should succeed with different ports
let yaml_content = run_athena_build(&ath_file, &output_file)
.expect("Build should succeed with different ports");

let parsed: Value = parse_yaml_safely(&yaml_content)
.expect("Generated YAML should be valid");

let services = parsed["services"].as_mapping().expect("Services should be a mapping");
assert!(services.contains_key("service1"), "Should contain service1");
assert!(services.contains_key("service2"), "Should contain service2");

// Verify both services have their respective ports
let service1 = &services["service1"];
let service2 = &services["service2"];

let service1_ports = service1["ports"].as_sequence().expect("Service1 should have ports");
let service2_ports = service2["ports"].as_sequence().expect("Service2 should have ports");

let port1_str = service1_ports[0].as_str().unwrap();
let port2_str = service2_ports[0].as_str().unwrap();

assert!(port1_str.contains("8080"), "Service1 should use port 8080");
assert!(port2_str.contains("8081"), "Service2 should use port 8081");
}
Loading