lib/blood_contracts/runner.rb in blood_contracts-0.1.0 vs lib/blood_contracts/runner.rb in blood_contracts-0.2.0
- old
+ new
@@ -1,95 +1,91 @@
+require_relative "contracts/validator"
+require_relative "contracts/matcher"
+require_relative "contracts/description"
+
module BloodContracts
class Runner
extend Dry::Initializer
param :checking_proc
- option :context, optional: true
option :suite
+ option :storage, default: -> { suite.storage }
option :iterations, ->(v) do
v = ENV["iterations"] if ENV["iterations"]
v.to_i.positive? ? v.to_i : 1
end, default: -> { 1 }
-
-
option :time_to_run, ->(v) do
v = ENV["duration"] if ENV["duration"]
v.to_f if v.to_f.positive?
end, optional: true
+ option :context, optional: true
option :stop_on_unexpected, default: -> { false }
+ option :statistics, default: -> { Statistics.new(iterations) }
+ option :matcher, default: -> { Contracts::Matcher.new(suite.contract) }
+ option :validator, default: -> { Contracts::Validator.new(suite.contract) }
+ option :contract_description, default: -> do
+ Contracts::Description.call(suite.contract)
+ end
+
def call
iterate do
- unexpected = match_rules(*run) do |input, output, rules|
- suite.storage.save_run(
- input: input, output: output, rules: rules, context: context,
- )
- end.empty?
- throw :unexpected_behavior, :stop if stop_on_unexpected && unexpected
+ next if match_rules?(matches_storage: statistics) do
+ input = suite.data_generator.call
+ [input, checking_proc.call(input)]
+ end
+ throw :unexpected_behavior, :stop if stop_on_unexpected
end
- valid?
- end
-
- def valid?
return if stopped_by_unexpected_behavior?
- return if found_unexpected_behavior?
- last_run_stats = run_stats
- expectations_checks.all? do |rule, check|
- percent = last_run_stats[rule.name]&.percent || 0.0
- check.call(percent, rule)
- end
+ validator.valid?(statistics)
end
- def found_unexpected_behavior?
- run_stats.key?(Storage::UNDEFINED_RULE)
- end
-
+ # FIXME: Move to locales
def failure_message
intro = "expected that given Proc would meet the contract:"
- if found_unexpected_behavior?
+ if validator.expected_behavior?
"#{intro}\n#{contract_description}\n"\
- " during #{iterations} run(s) but got unexpected behavior.\n\n"\
- "For further investigations open: #{unexpected_behavior_report_path}/"
+ " during #{iterations} run(s) but got:\n#{statistics}\n\n"\
+ "For further investigations open: #{storage.suggestion}"
else
"#{intro}\n#{contract_description}\n"\
- " during #{iterations} run(s) but got:\n#{stats_description}\n\n"\
- "For further investigations open: #{suite.storage.path}"
+ " during #{iterations} run(s) but got unexpected behavior.\n\n"\
+ "For further investigations open: #{storage.unexpected_suggestion}"
end
end
- def unexpected_behavior_report_path
- File.join(suite.storage.path, Storage::UNDEFINED_RULE.to_s)
- end
-
+ # FIXME: Move to locales
def description
"meet the contract:\n#{contract_description} \n"\
- " during #{iterations} run(s). Stats:\n#{stats_description}\n\n"\
- "For further investigations open: #{suite.storage.path}\n"
+ " during #{iterations} run(s). Stats:\n#{statistics}\n\n"\
+ "For further investigations open: #{storage.suggestion}\n"
end
- private
+ protected
- def run
- input = suite.data_generator.call
- [input, checking_proc.call(input)]
+ def match_rules?(matches_storage:)
+ matcher.call(*yield, storage: matches_storage) do |rules, options|
+ storage.store(options: options, rules: rules, context: context)
+ end
+ rescue StandardError => error
+ # FIXME: Possible recursion?
+ # Write test about error in the storage#store (e.g. writing error)
+ store_exception(error, input, output, context)
+ raise
end
def stopped_by_unexpected_behavior?
@_stopped_by_unexpected_behavior == :stop
end
- def stats
- suite.storage.stats
- end
-
def iterate
- run_iterations = iterations
+ run_iterations ||= iterations
if time_to_run
run_iterations = iterations_count_from_time_to_run { yield }
@iterations = run_iterations + 1
end
@@ -102,82 +98,18 @@
def iterations_count_from_time_to_run
time_per_action = Benchmark.measure { yield }
(time_to_run / time_per_action.real).ceil
end
- def match_rules(input, output)
- matched_rules = suite.contract.select do |_name, rule|
- rule.check.call(input, output)
- end.keys
- matched_rules = [Storage::UNDEFINED_RULE] if matched_rules.empty?
- yield(input, output, matched_rules)
-
- matched_rules
- # FIXME: Possible recursion?
- # Write test about error in the yield (e.g. writing error)
- rescue => e
- yield [Storage::UNDEFINED_RULE]
- raise e
- end
-
- def threshold_check(value, rule)
- value > rule.threshold
- end
-
- def limit_check(value, rule)
- value <= rule.limit
- end
-
- def expectations_checks
- Hash[
- suite.contract.map do |name, rule|
- if rule.threshold
- [rule.merge(name: name), method(:threshold_check)]
- elsif rule.limit
- [rule.merge(name: name), method(:limit_check)]
- else
- nil
- end
- end.compact
- ]
- end
-
- def contract_description
- suite.contract.map do |name, rule|
- rule_description = " - '#{name}' "
- if rule.threshold
- rule_description << <<~TEXT
- in more then #{(rule.threshold * 100).round(2)}% of cases;
- TEXT
- elsif rule.limit
- rule_description << <<~TEXT
- in less then #{(rule.limit * 100).round(2)}% of cases;
- TEXT
- else
- next
- end
- rule_description
- end.compact.join
- end
-
- def stats_description
- run_stats.map do |name, occasions|
- " - '#{name}' happened #{occasions.times} time(s) "\
- "(#{(occasions.percent * 100).round(2)}% of the time)"
- end.join("; \n")
- end
-
- def run_stats
- Hash[
- stats.map do |rule_name, times|
- [
- rule_name,
- Hashie::Mash.new(
- times: times,
- percent: (times.to_f / iterations),
- ),
- ]
- end
- ]
+ def store_exception(error, input, output, context)
+ storage.store(
+ options: {
+ input: input,
+ output: output || {},
+ meta: {exception: error},
+ },
+ rules: [Storage::EXCEPTION_CAUGHT],
+ context: context,
+ )
end
end
end