Skip to content

Commit

Permalink
feat: add ci flag and make output pipeable (#23)
Browse files Browse the repository at this point in the history
* feat: add ci context flag

* feat: make output redirect friendly

* refactor: remove unnecessary datasetup func

* test: resolve CI error
  • Loading branch information
ayuhito committed Feb 23, 2023
1 parent 212dc98 commit 5ae5eb2
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 93 deletions.
213 changes: 123 additions & 90 deletions client/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,6 @@ var (
bold = lipgloss.NewStyle().Bold(true)
)

func dataSetup(id string) (model.GetMeasurement, error) {
// Get results
data, err := GetAPI(id)
if err != nil {
return data, err
}

// Probe may not have started yet
for len(data.Results) == 0 {
time.Sleep(100 * time.Millisecond)
data, err = GetAPI(id)
if err != nil {
return data, err
}
}

return data, nil
}

// Used to slice the output to fit the terminal in live view
func sliceOutput(output string, w, h int) string {
// Split output into lines
Expand All @@ -63,24 +44,29 @@ func sliceOutput(output string, w, h int) string {
return strings.Join(lines, "\n")
}

func generateHeader(result model.MeasurementResponse) string {
var output strings.Builder
if result.Probe.State != "" {
output.WriteString(arrow + highlight.Render(fmt.Sprintf("%s, %s, (%s), %s, ASN:%d", result.Probe.Continent, result.Probe.Country, result.Probe.State, result.Probe.City, result.Probe.ASN)))
// Generate header that also checks if the probe has a state in it in the form %s, %s, (%s), %s, ASN:%d
func generateHeader(result model.MeasurementResponse, ctx model.Context) string {
baseFormat := "%s, %s, %s, ASN:%d"
stateFormat := "%s, %s, (%s), %s, ASN:%d"

// String builder for output
if ctx.CI {
if result.Probe.State != "" {
return "> " + fmt.Sprintf(stateFormat, result.Probe.Continent, result.Probe.Country, result.Probe.State, result.Probe.City, result.Probe.ASN)
} else {
return "> " + fmt.Sprintf(baseFormat, result.Probe.Continent, result.Probe.Country, result.Probe.City, result.Probe.ASN)
}
} else {
output.WriteString(arrow + highlight.Render(fmt.Sprintf("%s, %s, %s, ASN:%d", result.Probe.Continent, result.Probe.Country, result.Probe.City, result.Probe.ASN)))
if result.Probe.State != "" {
return arrow + highlight.Render(fmt.Sprintf(stateFormat, result.Probe.Continent, result.Probe.Country, result.Probe.State, result.Probe.City, result.Probe.ASN))
} else {
return arrow + highlight.Render(fmt.Sprintf(baseFormat, result.Probe.Continent, result.Probe.Country, result.Probe.City, result.Probe.ASN))
}
}

return output.String()
}

func LiveView(id string, ctx model.Context) {
// Get results
data, err := dataSetup(id)
if err != nil {
fmt.Println(err)
return
}
func LiveView(id string, data model.GetMeasurement, ctx model.Context) {
var err error

// Create new writer
writer, _ := pterm.DefaultArea.Start()
Expand All @@ -100,7 +86,7 @@ func LiveView(id string, ctx model.Context) {
// Output every result in case of multiple probes
for _, result := range data.Results {
// Output slightly different format if state is available
output.WriteString(generateHeader(result) + "\n")
output.WriteString(generateHeader(result, ctx) + "\n")

// Output only latency values if flag is set
output.WriteString(strings.TrimSpace(result.Result.RawOutput) + "\n\n")
Expand All @@ -124,44 +110,115 @@ func LiveView(id string, ctx model.Context) {

// If json flag is used, only output json
func OutputJson(id string) {
// Get results
data, err := dataSetup(id)
output, err := GetApiJson(id)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(output)
}

for data.Status == "in-progress" {
time.Sleep(100 * time.Millisecond)
data, err = GetAPI(id)
if err != nil {
fmt.Println(err)
return
// If latency flag is used, only output latency values
func OutputLatency(id string, data model.GetMeasurement, ctx model.Context) {
// String builder for output
var output strings.Builder

// Output every result in case of multiple probes
for _, result := range data.Results {
// Output slightly different format if state is available
output.WriteString(generateHeader(result, ctx) + "\n")

if ctx.CI {
if ctx.Cmd == "ping" {
output.WriteString(fmt.Sprintf("Min: %v ms\n", result.Result.Stats["min"]))
output.WriteString(fmt.Sprintf("Max: %v ms\n", result.Result.Stats["max"]))
output.WriteString(fmt.Sprintf("Avg: %v ms\n\n", result.Result.Stats["avg"]))
}

if ctx.Cmd == "dns" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(fmt.Sprintf("Total: %v ms\n", timings.Interface["total"]))
}

if ctx.Cmd == "http" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(fmt.Sprintf("Total: %v ms\n", timings.Interface["total"]))
output.WriteString(fmt.Sprintf("Download: %v ms\n", timings.Interface["download"]))
output.WriteString(fmt.Sprintf("First byte: %v ms\n", timings.Interface["firstByte"]))
output.WriteString(fmt.Sprintf("DNS: %v ms\n", timings.Interface["dns"]))
output.WriteString(fmt.Sprintf("TLS: %v ms\n", timings.Interface["tls"]))
output.WriteString(fmt.Sprintf("TCP: %v ms\n", timings.Interface["tcp"]))
}
} else {
if ctx.Cmd == "ping" {
output.WriteString(bold.Render("Min: ") + fmt.Sprintf("%v ms\n", result.Result.Stats["min"]))
output.WriteString(bold.Render("Max: ") + fmt.Sprintf("%v ms\n", result.Result.Stats["max"]))
output.WriteString(bold.Render("Avg: ") + fmt.Sprintf("%v ms\n\n", result.Result.Stats["avg"]))
}

if ctx.Cmd == "dns" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(bold.Render("Total: ") + fmt.Sprintf("%v ms\n", timings.Interface["total"]))
}

if ctx.Cmd == "http" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(bold.Render("Total: ") + fmt.Sprintf("%v ms\n", timings.Interface["total"]))
output.WriteString(bold.Render("Download: ") + fmt.Sprintf("%v ms\n", timings.Interface["download"]))
output.WriteString(bold.Render("First byte: ") + fmt.Sprintf("%v ms\n", timings.Interface["firstByte"]))
output.WriteString(bold.Render("DNS: ") + fmt.Sprintf("%v ms\n", timings.Interface["dns"]))
output.WriteString(bold.Render("TLS: ") + fmt.Sprintf("%v ms\n", timings.Interface["tls"]))
output.WriteString(bold.Render("TCP: ") + fmt.Sprintf("%v ms\n", timings.Interface["tcp"]))
}
}

}

output, err := GetApiJson(id)
if err != nil {
fmt.Println(err)
return
fmt.Println(strings.TrimSpace(output.String()))
}

func OutputCI(id string, data model.GetMeasurement, ctx model.Context) {
// String builder for output
var output strings.Builder

// Output every result in case of multiple probes
for _, result := range data.Results {
// Output slightly different format if state is available
output.WriteString(generateHeader(result, ctx) + "\n")

// Output only latency values if flag is set
output.WriteString(strings.TrimSpace(result.Result.RawOutput) + "\n\n")
}
fmt.Println(output)

fmt.Println(strings.TrimSpace(output.String()))
}

// If latency flag is used, only output latency values
func OutputLatency(id string, ctx model.Context) {
// Get results
data, err := dataSetup(id)
func OutputResults(id string, ctx model.Context) {
// Wait for first result to arrive from a probe before starting display (can be in-progress)
data, err := GetAPI(id)
if err != nil {
fmt.Println(err)
return
}

// String builder for output
var output strings.Builder

// Poll API every 100 milliseconds until the measurement is complete
for data.Status == "in-progress" {
// Probe may not have started yet
for len(data.Results) == 0 {
time.Sleep(100 * time.Millisecond)
data, err = GetAPI(id)
if err != nil {
Expand All @@ -170,54 +227,30 @@ func OutputLatency(id string, ctx model.Context) {
}
}

// Output every result in case of multiple probes
for _, result := range data.Results {
// Output slightly different format if state is available
output.WriteString(generateHeader(result) + "\n")

if ctx.Cmd == "ping" {
output.WriteString(bold.Render("Min: ") + fmt.Sprintf("%v ms\n", result.Result.Stats["min"]))
output.WriteString(bold.Render("Max: ") + fmt.Sprintf("%v ms\n", result.Result.Stats["max"]))
output.WriteString(bold.Render("Avg: ") + fmt.Sprintf("%v ms\n\n", result.Result.Stats["avg"]))
}

if ctx.Cmd == "dns" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(bold.Render("Total: ") + fmt.Sprintf("%v ms\n", timings.Interface["total"]))
}

if ctx.Cmd == "http" {
timings, err := DecodeTimings(ctx.Cmd, result.Result.TimingsRaw)
if ctx.CI || ctx.JsonOutput || ctx.Latency {
// Poll API every 100 milliseconds until the measurement is complete
for data.Status == "in-progress" {
time.Sleep(100 * time.Millisecond)
data, err = GetAPI(id)
if err != nil {
fmt.Println(err)
return
}
output.WriteString(bold.Render("Total: ") + fmt.Sprintf("%v ms\n", timings.Interface["total"]))
output.WriteString(bold.Render("Download: ") + fmt.Sprintf("%v ms\n", timings.Interface["download"]))
output.WriteString(bold.Render("First byte: ") + fmt.Sprintf("%v ms\n", timings.Interface["firstByte"]))
output.WriteString(bold.Render("DNS: ") + fmt.Sprintf("%v ms\n", timings.Interface["dns"]))
output.WriteString(bold.Render("TLS: ") + fmt.Sprintf("%v ms\n", timings.Interface["tls"]))
output.WriteString(bold.Render("TCP: ") + fmt.Sprintf("%v ms\n", timings.Interface["tcp"]))
}
}

fmt.Println(strings.TrimSpace(output.String()))
}

func OutputResults(id string, ctx model.Context) {
switch {
case ctx.JsonOutput:
OutputJson(id)
return
case ctx.Latency:
OutputLatency(id, ctx)
OutputLatency(id, data, ctx)
return
case ctx.CI:
OutputCI(id, data, ctx)
return
default:
LiveView(id, ctx)
LiveView(id, data, ctx)
return
}
}
2 changes: 0 additions & 2 deletions cmd/http_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -20,7 +19,6 @@ func TestHttpCmd(t *testing.T) {

func testParseUrl(t *testing.T) {
flags, _ := parseURL("https://cdn.jsdelivr.net:8080/npm/react/?query=3")
fmt.Printf("%+v", flags)
assert.Equal(t, "/npm/react/", flags.Path)
assert.Equal(t, "cdn.jsdelivr.net", flags.Host)
assert.Equal(t, "https", flags.Protocol)
Expand Down
16 changes: 15 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&ctx.From, "from", "F", "", "A continent, region (e.g eastern europe), country, US state or city (default \"world\")")
rootCmd.PersistentFlags().IntVarP(&ctx.Limit, "limit", "L", 1, "Limit the number of probes to use")
rootCmd.PersistentFlags().BoolVarP(&ctx.JsonOutput, "json", "J", false, "Output results in JSON format (default false)")
rootCmd.PersistentFlags().BoolVarP(&ctx.CI, "ci", "C", false, "Disable realtime terminal updates and color suitable for CI (default false)")
}

// checkCommandFormat checks if the command is in the correct format if using the from arg
Expand All @@ -69,10 +70,10 @@ func checkCommandFormat() cobra.PositionalArgs {
func createContext(cmd string, args []string) error {
ctx.Cmd = cmd // Get the command name

// Target
if len(args) == 0 {
return errors.New("provided target is empty")
}

ctx.Target = args[0]

// If no from arg is provided, use the default value
Expand All @@ -84,6 +85,19 @@ func createContext(cmd string, args []string) error {
if len(args) > 1 && args[1] == "from" {
ctx.From = strings.TrimSpace(strings.Join(args[2:], " "))
}

// Check env for CI
if os.Getenv("CI") != "" {
ctx.CI = true
}

// Check if it is a terminal or being piped/redirected
// We want to disable realtime updates if that is the case
o, _ := os.Stdout.Stat()
if (o.Mode() & os.ModeCharDevice) != os.ModeCharDevice {
ctx.CI = true
}

return nil
}

Expand Down
11 changes: 11 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func TestCreateContext(t *testing.T) {
"country": testContextCountry,
"country_whitespace": testContextCountryWhitespace,
"no_target": testContextNoTarget,
"ci_env": testContextCIEnv,
} {
t.Run(scenario, func(t *testing.T) {
ctx = model.Context{}
Expand Down Expand Up @@ -79,3 +80,13 @@ func testContextNoTarget(t *testing.T) {
err := createContext("test", []string{})
assert.Error(t, err)
}

func testContextCIEnv(t *testing.T) {
t.Setenv("CI", "true")
err := createContext("test", []string{"1.1.1.1"})
assert.Equal(t, "test", ctx.Cmd)
assert.Equal(t, "1.1.1.1", ctx.Target)
assert.Equal(t, "world", ctx.From)
assert.True(t, ctx.CI)
assert.NoError(t, err)
}
2 changes: 2 additions & 0 deletions model/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ type Context struct {
JsonOutput bool
// Latency is a flag that outputs only stats of a measurement
Latency bool
// CI flag is used to determine whether the output should be in a format that is easy to parse by a CI tool
CI bool
}

0 comments on commit 5ae5eb2

Please sign in to comment.