Exemple #1
0
    def _execute_phase_once(
        self,
        phase_desc: phase_descriptor.PhaseDescriptor,
        is_last_repeat: bool,
        run_with_profiling: bool,
        subtest_rec: Optional[test_record.SubtestRecord],
    ) -> Tuple[PhaseExecutionOutcome, Optional[pstats.Stats]]:
        """Executes the given phase, returning a PhaseExecutionOutcome."""
        # Check this before we create a PhaseState and PhaseRecord.
        if phase_desc.options.run_if and not phase_desc.options.run_if():
            _LOG.debug('Phase %s skipped due to run_if returning falsey.',
                       phase_desc.name)
            return PhaseExecutionOutcome(
                phase_descriptor.PhaseResult.SKIP), None

        override_result = None
        with self.test_state.running_phase_context(phase_desc) as phase_state:
            if subtest_rec:
                _LOG.debug('Executing phase %s under subtest %s',
                           phase_desc.name, subtest_rec.name)
                phase_state.set_subtest_name(subtest_rec.name)
            else:
                _LOG.debug('Executing phase %s', phase_desc.name)
            with self._current_phase_thread_lock:
                # Checking _stopping must be in the lock context, otherwise there is a
                # race condition: this thread checks _stopping and then switches to
                # another thread where stop() sets _stopping and checks
                # _current_phase_thread (which would not be set yet).  In that case, the
                # new phase thread will be still be started.
                if self._stopping.is_set():
                    # PhaseRecord will be written at this point, so ensure that it has a
                    # Killed result.
                    result = PhaseExecutionOutcome(
                        threads.ThreadTerminationError())
                    phase_state.result = result
                    return result, None
                phase_thread = PhaseExecutorThread(phase_desc, self.test_state,
                                                   run_with_profiling,
                                                   subtest_rec)
                phase_thread.start()
                self._current_phase_thread = phase_thread

            phase_state.result = phase_thread.join_or_die()
            if phase_state.result.is_repeat and is_last_repeat:
                _LOG.error('Phase returned REPEAT, exceeding repeat_limit.')
                phase_state.hit_repeat_limit = True
                override_result = PhaseExecutionOutcome(
                    phase_descriptor.PhaseResult.STOP)
            self._current_phase_thread = None

        # Refresh the result in case a validation for a partially set measurement
        # or phase diagnoser raised an exception.
        result = override_result or phase_state.result
        _LOG.debug('Phase %s finished with result %s', phase_desc.name,
                   result.phase_result)
        return (result, phase_thread.get_profile_stats()
                if run_with_profiling else None)
Exemple #2
0
    def JoinOrDie(self):
        """Wait for thread to finish, return a PhaseOutcome with its response."""
        if self._phase.options.timeout_s is not None:
            self.join(self._phase.options.timeout_s)
        else:
            self.join(DEFAULT_PHASE_TIMEOUT_S)

        # We got a return value or an exception and handled it.
        if isinstance(self._phase_outcome, PhaseOutcome):
            return self._phase_outcome

        # Check for timeout, indicated by None for PhaseOutcome.phase_result.
        if self.is_alive():
            self.Kill()
            return PhaseOutcome(None)

        # Phase was killed.
        return PhaseOutcome(threads.ThreadTerminationError())
