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.
@@ -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