Skip to content

fix: add NoCache option to host directory uploads #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
44 changes: 44 additions & 0 deletions .dagger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,47 @@ func (m *ContainerUse) Release(ctx context.Context,
Release().
Run(ctx)
}

// Test runs the test suite
func (m *ContainerUse) Test(ctx context.Context,
//+optional
//+default="."
// Package to test
pkg string,
//+optional
// Run tests with verbose output
verboseOutput bool,
//+optional
// Run tests including integration tests
integration bool,
) (string, error) {
ctr := dag.Go(m.Source).
Base().
WithMountedDirectory("/src", m.Source).
WithWorkdir("/src")

args := []string{"go", "test"}
if verboseOutput {
args = append(args, "-v")
}
if !integration {
args = append(args, "-short")
}
args = append(args, pkg)

return ctr.
WithExec(args).
Stdout(ctx)
}

// TestEnvironment runs the environment package tests specifically
func (m *ContainerUse) TestEnvironment(ctx context.Context,
//+optional
// Run tests with verbose output
verboseOutput bool,
//+optional
// Include integration tests
integration bool,
) (string, error) {
return m.Test(ctx, "./environment", verboseOutput, integration)
}
2 changes: 1 addition & 1 deletion .github/workflows/cu.yml → .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
exit 1
fi
echo "Binary created successfully"
ls -la cu
ls -la cu
30 changes: 30 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run unit tests
uses: dagger/[email protected]
with:
version: "latest"
verb: call
args: test-environment --verbose

- name: Run integration tests
uses: dagger/[email protected]
with:
version: "latest"
verb: call
args: test-environment --verbose --integration
94 changes: 94 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Contributing to Container Use

Thank you for your interest in contributing to Container Use! This document outlines the necessary steps and standards to follow when contributing.

## Development Setup

Follow these steps to set up your development environment:

1. **Install Go**: Ensure you have Go version 1.21 or higher installed.
2. **Clone the Repository**:

```bash
git clone [email protected]:dagger/container-use.git
```
3. **Install Dependencies**:

```bash
go mod download
```
4. **Container Runtime**: Ensure you have a compatible container runtime installed (e.g., Docker).

## Building

To build the `cu` binary without installing it to your `$PATH`, you can use either Dagger or Go directly:

### Using Go

```bash
go build -o cu ./cmd/cu
```

### Using Dagger

```bash
dagger call build --platform=current export --path ./cu
```

## Testing

Container Use includes both unit and integration tests to maintain high code quality and functionality.

### Running Tests

* **Run All Tests**:

```bash
go test ./...
```

* **Run Unit Tests Only** (fast, no containers):

```bash
go test -short ./...
```

* **Run Integration Tests Only**:

```bash
go test -count=1 -v ./environment
```

### Test Structure

Tests are structured as follows:

* **`environment_test.go`**: Contains unit tests for package logic.
* **`integration_test.go`**: Covers integration scenarios to verify environment stability and state transitions.
* **`test_helpers.go`**: Provides shared utility functions for writing tests.

### Writing Tests

When contributing new features or fixing issues, adhere to these guidelines:

1. Write clear **unit tests** for the core logic.
2. Create comprehensive **integration tests** for validating end-to-end functionality.
3. Utilize provided **test helpers** for common tasks to maintain consistency.
4. Follow existing test patterns and naming conventions.

## Code Style

Maintain code consistency and readability by:

* Following standard Go coding conventions.
* Formatting code using `go fmt` before committing.
* Ensuring all tests pass locally before submitting your pull request.

## Submitting Changes

Submit contributions using these steps:

1. Fork the Container Use repository.
2. Create a descriptive feature branch from the main branch.
3. Commit your changes, including relevant tests.
4. Open a pull request with a clear and descriptive explanation of your changes.
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,7 @@ curl -fsSL https://raw.githubusercontent.com/dagger/container-use/main/install.s

This will check for Docker & Git (required), detect your platform, and install the latest `cu` binary to your `$PATH`.

## Building

To build the `cu` binary without installing it to your `$PATH`, you can use either Dagger or Go directly:

### Using Go

```sh
go build -o cu ./cmd/cu
```

### Using Dagger

