qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Qemu-devel] [RFC] image-fuzzer: Trivial test runner


From: M.Kustova
Subject: Re: [Qemu-devel] [RFC] image-fuzzer: Trivial test runner
Date: Thu, 5 Jun 2014 12:49:36 +0400

On Wed, Jun 4, 2014 at 6:26 PM, Stefan Hajnoczi <address@hidden> wrote:
> On Sat, May 31, 2014 at 01:56:46PM +0400, Maria Kustova wrote:
>
> Please add a --format qcow2 (or maybe --image-generator) option using
> __import__() to load the image generator module.  That way people can
> drop in new image generator modules in the future and we don't hard-code
> their names into the runner.
>
>> This version of test runner executes only one test. In future it will be
>> extended to execute multiple tests in a run.
>>
>> Signed-off-by: Maria Kustova <address@hidden>
>> ---
>>  tests/image-fuzzer/runner.py | 225 
>> +++++++++++++++++++++++++++++++++++++++++++
>>  1 file changed, 225 insertions(+)
>>  create mode 100644 tests/image-fuzzer/runner.py
>>
>> diff --git a/tests/image-fuzzer/runner.py b/tests/image-fuzzer/runner.py
>> new file mode 100644
>> index 0000000..1dea8ef
>> --- /dev/null
>> +++ b/tests/image-fuzzer/runner.py
>> @@ -0,0 +1,225 @@
>> +# Tool for running fuzz tests
>> +#
>> +# Copyright (C) 2014 Maria Kustova <address@hidden>
>> +#
>> +# This program is free software: you can redistribute it and/or modify
>> +# it under the terms of the GNU General Public License as published by
>> +# the Free Software Foundation, either version 3 of the License, or
>> +# (at your option) any later version.
>> +#
>> +# This program is distributed in the hope that it will be useful,
>> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
>> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
>> +# GNU General Public License for more details.
>> +#
>> +# You should have received a copy of the GNU General Public License
>> +# along with this program.  If not, see <http://www.gnu.org/licenses/>.
>> +#
>> +
>> +import sys, os, signal
>> +import qcow2
>> +from time import gmtime, strftime
>> +import subprocess
>> +from shutil import rmtree
>> +import getopt
>> +# -----For local test environment only
>> +import resource
>> +resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
>> +# -----
>
> Enabling core dumps is important.  I'm not sure why it says "For local
> test environment only".
>
> This assumes that core dumps are written to the current working
> directory.  On Linux this is configurable and some distros default to a
> fancier setup where core dumps are not written to the current working
> directory.  For now, please add a note to the documentation explaining
> that core dumps should be configured to use the current working
> directory.
>
>> +
>> +def multilog(msg, *output):
>> +    """ Write an object to all of specified file descriptors
>> +    """
>> +
>> +    for fd in output:
>> +        fd.write(msg)
>> +        fd.flush()
>> +
>> +
>> +def str_signal(sig):
>> +    """ Convert a numeric value of a system signal to the string one
>> +    defined by the current operational system
>> +    """
>> +
>> +    for k, v in signal.__dict__.items():
>> +        if v == sig:
>> +            return k
>> +
>> +
>> +class TestEnv(object):
>> +    """ Trivial test object
>> +
>> +    The class sets up test environment, generates a test image and executes
>> +    qemu_img with specified arguments and a test image provided. All logs
>> +    are collected.
>> +    Summary log will contain short descriptions and statuses of all tests in
>> +    a run.
>> +    Test log will include application ('qemu-img') logs besides info sent
>> +    to the summary log.
>> +    """
>> +
>> +    def __init__(self, work_dir, run_log, exec_bin=None, cleanup=True):
>> +        """Set test environment in a specified work directory.
>> +
>> +        Path to qemu_img will be retrieved from 'QEMU_IMG' environment
>> +        variable, if not specified.
>> +        """
>> +
>> +        self.init_path = os.getcwd()
>> +        self.work_dir = work_dir
>> +        self.current_dir = os.path.join(work_dir, 
>> strftime("%Y_%m_%d_%H-%M-%S",
>> +                                                           gmtime()))
>> +        if exec_bin is not None:
>> +            self.exec_bin = exec_bin.strip().split(' ')
>> +        else:
>> +            self.exec_bin = os.environ.get('QEMU_IMG', 'qemu-img').strip()\
>> +            .split(' ')
>> +
>> +        try:
>> +            os.makedirs(self.current_dir)
>> +        except OSError:
>> +            e = sys.exc_info()[1]
>> +            print >>sys.stderr, 'Error: The working directory cannot be 
>> used.'\
>> +                ' Reason: %s' %e[1]
>> +            raise Exception('Internal error')
>
> I guess this exception is really sys.exit(1).
>
>> +
>> +        self.log = open(os.path.join(self.current_dir, "test.log"), "w")
>> +        self.parent_log = open(run_log, "a")
>> +        self.result = False
>> +        self.cleanup = cleanup
>> +
>> +    def _qemu_img(self, q_args):
>> +        """ Start qemu_img with specified arguments and return an exit code 
>> or
>> +        a kill signal depending on result of an execution.
>> +        """
>> +        devnull = open('/dev/null', 'r+')
>> +        return subprocess.call(self.exec_bin \
>> +                               + q_args +
>> +                               ['test_image.qcow2'], stdin=devnull,
>> +                               stdout=self.log, stderr=self.log)
>> +
>> +
>> +    def execute(self, q_args, seed, size=8*512):
>> +        """ Execute a test.
>> +
>> +        The method creates a test image, runs 'qemu_img' and analyzes its 
>> exit
>> +        status. If the application was killed by a signal, the test is 
>> marked
>> +        as failed.
>> +        """
>> +        os.chdir(self.current_dir)
>> +        seed = qcow2.create_image('test_image.qcow2', seed, size)
>
> The qcow2 module is missing from this patch series.

