v2.31.0-dev6 8056b57

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)
16
17var uiAdoptionHelperCalls = map[string]struct{}{
18	"cliWriteLine":        {},
19	"cliWritef":           {},
20	"cliRenderTitle":      {},
21	"cliRenderMuted":      {},
22	"cliRenderEmptyState": {},
23	"cliRenderListItem":   {},
24	"cliRenderMeta":       {},
25	"cliRenderSuccess":    {},
26	"cliRenderWarning":    {},
27	"cliRenderInfo":       {},
28}
29
30// uiAdoptionSeamMu guards mutations of the package-level cliWriteLine and
31// cliWritef variables. Any test that overrides these seams must hold this
32// mutex for the duration of the override and restore the originals on cleanup.
33var uiAdoptionSeamMu sync.Mutex
34
35func TestPresentationHelpers(t *testing.T) {
36	if got := cliRenderTitle("Title"); got == "" {
37		t.Fatal("cliRenderTitle returned empty output")
38	}
39	if got := cliRenderEmptyState("none"); got == "" {
40		t.Fatal("cliRenderEmptyState returned empty output")
41	}
42	if got := cliRenderSuccess("ok"); got == "" {
43		t.Fatal("cliRenderSuccess returned empty output")
44	}
45	if got := cliRenderWarning("warn"); got == "" {
46		t.Fatal("cliRenderWarning returned empty output")
47	}
48	if got := cliRenderInfo("info"); got == "" {
49		t.Fatal("cliRenderInfo returned empty output")
50	}
51	if got := cliRenderListItem("item"); got == "" {
52		t.Fatal("cliRenderListItem returned empty output")
53	}
54}
55
56func TestUIAdoption(t *testing.T) {
57	for _, expect := range uiAdoptionExpectations {
58		expect := expect
59		t.Run(expect.family, func(t *testing.T) {
60			fset := token.NewFileSet()
61			fileNode, err := parser.ParseFile(fset, expect.file, nil, parser.AllErrors)
62			if err != nil {
63				t.Fatalf("failed to parse %s: %v", expect.file, err)
64			}
65
66			for _, fnName := range expect.functions {
67				fnName := fnName
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
153type imagesClientStub struct {
154	listResp  []dto.Image
155	pruneResp *dto.ImagePruneResponse
156	err       error
157}
158
159func (s imagesClientStub) ListImages(context.Context) ([]dto.Image, error) {
160	if s.err != nil {
161		return nil, s.err
162	}
163	return s.listResp, nil
164}
165
166func (s imagesClientStub) PruneImages(context.Context, dto.ImagePruneRequest) (*dto.ImagePruneResponse, error) {
167	if s.err != nil {
168		return nil, s.err
169	}
170	return s.pruneResp, nil
171}
172
173func TestUIAdoptionRuntimeSeams(t *testing.T) {
174	uiAdoptionSeamMu.Lock()
175	defer uiAdoptionSeamMu.Unlock()
176
177	origWriteLine := cliWriteLine
178	origWritef := cliWritef
179	defer func() {
180		cliWriteLine = origWriteLine
181		cliWritef = origWritef
182	}()
183
184	lineCalls := 0
185	writefCalls := 0
186	cliWriteLine = func(w io.Writer, msg string) error {
187		lineCalls++
188		_, err := io.WriteString(w, msg+"\n")
189		return err
190	}
191	cliWritef = func(w io.Writer, format string, args ...any) error {
192		writefCalls++
193		_, err := io.WriteString(w, "formatted\n")
194		return err
195	}
196
197	versionCmd := newVersionCmd()
198	versionCmd.SetArgs(nil)
199	versionCmd.SetOut(new(bytes.Buffer))
200	if err := versionCmd.Execute(); err != nil {
201		t.Fatalf("version command execution failed: %v", err)
202	}
203
204	imgBuf := new(bytes.Buffer)
205	if err := runImagesList(context.Background(), imagesClientStub{listResp: []dto.Image{{Repository: "repo/app", Tag: "latest", ID: "sha256:abc", Size: 1}}}, imgBuf, false); err != nil {
206		t.Fatalf("runImagesList failed: %v", err)
207	}
208
209	pruneBuf := new(bytes.Buffer)
210	if err := runImagesPrune(context.Background(), imagesClientStub{listResp: []dto.Image{}, pruneResp: &dto.ImagePruneResponse{}}, imagesPruneOptions{DryRun: true}, pruneBuf); err != nil {
211		t.Fatalf("runImagesPrune failed: %v", err)
212	}
213
214	if lineCalls == 0 {
215		t.Fatal("expected cliWriteLine to be called")
216	}
217	if writefCalls == 0 {
218		t.Fatal("expected cliWritef to be called")
219	}
220}
221
222func findFuncDecl(file *ast.File, name string) *ast.FuncDecl {
223	for _, decl := range file.Decls {
224		fn, ok := decl.(*ast.FuncDecl)
225		if !ok {
226			continue
227		}
228		if fn.Name.Name == name {
229			return fn
230		}
231	}
232	return nil
233}

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 8

Tags 106

↑↓ moveenter openesc close