testutil

package
v1.1.13 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2025 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package testutil provides testing utilities and helpers for Hitch's test suite.

Overview

This package implements a comprehensive testing infrastructure that enables isolated, reproducible testing of Git operations. It provides test harnesses, mock repositories, and helper functions that make writing tests easier and more reliable.

Core Purpose

The package serves several key purposes:

  • Create isolated Git repositories for testing
  • Provide helper methods for common test operations
  • Ensure tests don't interfere with each other
  • Enable isolated integration testing
  • Simplify test setup and teardown

TestRepo Type

The primary type is TestRepo, which represents an isolated test repository:

type TestRepo struct {
    Path  string              // Temporary directory path
    Repo  *git.Repo           // Hitch's Repo wrapper
    T     *testing.T          // Test instance
    GoGit *git.Repository     // Underlying go-git repository
}

Creating Test Repositories

Create a fresh, isolated repository for each test:

func TestMyFeature(t *testing.T) {
    testRepo := testutil.NewTestRepo(t)
    defer testRepo.Cleanup()

    // Test repository is ready to use
    // - Has main branch
    // - Has initial commit
    // - Git user configured
    // - Clean working directory

    // Run your tests...
}

The test repository is created in a temporary directory and is completely isolated from other tests and the host system.

Initial Repository State

NewTestRepo creates a repository with:

  • Initialized git repository (git init)
  • Default branch set to 'main'
  • Git user configured (Test User <test@example.com>)
  • GPG signing disabled (for faster commits)
  • Initial commit with README.md
  • Clean working directory

Helper Methods

TestRepo provides many helper methods for common operations:

Branch Operations:

// Create and optionally switch to branch
err := testRepo.CreateBranch("feature/test", false)

// Switch to existing branch
err := testRepo.Checkout("feature/test")

// Check if branch exists
exists := testRepo.BranchExists("feature/test")

Commit Operations:

// Create a file and commit it
err := testRepo.CommitFile("test.txt", "content", "Add test file")

// Commit multiple files
files := map[string]string{
    "file1.txt": "content1",
    "file2.txt": "content2",
}
err := testRepo.CommitFiles(files, "Add multiple files")

Metadata Operations:

// Initialize Hitch metadata
meta := testRepo.InitializeMetadata([]string{"dev", "qa"}, "main")

// Write metadata
err := testRepo.WriteMetadata(meta, "Update metadata")

Cleanup

Always clean up test repositories:

testRepo := testutil.NewTestRepo(t)
defer testRepo.Cleanup()

Cleanup:

  • Removes the temporary directory
  • Deletes all files and git data
  • Frees disk space
  • Prevents test pollution

Using defer ensures cleanup happens even if test panics.

Complete Test Example

func TestPromoteOperation(t *testing.T) {
    // Create isolated repository
    testRepo := testutil.NewTestRepo(t)
    defer testRepo.Cleanup()

    // Initialize metadata
    meta := metadata.NewMetadata([]string{"dev"}, "main", "test@example.com")
    writer := metadata.NewWriter(testRepo.Repo)
    err := writer.WriteInitial(meta, "Test User", "test@example.com")
    if err != nil {
        t.Fatalf("Failed to initialize metadata: %v", err)
    }

    // Create feature branch
    err = testRepo.CreateBranch("feature/login", true)
    if err != nil {
        t.Fatalf("Failed to create branch: %v", err)
    }

    // Make changes
    err = testRepo.CommitFile("login.go", "package main", "Add login feature")
    if err != nil {
        t.Fatalf("Failed to commit: %v", err)
    }

    // Switch back to main
    err = testRepo.Checkout("main")
    if err != nil {
        t.Fatalf("Failed to checkout main: %v", err)
    }

    // Test promote operation
    err = meta.AddBranchToEnvironment("dev", "feature/login", "test@example.com")
    if err != nil {
        t.Errorf("Promote failed: %v", err)
    }

    // Verify
    if !meta.IsBranchInEnvironment("dev", "feature/login") {
        t.Error("Branch should be in dev environment")
    }
}

