prop_check 0.15.0 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/run_tests.yaml +31 -0
- data/CHANGELOG.md +7 -0
- data/README.md +26 -21
- data/lib/prop_check/generators.rb +70 -1
- data/lib/prop_check/helper.rb +0 -5
- data/lib/prop_check/property/configuration.rb +6 -1
- data/lib/prop_check/property.rb +63 -1
- data/lib/prop_check/version.rb +1 -1
- metadata +3 -4
- data/.travis.yml +0 -18
- data/lib/prop_check/helper/lazy_append.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e59cf670bcc8cf9720d6278e984925c50e75ca65023459174f8b511d6421fb71
|
4
|
+
data.tar.gz: '088e0eb8d2d301bc3c03cc63bf68196969e8653cad37ff0b9d5d10f9b417a1ae'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de5808bced970e0c474367bf125837ac0eefa4aa752acb31cd74e884fea858b98fb518e9fe15493c81f8238e5e0490f3cbc855eda3a9b687196ca7830daad0aa
|
7
|
+
data.tar.gz: 312957d1ab0f990985f43ae7e4b0793df583ff2d3d8bb95573c11fe3e4d9c5f964d91050fe9bafb98ceb92dd6790d2ffce04257b02b0c152d75d9b4772b7423a
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: Ruby RSpec tests
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
permissions:
|
10
|
+
contents: read
|
11
|
+
|
12
|
+
jobs:
|
13
|
+
test:
|
14
|
+
|
15
|
+
runs-on: ubuntu-latest
|
16
|
+
strategy:
|
17
|
+
matrix:
|
18
|
+
ruby-version: ['2.5', '2.6', '2.7', '3.0']
|
19
|
+
|
20
|
+
steps:
|
21
|
+
- uses: actions/checkout@v3
|
22
|
+
- name: Set up Ruby
|
23
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
24
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
25
|
+
# uses: ruby/setup-ruby@v1
|
26
|
+
uses: ruby/setup-ruby@0a29871fe2b0200a17a4497bae54fe5df0d973aa # v1.115.3
|
27
|
+
with:
|
28
|
+
ruby-version: ${{ matrix.ruby-version }}
|
29
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
30
|
+
- name: Run tests
|
31
|
+
run: bundle exec rake
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
- 0.17.0
|
2
|
+
- Features:
|
3
|
+
- Recursive generation using `PropCheck::Generators.tree`.
|
4
|
+
- 0.16.0
|
5
|
+
- Features:
|
6
|
+
- New option in `PropCheck::Property::Configuration` to resize all generators at once.
|
7
|
+
- Wrapper functions to modify this easily in `PropCheck::Property` called `#resize`, `#grow_fast`, `#grow_slowly`, `#grow_exponentially`, `#grow_quadratically`, `#grow_logarithmically`.
|
1
8
|
- 0.15.0
|
2
9
|
- Features:
|
3
10
|
- Generators for `Date`, `Time` and `DateTime`.
|
data/README.md
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
PropCheck allows you to do Property Testing in Ruby.
|
4
4
|
|
5
5
|
[![Gem](https://img.shields.io/gem/v/prop_check.svg)](https://rubygems.org/gems/prop_check)
|
6
|
-
[![
|
6
|
+
[![Ruby RSpec tests build status](https://github.com/Qqwy/ruby-prop_check/actions/workflows/run_tests.yaml/badge.svg)](https://github.com/Qqwy/ruby-prop_check/actions/workflows/run_tests.yaml)
|
7
7
|
[![Maintainability](https://api.codeclimate.com/v1/badges/71897f5e6193a5124a53/maintainability)](https://codeclimate.com/github/Qqwy/ruby-prop_check/maintainability)
|
8
|
-
[![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/
|
8
|
+
[![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/github/Qqwy/ruby-prop_check/master/)
|
9
9
|
|
10
10
|
It features:
|
11
11
|
|
@@ -61,11 +61,11 @@ Before releasing v1.0, we want to finish the following:
|
|
61
61
|
- [x] Before/after/around hooks to add setup/teardown logic to be called before/after/around each time a check is run with new data.
|
62
62
|
- [x] Possibility to resize generators.
|
63
63
|
- [x] `#instance` generator to allow the easy creation of generators for custom datatypes.
|
64
|
-
- [ ] A usage guide.
|
65
|
-
- [ ] A simple way to create recursive generators
|
66
64
|
- [x] Builtin generation of `Set`s
|
67
65
|
- [x] Builtin generation of `Date`s, `Time`s and `DateTime`s.
|
68
|
-
- [
|
66
|
+
- [x] Configuration option to resize all generators given to a particular Property instance.
|
67
|
+
- [x] A simple way to create recursive generators
|
68
|
+
- [ ] A usage guide.
|
69
69
|
|
70
70
|
## Nice-to-haves
|
71
71
|
|
@@ -100,9 +100,9 @@ The value(s) generated from the generator(s) passed to the `forall` will be give
|
|
100
100
|
Raise an exception from the block if there is a problem. If there is no problem, just return normally.
|
101
101
|
|
102
102
|
```ruby
|
103
|
-
|
103
|
+
G = PropCheck::Generators
|
104
104
|
# testing that Enumerable#sort sorts in ascending order
|
105
|
-
PropCheck.forall(array(integer)) do |numbers|
|
105
|
+
PropCheck.forall(G.array(G.integer)) do |numbers|
|
106
106
|
sorted_numbers = numbers.sort
|
107
107
|
|
108
108
|
# Check that no number is smaller than the previous number
|
@@ -124,8 +124,8 @@ end
|
|
124
124
|
```
|
125
125
|
```ruby
|
126
126
|
# And then in a test case:
|
127
|
-
|
128
|
-
PropCheck.forall(numbers: array(integer)) do |numbers:|
|
127
|
+
G = PropCheck::Generators
|
128
|
+
PropCheck.forall(numbers: G.array(G.integer)) do |numbers:|
|
129
129
|
result = naive_average(numbers)
|
130
130
|
unless result.is_a?(Integer) do
|
131
131
|
raise "Expected the average to be an integer!"
|
@@ -135,10 +135,10 @@ end
|
|
135
135
|
# Or if you e.g. are using RSpec:
|
136
136
|
describe "#naive_average" do
|
137
137
|
include PropCheck
|
138
|
-
|
138
|
+
G = PropCheck::Generators
|
139
139
|
|
140
140
|
it "returns an integer for any input" do
|
141
|
-
forall(numbers: array(integer)) do |numbers:|
|
141
|
+
forall(numbers: G.array(G.integer)) do |numbers:|
|
142
142
|
result = naive_average(numbers)
|
143
143
|
expect(result).to be_a(Integer)
|
144
144
|
end
|
@@ -208,11 +208,12 @@ It contains generators for:
|
|
208
208
|
- (any, only real-valued) floats,
|
209
209
|
- (any, printable only, alphanumeric only, etc) strings and symbols
|
210
210
|
- fixed-size arrays and hashes
|
211
|
-
- as well as varying-size arrays and
|
211
|
+
- as well as varying-size arrays, hashes and sets.
|
212
|
+
- dates, times, datetimes.
|
212
213
|
- and many more!
|
213
214
|
|
214
|
-
It is common to
|
215
|
-
|
215
|
+
It is common and recommended to set up a module alias by using `G = PropCheck::Generators` in e.g. your testing-suite files to be able to refer to all of them.
|
216
|
+
_(Earlier versions of the library recommended including the module instead. But this will make it very simple to accidentally shadow a generator with a local variable named `float` or `array` and similar.)_
|
216
217
|
|
217
218
|
### Writing Custom Generators
|
218
219
|
|
@@ -228,33 +229,37 @@ Always returns the given value. No shrinking.
|
|
228
229
|
|
229
230
|
Allows you to take the result of one generator and transform it into something else.
|
230
231
|
|
231
|
-
>>
|
232
|
-
=> "S"
|
232
|
+
>> G.choose(32..128).map(&:chr).sample(1, size: 10, Random.new(42))
|
233
|
+
=> ["S"]
|
233
234
|
|
234
235
|
#### Generator#bind
|
235
236
|
|
236
237
|
Allows you to create one or another generator conditionally on the output of another generator.
|
237
238
|
|
238
|
-
>>
|
239
|
-
=> [2, 79]
|
239
|
+
>> G.integer.bind { |a| G.integer.bind { |b| G.constant([a , b]) } }.sample(1, size: 100, rng: Random.new(42)
|
240
|
+
=> [[2, 79]]
|
241
|
+
|
242
|
+
This is an advanced feature. Often, you can use a combination of `Generators.tuple` and `Generator#map` instead:
|
240
243
|
|
244
|
+
>> G.tuple(integer, integer).sample(1, size: 100, rng: Random.new(42)
|
245
|
+
=> [[2, 79]]
|
241
246
|
|
242
247
|
#### Generators.one_of
|
243
248
|
|
244
249
|
Useful if you want to be able to generate a value to be one of multiple possibilities:
|
245
250
|
|
246
251
|
|
247
|
-
>>
|
252
|
+
>> G.one_of(G.constant(true), G.constant(false)).sample(5, size: 10, rng: Random.new(42))
|
248
253
|
=> [true, false, true, true, true]
|
249
254
|
|
250
|
-
(
|
255
|
+
(Note that for this example, you can also use `G.boolean`. The example happens to show how it is implemented under the hood.)
|
251
256
|
|
252
257
|
#### Generators.frequency
|
253
258
|
|
254
259
|
If `one_of` does not give you enough flexibility because you want some results to be more common than others,
|
255
260
|
you can use `Generators.frequency` which takes a hash of (integer_frequency => generator) keypairs.
|
256
261
|
|
257
|
-
>>
|
262
|
+
>> G.frequency(5 => G.integer, 1 => G.printable_ascii_char).sample(size: 10, rng: Random.new(42))
|
258
263
|
=> [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
|
259
264
|
|
260
265
|
#### Others
|
@@ -758,7 +758,6 @@ module PropCheck
|
|
758
758
|
offset_gen.map { |offset| Date.jd(epoch.jd + offset) }
|
759
759
|
else
|
760
760
|
offset_gen.with_config.map do |offset, config|
|
761
|
-
puts config.inspect
|
762
761
|
epoch = config.default_epoch.to_date
|
763
762
|
Date.jd(epoch.jd + offset)
|
764
763
|
end
|
@@ -873,5 +872,75 @@ module PropCheck
|
|
873
872
|
end
|
874
873
|
end
|
875
874
|
end
|
875
|
+
|
876
|
+
##
|
877
|
+
# Helper to build recursive generators
|
878
|
+
#
|
879
|
+
# Given a `leaf_generator`
|
880
|
+
# and a block which:
|
881
|
+
# - is given a generator that generates subtrees.
|
882
|
+
# - it should return the generator for intermediate tree nodes.
|
883
|
+
#
|
884
|
+
# This is best explained with an example.
|
885
|
+
# Say we want to generate a binary tree of integers.
|
886
|
+
#
|
887
|
+
# If we have a struct representing internal nodes:
|
888
|
+
# ```ruby
|
889
|
+
# Branch = Struct.new(:left, :right, keyword_init: true)
|
890
|
+
# ```
|
891
|
+
# we can generate trees like so:
|
892
|
+
# ```ruby
|
893
|
+
# Generators.tree(Generators.integer) do |subtree_gen|
|
894
|
+
# G.instance(Branch, left: subtree_gen, right: subtree_gen)
|
895
|
+
# end
|
896
|
+
# ```
|
897
|
+
#
|
898
|
+
# As another example, consider generating lists of integers:
|
899
|
+
#
|
900
|
+
# >> G = PropCheck::Generators
|
901
|
+
# >> G.tree(G.integer) {|child_gen| G.array(child_gen) }.sample(5, size: 37, rng: Random.new(42))
|
902
|
+
# => [[7, [2, 3], -10], [[-2], [-2, [3]], [[2, 3]]], [], [0, [-2, -3]], [[1], -19, [], [1, -1], [1], [-1, -1], [1]]]
|
903
|
+
#
|
904
|
+
# And finally, here is how one could create a simple generator for parsed JSON data:
|
905
|
+
#
|
906
|
+
# ```ruby`
|
907
|
+
# G = PropCheck::Generators
|
908
|
+
# def json
|
909
|
+
# G.tree(G.one_of(G.boolean, G.real_float, G.ascii_string)) do |json_gen|
|
910
|
+
# G.one_of(G.array(json_gen), G.hash_of(G.ascii_string, json_gen))
|
911
|
+
# end
|
912
|
+
# end
|
913
|
+
# ```
|
914
|
+
#
|
915
|
+
def tree(leaf_generator, &block)
|
916
|
+
# Implementation is based on
|
917
|
+
# https://hexdocs.pm/stream_data/StreamData.html#tree/2
|
918
|
+
Generator.new do |size:, rng:, **other_kwargs|
|
919
|
+
nodes_on_each_level = random_pseudofactors(size.pow(1.1).to_i, rng)
|
920
|
+
result = nodes_on_each_level.reduce(leaf_generator) do |subtree_generator, nodes_on_this_level|
|
921
|
+
frequency(1 => subtree_generator,
|
922
|
+
2 => block.call(subtree_generator).resize { |_size| nodes_on_this_level })
|
923
|
+
end
|
924
|
+
|
925
|
+
result.generate(size: size, rng: rng, **other_kwargs)
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
929
|
+
private def random_pseudofactors(size, rng)
|
930
|
+
return [size].to_enum if size < 2
|
931
|
+
|
932
|
+
Enumerator.new do |yielder|
|
933
|
+
loop do
|
934
|
+
factor = rng.rand(1..(Math.log2(size).to_i))
|
935
|
+
if factor == 1
|
936
|
+
yielder << size
|
937
|
+
break
|
938
|
+
else
|
939
|
+
yielder << factor
|
940
|
+
size /= factor
|
941
|
+
end
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
876
945
|
end
|
877
946
|
end
|
data/lib/prop_check/helper.rb
CHANGED
@@ -18,6 +18,9 @@ module PropCheck
|
|
18
18
|
# - `default_epoch:` The 'base' value to use for date/time generators like
|
19
19
|
# `PropCheck::Generators#date` `PropCheck::Generators#future_date` `PropCheck::Generators#time`, etc.
|
20
20
|
# (Default: `DateTime.now`)
|
21
|
+
# - `resize_function` A proc that can be used to resize _all_ generators.
|
22
|
+
# Takes the current size as integer and should return a new integer.
|
23
|
+
# (Default: `proc { |size| size }`)
|
21
24
|
Configuration = Struct.new(
|
22
25
|
:verbose,
|
23
26
|
:n_runs,
|
@@ -25,6 +28,7 @@ module PropCheck
|
|
25
28
|
:max_shrink_steps,
|
26
29
|
:max_consecutive_attempts,
|
27
30
|
:default_epoch,
|
31
|
+
:resize_function,
|
28
32
|
keyword_init: true
|
29
33
|
) do
|
30
34
|
def initialize(
|
@@ -33,7 +37,8 @@ module PropCheck
|
|
33
37
|
max_generate_attempts: 10_000,
|
34
38
|
max_shrink_steps: 10_000,
|
35
39
|
max_consecutive_attempts: 30,
|
36
|
-
default_epoch: DateTime.now
|
40
|
+
default_epoch: DateTime.now,
|
41
|
+
resize_function: proc { |size| size }
|
37
42
|
)
|
38
43
|
super
|
39
44
|
end
|
data/lib/prop_check/property.rb
CHANGED
@@ -111,6 +111,67 @@ module PropCheck
|
|
111
111
|
duplicate
|
112
112
|
end
|
113
113
|
|
114
|
+
##
|
115
|
+
# Resizes all generators in this property with the given function.
|
116
|
+
#
|
117
|
+
# Shorthand for manually wrapping `PropCheck::Property::Configuration.resize_function` with the new function.
|
118
|
+
def resize(&block)
|
119
|
+
raise '#resize called without a block' unless block_given?
|
120
|
+
|
121
|
+
orig_fun = @config.resize_function
|
122
|
+
with_config(resize_function: block)
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# Resizes all generators in this property. The new size is `2.pow(orig_size)`
|
127
|
+
#
|
128
|
+
# c.f. #resize
|
129
|
+
def growing_exponentially(&block)
|
130
|
+
orig_fun = @config.resize_function
|
131
|
+
fun = proc { |size| 2.pow(orig_fun.call(size)) }
|
132
|
+
with_config(resize_function: fun, &block)
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Resizes all generators in this property. The new size is `orig_size * orig_size`
|
137
|
+
#
|
138
|
+
# c.f. #resize
|
139
|
+
def growing_quadratically(&block)
|
140
|
+
orig_fun = @config.resize_function
|
141
|
+
fun = proc { |size| orig_fun.call(size).pow(2) }
|
142
|
+
with_config(resize_function: fun, &block)
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Resizes all generators in this property. The new size is `2 * orig_size`
|
147
|
+
#
|
148
|
+
# c.f. #resize
|
149
|
+
def growing_fast(&block)
|
150
|
+
orig_fun = @config.resize_function
|
151
|
+
fun = proc { |size| orig_fun.call(size) * 2 }
|
152
|
+
with_config(resize_function: fun, &block)
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Resizes all generators in this property. The new size is `0.5 * orig_size`
|
157
|
+
#
|
158
|
+
# c.f. #resize
|
159
|
+
def growing_slowly(&block)
|
160
|
+
orig_fun = @config.resize_function
|
161
|
+
fun = proc { |size| orig_fun.call(size) * 0.5 }
|
162
|
+
with_config(resize_function: fun, &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Resizes all generators in this property. The new size is `Math.log2(orig_size)`
|
167
|
+
#
|
168
|
+
# c.f. #resize
|
169
|
+
def growing_logarithmically(&block)
|
170
|
+
orig_fun = @config.resize_function
|
171
|
+
fun = proc { |size| Math.log2(orig_fun.call(size)) }
|
172
|
+
with_config(resize_function: fun, &block)
|
173
|
+
end
|
174
|
+
|
114
175
|
def with_bindings(*bindings, **kwbindings)
|
115
176
|
raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
|
116
177
|
|
@@ -279,8 +340,9 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
|
|
279
340
|
(0...@config.max_generate_attempts)
|
280
341
|
.lazy
|
281
342
|
.map do
|
343
|
+
generator_size = @config.resize_function.call(size).to_i
|
282
344
|
binding_generator.generate(
|
283
|
-
size:
|
345
|
+
size: generator_size,
|
284
346
|
rng: rng,
|
285
347
|
max_consecutive_attempts: @config.max_consecutive_attempts,
|
286
348
|
config: @config
|
data/lib/prop_check/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop_check
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.17.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Qqwy/Wiebe-Marten Wijnja
|
@@ -35,11 +35,11 @@ executables: []
|
|
35
35
|
extensions: []
|
36
36
|
extra_rdoc_files: []
|
37
37
|
files:
|
38
|
+
- ".github/workflows/run_tests.yaml"
|
38
39
|
- ".gitignore"
|
39
40
|
- ".rspec"
|
40
41
|
- ".rubocop.yml"
|
41
42
|
- ".tool-versions"
|
42
|
-
- ".travis.yml"
|
43
43
|
- CHANGELOG.md
|
44
44
|
- CODE_OF_CONDUCT.md
|
45
45
|
- Gemfile
|
@@ -53,7 +53,6 @@ files:
|
|
53
53
|
- lib/prop_check/generator.rb
|
54
54
|
- lib/prop_check/generators.rb
|
55
55
|
- lib/prop_check/helper.rb
|
56
|
-
- lib/prop_check/helper/lazy_append.rb
|
57
56
|
- lib/prop_check/hooks.rb
|
58
57
|
- lib/prop_check/lazy_tree.rb
|
59
58
|
- lib/prop_check/property.rb
|
@@ -84,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
83
|
- !ruby/object:Gem::Version
|
85
84
|
version: '0'
|
86
85
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
86
|
+
rubygems_version: 3.2.3
|
88
87
|
signing_key:
|
89
88
|
specification_version: 4
|
90
89
|
summary: PropCheck allows you to do property-based testing, including shrinking.
|
data/.travis.yml
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
---
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
rvm:
|
6
|
-
- 2.6.5
|
7
|
-
before_install: gem install bundler -v 2.0.2
|
8
|
-
env:
|
9
|
-
global:
|
10
|
-
- CC_TEST_REPORTER_ID=9d18f5b43e49eecd6c3da64d85ea9c765d3606c129289d7c8cadf6d448713311
|
11
|
-
before_script:
|
12
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
13
|
-
- chmod +x ./cc-test-reporter
|
14
|
-
- ./cc-test-reporter before-build
|
15
|
-
script:
|
16
|
-
- bundle exec rspec
|
17
|
-
after_script:
|
18
|
-
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# module PropCheck
|
2
|
-
# module Helper
|
3
|
-
# ##
|
4
|
-
# # A refinement for enumerators
|
5
|
-
# # to allow lazy appending of two (potentially lazy) enumerators:
|
6
|
-
# # >> [1,2,3].lazy_append([4,5.6]).to_a
|
7
|
-
# # => [1,2,3,4,5,6]
|
8
|
-
# module LazyAppend
|
9
|
-
# refine Enumerable do
|
10
|
-
# ## >> [1,2,3].lazy_append([4,5.6]).to_a
|
11
|
-
# ## => [1,2,3,4,5,6]
|
12
|
-
# def lazy_append(other_enumerator)
|
13
|
-
# [self, other_enumerator].lazy.flat_map(&:lazy)
|
14
|
-
# end
|
15
|
-
# end
|
16
|
-
# end
|
17
|
-
# end
|
18
|
-
# end
|