rutema 0.1
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/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
|