From e60e31ff8e3197038b6ffcb3d9b87b6ecb5612b1 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Fri, 7 Apr 2017 11:02:02 +0100 Subject: [PATCH 01/14] Imports: Updated Imports Switch remaining wlauto imports to wa. Added other missing imports. --- wa/commands/record.py | 6 ++++-- wa/framework/configuration/__init__.py | 9 ++++----- wa/framework/configuration/default.py | 8 ++++---- wa/framework/host.py | 2 +- wa/framework/plugin.py | 8 ++++---- wa/framework/signal.py | 6 +++--- wa/framework/workload.py | 3 ++- wa/tests/test_diff.py | 2 +- wa/utils/misc.py | 2 +- 9 files changed, 24 insertions(+), 22 deletions(-) diff --git a/wa/commands/record.py b/wa/commands/record.py index e45e8ff4..be72cd09 100644 --- a/wa/commands/record.py +++ b/wa/commands/record.py @@ -18,9 +18,11 @@ import sys from wa import Command, settings -from wa.framework.configuration import RunConfiguration +from wa.framework import pluginloader +from wa.framework.agenda import Agenda from wa.framework.resource import Executable, NO_ONE, ResourceResolver -from wa.utils.revent import ReventRecorder +from wa.framework.configuration import RunConfiguration +from wa.framework.workload import ApkUiautoWorkload class RecordCommand(Command): diff --git a/wa/framework/configuration/__init__.py b/wa/framework/configuration/__init__.py index a3593794..697a4811 100644 --- a/wa/framework/configuration/__init__.py +++ b/wa/framework/configuration/__init__.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from wlauto.core.configuration.configuration import (settings, - RunConfiguration, - JobGenerator, - ConfigurationPoint) -from wlauto.core.configuration.plugin_cache import PluginCache +from wa.framework.configuration.core import (settings, + RunConfiguration, + JobGenerator, + ConfigurationPoint) diff --git a/wa/framework/configuration/default.py b/wa/framework/configuration/default.py index 5145a6b4..ddfec4af 100644 --- a/wa/framework/configuration/default.py +++ b/wa/framework/configuration/default.py @@ -1,7 +1,7 @@ -from wlauto.core.configuration.configuration import MetaConfiguration, RunConfiguration -from wlauto.core.configuration.plugin_cache import PluginCache -from wlauto.utils.serializer import yaml -from wlauto.utils.doc import strip_inlined_text +from wa.framework.configuration.core import MetaConfiguration, RunConfiguration +from wa.framework.configuration.plugin_cache import PluginCache +from wa.utils.serializer import yaml +from wa.utils.doc import strip_inlined_text DEFAULT_INSTRUMENTS = ['execution_time', 'interrupts', diff --git a/wa/framework/host.py b/wa/framework/host.py index 33810b93..43b1bf59 100644 --- a/wa/framework/host.py +++ b/wa/framework/host.py @@ -1,6 +1,6 @@ import os -from wlauto.core.configuration import settings +from wa.framework.configuration.core import settings def init_user_directory(overwrite_existing=False): # pylint: disable=R0914 """ diff --git a/wa/framework/plugin.py b/wa/framework/plugin.py index 7c8e5dba..c9828839 100644 --- a/wa/framework/plugin.py +++ b/wa/framework/plugin.py @@ -26,7 +26,7 @@ from itertools import chain from copy import copy from wa.framework.configuration.core import settings, ConfigurationPoint as Parameter -from wa.framework.exception import (NotFoundError, PluginLoaderError, +from wa.framework.exception import (NotFoundError, PluginLoaderError, TargetError, ValidationError, ConfigError, HostError) from wa.utils import log from wa.utils.misc import (ensure_directory_exists as _d, walk_modules, load_class, @@ -430,7 +430,7 @@ class Plugin(object): get_module(name, owner, **kwargs) - and returns an instance of :class:`wlauto.core.plugin.Module`. If the + and returns an instance of :class:`wa.core.plugin.Module`. If the module with the specified name is not found, the loader must raise an appropriate exception. @@ -743,10 +743,10 @@ class PluginLoader(object): self.logger.warning('Got: {}'.format(e)) else: msg = 'Failed to load {}' - raise LoaderError(msg.format(filepath), sys.exc_info()) + raise PluginLoaderError(msg.format(filepath), sys.exc_info()) except Exception as e: message = 'Problem loading plugins from {}: {}' - raise LoaderError(message.format(filepath, e)) + raise PluginLoaderError(message.format(filepath, e)) def _discover_in_module(self, module): # NOQA pylint: disable=too-many-branches self.logger.debug('Checking module %s', module.__name__) diff --git a/wa/framework/signal.py b/wa/framework/signal.py index 20c6a0b2..4f643933 100644 --- a/wa/framework/signal.py +++ b/wa/framework/signal.py @@ -231,7 +231,7 @@ def connect(handler, signal, sender=dispatcher.Any, priority=0): .. note:: There is nothing that prevents instrumentation from sending their own signals that are not part of the standard set. However the signal - must always be an :class:`wlauto.core.signal.Signal` instance. + must always be an :class:`wa.core.signal.Signal` instance. :sender: The handler will be invoked only for the signals emitted by this sender. By default, this is set to :class:`louie.dispatcher.Any`, so the handler will @@ -270,7 +270,7 @@ def disconnect(handler, signal, sender=dispatcher.Any): :handler: The callback to be disconnected. :signal: The signal the handler is to be disconnected form. It will - be an :class:`wlauto.core.signal.Signal` instance. + be an :class:`wa.core.signal.Signal` instance. :sender: If specified, the handler will only be disconnected from the signal sent by this sender. @@ -284,7 +284,7 @@ def send(signal, sender=dispatcher.Anonymous, *args, **kwargs): Paramters: - :signal: Signal to be sent. This must be an instance of :class:`wlauto.core.signal.Signal` + :signal: Signal to be sent. This must be an instance of :class:`wa.core.signal.Signal` or its subclasses. :sender: The sender of the signal (typically, this would be ``self``). Some handlers may only be subscribed to signals from a particular sender. diff --git a/wa/framework/workload.py b/wa/framework/workload.py index b2f565e3..e548d74d 100644 --- a/wa/framework/workload.py +++ b/wa/framework/workload.py @@ -17,7 +17,8 @@ import os import time from wa.framework.plugin import TargetedPlugin -from wa.framework.resource import ApkFile, JarFile, ReventFile, NO_ONE +from wa.framework.resource import (ApkFile, JarFile, ReventFile, NO_ONE, + Executable) from wa.framework.exception import WorkloadError from devlib.utils.android import ApkInfo diff --git a/wa/tests/test_diff.py b/wa/tests/test_diff.py index cc1683cc..52f6fbdf 100644 --- a/wa/tests/test_diff.py +++ b/wa/tests/test_diff.py @@ -22,7 +22,7 @@ from unittest import TestCase from nose.tools import assert_equal -from wlauto.instrumentation.misc import _diff_interrupt_files +from wa.instrumentation.misc import _diff_interrupt_files class InterruptDiffTest(TestCase): diff --git a/wa/utils/misc.py b/wa/utils/misc.py index 4a564c90..a41bbd22 100644 --- a/wa/utils/misc.py +++ b/wa/utils/misc.py @@ -54,7 +54,7 @@ from devlib.utils.misc import (ABI_MAP, check_output, walk_modules, check_output_logger = logging.getLogger('check_output') -# Defined here rather than in wlauto.exceptions due to module load dependencies +# Defined here rather than in wa.exceptions due to module load dependencies def diff_tokens(before_token, after_token): """ Creates a diff of two tokens. From a17e11251ea89578ea9082772482b4f21719f8e3 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Tue, 11 Apr 2017 09:34:49 +0100 Subject: [PATCH 02/14] Workload: Corrected doc string --- wa/framework/workload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wa/framework/workload.py b/wa/framework/workload.py index e548d74d..d03cdddd 100644 --- a/wa/framework/workload.py +++ b/wa/framework/workload.py @@ -65,7 +65,7 @@ class Workload(TargetedPlugin): def run(self, context): """ Execute the workload. This is the method that performs the actual - "work" of the. + "work" of the workload. """ pass From b7ed59edcfd1e57a77f186634671699e2b9d2d81 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 14:47:34 +0100 Subject: [PATCH 03/14] Execution: Corrected spelling --- wa/framework/execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wa/framework/execution.py b/wa/framework/execution.py index d9fc5132..ca89b800 100644 --- a/wa/framework/execution.py +++ b/wa/framework/execution.py @@ -209,7 +209,7 @@ class Executor(object): """ The ``Executor``'s job is to set up the execution context and pass to a ``Runner`` along with a loaded run specification. Once the ``Runner`` has - done its thing, the ``Executor`` performs some final reporint before + done its thing, the ``Executor`` performs some final reporting before returning. The initial context set up involves combining configuration from various From cceecebfa7cb4b4f298d222a47ed081e906215f1 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Tue, 18 Apr 2017 14:40:26 +0100 Subject: [PATCH 04/14] Getters: Renamed import from `__base_filepath` to `_base_filepath` Previously trying to use the `__base_filepath` import from inside the `Package` class, resulted in the error "`_Package__base_filepath` is not defined." --- wa/framework/getters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wa/framework/getters.py b/wa/framework/getters.py index 651ca5a2..7c9986c6 100644 --- a/wa/framework/getters.py +++ b/wa/framework/getters.py @@ -31,7 +31,7 @@ import requests from devlib.utils.android import ApkInfo -from wa import Parameter, settings, __file__ as __base_filepath +from wa import Parameter, settings, __file__ as _base_filepath from wa.framework.resource import ResourceGetter, SourcePriority, NO_ONE from wa.framework.exception import ResourceError from wa.utils.misc import (ensure_directory_exists as _d, @@ -96,7 +96,7 @@ class Package(ResourceGetter): def get(self, resource): if resource.owner == NO_ONE: - basepath = os.path.join(os.path.dirname(__base_filepath), 'assets') + basepath = os.path.join(os.path.dirname(_base_filepath), 'assets') else: modname = resource.owner.__module__ basepath = os.path.dirname(sys.modules[modname].__file__) From 2406d016729be9cf5c15167151772200622d169b Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 11:39:46 +0100 Subject: [PATCH 05/14] Exec Control: Copied to WA3 and now uses default environment. Moved execution decorators from wlauto to wa. Modified to use a default environment if none is explicitly specified. --- wa/tests/test_exec_control.py | 269 ++++++++++++++++++++++++++++++++++ wa/utils/exec_control.py | 110 ++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 wa/tests/test_exec_control.py create mode 100644 wa/utils/exec_control.py diff --git a/wa/tests/test_exec_control.py b/wa/tests/test_exec_control.py new file mode 100644 index 00000000..490239d2 --- /dev/null +++ b/wa/tests/test_exec_control.py @@ -0,0 +1,269 @@ +# Copyright 2013-2015 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# pylint: disable=W0231,W0613,E0611,W0603,R0201 +from unittest import TestCase + +from nose.tools import assert_equal, assert_raises + +from wlauto.utils.exec_control import (init_environment, reset_environment, + activate_environment, once, + once_per_class, once_per_instance) + +class TestClass(object): + + def __init__(self): + self.count = 0 + + @once + def initilize_once(self): + self.count += 1 + + @once_per_class + def initilize_once_per_class(self): + self.count += 1 + + @once_per_instance + def initilize_once_per_instance(self): + self.count += 1 + + +class SubClass(TestClass): + + def __init__(self): + super(SubClass, self).__init__() + + +class SubSubClass(SubClass): + + def __init__(self): + super(SubSubClass, self).__init__() + + +class AnotherClass(object): + + def __init__(self): + self.count = 0 + + @once + def initilize_once(self): + self.count += 1 + + @once_per_class + def initilize_once_per_class(self): + self.count += 1 + + @once_per_instance + def initilize_once_per_instance(self): + self.count += 1 + + +class EnvironmentManagementTest(TestCase): + + def test_duplicate_environment(self): + init_environment('ENVIRONMENT') + assert_raises(ValueError, init_environment, 'ENVIRONMENT') + + def test_reset_missing_environment(self): + assert_raises(ValueError, reset_environment, 'MISSING') + + def test_reset_current_environment(self): + activate_environment('CURRENT_ENVIRONMENT') + t1 = TestClass() + t1.initilize_once() + assert_equal(t1.count, 1) + + reset_environment() + t1.initilize_once() + assert_equal(t1.count, 2) + + def test_switch_environment(self): + activate_environment('ENVIRONMENT1') + t1 = TestClass() + t1.initilize_once() + assert_equal(t1.count, 1) + + activate_environment('ENVIRONMENT2') + t1.initilize_once() + assert_equal(t1.count, 2) + + activate_environment('ENVIRONMENT1') + t1.initilize_once() + assert_equal(t1.count, 2) + + def test_reset_environment_name(self): + activate_environment('ENVIRONMENT') + t1 = TestClass() + t1.initilize_once() + assert_equal(t1.count, 1) + + reset_environment('ENVIRONMENT') + t1.initilize_once() + assert_equal(t1.count, 2) + + +class OnlyOnceEnvironmentTest(TestCase): + + def setUp(self): + activate_environment('TEST_ENVIRONMENT') + + def tearDown(self): + reset_environment('TEST_ENVIRONMENT') + + def test_single_instance(self): + t1 = TestClass() + ac = AnotherClass() + + t1.initilize_once() + assert_equal(t1.count, 1) + + t1.initilize_once() + assert_equal(t1.count, 1) + + ac.initilize_once() + assert_equal(ac.count, 1) + + + def test_mulitple_instances(self): + t1 = TestClass() + t2 = TestClass() + + t1.initilize_once() + assert_equal(t1.count, 1) + + t2.initilize_once() + assert_equal(t2.count, 0) + + + def test_sub_classes(self): + t1 = TestClass() + sc = SubClass() + ss = SubSubClass() + + t1.initilize_once() + assert_equal(t1.count, 1) + + sc.initilize_once() + sc.initilize_once() + assert_equal(sc.count, 0) + + ss.initilize_once() + ss.initilize_once() + assert_equal(ss.count, 0) + + +class OncePerClassEnvironmentTest(TestCase): + + def setUp(self): + activate_environment('TEST_ENVIRONMENT') + + def tearDown(self): + reset_environment('TEST_ENVIRONMENT') + + def test_single_instance(self): + t1 = TestClass() + ac = AnotherClass() + + t1.initilize_once_per_class() + assert_equal(t1.count, 1) + + t1.initilize_once_per_class() + assert_equal(t1.count, 1) + + ac.initilize_once_per_class() + assert_equal(ac.count, 1) + + + def test_mulitple_instances(self): + t1 = TestClass() + t2 = TestClass() + + t1.initilize_once_per_class() + assert_equal(t1.count, 1) + + t2.initilize_once_per_class() + assert_equal(t2.count, 0) + + + def test_sub_classes(self): + t1 = TestClass() + sc1 = SubClass() + sc2 = SubClass() + ss1 = SubSubClass() + ss2 = SubSubClass() + + t1.initilize_once_per_class() + assert_equal(t1.count, 1) + + sc1.initilize_once_per_class() + sc2.initilize_once_per_class() + assert_equal(sc1.count, 1) + assert_equal(sc2.count, 0) + + ss1.initilize_once_per_class() + ss2.initilize_once_per_class() + assert_equal(ss1.count, 1) + assert_equal(ss2.count, 0) + + +class OncePerInstanceEnvironmentTest(TestCase): + + def setUp(self): + activate_environment('TEST_ENVIRONMENT') + + def tearDown(self): + reset_environment('TEST_ENVIRONMENT') + + def test_single_instance(self): + t1 = TestClass() + ac = AnotherClass() + + t1.initilize_once_per_instance() + assert_equal(t1.count, 1) + + t1.initilize_once_per_instance() + assert_equal(t1.count, 1) + + ac.initilize_once_per_instance() + assert_equal(ac.count, 1) + + + def test_mulitple_instances(self): + t1 = TestClass() + t2 = TestClass() + + t1.initilize_once_per_instance() + assert_equal(t1.count, 1) + + t2.initilize_once_per_instance() + assert_equal(t2.count, 1) + + + def test_sub_classes(self): + t1 = TestClass() + sc = SubClass() + ss = SubSubClass() + + t1.initilize_once_per_instance() + assert_equal(t1.count, 1) + + sc.initilize_once_per_instance() + sc.initilize_once_per_instance() + assert_equal(sc.count, 1) + + ss.initilize_once_per_instance() + ss.initilize_once_per_instance() + assert_equal(ss.count, 1) diff --git a/wa/utils/exec_control.py b/wa/utils/exec_control.py new file mode 100644 index 00000000..29261937 --- /dev/null +++ b/wa/utils/exec_control.py @@ -0,0 +1,110 @@ +from inspect import getmro + +# "environment" management: +__environments = {} +__active_environment = None + + +def activate_environment(name): + """ + Sets the current tracking environment to ``name``. If an + environment with that name does not already exist, it will be + created. + """ + #pylint: disable=W0603 + global __active_environment + + if name not in __environments.keys(): + init_environment(name) + __active_environment = name + +def init_environment(name): + """ + Create a new environment called ``name``, but do not set it as the + current environment. + + :raises: ``ValueError`` if an environment with name ``name`` + already exists. + """ + if name in __environments.keys(): + msg = "Environment {} already exists".format(name) + raise ValueError(msg) + __environments[name] = [] + +def reset_environment(name=None): + """ + Reset method call tracking for environment ``name``. If ``name`` is + not specified or is ``None``, reset the current active environment. + + :raises: ``ValueError`` if an environment with name ``name`` + does not exist. + """ + + if name is not None: + if name not in __environments.keys(): + msg = "Environment {} does not exist".format(name) + raise ValueError(msg) + __environments[name] = [] + else: + if __active_environment is None: + activate_environment('default') + __environments[__active_environment] = [] + +# The decorators: +def once_per_instance(method): + """ + The specified method will be invoked only once for every bound + instance within the environment. + """ + def wrapper(*args, **kwargs): + if __active_environment is None: + activate_environment('default') + func_id = repr(args[0]) + if func_id in __environments[__active_environment]: + return + else: + __environments[__active_environment].append(func_id) + return method(*args, **kwargs) + + return wrapper + +def once_per_class(method): + """ + The specified method will be invoked only once for all instances + of a class within the environment. + """ + def wrapper(*args, **kwargs): + if __active_environment is None: + activate_environment('default') + + func_id = repr(method.func_name) + repr(args[0].__class__) + + if func_id in __environments[__active_environment]: + return + else: + __environments[__active_environment].append(func_id) + return method(*args, **kwargs) + + return wrapper + +def once(method): + """ + The specified method will be invoked only once within the + environment. + """ + def wrapper(*args, **kwargs): + if __active_environment is None: + activate_environment('default') + + func_id = repr(method.func_name) + # Store the least derived class, which isn't object, to account + # for subclasses. + func_id += repr(getmro(args[0].__class__)[-2]) + + if func_id in __environments[__active_environment]: + return + else: + __environments[__active_environment].append(func_id) + return method(*args, **kwargs) + + return wrapper From 7815df59d4323bb648cfdcfb8f35f191b368ba74 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 12:05:23 +0100 Subject: [PATCH 06/14] Getters: Added support for finding revent files. Revent files are automatically placed in the sub folder `revent_files` in the workload directory when recording, therefore when trying to retrieve recordings the getter now looks inside of the sub directory. --- wa/framework/getters.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/wa/framework/getters.py b/wa/framework/getters.py index 7c9986c6..777a2620 100644 --- a/wa/framework/getters.py +++ b/wa/framework/getters.py @@ -80,7 +80,16 @@ def get_from_location(basepath, resource): path = os.path.join(basepath, 'bin', resource.abi, resource.filename) if os.path.exists(path): return path - elif resource.kind in ['apk', 'jar', 'revent']: + elif resource.kind == 'revent': + path = os.path.join(basepath, 'revent_files') + if os.path.exists(path): + files = get_by_extension(path, resource.kind) + found_resource = get_generic_resource(resource, files) + if found_resource: + return found_resource + files = get_by_extension(basepath, resource.kind) + return get_generic_resource(resource, files) + elif resource.kind in ['apk', 'jar']: files = get_by_extension(basepath, resource.kind) return get_generic_resource(resource, files) From 6f0d18f92183b948741abf6375927d0ad0ad06d8 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 14:04:28 +0100 Subject: [PATCH 07/14] Executor: Renamed `device_manage` to `self.target_manager` --- wa/framework/execution.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wa/framework/execution.py b/wa/framework/execution.py index ca89b800..29095a5a 100644 --- a/wa/framework/execution.py +++ b/wa/framework/execution.py @@ -225,7 +225,7 @@ class Executor(object): self.error_logged = False self.warning_logged = False pluginloader = None - self.device_manager = None + self.target_manager = None self.device = None def execute(self, config_manager, output): @@ -249,12 +249,12 @@ class Executor(object): output.write_config(config) self.logger.info('Connecting to target') - target_manager = TargetManager(config.run_config.device, + self.target_manager = TargetManager(config.run_config.device, config.run_config.device_config) - output.write_target_info(target_manager.get_target_info()) + output.write_target_info(self.target_manager.get_target_info()) self.logger.info('Initializing execution conetext') - context = ExecutionContext(config_manager, target_manager, output) + context = ExecutionContext(config_manager, self.target_manager, output) self.logger.info('Generating jobs') config_manager.generate_jobs(context) @@ -262,7 +262,7 @@ class Executor(object): output.write_state() self.logger.info('Installing instrumentation') - for instrument in config_manager.get_instruments(target_manager.target): + for instrument in config_manager.get_instruments(self.target_manager.target): instrumentation.install(instrument) instrumentation.validate() From 9308855f14d6289c682c782485e986ae0ce2a71b Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 14:18:25 +0100 Subject: [PATCH 08/14] TargetDescriptor: Now adds parameters with default values. Previously if a parameter was not specified via config it would not be used during initialisation even if the parameter had a default value. Now any parameters with default values are populated as necessary. --- wa/framework/target/descriptor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wa/framework/target/descriptor.py b/wa/framework/target/descriptor.py index 9e01ebac..8515556f 100644 --- a/wa/framework/target/descriptor.py +++ b/wa/framework/target/descriptor.py @@ -33,6 +33,11 @@ def instantiate_target(tdesc, params, connect=None): tp, pp, cp = {}, {}, {} + for supported_params, new_params in (target_params, tp), (platform_params, pp), (conn_params, cp): + for name, value in supported_params.iteritems(): + if value.default: + new_params[name] = value.default + for name, value in params.iteritems(): if name in target_params: tp[name] = value @@ -62,6 +67,8 @@ def instantiate_assistant(tdesc, params, target): for param in tdesc.assistant_params: if param.name in params: assistant_params[param.name] = params[param.name] + elif param.default: + assistant_params[param.name] = param.default return tdesc.assistant(target, **assistant_params) From adaa83b6ebcb7dfb421a9c2efdd269bb0fee4837 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 17:01:01 +0100 Subject: [PATCH 09/14] ReventUtils: Added ReventRecorder Added `ReventRecorder` which is used to deal with the revent binary on the device including deloyment, running commands and cleaning up again. --- wa/utils/revent.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/wa/utils/revent.py b/wa/utils/revent.py index b7de2750..3d192881 100644 --- a/wa/utils/revent.py +++ b/wa/utils/revent.py @@ -16,9 +16,11 @@ from __future__ import division import os import struct +import signal from datetime import datetime from collections import namedtuple +from wa.framework.resource import Executable, NO_ONE, ResourceResolver GENERAL_MODE = 0 GAMEPAD_MODE = 1 @@ -249,3 +251,38 @@ class ReventRecording(object): def __del__(self): self.close() + + +def get_revent_binary(abi): + resolver = ResourceResolver() + resolver.load() + resource = Executable(NO_ONE, abi, 'revent') + return resolver.get(resource) + + +class ReventRecorder(object): + + def __init__(self, target): + self.target = target + self.executable = self.target.get_installed('revent') + + def deploy(self): + if not self.executable: + host_executable = get_revent_binary(self.target.abi) + self.executable = self.target.install(host_executable) + + def remove(self): + if self.executable: + self.target.uninstall('revent') + + def start_record(self, revent_file): + command = '{} record -s {}'.format(self.executable, revent_file) + self.target.kick_off(command, self.target.is_rooted) + + def stop_record(self): + self.target.killall('revent', signal.SIGINT, as_root=self.target.is_rooted) + + def replay(self, revent_file, timeout=None): + self.target.killall('revent') + command = "{} replay {}".format(self.executable, revent_file) + self.target.execute(command, timeout=timeout) From 4b84a68038e3bc517f18ade69f5b97d9657afe37 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 16:24:43 +0100 Subject: [PATCH 10/14] Workload: Added ReventWorkload and updated ReventGUI Added a new ReventWorkload as a base for revent based workloads. Updated ReventGui to use `revent_recorder` and now uses `target.model` instead of `taget.name`. --- wa/__init__.py | 2 +- wa/framework/workload.py | 155 ++++++++++++++++++++++++++++----------- 2 files changed, 114 insertions(+), 43 deletions(-) diff --git a/wa/__init__.py b/wa/__init__.py index d005ef27..0eb95c57 100644 --- a/wa/__init__.py +++ b/wa/__init__.py @@ -14,4 +14,4 @@ from wa.framework.plugin import Plugin, Parameter from wa.framework.processor import ResultProcessor from wa.framework.resource import (NO_ONE, JarFile, ApkFile, ReventFile, File, Executable) -from wa.framework.workload import Workload, ApkUiautoWorkload +from wa.framework.workload import Workload, ApkUiautoWorkload, ReventWorkload diff --git a/wa/framework/workload.py b/wa/framework/workload.py index d03cdddd..4b9302a7 100644 --- a/wa/framework/workload.py +++ b/wa/framework/workload.py @@ -18,8 +18,10 @@ import time from wa.framework.plugin import TargetedPlugin from wa.framework.resource import (ApkFile, JarFile, ReventFile, NO_ONE, - Executable) + Executable, File) from wa.framework.exception import WorkloadError +from wa.utils.revent import ReventRecorder +from wa.utils.exec_control import once from devlib.utils.android import ApkInfo @@ -94,25 +96,29 @@ class Workload(TargetedPlugin): return ''.format(self.name) -class ApkUiautoWorkload(Workload): - - platform = 'android' +class ApkUIWorkload(Workload): + + # May be optionally overwritten by subclasses + # Times are in seconds + loading_time = 10 def __init__(self, target, **kwargs): - super(ApkUiautoWorkload, self).__init__(target, **kwargs) - self.apk = ApkHander(self) - self.gui = UiAutomatorGUI(self) + super(ApkUIWorkload, self).__init__(target, **kwargs) + self.apk = None + self.gui = None def init_resources(self, context): self.apk.init_resources(context.resolver) self.gui.init_resources(context.resolver) self.gui.init_commands() + @once def initialize(self, context): self.gui.deploy() def setup(self, context): self.apk.setup(context) + time.sleep(self.loading_time) self.gui.setup() def run(self, context): @@ -125,10 +131,40 @@ class ApkUiautoWorkload(Workload): self.gui.teardown() self.apk.teardown() + @once def finalize(self, context): self.gui.remove() +class ApkUiautoWorkload(ApkUIWorkload): + + platform = 'android' + + def __init__(self, target, **kwargs): + super(ApkUiautoWorkload, self).__init__(target, **kwargs) + self.apk = ApkHander(self) + self.gui = UiAutomatorGUI(self) + + +class ReventWorkload(ApkUIWorkload): + + # May be optionally overwritten by subclasses + # Times are in seconds + setup_timeout = 5 * 60 + run_timeout = 10 * 60 + extract_results_timeout = 5 * 60 + teardown_timeout = 5 * 60 + + def __init__(self, target, **kwargs): + super(ReventWorkload, self).__init__(target, **kwargs) + self.apk = ApkHander(self) + self.gui = ReventGUI(self, target, + self.setup_timeout, + self.run_timeout, + self.extract_results_timeout, + self.teardown_timeout) + + class UiAutomatorGUI(object): stages = ['setup', 'runWorkload', 'extractResults', 'teardown'] @@ -209,57 +245,92 @@ class UiAutomatorGUI(object): class ReventGUI(object): - def __init__(self, workload, target, setup_timeout=5 * 60, run_timeout=10 * 60): + def __init__(self, workload, target, setup_timeout, run_timeout, + extract_results_timeout, teardown_timeout): self.workload = workload self.target = target self.setup_timeout = setup_timeout self.run_timeout = run_timeout + self.extract_results_timeout = extract_results_timeout + self.teardown_timeout = teardown_timeout + self.revent_recorder = ReventRecorder(self.target) self.on_target_revent_binary = self.target.get_workpath('revent') - self.on_target_setup_revent = self.target.get_workpath('{}.setup.revent'.format(self.target.name)) - self.on_target_run_revent = self.target.get_workpath('{}.run.revent'.format(self.target.name)) + self.on_target_setup_revent = self.target.get_workpath('{}.setup.revent'.format(self.target.model)) + self.on_target_run_revent = self.target.get_workpath('{}.run.revent'.format(self.target.model)) + self.on_target_extract_results_revent = self.target.get_workpath('{}.extract_results.revent'.format(self.target.model)) + self.on_target_teardown_revent = self.target.get_workpath('{}.teardown.revent'.format(self.target.model)) self.logger = logging.getLogger('revent') self.revent_setup_file = None self.revent_run_file = None + self.revent_extract_results_file = None + self.revent_teardown_file = None - def init_resources(self, context): - self.revent_setup_file = context.resolver.get(ReventFile(self.workload, 'setup')) - self.revent_run_file = context.resolver.get(ReventFile(self.workload, 'run')) + def init_resources(self, resolver): + self.revent_setup_file = resolver.get(ReventFile(owner=self.workload, + stage='setup', + target=self.target.model), + strict=False) + self.revent_run_file = resolver.get(ReventFile(owner=self.workload, + stage='run', + target=self.target.model)) + self.revent_extract_results_file = resolver.get(ReventFile(owner=self.workload, + stage='extract_results', + target=self.target.model), + strict=False) + self.revent_teardown_file = resolver.get(resource=ReventFile(owner=self.workload, + stage='teardown', + target=self.target.model), + strict=False) - def setup(self, context): - self._check_revent_files(context) - self.target.killall('revent') - command = '{} replay {}'.format(self.on_target_revent_binary, self.on_target_setup_revent) - self.target.execute(command, timeout=self.setup_timeout) + def deploy(self): + self.revent_recorder.deploy() - def run(self, context): - command = '{} replay {}'.format(self.on_target_revent_binary, self.on_target_run_revent) - self.logger.debug('Replaying {}'.format(os.path.basename(self.on_target_run_revent))) - self.target.execute(command, timeout=self.run_timeout) + def setup(self): + self._check_revent_files() + self.revent_recorder.replay(self.on_target_setup_revent, + timeout=self.setup_timeout) + + def run(self): + msg = 'Replaying {}' + self.logger.debug(msg.format(os.path.basename(self.on_target_run_revent))) + self.revent_recorder.replay(self.on_target_run_revent, + timeout=self.run_timeout) self.logger.debug('Replay completed.') - def teardown(self, context): + def extract_results(self): + if self.revent_extract_results_file: + self.revent_recorder.replay(self.on_target_extract_results_revent, + timeout=self.extract_results_timeout) + + def teardown(self): + if self.revent_teardown_file: + self.revent_recorder.replay(self.on_target_teardown_revent, + timeout=self.teardown_timeout) self.target.remove(self.on_target_setup_revent) self.target.remove(self.on_target_run_revent) + self.target.remove(self.on_target_extract_results_revent) + self.target.remove(self.on_target_teardown_revent) - def _check_revent_files(self, context): - # check the revent binary - revent_binary = context.resolver.get(Executable(NO_ONE, self.target.abi, 'revent')) - if not os.path.isfile(revent_binary): - message = '{} does not exist. '.format(revent_binary) - message += 'Please build revent for your system and place it in that location' - raise WorkloadError(message) - if not self.revent_setup_file: - # pylint: disable=too-few-format-args - message = '{0}.setup.revent file does not exist, Please provide one for your target, {0}' - raise WorkloadError(message.format(self.target.name)) + def remove(self): + self.revent_recorder.remove() + + def init_commands(self): + pass + + def _check_revent_files(self): if not self.revent_run_file: # pylint: disable=too-few-format-args - message = '{0}.run.revent file does not exist, Please provide one for your target, {0}' - raise WorkloadError(message.format(self.target.name)) + message = '{0}.run.revent file does not exist, ' \ + 'Please provide one for your target, {0}' + raise WorkloadError(message.format(self.target.model)) - self.on_target_revent_binary = self.target.install(revent_binary) self.target.push(self.revent_run_file, self.on_target_run_revent) - self.target.push(self.revent_setup_file, self.on_target_setup_revent) + if self.revent_setup_file: + self.target.push(self.revent_setup_file, self.on_target_setup_revent) + if self.revent_extract_results_file: + self.target.push(self.revent_extract_results_file, self.on_target_extract_results_revent) + if self.revent_teardown_file: + self.target.push(self.revent_teardown_file, self.on_target_teardown_revent) class ApkHander(object): @@ -281,10 +352,10 @@ class ApkHander(object): self.logcat_log = None def init_resources(self, resolver): - self.apk_file = resolver.get(ApkFile(self.owner, + self.apk_file = resolver.get(ApkFile(self.owner, variant=self.variant, - version=self.version), - strict=self.strict) + version=self.version), + strict=self.strict) self.apk_info = ApkInfo(self.apk_file) def setup(self, context): @@ -333,7 +404,7 @@ class ApkHander(object): def start_activity(self): cmd = 'am start -W -n {}/{}' - output = self.target.execute(cmd.format(self.apk_info.package, + output = self.target.execute(cmd.format(self.apk_info.package, self.apk_info.activity)) if 'Error:' in output: # this will dismiss any error dialogs From 3ad0c67c634ff0381783a2a2f9992760964bd320 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 14:08:13 +0100 Subject: [PATCH 11/14] AngrybirdsRio: Added revent workload --- wa/workloads/angrybirds_rio/__init__.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 wa/workloads/angrybirds_rio/__init__.py diff --git a/wa/workloads/angrybirds_rio/__init__.py b/wa/workloads/angrybirds_rio/__init__.py new file mode 100644 index 00000000..b56c66b3 --- /dev/null +++ b/wa/workloads/angrybirds_rio/__init__.py @@ -0,0 +1,27 @@ +# Copyright 2013-2015 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +from wa import ReventWorkload + + +class AngryBirdsRio(ReventWorkload): + + name = 'angrybirds_rio' + description = """ + Angry Birds Rio game. + + The sequel to the very popular Android 2D game. + """ From 311ac1b80372fef1db22eb80f1281b764f2d6f1d Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 17:33:58 +0100 Subject: [PATCH 12/14] Record Command: Updated record command Updated the record command to allow revent recordings to be made. They can be performed in 3 ways; the default is a standard recording as in WA2, it can start a specified package before starting the recording or a workload can be specified which launches the workload and prompts the user to record each stage, naming and storing the recordings appropriately. --- wa/commands/record.py | 188 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 155 insertions(+), 33 deletions(-) diff --git a/wa/commands/record.py b/wa/commands/record.py index be72cd09..830dee5d 100644 --- a/wa/commands/record.py +++ b/wa/commands/record.py @@ -15,23 +15,24 @@ import os import sys +from time import sleep - -from wa import Command, settings +from wa import Command from wa.framework import pluginloader -from wa.framework.agenda import Agenda -from wa.framework.resource import Executable, NO_ONE, ResourceResolver -from wa.framework.configuration import RunConfiguration -from wa.framework.workload import ApkUiautoWorkload +from wa.framework.resource import ResourceResolver +from wa.framework.target.manager import TargetManager +from wa.utils.revent import ReventRecorder class RecordCommand(Command): name = 'record' - description = '''Performs a revent recording + description = ''' + Performs a revent recording This command helps making revent recordings. It will automatically - deploy revent and even has the option of automatically opening apps. + deploy revent and has options to automatically open apps and record + specified stages of a workload. Revent allows you to record raw inputs such as screen swipes or button presses. This can be useful for recording inputs for workloads such as games that don't @@ -45,10 +46,18 @@ class RecordCommand(Command): it can be automatically determined. On Android device it will be obtained from ``build.prop``, on Linux devices it is obtained from ``/proc/device-tree/model``. - suffix is used by WA to determine which part of the app execution the - recording is for, currently these are either ``setup`` or ``run``. This - should be specified with the ``-s`` argument. + recording is for, currently these are either ``setup``, ``run``, ``extract_results`` + or ``teardown``. All stages except ``run`` are optional and these should + be specified with the ``-s``, ``-e`` or ``-t`` arguments respectively, + or optionally ``-a`` to indicate all stages should be recorded. ''' + def __init__(self, **kwargs): + super(RecordCommand, self).__init__(**kwargs) + self.tm = None + self.target = None + self.revent_recorder = None + def initialize(self, context): self.parser.add_argument('-d', '--device', metavar='DEVICE', help=''' @@ -56,39 +65,152 @@ class RecordCommand(Command): take precedence over the device (if any) specified in configuration. ''') + self.parser.add_argument('-o', '--output', help='Specify the output file', metavar='FILE') + self.parser.add_argument('-s', '--setup', help='Record a recording for setup stage', + action='store_true') + self.parser.add_argument('-e', '--extract_results', help='Record a recording for extract_results stage', + action='store_true') + self.parser.add_argument('-t', '--teardown', help='Record a recording for teardown stage', + action='store_true') + self.parser.add_argument('-a', '--all', help='Record recordings for available stages', + action='store_true') - def execute(state, args): + # Need validation + self.parser.add_argument('-C', '--clear', help='Clear app cache before launching it', + action='store_true') + group = self.parser.add_mutually_exclusive_group(required=False) + group.add_argument('-p', '--package', help='Package to launch before recording') + group.add_argument('-w', '--workload', help='Name of a revent workload (mostly games)') + + def validate_args(self, args): + if args.clear and not (args.package or args.workload): + self.logger.error("Package/Workload must be specified if you want to clear cache") + sys.exit() + if args.workload and args.output: + self.logger.error("Output file cannot be specified with Workload") + sys.exit() + if not args.workload and (args.setup or args.extract_results or + args.teardown or args.all): + self.logger.error("Cannot specify a recording stage without a Workload") + sys.exit() + + def execute(self, state, args): + self.validate_args(args) if args.device: - device = args.device + device = args.device device_config = {} else: device = state.run_config.device - device_config = state.run_config.device_config - target_manager = TargetManager(device, device_config) + device_config = state.run_config.device_config or {} + self.tm = TargetManager(device, device_config) + self.target = self.tm.target + self.revent_recorder = ReventRecorder(self.target) + self.revent_recorder.deploy() -def get_revent_binary(abi): - resolver = ResourceResolver() - resource = Executable(NO_ONE, abi, 'revent') - return resolver.get(resource) + if args.workload: + self.workload_record(args) + elif args.package: + self.package_record(args) + else: + self.manual_record(args) + self.revent_recorder.remove() -class ReventRecorder(object): + def record(self, revent_file, name, output_path): + msg = 'Press Enter when you are ready to record {}...' + self.logger.info(msg.format(name)) + raw_input('') + self.revent_recorder.start_record(revent_file) + msg = 'Press Enter when you have finished recording {}...' + self.logger.info(msg.format(name)) + raw_input('') + self.revent_recorder.stop_record() - def __init__(self, target): - self.target = target - self.executable = None - self.deploy() + if not os.path.isdir(output_path): + os.makedirs(output_path) - def deploy(self): - host_executable = get_revent_binary(self.target.abi) - self.executable = self.target.install(host_executable) + revent_file_name = self.target.path.basename(revent_file) + host_path = os.path.join(output_path, revent_file_name) + if os.path.exists(host_path): + msg = 'Revent file \'{}\' already exists, overwrite? [y/n]' + self.logger.info(msg.format(revent_file_name)) + if raw_input('') == 'y': + os.remove(host_path) + else: + msg = 'Did not pull and overwrite \'{}\'' + self.logger.warning(msg.format(revent_file_name)) + return + msg = 'Pulling \'{}\' from device' + self.logger.info(msg.format(self.target.path.basename(revent_file))) + self.target.pull(revent_file, output_path, as_root=self.target.is_rooted) - def record(self, path): - name = os.path.basename(path) - target_path = self.target.get_workpath(name) - command = '{} record {}' + def manual_record(self, args): + output_path, file_name = self._split_revent_location(args.output) + revent_file = self.target.get_workpath(file_name) + self.record(revent_file, '', output_path) + msg = 'Recording is available at: \'{}\'' + self.logger.info(msg.format(os.path.join(output_path, file_name))) - def remove(self): - if self.executable: - self.target.uninstall('revent') + def package_record(self, args): + if args.clear: + self.target.execute('pm clear {}'.format(args.package)) + self.logger.info('Starting {}'.format(args.package)) + cmd = 'monkey -p {} -c android.intent.category.LAUNCHER 1' + self.target.execute(cmd.format(args.package)) + + output_path, file_name = self._split_revent_location(args.output) + revent_file = self.target.get_workpath(file_name) + self.record(revent_file, '', output_path) + msg = 'Recording is available at: \'{}\'' + self.logger.info(msg.format(os.path.join(output_path, file_name))) + + def workload_record(self, args): + context = LightContext(self.tm) + setup_revent = '{}.setup.revent'.format(self.target.model) + run_revent = '{}.run.revent'.format(self.target.model) + extract_results_revent = '{}.extract_results.revent'.format(self.target.model) + teardown_file_revent = '{}.teardown.revent'.format(self.target.model) + setup_file = self.target.get_workpath(setup_revent) + run_file = self.target.get_workpath(run_revent) + extract_results_file = self.target.get_workpath(extract_results_revent) + teardown_file = self.target.get_workpath(teardown_file_revent) + + self.logger.info('Deploying {}'.format(args.workload)) + workload = pluginloader.get_workload(args.workload, self.target) + workload.apk.init_resources(context.resolver) + workload.apk.setup(context) + sleep(workload.loading_time) + + output_path = os.path.join(workload.dependencies_directory, + 'revent_files') + if args.setup or args.all: + self.record(setup_file, 'SETUP', output_path) + self.record(run_file, 'RUN', output_path) + if args.extract_results or args.all: + self.record(extract_results_file, 'EXTRACT_RESULTS', output_path) + if args.teardown or args.all: + self.record(teardown_file, 'TEARDOWN', output_path) + self.logger.info('Tearing down {}'.format(args.workload)) + workload.teardown(context) + self.logger.info('Recording(s) are available at: \'{}\''.format(output_path)) + + def _split_revent_location(self, output): + output_path = None + file_name = None + if output: + output_path, file_name, = os.path.split(output) + + if not file_name: + file_name = '{}.revent'.format(self.target.model) + if not output_path: + output_path = os.getcwdu() + + return output_path, file_name + +# Used to satisfy the workload API +class LightContext(object): + def __init__(self, tm): + self.tm = tm + self.resolver = ResourceResolver() + self.resolver.load() From 16f2bc69f0bfcc36edd17b2c8f084a5aaefecaf6 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Wed, 12 Apr 2017 17:40:24 +0100 Subject: [PATCH 13/14] Replay Command: Added a replay command The replay command can be used to replay an revent recording on a device. --- wa/commands/record.py | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/wa/commands/record.py b/wa/commands/record.py index 830dee5d..44639794 100644 --- a/wa/commands/record.py +++ b/wa/commands/record.py @@ -208,6 +208,58 @@ class RecordCommand(Command): return output_path, file_name +class ReplayCommand(Command): + + name = 'replay' + description = ''' + Replay a revent recording + + Revent allows you to record raw inputs such as screen swipes or button presses. + See ``wa show record`` to see how to make an revent recording. + ''' + + def initialize(self, context): + self.parser.add_argument('recording', help='The name of the file to replay', + metavar='FILE') + self.parser.add_argument('-d', '--device', help='The name of the device') + self.parser.add_argument('-p', '--package', help='Package to launch before recording') + self.parser.add_argument('-C', '--clear', help='Clear app cache before launching it', + action="store_true") + + # pylint: disable=W0201 + def execute(self, state, args): + if args.device: + device = args.device + device_config = {} + else: + device = state.run_config.device + device_config = state.run_config.device_config or {} + + target_manager = TargetManager(device, device_config) + self.target = target_manager.target + revent_file = self.target.path.join(self.target.working_directory, + os.path.split(args.revent)[1]) + + self.logger.info("Pushing file to target") + self.target.push(args.revent, self.target.working_directory) + + revent_recorder = ReventRecorder(target_manager.target) + revent_recorder.deploy() + + if args.clear: + self.target.execute('pm clear {}'.format(args.package)) + + if args.package: + self.logger.info('Starting {}'.format(args.package)) + cmd = 'monkey -p {} -c android.intent.category.LAUNCHER 1' + self.target.execute(cmd.format(args.package)) + + self.logger.info("Starting replay") + revent_recorder.replay(revent_file) + self.logger.info("Finished replay") + revent_recorder.remove() + + # Used to satisfy the workload API class LightContext(object): def __init__(self, tm): From 16a0e84469ae2e5785a11cc7f6f65a5350d7abe1 Mon Sep 17 00:00:00 2001 From: Marc Bonnici Date: Tue, 18 Apr 2017 12:00:00 +0100 Subject: [PATCH 14/14] Runner: Now finalizes all completed workloads at the end of a run. Previously when finalizing a run the workloads themselves were not included. This ensures that each completed workloads finalize method is called. --- wa/framework/execution.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wa/framework/execution.py b/wa/framework/execution.py index 29095a5a..9d4eb8f9 100644 --- a/wa/framework/execution.py +++ b/wa/framework/execution.py @@ -361,6 +361,10 @@ class Runner(object): self.pm.process_run_output(self.context) self.pm.export_run_output(self.context) self.pm.finalize() + log.indent() + for job in self.context.completed_jobs: + job.finalize(self.context) + log.dedent() def run_next_job(self, context): job = context.start_job()