clonetool/plan_test.go
2026-09-06 20:15:54 +02:00

147 lines
4.4 KiB
Go

package main
import "testing"
// mkLayout builds a GPT layout: ESP (vfat) + root (ext4) + data (ntfs).
func mkLayout(diskBytes int64) *DiskLayout {
d := &DiskLayout{
DiskPath: "/dev/src", DiskSize: diskBytes, LogicalSector: 512,
Scheme: "gpt", Label: "gpt", LabelID: "GUID",
SfdiskDump: gptDump, // any non-empty dump so a script is produced
Tools: map[string]bool{
"sfdisk": true, "ntfsclone": true,
"partclone.extfs": true, "partclone.restore": true, "e2image": true,
},
Partitions: []Partition{
{Num: 1, Start: 2048, Size: 204800, Type: gptESP, FSType: "vfat"}, // 100 MiB
{Num: 2, Start: 206848, Size: 10 * 2048 * 1024, FSType: "ext4", FSMinBytes: 2 << 30}, // 10 GiB, min 2 GiB
{Num: 3, Start: 206848 + 10*2048*1024, Size: 10 * 2048 * 1024, FSType: "ntfs", FSMinBytes: 3 << 30}, // 10 GiB, min 3 GiB
},
BootRegions: []Region{{Offset: 0, Length: 446, Note: "MBR bootstrap"}},
}
return d
}
func lastEndBytes(d *DiskLayout) int64 {
var e int64
for _, p := range d.Partitions {
if x := (p.Start + p.Size) * d.LogicalSector; x > e {
e = x
}
}
return e + gptTailSectors*d.LogicalSector
}
func TestPlanFitsDeviceKeepsLayout(t *testing.T) {
d := mkLayout(40 << 30)
plan, err := planTargetLayout(d, false, 40<<30, planOpts{})
if err != nil {
t.Fatal(err)
}
if len(plan.Parts) != 3 {
t.Fatalf("parts = %d", len(plan.Parts))
}
for _, p := range plan.Parts {
if p.SrcSizeB != p.DstSizeB || p.ShrinkToB != 0 {
t.Fatalf("p%d resized unexpectedly: %+v", p.Num, p)
}
if p.SrcStartB != p.DstStartB {
t.Fatalf("p%d start moved", p.Num)
}
}
if plan.Parts[0].Method != "raw" { // vfat, no partclone.fat in tool set
t.Fatalf("p1 method = %s", plan.Parts[0].Method)
}
if plan.Parts[1].Method != "fs-image" || plan.Parts[1].Tool != "partclone" {
t.Fatalf("p2 method/tool = %s/%s", plan.Parts[1].Method, plan.Parts[1].Tool)
}
if plan.Parts[2].Method != "fs-image" || plan.Parts[2].Tool != "ntfsclone" {
t.Fatalf("p3 method/tool = %s/%s", plan.Parts[2].Method, plan.Parts[2].Tool)
}
if plan.ESPPart != 1 || plan.RootPart != 2 || !plan.UEFI {
t.Fatalf("boot roles: esp=%d root=%d uefi=%v", plan.ESPPart, plan.RootPart, plan.UEFI)
}
}
func TestPlanTrailingFreeSpaceOK(t *testing.T) {
d := mkLayout(60 << 30)
// target smaller than the *disk* but larger than what the layout needs
need := lastEndBytes(d)
if _, err := planTargetLayout(d, false, need+1<<20, planOpts{}); err != nil {
t.Fatalf("should fit into %d (need %d): %v", need+1<<20, need, err)
}
}
func TestPlanNeedsShrinkRequiresFlag(t *testing.T) {
d := mkLayout(40 << 30)
need := lastEndBytes(d)
small := need - (4 << 30) // 4 GiB short; ntfs (min 3 GiB) can give it up
if _, err := planTargetLayout(d, false, small, planOpts{}); err == nil {
t.Fatalf("expected error without --allow-shrink")
}
plan, err := planTargetLayout(d, false, small, planOpts{AllowShrink: true})
if err != nil {
t.Fatalf("with --allow-shrink: %v", err)
}
p3 := plan.Parts[2]
if p3.ShrinkToB == 0 || p3.ShrinkToB >= p3.SrcSizeB {
t.Fatalf("p3 should shrink: %+v", p3)
}
if p3.Method != "fs-image" {
t.Fatalf("shrunk partition must be fs-image, got %s", p3.Method)
}
}
func TestPlanCannotFit(t *testing.T) {
d := mkLayout(40 << 30)
// absurdly small: even shrinking ntfs+ext to their minimums can't help
if _, err := planTargetLayout(d, false, 1<<30, planOpts{AllowShrink: true}); err == nil {
t.Fatalf("expected cannot-fit error")
}
}
func TestPlanFileTargetSizing(t *testing.T) {
d := mkLayout(40 << 30)
plan, err := planTargetLayout(d, true, 0, planOpts{})
if err != nil {
t.Fatal(err)
}
need := lastEndBytes(d)
if plan.ImageSize < need || plan.ImageSize > need+(2<<20) {
t.Fatalf("image size %d not ~%d", plan.ImageSize, need)
}
if plan.Script == "" {
t.Fatalf("expected a partition-table script for a file target")
}
}
func TestPlanNewIDsScript(t *testing.T) {
d := mkLayout(40 << 30)
plan, err := planTargetLayout(d, true, 0, planOpts{NewIDs: true})
if err != nil {
t.Fatal(err)
}
if plan.Script == "" || containsAny(plan.Script, "label-id:", "uuid=") {
t.Fatalf("--new-ids script still has ids:\n%s", plan.Script)
}
}
func containsAny(s string, subs ...string) bool {
for _, x := range subs {
if len(x) > 0 && indexOf(s, x) >= 0 {
return true
}
}
return false
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}