minitest-memory 1.1.0 → 1.1.1

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: c37208f0d913977a738af1587640fccfef97dfa89cbcde685653b809e42463f0
4
- data.tar.gz: 4889e9fa26a92c685079c12af6514d205c0b32c56bcdfdbcebae834f44801c9f
3
+ metadata.gz: 1e0dd689600ea1bb57af6388a94015c3bf9ec9dcfc737aa26967a822e005cde0
4
+ data.tar.gz: 1d9059075cd6a38d425cb32adcdac88528dcfa3dcfb4421b21776c94910feee9
5
5
  SHA512:
6
- metadata.gz: 308e793bd885a60f51b1b756a7520fb38e61a5032eff3db8e8e56ed033b6b1ab12477cafd5e98c899f04ee1bc01fa1dc0950b9fc75219bcfd842a808b3db64f0
7
- data.tar.gz: 9b84c7636fb559bcc634b7ed64eb8b58f48dd5ae5283eb991ab9d67f683bd175b8826a7fa604e6c5526f04911bafe4211128867b269b54c4ad3e5e43583e3955
6
+ metadata.gz: cce664e89b49ec5493d47254b07cdae98eb0496f86fb8795c9eb800ed04953e63455619ed63c5ffdb106985b9915c7851a91b41732038afbf692d21340dc2194
7
+ data.tar.gz: 74af65dfaf4c69b13c5e10122c497755d3af225e24440192254a0422379f6e9a21666d048675c183e911a3a6ddc738d1051d7e4d5de5ac971b42745770d4e40b
data/CHANGELOG.md CHANGED
@@ -2,9 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [1.1.1] - 2026-10-20
11
+
12
+ ### Fixed
13
+
14
+ - The gem now includes its RBS type signatures (`sig/minitest-memory.rbs`), which were left out of the package
15
+ - Loading on JRuby, whose `objspace` has no `ObjectSpace.memsize_of` (the gem raised `NoMethodError` as it loaded)
16
+ - Where allocations can't be traced (JRuby, TruffleRuby), assertions now run their block (they used to skip it) and then skip the test, instead of failing limits that expect allocations or passing without checking anything
17
+
8
18
  ## [1.1.0] - 2026-03-08
9
19
 
10
20
  ### Added
@@ -24,3 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
34
 
25
35
  - `Minitest::Memory::AllocationCounter.count` — counts object allocations by class within a block
26
36
  - `assert_allocations` — fails if any class exceeds its allocation limit within a block
37
+
38
+ [Unreleased]: https://github.com/sferik/minitest-memory/compare/v1.1.1...HEAD
39
+ [1.1.1]: https://github.com/sferik/minitest-memory/compare/v1.1.0...v1.1.1
40
+ [1.1.0]: https://github.com/sferik/minitest-memory/compare/v1.0.0...v1.1.0
41
+ [1.0.0]: https://github.com/sferik/minitest-memory/releases/tag/v1.0.0
data/README.md CHANGED
@@ -11,6 +11,16 @@ Add to your Gemfile:
11
11
  gem "minitest-memory"
