rutema 2.0.0 → 2.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.
@@ -1,4 +1,5 @@
1
- # Copyright (c) 2007-2017 Vassilis Rizopoulos. All rights reserved.
1
+ # Copyright (c) 2007-2021 Vassilis Rizopoulos. All rights reserved.
2
+
2
3
  require 'thread'
3
4
  require_relative 'parser'
4
5
  require_relative 'reporter'
@@ -6,13 +7,28 @@ require_relative 'runner'
6
7
  require_relative '../version'
7
8
 
8
9
  module Rutema
9
- #Rutema::Engine implements the rutema workflow.
10
+ ##
11
+ # Class implementing the inner workflow of rutema
10
12
  #
11
- #It instantiates the configured parser, runner and reporter instances and wires them together via Rutema::Dispatcher
13
+ # This class takes care of instantiating the configured parser, runner and
14
+ # reporters. The reporters then receive event information from the runner
15
+ # through a Dispatcher instance.
12
16
  #
13
- #The full workflow is Parse->Run->Report and corresponds to one call of the Engine#run method
17
+ # The full workflow consists of subsequent +parse+, +run+ and +report+ phases
18
+ # and corresponds to one call of the #run method.
14
19
  class Engine
15
20
  include Messaging
21
+
22
+ ##
23
+ # Initialize a new Engine instance and setup all class instances needed for
24
+ # test execution
25
+ #
26
+ # This brings up a parser, the runner and the reporters and wires them up as
27
+ # needed. After completion of this method the instance is ready for test
28
+ # execution by means of the #run method.
29
+ #
30
+ # * +configuration+ - a Configuration instance according to which Engine,
31
+ # its components and the test run shall be set up
16
32
  def initialize configuration
17
33
  @queue=Queue.new
18
34
  @parser=instantiate_class(configuration.parser,configuration) if configuration.parser
@@ -29,27 +45,38 @@ module Rutema
29
45
  @dispatcher=Dispatcher.new(@queue,configuration)
30
46
  @configuration=configuration
31
47
  end
32
- #Parse, run, report
48
+
49
+ ##
50
+ # Parse the test specifications, execute the test(s) and report the results
51
+ #
52
+ # * +test_identifier+ - an optional identifier of a single test case to be
53
+ # executed, this cannot be an arbitrary one but must still be contained
54
+ # in the configured test specification identifiers
33
55
  def run test_identifier=nil
34
56
  @dispatcher.run!
35
- #start
57
+ # start
36
58
  message("start")
37
59
  suite_setup,suite_teardown,setup,teardown,tests=*parse(test_identifier)
38
60
  if tests.empty?
39
61
  @dispatcher.exit
40
62
  raise RutemaError,"No tests to run!"
41
63
  else
42
- @runner.setup=setup
43
- @runner.teardown=teardown
44
- #running - at this point we've done any and all checks and we're stepping on the gas
64
+ # running - at this point all checks are done and the tests are active
45
65
  message("running")
46
- run_scenarios(tests,suite_setup,suite_teardown)
66
+ run_scenarios(tests,suite_setup,suite_teardown,setup,teardown)
47
67
  end
48
68
  message("end")
49
69
  @dispatcher.exit
50
70
  @dispatcher.report(tests)
51
71
  end
52
- #Parse a single test spec or all the specs listed in the configuration
72
+
73
+ ##
74
+ # Parse a single test specification or all the specs given by the
75
+ # configuration
76
+ #
77
+ # * +test_identifier+ - an optional identifier of a single test case to be
78
+ # executed, this cannot be an arbitrary one but must still be contained
79
+ # in the configured test specification identifiers
53
80
  def parse test_identifier=nil
54
81
  specs=[]
55
82
  #so, while we are parsing, we have a list of tests
@@ -68,12 +95,27 @@ module Rutema
68
95
  suite_setup,suite_teardown,setup,teardown=parse_specials(@configuration)
