def test_enrollment_map_reduce_job(self):
        self.maxDiff = None
        MOCK_NOW = 1427247511
        COURSE = 'xyzzy'
        NAMESPACE = 'ns_xyzzy'
        MIN_TIMESTAMP = (MOCK_NOW - (MOCK_NOW % enrollment.SECONDS_PER_HOUR) -
                         enrollment.StudentEnrollmentEventCounter.MAX_AGE)

        # Insert some bogus StudentEnrollmentEventEntity for the M/R job
        # to count or delete.
        very_old_enroll = enrollment.StudentEnrollmentEventDTO(None, {})
        very_old_enroll.timestamp = 0
        very_old_enroll.metric = messaging.Message.METRIC_ENROLLED

        very_old_unenroll = enrollment.StudentEnrollmentEventDTO(None, {})
        very_old_unenroll.timestamp = 0
        very_old_unenroll.metric = messaging.Message.METRIC_UNENROLLED

        just_too_old_enroll = enrollment.StudentEnrollmentEventDTO(None, {})
        just_too_old_enroll.timestamp = MIN_TIMESTAMP - 1
        just_too_old_enroll.metric = messaging.Message.METRIC_ENROLLED

        just_too_old_unenroll = enrollment.StudentEnrollmentEventDTO(None, {})
        just_too_old_unenroll.timestamp = MIN_TIMESTAMP - 1
        just_too_old_unenroll.metric = messaging.Message.METRIC_UNENROLLED

        young_enough_enroll = enrollment.StudentEnrollmentEventDTO(None, {})
        young_enough_enroll.timestamp = MIN_TIMESTAMP
        young_enough_enroll.metric = messaging.Message.METRIC_ENROLLED

        young_enough_unenroll = enrollment.StudentEnrollmentEventDTO(None, {})
        young_enough_unenroll.timestamp = MIN_TIMESTAMP
        young_enough_unenroll.metric = messaging.Message.METRIC_UNENROLLED

        now_enroll = enrollment.StudentEnrollmentEventDTO(None, {})
        now_enroll.timestamp = MOCK_NOW
        now_enroll.metric = messaging.Message.METRIC_ENROLLED

        now_unenroll = enrollment.StudentEnrollmentEventDTO(None, {})
        now_unenroll.timestamp = MOCK_NOW
        now_unenroll.metric = messaging.Message.METRIC_UNENROLLED

        dtos = [
            very_old_enroll,
            very_old_unenroll,
            just_too_old_enroll,
            just_too_old_unenroll,
            young_enough_enroll,
            young_enough_unenroll,
            now_enroll,
            now_unenroll,
        ]

        app_context = actions.simple_add_course(COURSE, ADMIN_EMAIL, 'Test')
        with common_utils.Namespace(NAMESPACE):
            enrollment.StudentEnrollmentEventDAO.save_all(dtos)

        # Run map/reduce job with a setup function replaced so that it will
        # always choose the same timestamp as the start time.
        job_class = enrollment.StudentEnrollmentEventCounter
        save_b_a_m_p = job_class.build_additional_mapper_params
        try:

            def fixed_time_b_a_m_p(self, app_context):
                return {self.MIN_TIMESTAMP: MIN_TIMESTAMP}

            job_class.build_additional_mapper_params = fixed_time_b_a_m_p

            # Actually run the job.
            enrollment.StudentEnrollmentEventCounter(app_context).submit()
            self.execute_all_deferred_tasks()
        finally:
            job_class.build_additional_mapper_params = save_b_a_m_p

        # Verify that the DTOs older than the cutoff have been removed from
        # the datastore.
        with common_utils.Namespace(NAMESPACE):
            dtos = enrollment.StudentEnrollmentEventDAO.get_all()
            dtos.sort(key=lambda dto: (dto.timestamp, dto.metric))
            self.assertEqual([
                young_enough_enroll.dict, young_enough_unenroll.dict,
                now_enroll.dict, now_unenroll.dict
            ], [d.dict for d in dtos])

        # Verify that we have messages for the new-enough items, and no
        # messages for the older items.
        messages = MockSender.get_sent()
        messages.sort(key=lambda m: (m['timestamp'], m['metric']))

        MOCK_NOW_HOUR = MOCK_NOW - (MOCK_NOW % enrollment.SECONDS_PER_HOUR)
        expected = [{
            messaging.Message._INSTALLATION:
            FAKE_INSTALLATION_ID,
            messaging.Message._COURSE:
            FAKE_COURSE_ID,
            messaging.Message._TIMESTAMP:
            MIN_TIMESTAMP,
            messaging.Message._VERSION:
            os.environ['GCB_PRODUCT_VERSION'],
            messaging.Message._METRIC:
            messaging.Message.METRIC_ENROLLED,
            messaging.Message._VALUE:
            1,
        }, {
            messaging.Message._INSTALLATION:
            FAKE_INSTALLATION_ID,
            messaging.Message._COURSE:
            FAKE_COURSE_ID,
            messaging.Message._TIMESTAMP:
            MIN_TIMESTAMP,
            messaging.Message._VERSION:
            os.environ['GCB_PRODUCT_VERSION'],
            messaging.Message._METRIC:
            messaging.Message.METRIC_UNENROLLED,
            messaging.Message._VALUE:
            1,
        }, {
            messaging.Message._INSTALLATION:
            FAKE_INSTALLATION_ID,
            messaging.Message._COURSE:
            FAKE_COURSE_ID,
            messaging.Message._TIMESTAMP:
            MOCK_NOW_HOUR,
            messaging.Message._VERSION:
            os.environ['GCB_PRODUCT_VERSION'],
            messaging.Message._METRIC:
            messaging.Message.METRIC_ENROLLED,
            messaging.Message._VALUE:
            1,
        }, {
            messaging.Message._INSTALLATION:
            FAKE_INSTALLATION_ID,
            messaging.Message._COURSE:
            FAKE_COURSE_ID,
            messaging.Message._TIMESTAMP:
            MOCK_NOW_HOUR,
            messaging.Message._VERSION:
            os.environ['GCB_PRODUCT_VERSION'],
            messaging.Message._METRIC:
            messaging.Message.METRIC_UNENROLLED,
            messaging.Message._VALUE:
            1,
        }]
        self.assertEquals(expected, messages)
        sites.reset_courses()
 def test_unexpected_field_raises(self):
     with self.assertRaises(ValueError):
         enrollment.StudentEnrollmentEventDTO(None, {'bad_field': 'x'})