Files
gitea/routers/web/devtest/mock_actions.go
bircni 1850a108e5 fix(actions): group reusable-workflow matrix legs in the workflow graph (#38475)
Fixes the UI issue reported in #38466: matrix jobs that invoke a
reusable workflow (`build-call (linux)`, `build-call (windows)`, …) were
rendered as separate nodes in the run's workflow graph, while an
equivalent regular matrix
(`build (linux)`, `build (windows)`, …) collapsed neatly into a single
matrix node.

Before: three loose `build-call (...)` boxes next to one grouped `build` box.
After: both matrices collapse into a single node.
2026-07-16 20:58:05 +08:00

575 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package devtest
import (
"fmt"
mathRand "math/rand/v2"
"net/http"
"slices"
"strconv"
"strings"
"time"
actions_model "gitea.dev/models/actions"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/web/repo/actions"
"gitea.dev/services/context"
)
type generateMockStepsLogOptions struct {
mockCountFirst int
mockCountGeneral int
groupRepeat int
}
func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOptions) (stepsLog []*actions.ViewStepLog) {
var mockedLogs []string
mockedLogs = append(mockedLogs, "::group::test group for: step={step}, cursor={cursor}")
mockedLogs = append(mockedLogs, slices.Repeat([]string{"in group msg for: step={step}, cursor={cursor}"}, opts.groupRepeat)...)
mockedLogs = append(mockedLogs, "::endgroup::")
mockedLogs = append(mockedLogs,
"message for: step={step}, cursor={cursor}",
"message for: step={step}, cursor={cursor}",
"##[group]test group for: step={step}, cursor={cursor}",
"in group msg for: step={step}, cursor={cursor}",
"##[endgroup]",
"::error::mock error for: step={step}, cursor={cursor}",
"::warning::mock warning for: step={step}, cursor={cursor}",
"::notice::mock notice for: step={step}, cursor={cursor}",
"::debug::mock debug for: step={step}, cursor={cursor}",
)
// usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally
cur := logCur.Cursor
// for the first batch, return as many as possible to test the auto-expand and auto-scroll
mockCount := util.Iif(logCur.Cursor == 0, opts.mockCountFirst, opts.mockCountGeneral)
for range mockCount {
logStr := mockedLogs[int(cur)%len(mockedLogs)]
cur++
logStr = strings.ReplaceAll(logStr, "{step}", strconv.Itoa(logCur.Step))
logStr = strings.ReplaceAll(logStr, "{cursor}", strconv.FormatInt(cur, 10))
stepsLog = append(stepsLog, &actions.ViewStepLog{
Step: logCur.Step,
Cursor: cur,
Started: time.Now().Unix() - 1,
Lines: []*actions.ViewStepLogLine{
{Index: cur, Message: logStr, Timestamp: float64(time.Now().UnixNano()) / float64(time.Second)},
},
})
}
return stepsLog
}
func MockActionsView(ctx *context.Context) {
if runID := ctx.PathParamInt64("run"); runID == 0 {
ctx.Redirect("/repo-action-view/runs/10")
return
}
ctx.Data["JobID"] = ctx.PathParamInt64("job")
ctx.Data["ActionsViewURL"] = ctx.Req.URL.Path
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
}
func MockActionsRunsJobs(ctx *context.Context) {
runID := ctx.PathParamInt64("run")
attemptID := ctx.PathParamInt64("attempt")
alignTime := func(v, unit int64) int64 {
return (v + unit) / unit * unit
}
resp := &actions.ViewResponse{}
resp.State.Run.RepoID = 12345
resp.State.Run.Index = runID
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
resp.State.Run.CanDeleteArtifact = true
resp.State.Run.WorkflowID = "workflow-id.yml"
resp.State.Run.TriggerEvent = "push"
renderUtils := templates.NewRenderUtils(ctx)
user2, _ := user_model.GetUserByID(ctx, 2)
if user2 == nil {
user2 = &user_model.User{Name: "user2"}
}
user3, _ := user_model.GetUserByID(ctx, 3)
if user3 == nil {
user3 = &user_model.User{Name: "user3"}
}
resp.State.Run.Commit = actions.ViewCommit{
ShortSha: "ccccdddd",
Link: "./commit-link",
Pusher: actions.ViewUser{
DisplayName: user2.GetDisplayName(),
Link: user2.HomeLink(),
AvatarLink: user2.AvatarLinkWithSize(ctx, 16),
},
Branch: actions.ViewBranch{
Name: "user2:commit-branch",
Link: "./branch-link",
IsDeleted: false,
},
}
resp.State.Run.PullRequest = &actions.ViewPullRequest{
Index: "#37658",
Link: "./pull/37658",
}
now := time.Now()
currentAttemptNum := int64(1)
if attemptID > 0 {
currentAttemptNum = attemptID
}
attempts := []*actions_model.ActionRunAttempt{{
Attempt: 1,
Status: actions_model.StatusSuccess,
Created: timeutil.TimeStamp(now.Add(-time.Hour).Unix()),
TriggerUserID: 2,
TriggerUser: user2,
}}
if runID == 10 {
attempts = []*actions_model.ActionRunAttempt{
{
Attempt: 3,
Status: actions_model.StatusSuccess,
Created: timeutil.TimeStamp(alignTime(now.Add(-time.Hour).Unix(), 3600)),
TriggerUserID: 2,
TriggerUser: user2,
},
{
Attempt: 2,
Status: actions_model.StatusFailure,
Created: timeutil.TimeStamp(alignTime(now.Add(-2*time.Hour).Unix(), 3600)),
TriggerUserID: 1,
TriggerUser: user3,
},
{
Attempt: 1,
Status: actions_model.StatusSuccess,
Created: timeutil.TimeStamp(alignTime(now.Add(-3*time.Hour).Unix(), 3600)),
TriggerUserID: 2,
TriggerUser: user2,
},
}
if attemptID == 0 {
currentAttemptNum = 3
}
}
latestAttempt := attempts[0]
resp.State.Run.RunAttempt = currentAttemptNum
resp.State.Run.Done = latestAttempt.Status.IsDone()
resp.State.Run.Status = latestAttempt.Status.String()
resp.State.Run.Duration = "1h 23m 45s"
resp.State.Run.TriggeredAt = latestAttempt.Created.AsTime().Unix()
resp.State.Run.ViewLink = resp.State.Run.Link
for _, attempt := range attempts {
link := resp.State.Run.Link
if attempt.Attempt != latestAttempt.Attempt {
link = fmt.Sprintf("%s/attempts/%d", resp.State.Run.Link, attempt.Attempt)
}
current := attempt.Attempt == currentAttemptNum
if current {
resp.State.Run.Status = attempt.Status.String()
resp.State.Run.Done = attempt.Status.IsDone()
resp.State.Run.TriggeredAt = attempt.Created.AsTime().Unix()
if attempt.Attempt != latestAttempt.Attempt {
resp.State.Run.ViewLink = link
}
}
resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{
Attempt: attempt.Attempt,
Status: attempt.Status.String(),
Done: attempt.Status.IsDone(),
Link: link,
Current: current,
Latest: attempt.Attempt == latestAttempt.Attempt,
TriggeredAt: attempt.Created.AsTime().Unix(),
TriggerUserName: attempt.TriggerUser.GetDisplayName(),
TriggerUserLink: attempt.TriggerUser.HomeLink(),
TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16),
})
}
isLatestAttempt := currentAttemptNum == latestAttempt.Attempt
resp.State.Run.CanCancel = runID == 10 && isLatestAttempt
resp.State.Run.CanApprove = runID == 20 && isLatestAttempt
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt
// Mock job summaries so the devtest page can preview the Summary panel rendering.
// Only some runs have summaries, so the page also exercises the "no summary" state.
if runID == 10 || runID == 20 {
resp.State.Run.JobSummaries = []*actions.ViewJobSummary{
{
JobID: runID * 10,
JobName: "job 100 (testsubname)",
SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"),
},
{
JobID: runID*10 + 2,
JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"),
},
}
}
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-a",
Size: 100 * 1024,
Status: "expired",
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-b",
Size: 1024 * 1024,
Status: "completed",
ExpiresUnix: alignTime(time.Now().Add(24*time.Hour).Unix(), 3600),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 100 * 1024,
Status: "expired",
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
})
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
Size: 1024 * 1024,
Status: "completed",
ExpiresUnix: 0,
})
jobLink := func(jobID int64) string {
return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID)
}
// Keep devtest mock runs minimal: use run 10 as a "complex graph" repro.
// This combines long durations, parallel roots, and a multi-dependency downstream job
// to validate the workflow graph rendering.
if runID == 10 {
resp.State.Run.WorkflowID = "workflow-devtest-complex"
resp.State.Run.Duration = "7h 12m 34s"
type mj struct {
jobID string
name string
status actions_model.Status
duration string
needs []string
}
mockJobs := []mj{
{jobID: "job-100", name: "job-100", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
{jobID: "job-101", name: "job-101", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"job-100"}},
{jobID: "job-102", name: "job-102", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"job-100", "job-101"}},
{jobID: "job-103", name: "job-103", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"job-100"}},
{jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
{jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
// Matrix expansion (the " (...)" suffix is the heuristic the frontend uses to group rows)
{jobID: "matrix-e2e-1-chromium", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
{jobID: "matrix-e2e-1-firefox", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
{jobID: "matrix-e2e-2-chromium", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
{jobID: "matrix-e2e-3-chromium", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
{jobID: "matrix-e2e-3-firefox", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
{jobID: "matrix-e2e-99-webkit", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
{jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
{jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
{jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
{jobID: "build-image", name: "build-image", status: actions_model.StatusSuccess, duration: "3s", needs: []string{
"unit-test",
"arch-test",
"integration-test",
"code-analysis",
"matrix-e2e-1-chromium",
"matrix-e2e-1-firefox",
"matrix-e2e-2-chromium",
"matrix-e2e-3-chromium",
"matrix-e2e-3-firefox",
"matrix-e2e-99-webkit",
}},
}
resp.State.Run.Jobs = nil
for i, j := range mockJobs {
id := runID*1000 + int64(i)
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: id,
Link: jobLink(id),
JobID: j.jobID,
Name: j.name,
Status: j.status.String(),
CanRerun: j.jobID == "job-100",
Duration: j.duration,
Needs: j.needs,
})
}
fillViewRunResponseCurrentJob(ctx, resp)
ctx.JSON(http.StatusOK, resp)
return
}
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID * 10,
Link: jobLink(runID * 10),
JobID: "job-100",
Name: "job 100 (testsubname)",
Status: actions_model.StatusRunning.String(),
CanRerun: true,
Duration: "1h23m45s",
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID*10 + 1,
Link: jobLink(runID*10 + 1),
JobID: "job-101",
Name: "job 101",
Status: actions_model.StatusWaiting.String(),
CanRerun: false,
Duration: "2h",
Needs: []string{"job-100"},
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID*10 + 2,
Link: jobLink(runID*10 + 2),
JobID: "job-102",
Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
Status: actions_model.StatusFailure.String(),
CanRerun: false,
Duration: "3h35m10s",
Needs: []string{"job-100", "job-101"},
})
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: runID*10 + 3,
Link: jobLink(runID*10 + 3),
JobID: "job-103",
Name: "job 103",
Status: actions_model.StatusCancelled.String(),
CanRerun: false,
Duration: "2m",
Needs: []string{"job-100"},
})
// add more jobs to a run for UI testing
if resp.State.Run.CanCancel {
for i := range 10 {
jobID := runID*1000 + int64(i)
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
ID: jobID,
Link: jobLink(jobID),
JobID: "job-dup-test-" + strconv.Itoa(i),
Name: "job dup test " + strconv.Itoa(i),
Status: actions_model.StatusSuccess.String(),
CanRerun: false,
Duration: "2m",
Needs: []string{"job-103", "job-101", "job-100"},
})
}
}
if runID == 40 {
// Reusable workflow caller demo: same-repo caller (with a nested same-repo caller inside),
// alongside a flat cross-repo caller.
// Layout:
// prepare (regular, top-level)
// local_caller (caller, same-repo, expanded)
// ├ lib_step (regular)
// └ inner_caller (caller, same-repo nested, expanded)
// └ deep_job (regular)
// cross_caller (caller, cross-repo, expanded)
// └ external_job (regular)
// build (linux|windows|macos) (regular matrix; graph folds into one "build" node)
// build-call (linux|windows|macos) (caller matrix, each calls build.yml; folds into one "build-call" node like "build")
// final (regular, needs local_caller + cross_caller)
const (
prepareID = int64(400)
localCallerID = int64(401)
libStepID = int64(402)
innerCallerID = int64(403)
deepJobID = int64(404)
crossCallerID = int64(405)
externalJobID = int64(406)
finalID = int64(407)
// Regular matrix set the graph already folds these into a single "build" node.
buildLinuxID = int64(410)
buildWindowsID = int64(411)
buildMacosID = int64(412)
// Caller matrix set each matrix leg calls the same reusable workflow. #38466: like the
// regular "build" matrix above, these fold into one "build-call" node. Matrix legs share a
// single JobID, so the legs below use JobID "build-call" and differ only by their name suffix.
buildCallLinuxID = int64(420)
buildCallWindowsID = int64(421)
buildCallMacosID = int64(422)
buildCallLinuxJobID = int64(423)
buildCallWinJobID = int64(424)
buildCallMacJobID = int64(425)
)
resp.State.Run.Jobs = []*actions.ViewJob{
{
ID: prepareID, Link: jobLink(prepareID), JobID: "prepare", Name: "prepare",
Status: actions_model.StatusSuccess.String(), Duration: "30s",
},
{
ID: localCallerID, Link: jobLink(localCallerID), JobID: "local_caller", Name: "local caller",
Status: actions_model.StatusRunning.String(), Duration: "5m",
Needs: []string{"prepare"},
IsReusableCaller: true, CallUses: "./.gitea/workflows/lib.yml",
},
{
ID: libStepID, Link: jobLink(libStepID), JobID: "lib_step", Name: "lib step",
Status: actions_model.StatusSuccess.String(), Duration: "1m",
ParentJobID: localCallerID,
},
{
ID: innerCallerID, Link: jobLink(innerCallerID), JobID: "inner_caller", Name: "inner caller (nested)",
Status: actions_model.StatusRunning.String(), Duration: "4m",
ParentJobID: localCallerID,
IsReusableCaller: true, CallUses: "./.gitea/workflows/inner.yml",
},
{
ID: deepJobID, Link: jobLink(deepJobID), JobID: "deep_job", Name: "deep job",
Status: actions_model.StatusRunning.String(), Duration: "2m",
ParentJobID: innerCallerID,
},
{
ID: crossCallerID, Link: jobLink(crossCallerID), JobID: "cross_caller", Name: "cross-repo caller",
Status: actions_model.StatusWaiting.String(), Duration: "0s",
Needs: []string{"prepare"},
IsReusableCaller: true, CallUses: "user2/lib-repo/.gitea/workflows/external.yml@main",
},
{
ID: externalJobID, Link: jobLink(externalJobID), JobID: "external_job", Name: "external job",
Status: actions_model.StatusWaiting.String(), Duration: "0s",
ParentJobID: crossCallerID,
},
// Regular matrix "build" these fold into one matrix node in the graph. The matrix legs
// share a single JobID ("build"); the " (variant)" name suffix distinguishes the legs.
{
ID: buildLinuxID, Link: jobLink(buildLinuxID), JobID: "build", Name: "build (linux)",
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
},
{
ID: buildWindowsID, Link: jobLink(buildWindowsID), JobID: "build", Name: "build (windows)",
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
},
{
ID: buildMacosID, Link: jobLink(buildMacosID), JobID: "build", Name: "build (macos)",
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
},
// Caller matrix "build-call" each leg calls the same reusable workflow. #38466: like the
// regular "build" matrix above, these fold into one node. The matrix legs share a single
// JobID ("build-call"); the " (variant)" name suffix distinguishes the legs.
{
ID: buildCallLinuxID, Link: jobLink(buildCallLinuxID), JobID: "build-call", Name: "build-call (linux)",
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
},
{
ID: buildCallLinuxJobID, Link: jobLink(buildCallLinuxJobID), JobID: "bc_linux_build", Name: "build",
Status: actions_model.StatusSuccess.String(), Duration: "1m", ParentJobID: buildCallLinuxID,
},
{
ID: buildCallWindowsID, Link: jobLink(buildCallWindowsID), JobID: "build-call", Name: "build-call (windows)",
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
},
{
ID: buildCallWinJobID, Link: jobLink(buildCallWinJobID), JobID: "bc_windows_build", Name: "build",
Status: actions_model.StatusSuccess.String(), Duration: "2m", ParentJobID: buildCallWindowsID,
},
{
ID: buildCallMacosID, Link: jobLink(buildCallMacosID), JobID: "build-call", Name: "build-call (macos)",
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
},
{
ID: buildCallMacJobID, Link: jobLink(buildCallMacJobID), JobID: "bc_macos_build", Name: "build",
Status: actions_model.StatusSuccess.String(), Duration: "90s", ParentJobID: buildCallMacosID,
},
{
ID: finalID, Link: jobLink(finalID), JobID: "final", Name: "final",
Status: actions_model.StatusBlocked.String(), Duration: "0s",
Needs: []string{"local_caller", "cross_caller"},
},
}
}
fillViewRunResponseCurrentJob(ctx, resp)
ctx.JSON(http.StatusOK, resp)
}
func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewResponse) {
jobID := ctx.PathParamInt64("job")
if jobID == 0 {
return
}
for _, job := range resp.State.Run.Jobs {
if job.ID == jobID {
resp.State.CurrentJob.Title = job.Name
resp.State.CurrentJob.Detail = job.Status
break
}
}
req := web.GetForm(ctx).(*actions.ViewRequest)
var mockLogOptions []generateMockStepsLogOptions
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 0 (mock slow)",
Duration: time.Hour.String(),
Status: actions_model.StatusRunning.String(),
})
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 1, groupRepeat: 3})
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 1 (mock fast)",
Duration: time.Hour.String(),
Status: actions_model.StatusRunning.String(),
})
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 20})
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 2 (mock error)",
Duration: time.Hour.String(),
Status: actions_model.StatusRunning.String(),
})
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 3})
if len(req.LogCursors) == 0 {
return
}
resp.Logs.StepsLog = []*actions.ViewStepLog{}
doSlowResponse := false
doErrorResponse := false
for _, logCur := range req.LogCursors {
if !logCur.Expanded {
continue
}
doSlowResponse = doSlowResponse || logCur.Step == 0
doErrorResponse = doErrorResponse || logCur.Step == 2
resp.Logs.StepsLog = append(resp.Logs.StepsLog, generateMockStepsLog(logCur, mockLogOptions[logCur.Step])...)
}
if doErrorResponse {
if mathRand.Float64() > 0.5 {
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
return
}
}
if doSlowResponse {
time.Sleep(time.Duration(3000) * time.Millisecond)
} else {
time.Sleep(time.Duration(100) * time.Millisecond) // actually, frontend reload every 1 second, any smaller delay is fine
}
}