69
96
  return [suite_setup,suite_teardown,setup,teardown,specs]
70
97
  end
98
+
71
99
  private
100
+
101
+ ##
102
+ # Parse an array of test specifications into Specification instances
103
+ #
104
+ # * +tests+ - an array containing paths to test specification files or the
105
+ # test specifications' texts themselves
72
106
  def parse_specifications tests
73
107
  tests.map do |t|
74
108
  parse_specification(t)
75
109
  end.compact
76
110
  end
111
+
112
+ ##
113
+ # Parse a single test specification into a Specification instance
114
+ #
115
+ # * +spec_identifier+ - either the path to a test specification file or the
116
+ # actual test specification text itself
117
+ #
118
+ # A ParserError is raised upon failure.
77
119
  def parse_specification spec_identifier
78
120
  begin
79
121
  @parser.parse_specification(spec_identifier)
@@ -82,6 +124,15 @@ module Rutema
82
124
  raise Rutema::ParserError, "In #{spec_identifier}: #{$!.message}"
83
125
  end
84
126
  end
127
+
128
+ ##
129
+ # Parse the special test (suite) setup and teardown methods if set
130
+ #
131
+ # This returns an array containing Specification instances for
132
+ # * test suite setup
133
+ # * test suite teardown
134
+ # * test setup
135
+ # * test teardown
85
136
  def parse_specials configuration
86
137
  suite_setup=nil
87
138
  suite_teardown=nil
@@ -101,33 +152,55 @@ module Rutema
101
152
  end
102
153
  return suite_setup,suite_teardown,setup,teardown
103
154
  end
104
- def run_scenarios specs,suite_setup,suite_teardown
155
+
156
+ ##
157
+ # Execute the passed test Specification instances
158
+ #
159
+ # * +specs+ - an array of Specification instances representing the actual
160
+ # tests
161
+ # * +suite_setup+ - a test suite setup Specification instance
162
+ # * +suite_teardown+ - a test suite teardown Specification instance
163
+ def run_scenarios(specs, suite_setup, suite_teardown, setup, teardown)
105
164
  if specs.empty?
106
165
  error(nil,"No tests to run")
107
166
  else
108
- if suite_setup
109
- if run_test(suite_setup)==:success
110
- specs.each{|s| run_test(s)}
111
- else
112
- error(nil,"Suite setup test failed")
113
- end
114
- else
167
+ @runner.setup = nil
168
+ @runner.teardown = nil
169
+
170
+ if !suite_setup || (run_test(suite_setup, true) == :success)
171
+ @runner.setup = setup
172
+ @runner.teardown = teardown
115
173
  specs.each{|spec| run_test(spec)}
174
+ else
175
+ error(nil, "Suite setup test failed")
116
176
  end
117
177
  if suite_teardown
118
- run_test(suite_teardown)
178
+ @runner.setup = nil
179
+ @runner.teardown = nil
180
+ run_test(suite_teardown, true)
119
181
  end
120
182
  end
121
183
  end
122
- def run_test specification
184
+
185
+ ##
186
+ #
187
+ def run_test(specification, is_special = false)
123
188
  if specification.scenario
124
- status=@runner.run(specification)["status"]
189
+ status = @runner.run(specification, is_special)["status"]
125
190
  else
126
191
  status=:not_executed
127
192
  message(:test=>specification.name,:text=>"No scenario", :status=>status)
128
193
  end
129
194
  return status
130
195
  end
196
+
197
+ ##
198
+ # Instantiate a new class of a given type passing it a given configuration
199
+ # upon construction
200
+ #
201
+ # * +definition+ - class of which a new instance shall be instantiated
202
+ # * +configuration+ - Configuration instance which shall be passed to the
203
+ # initializer of the to be created instance
131
204
  def instantiate_class definition,configuration
132
205
  if definition[:class]
133
206
  klass=definition[:class]
@@ -135,26 +208,55 @@ module Rutema
135
208
  end
