From ab9ea7c288853559e24aba5a94b582f8f1b5fc95 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Sat, 4 Apr 2026 00:42:59 -0400 Subject: [PATCH 1/8] build: collect per-test timing data. Run pytest with extra reporting enabled to generate files with per-test durations. The file is uploaded as a CI artifact so timing data can be downloaded and used to drive optimal shard rebalancing. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/unit-tests.yml | 11 ++++++++++- requirements/edx/development.txt | 3 +++ requirements/edx/testing.in | 1 + requirements/edx/testing.txt | 3 +++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7b7b14a43e88..c2c0b6a89962 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -119,7 +119,16 @@ jobs: - name: run tests shell: bash run: | - python -Wd -m pytest -p no:randomly --ds=${{ env.settings_path }} ${{ env.unit_test_paths }} --cov=. + python -Wd -m pytest -p no:randomly --ds=${{ env.settings_path }} ${{ env.unit_test_paths }} --cov=. \ + --report-log=reports/pytest-report-${{ matrix.shard_name }}.jsonl + + - name: Upload pytest timing report + if: always() + uses: actions/upload-artifact@v7 + with: + name: pytest-report-${{ matrix.shard_name }}-${{ matrix.python-version }}-${{ matrix.django-version }}-${{ matrix.mongo-version }}-${{ matrix.os-version }} + path: reports/pytest-report-${{ matrix.shard_name }}.jsonl + overwrite: true - name: rename warnings json file if: success() diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index a4b4a5baf329..c9473b8bea0f 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1721,6 +1721,7 @@ pytest==8.2.0 # pytest-json-report # pytest-metadata # pytest-randomly + # pytest-reportlog # pytest-xdist pytest-attrib==0.1.3 # via -r requirements/edx/testing.txt @@ -1736,6 +1737,8 @@ pytest-metadata==3.1.1 # pytest-json-report pytest-randomly==4.0.1 # via -r requirements/edx/testing.txt +pytest-reportlog==1.0.0 + # via -r requirements/edx/testing.txt pytest-xdist[psutil]==3.8.0 # via -r requirements/edx/testing.txt python-dateutil==2.9.0.post0 diff --git a/requirements/edx/testing.in b/requirements/edx/testing.in index 9784cb6cc946..b84fd39e3cab 100644 --- a/requirements/edx/testing.in +++ b/requirements/edx/testing.in @@ -38,6 +38,7 @@ pytest-django # Django support for pytest pytest-json-report # Output json formatted warnings after running pytest pytest-metadata # To prevent 'make upgrade' failure, dependency of pytest-json-report pytest-randomly # pytest plugin to randomly order tests +pytest-reportlog # Per-test timing data including setup/teardown (used for shard rebalancing) pytest-xdist[psutil] # Parallel execution of tests on multiple CPU cores or hosts singledispatch # Backport of functools.singledispatch from Python 3.4+, used in tests of XBlock rendering testfixtures # Provides a LogCapture utility used by several tests diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 4f309040abdc..be8a03ac36c9 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1307,6 +1307,7 @@ pytest==8.2.0 # pytest-json-report # pytest-metadata # pytest-randomly + # pytest-reportlog # pytest-xdist pytest-attrib==0.1.3 # via -r requirements/edx/testing.in @@ -1322,6 +1323,8 @@ pytest-metadata==3.1.1 # pytest-json-report pytest-randomly==4.0.1 # via -r requirements/edx/testing.in +pytest-reportlog==1.0.0 + # via -r requirements/edx/testing.in pytest-xdist[psutil]==3.8.0 # via -r requirements/edx/testing.in python-dateutil==2.9.0.post0 From 8aa5db62d77407b1cec4cdaa484d32b6ecbec271 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Sat, 4 Apr 2026 23:36:14 -0400 Subject: [PATCH 2/8] build: rebalance unit test shards to reduce critical path Redistribute test paths across 9 shards (down from 16) using a greedy bin-packing optimiser driven by real per-test timing data from pytest-reportlog. Predicted critical path: ~18.7m (down from ~29m). Key changes: - Rename shard groups to reflect semantic meaning: lms-*, shared-with-lms-*, shared-with-cms-*, cms-* (openedx/common/xmodule paths explicitly separated from lms-only and cms-only paths) - Split lms/djangoapps/discussion/ into its 4 subdirectories so the heavy rest_api/ shard (15.7m) can be distributed across bins independently - Remove outdated comment referencing unit-tests-gh-hosted.yml Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/unit-test-shards.json | 194 ++++++++++-------------- .github/workflows/unit-tests.yml | 15 +- 2 files changed, 84 insertions(+), 125 deletions(-) diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 2c1ffd4744b7..02a83a4ceb3a 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -2,168 +2,169 @@ "lms-1": { "settings": "lms.envs.test", "paths": [ - "lms/djangoapps/branding/", - "lms/djangoapps/bulk_email/", - "lms/djangoapps/bulk_enroll/", - "lms/djangoapps/bulk_user_retirement/", - "lms/djangoapps/ccx/", - "lms/djangoapps/certificates/", - "lms/djangoapps/commerce/" + "lms/djangoapps/discussion/rest_api/" ] }, "lms-2": { "settings": "lms.envs.test", "paths": [ + "lms/djangoapps/ccx/", + "lms/djangoapps/commerce/", "lms/djangoapps/course_api/", - "lms/djangoapps/course_blocks/", - "lms/djangoapps/course_goals/", - "lms/djangoapps/course_home_api/", "lms/djangoapps/course_wiki/", - "lms/djangoapps/coursewarehistoryextended/", - "lms/djangoapps/debug/" + "lms/djangoapps/discussion/notification_prefs/", + "lms/djangoapps/instructor_task/", + "lms/djangoapps/ora_staff_grader/", + "lms/djangoapps/survey/", + "lms/djangoapps/teams/", + "lms/djangoapps/verify_student/" ] }, "lms-3": { "settings": "lms.envs.test", "paths": [ - "lms/djangoapps/courseware/" + "lms/djangoapps/branding/", + "lms/djangoapps/bulk_enroll/", + "lms/djangoapps/courseware/", + "lms/djangoapps/instructor_analytics/", + "lms/djangoapps/learner_dashboard/", + "lms/djangoapps/lti_provider/", + "lms/djangoapps/program_enrollments/" ] }, "lms-4": { "settings": "lms.envs.test", "paths": [ - "lms/djangoapps/discussion/", - "lms/djangoapps/edxnotes/", - "lms/djangoapps/experiments/" - ] - }, - "lms-5": { - "settings": "lms.envs.test", - "paths": [ - "lms/djangoapps/gating/", + "lms/djangoapps/bulk_user_retirement/", + "lms/djangoapps/course_blocks/", + "lms/djangoapps/course_goals/", + "lms/djangoapps/course_home_api/", + "lms/djangoapps/coursewarehistoryextended/", + "lms/djangoapps/discussion/tests/", + "lms/djangoapps/experiments/", "lms/djangoapps/grades/", "lms/djangoapps/instructor/", - "lms/djangoapps/instructor_analytics/" + "lms/djangoapps/mfe_config_api/", + "lms/djangoapps/staticbook/", + "lms/lib/" ] }, - "lms-6": { + "lms-5": { "settings": "lms.envs.test", "paths": [ - "lms/djangoapps/instructor_task/", - "lms/djangoapps/learner_dashboard/", + "lms/djangoapps/bulk_email/", + "lms/djangoapps/certificates/", + "lms/djangoapps/debug/", + "lms/djangoapps/discussion/django_comment_client/", + "lms/djangoapps/edxnotes/", + "lms/djangoapps/gating/", "lms/djangoapps/learner_home/", "lms/djangoapps/lms_initialization/", "lms/djangoapps/lms_xblock/", - "lms/djangoapps/lti_provider/", "lms/djangoapps/mailing/", "lms/djangoapps/mobile_api/", "lms/djangoapps/monitoring/", - "lms/djangoapps/ora_staff_grader/", - "lms/djangoapps/program_enrollments/", "lms/djangoapps/rss_proxy/", "lms/djangoapps/static_template_view/", - "lms/djangoapps/staticbook/", "lms/djangoapps/support/", - "lms/djangoapps/survey/", - "lms/djangoapps/teams/", "lms/djangoapps/tests/", "lms/djangoapps/user_tours/", - "lms/djangoapps/verify_student/", - "lms/djangoapps/mfe_config_api/", "lms/envs/", - "lms/lib/", "lms/tests.py" ] }, - "openedx-1-with-lms": { + "shared-with-lms-1": { "settings": "lms.envs.test", "paths": [ - "openedx/core/djangoapps/ace_common/", - "openedx/core/djangoapps/cors_csrf/", + "common/djangoapps/", "openedx/core/djangoapps/agreements/", "openedx/core/djangoapps/api_admin/", + "openedx/core/djangoapps/authz/", + "openedx/core/djangoapps/cache_toolbox/", + "openedx/core/djangoapps/ccxcon/", + "openedx/core/djangoapps/content/", + "openedx/core/djangoapps/content_libraries/", + "openedx/core/djangoapps/course_apps/", + "openedx/core/djangoapps/credentials/", + "openedx/core/djangoapps/credit/", + "openedx/core/djangoapps/dark_lang/", + "openedx/core/djangoapps/django_comment_common/", + "openedx/core/djangoapps/embargo/", + "openedx/core/djangoapps/header_control/", + "openedx/core/djangoapps/heartbeat/", + "openedx/core/djangoapps/models/", + "openedx/core/djangoapps/notifications/", + "openedx/core/djangoapps/oauth_dispatch/", + "openedx/core/djangoapps/safe_sessions/", + "openedx/core/djangoapps/schedules/", + "openedx/core/djangoapps/user_api/", + "openedx/core/djangoapps/util/", + "openedx/core/djangoapps/video_pipeline/", + "openedx/core/djangoapps/waffle_utils/", + "openedx/core/djangoapps/xblock/", + "openedx/core/tests/", + "openedx/features/", + "openedx/tests/" + ] + }, + "shared-with-lms-2": { + "settings": "lms.envs.test", + "paths": [ + "openedx/core/djangoapps/ace_common/", "openedx/core/djangoapps/auth_exchange/", "openedx/core/djangoapps/bookmarks/", - "openedx/core/djangoapps/cache_toolbox/", "openedx/core/djangoapps/catalog/", - "openedx/core/djangoapps/ccxcon/", "openedx/core/djangoapps/commerce/", "openedx/core/djangoapps/common_initialization/", "openedx/core/djangoapps/common_views/", "openedx/core/djangoapps/config_model_utils/", - "openedx/core/djangoapps/content/", - "openedx/core/djangoapps/content_libraries/", "openedx/core/djangoapps/contentserver/", "openedx/core/djangoapps/cookie_metadata/", - "openedx/core/djangoapps/course_apps/", + "openedx/core/djangoapps/cors_csrf/", "openedx/core/djangoapps/course_date_signals/", "openedx/core/djangoapps/course_groups/", + "openedx/core/djangoapps/course_live/", "openedx/core/djangoapps/courseware_api/", "openedx/core/djangoapps/crawlers/", - "openedx/core/djangoapps/credentials/", - "openedx/core/djangoapps/credit/", - "openedx/core/djangoapps/course_live/", - "openedx/core/djangoapps/dark_lang/", "openedx/core/djangoapps/debug/", "openedx/core/djangoapps/discussions/", - "openedx/core/djangoapps/django_comment_common/", - "openedx/core/djangoapps/embargo/", "openedx/core/djangoapps/enrollments/", - "openedx/core/djangoapps/external_user_ids/" - ] - }, - "openedx-2-with-lms": { - "settings": "lms.envs.test", - "paths": [ + "openedx/core/djangoapps/external_user_ids/", "openedx/core/djangoapps/geoinfo/", - "openedx/core/djangoapps/header_control/", - "openedx/core/djangoapps/heartbeat/", "openedx/core/djangoapps/lang_pref/", - "openedx/core/djangoapps/models/", "openedx/core/djangoapps/monkey_patch/", - "openedx/core/djangoapps/notifications/", - "openedx/core/djangoapps/oauth_dispatch/", "openedx/core/djangoapps/olx_rest_api/", "openedx/core/djangoapps/password_policy/", "openedx/core/djangoapps/plugin_api/", "openedx/core/djangoapps/plugins/", "openedx/core/djangoapps/profile_images/", "openedx/core/djangoapps/programs/", - "openedx/core/djangoapps/safe_sessions/", - "openedx/core/djangoapps/schedules/", "openedx/core/djangoapps/service_status/", "openedx/core/djangoapps/session_inactivity_timeout/", "openedx/core/djangoapps/signals/", "openedx/core/djangoapps/site_configuration/", "openedx/core/djangoapps/system_wide_roles/", "openedx/core/djangoapps/theming/", - "openedx/core/djangoapps/user_api/", "openedx/core/djangoapps/user_authn/", - "openedx/core/djangoapps/util/", "openedx/core/djangoapps/verified_track_content/", "openedx/core/djangoapps/video_config/", - "openedx/core/djangoapps/video_pipeline/", - "openedx/core/djangoapps/waffle_utils/", - "openedx/core/djangoapps/xblock/", "openedx/core/djangoapps/xmodule_django/", "openedx/core/djangoapps/zendesk_proxy/", - "openedx/core/djangoapps/authz/", "openedx/core/djangolib/", "openedx/core/lib/", - "openedx/core/tests/", - "openedx/features/", "openedx/testing/", - "openedx/tests/" + "xmodule/" ] }, - "openedx-1-with-cms": { + "shared-with-cms-1": { "settings": "cms.envs.test", "paths": [ + "common/djangoapps/", "openedx/core/djangoapps/ace_common/", - "openedx/core/djangoapps/cors_csrf/", "openedx/core/djangoapps/agreements/", "openedx/core/djangoapps/api_admin/", "openedx/core/djangoapps/auth_exchange/", + "openedx/core/djangoapps/authz/", "openedx/core/djangoapps/bookmarks/", "openedx/core/djangoapps/cache_toolbox/", "openedx/core/djangoapps/catalog/", @@ -175,8 +176,10 @@ "openedx/core/djangoapps/content/", "openedx/core/djangoapps/content_libraries/", "openedx/core/djangoapps/content_staging/", + "openedx/core/djangoapps/content_tagging/", "openedx/core/djangoapps/contentserver/", "openedx/core/djangoapps/cookie_metadata/", + "openedx/core/djangoapps/cors_csrf/", "openedx/core/djangoapps/course_apps/", "openedx/core/djangoapps/course_date_signals/", "openedx/core/djangoapps/course_groups/", @@ -190,13 +193,7 @@ "openedx/core/djangoapps/django_comment_common/", "openedx/core/djangoapps/embargo/", "openedx/core/djangoapps/enrollments/", - "openedx/core/djangoapps/external_user_ids/" - ] - }, - "openedx-2-with-cms": { - "settings": "cms.envs.test", - "paths": [ - "openedx/core/djangoapps/content_tagging/", + "openedx/core/djangoapps/external_user_ids/", "openedx/core/djangoapps/geoinfo/", "openedx/core/djangoapps/header_control/", "openedx/core/djangoapps/heartbeat/", @@ -228,9 +225,9 @@ "openedx/core/djangoapps/xblock/", "openedx/core/djangoapps/xmodule_django/", "openedx/core/djangoapps/zendesk_proxy/", - "openedx/core/djangoapps/authz/", "openedx/core/lib/", - "openedx/tests/" + "openedx/tests/", + "xmodule/" ] }, "cms-1": { @@ -238,44 +235,15 @@ "paths": [ "cms/djangoapps/api/", "cms/djangoapps/cms_user_tasks/", + "cms/djangoapps/contentstore/", "cms/djangoapps/course_creators/", "cms/djangoapps/export_course_metadata/", - "cms/djangoapps/modulestore_migrator/", "cms/djangoapps/models/", + "cms/djangoapps/modulestore_migrator/", "cms/djangoapps/pipeline_js/", "cms/djangoapps/xblock_config/", "cms/envs/", "cms/lib/" ] - }, - "cms-2": { - "settings": "cms.envs.test", - "paths": [ - "cms/djangoapps/contentstore/" - ] - }, - "common-with-lms": { - "settings": "lms.envs.test", - "paths": [ - "common/djangoapps/" - ] - }, - "common-with-cms": { - "settings": "cms.envs.test", - "paths": [ - "common/djangoapps/" - ] - }, - "xmodule-with-lms": { - "settings": "lms.envs.test", - "paths": [ - "xmodule/" - ] - }, - "xmodule-with-cms": { - "settings": "cms.envs.test", - "paths": [ - "xmodule/" - ] } } diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c2c0b6a89962..4345130f9600 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -25,25 +25,16 @@ jobs: - "3.12" django-version: - "pinned" - # When updating the shards, remember to make the same changes in - # .github/workflows/unit-tests-gh-hosted.yml shard_name: - "lms-1" - "lms-2" - "lms-3" - "lms-4" - "lms-5" - - "lms-6" - - "openedx-1-with-lms" - - "openedx-2-with-lms" - - "openedx-1-with-cms" - - "openedx-2-with-cms" + - "shared-with-lms-1" + - "shared-with-lms-2" + - "shared-with-cms-1" - "cms-1" - - "cms-2" - - "common-with-lms" - - "common-with-cms" - - "xmodule-with-lms" - - "xmodule-with-cms" mongo-version: - "7.0" os-version: From 32208104dd3edb286d6fb6c2727aadeb1552d9f2 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Mon, 6 Apr 2026 08:59:38 -0400 Subject: [PATCH 3/8] test: move CourseFactory to setUpClass in SharedModuleStoreTestCase subclasses Three test classes in the certificates app were calling CourseFactory() in setUp() despite extending SharedModuleStoreTestCase. Unlike ModuleStoreTestCase, SharedModuleStoreTestCase shares a single modulestore across all tests in the class and only closes MongoDB connections at tearDownClass. Calling CourseFactory() in setUp() created a new MongoDB course (and opened connections) for every test method without releasing them, causing connection accumulation across the full test run. Affected classes: - CertificateFiltersTest (test_filters.py) - CertificateInvalidationTest (test_models.py) - CertificateAllowlistTest (test_models.py) In each case the course is only read by test methods (test data such as users, enrollments and certificates is written via Django ORM and rolled back between tests), so sharing a single course across the class is correct. See: https://github.com/openedx/openedx-platform/blob/master/xmodule/modulestore/tests/django_utils.py Co-Authored-By: Claude Sonnet 4.6 --- lms/djangoapps/certificates/tests/test_filters.py | 6 +++++- lms/djangoapps/certificates/tests/test_models.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/certificates/tests/test_filters.py b/lms/djangoapps/certificates/tests/test_filters.py index 7b01d6cb475f..d2ec59054fef 100644 --- a/lms/djangoapps/certificates/tests/test_filters.py +++ b/lms/djangoapps/certificates/tests/test_filters.py @@ -71,9 +71,13 @@ class CertificateFiltersTest(SharedModuleStoreTestCase): - CertificateCreationRequested """ + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.course_run = CourseFactory() + def setUp(self): # pylint: disable=arguments-differ super().setUp() - self.course_run = CourseFactory() self.user = UserFactory.create( username="somestudent", first_name="Student", diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index bea9afe73b69..44fe47427d8a 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -381,10 +381,10 @@ def setUpClass(cls): """ super().setUpClass() cls.start_events_isolation() + cls.course = CourseFactory() def setUp(self): super().setUp() - self.course = CourseFactory() self.course_overview = CourseOverviewFactory.create( id=self.course.id ) @@ -708,6 +708,7 @@ def setUpClass(cls): """ super().setUpClass() cls.start_events_isolation() + cls.course_run = CourseFactory() def setUp(self): super().setUp() @@ -716,7 +717,6 @@ def setUp(self): self.user = UserFactory(username=self.username, email=self.user_email) self.second_user = UserFactory() - self.course_run = CourseFactory() self.course_run_key = self.course_run.id # pylint: disable=no-member def test_get_allowlist_empty(self): From ca1cdac054191c6f980b4643c6f6508273fa2c46 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 8 Apr 2026 11:57:18 -0400 Subject: [PATCH 4/8] refactor: update imports from openedx_events.tests.utils to openedx_events.testing openedx_events/tests/utils.py was moved to openedx_events/testing.py in openedx/openedx-events#559 so the test utilities are included in the installed package (setup.py excludes the tests/ subpackage from the wheel). Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/tests/test_upstream_downstream_links.py | 2 +- cms/djangoapps/contentstore/tests/test_utils.py | 2 +- cms/djangoapps/contentstore/views/tests/test_block.py | 2 +- cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py | 2 +- common/djangoapps/student/tests/test_enrollment.py | 2 +- common/djangoapps/student/tests/test_events.py | 2 +- lms/djangoapps/certificates/tests/test_events.py | 2 +- lms/djangoapps/certificates/tests/test_models.py | 2 +- lms/djangoapps/certificates/tests/test_signals.py | 2 +- lms/djangoapps/grades/tests/test_events.py | 2 +- openedx/core/djangoapps/course_groups/tests/test_cohorts.py | 2 +- openedx/core/djangoapps/course_groups/tests/test_events.py | 2 +- openedx/core/djangoapps/user_authn/views/tests/test_events.py | 2 +- openedx/core/djangoapps/user_authn/views/tests/test_login.py | 2 +- openedx/core/djangoapps/user_authn/views/tests/test_register.py | 2 +- xmodule/modulestore/tests/test_mixed_modulestore.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_upstream_downstream_links.py b/cms/djangoapps/contentstore/tests/test_upstream_downstream_links.py index 594036dbd23f..f6ba31e2af34 100644 --- a/cms/djangoapps/contentstore/tests/test_upstream_downstream_links.py +++ b/cms/djangoapps/contentstore/tests/test_upstream_downstream_links.py @@ -10,7 +10,7 @@ from django.test import TestCase from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import LibraryContainerLocator, LibraryUsageLocatorV2 -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangolib.testing.utils import skip_unless_cms diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py index aab36e3c4b6e..ba69adc8cdbc 100644 --- a/cms/djangoapps/contentstore/tests/test_utils.py +++ b/cms/djangoapps/contentstore/tests/test_utils.py @@ -11,7 +11,7 @@ from django.test.utils import override_settings from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import CourseLocator, LibraryLocator -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from path import Path as path from pytz import UTC from rest_framework import status diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 0f425d325ade..98a3061c6647 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -21,7 +21,7 @@ from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from openedx_events.content_authoring.data import DuplicatedXBlockData from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from pytz import UTC from web_fragments.fragment import Fragment from webob import Response diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py index acbc8a9b497a..d421dfeeeca6 100644 --- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py +++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py @@ -11,7 +11,7 @@ XBLOCK_DELETED, XBLOCK_UPDATED, ) -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from openedx_tagging.models import Tag from organizations.models import Organization from rest_framework.test import APIClient diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index 767479ac2756..86babea716ba 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -8,7 +8,7 @@ import pytest from django.conf import settings from django.urls import reverse -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory diff --git a/common/djangoapps/student/tests/test_events.py b/common/djangoapps/student/tests/test_events.py index 64f13a4e4c94..d4b1caf05aa3 100644 --- a/common/djangoapps/student/tests/test_events.py +++ b/common/djangoapps/student/tests/test_events.py @@ -25,7 +25,7 @@ COURSE_ENROLLMENT_CREATED, COURSE_UNENROLLMENT_COMPLETED, ) -from openedx_events.tests.utils import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order +from openedx_events.testing import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole diff --git a/lms/djangoapps/certificates/tests/test_events.py b/lms/djangoapps/certificates/tests/test_events.py index eafe0c8bf143..7c017519d065 100644 --- a/lms/djangoapps/certificates/tests/test_events.py +++ b/lms/djangoapps/certificates/tests/test_events.py @@ -9,7 +9,7 @@ from openedx_events.learning.data import CertificateData, CourseData, UserData, UserPersonalData from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import UserFactory from common.test.utils import assert_dict_contains_subset diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index 44fe47427d8a..c75e51c77a78 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -13,7 +13,7 @@ from django.test import TestCase from django.test.utils import override_settings from opaque_keys.edx.locator import CourseKey, CourseLocator -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from path import Path as path from common.djangoapps.course_modes.models import CourseMode diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index 7b5552801349..748042b17470 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -13,7 +13,7 @@ from openedx_events.data import EventsMetadata from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from lms.djangoapps.certificates.api import has_self_generated_certificates_enabled diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index 2c47de903ead..962d22dab75b 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -20,7 +20,7 @@ COURSE_PASSING_STATUS_UPDATED, PERSISTENT_GRADE_SUMMARY_CHANGED, ) -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import AdminFactory, UserFactory from common.test.utils import assert_dict_contains_subset diff --git a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py index c18524b38668..d5c3a8f28532 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py +++ b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py @@ -13,7 +13,7 @@ from django.test import TestCase from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import CourseLocator -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import UserFactory diff --git a/openedx/core/djangoapps/course_groups/tests/test_events.py b/openedx/core/djangoapps/course_groups/tests/test_events.py index 055dc94106dd..7748f0f356d6 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_events.py +++ b/openedx/core/djangoapps/course_groups/tests/test_events.py @@ -15,7 +15,7 @@ from openedx_events.learning.signals import ( COHORT_MEMBERSHIP_CHANGED, # lint-amnesty, pylint: disable=wrong-import-order ) -from openedx_events.tests.utils import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order +from openedx_events.testing import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order from common.djangoapps.student.tests.factories import UserFactory from common.test.utils import assert_dict_contains_subset diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_events.py b/openedx/core/djangoapps/user_authn/views/tests/test_events.py index 5393a24adccc..d74bc78e514f 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_events.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_events.py @@ -13,7 +13,7 @@ from django.urls import reverse from openedx_events.learning.data import UserData, UserPersonalData from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED, STUDENT_REGISTRATION_COMPLETED -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory from common.test.utils import assert_dict_contains_subset diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index 8397d03a8914..5aa5a1501964 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -21,7 +21,7 @@ from django.urls import NoReverseMatch, reverse from edx_toggles.toggles.testutils import override_waffle_switch from freezegun import freeze_time -from openedx_events.tests.utils import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order +from openedx_events.testing import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order from common.djangoapps.student.models import LoginFailures from common.djangoapps.student.tests.factories import RegistrationFactory, UserFactory, UserProfileFactory diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 039090a0bdb4..ee812fa66dd8 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -16,7 +16,7 @@ from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from social_django.models import Partial, UserSocialAuth from testfixtures import LogCapture diff --git a/xmodule/modulestore/tests/test_mixed_modulestore.py b/xmodule/modulestore/tests/test_mixed_modulestore.py index 5a413a7b303b..61b37079938a 100644 --- a/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -33,7 +33,7 @@ XBLOCK_PUBLISHED, XBLOCK_UPDATED, ) -from openedx_events.tests.utils import OpenEdxEventsTestMixin +from openedx_events.testing import OpenEdxEventsTestMixin from web_fragments.fragment import Fragment from xblock.core import XBlockAside from xblock.fields import Scope, ScopeIds, String From 468219badd922377fa009175002683721a29470a Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 8 Apr 2026 12:14:29 -0400 Subject: [PATCH 5/8] test: fix OpenEdxEventsTestMixin MRO ordering in test classes When OpenEdxEventsTestMixin was listed after a TestCase subclass (e.g. Foo(SharedModuleStoreTestCase, OpenEdxEventsTestMixin)), it landed after unittest.case.TestCase in the MRO. Since unittest.case.TestCase.setUpClass and tearDownClass do not call super(), the mixin's lifecycle methods never ran. The workaround was to manually call cls.start_events_isolation() in each class's setUpClass, but there was no corresponding tearDownClass to restore event state, causing events disabled by one test class to leak into subsequent classes in the same process. Fix by placing OpenEdxEventsTestMixin first in the base class list so it appears before unittest.case.TestCase in the MRO. This lets setUpClass and tearDownClass run automatically through the cooperative super() chain, removing the need for manual start_events_isolation() calls. Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/views/tests/test_block.py | 18 +--- .../student/tests/test_enrollment.py | 13 +-- .../djangoapps/student/tests/test_events.py | 20 +---- .../certificates/tests/test_events.py | 13 +-- .../certificates/tests/test_models.py | 83 ++--------------- .../certificates/tests/test_signals.py | 7 +- lms/djangoapps/grades/tests/test_events.py | 33 +------ .../course_groups/tests/test_cohorts.py | 19 +--- .../course_groups/tests/test_events.py | 19 +--- .../user_authn/views/tests/test_events.py | 28 +----- .../user_authn/views/tests/test_login.py | 26 +----- .../user_authn/views/tests/test_register.py | 89 ++----------------- .../tests/test_mixed_modulestore.py | 12 +-- 13 files changed, 29 insertions(+), 351 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 98a3061c6647..750c59298a36 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -743,7 +743,7 @@ def _duplicate_item(self, parent_usage_key, source_usage_key, display_name=None) return self.response_usage_key(resp) -class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin): +class TestDuplicateItem(OpenEdxEventsTestMixin, ItemTest, DuplicateHelper): """ Test the duplicate method. """ @@ -752,22 +752,6 @@ class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin): "org.openedx.content_authoring.xblock.duplicated.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - - @classmethod - def tearDownClass(cls): - """ Don't let our event isolation affect other test cases """ - super().tearDownClass() - cls.enable_all_events() # Re-enable events other than the ENABLED_OPENEDX_EVENTS subset we isolated. - def setUp(self): """Creates the test course structure and a few components to 'duplicate'.""" super().setUp() diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index 86babea716ba..b7f480a8945f 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -30,7 +30,7 @@ @ddt.ddt @patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True}) @skip_unless_lms -class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase, OpenEdxEventsTestMixin): +class EnrollmentTest(OpenEdxEventsTestMixin, UrlResetMixin, ModuleStoreTestCase): """ Test student enrollment, especially with different course modes. """ @@ -42,17 +42,6 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase, OpenEdxEventsTestMixin) PASSWORD = "edx" URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - @patch.dict(settings.FEATURES, {'EMBARGO': True}) def setUp(self): """ Create a course and user, then log in. """ diff --git a/common/djangoapps/student/tests/test_events.py b/common/djangoapps/student/tests/test_events.py index d4b1caf05aa3..68bc26d8063c 100644 --- a/common/djangoapps/student/tests/test_events.py +++ b/common/djangoapps/student/tests/test_events.py @@ -211,7 +211,7 @@ def test_enrolled_after_email_change(self): @skip_unless_lms -class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class EnrollmentEventsTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for the Open edX Events associated with the enrollment process through the enroll method. @@ -229,17 +229,6 @@ class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): "org.openedx.learning.course.unenrollment.completed.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.course = CourseFactory.create() @@ -391,7 +380,7 @@ def test_unenrollment_completed_event_emitted(self): @skip_unless_lms @ddt.ddt -class TestCourseAccessRoleEvents(TestCase, OpenEdxEventsTestMixin): +class TestCourseAccessRoleEvents(OpenEdxEventsTestMixin, TestCase): """ Tests for the events associated with the CourseAccessRole model. """ @@ -400,11 +389,6 @@ class TestCourseAccessRoleEvents(TestCase, OpenEdxEventsTestMixin): 'org.openedx.learning.user.course_access_role.removed.v1', ] - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.start_events_isolation() - def setUp(self): self.course_key = CourseKey.from_string("course-v1:test+blah+blah") self.user = UserFactory.create( diff --git a/lms/djangoapps/certificates/tests/test_events.py b/lms/djangoapps/certificates/tests/test_events.py index 7c017519d065..931e72ac7489 100644 --- a/lms/djangoapps/certificates/tests/test_events.py +++ b/lms/djangoapps/certificates/tests/test_events.py @@ -23,7 +23,7 @@ @skip_unless_lms -class CertificateEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class CertificateEventTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for the Open edX Events associated with the student's certification process. @@ -43,17 +43,6 @@ class CertificateEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): "org.openedx.learning.certificate.revoked.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.course = CourseOverviewFactory() diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index c75e51c77a78..4a6ba7e33163 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -57,7 +57,7 @@ name_affirmation_service = get_name_affirmation_service() -class ExampleCertificateTest(TestCase, OpenEdxEventsTestMixin): +class ExampleCertificateTest(OpenEdxEventsTestMixin, TestCase): """Tests for the ExampleCertificate model. """ COURSE_KEY = CourseLocator(org='test', course='test', run='test') @@ -69,17 +69,6 @@ class ExampleCertificateTest(TestCase, OpenEdxEventsTestMixin): ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.cert_set = ExampleCertificateSet.objects.create(course_key=self.COURSE_KEY) @@ -127,24 +116,13 @@ def test_latest_status_is_course_specific(self): assert result is None -class CertificateHtmlViewConfigurationTest(TestCase, OpenEdxEventsTestMixin): +class CertificateHtmlViewConfigurationTest(OpenEdxEventsTestMixin, TestCase): """ Test the CertificateHtmlViewConfiguration model. """ ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.configuration_string = """{ @@ -234,7 +212,7 @@ def test_asset_file_saving_with_actual_name(self): assert certificate_template_asset.asset == 'certificate_template_assets/1/picture2.jpg' -class EligibleCertificateManagerTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class EligibleCertificateManagerTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Test the GeneratedCertificate model's object manager for filtering out ineligible certs. @@ -242,17 +220,6 @@ class EligibleCertificateManagerTest(SharedModuleStoreTestCase, OpenEdxEventsTes ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.user = UserFactory() @@ -292,24 +259,13 @@ def test_filter_certificates_for_nonexistent_courses(self): @ddt.ddt -class TestCertificateGenerationHistory(TestCase, OpenEdxEventsTestMixin): +class TestCertificateGenerationHistory(OpenEdxEventsTestMixin, TestCase): """ Test the CertificateGenerationHistory model's methods """ ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - @ddt.data( ({"student_set": "allowlisted_not_generated"}, "For exceptions", True), ({"student_set": "allowlisted_not_generated"}, "For exceptions", False), @@ -364,7 +320,7 @@ def test_get_task_name(self, is_regeneration, expected): assert certificate_generation_history.get_task_name() == expected -class CertificateInvalidationTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class CertificateInvalidationTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Test for the Certificate Invalidation model. """ @@ -373,14 +329,7 @@ class CertificateInvalidationTest(SharedModuleStoreTestCase, OpenEdxEventsTestMi @classmethod def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ super().setUpClass() - cls.start_events_isolation() cls.course = CourseFactory() def setUp(self): @@ -434,24 +383,13 @@ def test_revoke_program_certificates(self, mock_issuance, mock_revoke_task): @ddt.ddt -class GeneratedCertificateTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class GeneratedCertificateTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Test GeneratedCertificates """ ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.user = UserFactory() @@ -691,7 +629,7 @@ def test_unverified(self, mock_emit_certificate_event): self._assert_event_data(mock_emit_certificate_event, expected_event_data) -class CertificateAllowlistTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class CertificateAllowlistTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for the CertificateAllowlist model. """ @@ -700,14 +638,7 @@ class CertificateAllowlistTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin @classmethod def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ super().setUpClass() - cls.start_events_isolation() cls.course_run = CourseFactory() def setUp(self): diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index 748042b17470..958529d6cf96 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -300,17 +300,12 @@ def test_failing_grade_allowlist(self): assert cert.status == CertificateStatuses.downloadable -class LearnerIdVerificationTest(ModuleStoreTestCase, OpenEdxEventsTestMixin): +class LearnerIdVerificationTest(OpenEdxEventsTestMixin, ModuleStoreTestCase): """ Tests for certificate generation task firing on learner id verification """ ENABLED_OPENEDX_EVENTS = ['org.openedx.learning.idv_attempt.approved.v1'] - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.course_one = CourseFactory.create(self_paced=True) diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index 962d22dab75b..78ddabb8f530 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -32,7 +32,7 @@ from xmodule.modulestore.tests.factories import CourseFactory -class PersistentGradeEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class PersistentGradeEventsTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for the Open edX Events associated with the persistant grade process through the update_or_create method. @@ -45,17 +45,6 @@ class PersistentGradeEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixi "org.openedx.learning.course.persistent_grade_summary.changed.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.course = CourseFactory.create() @@ -113,7 +102,7 @@ def test_persistent_grade_event_emitted(self): ) -class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class CoursePassingStatusEventsTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for Open edX passing status update event. """ @@ -121,14 +110,6 @@ class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTest "org.openedx.learning.course.passing.status.updated.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.course = CourseFactory.create() @@ -179,7 +160,7 @@ def test_course_passing_status_updated_emitted(self): class CCXCoursePassingStatusEventsTest( - SharedModuleStoreTestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, SharedModuleStoreTestCase ): """ Tests for Open edX passing status update event in a CCX course. @@ -188,14 +169,6 @@ class CCXCoursePassingStatusEventsTest( "org.openedx.learning.ccx.course.passing.status.updated.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.course = CourseFactory.create() diff --git a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py index d5c3a8f28532..e01a97230128 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_cohorts.py +++ b/openedx/core/djangoapps/course_groups/tests/test_cohorts.py @@ -30,30 +30,13 @@ @patch("openedx.core.djangoapps.course_groups.cohorts.tracker", autospec=True) -class TestCohortSignals(TestCase, OpenEdxEventsTestMixin): +class TestCohortSignals(OpenEdxEventsTestMixin, TestCase): """ Test cases to validate event emissions for various cohort-related workflows """ ENABLED_OPENEDX_EVENTS = [] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - - @classmethod - def tearDownClass(cls): - """ Don't let our event isolation affect other test cases """ - super().tearDownClass() - cls.enable_all_events() # Re-enable events other than the ENABLED_OPENEDX_EVENTS subset we isolated. - def setUp(self): super().setUp() self.course_key = CourseLocator("dummy", "dummy", "dummy") diff --git a/openedx/core/djangoapps/course_groups/tests/test_events.py b/openedx/core/djangoapps/course_groups/tests/test_events.py index 7748f0f356d6..09450e0e292e 100644 --- a/openedx/core/djangoapps/course_groups/tests/test_events.py +++ b/openedx/core/djangoapps/course_groups/tests/test_events.py @@ -29,7 +29,7 @@ @skip_unless_lms -class CohortEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): +class CohortEventTest(OpenEdxEventsTestMixin, SharedModuleStoreTestCase): """ Tests for the Open edX Events associated with the cohort update process. @@ -43,23 +43,6 @@ class CohortEventTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): "org.openedx.learning.cohort_membership.changed.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - - @classmethod - def tearDownClass(cls): - """ Don't let our event isolation affect other test cases """ - super().tearDownClass() - cls.enable_all_events() # Re-enable events other than the ENABLED_OPENEDX_EVENTS subset we isolated. - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.course = CourseOverviewFactory() diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_events.py b/openedx/core/djangoapps/user_authn/views/tests/test_events.py index d74bc78e514f..b8e421747c86 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_events.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_events.py @@ -22,7 +22,7 @@ @skip_unless_lms -class RegistrationEventTest(UserAPITestCase, OpenEdxEventsTestMixin): +class RegistrationEventTest(OpenEdxEventsTestMixin, UserAPITestCase): """ Tests for the Open edX Events associated with the registration process through the registration view. @@ -36,19 +36,6 @@ class RegistrationEventTest(UserAPITestCase, OpenEdxEventsTestMixin): ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.student.registration.completed.v1"] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - So the Open edX Events Isolation starts, the setUpClass must be explicitly - called with the method that executes the isolation. We do this to avoid - MRO resolution conflicts with other sibling classes while ensuring the - isolation process begins. - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.url = reverse("user_api_registration") @@ -104,7 +91,7 @@ def test_send_registration_event(self): @skip_unless_lms -class LoginSessionEventTest(UserAPITestCase, OpenEdxEventsTestMixin): +class LoginSessionEventTest(OpenEdxEventsTestMixin, UserAPITestCase): """ Tests for the Open edX Events associated with the login process through the login_user view. @@ -118,17 +105,6 @@ class LoginSessionEventTest(UserAPITestCase, OpenEdxEventsTestMixin): ENABLED_OPENEDX_EVENTS = ["org.openedx.learning.auth.session.login.completed.v1"] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.url = reverse("user_api_login_session", kwargs={"api_version": "v1"}) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index 5aa5a1501964..9603eb969061 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -53,7 +53,7 @@ ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY=False, ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY=False, ) -class LoginTest(SiteMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin): +class LoginTest(OpenEdxEventsTestMixin, SiteMixin, CacheIsolationTestCase): """ Test login_user() view """ @@ -67,17 +67,6 @@ class LoginTest(SiteMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin): user_email = 'test@edx.org' password = 'test_password' - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): """Setup a test user along with its registration and profile""" super().setUp() @@ -1046,7 +1035,7 @@ def test_check_user_auth_flow_bad_email(self): @ddt.ddt @skip_unless_lms -class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin): +class LoginSessionViewTest(OpenEdxEventsTestMixin, ApiTestCase): """Tests for the login end-points of the user API. """ ENABLED_OPENEDX_EVENTS = [] @@ -1055,17 +1044,6 @@ class LoginSessionViewTest(ApiTestCase, OpenEdxEventsTestMixin): EMAIL = "bob@example.com" PASSWORD = "password" - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.url = reverse("user_api_login_session", kwargs={'api_version': 'v1'}) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index ee812fa66dd8..552f3fc162f5 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -74,7 +74,7 @@ @ddt.ddt @skip_unless_lms class RegistrationViewValidationErrorTest( - ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, ThirdPartyAuthTestMixin, UserAPITestCase, RetirementTestCase ): """ Tests for catching duplicate email and username validation errors within @@ -96,17 +96,6 @@ class RegistrationViewValidationErrorTest( COUNTRY = "US" GOALS = "Learn all the things!" - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.url = reverse("user_api_registration") @@ -497,7 +486,7 @@ def test_invalid_country_code_error(self): @ddt.ddt @skip_unless_lms class RegistrationViewTestV1( - ThirdPartyAuthTestMixin, UserAPITestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, ThirdPartyAuthTestMixin, UserAPITestCase ): """Tests for the registration end-points of the User API. """ @@ -563,17 +552,6 @@ class RegistrationViewTestV1( ] link_template = "{link_label}" - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super().setUp() self.url = reverse("user_api_registration") @@ -2077,17 +2055,6 @@ class RegistrationViewTestV2(RegistrationViewTestV1): # pylint: disable=test-inherits-tests - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ super(RegistrationViewTestV1, self).setUp() # lint-amnesty, pylint: disable=bad-super-call self.url = reverse("user_api_registration_v2") @@ -2513,7 +2480,7 @@ def test_registration_allowed_when_embargo_disabled(self): @httpretty.activate @ddt.ddt class ThirdPartyRegistrationTestMixin( - ThirdPartyOAuthTestMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, ThirdPartyOAuthTestMixin, CacheIsolationTestCase ): """ Tests for the User API registration endpoint with 3rd party authentication. @@ -2526,17 +2493,6 @@ class ThirdPartyRegistrationTestMixin( __test__ = False - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() self.url = reverse('user_api_registration') @@ -2731,7 +2687,7 @@ def test_expired_pipeline(self): @skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled") class TestFacebookRegistrationView( - ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase ): """Tests the User API registration endpoint with Facebook authentication.""" @@ -2739,17 +2695,6 @@ class TestFacebookRegistrationView( __test__ = True - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def test_social_auth_exception(self): """ According to the do_auth method in social_core.backends.facebook.py, @@ -2763,7 +2708,7 @@ def test_social_auth_exception(self): @skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled") class TestGoogleRegistrationView( - ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase, OpenEdxEventsTestMixin + OpenEdxEventsTestMixin, ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase ): """Tests the User API registration endpoint with Google authentication.""" @@ -2771,20 +2716,9 @@ class TestGoogleRegistrationView( __test__ = True - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - @ddt.ddt -class RegistrationValidationViewTests(test_utils.ApiTestCase, OpenEdxEventsTestMixin): +class RegistrationValidationViewTests(OpenEdxEventsTestMixin, test_utils.ApiTestCase): """ Tests for validity of user data in registration forms. """ @@ -2794,17 +2728,6 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase, OpenEdxEventsTestM endpoint_name = 'registration_validation' path = reverse(endpoint_name) - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): super().setUp() cache.clear() diff --git a/xmodule/modulestore/tests/test_mixed_modulestore.py b/xmodule/modulestore/tests/test_mixed_modulestore.py index 61b37079938a..285af3dbe450 100644 --- a/xmodule/modulestore/tests/test_mixed_modulestore.py +++ b/xmodule/modulestore/tests/test_mixed_modulestore.py @@ -69,7 +69,7 @@ log = logging.getLogger(__name__) -class CommonMixedModuleStoreSetup(CourseComparisonTest, OpenEdxEventsTestMixin): +class CommonMixedModuleStoreSetup(OpenEdxEventsTestMixin, CourseComparisonTest): """ Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and Location-based dbs) @@ -123,16 +123,6 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest, OpenEdxEventsTestMixin): "org.openedx.content_authoring.xblock.published.v1", ] - @classmethod - def setUpClass(cls): - """ - Set up class method for the Test class. - This method starts manually events isolation. Explanation here: - openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44 - """ - super().setUpClass() - cls.start_events_isolation() - def setUp(self): """ Set up the database for testing From 7628299f78d694e19aee2861cc87579493a5c8c6 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 8 Apr 2026 15:53:28 -0400 Subject: [PATCH 6/8] fix: Don't include OpenEdxEventsTestMixin twice. This mixin is already included via one of the other mixins on this test class so including it again was messing with the MRO for the test classes. --- .../core/djangoapps/user_authn/views/tests/test_register.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 552f3fc162f5..2900860c83be 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -2687,7 +2687,7 @@ def test_expired_pipeline(self): @skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled") class TestFacebookRegistrationView( - OpenEdxEventsTestMixin, ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase + ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinFacebook, TransactionTestCase ): """Tests the User API registration endpoint with Facebook authentication.""" @@ -2708,7 +2708,7 @@ def test_social_auth_exception(self): @skipUnless(settings.FEATURES.get("ENABLE_THIRD_PARTY_AUTH"), "third party auth not enabled") class TestGoogleRegistrationView( - OpenEdxEventsTestMixin, ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase + ThirdPartyRegistrationTestMixin, ThirdPartyOAuthTestMixinGoogle, TransactionTestCase ): """Tests the User API registration endpoint with Google authentication.""" From db5d8357a558567a79053324f0402e0d9c0acdce Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 9 Apr 2026 10:18:47 -0400 Subject: [PATCH 7/8] chore: Upgrade openedx-events to the latest version. --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 183ec08d6bfc..e294e4c4e55e 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -840,7 +840,7 @@ openedx-django-require==3.0.0 # via -r requirements/edx/kernel.in openedx-django-wiki==3.1.1 # via -r requirements/edx/kernel.in -openedx-events==11.1.0 +openedx-events==11.1.1 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index c9473b8bea0f..88bd5890f536 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1401,7 +1401,7 @@ openedx-django-wiki==3.1.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-events==11.1.0 +openedx-events==11.1.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index ebad2efd8d82..f771461a5860 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -1020,7 +1020,7 @@ openedx-django-require==3.0.0 # via -r requirements/edx/base.txt openedx-django-wiki==3.1.1 # via -r requirements/edx/base.txt -openedx-events==11.1.0 +openedx-events==11.1.1 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index be8a03ac36c9..e2586259a36e 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -1067,7 +1067,7 @@ openedx-django-require==3.0.0 # via -r requirements/edx/base.txt openedx-django-wiki==3.1.1 # via -r requirements/edx/base.txt -openedx-events==11.1.0 +openedx-events==11.1.1 # via # -r requirements/edx/base.txt # edx-enterprise From 65881f8ba4aa07fa51a2892ab497f2753fafe4d0 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Thu, 9 Apr 2026 15:38:11 -0400 Subject: [PATCH 8/8] temp: Change the order of the cms-1 tests. We seem to be hanging at the same point in CI so change the order of the tests to see if where we hang changes. --- .github/workflows/unit-test-shards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 02a83a4ceb3a..798c6265a167 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -233,9 +233,9 @@ "cms-1": { "settings": "cms.envs.test", "paths": [ + "cms/djangoapps/contentstore/", "cms/djangoapps/api/", "cms/djangoapps/cms_user_tasks/", - "cms/djangoapps/contentstore/", "cms/djangoapps/course_creators/", "cms/djangoapps/export_course_metadata/", "cms/djangoapps/models/",