Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Testing framework revamp #1029

Merged
merged 28 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8c43b72
Remove stale xmark from parellel testing attempt
juliocc Nov 26, 2022
cc1b9fb
New fixtures
juliocc Nov 30, 2022
dc1fda0
First tests using fast
juliocc Nov 30, 2022
61d5758
New test example for a module
juliocc Nov 30, 2022
b88f0cf
Bring back parallel tests
juliocc Nov 30, 2022
ace43b7
Update requirements for tests
juliocc Dec 1, 2022
0619b35
Fix fast test
juliocc Dec 1, 2022
25a3f86
If copying fixture, remove any auto vars files read by terraform
juliocc Dec 1, 2022
1815337
remove key from fast values inventory
juliocc Dec 1, 2022
354ab11
Simplify path handling
juliocc Dec 1, 2022
8631d69
Reorder fixture parameters
juliocc Dec 1, 2022
553ca3f
Allow defining tests via yaml
juliocc Dec 2, 2022
188ad23
Add tests for subnet factory
juliocc Dec 2, 2022
b4d3aa2
Migrate organizations tests
juliocc Dec 2, 2022
f546105
Fix boilerplate
juliocc Dec 4, 2022
0a6285f
Reorder code
juliocc Dec 4, 2022
2af4a82
Initial FAST bootstrap fixture
juliocc Dec 4, 2022
34f0176
Simplify fast bootstrap test
juliocc Dec 4, 2022
589f7a5
Simplify yaml test spec
juliocc Dec 5, 2022
284f8ff
Basic error handling
juliocc Dec 5, 2022
fded49c
Remove unneeded imports from tests/conftest.py and use pytest_plugins
juliocc Dec 5, 2022
be0e807
Bring back `tests` key in test yaml spec
juliocc Dec 5, 2022
f8d5f43
Use ignore instead of copy+delete in plan_summary()
juliocc Dec 6, 2022
a017fef
Removed refresh=True from plan() call
juliocc Dec 6, 2022
1981647
Custom context manager to create temp directory
juliocc Dec 6, 2022
8b664fc
Simplify test spec structure
juliocc Dec 6, 2022
51594c1
Fix tests
juliocc Dec 6, 2022
b840fcd
Plan summary tool
juliocc Dec 6, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 95 additions & 0 deletions tests/collectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Copyright 2022 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.
"""Pytest plugin to discover tests specified in YAML files.

This plugin uses the pytest_collect_file hook to collect all files
matching tftest*.yaml and runs plan_validate for each test found.
See FabricTestFile for details on the file structure.

"""

juliocc marked this conversation as resolved.
Show resolved Hide resolved
import pytest
import yaml

from .fixtures import plan_summary, plan_validator


class FabricTestFile(pytest.File):

def collect(self):
"""Read yaml test spec and yield test items for each test definition.

The test spec should contain a `module` key with the path of the
terraform module to test, relative to the root of the repository

Tests are defined within the top-level `tests` key, and should
have the following structure:

test-name:
juliocc marked this conversation as resolved.
Show resolved Hide resolved
tfvars:
- tfvars1.tfvars
- tfvars2.tfvars
inventory:
- inventory1.yaml
- inventory2.yaml

All paths specifications are relative to the location of the test
spec. The inventory key is optional, if omitted, the inventory
will be taken from the file test-name.yaml

"""

try:
raw = yaml.safe_load(self.path.open())
module = raw.pop('module')
except (IOError, OSError, yaml.YAMLError) as e:
raise Exception(f'cannot read test spec {self.path}: {e}')
except KeyError as e:
raise Exception(f'`module` key not found in {self.path}: {e}')
for test_name, spec in raw.get('tests', {}).items():
inventories = spec.get('inventory', [f'{test_name}.yaml'])
try:
tfvars = spec['tfvars']
except KeyError:
raise Exception(
f'test `{test_name}` in {self.path} does not contain a `tfvars` key'
)
for i in inventories:
name = test_name
if isinstance(inventories, list) and len(inventories) > 1:
name = f'{test_name}[{i}]'
yield FabricTestItem.from_parent(self, name=name, module=module,
inventory=[i], tfvars=tfvars)


class FabricTestItem(pytest.Item):

def __init__(self, name, parent, module, inventory, tfvars):
super().__init__(name, parent)
self.module = module
self.inventory = inventory
self.tfvars = tfvars

def runtest(self):
s = plan_validator(self.module, self.inventory, self.parent.path.parent,
self.tfvars)

def reportinfo(self):
return self.path, None, self.name


def pytest_collect_file(parent, file_path):
'Collect tftest*.yaml files and run plan_validator from them.'
if file_path.suffix == '.yaml' and file_path.name.startswith('tftest'):
return FabricTestFile.from_parent(parent, path=file_path)
144 changes: 6 additions & 138 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,144 +11,12 @@
# 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.
"Shared fixtures"

import inspect
import os
import shutil
import tempfile
'Pytest configuration.'

import pytest
import tftest

BASEDIR = os.path.dirname(os.path.dirname(__file__))


@pytest.fixture(scope='session')
def _plan_runner():
"Returns a function to run Terraform plan on a fixture."

def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, refresh=True, tmpdir=True, **tf_vars):
"Runs Terraform plan and returns parsed output."
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[2]
fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")

fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + "_"
with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
if tmpdir:
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path if tmpdir else fixture_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(extra_files=extra_files, upgrade=True)
plan = tf.plan(output=True, refresh=refresh, tf_var_file=tf_var_file,
tf_vars=tf_vars, targets=targets)
return plan

return run_plan


@pytest.fixture(scope='session')
def plan_runner(_plan_runner):
"Returns a function to run Terraform plan on a module fixture."

