Skip to content

Commit 7cf5153

Browse files
authored
Merge pull request #52 from jr-frazier/master
"added ability to output curl command to multiple files"
2 parents b065cc9 + d0f468b commit 7cf5153

6 files changed

Lines changed: 175 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
release
33
.vscode
44
.idea
5+
.history
56

67
# Created by https://www.gitignore.io
78

cmd/curl.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@ var (
3535
if err != nil {
3636
return err
3737
}
38-
return chkRunError(runCurl(c, args[0], curlHead, curlInsecure, curlHTTP2, from, nodeIDs, curlLimit))
38+
return chkRunError(runCurl(c, args[0], curlHead, curlInsecure, curlHTTP2, from, nodeIDs, curlLimit, fileOut))
3939
},
4040
}
4141

4242
curlHead bool
4343
curlInsecure bool
4444
curlHTTP2 bool
4545
curlLimit int
46+
fileOut string
4647
)
4748

4849
func initCurlCmd(parentCmd *cobra.Command) {
@@ -52,11 +53,13 @@ func initCurlCmd(parentCmd *cobra.Command) {
5253
curlCmd.Flags().BoolVarP(&curlInsecure, "insecure", "k", false, "Allow curl to proceed for server connections considered insecure")
5354
curlCmd.Flags().BoolVarP(&curlHTTP2, "http2", "", false, "Use HTTP version 2")
5455
curlCmd.Flags().IntVarP(&curlLimit, "limit", "L", 1, "The maximum number of nodes to use")
56+
curlCmd.Flags().StringVarP(&fileOut, "file", "f", "", "output to file")
5557

5658
parentCmd.AddCommand(curlCmd)
5759
}
5860

59-
func runCurl(c *perfops.Client, target string, head, insecure, http2 bool, from string, nodeIDs []int, limit int) error {
61+
func runCurl(c *perfops.Client, target string, head, insecure, http2 bool, from string, nodeIDs []int, limit int, fileOut string) error {
62+
6063
ctx := context.Background()
6164
curlReq := &perfops.CurlRequest{
6265
Target: target,
@@ -100,6 +103,7 @@ func runCurl(c *perfops.Client, target string, head, insecure, http2 bool, from
100103
if o, err = res.Output(); err != nil {
101104
return err
102105
}
106+
103107
if !outputJSON && o != nil {
104108
f.StopSpinner()
105109
internal.PrintOutput(f, o)
@@ -108,6 +112,10 @@ func runCurl(c *perfops.Client, target string, head, insecure, http2 bool, from
108112
break
109113
}
110114
}
115+
if len(fileOut) > 0 {
116+
f.StopSpinner()
117+
internal.OutputToFile(f, o, fileOut)
118+
}
111119
if outputJSON {
112120
f.StopSpinner()
113121
internal.PrintOutputJSON(o)

cmd/curl_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func TestInitCurlCmd(t *testing.T) {
2626
gotexp func() (interface{}, interface{})
2727
}{
2828
// Common flags
29+
"file": {[]string{"--file", "file.txt"}, func() (interface{}, interface{}) { return fileOut, "file.txt" }},
2930
"from": {[]string{"--from", "Europe"}, func() (interface{}, interface{}) { return from, "Europe" }},
3031
"nodeid": {[]string{"--nodeid", "1,2,3"}, func() (interface{}, interface{}) { return nodeIDs, []int{1, 2, 3} }},
3132
"json": {[]string{"--json"}, func() (interface{}, interface{}) { return outputJSON, true }},
@@ -49,7 +50,7 @@ func TestInitCurlCmd(t *testing.T) {
4950
t.Fatal("expected flag; got nil")
5051
}
5152

52-
got, exp := tc.gotexp();
53+
got, exp := tc.gotexp()
5354
if reflect.DeepEqual(got, exp) == false {
5455
t.Fatalf("expected %v; got %v", exp, got)
5556
}
@@ -81,7 +82,7 @@ func TestRunCurlResolve(t *testing.T) {
8182
}
8283
for name, tc := range testCases {
8384
t.Run(name, func(t *testing.T) {
84-
runCurl(c, "example.com", tc.head, tc.insecure, tc.http2, tc.from, tc.nodeIDs, 12)
85+
runCurl(c, "example.com", tc.head, tc.insecure, tc.http2, tc.from, tc.nodeIDs, 12, "file.txt")
8586
if got, exp := tr.req.URL.Path, "/run/curl"; got != exp {
8687
t.Fatalf("expected %v; got %v", exp, got)
8788
}

cmd/internal/runtest.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
package internal
1515

1616
import (
17+
"bufio"
1718
"bytes"
1819
"context"
1920
"encoding/json"
2021
"fmt"
2122
"io"
23+
"os"
2224
"strings"
2325
"sync"
2426
"time"
@@ -55,7 +57,7 @@ type (
5557
runOutputFunc func(ctx context.Context, pingID perfops.TestID) (*perfops.RunOutput, error)
5658
)
5759

58-
// RunTest runs an MTR or ping testm retrives its output and presents it to the user.
60+
// RunTest runs an MTR or ping test retrieves its output and presents it to the user.
5961
func RunTest(ctx context.Context, target, location string, nodeIDs []int, limit int, debug, outputJSON bool, runTest runFunc, runOutput runOutputFunc) error {
6062
runReq := &perfops.RunRequest{
6163
Target: target,
@@ -110,6 +112,98 @@ func RunTest(ctx context.Context, target, location string, nodeIDs []int, limit
110112
return nil
111113
}
112114

115+
// formatFileName Returns a file name based on provided string and number
116+
func formatFileName(name string, index int) string {
117+
split := strings.Split(name, ".")
118+
119+
n := split[len(split)-2]
120+
split[len(split)-2] = fmt.Sprintf("%v-%x", n, index+1)
121+
122+
return strings.Join(split, ".")
123+
124+
}
125+
126+
// OutputToFile Outputs perfops response to a file or multiple files if more than one check is being ran.
127+
func OutputToFile(f *Formatter, output *perfops.RunOutput, fileOut string) {
128+
if f.printID {
129+
f.Printf("Test ID: %v\n", output.ID)
130+
}
131+
spinner := f.s.Step()
132+
if !output.IsFinished() {
133+
f.Printf("%s", spinner)
134+
if len(output.Items) > 1 {
135+
finished := 0
136+
for _, item := range output.Items {
137+
if item.Result.IsFinished() {
138+
finished++
139+
}
140+
}
141+
f.Printf(" %d/%d", finished, len(output.Items))
142+
}
143+
f.Printf("\n")
144+
}
145+
for i, item := range output.Items {
146+
r := item.Result
147+
n := r.Node
148+
if item.Result.Message == "" {
149+
o := r.Output
150+
if o == "-2" {
151+
o = "The command timed-out. It either took too long to execute or we could not connect to your target at all."
152+
} else if a, ok := o.([]interface{}); ok {
153+
sb := strings.Builder{}
154+
for _, i := range a {
155+
sb.WriteString(fmt.Sprintf("%s\n", i))
156+
}
157+
s := sb.String()
158+
o = s[:len(s)-1]
159+
}
160+
161+
fileName := ""
162+
163+
if len(output.Items) > 1 {
164+
fileName = formatFileName(fileOut, i)
165+
} else {
166+
fileName = fileOut
167+
}
168+
169+
file, _ := os.Create(fileName)
170+
defer file.Close()
171+
172+
w := bufio.NewWriter(file)
173+
174+
fmt.Fprintf(w, "Node%d, AS%d, %s, %s\n%s\n", n.ID, n.AsNumber, n.City, n.Country.Name, o)
175+
176+
err := w.Flush()
177+
178+
if err != nil {
179+
return
180+
}
181+
182+
} else if r.Message != "NO DATA" {
183+
fileName := ""
184+
185+
if len(output.Items) > 1 {
186+
fileName = formatFileName(fileOut, i)
187+
} else {
188+
fileName = fileOut
189+
}
190+
191+
file, _ := os.Create(fileName)
192+
defer file.Close()
193+
194+
w := bufio.NewWriter(file)
195+
196+
fmt.Fprintf(w, "Node%d, AS%d, %s, %s\n%s\n", n.ID, n.AsNumber, n.City, n.Country.Name, r.Message)
197+
198+
w.Flush()
199+
}
200+
if !item.Result.IsFinished() {
201+
f.Printf("%s\n", spinner)
202+
}
203+
}
204+
f.Flush(!output.IsFinished())
205+
}
206+
113207
// PrintOutput prints run items that have been data.
114208
func PrintOutput(f *Formatter, output *perfops.RunOutput) {
115209
if f.printID {
@@ -144,6 +238,9 @@ func PrintOutput(f *Formatter, output *perfops.RunOutput) {
144238
s := sb.String()
145239
o = s[:len(s)-1]
146240
}
241+
if len(output.Items) > 1 {
242+
fmt.Println("Greater Than One")
243+
}
147244
f.Printf("Node%d, AS%d, %s, %s\n%s\n", n.ID, n.AsNumber, n.City, n.Country.Name, o)
148245
} else if r.Message != "NO DATA" {
149246
f.Printf("Node%d, AS%d, %s, %s\n%s\n", n.ID, n.AsNumber, n.City, n.Country.Name, r.Message)

cmd/internal/runtest_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/json"
2020
"errors"
2121
"io"
22+
"os"
2223
"testing"
2324

2425
"github.com/ProspectOne/perfops-cli/perfops"
@@ -60,6 +61,7 @@ func TestRunTest(t *testing.T) {
6061
nil,
6162
},
6263
}
64+
6365
ctx := context.Background()
6466
for name, tc := range testCases {
6567
t.Run(name, func(t *testing.T) {
@@ -116,6 +118,52 @@ func TestPrintOutput(t *testing.T) {
116118
}
117119
}
118120

121+
func TestOutputToFile(t *testing.T) {
122+
testCases := map[string]struct {
123+
output func() *perfops.RunOutput
124+
exp []byte
125+
}{
126+
"all": {
127+
func() *perfops.RunOutput {
128+
var o *perfops.RunOutput
129+
json.Unmarshal([]byte(`{"id":"706fc55e3377104da01f05569e35a30b","items":[{"id":"bba072473bd034d432df03730e116686","result":{"output":"121","finished": true,"node":{"id":27,"as_number":12345,"latitude":22.28512548314,"longitude":114.17507171631,"country":{"id":195,"name":"Hong Kong","continent":{"id":2,"name":"Asia","iso":"AS"},"iso":"HK","iso_numeric":"344"},"city":"Hong Kong","sub_region":"Eastern Asia"},"time":1508061924.088372}}],"requested":"sendergram.com","finished":true,"elapsedTime":0.66500000000000004}`), &o)
130+
return o
131+
},
132+
[]byte("\x1b[200DNode27, AS12345, Hong Kong, Hong Kong\n121\n"),
133+
},
134+
"timeout": {
135+
func() *perfops.RunOutput {
136+
var o *perfops.RunOutput
137+
json.Unmarshal([]byte(`{"id":"706fc55e3377104da01f05569e35a30b","items":[{"id":"bba072473bd034d432df03730e116686","result":{"output":"-2","finished": true,"node":{"id":27,"as_number":23456,"latitude":22.28512548314,"longitude":114.17507171631,"country":{"id":195,"name":"Hong Kong","continent":{"id":2,"name":"Asia","iso":"AS"},"iso":"HK","iso_numeric":"344"},"city":"Hong Kong","sub_region":"Eastern Asia"},"time":1508061924.088372}}],"requested":"sendergram.com","finished":true,"elapsedTime":0.66500000000000004}`), &o)
138+
return o
139+
},
140+
[]byte("\x1b[200DNode27, AS23456, Hong Kong, Hong Kong\nThe command timed-out. It either took too long to execute or we could not connect to your target at all.\n"),
141+
},
142+
"array output": {
143+
func() *perfops.RunOutput {
144+
var o *perfops.RunOutput
145+
json.Unmarshal([]byte(`{"id":"6e0c06f7445bb8c63949f84fcdbdae55","items":[{"id":"2bff5d6b3a4df8a268afca6c60977032","result":{"dnsServer":"","node":{"as_number":197328,"id":103,"latitude":41.030549854339,"longitude":28.987083435058,"country":{"id":93,"name":"Turkey","continent":{"id":2,"name":"Asia","iso":"AS"},"iso":"TR","iso_numeric":"792","is_eu":false},"city":"Istanbul","sub_region":"Western Asia"},"finished":true,"output":["header"," 1 row", " 10 row"],"time":1539517079.937741}}],"requested":"ns2.no-ip.com","finished":true,"elapsedTime":2.21,"creditsWithdrawn":1}`), &o)
146+
return o
147+
},
148+
[]byte("\x1b[200DNode103, AS197328, Istanbul, Turkey\nheader\n 1 row\n 10 row\n"),
149+
},
150+
}
151+
var b bytes.Buffer
152+
f := newTestFormatter(&b, false)
153+
for name, tc := range testCases {
154+
t.Run(name, func(t *testing.T) {
155+
OutputToFile(f, tc.output(), "test.txt")
156+
if _, err := os.Stat("test.txt"); err != nil {
157+
t.Error("File was not created")
158+
}
159+
err := os.Remove("test.txt")
160+
if err != nil {
161+
return
162+
}
163+
})
164+
}
165+
}
166+
119167
type testTerminalWriter struct {
120168
io.Writer
121169
}

go.mod

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/ProspectOne/perfops-cli
2+
3+
go 1.19
4+
5+
require (
6+
github.com/gosuri/uilive v0.0.3
7+
github.com/spf13/cobra v0.0.5
8+
github.com/spf13/pflag v1.0.5
9+
)
10+
11+
require (
12+
github.com/inconshreveable/mousetrap v1.0.0 // indirect
13+
github.com/mattn/go-isatty v0.0.9 // indirect
14+
golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed // indirect
15+
)

0 commit comments

Comments
 (0)