GithubHelp home page GithubHelp logo

boxed / mutmut Goto Github PK

View Code? Open in Web Editor NEW
864.0 15.0 100.0 584 KB

Mutation testing system

Home Page: https://mutmut.readthedocs.io

License: BSD 3-Clause "New" or "Revised" License

Python 99.15% Makefile 0.85%
testing mutation-testing python

mutmut's Introduction

mutmut - python mutation tester

image

Documentation Status

image

image

Mutmut is a mutation testing system for Python, with a strong focus on ease of use. If you don't know what mutation testing is try starting with this article.

Some highlight features:

  • Found mutants can be applied on disk with a simple command making it very easy to work with the results
  • Remembers work that has been done, so you can work incrementally
  • Supports all test runners (because mutmut only needs an exit code from the test command)
  • If you use the hammett test runner you can go extremely fast! There's special handling for this runner that has some pretty dramatic results.
  • Can use coverage data to only do mutation testing on covered lines
  • Battle tested on real libraries by multiple companies

If you need to run mutmut on a python 2 code base use mutmut 1.5.0. Mutmut 1.9.0 is the last version to support python 3.4, 3.5 and 3.6.

Install and run

You can get started with a simple:

pip install mutmut
mutmut run

This will by default run pytest (or unittest if pytest is unavailable) on tests in the "tests" or "test" folder and it will try to figure out where the code to mutate lies.

NOTE that mutmut will apply the mutations directly, one at a time; it is highly recommended to add all changes to source control before running.

Enter

mutmut run --help

for the available flags, to use other runners, etc. The recommended way to use mutmut if the defaults aren't working for you is to add a block in setup.cfg or project.toml. Then when you come back to mutmut weeks later you don't have to figure out the flags again, just run mutmut run and it works. Like this in setup.cfg:

[mutmut]
paths_to_mutate=src/
backup=False
runner=python -m hammett -x
tests_dir=tests/
dict_synonyms=Struct, NamedStruct

or like this in pyproject.toml:

[tool.mutmut]
paths_to_mutate="src"
runner="python -m hammett -x"

To use multiple paths either in the paths_to_mutate or tests_dir option use a comma or colon separated list. For example:

[mutmut]
paths_to_mutate=src/,src2/
tests_dir=tests/:tests2/

You can stop the mutation run at any time and mutmut will restart where you left off. It's also smart enough to retest only the surviving mutants when the test suite changes.

To print the results run mutmut show. It will give you a list of the mutants grouped by file. You can now look at a specific mutant diff with mutmut show 3, all mutants for a specific file with mutmut show path/to/file.py or all mutants with mutmut show all.

You can also write a mutant to disk with mutmut apply 3. You should REALLY have the file you mutate under source code control and committed before you apply a mutant!

To generate a HTML report for a web browser: mutmut html

Whitelisting

You can mark lines like this:

some_code_here()  # pragma: no mutate

to stop mutation on those lines. Some cases we've found where you need to whitelist lines are:

  • The version string on your library. You really shouldn't have a test for this :P
  • Optimizing break instead of continue. The code runs fine when mutating break to continue, but it's slower.

See also Advanced whitelisting and configuration

Example mutations

  • Integer literals are changed by adding 1. So 0 becomes 1, 5 becomes 6, etc.
  • < is changed to <=
  • break is changed to continue and vice versa

In general the idea is that the mutations should be as subtle as possible. See __init__.py for the full list.

Workflow

This section describes how to work with mutmut to enhance your test suite.

  1. Run mutmut with mutmut run. A full run is preferred but if you're just getting started you can exit in the middle and start working with what you have found so far.
  2. Show the mutants with mutmut results
  3. Apply a surviving mutant to disk running mutmut apply 3 (replace 3 with the relevant mutant ID from mutmut results)
  4. Write a new test that fails
  5. Revert the mutant on disk
  6. Rerun the new test to see that it now passes
  7. Go back to point 2.

Mutmut keeps a result cache in .mutmut-cache so if you want to make sure you run a full mutmut run just delete this file.

If you want to re-run all survivors after changing a lot of code or even the configuration, you can use for ID in $(mutmut result-ids survived); do mutmut run $ID; done (for bash).

You can also tell mutmut to just check a single mutant:

mutmut run 3

Advanced whitelisting and configuration

mutmut has an advanced configuration system. You create a file called mutmut_config.py. You can define two functions there: init() and pre_mutation(context). init gets called when mutmut starts and pre_mutation gets called before each mutant is applied and tested. You can mutate the context object as you need. You can modify the test command like this:

def pre_mutation(context):
    context.config.test_command = 'python -m pytest -x ' + something_else

or skip a mutant:

def pre_mutation(context):
    if context.filename == 'foo.py':
        context.skip = True

or skip logging:

def pre_mutation(context):
    line = context.current_source_line.strip()
    if line.startswith('log.'):
        context.skip = True

look at the code for the Context class for what you can modify. Please open a github issue if you need help.

It is also possible to disable mutation of specific node types by passing the --disable-mutation-types option. Multiple types can be specified by separating them by comma:

mutmut run --disable-mutation-types=string,decorator

Inversely, you can also only specify to only run specific mutations with --enable-mutation-types. Note that --disable-mutation-types and --enable-mutation-types are exclusive and cannot be combined.

Changes made to mutmut_config.py does not invalidate the cache. If you modify the configuration file, you will likely need to remove .mutmut-cache manually to ensure that the changes take effect.

Selecting tests to run

If you have a large test suite or long running tests, it can be beneficial to narrow the set of tests to run for each mutant down to the tests that have a chance of killing it. Determining the relevant subset of tests depends on your project, its structure, and the metadata that you know about your tests. mutmut provides information like the file to mutate and coverage contexts (if used with the --use-coverage switch). You can set the context.config.test_command in the pre_mutation(context) hook of mutmut_config.py. The test_command is reset after each mutant, so you don't have to explicitly (re)set it for each mutant.

This section gives examples to show how this could be done for some concrete use cases. All examples use the default test runner (python -m pytest -x --assert=plain).

Selection based on source and test layout

If the location of the test module has a strict correlation with your source code layout, you can simply construct the path to the corresponding test file from context.filename. Suppose your layout follows the following structure where the test file is always located right beside the production code:

mypackage
├── production_module.py
├── test_production_module.py
└── subpackage
    ├── submodule.py
    └── test_submodule.py

Your mutmut_config.py in this case would look like this:

import os.path

def pre_mutation(context):
    dirname, filename = os.path.split(context.filename)
    testfile = "test_" + filename
    context.config.test_command += ' ' + os.path.join(dirname, testfile)

Selection based on imports

If you can't rely on the directory structure or naming of the test files, you may assume that the tests most likely to kill the mutant are located in test files that directly import the module that is affected by the mutant. Using the ast module of the Python standard library, you can use the init() hook to build a map which test file imports which module, and then lookup all test files importing the mutated module and only run those:

import ast
from pathlib import Path

test_imports = {}