Exemple #3
0
    def join_or_die(self):
        """Wait for thread to finish, returning a PhaseExecutionOutcome instance."""
        if self._phase_desc.options.timeout_s is not None:
            self.join(self._phase_desc.options.timeout_s)
        else:
            self.join(DEFAULT_PHASE_TIMEOUT_S)

        # We got a return value or an exception and handled it.
        if isinstance(self._phase_execution_outcome, PhaseExecutionOutcome):
            return self._phase_execution_outcome

        # Check for timeout, indicated by None for
        # PhaseExecutionOutcome.phase_result.
        if self.is_alive():
            self.kill()
            return PhaseExecutionOutcome(None)

        # Phase was killed.
        return PhaseExecutionOutcome(threads.ThreadTerminationError())
    def _execute_phase_once(self, phase_desc, is_last_repeat):
        """Executes the given phase, returning a PhaseExecutionOutcome."""
        # Check this before we create a PhaseState and PhaseRecord.
        if phase_desc.options.run_if and not phase_desc.options.run_if():
            _LOG.debug('Phase %s skipped due to run_if returning falsey.',
                       phase_desc.name)
            return PhaseExecutionOutcome(openhtf.PhaseResult.SKIP)

        with self.test_state.running_phase_context(phase_desc) as phase_state:
            _LOG.debug('Executing phase %s', phase_desc.name)
            with self._current_phase_thread_lock:
                # Checking _stopping must be in the lock context, otherwise there is a
                # race condition: this thread checks _stopping and then switches to
                # another thread where stop() sets _stopping and checks
                # _current_phase_thread (which would not be set yet).  In that case, the
                # new phase thread will be still be started.
                if self._stopping.is_set():
                    # PhaseRecord will be written at this point, so ensure that it has a
                    # Killed result.
                    result = PhaseExecutionOutcome(
                        threads.ThreadTerminationError())
                    phase_state.result = result
                    return result
                phase_thread = PhaseExecutorThread(phase_desc, self.test_state)
                phase_thread.start()
                self._current_phase_thread = phase_thread

            result = phase_state.result = phase_thread.join_or_die()
            if phase_state.result.is_repeat and is_last_repeat:
                _LOG.error('Phase returned REPEAT, exceeding repeat_limit.')
                phase_state.hit_repeat_limit = True
                result = PhaseExecutionOutcome(openhtf.PhaseResult.STOP)
            self._current_phase_thread = None

        _LOG.debug('Phase %s finished with result %s', phase_desc.name,
                   result.phase_result)
        return result
Exemple #5
0
    def JoinOrDie(self):
        """Wait for thread to finish, return a TestPhaseResult with its response."""
        if self._phase.options.timeout_s is not None:
            self.join(self._phase.options.timeout_s)
        else:
            self.join(FLAGS.phase_default_timeout_ms / 1000.0)

        if isinstance(self._phase_result, TestPhaseResult):
            return self._phase_result

        if self.is_alive():
            # Timeout
            self.Kill()
            return self._MakePhaseResult(phase_data.PhaseResults.TIMEOUT)

        if self._phase_result is None:
            # Finished with no return value, assume continue.
            return self._MakePhaseResult(phase_data.PhaseResults.CONTINUE)

        if self._phase_result is DIDNT_FINISH:
            # Phase was killed
            return self._MakePhaseResult(threads.ThreadTerminationError())

        return self._MakePhaseResult(self._phase_result)
Exemple #6
0
    def join_or_die(self) -> PhaseExecutionOutcome:
        """Wait for thread to finish, returning a PhaseExecutionOutcome instance."""
        deadline = time.monotonic() + DEFAULT_PHASE_TIMEOUT_S
        if self._phase_desc.options.timeout_s is not None:
            deadline = time.monotonic() + self._phase_desc.options.timeout_s
        while time.monotonic() < deadline:
            # Using exception to kill thread is not honored when thread is busy,
            # so we leave the thread behind, and move on teardown.
            self.join(_JOIN_TRY_INTERVAL_SECONDS)
            if not self.is_alive() or self._killed.is_set():
                break

        # We got a return value or an exception and handled it.
        if self._phase_execution_outcome:
            return self._phase_execution_outcome

        # Check for timeout, indicated by None for
        # PhaseExecutionOutcome.phase_result.
        if self.is_alive():
            self.kill()
            return PhaseExecutionOutcome(None)

        # Phase was killed.
        return PhaseExecutionOutcome(threads.ThreadTerminationError())
