cricinfo-cli-go 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -0
- package/CONTRIBUTORS.md +75 -0
- package/LICENSE +21 -0
- package/Makefile +131 -0
- package/README.md +130 -0
- package/bin/cricinfo.js +44 -0
- package/cmd/cricinfo/main.go +15 -0
- package/go.mod +10 -0
- package/go.sum +10 -0
- package/internal/app/app.go +11 -0
- package/internal/app/app_test.go +122 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/internal/cli/analysis.go +262 -0
- package/internal/cli/analysis_test.go +175 -0
- package/internal/cli/competitions.go +154 -0
- package/internal/cli/competitions_test.go +165 -0
- package/internal/cli/leagues.go +297 -0
- package/internal/cli/leagues_test.go +194 -0
- package/internal/cli/matches.go +403 -0
- package/internal/cli/matches_test.go +413 -0
- package/internal/cli/players.go +263 -0
- package/internal/cli/players_test.go +384 -0
- package/internal/cli/root.go +141 -0
- package/internal/cli/search.go +119 -0
- package/internal/cli/teams.go +214 -0
- package/internal/cli/teams_test.go +192 -0
- package/internal/cricinfo/analysis.go +1401 -0
- package/internal/cricinfo/analysis_phase15_test.go +267 -0
- package/internal/cricinfo/client.go +471 -0
- package/internal/cricinfo/client_test.go +280 -0
- package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
- package/internal/cricinfo/competitions.go +405 -0
- package/internal/cricinfo/competitions_phase13_test.go +234 -0
- package/internal/cricinfo/coverage_ledger.go +122 -0
- package/internal/cricinfo/coverage_ledger_test.go +253 -0
- package/internal/cricinfo/decode.go +115 -0
- package/internal/cricinfo/decode_test.go +100 -0
- package/internal/cricinfo/entity_index.go +618 -0
- package/internal/cricinfo/entity_index_test.go +175 -0
- package/internal/cricinfo/fixture_matrix.go +243 -0
- package/internal/cricinfo/fixture_matrix_test.go +49 -0
- package/internal/cricinfo/fixtures_test.go +264 -0
- package/internal/cricinfo/historical_hydration.go +1641 -0
- package/internal/cricinfo/historical_phase14_test.go +542 -0
- package/internal/cricinfo/leagues.go +1210 -0
- package/internal/cricinfo/leagues_phase12_test.go +324 -0
- package/internal/cricinfo/live_leagues_test.go +169 -0
- package/internal/cricinfo/live_matches_test.go +203 -0
- package/internal/cricinfo/live_matrix_test.go +118 -0
- package/internal/cricinfo/live_players_test.go +122 -0
- package/internal/cricinfo/live_search_test.go +86 -0
- package/internal/cricinfo/live_smoke_test.go +213 -0
- package/internal/cricinfo/live_teams_test.go +104 -0
- package/internal/cricinfo/matches.go +1508 -0
- package/internal/cricinfo/matches_phase7_test.go +207 -0
- package/internal/cricinfo/matches_phase9_test.go +253 -0
- package/internal/cricinfo/normalize_entities.go +1727 -0
- package/internal/cricinfo/normalize_leagues.go +346 -0
- package/internal/cricinfo/players.go +1332 -0
- package/internal/cricinfo/players_phase10_test.go +174 -0
- package/internal/cricinfo/players_phase11_test.go +373 -0
- package/internal/cricinfo/render_contract.go +1088 -0
- package/internal/cricinfo/render_phase4_test.go +633 -0
- package/internal/cricinfo/renderer.go +1689 -0
- package/internal/cricinfo/resolver.go +813 -0
- package/internal/cricinfo/resolver_test.go +244 -0
- package/internal/cricinfo/teams.go +603 -0
- package/internal/cricinfo/teams_phase8_test.go +231 -0
- package/internal/cricinfo/testdata/fixtures/README.md +43 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
- package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
- package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
- package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
- package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
- package/internal/cricinfo/types.go +54 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +153 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
package cricinfo
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"sync/atomic"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestFetchRetriesServerErrorsAndPreservesCanonicalRef(t *testing.T) {
|
|
15
|
+
t.Parallel()
|
|
16
|
+
|
|
17
|
+
var attempts atomic.Int32
|
|
18
|
+
slept := make([]time.Duration, 0, 2)
|
|
19
|
+
|
|
20
|
+
var server *httptest.Server
|
|
21
|
+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
22
|
+
if r.UserAgent() == "" {
|
|
23
|
+
t.Fatalf("expected user agent to be set")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
current := attempts.Add(1)
|
|
27
|
+
if current <= 2 {
|
|
28
|
+
w.WriteHeader(http.StatusServiceUnavailable)
|
|
29
|
+
_, _ = w.Write([]byte(`{"error":"transient"}`))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
w.Header().Set("Content-Type", "application/json")
|
|
34
|
+
_, _ = w.Write([]byte(`{"$ref":"` + server.URL + `/canonical/events","count":0,"items":[],"pageCount":0,"pageIndex":1,"pageSize":25}`))
|
|
35
|
+
}))
|
|
36
|
+
defer server.Close()
|
|
37
|
+
|
|
38
|
+
client, err := NewClient(Config{
|
|
39
|
+
BaseURL: server.URL,
|
|
40
|
+
MaxRetries: 2,
|
|
41
|
+
RetryBaseDelay: 10 * time.Millisecond,
|
|
42
|
+
RetryMaxDelay: 25 * time.Millisecond,
|
|
43
|
+
RetryJitter: 0.25,
|
|
44
|
+
RandomFloat64: func() float64 { return 0.5 },
|
|
45
|
+
Sleep: func(_ context.Context, d time.Duration) error {
|
|
46
|
+
slept = append(slept, d)
|
|
47
|
+
return nil
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
if err != nil {
|
|
51
|
+
t.Fatalf("NewClient error: %v", err)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
doc, err := client.Fetch(context.Background(), "/events")
|
|
55
|
+
if err != nil {
|
|
56
|
+
t.Fatalf("Fetch error: %v", err)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if got := attempts.Load(); got != 3 {
|
|
60
|
+
t.Fatalf("expected 3 attempts, got %d", got)
|
|
61
|
+
}
|
|
62
|
+
if len(slept) != 2 {
|
|
63
|
+
t.Fatalf("expected 2 backoff sleeps, got %d", len(slept))
|
|
64
|
+
}
|
|
65
|
+
if slept[0] != 10*time.Millisecond || slept[1] != 20*time.Millisecond {
|
|
66
|
+
t.Fatalf("unexpected backoff durations: %v", slept)
|
|
67
|
+
}
|
|
68
|
+
if doc.RequestedRef != server.URL+"/events" {
|
|
69
|
+
t.Fatalf("unexpected requested ref: %q", doc.RequestedRef)
|
|
70
|
+
}
|
|
71
|
+
if doc.CanonicalRef != server.URL+"/canonical/events" {
|
|
72
|
+
t.Fatalf("unexpected canonical ref: %q", doc.CanonicalRef)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestFetchTimeout(t *testing.T) {
|
|
77
|
+
t.Parallel()
|
|
78
|
+
|
|
79
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
80
|
+
time.Sleep(200 * time.Millisecond)
|
|
81
|
+
w.WriteHeader(http.StatusOK)
|
|
82
|
+
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
83
|
+
}))
|
|
84
|
+
defer server.Close()
|
|
85
|
+
|
|
86
|
+
client, err := NewClient(Config{
|
|
87
|
+
BaseURL: server.URL,
|
|
88
|
+
Timeout: 25 * time.Millisecond,
|
|
89
|
+
MaxRetries: 0,
|
|
90
|
+
})
|
|
91
|
+
if err != nil {
|
|
92
|
+
t.Fatalf("NewClient error: %v", err)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
start := time.Now()
|
|
96
|
+
_, err = client.Fetch(context.Background(), "/slow")
|
|
97
|
+
if err == nil {
|
|
98
|
+
t.Fatal("expected timeout error")
|
|
99
|
+
}
|
|
100
|
+
if !errors.Is(err, context.DeadlineExceeded) {
|
|
101
|
+
t.Fatalf("expected deadline exceeded, got %v", err)
|
|
102
|
+
}
|
|
103
|
+
if elapsed := time.Since(start); elapsed > 150*time.Millisecond {
|
|
104
|
+
t.Fatalf("timeout took too long: %v", elapsed)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func TestFetchDoesNotRetryAfterContextCancel(t *testing.T) {
|
|
109
|
+
t.Parallel()
|
|
110
|
+
|
|
111
|
+
var hits atomic.Int32
|
|
112
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
113
|
+
hits.Add(1)
|
|
114
|
+
w.WriteHeader(http.StatusOK)
|
|
115
|
+
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
116
|
+
}))
|
|
117
|
+
defer server.Close()
|
|
118
|
+
|
|
119
|
+
client, err := NewClient(Config{BaseURL: server.URL, MaxRetries: 3})
|
|
120
|
+
if err != nil {
|
|
121
|
+
t.Fatalf("NewClient error: %v", err)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
125
|
+
cancel()
|
|
126
|
+
|
|
127
|
+
_, err = client.Fetch(ctx, "/events")
|
|
128
|
+
if err == nil {
|
|
129
|
+
t.Fatal("expected canceled context error")
|
|
130
|
+
}
|
|
131
|
+
if !errors.Is(err, context.Canceled) {
|
|
132
|
+
t.Fatalf("expected context canceled error, got %v", err)
|
|
133
|
+
}
|
|
134
|
+
if got := hits.Load(); got > 1 {
|
|
135
|
+
t.Fatalf("expected at most one hit after cancellation, got %d", got)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestRetryDelayBackoffJitterAndCap(t *testing.T) {
|
|
140
|
+
t.Parallel()
|
|
141
|
+
|
|
142
|
+
client := &Client{
|
|
143
|
+
retryBaseDelay: 100 * time.Millisecond,
|
|
144
|
+
retryMaxDelay: 250 * time.Millisecond,
|
|
145
|
+
retryJitter: 0.5,
|
|
146
|
+
randomFloat64: func() float64 { return 1.0 },
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if got := client.retryDelay(0); got != 150*time.Millisecond {
|
|
150
|
+
t.Fatalf("attempt 0 delay mismatch: %v", got)
|
|
151
|
+
}
|
|
152
|
+
if got := client.retryDelay(1); got != 250*time.Millisecond {
|
|
153
|
+
t.Fatalf("attempt 1 delay should cap at max delay, got %v", got)
|
|
154
|
+
}
|
|
155
|
+
if got := client.retryDelay(8); got != 250*time.Millisecond {
|
|
156
|
+
t.Fatalf("late-attempt delay should stay capped, got %v", got)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func TestResolveRefTreatsLeadingSlashAsAPIRelative(t *testing.T) {
|
|
161
|
+
t.Parallel()
|
|
162
|
+
|
|
163
|
+
client, err := NewClient(Config{BaseURL: DefaultBaseURL})
|
|
164
|
+
if err != nil {
|
|
165
|
+
t.Fatalf("NewClient error: %v", err)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
got, err := client.resolveRef("/events")
|
|
169
|
+
if err != nil {
|
|
170
|
+
t.Fatalf("resolveRef error: %v", err)
|
|
171
|
+
}
|
|
172
|
+
if got != DefaultBaseURL+"/events" {
|
|
173
|
+
t.Fatalf("unexpected API-relative URL: %q", got)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
absoluteGot, absoluteErr := client.resolveRef("/v2/sports/cricket/events")
|
|
177
|
+
if absoluteErr != nil {
|
|
178
|
+
t.Fatalf("resolveRef absolute path error: %v", absoluteErr)
|
|
179
|
+
}
|
|
180
|
+
if absoluteGot != DefaultBaseURL+"/events" {
|
|
181
|
+
t.Fatalf("unexpected absolute API URL: %q", absoluteGot)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestResolveRefChainFollowsPointers(t *testing.T) {
|
|
186
|
+
t.Parallel()
|
|
187
|
+
|
|
188
|
+
var server *httptest.Server
|
|
189
|
+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
190
|
+
switch r.URL.Path {
|
|
191
|
+
case "/start":
|
|
192
|
+
_, _ = w.Write([]byte(`{"$ref":"` + server.URL + `/middle"}`))
|
|
193
|
+
case "/middle":
|
|
194
|
+
_, _ = w.Write([]byte(`{"$ref":"/final"}`))
|
|
195
|
+
case "/final":
|
|
196
|
+
_, _ = w.Write([]byte(`{"$ref":"http://core.espnuk.org/v2/sports/cricket/leagues/1174248/events/1/competitions/1/status","summary":"ok"}`))
|
|
197
|
+
default:
|
|
198
|
+
w.WriteHeader(http.StatusNotFound)
|
|
199
|
+
}
|
|
200
|
+
}))
|
|
201
|
+
defer server.Close()
|
|
202
|
+
|
|
203
|
+
client, err := NewClient(Config{BaseURL: server.URL, MaxRetries: 0})
|
|
204
|
+
if err != nil {
|
|
205
|
+
t.Fatalf("NewClient error: %v", err)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resolved, err := client.ResolveRefChain(context.Background(), "/start")
|
|
209
|
+
if err != nil {
|
|
210
|
+
t.Fatalf("ResolveRefChain error: %v", err)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
expectedTraversed := []string{server.URL + "/start", server.URL + "/middle", server.URL + "/final"}
|
|
214
|
+
if len(resolved.TraversedRef) != len(expectedTraversed) {
|
|
215
|
+
t.Fatalf("expected %d traversed refs, got %d", len(expectedTraversed), len(resolved.TraversedRef))
|
|
216
|
+
}
|
|
217
|
+
for i := range expectedTraversed {
|
|
218
|
+
if resolved.TraversedRef[i] != expectedTraversed[i] {
|
|
219
|
+
t.Fatalf("unexpected traversed ref at %d: got %q want %q", i, resolved.TraversedRef[i], expectedTraversed[i])
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if resolved.RequestedRef != server.URL+"/start" {
|
|
224
|
+
t.Fatalf("unexpected requested ref: %q", resolved.RequestedRef)
|
|
225
|
+
}
|
|
226
|
+
if resolved.CanonicalRef != "http://core.espnuk.org/v2/sports/cricket/leagues/1174248/events/1/competitions/1/status" {
|
|
227
|
+
t.Fatalf("unexpected canonical ref: %q", resolved.CanonicalRef)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var payload map[string]any
|
|
231
|
+
if err := json.Unmarshal(resolved.Body, &payload); err != nil {
|
|
232
|
+
t.Fatalf("unmarshal resolved body: %v", err)
|
|
233
|
+
}
|
|
234
|
+
if payload["summary"] != "ok" {
|
|
235
|
+
t.Fatalf("unexpected summary: %v", payload["summary"])
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func TestResolveRefChainDetectsLoop(t *testing.T) {
|
|
240
|
+
t.Parallel()
|
|
241
|
+
|
|
242
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
243
|
+
switch r.URL.Path {
|
|
244
|
+
case "/a":
|
|
245
|
+
_, _ = w.Write([]byte(`{"$ref":"/b"}`))
|
|
246
|
+
case "/b":
|
|
247
|
+
_, _ = w.Write([]byte(`{"$ref":"/a"}`))
|
|
248
|
+
default:
|
|
249
|
+
w.WriteHeader(http.StatusNotFound)
|
|
250
|
+
}
|
|
251
|
+
}))
|
|
252
|
+
defer server.Close()
|
|
253
|
+
|
|
254
|
+
client, err := NewClient(Config{BaseURL: server.URL, MaxRetries: 0})
|
|
255
|
+
if err != nil {
|
|
256
|
+
t.Fatalf("NewClient error: %v", err)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_, err = client.ResolveRefChain(context.Background(), "/a")
|
|
260
|
+
if err == nil {
|
|
261
|
+
t.Fatal("expected loop error")
|
|
262
|
+
}
|
|
263
|
+
if !errors.Is(err, ErrPointerLoop) {
|
|
264
|
+
t.Fatalf("expected pointer loop error, got %v", err)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func TestFollowRefMissing(t *testing.T) {
|
|
269
|
+
t.Parallel()
|
|
270
|
+
|
|
271
|
+
client, err := NewClient(Config{})
|
|
272
|
+
if err != nil {
|
|
273
|
+
t.Fatalf("NewClient error: %v", err)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_, err = client.FollowRef(context.Background(), nil)
|
|
277
|
+
if !errors.Is(err, ErrMissingRef) {
|
|
278
|
+
t.Fatalf("expected missing ref error, got %v", err)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"flag"
|
|
9
|
+
"fmt"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"strings"
|
|
13
|
+
"time"
|
|
14
|
+
|
|
15
|
+
"github.com/amxv/cricinfo-cli/internal/cricinfo"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const defaultOutputRoot = "internal/cricinfo/testdata/fixtures"
|
|
19
|
+
|
|
20
|
+
func main() {
|
|
21
|
+
var (
|
|
22
|
+
familiesRaw string
|
|
23
|
+
outputRoot string
|
|
24
|
+
timeout time.Duration
|
|
25
|
+
maxRetries int
|
|
26
|
+
write bool
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
flag.StringVar(&familiesRaw, "families", "", "comma-separated fixture families to refresh")
|
|
30
|
+
flag.StringVar(&outputRoot, "output", defaultOutputRoot, "fixture output root directory")
|
|
31
|
+
flag.DurationVar(&timeout, "timeout", 12*time.Second, "per-request timeout")
|
|
32
|
+
flag.IntVar(&maxRetries, "max-retries", 3, "max retries per request")
|
|
33
|
+
flag.BoolVar(&write, "write", false, "write fixtures to disk (required for refresh)")
|
|
34
|
+
flag.Parse()
|
|
35
|
+
|
|
36
|
+
if !write {
|
|
37
|
+
fmt.Println("dry-run: pass --write to refresh fixture files")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
selected, err := cricinfo.ParseFixtureFamilies(familiesRaw)
|
|
42
|
+
if err != nil {
|
|
43
|
+
fatalf("parse families: %v", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
matrix := cricinfo.FixtureMatrix()
|
|
47
|
+
matrix = cricinfo.FilterFixtureMatrixByFamily(matrix, selected)
|
|
48
|
+
if len(matrix) == 0 {
|
|
49
|
+
fatalf("no fixture specs selected")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
client, err := cricinfo.NewClient(cricinfo.Config{
|
|
53
|
+
Timeout: timeout,
|
|
54
|
+
MaxRetries: maxRetries,
|
|
55
|
+
})
|
|
56
|
+
if err != nil {
|
|
57
|
+
fatalf("new client: %v", err)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if err := os.MkdirAll(outputRoot, 0o755); err != nil {
|
|
61
|
+
fatalf("create output root %q: %v", outputRoot, err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
refreshed := 0
|
|
65
|
+
keptExisting := 0
|
|
66
|
+
for _, spec := range matrix {
|
|
67
|
+
targetPath := filepath.Join(outputRoot, filepath.FromSlash(spec.FixturePath))
|
|
68
|
+
if err := ensurePathUnderRoot(outputRoot, targetPath); err != nil {
|
|
69
|
+
fatalf("invalid fixture path for %s/%s: %v", spec.Family, spec.Name, err)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
73
|
+
resolved, fetchErr := client.ResolveRefChain(ctx, spec.Ref)
|
|
74
|
+
cancel()
|
|
75
|
+
|
|
76
|
+
if fetchErr != nil {
|
|
77
|
+
var statusErr *cricinfo.HTTPStatusError
|
|
78
|
+
if errors.As(fetchErr, &statusErr) && statusErr.StatusCode == 503 {
|
|
79
|
+
if _, statErr := os.Stat(targetPath); statErr == nil {
|
|
80
|
+
fmt.Printf("! kept existing fixture after persistent 503: %s (%s)\n", spec.Name, spec.Ref)
|
|
81
|
+
keptExisting++
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
fatalf("persistent 503 and no existing fixture for %s (%s)", spec.Name, spec.Ref)
|
|
85
|
+
}
|
|
86
|
+
fatalf("refresh %s (%s): %v", spec.Name, spec.Ref, fetchErr)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
formatted := formatJSONOrOriginal(resolved.Body)
|
|
90
|
+
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
|
91
|
+
fatalf("create fixture directory for %q: %v", targetPath, err)
|
|
92
|
+
}
|
|
93
|
+
if err := os.WriteFile(targetPath, formatted, 0o644); err != nil {
|
|
94
|
+
fatalf("write fixture %q: %v", targetPath, err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fmt.Printf("+ refreshed %s -> %s\n", spec.Ref, targetPath)
|
|
98
|
+
refreshed++
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if err := writeMatrixTSV(outputRoot, matrix); err != nil {
|
|
102
|
+
fatalf("write endpoint matrix: %v", err)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fmt.Printf("refresh complete: refreshed=%d kept-existing=%d total=%d\n", refreshed, keptExisting, len(matrix))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func formatJSONOrOriginal(raw []byte) []byte {
|
|
109
|
+
var out bytes.Buffer
|
|
110
|
+
if err := json.Indent(&out, raw, "", " "); err != nil {
|
|
111
|
+
return raw
|
|
112
|
+
}
|
|
113
|
+
out.WriteByte('\n')
|
|
114
|
+
return out.Bytes()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func writeMatrixTSV(outputRoot string, matrix []cricinfo.FixtureSpec) error {
|
|
118
|
+
var b strings.Builder
|
|
119
|
+
b.WriteString("family\tname\tref\tfixture_path\tlive_probe\n")
|
|
120
|
+
for _, spec := range matrix {
|
|
121
|
+
line := fmt.Sprintf("%s\t%s\t%s\t%s\t%t\n", spec.Family, spec.Name, spec.Ref, spec.FixturePath, spec.LiveProbe)
|
|
122
|
+
b.WriteString(line)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
path := filepath.Join(outputRoot, "endpoint-matrix.tsv")
|
|
126
|
+
return os.WriteFile(path, []byte(b.String()), 0o644)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func ensurePathUnderRoot(root, path string) error {
|
|
130
|
+
cleanRoot := filepath.Clean(root)
|
|
131
|
+
cleanPath := filepath.Clean(path)
|
|
132
|
+
if cleanPath == cleanRoot {
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
prefix := cleanRoot + string(os.PathSeparator)
|
|
136
|
+
if !strings.HasPrefix(cleanPath, prefix) {
|
|
137
|
+
return fmt.Errorf("path %q escapes root %q", path, root)
|
|
138
|
+
}
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func fatalf(format string, args ...any) {
|
|
143
|
+
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
|
|
144
|
+
os.Exit(1)
|
|
145
|
+
}
|