Local Testing

Tests using testutil run in isolated repositories and can be executed locally:

go test ./...

The RunInTestRepo helper ensures that Hitch commands run in isolated test repositories, preventing repository poisoning of the current directory.

Each test gets its own isolated environment:

  • Clean Git repository
  • Temporary directory
  • No host system interference
  • Reproducible results

🚨 CRITICAL: Repository Safety

**NEVER run Hitch commands directly in tests without using RunInTestRepo()!**

Hitch commands that manipulate Git state MUST be wrapped in RunInTestRepo() to prevent poisoning the Hitch development repository.

❌ WRONG - This will poison your Hitch repo:

func TestBadExample(t *testing.T) {
	// This runs in the current directory (Hitch repo!)
	err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"})
	// ^^^ BAD: Modifies the Hitch repo itself!
}

✅ CORRECT - This runs in an isolated repo:

func TestGoodExample(t *testing.T) {
	testRepo := testutil.NewTestRepo(t)
	defer testRepo.Cleanup()

	// Run Hitch commands in isolated repository
	err := testRepo.RunInTestRepo(func() {
		promoteNoRebuild = true
		if err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"}); err != nil {
			t.Errorf("Promote failed: %v", err)
		}
	})
	if err != nil {
		t.Fatalf("Failed to run Hitch command in test repo: %v", err)
	}
	// ^^^ GOOD: Runs in temporary directory, Hitch repo stays clean!
}

📝 Test Writing Guidelines

## When to Use RunInTestRepo()

ALWAYS use RunInTestRepo() for ANY test that calls:

- Hitch command functions (runPromote, runRebuild, runStatus, etc.) - Any function that calls Hitch commands - Direct git operations that could affect the current repo - Metadata operations that modify the repository

## Safe Test Patterns

### Unit Tests (No RunInTestRepo needed)

func TestValidatorLogic(t *testing.T) {
	validator := security.NewSecurityValidator()
	result := validator.ValidateBranchName("feature/test")
	// ✅ SAFE: Pure logic, no repo operations
}

### Integration Tests with Hitch Commands (RunInTestRepo REQUIRED)

func TestPromoteWorkflow(t *testing.T) {
	testRepo := testutil.NewTestRepo(t)
	defer testRepo.Cleanup()

	// ✅ SAFE: Hitch commands run in isolated repo
	err := testRepo.RunInTestRepo(func() {
		promoteNoRebuild = true
		if err := runPromote(promoteCmd, []string{"feature/test", "to", "dev"}); err != nil {
			t.Errorf("Promote failed: %v", err)
		}
	})
	if err != nil {
		t.Fatalf("Failed to run Hitch command in test repo: %v", err)
	}
}

### Error Handling Patterns

For tests that expect Hitch commands to fail:

err := testRepo.RunInTestRepo(func() {
	promoteNoRebuild = true
	if err := runPromote(promoteCmd, []string{"invalid-branch", "to", "dev"}); err == nil {
		t.Error("Expected promote to fail")
	}
})
if err != nil {
	t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}

For tests that need to capture and inspect errors:

var promoteErr error
err := testRepo.RunInTestRepo(func() {
	promoteNoRebuild = true
	promoteErr = runPromote(promoteCmd, []string{"feature/test", "to", "dev"})
})
if err != nil {
	t.Fatalf("Failed to run Hitch command in test repo: %v", err)
}

if promoteErr != nil {
	t.Logf("Promote failed as expected: %v", promoteErr)
}

🛡️ What RunInTestRepo() Protects Against

Without RunInTestRepo(), Hitch tests can:

- Create branches in the Hitch development repo - Modify .hitch metadata files - Change current working directory - Leave uncommitted changes - Create lock files - Corrupt the development environment

RunInTestRepo() prevents ALL of these by:

