Allow per-module terraform fixtures (#1914)
* Allow terraform fixtures for examples * Allow defining multiple fixtures, and named fixtures under tests/fixtures/ * Enable e2e for wiktorn * Fix prepare_files call for e2e * Move fixture to separate file, fix test * Revert shallow-copying symlinks, performane penalty - 20% * Update tfdoc.py to list used fixtures --------- Co-authored-by: Wiktor Niesiobędzki <wiktorn@google.com>
This commit is contained in:
@@ -31,6 +31,7 @@ Due to the complexity of the underlying resources, changes to the configuration
|
||||
- [Files](#files)
|
||||
- [Variables](#variables)
|
||||
- [Outputs](#outputs)
|
||||
- [Fixtures](#fixtures)
|
||||
<!-- END TOC -->
|
||||
|
||||
### 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)
|
||||
<!-- END TFDOC -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,19 +18,34 @@ import yaml
|
||||
from pathlib import Path
|
||||
|
||||
BASE_PATH = Path(__file__).parent
|
||||
COUNT_TEST_RE = re.compile(r'# tftest +modules=(?P<modules>\d+) +resources=(?P<resources>\d+)' +
|
||||
r'(?: +files=(?P<files>[\w@,_-]+))?' +
|
||||
r'(?: +inventory=(?P<inventory>[\w\-.]+))?')
|
||||
COUNT_TEST_RE = re.compile(
|
||||
r'# tftest +modules=(?P<modules>\d+) +resources=(?P<resources>\d+)' +
|
||||
r'(?: +files=(?P<files>[\w@,_-]+))?' +
|
||||
r'(?: +fixtures=(?P<fixtures>[\w@,_/.-]+))?' +
|
||||
r'(?: +inventory=(?P<inventory>[\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']
|
||||
|
||||
@@ -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')])
|
||||
|
||||
@@ -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():
|
||||
|
||||
48
tests/fixtures/compute-mig-ab.tf
vendored
Normal file
48
tests/fixtures/compute-mig-ab.tf
vendored
Normal file
@@ -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 = {} }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user