class ImportVisitor(ast.NodeVisitor):
    """Visitor which records which modules are imported."""
    def __init__(self) -> None:
        super().__init__()
        self.imports = []

    def visit_Import(self, node: ast.Import) -> None:
        for alias in node.names:
            self.imports.append(alias.name)

    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
        self.imports.append(node.module)


def init():
    """Find all test files located under the 'tests' directory and create an abstract syntax tree for each.
    Let the ``ImportVisitor`` find out what modules they import and store the information in a global dictionary
    which can be accessed by ``pre_mutation(context)``."""
    test_files = (Path(__file__).parent / "tests").rglob("test*.py")
    for fpath in test_files:
        visitor = ImportVisitor()
        visitor.visit(ast.parse(fpath.read_bytes()))
        test_imports[str(fpath)] = visitor.imports


def pre_mutation(context):
    """Construct the module name from the filename and run all test files which import that module."""
    tests_to_run = []
    for testfile, imports in test_imports.items():
        module_name = context.filename.rstrip(".py").replace("/", ".")
        if module_name in imports:
            tests_to_run.append(testfile)
    context.config.test_command += f"{' '.join(tests_to_run)}"

Selection based on coverage contexts

If you recorded coverage contexts and use the --use-coverage switch, you can access this coverage data inside the pre_mutation(context) hook via the context.config.coverage_data attribute. This attribute is a dictionary in the form {filename: {lineno: [contexts]}}.

Let's say you have used the built-in dynamic context option of Coverage.py by adding the following to your .coveragerc file:

[run]
dynamic_context = test_function

coverage will create a new context for each test function that you run in the form module_name.function_name. With pytest, we can use the -k switch to filter tests that match a given expression.

import os.path

def pre_mutation(context):
    """Extract the coverage contexts if possible and only run the tests matching this data."""
    if not context.config.coverage_data:
        # mutmut was run without ``--use-coverage``
        return
    fname = os.path.abspath(context.filename)
    contexts_for_file = context.config.coverage_data.get(fname, {})
    contexts_for_line = contexts_for_file.get(context.current_line_index, [])
    test_names = [
        ctx.rsplit(".", 1)[-1]  # extract only the final part after the last dot, which is the test function name
        for ctx in contexts_for_line
        if ctx  # skip empty strings
    ]
    if not test_names:
        return
    context.config.test_command += f' -k "{" or ".join(test_names)}"'

Pay attention that the format of the context name varies depending on the tool you use for creating the contexts. For example, the pytest-cov plugin uses :: as separator between module and test function. Furthermore, not all tools are able to correctly pick up the correct contexts. coverage.py for instance is (at the time of writing) unable to pick up tests that are inside a class when using pytest. You will have to inspect your .coverage database using the Coverage.py API first to determine how you can extract the correct information to use with your test runner.

Making things more robust

Despite your best efforts in picking the right subset of tests, it may happen that the mutant survives because the test which is able to kill it was not included in the test set. You can tell mutmut to re-run the full test suite in that case, to verify that this mutant indeed survives. You can do so by passing the --rerun-all option to mutmut run. This option is disabled by default.

JUnit XML support

In order to better integrate with CI/CD systems, mutmut supports the generation of a JUnit XML report (using https://pypi.org/project/junit-xml/). This option is available by calling mutmut junitxml. In order to define how to deal with suspicious and untested mutants, you can use

mutmut junitxml --suspicious-policy=ignore --untested-policy=ignore

The possible values for these policies are:

  • ignore: Do not include the results on the report at all
  • skipped: Include the mutant on the report as "skipped"
  • error: Include the mutant on the report as "error"
  • failure: Include the mutant on the report as "failure"

If a failed mutant is included in the report, then the unified diff of the mutant will also be included for debugging purposes.

mutmut's People

Contributors

atugushev avatar blueyed avatar boxed avatar brodybits avatar dryobates avatar dudenr33 avatar ejahren avatar felipemfp avatar felixdivo avatar fltr avatar hukkinj1 avatar icncsx avatar jayvdb avatar michaelchampagne-elapse avatar moltob avatar nklapste avatar patrikvalkovic avatar peterfortuin avatar roxanebellot avatar s-kovacevic avatar samuelrazzell avatar sed-i avatar sobolevn avatar sonicxml avatar talamuyal avatar wirednerd avatar worr avatar yanekk avatar zac-hd avatar zedthree avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mutmut's Issues

Current state of tests is not great, let's fix them

As the title says current state of tests is not where it should be especially considering the purpose of the mutmut itself is to improve unit tests. I feel that we need to address these issues sooner rather than later since it will make maintenance much easier in the future.

  1. Current tests are more integration / end-to-end tests than unit tests
  2. Big chunk of code is not properly unit tested
  3. mutmut is not used on mutmut

Current tests are more integration / end-to-end tests than unit tests
Here is the article by Michael Feathers that pretty much explains this point. Each unit test should test only one unit of the code, also no spinning disks for unit tests and no IO at all. Some parts of the code will, for sure, require refactoring to make them testable. There is a lot of IO in functions that should be moved up a level to make the code easy to test. Here is a great talk by Brandon Rhodes on how to "hoist IO".

Big chunk of code is not properly unit tested
It is hard to determine actual unit test coverage since coverage is calculated while running all tests. Currently it looks good, but there is a decent amount of edge cases that are not tested because it is hard to reproduce edge cases by writing integration tests. Coverage should be calculated when running only true unit tests.

mutmut is not used on mutmut
This is the best way to make our unit tests better, that's what mutmut does. Also this would make it easier for us to spot new use cases and potential improvements. But first, things need to be refactored.

Steps to mitigate:

  • Making a clear separation between unit tests and integration / end-to-end tests. (different dirs/files and pytestmark)
  • Stop merging pull requests that aren't properly covered with unit tests.
  • Chunk by chunk refactor code to be testable and add appropriate unit tests for it.
  • After the old code is refactored, switch coverage to work only with unit tests.
  • Start using mutmut on unit tests

Good thing is that code base is not that big, it is a great time to do something like this. Also current tests are good for what they do, we just need to add proper unit tests.

Please let me know what you think. :)

Running with --use-coverage fails

Hello! I am trying out mutmut today, having tried out cosmic-ray recently, and mutmut appears to be a simpler implementation, so thanks for making this!

My project uses pytest, and leverages coverage, and during my initial mutmut run, it found mutants in a function that I have a pragma: no cover set, so I wanted to use the flag to leverage only covered lines for mutation (there's also a couple of private methods that aren't exercised).

When invoking:

mutmut run --use-coverage --paths-to-mutate unsilencer.py

I get this traceback:

1. Using cached time for baseline tests, to run baseline again delete the cache file
Using coverage data from .coverage file
Traceback (most recent call last):
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/bin/mutmut", line 5, in <module>
    main()
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/mutmut/__main__.py", line 59, in wrapper
    f(*args, **kwargs)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/mutmut/__main__.py", line 285, in main
    coverage_data = read_coverage_data(use_coverage)
  File "/Users/miketheman/.local/share/virtualenvs/unsilencer-YmPJJ04A/lib/python3.7/site-packages/mutmut/__main__.py", line 484, in read_coverage_data
    coverage_data.read_file('.coverage')
