Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions cmd/install_probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ Please confirm to pull and run our Docker container (globalping/globalping-probe
`, w.String())

expectedCtx := &view.Context{
History: view.NewHistoryBuffer(1),
From: "world",
Limit: 1,
RunSessionStartedAt: defaultCurrentTime,
History: view.NewHistoryBuffer(1),
From: "world",
Limit: 1,
}
assert.Equal(t, expectedCtx, ctx)
}
Expand Down
28 changes: 23 additions & 5 deletions cmd/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,12 @@ func (r *Root) pingInfinite(ctx context.Context, opts *globalping.MeasurementCre
done := make(chan struct{})
var infiniteTableOutput string
var err error

run := view.NewInfinitePingRun(r.ctx.Protocol, r.ctx.Packets, r.utils.Now(), r.utils.Now)

go func() {
defer close(done)
infiniteTableOutput, err = r.ping(runCtx, opts)
infiniteTableOutput, err = r.runInfinitePing(runCtx, opts, run)
}()

select {
Expand All @@ -159,7 +162,7 @@ func (r *Root) pingInfinite(ctx context.Context, opts *globalping.MeasurementCre
}

if err == nil && !r.ctx.ToLatency {
r.viewer.OutputSummary(infiniteTableOutput)
r.viewer.OutputPingSummary(infiniteTableOutput, run.Summary())
}

if errors.Is(err, view.ErrAllProbesFailed) {
Expand All @@ -172,12 +175,11 @@ func (r *Root) pingInfinite(ctx context.Context, opts *globalping.MeasurementCre
return err
}

func (r *Root) ping(ctx context.Context, opts *globalping.MeasurementCreate) (string, error) {
func (r *Root) runInfinitePing(ctx context.Context, opts *globalping.MeasurementCreate, run *view.InfinitePingRun) (string, error) {
var infiniteTableOutput string
var runErr error
mbuf := NewMeasurementsBuffer(10) // 10 is the maximum number of measurements that can be in progress at the same time
awaitStates := map[string]*measurementAwaitState{}
r.ctx.RunSessionStartedAt = r.utils.Now()

for {
if err := ctx.Err(); err != nil {
Expand Down Expand Up @@ -220,7 +222,21 @@ func (r *Root) ping(ctx context.Context, opts *globalping.MeasurementCreate) (st
continue
}

infiniteTableOutput, err = r.viewer.OutputInfinite(measurement)
streamRawOutput := len(measurement.Results) == 1 && !r.ctx.Table && !r.ctx.ToLatency
output, err := run.Update(measurement, streamRawOutput, r.ctx.MeasurementsCreated)

if err != nil {
r.Cmd.SilenceUsage = true

return infiniteTableOutput, err
}

infiniteTableOutput, err = r.viewer.OutputInfinite(output)

// Do not let ErrAllProbesFailed mask the earlier creation error
if runErr != nil && errors.Is(err, view.ErrAllProbesFailed) {
err = nil
}

if err != nil {
r.Cmd.SilenceUsage = true
Expand Down Expand Up @@ -251,6 +267,7 @@ func (r *Root) ping(ctx context.Context, opts *globalping.MeasurementCreate) (st
if err != nil {
runErr = err // Return the error after all measurements have finished
} else {
run.Add(hm.Id, hm.StartedAt)
mbuf.Append(hm)
}

Expand Down Expand Up @@ -300,6 +317,7 @@ func (r *Root) ping(ctx context.Context, opts *globalping.MeasurementCreate) (st
return infiniteTableOutput, err
}

run.Add(hm.Id, hm.StartedAt)
mbuf.Append(hm)
}
}
Expand Down
86 changes: 67 additions & 19 deletions cmd/ping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import (
"go.uber.org/mock/gomock"
)

func pingOutputFor(measurement *globalping.Measurement) gomock.Matcher {
return gomock.Cond(func(value any) bool {
output, ok := value.(*view.InfinitePingOutput)

return ok && output.Measurement == measurement
})
}

func Test_Execute_Ping_Default(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down Expand Up @@ -372,22 +380,22 @@ func Test_Execute_Ping_Infinite(t *testing.T) {
})

viewerMock := viewMocks.NewMockViewer(ctrl)
waitFn := func(_ *globalping.Measurement) (string, error) { time.Sleep(5 * time.Millisecond); return "", nil }
viewerMock.EXPECT().OutputInfinite(expectedMeasurement1).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement2).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement3).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement4).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement2).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement3).DoAndReturn(waitFn)
waitFn := func(_ *view.InfinitePingOutput) (string, error) { time.Sleep(5 * time.Millisecond); return "", nil }
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement1)).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement2)).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement3)).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement4)).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement2)).DoAndReturn(waitFn)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement3)).DoAndReturn(waitFn)
finalOutputStarted := make(chan struct{})
viewerMock.EXPECT().OutputInfinite(expectedMeasurement4).DoAndReturn(func(_ *globalping.Measurement) (string, error) {
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement4)).DoAndReturn(func(_ *view.InfinitePingOutput) (string, error) {
close(finalOutputStarted)
<-finalRunCtx.Done()

return "", nil
})

viewerMock.EXPECT().OutputSummary("").Times(1)
viewerMock.EXPECT().OutputPingSummary("", gomock.Any()).Times(1)
viewerMock.EXPECT().OutputShare().Times(1)

utilsMock := utilsMocks.NewMockUtils(ctrl)
Expand Down Expand Up @@ -426,7 +434,6 @@ func Test_Execute_Ping_Infinite(t *testing.T) {
Protocol: "ICMP",
Port: 80,
MeasurementsCreated: 4,
RunSessionStartedAt: defaultCurrentTime,
}
expectedCtx.History = &view.HistoryBuffer{
Index: 4,
Expand Down Expand Up @@ -497,6 +504,7 @@ func Test_Execute_Ping_Infinite_TableInCI(t *testing.T) {

expectedResponse := createDefaultMeasurementCreateResponse()
expectedMeasurement := createDefaultMeasurement("ping")
expectedMeasurement.Results[0].Result.StatsRaw = []byte(`{"total":0,"rcv":0,"drop":0,"loss":0}`)

gbMock := apiMocks.NewMockClient(ctrl)
gbMock.EXPECT().CreateMeasurement(gomock.Any(), expectedOpts).Return(expectedResponse, nil)
Expand All @@ -509,13 +517,13 @@ func Test_Execute_Ping_Infinite_TableInCI(t *testing.T) {

outputStarted := make(chan struct{})
viewerMock := viewMocks.NewMockViewer(ctrl)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement).DoAndReturn(func(*globalping.Measurement) (string, error) {
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement)).DoAndReturn(func(*view.InfinitePingOutput) (string, error) {
close(outputStarted)
<-runCtx.Done()

return "final table", nil
})
viewerMock.EXPECT().OutputSummary("final table").Times(1)
viewerMock.EXPECT().OutputPingSummary("final table", gomock.Any()).Times(1)
viewerMock.EXPECT().OutputShare().Times(1)

utilsMock := utilsMocks.NewMockUtils(ctrl)
Expand Down Expand Up @@ -558,8 +566,8 @@ func Test_Execute_Ping_Infinite_Output_Error(t *testing.T) {
gbMock.EXPECT().GetMeasurement(gomock.Any(), measurementID1).Return(expectedMeasurement, nil)

viewerMock := viewMocks.NewMockViewer(ctrl)
viewerMock.EXPECT().OutputInfinite(expectedMeasurement).Return("", errors.New("error message"))
viewerMock.EXPECT().OutputSummary(gomock.Any()).Times(0)
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement)).Return("", errors.New("error message"))
viewerMock.EXPECT().OutputPingSummary(gomock.Any(), gomock.Any()).Times(0)
viewerMock.EXPECT().OutputShare().Times(1)

utilsMock := utilsMocks.NewMockUtils(ctrl)
Expand Down Expand Up @@ -607,8 +615,8 @@ func Test_Execute_Ping_Infinite_AllProbesFailedExitsSuccessfully(t *testing.T) {
measurement := createDefaultMeasurement("ping")
client.EXPECT().GetMeasurement(gomock.Any(), measurementID1).Return(measurement, nil)
viewer := viewMocks.NewMockViewer(ctrl)
viewer.EXPECT().OutputInfinite(measurement).Return("failed output", view.ErrAllProbesFailed)
viewer.EXPECT().OutputSummary(gomock.Any()).Times(0)
viewer.EXPECT().OutputInfinite(pingOutputFor(measurement)).Return("failed output", view.ErrAllProbesFailed)
viewer.EXPECT().OutputPingSummary(gomock.Any(), gomock.Any()).Times(0)
viewer.EXPECT().OutputShare()
utils := utilsMocks.NewMockUtils(ctrl)
utils.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()
Expand Down Expand Up @@ -652,10 +660,10 @@ func Test_Execute_Ping_Infinite_Output_TooManyRequests_Error(t *testing.T) {
gbMock.EXPECT().GetMeasurement(gomock.Any(), measurementID1).Return(expectedMeasurement, nil)

viewerMock := viewMocks.NewMockViewer(ctrl)
waitFn := func(_ *globalping.Measurement) (string, error) { time.Sleep(5 * time.Millisecond); return "", nil }
viewerMock.EXPECT().OutputInfinite(expectedMeasurement).DoAndReturn(waitFn)
waitFn := func(_ *view.InfinitePingOutput) (string, error) { time.Sleep(5 * time.Millisecond); return "", nil }
viewerMock.EXPECT().OutputInfinite(pingOutputFor(expectedMeasurement)).DoAndReturn(waitFn)

viewerMock.EXPECT().OutputSummary(gomock.Any()).Times(0)
viewerMock.EXPECT().OutputPingSummary(gomock.Any(), gomock.Any()).Times(0)
viewerMock.EXPECT().OutputShare().Times(1)

utilsMock := utilsMocks.NewMockUtils(ctrl)
Expand Down Expand Up @@ -696,6 +704,46 @@ func Test_Execute_Ping_Infinite_Output_TooManyRequests_Error(t *testing.T) {
assert.Equal(t, expectedHistoryItems, items)
}

func Test_Ping_InfiniteCreationFailureFinishesPendingMeasurement(t *testing.T) {
for _, finalStatus := range []globalping.TestStatus{globalping.TestStatusFinished, globalping.TestStatusFailed} {
t.Run(string(finalStatus), func(t *testing.T) {
ctrl := gomock.NewController(t)
firstResponse := createDefaultMeasurementCreateResponse()
measurement := createDefaultMeasurement_MultipleProbes(globalping.MeasurementStatusInProgress, globalping.TestStatusInProgress)
measurement.Results[0].Result.Status = globalping.TestStatusFinished

client := apiMocks.NewMockClient(ctrl)
firstCreate := client.EXPECT().CreateMeasurement(gomock.Any(), gomock.Any()).Return(firstResponse, nil)
firstPoll := client.EXPECT().GetMeasurement(gomock.Any(), firstResponse.ID).Return(measurement, nil).After(firstCreate)
viewer := viewMocks.NewMockViewer(ctrl)
firstOutput := viewer.EXPECT().OutputInfinite(pingOutputFor(measurement)).Return("current output", nil).After(firstPoll)
failedCreate := client.EXPECT().CreateMeasurement(gomock.Any(), gomock.Any()).Return(nil, assert.AnError).After(firstOutput)
completed := createDefaultMeasurement_MultipleProbes(globalping.MeasurementStatusFinished, finalStatus)
var outputErr error

if finalStatus == globalping.TestStatusFailed {
outputErr = view.ErrAllProbesFailed
}

finalPoll := client.EXPECT().GetMeasurement(gomock.Any(), firstResponse.ID).Return(completed, nil).After(failedCreate)
viewer.EXPECT().OutputInfinite(pingOutputFor(completed)).Return("completed output", outputErr).After(finalPoll)
utilsMock := utilsMocks.NewMockUtils(ctrl)
utilsMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()
ctx := createDefaultContext()
ctx.Infinite = true
ctx.Packets = 16
root := NewRoot(view.NewPrinter(nil, new(bytes.Buffer), new(bytes.Buffer)), ctx, viewer, utilsMock, client, nil, nil)
opts := createDefaultMeasurementCreate("ping")

output, err := root.runInfinitePing(t.Context(), opts, view.NewInfinitePingRun(ctx.Protocol, ctx.Packets, utilsMock.Now(), utilsMock.Now))

assert.ErrorIs(t, err, assert.AnError)
assert.Equal(t, "completed output", output)
assert.Equal(t, globalping.MeasurementStatusFinished, ctx.History.Find(firstResponse.ID).Status)
})
}
}

func Test_Execute_Ping_IPv4(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func Execute() {
})

globalpingProbe := probe.NewProbe()
viewer := view.NewViewer(ctx, printer, _utils)
viewer := view.NewViewer(ctx, printer)
root := NewRoot(printer, ctx, viewer, _utils, apiClient, globalpingProbe, localStorage)

err := root.Cmd.Execute()
Expand Down
102 changes: 58 additions & 44 deletions cmd/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,49 +391,58 @@ func Test_TableFlag_IsAvailableOnlyForMeasurementCommands(t *testing.T) {

func Test_Execute_TableMeasurement(t *testing.T) {
for _, measurementType := range []globalping.MeasurementType{"ping", "traceroute", "mtr", "dns", "http"} {
t.Run(string(measurementType), func(t *testing.T) {
ctrl := gomock.NewController(t)
expectedOpts := createDefaultMeasurementCreate(measurementType)
locations, ok := expectedOpts.Locations.(globalping.LocationOptions)
require.True(t, ok)
locations[0].Magic = "world"

switch measurementType {
case "dns":
expectedOpts.Options.Query = &globalping.QueryOptions{}
case "http":
expectedOpts.Options.Request = &globalping.RequestOptions{Headers: map[string]string{}}
}

expectedResponse := createDefaultMeasurementCreateResponse()
expectedMeasurement := createDefaultMeasurement(measurementType)
gbMock := apiMocks.NewMockClient(ctrl)
gbMock.EXPECT().CreateMeasurement(t.Context(), expectedOpts).Return(expectedResponse, nil)
gbMock.EXPECT().GetMeasurement(t.Context(), expectedResponse.ID).Return(expectedMeasurement, nil)

viewerMock := viewMocks.NewMockViewer(ctrl)
viewerMock.EXPECT().OutputTable(expectedMeasurement).Return("", nil)
viewerMock.EXPECT().OutputShare()

utilsMock := utilsMocks.NewMockUtils(ctrl)
utilsMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()
for _, status := range []globalping.TestStatus{globalping.TestStatusFinished, globalping.TestStatusFailed} {
name := string(measurementType) + "/" + string(status)

t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
expectedOpts := createDefaultMeasurementCreate(measurementType)
locations, ok := expectedOpts.Locations.(globalping.LocationOptions)
require.True(t, ok)
locations[0].Magic = "world"

switch measurementType {
case "dns":
expectedOpts.Options.Query = &globalping.QueryOptions{}
case "http":
expectedOpts.Options.Request = &globalping.RequestOptions{Headers: map[string]string{}}
}

w := new(bytes.Buffer)
ctx := createDefaultContext()
storage := createDefaultTestStorage(t, utilsMock)
root := NewRoot(view.NewPrinter(nil, w, w), ctx, viewerMock, utilsMock, gbMock, nil, storage)
oldArgs := os.Args
t.Cleanup(func() { os.Args = oldArgs })
os.Args = []string{"globalping", string(measurementType), "jsdelivr.com", "--table", "--latency", "--json", "--ci"}
expectedResponse := createDefaultMeasurementCreateResponse()
expectedMeasurement := createDefaultMeasurement(measurementType)

err := root.Cmd.ExecuteContext(t.Context())
for i := range expectedMeasurement.Results {
expectedMeasurement.Results[i].Result.Status = status
}

require.NoError(t, err)
assert.True(t, ctx.Table)
assert.False(t, ctx.ToLatency)
assert.False(t, ctx.ToJSON)
assert.Empty(t, w.String())
})
gbMock := apiMocks.NewMockClient(ctrl)
gbMock.EXPECT().CreateMeasurement(t.Context(), expectedOpts).Return(expectedResponse, nil)
gbMock.EXPECT().GetMeasurement(t.Context(), expectedResponse.ID).Return(expectedMeasurement, nil)

viewerMock := viewMocks.NewMockViewer(ctrl)
viewerMock.EXPECT().OutputTable(expectedMeasurement).Return("", nil)
viewerMock.EXPECT().OutputShare()

utilsMock := utilsMocks.NewMockUtils(ctrl)
utilsMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

w := new(bytes.Buffer)
ctx := createDefaultContext()
storage := createDefaultTestStorage(t, utilsMock)
root := NewRoot(view.NewPrinter(nil, w, w), ctx, viewerMock, utilsMock, gbMock, nil, storage)
oldArgs := os.Args
t.Cleanup(func() { os.Args = oldArgs })
os.Args = []string{"globalping", string(measurementType), "jsdelivr.com", "--table", "--latency", "--json", "--ci"}

err := root.Cmd.ExecuteContext(t.Context())

require.NoError(t, err)
assert.True(t, ctx.Table)
assert.False(t, ctx.ToLatency)
assert.False(t, ctx.ToJSON)
assert.Empty(t, w.String())
})
}
}
}

Expand Down Expand Up @@ -497,22 +506,27 @@ func Test_HandleMeasurement_TableTakesOutputPrecedence(t *testing.T) {
assert.False(t, root.Cmd.SilenceErrors)
})

t.Run("table output errors do not receive outcome-specific handling", func(t *testing.T) {
t.Run("all-failed table output completes successfully", func(t *testing.T) {
ctrl := gomock.NewController(t)
measurement := createDefaultMeasurement("ping")

for i := range measurement.Results {
measurement.Results[i].Result.Status = globalping.TestStatusFailed
}

client := apiMocks.NewMockClient(ctrl)
client.EXPECT().GetMeasurement(t.Context(), measurement.ID).Return(measurement, nil)
viewer := viewMocks.NewMockViewer(ctrl)
viewer.EXPECT().OutputTable(measurement).Return("", view.ErrAllProbesFailed)
viewer.EXPECT().OutputTable(measurement).Return("", nil)
viewer.EXPECT().OutputShare()
ctx := createDefaultContext()
ctx.Table = true
utils := utilsMocks.NewMockUtils(ctrl)
utils.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()
root := NewRoot(view.NewPrinter(nil, new(bytes.Buffer), new(bytes.Buffer)), ctx, viewer, utils, client, nil, nil)

require.ErrorIs(t, root.handleMeasurement(t.Context(), measurement.ID, nil), view.ErrAllProbesFailed)
assert.True(t, root.Cmd.SilenceUsage)
require.NoError(t, root.handleMeasurement(t.Context(), measurement.ID, nil))
assert.False(t, root.Cmd.SilenceUsage)
assert.False(t, root.Cmd.SilenceErrors)
})

Expand Down
Loading