From 8bf3528bab8622f151987785ad4e65685005dacc Mon Sep 17 00:00:00 2001 From: Anik Bhattacharjee Date: Tue, 4 Feb 2025 14:50:58 -0500 Subject: [PATCH] (catalogd) add unit tests for indexing algo for query endpoint Closes #1697 Signed-off-by: Anik Bhattacharjee --- catalogd/internal/storage/index.go | 14 -- catalogd/internal/storage/index_test.go | 285 ++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 14 deletions(-) create mode 100644 catalogd/internal/storage/index_test.go diff --git a/catalogd/internal/storage/index.go b/catalogd/internal/storage/index.go index c40ac4b3e..510e23ff0 100644 --- a/catalogd/internal/storage/index.go +++ b/catalogd/internal/storage/index.go @@ -50,20 +50,6 @@ func (s *section) UnmarshalJSON(b []byte) error { return nil } -func (i index) Size() int64 { - size := 0 - for k, v := range i.BySchema { - size += len(k) + len(v)*16 - } - for k, v := range i.ByPackage { - size += len(k) + len(v)*16 - } - for k, v := range i.ByName { - size += len(k) + len(v)*16 - } - return int64(size) -} - func (i index) Get(r io.ReaderAt, schema, packageName, name string) io.Reader { sectionSet := i.getSectionSet(schema, packageName, name) diff --git a/catalogd/internal/storage/index_test.go b/catalogd/internal/storage/index_test.go new file mode 100644 index 000000000..66dee0c13 --- /dev/null +++ b/catalogd/internal/storage/index_test.go @@ -0,0 +1,285 @@ +package storage + +import ( + "bytes" + "encoding/json" + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-registry/alpha/declcfg" +) + +func TestIndexCreation(t *testing.T) { + // Create test Meta objects + metas := []*declcfg.Meta{ + { + Schema: "olm.package", + Package: "test", + Name: "test-package", + Blob: []byte(`{"test": "data1"}`), + }, + { + Schema: "olm.bundle", + Package: "test", + Name: "test-bundle", + Blob: []byte(`{"test": "data2"}`), + }, + } + + // Create channel and feed Metas + metasChan := make(chan *declcfg.Meta, len(metas)) + for _, meta := range metas { + metasChan <- meta + } + close(metasChan) + + // Create index + idx := newIndex(metasChan) + + // Verify schema index + require.Len(t, idx.BySchema, 2, "Expected 2 schema entries, got %d", len(idx.BySchema)) + require.Len(t, idx.BySchema["olm.package"], 1, "Expected 1 olm.package entry, got %d", len(idx.BySchema["olm.package"])) + require.Len(t, idx.BySchema["olm.bundle"], 1, "Expected 1 olm.bundle entry, got %d", len(idx.BySchema["olm.bundle"])) + + // Verify package index + require.Len(t, idx.ByPackage["test"], 2, "Expected 2 package entries, got %d", len(idx.ByPackage)) + + // Verify name index + require.Len(t, idx.ByName["test-package"], 1, "Expected 1 entry for name 'test-package', got %d", len(idx.ByName["test-package"])) + require.Len(t, idx.ByName["test-bundle"], 1, "Expected 1 entry for name 'test-bundle', got %d", len(idx.ByName["test-bundle"])) +} + +func TestIndexGet(t *testing.T) { + // Test data structure that represents a catalog + metas := []*declcfg.Meta{ + { + // Package definition + Schema: "olm.package", + Name: "test-package", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.package", + "name": "test-package", + "defaultChannel": "stable-v6.x", + }), + }, + { + // First channel (stable-5.x) + Schema: "olm.channel", + Package: "test-package", + Name: "stable-5.x", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.channel", + "name": "stable-5.x", + "package": "test-package", + "entries": []map[string]interface{}{ + {"name": "test-bunble.v5.0.3"}, + {"name": "test-bundle.v5.0.4", "replaces": "test-bundle.v5.0.3"}, + }, + }), + }, + { + // Second channel (stable-v6.x) + Schema: "olm.channel", + Package: "test-package", + Name: "stable-v6.x", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.channel", + "name": "stable-v6.x", + "package": "test-package", + "entries": []map[string]interface{}{ + {"name": "test-bundle.v6.0.0", "skipRange": "<6.0.0"}, + }, + }), + }, + { + // Bundle v5.0.3 + Schema: "olm.bundle", + Package: "test-package", + Name: "test-bundle.v5.0.3", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.bundle", + "name": "test-bundle.v5.0.3", + "package": "test-package", + "image": "test-image@sha256:a5d4f", + "properties": []map[string]interface{}{ + { + "type": "olm.package", + "value": map[string]interface{}{ + "packageName": "test-package", + "version": "5.0.3", + }, + }, + }, + }), + }, + { + // Bundle v5.0.4 + Schema: "olm.bundle", + Package: "test-package", + Name: "test-bundle.v5.0.4", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.bundle", + "name": "test-bundle.v5.0.4", + "package": "test-package", + "image": "test-image@sha256:f4233", + "properties": []map[string]interface{}{ + { + "type": "olm.package", + "value": map[string]interface{}{ + "packageName": "test-package", + "version": "5.0.4", + }, + }, + }, + }), + }, + { + // Bundle v6.0.0 + Schema: "olm.bundle", + Package: "test-package", + Name: "test-bundle.v6.0.0", + Blob: createBlob(t, map[string]interface{}{ + "schema": "olm.bundle", + "name": "test-bundle.v6.0.0", + "package": "test-package", + "image": "test-image@sha256:d3016b", + "properties": []map[string]interface{}{ + { + "type": "olm.package", + "value": map[string]interface{}{ + "packageName": "test-package", + "version": "6.0.0", + }, + }, + }, + }), + }, + } + + // Create and populate the index + metasChan := make(chan *declcfg.Meta, len(metas)) + for _, meta := range metas { + metasChan <- meta + } + close(metasChan) + + idx := newIndex(metasChan) + + // Create a reader from the metas + var combinedBlob bytes.Buffer + for _, meta := range metas { + combinedBlob.Write(meta.Blob) + } + fullData := bytes.NewReader(combinedBlob.Bytes()) + + tests := []struct { + name string + schema string + packageName string + blobName string + wantCount int + validate func(t *testing.T, entry map[string]interface{}) + }{ + { + name: "filter by schema - olm.package", + schema: "olm.package", + wantCount: 1, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["schema"] != "olm.package" { + t.Errorf("Expected olm.package schema blob got %v", entry["schema"]) + } + }, + }, + { + name: "filter by schema - olm.channel", + schema: "olm.channel", + wantCount: 2, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["schema"] != "olm.channel" { + t.Errorf("Expected olm.channel schema blob got %v", entry["schema"]) + } + }, + }, + { + name: "filter by schema - olm.bundle", + schema: "olm.bundle", + wantCount: 3, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["schema"] != "olm.bundle" { + t.Errorf("Expected olm.bundle schema blob got %v", entry["schema"]) + } + }, + }, + { + name: "filter by package", + packageName: "test-package", + wantCount: 5, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["package"] != "test-package" { + t.Errorf("Expected blobs with package name test-package, got blob with package name %v", entry["package"]) + } + }, + }, + { + name: "filter by specific bundle name", + blobName: "test-bundle.v5.0.3", + wantCount: 1, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["schema"] != "olm.bundle" && entry["name"] != "test-bundle.v5.0.3" { + t.Errorf("Expected blob with schema=olm.bundle and name=test-bundle.v5.0.3, got %v", entry) + } + }, + }, + { + name: "filter by schema and package", + schema: "olm.bundle", + packageName: "test-package", + wantCount: 3, + validate: func(t *testing.T, entry map[string]interface{}) { + if entry["schema"] != "olm.bundle" && entry["package"] != "test-package" { + t.Errorf("Expected blob with schema=olm.bundle and package=test-package, got %v", entry) + } + }, + }, + { + name: "no matches", + schema: "non.existent", + packageName: "not-found", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := idx.Get(fullData, tt.schema, tt.packageName, tt.blobName) + content, err := io.ReadAll(reader) + require.NoError(t, err, "Failed to read content: %v", err) + + var count int + decoder := json.NewDecoder(bytes.NewReader(content)) + for decoder.More() { + var entry map[string]interface{} + err := decoder.Decode(&entry) + require.NoError(t, err, "Failed to decode result: %v", err) + count++ + + if tt.validate != nil { + tt.validate(t, entry) + } + } + + require.Equal(t, tt.wantCount, count, "Got %d entries, want %d", count, tt.wantCount) + }) + } +} + +// createBlob is a helper function that creates a JSON blob with a trailing newline +func createBlob(t *testing.T, data map[string]interface{}) []byte { + blob, err := json.Marshal(data) + if err != nil { + t.Fatalf("Failed to create blob: %v", err) + } + return append(blob, '\n') +}