blood_contracts 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|