feedx 0.12.7 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -37
  3. data/.golangci.yml +13 -4
  4. data/.rubocop.yml +8 -14
  5. data/.tool-versions +1 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +54 -68
  8. data/Makefile +3 -3
  9. data/README.md +3 -1
  10. data/compression.go +29 -0
  11. data/compression_test.go +73 -61
  12. data/consumer.go +96 -152
  13. data/consumer_test.go +124 -59
  14. data/example_test.go +140 -0
  15. data/feedx.gemspec +2 -10
  16. data/feedx.go +16 -31
  17. data/feedx_ext_test.go +13 -3
  18. data/feedx_test.go +24 -26
  19. data/format.go +29 -19
  20. data/format_test.go +84 -56
  21. data/go.mod +11 -7
  22. data/go.sum +16 -138
  23. data/incremental.go +122 -0
  24. data/incremental_test.go +62 -0
  25. data/lib/feedx/cache/abstract.rb +3 -3
  26. data/lib/feedx/cache/value.rb +6 -6
  27. data/lib/feedx/compression/abstract.rb +2 -2
  28. data/lib/feedx/compression/gzip.rb +4 -4
  29. data/lib/feedx/consumer.rb +8 -8
  30. data/lib/feedx/format/abstract.rb +6 -6
  31. data/lib/feedx/format/json.rb +2 -2
  32. data/lib/feedx/format/protobuf.rb +6 -6
  33. data/lib/feedx/format.rb +1 -3
  34. data/lib/feedx/producer.rb +11 -11
  35. data/lib/feedx/stream.rb +2 -2
  36. data/lib/feedx.rb +2 -3
  37. data/manifest.go +65 -0
  38. data/producer.go +34 -137
  39. data/producer_test.go +46 -60
  40. data/reader.go +142 -41
  41. data/reader_test.go +86 -35
  42. data/scheduler.go +176 -0
  43. data/scheduler_test.go +128 -0
  44. data/writer.go +13 -13
  45. data/writer_test.go +61 -44
  46. metadata +12 -137
  47. data/.github/workflows/lint.yml +0 -18
  48. data/ext/parquet/decoder.go +0 -59
  49. data/ext/parquet/decoder_test.go +0 -88
  50. data/ext/parquet/encoder.go +0 -27
  51. data/ext/parquet/encoder_test.go +0 -70
  52. data/ext/parquet/go.mod +0 -12
  53. data/ext/parquet/go.sum +0 -193
  54. data/ext/parquet/parquet.go +0 -78
  55. data/ext/parquet/parquet_test.go +0 -28
  56. data/ext/parquet/testdata/alltypes_plain.parquet +0 -0
  57. data/lib/feedx/format/parquet.rb +0 -102
  58. data/spec/feedx/cache/memory_spec.rb +0 -23
  59. data/spec/feedx/cache/value_spec.rb +0 -19
  60. data/spec/feedx/compression/gzip_spec.rb +0 -17
  61. data/spec/feedx/compression/none_spec.rb +0 -15
  62. data/spec/feedx/compression_spec.rb +0 -19
  63. data/spec/feedx/consumer_spec.rb +0 -49
  64. data/spec/feedx/format/abstract_spec.rb +0 -21
  65. data/spec/feedx/format/json_spec.rb +0 -27
  66. data/spec/feedx/format/parquet_spec.rb +0 -30
  67. data/spec/feedx/format/protobuf_spec.rb +0 -23
  68. data/spec/feedx/format_spec.rb +0 -21
  69. data/spec/feedx/producer_spec.rb +0 -74
  70. data/spec/feedx/stream_spec.rb +0 -109
  71. data/spec/spec_helper.rb +0 -57
data/reader_test.go CHANGED
@@ -1,56 +1,107 @@
1
1
  package feedx_test
2
2
 
3
3
  import (
4
- "context"
5
4
  "io"
6
- "io/ioutil"
7
- "time"
5
+ "reflect"
6
+ "testing"
8
7
 
9
8
  "github.com/bsm/bfs"
10
9
  "github.com/bsm/feedx"
11
10
  "github.com/bsm/feedx/internal/testdata"
12
- . "github.com/bsm/ginkgo"
13
- . "github.com/bsm/gomega"
14
11
  )
15
12
 