136
209
  return nil
137
210
  end
211
+
212
+ ##
213
+ # Check if the given test identifier belongs to the normal test cases or to
214
+ # one of the special ones (the test (suite) setups and teardowns)
215
+ #
216
+ # * +test_identifier+ - the test identifier to check against membership in
217
+ # the normal or special test case identifier sets
138
218
  def is_spec_included? test_identifier
139
219
  full_path=File.expand_path(test_identifier)
140
- return @configuration.tests.include?(full_path) || is_special?(test_identifier)
220
+ return @configuration.tests.include?(full_path) || is_special?(test_identifier)
141
221
  end
222
+
223
+ ##
224
+ # Check if the given test identifier is a (suite) setup or teardown one
225
+ #
226
+ # This checks if the given identifier was configured as (suite) setup or
227
+ # teardown within the rutema run's configuration.
228
+ #
229
+ # * +test_identifier+ - the test identifier which shall be checked for being
230
+ # a special one
142
231
  def is_special? test_identifier
143
232
  full_path=File.expand_path(test_identifier)
144
233
  return full_path==@configuration.suite_setup ||
145
234
  full_path==@configuration.suite_teardown ||
146
235
  full_path==@configuration.setup ||
147
- full_path==@configuration.teardown
236
+ full_path==@configuration.teardown
148
237
  end
149
238
  end
150
- #The Rutema::Dispatcher functions as a demultiplexer between Rutema::Engine and the various reporters.
151
- #
152
- #In stream mode the incoming queue is popped periodically and the messages are destributed to the queues of any subscribed event reporters.
239
+
240
+ ##
241
+ # Class functioning as a demultiplexer between the Engine and the various
242
+ # Reporters instances
153
243
  #
154
- #By default this includes Rutema::Reporters::Collector which is then used at the end of a run to provide the collected data to all registered block mode reporters
244
+ # In stream mode the incoming queue is popped periodically and the messages
245
+ # are distributed to the queues of any subscribed event reporters. By default
246
+ # this includes Reporters::Collector which is then used at the end of a run to
247
+ # provide the collected data to all registered block mode reporters
155
248
  class Dispatcher
156
- #The interval between queue operations
157
- INTERVAL=0.01
249
+ ##
250
+ # The interval between queue operations
251
+ INTERVAL = 0.01
252
+
253
+ ##
254
+ # Initialize a new demultiplexer and instantiate all reporters requested by
255
+ # the passed configuration
256
+ #
257
+ # * +queue+ - the queue which will be shared between the Engine instance and
258
+ # the Reporter instances
259
+ # * +configuration+ - the Configuration instance of the rutema run
158
260
  def initialize queue,configuration
159
261
  @queue = queue
160
262
  @queues = {}
@@ -169,12 +271,25 @@ module Rutema
169
271
  @streaming_reporters<<@collector
170
272
  @configuration=configuration
171
273
  end
172
- #Call this to establish a queue with the given identifier
274
+
275
+ ##
276
+ # Call this to establish a queue with the given identifier
277
+ #
278
+ # This method will create a new queue within the Dispatcher instance into
279
+ # which data from the incoming queue from the Engine instance will be
280
+ # dispatched to.
281
+ #
282
+ # * +identifier+ - a unique identifier for the queue. If two identifiers
283
+ # collide the new subscriber will replace the earlier one
173
284
  def subscribe identifier
174
285
  @queues[identifier]=Queue.new
175
286
  return @queues[identifier]
176
287
  end
177
-
288
+
289
+ ##
290
+ # Start #update threads of all event/streaming reporters and then start a
291
+ # new locally managed thread which continually dispatches messages from the
292
+ # incoming queue
178
293
  def run!
179
294
  puts "Running #{@streaming_reporters.size} streaming reporters" if $DEBUG
180
295
  @streaming_reporters.each {|r| r.run!}
@@ -186,12 +301,19 @@ module Rutema
186
301
  end
