Skip to content

Commit 7ded8fe

Browse files
committed
Enhanced test debug information
Finishing prior effort to make test run easier to debug, this introduces a number of changes to the output: - high-contrast emojis to make it easier to spot relevant sections - 2 columns table formatting for commands informations - better spacing - lifecycle sections (cleanup, setup, run) - clearly called out assertive output Signed-off-by: apostasie <[email protected]>
1 parent a4a2e65 commit 7ded8fe

File tree

7 files changed

+252
-142
lines changed

7 files changed

+252
-142
lines changed

mod/tigron/.golangci.yml

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ linters:
3737
- cyclop # provided by revive
3838
- exhaustruct # does not serve much of a purpose
3939
- errcheck # provided by revive
40+
- errchkjson # forces handling of json err (eg: prevents _), which is too much
4041
- forcetypeassert # provided by revive
4142
- funlen # provided by revive
4243
- gocognit # provided by revive
@@ -65,7 +66,7 @@ linters:
6566
arguments: [60]
6667
- name: function-length
6768
# Default is 50, 75
68-
arguments: [80, 180]
69+
arguments: [80, 200]
6970
- name: cyclomatic
7071
# Default is 10
7172
arguments: [30]
@@ -93,11 +94,17 @@ linters:
9394
files:
9495
- $all
9596
allow:
97+
# Allowing go standard library and tigron itself
9698
- $gostd
9799
- github.com/containerd/nerdctl/mod/tigron
100+
# We use creack as our base for pty
98101
- github.com/creack/pty
102+
# Used for display width computation in internal/formatter
103+
- golang.org/x/text/width
104+
# errgroups and term (make raw) are used by internal/pipes
99105
- golang.org/x/sync
100106
- golang.org/x/term
107+
# EXPERIMENTAL: for go routines leakage detection
101108
- go.uber.org/goleak
102109
staticcheck:
103110
checks:

mod/tigron/go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ go 1.23.0
55
require (
66
github.com/creack/pty v1.1.24
77
go.uber.org/goleak v1.3.0
8-
golang.org/x/sync v0.12.0
8+
golang.org/x/sync v0.13.0
99
golang.org/x/term v0.30.0
10+
golang.org/x/text v0.24.0
1011
)
1112

1213
require golang.org/x/sys v0.31.0 // indirect

mod/tigron/go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
88
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
99
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
1010
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
11-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
12-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
11+
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
12+
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
1313
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
1414
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
1515
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
1616
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
17+
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
18+
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
1719
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1820
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

mod/tigron/internal/formatter/formatter.go

+69-47
Original file line numberDiff line numberDiff line change
@@ -19,77 +19,99 @@ package formatter
1919
import (
2020
"fmt"
2121
"strings"
22-
"unicode/utf8"
22+
23+
"golang.org/x/text/width"
2324
)
2425

2526
const (
2627
maxLineLength = 110
2728
maxLines = 100
2829
kMaxLength = 7
30+
spacer = " "
2931
)
3032

