Skip to content

Commit 58969cb

Browse files
Merge branch 'master' into firestore-held-write-acks
2 parents b5fcc0f + b3871fe commit 58969cb

File tree

25 files changed

+1904
-2047
lines changed

25 files changed

+1904
-2047
lines changed

ci/fireci/fireci/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
15+
from .internal import ci_command

ci/fireci/fireci/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import os
1717

1818
from . import gradle
19-
from .internal import ci_command
19+
from . import ci_command
2020

2121

2222
@click.argument('task', required=True, nargs=-1)

ci/fireci/fireci/emulator.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,127 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import logging
17+
import os
18+
import signal
19+
import subprocess
20+
import time
1621

1722
_logger = logging.getLogger('fireci.emulator')
1823

24+
EMULATOR_BINARY = 'emulator'
25+
ADB_BINARY = 'adb'
26+
EMULATOR_NAME = 'test'
27+
28+
EMULATOR_FLAGS = ['-no-audio', '-no-window', '-skin', '768x1280']
29+
1930

20-
# TODO(vkryachko): start/shutdown the emulator.
2131
class EmulatorHandler:
32+
"""
33+
Context manager that launches an android emulator for the duration of its execution.
2234
23-
def __init__(self, artifacts_dir):
35+
As part of its run it:
36+
* Launches the emulator
37+
* Waits for it to boot
38+
* Starts logcat to store on-device logs
39+
* Produces stdout.log, stderr.log, logcat.log in the artifacts directory
40+
"""
41+
42+
def __init__(
43+
self,
44+
artifacts_dir,
45+
*,
46+
name=EMULATOR_NAME,
47+
emulator_binary=EMULATOR_BINARY,
48+
adb_binary=ADB_BINARY,
49+
# for testing only
50+
emulator_stdin=None,
51+
wait_for_device_stdin=None,
52+
logcat_stdin=None):
2453
self._artifacts_dir = artifacts_dir
2554

55+
log_dir = '{}_emulator'.format(name)
56+
self._stdout = self._open(log_dir, 'stdout.log')
57+
self._stderr = self._open(log_dir, 'stderr.log')
58+
self._adb_log = self._open(log_dir, 'logcat.log')
59+
self._name = name
60+
61+
self._emulator_binary = emulator_binary
62+
self._adb_binary = adb_binary
63+
64+
self._emulator_stdin = emulator_stdin
65+
self._wait_for_device_stdin = wait_for_device_stdin
66+
self._logcat_stdin = logcat_stdin
67+
2668
def __enter__(self):
27-
_logger.debug('Pretend to start the emulator(TODO)')
69+
_logger.info('Starting avd "{}..."'.format(self._name))
70+
self._process = subprocess.Popen(
71+
[self._emulator_binary, '-avd', self._name] + EMULATOR_FLAGS,
72+
env=os.environ,
73+
stdin=self._emulator_stdin,
74+
stdout=self._stdout,
75+
stderr=self._stderr)
76+
try:
77+
self._wait_for_boot(datetime.timedelta(minutes=5))
78+
except:
79+
self._kill(self._process)
80+
self._close_files()
81+
raise
82+
83+
self._logcat = subprocess.Popen(
84+
[self._adb_binary, 'logcat'],
85+
stdin=self._logcat_stdin,
86+
stdout=self._adb_log,
87+
)
2888

2989
def __exit__(self, exception_type, exception_value, traceback):
30-
_logger.debug('Pretend to stop the emulator(TODO)')
90+
_logger.info('Shutting down avd "{}"...'.format(self._name))
91+
self._kill(self._process)
92+
_logger.info('Avd "{}" shut down.'.format(self._name))
93+
self._kill(self._logcat)
94+
self._close_files()
95+
96+
def _open(self, dirname, filename):
97+
"""Opens a file in a given directory, creates directory if required."""
98+
dirname = os.path.join(self._artifacts_dir, dirname)
99+
if (not os.path.exists(dirname)):
100+
os.makedirs(dirname)
101+
return open(os.path.join(dirname, filename), 'w')
102+
103+
def _wait_for_boot(self, timeout: datetime.timedelta):
104+
_logger.info('Waiting for avd to boot...')
105+
wait = subprocess.Popen(
106+
[self._adb_binary, 'wait-for-device'],
107+
stdin=self._wait_for_device_stdin,
108+
stdout=self._stdout,
109+
stderr=self._stderr,
110+
)
111+
112+
start = datetime.datetime.now()
113+
while self._process.poll() is None:
114+
wait_exitcode = wait.poll()
115+
if wait_exitcode is not None:
116+
if wait_exitcode == 0:
117+
_logger.info('Emulator booted successfully.')
118+
return
119+
raise RuntimeError("Waiting for emulator failed.")
120+
121+
time.sleep(0.1)
122+
now = datetime.datetime.now()
123+
if now - start >= timeout:
124+
self._kill(wait, sig=signal.SIGKILL)
125+
raise RuntimeError("Emulator startup timed out.")
126+
127+
self._kill(wait)
128+
raise RuntimeError(
129+
"Emulator failed to launch. See emulator logs for details.")
130+
131+
def _kill(self, process, sig=signal.SIGTERM):
132+
process.send_signal(sig)
133+
process.wait()
134+
135+
def _close_files(self):
136+
for f in (self._stdout, self._stderr, self._adb_log):
137+
if f is not None:
138+
f.close()

