blood_contracts 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +63 -119
- data/Rakefile +30 -0
- data/bin/console +3 -2
- data/blood_contracts.gemspec +10 -27
- data/lib/blood_contracts.rb +3 -38
- data/spec/blood_contracts_spec.rb +36 -0
- data/spec/spec_helper.rb +16 -0
- metadata +20 -91
- data/.travis.yml +0 -5
- data/lib/blood_contracts/base_contract.rb +0 -42
- data/lib/blood_contracts/contracts/description.rb +0 -25
- data/lib/blood_contracts/contracts/iterator.rb +0 -34
- data/lib/blood_contracts/contracts/matcher.rb +0 -38
- data/lib/blood_contracts/contracts/statistics.rb +0 -34
- data/lib/blood_contracts/contracts/validator.rb +0 -47
- data/lib/blood_contracts/debugger.rb +0 -39
- data/lib/blood_contracts/runner.rb +0 -92
- data/lib/blood_contracts/storage.rb +0 -79
- data/lib/blood_contracts/storages/base_backend.rb +0 -59
- data/lib/blood_contracts/storages/file_backend.rb +0 -108
- data/lib/blood_contracts/storages/serializer.rb +0 -38
- data/lib/blood_contracts/suite.rb +0 -41
- data/lib/blood_contracts/version.rb +0 -3
- data/lib/extensions/string.rb +0 -31
- data/lib/rspec/meet_contract_matcher.rb +0 -106
data/.travis.yml
DELETED
@@ -1,42 +0,0 @@
|
|
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
|
@@ -1,25 +0,0 @@
|
|
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
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module BloodContracts
|
2
|
-
module Contracts
|
3
|
-
class Iterator
|
4
|
-
extend Dry::Initializer
|
5
|
-
|
6
|
-
param :iterations, ->(v) do
|
7
|
-
v = ENV["iterations"] if ENV["iterations"]
|
8
|
-
v.to_i.positive? ? v.to_i : 1
|
9
|
-
end
|
10
|
-
param :time_to_run, ->(v) do
|
11
|
-
v = ENV["duration"] if ENV["duration"]
|
12
|
-
v.to_f if v.to_f.positive?
|
13
|
-
end, optional: true
|
14
|
-
|
15
|
-
def next
|
16
|
-
return iterations.times { yield } unless time_to_run
|
17
|
-
|
18
|
-
@iterations = iterations_from_time_to_run { yield }
|
19
|
-
[iterations - 1, 0].max.times { yield }
|
20
|
-
end
|
21
|
-
|
22
|
-
def count
|
23
|
-
@iterations
|
24
|
-
end
|
25
|
-
|
26
|
-
protected
|
27
|
-
|
28
|
-
def iterations_from_time_to_run
|
29
|
-
time_per_action = Benchmark.measure { yield }
|
30
|
-
(time_to_run / time_per_action.real).ceil
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,38 +0,0 @@
|
|
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
|
@@ -1,34 +0,0 @@
|
|
1
|
-
module BloodContracts
|
2
|
-
module Contracts
|
3
|
-
class Statistics
|
4
|
-
extend Dry::Initializer
|
5
|
-
param :iterator
|
6
|
-
option :storage, default: -> { Hash.new(0) }
|
7
|
-
|
8
|
-
def store(rule)
|
9
|
-
storage[rule] += 1
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_h
|
13
|
-
Hash[storage.map { |rule_name, times| [rule_name, rule_stats(times)] }]
|
14
|
-
end
|
15
|
-
|
16
|
-
def to_s
|
17
|
-
to_h.map do |name, occasions|
|
18
|
-
" - '#{name}' happened #{occasions.times} time(s) "\
|
19
|
-
"(#{(occasions.percent * 100).round(2)}% of the time)"
|
20
|
-
end.join("; \n")
|
21
|
-
end
|
22
|
-
|
23
|
-
def found_unexpected_behavior?
|
24
|
-
storage.key?(Storage::UNDEFINED_RULE)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def rule_stats(times)
|
30
|
-
Hashie::Mash.new(times: times, percent: (times.to_f / iterator.count))
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,47 +0,0 @@
|
|
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
|
@@ -1,39 +0,0 @@
|
|
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,92 +0,0 @@
|
|
1
|
-
require_relative "contracts/validator"
|
2
|
-
require_relative "contracts/matcher"
|
3
|
-
require_relative "contracts/description"
|
4
|
-
require_relative "contracts/iterator"
|
5
|
-
require_relative "contracts/statistics"
|
6
|
-
|
7
|
-
module BloodContracts
|
8
|
-
class Runner
|
9
|
-
extend Dry::Initializer
|
10
|
-
|
11
|
-
param :checking_proc
|
12
|
-
|
13
|
-
option :suite
|
14
|
-
option :storage, default: -> { suite.storage }
|
15
|
-
|
16
|
-
option :iterations, default: -> { 1 }
|
17
|
-
option :time_to_run, optional: true
|
18
|
-
option :stop_on_unexpected, default: -> { false }
|
19
|
-
option :iterator, default: -> do
|
20
|
-
Contracts::Iterator.new(iterations, time_to_run)
|
21
|
-
end
|
22
|
-
|
23
|
-
option :context, optional: true
|
24
|
-
|
25
|
-
option :statistics, default: -> { Contracts::Statistics.new(iterator) }
|
26
|
-
option :matcher, default: -> { Contracts::Matcher.new(suite.contract) }
|
27
|
-
option :validator, default: -> { Contracts::Validator.new(suite.contract) }
|
28
|
-
option :contract_description, default: -> do
|
29
|
-
Contracts::Description.call(suite.contract)
|
30
|
-
end
|
31
|
-
|
32
|
-
def call
|
33
|
-
return false if :halt == catch(:unexpected_behavior) do
|
34
|
-
iterator.next do
|
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, :halt if stop_on_unexpected
|
40
|
-
end
|
41
|
-
end
|
42
|
-
validator.valid?(statistics)
|
43
|
-
end
|
44
|
-
|
45
|
-
# FIXME: Move to locales
|
46
|
-
def failure_message
|
47
|
-
intro = "expected that given Proc would meet the contract:"
|
48
|
-
|
49
|
-
if stats.unexpected_behavior?
|
50
|
-
"#{intro}\n#{contract_description}\n"\
|
51
|
-
" during #{iterator.count} run(s) but got unexpected behavior.\n\n"\
|
52
|
-
"For further investigations open: #{storage.unexpected_suggestion}"
|
53
|
-
else
|
54
|
-
"#{intro}\n#{contract_description}\n"\
|
55
|
-
" during #{iterator.count} run(s) but got:\n#{statistics}\n\n"\
|
56
|
-
"For further investigations open: #{storage.suggestion}"
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
# FIXME: Move to locales
|
61
|
-
def description
|
62
|
-
"meet the contract:\n#{contract_description} \n"\
|
63
|
-
" during #{iterator.count} run(s). Stats:\n#{statistics}\n\n"\
|
64
|
-
"For further investigations open: #{storage.suggestion}\n"
|
65
|
-
end
|
66
|
-
|
67
|
-
protected
|
68
|
-
|
69
|
-
def match_rules?(matches_storage:)
|
70
|
-
matcher.call(*yield, storage: matches_storage) do |rules, options|
|
71
|
-
storage.store(options: options, rules: rules, context: context)
|
72
|
-
end
|
73
|
-
rescue StandardError => error
|
74
|
-
# FIXME: Possible recursion?
|
75
|
-
# Write test about error in the storage#store (e.g. writing error)
|
76
|
-
store_exception(error, input, output, context)
|
77
|
-
raise
|
78
|
-
end
|
79
|
-
|
80
|
-
def store_exception(error, input, output, context)
|
81
|
-
storage.store(
|
82
|
-
options: {
|
83
|
-
input: input,
|
84
|
-
output: output || {},
|
85
|
-
meta: {exception: error},
|
86
|
-
},
|
87
|
-
rules: [Storage::EXCEPTION_CAUGHT],
|
88
|
-
context: context,
|
89
|
-
)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
require_relative "./storages/base_backend.rb"
|
2
|
-
require_relative "./storages/file_backend.rb"
|
3
|
-
require_relative "./storages/serializer.rb"
|
4
|
-
|
5
|
-
module BloodContracts
|
6
|
-
class Storage
|
7
|
-
extend Dry::Initializer
|
8
|
-
extend Forwardable
|
9
|
-
|
10
|
-
Serializer = BloodContracts::Storages::Serializer
|
11
|
-
FileBackend = BloodContracts::Storages::FileBackend
|
12
|
-
|
13
|
-
option :contract_name
|
14
|
-
|
15
|
-
DEFAULT_WRITER = -> (options) do
|
16
|
-
"INPUT:\n#{options.input}\n\n#{'=' * 90}\n\nOUTPUT:\n#{options.output}"
|
17
|
-
end
|
18
|
-
|
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
|
41
|
-
end
|
42
|
-
|
43
|
-
def input_serializer=(serializer)
|
44
|
-
@input_serializer = Serializer.call(serializer)
|
45
|
-
end
|
46
|
-
|
47
|
-
def output_serializer=(serializer)
|
48
|
-
@output_serializer = Serializer.call(serializer)
|
49
|
-
end
|
50
|
-
|
51
|
-
def meta_serializer=(serializer)
|
52
|
-
@meta_serializer = Serializer.call(serializer)
|
53
|
-
end
|
54
|
-
|
55
|
-
def input_writer=(writer)
|
56
|
-
@input_writer = self.class.valid_writer(writer)
|
57
|
-
end
|
58
|
-
|
59
|
-
def output_writer=(writer)
|
60
|
-
@output_writer = self.class.valid_writer(writer)
|
61
|
-
end
|
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) }
|
68
|
-
end
|
69
|
-
|
70
|
-
def store(options:, rules:, context:)
|
71
|
-
options = Hashie::Mash.new(options)
|
72
|
-
|
73
|
-
Array(rules).each do |rule_name|
|
74
|
-
describe_sample(rule_name, options, context)
|
75
|
-
serialize_sample(rule_name, options, context)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|