diff --git a/modules/net-lb-app-ext/README.md b/modules/net-lb-app-ext/README.md index ad345b0ec..606076a8d 100644 --- a/modules/net-lb-app-ext/README.md +++ b/modules/net-lb-app-ext/README.md @@ -31,6 +31,7 @@ Due to the complexity of the underlying resources, changes to the configuration - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) +- [Fixtures](#fixtures) ### Minimal HTTP Example @@ -40,18 +41,18 @@ An HTTP load balancer with a backend service pointing to a GCE instance group: ```hcl module "glb-0" { source = "./fabric/modules/net-lb-app-ext" - project_id = "myprj" + project_id = var.project_id name = "glb-test-0" backend_service_configs = { default = { backends = [ - { backend = "projects/myprj/zones/europe-west8-b/instanceGroups/myig-b" }, - { backend = "projects/myprj/zones/europe-west8-c/instanceGroups/myig-c" }, + { backend = module.compute-mig-a.group.id }, + { backend = module.compute-mig-b.group.id }, ] } } } -# tftest modules=1 resources=5 +# tftest modules=3 resources=9 fixtures=fixtures/compute-mig-ab.tf e2e ``` ### Minimal HTTPS examples @@ -895,4 +896,8 @@ module "glb-0" { | [neg_ids](outputs.tf#L67) | Autogenerated network endpoint group ids. | | | [psc_neg_ids](outputs.tf#L74) | Autogenerated PSC network endpoint group ids. | | | [serverless_neg_ids](outputs.tf#L81) | Autogenerated serverless network endpoint group ids. | | + +## Fixtures + +- [compute-mig-ab.tf](../../tests/fixtures/compute-mig-ab.tf) diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 49570f2c7..bc6834249 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -23,8 +23,9 @@ import pytest 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') +Example = collections.namedtuple('Example', 'name code module files fixtures') File = collections.namedtuple('File', 'path content') @@ -49,16 +50,19 @@ def pytest_generate_tests(metafunc, test_group='example', doc = marko.parse(readme.read_text()) index = 0 files = collections.defaultdict(dict) + fixtures = {} # first pass: collect all examples tagged with tftest-file last_header = None for child in doc.children: if isinstance(child, marko.block.FencedCode): code = child.children[0].children - match = FILE_TEST_RE.search(code) - if match: + if match := FILE_TEST_RE.search(code): name, path = match.groups() files[last_header][name] = File(path, code) + if match := FIXTURE_TEST_RE.search(code): + name = match.groups()[0] + fixtures[name] = code elif isinstance(child, marko.block.Heading): last_header = child.children[0].children @@ -82,8 +86,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 [] - examples.append(pytest.param(Example(name, code, path, files[last_header]), marks=marks)) + marks = [pytest.mark.xdist_group("serial") + ] if 'serial' in tftest_tag else [] + example = Example(name, code, path, files[last_header], fixtures) + examples.append(pytest.param(example, marks=marks)) elif isinstance(child, marko.block.Heading): last_header = child.children[0].children index = 0 diff --git a/tests/examples/test_plan.py b/tests/examples/test_plan.py index 22525698e..345d15509 100644 --- a/tests/examples/test_plan.py +++ b/tests/examples/test_plan.py @@ -18,19 +18,34 @@ import yaml from pathlib import Path BASE_PATH = Path(__file__).parent -COUNT_TEST_RE = re.compile(r'# tftest +modules=(?P\d+) +resources=(?P\d+)' + - r'(?: +files=(?P[\w@,_-]+))?' + - r'(?: +inventory=(?P[\w\-.]+))?') +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, line): - if line is not None: - requested_files = line.split(',') +def prepare_files(example, test_path, files, fixtures): + if files is not None: + requested_files = files.split(',') for f in requested_files: destination = test_path / example.files[f].path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(example.files[f].content) + if fixtures is not None: + requested_fixtures = fixtures.split(',') + for f in requested_fixtures: + if f.startswith('fixtures/'): + # fixture is specified referencing a global fixture in (tests/fixtures/) + source = BASE_PATH.parent / f + destination = test_path / source.name + destination.symlink_to(source) + else: + # fixture is specified using an inline tftest-fixture tag + destination = test_path / f'{f}.tf' + destination.write_text(example.fixtures[f]) + def test_example(plan_validator, tmp_path, example): if match := COUNT_TEST_RE.search(example.code): @@ -42,26 +57,27 @@ def test_example(plan_validator, tmp_path, example): if assets_path.exists(): (tmp_path / 'assets').symlink_to(assets_path) - expected_modules = int(match.group("modules")) - expected_resources = int(match.group("resources")) + expected_modules = int(match.group('modules')) + expected_resources = int(match.group('resources')) - prepare_files(example, tmp_path, match.group("files")) + prepare_files(example, tmp_path, match.group('files'), + match.group('fixtures')) inventory = [] - if match.group("inventory") is not None: + 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 = inventory / match.group('inventory') # TODO: force plan_validator to never copy files (we're already # running from a temp dir) summary = plan_validator(module_path=tmp_path, inventory_paths=inventory, 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'] diff --git a/tests/examples_e2e/test_plan.py b/tests/examples_e2e/test_plan.py index c9936b9bb..f974cade8 100644 --- a/tests/examples_e2e/test_plan.py +++ b/tests/examples_e2e/test_plan.py @@ -21,7 +21,8 @@ BASE_PATH = Path(__file__).parent def test_example(e2e_validator, tmp_path, examples_e2e, e2e_tfvars_path): (tmp_path / 'fabric').symlink_to(BASE_PATH.parents[1]) - (tmp_path / 'variables.tf').symlink_to(BASE_PATH.parent / 'examples' / 'variables.tf') + (tmp_path / 'variables.tf').symlink_to(BASE_PATH.parent / 'examples' / + 'variables.tf') (tmp_path / 'main.tf').write_text(examples_e2e.code) assets_path = BASE_PATH.parent / str(examples_e2e.module).replace( '-', '_') / 'assets' @@ -31,7 +32,8 @@ def test_example(e2e_validator, tmp_path, examples_e2e, 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")) + prepare_files(examples_e2e, tmp_path, match.group("files"), + match.group('fixtures')) e2e_validator(module_path=tmp_path, extra_files=[], tf_var_files=[(tmp_path / 'terraform.tfvars')]) diff --git a/tests/fixtures.py b/tests/fixtures.py index be47283aa..d41a578ae 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -56,7 +56,9 @@ def _prepare_root_module(path): with tempfile.TemporaryDirectory(dir=path.parent) as tmp_path: tmp_path = Path(tmp_path) - shutil.copytree(path, tmp_path, dirs_exist_ok=True, + # Running tests in a copy made with symlinks=True makes them run + # ~20% slower than when run in a copy made with symlinks=False. + shutil.copytree(path, tmp_path, dirs_exist_ok=True, symlinks=False, ignore=ignore_patterns) lockfile = _REPO_ROOT / 'tools' / 'lockfile' / '.terraform.lock.hcl' if lockfile.exists(): diff --git a/tests/fixtures/compute-mig-ab.tf b/tests/fixtures/compute-mig-ab.tf new file mode 100644 index 000000000..f287cad2e --- /dev/null +++ b/tests/fixtures/compute-mig-ab.tf @@ -0,0 +1,48 @@ +# Copyright 2023 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. + + +module "compute-mig-a" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-a" + name = "my-ig-a" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + boot_disk = { + initialize_params = { + image = "cos-cloud/cos-stable" + } + } + group = { named_ports = {} } +} + +module "compute-mig-b" { + source = "./fabric/modules/compute-vm" + project_id = var.project_id + zone = "${var.region}-b" + name = "my-ig-b" + network_interfaces = [{ + network = var.vpc.self_link + subnetwork = var.subnet.self_link + }] + boot_disk = { + initialize_params = { + image = "cos-cloud/cos-stable" + } + } + group = { named_ports = {} } +} diff --git a/tools/tfdoc.py b/tools/tfdoc.py index d17d52d48..d41a35a7e 100755 --- a/tools/tfdoc.py +++ b/tools/tfdoc.py @@ -43,11 +43,18 @@ import glob import os import re import string +import sys import urllib.parse import click import marko +# manipulate path to import COUNT_TEST_RE from tests/examples/test_plan.py +REPO_ROOT = os.path.dirname(os.path.dirname(__file__)) +sys.path.append(os.path.join(REPO_ROOT, 'tests')) + +from examples.test_plan import COUNT_TEST_RE + __version__ = '2.1.0' # TODO(ludomagno): decide if we want to support variables*.tf and outputs*.tf @@ -227,6 +234,25 @@ def parse_variables(basepath, exclude_files=None): file=shortname, line=item['line'], nullable=nullable) +def parse_fixtures(basepath, readme): + 'Return a list of file paths of all the unique fixtures used in the module.' + doc = marko.parse(readme) + used_fixtures = set() + for child in doc.children: + 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'): + for fixture in fixtures.split(','): + fixture_full = os.path.join(REPO_ROOT, 'tests', fixture) + if not os.path.exists(fixture_full): + raise SystemExit(f'Unknown fixture: {fixture}') + fixture_relative = os.path.relpath(fixture_full, basepath) + used_fixtures.add(fixture_relative) + yield from sorted(used_fixtures) + + # formatting functions @@ -235,7 +261,7 @@ def _escape(s): return ''.join(c if c in UNESCAPED else ('&#%s;' % ord(c)) for c in s) -def format_tfref(outputs, variables, files, show_extra=False): +def format_tfref(outputs, variables, files, fixtures, show_extra=False): 'Return formatted document.' buffer = [] if files: @@ -247,6 +273,9 @@ def format_tfref(outputs, variables, files, show_extra=False): if outputs: buffer += ['', '## Outputs', ''] buffer += list(format_tfref_outputs(outputs, show_extra)) + if fixtures: + buffer += ['', '## Fixtures', ''] + buffer += list(format_tfref_fixtures(fixtures)) return '\n'.join(buffer).strip() @@ -323,6 +352,12 @@ def format_tfref_variables(items, show_extra=True): yield format +def format_tfref_fixtures(items): + 'Format fixtures table.' + for x in items: + yield f"- [{os.path.basename(x)}]({x})" + + def create_toc(readme): 'Create a Markdown table of contents a for README.' doc = marko.parse(readme) @@ -384,9 +419,11 @@ def create_tfref(module_path, files=False, show_extra=False, exclude_files=None, mod_files = list(parse_files(module_path, exclude_files)) if files else [] mod_variables = list(parse_variables(module_path, exclude_files)) mod_outputs = list(parse_outputs(module_path, exclude_files)) + mod_fixtures = list(parse_fixtures(module_path, readme)) except (IOError, OSError) as e: raise SystemExit(e) - doc = format_tfref(mod_outputs, mod_variables, mod_files, show_extra) + doc = format_tfref(mod_outputs, mod_variables, mod_files, mod_fixtures, + show_extra) return Document(doc, mod_files, mod_variables, mod_outputs)