pool_of_entropy 0.0.3 → 0.0.5

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.
@@ -1,26 +1,25 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'pool_of_entropy/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "pool_of_entropy"
8
+ spec.name = 'pool_of_entropy'
8
9
  spec.version = PoolOfEntropy::VERSION
9
- spec.authors = ["Neil Slater"]
10
- spec.email = ["slobo777@gmail.com"]
11
- spec.summary = %q{Random number generator with extra features for gamers.}
12
- spec.description = %q{PoolOfEntropy is a PRNG based on cryptographic secure PRNGs, intended to bring back the feeling of 'personal luck' that some gamers feel when rolling their own dice.}
13
- spec.homepage = "https://github.com/neilslater/pool_of_entropy"
14
- spec.license = "MIT"
10
+ spec.authors = ['Neil Slater']
11
+ spec.email = ['slobo777@gmail.com']
12
+ spec.summary = 'Random number generator with extra features for gamers.'
13
+ spec.description = 'PoolOfEntropy is a PRNG based on cryptographically secure PRNGs, ' \
14
+ "intended to bring back the feeling of 'personal luck' that some gamers " \
15
+ 'feel when rolling their own dice.'
16
+ spec.homepage = 'https://github.com/neilslater/pool_of_entropy'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '>= 3.3'
15
19
 
16
20
  spec.files = `git ls-files -z`.split("\x0")
17
21
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
22
+ spec.require_paths = ['lib']
20
23
 
21
- spec.add_development_dependency "yard", ">= 0.8.7.2"
22
- spec.add_development_dependency "bundler", ">= 1.3"
23
- spec.add_development_dependency "rspec", ">= 2.13.0"
24
- spec.add_development_dependency "rake", ">= 1.9.1"
25
- spec.add_development_dependency "coveralls", ">= 0.6.7"
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
26
25
  end
