spectre-core 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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