~amirouche/mutation

7b94957161308ac9884be1caa7c98482d60072ae — Amirouche 7 months ago 26e87bb v0.4.0
v0.4.0 - varia and cosmit
4 files changed, 194 insertions(+), 101 deletions(-)

M mutation.py
M poetry.lock
M pyproject.toml
M test.py
M mutation.py => mutation.py +107 -77
@@ 2,7 2,7 @@

Usage:
  mutation play [--verbose] [--exclude=<globs>] [--only-deadcode-detection] [--include=<globs>] [--sampling=<s>] [--randomly-seed=<n>] [--max-workers=<n>] [<file-or-directory> ...] [-- TEST-COMMAND ...]
  mutation replay
  mutation replay [--verbose] [--max_workers=<n>]
  mutation list
  mutation show MUTATION
  mutation apply MUTATION


@@ 22,7 22,6 @@ import os
import random
import re
import shlex
import subprocess
import sys
import time
from ast import Constant


@@ 31,7 30,7 @@ from contextlib import contextmanager
from copy import deepcopy
from datetime import timedelta
from difflib import unified_diff
from pathlib import Path
from pathlib3x import Path
from uuid import UUID

import git


@@ 48,7 47,7 @@ from lsm import LSM
from tqdm import tqdm
from ulid import ULID

__version__ = (0, 3, 3)
__version__ = (0, 4, 0)


MINUTE = 60  # seconds


@@ 267,13 266,17 @@ class MutateNumber(metaclass=Mutation):
            def randomize(x):
                return random.random() * x

        for size in range(6):
        for size in range(8, 32):
            if value < 2 ** size:
                break

        for _ in range(type(self).COUNT):
        count = 0
        while count != self.COUNT:
            count += 1
            root, new = node_copy_tree(node, index)
            new.value = str(randomize(2 ** size))
            if new.value == node.value:
                continue
            yield root, new




@@ 400,7 403,8 @@ def deltas_compute(source, path, coverage, mutations):
            delta = diff(source, target, path)
            yield delta
    if ignored > 1:
        msg = "Ignored {} mutations from file at {} because there is no associated coverage."
        msg = "Ignored {} mutations from file at {}"
        msg += " because there is no associated coverage."
        log.trace(msg, ignored, path)




@@ 435,7 439,8 @@ def mutation_create(item):
    path, source, coverage, mutation_predicate = item

    if not coverage:
        log.trace("Ignoring file {} because there is no associated coverage.", path)
        msg = "Ignoring file {} because there is no associated coverage."
        log.trace(msg, path)
        return []

    log.trace("Mutating file: {}...", path)


@@ 463,26 468,33 @@ def install_module_loader(uid):
    patched = patch(diff, source)

    import imp
    module_path = path[:-3].replace('/', '.')

    class MyLoader:
        def load_module(self, fullname):
            try:
                return sys.modules[fullname]
            except KeyError:
                pass
            my_module = imp.new_module(fullname)
            exec(patched, my_module.__dict__)
            sys.modules.setdefault(fullname, my_module)
            return my_module
    components = path[:-3].split('/')
    log.trace(components)
    while components:
        for pythonpath in sys.path:
            filepath = os.path.join(pythonpath, '/'.join(components))
            filepath += ".py"
            ok = os.path.exists(filepath)
            if ok:
                module_path = '.'.join(components)
                break
        else:
            components.pop()
            continue
        break
    if module_path is None:
        raise Exception("sys.path oops!")
    log.warning(module_path)

    class MyFinder:
        def find_module(self, fullname, path=None):
            if fullname == module_path:
                return MyLoader()
            return None
    patched_module = imp.new_module(module_path)
    try:
        exec(patched, patched_module.__dict__)
    except Exception:
        # TODO: syntaxerror, do not produce those mutations
        exec('', patched_module.__dict__)

    sys.meta_path.insert(0, MyFinder())
    sys.modules[module_path] = patched_module


def pytest_configure(config):


@@ 508,11 520,10 @@ def for_each_par_map(loop, pool, inc, proc, items):
def mutation_pass(args):  # TODO: rename
    command, uid, timeout = args
    command = command + ["--mutation={}".format(uid.hex)]
    out = run(command, timeout=timeout)

    out = run(command, timeout=timeout, silent=True)
    if out == 0:
        msg = "no error with mutation: {}"
        log.trace(msg, " ".join(command))
        msg = "no error with mutation: {} ({})"
        log.trace(msg, " ".join(command), out)
        with database_open(".") as db:
            db[lexode.pack([2, uid])] = b"\x00"
        return False


