spectre-core 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/spectre.rb ADDED
@@ -0,0 +1,434 @@
1
+ module Spectre
2
+ module Version
3
+ MAJOR = 1
4
+ MINOR = 8
5
+ TINY = 0
6
+ end
7
+
8
+ VERSION = [Version::MAJOR, Version::MINOR, Version::TINY].compact * '.'
9
+
10
+
11
+ ###########################################
12
+ # Custom Exceptions
13
+ ###########################################
14
+
15
+
16
+ class ExpectationFailure < Exception
17
+ attr_reader :expectation
18
+
19
+ def initialize message, expectation
20
+ super message
21
+ @expectation = expectation
22
+ end
23
+ end
24
+
25
+
26
+ ###########################################
27
+ # Internal Classes
28
+ ###########################################
29
+
30
+
31
+ # https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
32
+ class DslClass
33
+ def _evaluate &block
34
+ @__bound_self__ = eval 'self', block.binding
35
+ instance_eval(&block)
36
+ end
37
+
38
+ def _execute args, &block
39
+ @__bound_self__ = eval 'self', block.binding
40
+ instance_exec(args, &block)
41
+ end
42
+
43
+ def method_missing method, *args, **kwargs, &block
44
+ if @__bound_self__.respond_to? method
45
+ @__bound_self__.send method, *args, **kwargs, &block
46
+ else
47
+ Delegator.redirect method, *args, **kwargs, &block
48
+ end
49
+ end
50
+ end
51
+
52
+
53
+ class Subject
54
+ attr_reader :name, :desc, :specs
55
+
56
+ def initialize desc
57
+ @desc = desc
58
+ @specs = []
59
+ @name = desc.downcase.gsub(/[^a-z0-9]+/, '_')
60
+ end
61
+
62
+ def add_spec desc, tags, data, block, context, file
63
+ name = @name + '-' + (@specs.length+1).to_s
64
+ @specs << Spec.new(name, self, desc, tags, data, block, context, file)
65
+ end
66
+ end
67
+
68
+
69
+ class Spec
70
+ attr_reader :name, :subject, :context, :desc, :tags, :data, :block, :file
71
+
72
+ def initialize name, subject, desc, tags, data, block, context, file
73
+ @name = name
74
+ @context = context
75
+ @data = data
76
+ @subject = subject
77
+ @desc = desc
78
+ @tags = tags
79
+ @block = block
80
+ @file = file
81
+ end
82
+
83
+ def full_desc
84
+ @subject.desc + ' ' + desc
85
+ end
86
+ end
87
+
88
+
89
+ class RunInfo
90
+ attr_accessor :spec, :data, :started, :finished, :error, :failure, :skipped, :log, :properties
91
+
92
+ def initialize spec, data=nil
93
+ @spec = spec
94
+ @data = data
95
+ @started = nil
96
+ @finished = nil
97
+ @error = nil
98
+ @failure = nil
99
+ @skipped = false
100
+ @log = []
101
+ @properties = {}
102
+ end
103
+
104
+ def duration
105
+ @finished - @started
106
+ end
107
+
108
+ def skipped?
109
+ @skipped
110
+ end
111
+
112
+ def failed?
113
+ @error != nil
114
+ end
115
+ end
116
+
117
+
118
+ class Runner
119
+ @@current
120
+
121
+ def self.current
122
+ @@current
123
+ end
124
+
125
+ def run specs
126
+ runs = []
127
+
128
+ specs.group_by { |x| x.subject }.each do |subject, spec_group|
129
+ Logger.log_subject subject do
130
+ spec_group.group_by { |x| x.context }.each do |context, specs|
131
+ Logger.log_context(context) do
132
+ runs.concat run_context(context, specs)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ runs
139
+ end
140
+
141
+ private
142
+
143
+ def run_context context, specs
144
+ runs = []
145
+
146
+ if context.__setup_blocks.count > 0
147
+ setup_run = run_blocks('setup', context, context.__setup_blocks)
148
+ runs << setup_run
149
+ return runs if setup_run.error or setup_run.failure
150
+ end
151
+
152
+ begin
153
+ specs.each do |spec|
154
+ if spec.data.length > 0
155
+ spec.data.each do |data|
156
+ Logger.log_spec(spec, data) do
157
+ runs << run_spec(spec, data)
158
+ end
159
+ end
160
+ else
161
+ Logger.log_spec(spec) do
162
+ runs << run_spec(spec)
163
+ end
164
+ end
165
+ end
166
+ ensure
167
+ if context.__teardown_blocks.count > 0
168
+ runs << run_blocks('teardown', context, context.__teardown_blocks)
169
+ end
170
+ end
171
+
172
+ runs
173
+ end
174
+
175
+ def run_blocks name, context, blocks
176
+ ctx = SpecContext.new context.__subject, name
177
+ spec = Spec.new name, context.__subject, name, [], nil, nil, ctx, nil
178
+
179
+ run_info = RunInfo.new spec
180
+
181
+ @@current = run_info
182
+
183
+ run_info.started = Time.now
184
+
185
+ Logger.log_context ctx do
186
+ begin
187
+ blocks.each do |block|
188
+ block.call
189
+ end
190
+
191
+ run_info.finished = Time.now
192
+
193
+ rescue ExpectationFailure => e
194
+ run_info.failure = e
195
+
196
+ rescue Exception => e
197
+ run_info.error = e
198
+ Logger.log_error spec, e
199
+
200
+ end
201
+ end
202
+
203
+ run_info.finished = Time.now
204
+
205
+ @@current = nil
206
+
207
+ run_info
208
+ end
209
+
210
+ def run_spec spec, data=nil
211
+ run_info = RunInfo.new spec, data
212
+
213
+ @@current = run_info
214
+
215
+ run_info.started = Time.now
216
+
217
+ begin
218
+ if spec.context.__before_blocks.count > 0
219
+ before_ctx = SpecContext.new spec.subject, 'before'
220
+
221
+ Logger.log_context before_ctx do
222
+ spec.context.__before_blocks.each do |block|
223
+ block.call data
224
+ end
225
+ end
226
+ end
227
+
228
+ spec.block.call data
229
+
230
+ rescue ExpectationFailure => e
231
+ run_info.failure = e
232
+
233
+ rescue Interrupt
234
+ run_info.skipped = true
235
+ Logger.log_skipped spec
236
+
237
+ rescue Exception => e
238
+ run_info.error = e
239
+ Logger.log_error spec, e
240
+
241
+ ensure
242
+ if spec.context.__after_blocks.count > 0
243
+ after_ctx = SpecContext.new spec.subject, 'after'
244
+
245
+ Logger.log_context after_ctx do
246
+ begin
247
+ spec.context.__after_blocks.each do |block|
248
+ block.call
249
+ end
250
+
251
+ run_info.finished = Time.now
252
+
253
+ rescue ExpectationFailure => e
254
+ run_info.failure = e
255
+
256
+ rescue Exception => e
257
+ run_info.error = e
258
+ Logger.log_error spec, e
259
+
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ run_info.finished = Time.now
266
+
267
+ @@current = nil
268
+
269
+ run_info
270
+ end
271
+ end
272
+
273
+
274
+ ###########################################
275
+ # DSL Classes
276
+ ###########################################
277
+
278
+
279
+ class SpecContext < DslClass
280
+ attr_reader :__subject, :__desc, :__before_blocks, :__after_blocks, :__setup_blocks, :__teardown_blocks
281
+
282
+ def initialize subject, desc=nil
283
+ @__subject = subject
284
+ @__desc = desc
285
+
286
+ @__before_blocks = []
287
+ @__after_blocks = []
288
+ @__setup_blocks = []
289
+ @__teardown_blocks = []
290
+ end
291
+
292
+ def it desc, tags: [], with: [], &block
293
+
294
+ # Get the file, where the spec is defined.
295
+ # Nasty, but it works
296
+ # Maybe there is another way, but this works for now
297
+ spec_file = nil
298
+ begin
299
+ raise
300
+ rescue => e
301
+ spec_file = e.backtrace
302
+ .select { |file| !file.include? 'lib/spectre' }
303
+ .first
304
+ .match(/(.*\.rb):\d+/)
305
+ .captures
306
+ .first
307
+ end
308
+
309
+ @__subject.add_spec(desc, tags, with, block, self, spec_file)
310
+ end
311
+
312
+ def before &block
313
+ @__before_blocks << block
314
+ end
315
+
316
+ def after &block
317
+ @__after_blocks << block
318
+ end
319
+
320
+ def setup &block
321
+ @__setup_blocks << block
322
+ end
323
+
324
+ def teardown &block
325
+ @__teardown_blocks << block
326
+ end
327
+
328
+ def context desc=nil, &block
329
+ ctx = SpecContext.new(@__subject, desc)
330
+ ctx._evaluate &block
331
+ end
332
+ end
333
+
334
+
335
+ ###########################################
336
+ # Core Modules
337
+ ###########################################
338
+
339
+
340
+ module Delegator
341
+ @@mappings = {}
342
+
343
+ def self.delegate(*methods, target)
344
+ methods.each do |method_name|
345
+ define_method(method_name) do |*args, &block|
346
+ return super(*args, &block) if respond_to? method_name
347
+ target.send(method_name, *args, &block)
348
+ end
349
+
350
+ @@mappings[method_name] = target
351
+
352
+ private method_name
353
+ end
354
+ end
355
+
356
+ def self.redirect method_name, *args, **kwargs, &block
357
+ target = @@mappings[method_name]
358
+ raise "No method or variable '#{method_name}' defined" if !target
359
+ target.send(method_name, *args, **kwargs, &block)
360
+ end
361
+ end
362
+
363
+
364
+ class << self
365
+ @@subjects = []
366
+ @@modules = []
367
+
368
+ attr_reader :file_log, :logger
369
+
370
+
371
+ def specs spec_filter=[], tags=[]
372
+ @@subjects
373
+ .map { |x| x.specs }
374
+ .flatten
375
+ .select do |spec|
376
+ (spec_filter.empty? or spec_filter.any? { |x| spec.name.match('^' + x.gsub('*', '.*') + '$') }) and (tags.empty? or tags.any? { |x| has_tag(spec.tags, x) })
377
+ end
378
+ end
379
+
380
+
381
+ def has_tag tags, tag_exp
382
+ tags = tags.map { |x| x.to_s }
383
+ all_tags = tag_exp.split '+'
384
+ included_tags = all_tags.select { |x| !x.start_with? '!' }
385
+ excluded_tags = all_tags.select { |x| x.start_with? '!' }.map { |x| x[1..-1] }
386
+ included_tags & tags == included_tags and excluded_tags & tags == []
387
+ end
388
+
389
+
390
+ def delegate *method_names, to: nil
391
+ Spectre::Delegator.delegate *method_names, to
392
+ end
393
+
394
+
395
+ def register &block
396
+ @@modules << block
397
+ end
398
+
399
+
400
+ def configure config
401
+ @@modules.each do |block|
402
+ block.call(config)
403
+ end
404
+ end
405
+
406
+
407
+ ###########################################
408
+ # Global Functions
409
+ ###########################################
410
+
411
+
412
+ def describe desc, &block
413
+ subject = @@subjects.find { |x| x.desc == desc }
414
+
415
+ if !subject
416
+ subject = Subject.new(desc)
417
+ @@subjects << subject
418
+ end
419
+
420
+ ctx = SpecContext.new(subject)
421
+ ctx._evaluate &block
422
+ end
423
+
424
+ def property key, val
425
+ Spectre::Runner.current.properties[key] = val
426
+ end
427
+
428
+ end
429
+
430
+ delegate :describe, :property, to: Spectre
431
+ end
432
+
433
+
434
+ extend Spectre::Delegator
@@ -0,0 +1,277 @@
1
+ require 'ostruct'
2
+ require_relative 'logger'
3
+
4
+
5
+ module Spectre
6
+ module Assertion
7
+ class ::Object
8
+ def should_be(val)
9
+ raise AssertionFailure.new("The value '#{self.to_s.trim}' should be '#{val.to_s.trim}'", val, self) unless self.to_s == val.to_s
10
+ end
11
+
12
+ def should_be_empty
13
+ raise AssertionFailure.new("The value '#{self.to_s.trim}' should be empty", nil, self) unless self == nil
14
+ end
15
+
16
+ def should_not_be(val)
17
+ raise AssertionFailure.new("The value '#{self.to_s.trim}' should not be '#{val.to_s.trim}'", val, self) unless self.to_s != val.to_s
18
+ end
19
+
20
+ def should_not_exist
21
+ raise AssertionFailure.new("The value '#{self.to_s.trim}' should not exist, but it does", val, self) unless self.to_s != nil
22
+ end
23
+
24
+ def should_not_be_empty
25
+ raise AssertionFailure.new('The value should not be empty', 'nothing', self) unless self != nil
26
+ end
27
+
28
+ def or other
29
+ OrEvaluation.new self, other
30
+ end
31
+
32
+ def and other
33
+ AndEvaluation.new self, other
34
+ end
35
+ end
36
+
37
+
38
+ class ::NilClass
39
+ def should_be(val)
40
+ raise AssertionFailure.new("There is nothing, but the value should be '#{val.to_s.trim}'", val, nil) unless val == nil
41
+ end
42
+
43
+ def should_be_empty
44
+ end
45
+
46
+ def should_not_be(val)
47
+ raise AssertionFailure.new(val, 'nil') unless val != nil
48
+ end
49
+
50
+ def should_not_exist
51
+ end
52
+
53
+ def should_not_be_empty
54
+ raise AssertionFailure.new('not empty', 'nil')
55
+ end
56
+ end
57
+
58
+
59
+ class ::Hash
60
+ def should_contain(other)
61
+ raise AssertionFailure.new(other, self) unless self.merge(other) == self
62
+ end
63
+
64
+ def should_not_contain(other)
65
+ raise AssertionFailure.new(other, self) unless self.merge(other) != self
66
+ end
67
+ end
68
+
69
+
70
+ class ::Array
71
+ def should_contain(val)
72
+ list = self
73
+
74
+ if val.is_a? Hash and self.all? { |x| x.is_a? OpenStruct or x.is_a? Hash }
75
+ list = self.map { |x| OpenStruct.new(x) }
76
+ val = OpenStruct.new(val)
77
+ end
78
+
79
+ raise AssertionFailure.new("The list [#{list.join(', ').trim}] should contain '#{val.trim}'", val, list) unless list.include? val
80
+ end
81
+
82
+ def should_not_contain(val)
83
+ list = self
84
+
85
+ if val.is_a? Hash and self.all? { |x| x.is_a? OpenStruct or x.is_a? Hash }
86
+ list = self.map { |x| OpenStruct.new(x) }
87
+ val = OpenStruct.new(val)
88
+ end
89
+
90
+ raise AssertionFailure.new("The list [#{list.join(', ').trim}] should not contain '#{val.trim}'", val, list) if list.include? val
91
+ end
92
+
93
+ def should_be_empty
94
+ raise AssertionFailure.new('empty list', self) unless self.length == 0
95
+ end
96
+
97
+ def should_not_be_empty
98
+ raise AssertionFailure.new('no empty list', self) unless self.length > 0
99
+ end
100
+ end
101
+
102
+
103
+ class ::String
104
+ def should_be(val)
105
+ raise AssertionFailure.new("The text '#{self.trim}' should be '#{val.to_s.trim}'", val, self) unless self == val
106
+ end
107
+
108
+ def should_be_empty
109
+ raise AssertionFailure.new("The text '#{self.trim}' should be empty", nil, self) unless self.empty?
110
+ end
111
+
112
+ def should_not_be(val)
113
+ raise AssertionFailure.new("The text '#{self.trim}' should not be '#{val.to_s.trim}'", val, self) unless self != val
114
+ end
115
+
116
+ def should_not_be_empty
117
+ raise AssertionFailure.new('The text should not be empty', 'nothing', self) unless not self.empty?
118
+ end
119
+
120
+ def should_contain(value)
121
+ predicate = proc { |x| self.include? x.to_s }
122
+ evaluation = SingleEvaluation.new value
123
+ success = evaluation.call(predicate)
124
+
125
+ return if success
126
+
127
+ raise AssertionFailure.new("The text '#{self.to_s.trim}' should contain #{evaluation.to_s}", evaluation, self)
128
+ end
129
+
130
+ def should_not_contain(val)
131
+ raise AssertionFailure.new("The text '#{self.trim}' should not contain '#{val.trim}'", val, self) if self.include? val
132
+ end
133
+
134
+ def should_match(regex)
135
+ raise AssertionFailure.new("The text '#{self.trim}' should match '#{val}'", regex, self) unless self.match(regex)
136
+ end
137
+
138
+ def should_not_match(regex)
139
+ raise AssertionFailure.new("The text '#{self.trim}' should not match '#{val}'", regex, self) if self.match(regex)
140
+ end
141
+
142
+ alias :| :or
143
+ alias :& :and
144
+ end
145
+
146
+
147
+ class Evaluation
148
+ def initialize value, other
149
+ @value = value
150
+ @other = other
151
+ end
152
+
153
+ def eval_assertion predicate, val
154
+ if val.is_a? Evaluation
155
+ val.call(predicate)
156
+ else
157
+ predicate.call(val)
158
+ end
159
+ end
160
+
161
+ alias :| :or
162
+ alias :& :and
163
+ end
164
+
165
+
166
+ class SingleEvaluation < Evaluation
167
+ def initialize value
168
+ super value, nil
169
+ end
170
+
171
+ def call predicate
172
+ eval_assertion(predicate, @value)
173
+ end
174
+
175
+ def to_s
176
+ @value.to_s
177
+ end
178
+ end
179
+
180
+
181
+ class OrEvaluation < Evaluation
182
+ def initialize value, other
183
+ super value, other
184
+ end
185
+
186
+ def call predicate
187
+ eval_assertion(predicate, @value) or eval_assertion(predicate, @other)
188
+ end
189
+
190
+ def to_s
191
+ "(#{@value.to_s} or #{@other.to_s})"
192
+ end
193
+ end
194
+
195
+
196
+ class AndEvaluation < Evaluation
197
+ def initialize value, other
198
+ super value, other
199
+ end
200
+
201
+ def call predicate
202
+ eval_assertion(predicate, @value) and eval_assertion(predicate, @other)
203
+ end
204
+
205
+ def to_s
206
+ "(#{@value.to_s} and #{@other.to_s})"
207
+ end
208
+ end
209
+
210
+
211
+ class AssertionFailure < ExpectationFailure
212
+ attr_reader :expected, :actual
213
+
214
+ def initialize message, expected=nil, actual=nil, expectation=nil
215
+ super message, expectation
216
+ @expected = expected
217
+ @actual = actual
218
+ end
219
+ end
220
+
221
+
222
+ class << self
223
+ @@success = nil
224
+
225
+ def eval_assertion predicate, val
226
+ if val.is_a? Proc
227
+ val.call(predicate)
228
+ else
229
+ predicate.call(val)
230
+ end
231
+ end
232
+
233
+ def expect desc
234
+ begin
235
+ Logger.log_process("expect #{desc}")
236
+ yield
237
+ Logger.log_status(desc, Logger::Status::OK)
238
+
239
+ rescue Interrupt => e
240
+ raise e
241
+
242
+ rescue AssertionFailure => e
243
+ Logger.log_status(desc, Logger::Status::FAILED)
244
+ raise AssertionFailure.new(e.message, e.expected, e.actual, desc), cause: nil
245
+
246
+ rescue Exception => e
247
+ Logger.log_status(desc, Logger::Status::ERROR)
248
+ raise AssertionFailure.new("An unexpected error occured during expectation: #{e.message}", nil, nil, desc), cause: e
249
+ end
250
+ end
251
+
252
+ def observe desc = nil
253
+ begin
254
+ Logger.log_info("observing #{desc}") if desc
255
+ yield
256
+ @@success = true
257
+
258
+ rescue Interrupt => e
259
+ raise e
260
+
261
+ rescue Exception => e
262
+ @@success = false
263
+ end
264
+ end
265
+
266
+ def success?
267
+ @@success
268
+ end
269
+
270
+ def fail_with message
271
+ raise AssertionFailure.new(message)
272
+ end
273
+ end
274
+
275
+ Spectre.delegate :expect, :observe, :success?, :fail_with, to: self
276
+ end
277
+ end