Exemple #7
0
class TextTest(test.TestCase, parameterized.TestCase):
    def testColorFromTestOutcome_HasCorrespondingTestOutcomeName(self):
        """Catches OpenHTF test outcome not added in _ColorFromTestOutcome."""
        for member in test_record.Outcome.__members__:
            self.assertIn(member, text._ColorFromTestOutcome.__members__)

    def testHeadlineFromTestOutcome_HasCorrespondingTestOutcomeName(self):
        """Catches OpenHTF test outcome not added in _HeadlineFromTestOutcome."""
        for member in test_record.Outcome.__members__:
            self.assertIn(member, text._HeadlineFromTestOutcome.__members__)

    def testColorText_GetsColorSuccessfully(self):
        text_to_colorize = 'Foo Bar'
        self.assertEqual(
            text._ColorText(text_to_colorize, _GREEN),
            f'{_GREEN}{text_to_colorize}{colorama.Style.RESET_ALL}')

    # TODO(b/70517332): Pytype currently doesn't properly support the functional
    # API of enums: https://github.com/google/pytype/issues/459. Remove
    # disabling pytype once fixed.
    @parameterized.named_parameters(
        (headline_member.name, headline_member.name, headline_member.value)
        for headline_member in text._HeadlineFromTestOutcome.__iter__())  # pytype: disable=attribute-error
    def testGetTestOutcomeHeadline_TestNotColorized(self, outcome, headline):
        record = test_record.TestRecord(dut_id='TestDutId',
                                        station_id='test_station',
                                        outcome=test_record.Outcome[outcome])
        self.assertEqual(text._GetTestOutcomeHeadline(record), headline)

    # TODO(b/70517332): Pytype currently doesn't properly support the functional
    # API of enums: https://github.com/google/pytype/issues/459. Remove
    # disabling pytype once fixed.
    @parameterized.named_parameters(
        (headline_member.name, headline_member.name, headline_member.value)
        for headline_member in text._HeadlineFromTestOutcome.__iter__())  # pytype: disable=attribute-error
    def testGetTestOutcomeHeadline_TestColorized(self, outcome, headline):
        record = test_record.TestRecord(dut_id='TestDutId',
                                        station_id='test_station',
                                        outcome=test_record.Outcome[outcome])
        # TODO(b/70517332): Pytype currently doesn't properly support the functional
        # API of enums: https://github.com/google/pytype/issues/459. Remove
        # disabling pytype once fixed.
        self.assertEqual(
            text._GetTestOutcomeHeadline(record, colorize_text=True),
            f'{text._ColorFromTestOutcome[outcome].value}{headline}'  # pytype: disable=unsupported-operands
            f'{colorama.Style.RESET_ALL}')

    def testStringFromMeasurement_SuccessfullyConvertsUnsetMeasurement(self):
        self.assertEqual(
            text.StringFromMeasurement(
                openhtf.Measurement('text_measurement_a')),
            '| text_measurement_a was not set')

    def testStringFromMeasurement_SuccessfullyConvertsPassMeasurement(self):
        measurement = openhtf.Measurement('text_measurement_a')
        measurement._measured_value = measurements.MeasuredValue(
            'text_measurement_a')
        measurement._measured_value.set(10)
        measurement.notify_value_set()
        self.assertEqual(text.StringFromMeasurement(measurement),
                         '| text_measurement_a: 10')

    def testStringFromMeasurement_SuccessfullyConvertsFailMeasurement(self):
        measurement = openhtf.Measurement('text_measurement_a').in_range(
            maximum=3)
        measurement._measured_value = measurements.MeasuredValue(
            'text_measurement_a')
        measurement._measured_value.set(5)
        measurement.notify_value_set()
        output = text.StringFromMeasurement(measurement)
        self.assertEqual(
            output,
            "| text_measurement_a failed because 5 failed these checks: ['x <= 3']"
        )
        self.assertNotIn(text._BRIGHT_RED_STYLE, output)

    def testStringFromMeasurement_SuccessfullyConvertsFailMeasurementColorized(
            self):
        measurement = openhtf.Measurement('text_measurement_a').in_range(
            maximum=3)
        measurement._measured_value = measurements.MeasuredValue(
            'text_measurement_a')
        measurement._measured_value.set(5)
        measurement.notify_value_set()
        self.assertEqual(
            text.StringFromMeasurement(measurement, colorize_text=True).count(
                text._BRIGHT_RED_STYLE), 1)

    def testStringFromAttachment_SuccessfullyConvertsPassMeasurement(self):
        attachment = test_record.Attachment('content', 'text/plain')
        self.assertEqual(
            text.StringFromAttachment(attachment, 'attachment_a.txt'),
            '| attachment: attachment_a.txt (mimetype=text/plain)')

    @parameterized.named_parameters([
        {
            'testcase_name': 'None',
            'phase_result': None,
            'expected_str': ''
        },
        {
            'testcase_name': 'PhaseResult',
            'phase_result': phase_descriptor.PhaseResult.CONTINUE,
            'expected_str': 'CONTINUE'
        },
        {
            'testcase_name':
            'ExceptionInfo',
            'phase_result':
            phase_executor.ExceptionInfo(
                ValueError, ValueError('Invalid Value'),
                mock.create_autospec(types.TracebackType, spec_set=True)),
            'expected_str':
            'ValueError'
        },
        {
            'testcase_name': 'ThreadTerminationError',
            'phase_result': threads.ThreadTerminationError(),
            'expected_str': 'ThreadTerminationError'
        },
    ])
    def testStringFromPhaseExecutionOutcome_SuccessfullyConvertsOutcome(
            self, phase_result, expected_str):
        self.assertEqual(
            text.StringFromPhaseExecutionOutcome(
                phase_executor.PhaseExecutionOutcome(phase_result)),
            expected_str)

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseRecordPassPhase(
            self):
        record = self.execute_phase_or_test(PhaseThatSucceeds)
        output = text.StringFromPhaseRecord(record)
        self.assertEqual(
            output, 'Phase PhaseThatSucceeds\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            '| text_measurement_a: a\n'
            '| text_measurement_b: b\n'
            '| attachment: attachment_a.txt (mimetype=text/plain)\n'
            '| attachment: attachment_b.json (mimetype=application/json)')
        self.assertNotIn(text._BRIGHT_RED_STYLE, output)

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseRecordFailPhase(
            self):
        record = self.execute_phase_or_test(PhaseWithFailure)
        output = text.StringFromPhaseRecord(record)
        self.assertEqual(
            output, 'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]\n'
            '| text_measurement_b was not set\n'
            '| text_measurement_c: c')

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseFailLimitPhase(
            self):
        record = self.execute_phase_or_test(PhaseWithFailure)
        output = text.StringFromPhaseRecord(record, maximum_num_measurements=2)
        self.assertEqual(
            output, 'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]\n'
            '| text_measurement_b was not set\n'
            '...')

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseRecordOnlyFailPhase(
            self):
        record = self.execute_phase_or_test(PhaseWithFailure)
        output = text.StringFromPhaseRecord(record, only_failures=True)
        self.assertEqual(
            output, 'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]')

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseRecordFailPhaseColored(
            self):
        record = self.execute_phase_or_test(PhaseWithFailure)
        self.assertEqual(
            text.StringFromPhaseRecord(record, colorize_text=True).count(_RED),
            3)

    def testStringFromPhaseRecord_SuccessfullyConvertsPhaseRecordSkipPhaseColored(
            self):
        record = self.execute_phase_or_test(PhaseWithSkip)
        self.assertNotIn(
            text._BRIGHT_RED_STYLE,
            text.StringFromPhaseRecord(record, colorize_text=True))

    @parameterized.named_parameters([
        {
            'testcase_name':
            'OneOutcome',
            'outcome_details': [
                test_record.OutcomeDetails(code=1,
                                           description='Unknown exception.')
            ],
            'expected_str': ('The test thinks this may be the reason:\n'
                             '1: Unknown exception.'),
        },
        {
            'testcase_name':
            'TwoOutcomes',
            'outcome_details': [
                test_record.OutcomeDetails(code=1,
                                           description='Unknown exception.'),
                test_record.OutcomeDetails(code='FooError',
                                           description='Foo exception.')
            ],
            'expected_str': ('The test thinks these may be the reason:\n'
                             '1: Unknown exception.\n'
                             'FooError: Foo exception.'),
        },
    ])
    def testStringFromOutcomeDetails_SuccessfullyConvertsOutcomeDetails(
            self, outcome_details, expected_str):
        self.assertEqual(text.StringFromOutcomeDetails(outcome_details),
                         expected_str)

    def testStringFromTestRecord_SuccessfullyConvertsTestRecordSinglePassPhase(
            self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseThatSucceeds))
        self.assertEqual(
            text.StringFromTestRecord(record), 'Test finished with a PASS!\n'
            'Woohoo!\n'
            'Phase trigger_phase\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            'Phase PhaseThatSucceeds\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            '| text_measurement_a: a\n'
            '| text_measurement_b: b\n'
            '| attachment: attachment_a.txt (mimetype=text/plain)\n'
            '| attachment: attachment_b.json (mimetype=application/json)\n'
            'Test finished with a PASS!')

    def testStringFromTestRecord_SuccessfullyConvertsTestErrorPhase(self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseWithError))
        self.assertEqual(
            text.StringFromTestRecord(record), 'Test encountered an ERROR!!!\n'
            'Phase trigger_phase\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            'Phase PhaseWithError\n'
            '+ Outcome: ERROR Result: Exception\n'
            '| text_measurement_a was not set\n'
            '| text_measurement_b was not set\n'
            'The test thinks this may be the reason:\n'
            'Exception: Intentional exception from test case.\n'
            'Test encountered an ERROR!!!')

    def testStringFromTestRecord_SuccessfullyConvertsTestFailurePhase(self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseWithFailure))
        output = text.StringFromTestRecord(record)
        self.assertEqual(
            output, 'Test finished with a FAIL :(\n'
            'Phase trigger_phase\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]\n'
            '| text_measurement_b was not set\n'
            '| text_measurement_c: c\n'
            'Test finished with a FAIL :(')
        self.assertNotIn(text._BRIGHT_RED_STYLE, output)

    def testStringFromTestRecord_SuccessfullyConvertsTestOnlyFailurePhase(
            self):
        record = self.execute_phase_or_test(
            openhtf.Test(PhaseThatSucceeds, PhaseWithFailure))
        output = text.StringFromTestRecord(record, only_failures=True)
        self.assertEqual(
            output, 'Test finished with a FAIL :(\n'
            'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]\n'
            'Test finished with a FAIL :(')
        self.assertNotIn(text._BRIGHT_RED_STYLE, output)

    def testStringFromTestRecord_SuccessfullyConvertsTestFailurePhaseColored(
            self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseWithFailure))
        self.assertEqual(
            text.StringFromTestRecord(record, colorize_text=True).count(_RED),
            5)

    def testStringFromTestRecord_SuccessfullyConvertsTestFailureMultiplePhases(
            self):
        record = self.execute_phase_or_test(
            openhtf.Test(PhaseThatSucceeds, PhaseWithFailure))
        self.assertEqual(
            text.StringFromTestRecord(record), 'Test finished with a FAIL :(\n'
            'Phase trigger_phase\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            'Phase PhaseThatSucceeds\n'
            '+ Outcome: PASS Result: CONTINUE\n'
            '| text_measurement_a: a\n'
            '| text_measurement_b: b\n'
            '| attachment: attachment_a.txt (mimetype=text/plain)\n'
            '| attachment: attachment_b.json (mimetype=application/json)\n'
            'Phase PhaseWithFailure\n'
            '+ Outcome: FAIL Result: CONTINUE\n'
            '| text_measurement_a failed because intentionally wrong measurement '
            'failed these checks: ["\'x\' matches /^a$/"]\n'
            '| text_measurement_b was not set\n'
            '| text_measurement_c: c\n'
            'Test finished with a FAIL :(')

    def testPrintTestRecord_SuccessfullyLogsNotColored(self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseThatSucceeds))
        with mock.patch.object(sys, 'stdout', new_callable=io.StringIO) as cm:
            with mock.patch.object(sys.stdout,
                                   sys.stdout.isatty.__name__,
                                   autospec=True,
                                   spec_set=True,
                                   return_value=False):
                text.PrintTestRecord(record)
        self.assertTrue(cm.getvalue())
        self.assertNotIn(_GREEN, cm.getvalue())

    def testPrintTestRecord_SuccessfullyLogsColored(self):
        record = self.execute_phase_or_test(openhtf.Test(PhaseThatSucceeds))
        with mock.patch.object(sys, 'stdout', new_callable=io.StringIO) as cm:
            with mock.patch.object(sys.stdout,
                                   sys.stdout.isatty.__name__,
                                   autospec=True,
                                   spec_set=True,
                                   return_value=True):
                text.PrintTestRecord(record)
        self.assertIn(_GREEN, cm.getvalue())
