481 lines
20 KiB
Python
481 lines
20 KiB
Python
#!/usr/bin/python
|
|
|
|
import mock
|
|
import os
|
|
import re
|
|
import sys
|
|
import stat
|
|
import shutil
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
|
|
from gae_ext_runtime import ext_runtime
|
|
from gae_ext_runtime import testutil
|
|
|
|
RUNTIME_DEF_ROOT = os.path.dirname(os.path.dirname(__file__))
|
|
|
|
|
|
class RuntimeTests(testutil.TestBase):
|
|
|
|
def setUp(self):
|
|
self.runtime_def_root = RUNTIME_DEF_ROOT
|
|
super(RuntimeTests, self).setUp()
|
|
|
|
def read_dist_file(self, *args):
|
|
"""Read the entire contents of the file.
|
|
|
|
Returns the entire contents of the file identified by a set of
|
|
arguments forming a path relative to the root of the runtime
|
|
definition.
|
|
|
|
TODO: Move this down into the SDK.
|
|
|
|
Args:
|
|
*args: A set of path components (see full_path()). Note that
|
|
these are relative to the runtime definition root, not the
|
|
temporary directory.
|
|
"""
|
|
with open(os.path.join(self.runtime_def_root, *args)) as fp:
|
|
return fp.read()
|
|
|
|
def test_node_js_server_js_only(self):
|
|
self.write_file('server.js', 'fake contents')
|
|
self.generate_configs()
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='nodejs'))
|
|
|
|
self.generate_configs(deploy=True)
|
|
self.assert_file_exists_with_contents(
|
|
'Dockerfile',
|
|
self.read_dist_file('data', 'Dockerfile') + textwrap.dedent("""\
|
|
COPY . /app/
|
|
CMD node server.js
|
|
"""))
|
|
self.assert_file_exists_with_contents(
|
|
'.dockerignore',
|
|
self.read_dist_file('data', 'dockerignore'))
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'Dockerfile', '.dockerignore', 'app.yaml',
|
|
'server.js'})
|
|
|
|
def test_node_js_server_js_only_no_write(self):
|
|
"""Test generate_config_data with only .js files.
|
|
|
|
After running generate_configs(), app.yaml exists; after
|
|
generate_config_data(), only app.yaml should exist on disk --
|
|
Dockerfile and .dockerignore should be returned by the method."""
|
|
self.write_file('server.js', 'fake contents')
|
|
self.generate_configs()
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='nodejs'))
|
|
|
|
cfg_files = self.generate_config_data(deploy=True)
|
|
self.assert_genfile_exists_with_contents(
|
|
cfg_files,
|
|
'Dockerfile',
|
|
self.read_dist_file('data', 'Dockerfile') + textwrap.dedent("""\
|
|
COPY . /app/
|
|
CMD node server.js
|
|
"""))
|
|
self.assert_genfile_exists_with_contents(
|
|
cfg_files,
|
|
'.dockerignore',
|
|
self.read_dist_file('data', 'dockerignore'))
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'app.yaml', 'server.js'})
|
|
self.assertEqual({f.filename for f in cfg_files},
|
|
{'Dockerfile', '.dockerignore'})
|
|
|
|
def _validate_docker_files_for_npm(self):
|
|
base_dockerfile = self.read_dist_file('data', 'Dockerfile')
|
|
self.assert_file_exists_with_contents(
|
|
'Dockerfile',
|
|
base_dockerfile + 'COPY . /app/\n' +
|
|
self.read_dist_file('data', 'npm-package-json-install') +
|
|
'CMD npm start\n')
|
|
self.assert_file_exists_with_contents(
|
|
'.dockerignore',
|
|
self.read_dist_file('data', 'dockerignore'))
|
|
|
|
def test_node_js_package_json_npm(self):
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.generate_configs()
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='nodejs'))
|
|
self.generate_configs(deploy=True)
|
|
self._validate_docker_files_for_npm()
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'Dockerfile', '.dockerignore', 'app.yaml',
|
|
'foo.js', 'package.json'})
|
|
|
|
def _validate_docker_files_for_yarn(self):
|
|
base_dockerfile = self.read_dist_file('data', 'Dockerfile')
|
|
install_yarn = self.read_dist_file('data', 'install-yarn')
|
|
self.assert_file_exists_with_contents(
|
|
'Dockerfile',
|
|
base_dockerfile + install_yarn + 'COPY . /app/\n' +
|
|
self.read_dist_file('data', 'yarn-package-json-install') +
|
|
'CMD yarn start\n')
|
|
self.assert_file_exists_with_contents(
|
|
'.dockerignore',
|
|
self.read_dist_file('data', 'dockerignore'))
|
|
|
|
def test_node_js_package_json_yarn(self):
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.write_file('yarn.lock', 'yarn overridden')
|
|
self.generate_configs()
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='nodejs'))
|
|
self.generate_configs(deploy=True)
|
|
self._validate_docker_files_for_yarn()
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'Dockerfile', '.dockerignore', 'app.yaml',
|
|
'foo.js', 'package.json', 'yarn.lock'})
|
|
|
|
def _validate_file_list_for_skip_yarn_lock(self):
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'Dockerfile', '.dockerignore', 'yarn.lock',
|
|
'foo.js', 'package.json'})
|
|
|
|
def test_skip_yarn_lock_with_other_files(self):
|
|
"""Ensure use_yarn is False with yarn.lock present but is being skipped.
|
|
|
|
Further, this test verifies that use_yarn is False even if multiple
|
|
other entries are present in skip_files.
|
|
|
|
A yarn executable is injected that passes all checks to ensure that if
|
|
yarn.lock is set to be skipped, use_yarn is set to False even if yarn
|
|
can be executed and reports that the yarn.lock file is valid.
|
|
"""
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.write_file('foo.js', 'fake contents')
|
|
self.write_file('yarn.lock', 'fake contents')
|
|
config = testutil.AppInfoFake(runtime='nodejs',
|
|
skip_files=['^abc$',
|
|
'^xyz$',
|
|
'^yarn\.lock$',
|
|
'^node_modules$'])
|
|
configurator = self.detect(appinfo=config)
|
|
self.assertEqual(configurator.data['use_yarn'], False)
|
|
self.generate_configs(appinfo=config, deploy=True)
|
|
self._validate_docker_files_for_npm()
|
|
self._validate_file_list_for_skip_yarn_lock()
|
|
|
|
def test_only_skip_yarn_lock(self):
|
|
"""Ensure use_yarn is False with yarn.lock present but is being skipped.
|
|
|
|
Further, this test ensures use_yarn is false if the value obtained
|
|
from skip_files is a regex string and not a list of strings.
|
|
|
|
A yarn executable is injected that passes all checks to ensure that if
|
|
yarn.lock is set to be skipped, use_yarn is set to False even if yarn
|
|
can be executed and reports that the yarn.lock file is valid.
|
|
"""
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.write_file('foo.js', 'fake contents')
|
|
self.write_file('yarn.lock', 'fake contents')
|
|
config = testutil.AppInfoFake(runtime='nodejs',
|
|
skip_files='^yarn\.lock$')
|
|
configurator = self.detect(appinfo=config)
|
|
self.assertEqual(configurator.data['use_yarn'], False)
|
|
self.generate_configs(appinfo=config, deploy=True)
|
|
self._validate_docker_files_for_npm()
|
|
self._validate_file_list_for_skip_yarn_lock()
|
|
|
|
def test_do_not_skip_yarn_lock(self):
|
|
"""Ensure use_yarn is True with yarn.lock present and not skipped.
|
|
"""
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.write_file('foo.js', 'fake contents')
|
|
self.write_file('yarn.lock', 'fake contents')
|
|
# Here only 'node_modules' will be skipped
|
|
config = testutil.AppInfoFake(runtime='nodejs',
|
|
skip_files='^node_modules$')
|
|
configurator = self.detect(appinfo=config)
|
|
self.assertEqual(configurator.data['use_yarn'], True)
|
|
self.generate_configs(appinfo=config, deploy=True)
|
|
self._validate_docker_files_for_yarn()
|
|
self._validate_file_list_for_skip_yarn_lock()
|
|
|
|
def test_use_yarn_skip_files_not_present(self):
|
|
"""Ensure use_yarn is True with yarn.lock present and not skipped.
|
|
|
|
In particular, this test ensures use_yarn is True even if app.yaml
|
|
doesn't contain a skip_files section.
|
|
"""
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.write_file('foo.js', 'fake contents')
|
|
self.write_file('yarn.lock', 'fake contents')
|
|
config = testutil.AppInfoFake(runtime='nodejs')
|
|
configurator = self.detect(appinfo=config)
|
|
self.assertEqual(configurator.data['use_yarn'], True)
|
|
self.generate_configs(appinfo=config, deploy=True)
|
|
self._validate_docker_files_for_yarn()
|
|
self._validate_file_list_for_skip_yarn_lock()
|
|
|
|
def test_node_js_package_json_no_write(self):
|
|
"""Test generate_config_data with a nodejs file and package.json."""
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
self.generate_configs()
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='nodejs'))
|
|
|
|
cfg_files = self.generate_config_data(deploy=True)
|
|
|
|
base_dockerfile = self.read_dist_file('data', 'Dockerfile')
|
|
self.assert_genfile_exists_with_contents(
|
|
cfg_files,
|
|
'Dockerfile',
|
|
base_dockerfile + 'COPY . /app/\n' +
|
|
self.read_dist_file('data', 'npm-package-json-install') +
|
|
'CMD npm start\n')
|
|
self.assert_genfile_exists_with_contents(
|
|
cfg_files,
|
|
'.dockerignore',
|
|
self.read_dist_file('data', 'dockerignore'))
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'app.yaml', 'foo.js', 'package.json'})
|
|
self.assertEqual({f.filename for f in cfg_files},
|
|
{'Dockerfile', '.dockerignore'})
|
|
|
|
def test_detect_basic(self):
|
|
"""Ensure that appinfo will be generated in detect method."""
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
configurator = self.detect()
|
|
self.assertEqual(configurator.generated_appinfo,
|
|
{u'runtime': 'nodejs',
|
|
u'env': 'flex'})
|
|
|
|
def test_detect_custom(self):
|
|
"""Ensure that appinfo is correct with custom=True."""
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"start": "foo.js"}}')
|
|
configurator = self.detect(custom=True)
|
|
self.assertEqual(configurator.generated_appinfo,
|
|
{'runtime': 'custom',
|
|
'env': 'flex'})
|
|
|
|
def test_detect_no_start_no_server(self):
|
|
"""Ensure that detect fails if no scripts.start field, no server.js."""
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"not-start": "foo.js"}}')
|
|
configurator = self.detect()
|
|
self.assertEqual(configurator, None)
|
|
|
|
def test_detect_no_start_with_server(self):
|
|
"""Ensure appinfo generated if no scripts.start, server.js exists."""
|
|
self.write_file('server.js', 'bogus contents')
|
|
self.write_file('package.json', '{"scripts": {"not-start": "foo.js"}}')
|
|
configurator = self.detect()
|
|
self.assertEqual(configurator.generated_appinfo,
|
|
{'runtime': 'nodejs',
|
|
'env': 'flex'})
|
|
|
|
def test_node_js_with_engines(self):
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json',
|
|
'{"scripts": {"start": "foo.js"},'
|
|
'"engines": {"node": "0.12.3"}}')
|
|
self.generate_configs(deploy=True)
|
|
dockerfile_path = self.full_path('Dockerfile')
|
|
self.assertTrue(os.path.exists(dockerfile_path))
|
|
|
|
# This just verifies that the crazy node install line is generated, it
|
|
# says nothing about whether or not it works.
|
|
rx = re.compile(r'RUN npm install')
|
|
for line in open(dockerfile_path):
|
|
if rx.match(line):
|
|
break
|
|
else:
|
|
self.fail('node install line not generated')
|
|
|
|
def test_node_js_with_engines_no_write(self):
|
|
"""Test generate_config_data with 'engines' in package.json."""
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json',
|
|
'{"scripts": {"start": "foo.js"},'
|
|
'"engines": {"node": "0.12.3"}}')
|
|
cfg_files = self.generate_config_data(deploy=True)
|
|
self.assertIn('Dockerfile', [f.filename for f in cfg_files])
|
|
|
|
# This just verifies that the crazy node install line is generated, it
|
|
# says nothing about whether or not it works.
|
|
rx = re.compile(r'RUN npm install')
|
|
line_generated = False
|
|
for cfg_file in cfg_files:
|
|
if cfg_file.filename == 'Dockerfile':
|
|
for line in cfg_file.contents.split('\n'):
|
|
if rx.match(line):
|
|
line_generated = True
|
|
if not line_generated:
|
|
self.fail('node install line not generated')
|
|
|
|
def test_node_js_custom_runtime(self):
|
|
self.write_file('server.js', 'fake contents')
|
|
self.generate_configs(custom=True)
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='custom'))
|
|
self.assertEqual(sorted(os.listdir(self.temp_path)),
|
|
['.dockerignore', 'Dockerfile', 'app.yaml',
|
|
'server.js'])
|
|
|
|
def test_node_js_custom_runtime_no_write(self):
|
|
"""Test generate_config_data with custom runtime.
|
|
|
|
Should generate an app.yaml on disk, the Dockerfile and
|
|
.dockerignore in memory."""
|
|
self.write_file('server.js', 'fake contents')
|
|
cfg_files = self.generate_config_data(custom=True)
|
|
self.assert_file_exists_with_contents(
|
|
'app.yaml',
|
|
self.read_dist_file('data', 'app.yaml').format(runtime='custom'))
|
|
self.assertEqual(set(os.listdir(self.temp_path)),
|
|
{'app.yaml', 'server.js'})
|
|
self.assertEqual({f.filename for f in cfg_files},
|
|
{'Dockerfile', '.dockerignore'})
|
|
|
|
def test_node_js_runtime_field(self):
|
|
self.write_file('server.js', 'fake contents')
|
|
config = testutil.AppInfoFake(runtime='nodejs')
|
|
self.generate_configs(appinfo=config, deploy=True)
|
|
self.assertTrue(os.path.exists(self.full_path('Dockerfile')))
|
|
|
|
def test_node_js_custom_runtime_field(self):
|
|
self.write_file('server.js', 'fake contents')
|
|
config = testutil.AppInfoFake(runtime='custom')
|
|
self.assertTrue(self.generate_configs(appinfo=config, deploy=True))
|
|
|
|
def test_invalid_package_json(self):
|
|
self.write_file('package.json', '')
|
|
self.write_file('server.js', '')
|
|
self.generate_configs()
|
|
self.assertFalse(self.generate_configs())
|
|
|
|
# Tests that verify that the generated files match verbatim output.
|
|
# These will need to be maintained whenever the code generation changes,
|
|
# but this ensures that any diffs we introduce in the generate files will
|
|
# be reviewed.
|
|
|
|
def test_node_js_with_engines_retroactive(self):
|
|
self.write_file('foo.js', 'bogus contents')
|
|
self.write_file('package.json',
|
|
'{"scripts": {"start": "foo.js"},'
|
|
'"engines": {"node": "0.12.3"}}')
|
|
self.generate_configs(deploy=True)
|
|
self.assert_file_exists_with_contents(
|
|
'Dockerfile',
|
|
textwrap.dedent("""\
|
|
# Dockerfile extending the generic Node image with application files for a
|
|
# single application.
|
|
FROM gcr.io/google_appengine/nodejs
|
|
# Check to see if the the version included in the base runtime satisfies
|
|
# 0.12.3, if not then do an npm install of the latest available
|
|
# version that satisfies it.
|
|
RUN /usr/local/bin/install_node 0.12.3
|
|
COPY . /app/
|
|
# You have to specify "--unsafe-perm" with npm install
|
|
# when running as root. Failing to do this can cause
|
|
# install to appear to succeed even if a preinstall
|
|
# script fails, and may have other adverse consequences
|
|
# as well.
|
|
# This command will also cat the npm-debug.log file after the
|
|
# build, if it exists.
|
|
RUN npm install --unsafe-perm || \\
|
|
((if [ -f npm-debug.log ]; then \\
|
|
cat npm-debug.log; \\
|
|
fi) && false)
|
|
CMD npm start
|
|
"""))
|
|
|
|
|
|
class FailureLoggingTests(testutil.TestBase):
|
|
|
|
def setUp(self):
|
|
self.runtime_def_root = RUNTIME_DEF_ROOT
|
|
super(FailureLoggingTests, self).setUp()
|
|
|
|
self.errors = []
|
|
self.debug = []
|
|
self.warnings = []
|
|
|
|
def error_fake(self, message):
|
|
self.errors.append(message)
|
|
|
|
def debug_fake(self, message):
|
|
self.debug.append(message)
|
|
|
|
def warn_fake(self, message):
|
|
self.warnings.append(message)
|
|
|
|
def test_invalid_package_json(self):
|
|
self.write_file('package.json', '')
|
|
self.write_file('server.js', '')
|
|
|
|
variations = [
|
|
(testutil.AppInfoFake(runtime='nodejs'), None),
|
|
(None, 'nodejs'),
|
|
(None, None)
|
|
]
|
|
for appinfo, runtime in variations:
|
|
self.errors = []
|
|
with mock.patch.dict(ext_runtime._LOG_FUNCS,
|
|
{'error': self.error_fake}):
|
|
self.generate_configs(appinfo=appinfo, runtime=runtime)
|
|
|
|
self.assertTrue(self.errors[0].startswith(
|
|
'node.js checker: error accessing package.json'))
|
|
|
|
def test_no_startup_script(self):
|
|
with mock.patch.dict(ext_runtime._LOG_FUNCS,
|
|
{'debug': self.debug_fake}):
|
|
self.generate_configs()
|
|
print self.debug
|
|
self.assertTrue(self.debug[1].startswith(
|
|
'node.js checker: Neither "start" in the "scripts" section '
|
|
'of "package.json" nor the "server.js" file were found.'))
|
|
|
|
variations = [
|
|
(testutil.AppInfoFake(runtime='nodejs'), None),
|
|
(None, 'nodejs')
|
|
]
|
|
for appinfo, runtime in variations:
|
|
self.errors = []
|
|
with mock.patch.dict(ext_runtime._LOG_FUNCS,
|
|
{'error': self.error_fake}):
|
|
self.generate_configs(appinfo=appinfo, runtime=runtime)
|
|
self.assertTrue(self.errors[0].startswith(
|
|
'node.js checker: Neither "start" in the "scripts" section '
|
|
'of "package.json" nor the "server.js" file were found.'))
|
|
|
|
def test_package_json_no_startup_script(self):
|
|
self.write_file('package.json', '{"scripts": {"not-start": "foo.js"}}')
|
|
|
|
variations = [
|
|
(testutil.AppInfoFake(runtime='nodejs'), None),
|
|
(None, 'nodejs'),
|
|
(None, None)
|
|
]
|
|
for appinfo, runtime in variations:
|
|
self.errors = []
|
|
with mock.patch.dict(ext_runtime._LOG_FUNCS,
|
|
{'error': self.error_fake}):
|
|
self.generate_configs(appinfo=appinfo, runtime=runtime)
|
|
self.assertTrue(self.errors[0].startswith(
|
|
'node.js checker: Neither "start" in the "scripts" section '
|
|
'of "package.json" nor the "server.js" file were found.'))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|