feat: funciones Go — core (cron, join_by_key, validate_struct), datascience (pivot, diff_entities), infra (http, cache, cron_ticker)
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>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func tempDB(t *testing.T) string {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(t.TempDir(), "cache_*.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func TestCacheToSQLite_SetGet(t *testing.T) {
|
||||
t.Run("Set/Get basico", func(t *testing.T) {
|
||||
c, err := CacheToSQLite(tempDB(t), "default")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
payload, _ := json.Marshal(map[string]int{"x": 1})
|
||||
if err := c.Set("foo", payload, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := c.Get("foo")
|
||||
if !ok {
|
||||
t.Fatal("expected cache hit")
|
||||
}
|
||||
var result map[string]int
|
||||
json.Unmarshal(got, &result)
|
||||
if result["x"] != 1 {
|
||||
t.Errorf("got %v, want x=1", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheToSQLite_TTLExpirado(t *testing.T) {
|
||||
t.Run("TTL expirado", func(t *testing.T) {
|
||||
c, err := CacheToSQLite(tempDB(t), "default")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
payload, _ := json.Marshal("hello")
|
||||
c.Set("temp", payload, 50*time.Millisecond)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
_, ok := c.Get("temp")
|
||||
if ok {
|
||||
t.Error("expected cache miss after TTL expiry")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheToSQLite_GetOrSet(t *testing.T) {
|
||||
t.Run("GetOrSet con factory", func(t *testing.T) {
|
||||
c, err := CacheToSQLite(tempDB(t), "default")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
calls := 0
|
||||
factory := func() ([]byte, error) {
|
||||
calls++
|
||||
return json.Marshal("computed")
|
||||
}
|
||||
|
||||
v1, err := c.GetOrSet("k", factory, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v2, err := c.GetOrSet("k", factory, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(v1) != string(v2) {
|
||||
t.Errorf("v1=%s v2=%s, want equal", v1, v2)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Errorf("factory called %d times, want 1", calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCacheToSQLite_Concurrencia(t *testing.T) {
|
||||
t.Run("Concurrencia (goroutines)", func(t *testing.T) {
|
||||
c, err := CacheToSQLite(tempDB(t), "parallel")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, 40)
|
||||
for i := 0; i < 20; i++ {
|
||||
wg.Add(1)
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
key := fmt.Sprintf("key_%d", n)
|
||||
payload, _ := json.Marshal(n)
|
||||
if err := c.Set(key, payload, 0); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
got, ok := c.Get(key)
|
||||
if !ok {
|
||||
errs <- fmt.Errorf("miss for key %s", key)
|
||||
return
|
||||
}
|
||||
var val int
|
||||
json.Unmarshal(got, &val)
|
||||
if val != n {
|
||||
errs <- fmt.Errorf("key %s: got %d want %d", key, val, n)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user