Skip to content

Rewrite seeded_rand() as a Puppet 4.x function #1344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions lib/puppet/functions/seeded_rand.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# @summary
# Generates a random whole number greater than or equal to 0 and less than max, using the value of seed for repeatable randomness.
Puppet::Functions.create_function(:seeded_rand) do
# @param max The maximum value.
# @param seed The seed used for repeatable randomness.
#
# @return [Integer]
# A random number greater than or equal to 0 and less than max
dispatch :seeded_rand do
param 'Integer[1]', :max
param 'String', :seed
end

def seeded_rand(max, seed)
require 'digest/md5'

seed = Digest::MD5.hexdigest(seed).hex
Puppet::Util.deterministic_rand_int(seed, max)
end
end
30 changes: 0 additions & 30 deletions lib/puppet/parser/functions/seeded_rand.rb

This file was deleted.

69 changes: 17 additions & 52 deletions spec/functions/seeded_rand_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,22 @@

describe 'seeded_rand' do
it { is_expected.not_to eq(nil) }
it { is_expected.to run.with_params.and_raise_error(ArgumentError, %r{wrong number of arguments}i) }
it { is_expected.to run.with_params(1).and_raise_error(ArgumentError, %r{wrong number of arguments}i) }
it { is_expected.to run.with_params(0, '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params(1.5, '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params(-10, '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params('-10', '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params('string', '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params([], '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params({}, '').and_raise_error(ArgumentError, %r{first argument must be a positive integer}) }
it { is_expected.to run.with_params(1, 1).and_raise_error(ArgumentError, %r{second argument must be a string}) }
it { is_expected.to run.with_params(1, []).and_raise_error(ArgumentError, %r{second argument must be a string}) }
it { is_expected.to run.with_params(1, {}).and_raise_error(ArgumentError, %r{second argument must be a string}) }

it 'provides a random number strictly less than the given max' do
expect(seeded_rand(3, 'seed')).to satisfy { |n| n.to_i < 3 } # rubocop:disable Lint/AmbiguousBlockAssociation : Cannot parenthesize without break code or violating other Rubocop rules
end

it 'provides a random number greater or equal to zero' do
expect(seeded_rand(3, 'seed')).to satisfy { |n| n.to_i >= 0 } # rubocop:disable Lint/AmbiguousBlockAssociation : Cannot parenthesize without break code or violating other Rubocop rules
end

it "provides the same 'random' value on subsequent calls for the same host" do
expect(seeded_rand(10, 'seed')).to eql(seeded_rand(10, 'seed'))
end

it 'allows seed to control the random value on a single host' do
first_random = seeded_rand(1000, 'seed1')
second_different_random = seeded_rand(1000, 'seed2')

expect(first_random).not_to eql(second_different_random)
end

it 'does not return different values for different hosts' do
val1 = seeded_rand(1000, 'foo', host: 'first.host.com')
val2 = seeded_rand(1000, 'foo', host: 'second.host.com')

expect(val1).to eql(val2)
end

def seeded_rand(max, seed, args = {})
host = args[:host] || '127.0.0.1'

# workaround not being able to use let(:facts) because some tests need
# multiple different hostnames in one context
allow(scope).to receive(:lookupvar).with('::fqdn', {}).and_return(host)

scope.function_seeded_rand([max, seed])
end

context 'with UTF8 and double byte characters' do
it { is_expected.to run.with_params(1000, 'ǿňè') }
it { is_expected.to run.with_params(1000, '文字列') }
it { is_expected.to run.with_params.and_raise_error(ArgumentError, %r{'seeded_rand' expects 2 arguments, got none}i) }
it { is_expected.to run.with_params(1).and_raise_error(ArgumentError, %r{'seeded_rand' expects 2 arguments, got 1}i) }
it { is_expected.to run.with_params(0, '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer\[1\] value, got Integer\[0, 0\]}) }
it { is_expected.to run.with_params(1.5, '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer value, got Float}) }
it { is_expected.to run.with_params(-10, '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer\[1\] value, got Integer\[-10, -10\]}) }
it { is_expected.to run.with_params('string', '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer value, got String}) }
it { is_expected.to run.with_params([], '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer value, got Array}) }
it { is_expected.to run.with_params({}, '').and_raise_error(ArgumentError, %r{parameter 'max' expects an Integer value, got Hash}) }
it { is_expected.to run.with_params(1, 1).and_raise_error(ArgumentError, %r{parameter 'seed' expects a String value, got Integer}) }
it { is_expected.to run.with_params(1, []).and_raise_error(ArgumentError, %r{parameter 'seed' expects a String value, got Array}) }
it { is_expected.to run.with_params(1, {}).and_raise_error(ArgumentError, %r{parameter 'seed' expects a String value, got Hash}) }

context 'produce predictible and reproducible results' do
it { is_expected.to run.with_params(20, 'foo').and_return(1) }
it { is_expected.to run.with_params(100, 'bar').and_return(35) }
it { is_expected.to run.with_params(1000, 'ǿňè').and_return(247) }
it { is_expected.to run.with_params(1000, '文字列').and_return(67) }
end
end