Repository: Gatewatcher/pytest-vulture (language: python)

Issue:
Add code



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index b6e4761..27eff59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+# IDE
+/.idea
diff --git a/DEVELOPMENT.rst b/DEVELOPMENT.rst
new file mode 100644
index 0000000..951fa80
--- /dev/null
+++ b/DEVELOPMENT.rst
@@ -0,0 +1,48 @@
+pytest vulture
+-------------
+
+How it works
+============
+
+At the start of pytest execution (pytest_sessionstart),
+it parses the configuration file if it exists (otherwise it takes the default values),
+and then run vulture.
+
+When a file is manages by pytest (pytest_collect_file), it looks for vulture outputs related to this file
+and add it to the pytest output of the file.
+Vulture outputs are ignored if they are an entrypoint or if they are ignored by the vulture configuration file.
+If no vulture outputs are found for this file, it is skipped.
+
+Development Environment Suggestion
+==================================
+
+Use `pyenv <https://github.com/pyenv/pyenv>`_, and install all the versions supported by the plugin.
+
+.. code-block:: shell
+
+    pyenv install 3.7.7
+
+    pyenv install 3.8.2
+
+    pyenv install 3.9.13
+
+    pyenv install 3.10.6
+
+
+Set the installed versions as global, that will allow tox to find all of them.
+
+.. code-block:: shell
+
+    pyenv global 3.10.6 3.9.13 3.8.2 3.7.7
+
+In your virtualenv, install tox and the tests dependencies, and run tox :
+
+.. code-block:: shell
+
+    pip install tox
+
+    pip install .[test]
+
+    tox
+
+The development environment is complete.
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index bf05e79..0000000
--- a/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-# pytest-vulture
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..40f4163
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,109 @@
+pytest vulture
+-------------
+
+Run vulture (https://pypi.org/project/vulture/) with pytest to find dead code.
+
+Sample Usage
+============
+.. code-block:: shell
+
+   py.test --vulture
+
+would be the most simple usage and would run vulture for all error messages.
+
+.. code-block:: shell
+
+   py.test --vulture --vulture-cfg-file=/test/vulture.ini
+
+This would use the vulture with the /test/vulture.ini config path
+
+Ignoring vulture messages in source code
+============
+
+- ignoring lines :
+
+.. code-block:: python
+
+    def test():
+        a = 2    # vulture: ignore
+
+- ignoring methods :
+
+.. code-block:: python
+
+    def test():  # vulture: ignore
+        pass
+
+- ignoring classes :
+
+.. code-block:: python
+
+    class Test:  # vulture: ignore
+        pass
+
+
+Config file
+============
+
+The config file (the path can be defined by the --vulture-cfg-file option) can look like this ::
+
+    [vulture]
+    # completely exclude files for vulture
+    exclude =
+        */test/* # We usualy exclude tests because tests can cover dead code
+
+    # those file are ignored by pytest, but still computed by vulture
+    ignore =
+        src/toto.py
+
+    # ignoring names in code
+    ignore-names =
+        delimiter
+
+    # ignoring decorators
+    ignore-decorators =
+      @application.errorhandler
+      @application.route
+      @celery_app.task
+      @application.app.errorhandler
+
+    # ignore vulture type of messages
+    ignore-types =
+        attribute
+        variable
+
+
+
+Acknowledgements
+================
+
+This code depends on
+`vulture <https://pypi.org/project/vulture>`__
+
+Development
+===========
+
+If you want to help development, there is overview documentation in DEVELOPMENT.rst.
+
+Issues
+===========
+
+If you encounter any problems, please file an issue along with a detailed description.
+
+Releases
+========
+
+2.0.0
+~~~~~~
+
+- Uses vulture with pytest (tested with python 3.7 3.8 and 3.9, with vulture==2.3 and pytest 7.x)
+
+1.0.0
+~~~~~~
+
+- stable Gatewatcher internal use only
+
+0.x
+~~~~~~
+
+- unstable Gatewatcher internal use only
\ No newline at end of file
diff --git a/coverage.ini b/coverage.ini
new file mode 100644
index 0000000..6091f9c
--- /dev/null
+++ b/coverage.ini
@@ -0,0 +1,17 @@
+[run]
+branch = true
+omit =
+    src/pytest_vulture/plugin.py
+    src/pytest_vulture/vulture/print_parser.py
+
+[report]
+precision = 2
+fail_under = 100
+show_missing = true
+skip_covered = true
+exclude_lines =
+    pragma: no cover
+    def __repr__
+    raise AssertionError
+    raise NotImplementedError
+    if __name__ == (?:'|")__main__(?:'|"):
diff --git a/pylint.ini b/pylint.ini
new file mode 100644
index 0000000..cd18a26
--- /dev/null
+++ b/pylint.ini
@@ -0,0 +1,175 @@
+[MASTER]
+jobs=0
+limit-inference-results=100
+persistent=yes
+suggestion-mode=yes
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+disable=
+    duplicate-code,
+    too-few-public-methods,
+
+
+[REPORTS]
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+output-format=text
+reports=yes
+score=yes
+
+
+[REFACTORING]
+max-nested-blocks=5
+never-returning-functions=sys.exit
+
+
+[BASIC]
+
+argument-naming-style=snake_case
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+attr-naming-style=snake_case
+attr-rgx=[a-z_][a-z0-9_]{2,50}$
+bad-names=foo,
+          bar,
+          foobar,
+          baz,
+          toto,
+          tutu,
+          tata,
+          plop,
+          lol,
+          ploplol,
+          pwet
+class-attribute-naming-style=any
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,50}|(__.*__))$
+class-naming-style=PascalCase
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+const-naming-style=UPPER_CASE
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|(_logger)|(app(lication)?))$
+docstring-min-length=20
+function-naming-style=snake_case
+function-rgx=[a-z_][a-z0-9_]{2,50}$
+good-names=i,
+           j,
+           k,
+           mo,
+           np,
+           pd,
+           fd,
+           fo,
+           db,
+           _
+
+include-naming-hint=no
+inlinevar-naming-style=any
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+method-naming-style=snake_case
+method-rgx=[a-z_][a-z0-9_]{2,50}$
+module-naming-style=snake_case
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+no-docstring-rgx=^_
+property-classes=abc.abstractproperty
+variable-naming-style=snake_case
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+
+[FORMAT]
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+indent-after-paren=4
+indent-string='    '
+max-line-length=120
+max-module-lines=1000
+no-space-check=trailing-comma,
+               dict-separator
+single-line-class-stmt=no
+single-line-if-stmt=no
+
+[SPELLING]
+
+max-spelling-suggestions=4
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+contextmanager-decorators=contextlib.contextmanager
+ignore-mixin-members=yes
+ignore-none=yes
+ignore-on-opaque-inference=yes
+ignored-classes=optparse.Values,thread._local,_thread._local
+missing-member-hint=yes
+missing-member-hint-distance=1
+missing-member-max-choices=1
+
+[LOGGING]
+logging-format-style=old
+logging-modules=logging
+
+
+[VARIABLES]
+allow-global-unused-variables=yes
+callbacks=cb_,
+          _cb
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+ignored-argument-names=_.*|^ignored_|^unused_
+init-import=no
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[STRING]
+check-str-concat-over-line-jumps=no
+
+
+[MISCELLANEOUS]
+notes=FAIL,TODO,FIXME
+
+
+[SIMILARITIES]
+ignore-comments=yes
+ignore-docstrings=yes
+ignore-imports=no
+min-similarity-lines=4
+
+
+[DEPRECATED_BUILTINS]
+bad-functions=map,
+              filter
+
+
+[DESIGN]
+
+max-args=5
+max-attributes=7
+max-bool-expr=5
+max-branches=12
+max-locals=15
+max-parents=7
+max-public-methods=20
+max-returns=6
+max-statements=50
+min-public-methods=2
+
+
+[IMPORTS]
+allow-wildcard-with-all=no
+analyse-fallback-blocks=no
+deprecated-modules=optparse,tkinter.tix
+known-third-party=enchant
+
+[CLASSES]
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+valid-classmethod-first-arg=cls
+valid-metaclass-classmethod-first-arg=cls
+
+
+[EXCEPTIONS]
+overgeneral-exceptions=BaseException,
+                       Exception
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..43ab2dd
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,40 @@
+[aliases]
+test = pytest
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
+[sdist]
+owner = root
+group = root
+formats = bztar, xztar
+
+[isort]
+known_first_party =
+not_skip =
+    __init__.py,
+multi_line_output = 3
+force_grid_wrap = 2
+combine_as_imports = true
+combine_star = true
+include_trailing_comma = true
+lines_after_imports = 2
+lines_between_types = 1
+
+[pycodestyle]
+; E501 refers to line too long errors, which is already handled by pylint
+ignore = E501
+
+[mypy]
+warn_unused_ignores = true
+strict_optional = true
+cache_dir = .cache/mypy
+incremental = true
+ignore_missing_imports = true
+check_untyped_defs = true
+show_none_errors = true
+warn_no_return = true
+warn_return_any = true
+no_implicit_optional = true
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c730922
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""pytest-vulture
+=============
+Plugin for py.test for doing vulture tests
+"""
+import os
+
+from setuptools import (
+    find_packages,
+    setup,
+)
+
+
+IS_TEST = bool(os.environ.get("IS_UNIT_TEST", 0))
+
+install_requires = [
+    'vulture <3.0, >2.0 ',
+]
+if not IS_TEST:
+    install_requires.append("pytest >= 7.0.0")
+
+test_requires = [
+    'pylint==2.14.5',
+    'pytest==7.1.2',
+    'pytest-runner==5.2',
+    'pytest-cov==2.10.1',
+    'pytest-pycodestyle==2.3.0',
+    'pytest-pylint==0.18.0',
+    'pytest-isort==3.0.0',
+    'pytest-mccabe==2.0',
+    'pytest-mypy==0.9.1',
+]
+
+dev_requires = [
+]
+
+# to prevent coverage bugs
+ENTRY_POINTS = {
+    "pytest11": [
+        "vulture = pytest_vulture.plugin",
+    ]
+} if not IS_TEST else {}
+
+setup(
+    name='pytest-vulture',
+    version='2.0.0',
+    include_package_data=True,
+    package_dir={
+        '': 'src',
+    },
+    packages=find_packages(
+        'src'
+    ),
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points=ENTRY_POINTS,
+    classifiers=["Framework :: Pytest"],
+)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..1000d13
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,34 @@
+[tox]
+envlist = py3{7,8,9,10}
+distdir = {toxinidir}/dist
+
+[pytest]
+testpaths = test/tests/ src/
+cache_dir = .cache
+mccabe-complexity =
+    test_*.py 20
+    *.py 10
+log_format =
+    %(filename)s:%(lineno)d: [%(name)s:%(levelname)s] %(asctime)s: %(message)s
+filterwarnings =
+    ignore:.+is deprecated:DeprecationWarning
+log_date_format = %Y-%m-%d %H:%M:%S
+addopts =
+    --cov-config coverage.ini
+    --cov-report term
+    --cov-report html:coverage/html
+    --cov src/
+    --cov test/tests/
+    --doctest-modules
+    --isort
+    --mccabe
+    --mypy
+    --pylint --pylint-rcfile pylint.ini
+    --disable-pytest-warnings
+    --verbose
+
+
+[testenv]
+deps = .[test]
+setenv = IS_UNIT_TEST = 1
+commands = {envpython} setup.py test --addopts "--basetemp {envtmpdir} --confcutdir .. --cache-clear {posargs}"


Test patch:
diff --git a/src/pytest_vulture/__init__.py b/src/pytest_vulture/__init__.py
new file mode 100644
index 0000000..7785fab
--- /dev/null
+++ b/src/pytest_vulture/__init__.py
@@ -0,0 +1,21 @@
+"""The vulture pytest plugin code source"""
+
+__all__ = ["VultureError"]
+
+
+class VultureError(Exception):
+    """Any pytest vulture exception"""
+    __message: str
+    _name = "vulture exception"
+
+    def __init__(self, message: str = "An error occurred"):
+        super().__init__(self)
+        self.__message = message
+
+    @property
+    def message(self) -> str:  # pragma: no cover
+        """Get the error message if it has one"""
+        return self.__message
+
+    def __str__(self):  # pragma: no cover
+        return f"{self._name} : {self.message}"
diff --git a/src/pytest_vulture/conf/__init__.py b/src/pytest_vulture/conf/__init__.py
new file mode 100644
index 0000000..b01327d
--- /dev/null
+++ b/src/pytest_vulture/conf/__init__.py
@@ -0,0 +1,1 @@
+"""Manages pytest vulture config (set with --vulture-cfg-file)"""
diff --git a/src/pytest_vulture/conf/base.py b/src/pytest_vulture/conf/base.py
new file mode 100644
index 0000000..cde3633
--- /dev/null
+++ b/src/pytest_vulture/conf/base.py
@@ -0,0 +1,32 @@
+"""A pytest vulture configuration"""
+import re
+
+from abc import (
+    ABC,
+    abstractmethod,
+)
+from configparser import ConfigParser
+from typing import List
+
+
+class Configuration(ABC):
+    """The abstract class of a pytest vulture configuration"""
+    _TRUE_VALUES = ["true", "True", "1", "yes"]
+
+    @abstractmethod
+    def read_ini(self, config: ConfigParser) -> bool:
+        """Read the ini file"""
+
+    @staticmethod
+    def _to_list(config_string: str) -> List[str]:
+        """Convert a string config element to a list
+           Examples::
+                >>> Configuration._to_list(" */test/*\\n */test_2/*")
+                ['*/test/*', '*/test_2/*']
+        """
+        list_elements = re.split('[,\n]', config_string)
+        return [element.lstrip().rstrip() for element in list_elements if element.lstrip().rstrip()]
+
+    @classmethod
+    def _to_bool(cls, config_string: str) -> bool:
+        return config_string in cls._TRUE_VALUES
diff --git a/src/pytest_vulture/conf/package.py b/src/pytest_vulture/conf/package.py
new file mode 100644
index 0000000..7b88a55
--- /dev/null
+++ b/src/pytest_vulture/conf/package.py
@@ -0,0 +1,44 @@
+"""The package configuration"""
+from configparser import ConfigParser
+from contextlib import suppress
+from pathlib import Path
+
+from pytest_vulture.conf.base import Configuration
+
+
+class PackageConfiguration(Configuration):
+    """The package configuration (setup.py options)
+    Examples::
+        >>> config_file = Path("/tmp/test.ini")
+        >>> config_file.write_text("[package]\\nsetup_path = setup.py\\ncheck_entry_points = true")
+        57
+        >>> config = ConfigParser()
+        >>> config.read(Path(config_file))
+        ['/tmp/test.ini']
+        >>> package_config = PackageConfiguration()
+        >>> package_config.read_ini(config)
+        >>> package_config.setup_path
+        PosixPath('setup.py')
+        >>> package_config.check_entry_points
+        True
+    """
+    _setup_path: Path = Path("setup.py")
+    _check_entry_points: bool = True
+    _NAME = "package"
+
+    @property
+    def setup_path(self) -> Path:
+        return self._setup_path
+
+    @property
+    def check_entry_points(self) -> bool:
+        return self._check_entry_points
+
+    def read_ini(self, config: ConfigParser):
+        """Read the ini file"""
+        with suppress(KeyError):
+            self._setup_path = Path(config[self._NAME]['setup_path'])
+        with suppress(KeyError):
+            self._check_entry_points = self._to_bool(
+                config[self._NAME]['check_entry_points']
+            )
diff --git a/src/pytest_vulture/conf/reader.py b/src/pytest_vulture/conf/reader.py
new file mode 100644
index 0000000..55c7601
--- /dev/null
+++ b/src/pytest_vulture/conf/reader.py
@@ -0,0 +1,45 @@
+"""The full configuration manager"""
+from configparser import ConfigParser
+from pathlib import Path
+
+from pytest_vulture.conf.package import PackageConfiguration
+from pytest_vulture.conf.vulture import VultureConfiguration
+
+
+class IniReader:
+    """The full configuration parser
+     Examples::
+        >>> config_file = Path("/tmp/test.ini")
+        >>> config_file.write_text("[package]\\nsetup_path = custom.py\\n[vulture]\\nignore-names = test")
+        62
+        >>> package_config = IniReader(Path("/tmp/test.ini"))
+        >>> package_config.read_ini()
+        >>> package_config.package_configuration.setup_path
+        PosixPath('custom.py')
+        >>> package_config.vulture_configuration.ignore_names
+        ['test']
+    """
+    _vulture_configuration: VultureConfiguration
+    _package_configuration: PackageConfiguration
+    _path_ini: Path
+
+    def __init__(self, path_ini: Path = Path("vulture.ini")):
+        self._path_ini = path_ini
+        self._vulture_configuration = VultureConfiguration()
+        self._package_configuration = PackageConfiguration()
+
+    @property
+    def package_configuration(self) -> PackageConfiguration:
+        return self._package_configuration
+
+    @property
+    def vulture_configuration(self) -> VultureConfiguration:
+        return self._vulture_configuration
+
+    def read_ini(self):
+        """Read and parse the ini file"""
+        if self._path_ini.is_file():
+            config = ConfigParser()
+            config.read(self._path_ini)
+            self._vulture_configuration.read_ini(config)
+            self._package_configuration.read_ini(config)
diff --git a/src/pytest_vulture/conf/vulture.py b/src/pytest_vulture/conf/vulture.py
new file mode 100644
index 0000000..ac6174f
--- /dev/null
+++ b/src/pytest_vulture/conf/vulture.py
@@ -0,0 +1,88 @@
+"""The client pytest vulture configuration"""
+from configparser import ConfigParser
+from contextlib import suppress
+from typing import List
+
+from pytest_vulture.conf.base import Configuration
+
+
+class VultureConfiguration(Configuration):
+    """The vulture configuration (setup.py options)
+    Examples::
+        >>> from pathlib import Path
+        >>> config_file = Path("/tmp/test.ini")
+        >>> config_file.write_text("[vulture]\\nignore-decorators = @test")
+        35
+        >>> config = ConfigParser()
+        >>> config.read(Path(config_file))
+        ['/tmp/test.ini']
+        >>> package_config = VultureConfiguration()
+        >>> package_config.read_ini(config)
+        >>> package_config.ignore_decorators
+        ['@test']
+    """
+    EXCLUDE_DEFAULT: List[str] = [
+        "*/.tox/*", "*/.git/*", "*/.cache/*", "*/.idea/*", "*/.eggs/*"
+    ]
+    _exclude: List[str] = EXCLUDE_DEFAULT
+    _CONFIG_NAME: str = "vulture"
+    _ignore_names: List[str]
+    _ignore_decorators: List[str]
+    _ignore: List[str]
+    _ignore_types: List[str]
+
+    def __init__(self):
+        super().__init__()
+        self._ignore_names: List[str] = []
+        self._ignore_decorators: List[str] = []
+        self._ignore: List[str] = []
+        self._ignore_types: List[str] = []
+
+    @property
+    def ignore_types(self) -> List[str]:
+        return self._ignore_types
+
+    @property
+    def ignore(self) -> List[str]:
+        return self._ignore
+
+    @property
+    def ignore_decorators(self) -> List[str]:
+        return self._ignore_decorators
+
+    @property
+    def ignore_names(self) -> List[str]:
+        return self._ignore_names
+
+    @property
+    def exclude(self) -> List[str]:
+        return self._exclude
+
+    def read_ini(self, config: ConfigParser):
+        """Read the ini file"""
+        with suppress(KeyError):
+            self._exclude = self.EXCLUDE_DEFAULT + self._to_list(
+                config[self._CONFIG_NAME]['exclude']
+            )
+
+        self.__read_ignore(config)
+
+    def __read_ignore(self, config: ConfigParser):
+        with suppress(KeyError):
+            self._ignore_names = self._to_list(
+                config[self._CONFIG_NAME]['ignore-names']
+            )
+        with suppress(KeyError):
+            self._ignore_decorators = self._to_list(
+                config[self._CONFIG_NAME]['ignore-decorators']
+            )
+
+        with suppress(KeyError):
+            self._ignore = self._to_list(
+                config[self._CONFIG_NAME]['ignore']
+            )
+
+        with suppress(KeyError):
+            self._ignore_types = self._to_list(
+                config[self._CONFIG_NAME]['ignore-types']
+            )
diff --git a/src/pytest_vulture/plugin.py b/src/pytest_vulture/plugin.py
new file mode 100644
index 0000000..55496ac
--- /dev/null
+++ b/src/pytest_vulture/plugin.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+"""Pytest vulture plugins"""
+# pylint: disable=subclassed-final-class
+from pathlib import Path
+
+import pytest
+
+from _pytest.main import Session
+
+from pytest_vulture import VultureError
+from pytest_vulture.conf.reader import IniReader
+from pytest_vulture.vulture.manager import VultureManager
+
+
+def pytest_addoption(parser):
+    """Add all the pytest vulture command line options"""
+    group = parser.getgroup("general")
+    # Set the --vulture option in the setup.py
+    group.addoption(
+        '--vulture', action='store_true', help="run vulture"
+    )
+    group.addoption(
+        '--vulture-cfg-file', help="Defines the vulture config file path",
+        default="tox.ini",
+    )
+
+
+def pytest_sessionstart(session):
+    """Call vulture only at the start, before entering the run test loop."""
+    config = session.config
+    if config.option.vulture:
+        root_dir = session.startdir
+        reader = IniReader(path_ini=Path(config.option.vulture_cfg_file))
+        reader.read_ini()
+        session.vulture = VultureManager(root_dir, reader)
+        session.vulture.call()
+
+
+def pytest_collect_file(file_path, parent):
+    """Search and apply the vulture result (run at the session start) on the python file"""
+    config = parent.config
+    if config.option.vulture and file_path.suffix == '.py':
+        return VulturePinningFile.from_parent(parent=parent, path=file_path)
+    return None
+
+
+class _VultureSession(Session):  # type: ignore
+    vulture: VultureManager
+
+
+class VultureItem(pytest.Item):
+    """vulture pytest running class."""
+    session: _VultureSession
+
+    def runtest(self):
+        """Raise an error if a vulture message occurred in the tested file."""
+        error_message = self.session.vulture.get_file_errors(self.fspath)
+        if error_message:
+            raise VultureError(message=error_message)
+
+    def repr_failure(self, excinfo, *args, **kwargs):  # pylint: disable=signature-differs
+        """If VultureError is raised, it means that a vulture message occurred in the tested file."""
+        if excinfo.errisinstance(VultureError):
+            return excinfo.value.message
+        return super().repr_failure(excinfo, *args, **kwargs)
+
+
+class VulturePinningFile(pytest.File):
+    """File that is tested by pytest"""
+
+    def collect(self):
+        yield VultureItem.from_parent(parent=self, name="vulture")
diff --git a/src/pytest_vulture/setup_manager.py b/src/pytest_vulture/setup_manager.py
new file mode 100644
index 0000000..3bfb4f7
--- /dev/null
+++ b/src/pytest_vulture/setup_manager.py
@@ -0,0 +1,136 @@
+"""Manages the setup.py to get the entrypoint (and ignore then for vulture)"""
+# pylint: disable=eval-used
+import re
+
+from contextlib import suppress
+from pathlib import Path
+from typing import (
+    List,
+    Tuple,
+)
+
+from pytest_vulture import VultureError
+from pytest_vulture.conf.reader import IniReader
+from pytest_vulture.vulture.output_line import VultureOutputLine
+
+
+class EntryPointFileError(VultureError):
+    _name = "an entry point file is missing in the setup.py file"
+
+
+class EntryPointFunctionError(VultureError):
+    _name = "an entry point function is missing in the setup.py file"
+
+
+class SetupManager:
+    """The setup.py parser"""
+    _entry_points: List[str]
+    _config: IniReader
+
+    def __init__(self, config: IniReader):
+        self._entry_points = []
+        self._config = config
+        try:
+            content = self._config.package_configuration.setup_path.read_text("utf-8").replace("\n", "")
+        except (FileNotFoundError, UnicodeDecodeError):
+            return
+        self.__generate_entry_points(content)
+
+    def is_entry_point(self, vulture: VultureOutputLine) -> bool:
+        """Check if the vulture output is an entry point
+        Examples::
+            >>> config_file = Path("/tmp/test.ini")
+            >>> config_file.write_text("[package]\\nsetup_path = /tmp/setup.py")
+            36
+            >>> ini = IniReader(config_file)
+            >>> ini.read_ini()
+            >>> Path("/tmp/setup.py").write_text("entry_points={'console_scripts': ('test=test:main', )}")
+            54
+            >>> Path("/tmp/test.py").write_text("def main():pass")
+            15
+            >>> finder = SetupManager(ini)
+            >>> finder.is_entry_point(VultureOutputLine("toto.py:1: unused function 'test' (60% confidence)"))
+            False
+            >>> finder.is_entry_point(VultureOutputLine("test.py:1: unused function 'main' (60% confidence)"))
+            True
+        """
+        for entry_point in self._entry_points:
+            if entry_point.replace(".__init__", "") == vulture.python_path.replace(".__init__", ""):
+                return True
+        return False
+
+    def __generate_entry_points(self, content: str):
+        """Parse the setup.pu file to get the entry points"""
+        root_paths = self.__generate_root_paths(content)
+        entry_points = {}
+        find = re.findall("(?=entry_points={).*(?<=})", content.replace("\n", ""))
+        if find:
+            element = find[0]
+            element = element[:element.find("}") + 1]
+            with suppress(SyntaxError):
+                entry_points = eval(element.replace("entry_points=", ""))
+        for values in entry_points.values():
+            for equality in values:
+                self.__parse_entry_point_line(equality, root_paths)
+
+    def __parse_entry_point_line(self, equality: str, root_paths: List[Tuple[str, str]]):
+        """Parse an entry point value to get the entry point path"""
+        equality_split = equality.split("=")
+        if len(equality_split) != 2:
+            value = equality
+        else:
+            value = equality_split[1]
+        if not root_paths:
+            self._entry_points.append(value)
+        else:
+            for target, destination in root_paths:
+                if len(value) > len(target) and value[:len(target)] == target:
+                    self._entry_points.append(
+                        destination + "." + value[len(target):]
+                    )
+                else:  # pragma: no cover
+                    self._entry_points.append(value)
+        self.__check_entry_points()
+
+    def __check_entry_points(self):
+        """Checks if the entry points exists"""
+        if not self._config.package_configuration.check_entry_points:
+            return
+        for entry_points in self._entry_points:
+            try:
+                split_points = entry_points.split(":")
+                function_name = split_points[1]
+                path_dots = split_points[0]
+            except IndexError:
+                continue
+            dir_path = self._config.package_configuration.setup_path.absolute().parent
+            new_path = Path(path_dots.replace(".", "/"))
+            if (dir_path / new_path).is_dir():  # pragma: no cover
+                path = (dir_path / new_path / "__init__.py").absolute()
+            else:
+                path = (dir_path / (str(new_path) + ".py")).absolute()
+            if not path.exists():
+                raise EntryPointFileError(
+                    message=f"{path.as_posix()} can not be found."
+                )
+            with path.open() as file:
+                content = file.read()
+                if f"def{function_name}(" not in content.replace(" ", ""):
+                    raise EntryPointFunctionError(
+                        message=f"{entry_points} can not be found."
+                    )
+
+    @staticmethod
+    def __generate_root_paths(content: str) -> List[Tuple[str, str]]:
+        """Parse the setup.py to get the source root paths"""
+        root_paths: List[Tuple[str, str]] = []
+        package_dir = {}
+        find = re.findall("(?=package_dir={).*(?<=})", content.replace("\n", ""))
+        if find:
+            element = find[0]
+            element = element[:element.find("}") + 1]
+            with suppress(SyntaxError):
+                package_dir = eval(element.replace("package_dir=", ""))
+        for key, value in package_dir.items():
+            root_paths.append((key, value))
+        return root_paths
diff --git a/src/pytest_vulture/vulture/__init__.py b/src/pytest_vulture/vulture/__init__.py
new file mode 100644
index 0000000..d7d84b2
--- /dev/null
+++ b/src/pytest_vulture/vulture/__init__.py
@@ -0,0 +1,1 @@
+"""Manages vulture"""
diff --git a/src/pytest_vulture/vulture/comment.py b/src/pytest_vulture/vulture/comment.py
new file mode 100644
index 0000000..96d4542
--- /dev/null
+++ b/src/pytest_vulture/vulture/comment.py
@@ -0,0 +1,91 @@
+"""Manages vulture: ignore in source code"""
+import ast
+
+from contextlib import suppress
+from pathlib import Path
+from typing import List
+
+from pytest_vulture.vulture.output_line import VultureOutputLine
+
+
+class CommentFinder:
+    """Parse python code to find vulture: ignore comments"""
+    _tree = None
+    _content: str = ""
+    _ignored_lines: List[int]
+    _path: Path
+    _VULTURE_IGNORE = "vulture: ignore"
+
+    def __init__(self):
+        self._path = Path("")
+        self._ignored_lines = []
+
+    def check_comment(self, vulture: VultureOutputLine) -> bool:
+        """Check if the vulture output line is ignored with a # vulture: ignore comment
+         Examples::
+            >>> Path("/tmp/test.py").write_text("def test():pass")
+            15
+            >>> finder = CommentFinder()
+            >>> finder.check_comment(VultureOutputLine('/tmp/test.py:1: unused function test (60% confidence)'))
+            False
+            >>> Path("/tmp/test.py").write_text('def test():  # vulture: ignore\\n     pass')
+            40
+            >>> finder = CommentFinder()  # the file has changed, must recreate the instance
+            >>> finder.check_comment(VultureOutputLine('/tmp/test.py:1: unused function test (60% confidence)'))
+            True
+            >>> finder.check_comment(VultureOutputLine('/tmp/test.py:3: unused function test (60% confidence)'))
+            False
+        """
+        line_number = vulture.line_number
+        if line_number is None:  # pragma: no cover
+            return False
+        # Check if is the same file as before, if not, reload
+        if vulture.path.as_posix() != self._path.as_posix():
+            self.__reset(vulture.path)
+        return self.__find_rec(vulture)
+
+    def __find_rec(self, vulture: VultureOutputLine, tree=None, ignore_mode=False) -> bool:
+        """Find comments recursively"""
+        if tree is None:
+            tree = self._tree
+        with suppress(AttributeError):
+            line_nb = tree.lineno
+            if not ignore_mode:
+                if line_nb in self._ignored_lines:
+                    ignore_mode = True
+            if ignore_mode:
+                if vulture.line_number in [line_nb, line_nb - self.__get_decorators(tree)]:
+                    return True
+        with suppress(AttributeError):
+            for new_tree in tree.body:
+                if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):
+                    return True
+        with suppress(AttributeError):
+            for new_tree in tree.orelse:
+                if self.__find_rec(vulture, tree=new_tree, ignore_mode=ignore_mode):
+                    return True
+        return False
+
+    @staticmethod
+    def __get_decorators(tree) -> int:
+        try:
+            return len(tree.decorator_list)
+        except AttributeError:
+            return 0
+
+    def __reset(self, path: Path):
+        self._path = path
+        self._ignored_lines = []
+        try:
+            self._content = self._path.read_text(encoding="utf-8")
+            self._tree = ast.parse(self._content)
+            content_split = self._content.split("\n")
+            index_line = 0
+            for elem in content_split:
+                if self._VULTURE_IGNORE in elem:
+                    self._ignored_lines.append(index_line + 1)
+                index_line += 1
+
+        except OSError as err:  # pragma: no cover
+            print(f"warning : unable to read : {self._path.as_posix()} : {err}")
+            self._content = ""
diff --git a/src/pytest_vulture/vulture/manager.py b/src/pytest_vulture/vulture/manager.py
new file mode 100644
index 0000000..de9de96
--- /dev/null
+++ b/src/pytest_vulture/vulture/manager.py
@@ -0,0 +1,110 @@
+"""Manages vulture"""
+import re
+
+from pathlib import Path
+from typing import (
+    List,
+    Optional,
+)
+
+from vulture import Vulture
+
+from pytest_vulture.conf.reader import IniReader
+from pytest_vulture.setup_manager import SetupManager
+from pytest_vulture.vulture.comment import CommentFinder
+from pytest_vulture.vulture.output_line import VultureOutputLine
+from pytest_vulture.vulture.print_parser import PrintParser
+
+
+class VultureManager:
+    """The vulture manager for pytest"""
+    _results: List[VultureOutputLine]
+    _config: IniReader
+    _setup_manager: SetupManager
+    _COMMENT_FINDER: CommentFinder = CommentFinder()
+
+    def __init__(self, root_dir, config: IniReader):
+        """Type of root_dir can be py._path.local.LocalPath. It depends on the version of pytest."""
+        self.__root_dir = root_dir
+        self._config = config
+        self._results = []
+        self.__setup_manager = SetupManager(self._config)
+
+    def call(self):
+        """Call vulture on a folder
+        Example::
+            >>> import py
+            >>> from py._path.local import LocalPath
+            >>> config = IniReader(path_ini=Path("vulture.ini"))
+            >>> config.read_ini()
+            >>> manager = VultureManager(LocalPath("/tmp"), config)
+            >>> manager.call()
+        """
+        vulture_configuration = self._config.vulture_configuration
+        vulture = Vulture(
+            ignore_names=vulture_configuration.ignore_names,
+            ignore_decorators=vulture_configuration.ignore_decorators
+        )
+        vulture.scavenge([str(self.__root_dir)], exclude=vulture_configuration.exclude)
+        # Hacks to retrieve printed vulture output. A better solution will be appreciated
+        log = PrintParser()
+        log.start()
+        vulture.report()
+        log.stop()
+        results = [VultureOutputLine(elem) for elem in log.messages.split("\n") if elem]
+        self._results = [elem for elem in results if not self.__filter_results(elem)]
+
+    def get_file_errors(self, path) -> Optional[str]:
+        """Get file errors
+        Example::
+            >>> import py
+            >>> from pathlib import Path
+            >>> from py._path.local import LocalPath
+            >>> Path("/tmp/package").mkdir(exist_ok=True)
+            >>> Path("/tmp/package/test.py").write_text("def test():pass")
+            15
+            >>> config = IniReader(path_ini=Path("vulture.ini"))
+            >>> config.read_ini()
+            >>> manager = VultureManager(LocalPath("/tmp/package"), config)
+            >>> manager.call()
+            >>> manager.get_file_errors(LocalPath("/tmp/package/test.py"))
+            "line 1 :  unused function 'test' (60% confidence)"
+        """
+        errors = []
+        for error_vulture in self._results:
+            if self.__path_equals(path, error_vulture.path.as_posix(), str(self.__root_dir)):
+                errors.append(error_vulture.message)
+        if not errors:
+            return None
+        return "\n".join(errors)
+
+    def __filter_results(self, vulture_output: VultureOutputLine) -> bool:
+        """Check if the vulture output concerns an ignore path, an entry point or a # vulture: ignore"""
+        for ignored_path in self._config.vulture_configuration.ignore:
+            if self.__check_path(ignored_path, vulture_output.path):
+                return True
+        return self.__setup_manager.is_entry_point(
+            vulture_output
+        ) or self.__check_ignore_type(
+            vulture_output
+        ) or self._COMMENT_FINDER.check_comment(
+            vulture_output
+        )
+
+    def __check_path(self, path_exclude: str, path_to_check: Path) -> bool:
+        match = re.compile(path_exclude.replace("*", ".*")).match(path_to_check.as_posix())
+        if not match:
+            return self.__path_equals(
+                path_exclude, path_to_check.absolute().as_posix(), str(self.__root_dir)
+            )
+        return match.group() == path_to_check.as_posix()
+
+    def __check_ignore_type(self, vulture_output: VultureOutputLine):
+        return vulture_output.type in self._config.vulture_configuration.ignore_types
+
+    @classmethod
+    def __path_equals(cls, path_1, path_2, root_dir: str) -> bool:
+        """Manages tox and pytest weird path usages, to simplify"""
+        path_1_full = (Path(root_dir) / Path(str(path_1))).as_posix()
+        path_2_full = (Path(root_dir) / Path(str(path_2))).as_posix()
+        return path_1_full == path_2_full
diff --git a/src/pytest_vulture/vulture/output_line.py b/src/pytest_vulture/vulture/output_line.py
new file mode 100644
index 0000000..baf6922
--- /dev/null
+++ b/src/pytest_vulture/vulture/output_line.py
@@ -0,0 +1,107 @@
+"""Manages vulture output lines"""
+import re
+
+from pathlib import Path
+from typing import Optional
+
+
+class VultureOutputLine:
+    """A vulture output line
+        Examples::
+            >>> manager = VultureOutputLine("src/test.py:2: unused function 'test' (60% confidence)")
+            >>> manager.line_number
+            2
+            >>> manager.message
+            "line 2 :  unused function 'test' (60% confidence)"
+            >>> manager.path
+            PosixPath('src/test.py')
+            >>> manager.python_path
+            'src.test:test'
+            >>> manager.type
+            'function'
+    """
+    _message: str
+    _type: str = ""
+    _path: Path
+    _line_number: int = -1
+    _UNUSED_FUNCTION_MESSAGE = "unused function"
+    _python_path: str = ""
+
+    def __init__(self, line: str):
+        splitter = line.split(":")
+        self._path = Path(splitter[0])
+        self._message = splitter[-1]
+        try:
+            self._line_number = int(splitter[1])
+        except (ValueError, IndexError):
+            self._line_number = -1
+
+    @property
+    def type(self) -> str:
+        """get the type : function, attribute, property ...
+            Examples::
+                >>> VultureOutputLine("src/test.py:2: unused function 'test' (60% confidence)").type
+                'function'
+        """
+        if not self._type:
+            elements = re.findall("unused [a-z]*", self._message)
+            if elements:
+                self._type = elements[0].replace("unused ", "")
+        return self._type
+
+    @property
+    def line_number(self) -> Optional[int]:
+        """Get the vulture message line number
+            Examples::
+                >>> VultureOutputLine("src/test.py:5: unused function 'test' (60% confidence)").line_number
+                5
+        """
+        if self._line_number == -1:
+            return None
+        return self._line_number
+
+    @property
+    def message(self) -> str:
+        """Get the vulture message
+             Examples::
+                >>> VultureOutputLine("src/test.py:5: unused function 'test' (60% confidence)").message
+                "line 5 :  unused function 'test' (60% confidence)"
+        """
+        if self._line_number == -1:
+            return self._message
+        return f"line {self._line_number} : {self._message}"
+
+    @property
+    def path(self) -> Path:
+        """Get file path of the vulture message
+             Examples::
+                >>> VultureOutputLine("src/test.py:5: unused function 'test' (60% confidence)").path
+                PosixPath('src/test.py')
+        """
+        return self._path
+
+    @property
+    def python_path(self) -> str:
+        """Get the python path of the vulture message :
+            Examples::
+                >>> VultureOutputLine("src/test.py:5: unused function 'test' (60% confidence)").python_path
+                'src.test:test'
+        """
+        if not self._python_path:
+            try:
+                relative_path = self.path.relative_to(Path("").absolute())
+            except ValueError:
+                relative_path = self.path
+
+            self._python_path = relative_path.as_posix().replace("/", ".").replace(".py", "")
+            dots_message = f"{self._UNUSED_FUNCTION_MESSAGE} '"
+            find = re.findall(
+                f"(?={dots_message}).*(?<=')", self._message
+            )
+            if find:
+                function_name = find[0].replace(dots_message, "")
+                self._python_path += ":" + function_name[:-1]
+        return self._python_path
+
+    def __str__(self):  # pragma: no cover
+        return self.message
diff --git a/src/pytest_vulture/vulture/print_parser.py b/src/pytest_vulture/vulture/print_parser.py
new file mode 100644
index 0000000..afa0dbd
--- /dev/null
+++ b/src/pytest_vulture/vulture/print_parser.py
@@ -0,0 +1,48 @@
+"""Manages what functions prints"""
+# mypy: ignore-errors
+import sys
+
+from typing import List
+
+
+class PrintParser:
+    """A class that get the printing of methods
+        Example::
+            >>> log = PrintParser()
+            >>> log.start()
+            >>> print("toto")
+            >>> print("tata")
+            >>> log.stop()
+            >>> print("tutu")
+            tutu
+            >>> log.messages
+            'toto\\ntata\\n'
+
+    """
+    stdout = sys.stdout
+
+    _messages: List[str] = []
+    message = ""
+
+    def start(self):
+        """Starting to save the prints and not show them in the console"""
+        self.stdout = sys.stdout
+        sys.stdout = self
+        self._messages = []
+
+    def stop(self):
+        """Stop saving the prints"""
+        sys.stdout = self.stdout
+
+    def getvalue(self):
+        """hacks"""
+        return self.message
+
+    def write(self, text):
+        """Does not write the text in the console, but saves it"""
+        self._messages.append(text)
+
+    @property
+    def messages(self) -> str:
+        """Get the printed outputs in a string"""
+        return "".join(self._messages)
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..109fa9c
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,5 @@
+"""The pytest plugin tests"""
+import os
+
+
+TEST_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
diff --git a/test/examples/ini/full.ini b/test/examples/ini/full.ini
new file mode 100644
index 0000000..6202206
--- /dev/null
+++ b/test/examples/ini/full.ini
@@ -0,0 +1,21 @@
+[vulture]
+# test
+exclude =
+    */test/*
+    */test_2/*, */test_3/*
+
+ignore =
+    src/main.py
+
+ignore-names =
+    clean_
+
+ignore-decorators =
+    @app.route
+
+ignore-types =
+    attribute
+
+[package]
+setup_path = src/setup.py
+check_entry_points = False
diff --git a/test/examples/ini/mini.ini b/test/examples/ini/mini.ini
new file mode 100644
index 0000000..e69de29
diff --git a/test/examples/setups/setup b/test/examples/setups/setup
new file mode 100644
index 0000000..11aadd9
--- /dev/null
+++ b/test/examples/setups/setup
@@ -0,0 +1,47 @@
+"""
+    Setup the app
+"""
+from setuptools import setup
+
+
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points={
+        'console_scripts': (
+            'test=test.main:main',
+            'test_1=test.main:main_2',
+            'test_3',
+        ),
+        "pytest11": ["test = test.pytest.test"]
+
+    },
+    classifiers=["Framework :: Pytest"],
+
+)
diff --git a/test/examples/setups/setup_broken b/test/examples/setups/setup_broken
new file mode 100644
index 0000000..41643a1
--- /dev/null
+++ b/test/examples/setups/setup_broken
@@ -0,0 +1,45 @@
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    package_dir={
+        '': 'src',
+    },
+    packages=find_packages(
+        'src'
+    ),
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points={
+        'console_scripts': (
+            'test=te
+        ),
+        "pytest11": ["tes
+
+    },
+    classifiers=["Framework :: Pytest"],
+
+)
diff --git a/test/examples/setups/setup_entry_p_not_found b/test/examples/setups/setup_entry_p_not_found
new file mode 100644
index 0000000..4bcc5e4
--- /dev/null
+++ b/test/examples/setups/setup_entry_p_not_found
@@ -0,0 +1,42 @@
+"""
+    Setup the app
+"""
+from setuptools import setup
+
+
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points={
+        'console_scripts': (
+            'test=test.main:main2',
+        )
+    }
+
+)
diff --git a/test/examples/setups/setup_file_not_found b/test/examples/setups/setup_file_not_found
new file mode 100644
index 0000000..277da9e
--- /dev/null
+++ b/test/examples/setups/setup_file_not_found
@@ -0,0 +1,42 @@
+"""
+    Setup the app
+"""
+from setuptools import setup
+
+
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points={
+        'console_scripts': (
+            'test=test2.main:main',
+        )
+    }
+
+)
diff --git a/test/examples/setups/setup_no_entry_point b/test/examples/setups/setup_no_entry_point
new file mode 100644
index 0000000..050efe1
--- /dev/null
+++ b/test/examples/setups/setup_no_entry_point
@@ -0,0 +1,38 @@
+"""
+    Setup the app
+"""
+from setuptools import setup
+
+
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    classifiers=["Framework :: Pytest"],
+
+)
diff --git a/test/examples/setups/setup_with_entry_point b/test/examples/setups/setup_with_entry_point
new file mode 100644
index 0000000..ad30a1e
--- /dev/null
+++ b/test/examples/setups/setup_with_entry_point
@@ -0,0 +1,51 @@
+"""
+    Setup the app
+"""
+from setuptools import (
+    find_packages,
+    setup,
+)
+
+
+install_requires = [
+    'vulture>=1.0',
+]
+
+test_requires = [
+    'pytest >= 3.2.2',
+    'pytest-cov >= 2.5.1',
+    'pytest-codestyle',
+    'pytest-runner',
+    'pytest-pylint >= 0.7.1',
+    'pytest-isort >= 0.3.1',
+    'pytest-mccabe >= 0.1',
+    'pytest-mypy >= 0.4',
+    # 'pytest-mock >= 1.6.3',
+    # 'pytest-timeout >= 1.2.0',
+]
+
+dev_requires = [
+]
+
+setup(
+    version='0.0.2',
+    include_package_data=True,
+    package_dir={
+        '': 'src',
+    },
+    packages=find_packages(
+        'src'
+    ),
+    install_requires=install_requires,
+    tests_require=test_requires,
+    extras_require={
+        'test': test_requires,
+        'dev': test_requires + dev_requires,
+    },
+    entry_points={
+        'console_scripts': (
+            'test=test.main:main',
+            'test_1=test.main:main_2',
+        )
+    }
+)
diff --git a/test/examples/setups/test/main.py b/test/examples/setups/test/main.py
new file mode 100644
index 0000000..ee19ee6
--- /dev/null
+++ b/test/examples/setups/test/main.py
@@ -0,0 +1,15 @@
+"""
+    tests
+"""
+
+
+def main():
+    """
+        main
+    """
+
+
+def main_2():
+    """
+        main
+    """
\ No newline at end of file
diff --git a/test/examples/vulture_tests/bug_18_2020/main.py b/test/examples/vulture_tests/bug_18_2020/main.py
new file mode 100644
index 0000000..86897b2
--- /dev/null
+++ b/test/examples/vulture_tests/bug_18_2020/main.py
@@ -0,0 +1,34 @@
+"""
+    a bug < may 2020
+"""
+from pathlib import Path
+
+
+CONFIG_PATH = Path("test")
+
+
+def get_gweb() -> models.Test:  # vulture: ignore
+    """Getting SSL details about certs, CA, CRL.
+    """
+    ssl_infos = models.Test()
+    config = Test().json
+    ssl_infos.enabled = config["config"]["services"]
+    imported_cert = CONFIG_PATH / 'nginx'
+    imported_key = CONFIG_PATH / 'nginx'
+
+    if ssl_infos.valid_key == ssl_infos.cert:
+        ssl_infos.valid_key = 40
+    else:
+        ssl_infos.valid_key = 40
+
+    if imported_cert.exists() and imported_key.exists():
+        ssl_infos.imported_cert = 3 + 9
+        ssl_infos.valid_key = 10 - 2
+        ssl_infos.is_custom_cert = True
+    else:
+        ssl_infos.valid_key = 40
+        ssl_infos.is_custom_cert = False
+    ssl_infos.ca = 60
+    ssl_infos.crl = 70
+    ssl_infos.cert = 77
+    return ssl_infos
diff --git a/test/examples/vulture_tests/comments/main.py b/test/examples/vulture_tests/comments/main.py
new file mode 100644
index 0000000..7481cb9
--- /dev/null
+++ b/test/examples/vulture_tests/comments/main.py
@@ -0,0 +1,43 @@
+"""
+    the main function
+"""
+
+
+def main():  # vulture: ignore
+    """
+        the main
+        function
+    """
+    return sub_main()
+
+
+def sub_main():
+    """
+        pass
+    """
+    return 2
+
+
+@property  # vulture: ignore
+def other_main():  # vulture: ignore
+    return sub_main()
+
+
+@app.route("test")  # vulture: ignore
+@app.my_route  # test
+def test_other_main():  # vulture: ignore
+
+    @my_app.route("test")  # vulture: ignore
+    @my_app.test_route  # test
+    def test():  # vulture: ignore
+        pass
+    return sub_main()
+
+
+class Enum:  # vulture: ignore
+    """
+        Tests enum
+    """
+    A = 1
+    B = 1
+    C = 1
diff --git a/test/examples/vulture_tests/easy/main.py b/test/examples/vulture_tests/easy/main.py
new file mode 100644
index 0000000..90f552c
--- /dev/null
+++ b/test/examples/vulture_tests/easy/main.py
@@ -0,0 +1,21 @@
+"""
+    the main function
+"""
+from .other import other
+
+
+@property
+def main():
+    """
+        the main
+        function
+    """
+    other()
+    return sub_main()
+
+
+def sub_main():
+    """
+        pass
+    """
+    return 2
diff --git a/test/examples/vulture_tests/easy/other.py b/test/examples/vulture_tests/easy/other.py
new file mode 100644
index 0000000..b7eadda
--- /dev/null
+++ b/test/examples/vulture_tests/easy/other.py
@@ -0,0 +1,9 @@
+"""
+    the main function
+"""
+
+
+def other():
+    """
+        pass
+    """
diff --git a/test/tests/__init__.py b/test/tests/__init__.py
new file mode 100644
index 0000000..87485d0
--- /dev/null
+++ b/test/tests/__init__.py
@@ -0,0 +1,1 @@
+"""All the tests"""
diff --git a/test/tests/conftest.py b/test/tests/conftest.py
new file mode 100644
index 0000000..918c870
--- /dev/null
+++ b/test/tests/conftest.py
@@ -0,0 +1,17 @@
+"""The conftest"""
+from pathlib import Path
+from shutil import copytree
+from test import TEST_BASE_DIR
+
+import pytest
+
+
+@pytest.fixture
+def examples_path(tmp_path):
+    """Get copies of the example"""
+    mock_dir = Path(TEST_BASE_DIR + "/test/examples")
+    copytree(
+        mock_dir,
+        tmp_path / "examples"
+    )
+    return tmp_path / "examples"
diff --git a/test/tests/functional/__init__.py b/test/tests/functional/__init__.py
new file mode 100644
index 0000000..70ba74d
--- /dev/null
+++ b/test/tests/functional/__init__.py
@@ -0,0 +1,1 @@
+"""the functional tests"""
diff --git a/test/tests/functional/test_call.py b/test/tests/functional/test_call.py
new file mode 100644
index 0000000..807694c
--- /dev/null
+++ b/test/tests/functional/test_call.py
@@ -0,0 +1,65 @@
+"""tests the configuration system"""
+# pylint: disable=redefined-outer-name,protected-access,too-many-arguments
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+
+from pytest_vulture.vulture.manager import VultureManager
+
+
+@pytest.mark.parametrize(
+    "folder_path,results",
+    [
+        ("vulture_tests/comments", 0),
+        ("vulture_tests/bug_18_2020", 0),
+        ("vulture_tests/easy", 1),
+    ]
+)
+def test_with_comments(examples_path, folder_path, results):
+    """Tests the call function with comments to see if the vulture ignore disables the outputs"""
+    ini = Mock()
+    ini.package_configuration.setup_path = Path("not_found.py")
+    ini.vulture_configuration.exclude = []
+    ini.vulture_configuration.ignore_names = []
+    ini.vulture_configuration.ignore_decorators = []
+    ini.vulture_configuration.ignore = []
+    ini.vulture_configuration.ignore_types = []
+    manager = VultureManager(examples_path / folder_path, ini)
+
+    manager.call()
+
+    assert len(manager._results) == results
+
+
+@pytest.mark.parametrize(
+    "exclude,ignore,ignore_names,ignore_decorators,ignore_types,results,answers",
+    [
+        ([], [], [], [], [], 1, ["line 7 :  unused property 'main' (60% confidence)",
+                                 "line 8 :  unused property 'main' (60% confidence)"]),
+        ([".py"], [], [], [], [], 0, []),
+        (["main.py"], [], [], [], [], 1,  ["line 6 :  unused function 'other' (60% confidence)"]),
+        ([], ["main.py"], [], [], [], 0, []),
+        ([], ["easy/main.py"], [], [], [], 1, ["line 7 :  unused property 'main' (60% confidence)",
+                                               "line 8 :  unused property 'main' (60% confidence)"]),
+        ([], [], ["main"], [], [], 0, []),
+        ([], [], [], ["@property"], [], 0, []),
+        ([], [], [], [], ["property"], 0, []),
+    ]
+)
+def test_call(examples_path, exclude, ignore, ignore_names, ignore_decorators, ignore_types, results, answers):
+    """Tests the call option with vulture with the easy example"""
+    ini = Mock()
+    ini.package_configuration.setup_path = Path("not_found.py")
+    ini.vulture_configuration.exclude = exclude
+    ini.vulture_configuration.ignore_names = ignore_names
+    ini.vulture_configuration.ignore_decorators = ignore_decorators
+    ini.vulture_configuration.ignore = ignore
+    ini.vulture_configuration.ignore_types = ignore_types
+    manager = VultureManager(examples_path / "vulture_tests/easy", ini)
+
+    manager.call()
+
+    assert len(manager._results) == results
+    if results:
+        assert manager._results[0].message in answers
diff --git a/test/tests/functional/test_conf.py b/test/tests/functional/test_conf.py
new file mode 100644
index 0000000..ff809d1
--- /dev/null
+++ b/test/tests/functional/test_conf.py
@@ -0,0 +1,39 @@
+"""tests the configuration system"""
+# pylint: disable=redefined-outer-name
+from pathlib import Path
+
+from pytest_vulture.conf.reader import IniReader
+
+
+def test_read_full_ini(examples_path):
+    """Tests with the full.ini example"""
+    reader = IniReader(Path(examples_path / "ini/full.ini"))
+
+    reader.read_ini()
+    exclude = reader.vulture_configuration.exclude
+
+    assert len(exclude) == 3 + len(reader.vulture_configuration.EXCLUDE_DEFAULT)
+    assert "*/test/*" in exclude
+    assert "*/test_2/*" in exclude
+    assert "*/test_3/*" in exclude
+    assert reader.vulture_configuration.ignore_names == ["clean_"]
+    assert reader.vulture_configuration.ignore_decorators == ["@app.route"]
+    assert reader.vulture_configuration.ignore == ["src/main.py"]
+    assert reader.vulture_configuration.ignore_types == ["attribute"]
+    assert reader.package_configuration.setup_path.as_posix() == "src/setup.py"
+    assert reader.package_configuration.check_entry_points is False
+
+
+def test_read_light_ini(examples_path):
+    """Tests with the mini.ini example"""
+    reader = IniReader(Path(examples_path / "ini/mini.ini"))
+
+    reader.read_ini()
+
+    assert reader.vulture_configuration.exclude == reader.vulture_configuration.EXCLUDE_DEFAULT
+    assert reader.vulture_configuration.ignore_names == []
+    assert reader.vulture_configuration.ignore_decorators == []
+    assert reader.vulture_configuration.ignore == []
+    assert reader.vulture_configuration.ignore_types == []
+    assert reader.package_configuration.setup_path.as_posix() == "setup.py"
+    assert reader.package_configuration.check_entry_points is True
diff --git a/test/tests/functional/test_setup.py b/test/tests/functional/test_setup.py
new file mode 100644
index 0000000..13887df
--- /dev/null
+++ b/test/tests/functional/test_setup.py
@@ -0,0 +1,54 @@
+"""tests the setup parser system to find entry points"""
+# pylint: disable=redefined-outer-name,use-implicit-booleaness-not-comparison
+from unittest.mock import Mock
+
+import pytest
+
+from pytest_vulture.setup_manager import (
+    EntryPointFileError,
+    EntryPointFunctionError,
+    SetupManager,
+)
+from pytest_vulture.vulture.output_line import VultureOutputLine
+
+
+@pytest.mark.parametrize(
+    "check_entry_points, setup,line,is_entry_point",
+    [
+        (False, "setup_with_entry_point", "src/test/main.py:8: unused function 'main' (60% confidence)", True),
+        (False, "setup_with_entry_point", "src/test/main.py:8: unused function 'test' (60% confidence)", False),
+        (False, "setup_with_entry_point", "src/swagger_parser/conf/base.py:8: unused property 'main' (60% confidence)",
+         False),
+        (True, "setup", "test/main.py:7: unused function 'main' (60% confidence)", True),
+        (True, "setup", "src/test/main.py:7: unused function 'main' (60% confidence)", False),
+        (False, "setup_broken", "main.py:7: unused function 'main' (60% confidence)", False),
+        (False, "setup_no_entry_point", "main.py:7: unused function 'main' (60% confidence)", False),
+        (False, "setup_not_found", "main.py:7: unused function 'main' (60% confidence)", False),
+    ]
+)
+def test_is_entry_point(check_entry_points, examples_path, setup, line, is_entry_point):
+    """tests the entry point checking system"""
+    conf = Mock()
+    conf.package_configuration.check_entry_points = check_entry_points
+    conf.package_configuration.setup_path = examples_path / "setups" / setup
+    setup_entry_point = SetupManager(conf)
+    vulture = VultureOutputLine(line)
+
+    assert setup_entry_point.is_entry_point(vulture) == is_entry_point
+
+
+@pytest.mark.parametrize(
+    "path,error",
+    [
+        ("setup_file_not_found", EntryPointFileError),
+        ("setup_entry_p_not_found", EntryPointFunctionError),
+    ]
+)
+def test_errors(examples_path, path, error):
+    conf = Mock()
+    conf.package_configuration.check_entry_points = True
+    not_found_path = examples_path / "setups" / path
+    conf.package_configuration.setup_path = not_found_path
+
+    with pytest.raises(error):
+        SetupManager(conf)
diff --git a/test/tests/unit/__init__.py b/test/tests/unit/__init__.py
new file mode 100644
index 0000000..82d6f15
--- /dev/null
+++ b/test/tests/unit/__init__.py
@@ -0,0 +1,1 @@
+"""Unit tests"""
diff --git a/test/tests/unit/test_line_parsing.py b/test/tests/unit/test_line_parsing.py
new file mode 100644
index 0000000..67e47ee
--- /dev/null
+++ b/test/tests/unit/test_line_parsing.py
@@ -0,0 +1,82 @@
+"""tests the vulture line parsing"""
+import pytest
+
+from pytest_vulture.vulture.output_line import VultureOutputLine
+
+
+@pytest.mark.parametrize(
+    "vulture_message,pytest_message",
+    [
+        (
+            "src/test.py:15: unused function 'main' (60% confidence)",
+            "line 15 :  unused function 'main' (60% confidence)",
+        ),
+        (
+            "src/toto.py:14: unused function 'tata' (60% confidence)",
+            "line 14 :  unused function 'tata' (60% confidence)",
+        ),
+        (
+            "src/test.py unused function 'main' (60% confidence)",
+            "src/test.py unused function 'main' (60% confidence)",
+        ),
+    ]
+)
+def test_messages(vulture_message, pytest_message):
+    """Tests the message parsing of vulture output"""
+    vulture_line = VultureOutputLine(vulture_message)
+
+    assert vulture_line.message == pytest_message
+
+
+@pytest.mark.parametrize(
+    "vulture_message,path",
+    [
+        ("src/test.py:15: unused function 'main' (60% confidence)", "src/test.py"),
+        ("src/tutu.py:15: unused function 'main' (60% confidence)", "src/tutu.py"),
+    ]
+)
+def test_path(vulture_message, path):
+    vulture_line = VultureOutputLine(vulture_message)
+
+    assert vulture_line.path.as_posix() == path
+
+
+@pytest.mark.parametrize(
+    "vulture_message,path",
+    [
+        ("src/test.py:15: unused function 'main' (60% confidence)", "src.test:main"),
+        ("src/tutu.py:15: unused function 'tata' (60% confidence)", "src.tutu:tata"),
+    ]
+)
+def test_python_path(vulture_message, path):
+    vulture_line = VultureOutputLine(vulture_message)
+
+    assert vulture_line.python_path == path
+
+
+@pytest.mark.parametrize(
+    "vulture_message,type_vulture",
+    [
+        ("src/test.py:15: unused function 'main' (60% confidence)", "function"),
+        ("src/tutu.py:15: error", ""),
+    ]
+)
+def test_type(vulture_message, type_vulture):
+    vulture_line = VultureOutputLine(vulture_message)
+
+    assert vulture_line.type == type_vulture
+    assert vulture_line.type == type_vulture
+
+
+@pytest.mark.parametrize(
+    "vulture_message,line_number",
+    [
+        ("src/test.py:1: unused function 'main' (60% confidence)", 1),
+        ("src/test.py:15: unused function 'main' (60% confidence)", 15),
+        ("error", None),
+    ]
+)
+def test_line_number(vulture_message, line_number):
+    vulture_line = VultureOutputLine(vulture_message)
+
+    assert vulture_line.line_number == line_number
diff --git a/test/tests/unit/test_vulture_manager.py b/test/tests/unit/test_vulture_manager.py
new file mode 100644
index 0000000..415a9b1
--- /dev/null
+++ b/test/tests/unit/test_vulture_manager.py
@@ -0,0 +1,61 @@
+"""tests vulture_manager.py"""
+# pylint: disable=protected-access,too-many-arguments
+from unittest.mock import patch
+
+import pytest
+
+from vulture import Vulture
+
+from pytest_vulture.conf.reader import IniReader
+from pytest_vulture.vulture.manager import VultureManager
+from pytest_vulture.vulture.output_line import VultureOutputLine
+from pytest_vulture.vulture.print_parser import PrintParser
+
+
+@pytest.mark.parametrize(
+    "messages,size,ignore_path",
+    [
+        ("path.py:8: message", 1, "not_path.py"),
+        ("", 0, "path.py"),
+        ("path.py:8: message", 0, "path.py"),
+    ]
+)
+@patch.object(Vulture, "report")
+@patch.object(Vulture, "scavenge")
+def test_call(scavenge, report, monkeypatch, tmp_path, messages, size, ignore_path):
+    """Tests the vulture call"""
+    config_file = tmp_path / "test"
+    config_file.write_text(f"""
+[vulture],
+ignore = 
+    {ignore_path}
+    """)
+    config = IniReader(config_file)
+    config.read_ini()
+    manager = VultureManager(tmp_path / "test.py", config)
+    monkeypatch.setattr(PrintParser, "messages", messages)
+
+    manager.call()
+
+    assert scavenge.called is True
+    assert report.called is True
+    assert len(manager._results) == size
+    if size:
+        assert manager._results[0].message == "line 8 :  message"
+
+
+@pytest.mark.parametrize(
+    "results,file,answer",
+    [
+        ([], "test.py", None),
+        ([VultureOutputLine("src/test.py:8: test")], "not_test.py", None),
+        ([VultureOutputLine("src/test.py:8: test")], "test.py", None),
+        ([VultureOutputLine("src/test.py:8: test")], "src/test.py", "line 8 :  test"),
+    ]
+)
+def test_get_file_errors(tmp_path, results, file, answer):
+    """Tests the getting file"""
+    manager = VultureManager(tmp_path / "test2", IniReader(tmp_path / "test"))
+    manager._results = results
+
+    assert manager.get_file_errors(file) == answer