diff options
author | perezju <perezju@chromium.org> | 2015-02-11 08:44:23 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-02-11 16:45:03 +0000 |
commit | 734e006023ee23e636ea757bbe94f13f135941d3 (patch) | |
tree | 5311318f5f6c97a650890f6b3b0ae259344e06bc /build/android | |
parent | 2a7267cf6a9685383efa0de531a451116ba00511 (diff) | |
download | chromium_src-734e006023ee23e636ea757bbe94f13f135941d3.zip chromium_src-734e006023ee23e636ea757bbe94f13f135941d3.tar.gz chromium_src-734e006023ee23e636ea757bbe94f13f135941d3.tar.bz2 |
[Android] Make the instrumentation parser iterable
Provides an incremental parser for the output of Android instrumentation
tests. For long running tests, this will allow to parse and iterate over
instrumentation statuses as they are being emitted and without having
to wait for the test to finish.
Changes:
- The new parser lives now on its own module: instrumentation_parser
- ParseAmInstrumentRawOutput is now a thin wrapper around the new
incremental parser, still returning a triple: (code, result, statuses)
- The "result" is now parsed and returned as a dictionary bundle (not a
list of lines).
- For each (code, bundle) in statuses, the values of the bundle are now
each a single string (not a list of lines). When a value spans over
several lines, lines are joined together with "\n" as a separator.
- GenerateTestResult and GenerateMultiTestResult, the only clients
affected by these changes, are updated accordingly.
BUG=
Review URL: https://codereview.chromium.org/907833003
Cr-Commit-Position: refs/heads/master@{#315778}
Diffstat (limited to 'build/android')
4 files changed, 269 insertions, 194 deletions
diff --git a/build/android/pylib/instrumentation/instrumentation_parser.py b/build/android/pylib/instrumentation/instrumentation_parser.py new file mode 100644 index 0000000..1859f14 --- /dev/null +++ b/build/android/pylib/instrumentation/instrumentation_parser.py @@ -0,0 +1,96 @@ +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import logging +import re + +# http://developer.android.com/reference/android/test/InstrumentationTestRunner.html +STATUS_CODE_START = 1 +STATUS_CODE_OK = 0 +STATUS_CODE_ERROR = -1 +STATUS_CODE_FAILURE = -2 + +# http://developer.android.com/reference/android/app/Activity.html +RESULT_CODE_OK = -1 +RESULT_CODE_CANCELED = 0 + +_INSTR_LINE_RE = re.compile('^\s*INSTRUMENTATION_([A-Z_]+): (.*)$') + + +class InstrumentationParser(object): + + def __init__(self, stream): + """An incremental parser for the output of Android instrumentation tests. + + Example: + + stream = adb.IterShell('am instrument -r ...') + parser = InstrumentationParser(stream) + + for code, bundle in parser.IterStatus(): + # do something with each instrumentation status + print 'status:', code, bundle + + # do something with the final instrumentation result + code, bundle = parser.GetResult() + print 'result:', code, bundle + + Args: + stream: a sequence of lines as produced by the raw output of an + instrumentation test (e.g. by |am instrument -r| or |uiautomator|). + """ + self._stream = stream + self._code = None + self._bundle = None + + def IterStatus(self): + """Iterate over statuses as they are produced by the instrumentation test. + + Yields: + A tuple (code, bundle) for each instrumentation status found in the + output. + """ + def join_bundle_values(bundle): + for key in bundle: + bundle[key] = '\n'.join(bundle[key]) + return bundle + + bundle = {'STATUS': {}, 'RESULT': {}} + header = None + key = None + for line in self._stream: + m = _INSTR_LINE_RE.match(line) + if m: + header, value = m.groups() + key = None + if header in ['STATUS', 'RESULT'] and '=' in value: + key, value = value.split('=', 1) + bundle[header][key] = [value] + elif header == 'STATUS_CODE': + yield int(value), join_bundle_values(bundle['STATUS']) + bundle['STATUS'] = {} + elif header == 'CODE': + self._code = int(value) + else: + logging.warning('Unknown INSTRUMENTATION_%s line: %s', header, value) + elif key is not None: + bundle[header][key].append(line) + + self._bundle = join_bundle_values(bundle['RESULT']) + + def GetResult(self): + """Return the final instrumentation result. + + Returns: + A pair (code, bundle) with the final instrumentation result. The |code| + may be None if no instrumentation result was found in the output. + + Raises: + AssertionError if attempting to get the instrumentation result before + exhausting |IterStatus| first. + """ + assert self._bundle is not None, ( + 'The IterStatus generator must be exhausted before reading the final' + ' instrumentation result.') + return self._code, self._bundle diff --git a/build/android/pylib/instrumentation/instrumentation_parser_test.py b/build/android/pylib/instrumentation/instrumentation_parser_test.py new file mode 100755 index 0000000..092d10f --- /dev/null +++ b/build/android/pylib/instrumentation/instrumentation_parser_test.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# Copyright 2015 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +"""Unit tests for instrumentation.InstrumentationParser.""" + +import unittest + +from pylib.instrumentation import instrumentation_parser + + +class InstrumentationParserTest(unittest.TestCase): + + def testInstrumentationParser_nothing(self): + parser = instrumentation_parser.InstrumentationParser(['']) + statuses = list(parser.IterStatus()) + code, bundle = parser.GetResult() + self.assertEqual(None, code) + self.assertEqual({}, bundle) + self.assertEqual([], statuses) + + def testInstrumentationParser_noMatchingStarts(self): + raw_output = [ + '', + 'this.is.a.test.package.TestClass:.', + 'Test result for =.', + 'Time: 1.234', + '', + 'OK (1 test)', + ] + + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + code, bundle = parser.GetResult() + self.assertEqual(None, code) + self.assertEqual({}, bundle) + self.assertEqual([], statuses) + + def testInstrumentationParser_resultAndCode(self): + raw_output = [ + 'INSTRUMENTATION_RESULT: shortMsg=foo bar', + 'INSTRUMENTATION_RESULT: longMsg=a foo', + 'walked into', + 'a bar', + 'INSTRUMENTATION_CODE: -1', + ] + + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + code, bundle = parser.GetResult() + self.assertEqual(-1, code) + self.assertEqual( + {'shortMsg': 'foo bar', 'longMsg': 'a foo\nwalked into\na bar'}, bundle) + self.assertEqual([], statuses) + + def testInstrumentationParser_oneStatus(self): + raw_output = [ + 'INSTRUMENTATION_STATUS: foo=1', + 'INSTRUMENTATION_STATUS: bar=hello', + 'INSTRUMENTATION_STATUS: world=false', + 'INSTRUMENTATION_STATUS: class=this.is.a.test.package.TestClass', + 'INSTRUMENTATION_STATUS: test=testMethod', + 'INSTRUMENTATION_STATUS_CODE: 0', + ] + + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + + expected = [ + (0, { + 'foo': '1', + 'bar': 'hello', + 'world': 'false', + 'class': 'this.is.a.test.package.TestClass', + 'test': 'testMethod', + }) + ] + self.assertEqual(expected, statuses) + + def testInstrumentationParser_multiStatus(self): + raw_output = [ + 'INSTRUMENTATION_STATUS: class=foo', + 'INSTRUMENTATION_STATUS: test=bar', + 'INSTRUMENTATION_STATUS_CODE: 1', + 'INSTRUMENTATION_STATUS: test_skipped=true', + 'INSTRUMENTATION_STATUS_CODE: 0', + 'INSTRUMENTATION_STATUS: class=hello', + 'INSTRUMENTATION_STATUS: test=world', + 'INSTRUMENTATION_STATUS: stack=', + 'foo/bar.py (27)', + 'hello/world.py (42)', + 'test/file.py (1)', + 'INSTRUMENTATION_STATUS_CODE: -1', + ] + + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + + expected = [ + (1, {'class': 'foo', 'test': 'bar',}), + (0, {'test_skipped': 'true'}), + (-1, { + 'class': 'hello', + 'test': 'world', + 'stack': '\nfoo/bar.py (27)\nhello/world.py (42)\ntest/file.py (1)', + }), + ] + self.assertEqual(expected, statuses) + + def testInstrumentationParser_statusResultAndCode(self): + raw_output = [ + 'INSTRUMENTATION_STATUS: class=foo', + 'INSTRUMENTATION_STATUS: test=bar', + 'INSTRUMENTATION_STATUS_CODE: 1', + 'INSTRUMENTATION_RESULT: result=hello', + 'world', + '', + '', + 'INSTRUMENTATION_CODE: 0', + ] + + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + code, bundle = parser.GetResult() + + self.assertEqual(0, code) + self.assertEqual({'result': 'hello\nworld\n\n'}, bundle) + self.assertEqual([(1, {'class': 'foo', 'test': 'bar'})], statuses) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/build/android/pylib/instrumentation/instrumentation_test_instance.py b/build/android/pylib/instrumentation/instrumentation_test_instance.py index 0c4f566..45e6ee4 100644 --- a/build/android/pylib/instrumentation/instrumentation_test_instance.py +++ b/build/android/pylib/instrumentation/instrumentation_test_instance.py @@ -14,6 +14,7 @@ from pylib import flag_changer from pylib.base import base_test_result from pylib.base import test_instance from pylib.instrumentation import test_result +from pylib.instrumentation import instrumentation_parser from pylib.utils import apk_helper from pylib.utils import md5sum from pylib.utils import proguard @@ -47,48 +48,10 @@ def ParseAmInstrumentRawOutput(raw_output): - the bundle dump as a dict mapping string keys to a list of strings, one for each line. """ - INSTR_STATUS = 'INSTRUMENTATION_STATUS: ' - INSTR_STATUS_CODE = 'INSTRUMENTATION_STATUS_CODE: ' - INSTR_RESULT = 'INSTRUMENTATION_RESULT: ' - INSTR_CODE = 'INSTRUMENTATION_CODE: ' - - last = None - instr_code = None - instr_result = [] - instr_statuses = [] - bundle = {} - for line in raw_output: - if line.startswith(INSTR_STATUS): - instr_var = line[len(INSTR_STATUS):] - if '=' in instr_var: - k, v = instr_var.split('=', 1) - bundle[k] = [v] - last = INSTR_STATUS - last_key = k - else: - logging.debug('Unknown "%s" line: %s' % (INSTR_STATUS, line)) - - elif line.startswith(INSTR_STATUS_CODE): - instr_status = line[len(INSTR_STATUS_CODE):] - instr_statuses.append((int(instr_status), bundle)) - bundle = {} - last = INSTR_STATUS_CODE - - elif line.startswith(INSTR_RESULT): - instr_result.append(line[len(INSTR_RESULT):]) - last = INSTR_RESULT - - elif line.startswith(INSTR_CODE): - instr_code = int(line[len(INSTR_CODE):]) - last = INSTR_CODE - - elif last == INSTR_STATUS: - bundle[last_key].append(line) - - elif last == INSTR_RESULT: - instr_result.append(line) - - return (instr_code, instr_result, instr_statuses) + parser = instrumentation_parser.InstrumentationParser(raw_output) + statuses = list(parser.IterStatus()) + code, bundle = parser.GetResult() + return (code, bundle, statuses) def GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms): @@ -106,22 +69,15 @@ def GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms): Returns: An InstrumentationTestResult object. """ - INSTR_STATUS_CODE_START = 1 - INSTR_STATUS_CODE_OK = 0 - INSTR_STATUS_CODE_ERROR = -1 - INSTR_STATUS_CODE_FAIL = -2 - log = '' result_type = base_test_result.ResultType.UNKNOWN for status_code, bundle in instr_statuses: - if status_code == INSTR_STATUS_CODE_START: + if status_code == instrumentation_parser.STATUS_CODE_START: pass - elif status_code == INSTR_STATUS_CODE_OK: - bundle_test = '%s#%s' % ( - ''.join(bundle.get('class', [''])), - ''.join(bundle.get('test', ['']))) - skipped = ''.join(bundle.get('test_skipped', [''])) + elif status_code == instrumentation_parser.STATUS_CODE_OK: + bundle_test = '%s#%s' % (bundle.get('class', ''), bundle.get('test', '')) + skipped = bundle.get('test_skipped', '') if (test_name == bundle_test and result_type == base_test_result.ResultType.UNKNOWN): @@ -130,13 +86,13 @@ def GenerateTestResult(test_name, instr_statuses, start_ms, duration_ms): result_type = base_test_result.ResultType.SKIP logging.info('Skipped ' + test_name) else: - if status_code not in (INSTR_STATUS_CODE_ERROR, - INSTR_STATUS_CODE_FAIL): + if status_code not in (instrumentation_parser.STATUS_CODE_ERROR, + instrumentation_parser.STATUS_CODE_FAILURE): logging.error('Unrecognized status code %d. Handling as an error.', status_code) result_type = base_test_result.ResultType.FAIL if 'stack' in bundle: - log = '\n'.join(bundle['stack']) + log = bundle['stack'] return test_result.InstrumentationTestResult( test_name, result_type, start_ms, duration_ms, log=log) @@ -466,24 +422,22 @@ class InstrumentationTestInstance(test_instance.TestInstance): @staticmethod def GenerateMultiTestResult(errors, statuses): - INSTR_STATUS_CODE_START = 1 results = [] skip_counter = 1 for status_code, bundle in statuses: - if status_code != INSTR_STATUS_CODE_START: + if status_code != instrumentation_parser.STATUS_CODE_START: # TODO(rnephew): Make skipped tests still output test name. This is only # there to give skipped tests a unique name so they are counted if 'test_skipped' in bundle: test_name = str(skip_counter) skip_counter += 1 else: - test_name = '%s#%s' % ( - ''.join(bundle.get('class', [''])), - ''.join(bundle.get('test', ['']))) + test_name = '%s#%s' % (bundle.get('class', ''), + bundle.get('test', '')) results.append( GenerateTestResult(test_name, [(status_code, bundle)], 0, 0)) - for error in errors: + for error in errors.itervalues(): if _NATIVE_CRASH_RE.search(error): results.append( base_test_result.BaseTestResult( diff --git a/build/android/pylib/instrumentation/instrumentation_test_instance_test.py b/build/android/pylib/instrumentation/instrumentation_test_instance_test.py index 3bf3939..693f175 100755 --- a/build/android/pylib/instrumentation/instrumentation_test_instance_test.py +++ b/build/android/pylib/instrumentation/instrumentation_test_instance_test.py @@ -27,115 +27,6 @@ class InstrumentationTestInstanceTest(unittest.TestCase): options = mock.Mock() options.tool = '' - def testParseAmInstrumentRawOutput_nothing(self): - code, result, statuses = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput([''])) - self.assertEqual(None, code) - self.assertEqual([], result) - self.assertEqual([], statuses) - - def testParseAmInstrumentRawOutput_noMatchingStarts(self): - raw_output = [ - '', - 'this.is.a.test.package.TestClass:.', - 'Test result for =.', - 'Time: 1.234', - '', - 'OK (1 test)', - ] - - code, result, statuses = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput(raw_output)) - self.assertEqual(None, code) - self.assertEqual([], result) - self.assertEqual([], statuses) - - def testParseAmInstrumentRawOutput_resultAndCode(self): - raw_output = [ - 'INSTRUMENTATION_RESULT: foo', - 'bar', - 'INSTRUMENTATION_CODE: -1', - ] - - code, result, _ = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput(raw_output)) - self.assertEqual(-1, code) - self.assertEqual(['foo', 'bar'], result) - - def testParseAmInstrumentRawOutput_oneStatus(self): - raw_output = [ - 'INSTRUMENTATION_STATUS: foo=1', - 'INSTRUMENTATION_STATUS: bar=hello', - 'INSTRUMENTATION_STATUS: world=false', - 'INSTRUMENTATION_STATUS: class=this.is.a.test.package.TestClass', - 'INSTRUMENTATION_STATUS: test=testMethod', - 'INSTRUMENTATION_STATUS_CODE: 0', - ] - - _, _, statuses = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput(raw_output)) - - expected = [ - (0, { - 'foo': ['1'], - 'bar': ['hello'], - 'world': ['false'], - 'class': ['this.is.a.test.package.TestClass'], - 'test': ['testMethod'], - }) - ] - self.assertEqual(expected, statuses) - - def testParseAmInstrumentRawOutput_multiStatus(self): - raw_output = [ - 'INSTRUMENTATION_STATUS: class=foo', - 'INSTRUMENTATION_STATUS: test=bar', - 'INSTRUMENTATION_STATUS_CODE: 1', - 'INSTRUMENTATION_STATUS: test_skipped=true', - 'INSTRUMENTATION_STATUS_CODE: 0', - 'INSTRUMENTATION_STATUS: class=hello', - 'INSTRUMENTATION_STATUS: test=world', - 'INSTRUMENTATION_STATUS: stack=', - 'foo/bar.py (27)', - 'hello/world.py (42)', - 'test/file.py (1)', - 'INSTRUMENTATION_STATUS_CODE: -1', - ] - - _, _, statuses = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput(raw_output)) - - expected = [ - (1, {'class': ['foo'], 'test': ['bar'],}), - (0, {'test_skipped': ['true']}), - (-1, { - 'class': ['hello'], - 'test': ['world'], - 'stack': ['', 'foo/bar.py (27)', 'hello/world.py (42)', - 'test/file.py (1)'], - }), - ] - self.assertEqual(expected, statuses) - - def testParseAmInstrumentRawOutput_statusResultAndCode(self): - raw_output = [ - 'INSTRUMENTATION_STATUS: class=foo', - 'INSTRUMENTATION_STATUS: test=bar', - 'INSTRUMENTATION_STATUS_CODE: 1', - 'INSTRUMENTATION_RESULT: hello', - 'world', - '', - '', - 'INSTRUMENTATION_CODE: 0', - ] - - code, result, statuses = ( - instrumentation_test_instance.ParseAmInstrumentRawOutput(raw_output)) - - self.assertEqual(0, code) - self.assertEqual(['hello', 'world', '', ''], result) - self.assertEqual([(1, {'class': ['foo'], 'test': ['bar']})], statuses) - def testGenerateTestResult_noStatus(self): result = instrumentation_test_instance.GenerateTestResult( 'test.package.TestClass#testMethod', [], 0, 1000) @@ -147,12 +38,12 @@ class InstrumentationTestInstanceTest(unittest.TestCase): def testGenerateTestResult_testPassed(self): statuses = [ (1, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (0, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), ] result = instrumentation_test_instance.GenerateTestResult( @@ -162,15 +53,15 @@ class InstrumentationTestInstanceTest(unittest.TestCase): def testGenerateTestResult_testSkipped_first(self): statuses = [ (0, { - 'test_skipped': ['true'], + 'test_skipped': 'true', }), (1, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (0, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), ] result = instrumentation_test_instance.GenerateTestResult( @@ -180,15 +71,15 @@ class InstrumentationTestInstanceTest(unittest.TestCase): def testGenerateTestResult_testSkipped_last(self): statuses = [ (1, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (0, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (0, { - 'test_skipped': ['true'], + 'test_skipped': 'true', }), ] result = instrumentation_test_instance.GenerateTestResult( @@ -198,15 +89,15 @@ class InstrumentationTestInstanceTest(unittest.TestCase): def testGenerateTestResult_testSkipped_false(self): statuses = [ (0, { - 'test_skipped': ['false'], + 'test_skipped': 'false', }), (1, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (0, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), ] result = instrumentation_test_instance.GenerateTestResult( @@ -216,12 +107,12 @@ class InstrumentationTestInstanceTest(unittest.TestCase): def testGenerateTestResult_testFailed(self): statuses = [ (1, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), (-2, { - 'class': ['test.package.TestClass'], - 'test': ['testMethod'], + 'class': 'test.package.TestClass', + 'test': 'testMethod', }), ] result = instrumentation_test_instance.GenerateTestResult( |