karafka-testing 2.6.3 → 2.6.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2be14092c2b929e48d6849d330f913b9000006217a48a2ac0d9cbadc1a43fe77
4
- data.tar.gz: c575f2aee39ad9c80ac92c32729572cc882d6951ec5adbb056fc690ca330cd1f
3
+ metadata.gz: '08d462de8e80468e64eef6f71a2c17e47a4a1ca597dd9e290feaf0ccfc5f49d6'
4
+ data.tar.gz: d7971f8a44ed5f442de78467dce1bbd243596c5bc2bd05f2046b7778b3dfa2c7
5
5
  SHA512:
6
- metadata.gz: 384fc90788d47afbfa37d7fddb81a229c89fdfc5157b433f0cf5124bbba0309d9cb099c6fb63d3678eba786334352cad8cd257b09227f37b084baf54d88dd7ad
7
- data.tar.gz: 5a11804abcad1b3987e5d4c77a77c12cc9d39568cf3c833dcc0286300af64e077dd728bfda5b2cba714445623d9ee93b9ab4d22ba2d84ddb8b10c1a84e05f48c
6
+ metadata.gz: b94d77ccaed5dbb9c8d3fe1742e01b622b7a81a5fa9a3666584240217ef8100c12f76d1043cee6e313506d1e082c31becc1a11af7f7bcfea1d7f9edcc6c44dd0
7
+ data.tar.gz: a8682074bb85e6f5c1be92d3207933395311882f9c6fbebf446ea558b53e57b7f12a2ff5c2fc3a6142ce3adf6b6903ef740a8dc304a13ae728f72d2f261c956a
@@ -36,7 +36,7 @@ jobs:
36
36
  run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
37
37
 
38
38
  - name: Set up Ruby
39
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
39
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
40
40
  with:
41
41
  ruby-version: ${{matrix.ruby}}
42
42
  bundler: 'latest'
@@ -66,9 +66,9 @@ jobs:
66
66
  with:
67
67
  fetch-depth: 0
68
68
  - name: Set up Ruby
69
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
69
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
70
70
  with:
71
- ruby-version: '4.0.7'
71
+ ruby-version: '4.0.9'
72
72
  bundler-cache: true
73
73
  - name: Run rubocop
74
74
  run: bundle exec rubocop
@@ -81,9 +81,9 @@ jobs:
81
81
  steps:
82
82
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
83
83
  - name: Set up Ruby
84
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
84
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
85
85
  with:
86
- ruby-version: '4.0.7'
86
+ ruby-version: '4.0.9'
87
87
  bundler-cache: true
88
88
  - run: bundle exec yard-lint lib/
89
89
 
@@ -95,9 +95,9 @@ jobs:
95
95
  with:
96
96
  fetch-depth: 0
97
97
  - name: Set up Ruby
98
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
98
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
99
99
  with:
100
- ruby-version: '4.0.7'
100
+ ruby-version: '4.0.9'
101
101
  bundler: 'latest'
102
102
  - name: Install dependencies
103
103
  run: bundle install --jobs 4 --retry 3
@@ -112,9 +112,9 @@ jobs:
112
112
  with:
113
113
  fetch-depth: 0
114
114
  - name: Set up Ruby
115
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
115
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
116
116
  with:
117
- ruby-version: '4.0.7'
117
+ ruby-version: '4.0.9'
118
118
  bundler: 'latest'
119
119
  - name: Install dependencies
120
120
  run: bundle install --jobs 4 --retry 3
@@ -24,7 +24,7 @@ jobs:
24
24
  fetch-depth: 0
25
25
 
26
26
  - name: Set up Ruby
27
- uses: ruby/setup-ruby@824c0e29fdfc7d689c191c27acc969d2d04b7dd8 # v1.331.0
27
+ uses: ruby/setup-ruby@7f797815d8adceb1c412e7abefdd1ab4abc47a5f # v1.335.0
28
28
  with:
29
29
  bundler-cache: false
30
30
 
@@ -10,7 +10,14 @@ jobs:
10
10
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
11
11
  - name: Check SHA pins
12
12
  run: |
