diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 90dab57cae60b6d8ff45c56263ffd5be3c1ef82d..c3724623831834155db153dd4966c31af8d2a1a1 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -29,6 +29,11 @@ class CourseModeModelTest(TestCase): """ Tests for the CourseMode model """ + NOW = 'now' + DATES = { + NOW: datetime.now(), + None: None, + } def setUp(self): super(CourseModeModelTest, self).setUp() @@ -317,10 +322,11 @@ class CourseModeModelTest(TestCase): CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE ), - (datetime.now(), None), + (NOW, None), )) @ddt.unpack - def test_invalid_mode_expiration(self, mode_slug, exp_dt): + def test_invalid_mode_expiration(self, mode_slug, exp_dt_name): + exp_dt = self.DATES[exp_dt_name] is_error_expected = CourseMode.is_professional_slug(mode_slug) and exp_dt is not None try: self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10) diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py index c14ea5b2340ad021c9dff28347ec256b3be2918b..56517f7984f32a38b34aea3d5682d5b556e5c338 100644 --- a/common/djangoapps/student/roles.py +++ b/common/djangoapps/student/roles.py @@ -234,12 +234,16 @@ class CourseRole(RoleBase): def course_group_already_exists(self, course_key): return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists() + def __repr__(self): + return '<{}: course_key={}>'.format(self.__class__.__name__, self.course_key) + class OrgRole(RoleBase): """ A named role in a particular org independent of course """ - pass + def __repr__(self): + return '<{}>'.format(self.__class__.__name__) @register_access_role diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py index 75f58848cd988fc2d3e7186b01ea32ac830d6eac..40cbcb84337bfa93bf3b756ffa9dc490ecba1b97 100644 --- a/common/djangoapps/student/tests/test_verification_status.py +++ b/common/djangoapps/student/tests/test_verification_status.py @@ -33,8 +33,13 @@ from xmodule.modulestore.tests.factories import CourseFactory class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): """Tests for per-course verification status on the dashboard. """ - PAST = datetime.now(UTC) - timedelta(days=5) - FUTURE = datetime.now(UTC) + timedelta(days=5) + PAST = 'past' + FUTURE = 'future' + DATES = { + PAST: datetime.now(UTC) - timedelta(days=5), + FUTURE: datetime.now(UTC) + timedelta(days=5), + None: None, + } URLCONF_MODULES = ['verify_student.urls'] @@ -91,14 +96,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY) def test_need_to_verify_expiration(self): - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") response = self.client.get(self.dashboard_url) self.assertContains(response, self.BANNER_ALT_MESSAGES[VERIFY_STATUS_NEED_TO_VERIFY]) self.assertContains(response, "You only have 4 days left to verify for this course.") @ddt.data(None, FUTURE) def test_waiting_approval(self, expiration): - self._setup_mode_and_enrollment(expiration, "verified") + self._setup_mode_and_enrollment(self.DATES[expiration], "verified") # The student has submitted a photo verification attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -110,7 +115,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): @ddt.data(None, FUTURE) def test_fully_verified(self, expiration): - self._setup_mode_and_enrollment(expiration, "verified") + self._setup_mode_and_enrollment(self.DATES[expiration], "verified") # The student has an approved verification attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -127,7 +132,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_missed_verification_deadline(self): # Expiration date in the past - self._setup_mode_and_enrollment(self.PAST, "verified") + self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") # The student does NOT have an approved verification # so the status should show that the student missed the deadline. @@ -135,7 +140,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_missed_verification_deadline_verification_was_expired(self): # Expiration date in the past - self._setup_mode_and_enrollment(self.PAST, "verified") + self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") # Create a verification, but the expiration date of the verification # occurred before the deadline. @@ -143,7 +148,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): attempt.mark_ready() attempt.submit() attempt.approve() - attempt.created_at = self.PAST - timedelta(days=900) + attempt.created_at = self.DATES[self.PAST] - timedelta(days=900) attempt.save() # The student didn't have an approved verification at the deadline, @@ -152,14 +157,14 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_missed_verification_deadline_but_later_verified(self): # Expiration date in the past - self._setup_mode_and_enrollment(self.PAST, "verified") + self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") # Successfully verify, but after the deadline has already passed attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) attempt.mark_ready() attempt.submit() attempt.approve() - attempt.created_at = self.PAST - timedelta(days=900) + attempt.created_at = self.DATES[self.PAST] - timedelta(days=900) attempt.save() # The student didn't have an approved verification at the deadline, @@ -168,7 +173,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_verification_denied(self): # Expiration date in the future - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") # Create a verification with the specified status attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -182,7 +187,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_verification_error(self): # Expiration date in the future - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") # Create a verification with the specified status attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -196,7 +201,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) def test_verification_will_expire_by_deadline(self): # Expiration date in the future - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") # Create a verification attempt that: # 1) Is current (submitted in the last year) @@ -213,7 +218,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): @override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10}) def test_reverification_submitted_with_current_approved_verificaiton(self): # Expiration date in the future - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") # Create a verification attempt that is approved but expiring soon attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -236,7 +241,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_verification_occurred_after_deadline(self): # Expiration date in the past - self._setup_mode_and_enrollment(self.PAST, "verified") + self._setup_mode_and_enrollment(self.DATES[self.PAST], "verified") # The deadline has passed, and we've asked the student # to reverify (through the support team). @@ -250,7 +255,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): def test_with_two_verifications(self): # checking if a user has two verification and but most recent verification course deadline is expired - self._setup_mode_and_enrollment(self.FUTURE, "verified") + self._setup_mode_and_enrollment(self.DATES[self.FUTURE], "verified") # The student has an approved verification attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) @@ -274,7 +279,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): CourseModeFactory.create( course_id=course2.id, mode_slug="verified", - expiration_datetime=self.PAST + expiration_datetime=self.DATES[self.PAST] ) CourseEnrollmentFactory( course_id=course2.id, diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index c74b5e690d0e2cb07eee318d9b12e62a8a15cca6..073eb5c7958146b3fcebdbe2281ea62a4380c3a4 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -297,19 +297,18 @@ class StudentDashboardTests(SharedModuleStoreTestCase): @patch.multiple('django.conf.settings', **MOCK_SETTINGS) @ddt.data( *itertools.product( - [TOMORROW], [True, False], [True, False], [ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split], ) ) @ddt.unpack - def test_sharing_icons_for_future_course(self, start_date, set_marketing, set_social_sharing, modulestore_type): + def test_sharing_icons_for_future_course(self, set_marketing, set_social_sharing, modulestore_type): """ Verify that the course sharing icons show up if course is starting in future and any of marketing or social sharing urls are set. """ - self.course = CourseFactory.create(start=start_date, emit_signals=True, default_store=modulestore_type) + self.course = CourseFactory.create(start=self.TOMORROW, emit_signals=True, default_store=modulestore_type) self.course_enrollment = CourseEnrollmentFactory(course_id=self.course.id, user=self.user) self.set_course_sharing_urls(set_marketing, set_social_sharing) diff --git a/common/djangoapps/util/tests/test_db.py b/common/djangoapps/util/tests/test_db.py index a3c780149080ffc351a7de89b31c654bcfe0ea30..3830d178eae8d862204bb3c08a77a1c70747413a 100644 --- a/common/djangoapps/util/tests/test_db.py +++ b/common/djangoapps/util/tests/test_db.py @@ -31,20 +31,26 @@ class TransactionManagersTestCase(TransactionTestCase): To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db" """ + DECORATORS = { + 'outer_atomic': outer_atomic(), + 'outer_atomic_read_committed': outer_atomic(read_committed=True), + 'commit_on_success': commit_on_success(), + 'commit_on_success_read_committed': commit_on_success(read_committed=True), + } @ddt.data( - (outer_atomic(), IntegrityError, None, True), - (outer_atomic(read_committed=True), type(None), False, True), - (commit_on_success(), IntegrityError, None, True), - (commit_on_success(read_committed=True), type(None), False, True), + ('outer_atomic', IntegrityError, None, True), + ('outer_atomic_read_committed', type(None), False, True), + ('commit_on_success', IntegrityError, None, True), + ('commit_on_success_read_committed', type(None), False, True), ) @ddt.unpack - def test_concurrent_requests(self, transaction_decorator, exception_class, created_in_1, created_in_2): + def test_concurrent_requests(self, transaction_decorator_name, exception_class, created_in_1, created_in_2): """ Test that when isolation level is set to READ COMMITTED get_or_create() for the same row in concurrent requests does not raise an IntegrityError. """ - + transaction_decorator = self.DECORATORS[transaction_decorator_name] if connection.vendor != 'mysql': raise unittest.SkipTest('Only works on MySQL.') diff --git a/common/test/utils.py b/common/test/utils.py index cab85a99703fbb767ca81ce8b6f06cab0bea87dd..0457ead3ebacf9526806ac201a1ea361991b15a6 100644 --- a/common/test/utils.py +++ b/common/test/utils.py @@ -1,6 +1,7 @@ """ General testing utilities. """ +import functools import sys from contextlib import contextmanager @@ -124,3 +125,29 @@ class MockS3Mixin(object): def tearDown(self): self._mock_s3.stop() super(MockS3Mixin, self).tearDown() + + +class reprwrapper(object): + """ + Wrapper class for functions that need a normalized string representation. + """ + def __init__(self, func): + self._func = func + self.repr = 'Func: {}'.format(func.__name__) + functools.update_wrapper(self, func) + + def __call__(self, *args, **kw): + return self._func(*args, **kw) + + def __repr__(self): + return self.repr + + +def normalize_repr(func): + """ + Function decorator used to normalize its string representation. + Used to wrap functions used as ddt parameters, so pytest-xdist + doesn't complain about the sequence of discovered tests differing + between worker processes. + """ + return reprwrapper(func) diff --git a/lms/djangoapps/certificates/tests/test_queue.py b/lms/djangoapps/certificates/tests/test_queue.py index dbc84cf6a4d2f883a2f75a4b9a4d95fa504e7ac3..dac452ceca487b7edcb8cf8a1717528ea2b72550 100644 --- a/lms/djangoapps/certificates/tests/test_queue.py +++ b/lms/djangoapps/certificates/tests/test_queue.py @@ -207,42 +207,42 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): # Eligible and should stay that way ( CertificateStatuses.downloadable, - datetime.now(pytz.UTC) - timedelta(days=2), + timedelta(days=-2), 'Pass', CertificateStatuses.generating ), # Ensure that certs in the wrong state can be fixed by regeneration ( CertificateStatuses.downloadable, - datetime.now(pytz.UTC) - timedelta(hours=1), + timedelta(hours=-1), 'Pass', CertificateStatuses.audit_passing ), # Ineligible and should stay that way ( CertificateStatuses.audit_passing, - datetime.now(pytz.UTC) - timedelta(hours=1), + timedelta(hours=-1), 'Pass', CertificateStatuses.audit_passing ), # As above ( CertificateStatuses.audit_notpassing, - datetime.now(pytz.UTC) - timedelta(hours=1), + timedelta(hours=-1), 'Pass', CertificateStatuses.audit_passing ), # As above ( CertificateStatuses.audit_notpassing, - datetime.now(pytz.UTC) - timedelta(hours=1), + timedelta(hours=-1), None, CertificateStatuses.audit_notpassing ), ) @ddt.unpack @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) - def test_regen_audit_certs_eligibility(self, status, created_date, grade, expected_status): + def test_regen_audit_certs_eligibility(self, status, created_delta, grade, expected_status): """ Test that existing audit certificates remain eligible even if cert generation is re-run. @@ -254,6 +254,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): is_active=True, mode=CourseMode.AUDIT, ) + created_date = datetime.now(pytz.UTC) + created_delta with freezegun.freeze_time(created_date): GeneratedCertificateFactory( user=self.user_2, diff --git a/lms/djangoapps/commerce/api/v1/tests/test_views.py b/lms/djangoapps/commerce/api/v1/tests/test_views.py index 569f70ee9e817b4803dfbcde8e3f22d71075444a..177d1ea9f3552f85e7106b48433a722956b22a2d 100644 --- a/lms/djangoapps/commerce/api/v1/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v1/tests/test_views.py @@ -106,6 +106,11 @@ class CourseListViewTests(CourseApiViewTestMixin, ModuleStoreTestCase): @ddt.ddt class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase): """ Tests for CourseRetrieveUpdateView. """ + NOW = 'now' + DATES = { + NOW: datetime.now(), + None: None, + } def setUp(self): super(CourseRetrieveUpdateViewTests, self).setUp() @@ -276,12 +281,13 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) @ddt.data(*itertools.product( ('honor', 'audit', 'verified', 'professional', 'no-id-professional'), - (datetime.now(), None), + (NOW, None), )) @ddt.unpack - def test_update_professional_expiration(self, mode_slug, expiration_datetime): + def test_update_professional_expiration(self, mode_slug, expiration_datetime_name): """ Verify that pushing a mode with a professional certificate and an expiration datetime will be rejected (this is not allowed). """ + expiration_datetime = self.DATES[expiration_datetime_name] mode = self._serialize_course_mode( CourseMode( mode_slug=mode_slug, diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index d7a4732d46235db7af57beb832b509c4578a68ab..acc4948a9e13eb8667cc4605f202882b15be12d7 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -162,9 +162,14 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes """ Tests for the various access controls on the student dashboard """ - TOMORROW = datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1) - YESTERDAY = datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1) + TOMORROW = 'tomorrow' + YESTERDAY = 'yesterday' MODULESTORE = TEST_DATA_SPLIT_MODULESTORE + DATES = { + TOMORROW: datetime.datetime.now(pytz.utc) + datetime.timedelta(days=1), + YESTERDAY: datetime.datetime.now(pytz.utc) - datetime.timedelta(days=1), + None: None, + } def setUp(self): super(AccessTestCase, self).setUp() @@ -439,7 +444,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit.visible_to_staff_only = visible_to_staff_only - mock_unit.start = start + mock_unit.start = self.DATES[start] mock_unit.merged_group_access = {} self.verify_access(mock_unit, expected_access, expected_error_type) @@ -448,7 +453,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes mock_unit = Mock(user_partitions=[]) mock_unit._class_tags = {} mock_unit.days_early_for_beta = 2 - mock_unit.start = self.TOMORROW + mock_unit.start = self.DATES[self.TOMORROW] mock_unit.visible_to_staff_only = False mock_unit.merged_group_access = {} @@ -465,7 +470,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit.visible_to_staff_only = False - mock_unit.start = start + mock_unit.start = self.DATES[start] mock_unit.merged_group_access = {} self.verify_access(mock_unit, True) @@ -486,7 +491,7 @@ class AccessTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, MilestonesTes mock_unit = Mock(location=self.course.location, user_partitions=[]) mock_unit._class_tags = {} # Needed for detached check in _has_access_descriptor mock_unit.visible_to_staff_only = False - mock_unit.start = start + mock_unit.start = self.DATES[start] mock_unit.merged_group_access = {} self.verify_access(mock_unit, expected_access, expected_error_type) diff --git a/lms/djangoapps/courseware/tests/test_courses.py b/lms/djangoapps/courseware/tests/test_courses.py index f9229dbda57bc20a61f2fd8d1e751c441888ca46..50c6f137ed5d0a2c7ba0422b4e30c74273e53c51 100644 --- a/lms/djangoapps/courseware/tests/test_courses.py +++ b/lms/djangoapps/courseware/tests/test_courses.py @@ -49,6 +49,12 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT class CoursesTest(ModuleStoreTestCase): """Test methods related to fetching courses.""" ENABLED_SIGNALS = ['course_published'] + GET_COURSE_WITH_ACCESS = 'get_course_with_access' + GET_COURSE_OVERVIEW_WITH_ACCESS = 'get_course_overview_with_access' + COURSE_ACCESS_FUNCS = { + GET_COURSE_WITH_ACCESS: get_course_with_access, + GET_COURSE_OVERVIEW_WITH_ACCESS: get_course_overview_with_access, + } @override_settings(CMS_BASE=CMS_BASE_TEST) def test_get_cms_course_block_link(self): @@ -64,8 +70,9 @@ class CoursesTest(ModuleStoreTestCase): cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location)) self.assertEqual(cms_url, get_cms_block_link(self.course, 'course')) - @ddt.data(get_course_with_access, get_course_overview_with_access) - def test_get_course_func_with_access_error(self, course_access_func): + @ddt.data(GET_COURSE_WITH_ACCESS, GET_COURSE_OVERVIEW_WITH_ACCESS) + def test_get_course_func_with_access_error(self, course_access_func_name): + course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name] user = UserFactory.create() course = CourseFactory.create(visible_to_staff_only=True) @@ -76,11 +83,12 @@ class CoursesTest(ModuleStoreTestCase): self.assertFalse(error.exception.access_response.has_access) @ddt.data( - (get_course_with_access, 1), - (get_course_overview_with_access, 0), + (GET_COURSE_WITH_ACCESS, 1), + (GET_COURSE_OVERVIEW_WITH_ACCESS, 0), ) @ddt.unpack - def test_get_course_func_with_access(self, course_access_func, num_mongo_calls): + def test_get_course_func_with_access(self, course_access_func_name, num_mongo_calls): + course_access_func = self.COURSE_ACCESS_FUNCS[course_access_func_name] user = UserFactory.create() course = CourseFactory.create(emit_signals=True) with check_mongo_calls(num_mongo_calls): diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index 3c215bbff2ae1a57dd288dce8e5cc3bfe7dd919a..6ed47a59cdf456a9ee086b55a7e7c62e028c6b1b 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1570,11 +1570,11 @@ class TestStaffDebugInfo(SharedModuleStoreTestCase): PER_COURSE_ANONYMIZED_DESCRIPTORS = (LTIDescriptor, ) -# The "set" here is to work around the bug that load_classes returns duplicates for multiply-delcared classes. -PER_STUDENT_ANONYMIZED_DESCRIPTORS = set( +# The "set" here is to work around the bug that load_classes returns duplicates for multiply-declared classes. +PER_STUDENT_ANONYMIZED_DESCRIPTORS = sorted(set( class_ for (name, class_) in XModuleDescriptor.load_classes() if not issubclass(class_, PER_COURSE_ANONYMIZED_DESCRIPTORS) -) +), key=str) @attr(shard=1) diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index a4c04cb795fe854dd81b7175116178d5ea91406f..83d555dda56b34787dc2dd14c75c5bae37457281 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -13,6 +13,7 @@ from mock import MagicMock, Mock, patch from nose.plugins.attrib import attr from webob import Request +from common.test.utils import normalize_repr from openedx.core.djangoapps.contentserver.caching import del_cached_content from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore @@ -105,6 +106,7 @@ def _upload_file(subs_file, location, filename): del_cached_content(content.location) +@normalize_repr def attach_sub(item, filename): """ Attach `en` transcript. @@ -112,6 +114,7 @@ def attach_sub(item, filename): item.sub = filename +@normalize_repr def attach_bumper_transcript(item, filename, lang="en"): """ Attach bumper transcript. diff --git a/lms/djangoapps/courseware/tests/test_video_mongo.py b/lms/djangoapps/courseware/tests/test_video_mongo.py index 807a7e23cbde008ad3f2fccec6965784ad39440b..6f13abc44fc192a51e1d951f50b61289c1f71170 100644 --- a/lms/djangoapps/courseware/tests/test_video_mongo.py +++ b/lms/djangoapps/courseware/tests/test_video_mongo.py @@ -17,6 +17,7 @@ from path import Path as path from xmodule.contentstore.content import StaticContent from xmodule.exceptions import NotFoundError +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE from xmodule.tests.test_import import DummySystem @@ -29,6 +30,11 @@ from .helpers import BaseTestXmodule from .test_video_handlers import TestVideo from .test_video_xml import SOURCE_XML +MODULESTORES = { + ModuleStoreEnum.Type.mongo: TEST_DATA_MONGO_MODULESTORE, + ModuleStoreEnum.Type.split: TEST_DATA_SPLIT_MODULESTORE, +} + @attr(shard=1) class TestVideoYouTube(TestVideo): @@ -1162,14 +1168,14 @@ class TestEditorSavedMethod(BaseTestXmodule): self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname() self.file_path = self.test_dir + '/common/test/data/uploads/' + self.file_name - @ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_editor_saved_when_html5_sub_not_exist(self, default_store): """ When there is youtube_sub exist but no html5_sub present for html5_sources, editor_saved function will generate new html5_sub for video. """ - self.MODULESTORE = default_store # pylint: disable=invalid-name + self.MODULESTORE = MODULESTORES[default_store] # pylint: disable=invalid-name self.initialize_module(metadata=self.metadata) item = self.store.get_item(self.item_descriptor.location) with open(self.file_path, "r") as myfile: @@ -1184,13 +1190,13 @@ class TestEditorSavedMethod(BaseTestXmodule): self.assertIsInstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent) self.assertIsInstance(Transcript.get_asset(item.location, 'subs_video.srt.sjson'), StaticContent) - @ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_editor_saved_when_youtube_and_html5_subs_exist(self, default_store): """ When both youtube_sub and html5_sub already exist then no new sub will be generated by editor_saved function. """ - self.MODULESTORE = default_store + self.MODULESTORE = MODULESTORES[default_store] self.initialize_module(metadata=self.metadata) item = self.store.get_item(self.item_descriptor.location) with open(self.file_path, "r") as myfile: @@ -1205,12 +1211,12 @@ class TestEditorSavedMethod(BaseTestXmodule): item.editor_saved(self.user, old_metadata, None) self.assertFalse(manage_video_subtitles_save.called) - @ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_editor_saved_with_unstripped_video_id(self, default_store): """ Verify editor saved when video id contains spaces/tabs. """ - self.MODULESTORE = default_store + self.MODULESTORE = MODULESTORES[default_store] stripped_video_id = unicode(uuid4()) unstripped_video_id = u'{video_id}{tabs}'.format(video_id=stripped_video_id, tabs=u'\t\t\t') self.metadata.update({ @@ -1226,14 +1232,14 @@ class TestEditorSavedMethod(BaseTestXmodule): item.editor_saved(self.user, old_metadata, None) self.assertEqual(item.edx_video_id, stripped_video_id) - @ddt.data(TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE) + @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @patch('xmodule.video_module.video_module.edxval_api.get_url_for_profile', Mock(return_value='test_yt_id')) def test_editor_saved_with_yt_val_profile(self, default_store): """ Verify editor saved overrides `youtube_id_1_0` when a youtube val profile is there for a given `edx_video_id`. """ - self.MODULESTORE = default_store + self.MODULESTORE = MODULESTORES[default_store] self.initialize_module(metadata=self.metadata) item = self.store.get_item(self.item_descriptor.location) self.assertEqual(item.youtube_id_1_0, '3_yD_cEKoCk') diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index eb08d7934eb24baa887e7385f368b2502d0d0ec3..5cdf69af9b617734ceac6cba03c554a7f0aaf89d 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -251,6 +251,11 @@ class ViewsTestCase(ModuleStoreTestCase): """ Tests for views.py methods. """ + YESTERDAY = 'yesterday' + DATES = { + YESTERDAY: datetime.now(UTC) - timedelta(days=1), + None: None, + } def setUp(self): super(ViewsTestCase, self).setUp() @@ -751,7 +756,7 @@ class ViewsTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 200) self.assertIn('Financial Assistance Application', response.content) - @ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, datetime.now(UTC) - timedelta(days=1)), + @ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, YESTERDAY), ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None), ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None), ([CourseMode.AUDIT], CourseMode.AUDIT, False, None)) @@ -770,7 +775,7 @@ class ViewsTestCase(ModuleStoreTestCase): # Create Course Modes for mode in course_modes: - CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=expiration) + CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=self.DATES[expiration]) # Enroll user in the course CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode) @@ -1705,10 +1710,16 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): # Constants used in the test data NOW = datetime.now(UTC) DAY_DELTA = timedelta(days=1) - YESTERDAY = NOW - DAY_DELTA - TODAY = NOW - TOMORROW = NOW + DAY_DELTA + YESTERDAY = 'yesterday' + TODAY = 'today' + TOMORROW = 'tomorrow' GRADER_TYPE = 'Homework' + DATES = { + YESTERDAY: NOW - DAY_DELTA, + TODAY: NOW, + TOMORROW: NOW + DAY_DELTA, + None: None, + } def setUp(self): super(ProgressPageShowCorrectnessTests, self).setUp() @@ -1853,12 +1864,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): (ShowCorrectness.PAST_DUE, TOMORROW, True), ) @ddt.unpack - def test_progress_page_no_problem_scores(self, show_correctness, due_date, graded): + def test_progress_page_no_problem_scores(self, show_correctness, due_date_name, graded): """ Test that "no problem scores are present" for a course with no problems, regardless of the various show correctness settings. """ - self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) + self.setup_course(show_correctness=show_correctness, due_date=self.DATES[due_date_name], graded=graded) resp = self._get_progress_page() # Test that no problem scores are present @@ -1893,11 +1904,12 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): (ShowCorrectness.PAST_DUE, TOMORROW, True, False), ) @ddt.unpack - def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date, graded, show_grades): + def test_progress_page_hide_scores_from_learner(self, show_correctness, due_date_name, graded, show_grades): """ Test that problem scores are hidden on progress page when correctness is not available to the learner, and that they are visible when it is. """ + due_date = self.DATES[due_date_name] self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.add_problem() @@ -1944,10 +1956,11 @@ class ProgressPageShowCorrectnessTests(ProgressPageBaseTests): (ShowCorrectness.PAST_DUE, TOMORROW, True, True), ) @ddt.unpack - def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date, graded, show_grades): + def test_progress_page_hide_scores_from_staff(self, show_correctness, due_date_name, graded, show_grades): """ Test that problem scores are hidden from staff viewing a learner's progress page only if show_correctness=never. """ + due_date = self.DATES[due_date_name] self.setup_course(show_correctness=show_correctness, due_date=due_date, graded=graded) self.add_problem() diff --git a/lms/djangoapps/grades/tests/test_scores.py b/lms/djangoapps/grades/tests/test_scores.py index 286e3a3cad97cebde962b169259d6ac9e3c607a4..c134586c1b21bbcd580d470bcf895c90a5a47173 100644 --- a/lms/djangoapps/grades/tests/test_scores.py +++ b/lms/djangoapps/grades/tests/test_scores.py @@ -19,6 +19,35 @@ from xmodule.graders import ProblemScore NOW = now() +def submission_value_repr(self): + """ + String representation for the SubmissionValue namedtuple which excludes + the "created_at" attribute that changes with each execution. Needed for + consistency of ddt-generated test methods across pytest-xdist workers. + """ + return '<SubmissionValue exists={}>'.format(self.exists) + + +def csm_value_repr(self): + """ + String representation for the CSMValue namedtuple which excludes + the "created" attribute that changes with each execution. Needed for + consistency of ddt-generated test methods across pytest-xdist workers. + """ + return '<CSMValue exists={} raw_earned={}>'.format(self.exists, self.raw_earned) + + +def expected_result_repr(self): + """ + String representation for the ExpectedResult namedtuple which excludes + the "first_attempted" attribute that changes with each execution. Needed + for consistency of ddt-generated test methods across pytest-xdist workers. + """ + included = ('raw_earned', 'raw_possible', 'weighted_earned', 'weighted_possible', 'weight', 'graded') + attributes = ['{}={}'.format(name, getattr(self, name)) for name in included] + return '<ExpectedResult {}>'.format(' '.join(attributes)) + + class TestScoredBlockTypes(TestCase): """ Tests for the possibly_scored function. @@ -52,13 +81,16 @@ class TestGetScore(TestCase): location = 'test_location' SubmissionValue = namedtuple('SubmissionValue', 'exists, points_earned, points_possible, created_at') + SubmissionValue.__repr__ = submission_value_repr CSMValue = namedtuple('CSMValue', 'exists, raw_earned, raw_possible, created') + CSMValue.__repr__ = csm_value_repr PersistedBlockValue = namedtuple('PersistedBlockValue', 'exists, raw_possible, weight, graded') ContentBlockValue = namedtuple('ContentBlockValue', 'raw_possible, weight, explicit_graded') ExpectedResult = namedtuple( 'ExpectedResult', 'raw_earned, raw_possible, weighted_earned, weighted_possible, weight, graded, first_attempted' ) + ExpectedResult.__repr__ = expected_result_repr def _create_submissions_scores(self, submission_value): """ diff --git a/lms/djangoapps/grades/tests/test_signals.py b/lms/djangoapps/grades/tests/test_signals.py index 58890f82764405aab0ab8aaba11bc920750225dd..f28282125d72c195f1f00842f12def4e3a93d1fb 100644 --- a/lms/djangoapps/grades/tests/test_signals.py +++ b/lms/djangoapps/grades/tests/test_signals.py @@ -16,7 +16,6 @@ from util.date_utils import to_timestamp from ..constants import ScoreDatabaseTableEnum from ..signals.handlers import ( disconnect_submissions_signal_receiver, - enqueue_subsection_update, problem_raw_score_changed_handler, submissions_score_reset_handler, submissions_score_set_handler @@ -28,20 +27,30 @@ UUID_REGEX = re.compile(ur'%(hex)s{8}-%(hex)s{4}-%(hex)s{4}-%(hex)s{4}-%(hex)s{1 FROZEN_NOW_DATETIME = datetime.now().replace(tzinfo=pytz.UTC) FROZEN_NOW_TIMESTAMP = to_timestamp(FROZEN_NOW_DATETIME) -SUBMISSION_SET_KWARGS = { - 'points_possible': 10, - 'points_earned': 5, - 'anonymous_user_id': 'anonymous_id', - 'course_id': 'CourseID', - 'item_id': 'i4x://org/course/usage/123456', - 'created_at': FROZEN_NOW_TIMESTAMP, +SUBMISSIONS_SCORE_SET_HANDLER = 'submissions_score_set_handler' +SUBMISSIONS_SCORE_RESET_HANDLER = 'submissions_score_reset_handler' +HANDLERS = { + SUBMISSIONS_SCORE_SET_HANDLER: submissions_score_set_handler, + SUBMISSIONS_SCORE_RESET_HANDLER: submissions_score_reset_handler, } -SUBMISSION_RESET_KWARGS = { - 'anonymous_user_id': 'anonymous_id', - 'course_id': 'CourseID', - 'item_id': 'i4x://org/course/usage/123456', - 'created_at': FROZEN_NOW_TIMESTAMP, +SUBMISSION_SET_KWARGS = 'submission_set_kwargs' +SUBMISSION_RESET_KWARGS = 'submission_reset_kwargs' +SUBMISSION_KWARGS = { + SUBMISSION_SET_KWARGS: { + 'points_possible': 10, + 'points_earned': 5, + 'anonymous_user_id': 'anonymous_id', + 'course_id': 'CourseID', + 'item_id': 'i4x://org/course/usage/123456', + 'created_at': FROZEN_NOW_TIMESTAMP, + }, + SUBMISSION_RESET_KWARGS: { + 'anonymous_user_id': 'anonymous_id', + 'course_id': 'CourseID', + 'item_id': 'i4x://org/course/usage/123456', + 'created_at': FROZEN_NOW_TIMESTAMP, + }, } PROBLEM_RAW_SCORE_CHANGED_KWARGS = { @@ -82,6 +91,10 @@ class ScoreChangedSignalRelayTest(TestCase): This ensures that listeners in the LMS only have to handle one type of signal for all scoring events regardless of their origin. """ + SIGNALS = { + 'score_set': score_set, + 'score_reset': score_reset, + } def setUp(self): """ @@ -110,11 +123,11 @@ class ScoreChangedSignalRelayTest(TestCase): return mock @ddt.data( - [submissions_score_set_handler, SUBMISSION_SET_KWARGS, 5, 10], - [submissions_score_reset_handler, SUBMISSION_RESET_KWARGS, 0, 0], + [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS, 5, 10], + [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS, 0, 0], ) @ddt.unpack - def test_score_set_signal_handler(self, handler, kwargs, earned, possible): + def test_score_set_signal_handler(self, handler_name, kwargs, earned, possible): """ Ensure that on receipt of a score_(re)set signal from the Submissions API, the signal handler correctly converts it to a PROBLEM_WEIGHTED_SCORE_CHANGED @@ -122,7 +135,9 @@ class ScoreChangedSignalRelayTest(TestCase): Also ensures that the handler calls user_by_anonymous_id correctly. """ - handler(None, **kwargs) + local_kwargs = SUBMISSION_KWARGS[kwargs].copy() + handler = HANDLERS[handler_name] + handler(None, **local_kwargs) expected_set_kwargs = { 'sender': None, 'weighted_possible': possible, @@ -134,35 +149,36 @@ class ScoreChangedSignalRelayTest(TestCase): 'modified': FROZEN_NOW_TIMESTAMP, 'score_db_table': 'submissions', } - if handler == submissions_score_reset_handler: + if kwargs == SUBMISSION_RESET_KWARGS: expected_set_kwargs['score_deleted'] = True self.signal_mock.assert_called_once_with(**expected_set_kwargs) - self.get_user_mock.assert_called_once_with(kwargs['anonymous_user_id']) + self.get_user_mock.assert_called_once_with(local_kwargs['anonymous_user_id']) def test_tnl_6599_zero_possible_bug(self): """ Ensure that, if coming from the submissions API, signals indicating a a possible score of 0 are swallowed for reasons outlined in TNL-6559. """ - local_kwargs = SUBMISSION_SET_KWARGS.copy() + local_kwargs = SUBMISSION_KWARGS[SUBMISSION_SET_KWARGS].copy() local_kwargs['points_earned'] = 0 local_kwargs['points_possible'] = 0 submissions_score_set_handler(None, **local_kwargs) self.signal_mock.assert_not_called() @ddt.data( - [submissions_score_set_handler, SUBMISSION_SET_KWARGS], - [submissions_score_reset_handler, SUBMISSION_RESET_KWARGS] + [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS], + [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS] ) @ddt.unpack - def test_score_set_missing_kwarg(self, handler, kwargs): + def test_score_set_missing_kwarg(self, handler_name, kwargs): """ Ensure that, on receipt of a score_(re)set signal from the Submissions API that does not have the correct kwargs, the courseware model does not generate a signal. """ - for missing in kwargs: - local_kwargs = kwargs.copy() + handler = HANDLERS[handler_name] + for missing in SUBMISSION_KWARGS[kwargs]: + local_kwargs = SUBMISSION_KWARGS[kwargs].copy() del local_kwargs[missing] with self.assertRaises(KeyError): @@ -170,18 +186,19 @@ class ScoreChangedSignalRelayTest(TestCase): self.signal_mock.assert_not_called() @ddt.data( - [submissions_score_set_handler, SUBMISSION_SET_KWARGS], - [submissions_score_reset_handler, SUBMISSION_RESET_KWARGS] + [SUBMISSIONS_SCORE_SET_HANDLER, SUBMISSION_SET_KWARGS], + [SUBMISSIONS_SCORE_RESET_HANDLER, SUBMISSION_RESET_KWARGS] ) @ddt.unpack - def test_score_set_bad_user(self, handler, kwargs): + def test_score_set_bad_user(self, handler_name, kwargs): """ Ensure that, on receipt of a score_(re)set signal from the Submissions API that has an invalid user ID, the courseware model does not generate a signal. """ + handler = HANDLERS[handler_name] self.get_user_mock = self.setup_patch('lms.djangoapps.grades.signals.handlers.user_by_anonymous_id', None) - handler(None, **kwargs) + handler(None, **SUBMISSION_KWARGS[kwargs]) self.signal_mock.assert_not_called() def test_raw_score_changed_signal_handler(self): @@ -198,14 +215,18 @@ class ScoreChangedSignalRelayTest(TestCase): self.signal_mock.assert_called_with(**expected_set_kwargs) @ddt.data( - [score_set, 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler', SUBMISSION_SET_KWARGS], - [score_reset, 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler', SUBMISSION_RESET_KWARGS] + ['score_set', 'lms.djangoapps.grades.signals.handlers.submissions_score_set_handler', + SUBMISSION_SET_KWARGS], + ['score_reset', 'lms.djangoapps.grades.signals.handlers.submissions_score_reset_handler', + SUBMISSION_RESET_KWARGS] ) @ddt.unpack - def test_disconnect_manager(self, signal, handler, kwargs): + def test_disconnect_manager(self, signal_name, handler, kwargs): """ Tests to confirm the disconnect_submissions_signal_receiver context manager is working correctly. """ + signal = self.SIGNALS[signal_name] + kwargs = SUBMISSION_KWARGS[kwargs].copy() handler_mock = self.setup_patch(handler, None) # Receiver connected before we start diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index e010455ff56d41a63c3c70bd3ed73ff9ed903e70..95d9b6a84bf985f223bd6c0aafa23d1b6bee903e 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -7,6 +7,7 @@ from nose.plugins.attrib import attr from bulk_email.models import SEND_TO_LEARNERS, SEND_TO_MYSELF, SEND_TO_STAFF, CourseEmail from certificates.models import CertificateGenerationHistory, CertificateStatuses +from common.test.utils import normalize_repr from courseware.tests.factories import UserFactory from lms.djangoapps.instructor_task.api import ( SpecificStudentIdMissingError, @@ -147,21 +148,29 @@ class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase): self._test_submit_with_long_url(submit_delete_problem_state_for_all_students) @ddt.data( - (submit_rescore_problem_for_all_students, 'rescore_problem'), - (submit_rescore_problem_for_all_students, 'rescore_problem_if_higher', {'only_if_higher': True}), - (submit_rescore_problem_for_student, 'rescore_problem', {'student': True}), - (submit_rescore_problem_for_student, 'rescore_problem_if_higher', {'student': True, 'only_if_higher': True}), - (submit_reset_problem_attempts_for_all_students, 'reset_problem_attempts'), - (submit_delete_problem_state_for_all_students, 'delete_problem_state'), - (submit_rescore_entrance_exam_for_student, 'rescore_problem', {'student': True}), + (normalize_repr(submit_rescore_problem_for_all_students), 'rescore_problem'), ( - submit_rescore_entrance_exam_for_student, + normalize_repr(submit_rescore_problem_for_all_students), + 'rescore_problem_if_higher', + {'only_if_higher': True} + ), + (normalize_repr(submit_rescore_problem_for_student), 'rescore_problem', {'student': True}), + ( + normalize_repr(submit_rescore_problem_for_student), + 'rescore_problem_if_higher', + {'student': True, 'only_if_higher': True} + ), + (normalize_repr(submit_reset_problem_attempts_for_all_students), 'reset_problem_attempts'), + (normalize_repr(submit_delete_problem_state_for_all_students), 'delete_problem_state'), + (normalize_repr(submit_rescore_entrance_exam_for_student), 'rescore_problem', {'student': True}), + ( + normalize_repr(submit_rescore_entrance_exam_for_student), 'rescore_problem_if_higher', {'student': True, 'only_if_higher': True}, ), - (submit_reset_problem_attempts_in_entrance_exam, 'reset_problem_attempts', {'student': True}), - (submit_delete_entrance_exam_state_for_student, 'delete_problem_state', {'student': True}), - (submit_override_score, 'override_problem_score', {'student': True, 'score': 0}) + (normalize_repr(submit_reset_problem_attempts_in_entrance_exam), 'reset_problem_attempts', {'student': True}), + (normalize_repr(submit_delete_entrance_exam_state_for_student), 'delete_problem_state', {'student': True}), + (normalize_repr(submit_override_score), 'override_problem_score', {'student': True, 'score': 0}) ) @ddt.unpack def test_submit_task(self, task_function, expected_task_type, params=None): diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index d9167318f07f9cc3532a96315097d0791e95bc1a..88b50fd63ee07796d64ddbb6ca4e82d915941117 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -84,6 +84,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest LAST_WEEK = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7) ADVERTISED_START = "Spring 2016" ENABLED_SIGNALS = ['course_published'] + DATES = { + 'next_week': NEXT_WEEK, + 'last_week': LAST_WEEK, + 'default_start_date': DEFAULT_START_DATE, + } @patch.dict(settings.FEATURES, {"ENABLE_DISCUSSION_SERVICE": True}) def setUp(self, *args, **kwargs): @@ -175,12 +180,12 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest self.assertFalse(result['has_access']) @ddt.data( - (NEXT_WEEK, ADVERTISED_START, ADVERTISED_START, "string"), - (NEXT_WEEK, None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), - (NEXT_WEEK, '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), - (DEFAULT_START_DATE, ADVERTISED_START, ADVERTISED_START, "string"), - (DEFAULT_START_DATE, '', None, "empty"), - (DEFAULT_START_DATE, None, None, "empty"), + ('next_week', ADVERTISED_START, ADVERTISED_START, "string"), + ('next_week', None, defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), + ('next_week', '', defaultfilters.date(NEXT_WEEK, "DATE_FORMAT"), "timestamp"), + ('default_start_date', ADVERTISED_START, ADVERTISED_START, "string"), + ('default_start_date', '', None, "empty"), + ('default_start_date', None, None, "empty"), ) @ddt.unpack @patch.dict(settings.FEATURES, {'DISABLE_START_DATES': False, 'ENABLE_MKTG_SITE': True}) @@ -190,7 +195,7 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest case the course has not started """ self.login() - course = CourseFactory.create(start=start, advertised_start=advertised_start, mobile_available=True) + course = CourseFactory.create(start=self.DATES[start], advertised_start=advertised_start, mobile_available=True) self.enroll(course.id) response = self.api_response() diff --git a/lms/djangoapps/teams/tests/test_models.py b/lms/djangoapps/teams/tests/test_models.py index 7bcd29d1445fa781ad37e6afbbdc9442c4f19926..31499e23d1a0d57389a1d176cf6b5771e8ee2e11 100644 --- a/lms/djangoapps/teams/tests/test_models.py +++ b/lms/djangoapps/teams/tests/test_models.py @@ -118,17 +118,17 @@ class TeamMembershipTest(SharedModuleStoreTestCase): class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase): """Tests for handling of team-related signals.""" - SIGNALS_LIST = ( - thread_created, - thread_edited, - thread_deleted, - thread_voted, - comment_created, - comment_edited, - comment_deleted, - comment_voted, - comment_endorsed - ) + SIGNALS = { + 'thread_created': thread_created, + 'thread_edited': thread_edited, + 'thread_deleted': thread_deleted, + 'thread_voted': thread_voted, + 'comment_created': comment_created, + 'comment_edited': comment_edited, + 'comment_deleted': comment_deleted, + 'comment_voted': comment_voted, + 'comment_endorsed': comment_endorsed, + } DISCUSSION_TOPIC_ID = 'test_topic' @@ -180,30 +180,33 @@ class TeamSignalsTest(EventTestMixin, SharedModuleStoreTestCase): @ddt.data( *itertools.product( - SIGNALS_LIST, + SIGNALS.keys(), (('user', True), ('moderator', False)) ) ) @ddt.unpack - def test_signals(self, signal, (user, should_update)): + def test_signals(self, signal_name, (user, should_update)): """Test that `last_activity_at` is correctly updated when team-related signals are sent. """ with self.assert_last_activity_updated(should_update): user = getattr(self, user) + signal = self.SIGNALS[signal_name] signal.send(sender=None, user=user, post=self.mock_comment()) - @ddt.data(thread_voted, comment_voted) - def test_vote_others_post(self, signal): + @ddt.data('thread_voted', 'comment_voted') + def test_vote_others_post(self, signal_name): """Test that voting on another user's post correctly fires a signal.""" with self.assert_last_activity_updated(True): + signal = self.SIGNALS[signal_name] signal.send(sender=None, user=self.user, post=self.mock_comment(user=self.moderator)) - @ddt.data(*SIGNALS_LIST) - def test_signals_course_context(self, signal): + @ddt.data(*SIGNALS.keys()) + def test_signals_course_context(self, signal_name): """Test that `last_activity_at` is not updated when activity takes place in discussions outside of a team. """ with self.assert_last_activity_updated(False): + signal = self.SIGNALS[signal_name] signal.send(sender=None, user=self.user, post=self.mock_comment(context='course')) diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index c72c4de51878408265cd5f8c27bb15e90fb03224..3ff2b7b45873a9371ca274e92cb007bc7eb74e75 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -89,8 +89,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): PASSWORD = "test_password" NOW = datetime.now(pytz.UTC) - YESTERDAY = NOW - timedelta(days=1) - TOMORROW = NOW + timedelta(days=1) + YESTERDAY = 'yesterday' + TOMORROW = 'tomorrow' + NEXT_YEAR = 'next_year' + DATES = { + YESTERDAY: NOW - timedelta(days=1), + TOMORROW: NOW + timedelta(days=1), + NEXT_YEAR: NOW + timedelta(days=360), + None: None, + } URLCONF_MODULES = ['openedx.core.djangoapps.embargo'] @@ -492,7 +499,7 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ) @ddt.unpack def test_payment_confirmation_course_details(self, course_start, show_courseware_url): - course = self._create_course("verified", course_start=course_start) + course = self._create_course("verified", course_start=self.DATES[course_start]) self._enroll(course.id, "verified") response = self._get_page('verify_student_payment_confirmation', course.id) @@ -753,9 +760,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): self.assertContains(response, "verification deadline") self.assertContains(response, deadline) - @ddt.data(datetime.now(tz=pytz.UTC) + timedelta(days=360), None) + @ddt.data(NEXT_YEAR, None) def test_course_mode_expired_verification_deadline_in_future(self, verification_deadline): """Verify that student can not upgrade in expired course mode.""" + verification_deadline = self.DATES[verification_deadline] course_modes = ("verified", "credit") course = self._create_course(*course_modes) diff --git a/openedx/core/djangoapps/content/block_structure/tests/helpers.py b/openedx/core/djangoapps/content/block_structure/tests/helpers.py index 601815721398a758c827ee2885fcc10d6cd8347d..83e5b2d666f869c2d2c1b19382bf0df48cba1553 100644 --- a/openedx/core/djangoapps/content/block_structure/tests/helpers.py +++ b/openedx/core/djangoapps/content/block_structure/tests/helpers.py @@ -183,6 +183,9 @@ class MockTransformer(BlockStructureTransformer): def transform(self, usage_info, block_structure): pass + def __repr__(self): + return self.name() + class MockFilteringTransformer(FilteringTransformerMixin, BlockStructureTransformer): """ diff --git a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py index 5558d7e4aa9bec206720cebba0a66286ca7c7f35..b94458e04d61466713791723b754bad456263846 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py +++ b/openedx/core/djangoapps/content/course_overviews/tests/test_course_overviews.py @@ -46,10 +46,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase): """ TODAY = timezone.now() - LAST_MONTH = TODAY - datetime.timedelta(days=30) - LAST_WEEK = TODAY - datetime.timedelta(days=7) - NEXT_WEEK = TODAY + datetime.timedelta(days=7) - NEXT_MONTH = TODAY + datetime.timedelta(days=30) + LAST_MONTH = 'last_month' + LAST_WEEK = 'last_week' + NEXT_WEEK = 'next_week' + NEXT_MONTH = 'next_month' + DATES = { + 'default_start_date': DEFAULT_START_DATE, + LAST_MONTH: TODAY - datetime.timedelta(days=30), + LAST_WEEK: TODAY - datetime.timedelta(days=7), + NEXT_WEEK: TODAY + datetime.timedelta(days=7), + NEXT_MONTH: TODAY + datetime.timedelta(days=30), + None: None, + } COURSE_OVERVIEW_TABS = {'courseware', 'info', 'textbooks', 'discussion', 'wiki', 'progress'} @@ -229,7 +237,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase): }, { # # Don't set display name - "start": DEFAULT_START_DATE, # Default start and end dates + "start": 'default_start_date', # Default start and end dates "end": None, "advertised_start": None, # No advertised start "pre_requisite_courses": [], # No pre-requisites @@ -251,10 +259,15 @@ class CourseOverviewTestCase(ModuleStoreTestCase): modulestore_type (ModuleStoreEnum.Type): type of store to create the course in. """ + kwargs = course_kwargs.copy() + kwargs['start'] = self.DATES[course_kwargs['start']] + kwargs['end'] = self.DATES[course_kwargs['end']] + if 'announcement' in course_kwargs: + kwargs['announcement'] = self.DATES[course_kwargs['announcement']] # Note: We specify a value for 'run' here because, for some reason, # .create raises an InvalidKeyError if we don't (even though my # other test functions don't specify a run but work fine). - course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **course_kwargs) + course = CourseFactory.create(default_store=modulestore_type, run="TestRun", **kwargs) self.check_course_overview_against_course(course) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py index 50566faf3c5f72d45277ed6d85f93aaf4ee9c6ed..ead33b557493214455e50ffe9cba2632b22baf03 100644 --- a/openedx/core/djangoapps/credit/tests/test_signals.py +++ b/openedx/core/djangoapps/credit/tests/test_signals.py @@ -35,6 +35,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): VALID_DUE_DATE = datetime.now(pytz.UTC) + timedelta(days=20) EXPIRED_DUE_DATE = datetime.now(pytz.UTC) - timedelta(days=20) + DATES = { + 'valid': VALID_DUE_DATE, + 'expired': EXPIRED_DUE_DATE, + None: None, + } + def setUp(self): super(TestMinGradedRequirementStatus, self).setUp() self.course = CourseFactory.create( @@ -85,13 +91,13 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): self.assertEqual(req_status[0]['reason'], expected_reason) @ddt.data( - (0.6, VALID_DUE_DATE), + (0.6, 'valid'), (0.52, None), ) @ddt.unpack - def test_min_grade_requirement_with_valid_grade(self, grade, due_date): + def test_min_grade_requirement_with_valid_grade(self, grade, due_date_name): """Test with valid grades submitted before deadline""" - self.assert_requirement_status(grade, due_date, 'satisfied') + self.assert_requirement_status(grade, self.DATES[due_date_name], 'satisfied') def test_grade_changed(self): """ Verify successive calls to update a satisfied grade requirement are recorded. """ @@ -106,12 +112,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): @ddt.data( (0.50, None), (0.51, None), - (0.40, VALID_DUE_DATE), + (0.40, 'valid'), ) @ddt.unpack - def test_min_grade_requirement_failed_grade_valid_deadline(self, grade, due_date): + def test_min_grade_requirement_failed_grade_valid_deadline(self, grade, due_date_name): """Test with failed grades and deadline is still open or not defined.""" - self.assert_requirement_status(grade, due_date, None) + self.assert_requirement_status(grade, self.DATES[due_date_name], None) def test_min_grade_requirement_failed_grade_expired_deadline(self): """Test with failed grades and deadline expire""" diff --git a/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py b/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py index 09d73029fd7a75d3b4f2bcc39b2f8319963b3296..006fb3d359048c5b78d94f38337fa0510258eaee 100644 --- a/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py +++ b/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py @@ -29,6 +29,10 @@ COMMAND_MODULE = 'openedx.core.djangoapps.programs.management.commands.backpopul class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsApiConfigMixin, TestCase): """Tests for the backpopulate_program_credentials management command.""" course_run_key, alternate_course_run_key = (generate_course_run_key() for __ in range(2)) + # Constants for the _get_programs_data hierarchy types used in test_flatten() + SEPARATE_PROGRAMS = 'separate_programs' + SEPARATE_COURSES = 'separate_courses' + SAME_COURSE = 'same_course' def setUp(self): super(BackpopulateProgramCredentialsTests, self).setUp() @@ -44,6 +48,54 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp catalog_integration = self.create_catalog_integration() UserFactory(username=catalog_integration.service_username) + def _get_programs_data(self, hierarchy_type): + """ + Generate a mock response for get_programs() with the given type of + course hierarchy. Dramatically simplifies (and makes consistent + between test runs) the ddt-generated test_flatten methods. + """ + if hierarchy_type == self.SEPARATE_PROGRAMS: + return [ + ProgramFactory( + courses=[ + CourseFactory(course_runs=[ + CourseRunFactory(key=self.course_run_key), + ]), + ] + ), + ProgramFactory( + courses=[ + CourseFactory(course_runs=[ + CourseRunFactory(key=self.alternate_course_run_key), + ]), + ] + ), + ] + elif hierarchy_type == self.SEPARATE_COURSES: + return [ + ProgramFactory( + courses=[ + CourseFactory(course_runs=[ + CourseRunFactory(key=self.course_run_key), + ]), + CourseFactory(course_runs=[ + CourseRunFactory(key=self.alternate_course_run_key), + ]), + ] + ), + ] + else: # SAME_COURSE + return [ + ProgramFactory( + courses=[ + CourseFactory(course_runs=[ + CourseRunFactory(key=self.course_run_key), + CourseRunFactory(key=self.alternate_course_run_key), + ]), + ] + ), + ] + @ddt.data(True, False) def test_handle(self, commit, mock_task, mock_get_programs): """ @@ -112,49 +164,10 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp # The task should be called for both users since professional and no-id-professional are equivalent. mock_task.assert_has_calls([mock.call(self.alice.username), mock.call(self.bob.username)]) - @ddt.data( - [ - ProgramFactory( - courses=[ - CourseFactory(course_runs=[ - CourseRunFactory(key=course_run_key), - ]), - ] - ), - ProgramFactory( - courses=[ - CourseFactory(course_runs=[ - CourseRunFactory(key=alternate_course_run_key), - ]), - ] - ), - ], - [ - ProgramFactory( - courses=[ - CourseFactory(course_runs=[ - CourseRunFactory(key=course_run_key), - ]), - CourseFactory(course_runs=[ - CourseRunFactory(key=alternate_course_run_key), - ]), - ] - ), - ], - [ - ProgramFactory( - courses=[ - CourseFactory(course_runs=[ - CourseRunFactory(key=course_run_key), - CourseRunFactory(key=alternate_course_run_key), - ]), - ] - ), - ], - ) - def test_handle_flatten(self, data, mock_task, mock_get_programs): + @ddt.data(SEPARATE_PROGRAMS, SEPARATE_COURSES, SAME_COURSE) + def test_handle_flatten(self, hierarchy_type, mock_task, mock_get_programs): """Verify that program structures are flattened correctly.""" - mock_get_programs.return_value = data + mock_get_programs.return_value = self._get_programs_data(hierarchy_type) GeneratedCertificateFactory( user=self.alice, diff --git a/openedx/core/djangoapps/user_api/validation/tests/test_views.py b/openedx/core/djangoapps/user_api/validation/tests/test_views.py index 5ff294a0413f4c3c1f9f99415f354802c998dd00..4796cc8f60ab26f6d6dbc4b2ab65e48b841404f6 100644 --- a/openedx/core/djangoapps/user_api/validation/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/validation/tests/test_views.py @@ -53,11 +53,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase): ) @ddt.data( - ['name', (name for name in testutils.VALID_NAMES)], - ['email', (email for email in testutils.VALID_EMAILS)], - ['password', (password for password in testutils.VALID_PASSWORDS)], - ['username', (username for username in testutils.VALID_USERNAMES)], - ['country', (country for country in testutils.VALID_COUNTRIES)] + ['name', [name for name in testutils.VALID_NAMES]], + ['email', [email for email in testutils.VALID_EMAILS]], + ['password', [password for password in testutils.VALID_PASSWORDS]], + ['username', [username for username in testutils.VALID_USERNAMES]], + ['country', [country for country in testutils.VALID_COUNTRIES]] ) @ddt.unpack def test_positive_validation_decision(self, form_field_name, user_data): @@ -71,11 +71,11 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase): @ddt.data( # Skip None type for invalidity checks. - ['name', (name for name in testutils.INVALID_NAMES[1:])], - ['email', (email for email in testutils.INVALID_EMAILS[1:])], - ['password', (password for password in testutils.INVALID_PASSWORDS[1:])], - ['username', (username for username in testutils.INVALID_USERNAMES[1:])], - ['country', (country for country in testutils.INVALID_COUNTRIES[1:])] + ['name', [name for name in testutils.INVALID_NAMES[1:]]], + ['email', [email for email in testutils.INVALID_EMAILS[1:]]], + ['password', [password for password in testutils.INVALID_PASSWORDS[1:]]], + ['username', [username for username in testutils.INVALID_USERNAMES[1:]]], + ['country', [country for country in testutils.INVALID_COUNTRIES[1:]]] ) @ddt.unpack def test_negative_validation_decision(self, form_field_name, user_data): diff --git a/openedx/core/djangoapps/util/tests/test_user_messages.py b/openedx/core/djangoapps/util/tests/test_user_messages.py index 0f40f6b2412999893a7b6ff058e5869dbe158cd9..b94c0d7c6523f3196bb9d813d36d089d986c337e 100644 --- a/openedx/core/djangoapps/util/tests/test_user_messages.py +++ b/openedx/core/djangoapps/util/tests/test_user_messages.py @@ -6,6 +6,8 @@ import ddt from django.contrib.messages.middleware import MessageMiddleware from django.test import RequestFactory, TestCase + +from common.test.utils import normalize_repr from openedx.core.djangolib.markup import HTML, Text from student.tests.factories import UserFactory @@ -60,10 +62,10 @@ class UserMessagesTestCase(TestCase): self.assertEquals(messages[0].icon_class, expected_icon_class) @ddt.data( - (PageLevelMessages.register_error_message, UserMessageType.ERROR), - (PageLevelMessages.register_info_message, UserMessageType.INFO), - (PageLevelMessages.register_success_message, UserMessageType.SUCCESS), - (PageLevelMessages.register_warning_message, UserMessageType.WARNING), + (normalize_repr(PageLevelMessages.register_error_message), UserMessageType.ERROR), + (normalize_repr(PageLevelMessages.register_info_message), UserMessageType.INFO), + (normalize_repr(PageLevelMessages.register_success_message), UserMessageType.SUCCESS), + (normalize_repr(PageLevelMessages.register_warning_message), UserMessageType.WARNING), ) @ddt.unpack def test_message_type(self, register_message_function, expected_message_type): diff --git a/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py b/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py index b02e08904129f40045b5c53ff60519df39ae7652..4640109df23070a0ed806bec42ff4a6adc6be8e6 100644 --- a/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py +++ b/openedx/core/lib/xblock_builtin/xblock_discussion/tests.py @@ -16,7 +16,16 @@ from xblock.fields import ScopeIds, UNIQUE_ID, NO_CACHE_VALUE from xblock.runtime import Runtime +def attribute_pair_repr(self): + """ + Custom string representation for the AttributePair namedtuple which is + consistent between test runs. + """ + return '<AttributePair name={}>'.format(self.name) + + AttributePair = namedtuple("AttributePair", ["name", "value"]) +AttributePair.__repr__ = attribute_pair_repr ID_ATTR_NAMES = ("discussion_id", "id",) diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 2ba7122648940e5c8ff7c7a3fda5937fd451000b..70d03609d1be4c79b6db4d742d450e7ff7bc3e67 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -20,8 +20,6 @@ from xmodule.course_module import DEFAULT_START_DATE from .test_course_home import course_home_url TEST_PASSWORD = 'test' -FUTURE_DAY = datetime.datetime.now() + datetime.timedelta(days=30) -PAST_DAY = datetime.datetime.now() - datetime.timedelta(days=30) class TestCourseOutlinePage(SharedModuleStoreTestCase): @@ -343,14 +341,22 @@ class TestEmptyCourseOutlinePage(SharedModuleStoreTestCase): """ Test the new course outline view. """ + FUTURE_DAY = 'future_day' + PAST_DAY = 'past_day' + DATES = { + 'default_start_date': DEFAULT_START_DATE, + FUTURE_DAY: datetime.datetime.now() + datetime.timedelta(days=30), + PAST_DAY: datetime.datetime.now() - datetime.timedelta(days=30), + } + @ddt.data( (FUTURE_DAY, 'This course has not started yet, and will launch on'), (PAST_DAY, "We're still working on course content."), - (DEFAULT_START_DATE, 'This course has not started yet.'), + ('default_start_date', 'This course has not started yet.'), ) @ddt.unpack - def test_empty_course_rendering(self, start_date, expected_text): - course = CourseFactory.create(start=start_date) + def test_empty_course_rendering(self, start_date_name, expected_text): + course = CourseFactory.create(start=self.DATES[start_date_name]) test_user = UserFactory(password=TEST_PASSWORD) CourseEnrollment.enroll(test_user, course.id) self.client.login(username=test_user.username, password=TEST_PASSWORD)