summaryrefslogtreecommitdiffstats
path: root/build/android
diff options
context:
space:
mode:
authorperezju <perezju@chromium.org>2015-02-11 08:44:23 -0800
committerCommit bot <commit-bot@chromium.org>2015-02-11 16:45:03 +0000
commit734e006023ee23e636ea757bbe94f13f135941d3 (patch)
tree5311318f5f6c97a650890f6b3b0ae259344e06bc /build/android
parent2a7267cf6a9685383efa0de531a451116ba00511 (diff)
downloadchromium_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')
-rw-r--r--build/android/pylib/instrumentation/instrumentation_parser.py96
-rwxr-xr-xbuild/android/pylib/instrumentation/instrumentation_parser_test.py134
-rw-r--r--build/android/pylib/instrumentation/instrumentation_test_instance.py78
-rwxr-xr-xbuild/android/pylib/instrumentation/instrumentation_test_instance_test.py155
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(