pbt 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +1 -1
- data/CHANGELOG.md +17 -2
- data/README.md +282 -37
- data/lib/pbt/arbitrary/arbitrary.rb +58 -0
- data/lib/pbt/arbitrary/arbitrary_methods.rb +307 -0
- data/lib/pbt/arbitrary/array_arbitrary.rb +55 -0
- data/lib/pbt/arbitrary/choose_arbitrary.rb +25 -0
- data/lib/pbt/arbitrary/constant.rb +37 -0
- data/lib/pbt/arbitrary/constant_arbitrary.rb +23 -0
- data/lib/pbt/arbitrary/filter_arbitrary.rb +36 -0
- data/lib/pbt/arbitrary/fixed_hash_arbitrary.rb +30 -0
- data/lib/pbt/arbitrary/integer_arbitrary.rb +40 -0
- data/lib/pbt/arbitrary/map_arbitrary.rb +31 -0
- data/lib/pbt/arbitrary/one_of_arbitrary.rb +29 -0
- data/lib/pbt/arbitrary/tuple_arbitrary.rb +32 -0
- data/lib/pbt/check/case.rb +8 -0
- data/lib/pbt/check/configuration.rb +58 -0
- data/lib/pbt/check/property.rb +50 -0
- data/lib/pbt/check/runner_iterator.rb +59 -0
- data/lib/pbt/check/runner_methods.rb +177 -0
- data/lib/pbt/check/tosser.rb +32 -0
- data/lib/pbt/reporter/run_details.rb +21 -0
- data/lib/pbt/reporter/run_details_reporter.rb +39 -0
- data/lib/pbt/reporter/run_execution.rb +81 -0
- data/lib/pbt/version.rb +1 -1
- data/lib/pbt.rb +65 -5
- metadata +23 -4
- data/lib/pbt/generator.rb +0 -15
- data/lib/pbt/runner.rb +0 -41
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pbt
|
4
|
+
module Check
|
5
|
+
# Configuration for Pbt.
|
6
|
+
Configuration = Struct.new(
|
7
|
+
:verbose,
|
8
|
+
:worker,
|
9
|
+
:num_runs,
|
10
|
+
:seed,
|
11
|
+
:thread_report_on_exception,
|
12
|
+
keyword_init: true
|
13
|
+
) do
|
14
|
+
# @param verbose [Boolean] Whether to print verbose output. Default is `false`.
|
15
|
+
# @param worker [Symbol] The concurrency method to use. :ractor`, `:thread`, `:process` and `:none` are supported. Default is `:ractor`.
|
16
|
+
# @param num_runs [Integer] The number of runs to perform. Default is `100`.
|
17
|
+
# @param seed [Integer] 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.
|
18
|
+
# @param thread_report_on_exception [Boolean] Whether to report exceptions in threads. It's useful to suppress error logs on Ractor that reports many errors. Default is `false`.
|
19
|
+
def initialize(
|
20
|
+
verbose: false,
|
21
|
+
worker: :ractor,
|
22
|
+
num_runs: 100,
|
23
|
+
seed: Random.new.seed,
|
24
|
+
thread_report_on_exception: false
|
25
|
+
)
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module ConfigurationMethods
|
31
|
+
# Return the current configuration.
|
32
|
+
# If you modify the configuration, it will affect all future property-based tests.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# config = Pbt.configuration
|
36
|
+
# config.num_runs = 20
|
37
|
+
#
|
38
|
+
# @return [Configuration]
|
39
|
+
def configuration
|
40
|
+
@configuration ||= Configuration.new
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the current configuration.
|
44
|
+
# If you modify the configuration, it will affect all future property-based tests.
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# Pbt.configure do |config|
|
48
|
+
# config.num_runs = 20
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @yield [configuration] The current configuration.
|
52
|
+
# @yieldparam configuration [Configuration]
|
53
|
+
def configure
|
54
|
+
yield(configuration)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pbt
|
4
|
+
module Check
|
5
|
+
# Represents a property to be tested.
|
6
|
+
# This class holds an arbitrary to generate values and a predicate to test them.
|
7
|
+
class Property
|
8
|
+
# @param arb [Array<Arbitrary>]
|
9
|
+
# @param predicate [Proc] Predicate proc to test the generated values. Library users write this.
|
10
|
+
def initialize(arb, &predicate)
|
11
|
+
@arb = arb
|
12
|
+
@predicate = predicate
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate a next value to test.
|
16
|
+
#
|
17
|
+
# @param rng [Random] Random number generator.
|
18
|
+
# @return [Object]
|
19
|
+
def generate(rng)
|
20
|
+
@arb.generate(rng)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Shrink the `val` to a smaller one.
|
24
|
+
# This is used to find the smallest failing case after a failure.
|
25
|
+
#
|
26
|
+
# @param val [Object]
|
27
|
+
# @return [Enumerator<Object>]
|
28
|
+
def shrink(val)
|
29
|
+
@arb.shrink(val)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Run the predicate with the generated `val`.
|
33
|
+
#
|
34
|
+
# @param val [Object]
|
35
|
+
# @return [void]
|
36
|
+
def run(val)
|
37
|
+
@predicate.call(val)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Run the predicate with the generated `val` in a Ractor.
|
41
|
+
# This is used only for parallel testing with Ractors.
|
42
|
+
#
|
43
|
+
# @param val [Object]
|
44
|
+
# @return [Ractor]
|
45
|
+
def run_in_ractor(val)
|
46
|
+
Ractor.new(val, &@predicate)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
require "pbt/reporter/run_execution"
|
5
|
+
|
6
|
+
module Pbt
|
7
|
+
module Check
|
8
|
+
# This class is an iterator of the generated/shrunken values.
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
# @!attribute [r] run_execution [Reporter::RunExecution]
|
12
|
+
class RunnerIterator < DelegateClass(Array)
|
13
|
+
attr_reader :run_execution
|
14
|
+
|
15
|
+
# @param source_values [Enumerator] Enumerator of generated values by arbitrary. This is used to determine initial N (=`num_runs`) cases.
|
16
|
+
# @param property [Property] Property to test. This is used to shrink the failed case.
|
17
|
+
# @param verbose [Boolean] Controls the verbosity of the output.
|
18
|
+
def initialize(source_values, property, verbose)
|
19
|
+
@run_execution = Reporter::RunExecution.new(verbose)
|
20
|
+
@property = property
|
21
|
+
@next_values = source_values
|
22
|
+
enumerator = Enumerator.new do |y|
|
23
|
+
loop do
|
24
|
+
y.yield @next_values.next
|
25
|
+
end
|
26
|
+
end
|
27
|
+
super(enumerator) # delegate `#each` and etc. to enumerator
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check if there is a next value to test.
|
31
|
+
# If there is no next value, it returns false. Otherwise true.
|
32
|
+
#
|
33
|
+
# @return [Boolean]
|
34
|
+
def has_next?
|
35
|
+
@next_values.peek
|
36
|
+
true
|
37
|
+
rescue StopIteration
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
# Handle result of a test.
|
42
|
+
# When a test is successful, it records the success.
|
43
|
+
# When a test is failed, it records the failure and set up the next values to test with property#shrink.
|
44
|
+
#
|
45
|
+
# @param c [Case]
|
46
|
+
# @return [void]
|
47
|
+
def handle_result(c)
|
48
|
+
if c.exception
|
49
|
+
# failed run
|
50
|
+
@run_execution.record_failure(c)
|
51
|
+
@next_values = @property.shrink(c.val)
|
52
|
+
else
|
53
|
+
# successful run
|
54
|
+
@run_execution.record_success
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pbt/check/runner_iterator"
|
4
|
+
require "pbt/check/tosser"
|
5
|
+
require "pbt/check/case"
|
6
|
+
require "pbt/reporter/run_details_reporter"
|
7
|
+
|
8
|
+
module Pbt
|
9
|
+
module Check
|
10
|
+
# Module to be
|
11
|
+
module RunnerMethods
|
12
|
+
include Check::Tosser
|
13
|
+
|
14
|
+
# Run a property based test and report its result.
|
15
|
+
#
|
16
|
+
# @see Check::Configuration
|
17
|
+
# @param options [Hash] Optional parameters to customize the execution.
|
18
|
+
# @param property [Proc] Proc that returns Property instance.
|
19
|
+
# @return [void]
|
20
|
+
# @raise [PropertyFailure]
|
21
|
+
def assert(**options, &property)
|
22
|
+
out = check(**options, &property)
|
23
|
+
Reporter::RunDetailsReporter.new(out).report_run_details
|
24
|
+
end
|
25
|
+
|
26
|
+
# Run a property based test and return its result.
|
27
|
+
# This doesn't throw contrary to `assert`.
|
28
|
+
# Use `assert` unless you want to handle the result.
|
29
|
+
#
|
30
|
+
# @see RunnerMethods#assert
|
31
|
+
# @see Check::Configuration
|
32
|
+
# @param options [Hash] Optional parameters to customize the execution.
|
33
|
+
# @param property [Proc] Proc that returns Property instance.
|
34
|
+
# @return [RunDetails]
|
35
|
+
def check(**options, &property)
|
36
|
+
property = property.call
|
37
|
+
config = Pbt.configuration.to_h.merge(options.to_h)
|
38
|
+
|
39
|
+
initial_values = toss(property, config[:seed])
|
40
|
+
source_values = Enumerator.new(config[:num_runs]) do |y|
|
41
|
+
config[:num_runs].times do
|
42
|
+
y.yield initial_values.next
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
suppress_exception_report_for_ractor(config) do
|
47
|
+
run_it(property, source_values, config).to_run_details(config)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# If using Ractor, so many exception reports happen in Ractor and a console gets too messy. Suppress them to avoid that.
|
54
|
+
#
|
55
|
+
# @param config [Hash] Configuration parameters.
|
56
|
+
# @param block [Proc]
|
57
|
+
def suppress_exception_report_for_ractor(config, &block)
|
58
|
+
if config[:worker] == :ractor
|
59
|
+
original_report_on_exception = Thread.report_on_exception
|
60
|
+
Thread.report_on_exception = config[:thread_report_on_exception]
|
61
|
+
end
|
62
|
+
|
63
|
+
yield
|
64
|
+
ensure
|
65
|
+
Thread.report_on_exception = original_report_on_exception if config[:worker] == :ractor
|
66
|
+
end
|
67
|
+
|
68
|
+
# Run the property test for each value.
|
69
|
+
#
|
70
|
+
# @param property [Proc] Property to test.
|
71
|
+
# @param source_values [Enumerator] Enumerator of values to test.
|
72
|
+
# @param config [Hash] Configuration parameters.
|
73
|
+
# @return [RunExecution] Result of the test.
|
74
|
+
def run_it(property, source_values, config)
|
75
|
+
runner = Check::RunnerIterator.new(source_values, property, config[:verbose])
|
76
|
+
while runner.has_next?
|
77
|
+
case config[:worker]
|
78
|
+
in :ractor
|
79
|
+
run_it_in_ractors(property, runner)
|
80
|
+
in :process
|
81
|
+
run_it_in_processes(property, runner)
|
82
|
+
in :thread
|
83
|
+
run_it_in_threads(property, runner)
|
84
|
+
in :none
|
85
|
+
run_it_in_sequential(property, runner)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
runner.run_execution
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param property [Proc]
|
92
|
+
# @param runner [RunnerIterator]
|
93
|
+
# @return [void]
|
94
|
+
def run_it_in_ractors(property, runner)
|
95
|
+
runner.map.with_index { |val, index|
|
96
|
+
Case.new(val:, index:, ractor: property.run_in_ractor(val))
|
97
|
+
}.each do |c|
|
98
|
+
c.ractor.take
|
99
|
+
runner.handle_result(c)
|
100
|
+
rescue => e
|
101
|
+
c.exception = e.cause # Ractor error is wrapped in a Ractor::RemoteError. We need to get the cause.
|
102
|
+
runner.handle_result(c)
|
103
|
+
break # Ignore the rest of the cases. Just pick up the first failure.
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# @param property [Proc]
|
108
|
+
# @param runner [RunnerIterator]
|
109
|
+
# @return [void]
|
110
|
+
def run_it_in_threads(property, runner)
|
111
|
+
require_parallel
|
112
|
+
|
113
|
+
Parallel.map_with_index(runner, in_threads: Parallel.processor_count) do |val, index|
|
114
|
+
Case.new(val:, index:).tap do |c|
|
115
|
+
property.run(val)
|
116
|
+
rescue => e
|
117
|
+
c.exception = e
|
118
|
+
# It's possible to break this loop here by raising `Parallel::Break`.
|
119
|
+
# But if it raises, we cannot fetch all cases' result. So this loop continues until the end.
|
120
|
+
end
|
121
|
+
end.each do |c|
|
122
|
+
runner.handle_result(c)
|
123
|
+
break if c.exception
|
124
|
+
# Ignore the rest of the cases. Just pick up the first failure.
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param property [Proc]
|
129
|
+
# @param runner [RunnerIterator]
|
130
|
+
# @return [void]
|
131
|
+
def run_it_in_processes(property, runner)
|
132
|
+
require_parallel
|
133
|
+
|
134
|
+
Parallel.map_with_index(runner, in_processes: Parallel.processor_count) do |val, index|
|
135
|
+
Case.new(val:, index:).tap do |c|
|
136
|
+
property.run(val)
|
137
|
+
rescue => e
|
138
|
+
c.exception = e
|
139
|
+
# It's possible to break this loop here by raising `Parallel::Break`.
|
140
|
+
# But if it raises, we cannot fetch all cases' result. So this loop continues until the end.
|
141
|
+
end
|
142
|
+
end.each do |c|
|
143
|
+
runner.handle_result(c)
|
144
|
+
break if c.exception
|
145
|
+
# Ignore the rest of the cases. Just pick up the first failure.
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @param property [Proc]
|
150
|
+
# @param runner [RunnerIterator]
|
151
|
+
# @return [void]
|
152
|
+
def run_it_in_sequential(property, runner)
|
153
|
+
runner.each_with_index do |val, index|
|
154
|
+
c = Case.new(val:, index:)
|
155
|
+
begin
|
156
|
+
property.run(val)
|
157
|
+
runner.handle_result(c)
|
158
|
+
rescue => e
|
159
|
+
c.exception = e
|
160
|
+
runner.handle_result(c)
|
161
|
+
break # Ignore the rest of the cases. Just pick up the first failure.
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Load Parallel gem. If it's not installed, raise an error.
|
167
|
+
# @see https://github.com/grosser/parallel
|
168
|
+
# @raise [InvalidConfiguration]
|
169
|
+
def require_parallel
|
170
|
+
require "parallel"
|
171
|
+
rescue LoadError
|
172
|
+
raise InvalidConfiguration,
|
173
|
+
"Parallel gem (https://github.com/grosser/parallel) is required to use worker `:process` or `:thread`. Please add `gem 'parallel'` to your Gemfile."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pbt
|
4
|
+
module Check
|
5
|
+
# Module to be included in classes that need to generate values to test.
|
6
|
+
module Tosser
|
7
|
+
# Generate values.
|
8
|
+
#
|
9
|
+
# @param arb [Arbitrary] Arbitrary to generate a value.
|
10
|
+
# @param seed [Integer] Random number generator's seed.
|
11
|
+
# @return [Enumerator]
|
12
|
+
def toss(arb, seed)
|
13
|
+
Enumerator.new do |enum|
|
14
|
+
rng = Random.new(seed)
|
15
|
+
loop do
|
16
|
+
enum.yield toss_next(arb, rng)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Generate next value.
|
24
|
+
#
|
25
|
+
# @param arb [Arbitrary] Arbitrary to generate a value.
|
26
|
+
# @param rng [Random] Random number generator.
|
27
|
+
def toss_next(arb, rng)
|
28
|
+
arb.generate(rng)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pbt
|
4
|
+
module Reporter
|
5
|
+
# Details of a single run of a property test.
|
6
|
+
RunDetails = Struct.new(
|
7
|
+
:failed,
|
8
|
+
:num_runs,
|
9
|
+
:num_shrinks,
|
10
|
+
:seed,
|
11
|
+
:counterexample,
|
12
|
+
:counterexample_path,
|
13
|
+
:error_message,
|
14
|
+
:error_instance,
|
15
|
+
:failures,
|
16
|
+
:verbose,
|
17
|
+
:run_configuration,
|
18
|
+
keyword_init: true
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pbt
|
4
|
+
module Reporter
|
5
|
+
# Reporter for the run details of a property test.
|
6
|
+
class RunDetailsReporter
|
7
|
+
# @param run_details [Pbt::Reporter::RunExecution]
|
8
|
+
def initialize(run_details)
|
9
|
+
@run_details = run_details
|
10
|
+
end
|
11
|
+
|
12
|
+
# Report the run details of a property test.
|
13
|
+
# If the property test failed, raise a PropertyFailure.
|
14
|
+
#
|
15
|
+
# @raise [PropertyFailure]
|
16
|
+
def report_run_details
|
17
|
+
if @run_details.failed
|
18
|
+
message = []
|
19
|
+
|
20
|
+
message << <<~EOS
|
21
|
+
Property failed after #{@run_details.num_runs} test(s)
|
22
|
+
{ seed: #{@run_details.seed} }
|
23
|
+
Counterexample: #{@run_details.counterexample}
|
24
|
+
Shrunk #{@run_details.num_shrinks} time(s)
|
25
|
+
Got #{@run_details.error_instance.class}: #{@run_details.error_message}
|
26
|
+
EOS
|
27
|
+
|
28
|
+
if @run_details.verbose
|
29
|
+
message << " \n#{@run_details.error_instance.backtrace_locations.join("\n ")}"
|
30
|
+
message << "\nEncountered failures were:"
|
31
|
+
message << @run_details.failures.map { |f| "- #{f.val}" }
|
32
|
+
end
|
33
|
+
|
34
|
+
raise PropertyFailure, message.join("\n") if message.size > 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pbt/reporter/run_details"
|
4
|
+
|
5
|
+
module Pbt
|
6
|
+
module Reporter
|
7
|
+
# Represents the result of a single run of a property test.
|
8
|
+
class RunExecution
|
9
|
+
# @param verbose [Boolean] Whether to print verbose output.
|
10
|
+
def initialize(verbose)
|
11
|
+
@verbose = verbose
|
12
|
+
@path_to_failure = []
|
13
|
+
@failure = nil
|
14
|
+
@failures = []
|
15
|
+
@num_successes = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# Record a failure in the run.
|
19
|
+
#
|
20
|
+
# @param c [Pbt::Check::Case]
|
21
|
+
def record_failure(c)
|
22
|
+
@path_to_failure << c.index
|
23
|
+
@failures << c
|
24
|
+
|
25
|
+
# value and failure can be updated through shrinking
|
26
|
+
@value = c.val
|
27
|
+
@failure = c.exception
|
28
|
+
end
|
29
|
+
|
30
|
+
# Record a successful run.
|
31
|
+
#
|
32
|
+
# @return [void]
|
33
|
+
def record_success
|
34
|
+
@num_successes += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether the test was successful.
|
38
|
+
#
|
39
|
+
# @return [Boolean]
|
40
|
+
def success?
|
41
|
+
!@failure
|
42
|
+
end
|
43
|
+
|
44
|
+
# Convert execution to run details.
|
45
|
+
#
|
46
|
+
# @param config [Hash] Configuration parameters used for the run.
|
47
|
+
# @return [RunDetails] Details of the run.
|
48
|
+
def to_run_details(config)
|
49
|
+
if success?
|
50
|
+
RunDetails.new(
|
51
|
+
failed: false,
|
52
|
+
num_runs: @num_successes,
|
53
|
+
num_shrinks: 0,
|
54
|
+
seed: config[:seed],
|
55
|
+
counterexample: nil,
|
56
|
+
counterexample_path: nil,
|
57
|
+
error_message: nil,
|
58
|
+
error_instance: nil,
|
59
|
+
failures: @failures,
|
60
|
+
verbose: @verbose,
|
61
|
+
run_configuration: config
|
62
|
+
)
|
63
|
+
else
|
64
|
+
RunDetails.new(
|
65
|
+
failed: true,
|
66
|
+
num_runs: @path_to_failure[0] + 1,
|
67
|
+
num_shrinks: @path_to_failure.size - 1,
|
68
|
+
seed: config[:seed],
|
69
|
+
counterexample: @value,
|
70
|
+
counterexample_path: @path_to_failure.join(":"),
|
71
|
+
error_message: @failure.message,
|
72
|
+
error_instance: @failure,
|
73
|
+
failures: @failures,
|
74
|
+
verbose: @verbose,
|
75
|
+
run_configuration: config
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/pbt/version.rb
CHANGED
data/lib/pbt.rb
CHANGED
@@ -1,13 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "pbt/version"
|
4
|
-
require_relative "pbt/
|
5
|
-
require_relative "pbt/
|
4
|
+
require_relative "pbt/arbitrary/arbitrary_methods"
|
5
|
+
require_relative "pbt/check/runner_methods"
|
6
|
+
require_relative "pbt/check/property"
|
7
|
+
require_relative "pbt/check/configuration"
|
6
8
|
|
7
9
|
module Pbt
|
8
|
-
|
10
|
+
# Represents a property-based test failure.
|
11
|
+
class PropertyFailure < StandardError; end
|
9
12
|
|
10
|
-
|
11
|
-
|
13
|
+
# Represents an invalid configuration.
|
14
|
+
class InvalidConfiguration < StandardError; end
|
15
|
+
|
16
|
+
extend Arbitrary::ArbitraryMethods
|
17
|
+
extend Check::RunnerMethods
|
18
|
+
extend Check::ConfigurationMethods
|
19
|
+
|
20
|
+
# Create a property-based test with arbitraries. To run the test, pass the returned value to `Pbt.assert` method.
|
21
|
+
# Be aware that using both positional and keyword arguments is not supported.
|
22
|
+
#
|
23
|
+
# @example Basic usage
|
24
|
+
# Pbt.property(Pbt.integer) do |n|
|
25
|
+
# # your test code here
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# @example Use multiple arbitraries
|
29
|
+
# Pbt.property(Pbt.string, Pbt.symbol) do |str, sym|
|
30
|
+
# # your test code here
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @example Use hash arbitraries
|
34
|
+
# Pbt.property(x: Pbt.integer, y: Pbt.integer) do |x, y|
|
35
|
+
# # your test code here
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# @param args [Array<Arbitrary>] Arbitraries to generate values. You can pass one or more arbitraries.
|
39
|
+
# @param kwargs [Hash<Symbol,Arbitrary>] Arbitraries to generate values. You can pass arbitraries with keyword arguments.
|
40
|
+
# @param predicate [Proc] Test code that receives generated values and runs the test.
|
41
|
+
# @return [Property]
|
42
|
+
def self.property(*args, **kwargs, &predicate)
|
43
|
+
arb = to_arbitrary(args, kwargs)
|
44
|
+
Check::Property.new(arb, &predicate)
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
private
|
49
|
+
|
50
|
+
# Convert arguments to suitable arbitrary.
|
51
|
+
# If multiple arguments are given, wrap them by tuple arbitrary.
|
52
|
+
# If keyword arguments are given, wrap them by fixed hash arbitrary.
|
53
|
+
# Else, return the single arbitrary.
|
54
|
+
#
|
55
|
+
# @param args [Array<Arbitrary>]
|
56
|
+
# @param kwargs [Hash<Symbol,Arbitrary>]
|
57
|
+
# @return [Arbitrary]
|
58
|
+
# @raise [ArgumentError] When both positional and keyword arguments are given
|
59
|
+
def to_arbitrary(args, kwargs)
|
60
|
+
if args == [] && kwargs != {}
|
61
|
+
fixed_hash(kwargs)
|
62
|
+
elsif args != [] && kwargs == {}
|
63
|
+
# wrap by tuple arbitrary so that property class doesn't have to take care of an array
|
64
|
+
(args.size == 1) ? args.first : tuple(*args)
|
65
|
+
else
|
66
|
+
raise ArgumentError, <<~MSG
|
67
|
+
It's not supported to use both positional and keyword arguments at the same time.
|
68
|
+
cf. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
|
69
|
+
MSG
|
70
|
+
end
|
71
|
+
end
|
12
72
|
end
|
13
73
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pbt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ohbarye
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -25,8 +25,27 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- Rakefile
|
27
27
|
- lib/pbt.rb
|
28
|
-
- lib/pbt/
|
29
|
-
- lib/pbt/
|
28
|
+
- lib/pbt/arbitrary/arbitrary.rb
|
29
|
+
- lib/pbt/arbitrary/arbitrary_methods.rb
|
30
|
+
- lib/pbt/arbitrary/array_arbitrary.rb
|
31
|
+
- lib/pbt/arbitrary/choose_arbitrary.rb
|
32
|
+
- lib/pbt/arbitrary/constant.rb
|
33
|
+
- lib/pbt/arbitrary/constant_arbitrary.rb
|
34
|
+
- lib/pbt/arbitrary/filter_arbitrary.rb
|
35
|
+
- lib/pbt/arbitrary/fixed_hash_arbitrary.rb
|
36
|
+
- lib/pbt/arbitrary/integer_arbitrary.rb
|
37
|
+
- lib/pbt/arbitrary/map_arbitrary.rb
|
38
|
+
- lib/pbt/arbitrary/one_of_arbitrary.rb
|
39
|
+
- lib/pbt/arbitrary/tuple_arbitrary.rb
|
40
|
+
- lib/pbt/check/case.rb
|
41
|
+
- lib/pbt/check/configuration.rb
|
42
|
+
- lib/pbt/check/property.rb
|
43
|
+
- lib/pbt/check/runner_iterator.rb
|
44
|
+
- lib/pbt/check/runner_methods.rb
|
45
|
+
- lib/pbt/check/tosser.rb
|
46
|
+
- lib/pbt/reporter/run_details.rb
|
47
|
+
- lib/pbt/reporter/run_details_reporter.rb
|
48
|
+
- lib/pbt/reporter/run_execution.rb
|
30
49
|
- lib/pbt/version.rb
|
31
50
|
- sig/pbt.rbs
|
32
51
|
homepage: https://github.com/ohbarye/pbt
|
data/lib/pbt/generator.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pbt
|
4
|
-
module Generator
|
5
|
-
def self.integer(low = nil, high = nil)
|
6
|
-
rng = Random.new
|
7
|
-
size = 100
|
8
|
-
if low && high
|
9
|
-
-> { rng.rand(low..high) }
|
10
|
-
else
|
11
|
-
-> { rng.rand(-size..size) }
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|