mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	This refactors `parse_migration_status` to copy the algorithm of Django's `showmigrations` command instead of parsing its output. This is done so that the code is not susceptible to breaking changes if Django modifies showmigrations's implementation. The previous `parse_migration_status` logic has been repurposed into a test utility function (`prase_showmigrations`). It is used to verify that the new `parse_migration_status` generates output identical to the actual `showmigrations` command. The `test_clean_up_migration_status_json` is removed because `test_parse_migration_status` has covered that behavior.
		
			
				
	
	
		
			146 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			146 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from unittest.mock import patch
 | 
						|
 | 
						|
from django.db import connection
 | 
						|
from django.db.migrations.recorder import MigrationRecorder
 | 
						|
 | 
						|
from zerver.lib.migration_status import (
 | 
						|
    STALE_MIGRATIONS,
 | 
						|
    AppMigrations,
 | 
						|
    get_migration_status,
 | 
						|
    parse_migration_status,
 | 
						|
)
 | 
						|
from zerver.lib.test_classes import ZulipTestCase
 | 
						|
 | 
						|
 | 
						|
class MigrationStatusTests(ZulipTestCase):
 | 
						|
    def parse_showmigrations(
 | 
						|
        self,
 | 
						|
        migration_status_print: str,
 | 
						|
        stale_migrations: list[tuple[str, str]] = STALE_MIGRATIONS,
 | 
						|
    ) -> AppMigrations:
 | 
						|
        """
 | 
						|
        Parses the output of Django's `showmigrations` into a data structure
 | 
						|
        identical to the output `parse_migration_status` generates.
 | 
						|
 | 
						|
        Makes sure this accurately parses the output of `showmigrations`.
 | 
						|
        """
 | 
						|
        lines = migration_status_print.strip().split("\n")
 | 
						|
        migrations_dict: AppMigrations = {}
 | 
						|
        current_app = None
 | 
						|
        line_prefix = ("[X]", "[ ]", "[-]", "(no migrations)")
 | 
						|
 | 
						|
        stale_migrations_dict: dict[str, list[str]] = {}
 | 
						|
        for app, migration in stale_migrations:
 | 
						|
            if app not in stale_migrations_dict:
 | 
						|
                stale_migrations_dict[app] = []
 | 
						|
            stale_migrations_dict[app].append(migration)
 | 
						|
 | 
						|
        for line in lines:
 | 
						|
            line = line.strip()
 | 
						|
            if not line.startswith(line_prefix) and line:
 | 
						|
                current_app = line
 | 
						|
                migrations_dict[current_app] = []
 | 
						|
            elif line.startswith(line_prefix):
 | 
						|
                assert current_app is not None
 | 
						|
                apps_stale_migrations = stale_migrations_dict.get(current_app)
 | 
						|
                if (
 | 
						|
                    apps_stale_migrations is not None
 | 
						|
                    and line != "(no migrations)"
 | 
						|
                    and line[4:] in apps_stale_migrations
 | 
						|
                ):
 | 
						|
                    continue
 | 
						|
                migrations_dict[current_app].append(line)
 | 
						|
 | 
						|
        # Installed apps that have no migrations and we still use will have
 | 
						|
        # "(no migrations)" as its only "migrations" list. Ones that just
 | 
						|
        # have [] means it's just a left over stale app we can clean up.
 | 
						|
        return {app: migrations for app, migrations in migrations_dict.items() if migrations != []}
 | 
						|
 | 
						|
    def test_parse_showmigrations(self) -> None:
 | 
						|
        """
 | 
						|
        This function tests a helper test function `parse_showmigrations`.
 | 
						|
        It is critical that this correctly checks the behavior of
 | 
						|
        `parse_showmigrations`. Make sure it is accurately parsing the
 | 
						|
        output of `showmigrations`.
 | 
						|
        """
 | 
						|
        showmigrations_sample = """
 | 
						|
analytics
 | 
						|
 [X] 0001_squashed_0021_alter_fillstate_id (21 squashed migrations)
 | 
						|
auth
 | 
						|
 [ ] 0012_alter_user_first_name_max_length
 | 
						|
zerver
 | 
						|
 [-] 0015_alter_confirmation_object_id
 | 
						|
two_factor
 | 
						|
 (no migrations)
 | 
						|
"""
 | 
						|
        app_migrations = self.parse_showmigrations(showmigrations_sample)
 | 
						|
        expected: AppMigrations = {
 | 
						|
            "analytics": ["[X] 0001_squashed_0021_alter_fillstate_id (21 squashed migrations)"],
 | 
						|
            "auth": ["[ ] 0012_alter_user_first_name_max_length"],
 | 
						|
            "zerver": ["[-] 0015_alter_confirmation_object_id"],
 | 
						|
            "two_factor": ["(no migrations)"],
 | 
						|
        }
 | 
						|
        self.assertDictEqual(app_migrations, expected)
 | 
						|
 | 
						|
        # Run one with the real showmigrations. A more thorough tests of these
 | 
						|
        # functions are done in the test_import_export.py as part of the import-
 | 
						|
        # export suite.
 | 
						|
        showmigrations = get_migration_status(app_label="zerver")
 | 
						|
        app_migrations = self.parse_showmigrations(showmigrations)
 | 
						|
        zerver_migrations = app_migrations.get("zerver")
 | 
						|
        self.assertIsNotNone(zerver_migrations)
 | 
						|
        self.assertNotEqual(zerver_migrations, [])
 | 
						|
 | 
						|
    def test_parse_showmigrations_filters_out_stale_migrations(self) -> None:
 | 
						|
        """
 | 
						|
        This function tests a helper test function `parse_showmigrations`.
 | 
						|
        It is critical that this correctly checks the behavior of
 | 
						|
        `parse_showmigrations`. Make sure it is accurately parsing the
 | 
						|
        output of `showmigrations`.
 | 
						|
        """
 | 
						|
        assert ("guardian", "0001_initial") in STALE_MIGRATIONS
 | 
						|
        showmigrations_sample = """
 | 
						|
analytics
 | 
						|
 [X] 0001_squashed_0021_alter_fillstate_id (21 squashed migrations)
 | 
						|
auth
 | 
						|
 [ ] 0012_alter_user_first_name_max_length
 | 
						|
zerver
 | 
						|
 [-] 0015_alter_confirmation_object_id
 | 
						|
two_factor
 | 
						|
 (no migrations)
 | 
						|
guardian
 | 
						|
 [X] 0001_initial
 | 
						|
"""
 | 
						|
        app_migrations = self.parse_showmigrations(showmigrations_sample)
 | 
						|
        expected: AppMigrations = {
 | 
						|
            "analytics": ["[X] 0001_squashed_0021_alter_fillstate_id (21 squashed migrations)"],
 | 
						|
            "auth": ["[ ] 0012_alter_user_first_name_max_length"],
 | 
						|
            "zerver": ["[-] 0015_alter_confirmation_object_id"],
 | 
						|
            "two_factor": ["(no migrations)"],
 | 
						|
        }
 | 
						|
        self.assertDictEqual(app_migrations, expected)
 | 
						|
 | 
						|
    def test_parse_migration_status(self) -> None:
 | 
						|
        """
 | 
						|
        This test asserts that the algorithm in `parse_migration_status` is the same
 | 
						|
        as Django's `showmigrations`.
 | 
						|
        """
 | 
						|
        migration_status_print = get_migration_status()
 | 
						|
        parsed_showmigrations = self.parse_showmigrations(migration_status_print)
 | 
						|
        migration_status_dict = parse_migration_status()
 | 
						|
        self.assertDictEqual(migration_status_dict, parsed_showmigrations)
 | 
						|
 | 
						|
    def test_applied_but_not_recorded(self) -> None:
 | 
						|
        # Mock applied_migrations() to simulate empty recorded_migrations.
 | 
						|
        with patch(
 | 
						|
            "zerver.lib.migration_status.MigrationRecorder.applied_migrations",
 | 
						|
        ):
 | 
						|
            result = parse_migration_status()
 | 
						|
            self.assertIn("[-] 0010_alter_group_name_max_length", result["auth"])
 | 
						|
 | 
						|
    def test_generate_unapplied_migration(self) -> None:
 | 
						|
        recorder = MigrationRecorder(connection)
 | 
						|
        recorder.record_unapplied("auth", "0010_alter_group_name_max_length")
 | 
						|
        result = parse_migration_status()
 | 
						|
        self.assertIn("[ ] 0010_alter_group_name_max_length", result["auth"])
 |