Exemple #8
0
class TestTestApi(parameterized.TestCase):
    def setUp(self):
        super(TestTestApi, self).setUp()
        patcher = mock.patch.object(test_record.PhaseRecord,
                                    'record_start_time',
                                    return_value=11235)
        self.mock_record_start_time = patcher.start()
        self.addCleanup(patcher.stop)
        self.test_descriptor = test_descriptor.TestDescriptor(
            phase_collections.PhaseSequence((test_phase, )),
            test_record.CodeInfo.uncaptured(), {'config': {}})
        self.test_state = test_state.TestState(self.test_descriptor,
                                               'testing-123',
                                               test_descriptor.TestOptions())
        self.test_record = self.test_state.test_record
        self.running_phase_state = test_state.PhaseState.from_descriptor(
            test_phase, self.test_state, logging.getLogger())
        self.test_state.running_phase_state = self.running_phase_state
        self.test_api = self.test_state.test_api

    def test_get_attachment(self):
        attachment_name = 'attachment.txt'
        input_contents = b'This is some attachment text!'
        mimetype = 'text/plain'
        self.test_api.attach(attachment_name, input_contents, mimetype)

        output_attachment = self.test_api.get_attachment(attachment_name)
        if not output_attachment:
            # Need branch to appease pytype.
            self.fail('output_attachment not found')

        self.assertEqual(input_contents, output_attachment.data)
        self.assertEqual(mimetype, output_attachment.mimetype)

    def test_get_attachment_strict(self):
        attachment_name = 'attachment.txt'
        with self.assertRaises(test_descriptor.AttachmentNotFoundError):
            self.test_api.get_attachment_strict(attachment_name)

    def test_get_measurement(self):
        measurement_val = [1, 2, 3]
        self.test_api.measurements['test_measurement'] = measurement_val
        measurement = self.test_api.get_measurement('test_measurement')
        if not measurement:
            # Need branch to appease pytype.
            self.fail('measurement not found.')

        self.assertEqual(measurement_val, measurement.value)
        self.assertEqual('test_measurement', measurement.name)

    def test_get_measurement_immutable(self):
        measurement_val = [1, 2, 3]
        self.test_api.measurements['test_measurement'] = measurement_val
        measurement = self.test_api.get_measurement('test_measurement')
        if not measurement:
            # Need branch to appease pytype.
            self.fail('measurement not found.')

        self.assertEqual(measurement_val, measurement.value)
        self.assertEqual('test_measurement', measurement.name)

        measurement.value.append(4)
        self.assertNotEqual(measurement_val, measurement.value)

    def test_infer_mime_type_from_file_name(self):
        with tempfile.NamedTemporaryFile(suffix='.txt') as f:
            f.write(b'Mock text contents.')
            f.flush()
            file_name = f.name
            self.test_api.attach_from_file(file_name, 'attachment')
        attachment = self.test_api.get_attachment('attachment')
        if not attachment:
            # Need branch to appease pytype.
            self.fail('attachment not found.')
        self.assertEqual(attachment.mimetype, 'text/plain')

    def test_infer_mime_type_from_attachment_name(self):
        with tempfile.NamedTemporaryFile() as f:
            f.write(b'Mock text contents.')
            f.flush()
            file_name = f.name
            self.test_api.attach_from_file(file_name, 'attachment.png')
        attachment = self.test_api.get_attachment('attachment.png')
        if not attachment:
            # Need branch to appease pytype.
            self.fail('attachment not found.')
        self.assertEqual(attachment.mimetype, 'image/png')

    def test_phase_state_cache(self):
        basetypes = self.running_phase_state.as_base_types()
        expected_initial_basetypes = copy.deepcopy(
            PHASE_STATE_BASE_TYPE_INITIAL)
        expected_initial_basetypes['descriptor_id'] = basetypes[
            'descriptor_id']
        self.assertEqual(expected_initial_basetypes, basetypes)
        self.assertFalse(self.running_phase_state._update_measurements)
        self.test_api.measurements.test_measurement = 5
        self.assertEqual({'test_measurement'},
                         self.running_phase_state._update_measurements)
        self.running_phase_state.as_base_types()
        expected_after_basetypes = copy.deepcopy(expected_initial_basetypes)
        expected_after_basetypes['measurements']['test_measurement'].update({
            'outcome':
            'PASS',
            'measured_value':
            5,
        })
        self.assertEqual(expected_after_basetypes, basetypes)
        self.assertFalse(self.running_phase_state._update_measurements)

    def test_test_state_cache(self):
        basetypes = self.test_state.as_base_types()
        # The descriptor id is not static, so grab it.
        expected_initial_basetypes = copy.deepcopy(
            TEST_STATE_BASE_TYPE_INITIAL)
        descriptor_id = basetypes['running_phase_state']['descriptor_id']
        expected_initial_basetypes['running_phase_state']['descriptor_id'] = (
            descriptor_id)
        self.assertEqual(expected_initial_basetypes, basetypes)
        self.running_phase_state._finalize_measurements()
        self.test_record.add_phase_record(
            self.running_phase_state.phase_record)
        self.test_state.running_phase_state = None
        basetypes2 = self.test_state.as_base_types()
        expected_after_phase_record_basetypes = copy.deepcopy(
            PHASE_RECORD_BASE_TYPE)
        expected_after_phase_record_basetypes['descriptor_id'] = descriptor_id
        self.assertEqual(expected_after_phase_record_basetypes,
                         basetypes2['test_record']['phases'][0])
        self.assertIsNone(basetypes2['running_phase_state'])

    @parameterized.parameters(
        (phase_executor.PhaseExecutionOutcome(None),
         test_record.Outcome.TIMEOUT), (phase_executor.PhaseExecutionOutcome(
             phase_descriptor.PhaseResult.STOP), test_record.Outcome.FAIL),
        (phase_executor.PhaseExecutionOutcome(
            threads.ThreadTerminationError()), test_record.Outcome.ERROR))
    def test_test_state_finalize_from_phase_outcome(
            self, phase_exe_outcome: phase_executor.PhaseExecutionOutcome,
            test_record_outcome: test_record.Outcome):
        self.test_state.finalize_from_phase_outcome(phase_exe_outcome)
        self.assertEqual(self.test_state.test_record.outcome,
                         test_record_outcome)

    def test_test_state_finalize_from_phase_outcome_exception_info(self):
        try:
            raise ValueError('Exception for unit testing.')
        except ValueError:
            phase_exe_outcome = phase_executor.PhaseExecutionOutcome(
                phase_executor.ExceptionInfo(*sys.exc_info()))
            self.test_state.finalize_from_phase_outcome(phase_exe_outcome)
        self.assertEqual(self.test_state.test_record.outcome,
                         test_record.Outcome.ERROR)