@@ 532,6 543,8 @@ def coverage_read(root):
    coverage.load()
    data = coverage.get_data()
    files = data.measured_files()
    # TODO: if coverage contains files outside git repo, it will raise
    # an exception.
    out = {str(Path(path).relative_to(root)): set(data.lines(path)) for path in files}
    return out



@@ 563,18 576,19 @@ def git_open(root):
        return repository


def run(command, timeout=None):
    try:
        out = subprocess.run(
            command,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=None,
        )
    except Exception:
        return -1
    else:
        return out.returncode
def run(command, timeout=None, silent=True):
    if timeout and timeout < 60:
        timeout = 60

    if timeout:
        command.insert(0, "timeout {}".format(timeout))

    command.insert(0, "PYTHONDONTWRITEBYTECODE=1")

    if silent and not os.environ.get("DEBUG"):
        command.append("> /dev/null 2>&1")

    return os.system(" ".join(command))


def sampling_setup(sampling, total):


@@ 616,24 630,20 @@ def sampling_setup(sampling, total):
    return sampler, total


def play_test_tests(root, seed, repository, arguments, command=None):
# TODO: the `command` is a hack, maybe there is a way to avoid the
# following code that looks like `if command is not None.
def check_tests(root, seed, repository, arguments, command=None):
    max_workers = arguments["--max-workers"] or (os.cpu_count() - 1) or 1
    max_workers = int(max_workers)

    log.info("Let's check that the tests are green...")
    #
    # TODO: use the coverage program instead with something along the
    # lines of:
    #
    #   coverage run --omit=tests.py --include=hoply*.py -m pytest tests.py
    #
    # To be able to pass --omit and --include.
    #

    if arguments["<file-or-directory>"] and arguments["TEST-COMMAND"]:
        log.error("<file-or-directory> and TEST-COMMAND are exclusive!")
        sys.exit(1)

    if command is not None:
        command = list(command)
        if max_workers > 1:
            command.extend(
                [


@@ 643,13 653,19 @@ def play_test_tests(root, seed, repository, arguments, command=None):
                ]
            )
    else:
        command = list(arguments["TEST-COMMAND"] or PYTEST)
        if arguments["TEST-COMMAND"]:
            command = list(arguments["TEST-COMMAND"])
        else:
            command = list(PYTEST)
            command.extend(arguments["<file-or-directory>"])

        if max_workers > 1:
            command.append(
                # Use pytest-xdist to make sure it is possible to run
                # the tests in parallel
                "--numprocesses={}".format(max_workers)
            )

        command.extend(
            [
                # Setup coverage options to only mutate what is tested.


@@ 660,7 676,6 @@ def play_test_tests(root, seed, repository, arguments, command=None):
                "--randomly-seed={}".format(seed),
            ]
        )
        command.extend(arguments["<file-or-directory>"])

    with timeit() as alpha:
        out = run(command)