1. **Directory Isolation**: Changes to a temporary directory 2. **Automatic Cleanup**: Deletes the temp directory when test finishes 3. **Directory Restoration**: Always returns to original directory 4. **Panic Safety**: Cleanup runs even if test panics

🧪 Running Tests

## Local Development ```bash # Run all tests locally go test ./...

# Run specific package tests go test -v ./internal/security

# Run with coverage go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ```

## CI/CD Tests run the same way locally and in CI.

Test Isolation

TestRepo ensures perfect test isolation:

  • Each test gets its own temporary directory
  • No shared state between tests
  • Tests can run in parallel
  • No leftover files after tests

Enable parallel tests:

func TestSomething(t *testing.T) {
    t.Parallel()
    testRepo := testutil.NewTestRepo(t)
    defer testRepo.Cleanup()
    // Test runs in parallel with others
}

Helper Functions

Additional helper functions:

File operations:

// Write file to repository
err := testRepo.WriteFile("path/to/file.txt", "content")

// Read file from repository
content, err := testRepo.ReadFile("path/to/file.txt")

Git operations:

// Get current commit SHA
sha, err := testRepo.CurrentCommit()

// Get commit count
count, err := testRepo.CommitCount()

// Check if file exists
exists := testRepo.FileExists("test.txt")

Test Patterns

Common test patterns using testutil:

Table-Driven Tests:

func TestBranchOperations(t *testing.T) {
    testCases := []struct {
        name     string
        branch   string
        expected bool
    }{
        {"valid branch", "feature/test", true},
        {"invalid branch", "feat/test;", false},
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            testRepo := testutil.NewTestRepo(t)
            defer testRepo.Cleanup()

            // Test with isolated repository
        })
    }
}

Setup/Teardown Pattern:

func TestWithSetup(t *testing.T) {
    testRepo := testutil.NewTestRepo(t)
    defer testRepo.Cleanup()

    // Setup
    setupTestData(testRepo)

    // Execute test
    runTest(testRepo)

    // Verify
    verifyResults(testRepo)

    // Cleanup happens automatically via defer
}

Performance Considerations

Test repository creation is fast:

  • Uses os.MkdirTemp for temporary directories
  • Minimal git initialization
  • No network operations
  • Typical creation time: 10-50ms

For tests that need multiple repositories:

func TestMultiRepo(t *testing.T) {
    repo1 := testutil.NewTestRepo(t)
    defer repo1.Cleanup()

    repo2 := testutil.NewTestRepo(t)
    defer repo2.Cleanup()

    // Test interactions between repositories
}

Debugging Tests

When tests fail, TestRepo provides helpful information:

t.Logf("Test repository: %s", testRepo.Path)
t.Logf("Current branch: %s", testRepo.Repo.CurrentBranch())
t.Logf("Branches: %v", testRepo.Repo.Branches())

To inspect test repository after failure:

func TestDebug(t *testing.T) {
    testRepo := testutil.NewTestRepo(t)
    // Comment out cleanup to inspect repository
    // defer testRepo.Cleanup()

    // Test code...

    t.Logf("Repository at: %s", testRepo.Path)
    // Can cd to path and inspect git state
}

