package image import ( "bytes" "io" "os" "path/filepath" "testing" "metis/pkg/inject" ) type shortWriteOnly struct{} func (shortWriteOnly) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } return len(p) - 1, nil } type failAfterFirstRead struct{ done bool } func (r *failAfterFirstRead) Read(p []byte) (int, error) { if r.done { return 0, io.ErrUnexpectedEOF } r.done = true copy(p, []byte("ab")) return 2, nil } func TestInjectRootFSWithFakesReportsReplacingByteProgress(t *testing.T) { scripts := fakeRootfsCommands(t, true) t.Setenv("PATH", scripts+string(os.PathListSeparator)+os.Getenv("PATH")) imagePath := filepath.Join(t.TempDir(), "image.img") if err := os.WriteFile(imagePath, make([]byte, 4096), 0o644); err != nil { t.Fatal(err) } files := []inject.FileSpec{{Path: "etc/metis/firstboot.env", Content: []byte("METIS_HOSTNAME='titan-13'\n"), Mode: 0o600, RootFS: true}} var replacing []RootFSProgressUpdate if err := InjectRootFSWithProgress(imagePath, files, func(update RootFSProgressUpdate) { if update.Step == RootFSProgressReplacing && update.TotalBytes > 0 { replacing = append(replacing, update) } }); err != nil { t.Fatalf("InjectRootFSWithProgress: %v", err) } if len(replacing) == 0 { t.Fatal("expected replacing progress updates") } last := replacing[len(replacing)-1] if last.WrittenBytes != last.TotalBytes || last.TotalBytes != 1024 { t.Fatalf("unexpected replacing progress: %+v", last) } } func TestMkdirTempNearPathPrefersImageAdjacentScratch(t *testing.T) { imagePath := filepath.Join(t.TempDir(), "build", "node.img") if err := os.MkdirAll(filepath.Dir(imagePath), 0o755); err != nil { t.Fatal(err) } workDir, err := mkdirTempNearPath(imagePath, "metis-rootfs-") if err != nil { t.Fatalf("mkdirTempNearPath: %v", err) } defer os.RemoveAll(workDir) if got, want := filepath.Base(filepath.Dir(workDir)), ".metis-tmp"; got != want { t.Fatalf("temp parent = %q want %q", got, want) } } func TestCopyWithProgressReportsByteCounts(t *testing.T) { var dst bytes.Buffer var updates []RootFSProgressUpdate src := bytes.NewReader([]byte("hello world")) written, err := copyWithProgress(&dst, src, int64(src.Len()), func(written, total int64) { updates = append(updates, RootFSProgressUpdate{Step: RootFSProgressReplacing, WrittenBytes: written, TotalBytes: total}) }) if err != nil { t.Fatalf("copyWithProgress: %v", err) } if written != int64(len("hello world")) { t.Fatalf("written = %d", written) } if dst.String() != "hello world" { t.Fatalf("unexpected dst content: %q", dst.String()) } if len(updates) == 0 || updates[len(updates)-1].WrittenBytes != updates[len(updates)-1].TotalBytes { t.Fatalf("expected final byte-complete update, got %#v", updates) } } func TestReplacePartitionWritesDataAndReportsProgress(t *testing.T) { dir := t.TempDir() imagePath := filepath.Join(dir, "image.img") rootPath := filepath.Join(dir, "root.ext4") imageBytes := make([]byte, 1024) rootBytes := bytes.Repeat([]byte("r"), 512) if err := os.WriteFile(imagePath, imageBytes, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(rootPath, rootBytes, 0o644); err != nil { t.Fatal(err) } var updates []RootFSProgressUpdate if err := replacePartition(imagePath, rootPath, partitionTablePart{Start: 1, Size: 1}, 512, func(written, total int64) { updates = append(updates, RootFSProgressUpdate{Step: RootFSProgressReplacing, WrittenBytes: written, TotalBytes: total}) }); err != nil { t.Fatalf("replacePartition: %v", err) } got, err := os.ReadFile(imagePath) if err != nil { t.Fatal(err) } if !bytes.Equal(got[512:], rootBytes) { t.Fatalf("partition bytes were not replaced: %q", got[512:]) } if len(updates) == 0 || updates[len(updates)-1].WrittenBytes != 512 || updates[len(updates)-1].TotalBytes != 512 { t.Fatalf("expected final replace progress update, got %#v", updates) } } func TestMkdirTempNearPathHonorsOverrideAndFallback(t *testing.T) { t.Run("override", func(t *testing.T) { override := filepath.Join(t.TempDir(), "override") t.Setenv("METIS_ROOTFS_TMP_DIR", override) workDir, err := mkdirTempNearPath(filepath.Join(t.TempDir(), "build", "node.img"), "metis-rootfs-") if err != nil { t.Fatalf("mkdirTempNearPath override: %v", err) } defer os.RemoveAll(workDir) if got := filepath.Dir(workDir); got != override { t.Fatalf("override parent = %q want %q", got, override) } }) t.Run("fallback tempdir", func(t *testing.T) { workDir, err := mkdirTempNearPath("", "metis-rootfs-") if err != nil { t.Fatalf("mkdirTempNearPath fallback: %v", err) } defer os.RemoveAll(workDir) if workDir == "" { t.Fatal("expected fallback temp dir path") } }) } func TestCopyWithProgressPropagatesReaderAndWriterErrors(t *testing.T) { t.Run("short write", func(t *testing.T) { if _, err := copyWithProgress(shortWriteOnly{}, bytes.NewReader([]byte("abc")), 3, nil); err != io.ErrShortWrite { t.Fatalf("expected short write, got %v", err) } }) t.Run("reader error", func(t *testing.T) { if _, err := copyWithProgress(&bytes.Buffer{}, &failAfterFirstRead{}, 4, nil); err != io.ErrUnexpectedEOF { t.Fatalf("expected reader error, got %v", err) } }) }