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

Subscribers

People subscribed via source and target branches