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.
@@ -1,5 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.4.1
5
- before_install: gem install bundler -v 1.16.1
@@ -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