//go:build linux
package microvm
import (
"context"
"encoding/json"
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"tangled.org/core/spindle/config"
"tangled.org/core/spindle/models"
"tangled.org/core/workflow"
)
func writeTestImageSpec(t *testing.T, dir, name string, spec ImageSpec) {
t.Helper()
data, err := json.Marshal(spec)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, name+".json"), data, 0o644); err != nil {
t.Fatal(err)
}
}
func testEngine(t *testing.T, imageDir string) *Engine {
t.Helper()
return &Engine{
l: slog.Default(),
cfg: &config.Config{
MicroVMPipelines: config.MicroVMPipelines{
ImageDir: imageDir,
DefaultImage: "alpine",
},
},
}
}
func testPipelineIdentity() workflow.PipelineSpec {
return workflow.PipelineSpec{
TriggerMetadata: &workflow.TriggerMetadata{
Repo: &workflow.TriggerRepo{Did: "did:web:owner"},
},
}
}
func TestShouldCommitToplevel(t *testing.T) {
const cached = "/nix/store/aaa-nixos-system"
const built = "/nix/store/bbb-nixos-system"
tests := []struct {
name string
cached, built, key string
want bool
}{
{"fresh build", "", built, "key", true},
{"cached toplevel realised", cached, cached, "key", false},
{"cached toplevel rebuilt", cached, built, "key", true},
{"no config key", "", built, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shouldCommitToplevel(tt.cached, tt.built, tt.key); got != tt.want {
t.Fatalf("shouldCommitToplevel(%q, %q, %q) = %v, want %v", tt.cached, tt.built, tt.key, got, tt.want)
}
})
}
}
func TestWorkflowUploadURLOnlyForTrustedWorkflows(t *testing.T) {
e := testEngine(t, t.TempDir())
e.cfg.NixCache.UploadURL = " ssh-ng://cache "
if got := e.workflowUploadURL(&models.Workflow{TrustedSource: true}); got != "ssh-ng://cache" {
t.Fatalf("trusted upload URL = %q, want ssh-ng://cache", got)
}
if got := e.workflowUploadURL(&models.Workflow{}); got != "" {
t.Fatalf("untrusted upload URL = %q, want none", got)
}
}
func TestNewDefersAgentHubUntilWorkflowSetup(t *testing.T) {
e, err := New(context.Background(), &config.Config{}, nil, nil)
if err != nil {
t.Fatal(err)
}
if e.agent != nil {
t.Fatal("agent hub started during engine initialization")
}
}
func TestInitWorkflowRejectsConfigOnNonNixOSImage(t *testing.T) {
dir := t.TempDir()
writeTestImageSpec(t, dir, "alpine", validImageSpec())
e := testEngine(t, dir)
_, err := e.InitWorkflow(workflow.WorkflowSpec{
Raw: `
image: alpine
dependencies:
- nixpkgs#hello
steps:
- name: hello
command: hello
`,
}, testPipelineIdentity())
if err == nil {
t.Fatal("expected error for NixOS config options on a non-NixOS image")
}
if !strings.Contains(err.Error(), "NixOS") {
t.Fatalf("error should mention NixOS images, got: %v", err)
}
}
func TestInitWorkflowPlainStepsOnNonNixOSImage(t *testing.T) {
dir := t.TempDir()
writeTestImageSpec(t, dir, "alpine", validImageSpec())
e := testEngine(t, dir)
wf, err := e.InitWorkflow(workflow.WorkflowSpec{
Raw: `
image: alpine
steps:
- name: hello
command: echo hello
`,
}, testPipelineIdentity())
if err != nil {
t.Fatal(err)
}
if len(wf.Steps) != 2 {
t.Fatalf("expected checkout and user steps, got %d steps", len(wf.Steps))
}
state, ok := wf.Data.(*workflowState)
if !ok {
t.Fatal("workflow data is not workflowState")
}
if state.ConfigKey != "" {
t.Fatalf("non-NixOS workflow should not have a config key, got %q", state.ConfigKey)
}
}
func TestInitWorkflowConfigOnNixOSImage(t *testing.T) {
dir := t.TempDir()
spec := validImageSpec()
spec.BaseConfigHash = "abcdef123456"
writeTestImageSpec(t, dir, "nixos", spec)
e := testEngine(t, dir)
wf, err := e.InitWorkflow(workflow.WorkflowSpec{
Raw: `
image: nixos
dependencies:
- nixpkgs#hello
steps:
- name: hello
command: hello
`,
}, testPipelineIdentity())
if err != nil {
t.Fatal(err)
}
if len(wf.Steps) != 3 {
t.Fatalf("expected checkout, activation, and user steps, got %d steps", len(wf.Steps))
}
if step, ok := wf.Steps[1].(Step); !ok || step.action != activationStepAction {
t.Fatalf("second step should be the activation step, got %+v", wf.Steps[1])
}
}
func TestInitWorkflowCachesRenameHint(t *testing.T) {
dir := t.TempDir()
writeTestImageSpec(t, dir, "alpine", validImageSpec())
e := testEngine(t, dir)
_, err := e.InitWorkflow(workflow.WorkflowSpec{
Raw: `
image: alpine
caches:
https://cache.nixos.org: ""
steps:
- name: hello
command: hello
`,
}, workflow.PipelineSpec{})
if err == nil {
t.Fatal("expected an error for the old caches key")
}
if !strings.Contains(err.Error(), "renamed to substituters") {
t.Fatalf("error should point at substituters, got: %v", err)
}
}