Skip to content

Commit adec30b

Browse files
committed
(catalogd) add unit tests for indexing algo for query endpoint
Closes operator-framework#1697
1 parent 46cec30 commit adec30b

File tree

2 files changed

+284
-14
lines changed

2 files changed

+284
-14
lines changed

Diff for: catalogd/internal/storage/index.go

-14
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,6 @@ func (s *section) UnmarshalJSON(b []byte) error {
5050
return nil
5151
}
5252

53-
func (i index) Size() int64 {
54-
size := 0
55-
for k, v := range i.BySchema {
56-
size += len(k) + len(v)*16
57-
}
58-
for k, v := range i.ByPackage {
59-
size += len(k) + len(v)*16
60-
}
61-
for k, v := range i.ByName {
62-
size += len(k) + len(v)*16
63-
}
64-
return int64(size)
65-
}
66-
6753
func (i index) Get(r io.ReaderAt, schema, packageName, name string) io.Reader {
6854
sectionSet := i.getSectionSet(schema, packageName, name)
6955

Diff for: catalogd/internal/storage/index_test.go

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package storage
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"testing"
8+
9+
"github.com/operator-framework/operator-registry/alpha/declcfg"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestIndexCreation(t *testing.T) {
14+
// Create test Meta objects
15+
metas := []*declcfg.Meta{
16+
{
17+
Schema: "olm.package",
18+
Package: "test",
19+
Name: "test-package",
20+
Blob: []byte(`{"test": "data1"}`),
21+
},
22+
{
23+
Schema: "olm.bundle",
24+
Package: "test",
25+
Name: "test-bundle",
26+
Blob: []byte(`{"test": "data2"}`),
27+
},
28+
}
29+
30+
// Create channel and feed Metas
31+
metasChan := make(chan *declcfg.Meta, len(metas))
32+
for _, meta := range metas {
33+
metasChan <- meta
34+
}
35+
close(metasChan)
36+
37+
// Create index
38+
idx := newIndex(metasChan)
39+
40+
// Verify schema index
41+
require.Equal(t, 2, len(idx.BySchema), "Expected 2 schema entries, got %d", len(idx.BySchema))
42+
require.Equal(t, 1, len(idx.BySchema["olm.package"]), "Expected 1 olm.package entry, got %d", len(idx.BySchema["olm.package"]))
43+
require.Equal(t, 1, len(idx.BySchema["olm.bundle"]), "Expected 1 olm.bundle entry, got %d", len(idx.BySchema["olm.bundle"]))
44+
45+
// Verify package index
46+
require.Equal(t, 2, len(idx.ByPackage["test"]), "Expected 2 package entries, got %d", len(idx.ByPackage))
47+
48+
// Verify name index
49+
require.Equal(t, 1, len(idx.ByName["test-package"]), "Expected 1 entry for name 'test-package', got %d", len(idx.ByName["test-package"]))
50+
require.Equal(t, 1, len(idx.ByName["test-bundle"]), "Expected 1 entry for name 'test-bundle', got %d", len(idx.ByName["test-bundle"]))
51+
}
52+
53+
func TestIndexGet(t *testing.T) {
54+
// Test data structure that represents a catalog
55+
metas := []*declcfg.Meta{
56+
{
57+
// Package definition
58+
Schema: "olm.package",
59+
Name: "test-package",
60+
Blob: createBlob(t, map[string]interface{}{
61+
"schema": "olm.package",
62+
"name": "test-package",
63+
"defaultChannel": "stable-v6.x",
64+
}),
65+
},
66+
{
67+
// First channel (stable-5.x)
68+
Schema: "olm.channel",
69+
Package: "test-package",
70+
Name: "stable-5.x",
71+
Blob: createBlob(t, map[string]interface{}{
72+
"schema": "olm.channel",
73+
"name": "stable-5.x",
74+
"package": "test-package",
75+
"entries": []map[string]interface{}{
76+
{"name": "test-bunble.v5.0.3"},
77+
{"name": "test-bundle.v5.0.4", "replaces": "test-bundle.v5.0.3"},
78+
},
79+
}),
80+
},
81+
{
82+
// Second channel (stable-v6.x)
83+
Schema: "olm.channel",
84+
Package: "test-package",
85+
Name: "stable-v6.x",
86+
Blob: createBlob(t, map[string]interface{}{
87+
"schema": "olm.channel",
88+
"name": "stable-v6.x",
89+
"package": "test-package",
90+
"entries": []map[string]interface{}{
91+
{"name": "test-bundle.v6.0.0", "skipRange": "<6.0.0"},
92+
},
93+
}),
94+
},
95+
{
96+
// Bundle v5.0.3
97+
Schema: "olm.bundle",
98+
Package: "test-package",
99+
Name: "test-bundle.v5.0.3",
100+
Blob: createBlob(t, map[string]interface{}{
101+
"schema": "olm.bundle",
102+
"name": "test-bundle.v5.0.3",
103+
"package": "test-package",
104+
"image": "test-image@sha256:a5d4f",
105+
"properties": []map[string]interface{}{
106+
{
107+
"type": "olm.package",
108+
"value": map[string]interface{}{
109+
"packageName": "test-package",
110+
"version": "5.0.3",
111+
},
112+
},
113+
},
114+
}),
115+
},
116+
{
117+
// Bundle v5.0.4
118+
Schema: "olm.bundle",
119+
Package: "test-package",
120+
Name: "test-bundle.v5.0.4",
121+
Blob: createBlob(t, map[string]interface{}{
122+
"schema": "olm.bundle",
123+
"name": "test-bundle.v5.0.4",
124+
"package": "test-package",
125+
"image": "test-image@sha256:f4233",
126+
"properties": []map[string]interface{}{
127+
{
128+
"type": "olm.package",
129+
"value": map[string]interface{}{
130+
"packageName": "test-package",
131+
"version": "5.0.4",
132+
},
133+
},
134+
},
135+
}),
136+
},
137+
{
138+
// Bundle v6.0.0
139+
Schema: "olm.bundle",
140+
Package: "test-package",
141+
Name: "test-bundle.v6.0.0",
142+
Blob: createBlob(t, map[string]interface{}{
143+
"schema": "olm.bundle",
144+
"name": "test-bundle.v6.0.0",
145+
"package": "test-package",
146+
"image": "test-image@sha256:d3016b",
147+
"properties": []map[string]interface{}{
148+
{
149+
"type": "olm.package",
150+
"value": map[string]interface{}{
151+
"packageName": "test-package",
152+
"version": "6.0.0",
153+
},
154+
},
155+
},
156+
}),
157+
},
158+
}
159+
160+
// Create and populate the index
161+
metasChan := make(chan *declcfg.Meta, len(metas))
162+
for _, meta := range metas {
163+
metasChan <- meta
164+
}
165+
close(metasChan)
166+
167+
idx := newIndex(metasChan)
168+
169+
// Create a reader from the metas
170+
var combinedBlob bytes.Buffer
171+
for _, meta := range metas {
172+
combinedBlob.Write(meta.Blob)
173+
}
174+
fullData := bytes.NewReader(combinedBlob.Bytes())
175+
176+
tests := []struct {
177+
name string
178+
schema string
179+
packageName string
180+
blobName string
181+
wantCount int
182+
validate func(t *testing.T, entry map[string]interface{})
183+
}{
184+
{
185+
name: "filter by schema - olm.package",
186+
schema: "olm.package",
187+
wantCount: 1,
188+
validate: func(t *testing.T, entry map[string]interface{}) {
189+
if entry["schema"] != "olm.package" {
190+
t.Errorf("Expected olm.package schema blob got %v", entry["schema"])
191+
}
192+
},
193+
},
194+
{
195+
name: "filter by schema - olm.channel",
196+
schema: "olm.channel",
197+
wantCount: 2,
198+
validate: func(t *testing.T, entry map[string]interface{}) {
199+
if entry["schema"] != "olm.channel" {
200+
t.Errorf("Expected olm.channel schema blob got %v", entry["schema"])
201+
}
202+
},
203+
},
204+
{
205+
name: "filter by schema - olm.bundle",
206+
schema: "olm.bundle",
207+
wantCount: 3,
208+
validate: func(t *testing.T, entry map[string]interface{}) {
209+
if entry["schema"] != "olm.bundle" {
210+
t.Errorf("Expected olm.bundle schema blob got %v", entry["schema"])
211+
}
212+
},
213+
},
214+
{
215+
name: "filter by package",
216+
packageName: "test-package",
217+
wantCount: 5,
218+
validate: func(t *testing.T, entry map[string]interface{}) {
219+
if entry["package"] != "test-package" {
220+
t.Errorf("Expected blobs with package name test-package, got blob with package name %v", entry["package"])
221+
}
222+
},
223+
},
224+
{
225+
name: "filter by specific bundle name",
226+
blobName: "test-bundle.v5.0.3",
227+
wantCount: 1,
228+
validate: func(t *testing.T, entry map[string]interface{}) {
229+
if entry["schema"] != "olm.bundle" && entry["name"] != "test-bundle.v5.0.3" {
230+
t.Errorf("Expected blob with schema=olm.bundle and name=test-bundle.v5.0.3, got %v", entry)
231+
}
232+
},
233+
},
234+
{
235+
name: "filter by schema and package",
236+
schema: "olm.bundle",
237+
packageName: "test-package",
238+
wantCount: 3,
239+
validate: func(t *testing.T, entry map[string]interface{}) {
240+
if entry["schema"] != "olm.bundle" && entry["package"] != "test-package" {
241+
t.Errorf("Expected blob with schema=olm.bundle and package=test-package, got %v", entry)
242+
}
243+
},
244+
},
245+
{
246+
name: "no matches",
247+
schema: "non.existent",
248+
packageName: "not-found",
249+
wantCount: 0,
250+
},
251+
}
252+
253+
for _, tt := range tests {
254+
t.Run(tt.name, func(t *testing.T) {
255+
reader := idx.Get(fullData, tt.schema, tt.packageName, tt.blobName)
256+
content, err := io.ReadAll(reader)
257+
require.NoError(t, err, "Failed to read content: %v", err)
258+
259+
var count int
260+
decoder := json.NewDecoder(bytes.NewReader(content))
261+
for decoder.More() {
262+
var entry map[string]interface{}
263+
err := decoder.Decode(&entry)
264+
require.NoError(t, err, "Failed to decode result: %v", err)
265+
count++
266+
267+
if tt.validate != nil {
268+
tt.validate(t, entry)
269+
}
270+
}
271+
272+
require.Equal(t, tt.wantCount, count, "Got %d entries, want %d", count, tt.wantCount)
273+
})
274+
}
275+
}
276+
277+
// createBlob is a helper function that creates a JSON blob with a trailing newline
278+
func createBlob(t *testing.T, data map[string]interface{}) []byte {
279+
blob, err := json.Marshal(data)
280+
if err != nil {
281+
t.Fatalf("Failed to create blob: %v", err)
282+
}
283+
return append(blob, '\n')
284+
}

0 commit comments

Comments
 (0)