# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""
Noxfile used with nox-automation to run tests across all samples.
Use nox -l to see all possible sessions.
In general, you'll want to run:
nox -s lint
# or
nox -s list -- /path/to/sample/dir
And:
nox -s tests -- /path/to/sample/dir
"""
import fnmatch
import os
import subprocess
import tempfile
import nox
# Location of our common testing utilities. This isn't published to PyPI.
REPO_TOOLS_REQ =\
'git+https://github.com/GoogleCloudPlatform/python-repo-tools.git'
# Arguments used for every invocation of py.test.
COMMON_PYTEST_ARGS = [
'-x', '--no-success-flaky-report', '--cov', '--cov-config',
'.coveragerc', '--cov-append', '--cov-report=']
# Libraries that only work on Python 2.7
PY27_ONLY_LIBRARIES = ['mysql-python']
# Whether we're running on Travis CI
ON_TRAVIS = os.environ.get('TRAVIS', False)
def list_files(folder, pattern):
"""Lists all files below the given folder that match the pattern."""
for root, folders, files in os.walk(folder):
for filename in files:
if fnmatch.fnmatch(filename, pattern):
yield os.path.join(root, filename)
def collect_sample_dirs(start_dir, blacklist=set()):
"""Recursively collects a list of dirs that contain tests.
This works by listing the contents of directories and finding
directories that have `*_test.py` files.
"""
# Collect all the directories that have tests in them.
for parent, subdirs, files in os.walk(start_dir):
if any(f for f in files if f[-8:] == '_test.py'):
# Don't recurse further, since py.test will do that.
del subdirs[:]
# This dir has tests in it. yield it.
yield parent
else:
# Filter out dirs we don't want to recurse into
subdirs[:] = [s for s in subdirs
if s[0].isalpha() and
os.path.join(parent, s) not in blacklist]
def get_changed_files():
"""Uses travis environment variables to determine which files
have changed for this pull request / push."""
# Debug info
print('TRAVIS_PULL_REQUEST: {}'.format(
os.environ.get('TRAVIS_PULL_REQUEST')))
print('TRAVIS_COMMIT: {}'.format(os.environ.get('TRAVIS_COMMIT')))
print('TRAVIS_BRANCH: {}'.format(os.environ.get('TRAVIS_BRANCH')))
pr = os.environ.get('TRAVIS_PULL_REQUEST')
if pr == 'false':
# This is not a pull request.
changed = subprocess.check_output(
['git', 'show', '--pretty=format:', '--name-only',
os.environ.get('TRAVIS_COMMIT')])
elif pr is not None:
try:
changed = subprocess.check_output(
['git', 'diff', '--name-only',
os.environ.get('TRAVIS_COMMIT'),
os.environ.get('TRAVIS_BRANCH')])
except subprocess.CalledProcessError:
# Fallback to git head.
git_head = subprocess.check_output(
['git', 'rev-parse', 'HEAD']).strip()
print('Falling back to HEAD: {}'.format(git_head))
changed = subprocess.check_output(
['git', 'diff', '--name-only',
git_head,
os.environ.get('TRAVIS_BRANCH')])
else:
changed = ''
print('Uh... where are we?')
return set([x for x in changed.split('\n') if x])
def filter_samples(sample_dirs, changed_files):
"""Filers the list of sample directories to only include directories that
contain changed files."""
result = []
for sample_dir in sample_dirs:
if sample_dir.startswith('./'):
sample_dir = sample_dir[2:]
for changed_file in changed_files:
if changed_file.startswith(sample_dir):
result.append(sample_dir)
return list(set(result))
def setup_appengine(session):
"""Installs the App Engine SDK."""
# Install the app engine sdk and setup import paths.
if session.interpreter.startswith('python3'):
return
gae_root = os.environ.get('GAE_ROOT', tempfile.gettempdir())
session.env['GAE_SDK_PATH'] = os.path.join(gae_root, 'google_appengine')
session.run('gcprepotools', 'download-appengine-sdk', gae_root)
# Create a lib directory to prevent the GAE vendor library from
# complaining.
if not os.path.exists('lib'):
os.makedirs('lib')
def run_tests_in_sesssion(
session, interpreter, sample_directories, use_appengine=True,
skip_flaky=False):
"""This is the main function for executing tests.
It:
1. Install the common testing utilities.
2. Installs the test requirements.
3. Determines which pytest arguments to use. skip_flaky causes extra
arguments to be passed that will skip tests marked flaky.
7. For each sample directory, it runs py.test.
"""
session.interpreter = interpreter
session.install(REPO_TOOLS_REQ)
session.install('-r', 'requirements-dev.txt')
if use_appengine:
setup_appengine(session)
pytest_args = COMMON_PYTEST_ARGS[:]
if skip_flaky:
pytest_args.append('-m not slow and not flaky')
for sample in sample_directories:
# Ignore lib and env directories
ignore_args = [
'--ignore', os.path.join(sample, 'lib'),
'--ignore', os.path.join(sample, 'env')]
# output junit report.
junit_args = ['--junitxml', os.path.join(sample, 'junit.xml')]
session.run(
'py.test', sample,
*(pytest_args + ignore_args + junit_args),
success_codes=[0, 5]) # Treat no test collected as success.
@nox.parametrize('interpreter', ['python2.7', 'python3.4'])
def session_tests(session, interpreter):
"""Runs tests for all non-gae standard samples."""
# session.posargs is any leftover arguments from the command line,
# which allows users to run a particular test instead of all of them.
sample_directories = session.posargs
if not sample_directories:
sample_directories = collect_sample_dirs('.')
run_tests_in_sesssion(
session, interpreter, sample_directories)
def session_gae(session):
"""Runs test for GAE Standard samples."""
sample_directories = collect_sample_dirs('appengine/standard')
run_tests_in_sesssion(
session, 'python2.7', sample_directories, use_appengine=True)
@nox.parametrize('subsession', ['gae', 'tests'])
def session_travis(session, subsession):
"""On travis, just run with python3.4 and don't run slow or flaky tests."""
if subsession == 'tests':
interpreter = 'python3.4'
sample_directories = collect_sample_dirs(
'.', set('./appengine/standard'))
elif subsession == 'gae':
interpreter = 'python2.7'
sample_directories = collect_sample_dirs('appengine/standard')
changed_files = get_changed_files()
sample_directories = filter_samples(
sample_directories, changed_files)
if not sample_directories:
print('No samples changed.')
return
print('Running tests on a subset of samples: ')
print('\n'.join(sample_directories))
run_tests_in_sesssion(
session, interpreter, sample_directories, skip_flaky=True)
def session_lint(session):
"""Lints each sample."""
sample_directories = session.posargs
if not sample_directories:
sample_directories = collect_sample_dirs('.')
# On travis, on lint changed samples.
if ON_TRAVIS:
changed_files = get_changed_files()
sample_directories = filter_samples(
sample_directories, changed_files)
session.install('flake8', 'flake8-import-order')
for sample_directory in sample_directories:
# Determine local import names
local_names = [
basename
for basename, extension
in [os.path.splitext(path) for path
in os.listdir(sample_directory)]
if extension == '.py' or os.path.isdir(
os.path.join(sample_directory, basename))]
session.run(
'flake8',
'--show-source',
'--builtin', 'gettext',
'--max-complexity', '15',
'--import-order-style', 'google',
'--exclude', '.nox,.cache,env,lib',
'--application-import-names', ','.join(local_names),
sample_directory)
def session_reqcheck(session):
"""Checks for out of date requirements."""
session.install(REPO_TOOLS_REQ)
if 'update' in session.posargs:
command = 'update-requirements'
else:
command = 'check-requirements'
for reqfile in list_files('.', 'requirements*.txt'):
session.run('gcprepotools', command, reqfile)
def session_reqrollup(session):
"""Rolls up all requirements files into requirements-dev.txt.
This does not test for uniqueness. pip itself will validate that.
"""
requirements = set()
requirements_files = list(list_files('.', 'requirements*.txt'))
requirements_files.append('./requirements-dev.in')
for filename in requirements_files:
if filename == './requirements-dev.txt':
continue
with open(filename, 'r') as f:
lines = f.readlines()
requirements.update(lines)
def mark_if_necessary(requirement):
"""Adds environment markers to Python 2.7-only libraries."""
for library in PY27_ONLY_LIBRARIES:
if requirement.startswith(library):
return '{}; python_version == \'2.7\'\n'.format(
requirement.strip())
return requirement
requirements = [
mark_if_necessary(requirement) for requirement in requirements]
with open('requirements-dev.txt', 'w') as f:
f.write('# This file is generated by nox -s reqrollup. Do not edit.\n')
for requirement in sorted(requirements, key=lambda s: s.lower()):
if not requirement.startswith('#'):
f.write(requirement)