diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 5833f9c6a..391385848 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -14,29 +14,15 @@ """Pytest configuration for testing code examples.""" import collections -import re from pathlib import Path import marko import pytest +from .utils import Example, File, get_tftest_directive + FABRIC_ROOT = Path(__file__).parents[2] -FILE_TEST_RE = re.compile(r'# tftest-file +id=([\w_.-]+) +path=([\S]+)') -FIXTURE_TEST_RE = re.compile(r'# tftest-fixture +id=([\w_.-]+)') - -Example = collections.namedtuple('Example', - 'name code module files fixtures type') -File = collections.namedtuple('File', 'path content') - - -def get_tftest_directive(s): - """Returns tftest directive from code block or None when directive is not found""" - for x in s.splitlines(): - if x.strip().startswith("#") and 'tftest' in x: - return x - return None - def pytest_generate_tests(metafunc, test_group='example', filter_tests=lambda x: 'skip' not in x): @@ -58,11 +44,14 @@ def pytest_generate_tests(metafunc, test_group='example', for child in doc.children: if isinstance(child, marko.block.FencedCode): code = child.children[0].children - if match := FILE_TEST_RE.search(code): - name, path = match.groups() + directive = get_tftest_directive(code) + if directive is None: + continue + if directive.name == 'tftest-file': + name, path = directive.kwargs['id'], directive.kwargs['path'] files[last_header][name] = File(path, code) - if match := FIXTURE_TEST_RE.search(code): - name = match.groups()[0] + if directive.name == 'tftest-fixture': + name = directive.kwargs['id'] fixtures[name] = code elif isinstance(child, marko.block.Heading): last_header = child.children[0].children @@ -74,10 +63,10 @@ def pytest_generate_tests(metafunc, test_group='example', if isinstance(child, marko.block.FencedCode): index += 1 code = child.children[0].children - tftest_tag = get_tftest_directive(code) - if tftest_tag is None: + directive = get_tftest_directive(code) + if directive is None: continue - if tftest_tag and not filter_tests(tftest_tag): + if directive and not filter_tests(directive.args): continue if child.lang in ('hcl', 'tfvars'): path = module.relative_to(FABRIC_ROOT) @@ -89,10 +78,10 @@ def pytest_generate_tests(metafunc, test_group='example', # this, together with `--dist loadgroup` will ensure that those tests will be run one after another # even if multiple workers are used # see: https://pytest-xdist.readthedocs.io/en/latest/distribution.html - marks = [pytest.mark.xdist_group("serial") - ] if 'serial' in tftest_tag else [] + marks = [pytest.mark.xdist_group('serial') + ] if 'serial' in directive.args else [] example = Example(name, code, path, files[last_header], fixtures, - child.lang) + child.lang, directive) examples.append(pytest.param(example, marks=marks)) elif isinstance(child, marko.block.Heading): last_header = child.children[0].children diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index f618d7c54..c334743d6 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -13,18 +13,14 @@ # limitations under the License. import re -import subprocess -import yaml import shutil +import subprocess import tempfile from pathlib import Path +import yaml + BASE_PATH = Path(__file__).parent -COUNT_TEST_RE = re.compile( - r'# tftest +modules=(?P\d+) +resources=(?P\d+)' + - r'(?: +files=(?P[\w@,_-]+))?' + - r'(?: +fixtures=(?P[\w@,_/.-]+))?' + - r'(?: +inventory=(?P[\w\-.]+))?') def prepare_files(example, test_path, files, fixtures): @@ -50,58 +46,58 @@ def prepare_files(example, test_path, files, fixtures): def test_example(plan_validator, example): - if match := COUNT_TEST_RE.search(example.code): - # for tfvars-based tests, create the temporary directory with the - # same parent as the original module - directory = example.module.parent if example.type == 'tfvars' else None - prefix = f'pytest-{example.module.name}' - with tempfile.TemporaryDirectory(prefix=prefix, dir=directory) as tmp_path: - tmp_path = Path(tmp_path) - tf_var_files = [] - if example.type == 'hcl': - (tmp_path / 'fabric').symlink_to(BASE_PATH.parents[1]) - (tmp_path / 'variables.tf').symlink_to(BASE_PATH / 'variables.tf') - (tmp_path / 'main.tf').write_text(example.code) - assets_path = (BASE_PATH.parent / - str(example.module).replace('-', '_') / 'assets') - if assets_path.exists(): - (tmp_path / 'assets').symlink_to(assets_path) + directive = example.directive - prepare_files(example, tmp_path, match.group('files'), - match.group('fixtures')) - elif example.type == 'tfvars': - (tmp_path / 'terraform.auto.tfvars').write_text(example.code) - shutil.copytree(example.module, tmp_path, dirs_exist_ok=True) - tf_var_files = [(tmp_path / 'terraform.auto.tfvars').resolve()] + # for tfvars-based tests, create the temporary directory with the + # same parent as the original module + directory = example.module.parent if example.type == 'tfvars' else None + prefix = f'pytest-{example.module.name}' + with tempfile.TemporaryDirectory(prefix=prefix, dir=directory) as tmp_path: + tmp_path = Path(tmp_path) + tf_var_files = [] + if example.type == 'hcl': + (tmp_path / 'fabric').symlink_to(BASE_PATH.parents[1]) + (tmp_path / 'variables.tf').symlink_to(BASE_PATH / 'variables.tf') + (tmp_path / 'main.tf').write_text(example.code) + assets_path = (BASE_PATH.parent / str(example.module).replace('-', '_') / + 'assets') + if assets_path.exists(): + (tmp_path / 'assets').symlink_to(assets_path) - expected_modules = int(match.group('modules')) - expected_resources = int(match.group('resources')) + prepare_files(example, tmp_path, directive.kwargs.get('files'), + directive.kwargs.get('fixtures')) + elif example.type == 'tfvars': + (tmp_path / 'terraform.auto.tfvars').write_text(example.code) + shutil.copytree(example.module, tmp_path, dirs_exist_ok=True) + tf_var_files = [(tmp_path / 'terraform.auto.tfvars').resolve()] - inventory = [] - if match.group('inventory') is not None: - python_test_path = str(example.module).replace('-', '_') - inventory = BASE_PATH.parent / python_test_path / 'examples' - inventory = inventory / match.group('inventory') + inventory = [] + if directive.kwargs.get('inventory') is not None: + python_test_path = str(example.module).replace('-', '_') + inventory = BASE_PATH.parent / python_test_path / 'examples' + inventory = inventory / directive.kwargs['inventory'] - summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, - tf_var_files=tf_var_files) + summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, + tf_var_files=tf_var_files) - print('\n') - print(yaml.dump({'values': summary.values})) - print(yaml.dump({'counts': summary.counts})) - print(yaml.dump({'outputs': summary.outputs})) + print('\n') + print(yaml.dump({'values': summary.values})) + print(yaml.dump({'counts': summary.counts})) + print(yaml.dump({'outputs': summary.outputs})) - counts = summary.counts - num_modules, num_resources = counts['modules'], counts['resources'] + counts = summary.counts + num_modules, num_resources = counts['modules'], counts['resources'] + + if expected_modules := directive.kwargs.get('modules'): + expected_modules = int(expected_modules) assert expected_modules == num_modules, 'wrong number of modules' + if expected_resources := directive.kwargs.get('resources'): + expected_resources = int(expected_resources) assert expected_resources == num_resources, 'wrong number of resources' - # TODO(jccb): this should probably be done in check_documentation - # but we already have all the data here. - result = subprocess.run( - 'terraform fmt -check -diff -no-color main.tf'.split(), cwd=tmp_path, - stdout=subprocess.PIPE, encoding='utf-8') - assert result.returncode == 0, f'terraform code not formatted correctly\n{result.stdout}' - - else: - assert False, "can't find tftest directive" + # TODO(jccb): this should probably be done in check_documentation + # but we already have all the data here. + result = subprocess.run( + 'terraform fmt -check -diff -no-color main.tf'.split(), cwd=tmp_path, + stdout=subprocess.PIPE, encoding='utf-8') + assert result.returncode == 0, f'terraform code not formatted correctly\n{result.stdout}' diff --git a/tests/examples/utils.py b/tests/examples/utils.py new file mode 100644 index 000000000..60aa37b09 --- /dev/null +++ b/tests/examples/utils.py @@ -0,0 +1,39 @@ +# Copyright 2024 Google LLC +# +# 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. + +import collections +import re + +Directive = collections.namedtuple('Directive', 'name args kwargs') +Example = collections.namedtuple( + 'Example', 'name code module files fixtures type directive') +File = collections.namedtuple('File', 'path content') + + +def get_tftest_directive(s): + """Scan a code block and return a Directive object if there are any + tftest directives""" + regexp = rf"^ *# *(tftest\S*)(.*)$" + if match := re.search(regexp, s, re.M): + name, body = match.groups() + args = [] + kwargs = {} + for arg in body.split(): + if '=' in arg: + l, r = arg.split('=', 1) + kwargs[l] = r + else: + args.append(arg) + return Directive(name, args, kwargs) + return None diff --git a/tests/examples_e2e/conftest.py b/tests/examples_e2e/conftest.py index 91f9a2649..d0a94cfeb 100644 --- a/tests/examples_e2e/conftest.py +++ b/tests/examples_e2e/conftest.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +13,8 @@ # limitations under the License. """Pytest configuration for testing code examples.""" -from ..examples.conftest import pytest_generate_tests as _examples_generate_test +from ..examples.conftest import \ + pytest_generate_tests as _examples_generate_test def pytest_generate_tests(metafunc): diff --git a/tests/examples_e2e/test_plan.py b/tests/examples_e2e/test_plan.py index f974cade8..58f2f9426 100644 --- a/tests/examples_e2e/test_plan.py +++ b/tests/examples_e2e/test_plan.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,10 +11,11 @@ # 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. -import re from pathlib import Path -from ..examples.test_plan import COUNT_TEST_RE, prepare_files + +from ..examples.test_plan import prepare_files +from ..examples.utils import get_tftest_directive BASE_PATH = Path(__file__).parent @@ -31,9 +32,10 @@ def test_example(e2e_validator, tmp_path, examples_e2e, e2e_tfvars_path): (tmp_path / 'terraform.tfvars').symlink_to(e2e_tfvars_path) # add files the same way as it is done for examples - if match := COUNT_TEST_RE.search(examples_e2e.code): - prepare_files(examples_e2e, tmp_path, match.group("files"), - match.group('fixtures')) + directive = get_tftest_directive(examples_e2e.code) + if directive and directive.name == 'tftest': + prepare_files(examples_e2e, tmp_path, directive.kwargs.get('files'), + directive.kwargs.get('fixtures')) e2e_validator(module_path=tmp_path, extra_files=[], tf_var_files=[(tmp_path / 'terraform.tfvars')]) diff --git a/tools/tfdoc.py b/tools/tfdoc.py index 742260b12..85e6fbd5b 100755 --- a/tools/tfdoc.py +++ b/tools/tfdoc.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,18 +45,20 @@ import re import string import sys import urllib.parse +from pathlib import Path import click import marko +try: + from examples.utils import get_tftest_directive +except ImportError: + BASEDIR = Path(__file__).parents[1] + sys.path.append(str(BASEDIR / 'tests')) + from examples.utils import get_tftest_directive + __version__ = '2.1.0' -# COUNT_TEST_RE copied from tests/examples/test_plan.py -COUNT_TEST_RE = re.compile( - r'# tftest +modules=(?P\d+) +resources=(?P\d+)' + - r'(?: +files=(?P[\w@,_-]+))?' + - r'(?: +fixtures=(?P[\w@,_/.-]+))?' + - r'(?: +inventory=(?P[\w\-.]+))?') # TODO(ludomagno): decide if we want to support variables*.tf and outputs*.tf FILE_DESC_DEFAULTS = { 'main.tf': 'Module-level locals and resources.', @@ -390,8 +392,8 @@ def parse_fixtures(basepath, readme): if isinstance(child, marko.block.FencedCode): if child.lang == 'hcl': code = child.children[0].children - if match := COUNT_TEST_RE.search(code): - if fixtures := match.group('fixtures'): + if directive := get_tftest_directive(code): + if fixtures := directive.kwargs.get('fixtures'): for fixture in fixtures.split(','): fixture_full = os.path.join(REPO_ROOT, 'tests', fixture) if not os.path.exists(fixture_full):