13
- if grep -E -r "uses: .*/.*@(v[0-9]+|main|master)($|[[:space:]]|$)" --include="*.yml" --include="*.yaml" .github/workflows/ | grep -v "#"; then
13
+ # Every remote action must be pinned to a full 40-character commit SHA. Local actions
14
+ # (./) and docker:// images are not checked.
15
+ unpinned=$(grep -E -rn --include="*.yml" --include="*.yaml" \
16
+ "^[[:space:]]*(-[[:space:]]+)?uses:[[:space:]]*['\"]?[^.'\"[:space:]]" .github/workflows/ \
17
+ | grep -Ev "uses:[[:space:]]*['\"]?docker://" \
18
+ | grep -Ev "@[0-9a-f]{40}['\"]?([[:space:]]|$)" || true)
19
+ if [ -n "$unpinned" ]; then
20
+ echo "$unpinned"
14
21
  echo "::error::Actions should use SHA pins, not tags or branch names"
15
22
  exit 1
16
23
  fi
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.7
1
+ 4.0.9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Karafka Testing Changelog
2
2
 
3
+ ## 2.6.4 (2026-11-04)
4
+ - [Feature] Record consumer pauses and resumes. Assert them with `karafka.pauses` and `karafka.resumes` (#214).
5
+ - [Maintenance] Declare `rake` in the `Gemfile`. It came only through `karafka-rdkafka`, which will stop depending on it for precompiled platform gems.
6
+ - [Fix] Give each consumer in one test a batch of only its own messages. Several consumers no longer share one batch, and offsets count per consumer.
7
+ - [Fix] Stub `pause` and `resume` on the spec consumer client. Consumers that call `#pause` no longer fail with `NoMethodError`.
8
+ - [Fix] Make `Minitest::Spec.include(Karafka::Testing::Minitest::Helpers)` work. Specs no longer fail with `ArgumentError: wrong number of arguments`.
9
+ - [Fix] Make producing in a test safe from many threads. On JRuby, messages produced from 2+ threads at once were lost or got duplicate offsets.
10
+
3
11
  ## 2.6.3 (2026-10-25)
4
12
  - **[EOL]** Drop Ruby 3.2 support; require Ruby 3.3+.
5
13
  - [Maintenance] Align internal naming and YARD docs with the Karafka ecosystem conventions.
@@ -26,8 +34,8 @@
26
34
  - [Fix] Fix consumer mapping collision when multiple consumer groups listen to the same topic.
27
35
 
28
36
  ## 2.5.4 (2025-10-03)
29
- - [Fix] Topic present in second dynamically built subscription group not available for dispatch (#291).
30
37
  - **[EOL]** Remove Ruby `3.1` according to the EOL schedule.
38
+ - [Fix] Topic present in second dynamically built subscription group not available for dispatch (#291).
31
39
 
32
40
  ## 2.5.3 (2025-08-12)
33
41
  - [Fix] Allow testing consumers with Pro expansions (i.e. Piping).
data/Gemfile CHANGED
@@ -4,6 +4,8 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ gem "rake"
8
+
7
9
  group :test do
8
10
  gem "minitest"
9
11
  gem "mocha"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- karafka-testing (2.6.3)
4
+ karafka-testing (2.6.4)
5
5
  karafka (>= 2.6.0.rc2, < 2.7.0)
6
6
  waterdrop (>= 2.10.0)
7
7
 
@@ -12,22 +12,22 @@ GEM
12
12
  drb (2.2.3)
13
13
  ffi (1.17.4)
14
14
  ffi (1.17.4-x86_64-linux-gnu)
15
- json (3.0.2)
15
+ json (3.1.0)
16
16
  karafka (2.6.1)
17
17
  karafka-core (>= 2.6.2, < 2.7.0)
18
18
  karafka-rdkafka (>= 0.28.0)
19
19
  waterdrop (>= 2.10.2, < 3.0.0)
20
20
  zeitwerk (~> 2.3)
21
- karafka-core (2.6.4)
21
+ karafka-core (2.6.6)
22
22
  karafka-rdkafka (>= 0.20.0)
23
23
  logger (>= 1.6.0)
24
- karafka-rdkafka (0.30.1)
24
+ karafka-rdkafka (0.31.0)
25
25
  ffi (~> 1.17.1)
26
26
  json (> 2.0)
27
27
  logger
28
28
  mini_portile2 (~> 2.6)
29
29
  rake (> 12)
30
- karafka-rdkafka (0.30.1-x86_64-linux-gnu)
30
+ karafka-rdkafka (0.31.0-x86_64-linux-gnu)
31
31
  ffi (~> 1.17.1)
32
32
  json (> 2.0)
33
33
  logger
@@ -58,9 +58,9 @@ GEM
58
58
  ruby2_keywords (0.0.5)
59
59
  simplecov (1.3.2)
60
60
  warning (1.6.0)
61
- waterdrop (2.10.5)
61
+ waterdrop (2.10.9)
62
62
  karafka-core (>= 2.5.12, < 3.0.0)
63
- karafka-rdkafka (>= 0.24.0)
63
+ karafka-rdkafka (>= 0.31.0)
64
64
  zeitwerk (~> 2.3)
65
65
  zeitwerk (2.8.3)
66
66
 
@@ -72,6 +72,7 @@ DEPENDENCIES
72
72
  karafka-testing!
73
73
  minitest
74
74
  mocha
75
+ rake
75
76
  rspec
76
77
  simplecov
77
78
  warning
@@ -4,6 +4,7 @@ require "karafka/testing"
4
4
  require "karafka/testing/errors"
5
5
  require "karafka/testing/helpers"
6
6
  require "karafka/testing/spec_consumer_client"
7
+ require "karafka/testing/spec_consumer_resume"
7
8
  require "karafka/testing/spec_producer_client"
8
9
  require "karafka/testing/minitest/proxy"
9
10
 
@@ -39,18 +40,13 @@ module Karafka
39
40
 
40
41
  @_karafka_consumer_mappings = {}
41
42
 
42
- # Mocha is a dev dependency of many apps without being used in their Karafka tests,
43
- # so we stub with it only when its patches are actually available
44
- if Object.const_defined?("Mocha", false) && Karafka.producer.respond_to?(:stubs)
45
- Karafka.producer.stubs(:client).returns(@_karafka_producer_client)
46
- else
47
- producer_client = @_karafka_producer_client
48
- Karafka.producer.define_singleton_method(:client) { producer_client }
49
- @_karafka_producer_client_stubbed = true
50
- end
43
+ # Not a Mocha stub even when Mocha is loaded: Mocha records every call in an
44
+ # unsynchronized array, which breaks when code under test produces from many threads
45
+ producer_client = @_karafka_producer_client
46
+ Karafka.producer.define_singleton_method(:client) { producer_client }
47
+ @_karafka_producer_client_stubbed = true
51
48
  end
52
49
 
53
- # Mocha removes its own stubs, so we only remove the one we defined
54
50
  teardown_flow = lambda do
55
51
  next unless @_karafka_producer_client_stubbed
56
52
 
@@ -59,8 +55,9 @@ module Karafka
59
55
  end
60
56
 
61
57
  if base.to_s == "Minitest::Spec"
58
+ # `before` passes the test to its block, which the lambda does not take
62
59
  base.class_eval do
63
- before(&eval_flow)
60
+ before { instance_exec(&eval_flow) }
64
61
  end
65
62
  elsif base.respond_to?(:setup) && base.method(:setup).arity != 0
66
63
  base.class_eval do
@@ -149,8 +146,10 @@ module Karafka
149
146
  return if message[:consumer_group] &&
150
147
  message[:consumer_group].to_s != consumer_obj.topic.consumer_group.name
151
148
 
149
+ batch = _karafka_consumer_batch(consumer_obj)
150
+
152
151
  # Build message metadata and copy any metadata that would come from the message
153
- metadata = _karafka_message_metadata_defaults(consumer_obj)
152
+ metadata = _karafka_message_metadata_defaults(consumer_obj, batch)
154
153
 
155
154
  metadata.keys.each do |key|
156
155
  message_key = METADATA_DISPATCH_MAPPINGS.fetch(key, key)
@@ -171,6 +170,7 @@ module Karafka
171
170
 
172
171
  # Add this message to previously produced messages
173
172
  @_karafka_consumer_messages << karafka_message
173
+ batch << karafka_message unless batch.equal?(@_karafka_consumer_messages)
174
174
  # A real batch never mixes partitions, so the first message of this consumer decides
175
175
  # the partition
176
176
  partition = (@_karafka_batch_partitions ||= {})[consumer_obj] ||= karafka_message.partition
@@ -184,7 +184,7 @@ module Karafka
184
184
 
185
185
  # Update batch metadata
186
186
  batch_metadata = Karafka::Messages::Builders::BatchMetadata.call(
187
- @_karafka_consumer_messages,
187
+ batch,
188
188
  consumer_obj.topic,
189
189
  partition,
190
190
  Time.now
@@ -192,7 +192,7 @@ module Karafka
192
192
 
193
193
  # Update consumer messages batch
194
194
  consumer_obj.messages = Karafka::Messages::Messages.new(
195
- @_karafka_consumer_messages,
195
+ batch,
196
196
  batch_metadata
197
197
  )
198
198
  end
@@ -204,8 +204,9 @@ module Karafka
204
204
  # offsets auto-increment from 0).
205
205
  def _karafka_produce(payload, metadata = {})
206
206
  # Extract offset before passing to WaterDrop since it is not a valid
207
- # WaterDrop message attribute (Kafka assigns offsets, not producers)
208
- @_karafka_next_offset = metadata.delete(:offset)
207
+ # WaterDrop message attribute (Kafka assigns offsets, not producers). Kept per thread, as
208
+ # code under test may produce from many threads at once
209
+ Thread.current[:_karafka_next_offset] = metadata.delete(:offset)
209
210
 
210
211
  topic_name = if metadata[:topic]
211
212
  metadata[:topic]
@@ -222,7 +223,7 @@ module Karafka
222
223
  }.merge(metadata)
223
224
  )
224
225
  ensure
225
- @_karafka_next_offset = nil
226
+ Thread.current[:_karafka_next_offset] = nil
226
227
  end
227
228
 
228
229
  # @return [Array<Hash>] messages that were produced
@@ -236,6 +237,16 @@ module Karafka
236
237
  @_karafka_consumer_messages
237
238
  end
238
239
 
240
+ # @return [Array<Hash>] pauses requested by the consumers
241
+ def _karafka_pauses
242
+ @_karafka_consumer_client.pauses
243
+ end
244
+
245
+ # @return [Array<Hash>] resumes of paused consumers
246
+ def _karafka_resumes
247
+ @_karafka_consumer_client.resumes
248
+ end
249
+
239
250
  # Produces message to a specific consumer instance
240
251
  # Use when testing multiple consumers for the same topic
241
252
  #
@@ -260,6 +271,38 @@ module Karafka
260
271
 
261
272
  private
262
273
 
274
+ # Messages of the batch of a given consumer. The first consumer that gets a message uses
275
+ # the shared buffer, so altering `consumer_messages` alters its batch. Once a second
276
+ # consumer gets one, each consumer gets its own batch, as a real batch holds one topic.
277
+ #
278
+ # @param consumer_obj [Karafka::BaseConsumer] consumer reference
279
+ # @return [Array<Karafka::Messages::Message>] messages of this consumer's batch
280
+ def _karafka_consumer_batch(consumer_obj)
281
+ batches = (@_karafka_consumer_batches ||= {})
282
+
283
+ batches.fetch(consumer_obj) do
284
+ if batches.empty?
285
+ batches[consumer_obj] = @_karafka_consumer_messages
286
+ else
287
+ first_consumer, first_batch = batches.first
288
+
289
+ if first_batch.equal?(@_karafka_consumer_messages)
290
+ own_batch = first_batch.dup
291
+ batches[first_consumer] = own_batch
292
+
293
+ if first_consumer.messages
294
+ first_consumer.messages = Karafka::Messages::Messages.new(
295
+ own_batch,
296
+ first_consumer.messages.metadata
297
+ )
298
+ end
299
+ end
300
+
301
+ batches[consumer_obj] = []
302
+ end
303
+ end
304
+ end
305
+
263
306
  # Finds a consumer for the given message with backward-compatible fallback
264
307
  # @param message [Hash] the message being routed
265
308
  # @return [Object, nil] the consumer instance or nil
@@ -283,14 +326,15 @@ module Karafka
283
326
  end
284
327
 
285
328
  # @param consumer_obj [Karafka::BaseConsumer] consumer reference
329
+ # @param batch [Array<Karafka::Messages::Message>] messages already given to this consumer
286
330
  # @return [Hash] message default options
287
- def _karafka_message_metadata_defaults(consumer_obj)
331
+ def _karafka_message_metadata_defaults(consumer_obj, batch)
288
332
  {
289
333
  deserializers: consumer_obj.topic.deserializers,
290
334
  timestamp: Time.now,
291
335
  raw_headers: {},
292
336
  raw_key: nil,
293
- offset: @_karafka_next_offset.nil? ? @_karafka_consumer_messages.size : @_karafka_next_offset,
337
+ offset: Thread.current[:_karafka_next_offset] || batch.size,
294
338
  partition: 0,
295
339
  received_at: Time.now,
296
340
  topic: consumer_obj.topic.name
@@ -314,6 +358,7 @@ module Karafka
314
358
  @consumer.instance_variable_set(:@used, true)
315
359
  expansions = processing_cfg.consumer_groups.expansions_selector.find(topic)
316
360
  expansions.each { |expansion| @consumer.singleton_class.include(expansion) }
361
+ @consumer.singleton_class.prepend(Testing::SpecConsumerResume)
317
362
  @_karafka_consumer_mappings[topic.id] = @consumer
318
363
  @consumer
319
364
  end
@@ -39,6 +39,17 @@ module Karafka
39
39
  def consumer_messages
40
40
  @minitest_example._karafka_consumer_messages
41
41
  end
42
+
43
+ # @return [Array<Hash>] pauses requested by the consumers, each with the `topic`,
44
+ # `partition`, `offset` and `timeout`
45
+ def pauses
46
+ @minitest_example._karafka_pauses
47
+ end
48
+
49
+ # @return [Array<Hash>] resumes of paused consumers, each with the `topic` and `partition`
50
+ def resumes
51
+ @minitest_example._karafka_resumes
52
+ end
42
53
  end
43
54
  end
44
55
  end
@@ -5,6 +5,7 @@ require "karafka/testing"
5
5
  require "karafka/testing/errors"
6
6
  require "karafka/testing/helpers"
7
7
  require "karafka/testing/spec_consumer_client"
8
+ require "karafka/testing/spec_consumer_resume"
8
9
  require "karafka/testing/spec_producer_client"
9
10
  require "karafka/testing/rspec/proxy"
10
11
 
@@ -55,12 +56,23 @@ module Karafka
55
56
  # that patches are available because some users have Mocha as part of their
56
57
  # supply chain, but do not use it when running Karafka specs. In such cases, without
57
58
  # such check `karafka-testing` would falsely assume, that Mocha is in use.
59
+ # With Mocha we define the method instead of stubbing it: Mocha records every call in
60
+ # an unsynchronized array, which breaks when code under test produces from many threads
58
61
  if Object.const_defined?("Mocha", false) && Karafka.producer.respond_to?(:stubs)
59
- Karafka.producer.stubs(:client).returns(_karafka_producer_client)
62
+ producer_client = _karafka_producer_client
63
+ Karafka.producer.define_singleton_method(:client) { producer_client }
64
+ @_karafka_producer_client_stubbed = true
60
65
  else
61
66
  allow(Karafka.producer).to receive(:client).and_return(_karafka_producer_client)
62
67
  end
63
68
  end
69
+
70
+ base.after do
71
+ next unless @_karafka_producer_client_stubbed
72
+
73
+ Karafka.producer.singleton_class.remove_method(:client)
74
+ @_karafka_producer_client_stubbed = false
75
+ end
64
76
  end
65
77
  end
66
78
 
@@ -128,8 +140,10 @@ module Karafka
128
140
  return if message[:consumer_group] &&
129
141
  message[:consumer_group].to_s != consumer_obj.topic.consumer_group.name
130
142
 
143
+ batch = _karafka_consumer_batch(consumer_obj)
144
+
131
145
  # Build message metadata and copy any metadata that would come from the message
132
- metadata = _karafka_message_metadata_defaults(consumer_obj)
146
+ metadata = _karafka_message_metadata_defaults(consumer_obj, batch)
133
147
 
134
148
  metadata.keys.each do |key|
135
149
  message_key = METADATA_DISPATCH_MAPPINGS.fetch(key, key)
@@ -150,6 +164,7 @@ module Karafka
150
164
 
151
165
  # Add this message to previously produced messages
152
166
  _karafka_consumer_messages << karafka_message
167
+ batch << karafka_message unless batch.equal?(_karafka_consumer_messages)
153
168
 
154
169
  # A real batch never mixes partitions, so the first message of this consumer decides
155
170
  # the partition
@@ -164,7 +179,7 @@ module Karafka
164
179
 
165
180
  # Update batch metadata
166
181
  batch_metadata = Karafka::Messages::Builders::BatchMetadata.call(
167
- _karafka_consumer_messages,
182
+ batch,
168
183
  consumer_obj.topic,
169
184
  partition,
170
185
  Time.now
@@ -172,7 +187,7 @@ module Karafka
172
187
 
173
188
  # Update consumer messages batch
174
189
  consumer_obj.messages = Karafka::Messages::Messages.new(
175
- _karafka_consumer_messages,
190
+ batch,
176
191
  batch_metadata
177
192
  )
178
193
  end
@@ -184,8 +199,9 @@ module Karafka
184
199
  # offsets auto-increment from 0).
185
200
  def _karafka_produce(payload, metadata = {})
186
201
  # Extract offset before passing to WaterDrop since it is not a valid
187
- # WaterDrop message attribute (Kafka assigns offsets, not producers)
188
- @_karafka_next_offset = metadata.delete(:offset)
202
+ # WaterDrop message attribute (Kafka assigns offsets, not producers). Kept per thread, as
203
+ # code under test may produce from many threads at once
204
+ Thread.current[:_karafka_next_offset] = metadata.delete(:offset)
189
205
 
190
206
  topic_name = if metadata[:topic]
191
207
  metadata[:topic]
@@ -204,7 +220,7 @@ module Karafka
204
220
  }.merge(metadata)
205
221
  )
206
222
  ensure
207
- @_karafka_next_offset = nil
223
+ Thread.current[:_karafka_next_offset] = nil
208
224
  end
209
225
 
210
226
  # @return [Array<Hash>] messages that were produced
@@ -212,6 +228,16 @@ module Karafka
212
228
  _karafka_producer_client.messages
213
229
  end
214
230
 
231
+ # @return [Array<Hash>] pauses requested by the consumers
232
+ def _karafka_pauses
233
+ _karafka_consumer_client.pauses
234
+ end
235
+
236
+ # @return [Array<Hash>] resumes of paused consumers
237
+ def _karafka_resumes
238
+ _karafka_consumer_client.resumes
239
+ end
240
+
215
241
  # Produces message to a specific consumer instance
216
242
  # Use when testing multiple consumers for the same topic
217
243
  #
@@ -239,6 +265,38 @@ module Karafka
239
265
 
240
266
  private
241
267
 
268
+ # Messages of the batch of a given consumer. The first consumer that gets a message uses
269
+ # the shared buffer, so altering `consumer_messages` alters its batch. Once a second
270
+ # consumer gets one, each consumer gets its own batch, as a real batch holds one topic.
271
+ #
272
+ # @param consumer_obj [Karafka::BaseConsumer] consumer reference
273
+ # @return [Array<Karafka::Messages::Message>] messages of this consumer's batch
274
+ def _karafka_consumer_batch(consumer_obj)
275
+ batches = (@_karafka_consumer_batches ||= {})
276
+
277
+ batches.fetch(consumer_obj) do
278
+ if batches.empty?
279
+ batches[consumer_obj] = _karafka_consumer_messages
280
+ else
281
+ first_consumer, first_batch = batches.first
282
+
283
+ if first_batch.equal?(_karafka_consumer_messages)
284
+ own_batch = first_batch.dup
285
+ batches[first_consumer] = own_batch
286
+
287
+ if first_consumer.messages
288
+ first_consumer.messages = Karafka::Messages::Messages.new(
289
+ own_batch,
290
+ first_consumer.messages.metadata
291
+ )
292
+ end
293
+ end
294
+
295
+ batches[consumer_obj] = []
296
+ end
297
+ end
298
+ end
299
+
242
300
  # Checks if the RSpec described_class is a Karafka consumer class.
243
301
  # When true, the implicit RSpec subject can be used as a consumer fallback.
244
302
  #
@@ -288,14 +346,15 @@ module Karafka
288
346
  end
289
347
 
290
348
  # @param consumer_obj [Karafka::BaseConsumer] consumer reference
349
+ # @param batch [Array<Karafka::Messages::Message>] messages already given to this consumer
291
350
  # @return [Hash] message default options
292
- def _karafka_message_metadata_defaults(consumer_obj)
351
+ def _karafka_message_metadata_defaults(consumer_obj, batch)
293
352
  {
294
353
  deserializers: consumer_obj.topic.deserializers,
295
354
  timestamp: Time.now,
296
355
  raw_headers: {},
297
356
  raw_key: nil,
298
- offset: @_karafka_next_offset.nil? ? _karafka_consumer_messages.size : @_karafka_next_offset,
357
+ offset: Thread.current[:_karafka_next_offset] || batch.size,
299
358
  partition: 0,
300
359
  received_at: Time.now,
301
360
  topic: consumer_obj.topic.name
@@ -319,6 +378,7 @@ module Karafka
319
378
  consumer.instance_variable_set(:@used, true)
320
379
  expansions = processing_cfg.consumer_groups.expansions_selector.find(topic)
321
380
  expansions.each { |expansion| consumer.singleton_class.include(expansion) }
381
+ consumer.singleton_class.prepend(Testing::SpecConsumerResume)
322
382
 
323
383
  @_karafka_consumer_mappings[topic.id] = consumer
324
384
  consumer
@@ -39,6 +39,17 @@ module Karafka
39
39
  def consumer_messages
40
40
  @rspec_example._karafka_consumer_messages
41
41
  end
42
+
43
+ # @return [Array<Hash>] pauses requested by the consumers, each with the `topic`,
44
+ # `partition`, `offset` and `timeout`
45
+ def pauses
46
+ @rspec_example._karafka_pauses
47
+ end
48
+
49
+ # @return [Array<Hash>] resumes of paused consumers, each with the `topic` and `partition`
50
+ def resumes
51
+ @rspec_example._karafka_resumes
52
+ end
42
53
  end
43
54
  end
44
55
  end
@@ -6,6 +6,20 @@ module Karafka
6
6
  # For specs we do not mark anything as consumed, nor do we really send heartbeats.
7
7
  # Those things are tested in the framework itself
8
8
  class SpecConsumerClient
9
+ # @return [Array<Hash>] pauses requested on this client, each with the `topic`,
10
+ # `partition`, `offset` and `timeout`
11
+ attr_reader :pauses
12
+
13
+ # @return [Array<Hash>] resumes requested on this client, each with the `topic` and
14
+ # `partition`
15
+ attr_reader :resumes
16
+
17
+ # Builds a client with no pauses or resumes recorded
18
+ def initialize
19
+ @pauses = []
20
+ @resumes = []
21
+ end
22
+
9
23
  %i[
10
24
  mark_as_consumed
11
25
  mark_as_consumed!
@@ -17,6 +31,28 @@ module Karafka
17
31
  define_method(caught_delegator) { |*| true }
18
32
  end
19
33
 
34
+ # Records the pause instead of pausing the partition
35
+ #
36
+ # @param topic [String] topic name
37
+ # @param partition [Integer] partition
38
+ # @param offset [Integer, nil] offset to continue from or nil to keep the current position
39
+ # @param timeout [Integer] pause time in milliseconds
40
+ # @return [true]
41
+ def pause(topic, partition, offset = nil, timeout = 0)
42
+ @pauses << { topic: topic, partition: partition, offset: offset, timeout: timeout }
43
+ true
44
+ end
45
+
46
+ # Records the resume instead of resuming the partition
47
+ #
48
+ # @param topic [String] topic name
49
+ # @param partition [Integer] partition
50
+ # @return [true]
51
+ def resume(topic, partition)
52
+ @resumes << { topic: topic, partition: partition }
53
+ true
54
+ end
55
+
20
56
  # @return [Boolean] assignments are never lost for specs
21
57
  def assignment_lost?
22
58
  false
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Karafka
4
+ module Testing
5
+ # Resumes the partition on the client when a paused consumer resumes. In a real run the
6
+ # listener does it before the next poll, which never happens in specs.
7
+ module SpecConsumerResume
8
+ # Expires the pause and, if it was paused, resumes the partition as the listener does
9
+ def resume
10
+ pause_tracker = coordinator.pause_tracker
11
+ paused = pause_tracker.paused?
12
+
13
+ super
14
+
15
+ return unless paused
16
+
17
+ pause_tracker.resume
18
+ client.resume(topic.name, partition)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,21 +3,47 @@
3
3
  module Karafka
4
4
  module Testing
5
5
  # Spec producer client used to buffer messages that we send out in specs
6
+ #
7
+ # @note Code under test may produce from many threads at once, so every operation that touches
8
+ # the buffers runs under one lock
6
9
  class SpecProducerClient < ::WaterDrop::Clients::Buffered
7
10
  # @param example [RSpec::Core::ExampleGroup, Minitest::Test] test example we need to hold to
8
11
  # trigger actions on it that are test context aware
9
12
  def initialize(example)
10
13
  super(nil)
11
14
  @example = example
15
+ @mutex = Mutex.new
12
16
  end
13
17
 
14
18
  # "Produces" message to Kafka. That is, it acknowledges it locally, adds it to the internal
15
19
  # buffer and adds it (if needed) into the current consumer messages buffer
16
20
  # @param message [Hash] `Karafka.producer.produce_sync` message hash
17
21
  def produce(message)
18
- @example._karafka_add_message_to_consumer_if_needed(message)
22
+ @mutex.synchronize do
23
+ @example._karafka_add_message_to_consumer_if_needed(message)
19
24
 
20
- super
25
+ super
26
+ end
27
+ end
28
+
29
+ # Starts the transaction on a given level
30
+ def begin_transaction
31
+ @mutex.synchronize { super }
32
+ end
33
+
34
+ # Finishes given level of transaction
35
+ def commit_transaction
36
+ @mutex.synchronize { super }
37
+ end
38
+
39
+ # Aborts the transaction
40
+ def abort_transaction
41
+ @mutex.synchronize { super }
42
+ end
43
+
44
+ # Clears internal buffer. Used in between specs so messages do not leak out
45
+ def reset
46
+ @mutex.synchronize { super }
21
47
  end
22
48
  end
23
49
  end
@@ -3,6 +3,6 @@
3
3
  module Karafka
4
4
  module Testing
5
5
  # Current version of gem. It should match Karafka framework version
6
- VERSION = "2.6.3"
6
+ VERSION = "2.6.4"
7
7
  end
8
8
  end
data/package-lock.json CHANGED
@@ -286,9 +286,9 @@
286
286
  }
287
287
  },
288
288
  "node_modules/smol-toml": {
289
- "version": "1.9.0",
290
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz",
291
- "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==",
289
+ "version": "1.9.1",
290
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
291
+ "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
292
292
  "dev": true,
293
293
  "license": "BSD-3-Clause",
294
294
  "engines": {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: karafka-testing
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.3
4
+ version: 2.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -82,6 +82,7 @@ files:
82
82
  - lib/karafka/testing/rspec/helpers.rb
83
83
  - lib/karafka/testing/rspec/proxy.rb
84
84
  - lib/karafka/testing/spec_consumer_client.rb
85
+ - lib/karafka/testing/spec_consumer_resume.rb
85
86
  - lib/karafka/testing/spec_producer_client.rb
86
87
  - lib/karafka/testing/version.rb
87
88
  - package-lock.json