AttributeError: 'CoverageSqliteData' object has no attribute 'read_file'

Reference:

mutmut/mutmut/__main__.py

Lines 482 to 484 in a3c5efa

import coverage
coverage_data = coverage.CoverageData()
coverage_data.read_file('.coverage')

It appears that the read_file may have been removed upstream? I'm using pytest-cov which installs anything above 4.4, and using 5.0a4 the method is removed.

Manually back-tracking to coverage 4.5.2 (latest stable) provides a non-traceback execution, however it doesn't appear to respect the coverage rules, as it's mutating a no cover line, like so:

$ mutmut show 26
--- unsilencer.py
+++ unsilencer.py
@@ -77,7 +77,7 @@
             remove_from_list(suppression_list, email_address)


-if __name__ == "__main__":  # pragma: no cover
+if __name__ == "XX__main__XX":  # pragma: no cover
     try:
         input = sys.argv[1]
     except IndexError:

is this the desired behavior? I couldn't find any reference to coverage in the test/s directory, hoping to see how this is meant to work. Let me know if there's anything else I can do to help!

--use-coverage: No such file or directory: '.coverage'

With pytest:

% mutmut --use-coverage --runner 'python -m pytest testing/test_collection.py' src/_pytest/nodes.py
Running tests without mutations... Done
Using coverage data from .coverage file
Traceback (most recent call last):
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/coverage/data.py", line 292, in read_file
    with self._open_for_reading(filename) as f:
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/coverage/data.py", line 306, in _open_for_reading
    return open(filename, "r")
FileNotFoundError: [Errno 2] No such file or directory: '.coverage'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "…/Vcs/pytest/.venv/bin/mutmut", line 5, in <module>
    main()
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 55, in wrapper
    f(*args, **kwargs)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 215, in main
    coverage_data = read_coverage_data(use_coverage)
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 402, in read_coverage_data
    coverage_data.read_file('.coverage')
  File "…/Vcs/pytest/.venv/lib/python3.6/site-packages/coverage/data.py", line 297, in read_file
    filename, exc.__class__.__name__, exc,
coverage.misc.CoverageException: Couldn't read data from '.coverage': FileNotFoundError: [Errno 2] No such file or directory: '.coverage'

Mutate dict literal keys when they are written as dict(..)

When you have

foo = {'a': a}

we mutate the key 'a', but if you write the exact same code as

foo = dict(a=a)

that won't be mutated. This should be fixed, and we should also allow to specify synonyms for dict, like Struct from tri.struct. It's probably time to introduce a config file at this point! Probably just hook into setup.cfg

Add support for unittest-based Django tests

I'm attempting to run mutmut inside a Django project. There are certain assumptions in the codebase that I need to change in order to achieve that.

I will use this place to report my progress and welcome any ideas.

"Surviving" notation backwards?

Hello. First let me say I'm super excited for this repo! I literally learned about mutation testing today and immediately pip installed your package.

What's confusing me is what you call a "surviving mutant." My expectation was that surviving mutants would be those that still pass the test suite. However, line 388 defines surviving mutants as those that do not pass the test suite. This means mutations that aren't a problem wind up being reported as "FAILED," and I haven't found a way to show the mutations that have left the test in a passing state.

TL;DR I want to know which mutants passed my test suite, not which ones don't pass my test suite.

My proposal is to remove not from line 388, but this drastically changes the behavior of this code base. Like I said, I'm a mutation testing noob, so it's entirely possible I'm thinking about this the wrong way.

Thanks again for the awesome repo!

Mutating strings in type annotations

This is a special case from #28

I have some functions that use strings to annotate types. And this is a valid use case.
Like this example: https://github.com/wemake-services/wemake-python-styleguide/blob/master/wemake_python_styleguide/visitors/base.py#L65

But, mutmut tries to mutate this string. And tests do not fail.
Once again, I guess I have to run mypy as well. But in this case it will take too long.

Maybe we can have an option to switch off mutating type annotations?

Applied mutation causes syntax error

Hi,

mutmut version: 1.1.0

I'm using Python 3.7 so in my code base I'm using this syntax:
def except_to_bool(_func=None, *, exc=Exception, to=False):

When I apply mutmut it generates this mutant:
def except_to_bool(_func=None, /, exc=Exception, to=False):
which should not be generated as it produces syntax error.

If mutant should be generated here I imagine it should look like this:
def except_to_bool(_func=None, exc=Exception, to=False):
(simply remove star that enforces keyword arguments)

I even have test for this code (which is checking whether it is not possible to pass positional arguments to this function), but mutant still survives it. I think this is because mutant causes syntax error.

Either fix mutant generation with star, that enforces keyword arguments, or skip generating mutant at all in such case, please :)

More powerful whitelisting system

A lot of surviving mutants in a project are due to changing constants, e.g. FOO = 1, where FOO itself is used in tests then also.
Killing those would need a test where the value itself is used, which does not make much sense, does it?

