// Copyright (C) 2025 wangyusong // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // TODO: fix tests package block // import ( // // "context" // "encoding/json" // "os" // "path/filepath" // // "sync/atomic" // "testing" // "time" // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // "github.com/benbjohnson/clock" // . "github.com/onsi/gomega" // "github.com/stretchr/testify/mock" // "github.com/glidea/zenfeed/pkg/model" // "github.com/glidea/zenfeed/pkg/storage/feed/block/chunk" // // "github.com/glidea/zenfeed/pkg/storage/feed/block/index/inverted" // // "github.com/glidea/zenfeed/pkg/storage/feed/block/index/primary" // // "github.com/glidea/zenfeed/pkg/storage/feed/block/index/vector" // "github.com/glidea/zenfeed/pkg/test" // runtimeutil "github.com/glidea/zenfeed/pkg/util/runtime" // ) // func TestNew(t *testing.T) { // RegisterTestingT(t) // t0 := time.Now() // type givenDetail struct { // setupDir func(string) error // config *Config // chunkMock func(obj *mock.Mock) // primaryMock func(obj *mock.Mock) // invertedMock func(obj *mock.Mock) // vectorMock func(obj *mock.Mock) // } // type whenDetail struct{} // type thenExpected struct { // state State // chunkFactoryNewCallCount int // primaryFactoryNewCallCount int // invertedFactoryNewCallCount int // vectorFactoryNewCallCount int // err string // } // tests := []test.Case[givenDetail, whenDetail, thenExpected]{ // { // Scenario: "Creating new block with valid config", // Given: "A config with valid parameters", // When: "Calling New function", // Then: "Should create a new block successfully", // GivenDetail: givenDetail{ // setupDir: func(dir string) error { // return nil // No need to pre-setup, new directory will be created. // }, // config: &Config{ // Dir: "", // Will be set to temp dir in test. // Start: t0, // Duration: 24 * time.Hour, // SelectableEmbeddingLLMs: []EmbeddingLLM{{Name: "test", Start: time.Time{}}}, // }, // }, // ThenExpected: thenExpected{ // state: StateHot, // chunkFactoryNewCallCount: 1, // primaryFactoryNewCallCount: 1, // invertedFactoryNewCallCount: 1, // vectorFactoryNewCallCount: 1, // }, // }, // { // Scenario: "Recover hot block from disk", // Given: "Block data directory exists on disk and no archive metadata file", // When: "Calling New function", // Then: "Should recover block successfully", // GivenDetail: givenDetail{ // setupDir: func(dir string) error { // // Create chunk directory but not create archive.json. // chunkDir := filepath.Join(dir, chunkDirname) // if err := os.MkdirAll(chunkDir, 0755); err != nil { // return err // } // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(0))); err != nil { // return err // } // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(1))); err != nil { // return err // } // return nil // }, // chunkMock: func(obj *mock.Mock) { // obj.On("Range", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { // iter := args.Get(1).(func(feed *chunk.Feed, offset uint64) error) // runtimeutil.Must(iter(&chunk.Feed{ID: 1, Vectors: [][]float32{{1, 2, 3}, {4, 5, 6}}, Feed: &model.Feed{Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}}, 0)) // runtimeutil.Must(iter(&chunk.Feed{ID: 2, Vectors: [][]float32{{7, 8, 9}, {10, 11, 12}}, Feed: &model.Feed{Labels: model.Labels{model.Label{Key: "k2", Value: "v2"}}, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}}, 1)) // }).Return(nil) // Two chunk files. // }, // primaryMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), primaryindex.FeedRef{Chunk: 0, Offset: 0, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(2), primaryindex.FeedRef{Chunk: 0, Offset: 1, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(1), primaryindex.FeedRef{Chunk: 1, Offset: 0, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(2), primaryindex.FeedRef{Chunk: 1, Offset: 1, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}).Return(nil) // }, // invertedMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), model.Labels{model.Label{Key: "k1", Value: "v1"}}).Return(nil).Times(2) // Two chunk files. // obj.On("Add", mock.Anything, uint64(2), model.Labels{model.Label{Key: "k2", Value: "v2"}}).Return(nil).Times(2) // }, // vectorMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), [][]float32{{1, 2, 3}, {4, 5, 6}}).Return(nil).Times(2) // Two chunk files. // obj.On("Add", mock.Anything, uint64(2), [][]float32{{7, 8, 9}, {10, 11, 12}}).Return(nil).Times(2) // }, // config: &Config{ // Dir: "", // Will be set to temp dir in test. // Start: t0, // Retention: 7 * 24 * time.Hour, // Duration: 24 * time.Hour, // WriteableWindow: 48 * time.Hour, // Within this window. // SelectableEmbeddingLLMs: []EmbeddingLLM{{Name: "test", Start: time.Time{}}}, // }, // }, // ThenExpected: thenExpected{ // state: StateHot, // chunkFactoryNewCallCount: 2, // primaryFactoryNewCallCount: 1, // invertedFactoryNewCallCount: 1, // vectorFactoryNewCallCount: 1, // }, // }, // { // Scenario: "Recover hot readonly block from disk", // Given: "Block data directory exists on disk and no archive metadata file, and WriteableWindow is out of range", // When: "Calling New function", // Then: "Should recover block successfully and state is hot readonly", // GivenDetail: givenDetail{ // setupDir: func(dir string) error { // // Create chunk directory but not create archive.json. // chunkDir := filepath.Join(dir, chunkDirname) // if err := os.MkdirAll(chunkDir, 0755); err != nil { // return err // } // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(0))); err != nil { // return err // } // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(1))); err != nil { // return err // } // return nil // }, // chunkMock: func(obj *mock.Mock) { // obj.On("Range", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { // iter := args.Get(1).(func(feed *chunk.Feed, offset uint64) error) // runtimeutil.Must(iter(&chunk.Feed{ID: 1, Feed: &model.Feed{Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}}, 0)) // runtimeutil.Must(iter(&chunk.Feed{ID: 2, Feed: &model.Feed{Labels: model.Labels{model.Label{Key: "k2", Value: "v2"}}, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}}, 1)) // }).Return(nil) // Two chunk files. // }, // primaryMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), primaryindex.FeedRef{Chunk: 0, Offset: 0, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(2), primaryindex.FeedRef{Chunk: 0, Offset: 1, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(1), primaryindex.FeedRef{Chunk: 1, Offset: 0, Time: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC)}).Return(nil) // obj.On("Add", mock.Anything, uint64(2), primaryindex.FeedRef{Chunk: 1, Offset: 1, Time: time.Date(2025, 5, 20, 0, 0, 0, 0, time.UTC)}).Return(nil) // }, // invertedMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), model.Labels{model.Label{Key: "k1", Value: "v1"}}).Return(nil).Times(2) // Two chunk files. // obj.On("Add", mock.Anything, uint64(2), model.Labels{model.Label{Key: "k2", Value: "v2"}}).Return(nil).Times(2) // }, // vectorMock: func(obj *mock.Mock) { // obj.On("Add", mock.Anything, uint64(1), [][]float32{{1, 2, 3}, {4, 5, 6}}).Return(nil).Times(2) // Two chunk files. // obj.On("Add", mock.Anything, uint64(2), [][]float32{{7, 8, 9}, {10, 11, 12}}).Return(nil).Times(2) // }, // config: &Config{ // Dir: "", // Will be set to temp dir in test. // Start: t0.Add(-72 * time.Hour), // Retention: 7 * 24 * time.Hour, // Duration: 24 * time.Hour, // WriteableWindow: 25 * time.Hour, // SelectableEmbeddingLLMs: []EmbeddingLLM{{Name: "test", Start: time.Time{}}}, // }, // }, // ThenExpected: thenExpected{ // state: StateHotReadonly, // chunkFactoryNewCallCount: 2, // primaryFactoryNewCallCount: 1, // invertedFactoryNewCallCount: 1, // vectorFactoryNewCallCount: 1, // }, // }, // { // Scenario: "Recover cold block from disk", // Given: "Block data directory exists on disk and has archive metadata file", // When: "Calling New function", // Then: "Should recover block successfully as cold state", // GivenDetail: givenDetail{ // setupDir: func(dir string) error { // // Create block directory structure with archive.json // chunkDir := filepath.Join(dir, chunkDirname) // if err := os.MkdirAll(chunkDir, 0755); err != nil { // return err // } // // Create chunk files // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(0))); err != nil { // return err // } // if _, err := os.Create(filepath.Join(chunkDir, chunkFilename(1))); err != nil { // return err // } // // Create archive.json to indicate cold state // meta := archiveMetadata{FeedCount: 4} // bs := runtimeutil.Must1(json.Marshal(meta)) // if err := os.WriteFile(filepath.Join(dir, archiveMetaFilename), bs, 0644); err != nil { // return err // } // // Create index directory and files // indexDir := filepath.Join(dir, indexDirname) // if err := os.MkdirAll(indexDir, 0755); err != nil { // return err // } // for _, name := range []string{indexPrimaryFilename, indexInvertedFilename, indexVectorFilename} { // if _, err := os.Create(filepath.Join(indexDir, name)); err != nil { // return err // } // } // return nil // }, // chunkMock: func(obj *mock.Mock) { // // No chunk mock needed since cold block doesn't load chunks initially // }, // primaryMock: func(obj *mock.Mock) { // // No primary mock needed since cold block doesn't load indices initially // }, // invertedMock: func(obj *mock.Mock) { // // No inverted mock needed since cold block doesn't load indices initially // }, // vectorMock: func(obj *mock.Mock) { // // No vector mock needed since cold block doesn't load indices initially // }, // config: &Config{ // Dir: "", // Will be set to temp dir in test // Start: t0, // Retention: 7 * 24 * time.Hour, // Duration: 24 * time.Hour, // WriteableWindow: 48 * time.Hour, // SelectableEmbeddingLLMs: []EmbeddingLLM{{Name: "test", Start: time.Time{}}}, // }, // }, // ThenExpected: thenExpected{ // state: StateCold, // chunkFactoryNewCallCount: 0, // Cold block doesn't load chunks initially // primaryFactoryNewCallCount: 1, // invertedFactoryNewCallCount: 1, // vectorFactoryNewCallCount: 1, // }, // }, // { // Scenario: "Creating new block - invalid config - missing SelectableEmbeddingLLMs", // Given: "A config with missing SelectableEmbeddingLLMs", // When: "Calling New function", // Then: "Should return an error", // GivenDetail: givenDetail{ // config: &Config{ // Dir: "", // Will be set to temp dir in test. // Start: t0, // Duration: 24 * time.Hour, // }, // }, // ThenExpected: thenExpected{ // err: "selectable embedding LLMs is required", // }, // }, // } // for _, tt := range tests { // t.Run(tt.Scenario, func(t *testing.T) { // // Given. // tempDir, err := os.MkdirTemp("", "block_test_*") // Expect(err).NotTo(HaveOccurred()) // defer os.RemoveAll(tempDir) // // Set parent directory of config. // if tt.GivenDetail.config != nil && tt.GivenDetail.config.Dir == "" { // tt.GivenDetail.config.Dir = tempDir // } // // Execute setup of test scenario. // if tt.GivenDetail.setupDir != nil { // err := tt.GivenDetail.setupDir(tempDir) // Expect(err).NotTo(HaveOccurred()) // } // // Create mock factory. // chunkFactoryNewCallCount := 0 // mockChunkFactory := chunk.NewFactory(func(obj *mock.Mock) { // if tt.GivenDetail.chunkMock != nil { // tt.GivenDetail.chunkMock(obj) // } // chunkFactoryNewCallCount++ // }) // primaryFactoryNewCallCount := 0 // mockPrimaryFactory := primaryindex.NewFactory(func(obj *mock.Mock) { // if tt.GivenDetail.primaryMock != nil { // tt.GivenDetail.primaryMock(obj) // } // primaryFactoryNewCallCount++ // }) // invertedFactoryNewCallCount := 0 // mockInvertedFactory := invertedindex.NewFactory(func(obj *mock.Mock) { // if tt.GivenDetail.invertedMock != nil { // tt.GivenDetail.invertedMock(obj) // } // invertedFactoryNewCallCount++ // }) // vectorFactoryNewCallCount := 0 // mockVectorFactory := vectorindex.NewFactory(func(obj *mock.Mock) { // if tt.GivenDetail.vectorMock != nil { // tt.GivenDetail.vectorMock(obj) // } // vectorFactoryNewCallCount++ // }) // // When. // b, err := new("test", tt.GivenDetail.config, Dependencies{ // ChunkFactory: mockChunkFactory, // PrimaryFactory: mockPrimaryFactory, // InvertedFactory: mockInvertedFactory, // VectorFactory: mockVectorFactory, // }) // // Then. // if tt.ThenExpected.err != "" { // Expect(err).To(HaveOccurred()) // Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.err)) // } else { // Expect(err).NotTo(HaveOccurred()) // Expect(b).NotTo(BeNil()) // // Validate self properties. // rb := b.(*block) // Expect(rb.state.Load().(State)).To(Equal(tt.ThenExpected.state)) // Expect(rb.chunks).To(HaveLen(tt.ThenExpected.chunkFactoryNewCallCount)) // // Validate dependencies. // Expect(chunkFactoryNewCallCount).To(Equal(tt.ThenExpected.chunkFactoryNewCallCount)) // Expect(primaryFactoryNewCallCount).To(Equal(tt.ThenExpected.primaryFactoryNewCallCount)) // Expect(invertedFactoryNewCallCount).To(Equal(tt.ThenExpected.invertedFactoryNewCallCount)) // Expect(vectorFactoryNewCallCount).To(Equal(tt.ThenExpected.vectorFactoryNewCallCount)) // if tt.GivenDetail.config != nil { // chunkDir := filepath.Join(tt.GivenDetail.config.Dir, chunkDirname) // stat, err := os.Stat(chunkDir) // Expect(err).NotTo(HaveOccurred()) // Expect(stat.IsDir()).To(BeTrue()) // } // } // }) // } // } // // func TestRun(t *testing.T) { // // RegisterTestingT(t) // // t0 := time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC) // // type givenDetail struct { // // config *Config // // initialState State // // setup func(b *block, mockClock *clock.Mock) // // } // // type whenDetail struct{} // // type thenExpected struct { // // state State // // chunksLen int // // chunksNil bool // // primaryNil bool // // invertedNil bool // // vectorNil bool // // archiveExists bool // // } // // tests := []test.Case[givenDetail, whenDetail, thenExpected]{ // // { // // Scenario: "Hot to HotReadonly transition", // // Given: "A hot block that is about to exceed its writeable window", // // When: "Running the block", // // Then: "Should transition to hot-readonly state", // // GivenDetail: givenDetail{ // // config: &Config{ // // Dir: "/tmp", // Use Dir instead of ParentDir // // Start: t0, // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // }, // // initialState: StateHot, // // setup: func(b *block, mockClock *clock.Mock) { // // mockClock.Set(t0.Add(49 * time.Hour)) // Past writeable window. // // }, // // }, // // ThenExpected: thenExpected{ // // state: StateHotReadonly, // // }, // // }, // // { // // Scenario: "HotReadonly to Hot transition", // // Given: "A hot-readonly block with a new, wider writeable window", // // When: "Running the block", // // Then: "Should transition to hot state and add a new chunk", // // GivenDetail: givenDetail{ // // config: &Config{ // // Dir: "/tmp", // Use Dir instead of ParentDir // // Start: t0, // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // }, // // initialState: StateHotReadonly, // // setup: func(b *block, mockClock *clock.Mock) { // // mockClock.Set(t0.Add(47 * time.Hour)) // // }, // // }, // // ThenExpected: thenExpected{ // // state: StateHot, // // chunksLen: 2 + 1, // Add 1 chunk when rollback to Hot. // // }, // // }, // // { // // Scenario: "HotReadonly to Cold transition", // // Given: "A hot-readonly block with no recent data access", // // When: "Running the block", // // Then: "Should transition to cold state and release resources", // // GivenDetail: givenDetail{ // // config: &Config{ // // Dir: "/tmp", // Use Dir instead of ParentDir // // Start: t0.Add(-49 * time.Hour), // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // }, // // initialState: StateHotReadonly, // // setup: func(b *block, mockClock *clock.Mock) { // // b.lastDataAccess.Store(t0) // // mockClock.Set(t0.Add(6 * time.Minute)) // Past colding window (5 minutes) // // }, // // }, // // ThenExpected: thenExpected{ // // state: StateCold, // // chunksNil: true, // // primaryNil: true, // // invertedNil: true, // // vectorNil: true, // // }, // // }, // // { // // Scenario: "ColdLoaded to Cold transition", // // Given: "A cold-loaded block with no recent data access", // // When: "Running the block", // // Then: "Should transition to cold state and release resources", // // GivenDetail: givenDetail{ // // config: &Config{ // // Dir: "/tmp", // Use Dir instead of ParentDir // // Start: t0, // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // }, // // initialState: StateColdLoaded, // // setup: func(b *block, mockClock *clock.Mock) { // // b.lastDataAccess.Store(t0) // // mockClock.Set(t0.Add(6 * time.Minute)) // Past colding window (5 minutes) // // }, // // }, // // ThenExpected: thenExpected{ // // state: StateCold, // // chunksNil: true, // // primaryNil: true, // // invertedNil: true, // // vectorNil: true, // // }, // // }, // // { // // Scenario: "Hot to ColdExpired transition", // // Given: "A hot block exceeding retention period", // // When: "Running the block", // // Then: "Should transition to ColdExpired state and create archive file", // // GivenDetail: givenDetail{ // // config: &Config{ // // Dir: "/tmp", // Use Dir instead of ParentDir // // Start: t0.Add(-8 * 24 * time.Hour), // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // Retention: 7 * 24 * time.Hour, // // }, // // initialState: StateHot, // // setup: func(b *block, mockClock *clock.Mock) { // // mockClock.Set(t0) // // }, // // }, // // ThenExpected: thenExpected{ // // state: StateColdExpired, // // archiveExists: true, // // }, // // }, // // } // // for _, tt := range tests { // // t.Run(tt.Scenario, func(t *testing.T) { // // // Given. // // // Setup mock clock // // mockClock := clock.NewMock() // // clk = mockClock // // // Create test block // // chunkFactory := chunk.NewFactory(func(obj *mock.Mock) { // // obj.On("EnsureReadonly").Return(nil).Times(2) // // obj.On("Close").Return(nil).Times(3) // Max 3 chunks (2 initial + 1 possible new) // // }) // // chunk1, err := chunkFactory.New(&chunk.Config{}) // // Expect(err).NotTo(HaveOccurred()) // // chunk2, err := chunkFactory.New(&chunk.Config{}) // // Expect(err).NotTo(HaveOccurred()) // // primaryFactory := primaryindex.NewFactory(func(obj *mock.Mock) { // // obj.On("EncodeTo", mock.Anything).Return(nil) // // obj.On("Count").Return(uint32(4)) // // obj.On("Close").Return(nil) // // }) // // invertedFactory := invertedindex.NewFactory(func(obj *mock.Mock) { // // obj.On("EncodeTo", mock.Anything).Return(nil) // // obj.On("Close").Return(nil) // // }) // // vectorFactory := vectorindex.NewFactory(func(obj *mock.Mock) { // // obj.On("EncodeTo", mock.Anything).Return(nil) // // obj.On("Close").Return(nil) // // }) // // // Create a temporary directory for testing // // tempDir, err := os.MkdirTemp("", "block_run_test") // // Expect(err).NotTo(HaveOccurred()) // // defer os.RemoveAll(tempDir) // // // Update the config to use the temporary directory // // config := tt.GivenDetail.config // // config.Dir = tempDir // // ctx, cancel := context.WithCancel(context.Background()) // // b := &block{ // // ctx: ctx, // // cancel: cancel, // // chunks: chunkChain{chunk1, chunk2}, // // blockDirpath: filepath.Join(tempDir, blockDirname(config.Start, config.Start.Add(config.Duration))), // // indexDirpath: filepath.Join(tempDir, blockDirname(config.Start, config.Start.Add(config.Duration)), indexDirname), // // chunkDirpath: filepath.Join(tempDir, blockDirname(config.Start, config.Start.Add(config.Duration)), chunkDirname), // // chunkFactory: chunkFactory, // // primaryIndex: primaryFactory.New(), // // invertedIndex: invertedFactory.New(), // // vectorIndex: vectorFactory.New(), // // config: atomic.Pointer[Config]{}, // // } // // b.config.Store(config) // // b.state.Store(tt.GivenDetail.initialState) // // // Setup test scenario // // if tt.GivenDetail.setup != nil { // // tt.GivenDetail.setup(b, mockClock) // // } // // // When. // // // Run block in goroutine. // // done := make(chan error) // // go func() { // // done <- b.Run() // // }() // // // Wait for state transition. // // time.Sleep(100 * time.Millisecond) // // clk.(*clock.Mock).Add(35 * time.Second) // Wait tick // // // Then. // // // Verify state. // // Expect(b.state.Load()).To(Equal(tt.ThenExpected.state)) // // if tt.ThenExpected.chunksLen > 0 { // // Expect(b.chunks).To(HaveLen(tt.ThenExpected.chunksLen)) // // } // // if tt.ThenExpected.chunksNil { // // Expect(b.chunks).To(BeNil()) // // } // // if tt.ThenExpected.primaryNil { // // Expect(b.primaryIndex).To(BeNil()) // // } // // if tt.ThenExpected.invertedNil { // // Expect(b.invertedIndex).To(BeNil()) // // } // // if tt.ThenExpected.vectorNil { // // Expect(b.vectorIndex).To(BeNil()) // // } // // if tt.ThenExpected.archiveExists { // // archivePath := filepath.Join(b.blockDirpath, archiveMetaFilename) // // _, err := os.Stat(archivePath) // // Expect(err).NotTo(HaveOccurred()) // Check archive file exists // // } // // // Cleanup. // // b.cancel() // // err = <-done // // Expect(err).NotTo(HaveOccurred()) // // }) // // } // // } // // func TestReload(t *testing.T) { // // RegisterTestingT(t) // // t0 := time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC) // // tests := []struct { // // scenario string // // given string // // when string // // then string // // initialCfg *Config // // reloadCfg *Config // // expectedErr string // // }{ // // { // // scenario: "Successful reload with mutable fields", // // given: "a block with initial config", // // when: "reloading with valid changes to mutable fields", // // then: "should accept the new config", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // Retention: 7 * 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // WriteableWindow: 72 * time.Hour, // Changed // // Retention: 10 * 24 * time.Hour, // Changed // // }, // // }, // // { // // scenario: "Attempt to change ParentDir", // // given: "a block with initial config", // // when: "trying to change ParentDir", // // then: "should reject the change", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // ParentDir: "/new/path", // // }, // // expectedErr: "cannot reload the parent dir, MUST pass the same dir, or set it to empty for unchange", // // }, // // { // // scenario: "Attempt to change BlockDirname", // // given: "a block with initial config", // // when: "trying to change BlockDirname", // // then: "should reject the change", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // BlockDirname: blockDirname(t0.Add(24*time.Hour), t0.Add(48*time.Hour)), // // }, // // expectedErr: "cannot reload the block dirname, MUST pass the same dirname, or set it to empty for unchange", // // }, // // { // // scenario: "Attempt to change Start time", // // given: "a block with initial config", // // when: "trying to change Start time", // // then: "should reject the change", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // Start: t0.Add(24 * time.Hour), // // }, // // expectedErr: "cannot reload the start time, MUST pass the same start time, or set it to empty for unchange", // // }, // // { // // scenario: "Attempt to change Duration", // // given: "a block with initial config", // // when: "trying to change Duration", // // then: "should reject the change", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // Duration: 48 * time.Hour, // // }, // // expectedErr: "cannot reload the duration, MUST pass the same duration, or set it to empty for unchange", // // }, // // { // // scenario: "Invalid config validation", // // given: "a block with initial config", // // when: "trying to reload with invalid config", // // then: "should reject the change", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // WriteableWindow: 48 * time.Hour, // // Retention: 7 * 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // WriteableWindow: 1 * time.Hour, // Invalid: less than Duration // // }, // // expectedErr: "validate config: writeable window must be greater than 24h0m0s", // // }, // // { // // scenario: "Concurrent reload attempt", // // given: "a block that is already reloading", // // when: "trying to reload again", // // then: "should reject the concurrent reload", // // initialCfg: &Config{ // // ParentDir: "/tmp", // // BlockDirname: blockDirname(t0, t0.Add(24*time.Hour)), // // Start: t0, // // Duration: 24 * time.Hour, // // }, // // reloadCfg: &Config{ // // WriteableWindow: 72 * time.Hour, // // }, // // expectedErr: "concurrent reloading is forbidden", // // }, // // } // // for _, tt := range tests { // // t.Run(tt.scenario, func(t *testing.T) { // // // Create block with initial config // // b := &block{ // // config: atomic.Value{}, // // newConfigToReload: make(chan *Config, 1), // // newConfigReloadResult: make(chan error, 1), // // } // // b.config.Store(tt.initialCfg) // // // For concurrent reload test // // if tt.expectedErr == "concurrent reloading is forbidden" { // // b.reloading.Store(true) // // } // // // Setup goroutine to handle reload request for successful cases // // if tt.expectedErr == "" { // // go func() { // // cfg := <-b.newConfigToReload // // // Verify the config was properly merged // // Expect(cfg.ParentDir).To(Equal(tt.initialCfg.ParentDir)) // // Expect(cfg.BlockDirname).To(Equal(tt.initialCfg.BlockDirname)) // // Expect(cfg.Start).To(Equal(tt.initialCfg.Start)) // // Expect(cfg.Duration).To(Equal(tt.initialCfg.Duration)) // // if tt.reloadCfg.WriteableWindow != 0 { // // Expect(cfg.WriteableWindow).To(Equal(tt.reloadCfg.WriteableWindow)) // // } // // if tt.reloadCfg.Retention != 0 { // // Expect(cfg.Retention).To(Equal(tt.reloadCfg.Retention)) // // } // // b.config.Store(cfg) // // b.newConfigReloadResult <- nil // // }() // // } // // // Execute test // // err := b.Reload(tt.reloadCfg) // // // Verify results // // if tt.expectedErr != "" { // // Expect(err).To(HaveOccurred()) // // Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) // // } else { // // Expect(err).NotTo(HaveOccurred()) // // // Verify config was updated for successful cases // // newConfig := b.config.Load().(*Config) // // if tt.reloadCfg.WriteableWindow != 0 { // // Expect(newConfig.WriteableWindow).To(Equal(tt.reloadCfg.WriteableWindow)) // // } // // if tt.reloadCfg.Retention != 0 { // // Expect(newConfig.Retention).To(Equal(tt.reloadCfg.Retention)) // // } // // } // // }) // // } // // } // // func TestAppend(t *testing.T) { // // RegisterTestingT(t) // // t0 := time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC) // // clk = clock.NewMock() // // clk.(*clock.Mock).Set(t0) // // tests := []struct { // // scenario string // // given string // // when string // // then string // // state State // // setupChunkMocks func(chunkMock *mock.Mock) // // setupPrimaryMocks func(primaryMock *mock.Mock) // // setupInvertedMocks func(invertedMock *mock.Mock) // // setupVectorMocks func(vectorMock *mock.Mock) // // feeds []*chunk.Feed // // expectedErr string // // }{ // // { // // scenario: "Successfully append feeds to hot block", // // given: "a hot block", // // when: "appending feeds", // // then: "should write to chunk and update indices", // // state: StateHot, // // setupChunkMocks: func(chunkMock *mock.Mock) { // // chunkMock.On("Count").Return(uint32(0)) // // chunkMock.On("Append", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { // // feeds := args.Get(0).([]*chunk.Feed) // // callback := args.Get(1).(func(*chunk.Feed, uint64)) // // for i, feed := range feeds { // // callback(feed, uint64(i)) // // } // // }) // // }, // // setupPrimaryMocks: func(primaryMock *mock.Mock) { // // primaryMock.On("Add", uint64(1), primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 0, // // Time: t0, // // }).Return(nil) // // }, // // setupInvertedMocks: func(invertedMock *mock.Mock) { // // invertedMock.On("Add", uint64(1), model.Labels{model.Label{Key: "k1", Value: "v1"}}).Return(nil) // // }, // // setupVectorMocks: func(vectorMock *mock.Mock) { // // vectorMock.On("Add", uint64(1), [][]float32{{1, 2, 3}}).Return(nil) // // }, // // feeds: []*chunk.Feed{ // // { // // ID: 1, // // Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, // // Vectors: [][]float32{{1, 2, 3}}, // // Time: t0, // // }, // // }, // // }, // // { // // scenario: "Append to non-hot block", // // given: "a hot-readonly block", // // when: "appending feeds", // // then: "should return error", // // state: StateHotReadonly, // // feeds: []*chunk.Feed{{ID: 1}}, // // expectedErr: "block is not writable", // // }, // // { // // scenario: "Create new chunk when current is full", // // given: "a hot block with chunk near capacity", // // when: "appending feeds that would exceed capacity", // // then: "should create new chunk and write to it", // // state: StateHot, // // setupChunkMocks: func(chunkMock *mock.Mock) { // // chunkMock.On("Count").Return(uint32(estimatedChunkFeedsLimit)).Twice() // // chunkMock.On("EnsureReadonly").Return(nil).Once() // // chunkMock.On("Append", mock.Anything, mock.Anything).Return(nil).Once() // // }, // // setupPrimaryMocks: func(primaryMock *mock.Mock) { // // primaryMock.On("Add", uint64(1), primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 0, // // Time: t0, // // }).Return(nil) // // }, // // setupInvertedMocks: func(invertedMock *mock.Mock) { // // invertedMock.On("Add", uint64(1), model.Labels{model.Label{Key: "k1", Value: "v1"}}).Return(nil) // // }, // // setupVectorMocks: func(vectorMock *mock.Mock) { // // vectorMock.On("Add", uint64(1), [][]float32{{1, 2, 3}}).Return(nil) // // }, // // feeds: []*chunk.Feed{ // // { // // ID: 1, // // Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, // // Vectors: [][]float32{{1, 2, 3}}, // // Time: t0, // // }, // // }, // // }, // // } // // for _, tt := range tests { // // t.Run(tt.scenario, func(t *testing.T) { // // // Create mock factories // // chunkFactory := chunk.NewFactory(func(obj *mock.Mock) { // // if tt.setupChunkMocks != nil { // // tt.setupChunkMocks(obj) // // } // // }) // // primaryFactory := primaryindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupPrimaryMocks != nil { // // tt.setupPrimaryMocks(obj) // // } // // }) // // invertedFactory := invertedindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupInvertedMocks != nil { // // tt.setupInvertedMocks(obj) // // } // // }) // // vectorFactory := vectorindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupVectorMocks != nil { // // tt.setupVectorMocks(obj) // // } // // }) // // // Create block. // // chunk, err := chunkFactory.New(&chunk.Config{ // // Path: "/tmp/append", // // }) // // Expect(err).NotTo(HaveOccurred()) // // b := &block{ // // config: atomic.Value{}, // // state: atomic.Value{}, // // chunks: chunkChain{chunk}, // // primaryIndex: primaryFactory.New(), // // invertedIndex: invertedFactory.New(), // // vectorIndex: vectorFactory.New(), // // chunkFactory: chunkFactory, // // } // // b.config.Store(&Config{ // // ParentDir: "/tmp", // // Start: t0, // // Duration: 24 * time.Hour, // // }) // // b.state.Store(tt.state) // // // Execute test. // // err = b.Append(context.Background(), tt.feeds...) // // if tt.expectedErr != "" { // // Expect(err).To(HaveOccurred()) // // Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) // // } else { // // Expect(err).NotTo(HaveOccurred()) // // } // // }) // // } // // } // // func TestQuery(t *testing.T) { // // RegisterTestingT(t) // // t0 := time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC) // // clk = clock.NewMock() // // clk.(*clock.Mock).Set(t0) // // tests := []struct { // // scenario string // // given string // // when string // // then string // // state State // // setupChunkMocks func(chunkMock *mock.Mock) // // setupPrimaryMocks func(primaryMock *mock.Mock) // // setupInvertedMocks func(invertedMock *mock.Mock) // // setupVectorMocks func(vectorMock *mock.Mock) // // queryOpts QueryOptions // // expectedFeeds []*FeedVO // // expectedErr string // // }{ // // { // // scenario: "Query hot block with label filters", // // given: "a hot block with indexed feeds", // // when: "querying with label filters", // // then: "should return matching feeds", // // state: StateHot, // // setupInvertedMocks: func(m *mock.Mock) { // // m.On("Search", "k1", true, "v1").Return(map[uint64]bool{1: true}) // // }, // // setupPrimaryMocks: func(m *mock.Mock) { // // m.On("Search", uint64(1)).Return(primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 0, // // Time: t0, // // }, true) // // }, // // setupChunkMocks: func(m *mock.Mock) { // // m.On("Read", uint64(0)).Return(&chunk.Feed{ // // ID: 1, // // Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, // // Time: t0, // // }, nil) // // }, // // queryOpts: QueryOptions{ // // Filters: []LabelFilter{{ // // Label: "k1", // // Equal: true, // // Value: "v1", // // }}, // // Start: t0.Add(-1 * time.Hour), // // End: t0.Add(1 * time.Hour), // // }, // // expectedFeeds: []*FeedVO{{ // // ID: 1, // // Labels: model.Labels{model.Label{Key: "k1", Value: "v1"}}, // // Time: t0, // // Score: 0, // // }}, // // }, // // { // // scenario: "Query with semantic filter", // // given: "a block with vector indexed feeds", // // when: "querying with semantic filter", // // then: "should return semantically matching feeds with scores", // // state: StateHot, // // setupVectorMocks: func(m *mock.Mock) { // // m.On("Search", []float32{0.1, 0.2}, float32(0.8)).Return(map[uint64]float32{ // // 1: 0.9, // // }) // // }, // // setupPrimaryMocks: func(m *mock.Mock) { // // m.On("Search", uint64(1)).Return(primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 0, // // Time: t0, // // }, true) // // }, // // setupChunkMocks: func(m *mock.Mock) { // // m.On("Read", uint64(0)).Return(&chunk.Feed{ // // ID: 1, // // Labels: model.Labels{}, // // Time: t0, // // Vectors: [][]float32{{0.1, 0.2}}, // // }, nil) // // }, // // queryOpts: QueryOptions{ // // SemanticFilter: SemanticFilter{ // // QueryVector: []float32{0.1, 0.2}, // // Threshold: 0.8, // // }, // // Start: t0.Add(-1 * time.Hour), // // End: t0.Add(1 * time.Hour), // // }, // // expectedFeeds: []*FeedVO{{ // // ID: 1, // // Labels: model.Labels{}, // // Time: t0, // // Score: 0.9, // // }}, // // }, // // { // // scenario: "Query with time range filter", // // given: "a block with feeds at different times", // // when: "querying with time range", // // then: "should only return feeds within range", // // state: StateHot, // // setupPrimaryMocks: func(m *mock.Mock) { // // m.On("IDs").Return(map[uint64]bool{1: true, 2: true}) // // m.On("Search", uint64(1)).Return(primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 0, // // Time: t0, // // }, true) // // m.On("Search", uint64(2)).Return(primaryindex.FeedRef{ // // Chunk: 0, // // Offset: 1, // // Time: t0.Add(2 * time.Hour), // // }, true) // // }, // // setupChunkMocks: func(m *mock.Mock) { // // m.On("Read", uint64(0)).Return(&chunk.Feed{ // // ID: 1, // // Labels: model.Labels{}, // // Time: t0, // // }, nil) // // }, // // queryOpts: QueryOptions{ // // Start: t0.Add(-1 * time.Hour), // // End: t0.Add(1 * time.Hour), // // }, // // expectedFeeds: []*FeedVO{{ // // ID: 1, // // Labels: model.Labels{}, // // Time: t0, // // }}, // // }, // // } // // for _, tt := range tests { // // t.Run(tt.scenario, func(t *testing.T) { // // // Create mock factories // // chunkFactory := chunk.NewFactory(func(obj *mock.Mock) { // // if tt.setupChunkMocks != nil { // // tt.setupChunkMocks(obj) // // } // // }) // // primaryFactory := primaryindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupPrimaryMocks != nil { // // tt.setupPrimaryMocks(obj) // // } // // }) // // invertedFactory := invertedindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupInvertedMocks != nil { // // tt.setupInvertedMocks(obj) // // } // // }) // // vectorFactory := vectorindex.NewFactory(func(obj *mock.Mock) { // // if tt.setupVectorMocks != nil { // // tt.setupVectorMocks(obj) // // } // // }) // // // Create block // // chunk, err := chunkFactory.New(&chunk.Config{ // // Path: "/tmp", // // }) // // Expect(err).NotTo(HaveOccurred()) // // b := &block{ // // config: atomic.Value{}, // // state: atomic.Value{}, // // chunks: chunkChain{chunk}, // // primaryIndex: primaryFactory.New(), // // invertedIndex: invertedFactory.New(), // // vectorIndex: vectorFactory.New(), // // chunkFactory: chunkFactory, // // ctx: context.Background(), // // blockDirpath: "/tmp", // // chunkDirpath: "/tmp/chunk", // // indexDirpath: "/tmp/index", // // } // // b.config.Store(&Config{ // // ParentDir: "/tmp", // // Start: t0, // // Duration: 24 * time.Hour, // // }) // // b.state.Store(tt.state) // // // Execute test // // feeds, err := b.Query(context.Background(), tt.queryOpts) // // // Verify results // // if tt.expectedErr != "" { // // Expect(err).To(HaveOccurred()) // // Expect(err.Error()).To(ContainSubstring(tt.expectedErr)) // // } else { // // Expect(err).NotTo(HaveOccurred()) // // Expect(feeds).To(HaveLen(len(tt.expectedFeeds))) // // for i, feed := range feeds { // // Expect(feed.ID).To(Equal(tt.expectedFeeds[i].ID)) // // Expect(feed.Labels).To(Equal(tt.expectedFeeds[i].Labels)) // // Expect(feed.Time).To(Equal(tt.expectedFeeds[i].Time)) // // Expect(feed.Score).To(Equal(tt.expectedFeeds[i].Score)) // // } // // } // // }) // // } // // }