forked from instructure/canvas-lms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgreat_expectations.rb
More file actions
187 lines (160 loc) · 5.45 KB
/
Copy pathgreat_expectations.rb
File metadata and controls
187 lines (160 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
# Ensure we aren't doing silly things with expectations, such as:
#
# 1. `expect` in a `before` ... `before` implies it's before the spec, so
# why are you testing things there?
# 2. `expect` without a `to` / `not_to` ... it will never get checked
# 3. specs with no expectations... what's the point?
module GreatExpectations
class Error < StandardError
def self.for(message, location = nil)
error = new(message)
bt = caller
# not a legit backtrace, but this way the rspec error/context
# will point right at the example in the file
bt.unshift "#{File.expand_path(location)}:in block in <top (required)>'" if location
error.set_backtrace(bt)
error
end
end
# default behavior, can be overridden with `.with_config`
CONFIG = {
# what to do if there's an `expect` in a `before`
EARLY: :raise,
# what to do if an `expect` has no `to`
UNCHECKED: :raise,
# what to do if a spec has no `expect`s
MISSING: :warn
}.freeze
module Example
# allow expectations at the last possible second (right after the
# inner-most before hooks run)
def run_before_example
super
GreatExpectations.example_started(self)
end
# immediately before running any after hooks, ensure the spec had some
# expectations. this includes mocha/rspec-mocks which will be verified
# in the super call
def run_after_example
GreatExpectations.example_finished
super
end
end
module AssertionDelegator
def assert(*)
GreatExpectations.expectation_checked
super
end
end
module ExpectationTarget
def initialize(*)
GreatExpectations.expectation_created(self)
super
end
def to(*)
GreatExpectations.expectation_checked(self)
super
end
def not_to(*)
GreatExpectations.expectation_checked(self)
super
end
alias to_not not_to
end
class << self
attr_accessor :config
attr_accessor :current_example
attr_accessor :expectation_count
def install!
self.config = CONFIG
::RSpec::Core::Example.prepend Example
::RSpec::Expectations::ExpectationTarget.prepend ExpectationTarget
::RSpec::Rails::MinitestAssertionAdapter::AssertionDelegator.prepend AssertionDelegator
end
def with_config(config)
orig_config = @config
@config = orig_config.merge(config)
yield
ensure
@config = orig_config
end
def expectation_created(expectation)
assert_not_early!
unchecked_expectations << expectation
end
def expectation_checked(expectation = nil)
unchecked_expectations.delete(expectation) if expectation
self.expectation_count += 1
end
def unchecked_expectations
@unchecked_expectations ||= Set.new
end
def example_started(example)
self.current_example = example
self.expectation_count = 0
end
def example_finished
return if current_example.nil? || # like if we `skip` in a before
current_example.exception ||
current_example.skipped? ||
current_example.pending?
assert_not_unchecked!
assert_not_missing!
rescue Error
current_example.set_exception($ERROR_INFO)
ensure
self.current_example = nil
unchecked_expectations.clear
end
def assert_not_early!
return if current_example
generate_error config[:EARLY], "Don't `expect` outside of the spec itself. `before`/`after` should only be used for setup/teardown"
end
def assert_not_unchecked!
return if unchecked_expectations.empty?
generate_error config[:UNCHECKED], "This spec has unchecked expectations, i.e. you forgot to call `to` or `not_to`", current_example.location
end
def assert_not_missing!
# vanilla expectation
return if expectation_count > 0
# rspec message expectations
return if ::RSpec::Mocks.space.proxies.any? do |_, proxy|
proxy.instance_variable_get(:@method_doubles).any? do |_, double|
double.expectations.any?
end
end
return if ::RSpec::Mocks.space.any_instance_recorders.any? do |_, recorder|
recorder.instance_variable_get(:@expectation_set)
end
# mocha expectations
return if ::Mocha::Mockery.instance.send(:expectations).any? do |expectation|
expectation.instance_variable_get(:@cardinality).needs_verifying?
end
generate_error config[:MISSING], "This spec has no expectations. Add one!", current_example.location
end
def generate_error(action, message, location = nil)
if action == :raise
raise Error.for(message, location)
else
$stderr.puts "\e[31mWarning: #{message}"
$stderr.puts "See: " + (location || CallStackUtils.best_line_for(caller)) + "\e[0m"
end
end
end
end