-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathboundary_observability_test.go
More file actions
315 lines (268 loc) · 11.5 KB
/
Copy pathboundary_observability_test.go
File metadata and controls
315 lines (268 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
package gin_test
import (
"context"
"strings"
"testing"
gin "github.com/amikos-tech/ami-gin"
"github.com/amikos-tech/ami-gin/logging"
)
// --------------------------------------------------------------------------
// Task 1: BuildFromParquetContext wrapper tests
// --------------------------------------------------------------------------
func TestBuildFromParquetContextCompatibility(t *testing.T) {
// When there is no parquet file to open, both paths must return an error
// of the same shape (not a nil/non-nil mismatch or a panic).
_, oldErr := gin.BuildFromParquet("nonexistent.parquet", "json", gin.DefaultConfig())
_, newErr := gin.BuildFromParquetContext(context.Background(), "nonexistent.parquet", "json", gin.DefaultConfig())
if (oldErr == nil) != (newErr == nil) {
t.Fatalf("compatibility: old returned %v, new returned %v — mismatch", oldErr, newErr)
}
}
func TestBuildFromParquetContextPreservesResults(t *testing.T) {
// Build via old path and new path, then compare the resulting index
// structure to confirm the context-aware path does not alter behavior.
cfg := gin.DefaultConfig()
oldIdx, oldErr := buildIndexFromTestParquet(cfg)
newIdx, newErr := buildIndexFromTestParquetContext(context.Background(), cfg)
if (oldErr == nil) != (newErr == nil) {
t.Fatalf("old error: %v, new error: %v — mismatch", oldErr, newErr)
}
if oldErr != nil {
// Both errored consistently; nothing more to check.
return
}
if oldIdx.Header.NumRowGroups != newIdx.Header.NumRowGroups {
t.Errorf("NumRowGroups: old %d, new %d", oldIdx.Header.NumRowGroups, newIdx.Header.NumRowGroups)
}
if oldIdx.Header.NumPaths != newIdx.Header.NumPaths {
t.Errorf("NumPaths: old %d, new %d", oldIdx.Header.NumPaths, newIdx.Header.NumPaths)
}
}
func TestBuildFromParquetContextNilDisabledObservabilityNoChanges(t *testing.T) {
// A default config (noop logger, disabled signals) must produce the same
// result as an explicit config with disabled signals.
cfg := gin.DefaultConfig()
idx, err := buildIndexFromTestParquetContext(context.Background(), cfg)
if err != nil {
t.Skipf("no test parquet data available: %v", err)
}
if idx == nil {
t.Fatal("nil index returned with no error")
}
}
func TestBuildFromParquetContextEmitsParserNameWithoutInfoLeak(t *testing.T) {
// Parser identity must be observable only via traces or debug-level signals.
// INFO-level log attributes must not include any parser name fields and
// must stay within the frozen allowlist vocabulary.
capLogger := &infoAttrRecorder{}
cfg, err := gin.NewConfig(gin.WithLogger(capLogger))
if err != nil {
t.Fatalf("NewConfig: %v", err)
}
// Exercise the boundary. The parquet boundary currently emits observability
// via OTel spans/metrics rather than INFO-level log lines, so an empty
// capture is an acceptable outcome. The constraint this test guards is
// negative: IF any INFO-level attrs are captured (now or after a future
// instrumentation change), none of them may leak parser identity or step
// outside the frozen allowlist.
_, _ = gin.BuildFromParquetContext(context.Background(), "nonexistent.parquet", "json", cfg)
frozenAllowlist := map[string]bool{
"operation": true,
"predicate_op": true,
"path_mode": true,
"status": true,
"error.type": true,
}
for _, a := range capLogger.attrs {
if strings.Contains(strings.ToLower(a.Key), "parser") {
t.Errorf("INFO-level attr leaked parser identity: key=%q value=%q", a.Key, a.Value)
}
if strings.Contains(strings.ToLower(a.Value), "stdlib") ||
strings.Contains(strings.ToLower(a.Value), "parser") {
t.Errorf("INFO-level attr value leaked parser identity: key=%q value=%q", a.Key, a.Value)
}
if !frozenAllowlist[a.Key] {
t.Errorf("INFO-level attr key %q is outside the frozen allowlist", a.Key)
}
}
}
// infoAttrRecorder captures attrs emitted at INFO level only. Debug-level
// parser identifiers are ignored so the assertion targets the INFO vocabulary
// exclusively. It lives here rather than in the policy test file because the
// boundary tests need a level-aware recorder and the policy helper captures
// every level.
type infoAttrRecorder struct {
attrs []logging.Attr
}
func (r *infoAttrRecorder) Enabled(_ logging.Level) bool { return true }
func (r *infoAttrRecorder) Log(level logging.Level, _ string, attrs ...logging.Attr) {
if level != logging.LevelInfo {
return
}
r.attrs = append(r.attrs, attrs...)
}
// --------------------------------------------------------------------------
// Task 2: S3 BuildFromParquetContext compatibility tests
// --------------------------------------------------------------------------
func TestS3BuildFromParquetContextCompatibility(t *testing.T) {
// Verify the method exists on S3Client. We cannot run it live, but
// confirming the method signature compiles is the structural check.
// A compile-time type assertion serves as the test body.
var _ interface {
BuildFromParquet(bucket, key, jsonColumn string, ginCfg gin.GINConfig) (*gin.GINIndex, error)
BuildFromParquetContext(ctx context.Context, bucket, key, jsonColumn string, ginCfg gin.GINConfig) (*gin.GINIndex, error)
} = (*gin.S3Client)(nil)
}
func TestS3SidecarHelpersExposeContextAwareSiblings(t *testing.T) {
var _ interface {
ReadFile(bucket, key string) ([]byte, error)
ReadFileContext(ctx context.Context, bucket, key string) ([]byte, error)
WriteFile(bucket, key string, data []byte) error
WriteFileContext(ctx context.Context, bucket, key string, data []byte) error
Exists(bucket, key string) (bool, error)
ExistsContext(ctx context.Context, bucket, key string) (bool, error)
WriteSidecar(bucket, parquetKey string, idx *gin.GINIndex) error
WriteSidecarContext(ctx context.Context, bucket, parquetKey string, idx *gin.GINIndex) error
ReadSidecar(bucket, parquetKey string) (*gin.GINIndex, error)
ReadSidecarContext(ctx context.Context, bucket, parquetKey string) (*gin.GINIndex, error)
HasSidecar(bucket, parquetKey string) (bool, error)
HasSidecarContext(ctx context.Context, bucket, parquetKey string) (bool, error)
LoadIndex(bucket, parquetKey string, cfg gin.ParquetConfig) (*gin.GINIndex, error)
LoadIndexContext(ctx context.Context, bucket, parquetKey string, cfg gin.ParquetConfig) (*gin.GINIndex, error)
} = (*gin.S3Client)(nil)
}
func TestS3BuildFromParquetContextHonorsCancellationWithStubTransport(t *testing.T) {
// Use a pre-canceled context to confirm the method propagates cancellation.
// We use a fake S3 config pointing to an unreachable endpoint so no live
// AWS call is made; the context cancellation propagates through the
// builder call chain before or immediately after any TCP dial attempt.
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
client, err := gin.NewS3Client(gin.S3Config{
Endpoint: "http://127.0.0.1:1", // port 1 is always refused
Region: "us-east-1",
PathStyle: true,
})
if err != nil {
t.Fatalf("NewS3Client: %v", err)
}
_, err = client.BuildFromParquetContext(ctx, "fakebucket", "fakekey.parquet", "json", gin.DefaultConfig())
if err == nil {
t.Fatal("expected an error from canceled context or unreachable endpoint; got nil")
}
}
// --------------------------------------------------------------------------
// Task 3: EncodeContext / DecodeContext / EncodeWithLevelContext tests
// --------------------------------------------------------------------------
func TestEncodeContextCompatibility(t *testing.T) {
idx, err := buildSmallIndex()
if err != nil {
t.Fatalf("build index: %v", err)
}
oldData, oldErr := gin.Encode(idx)
newData, newErr := gin.EncodeContext(context.Background(), idx)
if (oldErr == nil) != (newErr == nil) {
t.Fatalf("Encode vs EncodeContext error mismatch: %v vs %v", oldErr, newErr)
}
if oldErr != nil {
return
}
if len(oldData) != len(newData) {
t.Errorf("Encode/EncodeContext produced different byte lengths: %d vs %d", len(oldData), len(newData))
}
}
func TestEncodeWithLevelContextCompatibility(t *testing.T) {
idx, err := buildSmallIndex()
if err != nil {
t.Fatalf("build index: %v", err)
}
oldData, oldErr := gin.EncodeWithLevel(idx, gin.CompressionNone)
newData, newErr := gin.EncodeWithLevelContext(context.Background(), idx, gin.CompressionNone)
if (oldErr == nil) != (newErr == nil) {
t.Fatalf("EncodeWithLevel vs EncodeWithLevelContext error mismatch: %v vs %v", oldErr, newErr)
}
if oldErr != nil {
return
}
if len(oldData) != len(newData) {
t.Errorf("EncodeWithLevel/EncodeWithLevelContext produced different byte lengths: %d vs %d", len(oldData), len(newData))
}
}
func TestDecodeContextCompatibility(t *testing.T) {
idx, err := buildSmallIndex()
if err != nil {
t.Fatalf("build index: %v", err)
}
data, err := gin.Encode(idx)
if err != nil {
t.Fatalf("encode: %v", err)
}
oldIdx, oldErr := gin.Decode(data)
newIdx, newErr := gin.DecodeContext(context.Background(), data)
if (oldErr == nil) != (newErr == nil) {
t.Fatalf("Decode vs DecodeContext error mismatch: %v vs %v", oldErr, newErr)
}
if oldErr != nil {
return
}
if oldIdx.Header.NumRowGroups != newIdx.Header.NumRowGroups {
t.Errorf("NumRowGroups: old %d, new %d", oldIdx.Header.NumRowGroups, newIdx.Header.NumRowGroups)
}
if oldIdx.Header.NumPaths != newIdx.Header.NumPaths {
t.Errorf("NumPaths: old %d, new %d", oldIdx.Header.NumPaths, newIdx.Header.NumPaths)
}
}
func TestSerializationContextRoundTrip(t *testing.T) {
idx, err := buildSmallIndex()
if err != nil {
t.Fatalf("build index: %v", err)
}
data, err := gin.EncodeContext(context.Background(), idx)
if err != nil {
t.Fatalf("EncodeContext: %v", err)
}
decoded, err := gin.DecodeContext(context.Background(), data)
if err != nil {
t.Fatalf("DecodeContext: %v", err)
}
if decoded.Header.NumRowGroups != idx.Header.NumRowGroups {
t.Errorf("round-trip NumRowGroups: want %d, got %d", idx.Header.NumRowGroups, decoded.Header.NumRowGroups)
}
if decoded.Header.NumPaths != idx.Header.NumPaths {
t.Errorf("round-trip NumPaths: want %d, got %d", idx.Header.NumPaths, decoded.Header.NumPaths)
}
}
func TestMetadataAndSidecarHelpersUseContextSiblings(t *testing.T) {
// This test ensures the sidecar and metadata helpers still work end-to-end.
// Since they call Encode/Decode internally (now via wrappers), the test
// confirms backward-compatible behavior without requiring real file I/O.
idx, err := buildSmallIndex()
if err != nil {
t.Fatalf("build index: %v", err)
}
key, value, err := gin.EncodeToMetadata(idx, gin.DefaultParquetConfig())
if err != nil {
t.Fatalf("EncodeToMetadata: %v", err)
}
if key == "" {
t.Fatal("EncodeToMetadata returned empty key")
}
decoded, err := gin.DecodeFromMetadata(value)
if err != nil {
t.Fatalf("DecodeFromMetadata: %v", err)
}
if decoded.Header.NumRowGroups != idx.Header.NumRowGroups {
t.Errorf("metadata round-trip NumRowGroups: want %d, got %d", idx.Header.NumRowGroups, decoded.Header.NumRowGroups)
}
}
// --------------------------------------------------------------------------
// Helpers shared across tasks
// --------------------------------------------------------------------------
// buildIndexFromTestParquet attempts to build from a parquet file in testdata.
// If no suitable file exists, it returns an error and callers should t.Skip.
func buildIndexFromTestParquet(cfg gin.GINConfig) (*gin.GINIndex, error) {
return gin.BuildFromParquet("testdata/test.parquet", "json", cfg)
}
func buildIndexFromTestParquetContext(ctx context.Context, cfg gin.GINConfig) (*gin.GINIndex, error) {
return gin.BuildFromParquetContext(ctx, "testdata/test.parquet", "json", cfg)
}