Skip to content

Commit

Permalink
(catalogd) add unit tests for indexing algo for query endpoint (#1702)
Browse files Browse the repository at this point in the history
Closes #1697

Signed-off-by: Anik Bhattacharjee <[email protected]>
  • Loading branch information
anik120 authored Feb 5, 2025
1 parent 38b4795 commit e639717
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 14 deletions.
14 changes: 0 additions & 14 deletions catalogd/internal/storage/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
285 changes: 285 additions & 0 deletions catalogd/internal/storage/index_test.go
Original file line number Diff line number Diff line change
@@ -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')
}

0 comments on commit e639717

Please sign in to comment.