-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprofile_test.go
169 lines (149 loc) · 4.38 KB
/
profile_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package profiler
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// CheckFunc encapsulates the file descriptor and exit code
// for standard output and standard error.
type CheckFunc func(t *testing.T, stdout, stderr string, exit int)
func TestProfilesEnabledExpectedOutput(t *testing.T) {
storage, err := os.MkdirTemp("", "profiles")
if err != nil {
t.Fatal(err)
}
defer func() {
if err := os.RemoveAll(storage); err != nil {
t.Error("problem removing file", err)
}
}()
tests := map[string]struct {
source string
checks []CheckFunc
}{
"cpu profiling works as expected": {
source: `package main
import "github.com/symonk/profiler"
func main() {
defer profiler.Start(profiler.WithCPUProfiler(), profiler.WithProfileFileLocation("` + storage + "\"" + `)).Stop()
}`,
checks: []CheckFunc{
exitedZero,
emptyStdOut,
stdErrOutMatchLines(
".*profiling completed. You can find the .*cpu.pprof.*",
".*to view the profile, run.*cpu.pprof",
"port can be any ephemeral port you wish to use",
"Graph interpretation is outlined here.*graphical-reports",
),
},
},
}
for name, tc := range tests {
t.Log(name)
t.Run(name, func(t *testing.T) {
// Execute the program, capturing meta data
stdout, stderr, exit := execute(t, tc.source)
t.Log(stdout, stderr)
// Assert the output is as expected
for _, checkFunc := range tc.checks {
checkFunc(t, stdout, stderr, exit)
}
})
}
}
// execute executes the source code written earlier and captures
// its stdout, stderr and exit code for inspection later.
func execute(t *testing.T, source string) (string, string, int) {
main, cleanup := createTempTestFile(t, source)
defer cleanup()
cmd := exec.Command("go", "run", main)
var stdOut, stdErr bytes.Buffer
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return stdOut.String(), stdErr.String(), exitErr.ExitCode()
}
}
return stdOut.String(), stdErr.String(), 0
}
// createTempTestFile creates a temporary test file with the source
// code ready for running.
func createTempTestFile(t *testing.T, source string) (string, func()) {
fatal := func(err error) {
if err != nil {
t.Fatal(err)
}
}
dir, err := os.MkdirTemp("", "go-profiler")
fatal(err)
main := filepath.Join(dir, "main.go")
// Create the source code file
err = os.WriteFile(main, []byte(source), 0644)
fatal(err)
// Create the go mod file so we can actually execute!
// TODO: This uses a hardcoded dependency version.
mod := filepath.Join(dir, "go.mod")
contents := `
module github.com/ok/example
go 1.23.2
require github.com/symonk/profiler v0.0.0-20241021143805-788e1dbe92a9
`
if err := os.WriteFile(mod, []byte(contents), 0644); err != nil {
t.Error("failed to write file", err)
}
// create the appropriate mod file etc
return main, func() {
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Error("failed to remove dir", err)
}
}()
}
}
func TestWithCustomPort(t *testing.T) {
t.Skip("not implemented yet")
}
// Check function implementations for asserting against the responses
func exitedZero(t *testing.T, _, _ string, code int) {
assert.Zero(t, code)
}
// patternMatchLines checks that the lines in stderr matched
func stdErrOutMatchLines(patterns ...string) CheckFunc {
return func(t *testing.T, stdout, stderr string, exit int) {
assert.NotEmpty(t, stderr)
patternMatchLines(t, stderr, patterns...)
}
}
// patternMatchLines checks that the lines in either stdout/err matched
// the user provided regexp patterns. No order is guarantee'd here and
// all are iterated for each pattern, this is not very performant and can
// be done in O(n) in future most likely.
func patternMatchLines(t *testing.T, input string, patterns ...string) bool {
seen := make(map[string]struct{}, len(patterns))
for _, p := range patterns {
seen[p] = struct{}{}
}
lines := strings.Split(input, "\n")
for i := 0; i < len(lines); i++ {
for j := 0; j < len(patterns); j++ {
if matched, _ := regexp.MatchString(patterns[j], lines[i]); matched {
delete(seen, patterns[j])
}
}
}
if len(seen) == 0 {
return true
}
t.Fatalf("expected all patterns to be matched, but the following were not: %v", seen)
return false
}
func emptyStdOut(t *testing.T, stdout, _ string, _ int) {
assert.Empty(t, stdout)
}