From 0c3b3da2b3417f48affed043d2cbaed2ffe3fa0a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 2 Jan 2019 12:45:46 -0700 Subject: [PATCH] Quickly implement `#random_sample` by copy-pasting This should be DRY'd up in the future. --- README.md | 2 +- src/spectator/dsl/structure_dsl.cr | 131 ++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 56612aa..810ca28 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ In no particular order, features that have been implemented and are planned: - [ ] DSL - [X] `describe` and `context` blocks - [X] Contextual values with `let`, `let!`, `subject`, `described_class` - - [ ] Test multiple and generated values - `sample`, `random_sample` + - [X] Test multiple and generated values - `sample`, `random_sample` - [ ] Concise syntax - `given` - [X] Before and after hooks - `before_each`, `before_all`, `after_each`, `after_all`, `around_each` - [ ] Pre- and post-conditions - `pre_condition`, `post_condition` diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index d771f66..fc9f0d0 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -291,6 +291,12 @@ module Spectator::DSL # and repeats the contents of the block with each value. # The `collection` argument should be a literal collection, # such as an array, or a function that returns an enumerable. + # Additionally, a count may be specified to limit the number of values tested. + # + # NOTE: If an infinite enumerable is provided for the collection, + # then a count must be specified. + # Only the first `count` items will be used. + # # The block can accept an argument. # If it does, then the argument's name is used to reference # the current item in the collection. @@ -320,7 +326,8 @@ module Spectator::DSL # is repeated for each element in `some_integers`. # `some_integers` is a ficticous collection. # The collection will be iterated once. - # `#sample` blocks can be nested, and work similarly to loops. + # `#sample` and `#random_sample` blocks can be nested, + # and work similarly to loops. # # A limit can be specified as well. # After the collection, a count can be added to limit @@ -334,6 +341,8 @@ module Spectator::DSL # end # end # ``` + # + # See also: `#random_sample` macro sample(collection, count = nil, &block) # Figure out the name to use for the current collection element. # If a block argument is provided, use it, otherwise use "value". @@ -415,6 +424,126 @@ module Spectator::DSL end end + # Creates a new example group to test multiple random values with. + # This method takes a collection of values and count + # and repeats the contents of the block with each value. + # This method randomly selects `count` items from the collection. + # The `collection` argument should be a literal collection, + # such as an array, or a function that returns an enumerable. + # + # NOTE: If an enumerable is used, it must be finite. + # + # The block can accept an argument. + # If it does, then the argument's name is used to reference + # the current item in the collection. + # If an argument isn't provided, then `#value` can be used instead. + # + # Example with a block argument: + # ``` + # random_sample some_integers, 5 do |integer| + # it "sets the value" do + # subject.value = integer + # expect(subject.value).to eq(integer) + # end + # end + # ``` + # + # Same spec, but without a block argument: + # ``` + # random_sample some_integers, 5 do + # it "sets the value" do + # subject.value = value + # expect(subject.value).to eq(value) + # end + # end + # ``` + # + # In the examples above, the test case (`#it` block) + # is repeated for 5 random elements in `some_integers`. + # `some_integers` is a ficticous collection. + # The collection will be iterated once. + # `#sample` and `#random_sample` blocks can be nested, + # and work similarly to loops. + # + # NOTE: If the count is the same or higher + # than the number of elements in the collection, + # then this method if functionaly equivalent to `#sample`. + # + # See also: `#sample` + macro random_sample(collection, count, &block) + # Figure out the name to use for the current collection element. + # If a block argument is provided, use it, otherwise use "value". + {% name = block.args.empty? ? "value".id : block.args.first %} + + # Method for retrieving the entire collection. + # This simplifies getting the element type. + # The name is uniquely generated to prevent namespace collision. + # This method should be called only once. + def %collection + {{collection}} + end + + # Class for generating an array with the collection's contents. + # This has to be a class that includes the parent module. + # The collection could reference a helper method + # or anything else in the parent scope. + class Collection%group + # Include the parent module. + include {{@type.id}} + + # Method that returns an array containing the collection. + # This method should be called only once. + # The framework stores the collection as an array for a couple of reasons. + # 1. The collection may not support multiple iterations. + # 2. The collection might contain randomly generated values. + # Iterating multiple times would generate inconsistent values at runtime. + def %to_a + %collection.to_a.sample({{count}}) + end + end + + # Module for the context. + # The module uses a generated unique name. + module Group%group + # Include the parent module. + # Since `@type` resolves immediately, + # this will reference the parent type. + include {{@type.id}} + + # Value wrapper for the current element. + @%wrapper : ::Spectator::Internals::ValueWrapper + + # Retrieves the current element from the collection. + def {{name}} + # Unwrap the value and return it. + # The `#first` method has a return type that matches the element type. + # So it is used on the collection method proxy to resolve the type at compile-time. + @%wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%collection.first))).value + end + + # Initializer to extract current element of the collection from sample values. + def initialize(sample_values : ::Spectator::Internals::SampleValues) + super + @%wrapper = sample_values.get_wrapper(:%group) + end + + # Start a new example group. + # Sample groups require additional configuration. + ::Spectator::DSL::Builder.start_sample_group( + {{collection.stringify}}, # String representation of the collection. + Collection%group.new.%to_a, # All elements in the collection. + {{name.stringify}}, # Name for the current element. + :%group # Unique identifier for retrieving elements for the associated collection. + ) + + # Nest the block's content in the module. + {{block.body}} + + # End the current group. + ::Spectator::DSL::Builder.end_group + end + end + # Explicitly defines the subject being tested. # The `#subject` method can be used in examples to retrieve the value (basically a method). #