I think it would be good if those mutations could be skipped (e.g. by not mutating anything that matches [A-Z_]+ maybe?

It would be good if those could be deselected either by name of the mutation (which would need to be something like mutate-constant-X), or by specifying a pattern of identifiers not to mutate.
(it is not possible to deselect mutations currently, is it?)

No mutations when running with --use-coverage

Steps to reproduce:

  1. In an empty directory, create the file example_test.py with the following content:
def test_stuff():
    assert True
  1. Run pytest --cov to generate .coverage file.
  2. Run mutmut run --paths-to-mutate=example_test.py --use-coverage.

Expected result: 1 killed mutant (I tried running mutmut without --use-coverage and there was indeed 1 mutant; and coverage html shows that that line is covered).

Actual result: no mutants whatsoever.

1. Running tests without mutations
⠇ Running... Done
Using coverage data from .coverage file

2. Checking mutants
⠏ 0/0  🎉 0  ⏰ 0  🤔 0  🙁 0

Version info:

Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
pytest==4.1.1
pytest-cov==2.6.1
mutmut==1.2.0

Constants mutation

Hello!
Can you mutate constants, for example in
for _ in range(1, 10):
to
for _ in range(1, 11):
?
this can help to find bugs.
thank you.

Support for HTML reports

While looking at mutation libraries for other languages I found out that PIT (Java) has a really nice HTML report output. I think that feature like this would make mutmut easier to grasp especially for people new to mutation testing.
Also mutmut is difficult to work with when you have a lot of mutations, like:
mutmut show 58
mutmut show 59

It seems like a lot of work, but I want to hear opinions do you guys think this would be worth doing.
Any other ideas that would improve result reading experience are welcome.

image

image

--show-times: improve output

% mutmut --show-times --runner 'python -m pytest testing/test_collection.py' src/_pytest/nodes.py
Running tests without mutations... Done
--- starting mutation ---
Unchanged tests, using cache
1 out of 193  (src/_pytest/nodes.py SEP = \"/\"⤑0)time: 0:00:04.674225

There should be a space, or ", " / something better in front of the timing information.

Can't manage to pass the mutmut test

Hi, I'm trying your lib but I can't manage to pass the mutmut test, here is my code :

def addition(a, b):
    return a < b

and the associated test :


def test_addition():
    a = 1
    b = 2

    result = addition(a, b)

    assert result == True


def test_addition_2():
    a = 1
    b = 1

    result = addition(a, b)

    assert result == False

and my command :

mutmut app/add.py
mutmut tests/

Is there anything wrong ?

Thanks,
Samuel

Mutating type aliases

We use mypy and type aliases a lot in my company.
And it works perfectly.

However, on lines like, (full source):

AnyImport = Union[ast.Import, ast.ImportFrom]

I see a failing mutation test. And that's kind of correct, since types do not affect runtime.

FAILED: mutmut wemake_python_styleguide/types.py --apply --mutation "AnyImport = Union[ast.Import, ast.ImportFrom]⤑0"

What can be done to fix it?

  1. # pragma: no mutate to ignore a single item. But, I guess it kind of user-unfriendly to force this pragma on all type aliases
  2. also run mypy after (or before) the actual tests
  3. automatically detect these aliases and ignore them (unsupported)

What do you think / recommend? Thanks!

Can't run mutmut

I have this error.

command:
mutmut run --paths-to-mutate=$PROJECT --tests-dir=$TESTS_DIR

Traceback (most recent call last):
  File "/usr/local/bin/mutmut", line 5, in <module>
    climain()
  File "/usr/local/lib/python3.5/site-packages/click/core.py", line 695, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.5/site-packages/click/core.py", line 654, in main
    _verify_python3_env()
  File "/usr/local/lib/python3.5/site-packages/click/_unicodefun.py", line 69, in _verify_python3_env
    if locale.lower().endswith(('.utf-8', '.utf8')):
TypeError: a bytes-like object is required, not 'str'


Docker:

Client:
 Version:      18.03.1-ce
 API version:  1.37
 Go version:   go1.9.5
 Git commit:   9ee9f40
 Built:        Thu Apr 26 07:17:20 2018
 OS/Arch:      linux/amd64
 Experimental: false
 Orchestrator: swarm

Server:
 Engine:
  Version:      18.03.1-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.5
  Git commit:   9ee9f40
  Built:        Thu Apr 26 07:15:30 2018
  OS/Arch:      linux/amd64
  Experimental: false

Docker Image:
amazonlinux:2017.09

requirements.txt:

...
mutmut==1.3.1
pytest==4.2.0

`mutations show <id>` results in `No mutation performed`

Thesis

Some mutations are marked as failed. However, they are not making any changes to the source code.
And the tests are working. So, there's an issue somewhere.

How to reproduce

Steps to reproduce (however, I am not sure that this can be reproduced with 100%):

  1. Follow the installation steps of https://github.com/wemake-services/wemake-django-template
  2. Install dependencies + install mutmut (it is not included)
  3. Add this lines to the setup.cfg:
[mutmut]
paths_to_mutate = server/main_app
backup = False
runner = pytest
tests_dir = tests/
  1. Run mutmut run

Related output

(.venv) ~/Desktop/test_project
» mutmut run

- Mutation testing starting -

These are the steps:
1. A full test suite run will be made to make sure we
   can run the tests successfully and we know how long
   it takes (to detect infinite loops for example)
2. Mutants will be generated and checked

Mutants are written to the cache in the .mutmut-cache
directory. Print found mutants with `mutmut results`.

Legend for output:
🎉 Killed mutants. The goal is for everything to end up in this bucket.
⏰ Timeout. Test suite took 10 times as long as the baseline so were killed.
🤔 Suspicious. Tests took a long time, but not long enough to be fatal.
🙁 Survived. This means your tests needs to be expanded.

1. Running tests without mutations
⠙ Running... Done

2. Checking mutants
⠦ 6/6  🎉 2  ⏰ 0  🤔 0  🙁 4

(.venv) ~/Desktop/test_project
» mutmut results
Timed out ⏰

Suspicious 🤔

Survived 🙁
mutmut apply 1
mutmut apply 2
mutmut apply 3
mutmut apply 4

(.venv) ~/Desktop/test_project
» mutmut show 1
No mutation performed

(.venv) ~/Desktop/test_project
» mutmut show 2
No mutation performed

(.venv) ~/Desktop/test_project
» mutmut show 3
--- server/main_app/urls.py
+++ server/main_app/urls.py
@@ -7,6 +7,6 @@
 # Place your URLs here:

 urlpatterns = [
-    url(r'^hello/$', index, name='hello'),
+    url(r'XX^hello/$XX', index, name='hello'),
 ]


(.venv) ~/Desktop/test_project
» mutmut show 4
--- server/main_app/urls.py
+++ server/main_app/urls.py
@@ -7,6 +7,6 @@
 # Place your URLs here:

 urlpatterns = [
-    url(r'^hello/$', index, name='hello'),
+    url(r'^hello/$', index, name='XXhelloXX'),
 ]

Originally posted as #31 (comment)

Top-down approach / applying multiple mutations at once

I wonder if it is a good idea to apply mutants top-down, i.e. initially apply all mutants and then remove them similar to bisecting.

This would only require a single test run with all mutants enabled for a project that has no surviving mutants anymore (best case), and would continue by removing half of the mutations etc afterwards, remembering which sets of mutants were caught etc.

The idea is to have less test runs in total.

Just thinking out loud..

`cache.py` is broken while running python2

I couldn't run tests on python2 because zip_longest does not exist needs to be replaced with izip_longest.

____________________________________________________________________________ ERROR collecting tests/test_misc.py _____________________________________________________________________________
ImportError while importing test module '/home/savo/Documents/Projects/mutmut/tests/test_misc.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.tox/py27/local/lib/python2.7/site-packages/six.py:709: in exec_
    exec("""exec _code_ in _globs_, _locs_""")
tests/test_misc.py:1: in <module>
    from mutmut.cache import sequence_ops
mutmut/cache.py:9: in <module>
    from itertools import groupby, zip_longest
E   ImportError: cannot import name zip_longest

Whitelist string split/rsplit optimizations

foo = 'a/b/c'.split('/', 1)[0]

the parameter 1 to split can be mutated to 2. But this is an optimization so it's very annoying to have to whitelist this. Same goes for rsplit.

the halting problem: when a mutation triggers an infinite loop

This is the simplest case I could come up with, some timeout handling is needed it seems?

t.py

def t(x):
    r = 0
    while x:
        r += 2 * x
        x -= 1
    return r

tests/test_t.py

from t import t


def test():
    assert t(3) == 12

output

$ mutmut t.py 
Running tests without mutations... Done
--- starting mutation ---
6 out of 7  (t.py         x -= 1⤑0)    

(and then a (presumably) indefinite hang)

Wrong "No mutation performed" message while another mutation is applied

While mutation 6107 was running I've tried mutmut show 6106 (the last failed one), and it said "No mutation performed".

However that was likely only due to the next mutation being applied already, which touched the same code.

The below commands were run later (after 6107 finished):

% mutmut show 6106
--- project/app/models.py
+++ project/app/models.py
@@ -113,7 +113,7 @@
 …
-FOO = 'BAR'  # legacy
+FOO = 'XXBARXX'  # legacy
 …

% mutmut show 6107
--- project/app/models.py
+++ project/app/models.py
@@ -113,7 +113,7 @@
 …
-FOO = 'BAR'  # legacy
+FOO = None  # legacy
 …

This indicates that mutmut show goes to the actual files contents or something like that?!

Output: final newline / "old line"

With the following output there are some problems:

  1. what does old line mean? Should be removed or replaced with some more descriptive message (in verbose mode only?)
  2. it is missing a final newline - as you can see since Zsh prints timing information in the same line
  3. it displays "1484 out of 133", i.e. the count / total appears to be off. It would be good to see what it refers to (mutations? files?)
% mutmut --use-coverage --runner 'python -m pytest testing/test_collection.py' src/_pytest/nodes.py
Running tests without mutations... Done
Using coverage data from .coverage file
--- starting mutation ---
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "        return self.fspath, None, \"\"⤑0"
…
old line
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "            cache = self.config.__dict__.setdefault(\"_bestrelpathcache\", {})⤑0"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                fspath = cache[location[0]]⤑0"                  
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                fspath = cache[location[0]]⤑1"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                fspath = self.session.fspath.bestrelpath(location[0])⤑0"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                fspath = self.session.fspath.bestrelpath(location[0])⤑1"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                cache[location[0]] = fspath⤑0"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "                cache[location[0]] = fspath⤑1"
FAILED: mutmut src/_pytest/nodes.py --apply --mutation "            location = (fspath, location[1], str(location[2]))⤑0"
1484 out of 133  (src/_pytest/nodes.py             self._location = location⤑0)                         mutmut --use-coverage --runner 'python -m pytest testing/test_collection.py'   341.91s user 16.20s system 98% cpu 6:02.87 total

Error on run with empty source

Hi,

I've got a problem when running mutmut v1.3.0. IndexError: string index out of range.
Full stack trace:

1. Using cached time for baseline tests, to run baseline again delete the cache file
Traceback (most recent call last):
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__main__.py", line 675, in <module>
    climain()
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__main__.py", line 66, in wrapper
    f(*args, **kwargs)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__main__.py", line 204, in climain
    version, suspicious_policy, untested_policy))
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__main__.py", line 331, in main
    add_mutations_by_file(mutations_by_file, filename, _exclude, dict_synonyms)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__main__.py", line 595, in add_mutations_by_file
    dict_synonyms=dict_synonyms,
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/mutmut/__init__.py", line 417, in __init__
    if source is not None and source[-1] != '\n':
