consistent_random 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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