From e49122455967eee290f821a9480e7db5b06d4d63 Mon Sep 17 00:00:00 2001 From: ChrisBAshton Date: Tue, 21 Jul 2020 13:22:53 +0100 Subject: [PATCH] Make RandomExample/RandomItemGenerator deterministic (use seed) 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. --- lib/govuk_schemas/random.rb | 32 +++++++++++++++++++++++------ lib/govuk_schemas/random_example.rb | 5 +++++ spec/lib/random_example_spec.rb | 10 +++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/govuk_schemas/random.rb b/lib/govuk_schemas/random.rb index 19dd6b7..e78480c 100644 --- a/lib/govuk_schemas/random.rb +++ b/lib/govuk_schemas/random.rb @@ -1,5 +1,3 @@ -require "securerandom" - module GovukSchemas # @private module Random @@ -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 @@ -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])$" @@ -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} @@ -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 diff --git a/lib/govuk_schemas/random_example.rb b/lib/govuk_schemas/random_example.rb index ef9f283..3846ecf 100644 --- a/lib/govuk_schemas/random_example.rb +++ b/lib/govuk_schemas/random_example.rb @@ -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:) diff --git a/spec/lib/random_example_spec.rb b/spec/lib/random_example_spec.rb index 080e8ce..afb031d 100644 --- a/spec/lib/random_example_spec.rb +++ b/spec/lib/random_example_spec.rb @@ -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")