Skip to content
Merged
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,12 @@ More detailed usage is described in the [tutorial][tutorial]
- install `pytest-rts`
3. Switch to directory with target project
4. Install all the dependencies needed for testing (should be installed into the same pytest-rts virtual environment)
5. Execute `pytest --rts` which will run the entire test suite and build a mapping database
5. Execute `pytest --cov=<path to code> --cov-context=test` which will run the entire test suite and build a mapping database with [pytest-cov](https://github.com/pytest-dev/pytest-cov)
6. Rename the coverage file produced by `pytest-cov` to your liking. Example: `mv .coverage pytest-rts-coverage`

#### Running tests related to the changes
#### Running new tests

1. execute `pytest --rts` after doing changes

#### Running evaluation code

1. execute `pytest_rts_eval` in target project directory
1. execute `pytest --rts --rts-coverage-db=<your coverage file>` after adding new tests

## <a name="dev"></a> Development

Expand Down
3 changes: 2 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[pytest]
# Prevent helper project tests being collected as tests for pytest-rts
pytester_example_dir = ./pytest_rts/tests/helper_project
addopts = -p pytester
norecursedirs = pytest_rts/tests/helper_project
2 changes: 1 addition & 1 deletion pytest_rts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""pytest-rts: avoid already imported warning: PYTEST_DONT_REWRITE"""
__version__ = "1.2.1"
__version__ = "2.0.0"
12 changes: 0 additions & 12 deletions pytest_rts/collect.py

This file was deleted.

140 changes: 20 additions & 120 deletions pytest_rts/plugin.py
Original file line number Diff line number Diff line change
@@ -1,136 +1,36 @@
"""Code for pytest-rts plugin logic"""
import logging
import os
import sqlite3

import pytest
from _pytest.config import Config
from _pytest.config.argparsing import Parser

from pytest_rts.pytest.init_phase_plugin import InitPhasePlugin
from pytest_rts.pytest.normal_phase_plugin import NormalPhasePlugin
from pytest_rts.pytest.update_phase_plugin import UpdatePhasePlugin
from pytest_rts.utils.git import (
get_current_head_hash,
is_git_repo,
repo_has_commits,
)
from pytest_rts.utils.selection import (
get_tests_and_data_committed,
get_tests_and_data_current,
)
from pytest_rts.utils.mappinghelper import MappingHelper
from pytest_rts.utils.testgetter import TestGetter
from pytest_rts.pytest.runner_plugin import RunnerPlugin
from pytest_rts.utils.common import get_existing_tests

DB_FILE_NAME = "mapping.db"


class MappingConn: # pylint: disable=too-few-public-methods
"""Mapping connection"""

_conn = None

@classmethod
def conn(cls):
"""SQLite connection"""
if not cls._conn:
cls._conn = sqlite3.connect(DB_FILE_NAME)
return cls._conn


def pytest_addoption(parser):
def pytest_addoption(parser: Parser) -> None:
"""Register pytest flags"""
parser.addoption("--rts", action="store_true", default=False, help="run rts")
group = parser.getgroup("pytest-rts")
group.addoption("--rts", action="store_true", default=False, help="Run pytest-rts")
group.addoption(
"--rts-coverage-db",
action="store",
default="",
help="Coverage file pytest-rts",
)


def pytest_configure(config):
def pytest_configure(config: Config) -> None:
"""Register RTS plugins based on state"""
logger = logging.getLogger()
logging.basicConfig(format="%(message)s", level=logging.INFO)

if not config.option.rts:
return

if not is_git_repo():
logger.info(
"Not a git repository! pytest-rts is disabled. Run git init before using pytest-rts."
)
return

if not repo_has_commits():
logger.info(
"No commits yet! pytest-rts is disabled. Create a git commit before using pytest-rts."
)
return

init_required = not os.path.isfile(DB_FILE_NAME)

mapping_helper = MappingHelper(MappingConn.conn())
test_getter = TestGetter(MappingConn.conn())

if init_required:
logger.info("No mapping database detected, starting initialization...")
config.pluginmanager.register(
InitPhasePlugin(mapping_helper), "rts-init-plugin"
)
return

workdir_data = get_tests_and_data_current(mapping_helper, test_getter)

logger.info("WORKING DIRECTORY CHANGES")
logger.info("Found %s changed test files", workdir_data.changed_testfiles_amount)
logger.info("Found %s changed src files", workdir_data.changed_srcfiles_amount)
logger.info("Found %s tests to execute\n", len(workdir_data.test_set))

if workdir_data.test_set:
logger.info(
"Running WORKING DIRECTORY test set and exiting without updating..."
)
config.pluginmanager.register(
NormalPhasePlugin(workdir_data.test_set, test_getter)
)
return

logger.info("No WORKING DIRECTORY tests to run, checking COMMITTED changes...")

current_hash = get_current_head_hash()
previous_hash = mapping_helper.last_update_hash
if current_hash == previous_hash:
pytest.exit("Database is updated to the current commit state", 0)

logger.info("Comparison: %s\n", " => ".join([current_hash, previous_hash]))

committed_data = get_tests_and_data_committed(mapping_helper, test_getter)

logger.info("COMMITTED CHANGES")
logger.info("Found %s changed test files", committed_data.changed_testfiles_amount)
logger.info("Found %s changed src files", committed_data.changed_srcfiles_amount)
logger.info(
"Found %s newly added tests",
committed_data.new_tests_amount,
)
logger.info("Found %s tests to execute\n", len(committed_data.test_set))

if committed_data.warning_needed:
logger.info(
"WARNING: New lines were added to the following files but no new tests discovered:"
)
logger.info("\n".join(committed_data.files_to_warn))

logger.info("=> Executing tests (if any) and updating database")
mapping_helper.set_last_update_hash(current_hash)

mapping_helper.update_mapping(committed_data.update_data)

if committed_data.test_set:
config.pluginmanager.register(
UpdatePhasePlugin(committed_data.test_set, mapping_helper, test_getter)
)
return

pytest.exit("No tests to run", 0)
if not config.option.rts_coverage_db:
pytest.exit("No coverage file provided", 2)

if not os.path.exists(config.option.rts_coverage_db):
pytest.exit("Provided coverage file does not exist", 2)

def pytest_unconfigure(config):
"""Cleanup after pytest run"""
if config.option.rts:
MappingConn.conn().commit()
MappingConn.conn().close()
existing_tests = get_existing_tests(config.option.rts_coverage_db)
config.pluginmanager.register(RunnerPlugin(existing_tests))
27 changes: 0 additions & 27 deletions pytest_rts/pytest/collect_plugin.py

This file was deleted.

3 changes: 2 additions & 1 deletion pytest_rts/pytest/fake_item.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""This module contains a fake pytest item class"""
from _pytest.config import Config


class FakeItem: # pylint: disable=too-few-public-methods
"""Fake class"""

def __init__(self, config):
def __init__(self, config: Config) -> None:
self.config = config
28 changes: 0 additions & 28 deletions pytest_rts/pytest/init_phase_plugin.py

This file was deleted.

46 changes: 0 additions & 46 deletions pytest_rts/pytest/mapper_plugin.py

This file was deleted.

25 changes: 0 additions & 25 deletions pytest_rts/pytest/normal_phase_plugin.py

This file was deleted.

30 changes: 30 additions & 0 deletions pytest_rts/pytest/runner_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""This module contains code for running a specific test set"""
# pylint: disable=too-few-public-methods
from typing import List, Set

from _pytest.main import Session
from _pytest.nodes import Item

from pytest_rts.pytest.fake_item import FakeItem
from pytest_rts.utils.common import filter_pytest_items


class RunnerPlugin:
"""Plugin class for pytest"""

def __init__(self, existing_tests: Set[str]) -> None:
"""Set existing tests"""
self.existing_tests = existing_tests

def pytest_collection_modifyitems(
self,
session: Session,
config, # pylint: disable=unused-argument
items: List[Item],
) -> None:
"""Select only specific tests for running"""
original_length = len(items)
items[:] = filter_pytest_items(items, self.existing_tests)
session.config.hook.pytest_deselected(
items=([FakeItem(session.config)] * (original_length - len(items)))
)
Loading