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

Commita4bba52

Browse files
feat(cli): add json output to coder speedtest (#13475)
1 parent9a757f8 commita4bba52

File tree

7 files changed

+226
-38
lines changed

7 files changed

+226
-38
lines changed

‎cli/cliui/output.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88
"strings"
99

10+
"github.com/jedib0t/go-pretty/v6/table"
1011
"golang.org/x/xerrors"
1112

1213
"github.com/coder/serpent"
@@ -143,7 +144,11 @@ func (f *tableFormat) AttachOptions(opts *serpent.OptionSet) {
143144

144145
// Format implements OutputFormat.
145146
func (f*tableFormat)Format(_ context.Context,dataany) (string,error) {
146-
returnDisplayTable(data,f.sort,f.columns)
147+
headers:=make(table.Row,len(f.allColumns))
148+
fori,header:=rangef.allColumns {
149+
headers[i]=header
150+
}
151+
returnrenderTable(data,f.sort,headers,f.columns)
147152
}
148153

149154
typejsonFormatstruct{}

‎cli/cliui/table.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ func Table() table.Writer {
2222
returntableWriter
2323
}
2424

25+
// This type can be supplied as part of a slice to DisplayTable
26+
// or to a `TableFormat` `Format` call to render a separator.
27+
// Leading separators are not supported and trailing separators
28+
// are ignored by the table formatter.
29+
// e.g. `[]any{someRow, TableSeparator, someRow}`
30+
typeTableSeparatorstruct{}
31+
2532
// filterTableColumns returns configurations to hide columns
2633
// that are not provided in the array. If the array is empty,
2734
// no filtering will occur!
@@ -47,8 +54,12 @@ func filterTableColumns(header table.Row, columns []string) []table.ColumnConfig
4754
returncolumnConfigs
4855
}
4956

50-
// DisplayTable renders a table as a string. The input argument must be a slice
51-
// of structs. At least one field in the struct must have a `table:""` tag
57+
// DisplayTable renders a table as a string. The input argument can be:
58+
// - a struct slice.
59+
// - an interface slice, where the first element is a struct,
60+
// and all other elements are of the same type, or a TableSeparator.
61+
//
62+
// At least one field in the struct must have a `table:""` tag
5263
// containing the name of the column in the outputted table.
5364
//
5465
// If `sort` is not specified, the field with the `table:"$NAME,default_sort"`
@@ -66,11 +77,20 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
6677
v:=reflect.Indirect(reflect.ValueOf(out))
6778

6879
ifv.Kind()!=reflect.Slice {
69-
return"",xerrors.Errorf("DisplayTable called with a non-slice type")
80+
return"",xerrors.New("DisplayTable called with a non-slice type")
81+
}
82+
vartableType reflect.Type
83+
ifv.Type().Elem().Kind()==reflect.Interface {
84+
ifv.Len()==0 {
85+
return"",xerrors.New("DisplayTable called with empty interface slice")
86+
}
87+
tableType=reflect.Indirect(reflect.ValueOf(v.Index(0).Interface())).Type()
88+
}else {
89+
tableType=v.Type().Elem()
7090
}
7191

7292
// Get the list of table column headers.
73-
headersRaw,defaultSort,err:=typeToTableHeaders(v.Type().Elem(),true)
93+
headersRaw,defaultSort,err:=typeToTableHeaders(tableType,true)
7494
iferr!=nil {
7595
return"",xerrors.Errorf("get table headers recursively for type %q: %w",v.Type().Elem().String(),err)
7696
}
@@ -82,9 +102,8 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
82102
}
83103
headers:=make(table.Row,len(headersRaw))
84104
fori,header:=rangeheadersRaw {
85-
headers[i]=header
105+
headers[i]=strings.ReplaceAll(header,"_"," ")
86106
}
87-
88107
// Verify that the given sort column and filter columns are valid.
89108
ifsort!=""||len(filterColumns)!=0 {
90109
headersMap:=make(map[string]string,len(headersRaw))
@@ -130,6 +149,11 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
130149
return"",xerrors.Errorf("specified sort column %q not found in table headers, available columns are %q",sort,strings.Join(headersRaw,`", "`))
131150
}
132151
}
152+
returnrenderTable(out,sort,headers,filterColumns)
153+
}
154+
155+
funcrenderTable(outany,sortstring,headers table.Row,filterColumns []string) (string,error) {
156+
v:=reflect.Indirect(reflect.ValueOf(out))
133157

134158
// Setup the table formatter.
135159
tw:=Table()
@@ -143,15 +167,22 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
143167

144168
// Write each struct to the table.
145169
fori:=0;i<v.Len();i++ {
170+
cur:=v.Index(i).Interface()
171+
_,ok:=cur.(TableSeparator)
172+
ifok {
173+
tw.AppendSeparator()
174+
continue
175+
}
146176
// Format the row as a slice.
147-
rowMap,err:=valueToTableMap(v.Index(i))
177+
// ValueToTableMap does what `reflect.Indirect` does
178+
rowMap,err:=valueToTableMap(reflect.ValueOf(cur))
148179
iferr!=nil {
149180
return"",xerrors.Errorf("get table row map %v: %w",i,err)
150181
}
151182

152183
rowSlice:=make([]any,len(headers))
153-
fori,h:=rangeheadersRaw {
154-
v,ok:=rowMap[h]
184+
fori,h:=rangeheaders {
185+
v,ok:=rowMap[h.(string)]
155186
if!ok {
156187
v=nil
157188
}
@@ -188,25 +219,28 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
188219
// returned. If the table tag is malformed, an error is returned.
189220
//
190221
// The returned name is transformed from "snake_case" to "normal text".
191-
funcparseTableStructTag(field reflect.StructField) (namestring,defaultSort,recursivebool,skipParentNamebool,errerror) {
222+
funcparseTableStructTag(field reflect.StructField) (namestring,defaultSort,noSortOpt,recursive,skipParentNamebool,errerror) {
192223
tags,err:=structtag.Parse(string(field.Tag))
193224
iferr!=nil {
194-
return"",false,false,false,xerrors.Errorf("parse struct field tag %q: %w",string(field.Tag),err)
225+
return"",false,false,false,false,xerrors.Errorf("parse struct field tag %q: %w",string(field.Tag),err)
195226
}
196227

197228
tag,err:=tags.Get("table")
198229
iferr!=nil||tag.Name=="-" {
199230
// tags.Get only returns an error if the tag is not found.
200-
return"",false,false,false,nil
231+
return"",false,false,false,false,nil
201232
}
202233

203234
defaultSortOpt:=false
235+
noSortOpt=false
204236
recursiveOpt:=false
205237
skipParentNameOpt:=false
206238
for_,opt:=rangetag.Options {
207239
switchopt {
208240
case"default_sort":
209241
defaultSortOpt=true
242+
case"nosort":
243+
noSortOpt=true
210244
case"recursive":
211245
recursiveOpt=true
212246
case"recursive_inline":
@@ -216,11 +250,11 @@ func parseTableStructTag(field reflect.StructField) (name string, defaultSort, r
216250
recursiveOpt=true
217251
skipParentNameOpt=true
218252
default:
219-
return"",false,false,false,xerrors.Errorf("unknown option %q in struct field tag",opt)
253+
return"",false,false,false,false,xerrors.Errorf("unknown option %q in struct field tag",opt)
220254
}
221255
}
222256

223-
returnstrings.ReplaceAll(tag.Name,"_"," "),defaultSortOpt,recursiveOpt,skipParentNameOpt,nil
257+
returnstrings.ReplaceAll(tag.Name,"_"," "),defaultSortOpt,noSortOpt,recursiveOpt,skipParentNameOpt,nil
224258
}
225259

226260
funcisStructOrStructPointer(t reflect.Type)bool {
@@ -244,12 +278,16 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
244278

245279
headers:= []string{}
246280
defaultSortName:=""
281+
noSortOpt:=false
247282
fori:=0;i<t.NumField();i++ {
248283
field:=t.Field(i)
249-
name,defaultSort,recursive,skip,err:=parseTableStructTag(field)
284+
name,defaultSort,noSort,recursive,skip,err:=parseTableStructTag(field)
250285
iferr!=nil {
251286
returnnil,"",xerrors.Errorf("parse struct tags for field %q in type %q: %w",field.Name,t.String(),err)
252287
}
288+
ifrequireDefault&&noSort {
289+
noSortOpt=true
290+
}
253291

254292
ifname==""&& (recursive&&skip) {
255293
returnnil,"",xerrors.Errorf("a name is required for the field %q. "+
@@ -292,8 +330,8 @@ func typeToTableHeaders(t reflect.Type, requireDefault bool) ([]string, string,
292330
headers=append(headers,name)
293331
}
294332

295-
ifdefaultSortName==""&&requireDefault {
296-
returnnil,"",xerrors.Errorf("no field marked as default_sort in type %q",t.String())
333+
ifdefaultSortName==""&&requireDefault&&!noSortOpt{
334+
returnnil,"",xerrors.Errorf("no field marked as default_sortor nosortin type %q",t.String())
297335
}
298336

299337
returnheaders,defaultSortName,nil
@@ -320,7 +358,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
320358
fori:=0;i<val.NumField();i++ {
321359
field:=val.Type().Field(i)
322360
fieldVal:=val.Field(i)
323-
name,_,recursive,skip,err:=parseTableStructTag(field)
361+
name,_,_,recursive,skip,err:=parseTableStructTag(field)
324362
iferr!=nil {
325363
returnnil,xerrors.Errorf("parse struct tags for field %q in type %T: %w",field.Name,val,err)
326364
}

‎cli/cliui/table_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,42 @@ Alice 25
218218
compareTables(t,expected,out)
219219
})
220220

221+
// This test ensures we can display dynamically typed slices
222+
t.Run("Interfaces",func(t*testing.T) {
223+
t.Parallel()
224+
225+
in:= []any{tableTest1{}}
226+
out,err:=cliui.DisplayTable(in,"",nil)
227+
t.Log("rendered table:\n"+out)
228+
require.NoError(t,err)
229+
other:= []tableTest1{{}}
230+
expected,err:=cliui.DisplayTable(other,"",nil)
231+
require.NoError(t,err)
232+
compareTables(t,expected,out)
233+
})
234+
235+
t.Run("WithSeparator",func(t*testing.T) {
236+
t.Parallel()
237+
expected:=`
238+
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
239+
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
240+
-------------------------------------------------------------------------------------------------------------------------------------------------------------
241+
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
242+
-------------------------------------------------------------------------------------------------------------------------------------------------------------
243+
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
244+
`
245+
246+
varinlineIn []any
247+
for_,v:=rangein {
248+
inlineIn=append(inlineIn,v)
249+
inlineIn=append(inlineIn, cliui.TableSeparator{})
250+
}
251+
out,err:=cliui.DisplayTable(inlineIn,"",nil)
252+
t.Log("rendered table:\n"+out)
253+
require.NoError(t,err)
254+
compareTables(t,expected,out)
255+
})
256+
221257
// This test ensures that safeties against invalid use of `table` tags
222258
// causes errors (even without data).
223259
t.Run("Errors",func(t*testing.T) {
@@ -255,14 +291,6 @@ Alice 25
255291
_,err:=cliui.DisplayTable(in,"",nil)
256292
require.Error(t,err)
257293
})
258-
259-
t.Run("WithData",func(t*testing.T) {
260-
t.Parallel()
261-
262-
in:= []any{tableTest1{}}
263-
_,err:=cliui.DisplayTable(in,"",nil)
264-
require.Error(t,err)
265-
})
266294
})
267295

268296
t.Run("NotStruct",func(t*testing.T) {

‎cli/speedtest.go

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"os"
77
"time"
88

9-
"github.com/jedib0t/go-pretty/v6/table"
109
"golang.org/x/xerrors"
1110
tsspeedtest"tailscale.com/net/speedtest"
1211
"tailscale.com/wgengine/capture"
@@ -19,12 +18,51 @@ import (
1918
"github.com/coder/serpent"
2019
)
2120

21+
typeSpeedtestResultstruct {
22+
OverallSpeedtestResultInterval`json:"overall"`
23+
Intervals []SpeedtestResultInterval`json:"intervals"`
24+
}
25+
26+
typeSpeedtestResultIntervalstruct {
27+
StartTimeSecondsfloat64`json:"start_time_seconds"`
28+
EndTimeSecondsfloat64`json:"end_time_seconds"`
29+
ThroughputMbitsfloat64`json:"throughput_mbits"`
30+
}
31+
32+
typespeedtestTableItemstruct {
33+
Intervalstring`table:"Interval,nosort"`
34+
Throughputstring`table:"Throughput"`
35+
}
36+
2237
func (r*RootCmd)speedtest()*serpent.Command {
2338
var (
2439
directbool
2540
duration time.Duration
2641
directionstring
2742
pcapFilestring
43+
formatter=cliui.NewOutputFormatter(
44+
cliui.ChangeFormatterData(cliui.TableFormat([]speedtestTableItem{}, []string{"Interval","Throughput"}),func(dataany) (any,error) {
45+
res,ok:=data.(SpeedtestResult)
46+
if!ok {
47+
// This should never happen
48+
return"",xerrors.Errorf("expected speedtestResult, got %T",data)
49+
}
50+
tableRows:=make([]any,len(res.Intervals)+2)
51+
fori,r:=rangeres.Intervals {
52+
tableRows[i]=speedtestTableItem{
53+
Interval:fmt.Sprintf("%.2f-%.2f sec",r.StartTimeSeconds,r.EndTimeSeconds),
54+
Throughput:fmt.Sprintf("%.4f Mbits/sec",r.ThroughputMbits),
55+
}
56+
}
57+
tableRows[len(res.Intervals)]= cliui.TableSeparator{}
58+
tableRows[len(res.Intervals)+1]=speedtestTableItem{
59+
Interval:fmt.Sprintf("%.2f-%.2f sec",res.Overall.StartTimeSeconds,res.Overall.EndTimeSeconds),
60+
Throughput:fmt.Sprintf("%.4f Mbits/sec",res.Overall.ThroughputMbits),
61+
}
62+
returntableRows,nil
63+
}),
64+
cliui.JSONFormat(),
65+
)
2866
)
2967
client:=new(codersdk.Client)
3068
cmd:=&serpent.Command{
@@ -124,24 +162,32 @@ func (r *RootCmd) speedtest() *serpent.Command {
124162
default:
125163
returnxerrors.Errorf("invalid direction: %q",direction)
126164
}
127-
cliui.Infof(inv.Stdout,"Starting a %ds %s test...",int(duration.Seconds()),tsDir)
165+
cliui.Infof(inv.Stderr,"Starting a %ds %s test...",int(duration.Seconds()),tsDir)
128166
results,err:=conn.Speedtest(ctx,tsDir,duration)
129167
iferr!=nil {
130168
returnerr
131169
}
132-
tableWriter:=cliui.Table()
133-
tableWriter.AppendHeader(table.Row{"Interval","Throughput"})
170+
varoutputResultSpeedtestResult
134171
startTime:=results[0].IntervalStart
135-
for_,r:=rangeresults {
172+
outputResult.Intervals=make([]SpeedtestResultInterval,len(results)-1)
173+
fori,r:=rangeresults {
174+
interval:=SpeedtestResultInterval{
175+
StartTimeSeconds:r.IntervalStart.Sub(startTime).Seconds(),
176+
EndTimeSeconds:r.IntervalEnd.Sub(startTime).Seconds(),
177+
ThroughputMbits:r.MBitsPerSecond(),
178+
}
136179
ifr.Total {
137-
tableWriter.AppendSeparator()
180+
interval.StartTimeSeconds=0
181+
outputResult.Overall=interval
182+
}else {
183+
outputResult.Intervals[i]=interval
138184
}
139-
tableWriter.AppendRow(table.Row{
140-
fmt.Sprintf("%.2f-%.2f sec",r.IntervalStart.Sub(startTime).Seconds(),r.IntervalEnd.Sub(startTime).Seconds()),
141-
fmt.Sprintf("%.4f Mbits/sec",r.MBitsPerSecond()),
142-
})
143185
}
144-
_,err=fmt.Fprintln(inv.Stdout,tableWriter.Render())
186+
out,err:=formatter.Format(inv.Context(),outputResult)
187+
iferr!=nil {
188+
returnerr
189+
}
190+
_,err=fmt.Fprintln(inv.Stdout,out)
145191
returnerr
146192
},
147193
}
@@ -173,5 +219,6 @@ func (r *RootCmd) speedtest() *serpent.Command {
173219
Value:serpent.StringOf(&pcapFile),
174220
},
175221
}
222+
formatter.AttachOptions(&cmd.Options)
176223
returncmd
177224
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp