consistent_random 2.1.0 → 2.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53c979ca8b32310fc3357e231ed29a029964396e19f1bafd0ac44220be535a1a
4
- data.tar.gz: 35e7431d888b473db4063a8cec225314aae0e445740388a97ee9296d5a559c03
3
+ metadata.gz: 87b396dd6b089d0fb4c99777531eb0824a4d78b2180bce9eeee6484efd06ddbd
4
+ data.tar.gz: f5ade642f1222468d68ca326cfad4a206b9e74b97fe786e2497d13e9a67a0459
5
5
  SHA512:
6
- metadata.gz: cbc32f5cbbf0ad48742e9ede2e14a187530a32b2e832a0b9af5b293c9716f3e9893795601853ccc4b32ad9cbb24b70decb88f9b94f2c17b7ba687bfca2cd1810
7
- data.tar.gz: 80ebaa9bfd1ba8519e755369cf76184e89c97c88302979a7b4fe0d176fbb107a3734a9fa2aa820ad80d0f5f3b370f7755b721080be7ec79219df48a0e6c5d3a9
6
+ metadata.gz: 8ab235ba4d2b6b447e05614cd603c739461e1d3e0efad6c088ff07b966b6daf207f76c413a49dc00d78125ee12f1ca252b91cba01154d00441bc03dd00523628
7
+ data.tar.gz: 4a5ca92427978a6f496bc75b057c6982bf7a9eac870eef9139ab5e1cce36aab236c2da6acaf50b551d483d696ce6235d063ba4883a37110569b8f4395571e5e7
data/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.2.0
8
+
9
+ ### Added
10
+
11
+ - Added `ConsistentRandom.testing` method to allow for deterministic testing of random values.
12
+ - Added `ConsistentRandom#name` method to return the name used for seeding the random value.
13
+
14
+ ## 2.1.1
15
+
16
+ ### Fixed
17
+
18
+ - Fixed typo in `==` method which caused a NoMethodError when comparing ConsistentRandom instances.
19
+
7
20
  ## 2.1.0
8
21
 
9
22
  ### Added