IndexError: string index out of range

I've tried it on this codebase by running python3 -m mutmut run --runner="python3 -m pytest" --paths-to-mutate="vakt/" --dict-synonyms="Struct, NamedStruct".
Seems this is caused by empty __init__.py files.

Possible fix:
in mutmut/__ init __.py do

class Context(object):
    def __init__(self, source=None, mutation_id=ALL, dict_synonyms=None, filename=None, exclude=lambda context: False, config=None):
        self.index = 0
        self.remove_newline_at_end = False
        if source is not None and source != '' and source[-1] != '\n':
            source += '\n'

Install error

I was trying to do an initial install of mutmut this morning, and I failed with an error. Here is the entire stream:
Admins-MacBook-Air:~ shammond$ sudo -H pip install mutmut
Collecting mutmut
Downloading https://files.pythonhosted.org/packages/a4/68/fe0bec09d1685a58eb5b84c4caf75a0b25d16ac2355479ed14e639e7671a/mutmut-0.0.20.tar.gz
Requirement already satisfied: parso in /Library/Python/2.7/site-packages (from mutmut) (0.3.1)
Requirement already satisfied: tri.declarative in /Library/Python/2.7/site-packages (from mutmut) (1.0.3)
Requirement already satisfied: click in /Library/Python/2.7/site-packages (from mutmut) (6.7)
Requirement already satisfied: tri.struct in /Library/Python/2.7/site-packages (from tri.declarative->mutmut) (2.5.5)
Installing collected packages: mutmut
Running setup.py install for mutmut ... error
Complete output from command /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python -u -c "import setuptools, tokenize;file='/private/tmp/pip-install-K6Bg3Q/mutmut/setup.py';f=getattr(tokenize, 'open', open)(file);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, file, 'exec'))" install --record /private/tmp/pip-record-EfYAk8/install-record.txt --single-version-externally-managed --compile:
running install
running build
running build_py
creating build
creating build/lib
creating build/lib/mutmut
copying ./mutmut/init.py -> build/lib/mutmut
copying ./mutmut/main.py -> build/lib/mutmut
running egg_info
writing requirements to mutmut.egg-info/requires.txt
writing mutmut.egg-info/PKG-INFO
writing top-level names to mutmut.egg-info/top_level.txt
writing dependency_links to mutmut.egg-info/dependency_links.txt
reading manifest file 'mutmut.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'mutmut.egg-info/SOURCES.txt'
running build_scripts
creating build/scripts-2.7
copying and adjusting bin/mutmut -> build/scripts-2.7
changing mode of build/scripts-2.7/mutmut from 644 to 755
running install_lib
running install_data
warning: install_data: setup script did not provide a directory for 'README.rst' -- installing right in '/System/Library/Frameworks/Python.framework/Versions/2.7'

copying README.rst -> /System/Library/Frameworks/Python.framework/Versions/2.7
error: [Errno 1] Operation not permitted: '/System/Library/Frameworks/Python.framework/Versions/2.7/README.rst'

----------------------------------------

Command "/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python -u -c "import setuptools, tokenize;file='/private/tmp/pip-install-K6Bg3Q/mutmut/setup.py';f=getattr(tokenize, 'open', open)(file);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, file, 'exec'))" install --record /private/tmp/pip-record-EfYAk8/install-record.txt --single-version-externally-managed --compile" failed with error code 1 in /private/tmp/pip-install-K6Bg3Q/mutmut/

Python 3 supported?

Is mutmut supposed to work with Python 3? On Mac OS X, using Python 3.6.1 I get this:

$ pip3 install mutmut
...
$ mutmut --help
  File "/Library/Frameworks/Python.framework/Versions/3.6/bin/mutmut", line 47
    print 'ERROR: no mutations performed. Are you sure the index is not too big?'
                                                                                ^
SyntaxError: Missing parentheses in call to 'print'

Mutate only changes from a patch

I think a very useful feature would be to only mutate code that has been changed in a patch (e.g. what is in the Git index etc).

This way you could run mutmut before/after committing changes to check if everything is covered with regard to mutations in a new commit (which not enforcing checks on the whole code base).

AttributeError in argument_mutation