ci/fireci/fireci/gradle.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
ADB_INSTALL_TIMEOUT = '5'
2323

2424

25+
def P(name, value):
26+
"""Returns name and value in the format of gradle's project property cli argument."""
27+
return '-P{}={}'.format(name, value)
28+
29+
2530
def run(*args, gradle_opts='', workdir=None):
2631
"""Invokes gradle with specified args and gradle_opts."""
2732
new_env = dict(os.environ)

ci/fireci/fireci/internal.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ def _artifact_handler(target_directory, artifact_patterns):
5151

5252

5353
@contextlib.contextmanager
54-
def _emulator_handler(enabled, target_directory):
54+
def _emulator_handler(enabled, *args, **kwargs):
5555
if not enabled:
5656
yield
5757
return
5858

59-
with emulator.EmulatorHandler(target_directory):
59+
with emulator.EmulatorHandler(*args, **kwargs):
6060
yield
6161

6262

@@ -71,14 +71,14 @@ class _CommonOptions:
7171
@click.option(
7272
'--artifact-target-dir',
7373
default='_artifacts',
74-
help='Directory where artifacts will be symlinked to.',
74+
help='Directory where artifacts will be copied to.',
7575
type=click.Path(dir_okay=True, resolve_path=True),
7676
)
7777
@click.option(
7878
'--artifact-patterns',
7979
default=('**/build/test-results', '**/build/reports'),
8080
help=
81-
'Shell-style artifact patterns that are symlinked into `artifact-target-dir`.'\
81+
'Shell-style artifact patterns that are copied into `artifact-target-dir`.'\
8282
'Can be specified multiple times.',
8383
multiple=True,
8484
type=str,
@@ -89,6 +89,21 @@ class _CommonOptions:
8989
help='Specifies whether to start an Android emulator a command executes.',
9090
is_flag=True,
9191
)
92+
@click.option(
93+
'--emulator-name',
94+
default='test',
95+
help='Specifies the AVD name to launch the emulator with.',
96+
)
97+
@click.option(
98+
'--emulator-binary',
99+
default='emulator',
100+
help='Specifies the name/full path to the emulator binary.',
101+
)
102+
@click.option(
103+
'--adb-binary',
104+
default='adb',
105+
help='Specifies the name/full path to the adb binary.',
106+
)
92107
@_pass_options
93108
def main(options, **kwargs):
94109
"""Main command group.
@@ -120,7 +135,10 @@ def new_func(ctx, options, *args, **kwargs):
120135
with _artifact_handler(options.artifact_target_dir,
121136
options.artifact_patterns), _emulator_handler(
122137
options.with_emulator,
123-
options.artifact_target_dir):
138+
options.artifact_target_dir,
139+
name=options.emulator_name,
140+
emulator_binary=options.emulator_binary,
141+
adb_binary=options.adb_binary):
124142
return ctx.invoke(f, *args, **kwargs)
125143

126144
return functools.update_wrapper(new_func, f)

ci/fireci/fireci/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
import logging
1616

1717
from . import commands
18+
from . import plugins
1819
from .internal import main
1920

2021
logging.basicConfig(
2122
format='%(name)s: [%(levelname)s] %(message)s',
2223
level=logging.DEBUG,
2324
)
2425

26+
plugins.discover()
27+
2528
cli = main

ci/fireci/fireci/plugins.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2018 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import importlib
16+
import pkgutil
17+
import fireciplugins
18+
19+
20+
def discover():
21+
"""Discovers fireci plugins available on PYTHONPATH under firebaseplugins subpackages.
22+
23+
Discovery works by importing all direct subpackages of firebaseplugins and importing them,
24+
plugins are supposed to register ci_command's with fireci in their __init__.py files directly
25+
or by importing from their own subpackages.
26+
27+
Note: plugins *must* define the `firebaseplugins` package as a namespace package.
28+
See: https://packaging.python.org/guides/packaging-namespace-packages/
29+
"""
30+
modules = pkgutil.iter_modules(fireciplugins.__path__,
31+
fireciplugins.__name__ + ".")
32+
for _, name, _ in modules:
33+
importlib.import_module(name)

ci/fireci/fireciplugins/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2018 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)

0 commit comments

Comments
 (0)