@@ 669,12 684,18 @@ def play_test_tests(root, seed, repository, arguments, command=None):
        log.info("Tests are green 💚")
        alpha = alpha() * max_workers
    else:
        msg = "Tests are not green or something... return code is {}..."
        msg = "Tests are not green... return code is {}..."
        log.warning(msg, out)
        log.warning("I tried the following command: `{}`", " ".join(command))

        command = list(arguments["TEST-COMMAND"] or PYTEST)
        command = command + [
        # Same command without parallelization
        if arguments["TEST-COMMAND"]:
            command = list(arguments["TEST-COMMAND"])
        else:
            command = list(PYTEST)
            command.extend(arguments["<file-or-directory>"])

        command += [
            # Setup coverage options to only mutate what is tested.
            "--cov=.",
            "--cov-branch",


@@ 682,7 703,6 @@ def play_test_tests(root, seed, repository, arguments, command=None):
            # Pass random seed
            "--randomly-seed={}".format(seed),
        ]
        command += arguments["<file-or-directory>"]

        with timeit() as alpha:
            out = run(command)


@@ 695,7 715,8 @@ def play_test_tests(root, seed, repository, arguments, command=None):

        # Otherwise, it is possible to run the tests but without
        # parallelization.
        log.info("Overriding max_workers=1 because tests do not pass in parallel")
        msg = "Setting max_workers=1 because tests do not pass in parallel"
        log.warning(msg)
        max_workers = 1
        alpha = alpha()



@@ 746,12 767,15 @@ async def play_create_mutations(loop, root, db, repository, max_workers, argumen
        return out

    items = (make_item(blob) for blob in blobs if coverage.get(blob.path, set()))
    # Start with biggest files first, because that is those that will
    # take most time, that way, it will make most / best use of the
    # workers.
    items = sorted(items, key=lambda x: len(x[1]), reverse=True)

    # prepare to create mutations
    total = 0

    log.info("Creating mutations from {} files...", len(items))
    log.info("Crafting mutations from {} files...", len(items))
    with tqdm(total=len(items), desc="Files") as progress:

        def on_mutations_created(items):


@@ 783,8 807,11 @@ async def play_mutations(loop, db, seed, alpha, total, max_workers, arguments):
    command.append("--randomly-seed={}".format(seed))
    command.extend(arguments["<file-or-directory>"])

    eta = humanize(alpha * total / max_workers)
    log.success("It will take at most {} to run the mutations", eta)

    timeout = alpha * 2
    uids = db[lexode.pack([1]) : lexode.pack([2])]
    uids = db[lexode.pack([1]):lexode.pack([2])]
    uids = ((command, lexode.unpack(key)[1], timeout) for (key, _) in uids)

    # sampling


@@ 851,9 878,6 @@ async def play_mutations(loop, db, seed, alpha, total, max_workers, arguments):


async def play(loop, arguments):
    # TODO: Always use git HEAD, and display a message as critical
    #       explaining what is happenning... Not sure about that.

    root = Path(".").resolve()
    repository = git_open(root)



@@ 861,12 885,15 @@ async def play(loop, arguments):
    log.info("Using random seed: {}".format(seed))
    random.seed(seed)

    alpha, max_workers = play_test_tests(root, seed, repository, arguments)
    alpha, max_workers = check_tests(root, seed, repository, arguments)

    with database_open(root, recreate=True) as db:
        # store arguments used to execute command
        command = list(arguments["TEST-COMMAND"] or PYTEST)
        command += arguments["<file-or-directory>"]
        if arguments["TEST-COMMAND"]:
            command = list(arguments["TEST-COMMAND"])
        else:
            command = list(PYTEST)
            command += arguments["<file-or-directory>"]
        command = dict(
            command=command,
            seed=seed,


@@ 874,21 901,22 @@ async def play(loop, arguments):
        value = list(command.items())
        db[lexode.pack((0, "command"))] = lexode.pack(value)

        # let's play!
        # let's create mutations!
        count = await play_create_mutations(
            loop, root, db, repository, max_workers, arguments
        )
        # Let's run tests against mutations!
        await play_mutations(loop, db, seed, alpha, count, max_workers, arguments)


def mutation_diff_size(db, uid):
    _, diff = lexode.unpack(db[lexode.pack([1, uid[0]])])
    _, diff = lexode.unpack(db[lexode.pack([1, uid])])
    out = len(zstd.decompress(diff))
    return out


def replay_mutation(db, uid, alpha, seed, max_workers, command):
    print("* Use Ctrl+C to exit.")
    log.info("* Use Ctrl+C to exit.")

    repository = git_open(".")



@@ 905,8 933,8 @@ def replay_mutation(db, uid, alpha, seed, max_workers, command):
        if not ok:
            mutation_show(uid.hex)
            msg = "* Type 'skip' to go to next mutation or just enter to retry."
            print(msg)
            skip = input("> ") == "skip"
            log.info(msg)
            skip = input() == "skip"
            if skip:
                db[lexode.pack([2, uid])] = b"\x01"
                return


@@ 915,13 943,14 @@ def replay_mutation(db, uid, alpha, seed, max_workers, command):
            non_indexed = repository.index.diff(None)
            indexed = repository.index.diff("HEAD")
            if indexed or non_indexed:
                print("* They are uncommited changes, do you want to commit?")
                yes = input("> ").startswith("y")
                msg = "* They are uncommited changes, do you want to commit? (yes/no)"
                log.warning(msg)
                yes = input().startswith("y")
                if not yes:
                    return

                for file in non_indexed:
                    repository.add(file)
                    repository.index.add(file)
                repository.index.commit("fixed mutation bug uid={}.".format(uid.hex))
            del db[lexode.pack([2, uid])]
            return


@@ 938,9 967,9 @@ def replay(arguments):
    command = dict(command)
    seed = command.pop("seed")
    random.seed(seed)
    command = list(command.pop("command"))
    command = command.pop("command")

    alpha, max_workers = play_test_tests(root, seed, repository, arguments, command)
    alpha, max_workers = check_tests(root, seed, repository, arguments, command)

    with database_open(root) as db:
        while True:


@@ 963,14 992,14 @@ def mutation_list():
        uids = ((lexode.unpack(k)[1], v) for k, v in db[lexode.pack([2]):])
        uids = sorted(
            uids,
            key=functools.partial(mutation_diff_size, db),
            key=lambda x: mutation_diff_size(db, x[0]),
            reverse=True
        )
    if not uids:
        log.info("No mutation failures 👍")
        sys.exit(0)
    for (uid, type) in uids:
        print("{}\t{}".format(uid.hex, "skipped" if type == b"\x01" else ""))
        log.info("{}\t{}".format(uid.hex, "skipped" if type == b"\x01" else ""))


def mutation_show(uid):


@@ 1029,6 1058,7 @@ def main():
        mutation_apply(arguments["MUTATION"])
        sys.exit(0)

    # Otherwise run play.
    loop = asyncio.get_event_loop()
    loop.run_until_complete(play(loop, arguments))
    loop.close()

M poetry.lock => poetry.lock +84 -19
@@ 105,10 105,21 @@ python-versions = "*"
pycparser = "*"

[[package]]
name = "cli-exit-tools"
version = "1.1.8"
description = "functions to exit an cli application properly"
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.dependencies]
click = "*"

[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"



@@ 152,17 163,6 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"

[[package]]
name = "diff-highlight"
version = "1.2.0"
description = "pretty diff highlighter; emphasis changed words in diff"
category = "main"
optional = false
python-versions = "*"

[package.extras]
testing = ["flake8", "mercurial", "mock", "nose", "six"]

[[package]]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"


@@ 218,6 218,22 @@ python-versions = ">=3.6"
tests = ["freezegun", "pytest", "pytest-cov"]

[[package]]
name = "importlib-metadata"
version = "3.4.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]

[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"


@@ 366,6 382,18 @@ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]

[[package]]
name = "pathlib3x"
version = "1.3.9"
description = "backport of pathlib 3.10 to python 3.6, 3.7, 3.8, 3.9 with a few extensions"
category = "main"
optional = false
python-versions = ">=3.6.0"

[package.dependencies]
cli-exit-tools = "*"
click = "*"

[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."


@@ 400,6 428,9 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}

[package.extras]
dev = ["pre-commit", "tox"]



@@ 466,6 497,7 @@ python-versions = ">=3.6"
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0.0a1"


@@ 511,6 543,7 @@ optional = false
python-versions = ">=3.5"

[package.dependencies]
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
pytest = "*"

[[package]]


@@ 627,7 660,7 @@ python-versions = "*"
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
category = "main"
optional = false
python-versions = "*"



@@ 651,6 684,18 @@ python-versions = ">=3.5"
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]

[[package]]
name = "zipp"
version = "3.4.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.6"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]

[[package]]
name = "zstandard"
version = "0.15.1"
description = "Zstandard bindings for Python"


@@ 666,8 711,8 @@ cffi = ["cffi (>=1.11)"]

[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "354b8f6a684c5556fff1a8a301d3a394aef6056135f73edfa0974b570db6bae8"
python-versions = "^3.7"
content-hash = "217757e0499e27be847632b730cf1af5684b3287be8765ffed9c92dac3608b08"

[metadata.files]
aiostream = [


@@ 742,6 787,14 @@ cffi = [
    {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"},
    {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"},
]
cli-exit-tools = [
    {file = "cli_exit_tools-1.1.8-py3-none-any.whl", hash = "sha256:e6bb2d990d24bc38ce57484a587a187f4237f118c347c87ba030f8321ee5a5c2"},
    {file = "cli_exit_tools-1.1.8-py3.6.egg", hash = "sha256:6866bc5d97f091fc72c064b270c88031e91252105bf1c3a044303dc7563b51fe"},
    {file = "cli_exit_tools-1.1.8-py3.7.egg", hash = "sha256:18526c6068d7f084e3d94fec8f5dd92f0638703383e3530dd69dc57560b3d8d2"},
    {file = "cli_exit_tools-1.1.8-py3.8.egg", hash = "sha256:55952aa5419bd0e1698ff3d23a2cc11f83b0589c8f5827b6ee50502cfecc4e32"},
    {file = "cli_exit_tools-1.1.8-py3.9.egg", hash = "sha256:8a6eb9f2f28ee492d42bcd044dad39c65f0032a8991d719f9c637d6056441c18"},
    {file = "cli_exit_tools-1.1.8.tar.gz", hash = "sha256:8f1d055d1c5b793d9355d7368a2f48cbb20c342dabc3f46ebd69e468dcfa2ab9"},
]
click = [
    {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
    {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},


@@ 808,10 861,6 @@ decorator = [
    {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
    {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
]
diff-highlight = [
    {file = "diff-highlight-1.2.0.tar.gz", hash = "sha256:b33321384e7bbee569b1ac0f4f3d09efef843befc3233a5ef899cf887650a4dc"},
    {file = "diff_highlight-1.2.0-py2.py3-none-any.whl", hash = "sha256:8e179aec4f9af984bc12bfef3d371ed0d1f96fa33b51827a81579f4b02d53d28"},
]
docopt = [
    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]


@@ 831,6 880,10 @@ humanize = [
    {file = "humanize-3.2.0-py3-none-any.whl", hash = "sha256:d47d80cd47c1511ed3e49ca5f10c82ed940ea020b45b49ab106ed77fa8bb9d22"},
    {file = "humanize-3.2.0.tar.gz", hash = "sha256:ab69004895689951b79f2ae4fdd6b8127ff0c180aff107856d5d98119a33f026"},
]
importlib-metadata = [
    {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
    {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
]
iniconfig = [
    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},


@@ 877,6 930,14 @@ parso = [
    {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"},
    {file = "parso-0.8.1.tar.gz", hash = "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"},
]
pathlib3x = [
    {file = "pathlib3x-1.3.9-py3-none-any.whl", hash = "sha256:fb0048de6f50395c5389e41a6d7d81a8751abf61e17e5fce35c619880ea420ee"},
    {file = "pathlib3x-1.3.9-py3.6.egg", hash = "sha256:d3cc9dca94be46335d2717d642c96f15a2ffd6fd7cb699c0dedbcdd22f2fabd2"},
    {file = "pathlib3x-1.3.9-py3.7.egg", hash = "sha256:58e2afdd4672e709e03b4305e40f34c86807d5a7f69933be44242996e3458f0d"},
    {file = "pathlib3x-1.3.9-py3.8.egg", hash = "sha256:42e52a5e1b3c53c93c5a1e8c1eff573ef2f438e0fdba8149ae50967f8a5d5fe3"},
    {file = "pathlib3x-1.3.9-py3.9.egg", hash = "sha256:d4caf49f8e8b61ca3418a27bface39fcbc359d440698f02d0eb1e5b648ef101c"},
    {file = "pathlib3x-1.3.9.tar.gz", hash = "sha256:47b186e0d9085eafe05c6e0742d45970bf1a096038e664f33123babe614f05e2"},
]
pathspec = [
    {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
    {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},


@@ 1057,6 1118,10 @@ win32-setctime = [
    {file = "win32_setctime-1.0.3-py3-none-any.whl", hash = "sha256:dc925662de0a6eb987f0b01f599c01a8236cb8c62831c22d9cada09ad958243e"},
    {file = "win32_setctime-1.0.3.tar.gz", hash = "sha256:4e88556c32fdf47f64165a2180ba4552f8bb32c1103a2fafd05723a0bd42bd4b"},
]
zipp = [
    {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
    {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
]
zstandard = [
    {file = "zstandard-0.15.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:f12d97f388fc9bb238280641367f49612016d0353a99eff13b58588f60444263"},
    {file = "zstandard-0.15.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:72af53b92990ba6118d5dabe8a777cdd36d9fb88d9bf665b84517d0efdcc6f8f"},

M pyproject.toml => pyproject.toml +3 -4
@@ 1,6 1,6 @@
[tool.poetry]
name = "mutation"
version = "0.3.3"
version = "0.4.0"
description = "test mutation for pytest."
authors = ["Amirouche <amirouche@hyper.dev>"]
license = "MIT"


@@ 20,14 20,13 @@ pytest-xdist = "^2.2.0"
pytest-cov = "^2.11.1"
pytest-randomly = "^3.5.0"
humanize = "^3.2.0"
diff-highlight = "^1.2.0"
astunparse = "^1.6.3"
tqdm = "^4.56.0"
pytest = "^6.2.1"
pathlib3x = "^1.3.9"

[tool.poetry.dev-dependencies]
debug = "^0.3.2"
ipython = "^7.19.0"
pytest = "^6.2.1"
tbvaccine = "^0.3.1"
black = "^20.8b1"
isort = "^5.7.0"

M test.py => test.py +0 -1
@@ 3,6 3,5 @@ from foobar.ex import decrement


def test_foobar():
    print(sys.meta_path)
    x = decrement(10)
    assert 7 < x < 9