rutema 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING.txt +340 -0
- data/History.txt +7 -0
- data/Manifest.txt +17 -0
- data/README.txt +96 -0
- data/Rakefile +22 -0
- data/lib/rutema/configuration.rb +142 -0
- data/lib/rutema/specification.rb +170 -0
- data/lib/rutema/system.rb +599 -0
- data/test/samples/check.spec +5 -0
- data/test/samples/setup.spec +7 -0
- data/test/samples/teardown.spec +7 -0
- data/test/samples/test.spec +7 -0
- data/test/samples/tests/no_title.spec +4 -0
- data/test/samples/tests/sample.spec +7 -0
- data/test/samples/valid_config.rb +20 -0
- data/test/test_configuration.rb +37 -0
- data/test/test_specification.rb +81 -0
- data/test/test_system.rb +121 -0
- metadata +99 -0
@@ -0,0 +1,599 @@
|
|
1
|
+
# Copyright (c) 2007 Vassilis Rizopoulos. All rights reserved.
|
2
|
+
|
3
|
+
require 'rexml/document'
|
4
|
+
|
5
|
+
require 'rutema/specification'
|
6
|
+
require 'rutema/configuration'
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'highline'
|
10
|
+
require 'mailfactory'
|
11
|
+
require 'patir/command'
|
12
|
+
|
13
|
+
module Rutema
|
14
|
+
VERSION_MAJOR=0
|
15
|
+
VERSION_MINOR=1
|
16
|
+
#Is raised when an error is found in a specification
|
17
|
+
class ParserError<RuntimeError
|
18
|
+
end
|
19
|
+
|
20
|
+
#Base class that bombs out when used....
|
21
|
+
class SpecificationParser
|
22
|
+
def parse_specification
|
23
|
+
raise ParserError,"not implemented. You should derive a parser implementation from SpecificationParser!"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class BaseXMLParser<SpecificationParser
|
28
|
+
ELEM_SPEC="specification"
|
29
|
+
ELEM_DESC="specification/description"
|
30
|
+
ELEM_TITLE="specification/title"
|
31
|
+
ELEM_SCENARIO="specification/scenario"
|
32
|
+
ELEM_REQ="requirement"
|
33
|
+
def initialize params
|
34
|
+
@logger=params[:logger]
|
35
|
+
@logger||=Patir.setup_logger
|
36
|
+
end
|
37
|
+
#Parses __param__ and returns the Rutema::TestSpecification instance
|
38
|
+
#
|
39
|
+
#param can be the filename of the specification or the contents of that file.
|
40
|
+
#
|
41
|
+
#Will throw ParserError if something goes wrong
|
42
|
+
def parse_specification param
|
43
|
+
@logger.debug("Loading #{param}")
|
44
|
+
begin
|
45
|
+
file=false
|
46
|
+
if File.exists?(param)
|
47
|
+
#read the file
|
48
|
+
txt=File.read(param)
|
49
|
+
file=true
|
50
|
+
else
|
51
|
+
#try to parse the parameter
|
52
|
+
txt=param
|
53
|
+
end
|
54
|
+
spec=parse_case(txt)
|
55
|
+
spec.filename=""
|
56
|
+
spec.filename=param if file
|
57
|
+
raise "Missing required attribute 'name' in specification element" unless spec.has_name? && !spec.name.empty?
|
58
|
+
return spec
|
59
|
+
rescue
|
60
|
+
@logger.debug($!)
|
61
|
+
raise ParserError,"Error loading #{param}: #{$!.message}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
#Parses the XML specification of a testcase and creates the corresponding TestSpecification instance
|
67
|
+
def parse_case xmltxt
|
68
|
+
#the testspec to return
|
69
|
+
spec=TestSpecification.new
|
70
|
+
#read the test spec
|
71
|
+
xmldoc=REXML::Document.new( xmltxt )
|
72
|
+
#validate it
|
73
|
+
validate_case(xmldoc)
|
74
|
+
#parse it
|
75
|
+
el=xmldoc.elements[ELEM_SPEC]
|
76
|
+
xmldoc.root.attributes.each do |attr,value|
|
77
|
+
add_attribute(spec,attr,value)
|
78
|
+
end
|
79
|
+
#get the title
|
80
|
+
spec.title=xmldoc.elements[ELEM_TITLE].text
|
81
|
+
spec.title||=""
|
82
|
+
spec.title.strip!
|
83
|
+
#get the description
|
84
|
+
#strip line feeds, cariage returns and remove all tabs
|
85
|
+
spec.description=xmldoc.elements[ELEM_DESC].text
|
86
|
+
spec.description||=""
|
87
|
+
begin
|
88
|
+
spec.description.strip!
|
89
|
+
spec.description.gsub!(/\t/,'')
|
90
|
+
end unless spec.description.empty?
|
91
|
+
#get the requirements
|
92
|
+
reqs=el.elements.select{|e| e.name==ELEM_REQ}
|
93
|
+
reqs.collect!{|r| r.attributes["name"]}
|
94
|
+
spec.requirements=reqs
|
95
|
+
#Get the scenario
|
96
|
+
spec.scenario=parse_scenario(xmldoc.elements[ELEM_SCENARIO].to_s) if xmldoc.elements[ELEM_SCENARIO]
|
97
|
+
return spec
|
98
|
+
end
|
99
|
+
#Validates the XML file from our point of view.
|
100
|
+
#
|
101
|
+
#Checks for the existence of ELEM_SPEC, ELEM_DESC and ELEM_TITLE and raises ParserError if they're missing.
|
102
|
+
def validate_case xmldoc
|
103
|
+
raise ParserError,"missing #{ELEM_SPEC} element" unless xmldoc.elements[ELEM_SPEC]
|
104
|
+
raise ParserError,"missing #{ELEM_DESC} element" unless xmldoc.elements[ELEM_DESC]
|
105
|
+
raise ParserError,"missing #{ELEM_TITLE} element" unless xmldoc.elements[ELEM_TITLE]
|
106
|
+
end
|
107
|
+
#Parses the scenario XML element and returns the Rutema::TestScenario instance
|
108
|
+
def parse_scenario xmltxt
|
109
|
+
scenario=Rutema::TestScenario.new
|
110
|
+
xmldoc=REXML::Document.new( xmltxt )
|
111
|
+
xmldoc.root.attributes.each do |attr,value|
|
112
|
+
add_attribute(scenario,attr,value)
|
113
|
+
end
|
114
|
+
number=0
|
115
|
+
xmldoc.root.elements.each do |el|
|
116
|
+
number+=1
|
117
|
+
step=parse_step(el.to_s)
|
118
|
+
step.number=number
|
119
|
+
scenario.add_step(step)
|
120
|
+
end
|
121
|
+
return scenario
|
122
|
+
end
|
123
|
+
#Parses xml and returns the Rutema::TestStep instance
|
124
|
+
def parse_step xmltxt
|
125
|
+
xmldoc=REXML::Document.new( xmltxt )
|
126
|
+
#any step element
|
127
|
+
step=Rutema::TestStep.new()
|
128
|
+
xmldoc.root.attributes.each do |attr,value|
|
129
|
+
add_attribute(step,attr,value)
|
130
|
+
end
|
131
|
+
step.text=xmldoc.root.text.strip if xmldoc.root.text
|
132
|
+
step.step_type=xmldoc.root.name
|
133
|
+
return step
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_attribute element,attr,value
|
137
|
+
if boolean?(value)
|
138
|
+
element.attribute(attr,eval(value))
|
139
|
+
else
|
140
|
+
element.attribute(attr,value)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
def boolean? attribute_value
|
144
|
+
return true if attribute_value=="true" || attribute_value=="false"
|
145
|
+
return false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
class MinimalXMLParser<BaseXMLParser
|
150
|
+
def parse_specification param
|
151
|
+
handle_specification(super(param))
|
152
|
+
end
|
153
|
+
private
|
154
|
+
def handle_specification spec
|
155
|
+
spec.scenario.steps.each do |step|
|
156
|
+
case step.step_type
|
157
|
+
when "echo"
|
158
|
+
step.cmd=Patir::RubyCommand.new("echo"){|cmd| cmd.output="#{step.text}";$stdout.puts(cmd.output) ;:success}
|
159
|
+
when "command"
|
160
|
+
raise ParserError,"missing required attribute cmd in #{step}"
|
161
|
+
wd=step.working_directory if step.has_working_directory?
|
162
|
+
step.cmd=Patir::ShellCommand.new(:cmd=>step.cmd,:working_directory=>wd)
|
163
|
+
when "prompt"
|
164
|
+
step.cmd=Patir::RubyCommand.new("prompt") do |cmd|
|
165
|
+
cmd.output=""
|
166
|
+
cmd.error=""
|
167
|
+
if HighLine.agree("#{step.text}?")
|
168
|
+
:success
|
169
|
+
else
|
170
|
+
:error
|
171
|
+
end#if
|
172
|
+
end#do rubycommand
|
173
|
+
end#case
|
174
|
+
end#do spec.scenario.steps
|
175
|
+
return spec
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class Coordinator
|
180
|
+
attr_accessor :configuration,:parse_errors,:parsed_files
|
181
|
+
def initialize configuration,logger=nil
|
182
|
+
@logger=logger
|
183
|
+
@logger||=Patir.setup_logger
|
184
|
+
@parse_errors=Array.new
|
185
|
+
@configuration=configuration
|
186
|
+
@parser=instantiate_class(@configuration.parser)
|
187
|
+
raise "Could not instantiate parser" unless @parser
|
188
|
+
@reporters=@configuration.reporters.collect{ |reporter| instantiate_class(reporter) }
|
189
|
+
@reporters.compact!
|
190
|
+
@configuration.tests.collect!{|t| File.expand_path(t)}
|
191
|
+
@runner=create_runner
|
192
|
+
@parsed_files=Array.new
|
193
|
+
end
|
194
|
+
#Runs a set of tests
|
195
|
+
#
|
196
|
+
#mode can be :all, :attended, :unattended or a test filename
|
197
|
+
def run mode
|
198
|
+
@configuration.context.start_time=Time.now
|
199
|
+
@logger.info("Run started")
|
200
|
+
case mode
|
201
|
+
when :all
|
202
|
+
specs=parse_all_specifications
|
203
|
+
run_scenarios(specs)
|
204
|
+
when :attended
|
205
|
+
specs=parse_all_specifications
|
206
|
+
@logger.debug(specs)
|
207
|
+
run_scenarios(specs.select{|s| s.scenario && s.scenario.attended?})
|
208
|
+
when :unattended
|
209
|
+
specs=parse_all_specifications
|
210
|
+
@logger.debug(specs)
|
211
|
+
run_scenarios(specs.select{|s| s.scenario && !s.scenario.attended?})
|
212
|
+
when String
|
213
|
+
spec=parse_specification(mode)
|
214
|
+
run_test(spec) if spec
|
215
|
+
else
|
216
|
+
@logger.error("Don't know how to run '#{mode}'")
|
217
|
+
end
|
218
|
+
@configuration.context.end_time=Time.now
|
219
|
+
@logger.info("Run completed in #{@configuration.context.end_time-@configuration.context.start_time}s")
|
220
|
+
end
|
221
|
+
|
222
|
+
#Delegates reporting to all configured reporters spawning one thread per reporter
|
223
|
+
#
|
224
|
+
#It then joins the threads and returns when all of them are finished.
|
225
|
+
def report
|
226
|
+
threads=Array.new
|
227
|
+
runner_status=Patir::CommandSequenceStatus.new("",@runner.states.values)
|
228
|
+
#get the runner stati and the configuration and give it to the reporters
|
229
|
+
@reporters.each do |reporter|
|
230
|
+
threads<<Thread.new(reporter,runner_status,@parse_errors,@configuration) do |reporter,status,perrors,configuration|
|
231
|
+
@logger.debug(reporter.report(status,perrors,configuration))
|
232
|
+
end
|
233
|
+
end
|
234
|
+
threads.each do |t|
|
235
|
+
@logger.debug("Joining #{t}")
|
236
|
+
t.join
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def to_s
|
241
|
+
runner_status=Patir::CommandSequenceStatus.new("",@runner.states.values)
|
242
|
+
"Parsed #{@parsed_files.size} files\n#{Rutema.text_report(runner_status,@parse_errors)}"
|
243
|
+
end
|
244
|
+
private
|
245
|
+
def instantiate_class definition
|
246
|
+
if definition[:class]
|
247
|
+
#add the logger to the definition
|
248
|
+
definition[:logger]=@logger
|
249
|
+
klass=definition[:class]
|
250
|
+
return klass.new(definition)
|
251
|
+
end
|
252
|
+
return nil
|
253
|
+
end
|
254
|
+
|
255
|
+
def create_runner
|
256
|
+
setup=nil
|
257
|
+
teardown=nil
|
258
|
+
if @configuration.setup
|
259
|
+
@logger.info("Parsing setup specification from '#{@configuration.setup}'")
|
260
|
+
setup=@parser.parse_specification(@configuration.setup).scenario
|
261
|
+
end
|
262
|
+
if @configuration.teardown
|
263
|
+
@logger.info("Parsing teardown specification from '#{@configuration.teardown}'")
|
264
|
+
teardown=@parser.parse_specification(@configuration.teardown).scenario
|
265
|
+
end
|
266
|
+
if @configuration.step
|
267
|
+
@logger.info("Using StepRunner")
|
268
|
+
return StepRunner.new(setup,teardown,@logger)
|
269
|
+
else
|
270
|
+
return Runner.new(setup,teardown,@logger)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def parse_specification spec_file
|
275
|
+
filename=File.expand_path(spec_file)
|
276
|
+
if File.exists?(filename)
|
277
|
+
begin
|
278
|
+
@parsed_files<<filename
|
279
|
+
@parsed_files.uniq!
|
280
|
+
return @parser.parse_specification(spec_file)
|
281
|
+
rescue ParserError
|
282
|
+
@logger.error("Error parsing '#{spec_file}': #{$!.message}")
|
283
|
+
@parse_errors<<{:filename=>filename,:error=>$!.message}
|
284
|
+
end
|
285
|
+
else
|
286
|
+
msg="'#{filename}' not found."
|
287
|
+
@logger.error(msg)
|
288
|
+
@parse_errors<<{:filename=>filename,:error=>msg}
|
289
|
+
end
|
290
|
+
return nil
|
291
|
+
end
|
292
|
+
def parse_all_specifications
|
293
|
+
@configuration.tests.collect{|t| parse_specification(t)}.compact
|
294
|
+
end
|
295
|
+
|
296
|
+
def run_scenarios specs
|
297
|
+
specs.compact!
|
298
|
+
if specs.empty?
|
299
|
+
@logger.error("No tests to run")
|
300
|
+
else
|
301
|
+
specs.each{|s| run_test(s)}
|
302
|
+
end
|
303
|
+
end
|
304
|
+
def run_test specification
|
305
|
+
@logger.info("Running #{specification.name} - #{specification.title}")
|
306
|
+
if specification.scenario
|
307
|
+
status=@runner.run(specification.name,specification.scenario)
|
308
|
+
else
|
309
|
+
@logger.warn("#{specification.name} has no scenario")
|
310
|
+
status=:not_executed
|
311
|
+
end
|
312
|
+
return status
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
#Runner executes TestScenario instances and maintains the state of all scenarios run.
|
317
|
+
class Runner
|
318
|
+
attr_reader :states,:number_of_runs
|
319
|
+
attr_accessor :setup,:teardown
|
320
|
+
attr_writer :attended
|
321
|
+
|
322
|
+
#setup and teardown are TestScenario instances that will run before and after each call
|
323
|
+
#to the scenario.
|
324
|
+
def initialize setup=nil, teardown=nil,logger=nil
|
325
|
+
@setup=setup
|
326
|
+
@teardown=teardown
|
327
|
+
@attended=false
|
328
|
+
@logger=logger
|
329
|
+
@logger||=Patir.setup_logger
|
330
|
+
@states=Hash.new
|
331
|
+
@number_of_runs=0
|
332
|
+
end
|
333
|
+
|
334
|
+
def attended?
|
335
|
+
return @attended
|
336
|
+
end
|
337
|
+
#Runs a scenario and stores the result internally
|
338
|
+
#
|
339
|
+
#Returns the result of the run as a Patir::CommandSequenceStatus
|
340
|
+
def run name,scenario
|
341
|
+
@logger.debug("Starting run for #{name} with #{scenario.inspect}")
|
342
|
+
@states[name]=Patir::CommandSequenceStatus.new(name,scenario.steps)
|
343
|
+
@states[name].sequence_id="s#{@number_of_runs}"
|
344
|
+
#if setup /teardown is defined we need to execute them before and after
|
345
|
+
if @setup
|
346
|
+
@logger.info("Setup for #{name}")
|
347
|
+
@states["#{name}_setup"]=run_scenario("#{name}_setup",@setup)
|
348
|
+
@states["#{name}_setup"].sequence_id="s#{@number_of_runs}"
|
349
|
+
if @states["#{name}_setup"].executed?
|
350
|
+
#do not execute the scenario unless the setup was succesfull
|
351
|
+
if @states["#{name}_setup"].success?
|
352
|
+
@logger.info("Scenario for #{name}")
|
353
|
+
@states[name]=run_scenario(name,scenario)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
else
|
357
|
+
@logger.info("Scenario for #{name}")
|
358
|
+
@states[name]=run_scenario(name,scenario)
|
359
|
+
end
|
360
|
+
if @teardown
|
361
|
+
#always execute teardown
|
362
|
+
@logger.warn("Teardown for #{name}")
|
363
|
+
@states["#{name}_teardown"]=run_scenario("#{name}_teardown",@teardown)
|
364
|
+
@states["#{name}_teardown"].sequence_id="t#{@number_of_runs}"
|
365
|
+
end
|
366
|
+
@number_of_runs+=1
|
367
|
+
return @states[name]
|
368
|
+
end
|
369
|
+
|
370
|
+
#Returns the state of the scenario with the given name.
|
371
|
+
#
|
372
|
+
#Will return nil if no scenario is found under that name.
|
373
|
+
def [](name)
|
374
|
+
return @states[name]
|
375
|
+
end
|
376
|
+
|
377
|
+
def reset
|
378
|
+
@states.clear
|
379
|
+
@number_of_runs=0
|
380
|
+
end
|
381
|
+
private
|
382
|
+
def run_scenario name,scenario
|
383
|
+
state=Patir::CommandSequenceStatus.new(name,scenario.steps)
|
384
|
+
if scenario.attended? && !self.attended?
|
385
|
+
@logger.warn("Attended scenario cannot be run in unattended mode")
|
386
|
+
state..status=:warning
|
387
|
+
else
|
388
|
+
stps=scenario.steps
|
389
|
+
if stps.empty?
|
390
|
+
@logger.warn("Scenario #{name} contains no steps")
|
391
|
+
state.status=:warning
|
392
|
+
else
|
393
|
+
stps.each do |s|
|
394
|
+
state.step=run_step(s)
|
395
|
+
break if !state.success?
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
state.stop_time=Time.now
|
400
|
+
state.sequence_id=@number_of_runs
|
401
|
+
return state
|
402
|
+
end
|
403
|
+
def run_step step
|
404
|
+
@logger.info("#{step.to_s}")
|
405
|
+
if step.has_cmd? && step.cmd.respond_to?(:run)
|
406
|
+
step.cmd.run
|
407
|
+
else
|
408
|
+
@logger.warn("No command associated with step '#{step.step_type}'")
|
409
|
+
end
|
410
|
+
msg="#{step.number} - #{step.step_type} - #{step.status}"
|
411
|
+
# we might not have a command object
|
412
|
+
if step.has_cmd? && step.cmd.executed? && !step.cmd.success?
|
413
|
+
msg<<"\n#{step.cmd.output}" unless step.cmd.output.empty?
|
414
|
+
end
|
415
|
+
if step.status==:error
|
416
|
+
@logger.error(msg)
|
417
|
+
else
|
418
|
+
@logger.info(msg)
|
419
|
+
end
|
420
|
+
return step
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
#StepRunner halts before every step and asks if it should be executed or not.
|
425
|
+
class StepRunner<Runner
|
426
|
+
def run_step step
|
427
|
+
if HighLine.agree("Execute #{step.to_s}?")
|
428
|
+
return super
|
429
|
+
else
|
430
|
+
state[:state]=:not_executed
|
431
|
+
msg="#{step.number} - #{step.step_type} - #{state[:state]}"
|
432
|
+
@logger.info(msg)
|
433
|
+
return state
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
#The application class
|
439
|
+
class RutemaX
|
440
|
+
require 'optparse'
|
441
|
+
def initialize command_line_args
|
442
|
+
parse_command_line(command_line_args)
|
443
|
+
@logger=Patir.setup_logger(@log_file)
|
444
|
+
begin
|
445
|
+
@configuration=RutemaConfigurator.new(@config_file,@logger).configuration
|
446
|
+
@coordinator=Coordinator.new(@configuration,@logger)
|
447
|
+
application_flow
|
448
|
+
rescue Patir::ConfigurationException
|
449
|
+
@logger.debug($!)
|
450
|
+
@logger.fatal("Configuration error '#{$!.message}'")
|
451
|
+
exit 1
|
452
|
+
rescue
|
453
|
+
@logger.debug($!)
|
454
|
+
@logger.fatal("#{$!.message}")
|
455
|
+
exit 1
|
456
|
+
end
|
457
|
+
end
|
458
|
+
private
|
459
|
+
def parse_command_line args
|
460
|
+
args.options do |opt|
|
461
|
+
opt.on("Options:")
|
462
|
+
opt.on("--debug", "-d","Turns on debug messages") { $DEBUG=true }
|
463
|
+
opt.on("--config FILE", "-c FILE",String,"Loads the configuration from FILE") { |@config_file|}
|
464
|
+
opt.on("--log FILE", "-l FILE",String,"Redirects the log output to FILE") { |@log_file|}
|
465
|
+
opt.on("--check","Runs just the check test"){@check=true}
|
466
|
+
opt.on("-V", "--version","Displays the version") { $stdout.puts("v#{VERSION_MAJOR}.#{VERSION_MINOR}");exit 0 }
|
467
|
+
opt.on("--help", "-h", "-?", "This text") { $stdout.puts opt; exit 0 }
|
468
|
+
opt.parse!
|
469
|
+
#and now the rest
|
470
|
+
if args.empty?
|
471
|
+
@mode=:unattended
|
472
|
+
else
|
473
|
+
command=args.shift
|
474
|
+
case command
|
475
|
+
when "attended"
|
476
|
+
@mode=:attended
|
477
|
+
when "all"
|
478
|
+
@mode=:all
|
479
|
+
when "unattended"
|
480
|
+
@mode=:unattended
|
481
|
+
else
|
482
|
+
if File.exists?(command)
|
483
|
+
@mode=command
|
484
|
+
else
|
485
|
+
$stderr.puts "Can't find '#{command}'. Don't know what to do with it."
|
486
|
+
exit 1
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
def application_flow
|
493
|
+
if @check
|
494
|
+
#run just the check test
|
495
|
+
if @configuration.check
|
496
|
+
@coordinator.run(@configuration.check)
|
497
|
+
else
|
498
|
+
@logger.error("There is no check test defined in the configuration.")
|
499
|
+
end
|
500
|
+
else
|
501
|
+
#run everything
|
502
|
+
@coordinator.run(@mode)
|
503
|
+
end
|
504
|
+
@logger.info("Report:\n#{@coordinator.to_s}")
|
505
|
+
@coordinator.report
|
506
|
+
end
|
507
|
+
end
|
508
|
+
#Reporter is meant as a base class for reporter classes.
|
509
|
+
class Reporter
|
510
|
+
#params should be a Hash containing the parameters used to initialize the class
|
511
|
+
def initialize params
|
512
|
+
end
|
513
|
+
|
514
|
+
#Coordinator will pass the Rutema __configuration__ giving you access to the context which can contain data like headings and version numbers to use in the report.
|
515
|
+
#
|
516
|
+
#runner_status is a Patir::CommandSequenceStatus containing the status of the last run (so it contains all the Scenario stati for the loaded tests)
|
517
|
+
#
|
518
|
+
#parse_errors is an Array of {:filename,:error} hashes containing the errors encounter by the parser when loading the specifications
|
519
|
+
def report runner_status,parse_errors,configuration
|
520
|
+
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
#The following configuration keys are used by EmailReporter:
|
525
|
+
#
|
526
|
+
#:server - the smtp server to use
|
527
|
+
#
|
528
|
+
#:port - the port to use (defaults to 25)
|
529
|
+
#
|
530
|
+
#:sender - the sender of the email (defaults to rutema@domain)
|
531
|
+
#
|
532
|
+
#:recipients - an array of strings with the recipients of the report emails
|
533
|
+
#
|
534
|
+
#The :logger key is set by the Coordinator
|
535
|
+
#
|
536
|
+
#Customization keys:
|
537
|
+
#
|
538
|
+
#:subject - the string of this key will be prefixed as a subject for the email
|
539
|
+
class EmailReporter
|
540
|
+
attr_reader :last_message
|
541
|
+
def initialize definition
|
542
|
+
#get the logger
|
543
|
+
@logger=definition[:logger]
|
544
|
+
@logger||=Patir.setup_logger
|
545
|
+
#extract the parameters from the definition
|
546
|
+
#address and port of the smtp server
|
547
|
+
@server=definition[:server]
|
548
|
+
@port=definition[:port]
|
549
|
+
@port||=25
|
550
|
+
#the domain we're coming from
|
551
|
+
@domain=definition[:domain]
|
552
|
+
#construct the mail factory object
|
553
|
+
@mail = MailFactory.new()
|
554
|
+
@mail.from = definition[:sender]
|
555
|
+
@mail.from||="rutema@#{@domain}"
|
556
|
+
@recipients=definition[:recipients]
|
557
|
+
@mail.to=@recipients
|
558
|
+
#customize
|
559
|
+
@subject=definition[:subject]
|
560
|
+
@subject||=""
|
561
|
+
#this is a way to test without sending
|
562
|
+
@dummy=true if definition[:dummy]
|
563
|
+
@logger.info("Reporter '#{self.to_s}' registered")
|
564
|
+
end
|
565
|
+
|
566
|
+
def to_s
|
567
|
+
list=@recipients.join(', ')
|
568
|
+
"EmailReporter - #{@server}:#{@port} from #{@mail.from} to #{list}"
|
569
|
+
end
|
570
|
+
|
571
|
+
def report runner_status,parse_errors,configuration
|
572
|
+
@mail.subject = "#{@subject}"
|
573
|
+
@mail.text = Rutema.text_report(runner_status,parse_errors)
|
574
|
+
begin
|
575
|
+
if @mail.to.empty?
|
576
|
+
@logger.error("No recipients for the report mail")
|
577
|
+
else
|
578
|
+
#
|
579
|
+
#~ if @password
|
580
|
+
#~ #if a password is defined, use cram_md5 authentication
|
581
|
+
#~ else
|
582
|
+
Net::SMTP.start(@server, @port, @domain) {|smtp| smtp.sendmail(@mail.to_s(),@mail.from,mail_to)} unless @dummy
|
583
|
+
#~ end
|
584
|
+
end#recipients empty
|
585
|
+
rescue
|
586
|
+
@logger.error("Sending of email report failed: #{$!}")
|
587
|
+
end
|
588
|
+
@mail.to_s
|
589
|
+
end
|
590
|
+
end
|
591
|
+
private
|
592
|
+
def self.text_report runner_status,parse_errors
|
593
|
+
msg="Parse errors: #{parse_errors.size}"
|
594
|
+
parse_errors.each{|e| msg<<"\n\t#{e[:filename]}"}
|
595
|
+
msg<<"\nCurrent run:\nScenarios: #{runner_status.step_states.size}"
|
596
|
+
msg<<"\n#{runner_status.summary}"
|
597
|
+
return msg
|
598
|
+
end
|
599
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake'
|
2
|
+
#
|
3
|
+
configuration.parser={:class=>Rutema::MinimalXMLParser}
|
4
|
+
configuration.reporter={:class=>Rutema::EmailReporter,
|
5
|
+
:server=>"localhost",
|
6
|
+
:port=>25,
|
7
|
+
:recipients=>["test"],
|
8
|
+
:sender=>"rutema",
|
9
|
+
:subject=>"test",
|
10
|
+
:dummy=>true
|
11
|
+
}
|
12
|
+
configuration.tool={:name=>"echo",:path=>"echo.exe"}
|
13
|
+
configuration.tool={:name=>"tool",:path=>"tool.exe"}
|
14
|
+
configuration.path={:name=>"SourcePath",:path=>"../../lib"}
|
15
|
+
configuration.setup="setup.spec"
|
16
|
+
configuration.teardown="teardown.spec"
|
17
|
+
configuration.check="check.spec"
|
18
|
+
configuration.tests=["test.spec"]
|
19
|
+
configuration.tests=Rake::FileList["tests/*.spec"]
|
20
|
+
configuration.context_data={:tester=>"riva"}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__),"..","lib")
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
module TestRutema
|
6
|
+
require 'rutema/system'
|
7
|
+
|
8
|
+
class TestRutemaConfigurator<Test::Unit::TestCase
|
9
|
+
def setup
|
10
|
+
@prev_dir=Dir.pwd
|
11
|
+
Dir.chdir(File.dirname(__FILE__))
|
12
|
+
end
|
13
|
+
def teardown
|
14
|
+
Dir.chdir(@prev_dir)
|
15
|
+
end
|
16
|
+
def test_configuration
|
17
|
+
cfg=nil
|
18
|
+
#load the valid configuration
|
19
|
+
assert_nothing_raised() { cfg=Rutema::RutemaConfigurator.new("samples/valid_config.rb").configuration}
|
20
|
+
assert_not_nil(cfg.parser)
|
21
|
+
assert_not_nil(cfg.reporters)
|
22
|
+
assert_equal(1, cfg.reporters.size)
|
23
|
+
assert_not_nil(cfg.tools)
|
24
|
+
assert_not_nil(cfg.paths)
|
25
|
+
assert_not_nil(cfg.setup)
|
26
|
+
assert_not_nil(cfg.teardown)
|
27
|
+
assert_not_nil(cfg.check)
|
28
|
+
assert_not_nil(cfg.tests)
|
29
|
+
assert_not_nil(cfg.context)
|
30
|
+
end
|
31
|
+
def test_specification_paths
|
32
|
+
cfg=Rutema::RutemaConfigurator.new("samples/valid_config.rb").configuration
|
33
|
+
assert_not_nil(cfg.tests)
|
34
|
+
p cfg.tests
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|