Skip to content

Commit a1f9117

Browse files
authored
Merge pull request #61 from codecrafters-io/progress-bar
Progress bar
2 parents 76cd8c3 + a991af8 commit a1f9117

File tree

3 files changed

+173
-9
lines changed

3 files changed

+173
-9
lines changed

internal/actions/action.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package actions
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/codecrafters-io/cli/internal/client"
@@ -10,6 +11,11 @@ type Action interface {
1011
Execute() error
1112
}
1213

14+
type InterruptibleAction interface {
15+
Action
16+
ExecuteWithContext(ctx context.Context) error
17+
}
18+
1319
func ActionFromDefinition(actionDefinition client.ActionDefinition) (Action, error) {
1420
switch actionDefinition.Type {
1521
case "await_terminal_build_status":
@@ -24,6 +30,8 @@ func ActionFromDefinition(actionDefinition client.ActionDefinition) (Action, err
2430
return NewPrintFileDiffAction(actionDefinition.Args)
2531
case "print_message":
2632
return NewPrintMessageAction(actionDefinition.Args)
33+
case "print_progress_bar":
34+
return NewPrintProgressBarAction(actionDefinition.Args)
2735
case "print_terminal_commands_box":
2836
return NewPrintTerminalCommandsBoxAction(actionDefinition.Args)
2937
case "sleep":

internal/actions/await_terminal_autofix_request_status.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package actions
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"time"
@@ -10,15 +11,17 @@ import (
1011
)
1112

1213
type AwaitTerminalAutofixRequestStatusAction struct {
13-
OnFailureActions []Action
14-
OnSuccessActions []Action
15-
SubmissionID string
14+
InProgressActions []Action
15+
OnFailureActions []Action
16+
OnSuccessActions []Action
17+
SubmissionID string
1618
}
1719

1820
type AwaitTerminalAutofixRequestStatusActionArgs struct {
19-
OnFailureActions []client.ActionDefinition `json:"on_failure_actions"`
20-
OnSuccessActions []client.ActionDefinition `json:"on_success_actions"`
21-
SubmissionID string `json:"submission_id"`
21+
InProgressActions []client.ActionDefinition `json:"in_progress_actions"`
22+
OnFailureActions []client.ActionDefinition `json:"on_failure_actions"`
23+
OnSuccessActions []client.ActionDefinition `json:"on_success_actions"`
24+
SubmissionID string `json:"submission_id"`
2225
}
2326

2427
func NewAwaitTerminalAutofixRequestStatusAction(argsJson json.RawMessage) (*AwaitTerminalAutofixRequestStatusAction, error) {
@@ -47,17 +50,41 @@ func NewAwaitTerminalAutofixRequestStatusAction(argsJson json.RawMessage) (*Awai
4750
onFailureActions = append(onFailureActions, action)
4851
}
4952

53+
inProgressActions := []Action{}
54+
for _, actionDefinition := range awaitTerminalAutofixRequestStatusActionArgs.InProgressActions {
55+
action, err := ActionFromDefinition(actionDefinition)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
inProgressActions = append(inProgressActions, action)
61+
}
62+
5063
return &AwaitTerminalAutofixRequestStatusAction{
51-
OnFailureActions: onFailureActions,
52-
OnSuccessActions: onSuccessActions,
53-
SubmissionID: awaitTerminalAutofixRequestStatusActionArgs.SubmissionID,
64+
InProgressActions: inProgressActions,
65+
OnFailureActions: onFailureActions,
66+
OnSuccessActions: onSuccessActions,
67+
SubmissionID: awaitTerminalAutofixRequestStatusActionArgs.SubmissionID,
5468
}, nil
5569
}
5670

5771
func (a *AwaitTerminalAutofixRequestStatusAction) Execute() error {
5872
attempts := 0
5973
autofixRequestStatus := "in_progress"
6074

75+
inProgressActionsDoneCh := make(chan bool)
76+
77+
ctx, cancel := context.WithCancel(context.Background())
78+
defer cancel()
79+
80+
go func() {
81+
if err := a.executeInProgressActions(ctx); err != nil {
82+
sentry.CaptureException(err)
83+
}
84+
85+
inProgressActionsDoneCh <- true
86+
}()
87+
6188
// We wait for upto 60 seconds (+ the time it takes to fetch status each time)
6289
for autofixRequestStatus == "in_progress" && attempts < 60 {
6390
var err error
@@ -75,6 +102,10 @@ func (a *AwaitTerminalAutofixRequestStatusAction) Execute() error {
75102
time.Sleep(time.Second)
76103
}
77104

105+
// Ensure interruptible actions (like printing progress bars) finish early
106+
cancel()
107+
<-inProgressActionsDoneCh
108+
78109
switch autofixRequestStatus {
79110
case "success":
80111
for _, action := range a.OnSuccessActions {
@@ -101,3 +132,19 @@ func (a *AwaitTerminalAutofixRequestStatusAction) Execute() error {
101132

102133
return nil
103134
}
135+
136+
func (a *AwaitTerminalAutofixRequestStatusAction) executeInProgressActions(ctx context.Context) error {
137+
for _, action := range a.InProgressActions {
138+
if interruptibleAction, ok := action.(InterruptibleAction); ok {
139+
if err := interruptibleAction.ExecuteWithContext(ctx); err != nil {
140+
return err
141+
}
142+
} else {
143+
if err := action.Execute(); err != nil {
144+
return err
145+
}
146+
}
147+
}
148+
149+
return nil
150+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package actions
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"math/rand"
8+
"strings"
9+
"time"
10+
)
11+
12+
// The maximum delay between prints in seconds
13+
const maxDelayBetweenPrintsInSeconds = 2
14+
15+
// The minimum number of times we will print the progress bar
16+
const minPrintCount = 5
17+
18+
// The length of the progress bar
19+
const progressBarLength = 20
20+
21+
type PrintProgressBarAction struct {
22+
ExpectedDelayInSeconds int `json:"expected_delay_in_seconds"`
23+
}
24+
25+
func NewPrintProgressBarAction(argsJson json.RawMessage) (PrintProgressBarAction, error) {
26+
var printProgressBarAction PrintProgressBarAction
27+
if err := json.Unmarshal(argsJson, &printProgressBarAction); err != nil {
28+
return PrintProgressBarAction{}, err
29+
}
30+
31+
return printProgressBarAction, nil
32+
}
33+
34+
func (a PrintProgressBarAction) numberOfPrints() int {
35+
return max(a.ExpectedDelayInSeconds/maxDelayBetweenPrintsInSeconds, minPrintCount)
36+
}
37+
38+
func (a PrintProgressBarAction) delayBetweenPrintsInMilliseconds(printsLeft int) int {
39+
if printsLeft <= 1 {
40+
return 60 * 1000 // 60 seconds (The action should timeout by then)
41+
}
42+
43+
return min(maxDelayBetweenPrintsInSeconds*1000, 1000*a.ExpectedDelayInSeconds/a.numberOfPrints())
44+
}
45+
46+
func (a PrintProgressBarAction) Execute() error {
47+
return a.ExecuteWithContext(context.Background())
48+
}
49+
50+
func (a PrintProgressBarAction) ExecuteWithContext(ctx context.Context) error {
51+
contextIsCancelled := false
52+
lastPrintedPercentage := 0
53+
54+
for i := 0; i < a.numberOfPrints(); i++ {
55+
percentage := (i + 1) * 100 / a.numberOfPrints()
56+
numberOfBars := percentage * progressBarLength / 100
57+
numberOfSpaces := progressBarLength - numberOfBars
58+
59+
bars := strings.Repeat("=", numberOfBars)
60+
if numberOfSpaces > 0 {
61+
bars = bars[:len(bars)-1] + ">"
62+
}
63+
64+
// Print with a random jitter of up to 5%
65+
percentageToPrint := percentage
66+
67+
// If this is in-progress, add a random jitter of up to 5%
68+
if percentage < 100 {
69+
percentageToPrint = percentage - 5 + rand.Intn(10)
70+
}
71+
72+
// Ensure the printed percentage is not greater than 100
73+
if percentageToPrint > 100 {
74+
percentageToPrint = 100
75+
}
76+
77+
// Ensure the printed percentage always increases
78+
if percentageToPrint < lastPrintedPercentage {
79+
percentageToPrint = lastPrintedPercentage
80+
}
81+
82+
// Use ANSI color codes for green (same pattern as print_message.go)
83+
greenStart := "\033[32m"
84+
greenEnd := "\033[0m"
85+
fmt.Printf("[%s%s%s%s] %s%s%s\n", greenStart, bars, greenEnd, strings.Repeat(" ", numberOfSpaces), greenStart, fmt.Sprintf("%d%%", percentageToPrint), greenEnd)
86+
lastPrintedPercentage = percentageToPrint
87+
88+
// If the context is cancelled, keep looping until we print all bars and exit (with no delay)
89+
if contextIsCancelled {
90+
continue
91+
}
92+
93+
sleepDurationInMilliseconds := a.delayBetweenPrintsInMilliseconds(a.numberOfPrints() - (i + 1))
94+
95+
// Add a random jitter of up to 500ms
96+
sleepDurationInMilliseconds = sleepDurationInMilliseconds - 500 + rand.Intn(1000)
97+
98+
// If the context is still active, sleep for the max delay between prints
99+
select {
100+
case <-ctx.Done():
101+
contextIsCancelled = true
102+
continue
103+
case <-time.After(time.Duration(sleepDurationInMilliseconds) * time.Millisecond):
104+
continue
105+
}
106+
}
107+
108+
return nil
109+
}

0 commit comments

Comments
 (0)