Skip to content

Commit 827c49c

Browse files
author
Nathan Sullivan
authored
adding test coverage for preflight.RunPreflights() (#949)
* adding test coverage for preflight.RunPreflights() TDD to work on #906 and verify the fix is successful * go.mod/go.sum: removing gnomock stuff since it's not in use (yet) * Makefile: try running the preflight integration test with the e2e tests, since there's a K3s instance in place already * Makefile add a dedicated test-integration task, which runs as it's own github action job * Makefile: exclude a few things from test-integration that break the github action job * WIP on preflight tests, addressing some of @banjoh's feedback, more to go though (specifically changing over to using assert) * preflight tests: use the testify libraries, restructure code to be formatted more like other tests in this project
1 parent de03710 commit 827c49c

File tree

5 files changed

+292
-0
lines changed

5 files changed

+292
-0
lines changed

.github/workflows/build-test-deploy.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ jobs:
3030

3131
- run: make test
3232

33+
test-integration:
34+
runs-on: ubuntu-20.04
35+
steps:
36+
- uses: actions/setup-go@v3
37+
with:
38+
go-version: "1.19"
39+
40+
- name: setup env
41+
run: |
42+
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
43+
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
44+
shell: bash
45+
46+
- uses: actions/checkout@v3
47+
- uses: replicatedhq/action-k3s@main
48+
id: k3s
49+
with:
50+
version: v1.23.6-k3s1
51+
52+
- run: make test-integration
53+
3354
ensure-schemas-are-generated:
3455
runs-on: ubuntu-latest
3556
steps:

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ ffi: fmt vet
4545
test: generate fmt vet
4646
go test ${BUILDFLAGS} ./pkg/... ./cmd/... -coverprofile cover.out
4747

48+
# Go tests that require a K8s instance
49+
# TODOLATER: merge with test, so we get unified coverage reports? it'll add 21~sec to the test job though...
50+
.PHONY: test-integration
51+
test-integration:
52+
go test -v --tags "integration exclude_graphdriver_devicemapper exclude_graphdriver_btrfs" ./pkg/... ./cmd/...
53+
4854
.PHONY: preflight-e2e-test
4955
preflight-e2e-test:
5056
./test/validate-preflight-e2e.sh

internal/testutils/utils.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package testutils
22

33
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
"fmt"
47
"os"
58
"path/filepath"
69
"runtime"
@@ -22,3 +25,10 @@ func FileDir() string {
2225
_, filename, _, _ := runtime.Caller(0)
2326
return filepath.Dir(filename)
2427
}
28+
29+
// Generates a temporary filename
30+
func TempFilename(prefix string) string {
31+
randBytes := make([]byte, 16)
32+
rand.Read(randBytes)
33+
return filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s", prefix, hex.EncodeToString(randBytes)))
34+
}

pkg/preflight/run_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//go:build integration
2+
3+
// NOTE: requires a Kubernetes cluster in place currently, hence hidden behind a tag above
4+
// TODOLATER: get a mocked or ephemeral/Docker based K8s for use? see below some approaches which haven't played out yet
5+
// Test using: go test --tags=integration
6+
7+
package preflight
8+
9+
import (
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/replicatedhq/troubleshoot/internal/testutils"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
/*
18+
See TODOLATER below
19+
20+
"k8s.io/client-go/kubernetes/fake"
21+
k8sruntime "k8s.io/apimachinery/pkg/runtime"
22+
discoveryfake "k8s.io/client-go/discovery/fake"
23+
*/)
24+
25+
func TestRunPreflights(t *testing.T) {
26+
t.Parallel()
27+
28+
// A very simple preflight spec (local file)
29+
preflightFile := filepath.Join(testutils.FileDir(), "../../testdata/preflightspec/troubleshoot_v1beta2_preflight_gotest.yaml")
30+
31+
wantOutputContentHuman := `
32+
--- PASS Compare JSON Example
33+
--- The collected data matches the value.
34+
--- PASS go-test-preflight
35+
PASS
36+
`
37+
38+
wantOutputContentJson := `{
39+
"pass": [
40+
{
41+
"title": "Compare JSON Example",
42+
"message": "The collected data matches the value."
43+
}
44+
]
45+
}
46+
`
47+
48+
wantOutputContentYaml := `pass:
49+
- title: Compare JSON Example
50+
message: The collected data matches the value.
51+
52+
`
53+
54+
tests := []struct {
55+
name string
56+
interactive bool
57+
output string
58+
format string
59+
args []string
60+
//
61+
wantErr bool
62+
// May be in stdout or file, depending on above value
63+
wantOutputContent string
64+
}{
65+
// TODOLATER: test interactive true as well
66+
{
67+
name: "noninteractive-stdout-human",
68+
interactive: false,
69+
output: "",
70+
format: "human",
71+
args: []string{preflightFile},
72+
wantErr: false,
73+
wantOutputContent: wantOutputContentHuman,
74+
},
75+
{
76+
name: "noninteractive-stdout-json",
77+
interactive: false,
78+
output: "",
79+
format: "json",
80+
args: []string{preflightFile},
81+
wantErr: false,
82+
wantOutputContent: wantOutputContentJson,
83+
},
84+
{
85+
name: "noninteractive-stdout-yaml",
86+
interactive: false,
87+
output: "",
88+
format: "yaml",
89+
args: []string{preflightFile},
90+
wantErr: false,
91+
wantOutputContent: wantOutputContentYaml,
92+
},
93+
{
94+
name: "noninteractive-tofile-human",
95+
interactive: false,
96+
output: testutils.TempFilename("preflight_out_test_"),
97+
format: "human",
98+
args: []string{preflightFile},
99+
wantErr: false,
100+
wantOutputContent: wantOutputContentHuman,
101+
},
102+
{
103+
name: "noninteractive-tofile-json",
104+
interactive: false,
105+
output: testutils.TempFilename("preflight_out_test_"),
106+
format: "json",
107+
args: []string{preflightFile},
108+
wantErr: false,
109+
wantOutputContent: wantOutputContentJson,
110+
},
111+
{
112+
name: "noninteractive-tofile-yaml",
113+
interactive: false,
114+
output: testutils.TempFilename("preflight_out_test_"),
115+
format: "yaml",
116+
args: []string{preflightFile},
117+
wantErr: false,
118+
wantOutputContent: wantOutputContentYaml,
119+
},
120+
}
121+
122+
// Use a fake/mocked K8s API, since some collectors are mandatory and need an API server to hit
123+
// TODOLATER: for this to work, we need to refactor all of the collectors and analyzers to allow passing in a fake clientset?
124+
// ...or we need to find a way to expose the fake clientset with a mocked API server and a respective kubeconfig for use
125+
// Using gnomock k3s for now instead (requires Docker for test execution)
126+
/*
127+
k8sObjects := []k8sruntime.Object{
128+
// TODO: populate with things that mandatory collectors need
129+
}
130+
k8sApi := fake.NewSimpleClientset(k8sObjects...)
131+
k8sApi.Discovery().(*discoveryfake.FakeDiscovery).FakedServerVersion = &version.Info{
132+
Major: "1",
133+
Minor: "26",
134+
}
135+
fmt.Printf("%+v\n", k8sApi)
136+
*/
137+
138+
// A K3s instance in Docker for testing, for mandatory collectors
139+
// NOTE: only has amd64 images, doesn't work on arm64 (MacOS)? need to build and specify custom images for it
140+
// ...plus there's no new images since 2020~?
141+
/*
142+
p := k3s.Preset(k3s.WithVersion("v1.19.12"))
143+
c, err := gnomock.Start(
144+
p,
145+
gnomock.WithContainerName("k3s"),
146+
gnomock.WithDebugMode(),
147+
)
148+
if err != nil {
149+
t.Fatal(err)
150+
}
151+
kubeconfig, err := k3s.Config(c)
152+
if err != nil {
153+
t.Fatal(err)
154+
}
155+
fmt.Printf("kubeconfig: %+v\n", kubeconfig)
156+
*/
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
// Capture stdout/stderr along the way
161+
rOut, wOut, err := os.Pipe()
162+
require.Nil(t, err)
163+
rErr, wErr, err := os.Pipe()
164+
require.Nil(t, err)
165+
// Redirect stdout/err to the pipes temporarily
166+
origStdout := os.Stdout
167+
os.Stdout = wOut
168+
origStderr := os.Stderr
169+
os.Stderr = wErr
170+
171+
tErr := RunPreflights(tt.interactive, tt.output, tt.format, tt.args)
172+
173+
// Stop redirection of stdout/stderr
174+
bufOut := make([]byte, 1024)
175+
nOut, err := rOut.Read(bufOut)
176+
require.Nil(t, err)
177+
bufErr := make([]byte, 1024)
178+
// nErr
179+
_, err = rErr.Read(bufErr)
180+
require.Nil(t, err)
181+
os.Stdout = origStdout
182+
os.Stderr = origStderr
183+
184+
if tt.wantErr {
185+
assert.Error(t, tErr)
186+
} else {
187+
require.NoError(t, tErr)
188+
}
189+
190+
useBufOut := string(bufOut[:nOut])
191+
//useBufErr := string(bufErr[:nErr])
192+
//fmt.Printf("stdout: %+v\n", useBufOut)
193+
//fmt.Printf("stderr: %+v\n", useBufErr)
194+
195+
if tt.output != "" {
196+
// Output file is expected, make sure it exists
197+
assert.FileExists(t, tt.output)
198+
// If it exists, check contents of output file against expected
199+
readOutputFile, err := os.ReadFile(tt.output)
200+
require.NoError(t, err)
201+
assert.Equal(t, string(readOutputFile), tt.wantOutputContent)
202+
} else {
203+
// Expected no output file, make sure it doesn't exist
204+
assert.NoFileExists(t, tt.output)
205+
// Check stdout against expected output
206+
assert.Equal(t, useBufOut, tt.wantOutputContent)
207+
}
208+
209+
// Remove the (temp) output file if it exists
210+
if _, err := os.Stat(tt.output); err == nil {
211+
err = os.Remove(tt.output)
212+
require.NoError(t, err)
213+
}
214+
})
215+
}
216+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
apiVersion: troubleshoot.sh/v1beta2
2+
kind: Preflight
3+
metadata:
4+
name: go-test-preflight
5+
spec:
6+
# TODO: mock a local HTTP server to send this to
7+
#uploadResultsTo: http://someurl
8+
collectors:
9+
- data:
10+
name: example.json
11+
data: |
12+
{
13+
"foo": "bar",
14+
"stuff": {
15+
"foo": "bar",
16+
"bar": true
17+
},
18+
"morestuff": [
19+
{
20+
"foo": {
21+
"bar": 123
22+
}
23+
}
24+
]
25+
}
26+
analyzers:
27+
- jsonCompare:
28+
checkName: Compare JSON Example
29+
fileName: example.json
30+
path: "morestuff.[0].foo.bar"
31+
value: |
32+
123
33+
outcomes:
34+
- fail:
35+
when: "false"
36+
message: The collected data does not match the value.
37+
- pass:
38+
when: "true"
39+
message: The collected data matches the value.

0 commit comments

Comments
 (0)