def init(config_file, session_file): """Initialize a mutation testing session from a configuration. This primarily creates a session - a database of "work to be done" - which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The configuration specifies the top-level module to mutate, the tests to run, and how to run them. This command doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. """ cfg = load_config(config_file) modules = cosmic_ray.modules.find_modules(Path(cfg['module-path'])) modules = cosmic_ray.modules.filter_paths(modules, cfg.get('exclude-modules', ())) if log.isEnabledFor(logging.INFO): log.info('Modules discovered:') per_dir = defaultdict(list) for m in modules: per_dir[m.parent].append(m.name) for directory, files in per_dir.items(): log.info(' - %s: %s', directory, ', '.join(sorted(files))) with use_db(session_file) as database: cosmic_ray.commands.init(modules, database, cfg) sys.exit(ExitCode.OK)
def report(show_output, show_diff, show_pending, session_file): """Print a nicely formatted report of test results and some basic statistics.""" with use_db(session_file, WorkDB.Mode.open) as db: for work_item, result in db.completed_work_items: display_work_item(work_item) print("worker outcome: {}, test outcome: {}".format(result.worker_outcome, result.test_outcome)) if show_output: print("=== OUTPUT ===") print(result.output) print("==============") if show_diff: print("=== DIFF ===") print(result.diff) print("============") if show_pending: for work_item in db.pending_work_items: display_work_item(work_item) num_items = db.num_work_items num_complete = db.num_results print("total jobs: {}".format(num_items)) if num_complete > 0: print("complete: {} ({:.2f}%)".format(num_complete, num_complete / num_items * 100)) num_killed = kills_count(db) print("surviving mutants: {} ({:.2f}%)".format(num_complete - num_killed, survival_rate(db))) else: print("no jobs completed")
def config(session_file): """Show the configuration for a session.""" with use_db(session_file) as database: cfg = database.get_config() print(serialize_config(cfg)) sys.exit(ExitCode.OK)
def report_xml(session_file): """Print an XML formatted report of test results for continuous integration systems""" with use_db(session_file, WorkDB.Mode.open) as db: xml_elem = _create_xml_report(db) xml_elem.write(sys.stdout.buffer, encoding="utf-8", xml_declaration=True)
def execute(db_name): """Execute any pending work in the database stored in `db_name`, recording the results. This looks for any work in `db_name` which has no results, schedules it to be executed, and records any results that arrive. """ try: with use_db(db_name, mode=WorkDB.Mode.open) as work_db: _update_progress(work_db) config = work_db.get_config() engine = get_execution_engine(config.execution_engine_name) def on_task_complete(job_id, work_result): work_db.set_result(job_id, work_result) _update_progress(work_db) log.info("Job %s complete", job_id) log.info("Beginning execution") engine( work_db.pending_work_items, config, on_task_complete=on_task_complete) log.info("Execution finished") except FileNotFoundError as exc: raise FileNotFoundError( str(exc).replace('Requested file', 'Corresponding database', 1)) from exc
def handle_init(args): """usage: cosmic-ray init <config-file> <session-file> Initialize a mutation testing session from a configuration. This primarily creates a session - a database of "work to be done" - which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The configuration specifies the top-level module to mutate, the tests to run, and how to run them. This command doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. The `session-file` is the filename for the database in which the work order will be stored. """ config_file = args['<config-file>'] config = load_config(config_file) modules = set(cosmic_ray.modules.find_modules(Path(config['module-path']))) log.info('Modules discovered: %s', [m for m in modules]) db_name = args['<session-file>'] with use_db(db_name) as database: cosmic_ray.commands.init(modules, database, config) return ExitCode.OK
def dump(session_file): """JSON dump of session data. This output is typically run through other programs to produce reports. Each line of output is a list with two elements: a WorkItem and a WorkResult, both JSON-serialized. The WorkResult can be null, indicating a WorkItem with no results. """ def item_to_dict(work_item): d = dataclasses.asdict(work_item) for m in d["mutations"]: m["module_path"] = str(m["module_path"]) return d def result_to_dict(result): d = dataclasses.asdict(result) d["worker_outcome"] = d["worker_outcome"].value d["test_outcome"] = d["test_outcome"].value return d with use_db(session_file, WorkDB.Mode.open) as database: for work_item, result in database.completed_work_items: print(json.dumps( (item_to_dict(work_item), result_to_dict(result)))) for work_item in database.pending_work_items: print(json.dumps((item_to_dict(work_item), None))) sys.exit(ExitCode.OK)
def handle_init(args): """usage: cosmic-ray init <config-file> <session-file> Initialize a mutation testing session from a configuration. This primarily creates a session - a database of "work to be done" - which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The configuration specifies the top-level module to mutate, the tests to run, and how to run them. This command doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. The `session-file` is the filename for the database in which the work order will be stored. """ config_file = args['<config-file>'] config = load_config(config_file) modules = set(cosmic_ray.modules.find_modules(Path(config['module-path']))) log.info('Modules discovered: %s', [m for m in modules]) db_name = get_db_name(args['<session-file>']) with use_db(db_name) as database: cosmic_ray.commands.init(modules, database, config) return ExitCode.OK
def handle_exec(session_file): """Perform the remaining work to be done in the specified session. This requires that the rest of your mutation testing infrastructure (e.g. worker processes) are already running. """ with use_db(session_file, mode=WorkDB.Mode.open) as work_db: cosmic_ray.commands.execute(work_db) sys.exit(ExitCode.OK)
def handle_init(args): """usage: cosmic-ray init <config-file> <session-file> Initialize a mutation testing session from a configuration. This primarily creates a session - a database of "work to be done" - which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The configuration specifies the top-level module to mutate, the tests to run, and how to run them. This command doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. The `session-file` is the filename for the database in which the work order will be stored. """ # This lets us import modules from the current directory. Should # probably be optional, and needs to also be applied to workers! sys.path.insert(0, '') config_file = args['<config-file>'] config = load_config(config_file) if 'timeout' in config: timeout = config['timeout'] elif 'baseline' in config: baseline_mult = config['baseline'] command = 'cosmic-ray baseline {}'.format(args['<config-file>']) # We run the baseline in a subprocess to more closely emulate the # runtime of a worker subprocess. with Timer() as timer: subprocess.check_call(command.split()) timeout = baseline_mult * timer.elapsed.total_seconds() else: raise ConfigValueError( "Config must specify either baseline or timeout") log.info('timeout = %f seconds', timeout) modules = set( cosmic_ray.modules.find_modules( cosmic_ray.modules.fixup_module_name(config['module']), config.get('exclude-modules', default=None))) log.info('Modules discovered: %s', [m.__name__ for m in modules]) db_name = get_db_name(args['<session-file>']) with use_db(db_name) as database: cosmic_ray.commands.init(modules, database, config, timeout) return ExitCode.OK
def handle_survival_rate(configuration): """usage: cosmic-ray survival-rate <session-name> Print the session's survival rate. """ db_name = _get_db_name(configuration['<session-name>']) with use_db(db_name, WorkDB.Mode.open) as db: rate = cosmic_ray.commands.survival_rate(db) print('{:.2f}'.format(rate))
def handle_dump(configuration): """usage: cosmic-ray dump <session-name> JSON dump of session data. """ db_name = _get_db_name(configuration['<session-name>']) with use_db(db_name, WorkDB.Mode.open) as db: for record in db.work_records: print(json.dumps(record))
def report(): """cr-report Usage: cr-report [--show-output] [--show-diff] [--show-pending] <session-file> Print a nicely formatted report of test results and some basic statistics. options: --show-output Display output of test executions --show-diff Display diff of mutants --show-pending Display results for incomplete tasks """ arguments = docopt.docopt(report.__doc__, version='cr-format 0.1') show_pending = arguments['--show-pending'] show_output = arguments['--show-output'] show_diff = arguments['--show-diff'] with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: for work_item, result in db.completed_work_items: print('{} {} {} {}'.format(work_item.job_id, work_item.module_path, work_item.operator_name, work_item.occurrence)) print('worker outcome: {}, test outcome: {}'.format( result.worker_outcome, result.test_outcome)) if show_output: print('=== OUTPUT ===') print(result.output) print('==============') if show_diff: print('=== DIFF ===') print(result.diff) print('============') if show_pending: for work_item in db.pending_work_items: print('{} {} {} {}'.format(work_item.job_id, work_item.module_path, work_item.operator_name, work_item.occurrence)) num_items = db.num_work_items num_complete = db.num_results print('total jobs: {}'.format(num_items)) if num_complete > 0: print('complete: {} ({:.2f}%)'.format( num_complete, num_complete / num_items * 100)) print('survival rate: {:.2f}%'.format(survival_rate(db))) else: print('no jobs completed')
def handle_config(args): """usage: cosmic-ray config <session-file> Show the configuration for in a session. """ session_file = get_db_name(args['<session-file>']) with use_db(session_file) as database: config, _ = database.get_config() print(json.dumps(config)) return os.EX_OK
def handle_exec(args): """usage: cosmic-ray exec <session-file> Perform the remaining work to be done in the specified session. This requires that the rest of your mutation testing infrastructure (e.g. worker processes) are already running. """ session_file = args.get('<session-file>') with use_db(session_file, mode=WorkDB.Mode.open) as work_db: cosmic_ray.commands.execute(work_db) return ExitCode.OK
def handle_config(args): """usage: cosmic-ray config <session-file> Show the configuration for in a session. """ session_file = get_db_name(args['<session-file>']) with use_db(session_file) as database: config = database.get_config() print(serialize_config(config)) return ExitCode.OK
def test_empty___init__(project_root): config = 'cosmic-ray.empty.conf' session = 'empty_test.session.json' subprocess.check_call(['cosmic-ray', 'init', config, session], cwd=str(project_root)) session_path = project_root / session with use_db(str(session_path), WorkDB.Mode.open) as work_db: rate = survival_rate(work_db.work_items) assert rate == 0.0
def handle_report(configuration): """usage: cosmic-ray report [--show-pending] <session-name> Print a nicely formatted report of test results and some basic statistics. """ db_name = _get_db_name(configuration['<session-name>']) show_pending = configuration['--show-pending'] with use_db(db_name, WorkDB.Mode.open) as db: for line in cosmic_ray.commands.create_report(db, show_pending): print(line)
def report_html(): """cr-html Usage: cr-html <session-file> Print an HTML formatted report of test results. """ arguments = docopt.docopt(report_html.__doc__, version='cr-rate 1.0') with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: doc = _generate_html_report(db) print(doc.getvalue())
def report_xml(): """cr-xml Usage: cr-xml <session-file> Print an XML formatted report of test results for continuos integration systems """ arguments = docopt.docopt(report_xml.__doc__, version='cr-rate 1.0') with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: xml_elem = _create_xml_report(db) xml_elem.write( sys.stdout.buffer, encoding='utf-8', xml_declaration=True)
def handle_exec(configuration): """usage: cosmic-ray exec <session-name> Perform the remaining work to be done in the specified session. This requires that the rest of your mutation testing infrastructure (e.g. worker processes) are already running. """ db_name = _get_db_name(configuration['<session-name>']) with use_db(db_name, mode=WorkDB.Mode.open) as db: cosmic_ray.commands.execute(db)
def test_e2e(project_root, test_runner, engine): config = 'cosmic-ray.{}.{}.conf'.format(test_runner, engine) session = 'adam-tests.{}.{}.session.json'.format(test_runner, engine) subprocess.check_call(['cosmic-ray', 'init', config, session], cwd=str(project_root)) subprocess.check_call(['cosmic-ray', 'exec', session], cwd=str(project_root)) session_path = project_root / session with use_db(str(session_path), WorkDB.Mode.open) as work_db: rate = survival_rate(work_db.work_items) assert rate == 0.0
def handle_dump(args): """usage: cosmic-ray dump <session-file> JSON dump of session data. This output is typically run through other programs to produce reports. """ session_file = get_db_name(args['<session-file>']) with use_db(session_file, WorkDB.Mode.open) as database: for record in database.work_items: print(json.dumps(record)) return os.EX_OK
def report_xml(): """cr-xml Usage: cr-xml <session-file> Print an XML formatted report of test results for continuous integration systems """ arguments = docopt.docopt(report_xml.__doc__, version='cr-rate 1.0') with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: xml_elem = _create_xml_report(db) xml_elem.write(sys.stdout.buffer, encoding='utf-8', xml_declaration=True)
def test_empty___init__(example_project_root, session): config = "cosmic-ray.empty.conf" subprocess.check_call( [sys.executable, "-m", "cosmic_ray.cli", "init", config, str(session)], cwd=str(example_project_root), ) session_path = example_project_root / session with use_db(str(session_path), WorkDB.Mode.open) as work_db: rate = survival_rate(work_db) assert rate == 0.0
def format_survival_rate(): """cr-rate Usage: cr-rate <session-file> Calculate the survival rate of a session. """ arguments = docopt.docopt(format_survival_rate.__doc__, version='cr-rate 1.0') with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: rate = survival_rate(db) print('{:.2f}'.format(rate))
def format_survival_rate(): """cr-rate Usage: cr-rate <session-file> Calculate the survival rate of a session. """ arguments = docopt.docopt( format_survival_rate.__doc__, version='cr-rate 1.0') with use_db(arguments['<session-file>'], WorkDB.Mode.open) as db: rate = survival_rate(db) print('{:.2f}'.format(rate))
def handle_init(configuration): """usage: cosmic-ray init [options] [--exclude-modules=P ...] (--timeout=T | --baseline=M) <session-name> <top-module> [-- <test-args> ...] Initialize a mutation testing run. The primarily creates a database of "work to be done" which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The testing run will mutate <top-module> (and submodules) using the tests in <test-dir>. This doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. The session-name argument identifies the run you're creating. It's most important role is that it's used to name the database file. options: --no-local-import Allow importing module from the current directory --test-runner=R Test-runner plugin to use [default: unittest] --exclude-modules=P Pattern of module names to exclude from mutation """ # This lets us import modules from the current directory. Should probably # be optional, and needs to also be applied to workers! sys.path.insert(0, '') if configuration['--timeout'] is not None: timeout = float(configuration['--timeout']) else: baseline_mult = float(configuration['--baseline']) assert baseline_mult is not None timeout = baseline_mult * cosmic_ray.timing.run_baseline( configuration['--test-runner'], configuration['<top-module>'], configuration['<test-args>']) LOG.info('timeout = {} seconds'.format(timeout)) modules = set( cosmic_ray.modules.find_modules( configuration['<top-module>'], configuration['--exclude-modules'])) LOG.info('Modules discovered: %s', [m.__name__ for m in modules]) db_name = _get_db_name(configuration['<session-name>']) with use_db(db_name) as db: cosmic_ray.commands.init( modules, db, configuration['--test-runner'], configuration['<test-args>'], timeout)
def handle_exec(configuration): """usage: cosmic-ray exec [--dist] <session-name> Perform the remaining work to be done in the specified session. This requires that the rest of your mutation testing infrastructure (e.g. worker processes) are already running. options: --dist Distribute tests to remote workers """ db_name = _get_db_name(configuration['<session-name>']) dist = configuration['--dist'] with use_db(db_name, mode=WorkDB.Mode.open) as db: cosmic_ray.commands.execute(db, dist)
def dump(session_file): """JSON dump of session data. This output is typically run through other programs to produce reports. Each line of output is a list with two elements: a WorkItem and a WorkResult, both JSON-serialized. The WorkResult can be null, indicating a WorkItem with no results. """ with use_db(session_file, WorkDB.Mode.open) as database: for work_item, result in database.completed_work_items: print(json.dumps((work_item, result), cls=WorkItemJsonEncoder)) for work_item in database.pending_work_items: print(json.dumps((work_item, None), cls=WorkItemJsonEncoder)) sys.exit(ExitCode.OK)
def test_baseline(example_project_root, config, session): subprocess.check_call( [sys.executable, "-m", "cosmic_ray.cli", "init", config, str(session)], cwd=str(example_project_root)) subprocess.check_call( [sys.executable, "-m", "cosmic_ray.cli", "baseline", str(session)], cwd=str(example_project_root)) session_path = session.parent / "{}.baseline{}".format(session.stem, session.suffix) with use_db(str(session_path), WorkDB.Mode.open) as work_db: rate = survival_rate(work_db) assert rate == 100.0
def handle_report(configuration): """usage: cosmic-ray report [--full-report] [--show-pending] <session-name> Print a nicely formatted report of test results and some basic statistics. options: --full-report Show test output and mutation diff for killed mutants """ db_name = _get_db_name(configuration['<session-name>']) show_pending = configuration['--show-pending'] full_report = configuration['--full-report'] with use_db(db_name, WorkDB.Mode.open) as db: for line in cosmic_ray.commands.create_report(db, show_pending, full_report): print(line)
def handle_init(configuration): """usage: cosmic-ray init [options] [--exclude-modules=P ...] (--timeout=T | --baseline=M) <session-name> <top-module> [-- <test-args> ...] Initialize a mutation testing run. The primarily creates a database of "work to be done" which describes all of the mutations and test runs that need to be executed for a full mutation testing run. The testing run will mutate <top-module> (and submodules) using the tests in <test-dir>. This doesn't actually run any tests. Instead, it scans the modules-under-test and simply generates the work order which can be executed with other commands. The session-name argument identifies the run you're creating. Its most important role is that it's used to name the database file. options: --no-local-import Allow importing module from the current directory --test-runner=R Test-runner plugin to use [default: unittest] --exclude-modules=P Pattern of module names to exclude from mutation """ # This lets us import modules from the current directory. Should probably # be optional, and needs to also be applied to workers! sys.path.insert(0, '') if configuration['--timeout'] is not None: timeout = float(configuration['--timeout']) else: baseline_mult = float(configuration['--baseline']) assert baseline_mult is not None with Timer() as t: handle_baseline(configuration) timeout = baseline_mult * t.elapsed.total_seconds() LOG.info('timeout = %f seconds', timeout) modules = set( cosmic_ray.modules.find_modules( cosmic_ray.modules.fixup_module_name( configuration['<top-module>']), configuration['--exclude-modules'])) LOG.info('Modules discovered: %s', [m.__name__ for m in modules]) db_name = _get_db_name(configuration['<session-name>']) with use_db(db_name) as db: cosmic_ray.commands.init(modules, db, configuration['--test-runner'], configuration['<test-args>'], timeout)
def test_e2e(example_project_root, config, session): subprocess.check_call( [sys.executable, "-m", "cosmic_ray.cli", "init", config, str(session)], cwd=str(example_project_root)) subprocess.check_call(['cr-filter-spor', str(session)], cwd=str(example_project_root)) subprocess.check_call( [sys.executable, "-m", "cosmic_ray.cli", "exec", str(session)], cwd=str(example_project_root)) session_path = example_project_root / session with use_db(str(session_path), WorkDB.Mode.open) as work_db: rate = survival_rate(work_db) assert rate == 0.0
def handle_dump(args): """usage: cosmic-ray dump <session-file> JSON dump of session data. This output is typically run through other programs to produce reports. Each line of output is a list with two elements: a WorkItem and a WorkResult, both JSON-serialized. The WorkResult can be null, indicating a WorkItem with no results. """ session_file = get_db_name(args['<session-file>']) with use_db(session_file, WorkDB.Mode.open) as database: for work_item, result in database.completed_work_items: print(json.dumps((work_item, result), cls=WorkItemJsonEncoder)) for work_item in database.pending_work_items: print(json.dumps((work_item, None), cls=WorkItemJsonEncoder)) return ExitCode.OK
def baseline(config_file, session_file): """Runs a baseline execution that executes the test suite over unmutated code. If ``--session-file`` is provided, the session used for baselining is stored in that file. Otherwise, the session is stored in a temporary file which is deleted after the baselining. Exits with 0 if the job has exited normally, otherwise 1. """ cfg = load_config(config_file) @contextmanager def path_or_temp(path): if path is None: with tempfile.TemporaryDirectory() as tmpdir: yield Path(tmpdir) / "session.sqlite" else: yield path with path_or_temp(session_file) as session_path: with use_db(session_path, mode=WorkDB.Mode.create) as db: db.clear() db.add_work_item(WorkItem( mutations=[], job_id="baseline", )) # Run the single-entry session. cosmic_ray.commands.execute(db, cfg) result = next(db.results)[1] if result.test_outcome == TestOutcome.KILLED: message = [ "Baseline failed. Execution with no mutation gives those following errors:" ] for line in result.output.split("\n"): message.append(" >>> {}".format(line)) log.error("\n".join(message)) sys.exit(1) else: log.info( "Baseline passed. Execution with no mutation works fine.") sys.exit(ExitCode.OK)
def work_db(): with use_db(':memory:', WorkDB.Mode.create) as db: yield db