Traceback
Running tests without mutations... Done
Traceback (most recent call last):
  File "env/bin/mutmut", line 5, in <module>
    main()
  File "env/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "env/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "env/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "env/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "env/lib/python3.6/site-packages/mutmut/__main__.py", line 56, in wrapper
    f(*args, **kwargs)
  File "env/lib/python3.6/site-packages/mutmut/__main__.py", line 317, in main
    add_mutations_by_file(mutations_by_file, filename, _exclude, dict_synonyms)
  File "env/lib/python3.6/site-packages/mutmut/__main__.py", line 519, in add_mutations_by_file
    dict_synonyms=dict_synonyms,
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 363, in list_mutations
    mutate(context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 273, in mutate
    mutate_list_of_nodes(result, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 320, in mutate_node
    children=getattr(i, 'children', None),
  File "env/lib/python3.6/site-packages/tri/declarative/__init__.py", line 340, in evaluate
    return func_or_value(**kwargs)
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 91, in argument_mutation
    children[0] = Name(c.value + 'XX', start_pos=c.start_pos, prefix=c.prefix)
AttributeError: 'PythonNode' object has no attribute 'value'
Value of c:
PythonNode(atom, [<Operator: (>, PythonNode(testlist_comp, [PythonNode(atom_expr, [<Name: key@145,24>, PythonNode(trailer, [<Operator: (>, <Name: field@145,28>, <Operator: )>])]), <Operator: ,>, <Keyword: True>]), <Operator: )>])

IndexError in argument_mutation function

Stacktrace:

  File "env/lib/python3.6/site-packages/mutmut/__main__.py", line 317, in main                                             
    add_mutations_by_file(mutations_by_file, filename, _exclude, dict_synonyms)                                                                                
  File "env/lib/python3.6/site-packages/mutmut/__main__.py", line 520, in add_mutations_by_file                            
    dict_synonyms=dict_synonyms,                                                                                                                              
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 363, in list_mutations
    mutate(context)                                                                                                                                           
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 271, in mutate
    mutate_list_of_nodes(result, context=context)                                                                                                             
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)                                                                                                                           
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)                                                                                                                  
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)                                                                                                                           
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 301, in mutate_node
    mutate_list_of_nodes(i, context=context)                                                                                                                   
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 342, in mutate_list_of_nodes
    mutate_node(i, context=context)                                                                                                                           
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 320, in mutate_node
    children=getattr(i, 'children', None),                                                                                                                    
  File "env/lib/python3.6/site-packages/tri/declarative/__init__.py", line 340, in evaluate
    return func_or_value(**kwargs) 
  File "env/lib/python3.6/site-packages/mutmut/__init__.py", line 80, in argument_mutation
    elif context.stack[-4].type == 'power':                
IndexError: list index out of range  

Value of context.stack:

[PythonNode(decorated, [<Decorator: @register.simple_tag(name='XXiconXX')@14,0>, <Function: icon@15-20>]), <Decorator: @register.simple_tag(name='XXiconXX')@14
,0>, PythonNode(argument, [<Name: name@14,21>, <Operator: =>, <String: 'XXiconXX'>])]

Relevant source code part:

@register.simple_tag(name='icon')
def icon(name):
    if name is None:
        return ''
    tpl = '<span class="glyphicon glyphicon-{}"></span>'
    return format_html(tpl, name)

Indicate result via exit code

Currently mutmut run and result always exits with code 0.

It would be very useful (e.g. for inclusion in CI systems) if both could give a return code depending on the type of found issues.

Improving output: verbose mode

How currently the output looks like?

