From ad9ed6d7f48342b9b9ba4db8aff23aec98eac413 Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Mon, 13 Nov 2023 12:21:07 -0600 Subject: [PATCH 1/3] feat(BE): Skip Trace Collection --- api/openapi.yaml | 16 ++++++ api/tests.yaml | 3 ++ server/executor/queue.go | 53 ++++++++++++++----- server/executor/test_pipeline.go | 19 ++++++- .../tracepollerworker/evaluator_worker.go | 29 +++++++--- .../tracepollerworker/fetcher_worker.go | 5 ++ .../tracepollerworker/starter_worker.go | 14 +++++ server/http/controller.go | 7 +++ .../35_skip_trace_collection.down.sql | 11 ++++ .../35_skip_trace_collection.up.sql | 15 ++++++ server/model/events/events.go | 15 ++++++ server/openapi/api.go | 2 + server/openapi/api_api.go | 28 ++++++++++ server/openapi/model_test_.go | 3 ++ server/test/run_repository.go | 10 +++- server/test/test_entities.go | 25 +++++---- server/test/test_repository.go | 10 +++- 17 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 server/migrations/35_skip_trace_collection.down.sql create mode 100644 server/migrations/35_skip_trace_collection.up.sql diff --git a/api/openapi.yaml b/api/openapi.yaml index aca41d5602..85fb6781c8 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -594,6 +594,22 @@ paths: 422: description: could not stop execution, probably it's not running anymore + /tests/{testId}/run/{runId}/skipPolling: + post: + tags: + - api + parameters: + - $ref: "./parameters.yaml#/components/parameters/testId" + - $ref: "./parameters.yaml#/components/parameters/runId" + summary: "skips the trace collection of a test run" + description: "skips the trace collection of a test run" + operationId: skipTraceCollection + responses: + 200: + description: successful operation + 422: + description: could not stop execution, probably it's not running anymore + # Test events /tests/{testId}/run/{runId}/events: get: diff --git a/api/tests.yaml b/api/tests.yaml index 8889eec147..700db2a2d4 100644 --- a/api/tests.yaml +++ b/api/tests.yaml @@ -42,6 +42,9 @@ components: format: date-time trigger: $ref: "./triggers.yaml#/components/schemas/Trigger" + skipTraceCollection: + type: boolean + description: If true, the test will not collect a trace specs: type: array items: diff --git a/server/executor/queue.go b/server/executor/queue.go index 0ec002eafc..d2b032cfe4 100644 --- a/server/executor/queue.go +++ b/server/executor/queue.go @@ -324,8 +324,8 @@ func (q Queue) listenPreprocess(ctx context.Context, job Job) (context.Context, ctx = context.WithValue(ctx, "LastInstanceID", job.Headers.Get("InstanceID")) - ctx, cancelCtx := context.WithCancel(ctx) - q.listenForStopRequests(context.Background(), cancelCtx, job) + ctx, cancelCtx := context.WithCancelCause(ctx) + q.listenForUserRequests(context.Background(), cancelCtx, job) return ctx, Job{ Headers: job.Headers, @@ -336,34 +336,44 @@ func (q Queue) listenPreprocess(ctx context.Context, job Job) (context.Context, PollingProfile: q.resolvePollingProfile(ctx, job), DataStore: q.resolveDataStore(ctx, job), } - } -type StopRequest struct { +type UserRequestType string + +var ( + UserRequestTypeStop UserRequestType = "stop" + UserRequestSkipTraceCollection UserRequestType = "skip_trace_collection" +) + +type UserRequest struct { TestID id.ID RunID int } -func (sr StopRequest) ResourceID() string { +func (sr UserRequest) ResourceID(requestType UserRequestType) string { runID := (test.Run{ID: sr.RunID, TestID: sr.TestID}).ResourceID() - return runID + "/stop" + return fmt.Sprintf("%s/%s", runID, requestType) } -func (q Queue) listenForStopRequests(ctx context.Context, cancelCtx context.CancelFunc, job Job) { +var ( + ErrSkipTraceCollection = errors.New("skip trace collection") +) + +func (q Queue) listenForUserRequests(ctx context.Context, cancelCtx context.CancelCauseFunc, job Job) { if q.subscriptor == nil { return } sfn := subscription.NewSubscriberFunction(func(m subscription.Message) error { - cancelCtx() - stopRequest, ok := m.Content.(StopRequest) + cancelCtx(nil) + request, ok := m.Content.(UserRequest) if !ok { return nil } - run, err := q.runs.GetRun(ctx, stopRequest.TestID, stopRequest.RunID) + run, err := q.runs.GetRun(ctx, request.TestID, request.RunID) if err != nil { - return fmt.Errorf("failed to get run %d for test %s: %w", stopRequest.RunID, stopRequest.TestID, err) + return fmt.Errorf("failed to get run %d for test %s: %w", request.RunID, request.TestID, err) } if run.State == test.RunStateStopped { @@ -371,10 +381,29 @@ func (q Queue) listenForStopRequests(ctx context.Context, cancelCtx context.Canc } return q.cancelRunHandlerFn(ctx, run) + }) + + spfn := subscription.NewSubscriberFunction(func(m subscription.Message) error { + request, ok := m.Content.(UserRequest) + if !ok { + return nil + } + + run, err := q.runs.GetRun(ctx, request.TestID, request.RunID) + if err != nil { + return fmt.Errorf("failed to get run %d for test %s: %w", request.RunID, request.TestID, err) + } + + if run.State == test.RunStateStopped || run.State.IsFinal() { + return nil + } + cancelCtx(ErrSkipTraceCollection) + return nil }) - q.subscriptor.Subscribe((StopRequest{job.Test.ID, job.Run.ID}).ResourceID(), sfn) + q.subscriptor.Subscribe((UserRequest{job.Test.ID, job.Run.ID}).ResourceID(UserRequestTypeStop), sfn) + q.subscriptor.Subscribe((UserRequest{job.Test.ID, job.Run.ID}).ResourceID(UserRequestSkipTraceCollection), spfn) } func (q Queue) resolveTestSuite(ctx context.Context, job Job) testsuite.TestSuite { diff --git a/server/executor/test_pipeline.go b/server/executor/test_pipeline.go index 5fb5c516e2..0c434addfc 100644 --- a/server/executor/test_pipeline.go +++ b/server/executor/test_pipeline.go @@ -66,6 +66,8 @@ func NewTestPipeline( } } +type key string + func (p *TestPipeline) Run(ctx context.Context, testObj test.Test, metadata test.RunMetadata, variableSet variableset.VariableSet, requiredGates *[]testrunner.RequiredGate) test.Run { run := test.NewRun() run.Metadata = metadata @@ -77,6 +79,7 @@ func (p *TestPipeline) Run(ctx context.Context, testObj test.Test, metadata test requiredGates = &rg } run = run.ConfigureRequiredGates(*requiredGates) + run.SkipTraceCollection = testObj.SkipTraceCollection run, err := p.runs.CreateRun(ctx, testObj, run) p.handleDBError(run, err) @@ -119,13 +122,25 @@ func (p *TestPipeline) Rerun(ctx context.Context, testObj test.Test, runID int) } func (p *TestPipeline) StopTest(ctx context.Context, testID id.ID, runID int) { - sr := StopRequest{ + sr := UserRequest{ + TestID: testID, + RunID: runID, + } + + p.updatePublisher.PublishUpdate(subscription.Message{ + ResourceID: sr.ResourceID(UserRequestTypeStop), + Content: sr, + }) +} + +func (p *TestPipeline) SkipTraceCollection(ctx context.Context, testID id.ID, runID int) { + sr := UserRequest{ TestID: testID, RunID: runID, } p.updatePublisher.PublishUpdate(subscription.Message{ - ResourceID: sr.ResourceID(), + ResourceID: sr.ResourceID(UserRequestSkipTraceCollection), Content: sr, }) } diff --git a/server/executor/tracepollerworker/evaluator_worker.go b/server/executor/tracepollerworker/evaluator_worker.go index f6f7ed32b3..8a01e61711 100644 --- a/server/executor/tracepollerworker/evaluator_worker.go +++ b/server/executor/tracepollerworker/evaluator_worker.go @@ -2,6 +2,7 @@ package tracepollerworker import ( "context" + "errors" "fmt" "log" "time" @@ -64,6 +65,11 @@ func (w *tracePollerEvaluatorWorker) ProcessItem(ctx context.Context, job execut ctx, span := w.state.tracer.Start(ctx, "Evaluating trace") defer span.End() + if job.Run.SkipTraceCollection { + w.donePolling(ctx, "Trace Collection Skipped", job) + return + } + traceNotFound := job.Headers.GetBool("traceNotFound") if traceNotFound && !tracePollerTimedOut(ctx, job) { @@ -71,7 +77,7 @@ func (w *tracePollerEvaluatorWorker) ProcessItem(ctx context.Context, job execut populateSpan(span, job, "", nil) emitEvent(ctx, w.state, events.TracePollingIterationInfo(job.Test.ID, job.Run.ID, 0, job.EnqueueCount(), false, "trace not found on data store")) - enqueueTraceFetchJob(ctx, job, w.state) + w.enqueueTraceFetchJob(ctx, job) return } @@ -135,13 +141,17 @@ func (w *tracePollerEvaluatorWorker) ProcessItem(ctx context.Context, job execut emitEvent(ctx, w.state, events.TracePollingIterationInfo(job.Test.ID, job.Run.ID, totalSpans, job.EnqueueCount(), false, reason)) log.Printf("[TracePoller] Test %s Run %d: Not done polling. (%s)", job.Test.ID, job.Run.ID, reason) - - enqueueTraceFetchJob(ctx, job, w.state) + w.enqueueTraceFetchJob(ctx, job) return } - log.Printf("[TracePoller] Test %s Run %d: Done polling. (%s)", job.Test.ID, job.Run.ID, reason) + w.donePolling(ctx, reason, job) +} + +func (w *tracePollerEvaluatorWorker) donePolling(ctx context.Context, reason string, job executor.Job) { + log.Printf("[TracePoller] Test %s Run %d: Done polling. (%s)", job.Test.ID, job.Run.ID, reason) log.Printf("[TracePoller] Test %s Run %d: Start Sorting", job.Test.ID, job.Run.ID) + if job.Run.Trace == nil { newTrace := traces.NewTrace(job.Run.TraceID.String(), []traces.Span{}) job.Run.Trace = &newTrace @@ -183,7 +193,7 @@ func tracePollerTimedOut(ctx context.Context, job executor.Job) bool { return timedOut } -func enqueueTraceFetchJob(ctx context.Context, job executor.Job, state *workerState) { +func (w *tracePollerEvaluatorWorker) enqueueTraceFetchJob(ctx context.Context, job executor.Job) { go func() { log.Printf("[TracePoller] Requeuing Test Run %d. Current iteration: %d\n", job.Run.ID, job.EnqueueCount()) time.Sleep(job.PollingProfile.Periodic.RetryDelayDuration()) @@ -194,10 +204,17 @@ func enqueueTraceFetchJob(ctx context.Context, job executor.Job, state *workerSt select { default: case <-ctx.Done(): + err := context.Cause(ctx) + if errors.Is(err, executor.ErrSkipTraceCollection) { + ctx = context.Background() + emitEvent(ctx, w.state, events.TracePollingSkipped(job.Test.ID, job.Run.ID)) + w.donePolling(ctx, "Trace Collection Skipped", job) + } + return // user requested to stop the process } // inputQueue is set as the trace fetch queue by our pipeline engine - state.inputQueue.Enqueue(ctx, job) + w.state.inputQueue.Enqueue(ctx, job) }() } diff --git a/server/executor/tracepollerworker/fetcher_worker.go b/server/executor/tracepollerworker/fetcher_worker.go index 1c18b42862..22330cc142 100644 --- a/server/executor/tracepollerworker/fetcher_worker.go +++ b/server/executor/tracepollerworker/fetcher_worker.go @@ -56,6 +56,11 @@ func (w *traceFetcherWorker) ProcessItem(ctx context.Context, job executor.Job) ctx, span := w.state.tracer.Start(ctx, "Fetching trace") defer span.End() + if job.Run.SkipTraceCollection { + w.outputQueue.Enqueue(ctx, job) + return + } + populateSpan(span, job, "", nil) traceDB, err := getTraceDB(ctx, w.state) diff --git a/server/executor/tracepollerworker/starter_worker.go b/server/executor/tracepollerworker/starter_worker.go index 4fe5f56b23..7af4f82ab0 100644 --- a/server/executor/tracepollerworker/starter_worker.go +++ b/server/executor/tracepollerworker/starter_worker.go @@ -2,6 +2,7 @@ package tracepollerworker import ( "context" + "errors" "fmt" "log" @@ -52,11 +53,24 @@ func (w *tracePollerStarterWorker) ProcessItem(ctx context.Context, job executor ctx, span := w.state.tracer.Start(ctx, "Start polling trace") defer span.End() + if job.Run.SkipTraceCollection { + emitEvent(ctx, w.state, events.TracePollingSkipped(job.Test.ID, job.Run.ID)) + w.outputQueue.Enqueue(ctx, job) + return + } + populateSpan(span, job, "", nil) select { default: case <-ctx.Done(): + err := context.Cause(ctx) + if errors.Is(err, executor.ErrSkipTraceCollection) { + ctx = context.Background() + emitEvent(ctx, w.state, events.TracePollingSkipped(job.Test.ID, job.Run.ID)) + w.outputQueue.Enqueue(ctx, job) + } + return } diff --git a/server/http/controller.go b/server/http/controller.go index f66505bc2c..6ab8228ce3 100644 --- a/server/http/controller.go +++ b/server/http/controller.go @@ -74,6 +74,7 @@ type testSuiteRunRepository interface { type testRunner interface { StopTest(_ context.Context, testID id.ID, runID int) + SkipTraceCollection(_ context.Context, testID id.ID, runID int) Run(context.Context, test.Test, test.RunMetadata, variableset.VariableSet, *[]testrunner.RequiredGate) test.Run Rerun(_ context.Context, _ test.Test, runID int) test.Run } @@ -291,6 +292,12 @@ func (c *controller) StopTestRun(ctx context.Context, testID string, runID int32 return openapi.Response(http.StatusOK, map[string]string{"result": "success"}), nil } +func (c *controller) SkipTraceCollection(ctx context.Context, testID string, runID int32) (openapi.ImplResponse, error) { + c.testRunner.SkipTraceCollection(ctx, id.ID(testID), int(runID)) + + return openapi.Response(http.StatusOK, map[string]string{"result": "success"}), nil +} + func (c *controller) DryRunAssertion(ctx context.Context, testID string, runID int32, def openapi.TestSpecs) (openapi.ImplResponse, error) { run, err := c.testRunRepository.GetRun(ctx, id.ID(testID), int(runID)) if err != nil { diff --git a/server/migrations/35_skip_trace_collection.down.sql b/server/migrations/35_skip_trace_collection.down.sql new file mode 100644 index 0000000000..0b8f9c8d2d --- /dev/null +++ b/server/migrations/35_skip_trace_collection.down.sql @@ -0,0 +1,11 @@ +BEGIN; + +-- Tests +ALTER TABLE + tests DROP COLUMN skip_trace_collection; + +-- Test Runs +ALTER TABLE + test_runs DROP COLUMN skip_trace_collection; + +COMMIT; \ No newline at end of file diff --git a/server/migrations/35_skip_trace_collection.up.sql b/server/migrations/35_skip_trace_collection.up.sql new file mode 100644 index 0000000000..ba434b9243 --- /dev/null +++ b/server/migrations/35_skip_trace_collection.up.sql @@ -0,0 +1,15 @@ +BEGIN; + +-- Tests +ALTER TABLE + tests +ADD + COLUMN skip_trace_collection BOOLEAN DEFAULT FALSE NOT NULL; + +-- Test Runs +ALTER TABLE + test_runs +ADD + COLUMN skip_trace_collection BOOLEAN DEFAULT FALSE NOT NULL; + +COMMIT; \ No newline at end of file diff --git a/server/model/events/events.go b/server/model/events/events.go index fd20a7885b..e208dc6a3b 100644 --- a/server/model/events/events.go +++ b/server/model/events/events.go @@ -236,6 +236,21 @@ func TracePollingSuccess(testID id.ID, runID int, reason string) model.TestRunEv } } +func TracePollingSkipped(testID id.ID, runID int) model.TestRunEvent { + return model.TestRunEvent{ + TestID: testID, + RunID: runID, + Stage: model.StageTrace, + Type: "POLLING_SKIPPED", + Title: "Trace polling has been skipped", + Description: "The polling strategy has been skipped", + CreatedAt: time.Now(), + DataStoreConnection: model.ConnectionResult{}, + Polling: model.PollingInfo{}, + Outputs: []model.OutputInfo{}, + } +} + func TracePollingError(testID id.ID, runID int, reason string, err error) model.TestRunEvent { return model.TestRunEvent{ TestID: testID, diff --git a/server/openapi/api.go b/server/openapi/api.go index 67226a43f3..4cce9a3d94 100644 --- a/server/openapi/api.go +++ b/server/openapi/api.go @@ -39,6 +39,7 @@ type ApiApiRouter interface { RerunTestRun(http.ResponseWriter, *http.Request) RunTest(http.ResponseWriter, *http.Request) RunTestSuite(http.ResponseWriter, *http.Request) + SkipTraceCollection(http.ResponseWriter, *http.Request) StopTestRun(http.ResponseWriter, *http.Request) TestConnection(http.ResponseWriter, *http.Request) UpdateTestRun(http.ResponseWriter, *http.Request) @@ -111,6 +112,7 @@ type ApiApiServicer interface { RerunTestRun(context.Context, string, int32) (ImplResponse, error) RunTest(context.Context, string, RunInformation) (ImplResponse, error) RunTestSuite(context.Context, string, RunInformation) (ImplResponse, error) + SkipTraceCollection(context.Context, string, int32) (ImplResponse, error) StopTestRun(context.Context, string, int32) (ImplResponse, error) TestConnection(context.Context, DataStore) (ImplResponse, error) UpdateTestRun(context.Context, string, int32, TestRun) (ImplResponse, error) diff --git a/server/openapi/api_api.go b/server/openapi/api_api.go index 3318da2edf..9dc7b73a60 100644 --- a/server/openapi/api_api.go +++ b/server/openapi/api_api.go @@ -176,6 +176,12 @@ func (c *ApiApiController) Routes() Routes { "/api/testsuites/{testSuiteId}/run", c.RunTestSuite, }, + { + "SkipTraceCollection", + strings.ToUpper("Post"), + "/api/tests/{testId}/run/{runId}/skipPolling", + c.SkipTraceCollection, + }, { "StopTestRun", strings.ToUpper("Post"), @@ -689,6 +695,28 @@ func (c *ApiApiController) RunTestSuite(w http.ResponseWriter, r *http.Request) } +// SkipTraceCollection - skips the trace collection of a test run +func (c *ApiApiController) SkipTraceCollection(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + testIdParam := params["testId"] + + runIdParam, err := parseInt32Parameter(params["runId"], true) + if err != nil { + c.errorHandler(w, r, &ParsingError{Err: err}, nil) + return + } + + result, err := c.service.SkipTraceCollection(r.Context(), testIdParam, runIdParam) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) + +} + // StopTestRun - stops the execution of a test run func (c *ApiApiController) StopTestRun(w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/server/openapi/model_test_.go b/server/openapi/model_test_.go index 170a7da47f..77120a5d6c 100644 --- a/server/openapi/model_test_.go +++ b/server/openapi/model_test_.go @@ -27,6 +27,9 @@ type Test struct { Trigger Trigger `json:"trigger,omitempty"` + // If true, the test will not collect a trace + SkipTraceCollection bool `json:"skipTraceCollection,omitempty"` + // specification of assertions that are going to be made Specs []TestSpec `json:"specs,omitempty"` diff --git a/server/test/run_repository.go b/server/test/run_repository.go index 63433a8c68..04f8f0e358 100644 --- a/server/test/run_repository.go +++ b/server/test/run_repository.go @@ -82,6 +82,8 @@ INSERT INTO test_runs ( -- required gates "required_gates_result", + "skip_trace_collection", + "tenant_id" ) VALUES ( nextval('` + runSequenceName + `'), -- id @@ -114,7 +116,8 @@ INSERT INTO test_runs ( $14, -- variable_set $15, -- linter $16, -- required_gates_result - $17 -- tenant_id + $17, -- skip_trace_collection + $18 -- tenant_id ) RETURNING "id"` @@ -189,6 +192,7 @@ func (r *runRepository) CreateRun(ctx context.Context, test Test, run Run) (Run, jsonVariableSet, jsonlinter, jsonGatesResult, + run.SkipTraceCollection, ) var runID int @@ -391,7 +395,8 @@ const ( test_suite_run_steps.test_suite_run_id, test_suite_run_steps.test_suite_run_test_suite_id, "linter", - "required_gates_result" + "required_gates_result", + "skip_trace_collection" ` baseSql = ` @@ -547,6 +552,7 @@ func readRunRow(row scanner) (Run, error) { &testSuiteID, &jsonLinter, &jsonGatesResult, + &r.SkipTraceCollection, ) if err != nil { diff --git a/server/test/test_entities.go b/server/test/test_entities.go index 64bd63563c..6538f7f755 100644 --- a/server/test/test_entities.go +++ b/server/test/test_entities.go @@ -22,15 +22,16 @@ const ( type ( // this struct yaml/json encoding is handled at ./test_json.go for custom encodings Test struct { - ID id.ID `json:"id,omitempty"` - CreatedAt *time.Time `json:"createdAt,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - Version *int `json:"version,omitempty"` - Trigger trigger.Trigger `json:"trigger,omitempty"` - Specs Specs `json:"specs,omitempty"` - Outputs Outputs `json:"outputs,omitempty"` - Summary *Summary `json:"summary,omitempty"` + ID id.ID `json:"id,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Version *int `json:"version,omitempty"` + Trigger trigger.Trigger `json:"trigger,omitempty"` + Specs Specs `json:"specs,omitempty"` + Outputs Outputs `json:"outputs,omitempty"` + Summary *Summary `json:"summary,omitempty"` + SkipTraceCollection bool `json:"skipTraceCollection,omitempty"` } Specs []TestSpec @@ -128,11 +129,13 @@ type ( VariableSet variableset.VariableSet // transaction + TestSuiteID string + TestSuiteRunID string - TestSuiteID string - TestSuiteRunID string + // pipeline Linter analyzer.LinterResult RequiredGatesResult testrunner.RequiredGatesResult + SkipTraceCollection bool } RunResults struct { diff --git a/server/test/test_repository.go b/server/test/test_repository.go index f82c2d1a2a..633507c533 100644 --- a/server/test/test_repository.go +++ b/server/test/test_repository.go @@ -74,6 +74,7 @@ const ( t.specs, t.outputs, t.created_at, + t.skip_trace_collection, (SELECT COUNT(*) FROM test_runs tr WHERE tr.test_id = t.id AND tr.tenant_id = t.tenant_id) as total_runs, last_test_run.created_at as last_test_run_time, last_test_run.pass as last_test_run_pass, @@ -273,6 +274,7 @@ func (r *repository) readRow(ctx context.Context, row scanner) (Test, error) { &jsonSpecs, &jsonOutputs, &test.CreatedAt, + &test.SkipTraceCollection, &test.Summary.Runs, &lastRunTime, &pass, @@ -325,8 +327,9 @@ INSERT INTO tests ( "specs", "outputs", "created_at", + "skip_trace_collection", "tenant_id" -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)` +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` func (r *repository) Create(ctx context.Context, test Test) (Test, error) { if test.HasID() { @@ -389,6 +392,7 @@ func (r *repository) insertTest(ctx context.Context, test Test) (Test, error) { specsJson, outputsJson, test.CreatedAt, + test.SkipTraceCollection, ) _, err = stmt.ExecContext(ctx, params...) @@ -464,7 +468,9 @@ func testHasChanged(oldTest Test, newTest Test) (bool, error) { nameHasChanged := oldTest.Name != newTest.Name descriptionHasChanged := oldTest.Description != newTest.Description - return outputsHaveChanged || definitionHasChanged || triggerHasChanged || nameHasChanged || descriptionHasChanged, nil + traceCollectionChanged := oldTest.SkipTraceCollection != newTest.SkipTraceCollection + + return outputsHaveChanged || definitionHasChanged || triggerHasChanged || nameHasChanged || descriptionHasChanged || traceCollectionChanged, nil } func testFieldHasChanged(oldField interface{}, newField interface{}) (bool, error) { From 7bfba1974bd8c9c6ec268ed5e5b5e5a32243751d Mon Sep 17 00:00:00 2001 From: Oscar Reyes Date: Mon, 13 Nov 2023 16:00:18 -0600 Subject: [PATCH 2/3] feat(FE): Skip Trace Collection --- cli/openapi/api_api.go | 96 +++++++++++++++++++ cli/openapi/model_test_.go | 37 +++++++ server/http/mappings/tests.go | 1 + .../RequestDetails/RequestDetailsForm.tsx | 6 +- .../RequestDetails/RequestDetails.styled.ts | 7 +- .../RequestDetails/RequestDetailsForm.tsx | 8 +- .../RequestDetails/RequestDetails.styled.ts | 6 ++ .../RequestDetails/RequestDetailsForm.tsx | 8 +- web/src/components/Fields/SSL/SSL.tsx | 2 +- .../SkipTraceCollection.tsx | 17 ++++ web/src/components/Fields/index.ts | 3 +- .../RunDetailLayout/HeaderRight.tsx | 26 ++--- .../RunDetailLayout/RunDetailLayout.styled.ts | 4 - .../RunDetailLayout/hooks/useSkipPolling.ts | 46 +++++++++ .../components/SkipPollingPopover/Content.tsx | 28 ++++++ .../SkipPollingPopover.styled.ts | 37 +++++++ .../SkipPollingPopover/SkipPollingPopover.tsx | 46 +++++++++ web/src/models/Test.model.ts | 2 + web/src/models/TestRun.model.ts | 4 + .../providers/TestRun/TestRun.provider.tsx | 18 ++-- .../Tracetest/endpoints/TestRun.endpoint.ts | 16 ++++ web/src/redux/apis/Tracetest/index.ts | 2 + web/src/services/Test.service.ts | 6 +- web/src/types/Generated.types.ts | 16 ++++ web/src/types/Test.types.ts | 1 + 25 files changed, 401 insertions(+), 42 deletions(-) create mode 100644 web/src/components/Fields/SkipTraceCollection/SkipTraceCollection.tsx create mode 100644 web/src/components/RunDetailLayout/hooks/useSkipPolling.ts create mode 100644 web/src/components/SkipPollingPopover/Content.tsx create mode 100644 web/src/components/SkipPollingPopover/SkipPollingPopover.styled.ts create mode 100644 web/src/components/SkipPollingPopover/SkipPollingPopover.tsx diff --git a/cli/openapi/api_api.go b/cli/openapi/api_api.go index 00009e09b0..47c1c388bf 100644 --- a/cli/openapi/api_api.go +++ b/cli/openapi/api_api.go @@ -2368,6 +2368,102 @@ func (a *ApiApiService) RunTestSuiteExecute(r ApiRunTestSuiteRequest) (*TestSuit return localVarReturnValue, localVarHTTPResponse, nil } +type ApiSkipTraceCollectionRequest struct { + ctx context.Context + ApiService *ApiApiService + testId string + runId int32 +} + +func (r ApiSkipTraceCollectionRequest) Execute() (*http.Response, error) { + return r.ApiService.SkipTraceCollectionExecute(r) +} + +/* +SkipTraceCollection skips the trace collection of a test run + +skips the trace collection of a test run + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param testId id of the test + @param runId id of the run + @return ApiSkipTraceCollectionRequest +*/ +func (a *ApiApiService) SkipTraceCollection(ctx context.Context, testId string, runId int32) ApiSkipTraceCollectionRequest { + return ApiSkipTraceCollectionRequest{ + ApiService: a, + ctx: ctx, + testId: testId, + runId: runId, + } +} + +// Execute executes the request +func (a *ApiApiService) SkipTraceCollectionExecute(r ApiSkipTraceCollectionRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ApiApiService.SkipTraceCollection") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/tests/{testId}/run/{runId}/skipPolling" + localVarPath = strings.Replace(localVarPath, "{"+"testId"+"}", url.PathEscape(parameterValueToString(r.testId, "testId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"runId"+"}", url.PathEscape(parameterValueToString(r.runId, "runId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + type ApiStopTestRunRequest struct { ctx context.Context ApiService *ApiApiService diff --git a/cli/openapi/model_test_.go b/cli/openapi/model_test_.go index 794182c84e..569cea82c8 100644 --- a/cli/openapi/model_test_.go +++ b/cli/openapi/model_test_.go @@ -27,6 +27,8 @@ type Test struct { Version *int32 `json:"version,omitempty"` CreatedAt *time.Time `json:"createdAt,omitempty"` Trigger *Trigger `json:"trigger,omitempty"` + // If true, the test will not collect a trace + SkipTraceCollection *bool `json:"skipTraceCollection,omitempty"` // specification of assertions that are going to be made Specs []TestSpec `json:"specs,omitempty"` // define test outputs, in a key/value format. The value is processed as an expression @@ -243,6 +245,38 @@ func (o *Test) SetTrigger(v Trigger) { o.Trigger = &v } +// GetSkipTraceCollection returns the SkipTraceCollection field value if set, zero value otherwise. +func (o *Test) GetSkipTraceCollection() bool { + if o == nil || isNil(o.SkipTraceCollection) { + var ret bool + return ret + } + return *o.SkipTraceCollection +} + +// GetSkipTraceCollectionOk returns a tuple with the SkipTraceCollection field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Test) GetSkipTraceCollectionOk() (*bool, bool) { + if o == nil || isNil(o.SkipTraceCollection) { + return nil, false + } + return o.SkipTraceCollection, true +} + +// HasSkipTraceCollection returns a boolean if a field has been set. +func (o *Test) HasSkipTraceCollection() bool { + if o != nil && !isNil(o.SkipTraceCollection) { + return true + } + + return false +} + +// SetSkipTraceCollection gets a reference to the given bool and assigns it to the SkipTraceCollection field. +func (o *Test) SetSkipTraceCollection(v bool) { + o.SkipTraceCollection = &v +} + // GetSpecs returns the Specs field value if set, zero value otherwise. func (o *Test) GetSpecs() []TestSpec { if o == nil || isNil(o.Specs) { @@ -365,6 +399,9 @@ func (o Test) ToMap() (map[string]interface{}, error) { if !isNil(o.Trigger) { toSerialize["trigger"] = o.Trigger } + if !isNil(o.SkipTraceCollection) { + toSerialize["skipTraceCollection"] = o.SkipTraceCollection + } if !isNil(o.Specs) { toSerialize["specs"] = o.Specs } diff --git a/server/http/mappings/tests.go b/server/http/mappings/tests.go index 668a0776c7..ebbf435af3 100644 --- a/server/http/mappings/tests.go +++ b/server/http/mappings/tests.go @@ -64,6 +64,7 @@ func (m OpenAPI) Test(in test.Test) openapi.Test { Version: int32(*in.Version), CreatedAt: *in.CreatedAt, Outputs: m.Outputs(in.Outputs), + SkipTraceCollection: in.SkipTraceCollection, Summary: openapi.TestSummary{ Runs: int32(in.Summary.Runs), LastRun: openapi.TestSummaryLastRun{ diff --git a/web/src/components/CreateTestPlugins/Grpc/steps/RequestDetails/RequestDetailsForm.tsx b/web/src/components/CreateTestPlugins/Grpc/steps/RequestDetails/RequestDetailsForm.tsx index b188c5bbdc..e0835155c9 100644 --- a/web/src/components/CreateTestPlugins/Grpc/steps/RequestDetails/RequestDetailsForm.tsx +++ b/web/src/components/CreateTestPlugins/Grpc/steps/RequestDetails/RequestDetailsForm.tsx @@ -4,7 +4,7 @@ import GrpcService from 'services/Triggers/Grpc.service'; import {IRpcValues, TDraftTestForm} from 'types/Test.types'; import {SupportedEditors} from 'constants/Editor.constants'; import {Editor, FileUpload} from 'components/Inputs'; -import {Auth, Metadata} from 'components/Fields'; +import {Auth, Metadata, SkipTraceCollection} from 'components/Fields'; interface IProps { form: TDraftTestForm; @@ -65,6 +65,10 @@ const RequestDetailsForm = ({form}: IProps) => { + + + + ); }; diff --git a/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetails.styled.ts b/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetails.styled.ts index 43ded08a5d..f525bbba16 100644 --- a/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetails.styled.ts +++ b/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetails.styled.ts @@ -16,9 +16,8 @@ export const HeaderContainer = styled.div` grid-template-columns: 40% 40% 19%; margin-bottom: 8px; `; - -export const SSLVerificationContainer = styled.div` - align-items: center; +export const SettingsContainer = styled.div` display: flex; - gap: 8px; + flex-direction: column; + gap: 14px; `; diff --git a/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetailsForm.tsx b/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetailsForm.tsx index 5b99597e12..9918feb859 100644 --- a/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetailsForm.tsx +++ b/web/src/components/CreateTestPlugins/Kafka/steps/RequestDetails/RequestDetailsForm.tsx @@ -1,8 +1,9 @@ import {Form, Tabs} from 'antd'; import KeyValueListInput from 'components/Fields/KeyValueList'; -import {PlainAuth, SSL} from 'components/Fields'; +import {PlainAuth, SSL, SkipTraceCollection} from 'components/Fields'; import {Editor} from 'components/Inputs'; import {SupportedEditors} from 'constants/Editor.constants'; +import * as S from './RequestDetails.styled'; const RequestDetailsForm = () => { return ( @@ -47,7 +48,10 @@ const RequestDetailsForm = () => { - + + + + ); diff --git a/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetails.styled.ts b/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetails.styled.ts index 48d716c722..683b9c837c 100644 --- a/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetails.styled.ts +++ b/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetails.styled.ts @@ -8,3 +8,9 @@ export const Row = styled.div` export const Label = styled(Typography.Text).attrs({as: 'div'})` margin-bottom: 8px; `; + +export const SettingsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 14px; +`; \ No newline at end of file diff --git a/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetailsForm.tsx b/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetailsForm.tsx index da399ef282..d154ad376a 100644 --- a/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetailsForm.tsx +++ b/web/src/components/CreateTestPlugins/Rest/steps/RequestDetails/RequestDetailsForm.tsx @@ -2,7 +2,8 @@ import {Form, Tabs} from 'antd'; import {DEFAULT_HEADERS} from 'constants/Test.constants'; import KeyValueListInput from 'components/Fields/KeyValueList'; import {Body} from 'components/Inputs'; -import {Auth, SSL} from 'components/Fields'; +import {Auth, SSL, SkipTraceCollection} from 'components/Fields'; +import * as S from './RequestDetails.styled'; export const FORM_ID = 'create-test'; @@ -30,7 +31,10 @@ const RequestDetailsForm = () => ( - + + + + ); diff --git a/web/src/components/Fields/SSL/SSL.tsx b/web/src/components/Fields/SSL/SSL.tsx index 82fd27f91a..969e338535 100644 --- a/web/src/components/Fields/SSL/SSL.tsx +++ b/web/src/components/Fields/SSL/SSL.tsx @@ -6,7 +6,7 @@ const SSL = () => ( - + diff --git a/web/src/components/Fields/SkipTraceCollection/SkipTraceCollection.tsx b/web/src/components/Fields/SkipTraceCollection/SkipTraceCollection.tsx new file mode 100644 index 0000000000..0bce47dfeb --- /dev/null +++ b/web/src/components/Fields/SkipTraceCollection/SkipTraceCollection.tsx @@ -0,0 +1,17 @@ +import {Form, Switch} from 'antd'; +import {TooltipQuestion} from 'components/TooltipQuestion/TooltipQuestion'; +import * as S from '../SSL/SSL.styled'; + +const SkipTraceCollection = () => { + return ( + + + + + + + + ); +}; + +export default SkipTraceCollection; diff --git a/web/src/components/Fields/index.ts b/web/src/components/Fields/index.ts index 0fbf723bc6..cfb95db40e 100644 --- a/web/src/components/Fields/index.ts +++ b/web/src/components/Fields/index.ts @@ -6,5 +6,6 @@ import Metadata from './Metadata/Metadata'; import MultiURL from './MultiURL/MultiURL'; import PlainAuth from './PlainAuth/PlainAuth'; import KeyValueList from './KeyValueList/KeyValueList'; +import SkipTraceCollection from './SkipTraceCollection/SkipTraceCollection'; -export {URL, Auth, SSL, Headers, Metadata, MultiURL, PlainAuth, KeyValueList}; +export {URL, Auth, SSL, Headers, Metadata, MultiURL, PlainAuth, KeyValueList, SkipTraceCollection}; diff --git a/web/src/components/RunDetailLayout/HeaderRight.tsx b/web/src/components/RunDetailLayout/HeaderRight.tsx index 50574c4798..899aa791c6 100644 --- a/web/src/components/RunDetailLayout/HeaderRight.tsx +++ b/web/src/components/RunDetailLayout/HeaderRight.tsx @@ -1,11 +1,9 @@ -import {CloseCircleOutlined} from '@ant-design/icons'; -import {Button, Tooltip} from 'antd'; import CreateButton from 'components/CreateButton'; import RunActionsMenu from 'components/RunActionsMenu'; import TestActions from 'components/TestActions'; import TestState from 'components/TestState'; import {TestState as TestStateEnum} from 'constants/TestRun.constants'; -import {isRunStateFinished, isRunStateStopped, isRunStateSucceeded} from 'models/TestRun.model'; +import {isRunPollingState, isRunStateFinished, isRunStateStopped, isRunStateSucceeded} from 'models/TestRun.model'; import {useTest} from 'providers/Test/Test.provider'; import {useTestRun} from 'providers/TestRun/TestRun.provider'; import {useTestSpecs} from 'providers/TestSpecs/TestSpecs.provider'; @@ -14,6 +12,8 @@ import * as S from './RunDetailLayout.styled'; import EventLogPopover from '../EventLogPopover/EventLogPopover'; import RunStatusIcon from '../RunStatusIcon/RunStatusIcon'; import VariableSetSelector from '../VariableSetSelector/VariableSetSelector'; +import TracePollingActions from '../SkipPollingPopover/SkipPollingPopover'; +import useSkipPolling from './hooks/useSkipPolling'; interface IProps { testId: string; @@ -24,14 +24,14 @@ const HeaderRight = ({testId}: IProps) => { const {isDraftMode: isTestOutputsDraftMode} = useTestOutput(); const isDraftMode = isTestSpecsDraftMode || isTestOutputsDraftMode; const { - isLoadingStop, - run: {state, requiredGatesResult}, + run: {state, requiredGatesResult, createdAt}, run, - stopRun, runEvents, } = useTestRun(); const {onRun} = useTest(); + const {onSkipPolling, isLoading} = useSkipPolling(); + return ( {isDraftMode && } @@ -39,18 +39,8 @@ const HeaderRight = ({testId}: IProps) => { Test status: - {state === TestStateEnum.AWAITING_TRACE && ( - - - + + + ); +}; + +export default Content; diff --git a/web/src/components/SkipPollingPopover/SkipPollingPopover.styled.ts b/web/src/components/SkipPollingPopover/SkipPollingPopover.styled.ts new file mode 100644 index 0000000000..9ad6e7fc97 --- /dev/null +++ b/web/src/components/SkipPollingPopover/SkipPollingPopover.styled.ts @@ -0,0 +1,37 @@ +import {Typography} from 'antd'; +import styled, {createGlobalStyle} from 'styled-components'; + +export const StopContainer = styled.div` + margin-left: 12px; +`; + +export const GlobalStyle = createGlobalStyle` + #skip-trace-popover { + .ant-popover-title { + padding: 14px; + border: 0; + padding-bottom: 0; + } + + .ant-popover-inner-content { + padding: 5px 14px; + padding-top: 0; + } + } +`; + +export const Actions = styled.div` + display: flex; + align-items: center; + gap: 12px; + justify-content: space-between; + margin-top: 24px; +`; + +export const Title = styled(Typography.Title).attrs({ + level: 3, +})` + && { + margin: 0; + } +`; diff --git a/web/src/components/SkipPollingPopover/SkipPollingPopover.tsx b/web/src/components/SkipPollingPopover/SkipPollingPopover.tsx new file mode 100644 index 0000000000..95eb1c486c --- /dev/null +++ b/web/src/components/SkipPollingPopover/SkipPollingPopover.tsx @@ -0,0 +1,46 @@ +import {Button, Popover} from 'antd'; +import {differenceInSeconds} from 'date-fns'; +import {useEffect, useState} from 'react'; +import {ForwardOutlined} from '@ant-design/icons'; +import * as S from './SkipPollingPopover.styled'; +import Content from './Content'; + +interface IProps { + isLoading: boolean; + skipPolling(shouldSave: boolean): void; + startTime: string; +} + +const TIMEOUT_TO_SHOW = 10; // seconds + +const SkipPollingPopover = ({isLoading, skipPolling, startTime}: IProps) => { + const [isOpen, setIsOpen] = useState(false); + const diff = differenceInSeconds(new Date(), new Date(startTime)); + + useEffect(() => { + if (diff > TIMEOUT_TO_SHOW) setIsOpen(true); + }, [diff, isOpen]); + + return ( + + + Taking too long to get the trace?} + content={} + visible={isOpen} + placement="bottomRight" + > +