Created
April 22, 2026 09:39
-
-
Save humitos/3fda5e1564fee34174ab8578a737a60f to your computer and use it in GitHub Desktop.
Migration test for 0163_automationrule_data_migration: RegexAutomationRule → projects.AutomationRule
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ============================= test session starts ============================== | |
| platform linux -- Python 3.12.3, pytest-9.0.3, pluggy-1.6.0 -- /home/humitos/rtfd/code/readthedocs.org/.tox/py312/bin/python3 | |
| cachedir: .pytest_cache | |
| django: version: 5.2.13, settings: readthedocs.settings.test (from option) | |
| rootdir: /home/humitos/rtfd/code/readthedocs.org | |
| configfile: pytest.ini | |
| plugins: django-4.12.0, mock-3.15.1, requests-mock-1.12.1, anyio-4.13.0, cov-7.1.0, custom-exit-code-0.3.0 | |
| collecting ... collected 6 items | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_basic_rule_fields_are_mapped PASSED [ 16%] | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_null_predefined_match_arg_becomes_custom_match PASSED [ 33%] | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_single_rule_with_multiple_matches PASSED [ 50%] | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_multiple_rules_with_multiple_matches_each PASSED [ 66%] | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_rules_across_multiple_projects_are_isolated PASSED [ 83%] | |
| readthedocs/projects/tests/test_migrations_automation_rule_data.py::test_migration_rule_with_no_matches PASSED [100%] | |
| ============================== 6 passed in 52.02s ============================== |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from datetime import timedelta | |
| from importlib import import_module | |
| import pytest | |
| from django.apps import apps | |
| from django.utils import timezone | |
| from readthedocs.builds.models import AutomationRuleMatch as LegacyAutomationRuleMatch | |
| from readthedocs.builds.models import RegexAutomationRule | |
| from readthedocs.projects.models import AutomationRule | |
| from readthedocs.projects.models import AutomationRuleMatch | |
| from readthedocs.projects.models import Project | |
| def _run_migration(): | |
| migration = import_module( | |
| "readthedocs.projects.migrations.0163_automationrule_data_migration" | |
| ) | |
| migration.forward_migrate_data(apps, schema_editor=None) | |
| @pytest.mark.django_db | |
| def test_migration_basic_rule_fields_are_mapped(): | |
| """Rule fields (priority, description, action, version_type, match patterns) are mapped correctly.""" | |
| project = Project.objects.create( | |
| name="Migration test project", | |
| slug="migration-test-project", | |
| repo="https://example.com/repo.git", | |
| ) | |
| now = timezone.now() | |
| old_rule = RegexAutomationRule.objects.create( | |
| project=project, | |
| priority=2, | |
| description="Migrate me", | |
| match_arg=r"^v\d+\.\d+$", | |
| predefined_match_arg="semver-versions", | |
| action="activate-version", | |
| version_type="tag", | |
| ) | |
| RegexAutomationRule.objects.filter(pk=old_rule.pk).update( | |
| created=now - timedelta(days=3), | |
| modified=now - timedelta(days=2), | |
| ) | |
| _run_migration() | |
| new_rule = AutomationRule.objects.get(project=project) | |
| assert new_rule.priority == old_rule.priority | |
| assert new_rule.description == old_rule.description | |
| assert new_rule.version_types == [old_rule.version_type] | |
| assert new_rule.version_match_pattern == old_rule.match_arg | |
| assert new_rule.version_predefined_match_pattern == old_rule.predefined_match_arg | |
| assert new_rule.action == old_rule.action | |
| assert new_rule.enabled is True | |
| assert new_rule.created == now - timedelta(days=3) | |
| assert new_rule.modified == now - timedelta(days=2) | |
| @pytest.mark.django_db | |
| def test_migration_null_predefined_match_arg_becomes_custom_match(): | |
| """A rule with no predefined_match_arg gets version_predefined_match_pattern='custom-match'.""" | |
| project = Project.objects.create( | |
| name="Custom match project", | |
| slug="custom-match-project", | |
| repo="https://example.com/repo.git", | |
| ) | |
| RegexAutomationRule.objects.create( | |
| project=project, | |
| priority=0, | |
| match_arg=r"^release-.*$", | |
| predefined_match_arg=None, | |
| action="set-default-version", | |
| version_type="branch", | |
| ) | |
| _run_migration() | |
| new_rule = AutomationRule.objects.get(project=project) | |
| assert new_rule.version_predefined_match_pattern == "custom-match" | |
| assert new_rule.version_match_pattern == r"^release-.*$" | |
| @pytest.mark.django_db | |
| def test_migration_single_rule_with_multiple_matches(): | |
| """All AutomationRuleMatch rows for a rule are migrated and their fields preserved.""" | |
| project = Project.objects.create( | |
| name="Multi-match project", | |
| slug="multi-match-project", | |
| repo="https://example.com/repo.git", | |
| ) | |
| now = timezone.now() | |
| old_rule = RegexAutomationRule.objects.create( | |
| project=project, | |
| priority=0, | |
| match_arg=r".*", | |
| predefined_match_arg="all-versions", | |
| action="activate-version", | |
| version_type="tag", | |
| ) | |
| match_data = [ | |
| ("v1.0", "tag", now - timedelta(hours=3), now - timedelta(hours=2)), | |
| ("v1.1", "tag", now - timedelta(hours=5), now - timedelta(hours=4)), | |
| ("v2.0", "branch", now - timedelta(hours=7), now - timedelta(hours=6)), | |
| ] | |
| old_matches = [] | |
| for version_name, version_type, created, modified in match_data: | |
| m = LegacyAutomationRuleMatch.objects.create( | |
| rule=old_rule, | |
| match_arg=old_rule.match_arg, | |
| action=old_rule.action, | |
| version_name=version_name, | |
| version_type=version_type, | |
| ) | |
| LegacyAutomationRuleMatch.objects.filter(pk=m.pk).update( | |
| created=created, | |
| modified=modified, | |
| ) | |
| old_matches.append(m) | |
| _run_migration() | |
| new_rule = AutomationRule.objects.get(project=project) | |
| new_matches = AutomationRuleMatch.objects.filter(rule=new_rule).order_by("version_name") | |
| assert new_matches.count() == 3 | |
| expected = sorted(match_data, key=lambda x: x[0]) | |
| for new_match, (version_name, version_type, created, modified) in zip(new_matches, expected): | |
| assert new_match.version_name == version_name | |
| assert new_match.version_type == version_type | |
| assert new_match.match_arg == old_rule.match_arg | |
| assert new_match.action == old_rule.action | |
| assert new_match.created == created | |
| assert new_match.modified == modified | |
| @pytest.mark.django_db | |
| def test_migration_multiple_rules_with_multiple_matches_each(): | |
| """Multiple rules each with multiple matches are all migrated and correctly associated.""" | |
| project = Project.objects.create( | |
| name="Bulk migration project", | |
| slug="bulk-migration-project", | |
| repo="https://example.com/repo.git", | |
| ) | |
| rules_spec = [ | |
| # (priority, match_arg, predefined_match_arg, action, version_type, match_version_names) | |
| (0, r".*", "all-versions", "activate-version", "tag", ["v1.0", "v1.1", "v1.2"]), | |
| (1, r"^release-.*$", None, "set-default-version", "branch", ["release-2024", "release-2025"]), | |
| (2, r"^v\d+$", "semver-versions", "hide-version", "tag", ["v3", "v4", "v5", "v6"]), | |
| ] | |
| old_rules = [] | |
| for priority, match_arg, predefined, action, version_type, version_names in rules_spec: | |
| rule = RegexAutomationRule.objects.create( | |
| project=project, | |
| priority=priority, | |
| match_arg=match_arg, | |
| predefined_match_arg=predefined, | |
| action=action, | |
| version_type=version_type, | |
| ) | |
| for name in version_names: | |
| LegacyAutomationRuleMatch.objects.create( | |
| rule=rule, | |
| match_arg=match_arg, | |
| action=action, | |
| version_name=name, | |
| version_type=version_type, | |
| ) | |
| old_rules.append(rule) | |
| _run_migration() | |
| new_rules = AutomationRule.objects.filter(project=project).order_by("priority") | |
| assert new_rules.count() == len(rules_spec) | |
| for new_rule, (priority, match_arg, predefined, action, version_type, version_names) in zip( | |
| new_rules, rules_spec | |
| ): | |
| assert new_rule.priority == priority | |
| assert new_rule.version_match_pattern == match_arg | |
| assert new_rule.version_predefined_match_pattern == (predefined or "custom-match") | |
| assert new_rule.action == action | |
| assert new_rule.version_types == [version_type] | |
| migrated_names = set( | |
| AutomationRuleMatch.objects.filter(rule=new_rule).values_list("version_name", flat=True) | |
| ) | |
| assert migrated_names == set(version_names) | |
| @pytest.mark.django_db | |
| def test_migration_rules_across_multiple_projects_are_isolated(): | |
| """Rules belonging to different projects are each migrated under their own project.""" | |
| project_a = Project.objects.create( | |
| name="Project A", | |
| slug="project-a", | |
| repo="https://example.com/a.git", | |
| ) | |
| project_b = Project.objects.create( | |
| name="Project B", | |
| slug="project-b", | |
| repo="https://example.com/b.git", | |
| ) | |
| rule_a = RegexAutomationRule.objects.create( | |
| project=project_a, | |
| priority=0, | |
| match_arg=r"^a-.*$", | |
| predefined_match_arg=None, | |
| action="activate-version", | |
| version_type="branch", | |
| ) | |
| LegacyAutomationRuleMatch.objects.create( | |
| rule=rule_a, match_arg=rule_a.match_arg, action=rule_a.action, | |
| version_name="a-branch", version_type="branch", | |
| ) | |
| rule_b = RegexAutomationRule.objects.create( | |
| project=project_b, | |
| priority=0, | |
| match_arg=r"^b-.*$", | |
| predefined_match_arg=None, | |
| action="hide-version", | |
| version_type="tag", | |
| ) | |
| for name in ["b-v1", "b-v2"]: | |
| LegacyAutomationRuleMatch.objects.create( | |
| rule=rule_b, match_arg=rule_b.match_arg, action=rule_b.action, | |
| version_name=name, version_type="tag", | |
| ) | |
| _run_migration() | |
| rules_a = AutomationRule.objects.filter(project=project_a) | |
| assert rules_a.count() == 1 | |
| assert rules_a[0].action == "activate-version" | |
| assert AutomationRuleMatch.objects.filter(rule=rules_a[0]).count() == 1 | |
| rules_b = AutomationRule.objects.filter(project=project_b) | |
| assert rules_b.count() == 1 | |
| assert rules_b[0].action == "hide-version" | |
| assert AutomationRuleMatch.objects.filter(rule=rules_b[0]).count() == 2 | |
| @pytest.mark.django_db | |
| def test_migration_rule_with_no_matches(): | |
| """A rule that has never matched anything is migrated with zero AutomationRuleMatch rows.""" | |
| project = Project.objects.create( | |
| name="No matches project", | |
| slug="no-matches-project", | |
| repo="https://example.com/repo.git", | |
| ) | |
| RegexAutomationRule.objects.create( | |
| project=project, | |
| priority=0, | |
| match_arg=r"^never-.*$", | |
| predefined_match_arg=None, | |
| action="delete-version", | |
| version_type="branch", | |
| ) | |
| _run_migration() | |
| new_rule = AutomationRule.objects.get(project=project) | |
| assert AutomationRuleMatch.objects.filter(rule=new_rule).count() == 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment