Files
zenfeed/pkg/storage/feed/block/block_test.go
glidea 8b33df8a05 init
2025-04-19 15:50:26 +08:00

1271 lines
44 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
// 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))
// // }
// // }
// // })
// // }
// // }