qed 1.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,495 @@
1
+ module QED
2
+ require 'yaml'
3
+ require 'facets/dir/ascend'
4
+
5
+ require 'ae'
6
+
7
+ require 'qed/reporter/dotprogress'
8
+ require 'qed/reporter/summary'
9
+ require 'qed/reporter/verbatim'
10
+
11
+ #Assertion = AE::Assertion
12
+ Expectation = Assertor
13
+
14
+ # Global Before
15
+ def self.Before(&procedure)
16
+ @_before = procedure if procedure
17
+ @_before
18
+ end
19
+
20
+ # Global After
21
+ def self.After(&procedure)
22
+ @_after = procedure if procedure
23
+ @_after
24
+ end
25
+
26
+ # New Specification
27
+ #def initialize(specs, output=nil)
28
+ # @specs = [specs].flatten
29
+ #end
30
+
31
+ # = Script
32
+ #
33
+ class Script
34
+
35
+ #def self.load(file, output=nil)
36
+ # new(File.read(file), output)
37
+ #end
38
+
39
+ # Path of demonstration script.
40
+ attr :file
41
+
42
+ # Reporter object to issue output calls.
43
+ attr :output
44
+
45
+ # List of helper scripts to require.
46
+ attr :helpers
47
+
48
+ # New Script
49
+ def initialize(file, output=nil)
50
+ @file = file
51
+ @output = output || Reporter::Verbatim.new #(self)
52
+ parse #_document(file)
53
+ end
54
+
55
+ # File basename less extension.
56
+ def name
57
+ @name ||= File.basename(file).chomp(File.extname(file))
58
+ end
59
+
60
+ #
61
+ def directory
62
+ @directory ||= Dir.pwd #File.dirname(File.expand_path(file))
63
+ end
64
+
65
+ #def convert
66
+ # @source.gsub(/^\w/, '# \1')
67
+ #end
68
+
69
+ # Run the script.
70
+ def run
71
+ @lineno = 0
72
+
73
+ $LOAD_PATH.unshift(directory)
74
+
75
+ import_helpers
76
+
77
+ steps.each do |step|
78
+ output.report_step(step)
79
+ case step
80
+ when /^[=#]/
81
+ output.report_header(step)
82
+ when /^\S/
83
+ output.report_comment(step)
84
+ context.When.each do |(regex, proc)|
85
+ if md = regex.match(step)
86
+ proc.call(*md[1..-1])
87
+ end
88
+ end
89
+ else
90
+ #if context.table
91
+ # run_table(step)
92
+ #else
93
+ run_step(step)
94
+ #end
95
+ end
96
+ @lineno += step.count("\n")
97
+ end
98
+
99
+ $LOAD_PATH.index(directory){ |i| $LOAD_PATH.delete_at(i) }
100
+ end
101
+
102
+ #--
103
+ # NOTE: The Around code is in place should we decide
104
+ # to use it. I'm not sure yet if it's really neccessary,
105
+ # since we have Before and After.
106
+ #++
107
+ def run_step(step=nil, &blk)
108
+ QED.Before.call if QED.Before
109
+ context.Before.call if context.Before
110
+ begin
111
+ if blk # TODO: Is this still used?
112
+ blk.call #eval(step, context._binding)
113
+ else
114
+ #if context.Around
115
+ # context.Around.call do
116
+ # eval(step, context._binding, @file, @lineno+1)
117
+ # end
118
+ #else
119
+ eval(step, context._binding, @file, @lineno+1)
120
+ #end
121
+ end
122
+ output.report_pass(step) if step
123
+ rescue Assertion => error
124
+ output.report_fail(step, error)
125
+ rescue Exception => error
126
+ output.report_error(step, error)
127
+ ensure
128
+ context.After.call if context.After
129
+ QED.After.call if QED.After
130
+ end
131
+ end
132
+
133
+ =begin
134
+ #
135
+ def run_table(step)
136
+ file = context.table
137
+ Dir.ascend(Dir.pwd) do |path|
138
+ f1 = File.join(path, file)
139
+ f2 = File.join(path, 'fixtures', file)
140
+ fr = File.file?(f1) ? f1 : File.exist?(f2) ? f2 : nil
141
+ (file = fr; break) if fr
142
+ end
143
+ output.report_pass(step) #step)
144
+
145
+ tbl = YAML.load(File.new(file))
146
+ key = tbl.shift
147
+ tbl.each do |set|
148
+ assign = key.zip(set).map{ |k, v| "#{k}=#{v.inspect};" }.join
149
+ run_table_step(assign + step, set)
150
+ #run_step(set.inspect.tabto(4)){ blk.call(set) }
151
+ #@_script.run_step(set.to_yaml.tabto(2)){ blk.call(set) }
152
+ #@_script.output.report_table(set)
153
+ end
154
+ #output.report_pass(step) #step)
155
+ context.table = nil
156
+ end
157
+
158
+ #
159
+ #def run_table_step(step, set)
160
+ def run_table_step(set, &blk)
161
+ context.before.call if context.before
162
+ begin
163
+ #eval(step, context._binding, @file) # TODO: would be nice to know file and lineno here
164
+ blk.call(*set)
165
+ output.report_pass(' ' + set.inspect) #step)
166
+ rescue Assertion => error
167
+ output.report_fail(set.inspect, error)
168
+ rescue Exception => error
169
+ output.report_error(set.inspect, error)
170
+ ensure
171
+ context.after.call if context.after
172
+ end
173
+ end
174
+ =end
175
+
176
+ # Cut-up script into steps.
177
+ def steps
178
+ @steps ||= (
179
+ code = false
180
+ str = ''
181
+ steps = []
182
+ @source.each_line do |line|
183
+ if /^\s*$/.match line
184
+ str << line
185
+ elsif /^[=]/.match line
186
+ steps << str #.chomp("\n")
187
+ steps << line #.chomp("\n")
188
+ str = ''
189
+ #str << line
190
+ code = false
191
+ elsif /^\S/.match line
192
+ if code
193
+ steps << str #.chomp("\n")
194
+ str = ''
195
+ str << line
196
+ code = false
197
+ else
198
+ str << line
199
+ end
200
+ else
201
+ if code
202
+ str << line
203
+ else
204
+ steps << str
205
+ str = ''
206
+ str << line
207
+ code = true
208
+ end
209
+ end
210
+ end
211
+ steps << str
212
+ #steps.map{ |s| s.chomp("\n") }
213
+ steps
214
+ )
215
+ end
216
+
217
+ #
218
+ def to_html
219
+ require 'tilt'
220
+ Tilt.new(file).render
221
+ end
222
+
223
+ #
224
+ def parse
225
+ steps = []
226
+ require 'nokogiri'
227
+ doc = Nokogiri::HTML(to_html)
228
+ doc.root.traverse do |elem|
229
+ case elem.name
230
+ when /h*/
231
+ steps << Head.new(elem.text)
232
+ when "p"
233
+ steps << Text.new(elem.text)
234
+ when 'pre'
235
+ steps << Code.new(elem.text)
236
+ end
237
+ end
238
+ steps
239
+ end
240
+
241
+ class Head
242
+ attr :text
243
+ def initialize(text)
244
+ @text = text
245
+ end
246
+ end
247
+
248
+ class Code
249
+ attr :text
250
+ def initialize(text)
251
+ @text = text
252
+ end
253
+ end
254
+
255
+ class Text
256
+ attr :text
257
+ def initialize(text)
258
+ @text = text
259
+ end
260
+ end
261
+
262
+ # The run context.
263
+ def context
264
+ @context ||= Context.new(self)
265
+ end
266
+
267
+ #
268
+ def import(helper)
269
+ code = File.read(helper)
270
+ eval(code, context._binding)
271
+ end
272
+
273
+ private
274
+
275
+ # Splits the document into main source and footer
276
+ # and extract the helper document references from
277
+ # the footer.
278
+ #
279
+ def parse_document(file)
280
+ text = File.read(file)
281
+ index = text.rindex('---') || text.size
282
+ source = text[0...index]
283
+ footer = text[index+3..-1].to_s.strip
284
+ #helpers = parse_helpers(footer)
285
+ @source = source
286
+ @footer = footer
287
+ end
288
+
289
+ #
290
+ def parse_helpers(footer)
291
+ helpers = []
292
+ footer.split("\n").each do |line|
293
+ next if line.strip == ''
294
+ case line
295
+ when /\[(.*?)\]\((.*?)\)/
296
+ helpers << $2
297
+ when /(.*?)\[(.*?)\]/
298
+ helpers << $2
299
+ end
300
+ end
301
+ helpers
302
+ end
303
+
304
+ =begin
305
+ # Looks for a master +helper.rb+ file and a special
306
+ # helpers/<name>.rb file. Both of these, when found, will
307
+ # be imported when this script is run.
308
+
309
+ def collect_helpers
310
+ dir = File.dirname(file)
311
+ list = []
312
+ list << "helper.rb" if File.exist?(File.join(dir, "helper.rb"))
313
+ list << "helpers/#{name}.rb" if File.exist?(File.join(dir, "helpers/#{name}.rb"))
314
+ list
315
+ end
316
+ =end
317
+
318
+ # TODO: How to determine where to find the env.rb file?
319
+ #def require_environment
320
+ # dir = File.dirname(file)
321
+ # dir = File.expand_path(dir)
322
+ # env = loop do
323
+ # file = File.join(dir, 'env.rb')
324
+ # break file if File.exist?(file)
325
+ # break nil if ['demo', 'demos', 'doc', 'docs', 'test', 'tests'].include? File.basename(dir)
326
+ # break nil if dir == Dir.pwd
327
+ # dir = File.dirname(dir)
328
+ # end
329
+ # require(env) if env
330
+ #end
331
+
332
+ # FIXME: where to stop looking for helpers.
333
+ def import_helpers
334
+ hlp = []
335
+ dir = Dir.pwd #File.expand_path(dir)
336
+ env = loop do
337
+ helpers.each do |helper|
338
+ file = File.join(dir, 'helpers', helper)
339
+ if File.exist?(file)
340
+ hlp << file
341
+ end
342
+ end
343
+ break if ['qed', 'demo', 'demos', 'doc', 'docs', 'test', 'tests'].include? File.basename(dir)
344
+ dir = File.dirname(dir)
345
+ end
346
+ hlp.each{ |helper| import(helper) }
347
+ end
348
+
349
+ end
350
+
351
+ #
352
+ class Context < Module
353
+
354
+ TABLE = /^TABLE\[(.*?)\]/i
355
+
356
+ def initialize(script)
357
+ @_script = script
358
+ @_when = []
359
+ @_tables = []
360
+ end
361
+
362
+ def _binding
363
+ @_binding ||= binding
364
+ end
365
+
366
+ # Before each step.
367
+ def Before(&procedure)
368
+ @_before = procedure if procedure
369
+ @_before
370
+ end
371
+
372
+ # After each step.
373
+ def After(&procedure)
374
+ @_after = procedure if procedure
375
+ @_after
376
+ end
377
+
378
+ # Run code around each step.
379
+ #
380
+ # Around procedures must take a block, in which the step is run.
381
+ #
382
+ # Around do |&step|
383
+ # ... do something here ...
384
+ # step.call
385
+ # ... do stiff stuff ...
386
+ # end
387
+ #
388
+ #def Around(&procedure)
389
+ # @_around = procedure if procedure
390
+ # @_around
391
+ #end
392
+
393
+ # Comment match procedure.
394
+ #
395
+ # This is useful for creating unobtrusive setup and (albeit more
396
+ # limited) teardown code. A pattern is matched against each comment
397
+ # as it is processed. If there is match, the code procedure is
398
+ # triggered, passing in any mathcing expression arguments.
399
+ #
400
+ def When(pattern=nil, &procedure)
401
+ return @_when unless procedure
402
+ raise ArgumentError unless pattern
403
+ unless Regexp === pattern
404
+ pattern = __when_string_to_regexp(pattern)
405
+ end
406
+ @_when << [pattern, procedure]
407
+ end
408
+
409
+ # Code match-and-transform procedure.
410
+ #
411
+ # This is useful to transform human readable code examples
412
+ # into proper exectuable code. For example, say you want to
413
+ # run shell code, but want to make if look like typical
414
+ # shelle examples:
415
+ #
416
+ # $ cp fixture/a.rb fixture/b.rb
417
+ #
418
+ # You can use a transform to convert lines starting with '$'
419
+ # into executable Ruby using #system.
420
+ #
421
+ # system('cp fixture/a.rb fixture/b.rb')
422
+ #
423
+ #def Transform(pattern=nil, &procedure)
424
+ #
425
+ #end
426
+
427
+ # Table-based steps.
428
+ def Table(file=nil, &blk)
429
+ file = file || @_tables.last
430
+ tbl = YAML.load(File.new(file))
431
+ tbl.each do |set|
432
+ blk.call(*set)
433
+ end
434
+ @_tables << file
435
+ end
436
+
437
+ # Read/Write a fixture.
438
+ def Data(file, &content)
439
+ raise if File.directory?(file)
440
+ if content
441
+ FileUtils.mkdir_p(File.dirname(fname))
442
+ case File.extname(file)
443
+ when '.yml', '.yaml'
444
+ File.open(file, 'w'){ |f| f << content.call.to_yaml }
445
+ else
446
+ File.open(file, 'w'){ |f| f << content.call }
447
+ end
448
+ else
449
+ #raise LoadError, "no such fixture file -- #{fname}" unless File.exist?(fname)
450
+ case File.extname(file)
451
+ when '.yml', '.yaml'
452
+ YAML.load(File.new(file))
453
+ else
454
+ File.read(file)
455
+ end
456
+ end
457
+ end
458
+
459
+ private
460
+
461
+ def __when_string_to_regexp(str)
462
+ str = str.split(/(\(\(.*?\)\))(?!\))/).map{ |x|
463
+ x =~ /\A\(\((.*)\)\)\z/ ? $1 : Regexp.escape(x)
464
+ }.join
465
+ str = str.gsub(/(\\\ )+/, '\s+')
466
+ Regexp.new(str, Regexp::IGNORECASE)
467
+
468
+ #rexps = []
469
+ #str = str.gsub(/\(\((.*?)\)\)/) do |m|
470
+ # rexps << '(' + $1 + ')'
471
+ # "\0"
472
+ #end
473
+ #str = Regexp.escape(str)
474
+ #rexps.each do |r|
475
+ # str = str.sub("\0", r)
476
+ #end
477
+ #str = str.gsub(/(\\\ )+/, '\s+')
478
+ #Regexp.new(str, Regexp::IGNORECASE)
479
+ end
480
+
481
+ #
482
+ # check only local and maybe start paths
483
+ #def __locate_file(file)
484
+ # Dir.ascend(Dir.pwd) do |path|
485
+ # f1 = File.join(path, file)
486
+ # f2 = File.join(path, 'fixtures', file)
487
+ # fr = File.file?(f1) ? f1 : File.exist?(f2) ? f2 : nil
488
+ # (file = fr; break) if fr
489
+ # end
490
+ #end
491
+
492
+ end
493
+
494
+ end
495
+