187
302
  end
188
303
 
304
+ ##
305
+ # Call all block reporters' BlockReporter#report method
189
306
  def report specs
190
307
  @block_reporters.each do |r|
191
308
  r.report(specs,@collector.states,@collector.errors)
192
309
  end
193
310
  Reporters::Summary.new(@configuration,self).report(specs,@collector.states,@collector.errors)
194
311
  end
312
+
313
+ ##
314
+ # Dispatch all messages in the incoming queue to the subscribed reporters,
315
+ # exit all streaming reporters' threads and then stop the own internal
316
+ # dispatch thread
195
317
  def exit
196
318
  puts "Exiting main dispatcher" if $DEBUG
197
319
  if @thread
@@ -200,7 +322,12 @@ module Rutema
200
322
  Thread.kill(@thread)
201
323
  end
202
324
  end
325
+
203
326
  private
327
+
328
+ ##
329
+ # Dispatch messages from the incoming queue to all subscribers until the
330
+ # incoming queue is empty
204
331
  def flush
205
332
  puts "Flushing queues" if $DEBUG
206
333
  if @thread
@@ -210,6 +337,18 @@ module Rutema
210
337
  end
211
338
  end
212
339
  end
340
+
341
+ ##
342
+ # Instantiate a new reporter instance and pass the configuration and this
343
+ # Dispatcher instance this method is called upon to
344
+ #
345
+ # * +definition+ - hash containing the class type which shall be
346
+ # instantiated referenced by its +:class+ key
347
+ # * +configuration+ - the Configuration instance which shall be passed to
348
+ # the initializer of the to be instantiated class
349
+ #
350
+ # This either returns the new class instance or _nil_ if the passed hash
351
+ # did not contain a key +:class+
213
352
  def instantiate_reporter definition,configuration
214
353
  if definition[:class]
215
354
  klass=definition[:class]
@@ -217,6 +356,11 @@ module Rutema
217
356
  end
218
357
  return nil
219
358
  end
359
+
360
+ ##
361
+ # Pop the last element of the incoming queue from the runner (if it's not
362
+ # empty) and distribute it to all subscribed Reporters::EventReporter
363
+ # instances
220
364
  def dispatch
221
365
  if @queue.size>0
222
366
  data=@queue.pop
@@ -224,4 +368,4 @@ module Rutema
224
368
  end
225
369
  end
226
370
  end
227
- end
371
+ end
@@ -1,13 +1,37 @@
1
+ # Copyright (c) 2021 Vassilis Rizopoulos. All rights reserved.
2
+
1
3
  module Rutema
2
- #Represents the data beeing shunted between the components in lieu of logging.
4
+ STATUS_CODES=[:started,:skipped,:success,:warning,:error]
5
+
6
+ ##
7
+ # Simple base for classes concerned with message passing to report test
8
+ # progress and failures
9
+ #
10
+ # This class and its descendants can be utilized as a container for data
11
+ # relevant to tests and their results. Currently they are being emitted by the
12
+ # Engine and Runners instances and consumed by classes within the Reporters
13
+ # module.
3
14
  #
4
- #This is the primary type passed to the event reporters
15
+ # Specialized descendants are ErrorMessage and RunnerMessage.
5
16
  class Message
6
- attr_accessor :test,:text,:timestamp
7
- #Keys used:
8
- # test - the test id/name
9
- # text - the text of the message
10
- # timestamp
17
+ ##
18
+ # The test whose execution originated the message
19
+ attr_accessor :test
20
+ ##
21
+ # The text of the message
22
+ attr_accessor :text
23
+ ##
24
+ # The timestamp of the message's creation
25
+ attr_accessor :timestamp
26
+
27
+ ##
28
+ # Initialize a new message from data passed in a hash
29
+ #
30
+ # The following keys of the hash are being utilized:
31
+ # * +:test+ - the test id/name of the test which originates the message
32
+ # * +:text+ - the text of the message
33
+ # * +:timestamp+ - most often the timestamp of the creation of the message,
34
+ # defaults to +Time.now+
11
35
  def initialize params
