Skip to content

Commit c02abf5

Browse files
authored
Merge pull request #4080 from apostasie/tigron-4-require
[Tigron]: Testing logs user experience
2 parents 3a35911 + 488e1e7 commit c02abf5

File tree

12 files changed

+333
-185
lines changed

12 files changed

+333
-185
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/expect/comparators.go

+13-13
Original file line numberDiff line numberDiff line change
@@ -30,59 +30,59 @@ import (
3030

3131
// All can be used as a parameter for expected.Output to group a set of comparators.
3232
func All(comparators ...test.Comparator) test.Comparator {
33-
return func(stdout, info string, t *testing.T) {
33+
return func(stdout, _ string, t *testing.T) {
3434
t.Helper()
3535

3636
for _, comparator := range comparators {
37-
comparator(stdout, info, t)
37+
comparator(stdout, "", t)
3838
}
3939
}
4040
}
4141

4242
// Contains can be used as a parameter for expected.Output and ensures a comparison string is found contained in the
4343
// output.
4444
func Contains(compare string) test.Comparator {
45-
return func(stdout, info string, t *testing.T) {
45+
return func(stdout, _ string, t *testing.T) {
4646
t.Helper()
47-
assertive.Contains(assertive.WithFailLater(t), stdout, compare, info)
47+
assertive.Contains(assertive.WithFailLater(t), stdout, compare, "Inspecting output (contains)")
4848
}
4949
}
5050

5151
// DoesNotContain is to be used for expected.Output to ensure a comparison string is NOT found in the output.
5252
func DoesNotContain(compare string) test.Comparator {
53-
return func(stdout, info string, t *testing.T) {
53+
return func(stdout, _ string, t *testing.T) {
5454
t.Helper()
55-
assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, info)
55+
assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, "Inspecting output (does not contain)")
5656
}
5757
}
5858

5959
// Equals is to be used for expected.Output to ensure it is exactly the output.
6060
func Equals(compare string) test.Comparator {
61-
return func(stdout, info string, t *testing.T) {
61+
return func(stdout, _ string, t *testing.T) {
6262
t.Helper()
63-
assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, info)
63+
assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, "Inspecting output (equals)")
6464
}
6565
}
6666

6767
// Match is to be used for expected.Output to ensure we match a regexp.
6868
func Match(reg *regexp.Regexp) test.Comparator {
69-
return func(stdout, info string, t *testing.T) {
69+
return func(stdout, _ string, t *testing.T) {
7070
t.Helper()
71-
assertive.Match(assertive.WithFailLater(t), stdout, reg, info)
71+
assertive.Match(assertive.WithFailLater(t), stdout, reg, "Inspecting output (match)")
7272
}
7373
}
7474

7575
// JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided
7676
// method.
7777
func JSON[T any](obj T, verifier func(T, string, tig.T)) test.Comparator {
78-
return func(stdout, info string, t *testing.T) {
78+
return func(stdout, _ string, t *testing.T) {
7979
t.Helper()
8080

8181
err := json.Unmarshal([]byte(stdout), &obj)
82-
assertive.ErrorIsNil(assertive.WithFailLater(t), err, "failed to unmarshal JSON from stdout")
82+
assertive.ErrorIsNil(assertive.WithSilentSuccess(t), err, "Unmarshalling JSON from stdout must succeed")
8383

8484
if verifier != nil && err == nil {
85-
verifier(obj, info, t)
85+
verifier(obj, "Inspecting output (JSON)", t)
8686
}
8787
}
8888
}

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/assertive/assertive.go

+57-30
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ import (
3131
"github.com/containerd/nerdctl/mod/tigron/tig"
3232
)
3333

34-
// TODO: once debugging output will be cleaned-up, reintroduce hexdump.
35-
3634
const (
35+
markLineLength = 20
3736
expectedSuccessDecorator = "✅️ does verify:\t\t"
38-
expectedFailDecorator = "❌ does not verify:\t"
37+
expectedFailDecorator = "❌ FAILED!\t\t"
3938
receivedDecorator = "👀 testing:\t\t"
39+
annotationDecorator = "🖊️"
4040
hyperlinkDecorator = "🔗"
4141
)
4242

@@ -163,8 +163,8 @@ func True(testing tig.T, comp bool, msg ...string) bool {
163163

164164
// WithFailLater will allow an assertion to not fail the test immediately.
165165
// Failing later is necessary when asserting inside go routines, and also if you want many
166-
// successive asserts to all
167-
// evaluate instead of stopping at the first failing one.
166+
// successive asserts to all evaluate instead of stopping at the first failing one.
167+
// FIXME: it should be possible to have both WithFailLater and WithSilentSuccess at the same time.
168168
func WithFailLater(t tig.T) tig.T {
169169
return &failLater{
170170
t,
@@ -205,49 +205,76 @@ func evaluate(testing tig.T, isSuccess bool, actual, expected any, msg ...string
205205
func decorate(testing tig.T, isSuccess bool, actual, expected any, msg ...string) {
206206
testing.Helper()
207207

208-
header := "\t"
208+
if _, ok := testing.(*silentSuccess); !isSuccess || !ok {
209+
head := strings.Repeat("<", markLineLength)
210+
footer := strings.Repeat(">", markLineLength)
211+
header := "\t"
209212

210-
hyperlink := getTopFrameFile()
211-
if hyperlink != "" {
212-
msg = append([]string{hyperlink + "\n"}, msg...)
213-
}
213+
custom := fmt.Sprintf("\t%s %s", annotationDecorator, strings.Join(msg, "\n"))
214214

215-
msg = append(msg, fmt.Sprintf("\t%s`%v`", receivedDecorator, actual))
215+
msg = append([]string{"", head}, custom)
216216

217-
if isSuccess {
218-
msg = append(msg,
219-
fmt.Sprintf("\t%s%v", expectedSuccessDecorator, expected),
220-
)
221-
} else {
222-
msg = append(msg,
223-
fmt.Sprintf("\t%s%v", expectedFailDecorator, expected),
224-
)
225-
}
217+
msg = append([]string{getTopFrameFile()}, msg...)
226218

227-
if _, ok := testing.(*silentSuccess); !isSuccess || !ok {
228-
testing.Log(header + strings.Join(msg, "\n") + "\n")
219+
msg = append(msg, fmt.Sprintf("\t%s`%v`", receivedDecorator, actual))
220+
221+
if isSuccess {
222+
msg = append(msg,
223+
fmt.Sprintf("\t%s%v", expectedSuccessDecorator, expected),
224+
)
225+
} else {
226+
msg = append(msg,
227+
fmt.Sprintf("\t%s%v", expectedFailDecorator, expected),
228+
)
229+
}
230+
231+
testing.Log(header + strings.Join(msg, "\n") + "\n" + footer + "\n")
229232
}
230233
}
231234

235+
// XXX FIXME #expert
236+
// Because of how golang testing works, the upper frame is the one from where t.Run is being called,
237+
// as (presumably) the passed function is starting with its own stack in a go routine.
238+
// In the case of subtests, t.Run being called from inside Tigron will make it so that the top frame
239+
// is case.go around line 233 (where we call Command.Run(), which is the one calling assertive).
240+
// To possibly address this:
241+
// plan a. just drop entirely OSC8 links and source extracts and trash all of this
242+
// plan b. get the top frame from the root test, and pass it to subtests on a custom property, the somehow into here
243+
// plan c. figure out a hack to call t.Run from the test file without ruining the Tigron UX
244+
// Dereference t.Run? Return a closure to be called from the top? w/o disabling inlining in the right place?
245+
// Short term, blacklisting /tigron (and /nerdtest) will at least prevent the wrong links from appearing in the output.
232246
func getTopFrameFile() string {
233-
// Get the frames.
247+
// Get the frames. Skip the first two frames - current one and caller.
234248
//nolint:mnd // Whatever mnd...
235-
pc := make([]uintptr, 20)
249+
pc := make([]uintptr, 40)
236250
//nolint:mnd // Whatever mnd...
237251
n := runtime.Callers(2, pc)
238252
callersFrames := runtime.CallersFrames(pc[:n])
239253

240-
var file string
254+
var (
255+
file string
256+
lineNumber int
257+
frame runtime.Frame
258+
)
241259

242-
var lineNumber int
260+
more := true
261+
for more {
262+
frame, more = callersFrames.Next()
243263

244-
var frame runtime.Frame
245-
for range 20 {
246-
frame, _ = callersFrames.Next()
264+
// Once we are in the go main stack, bail out
247265
if !strings.Contains(frame.Function, "/") {
248266
break
249267
}
250268

269+
// XXX see note above
270+
if strings.Contains(frame.File, "/tigron") {
271+
continue
272+
}
273+
274+
if strings.Contains(frame.File, "/nerdtest") {
275+
continue
276+
}
277+
251278
file = frame.File
252279
lineNumber = frame.Line
253280
}
@@ -282,6 +309,6 @@ func getTopFrameFile() string {
282309
return hyperlinkDecorator + " " + (&formatter.OSC8{
283310
Text: line,
284311
Location: "file://" + file,
285-
Line: frame.Line,
312+
Line: lineNumber,
286313
}).String()
287314
}

0 commit comments

Comments
 (0)