1# Copyright 2019 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
15from __future__ import print_function
16
17import os
18from pathlib import Path
19import sys
20from typing import Callable, Dict, List, Optional
21
22import nox
23
24
25# WARNING - WARNING - WARNING - WARNING - WARNING
26# WARNING - WARNING - WARNING - WARNING - WARNING
27#           DO NOT EDIT THIS FILE EVER!
28# WARNING - WARNING - WARNING - WARNING - WARNING
29# WARNING - WARNING - WARNING - WARNING - WARNING
30
31BLACK_VERSION = "black==19.10b0"
32
33# Copy `noxfile_config.py` to your directory and modify it instead.
34
35# `TEST_CONFIG` dict is a configuration hook that allows users to
36# modify the test configurations. The values here should be in sync
37# with `noxfile_config.py`. Users will copy `noxfile_config.py` into
38# their directory and modify it.
39
40TEST_CONFIG = {
41    # You can opt out from the test for specific Python versions.
42    "ignored_versions": [],
43    # Old samples are opted out of enforcing Python type hints
44    # All new samples should feature them
45    "enforce_type_hints": False,
46    # An envvar key for determining the project id to use. Change it
47    # to 'BUILD_SPECIFIC_GCLOUD_PROJECT' if you want to opt in using a
48    # build specific Cloud project. You can also use your own string
49    # to use your own Cloud project.
50    "gcloud_project_env": "GOOGLE_CLOUD_PROJECT",
51    # 'gcloud_project_env': 'BUILD_SPECIFIC_GCLOUD_PROJECT',
52    # If you need to use a specific version of pip,
53    # change pip_version_override to the string representation
54    # of the version number, for example, "20.2.4"
55    "pip_version_override": None,
56    # A dictionary you want to inject into your test. Don't put any
57    # secrets here. These values will override predefined values.
58    "envs": {},
59}
60
61
62try:
63    # Ensure we can import noxfile_config in the project's directory.
64    sys.path.append(".")
65    from noxfile_config import TEST_CONFIG_OVERRIDE
66except ImportError as e:
67    print("No user noxfile_config found: detail: {}".format(e))
68    TEST_CONFIG_OVERRIDE = {}
69
70# Update the TEST_CONFIG with the user supplied values.
71TEST_CONFIG.update(TEST_CONFIG_OVERRIDE)
72
73
74def get_pytest_env_vars() -> Dict[str, str]:
75    """Returns a dict for pytest invocation."""
76    ret = {}
77
78    # Override the GCLOUD_PROJECT and the alias.
79    env_key = TEST_CONFIG["gcloud_project_env"]
80    # This should error out if not set.
81    ret["GOOGLE_CLOUD_PROJECT"] = os.environ[env_key]
82
83    # Apply user supplied envs.
84    ret.update(TEST_CONFIG["envs"])
85    return ret
86
87
88# DO NOT EDIT - automatically generated.
89# All versions used to test samples.
90ALL_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"]
91
92# Any default versions that should be ignored.
93IGNORED_VERSIONS = TEST_CONFIG["ignored_versions"]
94
95TESTED_VERSIONS = sorted([v for v in ALL_VERSIONS if v not in IGNORED_VERSIONS])
96
97INSTALL_LIBRARY_FROM_SOURCE = os.environ.get("INSTALL_LIBRARY_FROM_SOURCE", False) in (
98    "True",
99    "true",
100)
101
102# Error if a python version is missing
103nox.options.error_on_missing_interpreters = True
104
105#
106# Style Checks
107#
108
109
110def _determine_local_import_names(start_dir: str) -> List[str]:
111    """Determines all import names that should be considered "local".
112
113    This is used when running the linter to insure that import order is
114    properly checked.
115    """
116    file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)]
117    return [
118        basename
119        for basename, extension in file_ext_pairs
120        if extension == ".py"
121        or os.path.isdir(os.path.join(start_dir, basename))
122        and basename not in ("__pycache__")
123    ]
124
125
126# Linting with flake8.
127#
128# We ignore the following rules:
129#   E203: whitespace before ‘:’
130#   E266: too many leading ‘#’ for block comment
131#   E501: line too long
132#   I202: Additional newline in a section of imports
133#
134# We also need to specify the rules which are ignored by default:
135# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121']
136FLAKE8_COMMON_ARGS = [
137    "--show-source",
138    "--builtin=gettext",
139    "--max-complexity=20",
140    "--import-order-style=google",
141    "--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py",
142    "--ignore=E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202",
143    "--max-line-length=88",
144]
145
146
147@nox.session
148def lint(session: nox.sessions.Session) -> None:
149    if not TEST_CONFIG["enforce_type_hints"]:
150        session.install("flake8", "flake8-import-order")
151    else:
152        session.install("flake8", "flake8-import-order", "flake8-annotations")
153
154    local_names = _determine_local_import_names(".")
155    args = FLAKE8_COMMON_ARGS + [
156        "--application-import-names",
157        ",".join(local_names),
158        ".",
159    ]
160    session.run("flake8", *args)
161
162
163#
164# Black
165#
166
167
168@nox.session
169def blacken(session: nox.sessions.Session) -> None:
170    session.install(BLACK_VERSION)
171    python_files = [path for path in os.listdir(".") if path.endswith(".py")]
172
173    session.run("black", *python_files)
174
175
176#
177# Sample Tests
178#
179
180
181PYTEST_COMMON_ARGS = ["--junitxml=sponge_log.xml"]
182
183
184def _session_tests(
185    session: nox.sessions.Session, post_install: Callable = None
186) -> None:
187    if TEST_CONFIG["pip_version_override"]:
188        pip_version = TEST_CONFIG["pip_version_override"]
189        session.install(f"pip=={pip_version}")
190    """Runs py.test for a particular project."""
191    if os.path.exists("requirements.txt"):
192        if os.path.exists("constraints.txt"):
193            session.install("-r", "requirements.txt", "-c", "constraints.txt")
194        else:
195            session.install("-r", "requirements.txt")
196
197    if os.path.exists("requirements-test.txt"):
198        if os.path.exists("constraints-test.txt"):
199            session.install("-r", "requirements-test.txt", "-c", "constraints-test.txt")
200        else:
201            session.install("-r", "requirements-test.txt")
202
203    if INSTALL_LIBRARY_FROM_SOURCE:
204        session.install("-e", _get_repo_root())
205
206    if post_install:
207        post_install(session)
208
209    session.run(
210        "pytest",
211        *(PYTEST_COMMON_ARGS + session.posargs),
212        # Pytest will return 5 when no tests are collected. This can happen
213        # on travis where slow and flaky tests are excluded.
214        # See http://doc.pytest.org/en/latest/_modules/_pytest/main.html
215        success_codes=[0, 5],
216        env=get_pytest_env_vars(),
217    )
218
219
220@nox.session(python=ALL_VERSIONS)
221def py(session: nox.sessions.Session) -> None:
222    """Runs py.test for a sample using the specified version of Python."""
223    if session.python in TESTED_VERSIONS:
224        _session_tests(session)
225    else:
226        session.skip(
227            "SKIPPED: {} tests are disabled for this sample.".format(session.python)
228        )
229
230
231#
232# Readmegen
233#
234
235
236def _get_repo_root() -> Optional[str]:
237    """ Returns the root folder of the project. """
238    # Get root of this repository. Assume we don't have directories nested deeper than 10 items.
239    p = Path(os.getcwd())
240    for i in range(10):
241        if p is None:
242            break
243        if Path(p / ".git").exists():
244            return str(p)
245        # .git is not available in repos cloned via Cloud Build
246        # setup.py is always in the library's root, so use that instead
247        # https://github.com/googleapis/synthtool/issues/792
248        if Path(p / "setup.py").exists():
249            return str(p)
250        p = p.parent
251    raise Exception("Unable to detect repository root.")
252
253
254GENERATED_READMES = sorted([x for x in Path(".").rglob("*.rst.in")])
255
256
257@nox.session
258@nox.parametrize("path", GENERATED_READMES)
259def readmegen(session: nox.sessions.Session, path: str) -> None:
260    """(Re-)generates the readme for a sample."""
261    session.install("jinja2", "pyyaml")
262    dir_ = os.path.dirname(path)
263
264    if os.path.exists(os.path.join(dir_, "requirements.txt")):
265        session.install("-r", os.path.join(dir_, "requirements.txt"))
266
267    in_file = os.path.join(dir_, "README.rst.in")
268    session.run(
269        "python", _get_repo_root() + "/scripts/readme-gen/readme_gen.py", in_file
270    )
271