@@ -0,0 +1,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PoolOfEntropy::CorePRNG do
6
+ shared_examples 'an adjustable reader' do |prng, read|
7
+ let(:original) { prng.clone }
8
+ let(:copy) { original.clone }
9
+
10
+ it 'returns the same output for the same adjustment' do
11
+ copy
12
+ 10.times do
13
+ expect(read.call(original, 'Hello!')).to eql read.call(copy, 'Hello!')
14
+ end
15
+ end
16
+
17
+ { 'different adjustments' => [%w[Hello!], %w[Goodbye!]],
18
+ 'an adjustment on only the original' => [%w[Hello!], []],
19
+ 'an adjustment on only the copy' => [[], %w[Goodbye!]],
20
+ 'multiple adjustments on only the original' => [%w[Hello Goodbye], []] }.each do |description, arguments|
21
+ it "returns different output for #{description}" do
22
+ copy
23
+ 10.times do
24
+ expect(read.call(original, *arguments.first)).not_to eql read.call(copy, *arguments.last)
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'when adjusted reads use mismatched arguments' do
30
+ before do
31
+ copy
32
+ [[%w[Hello!], %w[Goodbye!]], [[], %w[Goodbye!]], [%w[Hello!], []], [%w[Hello Goodbye], []]].each do |args|
33
+ read.call(original, *args.first)
34
+ read.call(copy, *args.last)
35
+ end
36
+ end
37
+
38
+ it 'does not include adjustments in state changes' do
39
+ expect(original.state).to eql copy.state
40
+ end
41
+
42
+ it 'returns to matching output' do
43
+ expect(read.call(original)).to eql read.call(copy)
44
+ end
45
+ end
46
+ end
47
+
48
+ describe '.new' do
49
+ it 'instantiates a default object' do
50
+ prng = described_class.new
51
+ expect(prng).to be_a(described_class).and have_attributes(size: 1)
52
+ end
53
+
54
+ it 'allows setting number of blocks in pool' do
55
+ prng = described_class.new(10)
56
+ expect(prng).to be_a(described_class).and have_attributes(size: 10)
57
+ end
58
+
59
+ { -43 => ArgumentError, -1 => ArgumentError, 0 => ArgumentError,
60
+ 257 => ArgumentError, 1000 => ArgumentError, nil => TypeError,
61
+ '' => ArgumentError, { foo: 2 } => TypeError }.each do |size, error|
62
+ it "rejects block count #{size.inspect}" do
63
+ expect { described_class.new(size) }.to raise_error error
64
+ end
65
+ end
66
+
67
+ it 'allows setting internal state' do
68
+ prng = described_class.new(1, "\x0" * 64)
69
+ expect(prng).to be_a(described_class).and have_attributes(size: 1, state: "\x0" * 64)
70
+ end
71
+
72
+ { [1, :boo] => TypeError, [1, []] => TypeError, [1, ''.dup] => ArgumentError,
73
+ [1, "\x0" * 63] => ArgumentError, [1, "\x0" * 200] => ArgumentError,
74
+ [2, "\x0" * 64] => ArgumentError }.each do |arguments, error|
75
+ it "rejects state data #{arguments.last.inspect}" do
76
+ expect { described_class.new(*arguments) }.to raise_error error
77
+ end
78
+ end
79
+
80
+ it 'allows setting mix_block_id' do
81
+ prng = described_class.new(3, "\x12" * 192, 1)
82
+ expect(prng).to be_a(described_class).and have_attributes(
83
+ size: 3, state: "\x12" * 192, mix_block_id: 1
84
+ )
85
+ end
86
+ end
87
+
88
+ describe '#clone' do
89
+ it 'copies all attributes' do
90
+ prng_orig = described_class.new
91
+ prng_copy = prng_orig.clone
92
+
93
+ expect(prng_copy).to have_attributes(
94
+ size: prng_orig.size, state: prng_orig.state, mix_block_id: prng_orig.mix_block_id
95
+ )
96
+ end
97
+
98
+ it 'deeps clone the internal state string' do
99
+ prng_orig = described_class.new
100
+ prng_copy = prng_orig.clone
101
+ expect(prng_copy.state).not_to be prng_orig.state
102
+ end
103
+ end
104
+
105
+ describe '#update' do
106
+ it 'changes the internal state' do
107
+ prng = described_class.new
108
+ init_state = prng.state.clone
109
+ prng.update('boo')
110
+ expect(prng.state).not_to eql init_state
111
+ end
112
+
113
+ { 1 => %w[boo boowgkjwrhqgioueqrhgiue2hguirhqwiughreuioghreuifhqwoifhr3iufghfwrgrwgetdfwd],
114
+ 5 => ['boo', 'getdfwd' * 1000, 'boefewfweo', 'geefewftdfwd' * 1000] }.each do |size, updates|
115
+ it "preserves the state length for a #{size}-block pool" do
116
+ prng = described_class.new(size)
117
+ lengths = state_lengths_after(prng, updates)
118
+ expect(lengths).to all be(size * 64)
119
+ end
120
+ end
121
+
122
+ ['boo', 'getdfwd' * 1000, 'boefewfweo', 'geefewftdfwd' * 1000].each_with_index do |data, index|
123
+ it "changes only state block #{index}" do
124
+ prng = described_class.new(5)
125
+ index.times { |prior| prng.update(['boo', 'getdfwd' * 1000, 'boefewfweo'][prior]) }
126
+ before = prng.state.clone
127
+ expect { prng.update(data) }.to change { [before, prng.state] }.to(change_only_block(index))
128
+ end
129
+ end
130
+ end
131
+
132
+ instance01 = described_class.new
133
+ instance01.update('QWertyuiopp')
134
+ instance01.update('Asdfghjkjl')
135
+ instance01.update('Zxcvbnm')
136
+
137
+ pool_types = [
138
+ [
139
+ 'default instance',
140
+ described_class.new
141
+ ],
142
+ [
143
+ 'instance with 2KB pool size',
144
+ described_class.new(32)
145
+ ],
146
+ [
147
+ 'instance with initial state all 0',
148
+ described_class.new(1, "\x0" * 64)
149
+ ],
150
+ [
151
+ 'instance with fixed initial state',
152
+ described_class.new(5, 'fiver' * 64, 3)
153
+ ],
154
+ [
155
+ 'instance cloned from 2KB instance',
156
+ described_class.new(32).clone
157
+ ],
158
+ [
159
+ 'instance that has been updated with user data',
160
+ instance01
161
+ ]
162
+ ]
163
+
164
+ # NB "probability" and "randomness" tests in the following block are very light, just
165
+ # intended to capture high-level failures in logic. See DIEHARDER_TEST.md for thorough
166
+ # checks on statistical randomness of PoolOfEntropy::CorePRNG
167
+ pool_types.each do |prng_name, prng|
168
+ context "with #{prng_name}" do
169
+ describe '#read_bytes' do
170
+ it 'always returns a 16 byte string' do
171
+ 100.times { expect(prng.read_bytes.length).to be 16 }
172
+ end
173
+
174
+ it 'has a high probability of returning a different string each time' do
175
+ expect(Set[*(1..100).map { prng.read_bytes }].size).to be 100
176
+ end
177
+
178
+ it 'always returns a 16 byte string with adjustments' do
179
+ 100.times { expect(prng.read_bytes('654321').length).to be 16 }
180
+ end
181
+
182
+ it 'has a high probability of returning a different adjusted string each time' do
183
+ expect(Set[*(1..100).map { prng.read_bytes('654321') }].size).to be 100
184
+ end
185
+
186
+ it_behaves_like 'an adjustable reader', prng, ->(reader, *args) { reader.read_bytes(*args) }
187
+ end
188
+
189
+ describe '#read_hex' do
190
+ it 'always returns a 32 digit hex string' do
191
+ 100.times do
192
+ expect(prng.read_hex).to match(/\A[0-9a-f]{32}\z/)
193
+ end
194
+ end
195
+
196
+ it 'has a high probability of returning a different string each time' do
197
+ expect(Set[*(1..100).map { prng.read_hex }].size).to be 100
198
+ end
199
+
200
+ it 'always returns a 32 digit hex string with adjustments' do
201
+ 100.times do
202
+ expect(prng.read_hex('QWertyeu')).to match(/\A[0-9a-f]{32}\z/)
203
+ end
204
+ end
205
+
206
+ it 'has a high probability of returning a different adjusted string each time' do
207
+ expect(Set[*(1..100).map { prng.read_hex('654321') }].size).to be 100
208
+ end
209
+
210
+ it_behaves_like 'an adjustable reader', prng, ->(reader, *args) { reader.read_hex(*args) }
211
+ end
212
+
213
+ describe '#read_bignum' do
214
+ it 'always returns a 128-bit unsigned integer' do
215
+ 100.times do
216
+ expect(prng.read_bignum).to be_an_integer_in(0...(2**128))
217
+ end
218
+ end
219
+
220
+ it 'has a high probability of returning a different number each time' do
221
+ expect(Set[*(1..100).map { prng.read_bignum }].size).to be 100
222
+ end
223
+
224
+ it 'always returns an adjusted 128-bit unsigned integer' do
225
+ 100.times do
226
+ expect(prng.read_bignum('Biggest')).to be_an_integer_in(0...(2**128))
227
+ end
228
+ end
229
+
230
+ it 'has a high probability of returning a different adjusted number each time' do
231
+ expect(Set[*(1..100).map { prng.read_bignum('654321') }].size).to be 100
232
+ end
233
+
234
+ it_behaves_like 'an adjustable reader', prng, ->(reader, *args) { reader.read_bignum(*args) }
235
+ end
236
+
237
+ describe '#read_float' do
238
+ it 'always returns a Float between 0.0 (inclusive) and 1.0 (exclusive)' do
239
+ 100.times do
240
+ expect(prng.read_float).to be_a_float_in(0.0...1.0)
241
+ end
242
+ end
243
+
244
+ it 'has a high probability of returning a different Float each time' do
245
+ expect(Set[*(1..100).map { prng.read_float }].size).to be 100
246
+ end
247
+
248
+ it 'always returns an adjusted Float between 0.0 (inclusive) and 1.0 (exclusive)' do
249
+ 100.times do
250
+ expect(prng.read_float('Boom')).to be_a_float_in(0.0...1.0)
251
+ end
252
+ end
253
+
254
+ it 'has a high probability of returning a different adjusted Float each time' do
255
+ expect(Set[*(1..100).map { prng.read_float('654321') }].size).to be 100
256
+ end
257
+
258
+ it_behaves_like 'an adjustable reader', prng, ->(reader, *args) { reader.read_float(*args) }
259
+ end
260
+
261
+ describe '#generate_integer' do
262
+ it 'always returns an integer between 0 (inclusive) and supplied top (exclusive)' do
263
+ [10, 100, 1000, 647_218_456].each do |maximum|
264
+ 100.times do
265
+ expect(prng.generate_integer(maximum)).to be_an_integer_in(0...maximum)
266
+ end
267
+ end
268
+ end
269
+
270
+ it 'generates integers within a range larger than 2**128' do
271
+ results = (0..100).map do
272
+ prng.generate_integer(2**1024)
273
+ end
274
+ expect(results).to all be_an_integer_in(0...(2**1024))
275
+ end
276
+
277
+ it 'can generate integers larger than 2**1020' do
278
+ results = (0..100).map { prng.generate_integer(2**1024) }
279
+ expect(results.select { |n| n > 2**1020 }.count).to be > 50
280
+ end
281
+
282
+ it 'returns different integers without adjustments when the upper bound is large' do
283
+ expect(Set[*(1..100).map { prng.generate_integer((2**75) - 7) }].size).to be 100
284
+ end
285
+
286
+ it 'covers a distribution 0...top' do
287
+ expect(Set[*(1..100).map { prng.generate_integer(10) }].size).to be 10
288
+ end
289
+
290
+ [[10, %w[jkffwe]], [100, %w[jkffweefewg]], [1000, %w[jkffweefewg efhwjkfgw]],
291
+ [647_218_456, %w[j*****g efhwjkfgw]]].each do |maximum, adjustments|
292
+ it "returns integers below #{maximum}" do
293
+ 100.times do
294
+ expect(prng.generate_integer(maximum, *adjustments)).to be_an_integer_in(0...maximum)
295
+ end
296
+ end
297
+ end
298
+
299
+ it 'returns different integers with adjustments when the upper bound is large' do
300
+ expect(Set[*(1..100).map { prng.generate_integer((2**80) - 5, '654321') }].size).to be 100
301
+ end
302
+
303
+ it_behaves_like 'an adjustable reader', prng,
304
+ ->(reader, *args) { reader.generate_integer((2**64) - 3, *args) }
305
+ end
306
+
307
+ describe 'all read methods' do
308
+ it 'generate higher and lower values in sync' do
309
+ orders = reader_orders(prng)
310
+ expect(orders).to match(
311
+ int: orders[:hex], hex: orders[:hex], float: orders[:hex],
312
+ num: orders[:bytes], bytes: orders[:bytes]
313
+ )
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ describe 'using predictable sequences' do
320
+ let(:prng) { described_class.new(1, "\x0" * 64) }
321
+
322
+ let(:zeroed_sequence) do
323
+ %w[da0cd77eb1c84458ddcc91e36b9dcb35
324
+ 498ec24d1126440047eed396d836b5e1 0b90df55c5e7c1513b072367eae4a4ce
325
+ f12b5b54b5594e785bbb9a4ac50ccec8 d506bd4e201a00dc30499bd8e59a30d8
326
+ 2557893cf995fe43bd00721fce6ab16a 41ee50244cdd02334bafc3e9d8f564d9
327
+ 8156c7c7bcfd856cb9d1012243cfc662 1116f840e2aee924bd2c7d722c602635
328
+ 96cf31967465b83f5ef3476c60afe20a f041f6df72aa2eab7394f08e83d52a0c
329
+ 5ccbb30077f2433bd765ddc86840a880 6a4339fd5d445024048ea8f91a3e02fd
330
+ 707e9499b9f0e9e906a0c0ddd0530803 553704d95703284df323f0aa4244cf81
331
+ cb1522ce0bdf2504fdd62df7416be73e 05c4932cb9d3a7c0675b0228826e661a
332
+ 59606f23e34e726a7912dacfde533d97 188cf17dde6947264ce05f8274874ffa
333
+ 24c184b56453891657953f557635b742 a5f969e50228b2ee0f38cc37c5033541
334
+ f1de0e5d149cc5a7f0e38c0ee501d7bc 5ca3a6beb0b810568be83ab2179eb550
335
+ 756fb9c58277eb8c6092142224caecf4 0ed9505eadb3def60ec42051f0bf15ef]
336
+ end
337
+
338
+ let(:alternating_sequence) do
339
+ %w[
340
+ da0cd77eb1c84458ddcc91e36b9dcb35 de4b20a0560263090c6fe11ebba6256e
341
+ 0b90df55c5e7c1513b072367eae4a4ce 0c92d534160cb268cb3282a9dd3f1efe
342
+ d506bd4e201a00dc30499bd8e59a30d8 089f799236a9cf64f22f1deafbe28214
343
+ 41ee50244cdd02334bafc3e9d8f564d9 84477b9a3d51bb72868ccb85bea71cad
344
+ 1116f840e2aee924bd2c7d722c602635 469e4bb2f38719c7f85d79c8900a8fa8
345
+ f041f6df72aa2eab7394f08e83d52a0c e21bf2508b8e1755ae70c74c0d36ec2e
346
+ 6a4339fd5d445024048ea8f91a3e02fd a94a80c80c9edd95808a67775c2b42d4
347
+ 553704d95703284df323f0aa4244cf81 edd6aea5bda7b2d9c494067c5cdbe410
348
+ 05c4932cb9d3a7c0675b0228826e661a cf81635ca2fa910fd11cf45508dc658b
349
+ 188cf17dde6947264ce05f8274874ffa bb4aaeb98e13400c1216c674a872ba93
350
+ a5f969e50228b2ee0f38cc37c5033541 1a795355f979575247b3fa9c92d97917
351
+ 5ca3a6beb0b810568be83ab2179eb550 1a98419cca1dd1b370ce8a4a2ab721b7
352
+ 0ed9505eadb3def60ec42051f0bf15ef
353
+ ]
354
+ end
355
+ let(:adjusted_sequence) do
356
+ %w[
357
+ fed9de9a612f4157ebb49582ca557a50 a7adc2374a4df2ed67846ac09a3b6645
358
+ cbdf8bbbe6145751fe14004719915160 8cd84be95376f72918c305bdea43e36d
359
+ 9e08e80ff40c942f0cf4d479b5378fa0 54d80c5330873f9733a0adef0197220f
360
+ 2abe07bd85d2066a624dd3630e59730a 88b6a697fe74aeb8ec83845e103b7b63
361
+ 9c6c9d613855f6535adb419cc564fd10 23f9b778b254035a0e4219423a52da77
362
+ 4cd50c14ee17fa29c8b1f209432a36b3 2b5646b164d863de716f67adef653859
363
+ fd81d0bbbd2828ecdfcba0b486ef786c 08e6594fc277ff7fafbf37475ecadbf6
364
+ 5e2333ecc800eb9a06e347924ed42e94 8a14e48013028b2c9f174a07ddd5ef49
365
+ e9a5c331f54b155f570c2cf2bcd37209 1ce84279195a5b23ffeb3063edbfab21
366
+ 7422e19e58f2fcf20805f601266bf676 8df0226465b74d830360ea8609d181bf
367
+ 3e5d5e8cf8ad032fb4005826bdf7bb94 65e3f9ab28c26bead6647c21bcc6245e
368
+ 8ed5d8892d464ebeaa8dcbcef0968936 2f000bc1ab14d914196cc1d5055db189
369
+ 8e9c4b8152f14e47ac31f25a80765ebf
370
+ ]
371
+ end
372
+
373
+ it 'generates expected hex strings from simple zeroed pool' do
374
+ expect((0..24).map { prng.read_hex }).to eql zeroed_sequence
375
+ end
376
+
377
+ it 'generates expected hex strings from simple zeroed pool and an adjustment' do
378
+ expect((0..24).map { prng.read_hex('bananas') }).to eql adjusted_sequence
379
+ end
380
+
381
+ it 'generates expected hex strings from simple zeroed pool and alternating adjustments' do
382
+ results = (0..24).map { |index| index.odd? ? prng.read_hex('bananas', index.to_s) : prng.read_hex }
383
+ expect(results).to eql alternating_sequence
384
+ end
385
+ end
386
+ end