Class: Toaster::TestRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/toaster/test/test_runner.rb

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(test_suite = nil, max_threads_active = nil, terminate_when_queue_empty = true) ⇒ TestRunner

Returns a new instance of TestRunner.



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/toaster/test/test_runner.rb', line 31

def initialize(test_suite=nil, max_threads_active=nil, terminate_when_queue_empty=true)
  @test_suite = test_suite
  @max_threads_active = max_threads_active ? max_threads_active : 5
  @active_threads = []
  @terminate_when_queue_empty = terminate_when_queue_empty
  @request_queue = Queue.new
  @result_map = BlockingMap.new
  @local_semaphore = Mutex.new
  @received_requests = []
  @handled_requests = []
  if !terminate_when_queue_empty
    start_worker_threads()
  end
end

Class Attribute Details

.delay_between_testsObject

Returns the value of attribute delay_between_tests.



28
29
30
# File 'lib/toaster/test/test_runner.rb', line 28

def delay_between_tests
  @delay_between_tests
end

.last_start_timeObject

Returns the value of attribute last_start_time.



28
29
30
# File 'lib/toaster/test/test_runner.rb', line 28

def last_start_time
  @last_start_time
end

.semaphoreObject

Returns the value of attribute semaphore.



28
29
30
# File 'lib/toaster/test/test_runner.rb', line 28

def semaphore
  @semaphore
end

.signalObject

Returns the value of attribute signal.



28
29
30
# File 'lib/toaster/test/test_runner.rb', line 28

def signal
  @signal
end

Class Method Details

.ensure_automation_exists_in_db(automation_name, recipes, test_suite, destroy_container = true, print_output = false) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/toaster/test/test_runner.rb', line 152

def self.ensure_automation_exists_in_db(automation_name,
  recipes, test_suite, destroy_container=true, print_output=false)

  chef_node_name = ChefUtil.extract_node_name(automation_name)
  # prepare run list (add toaster::testing recipe definitions)
  actual_run_list = ChefUtil.prepare_run_list(chef_node_name, recipes)
  reduced_run_list = ChefUtil.get_reduced_run_list(actual_run_list)
  automation = Automation.find_by_cookbook_and_runlist(automation_name, reduced_run_list)
  return automation if !automation.nil?

  # if we don't have a matching automation in the DB yet, execute an initial run..
  c = TestCase.new(test_suite)
  test_suite.test_cases << c
  puts "INFO: Executing initial automation run; test case '#{c.uuid}'"
  automation_run = execute_test(c, destroy_container, print_output)
  puts "DEBUG: Finished execution of initial automation run; test case '#{c.uuid}': #{automation_run}"
  return nil if !automation_run
  return automation_run.automation
end

.execute_test(test_case, destroy_container = true, print_output = false, num_attempts = 2) ⇒ Object

execute the provided test case



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/toaster/test/test_runner.rb', line 197

def self.execute_test(test_case, destroy_container=true, 
    print_output=false, num_attempts=2)

  test_suite = nil
  test_id = nil
  recipes = nil
  automation = nil
  self.semaphore.synchronize do
    test_suite = test_case.test_suite
    automation = test_suite.automation
    test_id = test_suite.uuid
    recipes = automation.recipes
  end

  sleep_time = 0
  self.semaphore.synchronize do
    time = TimeStamp.now()
    diff = time - self.last_start_time
    if diff < self.delay_between_tests.to_f
      sleep_time = self.delay_between_tests.to_f - diff
    end
    self.last_start_time = time + sleep_time
  end

  # sleep a bit, and then this test is ready to go...
  sleep(sleep_time)
  time_now = TimeStamp.now()
  test_case.start_time = time_now

  # start test case execution
  automation_run = nil
  error_output = nil
  while num_attempts > 0
    begin
      automation_run = nil
      if automation.is_chef?
        # generate automation attributes which represent this test case
        chef_node_attrs = test_case.create_chef_node_attrs()
        # set cookbook if necessary
        automation.cookbook = automation.get_short_name if !automation.cookbook
        # now run test!
        automation_run = TestRunner.do_execute_test_chef(automation.cookbook,
            automation.script, recipes, chef_node_attrs, test_suite.lxc_prototype, 
            test_id, destroy_container, print_output)
      else
        raise "Unknown automation language/type: '#{automation.language}'"
      end

      test_case.test_suite().test_cases << test_case if !test_case.test_suite().test_cases().include?(test_case)
      test_case.automation_run = automation_run

      num_attempts = 0
    rescue Object => ex
      error_output = ex
      num_attempts -= 1
      puts "WARN: cannot run test case '#{test_case.uuid}' (remaining attempts: #{num_attempts}): #{ex}"
      puts "#{ex.backtrace.join("\n")}"
    end
  end

  if !automation_run
    machine_id = Util.get_machine_id()
    automation = test_case.test_suite.automation
    automation_run = AutomationRun.new(
      :automation => automation, 
      :machine_id => machine_id,
      :user => test_case.test_suite.user
    )
    puts "WARN: Test case '#{test_case.uuid}' failed entirely, storing " +
        "an empty automation run '#{test_case.automation_run}' with success=false."
    automation_run.success = false
    automation_run.end_time = TimeStamp.now
    automation_run.error_details = "Test case '#{test_case.uuid}' failed " +
        "(no automation run created by test runner). Output:\n#{error_output}"
    automation_run.save
    test_case.automation_run = automation_run
  end

  test_case.end_time = TimeStamp.now().to_i
  test_case.save

  return automation_run
