startop: Refactor app running.
Collecting and app running share lots of common code. Create a new class to run app and allow callbacks to specify preprocess and postprocess and metrics selection. Test: pytest run_app_with_prefetch_test.py Test: pytest app_runner_test.py Bug: 138233615 Change-Id: I972c82fb9ff3a0f6cc7661bc3dc47b342716c26c
This commit is contained in:
@@ -33,12 +33,12 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Any, Callable, Iterable, List, NamedTuple, TextIO, Tuple, \
|
||||
TypeVar, Union
|
||||
TypeVar, Union, Optional
|
||||
|
||||
# local import
|
||||
DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
sys.path.append(os.path.dirname(DIR))
|
||||
import app_startup.run_app_with_prefetch as run_app_with_prefetch
|
||||
from app_startup.run_app_with_prefetch import PrefetchAppRunner
|
||||
import app_startup.lib.args_utils as args_utils
|
||||
from app_startup.lib.data_frame import DataFrame
|
||||
import lib.cmd_utils as cmd_utils
|
||||
@@ -62,6 +62,16 @@ _COLLECTOR_TIMEOUT_MULTIPLIER = 10 # take the regular --timeout and multiply
|
||||
_UNLOCK_SCREEN_SCRIPT = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'unlock_screen')
|
||||
|
||||
RunCommandArgs = NamedTuple('RunCommandArgs',
|
||||
[('package', str),
|
||||
('readahead', str),
|
||||
('activity', Optional[str]),
|
||||
('compiler_filter', Optional[str]),
|
||||
('timeout', Optional[int]),
|
||||
('debug', bool),
|
||||
('simulate', bool),
|
||||
('input', Optional[str])])
|
||||
|
||||
# This must be the only mutable global variable. All other global variables are constants to avoid magic literals.
|
||||
_debug = False # See -d/--debug flag.
|
||||
_DEBUG_FORCE = None # Ignore -d/--debug if this is not none.
|
||||
@@ -207,8 +217,7 @@ def parse_run_script_csv_file(csv_file: TextIO) -> DataFrame:
|
||||
return DataFrame(d)
|
||||
|
||||
def execute_run_combos(
|
||||
grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[
|
||||
run_app_with_prefetch.RunCommandArgs]]],
|
||||
grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[RunCommandArgs]]],
|
||||
simulate: bool,
|
||||
inodes_path: str,
|
||||
timeout: int):
|
||||
@@ -229,7 +238,7 @@ def execute_run_combos(
|
||||
combos = combos._replace(input=collector_tmp_output_file.name)
|
||||
|
||||
print_utils.debug_print(combos)
|
||||
output = run_app_with_prefetch.run_test(combos)
|
||||
output = PrefetchAppRunner(**combos._asdict()).run()
|
||||
|
||||
yield DataFrame(dict((x, [y]) for x, y in output)) if output else None
|
||||
|
||||
@@ -307,7 +316,7 @@ def main():
|
||||
output_file = opts.output and open(opts.output, 'w') or sys.stdout
|
||||
|
||||
combos = lambda: args_utils.generate_run_combinations(
|
||||
run_app_with_prefetch.RunCommandArgs,
|
||||
RunCommandArgs,
|
||||
coerce_to_list(vars(opts)),
|
||||
opts.loop_count)
|
||||
print_utils.debug_print_gen("run combinations: ", combos())
|
||||
|
||||
265
startop/scripts/app_startup/lib/app_runner.py
Normal file
265
startop/scripts/app_startup/lib/app_runner.py
Normal file
@@ -0,0 +1,265 @@
|
||||
# Copyright 2019, The Android Open Source Project
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Class to run an app."""
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
# local import
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)))))
|
||||
|
||||
import app_startup.lib.adb_utils as adb_utils
|
||||
import lib.cmd_utils as cmd_utils
|
||||
import lib.print_utils as print_utils
|
||||
|
||||
class AppRunnerListener(object):
|
||||
"""Interface for lisenter of AppRunner. """
|
||||
|
||||
def preprocess(self) -> None:
|
||||
"""Preprocess callback to initialized before the app is running. """
|
||||
pass
|
||||
|
||||
def postprocess(self, pre_launch_timestamp: str) -> None:
|
||||
"""Postprocess callback to cleanup after the app is running.
|
||||
|
||||
param:
|
||||
'pre_launch_timestamp': indicates the timestamp when the app is
|
||||
launching.. """
|
||||
pass
|
||||
|
||||
def metrics_selector(self, am_start_output: str,
|
||||
pre_launch_timestamp: str) -> None:
|
||||
"""A metrics selection callback that waits for the desired metrics to
|
||||
show up in logcat.
|
||||
params:
|
||||
'am_start_output': indicates the output of app startup.
|
||||
'pre_launch_timestamp': indicates the timestamp when the app is
|
||||
launching.
|
||||
returns:
|
||||
a string in the format of "<metric>=<value>\n<metric>=<value>\n..."
|
||||
for further parsing. For example "TotalTime=123\nDisplayedTime=121".
|
||||
"""
|
||||
pass
|
||||
|
||||
class AppRunner(object):
|
||||
""" Class to run an app. """
|
||||
# static variables
|
||||
DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
APP_STARTUP_DIR = os.path.dirname(DIR)
|
||||
IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
|
||||
'../../iorap/common'))
|
||||
DEFAULT_TIMEOUT = 30
|
||||
|
||||
def __init__(self,
|
||||
package: str,
|
||||
activity: Optional[str],
|
||||
compiler_filter: Optional[str],
|
||||
timeout: Optional[int],
|
||||
simulate: bool):
|
||||
self.package = package
|
||||
self.simulate = simulate
|
||||
|
||||
# If the argument activity is None, try to set it.
|
||||
self.activity = activity
|
||||
if self.simulate:
|
||||
self.activity = 'act'
|
||||
if self.activity is None:
|
||||
self.activity = AppRunner.get_activity(self.package)
|
||||
|
||||
self.compiler_filter = compiler_filter
|
||||
self.timeout = timeout if timeout else AppRunner.DEFAULT_TIMEOUT
|
||||
|
||||
self.listeners = []
|
||||
|
||||
def add_callbacks(self, listener: AppRunnerListener):
|
||||
self.listeners.append(listener)
|
||||
|
||||
def remove_callbacks(self, listener: AppRunnerListener):
|
||||
self.listeners.remove(listener)
|
||||
|
||||
@staticmethod
|
||||
def get_activity(package: str) -> str:
|
||||
""" Tries to set the activity based on the package. """
|
||||
passed, activity = cmd_utils.run_shell_func(
|
||||
AppRunner.IORAP_COMMON_BASH_SCRIPT,
|
||||
'get_activity_name',
|
||||
[package])
|
||||
|
||||
if not passed or not activity:
|
||||
raise ValueError(
|
||||
'Activity name could not be found, invalid package name?!')
|
||||
|
||||
return activity
|
||||
|
||||
def configure_compiler_filter(self) -> bool:
|
||||
"""Configures compiler filter (e.g. speed).
|
||||
|
||||
Returns:
|
||||
A bool indicates whether configure of compiler filer succeeds or not.
|
||||
"""
|
||||
if not self.compiler_filter:
|
||||
print_utils.debug_print('No --compiler-filter specified, don\'t'
|
||||
' need to force it.')
|
||||
return True
|
||||
|
||||
passed, current_compiler_filter_info = \
|
||||
cmd_utils.run_shell_command(
|
||||
'{} --package {}'.format(os.path.join(AppRunner.APP_STARTUP_DIR,
|
||||
'query_compiler_filter.py'),
|
||||
self.package))
|
||||
|
||||
if passed != 0:
|
||||
return passed
|
||||
|
||||
# TODO: call query_compiler_filter directly as a python function instead of
|
||||
# these shell calls.
|
||||
current_compiler_filter, current_reason, current_isa = \
|
||||
current_compiler_filter_info.split(' ')
|
||||
print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format(
|
||||
current_compiler_filter, current_reason, current_isa))
|
||||
|
||||
# Don't trust reasons that aren't 'unknown' because that means
|
||||
# we didn't manually force the compilation filter.
|
||||
# (e.g. if any automatic system-triggered compilations are not unknown).
|
||||
if current_reason != 'unknown' or \
|
||||
current_compiler_filter != self.compiler_filter:
|
||||
passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter '
|
||||
'--compiler-filter "{}" '
|
||||
'--package "{}"'
|
||||
' --activity "{}'.
|
||||
format(AppRunner.APP_STARTUP_DIR,
|
||||
self.compiler_filter,
|
||||
self.package,
|
||||
self.activity))
|
||||
else:
|
||||
adb_utils.debug_print('Queried compiler-filter matched requested '
|
||||
'compiler-filter, skip forcing.')
|
||||
passed = False
|
||||
return passed
|
||||
|
||||
def run(self) -> Optional[List[Tuple[str]]]:
|
||||
"""Runs an app.
|
||||
|
||||
Returns:
|
||||
A list of (metric, value) tuples.
|
||||
"""
|
||||
print_utils.debug_print('==========================================')
|
||||
print_utils.debug_print('===== START =====')
|
||||
print_utils.debug_print('==========================================')
|
||||
# Run the preprocess.
|
||||
for listener in self.listeners:
|
||||
listener.preprocess()
|
||||
|
||||
# Ensure the APK is currently compiled with whatever we passed in
|
||||
# via --compiler-filter.
|
||||
# No-op if this option was not passed in.
|
||||
if not self.configure_compiler_filter():
|
||||
print_utils.error_print('Compiler filter configuration failed!')
|
||||
return None
|
||||
|
||||
pre_launch_timestamp = adb_utils.logcat_save_timestamp()
|
||||
# Launch the app.
|
||||
results = self.launch_app(pre_launch_timestamp)
|
||||
|
||||
# Run the postprocess.
|
||||
for listener in self.listeners:
|
||||
listener.postprocess(pre_launch_timestamp)
|
||||
|
||||
return results
|
||||
|
||||
def launch_app(self, pre_launch_timestamp: str) -> Optional[List[Tuple[str]]]:
|
||||
""" Launches the app.
|
||||
|
||||
Returns:
|
||||
A list of (metric, value) tuples.
|
||||
"""
|
||||
print_utils.debug_print('Running with timeout {}'.format(self.timeout))
|
||||
|
||||
passed, am_start_output = cmd_utils.run_shell_command('timeout {timeout} '
|
||||
'"{DIR}/launch_application" '
|
||||
'"{package}" '
|
||||
'"{activity}"'.
|
||||
format(timeout=self.timeout,
|
||||
DIR=AppRunner.APP_STARTUP_DIR,
|
||||
package=self.package,
|
||||
activity=self.activity))
|
||||
if not passed and not self.simulate:
|
||||
return None
|
||||
|
||||
return self.wait_for_app_finish(pre_launch_timestamp, am_start_output)
|
||||
|
||||
def wait_for_app_finish(self,
|
||||
pre_launch_timestamp: str,
|
||||
am_start_output: str) -> Optional[List[Tuple[str]]]:
|
||||
""" Wait for app finish and all metrics are shown in logcat.
|
||||
|
||||
Returns:
|
||||
A list of (metric, value) tuples.
|
||||
"""
|
||||
if self.simulate:
|
||||
return [('TotalTime', '123')]
|
||||
|
||||
ret = []
|
||||
for listener in self.listeners:
|
||||
output = listener.metrics_selector(am_start_output,
|
||||
pre_launch_timestamp)
|
||||
ret = ret + AppRunner.parse_metrics_output(output)
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def parse_metrics_output(input: str) -> List[
|
||||
Tuple[str, str, str]]:
|
||||
"""Parses output of app startup to metrics and corresponding values.
|
||||
|
||||
It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]'
|
||||
|
||||
Returns:
|
||||
A list of tuples that including metric name, metric value and rest info.
|
||||
"""
|
||||
all_metrics = []
|
||||
for line in input.split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
splits = line.split('=')
|
||||
if len(splits) < 2:
|
||||
print_utils.error_print('Bad line "{}"'.format(line))
|
||||
continue
|
||||
metric_name = splits[0]
|
||||
metric_value = splits[1]
|
||||
rest = splits[2] if len(splits) > 2 else ''
|
||||
if rest:
|
||||
print_utils.error_print('Corrupt line "{}"'.format(line))
|
||||
print_utils.debug_print('metric: "{metric_name}", '
|
||||
'value: "{metric_value}" '.
|
||||
format(metric_name=metric_name,
|
||||
metric_value=metric_value))
|
||||
|
||||
all_metrics.append((metric_name, metric_value))
|
||||
return all_metrics
|
||||
|
||||
@staticmethod
|
||||
def parse_total_time( am_start_output: str) -> Optional[str]:
|
||||
"""Parses the total time from 'adb shell am start pkg' output.
|
||||
|
||||
Returns:
|
||||
the total time of app startup.
|
||||
"""
|
||||
for line in am_start_output.split('\n'):
|
||||
if 'TotalTime:' in line:
|
||||
return line[len('TotalTime:'):].strip()
|
||||
return None
|
||||
|
||||
104
startop/scripts/app_startup/lib/app_runner_test.py
Normal file
104
startop/scripts/app_startup/lib/app_runner_test.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# Copyright 2019, The Android Open Source Project
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Unit tests for the AppRunner."""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from app_runner import AppRunner, AppRunnerListener
|
||||
from mock import Mock, call, patch
|
||||
|
||||
# The path is "frameworks/base/startop/scripts/"
|
||||
sys.path.append(Path(os.path.realpath(__file__)).parents[2])
|
||||
import lib.cmd_utils as cmd_utils
|
||||
|
||||
class AppRunnerTestListener(AppRunnerListener):
|
||||
def preprocess(self) -> None:
|
||||
cmd_utils.run_shell_command('pre'),
|
||||
|
||||
def postprocess(self, pre_launch_timestamp: str) -> None:
|
||||
cmd_utils.run_shell_command('post'),
|
||||
|
||||
def metrics_selector(self, am_start_output: str,
|
||||
pre_launch_timestamp: str) -> None:
|
||||
return 'TotalTime=123\n'
|
||||
|
||||
RUNNER = AppRunner(package='music',
|
||||
activity='MainActivity',
|
||||
compiler_filter='speed',
|
||||
timeout=None,
|
||||
simulate=False)
|
||||
|
||||
|
||||
|
||||
def test_configure_compiler_filter():
|
||||
with patch('lib.cmd_utils.run_shell_command',
|
||||
new_callable=Mock) as mock_run_shell_command:
|
||||
mock_run_shell_command.return_value = (True, 'speed arm64 kUpToDate')
|
||||
|
||||
RUNNER.configure_compiler_filter()
|
||||
|
||||
calls = [call(os.path.realpath(
|
||||
os.path.join(RUNNER.DIR,
|
||||
'../query_compiler_filter.py')) + ' --package music')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
def test_parse_metrics_output():
|
||||
input = 'a1=b1\nc1=d1\ne1=f1'
|
||||
ret = RUNNER.parse_metrics_output(input)
|
||||
|
||||
assert ret == [('a1', 'b1'), ('c1', 'd1'), ('e1', 'f1')]
|
||||
|
||||
def _mocked_run_shell_command(*args, **kwargs):
|
||||
if args[0] == 'adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"':
|
||||
return (True, "2019-07-02 23:20:06.972674825")
|
||||
elif args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'':
|
||||
return (True, '9999')
|
||||
else:
|
||||
return (True, 'a1=b1\nc1=d1=d2\ne1=f1')
|
||||
|
||||
@patch('app_startup.lib.adb_utils.blocking_wait_for_logcat_displayed_time')
|
||||
@patch('lib.cmd_utils.run_shell_command')
|
||||
def test_run(mock_run_shell_command,
|
||||
mock_blocking_wait_for_logcat_displayed_time):
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
mock_blocking_wait_for_logcat_displayed_time.return_value = 123
|
||||
|
||||
test_listener = AppRunnerTestListener()
|
||||
RUNNER.add_callbacks(test_listener)
|
||||
|
||||
result = RUNNER.run()
|
||||
|
||||
RUNNER.remove_callbacks(test_listener)
|
||||
|
||||
calls = [call('pre'),
|
||||
call(os.path.realpath(
|
||||
os.path.join(RUNNER.DIR,
|
||||
'../query_compiler_filter.py')) +
|
||||
' --package music'),
|
||||
call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'),
|
||||
call(
|
||||
'timeout {timeout} "{DIR}/launch_application" "{package}" "{activity}"'
|
||||
.format(timeout=30,
|
||||
DIR=os.path.realpath(os.path.dirname(RUNNER.DIR)),
|
||||
package='music',
|
||||
activity='MainActivity',
|
||||
timestamp='2019-07-02 23:20:06.972674825')),
|
||||
call('post')
|
||||
]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
assert result == [('TotalTime', '123')]
|
||||
assert len(RUNNER.listeners) == 0
|
||||
@@ -30,32 +30,146 @@ import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import List, Tuple, Optional, NamedTuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
# local imports
|
||||
import lib.adb_utils as adb_utils
|
||||
from lib.app_runner import AppRunner, AppRunnerListener
|
||||
|
||||
# global variables
|
||||
DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
|
||||
'../iorap/common'))
|
||||
APP_STARTUP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
|
||||
'lib/common'))
|
||||
|
||||
sys.path.append(os.path.dirname(DIR))
|
||||
import lib.print_utils as print_utils
|
||||
import lib.cmd_utils as cmd_utils
|
||||
import iorap.lib.iorapd_utils as iorapd_utils
|
||||
|
||||
RunCommandArgs = NamedTuple('RunCommandArgs',
|
||||
[('package', str),
|
||||
('readahead', str),
|
||||
('activity', Optional[str]),
|
||||
('compiler_filter', Optional[str]),
|
||||
('timeout', Optional[int]),
|
||||
('debug', bool),
|
||||
('simulate', bool),
|
||||
('input', Optional[str])])
|
||||
class PrefetchAppRunner(AppRunnerListener):
|
||||
def __init__(self,
|
||||
package: str,
|
||||
activity: Optional[str],
|
||||
readahead: str,
|
||||
compiler_filter: Optional[str],
|
||||
timeout: Optional[int],
|
||||
simulate: bool,
|
||||
debug: bool,
|
||||
input:Optional[str],
|
||||
**kwargs):
|
||||
self.app_runner = AppRunner(package,
|
||||
activity,
|
||||
compiler_filter,
|
||||
timeout,
|
||||
simulate)
|
||||
self.app_runner.add_callbacks(self)
|
||||
|
||||
self.simulate = simulate
|
||||
self.readahead = readahead
|
||||
self.debug = debug
|
||||
self.input = input
|
||||
print_utils.DEBUG = self.debug
|
||||
cmd_utils.SIMULATE = self.simulate
|
||||
|
||||
|
||||
def run(self) -> Optional[List[Tuple[str]]]:
|
||||
"""Runs an app.
|
||||
|
||||
Returns:
|
||||
A list of (metric, value) tuples.
|
||||
"""
|
||||
return self.app_runner.run()
|
||||
|
||||
def preprocess(self):
|
||||
passed = self.validate_options()
|
||||
if not passed:
|
||||
return
|
||||
|
||||
# Sets up adb environment.
|
||||
adb_utils.root()
|
||||
adb_utils.disable_selinux()
|
||||
time.sleep(1)
|
||||
|
||||
# Kill any existing process of this app
|
||||
adb_utils.pkill(self.app_runner.package)
|
||||
|
||||
if self.readahead != 'warm':
|
||||
print_utils.debug_print('Drop caches for non-warm start.')
|
||||
# Drop all caches to get cold starts.
|
||||
adb_utils.vm_drop_cache()
|
||||
|
||||
if self.readahead != 'warm' and self.readahead != 'cold':
|
||||
iorapd_utils.enable_iorapd_readahead()
|
||||
|
||||
def postprocess(self, pre_launch_timestamp: str):
|
||||
passed = self._perform_post_launch_cleanup(pre_launch_timestamp)
|
||||
if not passed and not self.app_runner.simulate:
|
||||
print_utils.error_print('Cannot perform post launch cleanup!')
|
||||
return None
|
||||
|
||||
# Kill any existing process of this app
|
||||
adb_utils.pkill(self.app_runner.package)
|
||||
|
||||
def _perform_post_launch_cleanup(self, logcat_timestamp: str) -> bool:
|
||||
"""Performs cleanup at the end of each loop iteration.
|
||||
|
||||
Returns:
|
||||
A bool indicates whether the cleanup succeeds or not.
|
||||
"""
|
||||
if self.readahead != 'warm' and self.readahead != 'cold':
|
||||
passed = iorapd_utils.wait_for_iorapd_finish(self.app_runner.package,
|
||||
self.app_runner.activity,
|
||||
self.app_runner.timeout,
|
||||
self.debug,
|
||||
logcat_timestamp)
|
||||
|
||||
if not passed:
|
||||
return passed
|
||||
|
||||
return iorapd_utils.disable_iorapd_readahead()
|
||||
|
||||
# Don't need to do anything for warm or cold.
|
||||
return True
|
||||
|
||||
def metrics_selector(self, am_start_output: str,
|
||||
pre_launch_timestamp: str) -> str:
|
||||
"""Parses the metric after app startup by reading from logcat in a blocking
|
||||
manner until all metrics have been found".
|
||||
|
||||
Returns:
|
||||
the total time and displayed time of app startup.
|
||||
For example: "TotalTime=123\nDisplayedTime=121
|
||||
"""
|
||||
total_time = AppRunner.parse_total_time(am_start_output)
|
||||
displayed_time = adb_utils.blocking_wait_for_logcat_displayed_time(
|
||||
pre_launch_timestamp, self.app_runner.package, self.app_runner.timeout)
|
||||
|
||||
return 'TotalTime={}\nDisplayedTime={}'.format(total_time, displayed_time)
|
||||
|
||||
def validate_options(self) -> bool:
|
||||
"""Validates the activity and trace file if needed.
|
||||
|
||||
Returns:
|
||||
A bool indicates whether the activity is valid.
|
||||
"""
|
||||
needs_trace_file = self.readahead != 'cold' and self.readahead != 'warm'
|
||||
if needs_trace_file and (self.input is None or
|
||||
not os.path.exists(self.input)):
|
||||
print_utils.error_print('--input not specified!')
|
||||
return False
|
||||
|
||||
# Install necessary trace file. This must be after the activity checking.
|
||||
if needs_trace_file:
|
||||
passed = iorapd_utils.iorapd_compiler_install_trace_file(
|
||||
self.app_runner.package, self.app_runner.activity, self.input)
|
||||
if not cmd_utils.SIMULATE and not passed:
|
||||
print_utils.error_print('Failed to install compiled TraceFile.pb for '
|
||||
'"{}/{}"'.
|
||||
format(self.app_runner.package,
|
||||
self.app_runner.activity))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def parse_options(argv: List[str] = None):
|
||||
"""Parses command line arguments and return an argparse Namespace object."""
|
||||
@@ -101,271 +215,10 @@ def parse_options(argv: List[str] = None):
|
||||
|
||||
return parser.parse_args(argv)
|
||||
|
||||
def validate_options(args: RunCommandArgs) -> Tuple[bool, RunCommandArgs]:
|
||||
"""Validates the activity and trace file if needed.
|
||||
|
||||
Returns:
|
||||
A bool indicates whether the activity is valid and trace file exists if
|
||||
necessary.
|
||||
"""
|
||||
needs_trace_file = (args.readahead != 'cold' and args.readahead != 'warm')
|
||||
if needs_trace_file and (args.input is None or
|
||||
not os.path.exists(args.input)):
|
||||
print_utils.error_print('--input not specified!')
|
||||
return False, args
|
||||
|
||||
if args.simulate:
|
||||
args = args._replace(activity='act')
|
||||
|
||||
if not args.activity:
|
||||
_, activity = cmd_utils.run_shell_func(IORAP_COMMON_BASH_SCRIPT,
|
||||
'get_activity_name',
|
||||
[args.package])
|
||||
args = args._replace(activity=activity)
|
||||
|
||||
if not args.activity:
|
||||
print_utils.error_print('Activity name could not be found, '
|
||||
'invalid package name?!')
|
||||
return False, args
|
||||
|
||||
# Install necessary trace file. This must be after the activity checking.
|
||||
if needs_trace_file:
|
||||
passed = iorapd_utils.iorapd_compiler_install_trace_file(
|
||||
args.package, args.activity, args.input)
|
||||
if not cmd_utils.SIMULATE and not passed:
|
||||
print_utils.error_print('Failed to install compiled TraceFile.pb for '
|
||||
'"{}/{}"'.
|
||||
format(args.package, args.activity))
|
||||
return False, args
|
||||
|
||||
return True, args
|
||||
|
||||
def set_up_adb_env():
|
||||
"""Sets up adb environment."""
|
||||
adb_utils.root()
|
||||
adb_utils.disable_selinux()
|
||||
time.sleep(1)
|
||||
|
||||
def configure_compiler_filter(compiler_filter: str, package: str,
|
||||
activity: str) -> bool:
|
||||
"""Configures compiler filter (e.g. speed).
|
||||
|
||||
Returns:
|
||||
A bool indicates whether configure of compiler filer succeeds or not.
|
||||
"""
|
||||
if not compiler_filter:
|
||||
print_utils.debug_print('No --compiler-filter specified, don\'t'
|
||||
' need to force it.')
|
||||
return True
|
||||
|
||||
passed, current_compiler_filter_info = \
|
||||
cmd_utils.run_shell_command(
|
||||
'{} --package {}'.format(os.path.join(DIR, 'query_compiler_filter.py'),
|
||||
package))
|
||||
|
||||
if passed != 0:
|
||||
return passed
|
||||
|
||||
# TODO: call query_compiler_filter directly as a python function instead of
|
||||
# these shell calls.
|
||||
current_compiler_filter, current_reason, current_isa = \
|
||||
current_compiler_filter_info.split(' ')
|
||||
print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format(
|
||||
current_compiler_filter, current_reason, current_isa))
|
||||
|
||||
# Don't trust reasons that aren't 'unknown' because that means
|
||||
# we didn't manually force the compilation filter.
|
||||
# (e.g. if any automatic system-triggered compilations are not unknown).
|
||||
if current_reason != 'unknown' or current_compiler_filter != compiler_filter:
|
||||
passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter '
|
||||
'--compiler-filter "{}" '
|
||||
'--package "{}"'
|
||||
' --activity "{}'.
|
||||
format(DIR, compiler_filter,
|
||||
package, activity))
|
||||
else:
|
||||
adb_utils.debug_print('Queried compiler-filter matched requested '
|
||||
'compiler-filter, skip forcing.')
|
||||
passed = False
|
||||
return passed
|
||||
|
||||
def parse_metrics_output(input: str,
|
||||
simulate: bool = False) -> List[Tuple[str, str, str]]:
|
||||
"""Parses ouput of app startup to metrics and corresponding values.
|
||||
|
||||
It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]'
|
||||
|
||||
Returns:
|
||||
A list of tuples that including metric name, metric value and rest info.
|
||||
"""
|
||||
all_metrics = []
|
||||
for line in input.split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
splits = line.split('=')
|
||||
if len(splits) < 2:
|
||||
print_utils.error_print('Bad line "{}"'.format(line))
|
||||
continue
|
||||
metric_name = splits[0]
|
||||
metric_value = splits[1]
|
||||
rest = splits[2] if len(splits) > 2 else ''
|
||||
if rest:
|
||||
print_utils.error_print('Corrupt line "{}"'.format(line))
|
||||
print_utils.debug_print('metric: "{metric_name}", '
|
||||
'value: "{metric_value}" '.
|
||||
format(metric_name=metric_name,
|
||||
metric_value=metric_value))
|
||||
|
||||
all_metrics.append((metric_name, metric_value))
|
||||
return all_metrics
|
||||
|
||||
def _parse_total_time(am_start_output: str) -> Optional[str]:
|
||||
"""Parses the total time from 'adb shell am start pkg' output.
|
||||
|
||||
Returns:
|
||||
the total time of app startup.
|
||||
"""
|
||||
for line in am_start_output.split('\n'):
|
||||
if 'TotalTime:' in line:
|
||||
return line[len('TotalTime:'):].strip()
|
||||
return None
|
||||
|
||||
def blocking_parse_all_metrics(am_start_output: str, package: str,
|
||||
pre_launch_timestamp: str,
|
||||
timeout: int) -> str:
|
||||
"""Parses the metric after app startup by reading from logcat in a blocking
|
||||
manner until all metrics have been found".
|
||||
|
||||
Returns:
|
||||
the total time and displayed time of app startup.
|
||||
For example: "TotalTime=123\nDisplayedTime=121
|
||||
"""
|
||||
total_time = _parse_total_time(am_start_output)
|
||||
displayed_time = adb_utils.blocking_wait_for_logcat_displayed_time(
|
||||
pre_launch_timestamp, package, timeout)
|
||||
|
||||
return 'TotalTime={}\nDisplayedTime={}'.format(total_time, displayed_time)
|
||||
|
||||
def run(readahead: str,
|
||||
package: str,
|
||||
activity: str,
|
||||
timeout: int,
|
||||
simulate: bool,
|
||||
debug: bool) -> List[Tuple[str, str]]:
|
||||
"""Runs app startup test.
|
||||
|
||||
Returns:
|
||||
A list of tuples that including metric name, metric value and rest info.
|
||||
"""
|
||||
print_utils.debug_print('==========================================')
|
||||
print_utils.debug_print('===== START =====')
|
||||
print_utils.debug_print('==========================================')
|
||||
|
||||
# Kill any existing process of this app
|
||||
adb_utils.pkill(package)
|
||||
|
||||
if readahead != 'warm':
|
||||
print_utils.debug_print('Drop caches for non-warm start.')
|
||||
# Drop all caches to get cold starts.
|
||||
adb_utils.vm_drop_cache()
|
||||
|
||||
if readahead != 'warm' and readahead != 'cold':
|
||||
iorapd_utils.enable_iorapd_readahead()
|
||||
|
||||
print_utils.debug_print('Running with timeout {}'.format(timeout))
|
||||
|
||||
pre_launch_timestamp = adb_utils.logcat_save_timestamp()
|
||||
|
||||
passed, output = cmd_utils.run_shell_command('timeout {timeout} '
|
||||
'"{DIR}/launch_application" '
|
||||
'"{package}" '
|
||||
'"{activity}"'
|
||||
.format(timeout=timeout,
|
||||
DIR=DIR,
|
||||
package=package,
|
||||
activity=activity))
|
||||
if not passed and not simulate:
|
||||
return None
|
||||
|
||||
if simulate:
|
||||
results = [('TotalTime', '123')]
|
||||
else:
|
||||
output = blocking_parse_all_metrics(output,
|
||||
package,
|
||||
pre_launch_timestamp,
|
||||
timeout)
|
||||
results = parse_metrics_output(output, simulate)
|
||||
|
||||
passed = perform_post_launch_cleanup(
|
||||
readahead, package, activity, timeout, debug, pre_launch_timestamp)
|
||||
if not passed and not simulate:
|
||||
print_utils.error_print('Cannot perform post launch cleanup!')
|
||||
return None
|
||||
|
||||
adb_utils.pkill(package)
|
||||
return results
|
||||
|
||||
def perform_post_launch_cleanup(readahead: str,
|
||||
package: str,
|
||||
activity: str,
|
||||
timeout: int,
|
||||
debug: bool,
|
||||
logcat_timestamp: str) -> bool:
|
||||
"""Performs cleanup at the end of each loop iteration.
|
||||
|
||||
Returns:
|
||||
A bool indicates whether the cleanup succeeds or not.
|
||||
"""
|
||||
if readahead != 'warm' and readahead != 'cold':
|
||||
passed = iorapd_utils.wait_for_iorapd_finish(package,
|
||||
activity,
|
||||
timeout,
|
||||
debug,
|
||||
logcat_timestamp)
|
||||
|
||||
if not passed:
|
||||
return passed
|
||||
|
||||
return iorapd_utils.disable_iorapd_readahead()
|
||||
|
||||
# Don't need to do anything for warm or cold.
|
||||
return True
|
||||
|
||||
def run_test(args: RunCommandArgs) -> List[Tuple[str, str]]:
|
||||
"""Runs one test using given options.
|
||||
|
||||
Returns:
|
||||
A list of tuples that including metric name, metric value.
|
||||
"""
|
||||
print_utils.DEBUG = args.debug
|
||||
cmd_utils.SIMULATE = args.simulate
|
||||
|
||||
passed, args = validate_options(args)
|
||||
if not passed:
|
||||
return None
|
||||
|
||||
set_up_adb_env()
|
||||
|
||||
# Ensure the APK is currently compiled with whatever we passed in
|
||||
# via --compiler-filter.
|
||||
# No-op if this option was not passed in.
|
||||
if not configure_compiler_filter(args.compiler_filter, args.package,
|
||||
args.activity):
|
||||
return None
|
||||
|
||||
return run(args.readahead, args.package, args.activity, args.timeout,
|
||||
args.simulate, args.debug)
|
||||
|
||||
def get_args_from_opts(opts: argparse.Namespace) -> RunCommandArgs:
|
||||
kwargs = {}
|
||||
for field in RunCommandArgs._fields:
|
||||
kwargs[field] = getattr(opts, field)
|
||||
return RunCommandArgs(**kwargs)
|
||||
|
||||
def main():
|
||||
opts = parse_options()
|
||||
args = get_args_from_opts(opts)
|
||||
result = run_test(args)
|
||||
runner = PrefetchAppRunner(**vars(opts))
|
||||
result = runner.run()
|
||||
|
||||
if result is None:
|
||||
return 1
|
||||
|
||||
@@ -33,17 +33,18 @@ import io
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
import tempfile
|
||||
# global imports
|
||||
from contextlib import contextmanager
|
||||
|
||||
# pip imports
|
||||
import pytest
|
||||
# local imports
|
||||
import run_app_with_prefetch as run
|
||||
from mock import Mock, call, patch
|
||||
import run_app_with_prefetch as runner
|
||||
from mock import call, patch, Mock
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app_startup.lib.app_runner import AppRunner
|
||||
#
|
||||
# Argument Parsing Helpers
|
||||
#
|
||||
@@ -85,7 +86,7 @@ def parse_args(args):
|
||||
:return: dictionary of parsed key/values
|
||||
"""
|
||||
# "-a b -c d" => ['-a', 'b', '-c', 'd']
|
||||
return vars(run.parse_options(shlex.split(args)))
|
||||
return vars(runner.parse_options(shlex.split(args)))
|
||||
|
||||
def default_dict_for_parsed_args(**kwargs):
|
||||
"""Combines it with all of the "optional" parameters' default values."""
|
||||
@@ -154,130 +155,132 @@ def test_argparse():
|
||||
|
||||
def test_main():
|
||||
args = '--package com.fake.package --activity act -s'
|
||||
opts = run.parse_options(shlex.split(args))
|
||||
|
||||
args = run.get_args_from_opts(opts)
|
||||
result = run.run_test(args)
|
||||
opts = runner.parse_options(shlex.split(args))
|
||||
result = runner.PrefetchAppRunner(**vars(opts)).run()
|
||||
assert result == [('TotalTime', '123')]
|
||||
|
||||
def test_set_up_adb_env():
|
||||
def _mocked_run_shell_command(*args, **kwargs):
|
||||
if args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'':
|
||||
return (True, '9999')
|
||||
else:
|
||||
return (True, '')
|
||||
|
||||
def test_preprocess_no_cache_drop():
|
||||
with patch('lib.cmd_utils.run_shell_command',
|
||||
new_callable=Mock) as mock_run_shell_command:
|
||||
mock_run_shell_command.return_value = (True, '')
|
||||
run.set_up_adb_env()
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
prefetch_app_runner = runner.PrefetchAppRunner(package='music',
|
||||
activity='MainActivity',
|
||||
readahead='warm',
|
||||
compiler_filter=None,
|
||||
timeout=None,
|
||||
simulate=False,
|
||||
debug=False,
|
||||
input=None)
|
||||
|
||||
prefetch_app_runner.preprocess()
|
||||
|
||||
calls = [call('adb root'),
|
||||
call('adb shell "getenforce"'),
|
||||
call('adb shell "setenforce 0"'),
|
||||
call('adb shell "stop"'),
|
||||
call('adb shell "start"'),
|
||||
call('adb wait-for-device')]
|
||||
call('adb wait-for-device'),
|
||||
call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
def test_set_up_adb_env_with_permissive():
|
||||
def test_preprocess_with_cache_drop():
|
||||
with patch('lib.cmd_utils.run_shell_command',
|
||||
new_callable=Mock) as mock_run_shell_command:
|
||||
mock_run_shell_command.return_value = (True, 'Permissive')
|
||||
run.set_up_adb_env()
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
prefetch_app_runner = runner.PrefetchAppRunner(package='music',
|
||||
activity='MainActivity',
|
||||
readahead='cold',
|
||||
compiler_filter=None,
|
||||
timeout=None,
|
||||
simulate=False,
|
||||
debug=False,
|
||||
input=None)
|
||||
|
||||
calls = [call('adb root'), call('adb shell "getenforce"')]
|
||||
prefetch_app_runner.preprocess()
|
||||
|
||||
calls = [call('adb root'),
|
||||
call('adb shell "getenforce"'),
|
||||
call('adb shell "setenforce 0"'),
|
||||
call('adb shell "stop"'),
|
||||
call('adb shell "start"'),
|
||||
call('adb wait-for-device'),
|
||||
call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"'),
|
||||
call('adb shell "echo 3 > /proc/sys/vm/drop_caches"')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
def test_configure_compiler_filter():
|
||||
def test_preprocess_with_cache_drop_and_iorapd_enabled():
|
||||
with patch('lib.cmd_utils.run_shell_command',
|
||||
new_callable=Mock) as mock_run_shell_command:
|
||||
mock_run_shell_command.return_value = (True, 'speed arm64 kUpToDate')
|
||||
run.configure_compiler_filter('speed', 'music', 'MainActivity')
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
|
||||
calls = [call(os.path.join(run.DIR, 'query_compiler_filter.py') +
|
||||
' --package music')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
with tempfile.NamedTemporaryFile() as input:
|
||||
prefetch_app_runner = runner.PrefetchAppRunner(package='music',
|
||||
activity='MainActivity',
|
||||
readahead='fadvise',
|
||||
compiler_filter=None,
|
||||
timeout=None,
|
||||
simulate=False,
|
||||
debug=False,
|
||||
input=input.name)
|
||||
|
||||
def test_parse_metrics_output():
|
||||
input = 'a1=b1\nc1=d1\ne1=f1'
|
||||
ret = run.parse_metrics_output(input)
|
||||
prefetch_app_runner.preprocess()
|
||||
|
||||
assert ret == [('a1', 'b1'), ('c1', 'd1'), ('e1', 'f1')]
|
||||
|
||||
def _mocked_run_shell_command(*args, **kwargs):
|
||||
if args[0] == 'adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"':
|
||||
return (True, "2019-07-02 23:20:06.972674825")
|
||||
elif args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'':
|
||||
return (True, '9999')
|
||||
else:
|
||||
return (True, 'a1=b1\nc1=d1=d2\ne1=f1')
|
||||
calls = [call('adb root'),
|
||||
call('adb shell "getenforce"'),
|
||||
call('adb shell "setenforce 0"'),
|
||||
call('adb shell "stop"'),
|
||||
call('adb shell "start"'),
|
||||
call('adb wait-for-device'),
|
||||
call(
|
||||
'adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"'),
|
||||
call('adb shell "echo 3 > /proc/sys/vm/drop_caches"'),
|
||||
call('bash -c "source {}; iorapd_readahead_enable"'.
|
||||
format(AppRunner.IORAP_COMMON_BASH_SCRIPT))]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
@patch('lib.adb_utils.blocking_wait_for_logcat_displayed_time')
|
||||
@patch('lib.cmd_utils.run_shell_command')
|
||||
def test_run_no_vm_cache_drop(mock_run_shell_command,
|
||||
mock_blocking_wait_for_logcat_displayed_time):
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
mock_blocking_wait_for_logcat_displayed_time.return_value = 123
|
||||
|
||||
run.run('warm',
|
||||
'music',
|
||||
'MainActivity',
|
||||
timeout=10,
|
||||
simulate=False,
|
||||
debug=False)
|
||||
|
||||
calls = [call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"'),
|
||||
call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'),
|
||||
call(
|
||||
'timeout {timeout} "{DIR}/launch_application" "{package}" "{activity}"'
|
||||
.format(timeout=10,
|
||||
DIR=run.DIR,
|
||||
package='music',
|
||||
activity='MainActivity',
|
||||
timestamp='2019-07-02 23:20:06.972674825')),
|
||||
call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
@patch('lib.adb_utils.blocking_wait_for_logcat_displayed_time')
|
||||
@patch('lib.cmd_utils.run_shell_command')
|
||||
def test_run_with_vm_cache_drop_and_post_launch_cleanup(
|
||||
def test_postprocess_with_launch_cleanup(
|
||||
mock_run_shell_command,
|
||||
mock_blocking_wait_for_logcat_displayed_time):
|
||||
mock_run_shell_command.side_effect = _mocked_run_shell_command
|
||||
mock_blocking_wait_for_logcat_displayed_time.return_value = 123
|
||||
|
||||
run.run('fadvise',
|
||||
'music',
|
||||
'MainActivity',
|
||||
timeout=10,
|
||||
simulate=False,
|
||||
debug=False)
|
||||
with tempfile.NamedTemporaryFile() as input:
|
||||
prefetch_app_runner = runner.PrefetchAppRunner(package='music',
|
||||
activity='MainActivity',
|
||||
readahead='fadvise',
|
||||
compiler_filter=None,
|
||||
timeout=10,
|
||||
simulate=False,
|
||||
debug=False,
|
||||
input=input.name)
|
||||
|
||||
calls = [call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"'),
|
||||
call('adb shell "echo 3 > /proc/sys/vm/drop_caches"'),
|
||||
call('bash -c "source {}; iorapd_readahead_enable"'.
|
||||
format(run.IORAP_COMMON_BASH_SCRIPT)),
|
||||
call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'),
|
||||
call(
|
||||
'timeout {timeout} "{DIR}/launch_application" '
|
||||
'"{package}" "{activity}"'
|
||||
.format(timeout=10,
|
||||
DIR=run.DIR,
|
||||
package='music',
|
||||
activity='MainActivity',
|
||||
timestamp='2019-07-02 23:20:06.972674825')),
|
||||
call(
|
||||
'bash -c "source {script_path}; '
|
||||
prefetch_app_runner.postprocess('2019-07-02 23:20:06.972674825')
|
||||
|
||||
calls = [
|
||||
call('bash -c "source {script_path}; '
|
||||
'iorapd_readahead_wait_until_finished '
|
||||
'\'{package}\' \'{activity}\' \'{timestamp}\' \'{timeout}\'"'.
|
||||
format(timeout=10,
|
||||
package='music',
|
||||
activity='MainActivity',
|
||||
timestamp='2019-07-02 23:20:06.972674825',
|
||||
script_path=run.IORAP_COMMON_BASH_SCRIPT)),
|
||||
call('bash -c "source {}; iorapd_readahead_disable"'.
|
||||
format(run.IORAP_COMMON_BASH_SCRIPT)),
|
||||
call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
format(timeout=10,
|
||||
package='music',
|
||||
activity='MainActivity',
|
||||
timestamp='2019-07-02 23:20:06.972674825',
|
||||
script_path=AppRunner.IORAP_COMMON_BASH_SCRIPT)),
|
||||
call('bash -c "source {}; iorapd_readahead_disable"'.
|
||||
format(AppRunner.IORAP_COMMON_BASH_SCRIPT)),
|
||||
call('adb shell ps | grep "music" | awk \'{print $2;}\''),
|
||||
call('adb shell "kill 9999"')]
|
||||
mock_run_shell_command.assert_has_calls(calls)
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main()
|
||||
|
||||
Reference in New Issue
Block a user