16
- var _ = Describe("Reader", func() {
17
- var subject *feedx.Reader
18
- var obj *bfs.Object
19
- var ctx = context.Background()
13
+ func TestReader(t *testing.T) {
14
+ t.Run("reads", func(t *testing.T) {
15
+ r := fixReader(t)
20
16
 
21
- BeforeEach(func() {
22
- obj = bfs.NewInMemObject("path/to/file.json")
23
- Expect(writeMulti(obj, 3, time.Time{})).To(Succeed())
17
+ if data, err := io.ReadAll(r); err != nil {
18
+ t.Fatal("unexpected error", err)
19
+ } else if exp, got := 111, len(data); exp != got {
20
+ t.Errorf("expected %v, got %v", exp, got)
21
+ } else if exp, got := int64(0), r.NumRead(); exp != got {
22
+ t.Errorf("expected %v, got %v", exp, got)
23
+ }
24
+ })
24
25
 
25
- var err error
26
- subject, err = feedx.NewReader(ctx, obj, nil)
27
- Expect(err).NotTo(HaveOccurred())
26
+ t.Run("decodes", func(t *testing.T) {
27
+ r := fixReader(t)
28
+ msgs := drainReader(t, r)
29
+ if exp := seedN(3); !reflect.DeepEqual(exp, msgs) {
30
+ t.Errorf("expected %#v, got %#v", exp, msgs)
31
+ }
32
+ if exp, got := int64(3), r.NumRead(); exp != got {
33
+ t.Errorf("expected %v, got %v", exp, got)
34
+ }
28
35
  })
36
+ }
37
+
38
+ func fixReader(t *testing.T) *feedx.Reader {
39
+ t.Helper()
29
40
 
30
- AfterEach(func() {
31
- Expect(subject.Close()).To(Succeed())
41
+ obj := bfs.NewInMemObject("path/to/file.jsonz")
42
+ if err := writeN(obj, 3, 0); err != nil {
43
+ t.Fatal("unexpected error", err)
44
+ }
45
+
46
+ r, err := feedx.NewReader(t.Context(), obj, nil)
47
+ if err != nil {
48
+ t.Fatal("unexpected error", err)
49
+ }
50
+
51
+ t.Cleanup(func() {
52
+ _ = r.Close()
32
53
  })
33
54
 
34
- It("reads", func() {
35
- data, err := ioutil.ReadAll(subject)
36
- Expect(err).NotTo(HaveOccurred())
37
- Expect(len(data)).To(BeNumerically("~", 110, 20))
38
- Expect(subject.NumRead()).To(Equal(0))
55
+ return r
56
+ }
57
+
58
+ func TestMultiReader(t *testing.T) {
59
+ t.Run("reads", func(t *testing.T) {
60
+ r := fixMultiReader(t)
61
+
62
+ if data, err := io.ReadAll(r); err != nil {
63
+ t.Fatal("unexpected error", err)
64
+ } else if exp, got := 222, len(data); exp != got {
65
+ t.Errorf("expected %v, got %v", exp, got)
66
+ } else if exp, got := int64(0), r.NumRead(); exp != got {
67
+ t.Errorf("expected %v, got %v", exp, got)
68
+ }
39
69
  })
40
70
 
41
- It("decodes", func() {
42
- var msgs []*testdata.MockMessage
43
- for {
44
- var msg testdata.MockMessage
45
- err := subject.Decode(&msg)
46
- if err == io.EOF {
47
- break
48
- }
49
- Expect(err).NotTo(HaveOccurred())
50
- msgs = append(msgs, &msg)
71
+ t.Run("decodes", func(t *testing.T) {
72
+ r := fixMultiReader(t)
73
+ msgs := drainReader(t, r)
74
+ if exp := seedN(6); !reflect.DeepEqual(exp, msgs) {
75
+ t.Errorf("expected %#v, got %#v", exp, msgs)
76
+ }
77
+ if exp, got := int64(6), r.NumRead(); exp != got {
78
+ t.Errorf("expected %v, got %v", exp, got)
51
79
  }
80
+ })
81
+ }
82
+
83
+ func fixMultiReader(t *testing.T) *feedx.Reader {
84
+ t.Helper()
52
85
 
53
- Expect(msgs).To(ConsistOf(seed(), seed(), seed()))
54
- Expect(subject.NumRead()).To(Equal(3))
86
+ obj := bfs.NewInMemObject("path/to/file.jsonz")
87
+ if err := writeN(obj, 3, 0); err != nil {
88
+ t.Fatal("unexpected error", err)
89
+ }
90
+
91
+ r := feedx.MultiReader(t.Context(), []*bfs.Object{obj, obj}, nil)
92
+ t.Cleanup(func() {
93
+ _ = r.Close()
55
94
  })
56
- })
95
+
96
+ return r
97
+ }
98
+
99
+ func drainReader(t *testing.T, r interface{ Decode(any) error }) []*testdata.MockMessage {
100
+ t.Helper()
101
+
102
+ msgs, err := readMessages(r)
103
+ if err != nil {
104
+ t.Fatal("unexpected error", err)
105
+ }
106
+ return msgs
107
+ }
data/scheduler.go ADDED
@@ -0,0 +1,176 @@
1
+ package feedx
2
+
3
+ import (
4
+ "context"
5
+ "sync"
6
+ "time"
7
+ )
8
+
9
+ // BeforeHook callbacks are run before jobs are started. It receives the local
10
+ // version before sync as an argument and may return false to abort the cycle.
11
+ type BeforeHook func(version int64) bool
12
+
13
+ // AfterHook callbacks are run after jobs have finished.
14
+ type AfterHook func(*Status, error)
15
+
16
+ // VersionCheck callbacks return the latest local version.
17
+ type VersionCheck func(context.Context) (int64, error)
18
+
19
+ // Scheduler runs cronjobs in regular intervals.
20
+ type Scheduler struct {
21
+ ctx context.Context
22
+ interval time.Duration
23
+
24
+ readerOpt *ReaderOptions
25
+ writerOpt *WriterOptions
26
+ versionCheck VersionCheck
27
+
28
+ // hooks
29
+ beforeHooks []BeforeHook
30
+ afterHooks []AfterHook
31
+ }
32
+
33
+ // Every creates a scheduler.
34
+ func Every(interval time.Duration) *Scheduler {
35
+ return &Scheduler{ctx: context.Background(), interval: interval}
36
+ }
37
+
38
+ // WithContext sets a custom context for the run.
39
+ func (s *Scheduler) WithContext(ctx context.Context) *Scheduler {
40
+ s.ctx = ctx
41
+ return s
42
+ }
43
+
44
+ // BeforeSync adds custom before hooks.
45
+ func (s *Scheduler) BeforeSync(hooks ...BeforeHook) *Scheduler {
46
+ s.beforeHooks = append(s.beforeHooks, hooks...)
47
+ return s
48
+ }
49
+
50
+ // AfterSync adds before hooks.
51
+ func (s *Scheduler) AfterSync(hooks ...AfterHook) *Scheduler {
52
+ s.afterHooks = append(s.afterHooks, hooks...)
53
+ return s
54
+ }
55
+
56
+ // WithReaderOptions sets custom reader options for consumers.
57
+ func (s *Scheduler) WithReaderOptions(opt *ReaderOptions) *Scheduler {
58
+ s.readerOpt = opt
59
+ return s
60
+ }
61
+
62
+ // Consume starts a consumer job.
63
+ func (s *Scheduler) Consume(csm Consumer, cfn ConsumeFunc) *CronJob {
64
+ return newCronJob(s.ctx, s.interval, func(ctx context.Context) {
65
+ version := csm.Version()
66
+ if !s.runBeforeHooks(version) {
67
+ return
68
+ }
69
+
70
+ status, err := csm.Consume(ctx, s.readerOpt, cfn)
71
+ s.runAfterHooks(status, err)
72
+ })
73
+ }
74
+
75
+ // WithWriterOptions sets custom writer options for producers.
76
+ func (s *Scheduler) WithWriterOptions(opt *WriterOptions) *Scheduler {
77
+ s.writerOpt = opt
78
+ return s
79
+ }
80
+
81
+ // WithVersionCheck sets a custom version check for producers.
82
+ func (s *Scheduler) WithVersionCheck(fn VersionCheck) *Scheduler {
83
+ s.versionCheck = fn
84
+ return s
85
+ }
86
+
87
+ // Produce starts a producer job.
88
+ func (s *Scheduler) Produce(pcr *Producer, pfn ProduceFunc) *CronJob {
89
+ return s.produce(func(ctx context.Context, version int64) (*Status, error) {
90
+ return pcr.Produce(ctx, version, s.writerOpt, pfn)
91
+ })
92
+ }
93
+
94
+ // ProduceIncrementally starts an incremental producer job.
95
+ func (s *Scheduler) ProduceIncrementally(pcr *IncrementalProducer, pfn IncrementalProduceFunc) *CronJob {
96
+ return s.produce(func(ctx context.Context, version int64) (*Status, error) {
97
+ return pcr.Produce(ctx, version, s.writerOpt, pfn)
98
+ })
99
+ }
100
+
101
+ func (s *Scheduler) produce(fn func(context.Context, int64) (*Status, error)) *CronJob {
102
+ return newCronJob(s.ctx, s.interval, func(ctx context.Context) {
103
+ var version int64
104
+ if s.versionCheck != nil {
105
+ latest, err := s.versionCheck(s.ctx)
106
+ if err != nil {
107
+ return
108
+ }
109
+ version = latest
110
+ }
111
+
112
+ if !s.runBeforeHooks(version) {
113
+ return
114
+ }
115
+
116
+ status, err := fn(ctx, version)
117
+ s.runAfterHooks(status, err)
118
+ })
119
+ }
120
+
121
+ func (s *Scheduler) runBeforeHooks(version int64) bool {
122
+ for _, hook := range s.beforeHooks {
123
+ if !hook(version) {
124
+ return false
125
+ }
126
+ }
127
+ return true
128
+ }
129
+
130
+ func (s *Scheduler) runAfterHooks(status *Status, err error) {
131
+ for _, hook := range s.afterHooks {
132
+ hook(status, err)
133
+ }
134
+ }
135
+
136
+ // CronJob runs in regular intervals until it's stopped.
137
+ type CronJob struct {
138
+ ctx context.Context
139
+ cancel context.CancelFunc
140
+ interval time.Duration
141
+ perform func(context.Context)
142
+ wait sync.WaitGroup
143
+ }
144
+
145
+ func newCronJob(ctx context.Context, interval time.Duration, perform func(context.Context)) *CronJob {
146
+ ctx, cancel := context.WithCancel(ctx)
147
+
148
+ job := &CronJob{ctx: ctx, cancel: cancel, interval: interval, perform: perform}
149
+ job.perform(ctx) // perform immediately
150
+
151
+ job.wait.Add(1)
152
+ go job.loop()
153
+ return job
154
+ }
155
+
156
+ // Stop stops the job and waits until it is complete.
157
+ func (j *CronJob) Stop() {
158
+ j.cancel()
159
+ j.wait.Wait()
160
+ }
161
+
162
+ func (j *CronJob) loop() {
163
+ defer j.wait.Done()
164
+
165
+ ticker := time.NewTicker(j.interval)
166
+ defer ticker.Stop()
167
+
168
+ for {
169
+ select {
170
+ case <-j.ctx.Done():
171
+ return
172
+ case <-ticker.C:
173
+ j.perform(j.ctx)
174
+ }
175
+ }
176
+ }
data/scheduler_test.go ADDED
@@ -0,0 +1,128 @@
1
+ package feedx_test
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "sync/atomic"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/bsm/bfs"
11
+ "github.com/bsm/feedx"
12
+ )
13
+
14
+ func TestScheduler(t *testing.T) {
15
+ beforeCallbacks := new(atomic.Int32)
16
+ afterCallbacks := new(atomic.Int32)
17
+ numCycles := new(atomic.Int32)
18
+ numErrors := new(atomic.Int32)
19
+
20
+ obj := bfs.NewInMemObject("file.json")
21
+ defer obj.Close()
22
+
23
+ t.Run("produce", func(t *testing.T) {
24
+ pcr := feedx.NewProducerForRemote(obj)
25
+ defer pcr.Close()
26
+
27
+ job := feedx.Every(time.Millisecond).
28
+ BeforeSync(func(_ int64) bool {
29
+ beforeCallbacks.Add(1)
30
+ return true
31
+ }).
32
+ AfterSync(func(_ *feedx.Status, err error) {
33
+ afterCallbacks.Add(1)
34
+
35
+ if err != nil {
36
+ numErrors.Add(1)
37
+ }
38
+ }).
39
+ WithVersionCheck(func(_ context.Context) (int64, error) {
40
+ return 101, nil
41
+ }).
42
+ Produce(pcr, func(w *feedx.Writer) error {
43
+ if numCycles.Add(1)%2 == 0 {
44
+ return fmt.Errorf("failed!")
45
+ }
46
+ return nil
47
+ })
48
+
49
+ time.Sleep(5 * time.Millisecond)
50
+ job.Stop()
51
+ time.Sleep(2 * time.Millisecond)
52
+
53
+ ranTimes := numCycles.Load()
54
+ if min, got := 4, int(ranTimes); got <= min {
55
+ t.Errorf("expected %d >= %d", got, min)
56
+ }
57
+ if exp, got := ranTimes, beforeCallbacks.Load(); exp != got {
58
+ t.Errorf("expected %d, got %d", exp, got)
59
+ }
60
+ if exp, got := ranTimes, afterCallbacks.Load(); exp != got {
61
+ t.Errorf("expected %d, got %d", exp, got)
62
+ }
63
+ if exp, got := ranTimes/2, numErrors.Load(); exp != got {
64
+ t.Errorf("expected %d, got %d", exp, got)
65
+ }
66
+
67
+ // wait a little longer, make sure job was stopped
68
+ time.Sleep(2 * time.Millisecond)
69
+ if exp, got := ranTimes, numCycles.Load(); exp != got {
70
+ t.Errorf("expected %d, got %d", exp, got)
71
+ }
72
+ })
73
+
74
+ beforeCallbacks.Store(0)
75
+ afterCallbacks.Store(0)
76
+ numCycles.Store(0)
77
+ numErrors.Store(0)
78
+
79
+ t.Run("consume", func(t *testing.T) {
80
+
81
+ csm := feedx.NewConsumerForRemote(obj)
82
+ defer csm.Close()
83
+
84
+ job := feedx.Every(time.Millisecond).
85
+ BeforeSync(func(_ int64) bool {
86
+ beforeCallbacks.Add(1)
87
+ return true
88
+ }).
89
+ AfterSync(func(_ *feedx.Status, err error) {
90
+ afterCallbacks.Add(1)
91
+
92
+ if err != nil {
93
+ numErrors.Add(1)
94
+ }
95
+ }).
96
+ Consume(csm, func(ctx context.Context, r *feedx.Reader) error {
97
+ if numCycles.Add(1)%2 == 0 {
98
+ return fmt.Errorf("failed!")
99
+ }
100
+ return nil
101
+ })
102
+
103
+ time.Sleep(5 * time.Millisecond)
104
+ job.Stop()
105
+ time.Sleep(2 * time.Millisecond)
106
+
107
+ ranTimes := numCycles.Load()
108
+ if min, got := 4, int(ranTimes); got <= min {
109
+ t.Errorf("expected %d >= %d", got, min)
110
+ }
111
+ if exp, got := ranTimes, beforeCallbacks.Load(); exp != got {
112
+ t.Errorf("expected %d, got %d", exp, got)
113
+ }
114
+ if exp, got := ranTimes, afterCallbacks.Load(); exp != got {
115
+ t.Errorf("expected %d, got %d", exp, got)
116
+ }
117
+ if exp, got := ranTimes/2, numErrors.Load(); exp != got {
118
+ t.Errorf("expected %d, got %d", exp, got)
119
+ }
120
+
121
+ // wait a little longer, make sure job was stopped
122
+ time.Sleep(2 * time.Millisecond)
123
+ if exp, got := ranTimes, numCycles.Load(); exp != got {
124
+ t.Errorf("expected %d, got %d", exp, got)
125
+ }
126
+ })
127
+
128
+ }
data/writer.go CHANGED
@@ -3,8 +3,9 @@ package feedx
3
3
  import (
4
4
  "bufio"
5
5
  "context"
6
+ "errors"
6
7
  "io"
7
- "time"
8
+ "strconv"
8
9
 
9
10
  "github.com/bsm/bfs"
10
11
  )
@@ -19,9 +20,9 @@ type WriterOptions struct {
19
20
  // Default: auto-detected from URL path.
20
21
  Compression Compression
21
22
 
22
- // Provides an optional last modified timestamp which is stored with the remote metadata.
23
- // Default: time.Time{}.
24
- LastMod time.Time
23
+ // Provides an optional version which is stored with the remote metadata.
24
+ // Default: 0
25
+ Version int64
25
26
  }
26
27
 
27
28
  func (o *WriterOptions) norm(name string) {
@@ -38,7 +39,7 @@ type Writer struct {
38
39
  ctx context.Context
39
40
  remote *bfs.Object
40
41
  opt WriterOptions
41
- num int
42
+ num int64
42
43
 
43
44
  bw bfs.Writer
44
45
  cw io.WriteCloser // compression writer
@@ -100,7 +101,7 @@ func (w *Writer) Encode(v interface{}) error {
100
101
  }
101
102
 
102
103
  // NumWritten returns the number of written values.
103
- func (w *Writer) NumWritten() int {
104
+ func (w *Writer) NumWritten() int64 {
104
105
  return w.num
105
106
  }
106
107
 
@@ -109,7 +110,7 @@ func (w *Writer) Discard() error {
109
110
  err := w.close()
110
111
  if w.bw != nil {
111
112
  if e := w.bw.Discard(); e != nil {
112
- err = e
113
+ err = errors.Join(err, e)
113
114
  }
114
115
  }
115
116
  return err
@@ -120,7 +121,7 @@ func (w *Writer) Commit() error {
120
121
  err := w.close()
121
122
  if w.bw != nil {
122
123
  if e := w.bw.Commit(); e != nil {
123
- err = e
124
+ err = errors.Join(err, e)
124
125
  }
125
126
  }
126
127
  return err
@@ -129,17 +130,17 @@ func (w *Writer) Commit() error {
129
130
  func (w *Writer) close() (err error) {
130
131
  if w.fe != nil {
131
132
  if e := w.fe.Close(); e != nil {
132
- err = e
133
+ err = errors.Join(err, e)
133
134
  }
134
135
  }
135
136
  if w.ww != nil {
136
137
  if e := w.ww.Flush(); e != nil {
137
- err = e
138
+ err = errors.Join(err, e)
138
139
  }
139
140
  }
140
141
  if w.cw != nil {
141
142
  if e := w.cw.Close(); e != nil {
142
- err = e
143
+ err = errors.Join(err, e)
143
144
  }
144
145
  }
145
146
  return err
@@ -147,9 +148,8 @@ func (w *Writer) close() (err error) {
147
148
 
148
149
  func (w *Writer) ensureCreated() error {
149
150
  if w.bw == nil {
150
- ts := timestampFromTime(w.opt.LastMod)
151
151
  bw, err := w.remote.Create(w.ctx, &bfs.WriteOptions{
152
- Metadata: bfs.Metadata{metaLastModified: ts.String()},
152
+ Metadata: bfs.Metadata{metaVersion: strconv.FormatInt(w.opt.Version, 10)},
153
153
  })
154
154
  if err != nil {
155
155
  return err
data/writer_test.go CHANGED
@@ -2,66 +2,83 @@ package feedx_test
2
2
 
3
3
  import (
4
4
  "bytes"
5
- "context"
6
- "time"
5
+ "reflect"
6
+ "testing"
7
7
 
8
8
  "github.com/bsm/bfs"
9
9
  "github.com/bsm/feedx"
10
- . "github.com/bsm/ginkgo"
11
- . "github.com/bsm/gomega"
12
10
  )
13
11
 
14
- var _ = Describe("Writer", func() {
15
- var plain, compressed *bfs.Object
16
- var ctx = context.Background()
12
+ func TestWriter(t *testing.T) {
13
+ t.Run("writes plain", func(t *testing.T) {
14
+ obj := bfs.NewInMemObject("path/to/file.json")
15
+ info := testWriter(t, obj, &feedx.WriterOptions{
16
+ Version: 101,
17
+ })
18
+
19
+ if exp, got := int64(10000), info.Size; exp != got {
20
+ t.Errorf("expected %v, got %v", exp, got)
21
+ }
17
22
 
18
- BeforeEach(func() {
19
- plain = bfs.NewInMemObject("path/to/file.json")
20
- compressed = bfs.NewInMemObject("path/to/file.jsonz")
23
+ meta := bfs.Metadata{"X-Feedx-Version": "101"}
24
+ if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
25
+ t.Errorf("expected %#v, got %#v", exp, got)
26
+ }
21
27
  })
22
28
 
23
- It("writes plain", func() {
24
- w := feedx.NewWriter(context.Background(), plain, &feedx.WriterOptions{
25
- LastMod: time.Unix(1515151515, 123456789),
29
+ t.Run("writes compressed", func(t *testing.T) {
30
+ obj := bfs.NewInMemObject("path/to/file.jsonz")
31
+ info := testWriter(t, obj, &feedx.WriterOptions{
32
+ Version: 101,
26
33
  })
27
- defer w.Discard()
28
34
 
29
- Expect(w.Write(bytes.Repeat([]byte{'x'}, 10000))).To(Equal(10000))
30
- Expect(w.Commit()).To(Succeed())
35
+ if max, got := int64(100), info.Size; got > max {
36
+ t.Errorf("expected %v to be < %v", got, max)
37
+ }
31
38
 
32
- info, err := plain.Head(ctx)
33
- Expect(err).NotTo(HaveOccurred())
34
- Expect(info.Size).To(Equal(int64(10000)))
35
- Expect(info.Metadata).To(Equal(bfs.Metadata{"X-Feedx-Last-Modified": "1515151515123"}))
39
+ meta := bfs.Metadata{"X-Feedx-Version": "101"}
40
+ if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
41
+ t.Errorf("expected %#v, got %#v", exp, got)
42
+ }
36
43
  })
37
44
 
38
- It("writes compressed", func() {
39
- w := feedx.NewWriter(context.Background(), compressed, &feedx.WriterOptions{
40
- LastMod: time.Unix(1515151515, 123456789),
41
- })
42
- defer w.Discard()
45
+ t.Run("encodes", func(t *testing.T) {
46
+ obj := bfs.NewInMemObject("path/to/file.json")
47
+ if err := writeN(obj, 10, 101); err != nil {
48
+ t.Fatal("unexpected error", err)
49
+ }
43
50
 
44
- Expect(w.Write(bytes.Repeat([]byte{'x'}, 10000))).To(Equal(10000))
45
- Expect(w.Commit()).To(Succeed())
51
+ info, err := obj.Head(t.Context())
52
+ if err != nil {
53
+ t.Fatal("unexpected error", err)
54
+ }
55
+ if exp, got := int64(370), info.Size; exp != got {
56
+ t.Errorf("expected %v, got %v", exp, got)
57
+ }
46
58
 
47
- info, err := compressed.Head(ctx)
48
- Expect(err).NotTo(HaveOccurred())
49
- Expect(info.Size).To(BeNumerically("~", 50, 20))
50
- Expect(info.Metadata).To(Equal(bfs.Metadata{"X-Feedx-Last-Modified": "1515151515123"}))
59
+ meta := bfs.Metadata{"X-Feedx-Version": "101"}
60
+ if exp, got := meta, info.Metadata; !reflect.DeepEqual(exp, got) {
61
+ t.Errorf("expected %#v, got %#v", exp, got)
62
+ }
51
63
  })
64
+ }
52
65
 
53
- It("encodes", func() {
54
- Expect(writeMulti(plain, 10, time.Time{})).To(Succeed())
55
- Expect(writeMulti(compressed, 10, mockTime)).To(Succeed())
66
+ func testWriter(t *testing.T, obj *bfs.Object, opts *feedx.WriterOptions) *bfs.MetaInfo {
67
+ t.Helper()
56
68
 
57
- info, err := plain.Head(ctx)
58
- Expect(err).NotTo(HaveOccurred())
59
- Expect(info.Size).To(BeNumerically("~", 370, 10))
60
- Expect(info.Metadata).To(Equal(bfs.Metadata{"X-Feedx-Last-Modified": "0"}))
69
+ w := feedx.NewWriter(t.Context(), obj, opts)
70
+ t.Cleanup(func() { _ = w.Discard() })
61
71
 
62
- info, err = compressed.Head(ctx)
63
- Expect(err).NotTo(HaveOccurred())
64
- Expect(info.Size).To(BeNumerically("~", 76, 10))
65
- Expect(info.Metadata).To(Equal(bfs.Metadata{"X-Feedx-Last-Modified": "1515151515123"}))
66
- })
67
- })
72
+ if _, err := w.Write(bytes.Repeat([]byte{'x'}, 10000)); err != nil {
73
+ t.Fatal("unexpected error", err)
74
+ }
75
+ if err := w.Commit(); err != nil {
76
+ t.Fatal("unexpected error", err)
77
+ }
78
+
79
+ info, err := obj.Head(t.Context())
80
+ if err != nil {
81
+ t.Fatal("unexpected error", err)
82
+ }
83
+ return info
84
+ }