def run_plan(fixture_path=None, extra_files=None, tf_var_file=None,
targets=None, **tf_vars):
"Runs Terraform plan and returns plan and module resources."
plan = _plan_runner(fixture_path, extra_files=extra_files,
tf_var_file=tf_var_file, targets=targets, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
return plan, root_module['resources']

return run_plan


@pytest.fixture(scope='session')
def e2e_plan_runner(_plan_runner):
"Returns a function to run Terraform plan on an end-to-end fixture."

def run_plan(fixture_path=None, tf_var_file=None, targets=None,
refresh=True, include_bare_resources=False, **tf_vars):
"Runs Terraform plan on an end-to-end module using defaults, returns data."
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, **tf_vars)
# skip the fixture
root_module = plan.root_module['child_modules'][0]
modules = dict((mod['address'], mod['resources'])
for mod in root_module['child_modules'])
resources = [r for m in modules.values() for r in m]
if include_bare_resources:
bare_resources = root_module['resources']
resources.extend(bare_resources)
return modules, resources

return run_plan


@pytest.fixture(scope='session')
def recursive_e2e_plan_runner(_plan_runner):
"""Plan runner for end-to-end root module, returns total number of
(nested) modules and resources"""

def walk_plan(node, modules, resources):
# TODO(jccb): this would be better with node.get() but
# TerraformPlanOutput objects don't have it
new_modules = node.get('child_modules', [])
resources += node.get('resources', [])
modules += new_modules
for module in new_modules:
walk_plan(module, modules, resources)

def run_plan(fixture_path=None, tf_var_file=None, targets=None, refresh=True,
include_bare_resources=False, compute_sums=True, tmpdir=True,
**tf_vars):
"Runs Terraform plan on a root module using defaults, returns data."
plan = _plan_runner(fixture_path, tf_var_file=tf_var_file, targets=targets,
refresh=refresh, tmpdir=tmpdir, **tf_vars)
modules = []
resources = []
walk_plan(plan.root_module, modules, resources)
return len(modules), len(resources)

return run_plan


@pytest.fixture(scope='session')
def apply_runner():
"Returns a function to run Terraform apply on a fixture."

def run_apply(fixture_path=None, **tf_vars):
"Runs Terraform plan and returns parsed output."
if fixture_path is None:
# find out the fixture directory from the caller's directory
caller = inspect.stack()[1]
fixture_path = os.path.join(os.path.dirname(caller.filename), "fixture")

fixture_parent = os.path.dirname(fixture_path)
fixture_prefix = os.path.basename(fixture_path) + "_"

with tempfile.TemporaryDirectory(prefix=fixture_prefix,
dir=fixture_parent) as tmp_path:
# copy fixture to a temporary directory so we can execute
# multiple tests in parallel
shutil.copytree(fixture_path, tmp_path, dirs_exist_ok=True)
tf = tftest.TerraformTest(tmp_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
tf.setup(upgrade=True)
apply = tf.apply(tf_vars=tf_vars)
output = tf.output(json_format=True)
return apply, output

return run_apply


@pytest.fixture
def basedir():
return BASEDIR
pytest_plugins = (
'tests.fixtures',
'tests.legacy_fixtures',
'tests.collectors',
)
29 changes: 0 additions & 29 deletions tests/fast/stages/s00_bootstrap/fixture/main.tf

This file was deleted.

11 changes: 11 additions & 0 deletions tests/fast/stages/s00_bootstrap/simple.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
organization = {
domain = "fast.example.com"
id = 123456789012
customer_id = "C00000000"
}
billing_account = {
id = "000000-111111-222222"
organization_id = 123456789012
}
prefix = "fast"
outputs_location = "/fast-config"
49 changes: 49 additions & 0 deletions tests/fast/stages/s00_bootstrap/simple.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copyright 2022 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.

counts:
google_bigquery_dataset: 2
google_bigquery_dataset_iam_member: 2
google_bigquery_default_service_account: 3
google_logging_organization_sink: 2
google_organization_iam_binding: 19
google_organization_iam_custom_role: 2
google_organization_iam_member: 16
google_project: 3
google_project_iam_binding: 9
google_project_iam_member: 1
google_project_service: 29
google_project_service_identity: 2
google_service_account: 3
google_service_account_iam_binding: 3
google_storage_bucket: 4
google_storage_bucket_iam_binding: 2
google_storage_bucket_iam_member: 3
google_storage_bucket_object: 5
google_storage_project_service_account: 3
local_file: 5

outputs:
custom_roles:
organization_iam_admin: organizations/123456789012/roles/organizationIamAdmin
service_project_network_admin: organizations/123456789012/roles/serviceProjectNetworkAdmin
outputs_bucket: fast-prod-iac-core-outputs-0
project_ids:
automation: fast-prod-iac-core-0
billing-export: fast-prod-billing-exp-0
log-export: fast-prod-audit-logs-0
service_accounts:
bootstrap: fast-prod-bootstrap-0@fast-prod-iac-core-0.iam.gserviceaccount.com
cicd: fast-prod-cicd-0@fast-prod-iac-core-0.iam.gserviceaccount.com
resman: fast-prod-resman-0@fast-prod-iac-core-0.iam.gserviceaccount.com
33 changes: 33 additions & 0 deletions tests/fast/stages/s00_bootstrap/simple_projects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2022 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.

values:
module.automation-project.google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-iac-core-0
org_id: '123456789012'
project_id: fast-prod-iac-core-0
module.billing-export-project[0].google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-billing-exp-0
org_id: '123456789012'
project_id: fast-prod-billing-exp-0
module.log-export-project.google_project.project[0]:
auto_create_network: false
billing_account: 000000-111111-222222
name: fast-prod-audit-logs-0
org_id: '123456789012'
project_id: fast-prod-audit-logs-0