Skip to content

Commit e3a4c70

Browse files
committed
Adding mimicry (internal mock mechanism)
Mimicry is a lightweight, zero dependency mock mechanism created to ease testing of Tigron. Since Tigron heavily relies on *testing.T, it is currently hard to test. Moving away to a tig.T interface instead of *testing.T will unlock the ability to mock. Mimicry does provide: - recording of all function calls, with arguments and complete stack trace (see Report()) - optional custom handling of function calls (see Register()) - QOL: fancyfied OCS8 links allow opening files from traces in terminal UX is largely in flux right now and experimental, but the objective is to: - do not require code generation - do not abuse reflection - keep the amount of boilerplate to the absolute minimum for the mock consumer - ... and as small as possible for the mock creator - keep zero dependencies This commit also introduce the tig.T interface to be used everywhere inside Tigron in the future, along with a complete mock for it. Mimicry is not meant to be used directly for now, though, if there is interest, a future version might graduate out of `internal`. Signed-off-by: apostasie <[email protected]>
1 parent 2def90d commit e3a4c70

File tree

12 files changed

+792
-0
lines changed

12 files changed

+792
-0
lines changed

mod/tigron/internal/formatter/doc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package formatter provides simple formatting helpers for internal consumption.
18+
package formatter
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package formatter
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"unicode/utf8"
23+
)
24+
25+
const (
26+
maxLineLength = 110
27+
maxLines = 100
28+
kMaxLength = 7
29+
)
30+
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.
65+
//
66+
//nolint:mnd // Too annoying
67+
func Table(data [][]any) string {
68+
var output string
69+
70+
for _, row := range data {
71+
key := fmt.Sprintf("%v", row[0])
72+
value := strings.ReplaceAll(fmt.Sprintf("%v", row[1]), "\t", " ")
73+
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+
}
79+
80+
for _, line := range chunk(value, maxLineLength-kMaxLength-7) {
81+
output += fmt.Sprintf(
82+
"| %-*s | %-*s |\n",
83+
kMaxLength,
84+
key,
85+
maxLineLength-kMaxLength-7,
86+
line,
87+
)
88+
key = ""
89+
}
90+
}
91+
92+
output += fmt.Sprintf("+%s+", strings.Repeat("-", maxLineLength-2))
93+
94+
return output
95+
}

mod/tigron/internal/formatter/osc8.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package formatter
18+
19+
import "fmt"
20+
21+
// OSC8 hyperlinks implementation.
22+
type OSC8 struct {
23+
Location string `json:"location"`
24+
Line int `json:"line"`
25+
Text string `json:"text"`
26+
}
27+
28+
func (o *OSC8) String() string {
29+
// FIXME: not sure if any desktop software does support line numbers anchors?
30+
return fmt.Sprintf("\x1b]8;;%s#%d:1\x07%s\x1b]8;;\x07"+"\u001b[0m", o.Location, o.Line, o.Text)
31+
}

mod/tigron/internal/mimicry/doc.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package mimicry provides a very rough and rudimentary mimicry library to help with internal tigron testing.
18+
// It does not require generation, does not abuse reflect (too much), and keeps the amount of boilerplate baloney to a
19+
// minimum.
20+
// This is NOT a generic mock library. Use something else if you need one.
21+
package mimicry

mod/tigron/internal/mimicry/doc.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# [INTERNAL] [EXPERIMENTAL] Mimicry
2+
3+
## Creating a Mock
4+
5+
```golang
6+
package mymock
7+
8+
import "github.com/containerd/nerdctl/mod/tigron/internal/mimicry"
9+
10+
// Let's assume we want to mock the following, likely defined somewhere else
11+
// type InterfaceToBeMocked interface {
12+
// SomeMethod(one string, two int) error
13+
// }
14+
15+
// Compile time ensure the mock does fulfill the interface
16+
var _ InterfaceToBeMocked = &MyMock{}
17+
18+
type MyMock struct {
19+
// Embed mimicry core
20+
mimicry.Core
21+
}
22+
23+
// First, describe function parameters and return values.
24+
type (
25+
MyMockSomeMethodIn struct {
26+
one string
27+
two int
28+
}
29+
30+
MyMockSomeMethodOut = error
31+
)
32+
33+
// Satisfy the interface + wire-in the handler mechanism
34+
35+
func (m *MyMock) SomeMethod(one string, two int) error {
36+
// Call mimicry method Retrieve that will record the call, and return a custom handler if one is defined
37+
if handler := m.Retrieve(); handler != nil {
38+
// Call the optional handler if there is one.
39+
return handler.(mimicry.Function[MyMockSomeMethodIn, MyMockSomeMethodOut])(MyMockSomeMethodIn{
40+
one: one,
41+
two: two,
42+
})
43+
}
44+
45+
return nil
46+
}
47+
```
48+
49+
50+
## Using a Mock
51+
52+
For consumers, the simplest way to use the mock is to inspect calls after the fact:
53+
54+
```golang
55+
package mymock
56+
57+
import "testing"
58+
59+
// This is the code you want to test, that does depend on the interface we are mocking.
60+
// func functionYouWantToTest(o InterfaceToBeMocked, i int) {
61+
// o.SomeMethod("lala", i)
62+
// }
63+
64+
func TestOne(t *testing.T) {
65+
// Create the mock from above
66+
mocky := &MyMock{}
67+
68+
// Call the function you want to test
69+
functionYouWantToTest(mocky, 42)
70+
functionYouWantToTest(mocky, 123)
71+
72+
// Now you can inspect the calls log for that function.
73+
report := mocky.Report(InterfaceToBeMocked.SomeMethod)
74+
t.Log("Number of times it was called:", len(report))
75+
t.Log("Inspecting the last call:")
76+
t.Log(mimicry.PrintCall(report[len(report)-1]))
77+
}
78+
```
79+
80+
## Using handlers
81+
82+
Implementing handlers allows active interception of the calls for more elaborate scenarios.
83+
84+
```golang
85+
package main_test
86+
87+
import "testing"
88+
89+
// The method you want to test against the mock
90+
// func functionYouWantToTest(o InterfaceToBeMocked, i int) {
91+
// o.SomeMethod("lala", i)
92+
// }
93+
94+
func TestTwo(t *testing.T) {
95+
// Create the base mock
96+
mocky := &MyMock{}
97+
98+
// Declare a custom handler for the method `SomeMethod`
99+
mocky.Register(InterfaceToBeMocked.SomeMethod, func(in MyMockSomeMethodIn) MyMockSomeMethodOut {
100+
t.Log("Got parameters", in)
101+
102+
// We want to fail on that
103+
if in.two == 42 {
104+
// Print out the callstack
105+
report := mocky.Report(InterfaceToBeMocked.SomeMethod)
106+
t.Log(mimicry.PrintCall(report[len(report)-1]))
107+
t.Error("We do not want to ever receive 42. Inspect trace above.")
108+
}else{
109+
t.Log("all fine - we did not see 42")
110+
}
111+
112+
return nil
113+
})
114+
115+
functionYouWantToTest(mocky, 123)
116+
functionYouWantToTest(mocky, 42)
117+
}
118+
```

0 commit comments

Comments
 (0)