Merge lp:~zyga/lava-android-test/blackbox into lp:lava-android-test

Proposed by Zygmunt Krynicki
Status: Merged
Merged at revision: 205
Proposed branch: lp:~zyga/lava-android-test/blackbox
Merge into: lp:lava-android-test
Diff against target: 493 lines (+489/-0)
1 file modified
lava_android_test/test_definitions/blackbox.py (+489/-0)
To merge this branch: bzr merge lp:~zyga/lava-android-test/blackbox
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Linaro Validation Team Pending
Review via email: mp+121299@code.launchpad.net

Description of the change

Integration with the blackbox tests for android.

This is _not_ a typical test. Please read the implementation and especially all the comments to understand the decisions made.

To post a comment you must log in.
Revision history for this message
Yongqin Liu (liuyq0307) wrote :

from this file itself, it's no problem.
but I still feel that make it a new sub command(like run-blackbox) in the commands.py
will make the source more readable.
although that needs to modify more files (including lava-dispatcher) instead of only this one file.

And also suggest adding a provider in the provider.py to support such tests that only need to run and get the test result.
no need to use the parser to parse the output.

and maybe this is just a temporary place to integrate the blackbox test,
since I can't see how the blackbox will be in the future, so please also ask Andy to review this MP.

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

W dniu 29.08.2012 08:34, Yongqin Liu pisze:
> from this file itself, it's no problem.
> but I still feel that make it a new sub command(like run-blackbox) in the commands.py
> will make the source more readable.
> although that needs to modify more files (including lava-dispatcher) instead of only this one file.
>
> And also suggest adding a provider in the provider.py to support such tests that only need to run and get the test result.
> no need to use the parser to parse the output.

The only long term solution is to fix the API mistakes and remove all
other subcommands. There is no justification for having all those
subcommands. All of that are tests, they should have single install/run
interface.

> and maybe this is just a temporary place to integrate the blackbox test,
> since I can't see how the blackbox will be in the future, so please also ask Andy to review this MP.

OK

Thanks
ZK

Revision history for this message
Andy Doan (doanac) wrote :

Seems like some of the code isn't that necessary:

debuggable_* - cool, but now that the feature works is it needed?

SuperAdb/AdbMixin - seems like you could pretty much just add a new listdir helper

However, that's nothing major and its isolated. Also, as Yongqin said, the blackbox stuff will eventually move us to something else, so lets get this in and see it work!

review: Approve
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

W dniu 29.08.2012 17:52, Andy Doan pisze:
> Review: Approve
>
> Seems like some of the code isn't that necessary:
>
> debuggable_* - cool, but now that the feature works is it needed?

No, it's not needed but it's still a cool thing (since l-a-t swallows
most exceptions) for analyzing problems. Perhaps I could just move it to
some utils.py module?

> SuperAdb/AdbMixin - seems like you could pretty much just add a new listdir helper

Yes, that behaves much like os.listdir() and has more predictable
behavior. It's just a helper that does parsing essentially :-)

> However, that's nothing major and its isolated. Also, as Yongqin said, the blackbox stuff will eventually move us to something else, so lets get this in and see it work!

I don't know how it will evolve over time. My current intent is to
ensure we can continue to harvest data from l-b with l-a-t, and in near
future with l-t. Once the rest of the framework solidifies and matures
we could simplify the interaction but I don't suspect we'll go away form
the simple usefulness of adb-managed communication. It simply works very
well and has little complexity.

