Skip to content

Commit 2f901ca

Browse files
committed
fix: tab-delimited ZFS output parsing in branch functions
1 parent a8cc08b commit 2f901ca

2 files changed

Lines changed: 209 additions & 4 deletions

File tree

engine/internal/provision/thinclones/zfs/branching.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func (m *Manager) ListAllBranches(poolList []string) ([]models.BranchEntity, err
261261
const expectedColumns = 2
262262

263263
for _, line := range lines {
264-
fields := strings.Fields(line)
264+
fields := strings.SplitN(line, "\t", expectedColumns)
265265

266266
if len(fields) != expectedColumns {
267267
continue
@@ -300,7 +300,7 @@ func (m *Manager) listBranches() (map[string]string, error) {
300300
const expectedColumns = 2
301301

302302
for _, line := range lines {
303-
fields := strings.Fields(line)
303+
fields := strings.SplitN(line, "\t", expectedColumns)
304304

305305
if len(fields) != expectedColumns {
306306
continue
@@ -354,7 +354,7 @@ func (m *Manager) getRepo(cmdCfg cmdCfg) (*models.Repo, error) {
354354
repo := models.NewRepo()
355355

356356
for _, line := range lines {
357-
fields := strings.Fields(line)
357+
fields := strings.SplitN(line, "\t", len(repoFields))
358358

359359
if len(fields) != len(repoFields) {
360360
log.Dbg(fmt.Sprintf("Skip invalid line: %#v\n", line))
@@ -439,7 +439,7 @@ func (m *Manager) GetSnapshotProperties(snapshotName string) (thinclones.Snapsho
439439
return thinclones.SnapshotProperties{}, err
440440
}
441441

442-
fields := strings.Fields(strings.TrimSpace(out))
442+
fields := strings.SplitN(strings.TrimSpace(out), "\t", len(repoFields))
443443

444444
if len(fields) != len(repoFields) {
445445
log.Dbg("Retrieved fields values:", fields)

engine/internal/provision/thinclones/zfs/zfs_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package zfs
22

33
import (
4+
"encoding/base64"
45
"errors"
56
"sort"
67
"testing"
@@ -9,6 +10,8 @@ import (
910
"github.com/stretchr/testify/require"
1011

1112
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
13+
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones"
14+
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
1215
)
1316

1417
type runnerMock struct {
@@ -352,3 +355,205 @@ test_pool/other/dataset -`,
352355
})
353356
}
354357
}
358+
359+
func TestListAllBranches(t *testing.T) {
360+
testCases := []struct {
361+
name string
362+
output string
363+
expected []models.BranchEntity
364+
}{
365+
{
366+
name: "empty output",
367+
output: "",
368+
expected: []models.BranchEntity{},
369+
},
370+
{
371+
name: "tab-delimited output with space-containing branch name",
372+
output: "my branch\tpool/branch/main@snap1",
373+
expected: []models.BranchEntity{{Name: "my branch", Dataset: "pool", SnapshotID: "pool/branch/main@snap1"}},
374+
},
375+
{
376+
name: "multiple branches from comma-separated field",
377+
output: "br1,br2\tpool@snap1",
378+
expected: []models.BranchEntity{{Name: "br1", Dataset: "pool", SnapshotID: "pool@snap1"}, {Name: "br2", Dataset: "pool", SnapshotID: "pool@snap1"}},
379+
},
380+
{
381+
name: "line with wrong column count is skipped",
382+
output: "single_field_no_tab\nbranch1\tpool@snap2",
383+
expected: []models.BranchEntity{{Name: "branch1", Dataset: "pool", SnapshotID: "pool@snap2"}},
384+
},
385+
{
386+
name: "multiple lines with tabs",
387+
output: "main\tpool@snap1\nfeature\tpool@snap2",
388+
expected: []models.BranchEntity{
389+
{Name: "main", Dataset: "pool", SnapshotID: "pool@snap1"},
390+
{Name: "feature", Dataset: "pool", SnapshotID: "pool@snap2"},
391+
},
392+
},
393+
}
394+
395+
for _, tc := range testCases {
396+
t.Run(tc.name, func(t *testing.T) {
397+
m := Manager{runner: runnerMock{cmdOutput: tc.output}, config: Config{Pool: resources.NewPool("pool")}}
398+
399+
branches, err := m.ListAllBranches(nil)
400+
require.NoError(t, err)
401+
assert.Equal(t, tc.expected, branches)
402+
})
403+
}
404+
}
405+
406+
func TestListBranches(t *testing.T) {
407+
testCases := []struct {
408+
name string
409+
output string
410+
expected map[string]string
411+
}{
412+
{
413+
name: "empty output",
414+
output: "",
415+
expected: map[string]string{},
416+
},
417+
{
418+
name: "normal two-column tab-delimited output",
419+
output: "main\tpool@snap1\nfeature\tpool@snap2",
420+
expected: map[string]string{"main": "pool@snap1", "feature": "pool@snap2"},
421+
},
422+
{
423+
name: "snapshot name containing spaces is preserved",
424+
output: "main\tpool@snap with spaces",
425+
expected: map[string]string{"main": "pool@snap with spaces"},
426+
},
427+
{
428+
name: "single-field line without tab is skipped",
429+
output: "no_tab_here\nmain\tpool@snap1",
430+
expected: map[string]string{"main": "pool@snap1"},
431+
},
432+
{
433+
name: "comma-separated branches map to same snapshot",
434+
output: "br1,br2\tpool@snap1",
435+
expected: map[string]string{"br1": "pool@snap1", "br2": "pool@snap1"},
436+
},
437+
}
438+
439+
for _, tc := range testCases {
440+
t.Run(tc.name, func(t *testing.T) {
441+
m := Manager{runner: runnerMock{cmdOutput: tc.output}, config: Config{Pool: resources.NewPool("pool")}}
442+
443+
branches, err := m.listBranches()
444+
require.NoError(t, err)
445+
assert.Equal(t, tc.expected, branches)
446+
})
447+
}
448+
}
449+
450+
func TestGetRepo(t *testing.T) {
451+
msg := base64.StdEncoding.EncodeToString([]byte("initial commit"))
452+
453+
testCases := []struct {
454+
name string
455+
output string
456+
wantSnapshots int
457+
wantBranches map[string]string
458+
}{
459+
{
460+
name: "empty output",
461+
output: "",
462+
wantSnapshots: 0,
463+
wantBranches: map[string]string{},
464+
},
465+
{
466+
name: "all 8 fields populated",
467+
output: "pool@snap1\tparent1\tchild1\tmain\troot1\t20250101\t" + msg + "\t-",
468+
wantSnapshots: 1,
469+
wantBranches: map[string]string{"main": "pool@snap1"},
470+
},
471+
{
472+
name: "line with fewer than 8 fields is skipped",
473+
output: "pool@snap1\tparent1\tchild1\tmain\troot1\t20250101\t" + msg,
474+
wantSnapshots: 0,
475+
wantBranches: map[string]string{},
476+
},
477+
{
478+
name: "base64 commit message decodes correctly",
479+
output: "pool@snap1\t-\t-\tmain\t-\t20250101\t" + msg + "\t-",
480+
wantSnapshots: 1,
481+
wantBranches: map[string]string{"main": "pool@snap1"},
482+
},
483+
{
484+
name: "multiple snapshots with branches",
485+
output: "pool@snap1\t-\tpool@snap2\tmain\troot1\t20250101\t" + msg + "\t-\n" +
486+
"pool@snap2\tpool@snap1\t-\tfeature\troot1\t20250102\t" + msg + "\t-",
487+
wantSnapshots: 2,
488+
wantBranches: map[string]string{"main": "pool@snap1", "feature": "pool@snap2"},
489+
},
490+
}
491+
492+
for _, tc := range testCases {
493+
t.Run(tc.name, func(t *testing.T) {
494+
m := Manager{runner: runnerMock{cmdOutput: tc.output}, config: Config{Pool: resources.NewPool("pool")}}
495+
496+
repo, err := m.getRepo(cmdCfg{pool: "pool"})
497+
require.NoError(t, err)
498+
assert.Len(t, repo.Snapshots, tc.wantSnapshots)
499+
assert.Equal(t, tc.wantBranches, repo.Branches)
500+
501+
if tc.wantSnapshots > 0 {
502+
for _, snap := range repo.Snapshots {
503+
if snap.Message != "" && snap.Message != "-" {
504+
assert.Equal(t, "initial commit", snap.Message)
505+
}
506+
}
507+
}
508+
})
509+
}
510+
}
511+
512+
func TestGetSnapshotProperties(t *testing.T) {
513+
msg := base64.StdEncoding.EncodeToString([]byte("test message"))
514+
515+
testCases := []struct {
516+
name string
517+
output string
518+
expected thinclones.SnapshotProperties
519+
}{
520+
{
521+
name: "well-formed 8-field output",
522+
output: "pool@snap1\tparent1\tchild1\tmain\troot1\t20250101\t" + msg + "\tclone1",
523+
expected: thinclones.SnapshotProperties{
524+
Name: "pool@snap1", Parent: "parent1", Child: "child1", Branch: "main",
525+
Root: "root1", DataStateAt: "20250101", Message: "test message", Clones: "clone1",
526+
},
527+
},
528+
{
529+
name: "field containing spaces is preserved",
530+
output: "pool@snap1\tparent with spaces\tchild1\tmain\troot1\t20250101\t" + msg + "\tclone1",
531+
expected: thinclones.SnapshotProperties{
532+
Name: "pool@snap1", Parent: "parent with spaces", Child: "child1", Branch: "main",
533+
Root: "root1", DataStateAt: "20250101", Message: "test message", Clones: "clone1",
534+
},
535+
},
536+
}
537+
538+
for _, tc := range testCases {
539+
t.Run(tc.name, func(t *testing.T) {
540+
m := Manager{runner: runnerMock{cmdOutput: tc.output}, config: Config{Pool: resources.NewPool("pool")}}
541+
542+
props, err := m.GetSnapshotProperties("pool@snap1")
543+
require.NoError(t, err)
544+
assert.Equal(t, tc.expected, props)
545+
})
546+
}
547+
}
548+
549+
func TestGetSnapshotPropertiesMalformed(t *testing.T) {
550+
msg := base64.StdEncoding.EncodeToString([]byte("test message"))
551+
552+
m := Manager{
553+
runner: runnerMock{cmdOutput: "pool@snap1\tparent1\tchild1\tmain\troot1\t20250101\t" + msg},
554+
config: Config{Pool: resources.NewPool("pool")},
555+
}
556+
557+
_, err := m.GetSnapshotProperties("pool@snap1")
558+
assert.Error(t, err)
559+
}

0 commit comments

Comments
 (0)