```sh
dagger call build --platform=current export --path ./cu
```
For building from source, see [CONTRIBUTING.md](CONTRIBUTING.md#building).

## Integrate Agents

Expand Down
158 changes: 158 additions & 0 deletions environment/environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package environment

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Git command error handling ensures we gracefully handle git failures
func TestGitCommandErrors(t *testing.T) {
te := NewTestEnv(t, "git-errors")

// Test invalid command
_, err := runGitCommand(te.ctx, te.repoDir, "invalid-command")
assert.Error(t, err, "Should get error for invalid git command")

// Test command in non-existent directory
_, err = runGitCommand(te.ctx, "/nonexistent", "status")
assert.Error(t, err, "Should get error for non-existent directory")
}

// Worktree path generation must be consistent for environment isolation
func TestWorktreePaths(t *testing.T) {
env := &Environment{
ID: "test-env/happy-dog",
}

path, err := env.GetWorktreePath()
require.NoError(t, err, "Should get worktree path")

// Should end with our environment ID
assert.True(t, strings.HasSuffix(path, "test-env/happy-dog"), "Worktree path should end with env ID: %s", path)

// Should be in container-use worktrees
assert.Contains(t, path, ".config/container-use/worktrees", "Worktree should be in expected location")
}

// Empty directory handling prevents git commit failures when directories have no trackable files
func TestEmptyDirectoryHandling(t *testing.T) {
te := NewTestEnv(t, "empty-dir")

// Create empty directories (git doesn't track these)
te.CreateDir("empty1")
te.CreateDir("empty2/nested")

env := &Environment{
ID: "test/empty",
Name: "test",
Worktree: te.repoDir,
}

// This verifies that commitWorktreeChanges handles empty directories gracefully
// It should return nil (success) when there's nothing to commit
err := env.commitWorktreeChanges(te.ctx, te.repoDir, "Test", "Empty dirs")
assert.NoError(t, err, "commitWorktreeChanges should handle empty dirs gracefully")
}


// Selective file staging ensures problematic files are automatically excluded from commits
// This tests the actual user-facing behavior: "I want to commit my changes but not break git"
func TestSelectiveFileStaging(t *testing.T) {
// Test real-world scenarios that users encounter
scenarios := []struct {
name string
setup func(*TestEnv)
shouldStage []string
shouldSkip []string
reason string
}{
{
name: "python_project_with_pycache",
setup: func(te *TestEnv) {
te.WriteFile("main.py", "print('hello')")
te.WriteFile("utils.py", "def helper(): pass")
te.CreateDir("__pycache__")
te.WriteBinaryFile("__pycache__/main.cpython-39.pyc", 150)
te.WriteBinaryFile("__pycache__/utils.cpython-39.pyc", 200)
},
shouldStage: []string{"main.py", "utils.py"},
shouldSkip: []string{"__pycache__"},
reason: "Python cache files should never be committed",
},
{
name: "mixed_content_directory",
setup: func(te *TestEnv) {
te.CreateDir("mydir")
te.WriteFile("mydir/readme.txt", "Documentation")
te.WriteBinaryFile("mydir/compiled.bin", 100)
te.WriteFile("mydir/script.sh", "#!/bin/bash\necho hello")
te.WriteBinaryFile("mydir/image.jpg", 5000)
},
shouldStage: []string{"mydir/readme.txt", "mydir/script.sh"},
shouldSkip: []string{"mydir/compiled.bin", "mydir/image.jpg"},
reason: "Binary files in directories should be automatically excluded",
},
{
name: "node_modules_and_build_artifacts",
setup: func(te *TestEnv) {
te.WriteFile("index.js", "console.log('app')")
te.CreateDir("node_modules/lodash")
te.WriteFile("node_modules/lodash/index.js", "module.exports = {}")
te.CreateDir("build")
te.WriteBinaryFile("build/app.exe", 1024)
te.WriteFile("build/config.json", `{"prod": true}`)
},
shouldStage: []string{"index.js"},
shouldSkip: []string{"node_modules", "build"},
reason: "Dependencies and build outputs should be excluded",
},
// {
// name: "empty_file_edge_case",
// setup: func(te *TestEnv) {
// te.WriteFile("empty.txt", "")
// te.WriteFile("normal.txt", "content")
// },
// shouldStage: []string{"normal.txt"},
// shouldSkip: []string{}, // Note: empty.txt behavior is buggy, it should be staged
// reason: "Empty files handling (currently buggy)",
// },
}

for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
te := NewTestEnv(t, scenario.name)
env := &Environment{
ID: "test/staging",
Name: "test",
Worktree: te.repoDir,
}

// Setup the scenario
scenario.setup(te)

// Run the actual staging logic (testing the integration)
err := env.addNonBinaryFiles(te.ctx, te.repoDir)
require.NoError(t, err, "Staging should not error")

status := te.GitStatus()

// Verify expected behavior
for _, file := range scenario.shouldStage {
// Files should be staged (A prefix)
assert.Contains(t, status, "A "+file, "%s should be staged - %s", file, scenario.reason)
}

for _, pattern := range scenario.shouldSkip {
// Files should remain untracked (?? prefix), not staged (A prefix)
assert.NotContains(t, status, "A "+pattern, "%s should not be staged - %s", pattern, scenario.reason)
// They should appear as untracked
if !strings.Contains(pattern, "/") {
assert.Contains(t, status, "?? "+pattern, "%s should remain untracked - %s", pattern, scenario.reason)
}
}
})
}
}
8 changes: 6 additions & 2 deletions environment/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,17 @@ func (s *Environment) FileList(ctx context.Context, path string) (string, error)
func urlToDirectory(url string) *dagger.Directory {
switch {
case strings.HasPrefix(url, "file://"):
return dag.Host().Directory(url[len("file://"):])
return dag.Host().Directory(url[len("file://"):], dagger.HostDirectoryOpts{
NoCache: true,
})
case strings.HasPrefix(url, "git://"):
return dag.Git(url[len("git://"):]).Head().Tree()
case strings.HasPrefix(url, "https://"):
return dag.Git(url[len("https://"):]).Head().Tree()
default:
return dag.Host().Directory(url)
return dag.Host().Directory(url, dagger.HostDirectoryOpts{
NoCache: true,
})
}
}

Expand Down
Loading