Thanks
ZK

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lava_android_test/test_definitions/blackbox.py'
--- lava_android_test/test_definitions/blackbox.py 1970-01-01 00:00:00 +0000
+++ lava_android_test/test_definitions/blackbox.py 2012-08-29 00:06:21 +0000
@@ -0,0 +1,489 @@
1# Copyright (c) 2012 Linaro Limited
2
3# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
4#
5# This file is part of LAVA Android Test.
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20"""
21Bridge for the black-box testing implemented by android-lava-wrapper.
22
23See: https://github.com/zyga/android-lava-wrapper
24"""
25
26import datetime
27import functools
28import logging
29import os
30import pdb
31import shutil
32import subprocess
33import tempfile
34
35from linaro_dashboard_bundle.evolution import DocumentEvolution
36from linaro_dashboard_bundle.io import DocumentIO
37
38from lava_android_test.config import get_config
39
40
41def debuggable_real(func):
42 """
43 Helper for debugging functions that otherwise have their exceptions
44 consumed by the caller. Any exception raised from such a function will
45 trigger a pdb session when 'DEBUG_DEBUGGABLE' environment is set.
46 """
47 @functools.wraps(func)
48 def debuggable_decorator(*args, **kwargs):
49 try:
50 return func(*args, **kwargs)
51 except:
52 logging.exception("exception in @debuggable function")
53 pdb.post_mortem()
54 return debuggable_decorator
55
56
57def debuggable_noop(func):
58 return func
59
60
61if os.getenv("DEBUG_DEBUGGABLE"):
62 debuggable = debuggable_real
63else:
64 debuggable = debuggable_noop
65
66
67class SuperAdb(object):
68 """
69 Class that implements certain parts of ADB()-like API differently.
70 """
71
72 def __init__(self, stock_adb):
73 # Name of the adb executable with any required arguments,
74 # such as -s 'serial'
75 self._adb_cmd = stock_adb.adb.split()
76
77 def __call__(self, command, *args, **kwargs):
78 """
79 Invoke adb command.
80
81 This call is somewhat special that it wraps two subprocess helper
82 functions: check_call and check_output. They are called depending
83 on the keyword argument 'stdout', if passed as None then output
84 is _not_ saved and is instantly streamed to the stdout of the running
85 program. In any other case stdout is buffered and saved, then returned
86 """
87 cmd = self._adb_cmd + command
88 if "stdout" in kwargs and kwargs['stdout'] is None:
89 del kwargs['stdout']
90 return subprocess.check_call(cmd, *args, **kwargs)
91 else:
92 return subprocess.check_output(cmd, *args, **kwargs)
93
94 def listdir(self, dirname):
95 """
96 List directory entries on the android device.
97
98 Similar to adb.listdir() as implemented in ADB() but generates
99 subsequent lines instead of returning a big lump of text for the
100 developer to parse. Also, instead of using 'ls' on the target it
101 uses the special 'ls' command built into adb.
102
103 The two special entries, . and .., are omitted
104 """
105 for line in self(['ls', dirname]).splitlines():
106 # a, b and c are various pieces of stat data
107 # but we don't need that here.
108 a, b, c, pathname = line.split(' ', 3)
109 if pathname not in ('.', '..'):
110 yield pathname
111
112
113class AdbMixIn(object):
114 """
115 Mix-in class that assists in setting up ADB.
116
117 lava-android-test uses the setadb()/getadb() methods to pass the ADB object
118 (which encapsulates connection data for the specific device we will be
119 talking to).
120
121 Since the ADB object has fixed API and changes there are beyond the scope
122 of this test any extra stuff we want from ADB will be provided by the
123 SuperAdb class.
124
125 This mix-in class that has methods expected by lava-android-test and
126 exposes two properties, adb and super_adb.
127 """
128
129 adb = None
130
131 def setadb(self, adb=None):
132 if self.adb is None and adb is not None:
133 self.adb = adb
134 else:
135 self.adb = adb
136 self.super_adb = SuperAdb(adb)
137
138 def getadb(self):
139 return self.adb
140
141
142class Sponge(object):
143 """
144 A simple namespace-like object that anyone can assign and read freely.
145
146 To get some understanding of what is going on both reads and writes are
147 logged.
148 """
149
150 def __getattr__(self, attr):
151 return super(Sponge, self).__getattr__(attr)
152
153 def __setattr__(self, attr, value):
154 super(Sponge, self).__setattr__(attr, value)
155
156
157class FutureFormatDetected(Exception):
158 """
159 Exception raised when the code detects a new, unsupported
160 format that was created after this library was written.
161
162 Since formats do not have partial ordering we can only detect
163 a future format when the document format is already at the "latest"
164 value, as determined by DocumentEvolution.is_latest(), but the actual
165 format is not known to us.
166
167 Typically this won't happen often as document upgrades are not performed
168 unless necessary. The only case when this may happen is where the bundle
169 loaded from the device was already using a future format to begin with.
170 """
171
172 def __init__(self, format):
173 self.format = format
174
175 def __str__(self):
176 "Unsupported, future format: %s" % self.format
177
178 def __repr__(self):
179 return "FutureFormatDetected(%r)" % self.format
180
181
182class BlackBoxTestBridge(AdbMixIn):
183 """
184 Bridge for interacting with black box tests implemented as something that
185 looks like android test definition.
186 """
187
188 # NOTE: none of the tests will actually carry this ID, it is simply used
189 # here so that it's not a magic value.
190 testname = 'blackbox'
191
192 def __init__(self):
193 """
194 Initialize black-box test bridge
195 """
196 # The sponge object is just a requirement from the API, it is not
197 # actually used by us in any way. The framework assigns a skeleton
198 # test result there but we don't really need it. The Sponge object
199 # is a simple 'bag' or namespace that will happily accept and record
200 # any values.
201 self.parser = Sponge()
202
203 def install(self, install_options=None):
204 """
205 "Install" blackbox on the test device.
206
207 Black box tests cannot be installed, they must be pre-baked into the
208 image. To conform to the 'protocol' used by lava-android-test we will
209 perform a fake 'installation' of the black box tests by creating a
210 directory that lava-android-test is checking for. We do that only if
211 the lava-blackbox executable, which is the entry point to black box
212 tests exists in the image.
213
214 ..note::
215 This method is part of the lava-android-test framework API.
216 """
217 if not self.adb.exists(self._blackbox_pathname):
218 # Sadly lava-android-test has no exception hierarchy that we can
219 # use so all problems are reported as RuntimeError
220 raise RuntimeError(
221 'blackbox test cannot be "installed" as they must be built'
222 ' into the image.'
223 ' See https://github.com/zyga/android-lava-wrapper'
224 ' for details.')
225 else:
226 self.adb.makedirs(self._fake_install_path)
227
228 def uninstall(self):
229 """
230 Conformance method to keep up with the API required by
231 lava-android-test. It un-does what install() did by removing the
232 _fake_install_path directory from the device.
233
234 ..note::
235 This method is part of the lava-android-test framework API.
236 """
237 if self.adb.exists(self._fake_install_path):
238 self.adb.rmtree(self._fake_install_path)
239
240 @debuggable
241 def run(self, quiet=False, run_options=None):
242 """
243 Run the black-box test on the target device.
244
245 Use ADB to run the black-box executable on the device. Keep the results
246 in the place that lava-android-test expects us to use.
247
248 ..note::
249 This method is part of the lava-android-test framework API.
250 """
251 # The blackbox test runner will create a directory each time it is
252 # started. All of those directories will be created relative to a so
253 # called spool directory. Instead of using the default spool directory
254 # (which can also change) we will use the directory where
255 # lava-android-test keeps all of the results.
256 spool_dir = get_config().resultsdir_android
257 logging.debug("Using spool directory for black-box testing: %r", spool_dir)
258 stuff_before = frozenset(self.super_adb.listdir(spool_dir))
259 blackbox_command = [
260 'shell', self._blackbox_pathname,
261 '--spool', spool_dir,
262 '--run-all-tests']
263 # Let's run the blackbox executable via ADB
264 logging.debug("Starting black-box tests...")
265 self.super_adb(blackbox_command, stdout=None)
266 logging.debug("Black-box tests have finished!")
267 stuff_after = frozenset(self.super_adb.listdir(spool_dir))
268 # Check what got added to the spool directory
269 new_entries = stuff_after - stuff_before
270 if len(new_entries) == 0:
271 raise RuntimeError("Nothing got added to the spool directory")
272 elif len(new_entries) > 1:
273 raise RuntimeError("Multiple items added to the spool directory")
274 result_id = list(new_entries)[0]
275 print "The blackbox test have finished running, the result id is %r" % result_id
276 return result_id
277
278 def parse(self, result_id):
279 """
280 UNIMPLEMENTED METHOD
281
282 Sadly this method is never called as lava-android-test crashes before
283 it gets to realize it is processing blackbox results and load this
284 class. This crash _may_ be avoided by hiding the real results of
285 blackbox and instead populating the results directory with dummy test
286 results that only let LAVA figure out that blackbox is the test to
287 load. Then we could monkey patch other parts and it could be
288 implemented.
289
290 ONCE THIS IS FIXED THE FOLLOWING DESCRIPTION SHOULD APPLY
291
292 Parse and save results of previous test run.
293
294 The result_id is a name of a directory on the Android device (
295 relative to the resultsdir_android configuration option).
296
297 ..note::
298 This method is part of the lava-android-test framework API.
299 """
300 # Sadly since the goal is integration with lava lab I don't have the
301 # time to do it. In the lab we use lava-android-test run -o anyway.
302 raise NotImplementedError()
303
304 def _get_combined_bundle(self, result_id):
305 """
306 Compute the combined bundle of a past run and return it
307 """
308 config = get_config()
309 temp_dir = tempfile.mkdtemp()
310 remote_bundle_dir = os.path.join(config.resultsdir_android, result_id)
311 try:
312 self._copy_all_bundles(remote_bundle_dir, temp_dir)
313 bundle = self._combine_bundles(temp_dir)
314 finally:
315 shutil.rmtree(temp_dir)
316 return bundle
317
318 # Desired format name, used in a few methods below
319 _desired_format = "Dashboard Bundle Format 1.3"
320
321 def _copy_all_bundles(self, android_src, host_dest):
322 """
323 Use adb pull to copy all the files from android_src (android
324 fileystem) to host_dest (host filesystem).
325 """
326 logging.debug("Saving bundles from %s to %s", android_src, host_dest)
327 for name in self.super_adb.listdir(android_src):
328 logging.debug("Considering file %s", name)
329 # NOTE: We employ simple filtering for '.json' files. This prevents
330 # spurious JSON parsing errors if the result directory has
331 # additional files of any kind.
332 #
333 # We _might_ want to lessen that eventually restriction but at this
334 # time blackbox is really designed to be self-sufficient so there
335 # is no point of additional files.
336 if not name.endswith('.json'):
337 continue
338 remote_pathname = os.path.join(android_src, name)
339 local_pathname = os.path.join(host_dest, name)
340 try:
341 logging.debug(
342 "Copying %s to %s", remote_pathname, local_pathname)
343 self.adb.pull(remote_pathname, local_pathname)
344 except:
345 logging.exception("Unable to copy bundle %s", name)
346
347 def _combine_bundles(self, dirname):
348 """
349 Combine all bundles from a previous test run into one bundle.
350
351 Returns the aggregated bundle object
352
353 Load, parse and validate each bundle from the specified directory and
354 combine them into one larger bundle. This is somewhat tricky. Each
355 bundle we coalesce may be generated by a different, separate programs
356 and may, thus, use different formats.
357
358 To combine them all correctly we need to take two precautions:
359 1) All bundles must be updated to a single, common format
360 2) No bundle may be upgraded beyond the latest format known
361 to this code. Since the hypothetical 2.0 format may be widely
362 different that we cannot reliably interpret anything beyond
363 the format field. To prevent this we use the evolution API
364 to carefully upgrade only to the "sentinel" format, 1.3
365 (at this time)
366 """
367 # Use DocumentIO.loads() to preserve the order of entries.
368 # This is a very small touch but it makes reading the results
369 # far more pleasant.
370 aggregated_bundle = DocumentIO.loads(
371 '{\n'
372 '"format": "' + self._desired_format + '",\n'
373 '"test_runs": []\n'
374 '}\n')[1]
375 # Iterate over all files there
376 for name in os.listdir(dirname):
377 bundle_pathname = os.path.join(dirname, name)
378 # Process bundle one by one
379 try:
380 format, bundle = self._load_bundle(bundle_pathname)
381 self._convert_to_common_format(format, bundle)
382 self._combine_with_aggregated(aggregated_bundle, bundle)
383 except:
384 logging.exception("Unable to process bundle %s", name)
385 # Return the aggregated bundle
386 return aggregated_bundle
387
388 def _load_bundle(self, local_pathname):
389 """
390 Load the bundle from local_pathname.
391
392 There are various problems that can happen here but
393 they should all be treated equally, the bundle not
394 being used. This also transparently does schema validation
395 so the chance of getting wrong data is lower.
396 """
397 with open(local_pathname, 'rt') as stream:
398 format, bundle = DocumentIO.load(stream)
399 return format, bundle
400
401 def _convert_to_common_format(self, format, bundle):
402 """
403 Convert the bundle to the common format.
404
405 This is a careful and possibly fragile process that may
406 raise FutureFormatDetected exception. If that happens
407 then desired_format (encoded in the function itself) must be
408 changed and the code reviewed for any possible changes
409 required to support the more recent format.
410 """
411 while True:
412 # Break conditions, encoded separately for clarity
413 if format == self._desired_format:
414 # This is our desired break condition, when format
415 # becomes (or starts as) the desired format
416 break
417 if DocumentEvolution.is_latest(bundle):
418 # This is a less desired break condition, if we
419 # got here then the only possible explanation is
420 # that some program started with format > desired_format
421 # and the DocumentEvolution API is updated to understand
422 # it but we are not. In that case let's raise an exception
423 raise FutureFormatDetected(format)
424 # As long as the document format is old keep upgrading it
425 # step-by-step. Evolution is done in place
426 DocumentEvolution.evolve_document(bundle, one_step=True)
427
428 def _combine_with_aggregated(self, aggregated_bundle, bundle):
429 """
430 Combine the bundle with the contents of aggregated_bundle.
431
432 This method simply transplants all the test runs as that is what
433 the bundle format was designed to be - a simple container for test
434 runs.
435 """
436 assert bundle["format"] == self._desired_format
437 assert aggregated_bundle["format"] == self._desired_format
438 aggregated_bundle["test_runs"].extend(bundle.get("test_runs", []))
439
440 @property
441 def _blackbox_pathname(self):
442 """
443 The path to the blackbox bridge on the device.
444 """
445 return "/system/bin/lava-blackbox"
446
447 @property
448 def _fake_install_path(self):
449 """
450 The path that we create on the android system to
451 indicate that the black box test is installed.
452
453 This is used by uninstall() and install()
454 """
455 config = get_config()
456 return os.path.join(config.installdir_android, self.testname)
457
458 def _monkey_patch_lava(self):
459 """
460 Monkey patch the implementation of
461 lava_android_test.commands.generate_bundle
462
463 This change is irreversible but given the one-off nature of
464 lava-android-test this is okay. It should be safe to do this since
465 LAVA will only load the blackbox test module if we explicitly request
466 to run it. At that time no other tests will run in the same process.
467
468 This method should not be used once lava-android-test grows a better
469 API to allow us to control how bundles are generated.
470 """
471 from lava_android_test import commands
472 def _phony_generate_bundle(serial=None, result_id=None,
473 test=None, test_id=None, attachments=[]):
474 if result_id is None:
475 raise NotImplementedError
476 return self._get_combined_bundle(result_id)
477 commands.generate_bundle = _phony_generate_bundle
478 logging.warning(
479 "The 'blackbox' test definition has monkey-patched the function"
480 " lava_android_test.commands.generate_bundle() if you are _not_"
481 " running the blackbox test or are experiencing odd problems/crashes"
482 " below please look at this method first")
483
484
485# initialize the blackbox test definition object
486testobj = BlackBoxTestBridge()
487
488# Then monkey patch lava-android-test so that parse keeps working
489testobj._monkey_patch_lava()

Subscribers

People subscribed via source and target branches