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