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

447 lines
14 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 feed
// import (
// "context"
// "os"
// "testing"
// "time"
//
// "github.com/benbjohnson/clock"
// . "github.com/onsi/gomega"
// "github.com/stretchr/testify/mock"
// "github.com/glidea/zenfeed/pkg/config"
// "github.com/glidea/zenfeed/pkg/storage/feed/block"
// "github.com/glidea/zenfeed/pkg/storage/feed/block/chunk"
// "github.com/glidea/zenfeed/pkg/test"
// timeutil "github.com/glidea/zenfeed/pkg/util/time"
// )
// func TestNew(t *testing.T) {
// RegisterTestingT(t)
// type givenDetail struct {
// now time.Time
// blocksOnDisk []string // Block directory names in format "2006-01-02T15:04:05Z-2006-01-02T15:04:05Z"
// }
// type whenDetail struct {
// app *config.App
// }
// type thenExpected struct {
// storage storage
// storageHotLen int
// storageColdLen int
// blockCalls []func(obj *mock.Mock)
// }
// tests := []test.Case[givenDetail, whenDetail, thenExpected]{
// {
// Scenario: "Create a new storage from an empty directory",
// Given: "just mock a time",
// When: "call New with a config with a data directory",
// Then: "should return a new storage and a hot block created",
// GivenDetail: givenDetail{
// now: timeutil.MustParse("2025-03-03T10:00:00Z"),
// },
// WhenDetail: whenDetail{
// app: &config.App{
// DB: config.DB{
// Dir: "/tmp/TestNew",
// },
// },
// },
// ThenExpected: thenExpected{
// storage: storage{
// config: &Config{
// Dir: "/tmp/TestNew",
// },
// },
// storageHotLen: 1,
// storageColdLen: 0,
// },
// },
// {
// Scenario: "Create a storage from existing directory with blocks",
// Given: "existing blocks on disk",
// GivenDetail: givenDetail{
// now: timeutil.MustParse("2025-03-03T10:00:00Z"),
// blocksOnDisk: []string{
// "2025-03-02T10:00:00Z ~ 2025-03-03T10:00:00Z", // Hot block
// "2025-03-01T10:00:00Z ~ 2025-03-02T10:00:00Z", // Cold block
// "2025-02-28T10:00:00Z ~ 2025-03-01T10:00:00Z", // Cold block
// },
// },
// When: "call New with a config with existing data directory",
// WhenDetail: whenDetail{
// app: &config.App{
// DB: config.DB{
// Dir: "/tmp/TestNew",
// WriteableWindow: 49 * time.Hour,
// },
// },
// },
// Then: "should return a storage with existing blocks loaded",
// ThenExpected: thenExpected{
// storage: storage{
// config: &Config{
// Dir: "/tmp/TestNew",
// Block: BlockConfig{
// WriteableWindow: 49 * time.Hour,
// },
// },
// },
// storageHotLen: 1,
// storageColdLen: 2,
// blockCalls: []func(obj *mock.Mock){
// func(m *mock.Mock) {
// m.On("State").Return(block.StateHot).Once()
// },
// func(m *mock.Mock) {
// m.On("State").Return(block.StateCold).Once()
// },
// func(m *mock.Mock) {
// m.On("State").Return(block.StateCold).Once()
// },
// },
// },
// },
// }
// for _, tt := range tests {
// t.Run(tt.Scenario, func(t *testing.T) {
// // Given.
// c := clock.NewMock()
// c.Set(tt.GivenDetail.now)
// clk = c // Set global clock.
// defer func() { clk = clock.New() }()
// // Create test directories if needed
// if len(tt.GivenDetail.blocksOnDisk) > 0 {
// for _, blockDir := range tt.GivenDetail.blocksOnDisk {
// err := os.MkdirAll(tt.WhenDetail.app.DB.Dir+"/"+blockDir, 0755)
// Expect(err).To(BeNil())
// }
// }
// // When.
// var calls int
// var blockCalls []*mock.Mock
// blockFactory := block.NewFactory(func(obj *mock.Mock) {
// if calls < len(tt.ThenExpected.blockCalls) {
// tt.ThenExpected.blockCalls[calls](obj)
// calls++
// blockCalls = append(blockCalls, obj)
// }
// })
// s, err := new(tt.WhenDetail.app, blockFactory)
// defer os.RemoveAll(tt.WhenDetail.app.DB.Dir)
// // Then.
// Expect(err).To(BeNil())
// Expect(s).NotTo(BeNil())
// storage := s.(*storage)
// Expect(storage.config).To(Equal(tt.ThenExpected.storage.config))
// Expect(len(storage.hot.blocks)).To(Equal(tt.ThenExpected.storageHotLen))
// Expect(len(storage.cold.blocks)).To(Equal(tt.ThenExpected.storageColdLen))
// for _, call := range blockCalls {
// call.AssertExpectations(t)
// }
// })
// }
// }
// func TestAppend(t *testing.T) {
// RegisterTestingT(t)
// type givenDetail struct {
// hotBlocks []func(m *mock.Mock)
// coldBlocks []func(m *mock.Mock)
// }
// type whenDetail struct {
// feeds []*chunk.Feed
// }
// type thenExpected struct {
// err string
// }
// tests := []test.Case[givenDetail, whenDetail, thenExpected]{
// {
// Scenario: "Append feeds to hot block",
// Given: "a storage with one hot block",
// When: "append feeds within hot block time range",
// Then: "should append feeds to hot block successfully",
// GivenDetail: givenDetail{
// hotBlocks: []func(m *mock.Mock){
// func(m *mock.Mock) {
// m.On("Start").Return(timeutil.MustParse("2025-03-02T10:00:00Z")).Twice()
// m.On("End").Return(timeutil.MustParse("2025-03-03T10:00:00Z")).Twice()
// m.On("State").Return(block.StateHot).Twice()
// m.On("Append", mock.Anything, []*chunk.Feed{
// {ID: 1, Time: timeutil.MustParse("2025-03-02T11:00:00Z")},
// {ID: 2, Time: timeutil.MustParse("2025-03-02T12:00:00Z")},
// }).Return(nil)
// },
// },
// },
// WhenDetail: whenDetail{
// feeds: []*chunk.Feed{
// {ID: 1, Time: timeutil.MustParse("2025-03-02T11:00:00Z")},
// {ID: 2, Time: timeutil.MustParse("2025-03-02T12:00:00Z")},
// },
// },
// ThenExpected: thenExpected{
// err: "",
// },
// },
// {
// Scenario: "Append feeds to non-hot block",
// Given: "a storage with hot and cold blocks",
// When: "append feeds with time in cold block range",
// Then: "should return error",
// GivenDetail: givenDetail{
// coldBlocks: []func(m *mock.Mock){
// func(m *mock.Mock) {},
// },
// },
// WhenDetail: whenDetail{
// feeds: []*chunk.Feed{
// {ID: 1, Time: timeutil.MustParse("2025-03-01T11:00:00Z")},
// },
// },
// ThenExpected: thenExpected{
// err: "cannot find hot block",
// },
// },
// }
// for _, tt := range tests {
// t.Run(tt.Scenario, func(t *testing.T) {
// // Given.
// calls := 0
// var blockMocks []*mock.Mock
// blockFactory := block.NewFactory(func(obj *mock.Mock) {
// if calls < len(tt.GivenDetail.hotBlocks) {
// tt.GivenDetail.hotBlocks[calls](obj)
// calls++
// blockMocks = append(blockMocks, obj)
// }
// })
// var hotBlocks blockChain
// for range tt.GivenDetail.hotBlocks {
// block, err := blockFactory.New(nil, nil, nil, nil, nil)
// Expect(err).To(BeNil())
// hotBlocks.add(block)
// }
// blockFactory = block.NewFactory(func(obj *mock.Mock) {
// if calls < len(tt.GivenDetail.coldBlocks) {
// tt.GivenDetail.coldBlocks[calls](obj)
// calls++
// blockMocks = append(blockMocks, obj)
// }
// })
// var coldBlocks blockChain
// for range tt.GivenDetail.coldBlocks {
// block, err := blockFactory.New(nil, nil, nil, nil, nil)
// Expect(err).To(BeNil())
// coldBlocks.add(block)
// }
// s := storage{
// hot: &hotBlocks,
// cold: &coldBlocks,
// }
// // When.
// err := s.Append(context.Background(), tt.WhenDetail.feeds...)
// // Then.
// if tt.ThenExpected.err != "" {
// Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.err))
// } else {
// Expect(err).To(BeNil())
// }
// for _, m := range blockMocks {
// m.AssertExpectations(t)
// }
// })
// }
// }
// func TestQuery(t *testing.T) {
// RegisterTestingT(t)
// type givenDetail struct {
// hotBlocks []func(m *mock.Mock)
// coldBlocks []func(m *mock.Mock)
// }
// type whenDetail struct {
// query block.QueryOptions
// }
// type thenExpected struct {
// feeds []*block.FeedVO
// err string
// }
// tests := []test.Case[givenDetail, whenDetail, thenExpected]{
// {
// Scenario: "Query feeds from hot blocks",
// Given: "a storage with one hot block containing feeds",
// When: "querying with time range within hot block",
// Then: "should return matching feeds from hot block",
// GivenDetail: givenDetail{
// hotBlocks: []func(m *mock.Mock){
// func(m *mock.Mock) {
// m.On("Start").Return(timeutil.MustParse("2025-03-02T10:00:00Z")).Once()
// m.On("End").Return(timeutil.MustParse("2025-03-03T10:00:00Z")).Once()
// m.On("Query", mock.Anything, mock.MatchedBy(func(q block.QueryOptions) bool {
// return q.Start.Equal(timeutil.MustParse("2025-03-02T12:00:00Z")) &&
// q.End.Equal(timeutil.MustParse("2025-03-02T14:00:00Z"))
// })).Return([]*block.FeedVO{
// {ID: 1, Time: timeutil.MustParse("2025-03-02T12:30:00Z")},
// {ID: 2, Time: timeutil.MustParse("2025-03-02T13:00:00Z")},
// }, nil)
// },
// },
// },
// WhenDetail: whenDetail{
// query: block.QueryOptions{
// Start: timeutil.MustParse("2025-03-02T12:00:00Z"),
// End: timeutil.MustParse("2025-03-02T14:00:00Z"),
// Limit: 10,
// },
// },
// ThenExpected: thenExpected{
// feeds: []*block.FeedVO{
// {ID: 2, Time: timeutil.MustParse("2025-03-02T13:00:00Z")},
// {ID: 1, Time: timeutil.MustParse("2025-03-02T12:30:00Z")},
// },
// err: "",
// },
// },
// {
// Scenario: "Query feeds from multiple blocks",
// Given: "a storage with hot and cold blocks containing feeds",
// When: "querying with time range spanning multiple blocks",
// Then: "should return combined and sorted feeds from all matching blocks",
// GivenDetail: givenDetail{
// hotBlocks: []func(m *mock.Mock){
// func(m *mock.Mock) {
// m.On("Start").Return(timeutil.MustParse("2025-03-02T10:00:00Z"))
// m.On("End").Return(timeutil.MustParse("2025-03-03T10:00:00Z"))
// m.On("Query", mock.Anything, mock.MatchedBy(func(q block.QueryOptions) bool {
// return !q.Start.IsZero() && q.End.IsZero()
// })).Return([]*block.FeedVO{
// {ID: 3, Time: timeutil.MustParse("2025-03-02T15:00:00Z")},
// {ID: 4, Time: timeutil.MustParse("2025-03-02T16:00:00Z")},
// }, nil)
// },
// },
// coldBlocks: []func(m *mock.Mock){
// func(m *mock.Mock) {
// m.On("Start").Return(timeutil.MustParse("2025-03-01T10:00:00Z"))
// m.On("End").Return(timeutil.MustParse("2025-03-02T10:00:00Z"))
// m.On("Query", mock.Anything, mock.MatchedBy(func(q block.QueryOptions) bool {
// return !q.Start.IsZero() && q.End.IsZero()
// })).Return([]*block.FeedVO{
// {ID: 1, Time: timeutil.MustParse("2025-03-01T15:00:00Z")},
// {ID: 2, Time: timeutil.MustParse("2025-03-01T16:00:00Z")},
// }, nil)
// },
// },
// },
// WhenDetail: whenDetail{
// query: block.QueryOptions{
// Start: timeutil.MustParse("2025-03-01T12:00:00Z"),
// Limit: 3,
// },
// },
// ThenExpected: thenExpected{
// feeds: []*block.FeedVO{
// {ID: 4, Time: timeutil.MustParse("2025-03-02T16:00:00Z")},
// {ID: 3, Time: timeutil.MustParse("2025-03-02T15:00:00Z")},
// {ID: 2, Time: timeutil.MustParse("2025-03-01T16:00:00Z")},
// },
// err: "",
// },
// },
// }
// for _, tt := range tests {
// t.Run(tt.Scenario, func(t *testing.T) {
// // Given.
// calls := 0
// var blockMocks []*mock.Mock
// blockFactory := block.NewFactory(func(obj *mock.Mock) {
// if calls < len(tt.GivenDetail.hotBlocks) {
// tt.GivenDetail.hotBlocks[calls](obj)
// calls++
// blockMocks = append(blockMocks, obj)
// }
// })
// var hotBlocks blockChain
// for range tt.GivenDetail.hotBlocks {
// block, err := blockFactory.New(nil, nil, nil, nil, nil)
// Expect(err).To(BeNil())
// hotBlocks.add(block)
// }
// blockFactory = block.NewFactory(func(obj *mock.Mock) {
// if calls < len(tt.GivenDetail.hotBlocks)+len(tt.GivenDetail.coldBlocks) {
// tt.GivenDetail.coldBlocks[calls-len(tt.GivenDetail.hotBlocks)](obj)
// calls++
// blockMocks = append(blockMocks, obj)
// }
// })
// var coldBlocks blockChain
// for range tt.GivenDetail.coldBlocks {
// block, err := blockFactory.New(nil, nil, nil, nil, nil)
// Expect(err).To(BeNil())
// coldBlocks.add(block)
// }
// s := storage{
// hot: &hotBlocks,
// cold: &coldBlocks,
// }
// // When.
// feeds, err := s.Query(context.Background(), tt.WhenDetail.query)
// // Then.
// if tt.ThenExpected.err != "" {
// Expect(err).NotTo(BeNil())
// Expect(err.Error()).To(ContainSubstring(tt.ThenExpected.err))
// } else {
// Expect(err).To(BeNil())
// Expect(feeds).To(HaveLen(len(tt.ThenExpected.feeds)))
// // Check feeds match expected
// for i, feed := range feeds {
// Expect(feed.ID).To(Equal(tt.ThenExpected.feeds[i].ID))
// Expect(feed.Time).To(Equal(tt.ThenExpected.feeds[i].Time))
// Expect(feed.Labels).To(Equal(tt.ThenExpected.feeds[i].Labels))
// }
// }
// for _, m := range blockMocks {
// m.AssertExpectations(t)
// }
// })
// }
// }