This is an automated email from the ASF dual-hosted git repository. hanahmily pushed a commit to branch sidx/query in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit bb901df2e2cdf472896c19fa36922a273bfe263c Author: Gao Hongtao <[email protected]> AuthorDate: Sun Aug 24 15:29:15 2025 +0800 Add part iterator tests: Implement comprehensive unit tests for part iterator functionality, covering various scenarios including single and multiple series, key range filtering, and edge cases. Ensure robust validation of expected outcomes and error handling in part iteration. --- banyand/internal/sidx/part_iter_test.go | 448 ++++++++++++++++++++++++++++++++ banyand/internal/sidx/part_test.go | 2 +- 2 files changed, 449 insertions(+), 1 deletion(-) diff --git a/banyand/internal/sidx/part_iter_test.go b/banyand/internal/sidx/part_iter_test.go new file mode 100644 index 00000000..25df9a74 --- /dev/null +++ b/banyand/internal/sidx/part_iter_test.go @@ -0,0 +1,448 @@ +// Licensed to Apache Software Foundation (ASF) under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Apache Software Foundation (ASF) licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package sidx + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/apache/skywalking-banyandb/api/common" + "github.com/apache/skywalking-banyandb/pkg/fs" + pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1" +) + +func TestPartIterVerification(t *testing.T) { + testFS := fs.NewLocalFileSystem() + tempDir := t.TempDir() + + tests := []struct { + name string + elements []testElement + querySids []common.SeriesID + minKey int64 + maxKey int64 + expectedLen int + }{ + { + name: "single series single element", + elements: []testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }, + querySids: []common.SeriesID{1}, + minKey: 50, + maxKey: 150, + expectedLen: 1, + }, + { + name: "single series multiple elements", + elements: []testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 1, + userKey: 200, + data: []byte("data2"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }, + querySids: []common.SeriesID{1}, + minKey: 50, + maxKey: 250, + expectedLen: 1, // Elements from same series are grouped into 1 block + }, + { + name: "multiple series", + elements: []testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 2, + userKey: 150, + data: []byte("data2"), + tags: []tag{ + { + name: "service", + value: []byte("payment-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 3, + userKey: 200, + data: []byte("data3"), + tags: []tag{ + { + name: "service", + value: []byte("user-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }, + querySids: []common.SeriesID{1, 2, 3}, + minKey: 50, + maxKey: 250, + expectedLen: 3, + }, + { + name: "filtered by key range", + elements: []testElement{ + { + seriesID: 1, + userKey: 50, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 1, + userKey: 100, + data: []byte("data2"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 1, + userKey: 200, + data: []byte("data3"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }, + querySids: []common.SeriesID{1}, + minKey: 75, + maxKey: 150, + expectedLen: 1, // Block contains all elements [50-200], overlaps query range [75-150] + }, + { + name: "filtered by series ID", + elements: []testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("order-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 2, + userKey: 100, + data: []byte("data2"), + tags: []tag{ + { + name: "service", + value: []byte("payment-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + { + seriesID: 3, + userKey: 100, + data: []byte("data3"), + tags: []tag{ + { + name: "service", + value: []byte("user-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }, + querySids: []common.SeriesID{2}, + minKey: 50, + maxKey: 150, + expectedLen: 1, // Only series 2 should match + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Step 1: Create elements and initialize memPart + elements := createTestElements(tt.elements) + defer releaseElements(elements) + + mp := generateMemPart() + defer releaseMemPart(mp) + + mp.mustInitFromElements(elements) + + // Step 2: Create part from memPart by flushing to disk + partDir := filepath.Join(tempDir, fmt.Sprintf("part_%s", tt.name)) + mp.mustFlush(testFS, partDir) + + part := mustOpenPart(partDir, testFS) + defer part.close() + + // Step 3: Create partIter and blockMetadataArray + bma := &blockMetadataArray{} + defer bma.reset() + + pi := &partIter{} + + // Step 4: Initialize partIter with clean blockMetadataArray + bma.reset() // Keep blockMetadataArray clean before passing to init + pi.init(bma, part, tt.querySids, tt.minKey, tt.maxKey) + + // Step 5: Iterate through blocks and collect results + var foundElements []testElement + blockCount := 0 + + for pi.nextBlock() { + blockCount++ + curBlock := pi.curBlock + + t.Logf("Found block for seriesID %d, key range [%d, %d], count: %d", + curBlock.seriesID, curBlock.minKey, curBlock.maxKey, curBlock.count) + + // Verify the block overlaps with query range (partIter returns overlapping blocks) + overlaps := curBlock.maxKey >= tt.minKey && curBlock.minKey <= tt.maxKey + assert.True(t, overlaps, "block should overlap with query range [%d, %d], but got block range [%d, %d]", + tt.minKey, tt.maxKey, curBlock.minKey, curBlock.maxKey) + assert.Contains(t, tt.querySids, curBlock.seriesID, "block seriesID should be in query sids") + + // For verification, create a test element representing this block + // Note: In a real scenario, you'd read the actual block data + foundElements = append(foundElements, testElement{ + seriesID: curBlock.seriesID, + userKey: curBlock.minKey, // Use minKey as representative + data: nil, // Not reading actual data in this test + tags: nil, // Not reading actual tags in this test + }) + } + + // Step 6: Check for iteration errors + require.NoError(t, pi.error(), "partIter should not have errors") + + // Step 7: Verify results + assert.Equal(t, tt.expectedLen, len(foundElements), "should find expected number of elements") + + // Additional verification: ensure all found elements match expected series + for _, elem := range foundElements { + assert.Contains(t, tt.querySids, elem.seriesID, "found element should have expected seriesID") + } + + t.Logf("Test %s completed: found %d blocks, expected %d", tt.name, blockCount, tt.expectedLen) + }) + } +} + +func TestPartIterEdgeCases(t *testing.T) { + testFS := fs.NewLocalFileSystem() + tempDir := t.TempDir() + + t.Run("empty series list", func(t *testing.T) { + // Create a simple part with data + elements := createTestElements([]testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("test-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }) + defer releaseElements(elements) + + mp := generateMemPart() + defer releaseMemPart(mp) + mp.mustInitFromElements(elements) + + partDir := filepath.Join(tempDir, "empty_series_test") + mp.mustFlush(testFS, partDir) + + part := mustOpenPart(partDir, testFS) + defer part.close() + + // Test with empty series list + bma := &blockMetadataArray{} + defer bma.reset() + pi := &partIter{} + + bma.reset() + pi.init(bma, part, []common.SeriesID{}, 0, 1000) + + // Should not find any blocks with empty series list + foundAny := pi.nextBlock() + assert.False(t, foundAny, "should not find any blocks with empty series list") + }) + + t.Run("no matching key range", func(t *testing.T) { + // Create a part with data at key 100 + elements := createTestElements([]testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("test-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }) + defer releaseElements(elements) + + mp := generateMemPart() + defer releaseMemPart(mp) + mp.mustInitFromElements(elements) + + partDir := filepath.Join(tempDir, "no_match_key_range") + mp.mustFlush(testFS, partDir) + + part := mustOpenPart(partDir, testFS) + defer part.close() + + // Test with non-overlapping key range + bma := &blockMetadataArray{} + defer bma.reset() + pi := &partIter{} + + bma.reset() + pi.init(bma, part, []common.SeriesID{1}, 200, 300) // No overlap with key 100 + + // Should not find any blocks + foundAny := pi.nextBlock() + assert.False(t, foundAny, "should not find any blocks with non-overlapping key range") + }) + + t.Run("no matching series ID", func(t *testing.T) { + // Create a part with seriesID 1 + elements := createTestElements([]testElement{ + { + seriesID: 1, + userKey: 100, + data: []byte("data1"), + tags: []tag{ + { + name: "service", + value: []byte("test-service"), + valueType: pbv1.ValueTypeStr, + indexed: true, + }, + }, + }, + }) + defer releaseElements(elements) + + mp := generateMemPart() + defer releaseMemPart(mp) + mp.mustInitFromElements(elements) + + partDir := filepath.Join(tempDir, "no_match_series") + mp.mustFlush(testFS, partDir) + + part := mustOpenPart(partDir, testFS) + defer part.close() + + // Test with different series ID + bma := &blockMetadataArray{} + defer bma.reset() + pi := &partIter{} + + bma.reset() + pi.init(bma, part, []common.SeriesID{2}, 0, 200) // Different series ID + + // Should not find any blocks + foundAny := pi.nextBlock() + assert.False(t, foundAny, "should not find any blocks with non-matching series ID") + }) +} diff --git a/banyand/internal/sidx/part_test.go b/banyand/internal/sidx/part_test.go index a764cb00..12636cbf 100644 --- a/banyand/internal/sidx/part_test.go +++ b/banyand/internal/sidx/part_test.go @@ -516,7 +516,7 @@ func compareElements(t *testing.T, expected, actual *elements) { return actualTags[a].name < actualTags[b].name }) - for j := 0; j < len(expectedTags); j++ { + for j := range expectedTags { assert.Equal(t, expectedTags[j].name, actualTags[j].name, "tag name mismatch at element %d, tag %d", i, j) assert.Equal(t, expectedTags[j].value, actualTags[j].value,
