Class: Recheck::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/recheck/runner.rb

Constant Summary collapse

PASSTHROUGH_EXCEPTIONS =
[
  # ours
  HookDidNotYield, HookYieldedTwice, UnexpectedHookYield,
  # Ruby's
  NoMemoryError, SignalException, SystemExit
]

Instance Method Summary collapse

Constructor Details

#initialize(checkers: [], reporters: []) ⇒ Runner

Returns a new instance of Runner.



46
47
48
49
50
51
# File 'lib/recheck/runner.rb', line 46

def initialize(checkers: [], reporters: [])
  # maintain order and we want to check/report in user-provided order; Set lacks .reverse
  @checkers = checkers.uniq
  @reporters = reporters.uniq
  @yields = Yields.new
end

Instance Method Details

#cant_run(reporters:, checker:, queries:, checks:, type:) ⇒ Object

only for calling from inside run()



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/recheck/runner.rb', line 69

def cant_run reporters:, checker:, queries:, checks:, type:
  checker_counts = CountStats.new
  checker_counts.increment type
  @total_counts << checker_counts

  error = Error.new(checker:, query: nil, check: nil, record: nil, type:, exception: nil)
  reduce(reporters:, hook: :around_checker, kwargs: {checker:, queries:, checks:}) do
    reporters.each { _1.halt(checker:, query: nil, check: nil, error:) }
    checker_counts
  end
end

#reduce(hook:, kwargs: {}, reporters: [], &blk) ⇒ Object

compose reporter hooks so they each see the block fire once at ‘yield’



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/recheck/runner.rb', line 54

def reduce(hook:, kwargs: {}, reporters: [], &blk)
  reporters.reverse.reduce(blk) do |proc, reporter|
    @yields.expect(hook:, reporter:)
    -> {
      result = nil
      reporter.public_send(hook, **kwargs) {
        @yields.ran(hook:, reporter:)
        result = proc.call.freeze
      }
      result
    }
  end.call
end

#runObject

n queries * n check methods * n records = O(1) right?



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/recheck/runner.rb', line 82

def run
  @total_counts = CountStats.new
  # All happy families are alike; each unhappy family is unhappy in its own way.
  pass = Pass.new

  # for want of a monad...
  reduce(reporters: @reporters, hook: :around_run, kwargs: {checkers: @checkers}) do
    # for each checker...
    @checkers.each do |checker|
      checker_counts = CountStats.new
      if !checker.class.respond_to?(:query_methods)
        cant_run(reporters: @reporters, checker:, type: :no_query_methods, queries: nil, checks: nil)
        next
      end
      if (queries = checker.class.query_methods).empty?
        cant_run(reporters: @reporters, checker:, type: :no_queries, queries:, checks: nil)
        next
      end

      if !checker.class.respond_to?(:check_methods)
        cant_run(reporters: @reporters, checker:, type: :no_check_methods, queries:, checks: nil)
        next
      end
      if (checks = checker.class.check_methods).empty?
        cant_run(reporters: @reporters, checker:, type: :no_checks, queries:, checks:)
        next
      end

      reduce(reporters: @reporters, hook: :around_checker, kwargs: {checker:, queries:, checks:}) do
        # for each query_...
        queries.each do |query|
          reduce(reporters: @reporters, hook: :around_query, kwargs: {checker:, query:, checks:}) do
            checker_counts.increment :queries
            # for each record...
            # TODO: must handle if the query method yields (find_each) OR returns (current)
            (checker.public_send(query) || []).each do |record|
              # for each check_method...
              checks.each do |check|
                raw_result = nil
                reduce(reporters: @reporters, hook: :around_check, kwargs: {checker:, query:, check:, record:}) do
                  raw_result = checker.public_send(check, record)
                  result = raw_result ? pass : Error.new(checker:, query:, check:, record:, type: :fail, exception: nil)

                  checker_counts.increment(result.type)
                  break if checker_counts.reached_blanket_failure?

                  result
                rescue *PASSTHROUGH_EXCEPTIONS
                  raise
                rescue => e
                  Error.new(checker:, query:, check:, record:, type: :exception, exception: e)
                end
              end
              @yields.raise_unless_all_reporters_yielded(hook: :around_check)

              # if the first 20 error out, halt the check method, it's probably buggy
              if checker_counts.reached_blanket_failure?
                checker_counts.increment :blanket

                error = Error.new(checker:, query:, check: nil, record: nil, type: :blanket, exception: nil)
                @reporters.each { _1.halt(checker:, query:, check: nil, error:) }

                break
              end
            end
            nil # yield nothing around_query
          end
          @yields.raise_unless_all_reporters_yielded(hook: :around_query)
        rescue *PASSTHROUGH_EXCEPTIONS
          raise
        rescue => e
          # puts "outer rescue: #{e.inspect}"
          @reporters.each do |check_reporter|
            result = Error.new(checker:, query:, check: nil, record: nil, type: :exception, exception: e)
            check_reporter.around_check(checker:, query: query, check: nil, record: nil) { result }
          end
        end
        checker_counts
      end
      @yields.raise_unless_all_reporters_yielded(hook: :around_checker)
      @total_counts << checker_counts
    end
    @total_counts
  end
  @yields.raise_unless_all_reporters_yielded(hook: :around_run)
  @total_counts
end