Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5f099ea

Browse files
authored
feat: loadtest output formats (#4928)
1 parentf918977 commit5f099ea

File tree

4 files changed

+394
-81
lines changed

4 files changed

+394
-81
lines changed

‎cli/loadtest.go

Lines changed: 156 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package cli
22

33
import (
4-
"bufio"
5-
"bytes"
64
"context"
75
"encoding/json"
86
"fmt"
97
"io"
108
"os"
119
"strconv"
10+
"strings"
1211
"time"
1312

1413
"github.com/spf13/cobra"
@@ -21,44 +20,46 @@ import (
2120

2221
funcloadtest()*cobra.Command {
2322
var (
24-
configPathstring
23+
configPathstring
24+
outputSpecs []string
2525
)
2626
cmd:=&cobra.Command{
27-
Use:"loadtest --config <path>",
27+
Use:"loadtest --config <path> [--output json[:path]] [--output text[:path]]]",
2828
Short:"Load test the Coder API",
29-
// TODO: documentation and a JSON scheme file
30-
Long:"Perform load tests against the Coder server. The load tests "+
31-
"configurable via a JSON file.",
29+
// TODO: documentation and a JSON schema file
30+
Long:"Perform load tests against the Coder server. The load tests are configurable via a JSON file.",
31+
Example:formatExamples(
32+
example{
33+
Description:"Run a loadtest with the given configuration file",
34+
Command:"coder loadtest --config path/to/config.json",
35+
},
36+
example{
37+
Description:"Run a loadtest, reading the configuration from stdin",
38+
Command:"cat path/to/config.json | coder loadtest --config -",
39+
},
40+
example{
41+
Description:"Run a loadtest outputting JSON results instead",
42+
Command:"coder loadtest --config path/to/config.json --output json",
43+
},
44+
example{
45+
Description:"Run a loadtest outputting JSON results to a file",
46+
Command:"coder loadtest --config path/to/config.json --output json:path/to/results.json",
47+
},
48+
example{
49+
Description:"Run a loadtest outputting text results to stdout and JSON results to a file",
50+
Command:"coder loadtest --config path/to/config.json --output text --output json:path/to/results.json",
51+
},
52+
),
3253
Hidden:true,
3354
Args:cobra.ExactArgs(0),
3455
RunE:func(cmd*cobra.Command,args []string)error {
35-
ifconfigPath=="" {
36-
returnxerrors.New("config is required")
37-
}
38-
39-
var (
40-
configReader io.ReadCloser
41-
)
42-
ifconfigPath=="-" {
43-
configReader=io.NopCloser(cmd.InOrStdin())
44-
}else {
45-
f,err:=os.Open(configPath)
46-
iferr!=nil {
47-
returnxerrors.Errorf("open config file %q: %w",configPath,err)
48-
}
49-
configReader=f
50-
}
51-
52-
varconfigLoadTestConfig
53-
err:=json.NewDecoder(configReader).Decode(&config)
54-
_=configReader.Close()
56+
config,err:=loadLoadTestConfigFile(configPath,cmd.InOrStdin())
5557
iferr!=nil {
56-
returnxerrors.Errorf("read config file %q: %w",configPath,err)
58+
returnerr
5759
}
58-
59-
err=config.Validate()
60+
outputs,err:=parseLoadTestOutputs(outputSpecs)
6061
iferr!=nil {
61-
returnxerrors.Errorf("validate config: %w",err)
62+
returnerr
6263
}
6364

6465
client,err:=CreateClient(cmd)
@@ -117,63 +118,156 @@ func loadtest() *cobra.Command {
117118
}
118119

119120
// TODO: live progress output
120-
start:=time.Now()
121121
err=th.Run(testCtx)
122122
iferr!=nil {
123123
returnxerrors.Errorf("run test harness (harness failure, not a test failure): %w",err)
124124
}
125-
elapsed:=time.Since(start)
126125

127126
// Print the results.
128-
// TODO: better result printing
129-
// TODO: move result printing to the loadtest package, add multiple
130-
// output formats (like HTML, JSON)
131127
res:=th.Results()
132-
vartotalDuration time.Duration
133-
for_,run:=rangeres.Runs {
134-
totalDuration+=run.Duration
135-
ifrun.Error==nil {
136-
continue
128+
for_,output:=rangeoutputs {
129+
var (
130+
w=cmd.OutOrStdout()
131+
c io.Closer
132+
)
133+
ifoutput.path!="-" {
134+
f,err:=os.Create(output.path)
135+
iferr!=nil {
136+
returnxerrors.Errorf("create output file: %w",err)
137+
}
138+
w,c=f,f
137139
}
138140

139-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\n== FAIL: %s\n\n",run.FullID)
140-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tError: %s\n\n",run.Error)
141-
142-
// Print log lines indented.
143-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tLog:\n")
144-
rd:=bufio.NewReader(bytes.NewBuffer(run.Logs))
145-
for {
146-
line,err:=rd.ReadBytes('\n')
147-
iferr==io.EOF {
148-
break
149-
}
141+
switchoutput.format {
142+
caseloadTestOutputFormatText:
143+
res.PrintText(w)
144+
caseloadTestOutputFormatJSON:
145+
err=json.NewEncoder(w).Encode(res)
150146
iferr!=nil {
151-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\n\tLOG PRINT ERROR: %+v\n",err)
147+
returnxerrors.Errorf("encode JSON: %w",err)
152148
}
149+
}
153150

154-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\t\t%s",line)
151+
ifc!=nil {
152+
err=c.Close()
153+
iferr!=nil {
154+
returnxerrors.Errorf("close output file: %w",err)
155+
}
155156
}
156157
}
157158

158-
_,_=fmt.Fprintln(cmd.ErrOrStderr(),"\n\nTest results:")
159-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tPass: %d\n",res.TotalPass)
160-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tFail: %d\n",res.TotalFail)
161-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tTotal: %d\n",res.TotalRuns)
162-
_,_=fmt.Fprintln(cmd.ErrOrStderr(),"")
163-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tTotal duration: %s\n",elapsed)
164-
_,_=fmt.Fprintf(cmd.ErrOrStderr(),"\tAvg. duration: %s\n",totalDuration/time.Duration(res.TotalRuns))
165-
166159
// Cleanup.
167160
_,_=fmt.Fprintln(cmd.ErrOrStderr(),"\nCleaning up...")
168161
err=th.Cleanup(cmd.Context())
169162
iferr!=nil {
170163
returnxerrors.Errorf("cleanup tests: %w",err)
171164
}
172165

166+
ifres.TotalFail>0 {
167+
returnxerrors.New("load test failed, see above for more details")
168+
}
169+
173170
returnnil
174171
},
175172
}
176173

177174
cliflag.StringVarP(cmd.Flags(),&configPath,"config","","CODER_LOADTEST_CONFIG_PATH","","Path to the load test configuration file, or - to read from stdin.")
175+
cliflag.StringArrayVarP(cmd.Flags(),&outputSpecs,"output","","CODER_LOADTEST_OUTPUTS", []string{"text"},"Output formats, see usage for more information.")
178176
returncmd
179177
}
178+
179+
funcloadLoadTestConfigFile(configPathstring,stdin io.Reader) (LoadTestConfig,error) {
180+
ifconfigPath=="" {
181+
returnLoadTestConfig{},xerrors.New("config is required")
182+
}
183+
184+
var (
185+
configReader io.ReadCloser
186+
)
187+
ifconfigPath=="-" {
188+
configReader=io.NopCloser(stdin)
189+
}else {
190+
f,err:=os.Open(configPath)
191+
iferr!=nil {
192+
returnLoadTestConfig{},xerrors.Errorf("open config file %q: %w",configPath,err)
193+
}
194+
configReader=f
195+
}
196+
197+
varconfigLoadTestConfig
198+
err:=json.NewDecoder(configReader).Decode(&config)
199+
_=configReader.Close()
200+
iferr!=nil {
201+
returnLoadTestConfig{},xerrors.Errorf("read config file %q: %w",configPath,err)
202+
}
203+
204+
err=config.Validate()
205+
iferr!=nil {
206+
returnLoadTestConfig{},xerrors.Errorf("validate config: %w",err)
207+
}
208+
209+
returnconfig,nil
210+
}
211+
212+
typeloadTestOutputFormatstring
213+
214+
const (
215+
loadTestOutputFormatTextloadTestOutputFormat="text"
216+
loadTestOutputFormatJSONloadTestOutputFormat="json"
217+
// TODO: html format
218+
)
219+
220+
typeloadTestOutputstruct {
221+
formatloadTestOutputFormat
222+
// Up to one path (the first path) will have the value "-" which signifies
223+
// stdout.
224+
pathstring
225+
}
226+
227+
funcparseLoadTestOutputs(outputs []string) ([]loadTestOutput,error) {
228+
varstdoutFormatloadTestOutputFormat
229+
230+
validFormats:=map[loadTestOutputFormat]struct{}{
231+
loadTestOutputFormatText: {},
232+
loadTestOutputFormatJSON: {},
233+
}
234+
235+
varout []loadTestOutput
236+
fori,o:=rangeoutputs {
237+
parts:=strings.SplitN(o,":",2)
238+
format:=loadTestOutputFormat(parts[0])
239+
if_,ok:=validFormats[format];!ok {
240+
returnnil,xerrors.Errorf("invalid output format %q in output flag %d",parts[0],i)
241+
}
242+
243+
iflen(parts)==1 {
244+
ifstdoutFormat!="" {
245+
returnnil,xerrors.Errorf("multiple output flags specified for stdout")
246+
}
247+
stdoutFormat=format
248+
continue
249+
}
250+
iflen(parts)!=2 {
251+
returnnil,xerrors.Errorf("invalid output flag %d: %q",i,o)
252+
}
253+
254+
out=append(out,loadTestOutput{
255+
format:format,
256+
path:parts[1],
257+
})
258+
}
259+
260+
// Default to --output text
261+
ifstdoutFormat==""&&len(out)==0 {
262+
stdoutFormat=loadTestOutputFormatText
263+
}
264+
265+
ifstdoutFormat!="" {
266+
out=append([]loadTestOutput{{
267+
format:stdoutFormat,
268+
path:"-",
269+
}},out...)
270+
}
271+
272+
returnout,nil
273+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp