blood_contracts 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/Gemfile +1 -1
- data/README.md +2 -0
- data/Rakefile +1 -1
- data/bin/console +2 -5
- data/blood_contracts.gemspec +8 -5
- data/lib/blood_contracts/base_contract.rb +42 -0
- data/lib/blood_contracts/contracts/description.rb +25 -0
- data/lib/blood_contracts/contracts/matcher.rb +38 -0
- data/lib/blood_contracts/contracts/validator.rb +47 -0
- data/lib/blood_contracts/debugger.rb +39 -0
- data/lib/blood_contracts/runner.rb +49 -117
- data/lib/blood_contracts/statistics.rb +32 -0
- data/lib/blood_contracts/storage.rb +53 -53
- data/lib/blood_contracts/storages/base_backend.rb +59 -0
- data/lib/blood_contracts/storages/file_backend.rb +132 -0
- data/lib/blood_contracts/storages/serializer.rb +38 -0
- data/lib/blood_contracts/suite.rb +15 -5
- data/lib/blood_contracts/version.rb +1 -1
- data/lib/blood_contracts.rb +26 -76
- data/lib/extensions/string.rb +31 -0
- data/lib/rspec/meet_contract_matcher.rb +104 -0
- metadata +48 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6a3374d5e7af34959e56cb4f31d22cd807fe84f3ee1f74e9446646b7d4b49f27
|
4
|
+
data.tar.gz: 48907e94554abde9079f25e8a657fc24dbda9fac02468f239c547031eff0df0e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fc96fb5b769b22fb747573dd4ae5bf8da1cf2a40e429ddde58f82d78263aba3e9957d31183c0c5b0af3206abe0172e511d10e312d9624c0533cef030c502d84
|
7
|
+
data.tar.gz: c22e684f33f0d4f4ab37a5e08174c4dcefae73afa09079250eb1b158c2e03d572be92a40a202a2724f8bf0026e91fd8b89308cb1843de2f8f6ceff0d9fa4ff92
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -7,6 +7,8 @@ Possible use-cases:
|
|
7
7
|
- Automated detection of unexpected external API behavior (Rack::request/response pairs that don't match contract);
|
8
8
|
- Contract definition assistance tool (generate real-a-like requests and iterate through oddities of your system behavior)
|
9
9
|
|
10
|
+
<a href="https://evilmartians.com/">
|
11
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
|
10
12
|
|
11
13
|
## Installation
|
12
14
|
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -7,8 +7,5 @@ require "blood_contracts"
|
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
8
8
|
|
9
9
|
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
data/blood_contracts.gemspec
CHANGED
@@ -5,12 +5,13 @@ require "blood_contracts/version"
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "blood_contracts"
|
8
|
+
|
8
9
|
spec.version = BloodContracts::VERSION
|
9
10
|
spec.authors = ["Sergey Dolganov"]
|
10
11
|
spec.email = ["dolganov@evl.ms"]
|
11
12
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
13
|
+
spec.summary = "Ruby gem to define and validate behavior of API using contracts."
|
14
|
+
spec.description = "Ruby gem to define and validate behavior of API using contracts."
|
14
15
|
spec.homepage = "https://github.com/sclinede/blood_contracts"
|
15
16
|
spec.license = "MIT"
|
16
17
|
|
@@ -23,12 +24,14 @@ Gem::Specification.new do |spec|
|
|
23
24
|
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
25
|
spec.require_paths = ["lib"]
|
25
26
|
|
26
|
-
spec.
|
27
|
+
spec.required_ruby_version = '>= 2.2.0'
|
27
28
|
|
28
|
-
|
29
|
-
spec.add_runtime_dependency "
|
29
|
+
spec.add_runtime_dependency "dry-initializer", "~> 2.0"
|
30
|
+
spec.add_runtime_dependency "nanoid", "~> 0.2"
|
31
|
+
spec.add_runtime_dependency "hashie", "~> 3.0"
|
30
32
|
|
31
33
|
spec.add_development_dependency "bundler", "~> 1.16"
|
32
34
|
spec.add_development_dependency "rake", "~> 10.0"
|
33
35
|
spec.add_development_dependency "rspec", "~> 3.0"
|
36
|
+
spec.add_development_dependency "pry", "~> 0.9"
|
34
37
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
class BaseContract
|
3
|
+
extend Dry::Initializer
|
4
|
+
option :action
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def rules
|
8
|
+
@rules ||= Set.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def contract_rule(name, &block)
|
12
|
+
define_method("_#{name}", block)
|
13
|
+
rules << name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(data)
|
18
|
+
return yield(data) if block_given?
|
19
|
+
action.call(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
def contract
|
23
|
+
@contract ||= Hash[
|
24
|
+
self.class.rules.map { |name| [name, {check: method("_#{name}")}] }
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
def build_storage(name)
|
29
|
+
s = Storage.new(contract_name: name)
|
30
|
+
s.input_writer = method(:input_writer) if defined? input_writer
|
31
|
+
s.output_writer = method(:output_writer) if defined? output_writer
|
32
|
+
s.input_serializer = input_serializer if defined? input_serializer
|
33
|
+
s.output_serializer = output_serializer if defined? output_serializer
|
34
|
+
s.meta_serializer = meta_serializer if defined? meta_serializer
|
35
|
+
s
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_contract_suite(name: self.class.to_s.pathize)
|
39
|
+
Suite.new(storage: build_storage(name), contract: contract)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Contracts
|
3
|
+
class Description
|
4
|
+
def self.call(contract_hash)
|
5
|
+
Hashie::Mash.new(contract_hash).map do |name, rule|
|
6
|
+
rule_description = " - '#{name}' "
|
7
|
+
if rule.threshold
|
8
|
+
rule_description << <<~TEXT
|
9
|
+
in more then #{(rule.threshold * 100).round(2)}% of cases;
|
10
|
+
TEXT
|
11
|
+
elsif rule.limit
|
12
|
+
rule_description << <<~TEXT
|
13
|
+
in less then #{(rule.limit * 100).round(2)}% of cases;
|
14
|
+
TEXT
|
15
|
+
else
|
16
|
+
rule_description << <<~TEXT
|
17
|
+
in any number of cases;
|
18
|
+
TEXT
|
19
|
+
end
|
20
|
+
rule_description
|
21
|
+
end.compact.join
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Contracts
|
3
|
+
class Matcher
|
4
|
+
extend Dry::Initializer
|
5
|
+
|
6
|
+
param :contract_hash, ->(v) { Hashie::Mash.new(v) }
|
7
|
+
|
8
|
+
def call(input, output, meta = Hash.new, storage:)
|
9
|
+
options = Hashie::Mash.new(input: input, output: output, meta: meta)
|
10
|
+
|
11
|
+
rule_names = select_matched_rules!(options).keys
|
12
|
+
rule_names = [Storage::UNDEFINED_RULE] if rule_names.empty?
|
13
|
+
Array(rule_names).each(&storage.method(:store))
|
14
|
+
|
15
|
+
yield rule_names, options if block_given?
|
16
|
+
|
17
|
+
!storage.found_unexpected_behavior?
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def with_rule_options(rule_name, options)
|
23
|
+
rule_options = options.shallow_merge(meta: {})
|
24
|
+
result = yield(rule_options)
|
25
|
+
options.meta.merge!(rule_name.to_sym => rule_options.meta)
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def select_matched_rules!(options)
|
30
|
+
contract_hash.select do |name, rule|
|
31
|
+
with_rule_options(name, options) do |rule_options|
|
32
|
+
rule.check.call(rule_options)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Contracts
|
3
|
+
class Validator
|
4
|
+
extend Dry::Initializer
|
5
|
+
|
6
|
+
param :contract_hash, ->(v) { Hashie::Mash.new(v) }
|
7
|
+
|
8
|
+
def valid?(statistics)
|
9
|
+
return if statistics.found_unexpected_behavior?
|
10
|
+
|
11
|
+
last_run_stats = statistics.to_h
|
12
|
+
expectations.all? do |rule, check|
|
13
|
+
percent = last_run_stats[rule.name]&.percent || 0.0
|
14
|
+
check.call(percent, rule)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def expectations
|
21
|
+
Hash[
|
22
|
+
contract_hash.map do |name, rule|
|
23
|
+
if rule.threshold
|
24
|
+
[rule.merge(name: name), method(:threshold_check)]
|
25
|
+
elsif rule.limit
|
26
|
+
[rule.merge(name: name), method(:limit_check)]
|
27
|
+
else
|
28
|
+
[rule.merge(name: name), method(:anyway)]
|
29
|
+
end
|
30
|
+
end.compact
|
31
|
+
]
|
32
|
+
end
|
33
|
+
|
34
|
+
def threshold_check(value, rule)
|
35
|
+
value > rule.threshold
|
36
|
+
end
|
37
|
+
|
38
|
+
def limit_check(value, rule)
|
39
|
+
value <= rule.limit
|
40
|
+
end
|
41
|
+
|
42
|
+
def anyway(_value, _rule)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
class Debugger < Runner
|
3
|
+
def runs
|
4
|
+
@runs ||= storage.find_all_samples(ENV["debug"]).each
|
5
|
+
end
|
6
|
+
|
7
|
+
def iterations
|
8
|
+
runs.size
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
return super if debugging_samples?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def description
|
17
|
+
return super if debugging_samples?
|
18
|
+
"be skipped in current debugging session"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def match_rules?(matches_storage:)
|
24
|
+
matcher.call(*storage.load_sample(runs.next), storage: matches_storage)
|
25
|
+
end
|
26
|
+
|
27
|
+
def unexpected_further_investigation
|
28
|
+
ENV["debug"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def further_investigation
|
32
|
+
ENV["debug"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def debugging_samples?
|
36
|
+
runs.size.positive?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,93 +1,89 @@
|
|
1
|
+
require_relative "contracts/validator"
|
2
|
+
require_relative "contracts/matcher"
|
3
|
+
require_relative "contracts/description"
|
4
|
+
|
1
5
|
module BloodContracts
|
2
6
|
class Runner
|
3
7
|
extend Dry::Initializer
|
4
8
|
|
5
9
|
param :checking_proc
|
6
|
-
option :context, optional: true
|
7
10
|
|
8
11
|
option :suite
|
12
|
+
option :storage, default: -> { suite.storage }
|
9
13
|
|
10
14
|
option :iterations, ->(v) do
|
11
15
|
v = ENV["iterations"] if ENV["iterations"]
|
12
16
|
v.to_i.positive? ? v.to_i : 1
|
13
17
|
end, default: -> { 1 }
|
14
|
-
|
15
|
-
|
16
18
|
option :time_to_run, ->(v) do
|
17
19
|
v = ENV["duration"] if ENV["duration"]
|
18
20
|
v.to_f if v.to_f.positive?
|
19
21
|
end, optional: true
|
20
22
|
|
23
|
+
option :context, optional: true
|
21
24
|
option :stop_on_unexpected, default: -> { false }
|
22
25
|
|
26
|
+
option :statistics, default: -> { Statistics.new(iterations) }
|
27
|
+
option :matcher, default: -> { Contracts::Matcher.new(suite.contract) }
|
28
|
+
option :validator, default: -> { Contracts::Validator.new(suite.contract) }
|
29
|
+
option :contract_description, default: -> do
|
30
|
+
Contracts::Description.call(suite.contract)
|
31
|
+
end
|
32
|
+
|
23
33
|
def call
|
24
34
|
iterate do
|
25
|
-
|
26
|
-
suite.
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
throw :unexpected_behavior, :stop if stop_on_unexpected && unexpected
|
35
|
+
next if match_rules?(matches_storage: statistics) do
|
36
|
+
input = suite.data_generator.call
|
37
|
+
[input, checking_proc.call(input)]
|
38
|
+
end
|
39
|
+
throw :unexpected_behavior, :stop if stop_on_unexpected
|
31
40
|
end
|
32
|
-
valid?
|
33
|
-
end
|
34
|
-
|
35
|
-
def valid?
|
36
41
|
return if stopped_by_unexpected_behavior?
|
37
|
-
return if found_unexpected_behavior?
|
38
42
|
|
39
|
-
|
40
|
-
expectations_checks.all? do |rule, check|
|
41
|
-
percent = last_run_stats[rule.name]&.percent || 0.0
|
42
|
-
check.call(percent, rule)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def found_unexpected_behavior?
|
47
|
-
run_stats.key?(Storage::UNDEFINED_RULE)
|
43
|
+
validator.valid?(statistics)
|
48
44
|
end
|
49
45
|
|
46
|
+
# FIXME: Move to locales
|
50
47
|
def failure_message
|
51
48
|
intro = "expected that given Proc would meet the contract:"
|
52
49
|
|
53
|
-
if
|
50
|
+
if validator.expected_behavior?
|
54
51
|
"#{intro}\n#{contract_description}\n"\
|
55
|
-
|
56
|
-
|
52
|
+
" during #{iterations} run(s) but got:\n#{statistics}\n\n"\
|
53
|
+
"For further investigations open: #{storage.suggestion}"
|
57
54
|
else
|
58
55
|
"#{intro}\n#{contract_description}\n"\
|
59
|
-
" during #{iterations} run(s) but got
|
60
|
-
"For further investigations open: #{
|
56
|
+
" during #{iterations} run(s) but got unexpected behavior.\n\n"\
|
57
|
+
"For further investigations open: #{storage.unexpected_suggestion}"
|
61
58
|
end
|
62
59
|
end
|
63
60
|
|
64
|
-
|
65
|
-
File.join(suite.storage.path, Storage::UNDEFINED_RULE.to_s)
|
66
|
-
end
|
67
|
-
|
61
|
+
# FIXME: Move to locales
|
68
62
|
def description
|
69
63
|
"meet the contract:\n#{contract_description} \n"\
|
70
|
-
" during #{iterations} run(s). Stats:\n#{
|
71
|
-
"For further investigations open: #{
|
64
|
+
" during #{iterations} run(s). Stats:\n#{statistics}\n\n"\
|
65
|
+
"For further investigations open: #{storage.suggestion}\n"
|
72
66
|
end
|
73
67
|
|
74
|
-
|
68
|
+
protected
|
75
69
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
70
|
+
def match_rules?(matches_storage:)
|
71
|
+
matcher.call(*yield, storage: matches_storage) do |rules, options|
|
72
|
+
storage.store(options: options, rules: rules, context: context)
|
73
|
+
end
|
74
|
+
rescue StandardError => error
|
75
|
+
# FIXME: Possible recursion?
|
76
|
+
# Write test about error in the storage#store (e.g. writing error)
|
77
|
+
store_exception(error, input, output, context)
|
78
|
+
raise
|
79
79
|
end
|
80
80
|
|
81
81
|
def stopped_by_unexpected_behavior?
|
82
82
|
@_stopped_by_unexpected_behavior == :stop
|
83
83
|
end
|
84
84
|
|
85
|
-
def stats
|
86
|
-
suite.storage.stats
|
87
|
-
end
|
88
|
-
|
89
85
|
def iterate
|
90
|
-
run_iterations
|
86
|
+
run_iterations ||= iterations
|
91
87
|
|
92
88
|
if time_to_run
|
93
89
|
run_iterations = iterations_count_from_time_to_run { yield }
|
@@ -104,80 +100,16 @@ module BloodContracts
|
|
104
100
|
(time_to_run / time_per_action.real).ceil
|
105
101
|
end
|
106
102
|
|
107
|
-
def
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
rescue => e
|
118
|
-
yield [Storage::UNDEFINED_RULE]
|
119
|
-
raise e
|
120
|
-
end
|
121
|
-
|
122
|
-
def threshold_check(value, rule)
|
123
|
-
value > rule.threshold
|
124
|
-
end
|
125
|
-
|
126
|
-
def limit_check(value, rule)
|
127
|
-
value <= rule.limit
|
128
|
-
end
|
129
|
-
|
130
|
-
def expectations_checks
|
131
|
-
Hash[
|
132
|
-
suite.contract.map do |name, rule|
|
133
|
-
if rule.threshold
|
134
|
-
[rule.merge(name: name), method(:threshold_check)]
|
135
|
-
elsif rule.limit
|
136
|
-
[rule.merge(name: name), method(:limit_check)]
|
137
|
-
else
|
138
|
-
nil
|
139
|
-
end
|
140
|
-
end.compact
|
141
|
-
]
|
142
|
-
end
|
143
|
-
|
144
|
-
def contract_description
|
145
|
-
suite.contract.map do |name, rule|
|
146
|
-
rule_description = " - '#{name}' "
|
147
|
-
if rule.threshold
|
148
|
-
rule_description << <<~TEXT
|
149
|
-
in more then #{(rule.threshold * 100).round(2)}% of cases;
|
150
|
-
TEXT
|
151
|
-
elsif rule.limit
|
152
|
-
rule_description << <<~TEXT
|
153
|
-
in less then #{(rule.limit * 100).round(2)}% of cases;
|
154
|
-
TEXT
|
155
|
-
else
|
156
|
-
next
|
157
|
-
end
|
158
|
-
rule_description
|
159
|
-
end.compact.join
|
160
|
-
end
|
161
|
-
|
162
|
-
def stats_description
|
163
|
-
run_stats.map do |name, occasions|
|
164
|
-
" - '#{name}' happened #{occasions.times} time(s) "\
|
165
|
-
"(#{(occasions.percent * 100).round(2)}% of the time)"
|
166
|
-
end.join("; \n")
|
167
|
-
end
|
168
|
-
|
169
|
-
def run_stats
|
170
|
-
Hash[
|
171
|
-
stats.map do |rule_name, times|
|
172
|
-
[
|
173
|
-
rule_name,
|
174
|
-
Hashie::Mash.new(
|
175
|
-
times: times,
|
176
|
-
percent: (times.to_f / iterations),
|
177
|
-
),
|
178
|
-
]
|
179
|
-
end
|
180
|
-
]
|
103
|
+
def store_exception(error, input, output, context)
|
104
|
+
storage.store(
|
105
|
+
options: {
|
106
|
+
input: input,
|
107
|
+
output: output || {},
|
108
|
+
meta: {exception: error},
|
109
|
+
},
|
110
|
+
rules: [Storage::EXCEPTION_CAUGHT],
|
111
|
+
context: context,
|
112
|
+
)
|
181
113
|
end
|
182
114
|
end
|
183
115
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
class Statistics
|
3
|
+
extend Dry::Initializer
|
4
|
+
param :iterations
|
5
|
+
option :storage, default: -> { Hash.new(0) }
|
6
|
+
|
7
|
+
def store(rule)
|
8
|
+
storage[rule] += 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
Hash[storage.map { |rule_name, times| [rule_name, rule_stats(times)] }]
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
to_h.map do |name, occasions|
|
17
|
+
" - '#{name}' happened #{occasions.times} time(s) "\
|
18
|
+
"(#{(occasions.percent * 100).round(2)}% of the time)"
|
19
|
+
end.join("; \n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def found_unexpected_behavior?
|
23
|
+
storage.key?(Storage::UNDEFINED_RULE)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def rule_stats(times)
|
29
|
+
Hashie::Mash.new(times: times, percent: (times.to_f / iterations))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -1,79 +1,79 @@
|
|
1
|
+
require_relative "./storages/base_backend.rb"
|
2
|
+
require_relative "./storages/file_backend.rb"
|
3
|
+
require_relative "./storages/serializer.rb"
|
4
|
+
|
1
5
|
module BloodContracts
|
2
6
|
class Storage
|
3
7
|
extend Dry::Initializer
|
8
|
+
extend Forwardable
|
4
9
|
|
5
|
-
|
6
|
-
|
7
|
-
option :custom_path, optional: true
|
8
|
-
option :root, default: -> { Rails.root.join(path) }
|
9
|
-
option :stats, default: -> { Hash.new(0) }
|
10
|
-
option :input_writer, optional: true
|
11
|
-
option :output_writer, optional: true
|
10
|
+
Serializer = BloodContracts::Storages::Serializer
|
11
|
+
FileBackend = BloodContracts::Storages::FileBackend
|
12
12
|
|
13
|
-
|
13
|
+
option :contract_name
|
14
14
|
|
15
|
-
|
16
|
-
"
|
15
|
+
DEFAULT_WRITER = -> (options) do
|
16
|
+
"INPUT:\n#{options.input}\n\n#{'=' * 90}\n\nOUTPUT:\n#{options.output}"
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
option :input_writer,
|
20
|
+
->(v) { valid_writer(v) }, default: -> { DEFAULT_WRITER }
|
21
|
+
option :output_writer,
|
22
|
+
->(v) { valid_writer(v) }, default: -> { DEFAULT_WRITER }
|
23
|
+
|
24
|
+
option :input_serializer,
|
25
|
+
->(v) { Serializer.call(v) }, default: -> { default_serializer }
|
26
|
+
option :output_serializer,
|
27
|
+
->(v) { Serializer.call(v) }, default: -> { default_serializer }
|
28
|
+
option :meta_serializer,
|
29
|
+
->(v) { Serializer.call(v) }, default: -> { default_serializer }
|
30
|
+
|
31
|
+
option :backend, default: -> { FileBackend.new(self, contract_name) }
|
32
|
+
|
33
|
+
def_delegators :@backend, :sample_exists?,
|
34
|
+
:load_sample, :find_all_samples,
|
35
|
+
:serialize_sample, :describe_sample,
|
36
|
+
:suggestion, :unexpected_suggestion
|
37
|
+
|
38
|
+
def self.valid_writer(writer)
|
39
|
+
return writer if writer.respond_to?(:call) || writer.respond_to?(:to_sym)
|
40
|
+
raise ArgumentError
|
21
41
|
end
|
22
42
|
|
23
|
-
def
|
24
|
-
|
25
|
-
writer.respond_to?(:to_sym)
|
26
|
-
@input_writer = writer
|
43
|
+
def input_serializer=(serializer)
|
44
|
+
@input_serializer = Serializer.call(serializer)
|
27
45
|
end
|
28
46
|
|
29
|
-
def
|
30
|
-
|
31
|
-
writer.respond_to?(:to_sym)
|
32
|
-
@output_writer = writer
|
47
|
+
def output_serializer=(serializer)
|
48
|
+
@output_serializer = Serializer.call(serializer)
|
33
49
|
end
|
34
50
|
|
35
|
-
def
|
36
|
-
|
37
|
-
FileUtils.mkdir_p File.join(root, "#{tag}")
|
38
|
-
run_name
|
51
|
+
def meta_serializer=(serializer)
|
52
|
+
@meta_serializer = Serializer.call(serializer)
|
39
53
|
end
|
40
54
|
|
41
|
-
def
|
42
|
-
|
43
|
-
writer = context.method(writer) if context && writer.respond_to?(:to_sym)
|
44
|
-
writer.call(input, output)
|
55
|
+
def input_writer=(writer)
|
56
|
+
@input_writer = self.class.valid_writer(writer)
|
45
57
|
end
|
46
58
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
input,
|
51
|
-
"\n#{'=' * 90}\n",
|
52
|
-
"OUTPUT:",
|
53
|
-
output
|
54
|
-
].map(&:to_s).join("\n")
|
59
|
+
def output_writer=(writer)
|
60
|
+
@output_writer = self.class.valid_writer(writer)
|
61
|
+
end
|
55
62
|
|
63
|
+
UNDEFINED_RULE = :__no_tag_match__
|
64
|
+
EXCEPTION_CAUGHT = :__exception_raised__
|
65
|
+
|
66
|
+
def default_serializer
|
67
|
+
{ load: Oj.method(:load), dump: Oj.method(:dump) }
|
56
68
|
end
|
57
69
|
|
58
70
|
# Quick open: `vim -O tmp/contract_tests/<tstamp>/<tag>/<tstamp>.*`
|
59
|
-
def
|
71
|
+
def store(options:, rules:, context:)
|
72
|
+
options = Hashie::Mash.new(options)
|
73
|
+
|
60
74
|
Array(rules).each do |rule_name|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
# TODO: Write to HTML
|
65
|
-
input_fname = "#{run_name}.input"
|
66
|
-
output_fname = "#{run_name}.output"
|
67
|
-
File.open(input_fname, "w+") do |f|
|
68
|
-
f << write(input_writer, context, input, output).encode(
|
69
|
-
'UTF-8', invalid: :replace, undef: :replace, replace: '?'
|
70
|
-
)
|
71
|
-
end
|
72
|
-
File.open(output_fname, "w+") do |f|
|
73
|
-
f << write(output_writer, context, input, output).encode(
|
74
|
-
'UTF-8', invalid: :replace, undef: :replace, replace: '?'
|
75
|
-
)
|
76
|
-
end
|
75
|
+
describe_sample(rule_name, options, context)
|
76
|
+
serialize_sample(rule_name, options, context)
|
77
77
|
end
|
78
78
|
end
|
79
79
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'nanoid'
|
2
|
+
|
3
|
+
module BloodContracts
|
4
|
+
module Storages
|
5
|
+
class BaseBackend
|
6
|
+
extend Dry::Initializer
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
param :storage
|
10
|
+
param :example_name
|
11
|
+
option :name, default: -> do
|
12
|
+
BloodContracts.run_name || ::Nanoid.generate(size: 10)
|
13
|
+
end
|
14
|
+
def_delegators :@storage, :input_writer, :output_writer,
|
15
|
+
:input_serializer, :output_serializer, :meta_serializer
|
16
|
+
|
17
|
+
|
18
|
+
def sample_exists?(sample_name)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_all_samples(run, tag, sample)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_sample(_sample_name)
|
27
|
+
%i(input output meta).map do |type|
|
28
|
+
load_sample_chunk(type, _sample_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_sample_chunk(_dump_type, _sample_name)
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def describe_sample(_tag, _options, _context)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def serialize_sample(tag, options, context)
|
41
|
+
%i(input output meta).each do |type|
|
42
|
+
serialize_sample_chunk(type, tag, options, context)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def serialize_sample_chunk(_type, _tag, _option, _context)
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
def suggestion
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def unexpected_suggestion
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Storages
|
3
|
+
class FileBackend < BaseBackend
|
4
|
+
option :root, default: -> { Rails.root.join(path) }
|
5
|
+
|
6
|
+
def suggestion
|
7
|
+
"#{path}/*/*"
|
8
|
+
end
|
9
|
+
|
10
|
+
def unexpected_suggestion
|
11
|
+
"#{path}/#{Storage::UNDEFINED_RULE}/*"
|
12
|
+
end
|
13
|
+
|
14
|
+
def default_path
|
15
|
+
"./tmp/contract_tests/"
|
16
|
+
end
|
17
|
+
|
18
|
+
def timestamp
|
19
|
+
@timestamp ||= Time.current.to_s(:usec)[8..-3]
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset_timestamp!
|
23
|
+
@timestamp = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_sample_name(sample_name)
|
27
|
+
path_items = sample_name.split("/")
|
28
|
+
sample = path_items.pop
|
29
|
+
tag = path_items.pop
|
30
|
+
path_str = path_items.join("/")
|
31
|
+
run_n_example_str = path_str.sub(default_path, '')
|
32
|
+
if run_n_example_str.end_with?('*')
|
33
|
+
[
|
34
|
+
run_n_example_str.chomp("*"),
|
35
|
+
tag,
|
36
|
+
sample
|
37
|
+
]
|
38
|
+
elsif run_n_example_str.end_with?(example_name)
|
39
|
+
[
|
40
|
+
run_n_example_str.chomp(example_name),
|
41
|
+
tag,
|
42
|
+
sample
|
43
|
+
]
|
44
|
+
else
|
45
|
+
%w(__no_match__ __no_match__ __no_match__)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_all_samples(sample_name)
|
50
|
+
run, tag, sample = parse_sample_name(sample_name)
|
51
|
+
run_path = path(run_name: run)
|
52
|
+
files = Dir.glob("#{run_path}/#{tag.to_s}/#{sample}*")
|
53
|
+
files.select { |f| f.end_with?(".output") }
|
54
|
+
.map { |f| f.chomp(".output") }
|
55
|
+
end
|
56
|
+
|
57
|
+
def path(run_name: name)
|
58
|
+
File.join(default_path, run_name, example_name.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def sample_name(tag, run_path: root, sample: timestamp)
|
62
|
+
File.join(run_path, tag.to_s, sample)
|
63
|
+
end
|
64
|
+
|
65
|
+
def sample_exists?(sample_name)
|
66
|
+
run, tag, sample = parse_sample_name(sample_name)
|
67
|
+
name = sample_name(tag, run_path: path(run_name: run), sample: sample)
|
68
|
+
File.exist?("#{name}.input")
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_sample_chunk(dump_type, sample_name)
|
72
|
+
run, tag, sample = parse_sample_name(sample_name)
|
73
|
+
name = sample_name(tag, run_path: path(run_name: run), sample: sample)
|
74
|
+
send("#{dump_type}_serializer")[:load].call(
|
75
|
+
File.read("#{name}.#{dump_type}.dump")
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def write(writer, cntxt, options)
|
80
|
+
writer = cntxt.method(writer) if cntxt && writer.respond_to?(:to_sym)
|
81
|
+
writer.call(options).encode(
|
82
|
+
"UTF-8", invalid: :replace, undef: :replace, replace: "?",
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def describe_sample(tag, options, context)
|
87
|
+
FileUtils.mkdir_p File.join(root, tag.to_s)
|
88
|
+
|
89
|
+
reset_timestamp!
|
90
|
+
name = sample_name(tag)
|
91
|
+
File.open("#{name}.input", "w+") do |f|
|
92
|
+
f << write(input_writer, context, options)
|
93
|
+
end
|
94
|
+
File.open("#{name}.output", "w+") do |f|
|
95
|
+
f << write(output_writer, context, options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def serialize_sample_chunk(type, tag, options, context)
|
100
|
+
return unless (dump_proc = send("#{type}_serializer")[:dump])
|
101
|
+
name, data = sample_name(tag), options.send(type)
|
102
|
+
File.open("#{name}.#{type}.dump", "w+") do |f|
|
103
|
+
f << write(dump_proc, context, data)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
#
|
107
|
+
# def serialize_input(tag, options, context)
|
108
|
+
# return unless (dump_proc = input_serializer[:dump])
|
109
|
+
# name = sample_name(tag)
|
110
|
+
# File.open("#{name}.input.dump", "w+") do |f|
|
111
|
+
# f << write(dump_proc, context, options.input)
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# def serialize_output(tag, options, context)
|
116
|
+
# return unless (dump_proc = output_serializer[:dump])
|
117
|
+
# name = sample_name(tag)
|
118
|
+
# File.open("#{name}.output.dump", "w+") do |f|
|
119
|
+
# f << write(dump_proc, context, options.output)
|
120
|
+
# end
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# def serialize_meta(tag, options, context)
|
124
|
+
# return unless (dump_proc = meta_serializer[:dump])
|
125
|
+
# name = sample_name(tag)
|
126
|
+
# File.open("#{name}.meta.dump", "w+") do |f|
|
127
|
+
# f << write(dump_proc, context, options.meta)
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module BloodContracts
|
2
|
+
module Storages
|
3
|
+
class Serializer
|
4
|
+
extend Dry::Initializer
|
5
|
+
|
6
|
+
param :serializer
|
7
|
+
|
8
|
+
def self.call(*args)
|
9
|
+
new(*args).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
return object_serializer_to_hash if object_serializer?
|
14
|
+
return serializer.to_hash if hash_serializer?
|
15
|
+
|
16
|
+
raise "Both #dump and #load methods should be defined for serialization"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def object_serializer?
|
22
|
+
serializer.respond_to?(:dump) && serializer.respond_to?(:load)
|
23
|
+
end
|
24
|
+
|
25
|
+
def object_serializer_to_hash
|
26
|
+
{
|
27
|
+
load: serializer.method(:load),
|
28
|
+
dump: serializer.method(:dump),
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def hash_serializer?
|
33
|
+
return unless serializer.respond_to?(:to_hash)
|
34
|
+
(%i[dump load] - serializer.to_hash.keys).empty?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -3,19 +3,24 @@ module BloodContracts
|
|
3
3
|
extend Dry::Initializer
|
4
4
|
|
5
5
|
option :data_generator, optional: true
|
6
|
-
option :contract,
|
7
|
-
|
6
|
+
option :contract, ->(v) { Hashie::Mash.new(v) }, default: -> { Hash.new }
|
7
|
+
|
8
|
+
option :input_writer, optional: true
|
8
9
|
option :output_writer, optional: true
|
10
|
+
|
11
|
+
option :input_serializer, ->(v) { parse_serializer(v) }, optional: true
|
12
|
+
option :output_serializer, ->(v) { parse_serializer(v) }, optional: true
|
13
|
+
|
9
14
|
option :storage_backend, optional: true
|
10
15
|
option :storage, default: -> { default_storage }
|
11
16
|
|
12
17
|
def data_generator=(generator)
|
13
|
-
|
18
|
+
raise ArgumentError unless generator.respond_to?(:call)
|
14
19
|
@data_generator = generator
|
15
20
|
end
|
16
21
|
|
17
22
|
def contract=(contract)
|
18
|
-
|
23
|
+
raise ArgumentError unless contract.respond_to?(:to_h)
|
19
24
|
@contract = Hashie::Mash.new(contract.to_h)
|
20
25
|
end
|
21
26
|
|
@@ -28,7 +33,12 @@ module BloodContracts
|
|
28
33
|
end
|
29
34
|
|
30
35
|
def default_storage
|
31
|
-
Storage.new(
|
36
|
+
Storage.new(
|
37
|
+
input_writer: input_writer,
|
38
|
+
output_writer: output_writer,
|
39
|
+
input_serializer: input_serializer,
|
40
|
+
output_serializer: output_serializer,
|
41
|
+
)
|
32
42
|
end
|
33
43
|
end
|
34
44
|
end
|
data/lib/blood_contracts.rb
CHANGED
@@ -1,88 +1,38 @@
|
|
1
1
|
require "blood_contracts/version"
|
2
|
+
|
3
|
+
require_relative "extensions/string.rb"
|
4
|
+
require "dry-initializer"
|
5
|
+
require "hashie/mash"
|
6
|
+
|
2
7
|
require_relative "blood_contracts/suite"
|
3
8
|
require_relative "blood_contracts/storage"
|
9
|
+
require_relative "blood_contracts/statistics"
|
4
10
|
require_relative "blood_contracts/runner"
|
11
|
+
require_relative "blood_contracts/debugger"
|
12
|
+
require_relative "blood_contracts/base_contract"
|
5
13
|
|
6
14
|
module BloodContracts
|
15
|
+
def run_name
|
16
|
+
@__contracts_run_name
|
17
|
+
end
|
18
|
+
module_function :run_name
|
7
19
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
matcher :meet_contract_rules do |options|
|
14
|
-
match do |actual|
|
15
|
-
raise ArgumentError unless actual.respond_to?(:call)
|
16
|
-
|
17
|
-
@_contract_runner = Runner.new(
|
18
|
-
actual,
|
19
|
-
context: self,
|
20
|
-
suite: build_suite(options),
|
21
|
-
iterations: @_iterations,
|
22
|
-
time_to_run: @_time_to_run,
|
23
|
-
stop_on_unexpected: @_halt_on_unexpected,
|
24
|
-
)
|
25
|
-
@_contract_runner.call
|
26
|
-
end
|
27
|
-
|
28
|
-
def build_suite(options)
|
29
|
-
storage = Storage.new(custom_path: _example_name_to_path)
|
30
|
-
storage.input_writer = _input_writer if _input_writer
|
31
|
-
storage.output_writer = _output_writer if _output_writer
|
32
|
-
|
33
|
-
suite = options[:contract_suite] || Suite.new(storage: storage)
|
34
|
-
|
35
|
-
suite.data_generator = @_generator if @_generator
|
36
|
-
suite.contract = options[:contract] if options[:contract]
|
37
|
-
suite
|
38
|
-
end
|
39
|
-
|
40
|
-
def _example_name_to_path
|
41
|
-
method_missing(:class).
|
42
|
-
name.
|
43
|
-
sub("RSpec::ExampleGroups::", "").
|
44
|
-
snakecase
|
45
|
-
end
|
46
|
-
|
47
|
-
def _input_writer
|
48
|
-
input_writer = @_writers.to_h[:input]
|
49
|
-
input_writer ||= :input_writer if defined? self.input_writer
|
50
|
-
input_writer
|
51
|
-
end
|
52
|
-
|
53
|
-
def _output_writer
|
54
|
-
output_writer = @_writers.to_h[:output]
|
55
|
-
output_writer ||= :output_writer if defined? self.output_writer
|
56
|
-
output_writer
|
57
|
-
end
|
58
|
-
|
59
|
-
supports_block_expectations
|
60
|
-
|
61
|
-
failure_message { @_contract_runner.failure_message }
|
62
|
-
|
63
|
-
description { @_contract_runner.description }
|
64
|
-
|
65
|
-
chain :using_generator do |generator|
|
66
|
-
if generator.respond_to?(:to_sym)
|
67
|
-
@_generator = method(generator.to_sym)
|
68
|
-
else
|
69
|
-
fail ArgumentError unless generator.respond_to?(:call)
|
70
|
-
@_generator = generator
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
chain :during_n_iterations_run do |iterations|
|
75
|
-
@_iterations = Integer(iterations)
|
76
|
-
end
|
77
|
-
|
78
|
-
chain :during_n_seconds_run do |time_to_run|
|
79
|
-
@_time_to_run = Float(time_to_run)
|
80
|
-
end
|
20
|
+
def run_name=(run_name)
|
21
|
+
@__contracts_run_name = run_name
|
22
|
+
end
|
23
|
+
module_function :run_name=
|
81
24
|
|
82
|
-
|
83
|
-
|
84
|
-
end
|
25
|
+
if defined?(RSpec) && RSpec.respond_to?(:configure)
|
26
|
+
require_relative "rspec/meet_contract_matcher"
|
85
27
|
|
28
|
+
RSpec.configure do |config|
|
29
|
+
config.include ::RSpec::MeetContractMatcher
|
30
|
+
config.filter_run_excluding contract: true
|
31
|
+
config.before(:suite) do
|
32
|
+
BloodContracts.run_name = ::Nanoid.generate(size: 10)
|
33
|
+
end
|
34
|
+
config.define_derived_metadata(file_path: %r{/spec/contracts/}) do |meta|
|
35
|
+
meta[:contract] = true
|
86
36
|
end
|
87
37
|
end
|
88
38
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class String
|
2
|
+
# See gem Facets String#pathize
|
3
|
+
|
4
|
+
# Transforms a namespace, i.e. a class or module name, into a viable
|
5
|
+
# file path.
|
6
|
+
#
|
7
|
+
# "ExamplePathize".pathize #=> "example_pathize"
|
8
|
+
# "ExamplePathize::Example".pathize #=> "example_pathize/example"
|
9
|
+
#
|
10
|
+
# Compare this method to {String#modulize) and {String#methodize).
|
11
|
+
#
|
12
|
+
def pathize
|
13
|
+
gsub(/([A-Z]+)([A-Z])/,'\1_\2').
|
14
|
+
gsub(/([a-z])([A-Z])/,'\1_\2').
|
15
|
+
gsub('__','/').
|
16
|
+
gsub('::','/').
|
17
|
+
gsub(/\s+/, ''). # spaces are bad form
|
18
|
+
gsub(/[?%*:|"<>.]+/, ''). # reserved characters
|
19
|
+
downcase
|
20
|
+
end
|
21
|
+
|
22
|
+
# Compare to Rails definition:
|
23
|
+
#
|
24
|
+
# gsub(/__/, '/').
|
25
|
+
# gsub(/::/, '/').
|
26
|
+
# gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
27
|
+
# gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
28
|
+
# tr("-", "_").
|
29
|
+
# downcase
|
30
|
+
#
|
31
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module RSpec
|
2
|
+
module MeetContractMatcher
|
3
|
+
extend RSpec::Matchers::DSL
|
4
|
+
Runner = ::BloodContracts::Runner
|
5
|
+
Debugger = ::BloodContracts::Debugger
|
6
|
+
|
7
|
+
matcher :meet_contract_rules do |args|
|
8
|
+
match do |subject|
|
9
|
+
runner = ENV["debug"] ? Debugger : Runner
|
10
|
+
|
11
|
+
@_contract_runner = runner.new(
|
12
|
+
subject,
|
13
|
+
context: self,
|
14
|
+
suite: build_suite(args || subject),
|
15
|
+
iterations: @_iterations,
|
16
|
+
time_to_run: @_time_to_run,
|
17
|
+
stop_on_unexpected: @_halt_on_unexpected,
|
18
|
+
)
|
19
|
+
@_contract_runner.call
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_suite(args)
|
23
|
+
suite = nil
|
24
|
+
if args.respond_to?(:to_contract_suite)
|
25
|
+
suite = args.to_contract_suite(name: _example_name_to_path)
|
26
|
+
elsif args.respond_to?(:to_hash) && args.fetch(:contract) { false }
|
27
|
+
suite = ::BloodContracts::Suite.new(storage: new_storage)
|
28
|
+
suite.contract = args[:contract]
|
29
|
+
else
|
30
|
+
raise "Matcher arguments is not a Blood Contract"
|
31
|
+
end
|
32
|
+
suite.data_generator = @_generator if @_generator
|
33
|
+
suite
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_storage
|
37
|
+
storage = Storage.new(contract_name: _example_name_to_path)
|
38
|
+
storage.input_writer = _input_writer if _input_writer
|
39
|
+
storage.output_writer = _output_writer if _output_writer
|
40
|
+
if @_input_serializer
|
41
|
+
storage.input_serializer = @_input_serializer
|
42
|
+
end
|
43
|
+
if @_output_serializer
|
44
|
+
storage.output_serializer = @_output_serializer
|
45
|
+
end
|
46
|
+
storage
|
47
|
+
end
|
48
|
+
|
49
|
+
def _example_name_to_path
|
50
|
+
method_missing(:class)
|
51
|
+
.name
|
52
|
+
.sub("RSpec::ExampleGroups::", "")
|
53
|
+
.pathize
|
54
|
+
end
|
55
|
+
|
56
|
+
def _input_writer
|
57
|
+
input_writer = @_writers.to_h[:input]
|
58
|
+
input_writer ||= :input_writer if defined? self.input_writer
|
59
|
+
input_writer
|
60
|
+
end
|
61
|
+
|
62
|
+
def _output_writer
|
63
|
+
output_writer = @_writers.to_h[:output]
|
64
|
+
output_writer ||= :output_writer if defined? self.output_writer
|
65
|
+
output_writer
|
66
|
+
end
|
67
|
+
|
68
|
+
supports_block_expectations
|
69
|
+
|
70
|
+
failure_message { @_contract_runner.failure_message }
|
71
|
+
|
72
|
+
description { @_contract_runner.description }
|
73
|
+
|
74
|
+
chain :using_generator do |generator|
|
75
|
+
if generator.respond_to?(:to_sym)
|
76
|
+
@_generator = method(generator.to_sym)
|
77
|
+
else
|
78
|
+
raise ArgumentError unless generator.respond_to?(:call)
|
79
|
+
@_generator = generator
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
chain :during_n_iterations_run do |iterations|
|
84
|
+
@_iterations = Integer(iterations)
|
85
|
+
end
|
86
|
+
|
87
|
+
chain :during_n_seconds_run do |time_to_run|
|
88
|
+
@_time_to_run = Float(time_to_run)
|
89
|
+
end
|
90
|
+
|
91
|
+
chain :halt_on_unexpected do
|
92
|
+
@_halt_on_unexpected = true
|
93
|
+
end
|
94
|
+
|
95
|
+
chain :serialize_input do |serializer|
|
96
|
+
@_input_serializer = serializer
|
97
|
+
end
|
98
|
+
|
99
|
+
chain :serialize_output do |serializer|
|
100
|
+
@_output_serializer = serializer
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: blood_contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Dolganov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-initializer
|
@@ -25,19 +25,33 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: nanoid
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: hashie
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
83
111
|
description: Ruby gem to define and validate behavior of API using contracts.
|
84
112
|
email:
|
85
113
|
- dolganov@evl.ms
|
@@ -98,10 +126,21 @@ files:
|
|
98
126
|
- bin/setup
|
99
127
|
- blood_contracts.gemspec
|
100
128
|
- lib/blood_contracts.rb
|
129
|
+
- lib/blood_contracts/base_contract.rb
|
130
|
+
- lib/blood_contracts/contracts/description.rb
|
131
|
+
- lib/blood_contracts/contracts/matcher.rb
|
132
|
+
- lib/blood_contracts/contracts/validator.rb
|
133
|
+
- lib/blood_contracts/debugger.rb
|
101
134
|
- lib/blood_contracts/runner.rb
|
135
|
+
- lib/blood_contracts/statistics.rb
|
102
136
|
- lib/blood_contracts/storage.rb
|
137
|
+
- lib/blood_contracts/storages/base_backend.rb
|
138
|
+
- lib/blood_contracts/storages/file_backend.rb
|
139
|
+
- lib/blood_contracts/storages/serializer.rb
|
103
140
|
- lib/blood_contracts/suite.rb
|
104
141
|
- lib/blood_contracts/version.rb
|
142
|
+
- lib/extensions/string.rb
|
143
|
+
- lib/rspec/meet_contract_matcher.rb
|
105
144
|
homepage: https://github.com/sclinede/blood_contracts
|
106
145
|
licenses:
|
107
146
|
- MIT
|
@@ -114,7 +153,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
153
|
requirements:
|
115
154
|
- - ">="
|
116
155
|
- !ruby/object:Gem::Version
|
117
|
-
version:
|
156
|
+
version: 2.2.0
|
118
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
158
|
requirements:
|
120
159
|
- - ">="
|
@@ -122,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
161
|
version: '0'
|
123
162
|
requirements: []
|
124
163
|
rubyforge_project:
|
125
|
-
rubygems_version: 2.6
|
164
|
+
rubygems_version: 2.7.6
|
126
165
|
signing_key:
|
127
166
|
specification_version: 4
|
128
167
|
summary: Ruby gem to define and validate behavior of API using contracts.
|