As far as the qcow2 module was just a stub and an image format should
not be hardcoded, it will be sent as a patch as soon as it gets some
functionality implemented.

>> +        multilog("Seed: %s\nCommand: %s\nTest directory: %s\n"\
>> +                 %(seed, " ".join(q_args), self.current_dir),\
>> +                 sys.stdout, self.log, self.parent_log)
>
> It will probably be useful to dial back the logging for test cases that
> pass.

This information is kept for heisenbugs, that can be reproduced only
in the sequence of several tests. It's significant if all passed tests
data would be removed.

>
>> +        try:
>> +            retcode = self._qemu_img(q_args)
>> +        except OSError:
>> +            e = sys.exc_info()[1]
>> +            multilog("Error: Start of 'qemu_img' failed. Reason: %s\n"\
>> +                     %e[1], sys.stderr, self.log, self.parent_log)
>> +            raise Exception('Internal error')
>> +
>> +        if retcode < 0:
>> +            multilog('FAIL: Test terminated by signal %s\n'
>> +                     %str_signal(-retcode), sys.stderr, self.log, \
>> +                     self.parent_log)
>> +        else:
>> +            multilog("PASS: Application exited with the code '%d'\n"
>> +                     %retcode, sys.stdout, self.log, self.parent_log)
>> +            self.result = True
>> +
>> +    def finish(self):
>> +        """ Restore environment after a test execution. Remove folders of
>> +        passed tests
>> +        """
>> +        self.log.close()
>> +        # Delimiter between tests
>> +        self.parent_log.write("\n")
>> +        self.parent_log.close()
>> +        os.chdir(self.init_path)
>> +        if self.result and self.cleanup:
>> +            rmtree(self.current_dir)
>> +
>> +if __name__ == '__main__':
>> +
>> +    def usage():
>> +        print("""
>> +        Usage: runner.py [OPTION...] DIRECTORY
>> +
>> +        Set up test environment in DIRECTORY and run a test in it.
>> +
>> +        Optional arguments:
>> +          -h, --help           display this help and exit
>> +          -c, --command=STRING execute qemu-img with arguments specified,
>> +                               by default STRING="check"
>> +          -b, --binary=PATH    path to the application under test, by 
>> default
>> +                               "qemu-img" in PATH or QEMU_IMG environment
>> +                               variables
>> +          -s, --seed=STRING    seed for a test image generation, by default
>> +                               will be generated randomly
>> +          -k, --keep_passed    don't remove folders of passed tests
>> +        """)
>> +
>> +    try:
>> +        opts, args = getopt.getopt(sys.argv[1:], 'c:hb:s:k',
>> +                                   ['command=', 'help', 'binary=', 'seed=',
>> +                                    'keep_passed'])
>> +    except getopt.error:
>> +        e = sys.exc_info()[1]
>> +        print('Error: %s\n\nTry runner.py --help.' %e)
>> +        sys.exit(1)
>> +
>> +    if len(sys.argv) == 1:
>> +        usage()
>> +        sys.exit(1)
>
> The "if not len(args) == 1" check further down does the same thing.  It
> can be confusing to mix sys.argv with getopt so I suggest dropping this
> one.
>
>> +    command = ['check']
>> +    cleanup = True
>> +    test_bin = None
>> +    seed = None
>> +    for opt, arg in opts:
>> +        if opt in ('-h', '--help'):
>> +            usage()
>> +            sys.exit()
>> +        elif opt in ('-c', '--command'):
>> +            command = arg.split(" ")
>> +        elif opt in ('-k', '--keep_passed'):
>> +            cleanup = False
>> +        elif opt in ('-b', '--binary'):
>> +            test_bin = arg
>> +        elif opt in ('-s', '--seed'):
>> +            seed = arg
>> +
>> +    if not len(args) == 1:
>> +        print 'Error: required parameter "DIRECTORY" missed'
>> +        usage()
>> +        sys.exit(1)
>> +
>> +    work_dir = args[0]
>> +    # run_log created in 'main', because multiple tests are expected to \
>> +    # log in it
>> +    # TODO: Make unique run_log names on every run (for one test per run
>> +    # this functionality is omitted in favor of usability)
>
> I don't understand this TODO comment.  Here's roughly what I expected
> when I read the --help output:
>
>   work_dir/
>       run.log
>       failure-01/
>           core
>           input.img
>           cmdline
>           seed
>

