Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
fetch-depth: 0

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v8
id: golangci
with:
version: v2.4.0
Expand Down
2 changes: 0 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ linters:
forbid:
- pattern: os\.Getenv
msg: Use `viper.BindEnv` for new environment variables instead of `os.Getenv`
- pattern: '\.Skip\('
msg: Use `t.Skipf` with a descriptive reason instead of `t.Skip`
funlen:
lines: 60
statements: 40
Expand Down
44 changes: 36 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,47 @@ Atmos provides extensive template functions available in stack configurations:

## Testing Guidelines

### Test Strategy with Preconditions
Atmos uses **precondition-based test skipping** to provide a better developer experience. Tests check for required preconditions (AWS profiles, network access, Git configuration) and skip gracefully with helpful messages rather than failing. See:
- **[Testing Strategy PRD](docs/prd/testing-strategy.md)** - Complete design document
- **[Tests README](tests/README.md)** - Practical testing guide with examples
- **[Test Preconditions](tests/test_preconditions.go)** - Helper functions for precondition checks

### Running Tests
```bash
# Run all tests (will skip if preconditions not met)
go test ./...

# Bypass all precondition checks
export ATMOS_TEST_SKIP_PRECONDITION_CHECKS=true
go test ./...

# Run with verbose output to see skips
go test -v ./...
```

### Test File Locations
- Unit tests: `pkg/**/*_test.go`
- Integration tests: `tests/**/*_test.go` with fixtures in `tests/test-cases/`
- Command tests: `cmd/**/*_test.go`
- Test helpers: `tests/test_preconditions.go`

### Running Specific Tests
```bash
# Run specific test
go test ./pkg/config -run TestConfigLoad
# Run with coverage
go test ./pkg/config -cover
# Integration tests
go test ./tests -run TestCLI
### Writing Tests with Preconditions
```go
func TestAWSFeature(t *testing.T) {
// Check AWS precondition at test start
tests.RequireAWSProfile(t, "profile-name")
// ... test code
}

