--- title: Development description: How to build, test, and contribute to go-io. --- # Development This guide covers everything needed to work on `go-io` locally. ## Prerequisites - **Go 1.26.0** or later - **No C compiler required** -- all dependencies (including SQLite) are pure Go - The module is part of the Go workspace at `~/Code/go.work`. If you are working outside that workspace, ensure `GOPRIVATE=forge.lthn.ai/*` is set so the Go toolchain can fetch private dependencies. ## Building `go-io` is a library with no binary output. To verify it compiles: ```bash cd /path/to/go-io go build ./... ``` If using the Core CLI: ```bash core go fmt # format core go vet # static analysis core go lint # linter core go test # run all tests core go qa # fmt + vet + lint + test core go qa full # + race detector, vulnerability scan, security audit ``` ## Running Tests All packages have thorough test suites. Tests use `testify/assert` and `testify/require` for assertions. ```bash # All tests go test ./... # A single package go test ./sigil/ # A single test by name go test ./local/ -run TestValidatePath_Security # With race detector go test -race ./... # With coverage go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ``` Or via the Core CLI: ```bash core go test core go test --run TestChaChaPolySigil_Good_RoundTrip core go cov --open ``` ## Test Naming Convention Tests follow the `_Good`, `_Bad`, `_Ugly` suffix pattern: | Suffix | Meaning | |--------|---------| | `_Good` | Happy path -- the operation succeeds as expected | | `_Bad` | Expected error conditions -- missing files, invalid input, permission denied | | `_Ugly` | Edge cases and boundary conditions -- nil input, empty paths, panics | Example: ```go func TestDelete_Good(t *testing.T) { /* deletes a file successfully */ } func TestDelete_Bad_NotFound(t *testing.T) { /* returns error for missing file */ } func TestDelete_Bad_DirNotEmpty(t *testing.T) { /* returns error for non-empty dir */ } ``` ## Writing Tests Against Medium Use `MemoryMedium` from the root package for unit tests that need a storage backend but should not touch disk: ```go func TestMyFeature(t *testing.T) { memoryMedium := io.NewMemoryMedium() _ = memoryMedium.Write("config.yaml", "key: value") _ = memoryMedium.EnsureDir("data") result, err := myFunction(memoryMedium) assert.NoError(t, err) output, err := memoryMedium.Read("output.txt") require.NoError(t, err) assert.Equal(t, "expected", output) } ``` For tests that need a temporary filesystem, use `local.New` with `t.TempDir()`: ```go func TestLocalMedium_RoundTrip_Good(t *testing.T) { localMedium, err := local.New(t.TempDir()) require.NoError(t, err) _ = localMedium.Write("file.txt", "hello") content, _ := localMedium.Read("file.txt") assert.Equal(t, "hello", content) } ``` For SQLite-backed tests, use `:memory:`: ```go func TestSqliteMedium_RoundTrip_Good(t *testing.T) { sqliteMedium, err := sqlite.New(sqlite.Options{Path: ":memory:"}) require.NoError(t, err) defer sqliteMedium.Close() _ = sqliteMedium.Write("file.txt", "hello") } ``` ## Adding a New Backend To add a new `Medium` implementation: 1. Create a new package directory (e.g., `sftp/`). 2. Define a struct that implements all 17 methods of `io.Medium`. 3. Add a compile-time check at the top of your file: ```go var _ coreio.Medium = (*Medium)(nil) ``` 4. Normalise paths using `path.Clean("/" + p)` to prevent traversal escapes. This is the convention followed by every existing backend. 5. Handle `nil` and empty input consistently: check how `MemoryMedium` and `local.Medium` behave and match that behaviour. 6. Write tests using the `_Good` / `_Bad` / `_Ugly` naming convention. 7. Add your package to the table in `docs/index.md`. ## Adding a New Sigil To add a new data transformation: 1. Create a struct in `sigil/` that implements the `Sigil` interface (`In` and `Out`). 2. Handle `nil` input by returning `nil, nil`. 3. Handle empty input by returning `[]byte{}, nil`. 4. Register it in the `NewSigil` factory function in `sigils.go`. 5. Add tests covering `_Good` (round-trip), `_Bad` (invalid input), and `_Ugly` (nil/empty edge cases). ## Code Style - **UK English** in comments and documentation: colour, organisation, centre, serialise, defence. - **`declare(strict_types=1)`** equivalent: all functions have explicit parameter and return types. - Errors use the `go-log` helper: `coreerr.E("package.Method", "what failed", underlyingErr)`. - No blank imports except for database drivers (`_ "modernc.org/sqlite"`). - Formatting: standard `gofmt` / `goimports`. ## Project Structure ``` go-io/ ├── io.go # Medium interface, helpers, MemoryMedium ├── medium_test.go # Tests for MemoryMedium and helpers ├── bench_test.go # Benchmarks ├── go.mod ├── local/ │ ├── medium.go # Local filesystem backend │ └── medium_test.go ├── s3/ │ ├── s3.go # S3 backend │ └── s3_test.go ├── sqlite/ │ ├── sqlite.go # SQLite virtual filesystem │ └── sqlite_test.go ├── node/ │ ├── node.go # In-memory fs.FS + Medium │ └── node_test.go ├── datanode/ │ ├── medium.go # Borg DataNode Medium wrapper │ └── medium_test.go ├── store/ │ ├── store.go # KV store │ ├── medium.go # Medium adapter for KV store │ ├── store_test.go │ └── medium_test.go ├── sigil/ │ ├── sigil.go # Sigil interface, Transmute/Untransmute │ ├── sigils.go # Built-in sigils (hex, base64, gzip, hash, etc.) │ ├── crypto_sigil.go # ChaChaPolySigil + obfuscators │ ├── sigil_test.go │ └── crypto_sigil_test.go ├── workspace/ │ ├── service.go # Encrypted workspace service │ └── service_test.go ├── docs/ # This documentation └── .core/ ├── build.yaml # Build configuration └── release.yaml # Release configuration ``` ## Licence EUPL-1.2