You can do several test runs one by one in the same working directory.
Each run will have a unique name and all tests of the run will write
their output to the log with this unique name.
Something like:

work_dir/
       run-01.log
       run-02.log
       failure-02_01/
        core
        input.img
        test-02_01.log

For now outputs of all runs are appended to the same file with name
'run.log' while there is only one test per run.

>> +    run_log = os.path.join(work_dir, 'run.log')
>> +
>> +    try:
>> +        test = TestEnv(work_dir, run_log, test_bin, cleanup)
>> +    except:
>> +        e = sys.exc_info()[1]
>> +        print("FAIL: %s"  %e)
>> +        sys.exit(1)
>
> Since you're just printing the exception you might as well omit the
> exception handler.  Let Python's default unhandled exception handler
> terminate the script instead of duplicating that here.
>
>> +
>> +    # Python 2.4 doesn't support 'finally' and 'except' in the same 'try'
>> +    # block
>> +    try:
>> +        try:
>> +            test.execute(command, seed)
>> +            #Silent exit on user break
>> +        except (KeyboardInterrupt, SystemExit):
>> +            sys.exit(1)
>> +        except:
>> +            e = sys.exc_info()[1]
>> +            print("FAIL: %s"  %e)
>> +            sys.exit(1)
>
> Same thing about unhandled exceptions here.
>
>> +    finally:
>> +        test.finish()
>> --
>> 1.8.2.1
>>
>>

Thanks for your feedback.
BR, M.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]