From e0eb0ee5940125fd786bed3dce4e12e082365917 Mon Sep 17 00:00:00 2001 From: ohbarye Date: Wed, 3 Apr 2024 00:32:48 +0900 Subject: [PATCH 1/2] Refactor some arbitraries as MapArbitrary --- lib/pbt/arbitrary/arbitrary.rb | 28 ++++++++++++++ lib/pbt/arbitrary/arbitrary_methods.rb | 14 +++---- lib/pbt/arbitrary/array_arbitrary.rb | 2 +- lib/pbt/arbitrary/char_arbitrary.rb | 36 ------------------ lib/pbt/arbitrary/choose_arbitrary.rb | 2 +- lib/pbt/arbitrary/constant.rb | 5 +++ lib/pbt/arbitrary/fixed_hash_arbitrary.rb | 2 +- lib/pbt/arbitrary/integer_arbitrary.rb | 2 +- .../{string_arbitrary.rb => map_arbitrary.rb} | 22 ++++------- lib/pbt/arbitrary/one_of_arbitrary.rb | 2 +- lib/pbt/arbitrary/tuple_arbitrary.rb | 2 +- spec/pbt/arbitrary/arbitrary_methods_spec.rb | 21 +++++++++++ spec/pbt/arbitrary/char_arbitrary_spec.rb | 24 ------------ spec/pbt/arbitrary/map_arbitrary_spec.rb | 37 +++++++++++++++++++ 14 files changed, 111 insertions(+), 88 deletions(-) create mode 100644 lib/pbt/arbitrary/arbitrary.rb delete mode 100644 lib/pbt/arbitrary/char_arbitrary.rb rename lib/pbt/arbitrary/{string_arbitrary.rb => map_arbitrary.rb} (54%) delete mode 100644 spec/pbt/arbitrary/char_arbitrary_spec.rb create mode 100644 spec/pbt/arbitrary/map_arbitrary_spec.rb diff --git a/lib/pbt/arbitrary/arbitrary.rb b/lib/pbt/arbitrary/arbitrary.rb new file mode 100644 index 0000000..acdc509 --- /dev/null +++ b/lib/pbt/arbitrary/arbitrary.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Pbt + module Arbitrary + # @abstract + class Arbitrary + # @abstract + # @param rng [Random] + # @return [Object] + def generate(rng) + raise NotImplementedError + end + + # @abstract + # @param current [Object] + # @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. + def map(mapper, unmapper) + MapArbitrary.new(self, mapper, unmapper) + end + end + end +end diff --git a/lib/pbt/arbitrary/arbitrary_methods.rb b/lib/pbt/arbitrary/arbitrary_methods.rb index 11743cd..8f2010b 100644 --- a/lib/pbt/arbitrary/arbitrary_methods.rb +++ b/lib/pbt/arbitrary/arbitrary_methods.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true +require "pbt/arbitrary/arbitrary" require "pbt/arbitrary/constant" require "pbt/arbitrary/array_arbitrary" -require "pbt/arbitrary/char_arbitrary" require "pbt/arbitrary/integer_arbitrary" require "pbt/arbitrary/tuple_arbitrary" require "pbt/arbitrary/fixed_hash_arbitrary" require "pbt/arbitrary/choose_arbitrary" require "pbt/arbitrary/one_of_arbitrary" -require "pbt/arbitrary/string_arbitrary" +require "pbt/arbitrary/map_arbitrary" module Pbt module Arbitrary @@ -56,7 +56,7 @@ def one_of(*choices) # Generates a single unicode character (including printable and non-printable). def char - CharArbitrary.new + choose(0..0x10FFFF).map(CHAR_MAPPER, CHAR_UNMAPPER) end def alphanumeric_char @@ -64,7 +64,7 @@ def alphanumeric_char end def alphanumeric_string(**kwargs) - StringArbitrary.new(array(alphanumeric_char, **kwargs)) + array(alphanumeric_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end def ascii_char @@ -72,7 +72,7 @@ def ascii_char end def ascii_string(**kwargs) - StringArbitrary.new(array(ascii_char, **kwargs)) + array(ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end def printable_ascii_char @@ -80,7 +80,7 @@ def printable_ascii_char end def printable_ascii_string(**kwargs) - StringArbitrary.new(array(printable_ascii_char, **kwargs)) + array(printable_ascii_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end def printable_char @@ -88,7 +88,7 @@ def printable_char end def printable_string(**kwargs) - StringArbitrary.new(array(printable_char, **kwargs)) + array(printable_char, **kwargs).map(STRING_MAPPER, STRING_UNMAPPER) end end end diff --git a/lib/pbt/arbitrary/array_arbitrary.rb b/lib/pbt/arbitrary/array_arbitrary.rb index a23d8d9..4cede46 100644 --- a/lib/pbt/arbitrary/array_arbitrary.rb +++ b/lib/pbt/arbitrary/array_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class ArrayArbitrary + class ArrayArbitrary < Arbitrary DEFAULT_MAX_SIZE = 10 # @param min_length [Integer] diff --git a/lib/pbt/arbitrary/char_arbitrary.rb b/lib/pbt/arbitrary/char_arbitrary.rb deleted file mode 100644 index 7f64a2a..0000000 --- a/lib/pbt/arbitrary/char_arbitrary.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Pbt - module Arbitrary - class CharArbitrary - def initialize - @arb = ChooseArbitrary.new(0..0x10FFFF) - end - - # @return [String] - def generate(rng) - map(@arb.generate(rng)) - end - - # Shrinks towards characters with lower codepoints, e.g. ASCII - # @return [Enumerator] - def shrink(current) - Enumerator.new do |y| - @arb.shrink(unmap(current)).each do |v| - y << map(v) - end - end - end - - private - - def map(v) - [v].pack("U") - end - - def unmap(v) - v.unpack1("U") - end - end - end -end diff --git a/lib/pbt/arbitrary/choose_arbitrary.rb b/lib/pbt/arbitrary/choose_arbitrary.rb index 93a3de4..9e297e9 100644 --- a/lib/pbt/arbitrary/choose_arbitrary.rb +++ b/lib/pbt/arbitrary/choose_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class ChooseArbitrary + class ChooseArbitrary < Arbitrary # @param range [Range] def initialize(range) @range = range diff --git a/lib/pbt/arbitrary/constant.rb b/lib/pbt/arbitrary/constant.rb index be04ab4..5e6c043 100644 --- a/lib/pbt/arbitrary/constant.rb +++ b/lib/pbt/arbitrary/constant.rb @@ -11,5 +11,10 @@ module Arbitrary *("\u{E000}".."\u{FFFD}"), *("\u{10000}".."\u{10FFFF}") ].freeze + + CHAR_MAPPER = ->(v) { [v].pack("U") } + CHAR_UNMAPPER = ->(v) { v.unpack1("U") } + STRING_MAPPER = ->(v) { v.join } + STRING_UNMAPPER = ->(v) { v.chars } end end diff --git a/lib/pbt/arbitrary/fixed_hash_arbitrary.rb b/lib/pbt/arbitrary/fixed_hash_arbitrary.rb index 0f337ff..4b8897f 100644 --- a/lib/pbt/arbitrary/fixed_hash_arbitrary.rb +++ b/lib/pbt/arbitrary/fixed_hash_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class FixedHashArbitrary + class FixedHashArbitrary < Arbitrary # @param hash [HashPbt::Arbitrary>] def initialize(hash) @keys = hash.keys diff --git a/lib/pbt/arbitrary/integer_arbitrary.rb b/lib/pbt/arbitrary/integer_arbitrary.rb index bff0527..230c976 100644 --- a/lib/pbt/arbitrary/integer_arbitrary.rb +++ b/lib/pbt/arbitrary/integer_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class IntegerArbitrary + class IntegerArbitrary < Arbitrary DEFAULT_TARGET = 0 DEFAULT_SIZE = 1000000 diff --git a/lib/pbt/arbitrary/string_arbitrary.rb b/lib/pbt/arbitrary/map_arbitrary.rb similarity index 54% rename from lib/pbt/arbitrary/string_arbitrary.rb rename to lib/pbt/arbitrary/map_arbitrary.rb index 5d5a18d..10e391c 100644 --- a/lib/pbt/arbitrary/string_arbitrary.rb +++ b/lib/pbt/arbitrary/map_arbitrary.rb @@ -2,35 +2,27 @@ module Pbt module Arbitrary - class StringArbitrary + class MapArbitrary < Arbitrary # @param arb [ArrayArbitrary] - def initialize(arb) + def initialize(arb, mapper, unmapper) @arb = arb + @mapper = mapper + @unmapper = unmapper end # @return [Array] def generate(rng) - map(@arb.generate(rng)) + @mapper.call(@arb.generate(rng)) end # @return [Enumerator] def shrink(current) Enumerator.new do |y| - @arb.shrink(unmap(current)).each do |v| - y.yield map(v) + @arb.shrink(@unmapper.call(current)).each do |v| + y.yield @mapper.call(v) end end end - - private - - def map(v) - v.join - end - - def unmap(v) - v.chars - end end end end diff --git a/lib/pbt/arbitrary/one_of_arbitrary.rb b/lib/pbt/arbitrary/one_of_arbitrary.rb index f9b1dec..75df9f9 100644 --- a/lib/pbt/arbitrary/one_of_arbitrary.rb +++ b/lib/pbt/arbitrary/one_of_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class OneOfArbitrary + class OneOfArbitrary < Arbitrary # @param choices [Array] def initialize(choices) @choices = choices diff --git a/lib/pbt/arbitrary/tuple_arbitrary.rb b/lib/pbt/arbitrary/tuple_arbitrary.rb index 685133d..fff4f30 100644 --- a/lib/pbt/arbitrary/tuple_arbitrary.rb +++ b/lib/pbt/arbitrary/tuple_arbitrary.rb @@ -2,7 +2,7 @@ module Pbt module Arbitrary - class TupleArbitrary + class TupleArbitrary < Arbitrary # @param arbs [Array] def initialize(*arbs) @arbs = arbs diff --git a/spec/pbt/arbitrary/arbitrary_methods_spec.rb b/spec/pbt/arbitrary/arbitrary_methods_spec.rb index 13c048b..bf251dc 100644 --- a/spec/pbt/arbitrary/arbitrary_methods_spec.rb +++ b/spec/pbt/arbitrary/arbitrary_methods_spec.rb @@ -1,6 +1,27 @@ # frozen_string_literal: true RSpec.describe Pbt::Arbitrary::ArbitraryMethods do + describe ".char" do + it "generates a character" do + val = Pbt.char.generate(Random.new) + expect(val).to be_a(String) + expect(val.size).to eq(1) + end + + describe "#shrink" do + it "returns an Enumerator" do + arb = Pbt.char + val = arb.generate(Random.new) + expect(arb.shrink(val)).to be_a(Enumerator) + end + + it "returns an Enumerator that iterates characters shrinking towards lower codepoint" do + arb = Pbt.char + expect(arb.shrink("z").to_a).to eq ["=", "\u001F", "\u0010", "\b", "\u0004", "\u0002", "\u0001", "\u0000"] + end + end + end + describe ".alphanumeric_char" do it "generates a character" do val = Pbt.alphanumeric_char.generate(Random.new) diff --git a/spec/pbt/arbitrary/char_arbitrary_spec.rb b/spec/pbt/arbitrary/char_arbitrary_spec.rb deleted file mode 100644 index 83d0ffd..0000000 --- a/spec/pbt/arbitrary/char_arbitrary_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Pbt::Arbitrary::CharArbitrary do - describe "#generate" do - it "generates an character" do - val = Pbt::Arbitrary::CharArbitrary.new.generate(Random.new) - expect(val).to be_a(String) - expect(val.size).to eq(1) - end - end - - describe "#shrink" do - it "returns an Enumerator" do - arb = Pbt::Arbitrary::CharArbitrary.new - val = arb.generate(Random.new) - expect(arb.shrink(val)).to be_a(Enumerator) - end - - it "returns an Enumerator that iterates characters shrinking towards lower codepoint" do - arb = Pbt::Arbitrary::CharArbitrary.new - expect(arb.shrink("z").to_a).to eq ["=", "\u001F", "\u0010", "\b", "\u0004", "\u0002", "\u0001", "\u0000"] - end - end -end diff --git a/spec/pbt/arbitrary/map_arbitrary_spec.rb b/spec/pbt/arbitrary/map_arbitrary_spec.rb new file mode 100644 index 0000000..d880c31 --- /dev/null +++ b/spec/pbt/arbitrary/map_arbitrary_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe Pbt::Arbitrary::MapArbitrary do + describe "#initialize" do + it do + arb = Pbt.integer.map(->(n) { n.to_s }, ->(n) { n.to_i }) + expect(arb).to be_a(Pbt::Arbitrary::MapArbitrary) + end + end + + describe "#generate" do + it "generates mapped values of given arbitrary" do + val = Pbt.integer.map(->(n) { n.to_s }, ->(n) { n.to_i }).generate(Random.new) + expect(val).to be_a(String) + end + end + + describe "#shrink" do + it "returns an Enumerator" do + arb = Pbt.integer.map(->(n) { n.to_s }, ->(n) { n.to_i }) + val = arb.generate(Random.new) + expect(arb.shrink(val)).to be_a(Enumerator) + end + + it "returns an Enumerator that iterates halved arrays" do + arb = Pbt.integer.map(->(n) { n.to_s }, ->(n) { n.to_i }) + expect(arb.shrink("50").to_a).to eq ["25", "13", "7", "4", "2", "1", "0"] + end + + context "when current value and target is same" do + it "returns an empty Enumerator" do + arb = Pbt.integer.map(->(n) { n.to_s }, ->(n) { n.to_i }) + expect(arb.shrink("0").to_a).to eq [] + end + end + end +end From f14182c545e73bf02c590cf933c0f10adf94d45b Mon Sep 17 00:00:00 2001 From: ohbarye Date: Wed, 3 Apr 2024 01:01:41 +0900 Subject: [PATCH 2/2] Implement FilterArbitrary --- lib/pbt/arbitrary/arbitrary.rb | 5 +++ lib/pbt/arbitrary/arbitrary_methods.rb | 1 + lib/pbt/arbitrary/filter_arbitrary.rb | 36 ++++++++++++++++++++ spec/pbt/arbitrary/filter_arbitrary_spec.rb | 37 +++++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 lib/pbt/arbitrary/filter_arbitrary.rb create mode 100644 spec/pbt/arbitrary/filter_arbitrary_spec.rb diff --git a/lib/pbt/arbitrary/arbitrary.rb b/lib/pbt/arbitrary/arbitrary.rb index acdc509..fec10bd 100644 --- a/lib/pbt/arbitrary/arbitrary.rb +++ b/lib/pbt/arbitrary/arbitrary.rb @@ -23,6 +23,11 @@ def shrink(current) def map(mapper, unmapper) MapArbitrary.new(self, mapper, unmapper) end + + # @param refinement [Proc] a function to filter the generated value and shrunken values. + def filter(&refinement) + FilterArbitrary.new(self, &refinement) + end end end end diff --git a/lib/pbt/arbitrary/arbitrary_methods.rb b/lib/pbt/arbitrary/arbitrary_methods.rb index 8f2010b..34f2595 100644 --- a/lib/pbt/arbitrary/arbitrary_methods.rb +++ b/lib/pbt/arbitrary/arbitrary_methods.rb @@ -9,6 +9,7 @@ require "pbt/arbitrary/choose_arbitrary" require "pbt/arbitrary/one_of_arbitrary" require "pbt/arbitrary/map_arbitrary" +require "pbt/arbitrary/filter_arbitrary" module Pbt module Arbitrary diff --git a/lib/pbt/arbitrary/filter_arbitrary.rb b/lib/pbt/arbitrary/filter_arbitrary.rb new file mode 100644 index 0000000..0a5b922 --- /dev/null +++ b/lib/pbt/arbitrary/filter_arbitrary.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Pbt + module Arbitrary + class FilterArbitrary < Arbitrary + # @param arb [ArrayArbitrary] + # @param refinement [Proc] a function to filter the generated value and shrunken values. + # + def initialize(arb, &refinement) + @arb = arb + @refinement = refinement + end + + # @return [Array] + def generate(rng) + loop do + val = @arb.generate(rng) + return val if @refinement.call(val) + end + end + + # @return [Enumerator] + def shrink(current) + Enumerator.new do |y| + @arb.shrink(current).each do |v| + if @refinement.call(v) + y.yield v + else + next + end + end + end + end + end + end +end diff --git a/spec/pbt/arbitrary/filter_arbitrary_spec.rb b/spec/pbt/arbitrary/filter_arbitrary_spec.rb new file mode 100644 index 0000000..29f7487 --- /dev/null +++ b/spec/pbt/arbitrary/filter_arbitrary_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe Pbt::Arbitrary::FilterArbitrary do + describe "#initialize" do + it do + arb = Pbt.integer.filter { |n| n % 2 == 0 } + expect(arb).to be_a(Pbt::Arbitrary::FilterArbitrary) + end + end + + describe "#generate" do + it "generates filtered values of given arbitrary" do + val = Pbt.integer.filter { |n| n % 2 == 0 }.generate(Random.new) + expect(val).to be_even + end + end + + describe "#shrink" do + it "returns an Enumerator" do + arb = Pbt.integer.filter { |n| n % 2 == 0 } + val = arb.generate(Random.new) + expect(arb.shrink(val)).to be_a(Enumerator) + end + + it "returns an Enumerator that iterates filtered values" do + arb = Pbt.integer.filter { |n| n % 2 == 0 } + expect(arb.shrink(50).to_a).to eq [4, 2, 0] + end + + context "when current value and target is same" do + it "returns an empty Enumerator" do + arb = Pbt.integer.filter { |n| n % 2 == 0 } + expect(arb.shrink(0).to_a).to eq [] + end + end + end +end