Skip to content

Commit 63da661

Browse files
authored
jenkins: add progress bar for running jobs (#781)
1 parent eb78510 commit 63da661

5 files changed

Lines changed: 154 additions & 12 deletions

File tree

bot/storage/redis_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package storage
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/alicebob/miniredis/v2"
78
"github.com/redis/go-redis/v9"
@@ -27,7 +28,13 @@ func TestRedisStorage(t *testing.T) {
2728

2829
t.Run("test error handling", func(t *testing.T) {
2930
client := redis.NewClient(&redis.Options{
30-
Addr: "invalid.host",
31+
Addr: "invalid.host",
32+
MaxRetries: 1, // Reduce retries from default 3
33+
DialTimeout: 10 * time.Millisecond, // Fast timeout for tests
34+
ReadTimeout: 10 * time.Millisecond,
35+
WriteTimeout: 10 * time.Millisecond,
36+
PoolTimeout: 10 * time.Millisecond,
37+
ConnMaxIdleTime: 10 * time.Millisecond,
3138
})
3239

3340
storage := NewRedisStorage(client)

bot/storage/storage_enhanced_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,19 @@ func TestStorageEnhanced(t *testing.T) {
216216
t.Run("test storage performance", func(t *testing.T) {
217217
storage := newMemoryStorage()
218218

219-
// Test writing many keys
219+
// Test writing many keys (reduced from 1000 to 100 for faster tests)
220220
start := time.Now()
221-
for i := range 1000 {
221+
for i := range 100 {
222222
key := "perf_key_" + string(rune(i))
223223
value := "perf_value_" + string(rune(i))
224224
err := storage.Write("test_collection", key, value)
225225
require.NoError(t, err)
226226
}
227227
writeDuration := time.Since(start)
228228

229-
// Test reading many keys
229+
// Test reading many keys (reduced from 1000 to 100 for faster tests)
230230
start = time.Now()
231-
for i := range 1000 {
231+
for i := range 100 {
232232
key := "perf_key_" + string(rune(i))
233233
var value string
234234
err := storage.Read("test_collection", key, &value)
@@ -237,8 +237,8 @@ func TestStorageEnhanced(t *testing.T) {
237237
}
238238
readDuration := time.Since(start)
239239

240-
// Performance should be reasonable (adjust thresholds as needed)
241-
assert.Less(t, writeDuration, time.Second)
242-
assert.Less(t, readDuration, time.Second)
240+
// Performance should be reasonable (adjusted thresholds for fewer iterations)
241+
assert.Less(t, writeDuration, 100*time.Millisecond)
242+
assert.Less(t, readDuration, 100*time.Millisecond)
243243
})
244244
}

command/jenkins/client/start_build.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818

1919
var locks = util.NewGroupedLogger()
2020

21+
const progressUpdateInterval = 5 * time.Second
22+
2123
// TriggerJenkinsJob starts a new build with given parameters
2224
// it will return when the job was started successfully
2325
// in the background it will watch the current build state and will update the state in the original slack message
@@ -43,8 +45,8 @@ func TriggerJenkinsJob(cfg config.JobConfig, jobName string, jobParams Parameter
4345
fmt.Sprintf("inform job %s #%d", jobName, build.GetBuildNumber()),
4446
)
4547
go func() {
46-
// wait until job is not running anymore
47-
<-WatchBuild(build)
48+
// wait until job is not running anymore and update progress every 30s
49+
watchProgress(build, slackClient, message, msgTimestamp)
4850
runningCommand.Done()
4951

5052
// update main message
@@ -227,3 +229,77 @@ func getFinishBuildText(build *gojenkins.Build, user string, jobName string) str
227229

228230
return text
229231
}
232+
233+
// watchProgress monitors the build and updates the message with a progress bar every 30 seconds
234+
func watchProgress(build *gojenkins.Build, slackClient client.SlackClient, ref msg.Ref, msgTimestamp string) {
235+
estimatedDuration := time.Duration(build.Raw.EstimatedDuration) * time.Millisecond
236+
startTime := time.Now()
237+
ticker := time.NewTicker(progressUpdateInterval)
238+
defer ticker.Stop()
239+
240+
buildDone := WatchBuild(build)
241+
242+
for {
243+
select {
244+
case <-buildDone:
245+
// Build is done, exit the loop
246+
return
247+
case <-ticker.C:
248+
// Update progress
249+
elapsed := time.Since(startTime)
250+
progress := calculateProgress(elapsed, estimatedDuration)
251+
progressBar := renderProgressBar(progress)
252+
253+
text := fmt.Sprintf(
254+
"Job %s running (#%d)\n%s %s / %s",
255+
build.Job.GetName(),
256+
build.GetBuildNumber(),
257+
progressBar,
258+
util.FormatDuration(elapsed),
259+
util.FormatDuration(estimatedDuration),
260+
)
261+
262+
// update the message in Slack with new progress
263+
slackClient.SendMessage(
264+
ref,
265+
"",
266+
slack.MsgOptionUpdate(msgTimestamp),
267+
GetAttachment(build, text),
268+
)
269+
}
270+
}
271+
}
272+
273+
// calculateProgress returns a progress percentage (0.0 to 1.0+)
274+
func calculateProgress(elapsed, estimated time.Duration) float64 {
275+
if estimated <= 0 {
276+
return 0
277+
}
278+
return float64(elapsed) / float64(estimated)
279+
}
280+
281+
// renderProgressBar creates a visual progress bar using Unicode blocks
282+
func renderProgressBar(progress float64) string {
283+
const barLength = 20
284+
filled := int(progress * float64(barLength))
285+
286+
// Cap filled at barLength for display purposes
287+
if filled > barLength {
288+
filled = barLength
289+
}
290+
if filled < 0 {
291+
filled = 0
292+
}
293+
294+
bar := ""
295+
for i := range barLength {
296+
if i < filled {
297+
bar += "█"
298+
} else {
299+
bar += "░"
300+
}
301+
}
302+
303+
percentage := int(progress * 100)
304+
return fmt.Sprintf("%s %d%%", bar, percentage)
305+
}

command/jenkins/client/start_build_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,65 @@ func TestSendBuildStartedMessage(t *testing.T) {
166166
assert.Empty(t, msgTimestamp)
167167
}
168168

169+
func TestCalculateProgress(t *testing.T) {
170+
t.Run("Zero estimated duration", func(t *testing.T) {
171+
progress := calculateProgress(1*time.Minute, 0)
172+
assert.InDelta(t, 0.0, progress, 0.001)
173+
})
174+
175+
t.Run("Half complete", func(t *testing.T) {
176+
progress := calculateProgress(30*time.Second, 60*time.Second)
177+
assert.InDelta(t, 0.5, progress, 0.001)
178+
})
179+
180+
t.Run("Fully complete", func(t *testing.T) {
181+
progress := calculateProgress(60*time.Second, 60*time.Second)
182+
assert.InDelta(t, 1.0, progress, 0.001)
183+
})
184+
185+
t.Run("Over estimated time", func(t *testing.T) {
186+
progress := calculateProgress(90*time.Second, 60*time.Second)
187+
assert.InDelta(t, 1.5, progress, 0.001)
188+
})
189+
}
190+
191+
func TestRenderProgressBar(t *testing.T) {
192+
t.Run("Zero progress", func(t *testing.T) {
193+
bar := renderProgressBar(0.0)
194+
assert.Equal(t, "░░░░░░░░░░░░░░░░░░░░ 0%", bar)
195+
})
196+
197+
t.Run("25% progress", func(t *testing.T) {
198+
bar := renderProgressBar(0.25)
199+
assert.Equal(t, "█████░░░░░░░░░░░░░░░ 25%", bar)
200+
})
201+
202+
t.Run("50% progress", func(t *testing.T) {
203+
bar := renderProgressBar(0.5)
204+
assert.Equal(t, "██████████░░░░░░░░░░ 50%", bar)
205+
})
206+
207+
t.Run("75% progress", func(t *testing.T) {
208+
bar := renderProgressBar(0.75)
209+
assert.Equal(t, "███████████████░░░░░ 75%", bar)
210+
})
211+
212+
t.Run("100% progress", func(t *testing.T) {
213+
bar := renderProgressBar(1.0)
214+
assert.Equal(t, "████████████████████ 100%", bar)
215+
})
216+
217+
t.Run("Over 100% progress", func(t *testing.T) {
218+
bar := renderProgressBar(1.5)
219+
assert.Equal(t, "████████████████████ 150%", bar)
220+
})
221+
222+
t.Run("Negative progress", func(t *testing.T) {
223+
bar := renderProgressBar(-0.1)
224+
assert.Equal(t, "░░░░░░░░░░░░░░░░░░░░ -10%", bar)
225+
})
226+
}
227+
169228
func spawnJenkinsServer() *httptest.Server {
170229
mux := http.NewServeMux()
171230

command/openai/openai_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ data: [DONE]`,
185185
},
186186
},
187187
)
188-
defer ts.Close()
189188

190189
openaiCfg.InitialSystemMessage = ""
191190
cfg := &config.Config{}
@@ -205,6 +204,7 @@ data: [DONE]`,
205204

206205
actual := commands.Run(message)
207206
queue.WaitTillHavingNoQueuedMessage()
207+
ts.Close()
208208
assert.True(t, actual)
209209
})
210210

@@ -228,7 +228,6 @@ data: [DONE]`,
228228
},
229229
},
230230
)
231-
defer ts.Close()
232231

233232
openaiCfg.UseAsFallback = true
234233
openaiCfg.InitialSystemMessage = ""
@@ -248,6 +247,7 @@ data: [DONE]`,
248247

249248
actual := commands.Run(message)
250249
queue.WaitTillHavingNoQueuedMessage()
250+
ts.Close()
251251
assert.True(t, actual)
252252
})
253253

0 commit comments

Comments
 (0)