func TestGitHubVendoring(t *testing.T) {
// Check GitHub access with rate limits
rateLimits := tests.RequireGitHubAccess(t)
if rateLimits != nil && rateLimits.Remaining < 20 {
t.Skipf("Need at least 20 GitHub API requests, only %d remaining", rateLimits.Remaining)
}
// ... test code
}
```

### Test Data
Expand Down
161 changes: 161 additions & 0 deletions docs/prd/test-preconditions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Test Preconditions and Intelligent Skipping

## Overview

This document defines the precondition checking and intelligent test skipping capabilities in Atmos's testing framework. This is one component of Atmos's broader testing strategy, specifically addressing how tests handle missing environmental dependencies.

## Problem Statement

Tests were failing when environmental dependencies weren't available (e.g., AWS profiles, network access, Git configuration), making it difficult to:
- Distinguish between actual code failures and missing prerequisites
- Run partial test suites during development
- Onboard new developers who haven't configured all dependencies

## Solution: Intelligent Precondition Checking

### Core Capability

Tests can detect missing preconditions and skip gracefully with informative messages rather than failing. This allows developers to run test suites even without full environment setup while maintaining clear visibility into what was skipped and why.

## Implementation

### Precondition Helper Functions

Created a centralized set of helper functions in `tests/test_preconditions.go` that check for specific preconditions and skip tests when not met.

**Available Helpers**:

| Function | Purpose | Skip Condition |
|----------|---------|----------------|
| `RequireAWSProfile(t, profile)` | Verify AWS profile exists | Profile not configured |
| `RequireGitRepository(t)` | Ensure running in Git repo | Not a Git repository |
| `RequireGitRemoteWithValidURL(t)` | Check Git remote configuration | No valid remote URL |
| `RequireGitHubAccess(t)` | Test GitHub API connectivity | Network issues or rate limits |
| `RequireNetworkAccess(t, url)` | Verify network connectivity | URL unreachable |
| `RequireExecutable(t, name)` | Check executable availability | Not found in PATH |
| `RequireEnvVar(t, name)` | Ensure environment variable set | Variable not set |

**Key Features**:
- Helpers return useful data when preconditions pass (e.g., GitHub rate limit info)
- Cross-platform compatible with OS-appropriate checks
- Consistent skip message format across all helpers

### Skip Message Standards

All skip messages follow a consistent format to maximize developer understanding:

```
<what's missing>: <why needed>. <how to fix> or set ATMOS_TEST_SKIP_PRECONDITION_CHECKS=true
```

**Examples**:
- `"AWS profile 'dev' not configured: required for S3 backend testing. Configure AWS credentials or set ATMOS_TEST_SKIP_PRECONDITION_CHECKS=true"`
- `"GitHub API rate limit too low (5 remaining): need at least 20 requests. Wait for reset at 15:30 or set ATMOS_TEST_SKIP_PRECONDITION_CHECKS=true"`

### Override Mechanism

**Environment Variable**: `ATMOS_TEST_SKIP_PRECONDITION_CHECKS`
- When set to `true`, all precondition checks are bypassed
- Useful for CI environments with mocked dependencies
- Enables focused testing without full environment setup

**Implementation**:
```go
func ShouldCheckPreconditions() bool {
return os.Getenv("ATMOS_TEST_SKIP_PRECONDITION_CHECKS") != "true"
}
```

### Linting and Enforcement

**Enforced Rules**:
1. **Must use `t.Skipf()` instead of `t.Skip()`**: Ensures all skips include descriptive reasons
- Enforced via `forbidigo` linter pattern: `\.Skip\(`
- Message: "Use t.Skipf with a descriptive reason instead of t.Skip"

2. **Environment variable handling**:
- Production code must use `viper.BindEnv` (enforced by `forbidigo`)
- Test files and test helpers can use `os.Getenv`/`os.Setenv`
- Configured via `exclude-rules` in `.golangci.yml`

### Binary Freshness Detection

For CLI tests that depend on built binaries:
- TestMain checks if binary is outdated
- Sets package-level `skipReason` if rebuild needed
- Individual tests check and skip with clear message about running `make build`

## Usage Patterns

### In Test Files

```go
func TestAWSIntegration(t *testing.T) {
// Check precondition at test start
tests.RequireAWSProfile(t, "dev-profile")

// Test code only runs if precondition met
// ...
}

func TestGitHubVendoring(t *testing.T) {
// Check multiple preconditions
tests.RequireGitRepository(t)
rateLimits := tests.RequireGitHubAccess(t)

// Can make decisions based on returned data
if rateLimits != nil && rateLimits.Remaining < 50 {
t.Skipf("Need at least 50 API requests, only %d remaining", rateLimits.Remaining)
}

// Test code
// ...
}
```

### Developer Workflow

1. **Run tests to see requirements**:
```bash
go test ./...
# SKIP: AWS profile 'dev' not configured: required for S3 backend testing...
```

2. **Either configure dependencies**:
```bash
aws configure --profile dev
export GITHUB_TOKEN=ghp_...
```

3. **Or bypass checks**:
```bash
export ATMOS_TEST_SKIP_PRECONDITION_CHECKS=true
go test ./...
```

## Benefits Achieved

1. **Improved Developer Experience**: Tests skip gracefully instead of failing mysteriously
2. **Clear Communication**: Every skip explains what's missing and how to fix it
3. **Flexibility**: Developers can bypass checks when appropriate
4. **Consistency**: Standardized patterns across the test suite
5. **Maintainability**: Centralized helpers reduce duplication

## Relationship to Overall Testing Strategy

This precondition checking system is one component of Atmos's comprehensive testing approach, which also includes:
- Unit tests with mocked dependencies
- Integration tests with real services
- Acceptance tests for end-to-end validation
- Performance benchmarks
- Fuzz testing for input validation
- CI/CD automation with GitHub Actions

The precondition system specifically enhances the integration and acceptance test layers by making them more accessible to developers working in varied environments.

## Future Enhancements

- `ATMOS_TEST_MOCK_AWS`: Automatically use mocked AWS services
- `ATMOS_TEST_OFFLINE`: Skip all network-dependent tests
- `ATMOS_TEST_VERBOSE_SKIP`: Provide detailed skip reasoning
- Integration with test coverage tools to track skip patterns
1 change: 1 addition & 0 deletions examples/quick-start-advanced/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
atmos 1.189.0
4 changes: 4 additions & 0 deletions internal/aws_utils/aws_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import (
"time"

"github.com/stretchr/testify/assert"

"github.com/cloudposse/atmos/tests"
)

func TestLoadAWSConfig(t *testing.T) {
// Check for AWS profile precondition
tests.RequireAWSProfile(t, "cplive-core-gbl-identity")
tests := []struct {
name string
region string
Expand Down
4 changes: 4 additions & 0 deletions internal/exec/atlantis_generate_repo_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/tests"
)

func TestExecuteAtlantisGenerateRepoConfigWithStackNameTemplate(t *testing.T) {
Expand Down Expand Up @@ -37,6 +38,9 @@ func TestExecuteAtlantisGenerateRepoConfigWithStackNameTemplate(t *testing.T) {
}

func TestExecuteAtlantisGenerateRepoConfigAffectedOnly(t *testing.T) {
// Check for Git repository with valid remotes precondition
tests.RequireGitRemoteWithValidURL(t)

stacksPath := "../../tests/fixtures/scenarios/atlantis-generate-repo-config"
t.Setenv("ATMOS_CLI_CONFIG_PATH", stacksPath)
t.Setenv("ATMOS_BASE_PATH", stacksPath)
Expand Down
7 changes: 7 additions & 0 deletions internal/exec/describe_affected_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/cloudposse/atmos/pkg/pager"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/cloudposse/atmos/tests"
)

func TestDescribeAffected(t *testing.T) {
Expand Down Expand Up @@ -93,6 +94,9 @@ func TestDescribeAffected(t *testing.T) {
}

func TestExecuteDescribeAffectedWithTargetRepoPath(t *testing.T) {
// Check for Git repository with valid remotes precondition
tests.RequireGitRemoteWithValidURL(t)

stacksPath := "../../tests/fixtures/scenarios/atmos-describe-affected"
t.Setenv("ATMOS_CLI_CONFIG_PATH", stacksPath)
t.Setenv("ATMOS_BASE_PATH", stacksPath)
Expand Down Expand Up @@ -127,6 +131,9 @@ func TestExecuteDescribeAffectedWithTargetRepoPath(t *testing.T) {
}

func TestDescribeAffectedScenarios(t *testing.T) {
// Check for valid Git remote URL before running test
tests.RequireGitRemoteWithValidURL(t)

basePath := "tests/fixtures/scenarios/atmos-describe-affected-with-dependents-and-locked"
pathPrefix := "../../"

Expand Down
4 changes: 4 additions & 0 deletions internal/exec/terraform_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/tests"
)

// Helper function to create a bool pointer for testing.
Expand Down Expand Up @@ -88,6 +89,9 @@ func TestIsWorkspacesEnabled(t *testing.T) {
}

func TestExecuteTerraformAffectedWithDependents(t *testing.T) {
// Check for valid Git remote URL before running test
tests.RequireGitRemoteWithValidURL(t)

os.Unsetenv("ATMOS_BASE_PATH")
os.Unsetenv("ATMOS_CLI_CONFIG_PATH")

Expand Down
14 changes: 14 additions & 0 deletions internal/exec/vendor_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import (
"github.com/stretchr/testify/require"

"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/tests"
)

func TestExecuteVendorPullCommand(t *testing.T) {
// Check for GitHub access with rate limit check
rateLimits := tests.RequireGitHubAccess(t)
if rateLimits != nil && rateLimits.Remaining < 10 {
t.Skipf("Insufficient GitHub API requests remaining (%d). Test may require ~10 requests", rateLimits.Remaining)
}
stacksPath := "../../tests/fixtures/scenarios/vendor2"

err := os.Setenv("ATMOS_CLI_CONFIG_PATH", stacksPath)
Expand Down Expand Up @@ -73,6 +79,14 @@ func TestReadAndProcessVendorConfigFile(t *testing.T) {
// and that the vendor components are correctly pulled.
// The function also verifies that the state files are existing and deleted after the vendor pull command is executed.
func TestExecuteVendorPull(t *testing.T) {
// Check for GitHub access with rate limit check
rateLimits := tests.RequireGitHubAccess(t)
if rateLimits != nil && rateLimits.Remaining < 20 {
t.Skipf("Insufficient GitHub API requests remaining (%d). Test may require ~20 requests", rateLimits.Remaining)
}

// Check for OCI authentication (GitHub token) for pulling images from ghcr.io
tests.RequireOCIAuthentication(t)
if os.Getenv("ATMOS_CLI_CONFIG_PATH") != "" {
err := os.Unsetenv("ATMOS_CLI_CONFIG_PATH")
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions internal/terraform_backend/terraform_backend_s3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import (
errUtils "github.com/cloudposse/atmos/errors"
tb "github.com/cloudposse/atmos/internal/terraform_backend"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/tests"
)

func TestReadTerraformBackendS3_InvalidConfig(t *testing.T) {
// Check for AWS profile precondition
tests.RequireAWSProfile(t, "cplive-core-gbl-identity")
tests := []struct {
name string
componentData map[string]any
Expand Down
4 changes: 4 additions & 0 deletions pkg/atlantis/atlantis_generate_repo_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/cloudposse/atmos/tests"
)

func TestAtlantisGenerateRepoConfig(t *testing.T) {
Expand Down Expand Up @@ -74,6 +75,9 @@ func TestExecuteAtlantisGenerateRepoConfig2(t *testing.T) {
}

func TestExecuteAtlantisGenerateRepoConfigAffectedOnly(t *testing.T) {
// Check for Git repository with valid remotes precondition
tests.RequireGitRemoteWithValidURL(t)

atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{}, true)
assert.Nil(t, err)

Expand Down
Loading
Loading