v2.31.1 f37fe67

ui_adoption_test.go RawHistoryFolder

1package cli
2
3import (
4	"bytes"
5	"context"
6	"go/ast"
7	"go/parser"
8	"go/token"
9	"io"
10	"strings"
11	"sync"
12	"testing"
13
14	"github.com/bnema/gordon/internal/adapters/dto"
15	climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
16)
17
18var uiAdoptionHelperCalls = map[string]struct{}{
19	"cliWriteLine":        {},
20	"cliWritef":           {},
21	"cliRenderTitle":      {},
22	"cliRenderMuted":      {},
23	"cliRenderEmptyState": {},
24	"cliRenderListItem":   {},
25	"cliRenderMeta":       {},
26	"cliRenderSuccess":    {},
27	"cliRenderWarning":    {},
28	"cliRenderInfo":       {},
29}
30
31// uiAdoptionSeamMu guards mutations of the package-level cliWriteLine and
32// cliWritef variables. Any test that overrides these seams must hold this
33// mutex for the duration of the override and restore the originals on cleanup.
34var uiAdoptionSeamMu sync.Mutex
35
36func TestPresentationHelpers(t *testing.T) {
37	if got := cliRenderTitle("Title"); got == "" {
38		t.Fatal("cliRenderTitle returned empty output")
39	}
40	if got := cliRenderEmptyState("none"); got == "" {
41		t.Fatal("cliRenderEmptyState returned empty output")
42	}
43	if got := cliRenderSuccess("ok"); got == "" {
44		t.Fatal("cliRenderSuccess returned empty output")
45	}
46	if got := cliRenderWarning("warn"); got == "" {
47		t.Fatal("cliRenderWarning returned empty output")
48	}
49	if got := cliRenderInfo("info"); got == "" {
50		t.Fatal("cliRenderInfo returned empty output")
51	}
52	if got := cliRenderListItem("item"); got == "" {
53		t.Fatal("cliRenderListItem returned empty output")
54	}
55}
56
57func TestUIAdoption(t *testing.T) {
58	for i := range uiAdoptionExpectations {
59		expect := uiAdoptionExpectations[i]
60		t.Run(expect.family, func(t *testing.T) {
61			fset := token.NewFileSet()
62			fileNode, err := parser.ParseFile(fset, expect.file, nil, parser.AllErrors)
63			if err != nil {
64				t.Fatalf("failed to parse %s: %v", expect.file, err)
65			}
66
67			for _, fnName := range expect.functions {
68				t.Run(fnName, func(t *testing.T) {
69					fn := findFuncDecl(fileNode, fnName)
70					if fn == nil {
71						t.Fatalf("function %s not found in %s", fnName, expect.file)
72					}
73
74					hasHelperCall := false
75					hasSharedUIUsage := false
76					hasForbiddenRawPrint := false
77
78					ast.Inspect(fn.Body, func(n ast.Node) bool {
79						call, ok := n.(*ast.CallExpr)
80						if !ok {
81							return true
82						}
83
84						switch fun := call.Fun.(type) {
85						case *ast.Ident:
86							if _, ok := uiAdoptionHelperCalls[fun.Name]; ok {
87								hasHelperCall = true
88							}
89						case *ast.SelectorExpr:
90							if isForbiddenRawPrintCall(call, fun) {
91								hasForbiddenRawPrint = true
92							}
93							pkgIdent, ok := fun.X.(*ast.Ident)
94							if !ok {
95								return true
96							}
97							if pkgIdent.Name == "styles" || pkgIdent.Name == "components" {
98								hasSharedUIUsage = true
99							}
100						}
101
102						return true
103					})
104
105					if !hasHelperCall && !hasSharedUIUsage {
106						t.Fatalf("%s in %s does not use presentation helpers or shared ui styles/components", fnName, expect.file)
107					}
108
109					if hasForbiddenRawPrint {
110						t.Fatalf("%s in %s still uses forbidden raw print calls", fnName, expect.file)
111					}
112				})
113			}
114		})
115	}
116}
117
118func isForbiddenRawPrintCall(call *ast.CallExpr, sel *ast.SelectorExpr) bool {
119	pkgIdent, ok := sel.X.(*ast.Ident)
120	if !ok {
121		return false
122	}
123
124	if pkgIdent.Name == "fmt" {
125		switch sel.Sel.Name {
126		case "Print", "Printf", "Println":
127			return true
128		case "Fprint", "Fprintf", "Fprintln":
129			if len(call.Args) == 0 {
130				return true
131			}
132
133			switch dst := call.Args[0].(type) {
134			case *ast.SelectorExpr:
135				if dstPkg, ok := dst.X.(*ast.Ident); ok && dstPkg.Name == "os" && (dst.Sel.Name == "Stdout" || dst.Sel.Name == "Stderr") {
136					return true
137				}
138			case *ast.Ident:
139				if dst.Name == "out" {
140					return true
141				}
142			}
143		}
144	}
145
146	if pkgIdent.Name == "cmd" && strings.HasPrefix(sel.Sel.Name, "Print") {
147		return true
148	}
149
150	return false
151}
152
153func TestUIAdoptionRuntimeSeams(t *testing.T) {
154	uiAdoptionSeamMu.Lock()
155	defer uiAdoptionSeamMu.Unlock()
156
157	origWriteLine := cliWriteLine
158	origWritef := cliWritef
159	defer func() {
160		cliWriteLine = origWriteLine
161		cliWritef = origWritef
162	}()
163
164	lineCalls := 0
165	writefCalls := 0
166	cliWriteLine = func(w io.Writer, msg string) error {
167		lineCalls++
168		_, err := io.WriteString(w, msg+"\n")
169		return err
170	}
171	cliWritef = func(w io.Writer, format string, args ...any) error {
172		writefCalls++
173		_, err := io.WriteString(w, "formatted\n")
174		return err
175	}
176
177	versionCmd := newVersionCmd()
178	versionCmd.SetArgs(nil)
179	versionCmd.SetOut(new(bytes.Buffer))
180	if err := versionCmd.Execute(); err != nil {
181		t.Fatalf("version command execution failed: %v", err)
182	}
183
184	imagesMock := climocks.NewMockimagesClient(t)
185	imagesMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}, nil).Once()
186
187	imgBuf := new(bytes.Buffer)
188	if err := runImagesList(context.Background(), imagesMock, imgBuf, false); err != nil {
189		t.Fatalf("runImagesList failed: %v", err)
190	}
191
192	pruneMock := climocks.NewMockimagesClient(t)
193	pruneMock.EXPECT().ListImages(context.Background()).Return([]dto.Image{}, nil).Once()
194
195	pruneBuf := new(bytes.Buffer)
196	if err := runImagesPrune(context.Background(), pruneMock, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {
197		t.Fatalf("runImagesPrune failed: %v", err)
198	}
199
200	if lineCalls == 0 {
201		t.Fatal("expected cliWriteLine to be called")
202	}
203	if writefCalls == 0 {
204		t.Fatal("expected cliWritef to be called")
205	}
206}
207
208func findFuncDecl(file *ast.File, name string) *ast.FuncDecl {
209	for _, decl := range file.Decls {
210		fn, ok := decl.(*ast.FuncDecl)
211		if !ok {
212			continue
213		}
214		if fn.Name.Name == name {
215			return fn
216		}
217	}
218	return nil
219}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close