File: //snap/google-cloud-cli/394/platform/ext-runtime/nodejs/test/runtime_test.py
#!/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()