diff --git a/.pylintrc b/.pylintrc index e180b03020..4230edc650 100644 --- a/.pylintrc +++ b/.pylintrc @@ -304,7 +304,7 @@ valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. -exclude-protected=_asdict,_fields,_replace,_source,_make,_thread +exclude-protected=_asdict,_fields,_replace,_source,_make [DESIGN] diff --git a/chalice/cli/__init__.py b/chalice/cli/__init__.py index 89ae3fe3c1..3eebad6ab4 100644 --- a/chalice/cli/__init__.py +++ b/chalice/cli/__init__.py @@ -8,35 +8,17 @@ import sys import tempfile import shutil -<<<<<<< HEAD -<<<<<<< HEAD import traceback +import functools import botocore.exceptions import click from typing import Dict, Any, Optional # noqa -======= -import time -import subprocess -import threading -import types -======= ->>>>>>> Extract reloader into separate module - -import botocore.exceptions -import click -from typing import Dict, Any, Optional, MutableMapping # noqa -<<<<<<< HEAD -from six.moves import _thread ->>>>>>> Add auto reloading stuff -======= ->>>>>>> Extract reloader into separate module from chalice import __version__ as chalice_version from chalice.app import Chalice # noqa from chalice.awsclient import TypedAWSClient from chalice.cli.factory import CLIFactory -from chalice.cli.reload import Reloader from chalice.config import Config # noqa from chalice.logs import display_logs from chalice.utils import create_zip_file @@ -45,7 +27,7 @@ from chalice.constants import CONFIG_VERSION, TEMPLATE_APP, GITIGNORE from chalice.constants import DEFAULT_STAGE_NAME from chalice.constants import DEFAULT_APIGATEWAY_STAGE_NAME -from chalice.constants import DEFAULT_AUTORELOAD_INTERVAL +from chalice.local import LocalDevServer # noqa def create_new_project_skeleton(project_name, profile=None): @@ -97,88 +79,40 @@ def cli(ctx, project_dir, debug=False): @click.option('--port', default=8000, type=click.INT) @click.option('--stage', default=DEFAULT_STAGE_NAME, help='Name of the Chalice stage for the local server to use.') -@click.option('--autoreload-interval', default=DEFAULT_AUTORELOAD_INTERVAL, - type=click.INT) -@click.option('--no-autoreload', is_flag=True) +@click.option('--autoreload/--no-autoreload', + default=True, + help='Automatically restart server when code changes.') @click.pass_context def local(ctx, host='127.0.0.1', port=8000, stage=DEFAULT_STAGE_NAME, - autoreload_interval=DEFAULT_AUTORELOAD_INTERVAL, - no_autoreload=False): - # type: (click.Context, str, int, str, int, bool) -> None + autoreload=True): + # type: (click.Context, str, int, str, bool) -> None factory = ctx.obj['factory'] # type: CLIFactory -<<<<<<< HEAD -<<<<<<< HEAD + from chalice.cli import reloader + # We don't create the server here because that will bind the + # socket and we only want to do this in the worker process. + server_factory = functools.partial( + create_local_server, factory, host, port, stage) + # When running `chalice local`, a stdout logger is configured + # so you'll see the same stdout logging as you would when + # running in lambda. This is configuring the root logger. + # The app-specific logger (app.log) will still continue + # to work. + logging.basicConfig( + stream=sys.stdout, level=logging.INFO, format='%(message)s') + if autoreload: + project_dir = factory.create_config_obj( + chalice_stage_name=stage).project_dir + rc = reloader.run_with_reloader( + server_factory, os.environ, project_dir) + # Click doesn't sys.exit() with the RC this function. The + # recommended way to do this is to use sys.exit() directly, + # see: https://github.com/pallets/click/issues/747 + sys.exit(rc) run_local_server(factory, host, port, stage) -======= - with Reloader(autoreload): -======= - if no_autoreload: - run_local_server(factory, host, port, stage, os.environ) -<<<<<<< HEAD - with Reloader(): ->>>>>>> Enable autoreloading by default -======= - with Reloader(autoreload_interval): ->>>>>>> Add --autoreload-interval parameter - run_local_server(factory, host, port, stage, os.environ) - - -<<<<<<< HEAD -class Reloader(threading.Thread): - def __init__(self, autoreload=True): - super(Reloader, self).__init__() - self.autoreload = autoreload - self.triggered = False - self.mtimes = {} - - def __enter__(self): - if self.autoreload: - self.setDaemon(True) - self.start() - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type is KeyboardInterrupt and self.triggered: - sys.exit(1) - - def run(self): - while True: - time.sleep(1) - if self.is_changes(): - self.reload() - - def is_changes(self): - for module in list(sys.modules.values()): - if not isinstance(module, types.ModuleType): - continue - path = getattr(module, '__file__', None) - if not path: - continue - if path.endswith('.pyc') or path.endswith('.pyo'): - path = path[:-1] - try: - mtime = os.path.getmtime(path) - except OSError: - continue - old_time = self.mtimes.setdefault(path, mtime) - if mtime > old_time: - return True - - def reload(self): - self.triggered = True - if sys.platform == 'win32': - subprocess.Popen(sys.argv, close_fds=True) - _thread.interrupt_main() - else: - os.execv(sys.executable, [sys.executable] + sys.argv) ->>>>>>> Add auto reloading stuff -def run_local_server(factory, host, port, stage): - # type: (CLIFactory, str, int, str) -> None -======= -def run_local_server(factory, host, port, stage, env): - # type: (CLIFactory, str, int, str, MutableMapping) -> None ->>>>>>> Extract reloader into separate module +def create_local_server(factory, host, port, stage): + # type: (CLIFactory, str, int, str) -> LocalDevServer config = factory.create_config_obj( chalice_stage_name=stage ) @@ -187,13 +121,13 @@ def run_local_server(factory, host, port, stage, env): # there is no point in testing locally. routes = config.chalice_app.routes validate_routes(routes) - # When running `chalice local`, a stdout logger is configured - # so you'll see the same stdout logging as you would when - # running in lambda. This is configuring the root logger. - # The app-specific logger (app.log) will still continue - # to work. - logging.basicConfig(stream=sys.stdout) server = factory.create_local_server(app_obj, config, host, port) + return server + + +def run_local_server(factory, host, port, stage): + # type: (CLIFactory, str, int, str) -> None + server = create_local_server(factory, host, port, stage) server.serve_forever() diff --git a/chalice/cli/reload.py b/chalice/cli/reload.py deleted file mode 100644 index 8305c46878..0000000000 --- a/chalice/cli/reload.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import sys -import threading -import time -import types - -from typing import Optional, Dict, Type # noqa - -from chalice.compat import reload_process - - -class Reloader(threading.Thread): - def __init__(self, autoreload_interval): - # type: (int) -> None - super(Reloader, self).__init__() - self.autoreload_interval = autoreload_interval - self.triggered = False - self.mtimes = {} # type: Dict[str, float] - - def __enter__(self): - # type: () -> Reloader - self.setDaemon(True) - self.start() - return self - - def __exit__(self, - exc_type, # type: Optional[Type[BaseException]] - exc_value, # type: Optional[BaseException] - traceback # type: Optional[types.TracebackType] - ): - # type: (...) -> None - if exc_type is KeyboardInterrupt and self.triggered: - sys.exit(1) - - def run(self): - # type: () -> None - while True: - time.sleep(self.autoreload_interval) - if self.find_changes(): - self.reload() - - def find_changes(self): - # type: () -> bool - for module in list(sys.modules.values()): - if not isinstance(module, types.ModuleType): - continue - path = getattr(module, '__file__', None) - if not path: - continue - if path.endswith('.pyc') or path.endswith('.pyo'): - path = path[:-1] - try: - mtime = os.path.getmtime(path) - except OSError: - continue - old_time = self.mtimes.setdefault(path, mtime) - if mtime > old_time: - return True - return False - - def reload(self): - # type: () -> None - self.triggered = True - reload_process() diff --git a/chalice/cli/reloader.py b/chalice/cli/reloader.py new file mode 100644 index 0000000000..91cef21de6 --- /dev/null +++ b/chalice/cli/reloader.py @@ -0,0 +1,168 @@ +"""Automatically reload chalice app when files change. + +How It Works +============ + +This approach borrow from what django, flask, and other frameworks do. +Essentially, with reloading enabled ``chalice local`` will actually start up +two processes (both will show as ``chalice local`` in ps). One process is the +parent process. It's job is to start up a child process and restart it +if it exits (due to a restart request). The child process is the process +that actually starts up the web server for local mode. The child process +also sets up a watcher thread. It's job is to monitor directories for +changes. If a change is encountered it sys.exit()s the process with a known +RC (the RESTART_REQUEST_RC constant in the module). + +The parent process runs in an infinite loop. If the child process exits with +an RC of RESTART_REQUEST_RC the parent process starts up another child process. + +The child worker is denoted by setting the ``CHALICE_WORKER`` env var. +If this env var is set, the process is intended to be a worker process (as +opposed the parent process which just watches for restart requests from the +worker process). + +""" +import subprocess +import threading +import logging +import copy +import sys + +import watchdog.observers +from watchdog.events import FileSystemEventHandler +from watchdog.events import FileSystemEvent # noqa +from typing import MutableMapping, Type, Callable, Optional # noqa + +from chalice.local import LocalDevServer # noqa + + +RESTART_REQUEST_RC = 3 +LOGGER = logging.getLogger(__name__) + + +def start_parent_process(env): + # type: (MutableMapping) -> None + process = ParentProcess(env, subprocess.Popen) + process.main() + + +class Restarter(FileSystemEventHandler): + + def __init__(self, restart_event): + # type: (threading.Event) -> None + # The reason we're using threading + self.restart_event = restart_event + + def on_any_event(self, event): + # type: (FileSystemEvent) -> None + # If we modify a file we'll get a FileModifiedEvent + # as well as a DirectoryModifiedEvent. + # We only care about reloading is a file is modified. + if event.is_directory: + return + self.restart_event.set() + + +def start_worker_process(server_factory, root_dir): + # type: (Callable[[], LocalDevServer], str) -> int + t = HTTPServerThread(server_factory) + worker = WorkerProcess(t) + LOGGER.debug("Starting worker...") + rc = worker.main(root_dir) + LOGGER.info("Restarting local dev server.") + return rc + + +class HTTPServerThread(threading.Thread): + """Thread that manages starting/stopping local HTTP server. + + This is a small wrapper around a normal threading.Thread except + that it adds shutdown capability of the HTTP server, which is + not part of the normal threading.Thread interface. + + """ + def __init__(self, server_factory): + # type: (Callable[[], LocalDevServer]) -> None + threading.Thread.__init__(self) + self._server_factory = server_factory + self._server = None # type: Optional[LocalDevServer] + self.daemon = True + + def run(self): + # type: () -> None + self._server = self._server_factory() + self._server.serve_forever() + + def shutdown(self): + # type: () -> None + if self._server is not None: + self._server.shutdown() + + +class ParentProcess(object): + """Spawns a child process and restarts it as needed.""" + def __init__(self, env, popen): + # type: (MutableMapping, Type[subprocess.Popen]) -> None + self._env = copy.copy(env) + self._popen = popen + + def main(self): + # type: () -> None + # This method launches a child worker and restarts it if it + # exists with RESTART_REQUEST_RC. This method doesn't return. + # A user can Ctrl-C to stop the parent process. + while True: + self._env['CHALICE_WORKER'] = 'true' + LOGGER.debug("Parent process starting child worker process...") + process = self._popen(sys.argv, env=self._env) + try: + process.communicate() + if process.returncode != RESTART_REQUEST_RC: + return + except KeyboardInterrupt: + process.terminate() + raise + + +class WorkerProcess(object): + """Worker that runs the chalice dev server.""" + def __init__(self, http_thread): + # type: (HTTPServerThread) -> None + self._http_thread = http_thread + self._restart_event = threading.Event() + + def main(self, project_dir, timeout=None): + # type: (str, Optional[int]) -> int + self._http_thread.start() + self._start_file_watcher(project_dir) + if self._restart_event.wait(timeout): + self._http_thread.shutdown() + return RESTART_REQUEST_RC + return 0 + + def _start_file_watcher(self, project_dir): + # type: (str) -> None + observer = watchdog.observers.Observer() + restarter = Restarter(self._restart_event) + observer.schedule(restarter, project_dir, recursive=True) + observer.start() + + +def run_with_reloader(server_factory, env, root_dir): + # type: (Callable, MutableMapping, str) -> int + # This function is invoked in two possible modes, as the parent process + # or as a chalice worker. + try: + if env.get('CHALICE_WORKER') is not None: + # This is a chalice worker. We need to start the main dev server + # in a daemon thread and install a file watcher. + return start_worker_process(server_factory, root_dir) + else: + # This is the parent process. It's just is to spawn an identical + # process but with the ``CHALICE_WORKER`` env var set. It then + # will monitor this process and restart it if it exits with a + # RESTART_REQUEST exit code. + start_parent_process(env) + except KeyboardInterrupt: + pass + return 0 diff --git a/chalice/local.py b/chalice/local.py index f00da45c80..af45adebc9 100644 --- a/chalice/local.py +++ b/chalice/local.py @@ -631,3 +631,9 @@ def serve_forever(self): # type: () -> None print("Serving on %s:%s" % (self.host, self.port)) self.server.serve_forever() + + def shutdown(self): + # type: () -> None + # This must be called from another thread of else it + # will deadlock. + self.server.shutdown() diff --git a/tests/functional/basicapp/.chalice/config.json b/tests/functional/basicapp/.chalice/config.json new file mode 100644 index 0000000000..86a8c21607 --- /dev/null +++ b/tests/functional/basicapp/.chalice/config.json @@ -0,0 +1,9 @@ +{ + "version": "2.0", + "app_name": "basicapp", + "stages": { + "dev": { + "api_gateway_stage": "api" + } + } +} diff --git a/tests/functional/basicapp/.gitignore b/tests/functional/basicapp/.gitignore new file mode 100644 index 0000000000..3dd60a9721 --- /dev/null +++ b/tests/functional/basicapp/.gitignore @@ -0,0 +1,2 @@ +.chalice/deployments/ +.chalice/venv/ diff --git a/tests/functional/basicapp/app.py b/tests/functional/basicapp/app.py new file mode 100644 index 0000000000..ee78431401 --- /dev/null +++ b/tests/functional/basicapp/app.py @@ -0,0 +1,8 @@ +from chalice import Chalice + +app = Chalice(app_name='basicapp') + + +@app.route('/') +def index(): + return {'version': 'original'} diff --git a/tests/functional/basicapp/requirements.txt b/tests/functional/basicapp/requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/functional/cli/test_reloader.py b/tests/functional/cli/test_reloader.py new file mode 100644 index 0000000000..34f3be06fd --- /dev/null +++ b/tests/functional/cli/test_reloader.py @@ -0,0 +1,46 @@ +import mock +import threading + +from chalice.cli import reloader + + +DEFAULT_DELAY = 0.1 +MAX_TIMEOUT = 5.0 + + +def modify_file_after_n_seconds(filename, contents, delay=DEFAULT_DELAY): + t = threading.Timer(delay, function=modify_file, args=(filename, contents)) + t.daemon = True + t.start() + + +def modify_file(filename, contents): + if filename is None: + return + with open(filename, 'w') as f: + f.write(contents) + + +def assert_reload_happens(root_dir, when_modified_file): + http_thread = mock.Mock(spec=reloader.HTTPServerThread) + p = reloader.WorkerProcess(http_thread) + modify_file_after_n_seconds(when_modified_file, 'contents') + rc = p.main(root_dir, MAX_TIMEOUT) + assert rc == reloader.RESTART_REQUEST_RC + + +def test_can_reload_when_file_created(tmpdir): + top_level_file = str(tmpdir.join('foo')) + assert_reload_happens(str(tmpdir), when_modified_file=top_level_file) + + +def test_can_reload_when_subdir_file_created(tmpdir): + subdir_file = str(tmpdir.join('subdir').mkdir().join('foo.txt')) + assert_reload_happens(str(tmpdir), when_modified_file=subdir_file) + + +def test_rc_0_when_no_file_modified(tmpdir): + http_thread = mock.Mock(spec=reloader.HTTPServerThread) + p = reloader.WorkerProcess(http_thread) + rc = p.main(str(tmpdir), timeout=0.2) + assert rc == 0 diff --git a/tests/functional/test_awsclient.py b/tests/functional/test_awsclient.py index 174e29433a..3b1f48ee88 100644 --- a/tests/functional/test_awsclient.py +++ b/tests/functional/test_awsclient.py @@ -450,27 +450,6 @@ def test_create_function_with_memory_size(self, stubbed_session): memory_size=256) == 'arn:12345:name' stubbed_session.verify_stubs() - def test_create_function_with_vpc_config(self, stubbed_session): - stubbed_session.stub('lambda').create_function( - FunctionName='name', - Runtime='python2.7', - Code={'ZipFile': b'foo'}, - Handler='app.app', - Role='myarn', - VpcConfig={ - 'SecurityGroupIds': ['sg1', 'sg2'], - 'SubnetIds': ['sn1', 'sn2'] - } - ).returns({'FunctionArn': 'arn:12345:name'}) - stubbed_session.activate_stubs() - awsclient = TypedAWSClient(stubbed_session) - assert awsclient.create_function( - 'name', 'myarn', b'foo', 'python2.7', 'app.app', - subnet_ids=['sn1', 'sn2'], - security_group_ids=['sg1', 'sg2'], - ) == 'arn:12345:name' - stubbed_session.verify_stubs() - def test_create_function_is_retried_and_succeeds(self, stubbed_session): kwargs = { 'FunctionName': 'name', @@ -498,35 +477,6 @@ def test_create_function_is_retried_and_succeeds(self, stubbed_session): 'python2.7', 'app.app') == 'arn:12345:name' stubbed_session.verify_stubs() - def test_retry_happens_on_insufficient_permissions(self, stubbed_session): - # This can happen if we deploy a lambda in a VPC. Instead of the role - # not being able to be assumed, we can instead not have permissions - # to modify ENIs. These can be retried. - kwargs = { - 'FunctionName': 'name', - 'Runtime': 'python2.7', - 'Code': {'ZipFile': b'foo'}, - 'Handler': 'app.app', - 'Role': 'myarn', - 'VpcConfig': {'SubnetIds': ['sn-1'], - 'SecurityGroupIds': ['sg-1']}, - } - stubbed_session.stub('lambda').create_function( - **kwargs).raises_error( - error_code='InvalidParameterValueException', - message=('The provided execution role does not have permissions ' - 'to call CreateNetworkInterface on EC2 be assumed by ' - 'Lambda.')) - stubbed_session.stub('lambda').create_function( - **kwargs).returns({'FunctionArn': 'arn:12345:name'}) - stubbed_session.activate_stubs() - awsclient = TypedAWSClient(stubbed_session, mock.Mock(spec=time.sleep)) - assert awsclient.create_function( - 'name', 'myarn', b'foo', - 'python2.7', 'app.app', security_group_ids=['sg-1'], - subnet_ids=['sn-1']) == 'arn:12345:name' - stubbed_session.verify_stubs() - def test_create_function_fails_after_max_retries(self, stubbed_session): kwargs = { 'FunctionName': 'name', @@ -752,25 +702,6 @@ def test_update_function_code_with_memory(self, stubbed_session): awsclient.update_function('name', b'foo', memory_size=256) stubbed_session.verify_stubs() - def test_update_function_with_vpc_config(self, stubbed_session): - lambda_client = stubbed_session.stub('lambda') - lambda_client.update_function_code( - FunctionName='name', ZipFile=b'foo').returns({}) - lambda_client.update_function_configuration( - FunctionName='name', VpcConfig={ - 'SecurityGroupIds': ['sg1', 'sg2'], - 'SubnetIds': ['sn1', 'sn2'] - } - ).returns({}) - stubbed_session.activate_stubs() - awsclient = TypedAWSClient(stubbed_session) - awsclient.update_function( - 'name', b'foo', - subnet_ids=['sn1', 'sn2'], - security_group_ids=['sg1', 'sg2'], - ) - stubbed_session.verify_stubs() - def test_update_function_with_adding_tags(self, stubbed_session): function_arn = 'arn' diff --git a/tests/functional/test_local.py b/tests/functional/test_local.py index bedb43c9de..04ea9362a7 100644 --- a/tests/functional/test_local.py +++ b/tests/functional/test_local.py @@ -1,5 +1,6 @@ import os import socket +import time import contextlib from threading import Thread from threading import Event @@ -15,11 +16,24 @@ from chalice import app from chalice.local import LocalDevServer from chalice.config import Config +from chalice.utils import OSUtils -ENV_APP_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'envapp', -) +APPS_DIR = os.path.dirname(os.path.abspath(__file__)) +ENV_APP_DIR = os.path.join(APPS_DIR, 'envapp') +BASIC_APP = os.path.join(APPS_DIR, 'basicapp') + + +NEW_APP_VERSION = """ +from chalice import Chalice + +app = Chalice(app_name='basicapp') + + +@app.route('/') +def index(): + return {'version': 'reloaded'} +""" @contextmanager @@ -32,6 +46,13 @@ def cd(path): os.chdir(original_dir) +@pytest.fixture() +def basic_app(tmpdir): + tmpdir = str(tmpdir.mkdir('basicapp')) + OSUtils().copytree(BASIC_APP, tmpdir) + return tmpdir + + class ThreadedLocalServer(Thread): def __init__(self, port, host='localhost'): super(ThreadedLocalServer, self).__init__() @@ -77,6 +98,30 @@ def unused_tcp_port(): return sock.getsockname()[1] +@pytest.fixture() +def http_session(): + session = requests.Session() + retry = Retry( + # How many connection-related errors to retry on. + connect=5, + # How many connection-related errors to retry on. + # A backoff factor to apply between attempts after the second try. + backoff_factor=2, + ) + session.mount('http://', HTTPAdapter(max_retries=retry)) + return HTTPFetcher(session) + + +class HTTPFetcher(object): + def __init__(self, session): + self.session = session + + def json_get(self, url): + response = self.session.get(url) + response.raise_for_status() + return json.loads(response.content) + + @pytest.fixture() def local_server_factory(unused_tcp_port): threaded_server = ThreadedLocalServer(unused_tcp_port) @@ -167,7 +212,7 @@ def test_can_accept_multiple_connections(config, sample_app, assert response.text == '{"hello": "world"}' -def test_can_import_env_vars(unused_tcp_port): +def test_can_import_env_vars(unused_tcp_port, http_session): with cd(ENV_APP_DIR): p = subprocess.Popen(['chalice', 'local', '--port', str(unused_tcp_port)], @@ -175,7 +220,7 @@ def test_can_import_env_vars(unused_tcp_port): stderr=subprocess.PIPE) _wait_for_server_ready(p) try: - _assert_env_var_loaded(unused_tcp_port) + _assert_env_var_loaded(unused_tcp_port, http_session) finally: p.terminate() @@ -187,21 +232,25 @@ def _wait_for_server_ready(process): ) -def _assert_env_var_loaded(port_number): - session = _get_requests_session_with_timeout() - response = session.get('http://localhost:%s/' % port_number) - response.raise_for_status() - assert json.loads(response.content) == {'hello': 'bar'} +def _assert_env_var_loaded(port_number, http_session): + response = http_session.json_get('http://localhost:%s/' % port_number) + assert response == {'hello': 'bar'} -def _get_requests_session_with_timeout(): - session = requests.Session() - retry = Retry( - # How many connection-related errors to retry on. - connect=5, - # How many connection-related errors to retry on. - # A backoff factor to apply between attempts after the second try. - backoff_factor=2, - ) - session.mount('http://', HTTPAdapter(max_retries=retry)) - return session +def test_can_reload_server(unused_tcp_port, basic_app, http_session): + with cd(basic_app): + p = subprocess.Popen(['chalice', 'local', '--port', + str(unused_tcp_port)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + _wait_for_server_ready(p) + url = 'http://localhost:%s/' % unused_tcp_port + try: + assert http_session.json_get(url) == {'version': 'original'} + # Updating the app should trigger a reload. + with open(os.path.join(basic_app, 'app.py'), 'w') as f: + f.write(NEW_APP_VERSION) + time.sleep(2) + assert http_session.json_get(url) == {'version': 'reloaded'} + finally: + p.terminate() diff --git a/tests/functional/test_reload.py b/tests/functional/test_reload.py deleted file mode 100644 index 7f78473bb8..0000000000 --- a/tests/functional/test_reload.py +++ /dev/null @@ -1,27 +0,0 @@ -import subprocess -import tempfile -import time -import sys - -code = b""" -from chalice.cli.reload import Reloader - -with Reloader(0) as r: - r.join() -""" - -modified_code = b'print("reloaded")' - - -def test_reload(): - with tempfile.NamedTemporaryFile() as program_file: - program_file.write(code) - program_file.flush() - args = [sys.executable, program_file.name] - program = subprocess.Popen(args, stdout=subprocess.PIPE) - time.sleep(1) - program_file.seek(0) - program_file.truncate() - program_file.write(modified_code) - program_file.flush() - assert b'reloaded' in program.stdout.read()