This document describes how to test nbc using disk image files and loop devices.
Test Types
Unit Tests
Unit tests that don't require root or physical devices:
make test-unit
These tests cover:
TestFormatSize- Size formatting utilitiesTestGetBootDeviceFromPartition- Device name parsingTestGetDiskByPath- Path resolution (skips non-existent devices)
Integration Tests
Integration tests that use disk images and require root privileges:
sudo make test-integration
These tests cover:
TestCreatePartitions- GPT partition table creationTestFormatPartitions- Filesystem formattingTestMountPartitions- Partition mounting/unmountingTestDetectExistingPartitionScheme- Partition scheme detection
Test Infrastructure
Disk Image Creation
The test suite uses loop devices to simulate physical disks without requiring actual hardware. The testutil package provides utilities for creating and managing test disk images:
// Create a 50GB test disk image attached to a loop device
disk, err := testutil.CreateTestDisk(t, 50)
if err != nil {
t.Fatalf("Failed to create test disk: %v", err)
}
// Use the disk in tests
device := disk.GetDevice() // e.g., /dev/loop0
// Cleanup is automatic via t.Cleanup()
How It Works
- Sparse Files: Creates sparse disk image files (don't use actual disk space)
- Loop Devices: Attaches images to loop devices (e.g.,
/dev/loop0) - Automatic Cleanup: Test framework automatically detaches loop devices and removes image files
- Root Required: Loop device operations require root privileges
Test Utilities
The pkg/testutil package provides:
CreateTestDisk(t, sizeGB)- Create disk image and attach to loop deviceRequireRoot(t)- Skip test if not running as rootRequireTools(t, tools...)- Skip test if required tools are missingCreateMockContainer(t, imageName)- Create minimal test container imageWaitForDevice(device)- Wait for device to be ready after partitioningCleanupMounts(t, mountPoint)- Force unmount all mounts under a path
Running Tests
Prerequisites
# Required tools
sudo apt install gdisk dosfstools e2fsprogs podman rsync
# Or on Fedora/RHEL
sudo dnf install gdisk dosfstools e2fsprogs podman rsync
Run All Tests
# Run unit tests (no root needed)
make test-unit
# Run integration tests (requires root)
sudo make test-integration
# Or use the test script
sudo ./test_integration.sh
Run Specific Tests
# Run specific unit test
go test -v ./pkg/... -run TestFormatSize
# Run specific integration test (requires root)
sudo go test -v ./pkg/... -run TestCreatePartitions
# Run installation tests (requires root)
sudo make test-install
# Or directly:
sudo go test -v ./pkg/... -run TestBootcInstaller -timeout 20m
# Run update tests (requires root)
sudo make test-update
# Or directly:
sudo go test -v ./pkg/... -run TestSystemUpdater -timeout 20m
Test Coverage
Unit Tests (No Root Required)
- Device name parsing (
TestGetBootDeviceFromPartition) - Size formatting (
TestFormatSize) - Path resolution (
TestGetDiskByPath)
Integration Tests (Root Required)
- Partition creation (
TestCreatePartitions) - Partition formatting (
TestFormatPartitions) - Partition mounting (
TestMountPartitions) - Partition scheme detection (
TestDetectExistingPartitionScheme)
Installation Tests (Root Required)
- Full system installation (
TestBootcInstaller_Install) - Dry-run mode (
TestBootcInstaller_DryRun) - Kernel arguments persistence (
TestBootcInstaller_WithKernelArgs)
Update Tests (Root Required)
- System updates (
TestSystemUpdater_Update) - /etc configuration persistence (
TestSystemUpdater_EtcPersistence)
Writing Tests
Example Unit Test
func TestMyFunction(t *testing.T) {
result := MyFunction("input")
if result != "expected" {
t.Errorf("got %s, want %s", result, "expected")
}
}
Example Integration Test
func TestMyDiskOperation(t *testing.T) {
// Check prerequisites
testutil.RequireRoot(t)
testutil.RequireTools(t, "sgdisk", "mkfs.ext4")
// Create test disk
disk, err := testutil.CreateTestDisk(t, 10) // 10GB
if err != nil {
t.Fatalf("Failed to create test disk: %v", err)
}
// Perform operations
device := disk.GetDevice()
// ... your test code here ...
// Cleanup is automatic
}
Example Installation Test
func TestMyInstallation(t *testing.T) {
testutil.RequireRoot(t)
testutil.RequireTools(t, "losetup", "sgdisk", "mkfs.vfat", "mkfs.ext4", "podman")
// Create test disk
disk, err := testutil.CreateTestDisk(t, 50)
if err != nil {
t.Fatalf("Failed to create test disk: %v", err)
}
// Create mock container
imageName := "localhost/my-test:latest"
if err := testutil.CreateMockContainer(t, imageName); err != nil {
t.Fatalf("Failed to create container: %v", err)
}
// Perform installation
mountPoint := filepath.Join(t.TempDir(), "mnt")
installer := NewBootcInstaller(imageName, disk.GetDevice())
installer.SetMountPoint(mountPoint)
installer.SetVerbose(true)
defer testutil.CleanupMounts(t, mountPoint)
if err := installer.Install(); err != nil {
t.Fatalf("Install failed: %v", err)
}
// Verify installation
// ... verification code ...
}
Test Isolation
Each test:
- Uses isolated temporary directories (
t.TempDir()) - Gets its own loop device
- Cleans up automatically on completion or failure
- Does not interfere with other tests
Continuous Integration
For CI environments:
# GitHub Actions example
- name: Run tests
run: |
# Unit tests (no root)
make test-unit
# Integration tests (with root)
sudo make test-integration
Troubleshooting
"Test requires root privileges"
Integration tests need root to create loop devices:
sudo make test-integration
"Required tool not found"
Install missing tools:
# Ubuntu/Debian
sudo apt install gdisk dosfstools e2fsprogs
# Fedora/RHEL
sudo dnf install gdisk dosfstools e2fsprogs
"No loop devices available"
Load the loop module:
sudo modprobe loop
Or increase max loop devices:
sudo modprobe loop max_loop=16
Tests hang or leave loop devices attached
The test framework should clean up automatically, but if tests are interrupted:
# List loop devices
sudo losetup -a
# Detach specific loop device
sudo losetup -d /dev/loop0
# Remove test images
rm -f /tmp/nbc-test-*.img
Performance
Test disk images use sparse files, so:
- A 50GB test disk uses ~0 bytes initially
- Only grows as partitions are written
- Typical test disk uses < 100MB actual space
Safety
Tests are designed to be safe:
- Only operate on loop devices (never real disks)
- Automatically clean up on completion
- Use temporary directories
- No system modification outside test scope
Tests cannot accidentally wipe your real disks because they:
- Only work with loop devices from test images
- Don't have access to actual
/dev/sd*or/dev/nvme*devices during tests - Clean up completely on exit
Continuous Integration
The project uses GitHub Actions for automated testing on every push and pull request.
CI Workflow
The .github/workflows/test.yml workflow runs:
| Job | Description | Runs on |
|---|---|---|
| Lint | golangci-lint for code quality | ubuntu-latest |
| Unit Tests | All unit tests with coverage | ubuntu-latest |
| Build | Cross-compilation for linux/amd64 and linux/arm64 | ubuntu-latest |
| Integration Tests | Info only (requires root/loop devices) | - |
Coverage Reporting
Unit tests generate coverage data and upload to Codecov (when configured):
# Generate coverage locally
make test-coverage
# View coverage report
go tool cover -html=coverage.out
What CI Cannot Test
Due to GitHub Actions limitations, these tests require manual execution:
- Integration tests (
sudo make test-integration) - Require root and loop devices - Incus E2E tests (
./test_incus.sh) - Require VM creation capability - Encryption tests (
./test_incus_encryption.sh) - Require LUKS and TPM emulation
Running CI Locally
You can simulate the CI workflow locally using act:
# Install act (macOS)
brew install act
# Run all CI jobs
act
# Run specific job
act -j unit-test