1#!/usr/bin/env python 2# 3# Copyright (C) 2012-2013 by Eero Tamminen <oak at helsinkinet fi> 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14""" 15Tester boots the given TOS versions under Hatari with all the possible 16combinations of the given machine HW configuration options, that are 17supported by the tested TOS version. 18 19Verification screenshot is taken at the end of each boot before 20proceeding to testing of the next combination. Screenshot name 21indicates the used combination, for example: 22 etos512k-falcon-rgb-gemdos-14M.png 23 etos512k-st-mono-floppy-1M.png 24 25 26NOTE: If you want to test the latest, uninstalled version of Hatari, 27you need to set PATH to point to your Hatari binary directory, like 28this: 29 PATH=../../build/src:$PATH tos_tester.py <TOS images> 30 31If hconsole isn't installed to one of the standard locations (under 32/usr or /usr/local), or you don't run this from within Hatari sources, 33you also need to specify hconsole.py location with: 34 export PYTHONPATH=/path/to/hconsole 35""" 36 37import getopt, os, signal, select, sys, time 38 39def add_hconsole_paths(): 40 "add most likely hconsole locations to module import path" 41 # prefer the devel version in Hatari sources, if it's found 42 subdirs = len(os.path.abspath(os.curdir).split(os.path.sep))-1 43 for level in range(subdirs): 44 f = level*(".." + os.path.sep) + "tools/hconsole/hconsole.py" 45 if os.path.isfile(f): 46 f = os.path.dirname(f) 47 sys.path.append(f) 48 print "Added local hconsole path: %s" % f 49 break 50 sys.path += ["/usr/local/share/hatari/hconsole", 51 "/usr/share/hatari/hconsole"] 52 53add_hconsole_paths() 54import hconsole 55 56 57def warning(msg): 58 "output warning message" 59 sys.stderr.write("WARNING: %s\n" % msg) 60 61 62# ----------------------------------------------- 63class TOS: 64 "class for TOS image information" 65 # objects have members: 66 # - path (string), given TOS image file path/name 67 # - name (string), filename with path and extension stripped 68 # - size (int), image file size, in kB 69 # - etos (bool), is EmuTOS? 70 # - version (int), TOS version 71 # - memwait (int), how many secs to wait before memcheck key press 72 # - fullwait (int), after which time safe to conclude boot to have failed 73 # - machines (tuple of strings), which Atari machines this TOS supports 74 75 def __init__(self, path): 76 self.path, self.size, self.name = self._add_file(path) 77 self.version, self.etos = self._add_version() 78 self.memwait, self.fullwait, self.machines = self._add_info() 79 80 81 def _add_file(self, img): 82 "get TOS file size and basename for 'img'" 83 if not os.path.isfile(img): 84 raise AssertionError("'%s' given as TOS image isn't a file" % img) 85 86 size = os.stat(img).st_size 87 tossizes = (196608, 262144, 524288) 88 if size not in tossizes: 89 raise AssertionError("image '%s' size not one of TOS sizes %s" % (img, repr(tossizes))) 90 91 name = os.path.basename(img) 92 name = name[:name.rfind('.')] 93 return (img, size/1024, name) 94 95 96 def _add_version(self): 97 "get TOS version and whether it's EmuTOS & supports GEMDOS HD" 98 f = open(self.path) 99 f.seek(0x2, 0) 100 version = (ord(f.read(1)) << 8) + ord(f.read(1)) 101 # older TOS versions don't support autostarting 102 # programs from GEMDOS HD dir with *.INF files 103 f.seek(0x2C, 0) 104 etos = (f.read(4) == "ETOS") 105 return (version, etos) 106 107 108 def _add_info(self): 109 "add TOS version specific info of supported machines etc" 110 name, version = self.name, self.version 111 112 if self.etos: 113 # EmuTOS 512k, 256k and 192k versions have different machine support 114 if self.size == 512: 115 # startup screen on falcon 14MB is really slow 116 info = (5, 10, ("st", "ste", "tt", "falcon")) 117 elif self.size == 256: 118 info = (2, 8, ("st", "ste", "tt")) 119 elif self.size == 192: 120 info = (0, 6, ("st",)) 121 else: 122 raise AssertionError("'%s' image size %dkB isn't valid for EmuTOS" % (name, size)) 123 elif version <= 0x100: 124 # boots up really slow with 4MB 125 info = (0, 16, ("st",)) 126 elif version <= 0x104: 127 info = (0, 6, ("st",)) 128 elif version < 0x200: 129 info = (0, 6, ("ste",)) 130 elif version < 0x300: 131 info = (1, 6, ("st", "ste", "tt")) 132 elif version < 0x400: 133 # memcheck comes up fast, but boot takes time 134 info = (2, 8, ("tt",)) 135 elif version < 0x500: 136 # memcheck takes long to come up with 14MB 137 info = (3, 8, ("falcon",)) 138 else: 139 raise AssertionError("'%s' TOS version 0x%x isn't valid" % (name, version)) 140 141 if self.etos: 142 print "%s is EmuTOS v%x %dkB" % (name, version, self.size) 143 else: 144 print "%s is normal TOS v%x" % (name, version) 145 # 0: whether / how long to wait to dismiss memory test 146 # 1: how long to wait until concluding test failed 147 # 2: list of machines supported by this TOS version 148 return info 149 150 def supports_gemdos_hd(self): 151 "whether TOS version supports Hatari's GEMDOS HD emulation" 152 return (self.version >= 0x0104) 153 154 def supports_hdinterface(self, hdinterface): 155 "whether TOS version supports monitor that is valid for given machine" 156 # EmuTOS doesn't require drivers to access DOS formatted disks 157 if self.etos: 158 # NOTE: IDE support is in EmuTOS since 0.9.0 159 if hdinterface == "ide" and self.size == 192: 160 return False 161 return True 162 # As ACSI (big endian) and IDE (little endian) images would require 163 # diffent binary drivers on them and it's not possible to generate 164 # such images automatically, testing ACSI & IDE images for normal 165 # TOS isn't support. 166 # 167 # (And even with a driver, only TOS 4.x supports IDE.) 168 return False 169 170 def supports_monitor(self, monitortype, machine): 171 "whether TOS version supports monitor that is valid for given machine" 172 # other monitor types valid for the machine are 173 # valid also for TOS that works on it 174 if monitortype.startswith("vdi"): 175 # sensible sized VDI modes don't work with TOS4 176 # (nor make sense with its Videl expander support) 177 if self.version >= 0x400: 178 return False 179 if self.etos: 180 # smallest EmuTOS image doesn't have any Falcon support 181 if machine == "falcon" and self.size == 192: 182 return False 183 # 2-plane modes don't work properly with real TOS 184 elif monitortype.endswith("2"): 185 return False 186 return True 187 188 189# ----------------------------------------------- 190def validate(args, full): 191 "return set of members not in the full set and given args" 192 return (set(args).difference(full), args) 193 194class Config: 195 "Test configuration and validator class" 196 # full set of possible options 197 all_disks = ("floppy", "gemdos", "acsi", "ide") 198 all_graphics = ("mono", "rgb", "vga", "tv", "vdi1", "vdi2", "vdi4") 199 all_machines = ("st", "ste", "tt", "falcon") 200 all_memsizes = (0, 1, 2, 4, 6, 8, 10, 12, 14) 201 202 # defaults 203 fast = False 204 bools = [] 205 disks = ("floppy", "gemdos") 206 graphics = ("mono", "rgb", "vdi1") 207 machines = ("st", "ste", "tt", "falcon") 208 memsizes = (0, 4, 14) 209 210 def __init__(self, argv): 211 longopts = ["bool=", "disks=", "fast", "graphics=", "help", "machines=", "memsizes="] 212 try: 213 opts, paths = getopt.gnu_getopt(argv[1:], "b:d:fg:hm:s:", longopts) 214 except getopt.GetoptError as error: 215 self.usage(error) 216 self.handle_options(opts) 217 self.images = self.check_images(paths) 218 print "Test configuration:\n\t", self.disks, self.graphics, self.machines, self.memsizes 219 220 221 def check_images(self, paths): 222 "validate given TOS images" 223 images = [] 224 for img in paths: 225 try: 226 images.append(TOS(img)) 227 except AssertionError as msg: 228 self.usage(msg) 229 if len(images) < 1: 230 self.usage("no TOS image files given") 231 return images 232 233 234 def handle_options(self, opts): 235 "parse command line options" 236 unknown = None 237 for opt, arg in opts: 238 args = arg.split(",") 239 if opt in ("-h", "--help"): 240 self.usage() 241 if opt in ("-f", "--fast"): 242 self.fast = True 243 elif opt in ("-b", "--bool"): 244 self.bools += args 245 elif opt in ("-d", "--disks"): 246 unknown, self.disks = validate(args, self.all_disks) 247 elif opt in ("-g", "--graphics"): 248 unknown, self.graphics = validate(args, self.all_graphics) 249 elif opt in ("-m", "--machines"): 250 unknown, self.machines = validate(args, self.all_machines) 251 elif opt in ("-s", "--memsizes"): 252 try: 253 args = [int(i) for i in args] 254 except ValueError: 255 self.usage("non-numeric memory sizes: %s" % arg) 256 unknown, self.memsizes = validate(args, self.all_memsizes) 257 if unknown: 258 self.usage("%s are invalid values for %s" % (list(unknown), opt)) 259 260 261 def usage(self, msg=None): 262 "output program usage information" 263 name = os.path.basename(sys.argv[0]) 264 print __doc__ 265 print(""" 266Usage: %s [options] <TOS image files> 267 268Options: 269\t-h, --help\tthis help 270\t-f, --fast\tdo tests with "--fastfdc yes --fast-forward yes" 271\t-d, --disks\t%s 272\t-g, --graphics\t%s 273\t-m, --machines\t%s 274\t-s, --memsizes\t%s 275\t-b, --bool\t(extra boolean Hatari options to test) 276 277Multiple values for an option need to be comma separated. If some 278option isn't given, default list of values will be used for that. 279 280For example: 281 %s \\ 282\t--disks gemdos \\ 283\t--machines st,tt \\ 284\t--memsizes 0,4,14 \\ 285\t--graphics mono,rgb \\ 286\t-bool --compatible,--rtc 287""" % (name, self.all_disks, self.all_graphics, self.all_machines, self.all_memsizes, name)) 288 if msg: 289 print("ERROR: %s\n" % msg) 290 sys.exit(1) 291 292 293 def valid_disktype(self, machine, tos, disktype): 294 "return whether given disk type is valid for given machine / TOS version" 295 if disktype == "floppy": 296 return True 297 if disktype == "gemdos": 298 return tos.supports_gemdos_hd() 299 300 if machine in ("st", "ste"): 301 hdinterface = "acsi" 302 elif machine == "tt": 303 # TODO: according to todo.txt, Hatari ACSI emulation 304 # doesn't currently work for TT 305 hdinterface = "acsi" 306 elif machine == "falcon": 307 hdinterface = "ide" 308 else: 309 raise AssertionError("unknown machine %s" % machine) 310 311 if disktype in hdinterface: 312 return tos.supports_hdinterface(hdinterface) 313 return False 314 315 def valid_monitortype(self, machine, tos, monitortype): 316 "return whether given monitor type is valid for given machine / TOS version" 317 if machine in ("st", "ste"): 318 monitors = ("mono", "rgb", "tv", "vdi1", "vdi2", "vdi4") 319 elif machine == "tt": 320 monitors = ("mono", "vga", "vdi1", "vdi2", "vdi4") 321 elif machine == "falcon": 322 monitors = ("mono", "rgb", "vga", "vdi1", "vdi2", "vdi4") 323 else: 324 raise AssertionError("unknown machine %s" % machine) 325 if monitortype in monitors: 326 return tos.supports_monitor(monitortype, machine) 327 return False 328 329 def valid_memsize(self, machine, memsize): 330 "return whether given memory size is valid for given machine" 331 if machine in ("st", "ste"): 332 sizes = (0, 1, 2, 4) 333 elif machine in ("tt", "falcon"): 334 # 0 (512kB) isn't valid memory size for Falcon/TT 335 sizes = self.all_memsizes[1:] 336 else: 337 raise AssertionError("unknown machine %s" % machine) 338 if memsize in sizes: 339 return True 340 return False 341 342 343# ----------------------------------------------- 344def verify_match(srcfile, dstfile): 345 "return error string if given files are not identical" 346 if not os.path.exists(dstfile): 347 return "file '%s' missing" % dstfile 348 i = 0 349 f2 = open(srcfile) 350 for line in open(dstfile).readlines(): 351 i += 1 352 if line != f2.readline(): 353 return "file '%s' line %d doesn't match file '%s'" % (dstfile, i, srcfile) 354 355def verify_empty(srcfile): 356 "return error string if given file isn't empty" 357 if not os.path.exists(srcfile): 358 return "file '%s' missing" % srcfile 359 lines = len(open(srcfile).readlines()) 360 if lines > 0: 361 return "file '%s' isn't empty (%d lines)" % (srcfile, lines) 362 363class Tester: 364 "test driver class" 365 output = "output" + os.path.sep 366 report = output + "report.txt" 367 # dummy Hatari config file to force suitable default options 368 dummycfg = "dummy.cfg" 369 defaults = [sys.argv[0], "--configfile", dummycfg] 370 testprg = "disk" + os.path.sep + "GEMDOS.PRG" 371 textinput = "disk" + os.path.sep + "TEXT" 372 textoutput= "disk" + os.path.sep + "TEST" 373 printout = output + "printer-out" 374 serialout = output + "serial-out" 375 fifofile = output + "midi-out" 376 bootauto = "bootauto.st.gz" # TOS old not to support GEMDOS HD either 377 bootdesk = "bootdesk.st.gz" 378 hdimage = "hd.img" 379 ideimage = "hd.img" # for now use the same image as for ACSI 380 results = None 381 382 def __init__(self): 383 "test setup initialization" 384 self.cleanup_all_files() 385 self.create_config() 386 self.create_files() 387 signal.signal(signal.SIGALRM, self.alarm_handler) 388 389 def alarm_handler(self, signum, dummy): 390 "output error if (timer) signal came before passing current test stage" 391 if signum == signal.SIGALRM: 392 print "ERROR: timeout triggered -> test FAILED" 393 else: 394 print "ERROR: unknown signal %d received" % signum 395 raise AssertionError 396 397 def create_config(self): 398 "create Hatari configuration file for testing" 399 # write specific configuration to: 400 # - avoid user's own config 401 # - get rid of the dialogs 402 # - limit Videl zooming to same sizes as ST screen zooming 403 # - get rid of statusbar and borders in TOS screenshots 404 # to make them smaller & more consistent 405 # - disable GEMDOS emu by default 406 # - use empty floppy disk image to avoid TOS error when no disks 407 # - set printer output file 408 # - disable serial in and set serial output file 409 # - disable MIDI in, use MIDI out as fifo file to signify test completion 410 dummy = open(self.dummycfg, "w") 411 dummy.write("[Log]\nnAlertDlgLogLevel = 0\nbConfirmQuit = FALSE\n") 412 dummy.write("[Screen]\nnMaxWidth=832\nnMaxHeight=576\nbCrop = TRUE\nbAllowOverscan=FALSE\n") 413 dummy.write("[HardDisk]\nbUseHardDiskDirectory = FALSE\n") 414 dummy.write("[Floppy]\nszDiskAFileName = blank-a.st.gz\n") 415 dummy.write("[Printer]\nbEnablePrinting = TRUE\nszPrintToFileName = %s\n" % self.printout) 416 dummy.write("[RS232]\nbEnableRS232 = TRUE\nszInFileName = \nszOutFileName = %s\n" % self.serialout) 417 dummy.write("[Midi]\nbEnableMidi = TRUE\nsMidiInFileName = \nsMidiOutFileName = %s\n" % self.fifofile) 418 dummy.close() 419 420 def cleanup_all_files(self): 421 "clean out any files left over from last run" 422 for path in (self.fifofile, "grab0001.png", "grab0001.bmp"): 423 if os.path.exists(path): 424 os.remove(path) 425 self.cleanup_test_files() 426 427 def create_files(self): 428 "create files needed during testing" 429 if not os.path.exists(self.output): 430 os.mkdir(self.output) 431 if not os.path.exists(self.fifofile): 432 os.mkfifo(self.fifofile) 433 434 def get_screenshot(self, instance, identity): 435 "save screenshot of test end result" 436 instance.run("screenshot") 437 if os.path.isfile("grab0001.png"): 438 os.rename("grab0001.png", self.output + identity + ".png") 439 elif os.path.isfile("grab0001.bmp"): 440 os.rename("grab0001.bmp", self.output + identity + ".bmp") 441 else: 442 warning("failed to locate screenshot grab0001.{png,bmp}") 443 444 def cleanup_test_files(self): 445 "remove unnecessary files at end of test" 446 for path in (self.serialout, self.printout): 447 if os.path.exists(path): 448 os.remove(path) 449 450 def verify_output(self, identity, tos, memory): 451 "do verification on all test output" 452 # both tos version and amount of memory affect what 453 # GEMDOS operations work properly... 454 ok = True 455 # check file truncate 456 error = verify_empty(self.textoutput) 457 if error: 458 print "ERROR: file wasn't truncated:\n\t%s" % error 459 os.rename(self.textoutput, "%s.%s" % (self.textoutput, identity)) 460 ok = False 461 # check serial output 462 error = verify_match(self.textinput, self.serialout) 463 if error: 464 print "ERROR: serial output doesn't match input:\n\t%s" % error 465 os.rename(self.serialout, "%s.%s" % (self.serialout, identity)) 466 ok = False 467 # check printer output 468 error = verify_match(self.textinput, self.printout) 469 if error: 470 if tos.etos or tos.version > 0x206 or (tos.version == 0x100 and memory > 1): 471 print "ERROR: printer output doesn't match input (EmuTOS, TOS v1.00 or >v2.06)\n\t%s" % error 472 os.rename(self.printout, "%s.%s" % (self.printout, identity)) 473 ok = False 474 else: 475 if os.path.exists(self.printout): 476 error = verify_empty(self.printout) 477 if error: 478 print "WARNING: unexpected printer output (TOS v1.02 - TOS v2.06):\n\t%s" % error 479 os.rename(self.printout, "%s.%s" % (self.printout, identity)) 480 self.cleanup_test_files() 481 return ok 482 483 484 def wait_fifo(self, fifo, timeout): 485 "wait_fifo(fifo) -> wait until fifo has input until given timeout" 486 print("Waiting %ss for fifo '%s' input..." % (timeout, self.fifofile)) 487 sets = select.select([fifo], [], [], timeout) 488 if sets[0]: 489 print "...test program is READY, read what's in its fifo:", 490 try: 491 # read can block, make sure it's eventually interrupted 492 signal.alarm(timeout) 493 line = fifo.readline().strip() 494 signal.alarm(0) 495 print line 496 return (True, (line == "success")) 497 except IOError: 498 pass 499 print "ERROR: TIMEOUT without fifo input, BOOT FAILED" 500 return (False, False) 501 502 503 def open_fifo(self, timeout): 504 "open fifo for test program output" 505 try: 506 signal.alarm(timeout) 507 # open returns after Hatari has opened the other 508 # end of fifo, or when SIGALARM interrupts it 509 fifo = open(self.fifofile, "r") 510 # cancel signal 511 signal.alarm(0) 512 return fifo 513 except IOError: 514 print "ERROR: fifo open IOError!" 515 return None 516 517 518 def test(self, identity, testargs, tos, memory): 519 "run single boot test with given args and waits" 520 # Hatari command line options, don't exit if Hatari exits 521 instance = hconsole.Main(self.defaults + testargs, False) 522 fifo = self.open_fifo(tos.fullwait) 523 if not fifo: 524 print "ERROR: failed to get fifo to Hatari!" 525 self.get_screenshot(instance, identity) 526 instance.run("kill") 527 return (False, False, False, False) 528 else: 529 init_ok = True 530 531 if tos.memwait: 532 # pass memory test 533 time.sleep(tos.memwait) 534 instance.run("keypress %s" % hconsole.Scancode.Space) 535 536 # wait until test program has been run and output something to fifo 537 prog_ok, tests_ok = self.wait_fifo(fifo, tos.fullwait) 538 if tests_ok: 539 output_ok = self.verify_output(identity, tos, memory) 540 else: 541 print "TODO: collect info on failure, regs etc" 542 output_ok = False 543 544 # get screenshot after a small wait (to guarantee all 545 # test program output got to screen even with frameskip) 546 time.sleep(0.2) 547 self.get_screenshot(instance, identity) 548 # get rid of this Hatari instance 549 instance.run("kill") 550 return (init_ok, prog_ok, tests_ok, output_ok) 551 552 553 def prepare_test(self, config, tos, machine, monitor, disk, memory, extra): 554 "compose test ID and Hatari command line args, then call .test()" 555 identity = "%s-%s-%s-%s-%sM" % (tos.name, machine, monitor, disk, memory) 556 testargs = ["--tos", tos.path, "--machine", machine, "--memsize", str(memory)] 557 558 if extra: 559 identity += "-%s%s" % (extra[0].replace("-", ""), extra[1]) 560 testargs += extra 561 562 if monitor.startswith("vdi"): 563 planes = monitor[-1] 564 testargs += ["--vdi-planes", planes] 565 if planes == "1": 566 testargs += ["--vdi-width", "800", "--vdi-height", "600"] 567 elif planes == "2": 568 testargs += ["--vdi-width", "640", "--vdi-height", "480"] 569 else: 570 testargs += ["--vdi-width", "640", "--vdi-height", "400"] 571 else: 572 testargs += ["--monitor", monitor] 573 574 if config.fast: 575 testargs += ["--fastfdc", "yes", "--fast-forward", "yes"] 576 577 if disk == "gemdos": 578 # use Hatari autostart, must be last thing added to testargs! 579 testargs += [self.testprg] 580 elif disk == "floppy": 581 if tos.supports_gemdos_hd(): 582 # GEMDOS HD supporting TOSes support also INF file autostart 583 testargs += ["--disk-a", self.bootdesk] 584 else: 585 testargs += ["--disk-a", self.bootauto] 586 elif disk == "acsi": 587 testargs += ["--acsi", self.hdimage] 588 elif disk == "ide": 589 testargs += ["--ide-master", self.ideimage] 590 else: 591 raise AssertionError("unknown disk type '%s'" % disk) 592 593 results = self.test(identity, testargs, tos, memory) 594 self.results[tos.name].append((identity, results)) 595 596 def run(self, config): 597 "run all TOS boot test combinations" 598 self.results = {} 599 for tos in config.images: 600 self.results[tos.name] = [] 601 print 602 print "***** TESTING: %s *****" % tos.name 603 print 604 count = 0 605 for machine in config.machines: 606 if machine not in tos.machines: 607 continue 608 for monitor in config.graphics: 609 if not config.valid_monitortype(machine, tos, monitor): 610 continue 611 for memory in config.memsizes: 612 if not config.valid_memsize(machine, memory): 613 continue 614 for disk in config.disks: 615 if not config.valid_disktype(machine, tos, disk): 616 continue 617 if config.bools: 618 for opt in config.bools: 619 for val in ('on', 'off'): 620 self.prepare_test(config, tos, machine, monitor, disk, memory, [opt, val]) 621 count += 1 622 else: 623 self.prepare_test(config, tos, machine, monitor, disk, memory, None) 624 count += 1 625 if not count: 626 warning("no matching configuration for TOS '%s'" % tos.name) 627 self.cleanup_all_files() 628 629 def summary(self): 630 "summarize test results" 631 cases = [0, 0, 0, 0] 632 passed = [0, 0, 0, 0] 633 tosnames = self.results.keys() 634 tosnames.sort() 635 636 report = open(self.report, "w") 637 report.write("\nTest report:\n------------\n") 638 for tos in tosnames: 639 configs = self.results[tos] 640 if not configs: 641 report.write("\n+ WARNING: no configurations for '%s' TOS!\n" % tos) 642 continue 643 report.write("\n+ %s:\n" % tos) 644 for config, results in configs: 645 # convert True/False bools to FAIL/pass strings 646 values = [("FAIL","pass")[int(r)] for r in results] 647 report.write(" - %s: %s\n" % (config, values)) 648 # update statistics 649 for idx in range(len(results)): 650 cases[idx] += 1 651 passed[idx] += results[idx] 652 653 report.write("\nSummary of FAIL/pass values:\n") 654 idx = 0 655 for line in ("Hatari init", "Test program running", "Test program test-cases", "Test program output"): 656 passes, total = passed[idx], cases[idx] 657 if passes < total: 658 if not passes: 659 result = "all %d FAILED" % total 660 else: 661 result = "%d/%d passed" % (passes, total) 662 else: 663 result = "all %d passed" % total 664 report.write("- %s: %s\n" % (line, result)) 665 idx += 1 666 report.write("\n") 667 668 # print report out too 669 print "--- %s ---" % self.report 670 report = open(self.report, "r") 671 for line in report.readlines(): 672 print line.strip() 673 674 675# ----------------------------------------------- 676def main(): 677 "tester main function" 678 info = "Hatari TOS bootup tester" 679 print "\n%s\n%s\n" % (info, "-"*len(info)) 680 config = Config(sys.argv) 681 tester = Tester() 682 tester.run(config) 683 tester.summary() 684 685if __name__ == "__main__": 686 main() 687