Best Practices

  1. Always use defer for Cleanup()
  2. Create new TestRepo for each test (don't share)
  3. Use t.Helper() in helper functions
  4. Use t.Fatal for setup errors, t.Error for test failures
  5. Keep tests focused and isolated
  6. Use descriptive test names
  7. Clean up even if test fails

Error Handling

TestRepo methods that can fail return errors:

err := testRepo.CreateBranch("feature/test", false)
if err != nil {
    t.Fatalf("Setup failed: %v", err)
}

Use t.Fatalf for setup errors (stop test immediately) Use t.Errorf for test assertions (continue to see all failures)

Integration with Other Packages

TestRepo integrates seamlessly with other Hitch packages:

// With metadata package
meta := metadata.NewMetadata(envs, "main", "test@example.com")
writer := metadata.NewWriter(testRepo.Repo)

// With safety package
tester := safety.NewTempBranchTester(testRepo.Repo)

// With git package
validator := git.NewRepositoryValidator(testRepo.Repo)

Test Coverage

Using testutil enables high test coverage:

  • Unit tests: Test individual functions
  • Integration tests: Test component interactions
  • End-to-end tests: Test complete workflows

Measure coverage:

go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

Example: Complex Integration Test

func TestCompleteWorkflow(t *testing.T) {
    // Create repository
    testRepo := testutil.NewTestRepo(t)
    defer testRepo.Cleanup()

    // Initialize Hitch
    meta := metadata.NewMetadata([]string{"dev", "qa"}, "main", "test@example.com")
    writer := metadata.NewWriter(testRepo.Repo)
    err := writer.WriteInitial(meta, "Test User", "test@example.com")
    require.NoError(t, err)

    // Create feature
    err = testRepo.CreateBranch("feature/workflow", true)
    require.NoError(t, err)

    err = testRepo.CommitFile("feature.go", "package main", "Add feature")
    require.NoError(t, err)

    // Promote to dev
    err = meta.AddBranchToEnvironment("dev", "feature/workflow", "test@example.com")
    require.NoError(t, err)

    // Promote to qa
    err = meta.AddBranchToEnvironment("qa", "feature/workflow", "test@example.com")
    require.NoError(t, err)

    // Verify final state
    assert.True(t, meta.IsBranchInEnvironment("dev", "feature/workflow"))
    assert.True(t, meta.IsBranchInEnvironment("qa", "feature/workflow"))
}

This package is the foundation of Hitch's testing infrastructure, enabling comprehensive, reliable, and isolated testing of all Git operations.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type TestRepo

type TestRepo struct {
	Path       string
	Repo       *hitchgit.Repo
	T          *testing.T
	GoGit      *git.Repository // Underlying go-git repository
	RemotePath string          // Path to bare remote repository if created
}

TestRepo represents an isolated Git repository for testing

func NewTestRepo

func NewTestRepo(t *testing.T) *TestRepo

NewTestRepo creates a new isolated Git repository in a temporary directory

func (*TestRepo) BranchExists

func (tr *TestRepo) BranchExists(name string) bool

BranchExists checks if a branch exists

func (*TestRepo) Cleanup

func (tr *TestRepo) Cleanup()

Cleanup removes the test repository and remote if created

func (*TestRepo) CommitFile

func (tr *TestRepo) CommitFile(filename, content, message string) error

CommitFile creates a file and commits it to the current branch

func (*TestRepo) CreateBranch

func (tr *TestRepo) CreateBranch(name string, createCommit bool) error

CreateBranch creates a new branch with an optional commit

func (*TestRepo) CreateRemote added in v1.1.10

func (tr *TestRepo) CreateRemote(remoteName string) error

CreateRemote creates a bare Git repository to act as a remote This simulates a private Git repository for testing push operations

func (*TestRepo) GetCurrentBranch

func (tr *TestRepo) GetCurrentBranch() (string, error)

GetCurrentBranch returns the current branch name

func (*TestRepo) HasRemote added in v1.1.10

func (tr *TestRepo) HasRemote(remoteName string) bool

HasRemote checks if a remote exists

func (*TestRepo) PushBranch added in v1.1.10

func (tr *TestRepo) PushBranch(remoteName, branchName string) error

PushBranch pushes a specific branch to the remote

func (*TestRepo) ResetWorkingDirectory

func (tr *TestRepo) ResetWorkingDirectory()

ResetWorkingDirectory ensures the git working directory is clean and on the correct branch

func (*TestRepo) RunInTestRepo

func (tr *TestRepo) RunInTestRepo(testFunc func()) error

RunInTestRepo changes to the test repository directory and executes a function This ensures Hitch commands run in the isolated test repository instead of poisoning the current repo

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL