Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smoke unit test for redis migrations #9117

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from

Conversation

archibald1418
Copy link
Contributor

@archibald1418 archibald1418 commented Feb 18, 2025

Motivation and context

Basic coverage for redis migrations support from #8898

How has this been tested?

Prepare

  • A .py migration file with an incorrect migration class is generated
  • Redis is patched with fakeredis instance throughout the whole test suite since our main concern is the migration tool itself, not the Redis database.

Case 1: Migration is added and applied successfully

  • Migrations not applied yet: migrateredis --check should exit the program
  • Apply migrations with migrateredis, should return correctly
  • Assert that migration names from existing files match the ones added to cvat:applied_migrations key

Case 2: Migration is not added and not applied

  • Generate a bad stub file named 000_second.py with the following contents:
class Migration:
    @classmethod
    def run(cls): ...

Since the Migration class is not a subclass of BaseMigration, we expect that

  • LoaderError is raised
  • cvat:applied_migrations is an empty set
  • Migration.run is not called

Checklist

  • I submit my changes into the develop branch
  • I have created a changelog fragment
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • I have linked related issues (see GitHub docs)

License

  • I submit my code changes under the same MIT License that covers the project.
    Feel free to contact the maintainers if that's a concern.

Oleg Valiulin added 2 commits February 18, 2025 22:03
@codecov-commenter
Copy link

codecov-commenter commented Feb 18, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 73.31%. Comparing base (47e2ed1) to head (30a3279).
Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #9117      +/-   ##
===========================================
+ Coverage    73.24%   73.31%   +0.06%     
===========================================
  Files          447      448       +1     
  Lines        45837    45843       +6     
  Branches      3908     3908              
===========================================
+ Hits         33573    33609      +36     
+ Misses       12264    12234      -30     
Components Coverage Δ
cvat-ui 77.06% <ø> (-0.02%) ⬇️
cvat-server 70.31% <100.00%> (+0.14%) ⬆️

self.addCleanup(self.test_migration.cleanup)

call_command("migrateredis")
exp_migrations = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Could you please explain the purpose of generation test migration? why existing migrations are not enough? I do not really think that we need to modify/add files from cvat/apps when running tests. Otherwise, the generated files may not be removed e.g. when we stop debugging before cleanup is called.
  • Do I understand correctly that we need to update this test each time when a new Redis migration is added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the existing migration files are correct, which means I cannot test a scenario where the tool throws LoaderError.
Though, case 1 could be rewritten to use existing migration files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to update this test each time when a new Redis migration is added?

I didn't get it, why would that be the case? Tests are using fake Redis database

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get it, why would that be the case? Tests are using fake Redis database

I'm talking about the case when we add one more redis migration. In that case the self.assertEqual(conn.smembers("cvat:applied_migrations"), exp_migrations) will fail. Please, check that the expected by the test migration keys are added to cvat:applied_migrations instead of comparing all set members.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes totally makes sense, applied

self.assertEqual(conn.smembers("cvat:applied_migrations"), exp_migrations)
self.test_migration.runner_mock.assert_called_once()

def test_migration_not_applied(self, redis1, _):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the more appropriate test should cover a case when migrateredis is called with --check arg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

BAD_MIGRATION_FILE_CONTENT = MIGRATION_FILE_CONTENT.replace("(BaseMigration)", "")

with (
patch(f"{__name__}.MIGRATION_FILE_CONTENT", new=BAD_MIGRATION_FILE_CONTENT),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks a bit odd to me that you patch MIGRATION_FILE_CONTENT defined in the current module and not used anywhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, reworked the class to only generate bad migration

MUT = "cvat.apps.redis_handler"


@patch(f"{MUT}.migration_loader.Redis", return_value=fakeredis.FakeRedis())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to patch it? It is used only as type annotations in the migration_loader module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're right, removing this


from .utils import path_to_module

WORKDIR = PosixPath("cvat/apps")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path? Tests can be run on windows

from pathlib import PosixPath


def path_to_module(path: PosixPath) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path?

self.patcher: _patch = patch.object(self.test_class, "run")
self.runner_mock: Mock | None = self.patcher.start()

self.exp_migration_name = app_name + "." + self.make_migration_name()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, why did you decide not to check whether engine.001_cleanup_scheduled_jobs is added to cvat:applied_migrations after running migrateredis?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood, I thought --check would be enough for this. Applying

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I reworked it to this: now the it looks for all cvat/apps/**/redis_migrations/[0-9]*.py files, extracts the expected migration names and asserts they are identical to members of cvat:applied_migrations

Comment on lines 54 to 55
self.patcher = patch.object(self.test_class, "run")
self.runner_mock: Mock | None = self.patcher.start()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to initialize patcher and runner_mock here? Would it be better just to mock the method in the corresponding test?

with patch.object(self.test_migration.test_class, "run") as mock_run:
    mock_run.side_effect = original_run_method
    ...
    mock_run.assert_not_called()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, less hassle with the cleanup


def test_migration_bad(self, redis):
with redis() as conn:
conn.flushall()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that this test should flush Redis data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing

file_to_migration_name(file).encode("utf8") for file in migration_files
)
with redis() as conn:
applied_migrations = conn.smembers("cvat:applied_migrations")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
applied_migrations = conn.smembers("cvat:applied_migrations")
applied_migrations = conn.smembers(AppliedMigration.SET_KEY)

Comment on lines 94 to 97
migration_files = WORKDIR.glob(MIGRATION_FILES)
expected_migrations = set(
file_to_migration_name(file).encode("utf8") for file in migration_files
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use existing logic instead of adding custom file_to_migration_name

Suggested change
migration_files = WORKDIR.glob(MIGRATION_FILES)
expected_migrations = set(
file_to_migration_name(file).encode("utf8") for file in migration_files
)
expected_migrations = {
f"{app_config.label}.{f.stem}".encode()
for app_config in MigrationLoader.find_app_configs()
for f in (Path(app_config.path) / MigrationLoader.REDIS_MIGRATIONS_DIR_NAME).glob("[0-9]*.py")
}
assert len(expected_migrations)

Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants