Skip to content

Instantly share code, notes, and snippets.

@humitos
Created April 22, 2026 09:39
Show Gist options
  • Select an option

  • Save humitos/3fda5e1564fee34174ab8578a737a60f to your computer and use it in GitHub Desktop.

Select an option

Save humitos/3fda5e1564fee34174ab8578a737a60f to your computer and use it in GitHub Desktop.
Migration test for 0163_automationrule_data_migration: RegexAutomationRule → projects.AutomationRule
============================= 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 ==============================
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