12
36
  @test=params.fetch(:test,"")
13
37
  @test||=""
@@ -15,6 +39,8 @@ module Rutema
15
39
  @timestamp=params.fetch(:timestamp,Time.now)
16
40
  end
17
41
 
42
+ ##
43
+ # Convert the instance to a convenient textual representation
18
44
  def to_s
19
45
  msg=""
20
46
  msg<<"#{@test} " unless @test.empty?
@@ -22,8 +48,16 @@ module Rutema
22
48
  return msg
23
49
  end
24
50
  end
25
- #What it says on the tin.
51
+
52
+ ##
53
+ # Message container to report test errors
54
+ #
55
+ # The reported on errors may concern the test specifications, parser errors or
56
+ # errors which occurred during test execution. Logic errors of rutema itself
57
+ # are not reported by means of this class.
26
58
  class ErrorMessage<Message
59
+ ##
60
+ # Convert the instance to a convenient textual representation
27
61
  def to_s
28
62
  msg="ERROR - "
29
63
  msg<<"#{@test} " unless @test.empty?
@@ -31,12 +65,26 @@ module Rutema
31
65
  return msg
32
66
  end
33
67
  end
34
- #The Runner continuously sends these when executing tests
68
+
69
+ ##
70
+ # Message container continually being emitted by runners (see Runners module)
71
+ # during test execution
35
72
  #
36
- #If there is an engine error (e.g. when parsing) you will get an ErrorMessage, if it is a test error
37
- #you will get a RunnerMessage with :error in the status.
73
+ # These messages inform about the progress of test execution. Test errors are
74
+ # propagated through instances of this class as well. If it's an engine error
75
+ # (e.g. during parsing), then an ErrorMessage will be used in that case.
38
76
  class RunnerMessage<Message
39
- attr_accessor :duration,:status,:number,:out,:err
77
+ attr_accessor :duration, :status, :number, :out, :err, :is_special
78
+
79
+ ##
80
+ # Initialize a new runner message from data passed in a hash
81
+ #
82
+ # Additionally to the keys of the Message initializer the following keys of
83
+ # the hash are being utilized:
84
+ # * "duration" - the time a test step took for execution
85
+ # * "status" - the status of the respective step
86
+ # * +:timestamp+ - most often the timestamp of the creation of the message,
87
+ # defaults to +Time.now+
40
88
  def initialize params
41
89
  super(params)
42
90
  @duration=params.fetch("duration",0)
@@ -44,13 +92,18 @@ module Rutema
44
92
  @number=params.fetch("number",1)
45
93
  @out=params.fetch("out","")
46
94
  @err=params.fetch("err","")
95
+ @backtrace=params.fetch("backtrace","")
96
+ @is_special=params.fetch("is_special","")
47
97
  end
48
98
 
99
+ ##
100
+ # Convert the instance to a convenient textual representation
49
101
  def to_s
50
102
  msg="#{@test}:"
103
+ msg<<" #{@timestamp.strftime("%H:%M:%S")} :"
51
104
  msg<<"#{@text}." unless @text.empty?
52
105
  outpt=output()
53
- msg<<" Output:\n#{outpt}" unless outpt.empty? || @status!=:error
106
+ msg<<" Output" + (outpt.empty? ? "." : ":\n#{outpt}") # unless outpt.empty? || @status!=:error
54
107
  return msg
55
108
  end
56
109
 
@@ -58,9 +111,11 @@ module Rutema
58
111
  msg=""
59
112
  msg<<"#{@out}\n" unless @out.empty?
60
113
  msg<<@err unless @err.empty?
114
+ msg<<"\n" + (@backtrace.kind_of?(Array) ? @backtrace.join("\n") : @backtrace) unless @backtrace.empty?
61
115
  return msg.chomp
