pbt 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|