31-
func chunk(s string, length int) []string {
32-
var chunks []string
33-
34-
lines := strings.Split(s, "\n")
35-
36-
for x := 0; x < maxLines && x < len(lines); x++ {
37-
line := lines[x]
38-
if utf8.RuneCountInString(line) < length {
39-
chunks = append(chunks, line)
40-
41-
continue
42-
}
43-
44-
for index := 0; index < utf8.RuneCountInString(line); index += length {
45-
end := index + length
46-
if end > utf8.RuneCountInString(line) {
47-
end = utf8.RuneCountInString(line)
48-
}
49-
50-
chunks = append(chunks, string([]rune(line)[index:end]))
51-
}
52-
}
53-
54-
if len(chunks) == maxLines {
55-
chunks = append(chunks, "...")
56-
}
57-
58-
return chunks
59-
}
60-
61-
// Table formats a `n x 2` dataset into a series of rows.
62-
// FIXME: the problem with full-width emoji is that they are going to eff-up the maths and display
63-
// here...
64-
// Maybe the csv writer could be cheat-used to get the right widths.
33+
// Table formats a `n x 2` dataset into a series of n rows by 2 columns.
6534
//
6635
//nolint:mnd // Too annoying
67-
func Table(data [][]any) string {
36+
func Table(data [][]any, mark string) string {
6837
var output string
6938

7039
for _, row := range data {
7140
key := fmt.Sprintf("%v", row[0])
7241
value := strings.ReplaceAll(fmt.Sprintf("%v", row[1]), "\t", " ")
7342

74-
output += fmt.Sprintf("+%s+\n", strings.Repeat("-", maxLineLength-2))
75-
76-
if utf8.RuneCountInString(key) > kMaxLength {
77-
key = string([]rune(key)[:kMaxLength-3]) + "..."
78-
}
43+
output += fmt.Sprintf("+%s+\n", strings.Repeat(mark, maxLineLength-2))
7944

80-
for _, line := range chunk(value, maxLineLength-kMaxLength-7) {
45+
for _, line := range chunk(value, maxLineLength-kMaxLength-7, maxLines) {
8146
output += fmt.Sprintf(
82-
"| %-*s | %-*s |\n",
83-
kMaxLength,
84-
key,
85-
maxLineLength-kMaxLength-7,
47+
"| %s | %s |\n",
48+
// Keys longer than one line of kMaxLength will be striped to one line
49+
chunk(key, kMaxLength, 1)[0],
8650
line,
8751
)
8852
key = ""
8953
}
9054
}
9155

92-
output += fmt.Sprintf("+%s+", strings.Repeat("-", maxLineLength-2))
56+
output += fmt.Sprintf("+%s+", strings.Repeat(mark, maxLineLength-2))
9357

9458
return output
9559
}
60+
61+
// chunk does take a string and split it in lines of maxLength size, accounting for characters display width.
62+
func chunk(s string, maxLength, maxLines int) []string {
63+
chunks := []string{}
64+
65+
runes := []rune(s)
66+
67+
size := 0
68+
start := 0
69+
70+
for index := range runes {
71+
var segment string
72+
73+
switch width.LookupRune(runes[index]).Kind() {
74+
case width.EastAsianWide, width.EastAsianFullwidth:
75+
size += 2
76+
case width.EastAsianAmbiguous, width.Neutral, width.EastAsianHalfwidth, width.EastAsianNarrow:
77+
size++
78+
default:
79+
size++
80+
}
81+
82+
switch {
83+
case runes[index] == '\n':
84+
// Met a line-break. Pad to size (removing the line break)
85+
segment = string(runes[start:index])
86+
segment += strings.Repeat(spacer, maxLength-size+1)
87+
start = index + 1
88+
size = 0
89+
case size == maxLength:
90+
// Line is full. Add the segment.
91+
segment = string(runes[start : index+1])
92+
start = index + 1
93+
size = 0
94+
case size > maxLength:
95+
// Last char was double width. Push it back to next line, and pad with a single space.
96+
segment = string(runes[start:index]) + spacer
97+
start = index
98+
size = 2
99+
case index == len(runes)-1:
100+
// End of string. Pad it to size.
101+
segment = string(runes[start : index+1])
102+
segment += strings.Repeat(spacer, maxLength-size)
103+
default:
104+
continue
105+
}
106+
107+
chunks = append(chunks, segment)
108+
}
109+
110+
if len(chunks) > maxLines {
111+
chunks = append(chunks[0:maxLines], "...")
112+
} else if len(chunks) == 0 {
113+
chunks = []string{strings.Repeat(spacer, maxLength)}
114+
}
115+
116+
return chunks
117+
}

mod/tigron/internal/mimicry/print.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func PrintCall(call *Call) string {
4040
}
4141

4242
output := []string{
43-
formatter.Table(debug),
43+
formatter.Table(debug, "-"),
4444
sectionSeparator,
4545
}
4646

mod/tigron/test/case.go

+82-39
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
package test
1818

1919
import (
20+
"encoding/json"
21+
"fmt"
2022
"slices"
2123
"testing"
2224

2325
"github.com/containerd/nerdctl/mod/tigron/internal/assertive"
26+
"github.com/containerd/nerdctl/mod/tigron/internal/formatter"
2427
)
2528

2629
// Case describes an entire test-case, including data, setup and cleanup routines, command and
@@ -62,27 +65,33 @@ type Case struct {
6265
parent *Case
6366
}
6467

68+
const (
69+
startDecorator = "🚀"
70+
cleanDecorator = "🧽"
71+
setupDecorator = "🏗"
72+
subinDecorator = "⤵️"
73+
suboutDecorator = "↩️"
74+
)
75+
6576
// Run prepares and executes the test, and any possible subtests.
66-
//
67-
//nolint:gocognit
6877
func (test *Case) Run(t *testing.T) {
6978
t.Helper()
7079
// Run the test
7180
//nolint:thelper
7281
testRun := func(subT *testing.T) {
7382
subT.Helper()
7483

75-
assertive.True(subT, test.t == nil, "You cannot run a test multiple times")
84+
silentT := assertive.WithSilentSuccess(subT)
85+
86+
assertive.True(silentT, test.t == nil, "You cannot run a test multiple times")
87+
assertive.True(silentT, test.Description != "" || test.parent == nil,
88+
"A subtest description cannot be empty")
89+
assertive.True(silentT, test.Command == nil || test.Expected != nil,
90+
"Expectations for a test command cannot be nil. You may want to use `Setup` instead"+
91+
"of `Command`.")
7692

7793
// Attach testing.T
7894
test.t = subT
79-
assertive.True(
80-
test.t,
81-
test.Description != "" || test.parent == nil,
82-
"A test description cannot be empty",
83-
)
84-
assertive.True(test.t, test.Command == nil || test.Expected != nil,
85-
"Expectations for a test command cannot be nil. You may want to use Setup instead.")
8695

8796
// Ensure we have env
8897
if test.Env == nil {
@@ -168,49 +177,83 @@ func (test *Case) Run(t *testing.T) {
168177
}
169178

170179
// Execute cleanups now
171-
test.t.Log("")
172-
test.t.Log("======================== Pre-test cleanup ========================")
173-
174-
for _, cleanup := range cleanups {
175-
cleanup(test.Data, test.helpers)
176-
}
177-
178-
// Register the cleanups, in reverse
179-
test.t.Cleanup(func() {
180-
test.t.Log("")
181-
test.t.Log("======================== Post-test cleanup ========================")
182-
183-
slices.Reverse(cleanups)
180+
if len(cleanups) > 0 {
181+
test.t.Log(
182+
"\n\n" + formatter.Table(
183+
[][]any{{cleanDecorator, fmt.Sprintf("%q: initial cleanup", test.t.Name())}},
184+
"=",
185+
) + "\n",
186+
)
184187

185188
for _, cleanup := range cleanups {
186189
cleanup(test.Data, test.helpers)
187190
}
188-
})
189191

190-
// Run the setups
191-
test.t.Log("")
192-
test.t.Log("======================== Test setup ========================")
192+
// Register the cleanups, in reverse
193+
test.t.Cleanup(func() {
194+
test.t.Helper()
195+
test.t.Log(
196+
"\n\n" + formatter.Table(
197+
[][]any{{cleanDecorator, fmt.Sprintf("%q: post-cleanup", test.t.Name())}},
198+
"=",
199+
) + "\n",
200+
)
201+
202+
slices.Reverse(cleanups)
193203

194-
for _, setup := range setups {
195-
setup(test.Data, test.helpers)
204+
for _, cleanup := range cleanups {
205+
cleanup(test.Data, test.helpers)
206+
}
207+
})
208+
}
209+
210+
// Run the setups
211+
if len(setups) > 0 {
212+
test.t.Log(
213+
"\n\n" + formatter.Table(
214+
[][]any{{setupDecorator, fmt.Sprintf("%q: setup", test.t.Name())}},
215+
"=",
216+
) + "\n",
217+
)
218+
219+
for _, setup := range setups {
220+
setup(test.Data, test.helpers)
221+
}
196222
}
197223

198224
// Run the command if any, with expectations
199225
// Note: if we have a command, we already know we DO have Expected
200-
test.t.Log("")
201-
test.t.Log("======================== Test Run ========================")
202-
203226
if test.Command != nil {
204-
test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers))
227+
cmd := test.Command(test.Data, test.helpers)
228+
229+
debugConfig, _ := json.MarshalIndent(test.Config.(*config).config, "", " ")
230+
debugData, _ := json.MarshalIndent(test.Data.(*data).labels, "", " ")
231+
232+
test.t.Log(
233+
"\n\n" + formatter.Table(
234+
[][]any{
235+
{startDecorator, fmt.Sprintf("%q: starting test!", test.t.Name())},
236+
{"cwd", test.Data.TempDir()},
237+
{"config", string(debugConfig)},
238+
{"data", string(debugData)},
239+
},
240+
"=",
241+
) + "\n",
242+
)
243+
244+
cmd.Run(test.Expected(test.Data, test.helpers))
205245
}
206246

207-
// Now go for the subtests
208-
test.t.Log("")
209-
test.t.Log("======================== Processing subtests ========================")
247+
if len(test.SubTests) > 0 {
248+
// Now go for the subtests
249+
test.t.Logf("\n%s️ %q: into subtests prep", subinDecorator, test.t.Name())
250+
251+
for _, subTest := range test.SubTests {
252+
subTest.parent = test
253+
subTest.Run(test.t)
254+
}
210255

211-
for _, subTest := range test.SubTests {
212-
subTest.parent = test
213-
subTest.Run(test.t)
256+
test.t.Logf("\n%s️ %q: done with subtests prep", suboutDecorator, test.t.Name())
214257
}
215258
}
216259

0 commit comments

Comments
 (0)