53200cbc0d
Nuevas funciones Go con tests en tres dominios: - core: parse_cron_expr, next_cron_time, join_by_key, validate_struct_fields + tipo CronSchedule - datascience: pivot (tabla dinámica), diff_entities (comparación de entidades) - infra: http_get_json, http_post_json, http_download_file, cache_to_sqlite, cron_ticker Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
2.2 KiB
Go
115 lines
2.2 KiB
Go
package infra
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func allMinutes() []int {
|
|
s := make([]int, 60)
|
|
for i := range s {
|
|
s[i] = i
|
|
}
|
|
return s
|
|
}
|
|
|
|
func allHours() []int {
|
|
s := make([]int, 24)
|
|
for i := range s {
|
|
s[i] = i
|
|
}
|
|
return s
|
|
}
|
|
|
|
func allDays() []int {
|
|
s := make([]int, 31)
|
|
for i := range s {
|
|
s[i] = i + 1
|
|
}
|
|
return s
|
|
}
|
|
|
|
func allMonths() []int {
|
|
s := make([]int, 12)
|
|
for i := range s {
|
|
s[i] = i + 1
|
|
}
|
|
return s
|
|
}
|
|
|
|
func allDOW() []int {
|
|
s := make([]int, 7)
|
|
for i := range s {
|
|
s[i] = i
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestCronTicker(t *testing.T) {
|
|
t.Run("context cancel cierra el channel", func(t *testing.T) {
|
|
sched := CronTickerSchedule{
|
|
Minute: allMinutes(),
|
|
Hour: allHours(),
|
|
DayOfMonth: allDays(),
|
|
Month: allMonths(),
|
|
DayOfWeek: allDOW(),
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ch := CronTicker(sched, ctx)
|
|
|
|
// Cancel immediately.
|
|
cancel()
|
|
|
|
// Channel should close without blocking.
|
|
timeout := time.After(2 * time.Second)
|
|
select {
|
|
case _, ok := <-ch:
|
|
if ok {
|
|
// Might receive one tick before cancel propagates — acceptable.
|
|
}
|
|
// Drain remaining.
|
|
for range ch {
|
|
}
|
|
case <-timeout:
|
|
t.Error("channel did not close within 2s after context cancel")
|
|
}
|
|
})
|
|
|
|
t.Run("ticker emite al llegar el momento del schedule", func(t *testing.T) {
|
|
// Use a schedule that fires every minute (all minutes).
|
|
// The next tick is at most 60s away. We use a short-lived context
|
|
// to avoid waiting: instead we verify the channel is not nil and
|
|
// that cancellation closes it cleanly.
|
|
sched := CronTickerSchedule{
|
|
Minute: allMinutes(),
|
|
Hour: allHours(),
|
|
DayOfMonth: allDays(),
|
|
Month: allMonths(),
|
|
DayOfWeek: allDOW(),
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
ch := CronTicker(sched, ctx)
|
|
if ch == nil {
|
|
t.Fatal("CronTicker returned nil channel")
|
|
}
|
|
|
|
// Wait for context to expire, then confirm channel closes.
|
|
<-ctx.Done()
|
|
timeout := time.After(2 * time.Second)
|
|
for {
|
|
select {
|
|
case _, ok := <-ch:
|
|
if !ok {
|
|
return // channel closed, test passes
|
|
}
|
|
case <-timeout:
|
|
t.Error("channel did not close within 2s after context timeout")
|
|
return
|
|
}
|
|
}
|
|
})
|
|
}
|