Skip to content

Commit

Permalink
Make RandomExample/RandomItemGenerator deterministic (use seed)
Browse files Browse the repository at this point in the history
We're now building govuk-developer-docs hourly, but our heavy use
of GovukSchemas::RandomExample is causing ~50,000 line changes
for every commit to gh-pages, despite the underlying schemas
remaining unchanged.

We want the example to be random and schema-validated, but not
necessarily a new random on every build. Hence we would like to
set the 'seed' so that we can predictably execute the same random
generation every time. In theory, this means our gh-pages output
will be identical between builds until an underlying schema changes.

govuk_schemas did not support this out of the box because of its
reliance on SecureRandom, which, as the name implies, does not
allow outside interference/configuration by way of seed values
(it gives a truly random result every time). We don't _need_ a
'secure' random output - just something we can be reasonably
confident will give a different result every time if the seed is
omitted. I've reproduced its 'uuid' and 'hex' methods using arrays
of chars/numbers and the native 'rand' method, which _does_
respect the global seed value.
  • Loading branch information
ChrisBAshton committed Jul 22, 2020
1 parent 0551260 commit e491224
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 6 deletions.
32 changes: 26 additions & 6 deletions lib/govuk_schemas/random.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require "securerandom"

module GovukSchemas
# @private
module Random
Expand Down Expand Up @@ -27,7 +25,7 @@ def uri
end

def base_path
"/" + rand(1..5).times.map { SecureRandom.uuid }.join("/")
"/" + rand(1..5).times.map { uuid }.join("/")
end

def govuk_subdomain_url
Expand All @@ -48,19 +46,29 @@ def bool
end

def anchor
"##{SecureRandom.hex}"
"##{hex}"
end

def random_identifier(separator:)
Utils.parameterize(WORDS.sample(rand(1..10)).join("-")).gsub("-", separator)
end

def uuid
# matches uuid regex e.g. e058aad7-ce86-5181-8801-4ddcb3c8f27c
# /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/
"#{hex(8)}-#{hex(4)}-1#{hex(3)}-a#{hex(3)}-#{hex(12)}"
end

def hex(length = 10)
length.times.map { bool ? random_letter : random_number }.join("")
end

def string_for_regex(pattern)
case pattern.to_s
when "^(placeholder|placeholder_.+)$"
["placeholder", "placeholder_#{WORDS.sample}"].sample
when "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
SecureRandom.uuid
uuid
when "^/(([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})+(/([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})*)*)?$"
base_path
when "^[1-9][0-9]{3}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12][0-9]|3[0-1])$"
Expand All @@ -80,7 +88,7 @@ def string_for_regex(pattern)
when "^https://([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[A-Za-z0-9])?\\.)*gov\\.uk(/(([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})+(/([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})*)*)?(\\?([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})*)?(#([a-zA-Z0-9._~!$&'()*+,;=:@-]|%[0-9a-fA-F]{2})*)?)?$"
govuk_subdomain_url
when '[a-z0-9\-_]'
"#{SecureRandom.hex}-#{SecureRandom.hex}"
"#{hex}-#{hex}"
else
raise <<-DOC
Don't know how to generate random string for pattern #{pattern.inspect}
Expand All @@ -96,6 +104,18 @@ def string_for_regex(pattern)
DOC
end
end

private

def random_letter
letters = ("a".."f").to_a
letters[rand(0..letters.count - 1)]
end

def random_number
numbers = ("0".."9").to_a
numbers[rand(0..numbers.count - 1)]
end
end
end
end
5 changes: 5 additions & 0 deletions lib/govuk_schemas/random_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ class RandomExample
# schema = GovukSchemas::Schema.find(frontend_schema: "detailed_guide")
# GovukSchemas::RandomExample.new(schema: schema).payload
#
# If you need a 'consistent' random response, you can set the seed using
# (for example) `srand(777)` and know that the payload will always be the
# same. Note that timestamps in the payload will continue to reflect the
# current time.
#
# @param [Hash] schema A JSON schema.
# @return [GovukSchemas::RandomExample]
def initialize(schema:)
Expand Down
10 changes: 10 additions & 0 deletions spec/lib/random_example_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
end
end

it "returns the same output if a seed is detected" do
schema = GovukSchemas::Schema.random_schema(schema_type: "frontend")
srand(777) # these srand calls would be in the upstream application
first_payload = GovukSchemas::RandomExample.new(schema: schema).payload
srand(777)
second_payload = GovukSchemas::RandomExample.new(schema: schema).payload

expect(first_payload).to eql(second_payload)
end

it "can customise the payload" do
schema = GovukSchemas::Schema.random_schema(schema_type: "frontend")

Expand Down

0 comments on commit e491224

Please sign in to comment.