Skip to content

Commit fad0b59

Browse files
authored
chore: Optimizing catalog performance (#5973)
* chore: Integrating `vfs` and optimizing some low-hanging fruit * fix: Using synctest to drive the model * fix: Updating tests and exporting `ReadDirEntries` * fix: Adding upper bound for `driveModel` loop * fix: Addressing long lines * fix: Moving more vfs tests to in-memory fs implementations * chore: Switching over to memFS in repo.Modules * chore: Explicitly threading logger through
1 parent cd8fe34 commit fad0b59

32 files changed

Lines changed: 1835 additions & 656 deletions

internal/cli/commands/catalog/catalog_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/gruntwork-io/terragrunt/internal/services/catalog"
1212
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
13+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1314
"github.com/gruntwork-io/terragrunt/pkg/config"
1415
"github.com/gruntwork-io/terragrunt/pkg/log"
1516
"github.com/gruntwork-io/terragrunt/pkg/options"
@@ -26,7 +27,7 @@ func TestCatalogCommandInitialization(t *testing.T) {
2627
require.NoError(t, err)
2728

2829
// Create mock repository function for testing
29-
mockNewRepo := func(ctx context.Context, logger log.Logger, repoOpts module.RepoOpts) (*module.Repo, error) {
30+
mockNewRepo := func(ctx context.Context, logger log.Logger, fsys vfs.FS, repoOpts *module.RepoOpts) (*module.Repo, error) {
3031
repoURL := repoOpts.CloneURL
3132
// Create a temporary directory structure for testing
3233
dummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, "github.com/gruntwork-io/", ""))
@@ -46,7 +47,7 @@ func TestCatalogCommandInitialization(t *testing.T) {
4647

4748
repoOpts.CloneURL = dummyRepoDir
4849

49-
return module.NewRepo(ctx, logger, repoOpts)
50+
return module.NewRepo(ctx, logger, fsys, repoOpts)
5051
}
5152

5253
// Create a temporary root config file
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package buttonbar_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
tea "charm.land/bubbletea/v2"
8+
9+
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/components/buttonbar"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// TestNew verifies that New constructs a ButtonBar with the supplied buttons
15+
// and that the zero-value active button is the first one.
16+
func TestNew(t *testing.T) {
17+
t.Parallel()
18+
19+
buttons := []string{"Yes", "No", "Cancel"}
20+
b := buttonbar.New(buttons)
21+
22+
require.NotNil(t, b)
23+
assert.NotEmpty(t, b.View().Content)
24+
}
25+
26+
// TestInitResetsActiveButton verifies that Init returns nil and resets the
27+
// active button by checking the rendered view after navigating away.
28+
func TestInitResetsActiveButton(t *testing.T) {
29+
t.Parallel()
30+
31+
b := buttonbar.New([]string{"One", "Two", "Three"})
32+
33+
// Move active button to index 1.
34+
_, _ = b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
35+
36+
cmd := b.Init()
37+
assert.Nil(t, cmd)
38+
39+
// After Init, the first button should be focused again. We assert this
40+
// through View by ensuring "One" is rendered with the focused style and
41+
// "Two" with the blurred style.
42+
view := b.View().Content
43+
assert.Contains(t, view, "One")
44+
assert.Contains(t, view, "Two")
45+
assert.Contains(t, view, "Three")
46+
}
47+
48+
// TestUpdateTabAdvancesActiveButton verifies that pressing tab advances the
49+
// active button and that the resulting Cmd emits an ActiveBtnMsg with the
50+
// new index.
51+
func TestUpdateTabAdvancesActiveButton(t *testing.T) {
52+
t.Parallel()
53+
54+
b := buttonbar.New([]string{"A", "B", "C"})
55+
56+
model, cmd := b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
57+
require.NotNil(t, model)
58+
require.NotNil(t, cmd)
59+
60+
msg := cmd()
61+
active, ok := msg.(buttonbar.ActiveBtnMsg)
62+
require.True(t, ok, "expected ActiveBtnMsg, got %T", msg)
63+
assert.Equal(t, buttonbar.ActiveBtnMsg(1), active)
64+
}
65+
66+
// TestUpdateTabWraps verifies that tab wraps from the last button back to
67+
// the first.
68+
func TestUpdateTabWraps(t *testing.T) {
69+
t.Parallel()
70+
71+
b := buttonbar.New([]string{"A", "B"})
72+
73+
// Tab once -> index 1.
74+
_, _ = b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
75+
76+
// Tab again -> wraps to index 0.
77+
_, cmd := b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
78+
require.NotNil(t, cmd)
79+
assert.Equal(t, buttonbar.ActiveBtnMsg(0), cmd().(buttonbar.ActiveBtnMsg))
80+
}
81+
82+
// TestUpdateShiftTabGoesBackward verifies that shift+tab moves the active
83+
// button backwards and wraps around when at index 0.
84+
func TestUpdateShiftTabGoesBackward(t *testing.T) {
85+
t.Parallel()
86+
87+
b := buttonbar.New([]string{"A", "B", "C"})
88+
89+
// shift+tab from index 0 -> wraps to last index (2).
90+
_, cmd := b.Update(tea.KeyPressMsg{Code: tea.KeyTab, Mod: tea.ModShift})
91+
require.NotNil(t, cmd)
92+
assert.Equal(t, buttonbar.ActiveBtnMsg(2), cmd().(buttonbar.ActiveBtnMsg))
93+
94+
// shift+tab again -> index 1.
95+
_, cmd = b.Update(tea.KeyPressMsg{Code: tea.KeyTab, Mod: tea.ModShift})
96+
require.NotNil(t, cmd)
97+
assert.Equal(t, buttonbar.ActiveBtnMsg(1), cmd().(buttonbar.ActiveBtnMsg))
98+
}
99+
100+
// TestUpdateUnknownKeyIsNoop verifies that key presses that are not tab or
101+
// shift+tab leave the active button unchanged and produce no cmd.
102+
func TestUpdateUnknownKeyIsNoop(t *testing.T) {
103+
t.Parallel()
104+
105+
b := buttonbar.New([]string{"A", "B"})
106+
107+
model, cmd := b.Update(tea.KeyPressMsg{Code: 'q', Text: "q"})
108+
require.NotNil(t, model)
109+
110+
// tea.Batch with no commands returns nil.
111+
assert.Nil(t, cmd)
112+
}
113+
114+
// TestUpdateSelectBtnMsgValidIndex verifies that SelectBtnMsg with a valid
115+
// index updates the active button.
116+
func TestUpdateSelectBtnMsgValidIndex(t *testing.T) {
117+
t.Parallel()
118+
119+
b := buttonbar.New([]string{"A", "B", "C"})
120+
121+
_, cmd := b.Update(buttonbar.SelectBtnMsg(2))
122+
// SelectBtnMsg path does not enqueue an ActiveBtnMsg cmd.
123+
assert.Nil(t, cmd)
124+
125+
// Confirm the selection took effect by issuing a tab and observing the
126+
// resulting active index wrap to 0.
127+
_, cmd = b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
128+
require.NotNil(t, cmd)
129+
assert.Equal(t, buttonbar.ActiveBtnMsg(0), cmd().(buttonbar.ActiveBtnMsg))
130+
}
131+
132+
// TestUpdateSelectBtnMsgOutOfRange verifies that SelectBtnMsg with an
133+
// out-of-range index leaves the active button unchanged.
134+
func TestUpdateSelectBtnMsgOutOfRange(t *testing.T) {
135+
t.Parallel()
136+
137+
b := buttonbar.New([]string{"A", "B"})
138+
139+
// Negative index ignored.
140+
_, _ = b.Update(buttonbar.SelectBtnMsg(-1))
141+
_, cmd := b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
142+
require.NotNil(t, cmd)
143+
assert.Equal(t, buttonbar.ActiveBtnMsg(1), cmd().(buttonbar.ActiveBtnMsg))
144+
145+
// Reset and test too-large index.
146+
b = buttonbar.New([]string{"A", "B"})
147+
_, _ = b.Update(buttonbar.SelectBtnMsg(99))
148+
_, cmd = b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
149+
require.NotNil(t, cmd)
150+
assert.Equal(t, buttonbar.ActiveBtnMsg(1), cmd().(buttonbar.ActiveBtnMsg))
151+
}
152+
153+
// TestUpdateUnrelatedMessage verifies that messages that are neither key
154+
// presses nor SelectBtnMsg are ignored.
155+
func TestUpdateUnrelatedMessage(t *testing.T) {
156+
t.Parallel()
157+
158+
b := buttonbar.New([]string{"A", "B"})
159+
160+
type unknownMsg struct{}
161+
162+
model, cmd := b.Update(unknownMsg{})
163+
assert.NotNil(t, model)
164+
assert.Nil(t, cmd)
165+
}
166+
167+
// TestViewRendersAllButtons verifies that View renders every button label
168+
// wrapped in the configured name format.
169+
func TestViewRendersAllButtons(t *testing.T) {
170+
t.Parallel()
171+
172+
buttons := []string{"Save", "Discard", "Cancel"}
173+
b := buttonbar.New(buttons)
174+
175+
view := b.View().Content
176+
for _, label := range buttons {
177+
assert.Contains(t, view, label)
178+
}
179+
180+
// Default format wraps each button in "[ ... ]"; expect at least
181+
// len(buttons) opening brackets.
182+
assert.GreaterOrEqual(t, strings.Count(view, "["), len(buttons))
183+
}
184+
185+
// TestViewSingleButton verifies that a single-button bar renders without
186+
// trailing separator artifacts and that tab is a no-op (wraps to itself).
187+
func TestViewSingleButton(t *testing.T) {
188+
t.Parallel()
189+
190+
b := buttonbar.New([]string{"Only"})
191+
192+
view := b.View().Content
193+
assert.Contains(t, view, "Only")
194+
195+
_, cmd := b.Update(tea.KeyPressMsg{Code: tea.KeyTab})
196+
require.NotNil(t, cmd)
197+
assert.Equal(t, buttonbar.ActiveBtnMsg(0), cmd().(buttonbar.ActiveBtnMsg))
198+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package tui_test
2+
3+
import (
4+
"testing"
5+
6+
"charm.land/bubbles/v2/key"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui"
11+
)
12+
13+
func TestKeyDelegateKeyMapShortHelp(t *testing.T) {
14+
t.Parallel()
15+
16+
km := tui.NewDelegateKeyMap()
17+
got := km.ShortHelp()
18+
19+
require.Len(t, got, 2)
20+
assert.Equal(t, km.Choose, got[0])
21+
assert.Equal(t, km.Scaffold, got[1])
22+
23+
for i, b := range got {
24+
assert.NotEmpty(t, b.Keys(), "binding %d has no keys", i)
25+
}
26+
}
27+
28+
func TestKeyDelegateKeyMapFullHelp(t *testing.T) {
29+
t.Parallel()
30+
31+
km := tui.NewDelegateKeyMap()
32+
got := km.FullHelp()
33+
34+
require.Len(t, got, 1)
35+
require.Len(t, got[0], 2)
36+
assert.Equal(t, km.Choose, got[0][0])
37+
assert.Equal(t, km.Scaffold, got[0][1])
38+
}
39+
40+
func TestKeyPagerKeyMapShortHelp(t *testing.T) {
41+
t.Parallel()
42+
43+
km := tui.NewPagerKeyMap()
44+
got := km.ShortHelp()
45+
46+
want := []key.Binding{
47+
km.Up,
48+
km.Down,
49+
km.PageUp,
50+
km.PageDown,
51+
km.Navigation,
52+
km.NavigationBack,
53+
km.Choose,
54+
km.Scaffold,
55+
km.Help,
56+
km.Quit,
57+
}
58+
59+
require.Len(t, got, len(want))
60+
61+
for i := range want {
62+
assert.Equal(t, want[i], got[i], "binding at index %d differs", i)
63+
}
64+
}
65+
66+
func TestKeyPagerKeyMapFullHelp(t *testing.T) {
67+
t.Parallel()
68+
69+
km := tui.NewPagerKeyMap()
70+
got := km.FullHelp()
71+
72+
require.Len(t, got, 3)
73+
74+
require.Len(t, got[0], 4)
75+
assert.Equal(t, km.Up, got[0][0])
76+
assert.Equal(t, km.Down, got[0][1])
77+
assert.Equal(t, km.PageDown, got[0][2])
78+
assert.Equal(t, km.PageUp, got[0][3])
79+
80+
require.Len(t, got[1], 4)
81+
assert.Equal(t, km.Navigation, got[1][0])
82+
assert.Equal(t, km.NavigationBack, got[1][1])
83+
assert.Equal(t, km.Choose, got[1][2])
84+
assert.Equal(t, km.Scaffold, got[1][3])
85+
86+
require.Len(t, got[2], 3)
87+
assert.Equal(t, km.Help, got[2][0])
88+
assert.Equal(t, km.Quit, got[2][1])
89+
assert.Equal(t, km.ForceQuit, got[2][2])
90+
}

internal/cli/commands/catalog/tui/model_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui"
1717
"github.com/gruntwork-io/terragrunt/internal/services/catalog"
1818
"github.com/gruntwork-io/terragrunt/internal/services/catalog/module"
19+
"github.com/gruntwork-io/terragrunt/internal/vfs"
1920
"github.com/gruntwork-io/terragrunt/pkg/config"
2021
"github.com/gruntwork-io/terragrunt/pkg/log"
2122
"github.com/gruntwork-io/terragrunt/pkg/options"
@@ -78,7 +79,7 @@ func runModel(t *testing.T, m tui.Model, width, height int, interact func(p *tea
7879
func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) catalog.CatalogService {
7980
t.Helper()
8081

81-
mockNewRepo := func(ctx context.Context, logger log.Logger, repoOpts module.RepoOpts) (*module.Repo, error) {
82+
mockNewRepo := func(ctx context.Context, logger log.Logger, fsys vfs.FS, repoOpts *module.RepoOpts) (*module.Repo, error) {
8283
repoURL := repoOpts.CloneURL
8384
// Create a temporary directory structure for testing
8485
dummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, "github.com/gruntwork-io/", ""))
@@ -132,7 +133,7 @@ func createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) cat
132133

133134
repoOpts.CloneURL = dummyRepoDir
134135

135-
return module.NewRepo(ctx, logger, repoOpts)
136+
return module.NewRepo(ctx, logger, fsys, repoOpts)
136137
}
137138

138139
// Create a temporary root config file

0 commit comments

Comments
 (0)