diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d101fe..1e66e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ ## [Unreleased] +## [0.1.0] - 2024-04-13 + +- Implement basic primitive arbitraries +- Implement composite arbitraries +- Support shrinking +- Support multiple concurrency methods + - Ractor + - Process + - Thread + - None (Run tests sequentially) +- Documentation + - Add better examples + - Arbitrary usage + - Configuration + ## [0.0.1] - 2024-01-27 -- Initial release +- Initial release (Proof of concept) diff --git a/README.md b/README.md index da8f281..fff594b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,48 @@ # Property-Based Testing in Ruby -⚠️ This gem is currently in the proof of concept phase. It's experimental and not production quality for now! +[![Gem Version](https://badge.fury.io/rb/pbt.svg)](https://rubygems.org/gems/pbt) +[![Build Status](https://github.com/ohbarye/pbt/actions/workflows/main.yml/badge.svg)](https://github.com/ohbarye/pbt/actions/workflows/main.yml) +[![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/gems/pbt) -A property-based testing tool for Ruby, utilizing Ractor for parallelizing test cases. +An experimental property-based testing tool for Ruby that allows you to run test cases in parallel. + +PBT stands for Property-Based Testing. + +## What's Property-Based Testing? + +Property-Based Testing is a testing methodology that focuses on the properties a system should always satisfy, rather than checking individual examples. Instead of writing tests for predefined inputs and outputs, PBT allows you to specify the general characteristics that your code should adhere to and then automatically generates a wide range of inputs to verify these properties. + +The key benefits of property-based testing include the ability to cover more edge cases and the potential to discover bugs that traditional example-based tests might miss. It's particularly useful for identifying unexpected behaviors in your code by testing it against a vast set of inputs, including those you might not have considered. + +For a more in-depth understanding of Property-Based Testing, please refer to external resources. + +- Original ideas + - [Property-based testing of privileged programs](https://ieeexplore.ieee.org/document/367311) (1994) + - [Property-based testing: a new approach to testing for assurance](https://dl.acm.org/doi/abs/10.1145/263244.263267) (1997) + - [QuickCheck: a lightweight tool for random testing of Haskell programs](https://dl.acm.org/doi/10.1145/351240.351266) (2000) +- Rather new introductory resources + - Fred Hebert's book [Property-Based Testing With PropEr, Erlang and Elixir](https://propertesting.com/). + - [fast-check - Why Property-Based?](https://fast-check.dev/docs/introduction/why-property-based/) ## Installation -```shell -$ gem install pbt +Add this line to your application's Gemfile and run `bundle install`. + +```ruby +gem 'pbt' ``` -If you want to use concurrency methods other than Ractor (`process`, `thread`), you need to install [parallel](https://github.com/grosser/parallel) gem as well. +If you want to use multi-processes or multi-threads (other than Ractor) as workers to run tests, install the [parallel](https://github.com/grosser/parallel) gem. -```shell -$ gem install parallel +```ruby +gem 'parallel' ``` -## Usage +Off course you can install with `gem intstall pbt`. + +## Basic Usage + +### Simple property ```ruby # Let's say you have a method that returns just a multiplicative inverse. @@ -24,77 +50,201 @@ def multiplicative_inverse(number) Rational(1, number) end -RSpec.describe Pbt do - it "works" do - Pbt.assert do - # The given block is executed 100 times with different random numbers. - # Besides, the block runs in parallel by Ractor. - Pbt.property(Pbt.integer) do |number| - result = multiplicative_inverse(number) - raise "Result should be the multiplicative inverse of the number" if result * number != 1 - end - end - - # If the function has a bug, the test fails with a counterexample. - # For example, the multiplicative_inverse method doesn't work for 0 regardless of the behavior is intended or not. - # - # Pbt::PropertyFailure: - # Property failed after 23 test(s) - # { seed: 11001296583699917659214176011685741769 } - # Counterexample: 0 - # Shrunk 3 time(s) - # Got ZeroDivisionError: divided by 0 +Pbt.assert do + # The given block is executed 100 times with different random numbers. + # Besides, the block runs in parallel by Ractor. + Pbt.property(Pbt.integer) do |number| + result = multiplicative_inverse(number) + raise "Result should be the multiplicative inverse of the number" if result * number != 1 end end + +# If the function has a bug, the test fails with a counterexample. +# For example, the multiplicative_inverse method doesn't work for 0 regardless of the behavior is intended or not. +# +# Pbt::PropertyFailure: +# Property failed after 23 test(s) +# { seed: 11001296583699917659214176011685741769 } +# Counterexample: 0 +# Shrunk 3 time(s) +# Got ZeroDivisionError: divided by 0 ``` +### Explain The Snippet + +The above snippet is very simple but contains the basic components. + +#### Runner + +`Pbt.assert` is the runner. The runner interprets and executes the given property. `Pbt.assert` takes a property and runs it multiple times. If the property fails, it tries to shrink the input that caused the failure. + +#### Property + +The snippet above declared a property by calling `Pbt.property`. The property describes the following: + +1. What the user wants to evaluate. This corresponds to the block (let's call this `predicate`) enclosed by `do` `end` +2. How to generate inputs for the predicate — using `Arbitrary` + +The `predicate` block is a function that directly asserts, taking values generated by `Arbitrary` as input. + +#### Arbitrary + +Arbitrary generates random values. It is also responsible for shrinking those values if asked to shrink a failed value as input. + +Here, we used only one type of arbitrary, `Pbt.integer`. There are many other built-in arbitraries, and you can create a variety of inputs by combining existing ones. + +#### Shrink + +In PBT, If a test fails, it attempts to shrink the case that caused the failure into a form that is easier for humans to understand. +In other words, instead of stopping the test itself the first time it fails and reporting the failed value, it tries to find the minimal value that causes the error. + +When there is a test that fails when given an even number, a counterexample of `2` is simpler and easier to understand than `432743417662`. + ### Arbitrary -TBA +There are many built-in arbitraries in `Pbt`. You can use them to generate random values for your tests. Here are some representative arbitraries. + +#### Primitives + +```ruby +rng = Random.new( -### Configuration +Pbt.integer.generate(rng) # => 42 +Pbt.integer(min: -1, max: 8).generate(rng) # => Integer between -1 and 8 -TBA +Pbt.symbol.generate(rng) # => :atq -### Concurrent methods +Pbt.ascii_char.generate(rng) # => "a" +Pbt.ascii_string.generate(rng) # => "aagjZfao" -Pbt supports 3 concurrency methods and 1 sequential one. You can choose one of them by setting the `concurrency_method` option. +Pbt.boolean.generate(rng) # => true or false +Pbt.constant(42).generate(rng) # => 42 always +``` -#### Ractor +#### Composites ```ruby -Pbt.assert(params: { concurrency_method: :ractor }) do - Pbt.property(Pbt.integer) do |number| +rng = Random.new + +Pbt.array(Pbt.integer).generate(rng) # => [121, -13141, 9825] +Pbt.array(Pbt.integer, max: 1, empty: true).generate(rng) # => [] or [42] etc. + +Pbt.tuple(Pbt.symbol, Pbt.integer).generate(rng) # => [:atq, 42] + +Pbt.fixed_hash(x: Pbt.symbol, y: Pbt.integer).generate(rng) # => {x: :atq, y: 42} +Pbt.hash(Pbt.symbol, Pbt.integer).generate(rng) # => {atq: 121, ygab: -1142} + +Pbt.one_of(:a, 1, 0.1).generate(rng) # => :a or 1 or 0.1 +```` + +See [ArbitraryMethods](https://github.com/ohbarye/pbt/blob/main/lib/pbt/arbitrary/arbitrary_methods.rb) module for more details. + +## Configuration + +You can configure `Pbt` by calling `Pbt.configure` before running tests. + +```ruby +Pbt.configure do |config| + # Whether to print verbose output. Default is `false`. + config.verbose = 100 + + # The concurrency method to use. :ractor`, `:thread`, `:process` and `:none` are supported. Default is `:ractor`. + config.worker = :ractor + + # The number of runs to perform. Default is `100`. + config.num_runs = 100 + + # The seed to use for random number generation. + # It's useful to reproduce failed test with the seed you'd pick up from failure messages. Default is a random seed. + config.seed = 42 + + # Whether to report exceptions in threads. + # It's useful to suppress error logs on Ractor that reports many errors. Default is `false`. + config.thread_report_on_exception = false +end +``` + +Or, you can pass the configuration to `Pbt.assert` as an argument. + +```ruby +Pbt.assert(num_runs: 100, seed: 42) do + # ... +end +``` + +## Concurrent methods + +One of the key features of `Pbt` is its ability to rapidly execute test cases in parallel or concurrently, using a large number of values (by default, `100`) generated by `Arbitrary`. + +For concurrent processing, you can specify any of the three workers—`:ractor`, `:process`, or `:thread`—using the `worker` option. Alternatively, choose `:none` for serial execution. + +`Pbt` supports 3 concurrency methods and 1 sequential one. You can choose one of them by setting the `worker` option. + +### Ractor + +```ruby +Pbt.assert(worker: :ractor) do + Pbt.property(Pbt.integer) do |n| # ... end end ``` -#### Process +#### Limitation + +Please note that Ractor support is an experimental feature of this gem. Due to Ractor's limitations, you may encounter some issues when using it. + +For example, you cannot access anything out of block. ```ruby -Pbt.assert(params: { concurrency_method: :process }) do - Pbt.property(Pbt.integer) do |number| +a = 1 + +Pbt.assert(worker: :ractor) do + Pbt.property(Pbt.integer) do |n| + # You cannot access `a` here because this block is executed in a Ractor and it doesn't allow implicit sharing of objects. + a + n # => Ractor::RemoteError (can not share object between ractors) + end +end +``` + +You cannot use any methods provided by test frameworks like `expect` or `assert` because they are not available in a Ractor. + +```ruby +it do + Pbt.assert(worker: :ractor) do + Pbt.property(Pbt.integer) do |n| + # This is not possible because `self` if a Ractor here. + expect(n).to be_an(Integer) # => Ractor::RemoteError (cause by NoMethodError for `expect` or `be_an`) + end + end +end +``` + +### Process + +```ruby +Pbt.assert(worker: :process) do + Pbt.property(Pbt.integer) do |n| # ... end end ``` -#### Thread +### Thread ```ruby -Pbt.assert(params: { concurrency_method: :thread }) do - Pbt.property(Pbt.integer) do |number| +Pbt.assert(worker: :thread) do + Pbt.property(Pbt.integer) do |n| # ... end end ``` -#### None +### None ```ruby -Pbt.assert(params: { concurrency_method: :none }) do - Pbt.property(Pbt.integer) do |number| +Pbt.assert(worker: :none) do + Pbt.property(Pbt.integer) do |n| # ... end end @@ -102,29 +252,40 @@ end ## TODOs -- [x] Enable to combine arbitraries (e.g. `Pbt.array(Pbt.integer)`) +Once this project finishes the following, we will release v1.0.0. + +- [x] Implement basic primitive arbitraries +- [x] Implement composite arbitraries - [x] Support shrinking -- [x] Implement basic arbitraries - - https://proper-testing.github.io/apidocs/ - - https://fast-check.dev/docs/core-blocks/arbitraries/ - [x] Support multiple concurrency methods - [x] Ractor - [x] Process - [x] Thread - [x] None (Run tests sequentially) +- [x] Documentation + - [x] Add better examples + - [x] Arbitrary usage + - [x] Configuration - [ ] Rich report like verbose mode -- [ ] Allow to use assertions provided by RSpec etc. if possible - - It'd be so hard to pass assertions like `expect`, `assert` to a Ractor. But it's worth trying at least for `process`, `thread` concurrency methods. -- [ ] Documentation - - [ ] Add better examples - - [ ] Arbitrary usage - - [ ] Configuration +- [ ] Allow to use expectations and matchers provided by test framework in Ractor if possible. + - It'd be so hard to pass assertions like `expect`, `assert` to a Ractor. +- [ ] Benchmark +- [ ] More parallelism or faster execution if possible ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +### Setup + +```shell +bin/setup +bundle exec rake # Run tests and lint at once +``` -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). +### Test + +```shell +bundle exec rspec +``` ### Lint @@ -134,12 +295,24 @@ bundle exec rake standard:fix ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/ohbarye/pbt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pbt/blob/master/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/ohbarye/pbt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ohbarye/pbt/blob/master/CODE_OF_CONDUCT.md). ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). +## Credits + +This project draws a lot of inspiration from other testing tools, namely + +- [fast-check](https://fast-check.dev/) +- [Loupe](https://github.com/vinistock/loupe) +- [RSpec](https://github.com/rspec/rspec) +- [Minitest](https://github.com/seattlerb/minitest) +- [Parallel](https://github.com/grosser/parallel) +- [PropCheck for Ruby](https://github.com/Qqwy/ruby-prop_check) +- [PropCheck for Elixir](https://github.com/alfert/propcheck) + ## Code of Conduct Everyone interacting in the Pbt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ohbarye/pbt/blob/master/CODE_OF_CONDUCT.md). diff --git a/lib/pbt.rb b/lib/pbt.rb index 0c61533..aacbef6 100644 --- a/lib/pbt.rb +++ b/lib/pbt.rb @@ -7,16 +7,37 @@ require_relative "pbt/check/configuration" module Pbt + # Represents a property-based test failure. class PropertyFailure < StandardError; end + # Represents an invalid configuration. class InvalidConfiguration < StandardError; end extend Arbitrary::ArbitraryMethods extend Check::RunnerMethods extend Check::ConfigurationMethods - # @param args [Array] - # @param kwargs [HashPbt::Arbitrary>] + # Create a property-based test with arbitraries. To run the test, pass the returned value to `Pbt.assert` method. + # Be aware that using both positional and keyword arguments is not supported. + # + # @example Basic usage + # Pbt.property(Pbt.integer) do |n| + # # your test code here + # end + # + # @example Use multiple arbitraries + # Pbt.property(Pbt.string, Pbt.symbol) do |str, sym| + # # your test code here + # end + # + # @example Use hash arbitraries + # Pbt.property(x: Pbt.integer, y: Pbt.integer) do |x, y| + # # your test code here + # end + # + # @param args [Array] Arbitraries to generate values. You can pass one or more arbitraries. + # @param kwargs [Hash] Arbitraries to generate values. You can pass arbitraries with keyword arguments. + # @param predicate [Proc] Test code that receives generated values and runs the test. # @return [Property] def self.property(*args, **kwargs, &predicate) arb = to_arbitrary(args, kwargs) @@ -30,6 +51,11 @@ class << self # If multiple arguments are given, wrap them by tuple arbitrary. # If keyword arguments are given, wrap them by fixed hash arbitrary. # Else, return the single arbitrary. + # + # @param args [Array] + # @param kwargs [Hash] + # @return [Arbitrary] + # @raise [ArgumentError] When both positional and keyword arguments are given def to_arbitrary(args, kwargs) if args == [] && kwargs != {} fixed_hash(kwargs) diff --git a/lib/pbt/arbitrary/arbitrary.rb b/lib/pbt/arbitrary/arbitrary.rb index fec10bd..93bd367 100644 --- a/lib/pbt/arbitrary/arbitrary.rb +++ b/lib/pbt/arbitrary/arbitrary.rb @@ -2,29 +2,54 @@ module Pbt module Arbitrary + # Abstract class for generating random values on type `T`. + # # @abstract class Arbitrary + # Generate a value of type `T`, based on the provided random number generator. + # # @abstract - # @param rng [Random] - # @return [Object] + # @param rng [Random] Random number generator. + # @return [Object] Random value of type `T`. def generate(rng) raise NotImplementedError end + # Shrink a value of type `T`. + # Must never be called with possibly invalid values. + # # @abstract # @param current [Object] - # @return [Enumerator] + # @return [Enumerator] def shrink(current) raise NotImplementedError end - # @param mapper [Proc] a function to map the generated value. it's mainly used for #generate. - # @param unmapper [Proc] a function to unmap the generated value. it's used for #shrink. + # Create another arbitrary by applying `mapper` value by value. + # + # @example + # integer_generator = Pbt.integer + # num_str_generator = integer_arb.map(->(n){ n.to_s }, ->(s) {s.to_i}) + # + # @param mapper [Proc] Proc to map generated values. Mainly used for generation. + # @param unmapper [Proc] Proc to unmap generated values. Used for shrinking. + # @return [MapArbitrary] New arbitrary with mapped elements def map(mapper, unmapper) MapArbitrary.new(self, mapper, unmapper) end - # @param refinement [Proc] a function to filter the generated value and shrunken values. + # Create another arbitrary by filtering values against `refinement`. + # All the values produced by the resulting arbitrary satisfy `!!refinement(value) == true`. + # + # Be aware that using `filter` may reduce possible valid values and may impact the time required to generate a valid value. + # + # @example + # integer_generator = Pbt.integer + # even_integer_generator = integer_arb.filter { |x| x.even? } + # # or `integer_arb.filter(&:even?)` + # + # @param refinement [Proc] Predicate proc to test each produced element. Return true to keep the element, false otherwise. + # @return [FilterArbitrary] New arbitrary filtered using `refinement`. def filter(&refinement) FilterArbitrary.new(self, &refinement) end diff --git a/lib/pbt/arbitrary/arbitrary_methods.rb b/lib/pbt/arbitrary/arbitrary_methods.rb index c863f7e..25ef3f4 100644 --- a/lib/pbt/arbitrary/arbitrary_methods.rb +++ b/lib/pbt/arbitrary/arbitrary_methods.rb @@ -15,105 +15,201 @@ module Pbt module Arbitrary module ArbitraryMethods - # @param min [Integer] - # @param max [Integer] - def integer(min: nil, max: nil) + # For integers between min (included) and max (included). + # + # @param min [Integer] Lower limit for the generated integers (included). + # @param max [Integer] Upper limit for the generated integers (included). + # @return [Arbitrary] + def integer(min: -1000000, max: 1000000) IntegerArbitrary.new(min, max) end - # @param max [Integer] + # For natural numbers (non-negative integers) between 0 (included) and max (included). + # + # @param max [Integer] Upper limit for the generated integers (included). + # @return [Arbitrary] def nat(max: nil) integer(min: 0, max: max) end - # @param arbitrary [Arbitrary] - # @param min [Integer] - # @param max [Integer] - # @param empty [Boolean] - def array(arbitrary, min: 0, max: 10, empty: true) + # For arrays of values generated by `arb`. + # + # @param arb [Arbitrary] Arbitrary used to generate the values of the array. + # @param min [Integer] Lower limit of the generated array size. + # @param max [Integer] Upper limit of the generated array size. + # @param empty [Boolean] Whether the array can be empty or not. + # @return [Arbitrary] + def array(arb, min: 0, max: 10, empty: true) raise ArgumentError if min < 0 min = 1 if min.zero? && !empty - ArrayArbitrary.new(arbitrary, min, max) + + ArrayArbitrary.new(arb, min, max) end - # @param arbs [Array + # For tuples of values generated by `arbs`. + # + # @param arbs [Array>] Arbitraries used to generate the values of the tuple. + # @return [Arbitrary>] def tuple(*arbs) TupleArbitrary.new(*arbs) end - # @param hash [HashPbt::Arbitrary>] + # For fixed hashes of values generated by `hash`. + # + # @example + # arb = Pbt.fixed_hash(x: Pbt.integer, y: Pbt.integer) + # arb.generate(Random.new) # => {x: -450108, y: 42} + # + # @param hash [Hash>] Hash with any keys and arbitraries as values. + # @return [Arbitrary>] def fixed_hash(hash) FixedHashArbitrary.new(hash) end - # @param range [Range] + # Picks a random integer in the given range. + # + # @see Pbt.one_of + # @param range [Range] Range of integers to choose from. + # @return [Arbitrary] def choose(range) ChooseArbitrary.new(range) end - # @param choices [Array] + # Picks a random element from the given choices. + # The choices can be of any type. + # + # @see Pbt.one_of + # @param choices [Array] Array of choices. + # @return [Arbitrary] def one_of(*choices) OneOfArbitrary.new(choices) end - # One lowercase hexadecimal character + # For a lowercase hexadecimal character. + # + # @return [Arbitrary] def hexa one_of(*HEXA_CHARS) end + # For lowercase hexadecimal stings. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def hexa_string(**kwargs) array(hexa, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end - # Generates a single unicode character (including printable and non-printable). + # For a single unicode character (including printable and non-printable). + # + # @return [Arbitrary] def char choose(CHAR_RANGE).map(CHAR_MAPPER, CHAR_UNMAPPER) end + # For an alphanumeric character. + # + # @return [Arbitrary] def alphanumeric_char one_of(*ALPHANUMERIC_CHARS) end + # For alphanumeric strings. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def alphanumeric_string(**kwargs) array(alphanumeric_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end + # For an ascii character. + # + # @return [Arbitrary] def ascii_char one_of(*ASCII_CHARS) end + # For ascii strings. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def ascii_string(**kwargs) array(ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end + # For a printable ascii character. + # + # @return [Arbitrary] def printable_ascii_char one_of(*PRINTABLE_ASCII_CHARS) end + # For printable ascii strings. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def printable_ascii_string(**kwargs) array(printable_ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end + # For a printable character. + # + # @return [Arbitrary] def printable_char one_of(*PRINTABLE_CHARS) end + # For printable strings. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def printable_string(**kwargs) array(printable_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end + # For symbols. + # + # @see Pbt.array + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary] def symbol(**kwargs) array(one_of(*SYMBOL_SAFE_CHARS), empty: false, **kwargs).map(SYMBOL_MAPPER, SYMBOL_UNMAPPER) end + # For floats. + # + # @return [Arbitrary] def float tuple(integer, integer).map(FLOAT_MAPPER, FLOAT_UNMAPPER) end - def set(arbitrary, min: 0, max: nil, empty: true) - array(arbitrary, min: min, max: max, empty: empty).map(SET_MAPPER, SET_UNMAPPER) + # For symbols. + # + # @param arb [Arbitrary] Arbitrary used to generate the values of the array. + # @param min [Integer] Lower limit of the generated set size. + # @param max [Integer] Upper limit of the generated set size. + # @param empty [Boolean] Whether the array can be empty or not. + # @return [Arbitrary] + def set(arb, min: 0, max: 10, empty: true) + array(arb, min: min, max: max, empty: empty).map(SET_MAPPER, SET_UNMAPPER) end + # For hashes of any keys and values. + # If you want to call `Object#hash` for `Pbt`, call this method without arguments. + # + # @example + # hash_generator = Pbt.hash(Pbt.symbol, Pbt.integer) + # hash_generator.generate(Random.new) # => {:buo=>466214, :cwftzvglq=>331431, :wweccnzg=>-848867} + # + # @see Pbt.array + # @param args [Array>] Arbitraries to generate Hash. First one is for key and second is for value. + # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information. + # @return [Arbitrary>] def hash(*args, **kwargs) if args.size == 2 key_arbitrary, value_arbitrary = args @@ -123,41 +219,86 @@ def hash(*args, **kwargs) end end + # For booleans. + # + # @return [Arbitrary] def boolean one_of(true, false) end + # For any constant values. + # It's useful when you want to use a constant value that behaves like an arbitrary. + # + # @example + # Pbt.constant(42).generate(Random.new) # => 42 + # # @param val [Object] + # @return [Arbitrary] def constant(val) ConstantArbitrary.new(val) end + # For nil. + # + # @return [Arbitrary] def nil constant(nil) end + # For dates between `base_date + past_offset_days` and `base_date + future_offset_days`. + # + # @param base_date [Date] Base date for the generated dates. + # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years). + # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years). + # @return [Arbitrary] def date(base_date: Date.today, past_offset_days: -18250, future_offset_days: 18250) offset_arb = integer(min: past_offset_days, max: future_offset_days) offset_arb.map(DATE_MAPPER.call(base_date), DATE_UNMAPPER.call(base_date)) end + # For past dates between `base_date - past_offset_days` and `base_date`. + # + # @param base_date [Date] Base date for the generated dates. + # @param past_offset_days [Integer] Offset days for the past. Default is -18250 (about 50 years). + # @return [Arbitrary] def past_date(base_date: Date.today, past_offset_days: -18250) date(base_date: base_date, past_offset_days: past_offset_days, future_offset_days: 0) end + # For future dates between `base_date` and `base_date - future_offset_days`. + # + # @param base_date [Date] Base date for the generated dates. + # @param future_offset_days [Integer] Offset days for the future. Default is 18250 (about 50 years). + # @return [Arbitrary] def future_date(base_date: Date.today, future_offset_days: 18250) date(base_date: base_date, past_offset_days: 0, future_offset_days: future_offset_days) end + # For times between `base_time + past_offset_seconds` and `base_time + future_offset_seconds`. + # + # @param base_time [Date] Base time for the generated times. + # @param past_offset_seconds [Integer] Offset seconds for the past. Default is -1576800000 (about 50 years). + # @param future_offset_seconds [Integer] Offset seconds for the future. Default is 1576800000 (about 50 years). + # @return [Arbitrary