2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 10:10:12 +00:00
Emerle, Ryan 0a1b533192 WIP - Functional Test Updates
- Update `dnsjava` library
- Add support for H2 database
- Update functional tests to support parallel runs
- Remove the ability to specify number of processes for functional tests - always 4 now
- Add `Makefile` and `Dockerfile` in `functional_test` to make it easier to run tests without spinning up multiple containers
2021-10-08 15:52:09 -04:00

160 lines
7.3 KiB
Python

import ipaddress
import logging
import os
import ssl
import sys
import traceback
from collections import OrderedDict
from typing import MutableMapping, List
import _pytest.config
import pytest
from xdist.scheduler import LoadScopeScheduling
from vinyldns_context import VinylDNSTestContext
logger = logging.getLogger(__name__)
logging.basicConfig(
level=os.environ.get("VINYL_LOG_LEVEL") or logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(stream=sys.stderr)
]
)
config_context = {}
def pytest_addoption(parser: _pytest.config.argparsing.Parser) -> None:
"""
Adds additional options that we can parse when we run the tests, stores them in the parser / py.test context
"""
parser.addoption("--url", dest="url", action="store", default="http://localhost:9000", help="URL for application to root")
parser.addoption("--dns-ip", dest="dns_ip", action="store", default="127.0.0.1", help="The ip address for the dns name server to update")
parser.addoption("--resolver-ip", dest="resolver_ip", action="store", help="The ip address for the dns server to use for the tests during resolution. This is usually the same as `--dns-ip`")
parser.addoption("--dns-zone", dest="dns_zone", action="store", default="vinyldns.", help="The zone name that will be used for testing")
parser.addoption("--dns-key-name", dest="dns_key_name", action="store", default="vinyldns.", help="The name of the key used to sign updates for the zone")
parser.addoption("--dns-key", dest="dns_key", action="store", default="nzisn+4G2ldMn0q1CV3vsg==", help="The TSIG key")
parser.addoption("--dns-key-algo", dest="dns_key_algo", action="store", default="HMAC-MD5", help="The TSIG key algorithm")
# optional
parser.addoption("--basic-auth", dest="basic_auth_creds", help="Basic auth credentials in `user:pass` format")
parser.addoption("--basic-auth-realm", dest="basic_auth_realm", help="Basic auth realm to use with credentials supplied by `-b`")
parser.addoption("--iauth-creds", dest="iauth_creds", help="Intermediary auth in `key:secret` format")
parser.addoption("--oauth-creds", dest="oauth_creds", help="OAuth credentials in `consumer:secret` format")
parser.addoption("--environment", dest="environment", action="store", default="test", help="Environment that we are testing against")
parser.addoption("--teardown", dest="teardown", action="store", default="True", help="True to teardown the test fixture; false to leave it for another run")
parser.addoption("--enable-safety_check", dest="enable_safety_check", action="store_true",
help="If provided, enable object mutation safety checks; otherwise safety checks are disable. "
"This is a handy development tool to catch rogue tests mutating data which can affect other tests.")
def pytest_configure(config: _pytest.config.Config) -> None:
"""
Loads the test context since we are no longer using run.py
"""
logger.info("Starting configuration")
# Monkey patch ssl so we do not verify ssl certs
_create_unverified_https_context = ssl._create_unverified_context
# Handle target environment that doesn't support HTTPS verification
ssl._create_default_https_context = _create_unverified_https_context
url = config.getoption("url")
if not url.endswith("/"):
url += "/"
# Define markers
config.addinivalue_line("markers", "serial")
config.addinivalue_line("markers", "skip_production")
config.addinivalue_line("markers", "manual_batch_review")
name_server_ip = retrieve_resolver(config.getoption("dns_ip"))
VinylDNSTestContext.configure(name_server_ip=name_server_ip,
resolver_ip=retrieve_resolver(config.getoption("resolver_ip", name_server_ip) or name_server_ip),
zone=config.getoption("dns_zone"),
key_name=config.getoption("dns_key_name"),
key=config.getoption("dns_key"),
url=url,
teardown=config.getoption("teardown").lower() == "true",
key_algo=config.getoption("dns_key_algo"),
enable_safety_check=config.getoption("enable_safety_check"))
def pytest_report_header(config: _pytest.config.Config) -> str:
"""
Overrides the test result header like we do in pyfunc test
"""
logger.debug("testing!")
header = "Testing against environment " + config.getoption("environment")
header += "\nURL: " + config.getoption("url")
header += "\nRunning from directory " + os.getcwd()
header += "\nTest shim directory " + os.path.dirname(__file__)
header += "\nDNS IP: " + config.getoption("dns_ip")
return header
def retrieve_resolver(resolver_name: str) -> str:
"""
Retrieves the ip address of the DNS resolver when given a hostname
:param resolver_name: The name/ip of the resolver
:return: The IP address, and optionally port, of the resolver
"""
parts = resolver_name.split(":")
resolver_address = parts[0]
try:
ipaddress.ip_address(parts[0])
return resolver_name
except ValueError:
logger.warning("`--dns_ip` is set to `%s`, which isn't a valid ip/port combination (hostname?)", resolver_name)
try:
import socket
resolver_address = socket.gethostbyname(parts[0])
resolver_address = [resolver_address] + parts[1:]
resolver_address = ":".join(resolver_address)
logger.warning("Translating `%s` resolver to `%s`", resolver_name, resolver_address)
except Exception:
traceback.print_exc()
logger.error("Cannot translate `%s` into a usable resolver address", resolver_name)
pytest.exit(1)
return resolver_address
class WorkerScheduler(LoadScopeScheduling):
worker_assignments: List[MutableMapping] = [{"name": "list_batch_change_summaries_test.py", "worker": 0}]
def _assign_work_unit(self, node):
"""Assign a work unit to a node."""
assert self.workqueue
# Grab a unit of work
scope, work_unit = self.workqueue.popitem(last=False)
# Always run list_batch_change_summaries_test on the first worker
for assignment in WorkerScheduler.worker_assignments:
while assignment["name"] in scope:
self.run_work_on_node(self.nodes[assignment["worker"]], scope, work_unit)
scope, work_unit = self.workqueue.popitem(last=False)
self.run_work_on_node(node, scope, work_unit)
def run_work_on_node(self, node, scope, work_unit):
# Keep track of the assigned work
assigned_to_node = self.assigned_work.setdefault(node, default=OrderedDict())
assigned_to_node[scope] = work_unit
# Ask the node to execute the workload
worker_collection = self.registered_collections[node]
nodeids_indexes = [
worker_collection.index(nodeid)
for nodeid, completed in work_unit.items()
if not completed
]
node.send_runtest_some(nodeids_indexes)
def _split_scope(self, nodeid):
return nodeid
def pytest_xdist_make_scheduler(config, log):
return WorkerScheduler(config, log)