end

.schedule_tests(test_suite, test_case_uuids) ⇒ Object



133
134
135
136
137
# File 'lib/toaster/test/test_runner.rb', line 133

def self.schedule_tests(test_suite, test_case_uuids)
  runner = TestRunner.new(test_suite)
  runner.schedule_tests(test_suite, test_case_uuids)
  runner.start_worker_threads()
end

Instance Method Details

#execute_test_case(test_case, do_wait_until_finished = true) ⇒ Object



175
176
177
# File 'lib/toaster/test/test_runner.rb', line 175

def execute_test_case(test_case, do_wait_until_finished=true)
  execute_tests(test_case, do_wait_until_finished)
end

#execute_tests(test_cases, do_wait_until_finished = true) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/toaster/test/test_runner.rb', line 178

def execute_tests(test_cases, do_wait_until_finished=true)
  test_cases = [test_cases] if !test_cases.kind_of?(Array)
  test_cases.each do |test|
    puts "INFO: Pushing test case to queue: #{test.uuid}"
    @request_queue.push(test)
  end

  # start worker threads (if they are not already running..)
  start_worker_threads()

  if do_wait_until_finished
    puts "INFO: Waiting until #{test_cases.size} test cases are finished."
    wait_until_finished(test_cases)
  end
end

#schedule_test_case(test_case) ⇒ Object



172
173
174
# File 'lib/toaster/test/test_runner.rb', line 172

def schedule_test_case(test_case)
  execute_test_case(test_case, false)
end

#schedule_tests(test_suite, test_case_uuids) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/toaster/test/test_runner.rb', line 139

def schedule_tests(test_suite, test_case_uuids)
  test_cases = []
  test_case_uuids.each do |tc|
    if !tc.kind_of?(TestCase)
      tc = TestCase.find(:uuid => tc)[0]
    end
    test_cases << tc
  end
  test_cases.each do |tc|
    schedule_test_case(tc)
  end
end

#startObject



46
47
48
# File 'lib/toaster/test/test_runner.rb', line 46

def start()
  start_worker_threads()
end

#start_test_suite(blocking = true) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/toaster/test/test_runner.rb', line 110

def start_test_suite(blocking=true)

  start_worker_threads()

  @test_generator = TestGenerator.new(@test_suite) if @test_suite && !@test_generator

  @test_suite.save()

  # generate tests
  execute_tests(@test_generator.gen_all_tests())

  @test_suite.save()

end

#start_worker_threadsObject



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/toaster/test/test_runner.rb', line 49

def start_worker_threads()
  current_num = 0
  @local_semaphore.synchronize do
    current_num = @active_threads.size
  end
  puts "DEBUG: currently active worker threads: #{current_num} of #{@max_threads_active}"
  ((current_num)..(@max_threads_active-1)).each do 
    t = Thread.start {
      running = true
      while running
        begin
          test_case = nil
          @local_semaphore.synchronize do
            # terminate if no more requests are queued
            if @request_queue.empty? && @terminate_when_queue_empty
              # do NOT add this check --> leads to busy wait loop!
              #if @handled_requests.size >= @received_requests.size
                running = false
              #end
            end
            if running
              test_case = @request_queue.pop()
              @received_requests << test_case
            end
          end
          if test_case
            begin
              automation_run = TestRunner.execute_test(test_case)
              @result_map.put(test_case, automation_run)
            rescue Object => ex
              err = "WARN: exception when running test case: #{ex}\n#{ex.backtrace.join("\n")}"
              puts err
              @result_map.put(test_case, err)
            end
            @local_semaphore.synchronize do
              @handled_requests << test_case
            end
          end
        rescue Exception => ex
          puts "WARN: exception in test runner thread: #{ex}\n#{ex.backtrace.join("\n")}"
          @result_map.put(test_case, nil)
        end
      end
      @local_semaphore.synchronize do
        @active_threads.delete(self)
      end
    }
    @active_threads << t
  end
end

#stopObject



100
101
102
# File 'lib/toaster/test/test_runner.rb', line 100

def stop()
  stop_threads()
end

#stop_threadsObject



103
104
105
106
107
108
# File 'lib/toaster/test/test_runner.rb', line 103

def stop_threads()
  @active_threads.dup.each do |t|
    t.terminate()
    @active_threads.delete(t)
  end
end

#wait_until_finished(test_cases) ⇒ Object



125
126
127
128
129
130
131
# File 'lib/toaster/test/test_runner.rb', line 125

def wait_until_finished(test_cases)
  test_cases = [test_cases] if !test_cases.kind_of?(Array)
  test_cases.dup.each do |t|
    # this operation will block until a results becomes available..
    @result_map.get(t)
  end
end