12
12
  ```
13
13
 
14
+ ## Supported Rubies
15
+
16
+ minitest-memory requires Ruby 3.2 or later. Counting allocations relies on
17
+ CRuby's `ObjectSpace` allocation tracing, so it works only on CRuby.
18
+
19
+ On JRuby and TruffleRuby, allocations can't be counted. The assertions and
20
+ expectations still run their block, then skip the test, so the test run
21
+ shows that nothing was checked instead of passing. Because a skip ends the
22
+ test, any assertions after it in the same test don't run on those Rubies.
23
+
14
24
  ## Usage
15
25
 
16
26
  Include `Minitest::Memory` in your test class, then use `assert_allocations`
@@ -3,6 +3,6 @@ module Minitest # :nodoc:
3
3
  # Version information for minitest-memory.
4
4
  module Memory
5
5
  # Current version of minitest-memory.
6
- VERSION = "1.1.0".freeze
6
+ VERSION = "1.1.1".freeze
7
7
  end
8
8
  end
@@ -1,4 +1,10 @@
1
- require "objspace"
1
+ # :nocov:
2
+ begin
3
+ require "objspace"
4
+ rescue LoadError
5
+ # objspace is not available on JRuby
6
+ end
7
+ # :nocov:
2
8
  require "minitest/strict"
3
9
  require_relative "memory/version"
4
10
 
@@ -25,7 +31,14 @@ module Minitest
25
31
 
26
32
  ##
27
33
  # Base memory size of an empty Ruby object slot.
28
- SLOT_SIZE = ObjectSpace.memsize_of(Object.new)
34
+ # :nocov:
35
+ SLOT_SIZE =
36
+ if ObjectSpace.respond_to?(:memsize_of)
37
+ ObjectSpace.memsize_of(Object.new)
38
+ else
39
+ 0
40
+ end
41
+ # :nocov:
29
42
 
30
43
  empty_sources = {} #: Hash[String, Integer]
31
44
 
@@ -38,9 +51,7 @@ module Minitest
38
51
  # is not supported, +true+ otherwise.
39
52
 
40
53
  def self.supported?
41
- # :nocov:
42
- return false if RUBY_ENGINE == "truffleruby"
43
- # :nocov:
54
+ return false if RUBY_ENGINE == "truffleruby" # simplecov:disable
44
55
 
45
56
  ObjectSpace.respond_to?(:trace_object_allocations)
46
57
  end
@@ -88,9 +99,7 @@ module Minitest
88
99
  # running GC to identify retained objects. Returns a Result.
89
100
 
90
101
  def self.trace(klasses, retain: false, &)
91
- # :nocov:
92
- return Result.new({}, {}, EMPTY) unless supported?
93
- # :nocov:
102
+ return untraced(&) unless supported?
94
103
 
95
104
  GC.start
96
105
  GC.disable
@@ -103,6 +112,16 @@ module Minitest
103
112
  end
104
113
  private_class_method :trace
105
114
 
115
+ ##
116
+ # Runs the block where allocations can't be traced, returning an
117
+ # empty Result.
118
+
119
+ def self.untraced
120
+ yield # steep:ignore InsufficientPositionalArguments
121
+ Result.new({}, {}, EMPTY)
122
+ end
123
+ private_class_method :untraced
124
+
106
125
  ##
107
126
  # Tallies one object's count and byte size into both the
108
127
  # +total+ and per-class +bucket+ entries.
@@ -123,9 +142,7 @@ module Minitest
123
142
 
124
143
  def self.record_source(obj, total, bucket)
125
144
  file = ObjectSpace.allocation_sourcefile(obj)
126
- # :nocov:
127
- return unless file
128
- # :nocov:
145
+ return unless file # simplecov:disable
129
146
 
130
147
  source = "#{file}:#{ObjectSpace.allocation_sourceline(obj)}"
131
148
  total.sources[source] += 1
@@ -177,19 +194,14 @@ module Minitest
177
194
  # assert_allocations(String => 1, count: 10) { "hello" }
178
195
 
179
196
  def assert_allocations(limits, &)
180
- klasses = limits.keys.select { |k| k.is_a?(Module) }
197
+ skip_untraced(&) unless AllocationCounter.supported?
198
+
199
+ klasses = limits.keys.grep(Module)
181
200
  has_total_limit = limits.key?(:count) || limits.key?(:size)
182
201
  result = AllocationCounter.count(klasses, &)
183
202
 
184
203
  check_limits(limits, result)
185
-
186
- return if has_total_limit
187
-
188
- result.ignored.each do |klass, alloc|
189
- msg = "Allocated #{alloc.count} #{klass} instances, #{alloc.size} bytes, " \
190
- "but it was not specified#{format_sources(alloc.sources)}"
191
- flunk msg
192
- end
204
+ check_unspecified(result) unless has_total_limit
193
205
  end
194
206
 
195
207
  ##
@@ -205,7 +217,9 @@ module Minitest
205
217
  # assert_retentions(String => {count: 1, size: 1024}) { "hello" }
206
218
 
207
219
  def assert_retentions(limits, &)
208
- klasses = limits.keys.select { |k| k.is_a?(Module) }
220
+ skip_untraced(&) unless AllocationCounter.supported?
221
+
222
+ klasses = limits.keys.grep(Module)
209
223
  result = AllocationCounter.count_retained(klasses, &)
210
224
 
211
225
  check_limits(limits, result, metric: "retentions", size_metric: "retained bytes")
@@ -218,6 +232,8 @@ module Minitest
218
232
  # refute_allocations(String, Array) { 1 + 1 }
219
233
 
220
234
  def refute_allocations(*classes, &)
235
+ skip_untraced(&) unless AllocationCounter.supported?
236
+
221
237
  check_zero(AllocationCounter.count(classes, &), classes)
222
238
  end
223
239
 
@@ -228,6 +244,8 @@ module Minitest
228
244
  # refute_retentions(String, Array) { 1 + 1 }
229
245
 
230
246
  def refute_retentions(*classes, &)
247
+ skip_untraced(&) unless AllocationCounter.supported?
248
+
231
249
  check_zero(AllocationCounter.count_retained(classes, &), classes, metric: "retentions")
232
250
  end
233
251
 
@@ -238,9 +256,7 @@ module Minitest
238
256
 
239
257
  def self.included(base) # :nodoc:
240
258
  super
241
- # :nocov:
242
- Minitest::Expectation.include(Expectations) if defined?(Minitest::Expectation)
243
- # :nocov:
259
+ Minitest::Expectation.include(Expectations) if defined?(Minitest::Expectation) # simplecov:disable
244
260
  end
245
261
 
246
262
  ##
@@ -287,6 +303,28 @@ module Minitest
287
303
 
288
304
  private
289
305
 
306
+ ##
307
+ # Runs the block where allocations can't be traced (JRuby,
308
+ # TruffleRuby), then skips the test: nothing was checked, so it
309
+ # shouldn't pass.
310
+
311
+ def skip_untraced
312
+ yield # steep:ignore InsufficientPositionalArguments
313
+ skip "Allocations can't be traced on #{RUBY_ENGINE}"
314
+ end
315
+
316
+ ##
317
+ # Fails for allocations of classes +limits+ didn't name (when it set
318
+ # no total limit).
319
+
320
+ def check_unspecified(result)
321
+ result.ignored.each do |klass, alloc|
322
+ msg = "Allocated #{alloc.count} #{klass} instances, #{alloc.size} bytes, " \
323
+ "but it was not specified#{format_sources(alloc.sources)}"
324
+ flunk msg
325
+ end
326
+ end
327
+
290
328
  ##
291
329
  # Checks all +limits+ entries against +result+. Routes +:count+
292
330
  # and +:size+ to total-limit checks, and Module keys to
@@ -0,0 +1,128 @@
1
+ module Minitest
2
+ module Memory
3
+ VERSION: String
4
+
5
+ class AllocationCounter
6
+ class Allocation < ::Struct[Integer]
7
+ attr_accessor count: Integer
8
+ attr_accessor size: Integer
9
+ attr_accessor sources: Hash[String, Integer]
10
+
11
+ def self.new: (Integer count, Integer size, Hash[String, Integer] sources) -> instance
12
+ end
13
+
14
+ class Result < ::Struct[untyped]
15
+ attr_accessor allocated: Hash[untyped, Allocation]
16
+ attr_accessor ignored: Hash[untyped, Allocation]
17
+ attr_accessor total: Allocation
18
+
19
+ def self.new: (Hash[untyped, Allocation] allocated, Hash[untyped, Allocation] ignored, Allocation total) -> instance
20
+ end
21
+
22
+ SLOT_SIZE: Integer
23
+ EMPTY: Allocation
24
+
25
+ # Returns false on TruffleRuby, true otherwise.
26
+ def self.supported?: () -> bool
27
+
28
+ # Counts allocations by class within a block.
29
+ def self.count: (?Array[Module] klasses) { (untyped) -> untyped } -> Result
30
+
31
+ # Counts retained allocations by class within a block.
32
+ def self.count_retained: (?Array[Module] klasses) { (untyped) -> untyped } -> Result
33
+
34
+ # Returns a Result of allocations from a given generation.
35
+ def self.count_allocations: (Integer generation, ?Array[Module] klasses) -> Result
36
+
37
+ private
38
+
39
+ # Traces object allocations within a block.
40
+ def self.trace: (Array[Module] klasses, ?retain: bool) { (untyped) -> untyped } -> Result
41
+
42
+ # Runs the block where allocations can't be traced; returns an empty Result.
43
+ def self.untraced: () { (untyped) -> untyped } -> Result
44
+
45
+ # Tallies one object's count and byte size into total and bucket.
46
+ def self.tally: (untyped obj, Allocation total, Allocation bucket) -> void
47
+
48
+ # Records the source location of obj into total and bucket source hashes.
49
+ def self.record_source: (untyped obj, Allocation total, Allocation bucket) -> void
50
+
51
+ # Finds or creates the Allocation entry for obj.
52
+ def self.bucket_for: (untyped obj, Array[Module] klasses, Hash[untyped, Allocation] allocated, Hash[untyped, Allocation] ignored) -> Allocation
53
+
54
+ # Creates a new zeroed Allocation with a default-value sources hash.
55
+ def self.new_allocation: () -> Allocation
56
+ end
57
+
58
+ include Minitest::Assertions
59
+
60
+ type allocation_count_limit = Integer | Range[Integer]
61
+ type allocation_limit = allocation_count_limit | Hash[Symbol, allocation_count_limit]
62
+
63
+ # Includes Expectations into Minitest::Expectation when minitest/spec is loaded.
64
+ def self.included: (Module base) -> void
65
+
66
+ # Fails if any class in limits exceeds its allocation limit.
67
+ def assert_allocations: (Hash[untyped, allocation_limit] limits) { (untyped) -> untyped } -> void
68
+
69
+ # Fails if any class in limits exceeds its retention limit.
70
+ def assert_retentions: (Hash[untyped, allocation_limit] limits) { (untyped) -> untyped } -> void
71
+
72
+ # Fails if any of the given classes are allocated within a block.
73
+ def refute_allocations: (*Class classes) { (untyped) -> untyped } -> void
74
+
75
+ # Fails if any of the given classes are retained within a block.
76
+ def refute_retentions: (*Class classes) { (untyped) -> untyped } -> void
77
+
78
+ # Minitest::Spec expectations for memory allocation assertions.
79
+ module Expectations
80
+ # The test context from Minitest::Expectation.
81
+ def ctx: () -> untyped
82
+
83
+ # The target value or block from Minitest::Expectation.
84
+ def target: () -> untyped
85
+
86
+ # See Minitest::Memory#assert_allocations.
87
+ def must_limit_allocations: (Hash[untyped, allocation_limit] limits) -> void
88
+
89
+ # See Minitest::Memory#assert_retentions.
90
+ def must_limit_retentions: (Hash[untyped, allocation_limit] limits) -> void
91
+
92
+ # See Minitest::Memory#refute_allocations.
93
+ def wont_allocate: (*Class classes) -> void
94
+
95
+ # See Minitest::Memory#refute_retentions.
96
+ def wont_retain: (*Class classes) -> void
97
+ end
98
+
99
+ private
100
+
101
+ # Runs the block where allocations can't be traced, then skips the test.
102
+ def skip_untraced: () { (untyped) -> untyped } -> bot
103
+
104
+ # Fails for allocations of classes the limits didn't name.
105
+ def check_unspecified: (AllocationCounter::Result result) -> void
106
+
107
+ # Checks all limits entries against result.
108
+ def check_limits: (Hash[untyped, allocation_limit] limits, AllocationCounter::Result result, ?metric: String, ?size_metric: String) -> void
109
+
110
+ # Dispatches a single limit entry for the given klass against the result.
111
+ def check_limit_entry: (untyped klass, allocation_limit limit, AllocationCounter::Result result, metric: String, size_metric: String) -> void
112
+
113
+ # Checks a total :count or :size limit against the aggregate total allocation.
114
+ def check_total_limit: (:count | :size klass, allocation_count_limit limit, AllocationCounter::Allocation total, size_metric: String) -> void
115
+
116
+ # Checks per-class limit against allocation for the given klass.
117
+ def check_class_limit: (untyped klass, AllocationCounter::Allocation allocation, allocation_limit limit, metric: String, size_metric: String) -> void
118
+
119
+ # Asserts that actual falls within limit for the given klass and metric.
120
+ def check_limit: (untyped klass, allocation_count_limit limit, Integer actual, ?metric: String, sources: Hash[String, Integer]) -> void
121
+
122
+ # Asserts zero allocations for each class in classes.
123
+ def check_zero: (AllocationCounter::Result result, Array[Class] classes, ?metric: String) -> void
124
+
125
+ # Formats allocation sources as a newline-separated list of source locations.
126
+ def format_sources: (Hash[String, Integer] sources) -> String
127
+ end
128
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -43,8 +43,9 @@ dependencies:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
45
  version: '1.0'
46
- description: Provides assert_allocations to verify object allocation counts within
47
- a block.
46
+ description: Assertions and expectations that limit the objects a block allocates
47
+ or retains, by count and size, with the source location of each allocation in failure
48
+ messages.
48
49
  email:
49
50
  - sferik@gmail.com
50
51
  executables: []
@@ -56,6 +57,7 @@ files:
56
57
  - README.md
57
58
  - lib/minitest/memory.rb
58
59
  - lib/minitest/memory/version.rb
60
+ - sig/minitest-memory.rbs
59
61
  homepage: https://sferik.github.io/minitest-memory
60
62
  licenses:
61
63
  - MIT
@@ -79,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
81
  - !ruby/object:Gem::Version
80
82
  version: '0'
81
83
  requirements: []
82
- rubygems_version: 4.0.7
84
+ rubygems_version: 4.0.23
83
85
  specification_version: 4
84
86
  summary: Memory allocation assertions for Minitest
85
87
  test_files: []