asautotest 0.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.
@@ -0,0 +1,50 @@
1
+ # -*- coding: utf-8 -*-
2
+ # stopwatch.rb --- utility class for measuring delays
3
+ # Copyright (C) 2010 Go Interactive
4
+
5
+ # This file is part of ASAutotest.
6
+
7
+ # ASAutotest is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+
12
+ # ASAutotest is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with ASAutotest. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ module ASAutotest
21
+ class Stopwatch
22
+ def initialize
23
+ @start_time = current_time
24
+ end
25
+
26
+ def to_s(decimals = 3)
27
+ round(n_elapsed_seconds, decimals).to_s
28
+ end
29
+
30
+ def stop
31
+ @end_time = current_time
32
+ end
33
+
34
+ def end_time
35
+ @end_time || current_time
36
+ end
37
+
38
+ def current_time
39
+ Time.new
40
+ end
41
+
42
+ def n_elapsed_seconds
43
+ end_time - @start_time
44
+ end
45
+
46
+ def round(number, n_decimals)
47
+ (number * 10 ** n_decimals).round.to_f / 10 ** n_decimals
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,424 @@
1
+ # -*- coding: utf-8 -*-
2
+ # test-runner.rb --- run tests and report the results
3
+ # Copyright (C) 2010 Go Interactive
4
+
5
+ # This file is part of ASAutotest.
6
+
7
+ # ASAutotest is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+
12
+ # ASAutotest is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with ASAutotest. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ require "socket"
21
+ require "timeout"
22
+ require "rexml/document"
23
+
24
+ module ASAutotest
25
+ class TestRunner
26
+ class TestMisbehaving < Exception ; end
27
+ class TestMisbehavingFatally < Exception ; end
28
+
29
+ include Logging
30
+
31
+ EXPECTED_GREETING = "Hello, this is a test.\n"
32
+ POLICY_FILE_REQUEST = "<policy-file-request/>\0"
33
+
34
+ # Make sure we can accept a policy file request as a greeting.
35
+ EXPECTED_GREETING.size >= POLICY_FILE_REQUEST.size or
36
+ raise "Internal error: Expected greeting is too short."
37
+
38
+ def initialize(binary_name, port)
39
+ @binary_name = binary_name
40
+ @port = port
41
+ @n_planned_tests = nil
42
+ @suites = {}
43
+ end
44
+
45
+ def run
46
+ whisper "Running tests via socket connection."
47
+ with_server_running { run_test }
48
+ rescue TestMisbehaving
49
+ shout "Terminating misbehaving test."
50
+ rescue TestMisbehavingFatally
51
+ exit -1
52
+ end
53
+
54
+ def run_test
55
+ with_flash_running do
56
+ accept_connection
57
+ shake_hands
58
+ talk_to_test
59
+ end
60
+ end
61
+
62
+ # ------------------------------------------------------
63
+
64
+ def with_server_running
65
+ start_server
66
+ begin
67
+ yield
68
+ ensure
69
+ stop_server
70
+ end
71
+ end
72
+
73
+ def start_server
74
+ @server = TCPServer.new(@port)
75
+ @server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
76
+ end
77
+
78
+ def stop_server
79
+ @server.close
80
+ end
81
+
82
+ # ------------------------------------------------------
83
+
84
+ def with_flash_running
85
+ start_flash
86
+ begin
87
+ yield
88
+ ensure
89
+ stop_flash
90
+ end
91
+ end
92
+
93
+ def start_flash
94
+ @flash_pid = fork { exec FLASHPLAYER, @binary_name }
95
+ end
96
+
97
+ def stop_flash
98
+ Process.kill("TERM", @flash_pid)
99
+ Process.wait(@flash_pid)
100
+ end
101
+
102
+ # ------------------------------------------------------
103
+
104
+ def misbehavior!(*descriptions)
105
+ print_warnings(descriptions)
106
+ raise TestMisbehaving
107
+ end
108
+
109
+ def fatal_misbehavior!(*descriptions)
110
+ print_warnings(descriptions)
111
+ raise TestMisbehavingFatally
112
+ end
113
+
114
+ def print_warnings(warnings)
115
+ for warning in warnings do
116
+ shout warning
117
+ end
118
+ end
119
+
120
+ # ------------------------------------------------------
121
+
122
+ def accept_connection
123
+ whisper "Accepting connection" do
124
+ # It takes at least 3 seconds to get a policy file request.
125
+ @socket = Timeout.timeout(4) { @server.accept }
126
+ end
127
+ rescue Timeout::Error
128
+ misbehavior! "Test did not connect to localhost:#@port."
129
+ end
130
+
131
+ def shake_hands
132
+ Timeout.timeout(1) { parse_greeting(read_greeting) }
133
+ rescue Timeout::Error
134
+ misbehavior! "Handshake took too long."
135
+ end
136
+
137
+ def read_greeting
138
+ @socket.read(EXPECTED_GREETING.size)
139
+ end
140
+
141
+ def parse_greeting(greeting)
142
+ case greeting
143
+ when EXPECTED_GREETING
144
+ whisper "Performed handshake."
145
+ when nil
146
+ misbehavior! "Test closed connection without sending anything."
147
+ when POLICY_FILE_REQUEST
148
+ fatal_misbehavior! \
149
+ "Received cross-domain policy file request; aborting.",
150
+ "Please run a policy server on port 843 (root usually needed).",
151
+ "See ‘bin/policy-server.rb’ in the ASAutotest distribution."
152
+ else
153
+ misbehavior! "Unrecognized greeting: #{greeting.inspect}"
154
+ end
155
+ end
156
+
157
+ def talk_to_test
158
+ @test_stopwatch = Stopwatch.new
159
+ Timeout.timeout(10) { talk_patiently_to_test }
160
+ report_results
161
+ rescue Timeout::Error
162
+ misbehavior! "Test run taking too long; aborting."
163
+ end
164
+
165
+ def report_results
166
+ if failed_tests?
167
+ suites.each &:print_report!
168
+ puts
169
+ else
170
+ say test_count_report
171
+ shout "Missing #{n_missing_tests} tests." if missing_tests?
172
+ end
173
+ end
174
+
175
+ def test_count_report
176
+ build_string do |result|
177
+ result << "Ran #{n_completed_tests} tests"
178
+
179
+ if new_tests?
180
+ result << " (#{n_new_tests} new)"
181
+ elsif missing_tests?
182
+ result << " (too few)"
183
+ end
184
+
185
+ result << " in ~#@test_stopwatch seconds."
186
+ end
187
+ end
188
+
189
+ def talk_patiently_to_test
190
+ catch(:done) do
191
+ loop do
192
+ line = @socket.readline.chomp
193
+ case line
194
+ when /^plan (\d+)$/
195
+ if @n_planned_tests != nil
196
+ misbehavior! "Got another plan: #{line.inspect}"
197
+ elsif n_completed_tests > 0
198
+ misbehavior! "Got plan too late: #{line.inspect}"
199
+ else
200
+ @n_planned_tests = $1.to_i
201
+ whisper "Planning to run #{@n_planned_tests} tests."
202
+ end
203
+ when "done"
204
+ throw :done
205
+ when /^xml-result: (.*)/
206
+ begin
207
+ result = Result.parse_xml($1)
208
+ get_suite(result.suite_name) << result
209
+ rescue Result::ParseError
210
+ misbehavior! "Could not interpret XML result: #$1"
211
+ end
212
+ else
213
+ puts ">> #{line.inspect}"
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ def n_completed_tests
220
+ suites.map(&:n_results).sum
221
+ end
222
+
223
+ def n_failed_tests
224
+ suites.map(&:n_failures).sum
225
+ end
226
+
227
+ def suites
228
+ @suites.values
229
+ end
230
+
231
+ def get_suite(name)
232
+ if @suites.include? name
233
+ @suites[name]
234
+ else
235
+ @suites[name] = Suite.new(name)
236
+ end
237
+ end
238
+
239
+ class Suite
240
+ attr_reader :name
241
+
242
+ def initialize(name)
243
+ @name = name
244
+ @results = []
245
+ end
246
+
247
+ def << result
248
+ @results << result
249
+ end
250
+
251
+ def print_report!
252
+ if @results.any? &:failed?
253
+ print_header!
254
+ @results.each &:print_report!
255
+ end
256
+ end
257
+
258
+ def print_header!
259
+ puts
260
+ puts "\e[1m#{display_name}\e[0m"
261
+ end
262
+
263
+ def display_name
264
+ @name or "(Unnamed suite)"
265
+ end
266
+
267
+ def n_results
268
+ @results.size
269
+ end
270
+
271
+ def n_failures
272
+ @results.count &:failed?
273
+ end
274
+ end
275
+
276
+ class Result
277
+ class ParseError < Exception ; end
278
+
279
+ include Logging
280
+
281
+ attr_reader :test_name
282
+
283
+ def initialize(test_name)
284
+ @test_name = test_name
285
+ end
286
+
287
+ def self.parse_xml(input)
288
+ XMLResult.new(REXML::Document.new(input).root).result
289
+ rescue
290
+ raise ParseError
291
+ end
292
+
293
+ class XMLResult
294
+ def initialize(root)
295
+ @root = root
296
+ end
297
+
298
+ def result
299
+ case @root.name
300
+ when "success"
301
+ Success.new(test_name)
302
+ when "failure"
303
+ failure
304
+ else
305
+ raise ParseError
306
+ end
307
+ end
308
+
309
+ def test_name
310
+ @root.attributes["test-name"] or raise ParseError
311
+ end
312
+
313
+ def failure
314
+ case failure_type
315
+ when "equality"
316
+ Failure::Equality.new \
317
+ test_name,
318
+ failure_attribute("expected"),
319
+ failure_attribute("actual")
320
+ else
321
+ Failure::Simple.new(test_name, description)
322
+ end
323
+ end
324
+
325
+ def failure_type
326
+ failure_element.name if failure_element
327
+ end
328
+
329
+ def description
330
+ @root.attributes["description"]
331
+ end
332
+
333
+ def failure_attribute(name)
334
+ failure_element.attributes[name] or raise ParseError
335
+ end
336
+
337
+ def failure_element
338
+ @root.elements[1]
339
+ end
340
+ end
341
+
342
+ def passed? ; not failed? end
343
+
344
+ def local_name
345
+ local_and_suite_names[1]
346
+ end
347
+
348
+ def suite_name
349
+ local_and_suite_names[2]
350
+ end
351
+
352
+ def local_and_suite_names
353
+ test_name.match /^(.*?)(?: \((\S+)\))?$/
354
+ end
355
+
356
+ class Success < Result
357
+ def failed? ; false end
358
+
359
+ def print_report!
360
+ whisper "Passed: #{test_name}"
361
+ end
362
+ end
363
+
364
+ class Failure < Result
365
+ def failed? ; true end
366
+
367
+ def print_report!
368
+ puts " \e[1;31mFailed:\e[0m \e[0;4m#{local_name}\e[0m"
369
+ report_reason!
370
+ end
371
+
372
+ def report_reason! ; end
373
+
374
+ class Simple < Failure
375
+ def initialize(test_name, description)
376
+ super(test_name)
377
+ @description = description
378
+ end
379
+
380
+ def report_reason!
381
+ puts " \e[0m#@description\e[0m" if @description
382
+ end
383
+ end
384
+
385
+ class Equality < Failure
386
+ def initialize(test_name, expected, actual)
387
+ super(test_name)
388
+ @expected = expected
389
+ @actual = actual
390
+ end
391
+
392
+ def report_reason!
393
+ puts " \e[0mActual:\e[0m \e[0m#@actual\e[0m"
394
+ puts " \e[0mExpected:\e[0m \e[0m#@expected\e[0m"
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ # ------------------------------------------------------
401
+
402
+ attr_reader :n_planned_tests
403
+
404
+ def n_missing_tests
405
+ n_planned_tests - n_completed_tests
406
+ end
407
+
408
+ def n_new_tests
409
+ n_completed_tests - n_planned_tests
410
+ end
411
+
412
+ def new_tests?
413
+ n_new_tests > 0
414
+ end
415
+
416
+ def missing_tests?
417
+ n_missing_tests > 0
418
+ end
419
+
420
+ def failed_tests?
421
+ n_failed_tests > 0
422
+ end
423
+ end
424
+ end