data/README.md CHANGED
@@ -18,6 +18,7 @@ For example, consider rolling out a new feature to a subset of requests. You may
18
18
  - [Rack Middleware](#rack-middleware)
19
19
  - [Sidekiq Middleware](#sidekiq-middleware)
20
20
  - [ActiveJob](#activejob)
21
+ - [Testing](#testing)
21
22
  - [Installation](#installation)
22
23
  - [Contributing](#contributing)
23
24
  - [License](#license)
@@ -206,6 +207,36 @@ class MyJob < ApplicationJob
206
207
  end
207
208
  ```
208
209
 
210
+ ## Testing
211
+
212
+ The gem provides a `ConsistentRandom.testing` method to allow for deterministic testing of random values. This method can be used to set fixed values within the block so that your tests will produce consistent results.
213
+
214
+ ```ruby
215
+ # Specify that all random values should be 0.5
216
+ ConsistentRandom.testing.rand(0.5) do
217
+ expect(ConsistentRandom.new("foo").rand).to eq(0.5)
218
+ expect(ConsistentRandom.new("bar").rand).to eq(0.5)
219
+
220
+ # The rand value must be between 0 and 1, but it will be scaled to fit
221
+ # any size or range specified for `rand`.
222
+ expect(ConsistentRandom.new("foo").rand(10)).to eq(5)
223
+ end
224
+
225
+ # You can also specify values for specific names.
226
+ # If a values isn't specified, it will return a random value.
227
+ ConsistentRandom.testing(foo: 0.5, bar: 0.8) do
228
+ expect(ConsistentRandom.new("foo").rand).to eq(0.5)
229
+ expect(ConsistentRandom.new("bar").rand).to eq(0.8)
230
+ end
231
+
232
+ # You can also specify values for the `bytes` and `seed` methods. The methods
233
+ # for setting test valus can be chained together.
234
+ ConsistentRandom.testing.bytes(foo: "bar").seed(baz: 123) do
235
+ expect(ConsistentRandom.new("foo").bytes(6)).to eq("barbar")
236
+ expect(ConsistentRandom.new("baz").seed).to eq(123)
237
+ end
238
+ ```
239
+
209
240
  ## Installation
210
241
 
211
242
  Add this line to your application's Gemfile:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.0
1
+ 2.2.0
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ConsistentRandom
4
+ # This class returns an object that can be used to generate deterministic values
5
+ # for use in testing.
6
+ #
7
+ # @example
8
+ # ConsistentRandom.testing.rand(foo: 0.5).bytes(foo: "foobar").seed(123) do
9
+ # expect(ConsistentRandom.new("foo").rand).to eq(0.5)
10
+ # expect(ConsistentRandom.new("bar").rand).not_to eq(0.5)
11
+ #
12
+ # expect(ConsistentRandom.new("foo").bytes(12)).to eq("foobarfoobar")
13
+ #
14
+ # expect(ConsistentRandom.new("foo").seed).to eq(123)
15
+ # end
16
+ class ConsistentRandom::Testing
17
+ class << self
18
+ # Get the testing object if any for the current block.
19
+ #
20
+ # @return [ConsistentRandom::Testing, nil] the testing object or nil if not set
21
+ # @api private
22
+ def current
23
+ Thread.current[:consistent_random_testing]
24
+ end
25
+ end
26
+
27
+ def initialize
28
+ @rand_hash = {}
29
+ @bytes_hash = {}
30
+ @seed_hash = {}
31
+ end
32
+
33
+ # Set the random value returned by ConsistentRandom#rand.
34
+ #
35
+ # param options [Float, Hash<String, Float>] the value to return for rand. If a Float is given
36
+ # then it will be used as the value for all calls to rand. If a Hash is given then the value
37
+ # will only be returned for ConsitentRandom objects with the names specified in the keys.
38
+ # @yield block of code to execute with the test values.
39
+ # @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
40
+ # of the block is returned. Otherwise the testing object is returned so that you can
41
+ # chain calls to set up more test values.
42
+ def rand(options, &block)
43
+ options = validate_rand(options)
44
+ unless options
45
+ raise ArgumentError.new("Argument must be a Float between 0 and 1 or a Hash with Float values")
46
+ end
47
+
48
+ @rand_hash = options.default ? options.merge(@rand_hash) : @rand_hash.merge(options)
49
+ if block
50
+ use(&block)
51
+ else
52
+ self
53
+ end
54
+ end
55
+
56
+ # Set the random bytes returned by ConsistentRandom#bytes.
57
+ #
58
+ # param options [String, Hash<String, String>] the value to return for bytes. If a String is given
59
+ # then it will be used as the value for all calls to bytes. If a Hash is given then the value
60
+ # will only be returned for ConsitentRandom objects with the names specified in the keys.
61
+ # @yield block of code to execute with the test values.
62
+ # @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
63
+ # of the block is returned. Otherwise the testing object is returned so that you can
64
+ # chain calls to set up more test values.
65
+ def bytes(options, &block)
66
+ options = validate_bytes(options)
67
+ unless options
68
+ raise ArgumentError.new("Argument must be a String or a Hash with String values")
69
+ end
70
+
71
+ @bytes_hash = options.default ? options.merge(@bytes_hash) : @bytes_hash.merge(options)
72
+ if block
73
+ use(&block)
74
+ else
75
+ self
76
+ end
77
+ end
78
+
79
+ # Set the seed value returned by ConsistentRandom#seed.
80
+ #
81
+ # param options [Integer, Hash<String, Integer>] the value to return for seed. If an Integer is given
82
+ # then it will be used as the value for all calls to seed. If a Hash is given then the value
83
+ # will only be returned for ConsitentRandom objects with the names specified in the keys.
84
+ # @yield block of code to execute with the test values.
85
+ # @return [ConsistentRandom::Testing, Object] If a block is specified, then the result
86
+ # of the block is returned. Otherwise the testing object is returned so that you can
87
+ # chain calls to set up more test
88
+ def seed(options, &block)
89
+ options = validate_seed(options)
90
+ unless options
91
+ raise ArgumentError.new("Argument must be an Integer or a Hash with Integer values")
92
+ end
93
+
94
+ @seed_hash = options.default ? options.merge(@seed_hash) : @seed_hash.merge(options)
95
+ if block
96
+ use(&block)
97
+ else
98
+ self
99
+ end
100
+ end
101
+
102
+ # Use the test values within a block of code.
103
+ #
104
+ # @yield block of code to execute with the test values.
105
+ # @return [Object] the result of the block
106
+ def use(&block)
107
+ save_val = Thread.current[:consistent_random_testing]
108
+ begin
109
+ Thread.current[:consistent_random_testing] = self
110
+ yield
111
+ ensure
112
+ Thread.current[:consistent_random_testing] = save_val
113
+ end
114
+ end
115
+
116
+ # Get the test value for rand.
117
+ #
118
+ # @param name [String] the name of the ConsistentRandom object
119
+ # @return [Float, nil] the test value for rand if there is a test value for the name
120
+ # @api private
121
+ def rand_for(name)
122
+ @rand_hash[name]
123
+ end
124
+
125
+ # Get the test value for bytes.
126
+ #
127
+ # @param name [String] the name of the ConsistentRandom object
128
+ # @return [String, nil] the test value for bytes if there is a test value for the name
129
+ # @api private
130
+ def bytes_for(name)
131
+ @bytes_hash[name]
132
+ end
133
+
134
+ # Get the test value for seed.
135
+ #
136
+ # @param name [String] the name of the ConsistentRandom object
137
+ # @return [Integer, nil] the test value for seed if there is a test value for the name
138
+ # @api private
139
+ def seed_for(name)
140
+ @seed_hash[name]
141
+ end
142
+
143
+ private
144
+
145
+ def validate_rand(options)
146
+ if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Float) && (0...1).cover?(value) }
147
+ options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
148
+ elsif options.is_a?(Float) && (0...1).cover?(options)
149
+ Hash.new(options)
150
+ end
151
+ end
152
+
153
+ def validate_bytes(options)
154
+ if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(String) }
155
+ options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value.encode(Encoding::ASCII_8BIT) }
156
+ elsif options.is_a?(String)
157
+ Hash.new(options)
158
+ end
159
+ end
160
+
161
+ def validate_seed(options)
162
+ if options.is_a?(Hash) && options.values.all? { |value| value.is_a?(Integer) }
163
+ options.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
164
+ elsif options.is_a?(Integer)
165
+ Hash.new(options)
166
+ end
167
+ end
168
+ end
169
+ end
@@ -51,11 +51,17 @@ class ConsistentRandom
51
51
  def current_seed
52
52
  Thread.current[:consistent_random_seed]
53
53
  end
54
+
55
+ def testing
56
+ Testing.new
57
+ end
54
58
  end
55
59
 
60
+ attr_reader :name
61
+
56
62
  # @param name [Object] a name used to identifuy a consistent random value
57
63
  def initialize(name)
58
- @name = name
64
+ @name = (name.is_a?(String) ? name.dup : name.to_s).freeze
59
65
  end
60
66
 
61
67
  # Generate a random number. The same number will be generated within a scope block.
@@ -68,7 +74,7 @@ class ConsistentRandom
68
74
  # a number in that range. If max is an number, then it will be an integer between 0 and that
69
75
  # value. Otherwise, it will be a float between 0 and 1.
70
76
  def rand(max = nil)
71
- value = seed / SEED_DIVISOR
77
+ value = Testing.current&.rand_for(name) || seed / SEED_DIVISOR
72
78
  case max
73
79
  when nil
74
80
  value
@@ -85,8 +91,14 @@ class ConsistentRandom
85
91
  # @param size [Integer] the number of bytes to generate.
86
92
  # @return [String] a string of random bytes.
87
93
  def bytes(size)
94
+ test_bytes = Testing.current&.bytes_for(name)
95
+ test_bytes = nil if test_bytes&.empty?
96
+ chunk_size = (test_bytes ? test_bytes.length : 20)
97
+
88
98
  bytes = []
89
- ((size + 19) / 20).times { |i| bytes << seed_hash("#{@name}#{i}").to_s }
99
+ (size / chunk_size.to_f).ceil.times do |i|
100
+ bytes << (test_bytes || seed_hash("#{name}#{i}").to_s)
101
+ end
90
102
  bytes.join[0, size]
91
103
  end
92
104
 
@@ -95,15 +107,19 @@ class ConsistentRandom
95
107
  #
96
108
  # @return [Integer] a 64 bit integer for seeding random values
97
109
  def seed
98
- hash = seed_hash(@name)
110
+ test_seed = Testing.current&.seed_for(name)
111
+ return test_seed if test_seed
112
+
113
+ hash = seed_hash(name)
99
114
  hash.byteslice(0, 8).unpack1("Q>")
100
115
  end
101
116
 
102
117
  # @return [Boolean] true if the other object is a ConsistentRandom that returns
103
118
  # the same random number generator. If called outside of a scope, then it will
104
- # always return false.
119
+ # always return false. The functionality is designed to be similar to the
120
+ # same behavior as Random.
105
121
  def ==(other)
106
- other.is_a?(self.class) && other.seed = seed
122
+ other.is_a?(self.class) && other.seed == seed
107
123
  end
108
124
 
109
125
  # Generate a random number generator for the given name. The generator will always
@@ -138,3 +154,5 @@ class ConsistentRandom
138
154
  int_range ? val.to_i : val
139
155
  end
140
156
  end
157
+
158
+ require_relative "consistent_random/testing"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consistent_random
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-27 00:00:00.000000000 Z
11
+ date: 2024-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -41,6 +41,7 @@ files:
41
41
  - lib/consistent_random/rack_middleware.rb
42
42
  - lib/consistent_random/sidekiq_client_middleware.rb
43
43
  - lib/consistent_random/sidekiq_middleware.rb
44
+ - lib/consistent_random/testing.rb
44
45
  homepage: https://github.com/bdurand/consistent_random
45
46
  licenses:
46
47
  - MIT