Handle warning as error and exclude warning tests

This commit is contained in:
David Wobrock committed 2020-02-07 23:29:15 +01:00
1 parent d01875b3be
commit 71614bd377
6 files changed
+94 -46

No files matched your search

@@ -109,6 +109,9 @@ class Command(BaseCommand):
choices=MessageType.values(), choices=MessageType.values(),
help="don't print linting messages to stdout", help="don't print linting messages to stdout",
) )
parser.add_argument(
"--warning-as-error", action="store_true", help="handle warnings as errors",
)
def handle(self, *args, **options): def handle(self, *args, **options):
if options["project_root_path"]: if options["project_root_path"]:
@@ -136,6 +139,7 @@ class Command(BaseCommand):
only_unapplied_migrations=options["unapplied_migrations"], only_unapplied_migrations=options["unapplied_migrations"],
exclude_migration_tests=options["exclude_migration_tests"], exclude_migration_tests=options["exclude_migration_tests"],
quiet=options["quiet"], quiet=options["quiet"],
warning_as_error=options["warning_as_error"],
) )
linter.lint_all_migrations( linter.lint_all_migrations(
git_commit_id=options["commit_id"], git_commit_id=options["commit_id"],
+44 -30
View File
@@ -52,6 +52,7 @@ class MigrationLinter(object):
only_unapplied_migrations=False, only_unapplied_migrations=False,
exclude_migration_tests=None, exclude_migration_tests=None,
quiet=None, quiet=None,
warning_as_error=False,
): ):
# Store parameters and options # Store parameters and options
self.django_path = path self.django_path = path
@@ -66,6 +67,7 @@ class MigrationLinter(object):
self.only_applied_migrations = only_applied_migrations self.only_applied_migrations = only_applied_migrations
self.only_unapplied_migrations = only_unapplied_migrations self.only_unapplied_migrations = only_unapplied_migrations
self.quiet = quiet or [] self.quiet = quiet or []
self.warning_as_error = warning_as_error
# Initialise counters # Initialise counters
self.nb_valid = 0 self.nb_valid = 0
@@ -134,12 +136,17 @@ class MigrationLinter(object):
self.exclude_migration_tests, self.exclude_migration_tests,
) )
err, warnings = self.analyse_data_migration( err, ignored_data, warnings = self.analyse_data_migration(app_label, migration_name)
app_label, migration_name, self.exclude_migration_tests
)
if err: if err:
errors += err errors += err
if ignored_data:
ignored += ignored_data
if self.warning_as_error:
errors += warnings
warnings = []
# Fixme: have a more generic approach to handling errors/warnings/ignored/ok?
if errors: if errors:
self.print_linting_msg(app_label, migration_name, "ERR", MessageType.ERROR) self.print_linting_msg(app_label, migration_name, "ERR", MessageType.ERROR)
self.nb_erroneous += 1 self.nb_erroneous += 1
@@ -154,6 +161,7 @@ class MigrationLinter(object):
self.nb_warnings += 1 self.nb_warnings += 1
self.print_warnings(warnings) self.print_warnings(warnings)
value_to_cache = {"result": "WARNING", "warnings": warnings} value_to_cache = {"result": "WARNING", "warnings": warnings}
# Fixme: not displaying ignored errors, when
else: else:
if ignored: if ignored:
self.print_linting_msg( self.print_linting_msg(
@@ -215,10 +223,10 @@ class MigrationLinter(object):
if MessageType.ERROR.value in self.quiet: if MessageType.ERROR.value in self.quiet:
return return
for err in errors: for err in errors:
error_str = "\t{0}".format(err["err_msg"]) error_str = "\t{0}".format(err["msg"])
if err["table"]: if err.get("table"):
error_str += " (table: {0}".format(err["table"]) error_str += " (table: {0}".format(err["table"])
if err["column"]: if err.get("column"):
error_str += ", column: {0}".format(err["column"]) error_str += ", column: {0}".format(err["column"])
error_str += ")" error_str += ")"
print(error_str) print(error_str)
@@ -227,11 +235,8 @@ class MigrationLinter(object):
if MessageType.WARNING.value in self.quiet: if MessageType.WARNING.value in self.quiet:
return return
for function_name, warning_details in warnings.items(): for warning_details in warnings:
for warn_detail in warning_details: warn_str = "\tWARN {}".format(warning_details["msg"])
warn_str = "\tWARN '{}': {}".format(
function_name, warn_detail["warn_msg"]
)
print(warn_str) print(warn_str)
def print_summary(self): def print_summary(self):
@@ -361,45 +366,54 @@ class MigrationLinter(object):
) )
) )
def analyse_data_migration( def analyse_data_migration(self, app_label, migration_name):
self, app_label, migration_name, exclude_migration_tests
):
migration = self.migration_loader.disk_migrations[(app_label, migration_name)] migration = self.migration_loader.disk_migrations[(app_label, migration_name)]
errors = [] errors = []
warnings = defaultdict(list) ignored = []
warnings = []
for operation in migration.operations: for operation in migration.operations:
if isinstance(operation, migrations.RunPython): if isinstance(operation, migrations.RunPython):
function_name = operation.code.__name__ op_errors, op_ignored, op_warnings = self.lint_runpython(operation)
op_errors, op_warnings = self.lint_runpython(operation)
if op_errors: if op_errors:
errors += op_errors errors += op_errors
if op_ignored:
ignored += op_ignored
if op_warnings: if op_warnings:
warnings[function_name] = op_warnings warnings += op_warnings
return errors, warnings return errors, ignored, warnings
@staticmethod def lint_runpython(self, runpython):
def lint_runpython(runpython): function_name = runpython.code.__name__
error = [] error = []
ignored = []
warning = [] warning = []
if not runpython.reversible: if not runpython.reversible:
warning.append( issue = {
{
"code": "REVERSIBLE_DATA_MIGRATION", "code": "REVERSIBLE_DATA_MIGRATION",
"warn_msg": "RunPython data migration is not reversible", "msg": "'{}': RunPython data migration is not reversible".format(
function_name
),
} }
) if issue["code"] in self.exclude_migration_tests:
ignored.append(issue)
else:
warning.append(issue)
args_spec = inspect.getfullargspec(runpython.code) args_spec = inspect.getfullargspec(runpython.code)
if tuple(args_spec.args) != EXPECTED_DATA_MIGRATION_ARGS: if tuple(args_spec.args) != EXPECTED_DATA_MIGRATION_ARGS:
warning.append( issue = {
{
"code": "NAMING_CONVENTION_RUNPYTHON_ARGS", "code": "NAMING_CONVENTION_RUNPYTHON_ARGS",
"warn_msg": "By convention, RunPython names the two arguments: apps, schema_editor", "msg": "'{}': By convention, RunPython names the two arguments: apps, schema_editor".format(
function_name
),
} }
) if issue["code"] in self.exclude_migration_tests:
ignored.append(issue)
else:
warning.append(issue)
# TODO: test if importing model directly or correctly using "get_model()" # TODO: test if importing model directly or correctly using "get_model()"
return error, warning return error, ignored, warning
+8 -8
View File
@@ -37,32 +37,32 @@ class BaseAnalyser(object):
"code": "RENAME_TABLE", "code": "RENAME_TABLE",
"fn": lambda sql, **kw: re.search("RENAME TABLE", sql) "fn": lambda sql, **kw: re.search("RENAME TABLE", sql)
or re.search("ALTER TABLE .* RENAME TO", sql), or re.search("ALTER TABLE .* RENAME TO", sql),
"err_msg": "RENAMING tables", "msg": "RENAMING tables",
"mode": "one_liner", "mode": "one_liner",
}, },
{ {
"code": "NOT_NULL", "code": "NOT_NULL",
"fn": has_not_null_column, "fn": has_not_null_column,
"err_msg": "NOT NULL constraint on columns", "msg": "NOT NULL constraint on columns",
"mode": "transaction", "mode": "transaction",
}, },
{ {
"code": "DROP_COLUMN", "code": "DROP_COLUMN",
"fn": lambda sql, **kw: re.search("DROP COLUMN", sql), "fn": lambda sql, **kw: re.search("DROP COLUMN", sql),
"err_msg": "DROPPING columns", "msg": "DROPPING columns",
"mode": "one_liner", "mode": "one_liner",
}, },
{ {
"code": "DROP_TABLE", "code": "DROP_TABLE",
"fn": lambda sql, **kw: sql.startswith("DROP TABLE"), "fn": lambda sql, **kw: sql.startswith("DROP TABLE"),
"err_msg": "DROPPING table", "msg": "DROPPING table",
"mode": "one_liner", "mode": "one_liner",
}, },
{ {
"code": "RENAME_COLUMN", "code": "RENAME_COLUMN",
"fn": lambda sql, **kw: re.search("ALTER TABLE .* CHANGE", sql) "fn": lambda sql, **kw: re.search("ALTER TABLE .* CHANGE", sql)
or re.search("ALTER TABLE .* RENAME COLUMN", sql), or re.search("ALTER TABLE .* RENAME COLUMN", sql),
"err_msg": "RENAMING columns", "msg": "RENAMING columns",
"mode": "one_liner", "mode": "one_liner",
}, },
{ {
@@ -70,7 +70,7 @@ class BaseAnalyser(object):
"fn": lambda sql, **kw: re.search( "fn": lambda sql, **kw: re.search(
"ALTER TABLE .* ALTER COLUMN .* TYPE", sql "ALTER TABLE .* ALTER COLUMN .* TYPE", sql
), ),
"err_msg": ( "msg": (
"ALTERING columns (Could be backward compatible. " "ALTERING columns (Could be backward compatible. "
"You may ignore this migration.)" "You may ignore this migration.)"
), ),
@@ -79,7 +79,7 @@ class BaseAnalyser(object):
{ {
"code": "ADD_UNIQUE", "code": "ADD_UNIQUE",
"fn": has_add_unique, "fn": has_add_unique,
"err_msg": "ADDING unique constraint", "msg": "ADDING unique constraint",
"mode": "transaction", "mode": "transaction",
}, },
] ]
@@ -126,7 +126,7 @@ class BaseAnalyser(object):
table = self.detect_table(sql_statement) table = self.detect_table(sql_statement)
col = self.detect_column(sql_statement) col = self.detect_column(sql_statement)
return { return {
"err_msg": migration_test["err_msg"], "msg": migration_test["msg"],
"code": migration_test["code"], "code": migration_test["code"],
"table": table, "table": table,
"column": col, "column": col,
@@ -35,7 +35,7 @@ class SqliteAnalyser(BaseAnalyser):
sql.startswith("DROP TABLE") for sql in sql_statements sql.startswith("DROP TABLE") for sql in sql_statements
) )
and not any(sql.startswith("CREATE TABLE") for sql in sql_statements), and not any(sql.startswith("CREATE TABLE") for sql in sql_statements),
"err_msg": "DROPPING table", "msg": "DROPPING table",
"mode": "transaction", "mode": "transaction",
}, },
{ {
+4 -4
View File
@@ -53,7 +53,7 @@ class CacheTestCase(unittest.TestCase):
self.assertListEqual( self.assertListEqual(
[ [
{ {
"err_msg": "NOT NULL constraint on columns", "msg": "NOT NULL constraint on columns",
"code": "NOT_NULL", "code": "NOT_NULL",
"table": None, "table": None,
"column": None, "column": None,
@@ -105,7 +105,7 @@ class CacheTestCase(unittest.TestCase):
self.assertListEqual( self.assertListEqual(
[ [
{ {
"err_msg": "NOT NULL constraint on columns", "msg": "NOT NULL constraint on columns",
"code": "NOT_NULL", "code": "NOT_NULL",
"table": None, "table": None,
"column": None, "column": None,
@@ -132,7 +132,7 @@ class CacheTestCase(unittest.TestCase):
self.assertListEqual( self.assertListEqual(
[ [
{ {
"err_msg": "NOT NULL constraint on columns", "msg": "NOT NULL constraint on columns",
"code": "NOT_NULL", "code": "NOT_NULL",
"table": None, "table": None,
"column": None, "column": None,
@@ -245,7 +245,7 @@ class CacheTestCase(unittest.TestCase):
self.assertListEqual( self.assertListEqual(
[ [
{ {
"err_msg": "NOT NULL constraint on columns", "msg": "NOT NULL constraint on columns",
"code": "NOT_NULL", "code": "NOT_NULL",
"table": None, "table": None,
"column": None, "column": None,
+33 -3
View File
@@ -11,8 +11,7 @@ class DataMigrationDetectionTestCase(unittest.TestCase):
def setUp(self, *args, **kwargs): def setUp(self, *args, **kwargs):
self.test_project_path = os.path.dirname(settings.BASE_DIR) self.test_project_path = os.path.dirname(settings.BASE_DIR)
self.linter = MigrationLinter( self.linter = MigrationLinter(
self.test_project_path, self.test_project_path, include_apps=fixtures.DATA_MIGRATIONS,
include_apps=fixtures.DATA_MIGRATIONS,
) )
def test_reverse_data_migration(self): def test_reverse_data_migration(self):
@@ -26,7 +25,6 @@ class DataMigrationDetectionTestCase(unittest.TestCase):
self.assertFalse(self.linter.has_errors) self.assertFalse(self.linter.has_errors)
def test_reverse_data_migration_ignore(self): def test_reverse_data_migration_ignore(self):
self.assertEqual(0, self.linter.nb_warnings)
reverse_migration = self.linter.migration_loader.disk_migrations[ reverse_migration = self.linter.migration_loader.disk_migrations[
("app_data_migrations", "0003_incorrect_arguments") ("app_data_migrations", "0003_incorrect_arguments")
] ]
@@ -34,3 +32,35 @@ class DataMigrationDetectionTestCase(unittest.TestCase):
self.assertEqual(1, self.linter.nb_warnings) self.assertEqual(1, self.linter.nb_warnings)
self.assertFalse(self.linter.has_errors) self.assertFalse(self.linter.has_errors)
def test_exclude_warning_from_test(self):
self.linter = MigrationLinter(
self.test_project_path,
include_apps=fixtures.DATA_MIGRATIONS,
exclude_migration_tests=("REVERSIBLE_DATA_MIGRATION",),
)
reverse_migration = self.linter.migration_loader.disk_migrations[
("app_data_migrations", "0002_missing_reverse")
]
self.linter.lint_migration(reverse_migration)
self.assertEqual(0, self.linter.nb_warnings)
self.assertEqual(1, self.linter.nb_valid)
self.assertFalse(self.linter.has_errors)
def test_warning_as_error(self):
self.linter = MigrationLinter(
self.test_project_path,
include_apps=fixtures.DATA_MIGRATIONS,
warning_as_error=True,
)
reverse_migration = self.linter.migration_loader.disk_migrations[
("app_data_migrations", "0003_incorrect_arguments")
]
self.linter.lint_migration(reverse_migration)
self.assertEqual(0, self.linter.nb_warnings)
self.assertEqual(1, self.linter.nb_erroneous)
self.assertTrue(self.linter.has_errors)