diff --git a/.gitignore b/.gitignore index ece8b1e..3e1f37a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ __pycache__ -/.coverage +.coverage .DS_Store -/.mypy_cache -/.pytest_cache -/.tox +.mypy_cache +.pytest_cache +.tox *.egg-info -/docs/_build -/htmlcov -/dist -/build -/.venv +docs/_build +htmlcov +dist +build +.venv diff --git a/.travis.yml b/.travis.yml index df0726e..579e365 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ matrix: env: TOX_ENV: py36-unit-tests COVERAGE_FLAG: unit + - python: 3.6 env: TOX_ENV: py36-integration-tests @@ -24,31 +25,32 @@ matrix: PGHOST: 127.0.0.1 COVERAGE_FLAG: integration - - python: 3.7 + - python: 3.6 env: - TOX_ENV: py37-unit-tests - COVERAGE_FLAG: unit + TOX_ENV: py36-acceptance-tests + PGUSER: postgres + PGPORT: 5432 + PGPASSWORD: "" + PGHOST: 127.0.0.1 + COVERAGE_FLAG: "" + - python: 3.7 env: - TOX_ENV: py37-integration-tests + TOX_ENV: py37-unit-tests,py37-integration-tests,py37-acceptance-tests PGUSER: postgres PGPORT: 5432 PGPASSWORD: "" PGHOST: 127.0.0.1 - COVERAGE_FLAG: integration + COVERAGE_FLAG: "" - python: 3.8 env: - TOX_ENV: py38-unit-tests - COVERAGE_FLAG: unit - - python: 3.8 - env: - TOX_ENV: py38-integration-tests + TOX_ENV: py38-unit-tests,py38-integration-tests,py38-acceptance-tests PGUSER: postgres PGPORT: 5432 PGPASSWORD: "" PGHOST: 127.0.0.1 - COVERAGE_FLAG: integration + COVERAGE_FLAG: "" install: - pip install tox codecov @@ -57,4 +59,7 @@ script: - tox -e $TOX_ENV after_success: - - bash <(curl -s https://codecov.io/bash) -c -F $COVERAGE_FLAG + - | + if [ -n "$COVERAGE_FLAG" ]; + then bash <(curl -s https://codecov.io/bash) -c -F $COVERAGE_FLAG; + fi diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ab16962..752260a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -212,26 +212,9 @@ Try our demo With a running database: -Launch a worker with: - -.. code-block:: console - - (venv) $ export PROCRASTINATE_APP=procrastinate_demo.app.app - (venv) $ procrastinate migrate - (venv) $ procrastinate worker - -Schedule some tasks with: - .. code-block:: console - (venv) $ python -m procrastinate_demo - -Wait, there are ``async`` and ``await`` keywords everywhere!? -------------------------------------------------------------- - -Yes, in order to provide both a synchronous **and** asynchronous API, Procrastinate -needs to be asynchronous at core. Find out more in the documentation, in the Discussions -section. If you need informations on how to work with asynchronous Python, check out: - -- The official documentation: https://docs.python.org/3/library/asyncio.html -- A more accessible guide by Brad Solomon: https://realpython.com/async-io-python/ + (venv) $ export PGDATABASE=septentrion PGHOST=localhost PGUSER=postgres + (venv) $ createdb + (venv) $ export SEPTENTRION_MIGRATIONS_ROOT=example_migrations SEPTENTRION_TARGET_VERSION=1.1 + (venv) $ septentrion migrate diff --git a/septentrion/cli.py b/septentrion/cli.py index 2eecf16..ec3ecf5 100644 --- a/septentrion/cli.py +++ b/septentrion/cli.py @@ -174,6 +174,11 @@ def split_envvar_value(self, rv: str): "flag as many times as necessary) (env: SEPTENTRION_ADDITIONAL_SCHEMA_FILE, comma " "separated values)", ) +@click.option( + "--ignore-symlinks/--no-ignore-symlinks", + default=configuration.DEFAULTS["ignore_symlinks"], + help="Ignore migration files that are symlinks", +) def cli(ctx: click.Context, **kwargs): if kwargs.pop("password_flag"): password = click.prompt("Database password", hide_input=True) @@ -210,7 +215,6 @@ def show_migrations(settings: configuration.Settings): def migrate_func(settings: configuration.Settings): """ Run unapplied migrations. - """ migrate.migrate(settings=settings, stylist=style.stylist) diff --git a/septentrion/configuration.py b/septentrion/configuration.py index 0fa8e40..12b23ba 100644 --- a/septentrion/configuration.py +++ b/septentrion/configuration.py @@ -4,7 +4,8 @@ """ import configparser import logging -from typing import Any, Dict, Tuple +import pathlib +from typing import Any, Dict, Tuple, Union from septentrion import exceptions @@ -28,6 +29,7 @@ "schema_template": "schema_{}.sql", "fixtures_template": "fixtures_{}.sql", "non_transactional_keyword": ["CONCURRENTLY", "ALTER TYPE", "VACUUM"], + "ignore_symlinks": False, # Values that don't have an explicit default need to be present too "verbosity": 0, "host": None, @@ -92,21 +94,34 @@ def log_level(verbosity: int) -> int: return 40 - 10 * min(verbosity, 3) -def clean_key(key: str) -> str: - # CLI settings are lowercase - return key.upper() - - class Settings: def __init__(self): self._settings = {} self.update(DEFAULTS) def __getattr__(self, key: str) -> Any: - return self._settings[key] + try: + return self._settings[key] + except KeyError: + raise AttributeError(key) def set(self, key: str, value: Any) -> None: - self._settings[clean_key(key)] = value + try: + method = getattr(self, f"clean_{key.lower()}") + except AttributeError: + pass + else: + value = method(value) + # TODO: remove the .upper() and fix the tests: from_cli() should be + # the only one doing the .upper() + self._settings[key.upper()] = value + + def clean_migrations_root( + self, migrations_root: Union[str, pathlib.Path] + ) -> pathlib.Path: + if isinstance(migrations_root, str): + migrations_root = pathlib.Path(migrations_root) + return migrations_root def __repr__(self): return repr(self._settings) @@ -118,6 +133,7 @@ def update(self, values: Dict) -> None: @classmethod def from_cli(cls, cli_settings: Dict): settings = cls() - settings.update(cli_settings) + # CLI settings are lowercase + settings.update({key.upper(): value for key, value in cli_settings.items()}) return settings diff --git a/septentrion/core.py b/septentrion/core.py index 558cec6..d7bfbda 100644 --- a/septentrion/core.py +++ b/septentrion/core.py @@ -3,7 +3,6 @@ from the existing files (septentrion.files) and from the db (septentrion.db) """ -import os from typing import Any, Dict, Iterable, Optional from septentrion import configuration, db, exceptions, files, style, utils @@ -21,6 +20,8 @@ def get_applied_versions(settings: configuration.Settings) -> Iterable[str]: return utils.sort_versions(applied_versions & known_versions) +# TODO: Refactor: this should just work with version numbers, not sql_tpl and +# not force_version def get_closest_version( settings: configuration.Settings, target_version: str, @@ -68,16 +69,21 @@ def get_closest_version( return None +# TODO: refactor this and the function below +# TODO: also remove files.get_special_files, it's not really useful def get_best_schema_version(settings: configuration.Settings) -> str: """ Get the best candidate to init the DB. """ + schema_files = files.get_special_files( + root=settings.MIGRATIONS_ROOT, folder="schemas" + ) version = get_closest_version( settings=settings, target_version=settings.TARGET_VERSION, sql_tpl=settings.SCHEMA_TEMPLATE, - existing_files=files.get_known_schemas(settings=settings), force_version=settings.SCHEMA_VERSION, + existing_files=schema_files, ) if version is None: @@ -90,10 +96,13 @@ def get_fixtures_version(settings: configuration.Settings, target_version: str) Get the closest fixtures to use to init a new DB to the current target version. """ + fixture_files = files.get_special_files( + root=settings.MIGRATIONS_ROOT, folder="fixtures" + ) version = get_closest_version( settings=settings, target_version=target_version, - existing_files=files.get_known_fixtures(settings=settings), + existing_files=fixture_files, sql_tpl=settings.FIXTURES_TEMPLATE, ) @@ -136,7 +145,10 @@ def build_migration_plan(settings: configuration.Settings) -> Iterable[Dict[str, for mig in migs: applied = mig in applied_migrations path = migrations_to_apply[mig] - is_manual = files.is_manual_migration(os.path.abspath(path)) + contents = files.file_lines_generator(path) + is_manual = files.is_manual_migration( + migration_path=path, migration_contents=contents + ) version_plan.append((mig, applied, path, is_manual)) yield {"version": version, "plan": version_plan} diff --git a/septentrion/files.py b/septentrion/files.py index 9a55890..dfbf9df 100644 --- a/septentrion/files.py +++ b/septentrion/files.py @@ -2,19 +2,25 @@ Interact with the migration files. """ -import io -import os -from typing import Iterable +import pathlib +from typing import Dict, Iterable, List, Tuple -from septentrion import configuration, utils +from septentrion import configuration, exceptions, utils -def list_dirs(root: str) -> Iterable[str]: - return [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] +def iter_dirs(root: pathlib.Path) -> Iterable[pathlib.Path]: + return (d for d in root.iterdir() if d.is_dir()) -def list_files(root: str) -> Iterable[str]: - return [d for d in os.listdir(root) if os.path.isfile(os.path.join(root, d))] +def iter_files( + root: pathlib.Path, ignore_symlinks: bool = False +) -> Iterable[pathlib.Path]: + for f in root.iterdir(): + if not f.is_file(): + continue + if ignore_symlinks and f.is_symlink(): + continue + yield f def get_known_versions(settings: configuration.Settings) -> Iterable[str]: @@ -23,85 +29,82 @@ def get_known_versions(settings: configuration.Settings) -> Iterable[str]: ordered. Ignore symlinks. """ - # get all subfolders + # exclude symlinks and some folders (like schemas, fixtures, etc) try: - dirs = list_dirs(settings.MIGRATIONS_ROOT) + folders_names = [str(d.name) for d in iter_dirs(settings.MIGRATIONS_ROOT)] except OSError: - raise ValueError("settings.MIGRATIONS_ROOT is improperly configured.") - - # exclude symlinks and some folders (like schemas, fixtures, etc) - versions = [ - d - for d in dirs - if not os.path.islink(os.path.join(settings.MIGRATIONS_ROOT, d)) - and utils.is_version(d) - ] + raise exceptions.SeptentrionException( + "settings.MIGRATIONS_ROOT is improperly configured." + ) - return utils.sort_versions(versions) + return utils.sort_versions(name for name in folders_names if utils.is_version(name)) -def is_manual_migration(migration_path: str) -> bool: +def is_manual_migration( + migration_path: pathlib.Path, migration_contents: Iterable[str] +) -> bool: - if "/manual/" in migration_path: + if "manual" in migration_path.parts: return True - if not migration_path.endswith("dml.sql"): + if not migration_path.suffixes[-2:] == [".dml", ".sql"]: return False - with io.open(migration_path, "r", encoding="utf8") as f: - for line in f: - if "--meta-psql:" in line: - return True + for line in migration_contents: + if "--meta-psql:" in line: + return True return False -def get_known_schemas(settings: configuration.Settings) -> Iterable[str]: - return os.listdir(os.path.join(settings.MIGRATIONS_ROOT, "schemas")) - - -def get_known_fixtures(settings: configuration.Settings) -> Iterable[str]: +# TODO: remove this function when get_best_schema_version is refactored +def get_special_files(root: pathlib.Path, folder: str) -> List[str]: try: - return os.listdir(os.path.join(settings.MIGRATIONS_ROOT, "fixtures")) + return [str(f.name) for f in iter_files(root / folder)] except FileNotFoundError: return [] -def get_migrations_files_mapping(settings: configuration.Settings, version: str): +def get_migrations_files_mapping( + settings: configuration.Settings, version: str +) -> Dict[str, pathlib.Path]: """ Return an dict containing the list of migrations for the given version. Key: name of the migration. Value: path to the migration file. """ + ignore_symlinks = settings.IGNORE_SYMLINKS - def filter_migrations(files: Iterable[str]) -> Iterable[str]: - return [f for f in files if f.endswith("ddl.sql") or f.endswith("dml.sql")] - - version_root = os.path.join(settings.MIGRATIONS_ROOT, version) + version_root = settings.MIGRATIONS_ROOT / version migrations = {} - # list auto migrations - try: - files = list_files(version_root) - except OSError: - raise ValueError("No sql folder found for version {}.".format(version)) - # filter files (keep *ddl.sql and *dml.sql) - auto_migrations = filter_migrations(files) - # store migrations - for mig in auto_migrations: - migrations[mig] = os.path.join(version_root, mig) - - # list manual migrations - manual_root = os.path.join(version_root, "manual") - try: - files = list_files(manual_root) - except OSError: - files = [] - # filter files (keep *ddl.sql and *dml.sql) - auto_migrations = filter_migrations(files) - # store migrations - for mig in auto_migrations: - migrations[mig] = os.path.join(manual_root, mig) + # TODO: should be a setting + subfolders = [".", "manual"] + for subfolder_name in subfolders: + subfolder = version_root / subfolder_name + if not subfolder.exists(): + continue + for mig, path in list_migrations_and_paths( + folder=subfolder, ignore_symlinks=ignore_symlinks + ): + migrations[mig] = path return migrations + + +def list_migrations_and_paths( + folder: pathlib.Path, ignore_symlinks: bool +) -> Iterable[Tuple[str, pathlib.Path]]: + + for file in iter_files(root=folder, ignore_symlinks=ignore_symlinks): + if not file.suffix == ".sql" or not file.stem[-3:] in ("ddl", "dml"): + continue + + yield file.name, file + + +def file_lines_generator(path: pathlib.Path): + with open(path) as f: + for line in f: + yield line diff --git a/septentrion/migrate.py b/septentrion/migrate.py index 6ce0a51..aad3c65 100644 --- a/septentrion/migrate.py +++ b/septentrion/migrate.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import io import logging -import os.path +import pathlib from septentrion import configuration, core, db, exceptions, files, runner, style, utils @@ -62,7 +62,7 @@ def init_schema( for file_name in additional_files: if not file_name: continue - file_path = os.path.join(settings.MIGRATIONS_ROOT, "schemas", file_name) + file_path = settings.MIGRATIONS_ROOT / "schemas" / file_name logger.info("Loading %s", file_path) run_script(settings=settings, path=file_path) @@ -70,11 +70,12 @@ def init_schema( with stylist.activate("title") as echo: echo("Loading schema") - schema_path = os.path.join( - settings.MIGRATIONS_ROOT, - "schemas", - settings.SCHEMA_TEMPLATE.format(init_version), + schema_path = ( + settings.MIGRATIONS_ROOT + / "schemas" + / settings.SCHEMA_TEMPLATE.format(init_version) ) + logger.info("Loading %s", schema_path) with stylist.checkbox( @@ -91,11 +92,12 @@ def init_schema( fixtures_version = core.get_fixtures_version( settings=settings, target_version=init_version ) - fixtures_path = os.path.join( - settings.MIGRATIONS_ROOT, - "fixtures", - settings.FIXTURES_TEMPLATE.format(fixtures_version), + fixtures_path = ( + settings.MIGRATIONS_ROOT + / "fixtures" + / settings.FIXTURES_TEMPLATE.format(fixtures_version) ) + with stylist.activate("title") as echo: echo("Loading fixtures") logger.info("Applying fixture %s (file %s)", fixtures_version, fixtures_path) @@ -143,8 +145,8 @@ def create_fake_entries( ) -def run_script(settings: configuration.Settings, path: str) -> None: +def run_script(settings: configuration.Settings, path: pathlib.Path) -> None: logger.info("Running SQL file %s", path) with io.open(path, "r", encoding="utf8") as f: - script = runner.Script(settings=settings, file_handler=f, name=f.name) + script = runner.Script(settings=settings, file_handler=f, path=path) script.run(connection=db.get_connection(settings=settings)) diff --git a/septentrion/runner.py b/septentrion/runner.py index 0f6df9f..dcbdc4b 100644 --- a/septentrion/runner.py +++ b/septentrion/runner.py @@ -1,4 +1,5 @@ import logging +import pathlib from typing import Iterable import sqlparse @@ -91,17 +92,23 @@ def run(self, cursor: Cursor) -> int: class Script(object): def __init__( - self, settings: configuration.Settings, file_handler: Iterable[str], name: str + self, + settings: configuration.Settings, + file_handler: Iterable[str], + path: pathlib.Path, ): - is_manual = files.is_manual_migration(name) + file_lines = list(file_handler) + is_manual = files.is_manual_migration( + migration_path=path, migration_contents=file_lines + ) self.settings = settings if is_manual: self.block_list = [Block()] - elif self.contains_non_transactional_keyword(file_handler): + elif self.contains_non_transactional_keyword(file_lines): self.block_list = [Block()] else: self.block_list = [SimpleBlock()] - for line in file_handler: + for line in file_lines: if line.startswith("--meta-psql:") and is_manual: self.block_list[-1].close() command = line.split(":")[1].strip() @@ -120,13 +127,11 @@ def run(self, connection): for block in self.block_list: block.run(cursor) - def contains_non_transactional_keyword(self, file_handler): + def contains_non_transactional_keyword(self, file_lines: Iterable[str]) -> bool: keywords = self.settings.NON_TRANSACTIONAL_KEYWORD - for line in file_handler: + for line in file_lines: for kw in keywords: if kw.lower() in line.lower(): - file_handler.seek(0) return True - file_handler.seek(0) return False diff --git a/setup.cfg b/setup.cfg index c29e8f0..8062cdd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,6 +89,7 @@ addopts = testpaths = tests/unit tests/integration + tests/acceptance [mypy-setuptools.*,colorama.*,psycopg2.*,sqlparse.*] ignore_missing_imports = True diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance/__init__.py b/tests/acceptance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_cli.py b/tests/acceptance/test_cli.py similarity index 100% rename from tests/integration/test_cli.py rename to tests/acceptance/test_cli.py diff --git a/tests/conftest.py b/tests/conftest.py index d67af3c..0502207 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,8 @@ def db(): cursor.execute(f"DROP DATABASE IF EXISTS {test_db_name}") cursor.execute(f"CREATE DATABASE {test_db_name}") - yield connection.get_dsn_parameters() + params = connection.get_dsn_parameters() + params["dbname"] = test_db_name + yield params cursor.execute(f"DROP DATABASE {test_db_name}") diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_files.py b/tests/integration/test_files.py new file mode 100644 index 0000000..8e31487 --- /dev/null +++ b/tests/integration/test_files.py @@ -0,0 +1,23 @@ +import pytest + +from septentrion import files + + +def test_iter_dirs(tmp_path): + (tmp_path / "15.0").mkdir() + (tmp_path / "16.0").touch() + + assert list(files.iter_dirs(tmp_path)) == [tmp_path / "15.0"] + + +@pytest.mark.parametrize( + "ignore_symlinks, expected", [(True, ["16.0"]), (False, ["16.0", "17.0"])] +) +def test_iter_files(tmp_path, ignore_symlinks, expected): + (tmp_path / "15.0").mkdir() + (tmp_path / "16.0").touch() + (tmp_path / "17.0").symlink_to("16.0") + + assert list(files.iter_files(tmp_path, ignore_symlinks=ignore_symlinks)) == [ + tmp_path / e for e in expected + ] diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index fbe7111..814b1d4 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -105,7 +105,7 @@ def test_get_closest_version_schema_force_dont_exist(known_versions): def test_get_best_schema_version_ok(mocker, known_versions): mocker.patch( - "septentrion.core.files.get_known_schemas", + "septentrion.core.files.get_special_files", return_value=["schema_1.1.sql", "schema_1.2.sql"], ) settings = configuration.Settings.from_cli({"target_version": "1.2"}) @@ -117,7 +117,7 @@ def test_get_best_schema_version_ok(mocker, known_versions): def test_get_best_schema_version_ko(mocker, known_versions): mocker.patch( - "septentrion.core.files.get_known_schemas", + "septentrion.core.files.get_special_files", return_value=["schema_1.0.sql", "schema_1.3.sql"], ) settings = configuration.Settings.from_cli({"target_version": "1.2"}) diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py index dffafb6..3a33646 100644 --- a/tests/unit/test_files.py +++ b/tests/unit/test_files.py @@ -1,86 +1,55 @@ -import io +import pathlib import pytest -from septentrion import configuration, files - - -@pytest.mark.parametrize("isdir,expected", [(True, ["15.0"]), (False, [])]) -def test_list_dirs(mocker, isdir, expected): - mocker.patch("os.listdir", return_value=["15.0"]) - mocker.patch("os.path.isdir", return_value=isdir) - - dirs = files.list_dirs("tests/test_data") - - assert dirs == expected - - -@pytest.mark.parametrize("isfile,expected", [(True, ["file.sql"]), (False, [])]) -def test_list_files(mocker, isfile, expected): - mocker.patch("os.listdir", return_value=["file.sql"]) - mocker.patch("os.path.isfile", return_value=isfile) - - values = files.list_files("tests/test_data/sql/fixtures") - - assert values == expected +from septentrion import configuration, exceptions, files @pytest.mark.parametrize( - "value,expected", [("/foo/manual/bar", True), ("/blah.tgz", False)] + "migration_path,migration_contents,expected", + [ + ("/foo/manual/bar", [], True), + ("/foo.dml.sql", ["--meta-psql:done"], True), + ("/blah.tgz", [], False), + ("/foo.dml.sql", ["foo"], False), + ], ) -def test_is_manual_migration(value, expected): - assert files.is_manual_migration(value) == expected - - -def test_is_manual_migration_true(mocker): - mocker.patch("io.open", return_value=io.StringIO("--meta-psql:done")) - - assert files.is_manual_migration("/foo.dml.sql") is True - - -def test_is_manual_migration_false(mocker): - mocker.patch("io.open", return_value=io.StringIO("foo")) - - assert files.is_manual_migration("/foo.dml.sql") is False - - -def test_get_known_schemas(mocker): - mocker.patch("os.listdir", return_value=["schema_17.02.sql", "schema_16.12.sql"]) - settings = configuration.Settings.from_cli( - {"migrations_root": "tests/test_data/sql"} +def test_is_manual_migration(migration_path, migration_contents, expected): + assert ( + files.is_manual_migration( + migration_path=pathlib.Path(migration_path), + migration_contents=migration_contents, + ) + == expected ) - values = files.get_known_schemas(settings=settings) - - expected = ["schema_16.12.sql", "schema_17.02.sql"] - assert sorted(values) == expected - -def test_get_known_fixtures(mocker): - mocker.patch("os.listdir", return_value=["fixtures_16.12.sql"]) - settings = configuration.Settings.from_cli( - {"migrations_root": "tests/test_data/sql"} +def test_get_special_files(mocker): + mocker.patch( + "septentrion.files.iter_files", + return_value=[ + pathlib.Path("schema_17.02.sql"), + pathlib.Path("schema_16.12.sql"), + ], ) - values = files.get_known_fixtures(settings=settings) - - assert values == ["fixtures_16.12.sql"] - - -def test_get_known_fixtures_unknown_path(mocker): - mocker.patch("os.listdir", side_effect=FileNotFoundError()) - settings = configuration.Settings.from_cli( - {"migrations_root": "tests/test_data/sql"} + values = files.get_special_files( + root=pathlib.Path("tests/test_data/sql"), folder="schemas" ) - values = files.get_known_fixtures(settings=settings) - - assert values == [] + expected = ["schema_16.12.sql", "schema_17.02.sql"] + assert sorted(values) == expected def test_get_known_versions(mocker): - mocker.patch("septentrion.files.list_dirs", return_value=["16.11", "16.12", "16.9"]) - mocker.patch("os.path.islink", return_value=False) + mocker.patch( + "septentrion.files.iter_dirs", + return_value=[ + pathlib.Path("16.11"), + pathlib.Path("16.12"), + pathlib.Path("16.9"), + ], + ) settings = configuration.Settings.from_cli({}) values = files.get_known_versions(settings=settings) @@ -89,36 +58,29 @@ def test_get_known_versions(mocker): def test_get_known_versions_error(mocker): - mocker.patch("septentrion.files.list_dirs", side_effect=OSError) + mocker.patch("septentrion.files.iter_dirs", side_effect=OSError) settings = configuration.Settings.from_cli({}) - with pytest.raises(ValueError): + with pytest.raises(exceptions.SeptentrionException): files.get_known_versions(settings=settings) -def test_get_migrations_files_mapping_ok(mocker): +def test_get_migrations_files_mapping(mocker): mocker.patch( - "septentrion.files.list_files", - return_value=["file.sql", "file.dml.sql", "file.ddl.sql"], + "septentrion.files.iter_files", + return_value=[ + pathlib.Path("tests/test_data/sql/17.1/manual/file.sql"), + pathlib.Path("tests/test_data/sql/17.1/manual/file.dml.sql"), + pathlib.Path("tests/test_data/sql/17.1/manual/file.ddl.sql"), + ], ) settings = configuration.Settings.from_cli( - {"migrations_root": "tests/test_data/sql"} + {"migrations_root": "tests/test_data/sql", "ignore_symlinks": True} ) values = files.get_migrations_files_mapping(settings=settings, version="17.1") assert values == { - "file.dml.sql": "tests/test_data/sql/17.1/manual/file.dml.sql", - "file.ddl.sql": "tests/test_data/sql/17.1/manual/file.ddl.sql", + "file.dml.sql": pathlib.Path("tests/test_data/sql/17.1/manual/file.dml.sql"), + "file.ddl.sql": pathlib.Path("tests/test_data/sql/17.1/manual/file.ddl.sql"), } - - -def test_get_migrations_files_mapping_ko(mocker): - mocker.patch("septentrion.files.list_files", side_effect=OSError) - - settings = configuration.Settings.from_cli( - {"migrations_root": "tests/test_data/sql"} - ) - - with pytest.raises(ValueError): - files.get_migrations_files_mapping(settings=settings, version="17.1") diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index 03256fe..f3e1b72 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -1,5 +1,6 @@ import io import logging +import pathlib import pytest @@ -123,7 +124,7 @@ def test_script_manual(): ) settings = configuration.Settings.from_cli({}) script = runner.Script( - settings=settings, file_handler=handler, name="/manual/foo.sql" + settings=settings, file_handler=handler, path=pathlib.Path("/manual/foo.sql") ) b1, b2, b3 = script.block_list diff --git a/tox.ini b/tox.ini index f732926..acced2b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{36}-{integration,unit}-tests,check-lint + py{36,37,38}-{integration,unit,acceptance}-tests,check-lint [testenv] whitelist_externals = make @@ -12,6 +12,7 @@ commands = pip freeze -l unit-tests: pytest tests/unit integration-tests: pytest tests/integration + acceptance-tests: pytest tests/acceptance [testenv:check-lint] extras = @@ -58,4 +59,3 @@ commands = # If this line breaks, fix with: # sort -bdfi docs/spelling_wordlist.txt -o docs/spelling_wordlist.txt sort -cbdfi docs/spelling_wordlist.txt -