900 out of 1067  (wemake_python_styleguide/violations/consistency.py     should_use_text =                                                                                          FAILED: mutmut wemake_python_styleguide/violations/consistency.py --apply --mutation "    should_use_text = False⤑1"

901 out of 1067  (wemake_python_styleguide/violations/consistency.py     error_template =                                                                                           FAILED: mutmut wemake_python_styleguide/violations/consistency.py --apply --mutation "    error_template = 'Found parens right after a keyword'⤑0"

902 out of 1067  (wemake_python_styleguide/violations/consistency.py     error_template = 
903 out of 1067  (wemake_python_styleguide/violations/consistency.py     code = 313⤑0)

It shows two cases: failing and successful state.
Let's talk about both of them. I will also try to cover some general thoughts that I have.

First of all, I would like to analyze what happened. Since this process it rather long, I am not able to watch it in real time. The final output is also quite verbose, so it is hard to read it though. What do I suggest? It would be nice to have some short report that will cover:

  1. What mutants managed to survive: what was changed, what was the change, in what file
  2. What tests killed the most mutants (so these tests can be documented as important ones, etc), what tests did not caught any (so these tests might be removed in the future or refactored)

Secondly, it is possible to have better "inline experience". What do I mean by that?

  1. Currently when reading through the test cases I am missing some required information: what source line number was changed, to what, what mutation rule caused this
  2. In case that the mutant was killed I guess we can show how many tests did fail. It would be very helpful and will give you a better understanding of what is going on

I would like to say, that this tool is absolutely awesome! Thank you for building it!

mutmut command fails if no arguments are given

running the mutmut cli without arguments prints a trace back. I realize that this is not how it is supposed to be used but it is not very friendly. Perhaps the help text could be printed.

Windows 10 Enterprise
Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
mutmut (0.0.3)

Be more verbose

There should be some output when running mutmut by default.

I am just trying it out (seen it via tarpas/pytest-testmon#76), but it seems to be very silent during the initial run for example.
It would be nice to just get the normal output there - which should include wrapping in pytest-sugar if it is installed/enabled.

Add --cache-only flag

Instead of having CI run the full mutation stage we could check in the entire mutmut cache dir and in CI run with a a --cache-only flag where it fails if the cache is out of date. This would make it nice to have a mutation testing badge on your project on github, but without the hassle of storing and loading the mutmut cache database or the cost of running the full mutation testing on travis on each commit.

OSError raised when waiting for pytest to finish

I checked out master/commit 63089e9 to try the new UI (pretty nice!) but ran into trouble:

mutmut appears to be waiting for a spawned process's returncode to be set, continually asking for a line from stdout until this condition holds. However, it appears that, at some point:

  1. p.returncode is still None
  2. stdout.readline() raises an OSError

Wrapping the readline() call in a try/except pass seems to fix the issue. It let the initial test run complete, and then started counting up the killed/survived totals.

Stacktrace:

⠙ Running...Traceback (most recent call last):
  File "/home/flux/anaconda3/bin/mutmut", line 5, in <module>
    main()
  File "/home/flux/.local/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/home/flux/.local/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/home/flux/.local/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/flux/.local/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/home/flux/anaconda3/lib/python3.6/site-packages/mutmut/__main__.py", line 57, in wrapper
    f(*args, **kwargs)
  File "/home/flux/anaconda3/lib/python3.6/site-packages/mutmut/__main__.py", line 236, in main
    baseline_time_elapsed = time_test_suite(swallow_output=not s, test_command=runner, using_testmon=using_testmon)
  File "/home/flux/anaconda3/lib/python3.6/site-packages/mutmut/__main__.py", line 431, in time_test_suite
    returncode = popen_streaming_output(test_command, feedback)
  File "/home/flux/anaconda3/lib/python3.6/site-packages/mutmut/__main__.py", line 306, in popen_streaming_output
    line = stdout.readline()[:-1]  # -1 to remove the newline at the end
OSError: [Errno 5] Input/output error

The bandaid I applied change line 306 to look like this:

        try:
            line = stdout.readline()[:-1]  # -1 to remove the newline at the end
        except OSError:
            pass

The except block is only hit once. p.returncode appears to be set properly afterwards, and the program carries on.

My Python version is Python 3.6.5 :: Anaconda custom (64-bit), if that's relevant.

Ctrl-C needs to be used twice to interrupt

2. Checking mutants
⠇ 7/282  🎉 3  ⏰ 0  🤔 0  🙁 4^C
Aborted!
^CException ignored in: <module 'threading' from '/usr/lib/python3.7/threading.py'>
Traceback (most recent call last):
  File "/usr/lib/python3.7/threading.py", line 1273, in _shutdown
    t.join()
  File "/usr/lib/python3.7/threading.py", line 1032, in join
    self._wait_for_tstate_lock()
  File "/usr/lib/python3.7/threading.py", line 1048, in _wait_for_tstate_lock
    elif lock.acquire(block, timeout):
KeyboardInterrupt

mutmut 1.0.0.

Handle errors from baron better

I've seen an error via baron when running mutmut, which seems to display the
same exception twice, and provides no reference to the source:

% mutmut project --tests-dir project/app/tests
Traceback (most recent call last):
  File "…/project/.venv/lib/python3.6/site-packages/baron/baron.py", line 20, in _parse
    return parser(tokens)
  File "…/project/.venv/lib/python3.6/site-packages/baron/grammator.py", line 695, in parse
    return parser.parse(iter(tokens))
  File "…/project/.venv/lib/python3.6/site-packages/baron/parser.py", line 168, in parse
    raise ParsingError(debug_output)
baron.parser.ParsingError: Error, got an unexpected token DOUBLE_STAR here:

1381 … 
1382 …            
1383 …
1384 …       
1385 …              
1386 …
1387 
1388         foo_kwargs = {**<---- here

The token DOUBLE_STAR should be one of those: BACKQUOTE, BINARY, BINARY_RAW_STRING, BINARY_STRING, COMPLEX, FLOAT, FLOAT_EXPONANT, FLOAT_EXPONANT_COMPLEX, HEXA, INT, LAMBDA, LEFT_BRACKET, LEFT_PARENTHESIS, LEFT_SQUARE_BRACKET, LONG, MINUS, NAME, NOT, OCTA, PLUS, RAW_STRING, RIGHT_BRACKET, STRING, TILDE, UNICODE_RAW_STRING, UNICODE_STRING

It is not normal that you see this error, it means that Baron has failed to parse valid Python code. It would be kind if you can extract the snippet of your code that makes Baron fail and open a bug here: https://github.com/Psycojoker/baron/issues

Sorry for the inconvenience.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "…/project/.venv/bin/mutmut", line 5, in <module>
    main()
  File "…/project/.venv/lib/python3.6/site-packages/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "…/project/.venv/lib/python3.6/site-packages/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "…/project/.venv/lib/python3.6/site-packages/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "…/project/.venv/lib/python3.6/site-packages/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "…/project/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 35, in wrapper
    f(*args, **kwargs)
  File "…/project/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 135, in main
    mutations_by_file[filename] = count_mutations(open(filename).read(), context__filename=filename, context__exclude=exclude)
  File "…/project/.venv/lib/python3.6/site-packages/tri/declarative/__init__.py", line 587, in wrapper
    return f(*args, **setdefaults_path(Namespace(), kwargs, defaults))
  File "…/project/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 365, in count_mutations
    return mutate(source, ALL, context=context)[1]
  File "…/project/.venv/lib/python3.6/site-packages/tri/declarative/__init__.py", line 587, in wrapper
    return f(*args, **setdefaults_path(Namespace(), kwargs, defaults))
  File "…/project/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 282, in mutate
    result = parse(source)
  File "…/project/.venv/lib/python3.6/site-packages/baron/baron.py", line 66, in parse
    return _parse(tokens, print_function)
  File "…/project/.venv/lib/python3.6/site-packages/baron/baron.py", line 26, in _parse
    raise e
  File "…/project/.venv/lib/python3.6/site-packages/baron/baron.py", line 24, in _parse
    return parser(tokens)
  File "…/project/.venv/lib/python3.6/site-packages/baron/grammator.py", line 695, in parse
    return parser.parse(iter(tokens))
  File "…/project/.venv/lib/python3.6/site-packages/baron/parser.py", line 168, in parse
    raise ParsingError(debug_output)
baron.parser.ParsingError: Error, got an unexpected token DOUBLE_STAR here:

1381 … 
1382 …            
1383 …
1384 …       
1385 …              
1386 …
1387 
1388         foo_kwargs = {**<---- here

The token DOUBLE_STAR should be one of those: BACKQUOTE, BINARY, BINARY_RAW_STRING, BINARY_STRING, COMPLEX, FLOAT, FLOAT_EXPONANT, FLOAT_EXPONANT_COMPLEX, HEXA, INT, LAMBDA, LEFT_BRACKET, LEFT_PARENTHESIS, LEFT_SQUARE_BRACKET, LONG, MINUS, NAME, NOT, OCTA, PLUS, RAW_STRING, RIGHT_BRACKET, STRING, TILDE, UNICODE_RAW_STRING, UNICODE_STRING

It is not normal that you see this error, it means that Baron has failed to parse valid Python code. It would be kind if you can extract the snippet of your code that makes Baron fail and open a bug here: https://github.com/Psycojoker/baron/issues

Sorry for the inconvenience.

Confusing output with Ctrl-C

% mutmut
^C
Aborted!
Running tests without mutations...

It should either display the "Running tests without mutations..." before (assuming it was captured?!), or without any capturing in the first place, or not at all.

Maybe that's confusing me with #31 - that I do not see the "Running tests without mutations..." in the first place?

mutmut==0.0.24 - there is no --version flag?!

cache: take runner into account? / use args for "run"

I've used mutmut run --use-coverage --runner 'python -m pytest -x testing/test_collection.py' --paths-to-mutate src/_pytest/main.py first.

The first survived mutant was:

% mutmut show 1
EXIT_OK = 0
   |
   V
EXIT_OK = 1

I've then created a test_mutmut.py file, where I've checked that EXIT_OK == 0, but it appears like everything was handled through the cache (according to the speed, there is no actual feedback/count for that).

I assume that using a test file through the --runner argument is considered to be a hack, but I think it is rather important which runner (options) were used, and therefore it should be taken into account for the cache key.

I've seen that there is --tests-dir - maybe that should become --tests-arg?

I've tried using --tests-dir testing/test_mutmut.py, but it appears to run the whole tests?!
(the actual command being used would be useful to see with "1. Running tests without mutations" here)

When using run as a click sub-command, it could take any non-option arguments and append them to the --runner command maybe, making --tests-dir obsolete?!

mutmut doesn't seem to work on windows

It would appear that there are hard coded paths that prevent mutmut from running on windows. Would the accepted solution here be an acceptable fix?

>mutmut-cmd --tests-dir execution\tests execution                                                                                                                     Traceback (most recent call last):
File "C:\Users\fmoor\.venv\barometer\Scripts\mutmut-cmd.py", line 185, in <module>
    main()
File "c:\users\fmoor\.venv\barometer\lib\site-packages\click\core.py", line 722, in __call__
    return self.main(*args, **kwargs)
File "c:\users\fmoor\.venv\barometer\lib\site-packages\click\core.py", line 697, in main
    rv = self.invoke(ctx)
File "c:\users\fmoor\.venv\barometer\lib\site-packages\click\core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
File "c:\users\fmoor\.venv\barometer\lib\site-packages\click\core.py", line 535, in invoke
    return callback(*args, **kwargs)
File "c:\users\fmoor\.venv\barometer\lib\site-packages\mutmut\__init__.py", line 35, in wrapper
    f(*args, **kwargs)
File "C:\Users\fmoor\.venv\barometer\Scripts\mutmut-cmd.py", line 73, in main
    null_stdout = open('/dev/null', 'w') if not s else None
FileNotFoundError: [Errno 2] No such file or directory: '/dev/null'

Windows 10 Enterprise
Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
mutmut (0.0.3)

Number mutation doesn't handle decimal numbers between 0 and 1

Hi,

Attempting to mutate the following line fails with an assertion error:

self.something = 0.1

File "[...]/venv/lib/python3.5/site-packages/mutmut/init.py", line 44, in number_mutation

assert base == 10
AssertionError

This seems to be related to the following lines of mutmut/init.py (lines 37-45):

...
elif value.startswith('0') and len(value) > 1:  # pragma: no cover (python 2 specific)
       base = 8
       value = value[1:]
   else:
       base = 10

   if '.' in value:
       assert base == 10
       parsed = float(value)
...

self.something = 10.1 works
self.something = .1 works
but
self.something = 0.1 does not work

I've worked around this currently by replacing all numbers between 0 and 1 without a '0' before the decimal point, however this is not a long term solution or suitable for a large project.

I am using Version: 0.0.22

Thanks

Option to skip initial test run

This is useful for testing scenarios and to speed up re-running stuff if you're pretty sure this step will succeed. The downside is of course that mutmut can't detect excessively long test runs.

`IndexError` inside `expression_mutation`

Hi, thanks for this module! I am trying to use it with my app: https://github.com/wemake-services/wemake-python-styleguide

But, it is failing. Output:

» mutmut 
Running tests without mutations... Done
Traceback (most recent call last):
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/bin/mutmut", line 5, in <module>
    main()
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 55, in wrapper
    f(*args, **kwargs)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 225, in main
    add_mutations_by_file(mutations_by_file, filename, _exclude, dict_synonyms)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__main__.py", line 417, in add_mutations_by_file
    dict_synonyms=dict_synonyms,
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 373, in list_mutations
    mutate(context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 286, in mutate
    mutate_list_of_nodes(result, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 311, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 311, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 311, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 311, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 311, in mutate_node
    mutate_list_of_nodes(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 352, in mutate_list_of_nodes
    mutate_node(i, context=context)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 330, in mutate_node
    children=getattr(i, 'children', None),
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/tri/declarative/__init__.py", line 341, in evaluate
    return func_or_value(**kwargs)
  File "/Users/sobolev/Documents/github/wemake-python-styleguide/.venv/lib/python3.6/site-packages/mutmut/__init__.py", line 184, in expression_mutation
    if children[2].value == '=':
IndexError: list index out of range

Versions

» pip freeze
Could not find setup.py for directory /Users/sobolev/Documents/github/wemake-python-styleguide (tried all parent directories)
added-value==0.8
alabaster==0.7.12
atomicwrites==1.2.1
attrs==18.2.0
Babel==2.6.0
bandit==1.5.1
certifi==2018.10.15
chardet==3.0.4
Click==7.0
coverage==4.5.1
dataclasses==0.6
doc8==0.8.0
docutils==0.14
eradicate==0.2.1
flake8==3.6.0
flake8-bandit==1.0.2
flake8-broken-line==0.1.0
flake8-bugbear==18.8.0
flake8-builtins==1.4.1
flake8-coding==1.3.1
flake8-commas==2.0.0
flake8-comprehensions==1.4.1
flake8-debugger==3.1.0
flake8-docstrings==1.3.0
flake8-eradicate==0.1.1
flake8-isort==2.5
flake8-logging-format==0.5.0
flake8-pep3101==1.2.1
flake8-polyfill==1.0.2
flake8-print==3.1.0
flake8-pytest==1.3
flake8-quotes==1.0.0
flake8-string-format==0.2.3
flake8-type-annotations==0.1.0
gitdb2==2.0.5
GitPython==2.1.11
glob2==0.6
idna==2.7
imagesize==1.1.0
isort==4.3.4
Jinja2==2.10
m2r==0.2.1
MarkupSafe==1.0
mccabe==0.6.1
mistune==0.8.4
more-itertools==4.3.0
mutmut==0.0.22
mypy==0.641
mypy-extensions==0.4.1
packaging==18.0
parso==0.3.1
pbr==5.1.0
pep8-naming==0.7.0
pluggy==0.8.0
pockets==0.7.2
py==1.7.0
pycodestyle==2.4.0
pydocstyle==3.0.0
pyflakes==2.0.0
Pygments==2.2.0
Pympler==0.6
pyparsing==2.2.2
pytest==3.9.3
pytest-cov==2.6.0
pytest-flake8==1.0.2
pytest-isort==0.2.1
pytest-randomly==1.2.3
pytz==2018.7
PyYAML==3.13
requests==2.20.0
restructuredtext-lint==1.1.3
six==1.11.0
smmap2==2.0.5
snowballstemmer==1.2.1
Sphinx==1.8.1
sphinx-autodoc-typehints==1.3.0
sphinx-readable-theme==1.3.0
sphinxcontrib-napoleon==0.7
sphinxcontrib-websupport==1.1.0
stevedore==1.30.0
testfixtures==6.3.0
tomlkit==0.4.6
tri.declarative==1.0.6
tri.struct==2.5.6
typed-ast==1.1.0
typing==3.6.6
typing-extensions==3.6.6
urllib3==1.24
-e git+https://github.com/wemake-services/wemake-python-styleguide.git@8b9cfd896ae500e99a5a47a5f10386150a26082c#egg=wemake_python_styleguide
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

I am using:

  • python3.6
  • poetry@12

Configuration

I have added these values to my setup.cfg:

[mutmut]
paths_to_mutate = wemake_python_styleguide/
backup = False
runner = pytest
tests_dir = tests/

Any ideas?

mutmut cli doesn't work on windows

It appears that the mutmut command line script is just called mutmut. On windows this doesn't work. The script has to have an extension i.e. *.py. I renamed the cli to mutmut.py and this didn't work either because the cli name was now colliding with the mutmut library. Changing the name again to mutmut-cmd.py fixed the collision.

Windows 10 Enterprise
Python 3.6.2 (v3.6.2:5fd33b5, Jul 8 2017, 04:14:34) [MSC v.1900 32 bit (Intel)] on win32
mutmut (0.0.3)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.