62
116
  end
63
117
  end
118
+
64
119
  #While executing tests the state of each test is collected in an
65
120
  #instance of ReportState and the collection is at the end passed to the available block reporters
66
121
  #
@@ -68,7 +123,7 @@ module Rutema
68
123
  #and accumulates the duration reported by all messages in it's collection.
69
124
  class ReportState
70
125
  attr_accessor :steps
71
- attr_reader :test,:timestamp,:duration,:status
126
+ attr_reader :test, :timestamp, :duration, :status, :is_special
72
127
 
73
128
  def initialize message
74
129
  @test=message.test
@@ -76,25 +131,43 @@ module Rutema
76
131
  @duration=message.duration
77
132
  @status=message.status
78
133
  @steps=[message]
134
+ @is_special=message.is_special
79
135
  end
80
136
 
81
137
  def <<(message)
82
138
  @steps<<message
83
139
  @duration+=message.duration
84
- @status=message.status
140
+ @status = message.status unless message.status.nil? \
141
+ || (!@status.nil? && STATUS_CODES.find_index(message.status) < STATUS_CODES.find_index(@status))
85
142
  end
86
143
  end
87
144
 
145
+ ##
146
+ # Mix-in module which offers an interface to push messages to a queue
147
+ #
148
+ # Instances of the class including this module need a @queue member variable.
88
149
  module Messaging
89
- #Signal an error - use the test name/id as the identifier
150
+ ##
151
+ # Push a new ErrorMessage instance to the queue
152
+ #
153
+ # * +identifier+ - in most cases this would be the name of a test or its
154
+ # specification file
155
+ # * +message+ - a short descriptive message detailing the error condition
90
156
  def error identifier,message
91
157
  @queue.push(ErrorMessage.new(:test=>identifier,:text=>message,:timestamp=>Time.now))
92
158
  end
93
- #Informational message during test runs
159
+
160
+ ##
161
+ # Push a new Message or RunnerMessage instance to the queue
162
+ #
163
+ # If +message+ is of type String a Message instance will be pushed to the
164
+ # queue. If it's of type Hash it will be passed to the initializer of
165
+ # RunnerMessage if it has both the keys :test and "status" or to the
166
+ # initializer of Message if not so.
94
167
  def message message
95
168
  case message
96
169
  when String
97
- Message.new(:text=>message,:timestamp=>Time.now)
170
+ @queue.push(Message.new(:text=>message,:timestamp=>Time.now))
98
171
  when Hash
99
172
  hm=Message.new(message)
100
173
  hm=RunnerMessage.new(message) if message[:test] && message["status"]
@@ -103,16 +176,32 @@ module Rutema
103
176
  end
104
177
  end
105
178
  end
106
- #Generic error class for errors in the engine
179
+
180
+ ##
181
+ # Generic error class which is being used as base class for all other rutema
182
+ # errors and for Engine related errors.
183
+ #
184
+ # This is being inherited by:
185
+ # * ParserError
186
+ # * ReportError
187
+ # * RunnerError
107
188
  class RutemaError<RuntimeError
108
189
  end
109
- #Is raised when an error is found in a specification
110
- class ParserError<RutemaError
190
+
191
+ ##
192
+ # Specialized error class particular to the parsing of rutema test
193
+ # specifications
194
+ class ParserError < RutemaError
111
195
  end
112
- #Is raised on an unexpected error during execution
113
- class RunnerError<RutemaError
196
+
197
+ ##
198
+ # Specialized error class designated to errors within runner classes
199
+ class RunnerError < RutemaError
114
200
  end
115
- #Errors in reporters should use this class
116
- class ReportError<RutemaError
201
+
202
+ ##
203
+ # Specialized error class which should be utilized by Reporters members to
204
+ # signal errors upon reporting
205
+ class ReportError < RutemaError
117
206
  end
118
- end
207
+ end