diff --git a/README.rst b/README.rst index d1c5161..9c47e3f 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,8 @@ Usage ``--exclude-apps EXCLUDE_APPS [EXCLUDE_APPS ...]`` Ignore migrations that are in the specified django apps. ``--verbose or -v`` Print more information during execution. ``--database DATABASE`` Specify the database for which to generate the SQL. Defaults to *default*. +``--cache-path PATH`` specify a directory that should be used to store cache-files in. +``--no-cache`` Don't use a cache. ================================================== =========================================================================================================================== Examples @@ -68,6 +70,15 @@ You can also ignore migrations by adding this to your migrations: ] # ... +Cache +----- +By default, the linter uses a cache to prevent linting the same migration again. +The default location of the cache on Linux is +``/home//.cache/django-migration-linter//.pickle``. + +Since the linter uses hashes, modifying migration files, will re-run the linter on that migration. +If you want to run the linter without cache, use the flag ``--no-cache``. +If you want to invalidate the cache, delete the cache folder. Tests ----- diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py new file mode 100644 index 0000000..0ddd08c --- /dev/null +++ b/django_migration_linter/cache.py @@ -0,0 +1,52 @@ +# Copyright 2018 3YOURMIND GmbH + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import hashlib +import os +import pickle + +from django_migration_linter.utils import split_path, compose_migration_path + + +class Cache(dict): + def __init__(self, django_folder, cache_path): + self.django_folder = django_folder + project_name = split_path(django_folder)[-2] + self.filename = \ + os.path.join(cache_path, '{0}.pickle'.format(project_name)) + + if not os.path.exists(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + + super(Cache, self).__init__() + + def load(self): + try: + with open(self.filename, 'rb') as f: + tmp_dict = pickle.load(f) + self.update(tmp_dict) + except IOError: + pass + + def save(self): + with open(self.filename, 'wb') as f: + pickle.dump(self, f, protocol=2) + + def md5(self, app_name, migration): + path = compose_migration_path(self.django_folder, app_name, migration) + hash_md5 = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py new file mode 100644 index 0000000..1bdfc9b --- /dev/null +++ b/django_migration_linter/constants.py @@ -0,0 +1,23 @@ +# Copyright 2018 3YOURMIND GmbH + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from appdirs import user_cache_dir + +__version__ = '0.0.7' + +MIGRATION_FOLDER_NAME = 'migrations' + +DEFAULT_CACHE_PATH = user_cache_dir( + "django-migration-linter", version=__version__ +) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index e049ed5..e3d1fd3 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -19,6 +19,10 @@ import re from subprocess import Popen, PIPE import sys +from django_migration_linter.cache import Cache +from django_migration_linter.constants import DEFAULT_CACHE_PATH, \ + MIGRATION_FOLDER_NAME +from django_migration_linter.utils import split_migration_path from . import utils from .sql_analyser import analyse_sql_statements @@ -26,7 +30,6 @@ logger = logging.getLogger(__name__) class MigrationLinter(object): - MIGRATION_FOLDER_NAME = 'migrations' def __init__(self, project_path, **kwargs): # Verify correctness @@ -46,6 +49,9 @@ class MigrationLinter(object): self.include_apps = kwargs.get('include_apps', None) self.exclude_apps = kwargs.get('exclude_apps', None) self.database = kwargs.get('database', None) or 'default' + self.cache_path = kwargs.get('cache_path', None) or DEFAULT_CACHE_PATH + self.no_cache = kwargs.get('no_cache', None) or False + self.python_exe = '{0}/bin/{1}'.format(sys.prefix, 'python') if \ hasattr(sys, 'real_prefix') else 'python' @@ -55,10 +61,34 @@ class MigrationLinter(object): self.nb_erroneous = 0 self.nb_total = 0 + # Initialise cache. Read from old, write to new to prune old entries. + self.old_cache = Cache(self.django_path, self.cache_path) + self.new_cache = Cache(self.django_path, self.cache_path) + if not self.no_cache: + self.old_cache.load() + def lint_migration(self, app_name, migration_name): print('({0}, {1})... '.format(app_name, migration_name), end='') self.nb_total += 1 + md5hash = self.old_cache.md5(app_name, migration_name) + if md5hash in self.old_cache: + if self.old_cache[md5hash]['result'] == 'IGNORE': + print('IGNORE (cached)') + self.nb_ignored += 1 + elif self.old_cache[md5hash]['result'] == 'OK': + print('OK (cached)') + self.nb_valid += 1 + else: + print('ERR (cached)') + self.nb_erroneous += 1 + + if 'errors' in self.old_cache[md5hash]: + self.print_errors(self.old_cache[md5hash]['errors']) + + self.new_cache[md5hash] = self.old_cache[md5hash] + return + if self.should_ignore_migration(app_name, migration_name): print('IGNORE') self.nb_ignored += 1 @@ -72,17 +102,23 @@ class MigrationLinter(object): if analysis_result['ignored']: print('IGNORE') + self.new_cache[md5hash] = {'result': 'IGNORE'} self.nb_ignored += 1 return if not errors: print('OK') + self.new_cache[md5hash] = {'result': 'OK'} self.nb_valid += 1 return - # Print errors print('ERR') + self.new_cache[md5hash] = {'result': 'ERR', 'errors': errors} self.nb_erroneous += 1 + self.print_errors(errors) + + @staticmethod + def print_errors(errors): for err in errors: error_str = '\t{0}'.format(err['err_msg']) if err['table']: @@ -108,6 +144,9 @@ class MigrationLinter(object): for m in migrations: self.lint_migration(*m) + if not self.no_cache: + self.new_cache.save() + def print_summary(self): print('*** Summary:') print(('Valid migrations: {1}/{0} - ' @@ -173,10 +212,10 @@ class MigrationLinter(object): utils.clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if re.search( - r'\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME), + r'\/{0}\/.*\.py'.format(MIGRATION_FOLDER_NAME), line) and \ '__init__' not in line: - app_name, migration_name = self._split_migration_path(line) + app_name, migration_name = split_migration_path(line) migrations.append((app_name, migration_name)) diff_process.wait() @@ -194,11 +233,11 @@ class MigrationLinter(object): migrations = [] for root, dirs, files in os.walk(self.django_path): for file_name in sorted(files): - if os.path.basename(root) == self.MIGRATION_FOLDER_NAME and \ + if os.path.basename(root) == MIGRATION_FOLDER_NAME and \ file_name.endswith('.py') and \ file_name != '__init__.py': full_migration_path = os.path.join(root, file_name) - app_name, migration_name = self._split_migration_path( + app_name, migration_name = split_migration_path( full_migration_path) migrations.append((app_name, migration_name)) return migrations @@ -212,14 +251,6 @@ class MigrationLinter(object): self.ignore_name_contains in migration_name)\ or (migration_name in self.ignore_name) - @classmethod - def _split_migration_path(cls, migration_path): - decomposed_path = utils.split_path(migration_path) - for i, p in enumerate(decomposed_path): - if p == cls.MIGRATION_FOLDER_NAME: - return (decomposed_path[i-1], - os.path.splitext(decomposed_path[i+1])[0]) - def _main(): import argparse @@ -260,6 +291,18 @@ def _main(): help=('specify the database for which to generate the SQL. ' 'Defaults to default')) + cache_group = parser.add_mutually_exclusive_group( + required=False) + cache_group.add_argument( + '--cache-path', + type=str, + help='specify a directory that should be used to' + 'store cache-files in.') + cache_group.add_argument( + '--no-cache', + action='store_true', + help='don\'t use a cache') + incl_excl_group = parser.add_mutually_exclusive_group( required=False) incl_excl_group.add_argument( @@ -287,7 +330,10 @@ def _main(): ignore_name=args.ignore_name, include_apps=args.include_apps, exclude_apps=args.exclude_apps, - database=args.database) + database=args.database, + cache_path=args.cache_path, + no_cache=args.no_cache + ) linter.lint_all_migrations( git_commit_id=args.commit_id) linter.print_summary() diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index b2b8f23..355df5f 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -13,9 +13,12 @@ # limitations under the License. from __future__ import print_function + import os import sys +from django_migration_linter.constants import MIGRATION_FOLDER_NAME + def is_django_project(path): django_manage_file = os.path.join(path, 'manage.py') @@ -52,5 +55,22 @@ def split_path(path): return (split_path(a) if (len(a) > 0 and a != '/') else []) + [b] +def split_migration_path(migration_path): + decomposed_path = split_path(migration_path) + for i, p in enumerate(decomposed_path): + if p == MIGRATION_FOLDER_NAME: + return (decomposed_path[i-1], + os.path.splitext(decomposed_path[i+1])[0]) + + +def compose_migration_path(django_folder, app_name, migration): + return os.path.join( + django_folder, + app_name, + MIGRATION_FOLDER_NAME, + '{0}.py'.format(migration) + ) + + def clean_bytes_to_str(byte_input): return byte_input.decode('utf-8').strip() diff --git a/setup.py b/setup.py index 79ca34d..dd6bee3 100644 --- a/setup.py +++ b/setup.py @@ -11,18 +11,30 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import ast +import re from os import path from setuptools import setup, find_packages PROJECT_DIR = path.abspath(path.dirname(__file__)) + +def get_version(): + constants = path.join(PROJECT_DIR, 'django_migration_linter', 'constants.py') + _version_re = re.compile(r"__version__\s+=\s+(?P.*)") + with open(constants, "r") as f: + match = _version_re.search(f.read()) + version = match.group("version") if match is not None else '"unknown"' + return str(ast.literal_eval(version)) + + with open(path.join(PROJECT_DIR, 'README.rst')) as f: long_description = f.read() install_requirements = [ 'django>=1.11', + 'appdirs==1.4.3' ] test_requirements = [ @@ -31,9 +43,10 @@ test_requirements = [ 'django-fake-database-backends', ] +# noinspection PyUnresolvedReferences setup( name='django-migration-linter', - version='0.0.7', + version=get_version(), description='Detect backward incompatible migrations for your django project', long_description=long_description, diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py new file mode 100644 index 0000000..ae289b2 --- /dev/null +++ b/tests/functional/test_cache.py @@ -0,0 +1,165 @@ +# Copyright 2018 3YOURMIND GmbH + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import pprint +import shutil +import tempfile +import unittest +import sys + +from tests import fixtures + +from django_migration_linter import MigrationLinter, Cache, DEFAULT_CACHE_PATH + +if sys.version_info >= (3, 3): + import unittest.mock as mock +else: + import mock + + +class CacheTest(unittest.TestCase): + MIGRATION_FILE = os.path.join(fixtures.ALTER_COLUMN_PROJECT, 'test_app', 'migrations', '0001_initial.py') + fd, temp_path = tempfile.mkstemp() + + def setUp(self): + shutil.copy2(self.MIGRATION_FILE, self.temp_path) + + def tearDown(self): + shutil.copy2(self.temp_path, self.MIGRATION_FILE) + os.remove(self.temp_path) + + def test_cache_normal(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_add_not_null_column.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + linter.lint_all_migrations() + self.assertEqual(sql_mock.call_count, 2) + + cache = Cache( + fixtures.ADD_NOT_NULL_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertEqual(cache['3ef74e7f3e53e273e2fc95379248d58d']['result'], 'OK') + self.assertEqual(cache['e1e312b6d08ecbe017c25c58fc2be257']['result'], 'ERR') + self.assertListEqual( + cache['e1e312b6d08ecbe017c25c58fc2be257']['errors'], + [{'err_msg': 'RENAMING tables', 'code': 'RENAME_TABLE', 'table': None, 'column': None}] + ) + + # Start the Linter again -> should use cache now. + linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: + linter.lint_all_migrations() + self.assertEqual(sql_mock.call_count, 0) + + self.assertTrue(linter.has_errors) + + def test_cache_ignored(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_ignore_migration.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: + linter.lint_all_migrations() + self.assertEqual(sql_mock.call_count, 2) + + cache = Cache( + fixtures.IGNORE_MIGRATION_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertEqual(cache['63230606af0eccaef7f1f78c537c624c']['result'], 'OK') + self.assertEqual(cache['5c5ca1780a9f28439c1defc1f32af894']['result'], 'IGNORE') + + # Start the Linter again -> should use cache now. + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: + linter.lint_all_migrations() + self.assertEqual(sql_mock.call_count, 0) + + def test_cache_ignored_command_line(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_ignore_migration.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT, + ignore_name_contains='0001') + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: + linter.lint_all_migrations() + self.assertEqual(sql_mock.call_count, 1) + + cache = Cache( + fixtures.IGNORE_MIGRATION_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertNotIn('63230606af0eccaef7f1f78c537c624c', cache) + self.assertEqual(cache['5c5ca1780a9f28439c1defc1f32af894']['result'], 'IGNORE') + + def test_cache_modified(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_alter_column.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.ALTER_COLUMN_PROJECT) + linter.lint_all_migrations() + cache = Cache( + fixtures.ALTER_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertEqual(cache['8589aa107b6da296c4b49cd2681d2230']['result'], 'OK', + 'If this fails, tearDown might have failed to remove ' + 'the modification from tests/test_project_fixtures/' + 'test_project_alter_column/test_app/migrations/0001_initial.py' + ) + self.assertEqual(cache['8f54c4a434cfaa9838e8ca12eb988255']['result'], 'ERR') + self.assertListEqual( + cache['8f54c4a434cfaa9838e8ca12eb988255']['errors'], + [{u'err_msg': u'ALTERING columns (Could be backward compatible. ' + u'You may ignore this migration.)', + u'code': u'ALTER_COLUMN', + u'table': u'test_app_a', + u'column': None} + ] + ) + + # Modify migration + with open(self.MIGRATION_FILE, "a") as f: + f.write("# modification at the end of the file\n") + + # Start the Linter again -> Cache should look different now + linter = MigrationLinter(fixtures.ALTER_COLUMN_PROJECT) + linter.lint_all_migrations() + cache = Cache( + fixtures.ALTER_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(cache) + + self.assertNotIn('8589aa107b6da296c4b49cd2681d2230', cache) + self.assertEqual(cache['44f9d6d019d6f158946c2819b2bd8bf9']['result'], 'OK') diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index abc9479..b8ddabe 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import shutil import unittest from subprocess import Popen, PIPE -from django_migration_linter import utils +from django_migration_linter import utils, DEFAULT_CACHE_PATH from tests import fixtures import sys @@ -27,7 +29,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): cls.linter_exec = '{0}/bin/{1}'.format(sys.prefix, linter_name) if hasattr(sys, 'real_prefix') else linter_name def test_call_linter_cmd_line_working(self): - cmd = '{0} {1}'.format(self.linter_exec, fixtures.CORRECT_PROJECT) + cmd = '{0} --no-cache {1}'.format(self.linter_exec, fixtures.CORRECT_PROJECT) process = Popen( cmd, shell=True, stdout=PIPE, stderr=PIPE) @@ -45,7 +47,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): ) def test_call_linter_cmd_line_errors(self): - cmd = '{0} {1}'.format( + cmd = '{0} --no-cache {1}'.format( self.linter_exec, fixtures.ADD_NOT_NULL_COLUMN_PROJECT) @@ -60,7 +62,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue('RENAMING tables' in lines[2]) def test_call_linter_cmd_line_exclude_apps(self): - cmd = '{0} {1} --exclude-apps test_app2'.format( + cmd = '{0} --no-cache {1} --exclude-apps test_app2'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -80,7 +82,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): ) def test_call_linter_cmd_line_include_apps(self): - cmd = '{0} {1} --include-apps test_app2'.format( + cmd = '{0} --no-cache {1} --include-apps test_app2'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -100,7 +102,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): ) def test_call_linter_cmd_line_ignore_name(self): - cmd = '{0} {1} --ignore-name 0001_initial'.format( + cmd = '{0} --no-cache {1} --ignore-name 0001_initial'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -120,7 +122,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): ) def test_call_linter_cmd_line_ignore_name_contains(self): - cmd = '{0} {1} --ignore-name-contains 0001'.format( + cmd = '{0} --no-cache {1} --ignore-name-contains 0001'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -139,8 +141,53 @@ class CallLinterFromCommandLineTest(unittest.TestCase): ]) ) + def test_call_linter_cmd_line_cache(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_correct_project.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + cmd = '{0} {1}'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertTrue(os.path.exists(DEFAULT_CACHE_PATH)) + + def test_call_linter_cmd_line_cache_path(self): + if os.path.exists('/tmp/migration-linter-cache-tests/'): + shutil.rmtree('/tmp/migration-linter-cache-tests/') + cmd = '{0} {1} --cache-path={2}'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT, + '/tmp/migration-linter-cache-tests/' + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + self.assertTrue(os.path.exists('/tmp/migration-linter-cache-tests')) + + def test_call_linter_cmd_line_no_cache(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_correct_project.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + + cmd = '{0} --no-cache {1}'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + self.assertFalse(os.path.exists(cache_file)) + def test_call_linter_cmd_line_git_id(self): - cmd = '{0} {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( + cmd = '{0} --no-cache {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( self.linter_exec, fixtures.MULTI_COMMIT_PROJECT) fixtures.prepare_git_project(fixtures.MULTI_COMMIT_PROJECT) @@ -155,7 +202,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[1].startswith('*** Summary')) def test_call_linter_with_deleted_migrations(self): - cmd = '{0} {1} 154ecf6119325cc3b1f3f5a4e709bfbd61a4a4ba'.format( + cmd = '{0} --no-cache {1} 154ecf6119325cc3b1f3f5a4e709bfbd61a4a4ba'.format( self.linter_exec, fixtures.DELETED_MIGRATION_PROJECT) fixtures.prepare_git_project(fixtures.DELETED_MIGRATION_PROJECT) diff --git a/tests/functional/test_multiple_calls.py b/tests/functional/test_multiple_calls.py index bea9262..0920c1b 100644 --- a/tests/functional/test_multiple_calls.py +++ b/tests/functional/test_multiple_calls.py @@ -24,10 +24,10 @@ class TestMultipleLinters(unittest.TestCase): l2 = MigrationLinter(fixtures.RENAME_COLUMN_PROJECT) l3 = MigrationLinter(fixtures.CORRECT_PROJECT) - l1.lint_migration('test_app', '0002') - l2.lint_migration('test_app', '0002') - l3.lint_migration('test_app1', '0001') - l3.lint_migration('test_app1', '0002') + l1.lint_migration('test_app', '0002_add_new_not_null_field') + l2.lint_migration('test_app', '0002_rename_column') + l3.lint_migration('test_app1', '0001_initial') + l3.lint_migration('test_app1', '0002_a_new_null_field') self.assertTrue(l1.has_errors) self.assertTrue(l2.has_errors) diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 57e449e..1647b10 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -32,13 +32,13 @@ class TestLinterFunctions(unittest.TestCase): linter = MigrationLinter(project_path) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0001') + linter.lint_migration('test_app', '0001_create_table') self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0002') + linter.lint_migration('test_app', '0002_add_new_not_null_field') self.assertTrue(linter.has_errors) - linter.lint_migration('test_app', '0001') + linter.lint_migration('test_app', '0001_create_table') self.assertTrue(linter.has_errors) def test_linter_creation(self): @@ -49,24 +49,6 @@ class TestLinterFunctions(unittest.TestCase): with self.assertRaises(ValueError): MigrationLinter(fixtures.NOT_DJANGO_GIT_PROJECT) - def test_split_migration_long_path(self): - input_path = 'apps/the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - - def test_split_migration_path(self): - input_path = 'the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - - def test_split_migration_full_path(self): - input_path = '/home/user/djangostuff/apps/the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - def test_ignore_migration_include_apps(self): linter = MigrationLinter( fixtures.ADD_NOT_NULL_COLUMN_PROJECT, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 88d20af..7ac89d5 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -16,7 +16,8 @@ import os import unittest from tests import fixtures -from django_migration_linter.utils import is_django_project, is_git_project, is_directory, find_project_settings_module, split_path +from django_migration_linter.utils import is_django_project, is_git_project, is_directory, find_project_settings_module, \ + split_path, split_migration_path class UtilityFunctionTest(unittest.TestCase): @@ -70,3 +71,21 @@ class UtilityFunctionTest(unittest.TestCase): self.assertEqual(splitted[0], 'foo') self.assertEqual(splitted[1], 'bar') self.assertEqual(splitted[2], 'fuz.py') + + def test_split_migration_long_path(self): + input_path = 'apps/the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') + + def test_split_migration_path(self): + input_path = 'the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') + + def test_split_migration_full_path(self): + input_path = '/home/user/djangostuff/apps/the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') diff --git a/tox.ini b/tox.ini index 7ffc655..a420feb 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,8 @@ pip_pre = true deps = django111: django>=1.11,<2.0 django20: django>=2.0 + appdirs==1.4.3 + py27: mock==2.0.0 [testenv:lint] basepython = python3.6