1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00

Move the vm definitions to test/container.yaml.

The definitions move out of common/vm.py into a vm section at the top of test/container.yaml, next to the container revisions that go with them, and are read and checked when the module is imported as they were before. The revisions move under a revision key to make room.

The declaration is read with the loader the other declarations use, so a version keeps the text it was written as rather than arriving as a float. A new yaml_map_dict() reads a mapping as a dict where a key is looked up by name, which also reports a key that was written twice instead of silently keeping the last one.

The constants that named each vm are gone. A vm is named by the string that --vm accepts, which is how the CI workflows and the documentation build already name them. A name used as a lookup is validated by vm_get(), and a name used in a comparison is protected by branch coverage since a comparison that can never match leaves a branch unhittable. VM_NONE, VM_ALL, and the os base and architecture constants remain because they name kinds rather than vms. VM_LIST is now the vms declared as default, so it is in declaration order.

The container build no longer tests a vm by name. What a vm installs and configures is declared instead: valgrind, epel, powertools, python, coverage-python, pg-repo-key, pg-repo-release, dnf-module, and ssh-rsa. The three near identical blocks that added the PostgreSQL repo collapse into one, which removes the uncoverable branch annotation on the fedora branch. Every generated script is unchanged, so no image is rebuilt.

The supported PostgreSQL versions are generated from build/postgres.yaml rather than declared a second time. A new bld_pg_version_list() reads just the versions, since a tool that only needs to know which ones are supported has no use for the interface that bld_pg_parse() scans the vendored headers for. The release flag comes with them, so the beta repository is added for whichever installed version has not been released rather than for a hardcoded vm and version.

A vm with no container declares only default and coverage-c, since every other key says how a container is built. The os base declared for none was never read, and the os the tests run on with --vm=none is whatever the host is rather than the debian it claimed. The PostgreSQL versions declared for it were never read either, since only an integration test runs against a version and those need a container.

A PostgreSQL version installed on a vm must now be one that build/postgres.yaml declares, and a version tested on a vm must be one installed there, both of which were only true by inspection before.
This commit is contained in:
David Steele
2026-07-31 22:32:02 +07:00
parent 369f18a6c7
commit 8e5fcfa690
11 changed files with 715 additions and 211 deletions

View File

@@ -75,3 +75,22 @@ def yaml_map_empty(value, name):
"""Check that a mapping is empty, i.e. the key is a name in a list rather than something with a definition."""
check(isinstance(value, YamlMap) and len(value) == 0, "%s must be an empty map" % name)
####################################################################################################################################
def yaml_map_dict(value, name):
"""A mapping as a dict, for a declaration where a key is looked up by name and repeating one is a mistake.
The loader keeps a repeated key because some declarations mean it, so a declaration that does not has to say so here rather than
silently keeping the last value written."""
check(isinstance(value, YamlMap), "%s must be a map" % name)
result = {}
for key, entry in value:
check(key not in result, "%s has duplicate key '%s'" % (name, key))
result[key] = entry
return result

View File

@@ -36,9 +36,13 @@ class BldPg:
####################################################################################################################################
def _version_list(path):
"""Parse the version list."""
def bld_pg_version_list(path_repo):
"""Parse the supported versions, oldest first.
Separate from the interface below because the versions are the whole of what the declaration says, so a tool that needs to know
which versions are supported does not also scan the vendored headers for an interface it has no use for."""
path = os.path.join(path_repo, "build/postgres.yaml")
result = []
for key, value in yaml_load(file_read(path), path):
@@ -139,6 +143,4 @@ def bld_pg_parse(path_repo):
# Functions are defined as macros, which each interface expands for its own version
function_list = _define_list(file_read(os.path.join(path_repo, "src/postgres/interface/version.intern.h")))
path_yaml = os.path.join(path_repo, "build/postgres.yaml")
return BldPg(_version_list(path_yaml), type_list, define_list, function_list)
return BldPg(bld_pg_version_list(path_repo), type_list, define_list, function_list)

View File

@@ -1,3 +1,172 @@
# **********************************************************************************************************************************
# Vm Definitions
#
# The vms tests can run on, the PostgreSQL versions each one provides, and what the container build needs to know about them. A vm
# name is what --vm accepts, e.g. u24 for Ubuntu 24.04. The definitions are declared here rather than in the code so that a vm and
# the container revision that goes with it are read from the same file.
#
# The definitions are checked when they are read: every supported PostgreSQL version must be tested on exactly one default vm and at
# least one default vm must collect C coverage. Without the check a new version could be added and silently never tested.
#
# - 'default' marks a vm that --vm=all selects
# - 'image' is the image the container is built from, and a vm without one has no container, i.e. it runs on the host
# - 'os-base' is the os the container is built on, which decides its package manager and where PostgreSQL is installed
# - 'db' is the PostgreSQL versions installed and 'db-test' the ones integration tests run against, which defaults to all of them
# - the rest is what the container build installs or configures, and each defaults to false or unset
#
# A vm with no container declares only 'default' and 'coverage-c'. Everything else says how a container is built, and the os the
# tests run on is then whatever the host is rather than anything that can be declared here.
# **********************************************************************************************************************************
vm:
# No container, i.e. run directly on the host
none:
coverage-c: true
# Alpine 3.21
a321:
default: true
os-base: alpine
image: alpine:3.21
db:
- 15
- 16
- 17
db-test:
- 16
pg-repo: false
ssh-rsa: true
# Alpine 3.24
a324:
default: true
os-base: alpine
image: alpine:3.24
db:
- 16
- 17
- 18
db-test:
- 17
pg-repo: false
# Debian 12
d12:
default: true
os-base: debian
image: debian:12
db:
- 15
pg-repo: false
valgrind: false
# RHEL 8
rh8:
default: true
os-base: rhel
image: rockylinux/rockylinux:8
db:
- 10
pg-repo: false
valgrind: false
epel: true
powertools: true
python: python3.12
# RHEL 9
rh9:
default: true
os-base: rhel
image: rockylinux/rockylinux:9
db:
- 14
- 15
- 16
- 17
- 18
db-test:
- 14
pg-repo-release: EL-9
dnf-module: true
coverage-python: pip
epel: true
# RHEL 10
rh10:
default: true
os-base: rhel
image: rockylinux/rockylinux:10
db:
- 14
- 15
- 16
- 17
- 18
db-test:
- 18
pg-repo-release: EL-10
pg-repo-key: false
coverage-python: package
epel: true
# Fedora 44
f44:
os-base: rhel
image: fedora:44
db:
- 14
- 15
- 16
- 17
- 18
db-test:
- 15
pg-repo-release: F-44
coverage-c: true
coverage-python: package
# Ubuntu 22.04
u22:
default: true
os-base: debian
image: ubuntu:22.04
db:
- 9.6
- 10
- 11
- 12
- 13
- 14
db-test:
- 9.6
- 11
coverage-c: true
ssh-rsa: true
# Ubuntu 24.04
u24:
default: true
os-base: debian
image: ubuntu:24.04
db:
- 9.6
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
db-test:
- 12
- 13
- 19
coverage-c: true
coverage-python: package
ssh-rsa: true
# **********************************************************************************************************************************
# Container Cache Revisions
#
@@ -26,4 +195,5 @@
# rh10: 20260710A # rebuild rh10 on every architecture (independent of "all")
# rh10-x86_64: 20260710A # rebuild rh10 on x86_64 only (independent of "all" and "rh10")
# **********************************************************************************************************************************
all: 20260710A
revision:
all: 20260710A

View File

@@ -3,6 +3,9 @@
Generates a Dockerfile for each vm and builds it. Every vm has a base image with the packages the tests need and a test image that
adds the test user, ssh, and the certificate authority, so a change to the test setup does not rebuild the packages.
What a vm installs and configures comes from its definition in test/container.yaml rather than from a test on its name here, so a
vm that needs a package or a repository the others do not is a change to the declaration rather than to the script generated here.
Base images are cached in a container registry keyed by a hash of the generated Dockerfile combined with the manual revision in
test/container.yaml. The revision forces a rebuild when the Dockerfile is unchanged but the upstream packages have changed, e.g. a
new PostgreSQL beta.
@@ -15,14 +18,13 @@ import hashlib
import os
import re
import yaml
from common.error import check
from common.exec import exec_one, exec_status
from common.log import *
from common.storage import file_read, file_write, path_create
from common.user import group_id, group_name, user_id, user_name
from common.vm import *
from common.yaml import yaml_load, yaml_map_dict
# Separator between sections of the generated script. Every section is a continuation of a single RUN so the image has one layer.
_SECTION = " && \\\n\n"
@@ -176,7 +178,7 @@ def _entry_point_setup(os_base):
####################################################################################################################################
def _script_package(name, vm):
def _script_package(vm):
"""Install the packages the tests need."""
result = ""
@@ -195,17 +197,14 @@ def _script_package(name, vm):
result += _SECTION + "# Install packages\n"
if vm.os_base == VM_OS_BASE_RHEL:
if name == VM_RH8:
result += (
" dnf install -y dnf-plugins-core && \\\n"
+ " dnf config-manager --set-enabled powertools && \\\n"
+ " dnf -y install epel-release && \\\n"
+ " crb enable && \\\n"
)
elif name in (VM_RH9, VM_RH10):
result += (
" dnf install -y dnf-plugins-core && \\\n" + " dnf -y install epel-release && \\\n" + " crb enable && \\\n"
)
# Packages the tests need that rhel does not ship are in EPEL and CRB, which was called powertools before EL-9
if vm.epel:
result += " dnf install -y dnf-plugins-core && \\\n"
if vm.powertools:
result += " dnf config-manager --set-enabled powertools && \\\n"
result += " dnf -y install epel-release && \\\n" + " crb enable && \\\n"
result += (
" yum -y update && \\\n"
@@ -214,22 +213,22 @@ def _script_package(name, vm):
+ " systemd-devel"
)
if name != VM_RH8:
if vm.valgrind:
result += " valgrind"
# Coverage for the python tests. rh10 and later package a version new enough while rh9 packages one that predates the
# per line branch detail the report needs, so there it comes from pip below.
if name == VM_RH9:
# Coverage for the python tests, which comes from pip where the packaged version predates the per line branch detail the
# report needs. The pip install itself is below, since it needs the package installed here first.
if vm.coverage_python == "pip":
result += " python3-pip"
elif name != VM_RH8:
elif vm.coverage_python is not None:
result += " python3-coverage"
# The test harness needs a python3 that PyYAML is packaged for. On RH8 that is not the platform python, which meson
# installs and which claims the python3 alternative at a priority nothing else can outrank, so point it explicitly.
if name == VM_RH8:
result += " python3.12 python3.12-pyyaml && \\\n" + " alternatives --set python3 /usr/bin/python3.12"
else:
# The test harness needs a python3 that PyYAML is packaged for. Where that is not the platform python, which meson installs
# and which claims the python3 alternative at a priority nothing else can outrank, point it explicitly.
if vm.python is None:
result += " python3-pyyaml"
else:
result += " %s %s-pyyaml && \\\n" % (vm.python, vm.python) + " alternatives --set python3 /usr/bin/%s" % vm.python
elif vm.os_base == VM_OS_BASE_DEBIAN:
result += (
" export DEBCONF_NONINTERACTIVE_SEEN=true DEBIAN_FRONTEND=noninteractive && \\\n"
@@ -240,11 +239,11 @@ def _script_package(name, vm):
+ " libssh2-1-dev libcurl4-openssl-dev libsystemd-dev python3-yaml"
)
if name != VM_D12:
if vm.valgrind:
result += " valgrind"
# Coverage for the python tests, which are run on u24 and later
if name not in (VM_D12, VM_U22):
if vm.coverage_python is not None:
result += " python3-coverage"
result += " zstd libzstd-dev"
@@ -257,16 +256,15 @@ def _script_package(name, vm):
+ " openssh-keygen zlib-dev libssh2-dev valgrind lz4 zstd zstd-dev py3-yaml"
)
# Coverage for python tests where the packaged version predates the per line branch detail the report needs, so it comes from
# pip rather than from the distribution
if name == VM_RH9:
# Coverage that comes from pip rather than from the distribution
if vm.coverage_python == "pip":
result += _SECTION + "# Install python coverage\n" + " pip3 install --no-cache-dir 'coverage>=6.5'"
return result
####################################################################################################################################
def _script_pg(name, vm, arch):
def _script_pg(vm, arch):
"""Install PostgreSQL."""
result = ""
@@ -275,50 +273,51 @@ def _script_pg(name, vm, arch):
result += _SECTION + "# Install PostgreSQL packages\n"
if vm.os_base == VM_OS_BASE_RHEL:
# RHEL 10's rpm-sequoia OpenPGP backend rejects the SHA-1 binding signature in the PGDG GPG key, and the crypto-policy
# SHA1 subpolicy that could re-enable it was removed in EL-10. So rh10 skips the key import and installs the repo/packages
# with gpg checks disabled (acceptable for a throwaway test container).
if name != VM_RH10 and vm.pg_repo:
result += " rpm --import https://download.postgresql.org/pub/repos/yum/keys/RPM-GPG-KEY-PGDG && \\\n"
if not vm.pg_repo:
# Enable the native PostgreSQL application stream instead of adding the PGDG repo
result += " dnf -y module enable postgresql:%s && \\\n" % vm.db_list[-1]
elif name == VM_RH9:
result += (
" rpm -ivh \\\n"
+ " https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-%s/" % arch
+ "pgdg-redhat-repo-latest.noarch.rpm && \\\n"
+ " dnf -qy module disable postgresql && \\\n"
+ " yum -y install libcurl-devel && \\\n"
)
elif name == VM_RH10:
# Install the repo without a gpg check and disable gpgcheck on the PGDG repos (see the SHA-1 note above). RHEL 10 also
# dropped dnf modularity, so there is no postgresql module to disable.
result += (
" dnf -y install --nogpgcheck \\\n"
+ " https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-%s/" % arch
+ "pgdg-redhat-repo-latest.noarch.rpm && \\\n"
+ " sed -i 's/gpgcheck=1/gpgcheck=0/g' /etc/yum.repos.d/pgdg-redhat-all.repo && \\\n"
+ " yum -y install libcurl-devel && \\\n"
)
# Every rhel vm that installs from the PostgreSQL repo is one of the three above, so there is nothing to fall through to
elif name == VM_F44: # {uncoverable_branch}
result += (
" rpm -ivh \\\n"
+ " https://download.postgresql.org/pub/repos/yum/reporpms/F-44-%s/" % arch
+ "pgdg-fedora-repo-latest.noarch.rpm && \\\n"
+ " yum -y install libcurl-devel && \\\n"
else:
# The repo package is named for the distribution the release belongs to, e.g. EL-9 is redhat and F-44 is fedora
distro = "fedora" if vm.pg_repo_release.startswith("F-") else "redhat"
package = "https://download.postgresql.org/pub/repos/yum/reporpms/%s-%s/pgdg-%s-repo-latest.noarch.rpm" % (
vm.pg_repo_release,
arch,
distro,
)
# The rpm-sequoia OpenPGP backend rejects the SHA-1 binding signature in the PGDG GPG key, and the crypto-policy SHA1
# subpolicy that could re-enable it was removed in EL-10. A vm with that backend skips the key and installs the repo
# and its packages with gpg checks disabled, which is acceptable for a throwaway test container.
if vm.pg_repo_key:
result += (
" rpm --import https://download.postgresql.org/pub/repos/yum/keys/RPM-GPG-KEY-PGDG && \\\n"
+ " rpm -ivh \\\n"
+ " %s && \\\n" % package
)
else:
result += (
" dnf -y install --nogpgcheck \\\n"
+ " %s && \\\n" % package
+ " sed -i 's/gpgcheck=1/gpgcheck=0/g' /etc/yum.repos.d/pgdg-%s-all.repo && \\\n" % distro
)
# Where the distribution still has dnf modularity its postgresql module shadows the PGDG packages, so disable it
if vm.dnf_module:
result += " dnf -qy module disable postgresql && \\\n"
result += " yum -y install libcurl-devel && \\\n"
result += " yum -y install postgresql-devel"
elif vm.os_base == VM_OS_BASE_DEBIAN:
# Install repo from apt.postgresql.org
# Install repo from apt.postgresql.org, adding the beta repo when an unreleased version is installed. The beta repo is not
# built for every architecture, so it is only added on the ones it is built for.
if vm.pg_repo:
beta = vm.pg_beta is not None and arch in (VM_ARCH_AARCH64, VM_ARCH_X86_64)
result += (
" apt-get install -y --no-install-recommends postgresql-common && \\\n"
+ " /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y"
+ (" -c 19" if name == VM_U24 and arch in (VM_ARCH_AARCH64, VM_ARCH_X86_64) else "")
+ (" -c %s" % vm.pg_beta if beta else "")
+ " && \\\n"
)
@@ -356,6 +355,7 @@ def _script_pg(name, vm, arch):
result += " postgresql-%s" % db_version
else:
result += " postgresql%s" % db_version
# On other architectures (e.g. ppc64le, s390x) install a single PostgreSQL version for the smoke test via the postgresql
# metapackage (its default from the configured repo). The full version matrix is skipped to keep the emulated build fast; one
# version is enough to check the build and exercise checksums end-to-end on big-endian.
@@ -381,7 +381,7 @@ def script_base(name, arch):
"""Generate the script for the base image, which holds everything installed from a package."""
vm = vm_get(name)
result = _script_package(name, vm)
result = _script_package(vm)
result += (
_SECTION
@@ -394,7 +394,7 @@ def script_base(name, arch):
result += _SECTION + "# Fix root tty\n" + " sed -i 's/^mesg n/tty -s \\&\\& mesg n/g' /root/.profile"
result += _SECTION + "# Suppress dpkg interactive output\n" + " rm /etc/apt/apt.conf.d/70debconf"
result += _script_pg(name, vm, arch)
result += _script_pg(vm, arch)
if vm.os_base == VM_OS_BASE_DEBIAN:
result += (
@@ -431,7 +431,7 @@ def script_test(name, path_repo):
+ " echo 'Banner /etc/issue.net' >> /etc/ssh/sshd_config"
)
if name in (VM_U22, VM_U24, VM_A321):
if vm.ssh_rsa:
result += (
_SECTION
+ " echo '# Add PubkeyAcceptedAlgorithms (required for SFTP)' >> /etc/ssh/sshd_config && \\\n"
@@ -504,29 +504,36 @@ def _revision_get(revision, name, arch):
####################################################################################################################################
def revision_check(revision):
"""Check the revisions read from container.yaml.
"""Check the revisions declared in container.yaml, returning them keyed as they were declared.
A revision is keyed by "all", a vm, or a vm qualified with an architecture, e.g. "u22-x86_64". Without this an invalid key
would silently fall back to a less specific revision and the expected rebuild would never happen."""
result = yaml_map_dict(revision, "the 'revision' section in %s" % VM_PATH_CONTAINER)
# The "all" revision is required so it can never be accidentally unset, which would change every image hash at once. It can
# only be bumped.
check(isinstance(revision, dict) and VM_ALL in revision, "the 'all' revision is required in test/container.yaml")
check(VM_ALL in result, "the 'all' revision is required in %s" % VM_PATH_CONTAINER)
for key in sorted(revision):
check(revision[key] is not None, "revision '%s' in test/container.yaml must be set to a value" % key)
for key in sorted(result):
check(result[key] != "", "revision '%s' in %s must be set to a value" % (key, VM_PATH_CONTAINER))
if key == VM_ALL:
continue
key_vm, _, key_arch = key.partition("-")
check(vm_valid(key_vm) and key_vm != VM_NONE, "revision '%s' in test/container.yaml has invalid vm '%s'" % (key, key_vm))
check(
vm_valid(key_vm) and key_vm != VM_NONE,
"revision '%s' in %s has invalid vm '%s'" % (key, VM_PATH_CONTAINER, key_vm),
)
check(
key_arch == "" or key_arch in VM_ARCH_LIST,
"revision '%s' in test/container.yaml has invalid architecture '%s'" % (key, key_arch),
"revision '%s' in %s has invalid architecture '%s'" % (key, VM_PATH_CONTAINER, key_arch),
)
return result
####################################################################################################################################
def _container_write(path_repo, name, arch, title, image_parent, image, script, revision_map, cache, show_output):
@@ -616,10 +623,14 @@ def cmd_vm_build(config):
path_create(path_temp, mode=0o770)
with open(os.path.join(path_repo, "test/container.yaml"), "r") as file:
revision_map = yaml.safe_load(file)
# Revisions come from the repository being built rather than from the definitions, which are read from the repository this
# module is part of
path_container = os.path.join(path_repo, VM_PATH_CONTAINER)
container = yaml_map_dict(yaml_load(file_read(path_container), VM_PATH_CONTAINER), VM_PATH_CONTAINER)
revision_check(revision_map)
check("revision" in container, "the 'revision' section is required in %s" % VM_PATH_CONTAINER)
revision_map = revision_check(container["revision"])
for name in VM_LIST if config.vm == VM_ALL else [config.vm]:
# Base image

View File

@@ -3,19 +3,30 @@
The vms tests can run on, the PostgreSQL versions each one provides, and what the container build needs to know about them. A vm
name is what --vm accepts, e.g. u24 for Ubuntu 24.04.
The default list is checked when this module is imported: every supported PostgreSQL version must be tested on exactly one default
vm and at least one default vm must collect C coverage. Without the check a new version could be added and silently never tested."""
The definitions are declared in test/container.yaml and are read when this module is imported, so a definition that does not parse
fails before any tool that reads it runs. The declaration is checked as well: every supported PostgreSQL version must be tested on
exactly one default vm and at least one default vm must collect C coverage. Without the check a new version could be added and
silently never tested.
The versions the project supports are not declared here either. They come from build/postgres.yaml, which is what the version
interfaces are generated from, so a version is supported and tested because one declaration says so."""
####################################################################################################################################
import os
import platform
from common.error import ToolError, check
from common.storage import file_read
from common.yaml import yaml_bool, yaml_load, yaml_map_dict
from postgres.parse import bld_pg_version_list
# Os base a vm is built on, which determines its package manager and where PostgreSQL is installed
VM_OS_BASE_ALPINE = "alpine"
VM_OS_BASE_DEBIAN = "debian"
VM_OS_BASE_RHEL = "rhel"
VM_OS_BASE_LIST = (VM_OS_BASE_ALPINE, VM_OS_BASE_DEBIAN, VM_OS_BASE_RHEL)
# Architectures a container can be built for
VM_ARCH_AARCH64 = "aarch64"
VM_ARCH_I386 = "i386"
@@ -29,76 +40,155 @@ VM_ARCH_LIST = (VM_ARCH_AARCH64, VM_ARCH_I386, VM_ARCH_PPC64LE, VM_ARCH_S390X, V
VM_ALL = "all"
VM_NONE = "none"
# Vms
VM_A321 = "a321"
VM_A324 = "a324"
VM_D12 = "d12"
VM_F44 = "f44"
VM_RH8 = "rh8"
VM_RH9 = "rh9"
VM_RH10 = "rh10"
VM_U22 = "u22"
VM_U24 = "u24"
# Where the python coverage package comes from, since not every vm packages a version new enough to report branch detail
_VM_COVERAGE_PYTHON_LIST = ("package", "pip")
# Default vms, i.e. the ones --vm=all selects
VM_LIST = (VM_D12, VM_RH8, VM_RH9, VM_RH10, VM_U22, VM_U24, VM_A321, VM_A324)
# Declaration the definitions are read from, named as the tools report it rather than as it is opened
VM_PATH_CONTAINER = "test/container.yaml"
# Repository the declarations are read from. This module is part of the repository, so its own path locates it, and a tool imports
# it before it has a repository path of its own to give.
_PATH_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# Supported PostgreSQL versions, oldest first
_PG_LIST = bld_pg_version_list(_PATH_REPO)
# PostgreSQL versions the project supports. Each one must be tested on a default vm, which vm_check() enforces.
PG_VERSION_LIST = ("9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19")
PG_VERSION_LIST = tuple(pg.version for pg in _PG_LIST)
# Versions that have not been released yet, which are only in the beta repository
_PG_BETA_LIST = tuple(pg.version for pg in _PG_LIST if not pg.release)
# Keys a vm with no container may declare, since every other key says how a container is built
_VM_KEY_HOST_LIST = ("coverage-c", "default")
# Keys a vm may declare
_VM_KEY_LIST = (
"coverage-c",
"coverage-python",
"db",
"db-test",
"default",
"dnf-module",
"epel",
"image",
"os-base",
"pg-repo",
"pg-repo-key",
"pg-repo-release",
"powertools",
"python",
"ssh-rsa",
"valgrind",
)
####################################################################################################################################
class Vm:
"""A vm definition."""
def __init__(self, os_base, db_list, image=None, db_test_list=None, pg_repo=True, coverage_c=False):
self.os_base = os_base
self.image = image # Image the container is built from, None when the vm has no container
self.pg_repo = pg_repo # Install PostgreSQL from the PGDG repo rather than from the distribution?
self.coverage_c = coverage_c # Is C coverage collected on this vm?
self.db_list = db_list # PostgreSQL versions installed
self.db_test_list = db_list if db_test_list is None else db_test_list # Versions integration tests run against
def __init__(self, name):
self.name = name
self.default = False # Is this a default vm, i.e. one of the vms --vm=all selects?
self.os_base = None # Os the container is built on, which determines its package manager and where PostgreSQL is installed
self.image = None # Image the container is built from, None when the vm has no container
self.db_list = [] # PostgreSQL versions installed
self.db_test_list = [] # Versions integration tests run against, which is every version installed unless declared
self.pg_beta = None # Unreleased version installed, which is only in the beta repository
self.pg_repo = True # Install PostgreSQL from the PGDG repo rather than from the distribution?
self.pg_repo_key = True # Import the PGDG signing key, which not every rpm backend accepts?
self.pg_repo_release = None # Release the PGDG repo package is built for, e.g. EL-9
self.dnf_module = False # Disable the distribution's postgresql dnf module so the PGDG packages are used?
self.coverage_c = False # Is C coverage collected on this vm?
self.coverage_python = None # Where the python coverage package comes from, None when python coverage is not collected
self.valgrind = True # Install valgrind?
self.epel = False # Install EPEL and enable CRB, which is where the packages rhel does not ship itself are?
self.powertools = False # Enable powertools, which is what CRB was called before EL-9?
self.python = None # Python to install and make the default, when the platform python is not the one to use
self.ssh_rsa = False # Add back the ssh-rsa algorithms, which SFTP needs and a newer sshd rejects by default?
####################################################################################################################################
# fmt: off
_VM = {
# No container, i.e. run directly on the host
VM_NONE: Vm(VM_OS_BASE_DEBIAN, ["10"], coverage_c=True),
def _vm_parse(name, definition):
"""Parse a single vm definition."""
# Alpine 3.21
VM_A321: Vm(VM_OS_BASE_ALPINE, ["15", "16", "17"], image="alpine:3.21", db_test_list=["16"], pg_repo=False),
result = Vm(name)
key_map = yaml_map_dict(definition, "vm '%s'" % name)
# Alpine 3.24
VM_A324: Vm(VM_OS_BASE_ALPINE, ["16", "17", "18"], image="alpine:3.24", db_test_list=["17"], pg_repo=False),
for key in key_map:
check(key in _VM_KEY_LIST, "unknown key '%s' in vm '%s'" % (key, name))
# Debian 12
VM_D12: Vm(VM_OS_BASE_DEBIAN, ["15"], image="debian:12", pg_repo=False),
def flag(key, default="false"):
"""A boolean key, which defaults to what a vm that does not declare it gets."""
# RHEL 8
VM_RH8: Vm(VM_OS_BASE_RHEL, ["10"], image="rockylinux/rockylinux:8", pg_repo=False),
return yaml_bool(key_map.get(key, default), "vm '%s' key '%s'" % (name, key))
# RHEL 9
VM_RH9: Vm(VM_OS_BASE_RHEL, ["14", "15", "16", "17", "18"], image="rockylinux/rockylinux:9", db_test_list=["14"]),
def version_list(key, default):
"""A list of PostgreSQL versions, which must be written as a list even when there is only one of them."""
# RHEL 10
VM_RH10: Vm(VM_OS_BASE_RHEL, ["14", "15", "16", "17", "18"], image="rockylinux/rockylinux:10", db_test_list=["18"]),
value = key_map.get(key, default)
# Fedora 44
VM_F44: Vm(
VM_OS_BASE_RHEL, ["14", "15", "16", "17", "18"], image="fedora:44", db_test_list=["15"], coverage_c=True),
check(isinstance(value, list), "vm '%s' key '%s' must be a list" % (name, key))
# Ubuntu 22.04
VM_U22: Vm(
VM_OS_BASE_DEBIAN, ["9.6", "10", "11", "12", "13", "14"], image="ubuntu:22.04", db_test_list=["9.6", "11"],
coverage_c=True),
return list(value)
# Ubuntu 24.04
VM_U24: Vm(
VM_OS_BASE_DEBIAN, ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"], image="ubuntu:24.04",
db_test_list=["12", "13", "19"], coverage_c=True),
}
# fmt: on
result.default = flag("default")
result.os_base = key_map.get("os-base")
result.image = key_map.get("image")
result.db_list = version_list("db", [])
result.db_test_list = version_list("db-test", result.db_list)
result.pg_repo = flag("pg-repo", "true")
result.pg_repo_key = flag("pg-repo-key", "true")
result.pg_repo_release = key_map.get("pg-repo-release")
result.dnf_module = flag("dnf-module")
result.coverage_c = flag("coverage-c")
result.coverage_python = key_map.get("coverage-python")
result.valgrind = flag("valgrind", "true")
result.epel = flag("epel")
result.powertools = flag("powertools")
result.python = key_map.get("python")
result.ssh_rsa = flag("ssh-rsa")
# A vm with no image has no container to build, so it declares nothing about how one would be built. That includes the os it
# runs on, which is whatever the host is rather than anything this can know.
if result.image is None:
for key in key_map:
check(key in _VM_KEY_HOST_LIST, "vm '%s' has no container so it may not declare '%s'" % (name, key))
else:
check(
result.os_base in VM_OS_BASE_LIST,
"vm '%s' must declare one of these os bases: %s" % (name, ", ".join(VM_OS_BASE_LIST)),
)
check(
result.coverage_python in (None,) + _VM_COVERAGE_PYTHON_LIST,
"invalid python coverage '%s' in vm '%s'" % (result.coverage_python, name),
)
for db in result.db_list:
check(db in PG_VERSION_LIST, "PostgreSQL %s in vm '%s' is not a supported version" % (db, name))
for db in result.db_test_list:
check(db in result.db_list, "PostgreSQL %s tested on vm '%s' is not installed there" % (db, name))
# An unreleased version comes from the beta repository, which the container build needs to add
result.pg_beta = next((db for db in result.db_list if db in _PG_BETA_LIST), None)
return result
####################################################################################################################################
def vm_parse(content):
"""Parse the vm definitions, keyed by vm name."""
section = yaml_map_dict(yaml_load(content, VM_PATH_CONTAINER), VM_PATH_CONTAINER)
check("vm" in section, "the 'vm' section is required in %s" % VM_PATH_CONTAINER)
return {
name: _vm_parse(name, definition)
for name, definition in yaml_map_dict(section["vm"], "the 'vm' section in %s" % VM_PATH_CONTAINER).items()
}
####################################################################################################################################
@@ -118,18 +208,27 @@ def vm_get(vm):
return _VM[vm]
####################################################################################################################################
def vm_default_list(vm_map):
"""The default vms in a definition map, i.e. the ones --vm=all selects."""
return tuple(name for name, vm in vm_map.items() if vm.default)
####################################################################################################################################
def vm_check(vm_map):
"""Check that C coverage and every supported PostgreSQL version are tested on a default vm.
Called below on the definitions in this module, so a version that no vm tests is an error rather than a silent gap."""
Called below on the definitions that were read, so a version that no vm tests is an error rather than a silent gap."""
check(any(vm_map[vm].coverage_c for vm in VM_LIST), "C coverage is not configured to run on a default vm")
vm_list = vm_default_list(vm_map)
check(any(vm_map[vm].coverage_c for vm in vm_list), "C coverage is not configured to run on a default vm")
for version in PG_VERSION_LIST:
vm_found = None
for vm in VM_LIST:
for vm in vm_list:
if version in vm_map[vm].db_test_list:
check(vm_found is None, "PostgreSQL %s is already configured to run on default vm %s" % (version, vm_found))
@@ -150,4 +249,10 @@ def host_arch():
####################################################################################################################################
# Definitions, read and checked on import so a tool that reads them never has to
_VM = vm_parse(file_read(os.path.join(_PATH_REPO, VM_PATH_CONTAINER)))
vm_check(_VM)
# Default vms, i.e. the ones --vm=all selects
VM_LIST = vm_default_list(_VM)

View File

@@ -59,6 +59,23 @@ def test_yaml_bool():
assert_equal(str(error.exception), "invalid boolean 'yes' for key")
####################################################################################################################################
def test_yaml_map_dict():
"""A mapping is a dict where a key is looked up by name, and a repeated key is reported rather than kept."""
assert_equal(yaml_map_dict(yaml_load("b: 1\na: 2\n", "test.yaml"), "key"), {"b": "1", "a": "2"})
with assert_raises(ToolError) as error:
yaml_map_dict(yaml_load("- 1\n", "test.yaml"), "key")
assert_equal(str(error.exception), "key must be a map")
with assert_raises(ToolError) as error:
yaml_map_dict(yaml_load("b: 1\na: 2\nb: 3\n", "test.yaml"), "key")
assert_equal(str(error.exception), "key has duplicate key 'b'")
####################################################################################################################################
def test_yaml_map_empty():
"""A key that is a name in a list has an empty map under it and nothing else."""

View File

@@ -226,7 +226,7 @@ def test_job_command_option():
assert_in(" --no-coverage ", job.exec.command)
# Nor on a vm that cannot collect it
job = _job(Config(path), _run(vm=VM_D12))
job = _job(Config(path), _run(vm="d12"))
_capture(job, lambda job: job.begin())
@@ -271,7 +271,7 @@ def test_job_container():
with tempfile.TemporaryDirectory() as path:
config = Config(path, vm_arch="x86_64")
job = _job(config, _run(vm=VM_U24), vm_idx=1, vm_max=2, test_idx=3, test_max=12)
job = _job(config, _run(vm="u24"), vm_idx=1, vm_max=2, test_idx=3, test_max=12)
started, command_list, output = _capture(job, lambda job: job.begin())
@@ -296,7 +296,7 @@ def test_job_container():
assert_equal(job.description, "P2-T04/12 - vm=u24, module=common/error")
# An integration test starts its own containers and has nothing built for it, so neither is done here
job = _job(config, _run(name="integration/all", type=TEST_TYPE_INTEGRATION, vm=VM_U24, test_list=[1]))
job = _job(config, _run(name="integration/all", type=TEST_TYPE_INTEGRATION, vm="u24", test_list=[1]))
started, command_list, output = _capture(job, lambda job: job.begin())
@@ -441,7 +441,7 @@ def test_job_end_fail():
with tempfile.TemporaryDirectory() as path:
config = Config(path)
job = _job(config, _run(vm=VM_U24))
job = _job(config, _run(vm="u24"))
_capture(job, lambda job: job.begin())
(done, fail), command_list, output = _capture(job, lambda job: job.end(), status=25, output="it broke\n")
@@ -475,7 +475,7 @@ def test_job_profile():
with tempfile.TemporaryDirectory() as path:
config = Config(path, profile=True)
job = _job(config, _run(vm=VM_U24))
job = _job(config, _run(vm="u24"))
_capture(job, lambda job: job.begin())

View File

@@ -54,7 +54,7 @@ MODULE_LIST = [
_module("common/error/error", coverage_list=["common/error"]),
_module("common/exec"),
_module("common/socket", container_required=True),
_module("doc/build", vm_list=[VM_U24]),
_module("doc/build", vm_list=["u24"]),
_module("test/common/log", type=TEST_TYPE_TOOL, lang=TEST_LANG_PYTHON, coverage_list=["test/common/log"]),
_module("integration/all", type=TEST_TYPE_INTEGRATION, total=3, pg_required=True),
_module("performance/type", type=TEST_TYPE_PERFORMANCE, total=2),
@@ -72,17 +72,17 @@ def test_list_all():
)
# A vm with a container runs everything, and the integration test runs once per sub-test and PostgreSQL version
test_list = test_list_get(MODULE_LIST, _Config(vm=VM_U24))
test_list = test_list_get(MODULE_LIST, _Config(vm="u24"))
assert_equal(
_name_list(test_list),
["common/error/error", "common/exec", "common/socket", "doc/build", "test/common/log"]
+ ["integration/all"] * 3 * len(vm_get(VM_U24).db_test_list)
+ ["integration/all"] * 3 * len(vm_get("u24").db_test_list)
+ ["performance/type"],
)
# A test that only runs on one vm is left out of the others
assert_true("doc/build" not in _name_list(test_list_get(MODULE_LIST, _Config(vm=VM_D12))))
assert_true("doc/build" not in _name_list(test_list_get(MODULE_LIST, _Config(vm="d12"))))
# Every default vm runs its own copy of the list
assert_equal(len(set(run.vm for run in test_list_get(MODULE_LIST, _Config(vm=VM_ALL)))), len(VM_LIST))
@@ -122,12 +122,12 @@ def test_list_filter():
"""The filters leave out the kinds of test they name."""
# Integration tests run the binary rather than a unit test, so they are what --c-only leaves out
assert_true("integration/all" not in _name_list(test_list_get(MODULE_LIST, _Config(vm=VM_U24, c_only=True))))
assert_true("integration/all" not in _name_list(test_list_get(MODULE_LIST, _Config(vm="u24", c_only=True))))
# Only tests that must have a container, which includes every integration test
assert_equal(
_name_list(test_list_get(MODULE_LIST, _Config(vm=VM_U24, container_only=True))),
["common/socket"] + ["integration/all"] * 3 * len(vm_get(VM_U24).db_test_list),
_name_list(test_list_get(MODULE_LIST, _Config(vm="u24", container_only=True))),
["common/socket"] + ["integration/all"] * 3 * len(vm_get("u24").db_test_list),
)
# Only tests that provide coverage, which is what the documentation summary is built from
@@ -141,22 +141,22 @@ def test_list_filter():
def test_list_pg_version():
"""An integration test runs against the PostgreSQL versions the vm tests, most recent first."""
version_list = vm_get(VM_U24).db_test_list
version_list = vm_get("u24").db_test_list
assert_equal(
[run.pg_version for run in test_list_get(MODULE_LIST, _Config(vm=VM_U24, module=["integration"]))],
[run.pg_version for run in test_list_get(MODULE_LIST, _Config(vm="u24", module=["integration"]))],
[version for version in reversed(version_list) for _ in range(3)],
)
# A single version can be selected, which is how a version specific problem is chased down
assert_equal(
[run.pg_version for run in test_list_get(MODULE_LIST, _Config(vm=VM_U24, module=["integration"], pg_version="12"))],
[run.pg_version for run in test_list_get(MODULE_LIST, _Config(vm="u24", module=["integration"], pg_version="12"))],
["12"] * 3,
)
# A version that the vm does not test selects nothing
assert_equal(
test_list_get(MODULE_LIST, _Config(vm=VM_U24, module=["integration"], pg_version="8.0")),
test_list_get(MODULE_LIST, _Config(vm="u24", module=["integration"], pg_version="8.0")),
[],
)
@@ -165,7 +165,7 @@ def test_list_pg_version():
def test_list_run():
"""Each sub-test of an integration test is a run of its own, since each one needs its own containers."""
test_list = test_list_get(MODULE_LIST, _Config(vm=VM_U24, module=["integration"], pg_version="12"))
test_list = test_list_get(MODULE_LIST, _Config(vm="u24", module=["integration"], pg_version="12"))
assert_equal([run.test_list for run in test_list], [[1], [2], [3]])
assert_equal(test_list[0].coverage_name, "integration-all")
@@ -173,7 +173,7 @@ def test_list_run():
assert_false(test_list[0].performance)
# A run can be selected, which is the only way to get one container out of an integration test
config = _Config(vm=VM_U24, module=["integration/all"], pg_version="12", test=[2])
config = _Config(vm="u24", module=["integration/all"], pg_version="12", test=[2])
assert_equal([run.test_list for run in test_list_get(MODULE_LIST, config)], [[2]])

View File

@@ -348,7 +348,7 @@ def test_test_build():
assert_equal(started, ["common/error", "common/exec"])
# An integration test runs the binary so it is built. The build is already set up so only the binary is built.
status, command_list, started, output = _cmd_test(Config(path_repo, path_test, vm=VM_U24, module=["integration"]))
status, command_list, started, output = _cmd_test(Config(path_repo, path_test, vm="u24", module=["integration"]))
assert_in("ninja -C %s/build/u24 src/pgbackrest 2>&1" % path_test, " ".join(command_list))
@@ -379,7 +379,7 @@ def test_test_container():
path_repo = _repo_create(path)
path_test = os.path.join(path, "test")
status, command_list, started, output = _cmd_test(Config(path_repo, path_test, vm=VM_U24, module=["common"]))
status, command_list, started, output = _cmd_test(Config(path_repo, path_test, vm="u24", module=["common"]))
assert_equal(_cmd_test.container_remove.call_args[0][0], "test-([0-9]+|build)")
assert_in("cleanup old data and containers", output)
@@ -478,7 +478,7 @@ def test_test_coverage():
assert_equal(_cmd_test.coverage.call_count, 0)
_cmd_test(Config(path_repo, path_test, vm=VM_D12, module=["common"]))
_cmd_test(Config(path_repo, path_test, vm="d12", module=["common"]))
assert_equal(_cmd_test.coverage.call_count, 0)

View File

@@ -21,9 +21,13 @@ from common.log import *
from common.storage import file_read, file_write
from common.user import group_id, group_name, user_id, user_name
from common.vm import *
from common.yaml import yaml_load
# Revisions as test/container.yaml declares them
REVISION = {"all": "1", VM_U24: "2", "%s-%s" % (VM_U24, VM_ARCH_X86_64): "3"}
# Revisions the declaration written here holds, as they are read
REVISION = {"all": "1", "u24": "2", "u24-x86_64": "3"}
# Revision section of test/container.yaml, which is all of it that the build reads
REVISION_YAML = "revision:\n all: '1'\n u24: '2'\n u24-x86_64: '3'\n"
# The base script for a vm that installs from the PostgreSQL repo and collects coverage, i.e. one that exercises every option the
# debian build has
@@ -142,10 +146,12 @@ def _collapse(script):
####################################################################################################################################
def _repo_write(path):
"""Write the part of the repository the vm build reads."""
def _repo_write(path, container=REVISION_YAML):
"""Write the part of the repository the vm build reads.
file_write(os.path.join(path, "test/container.yaml"), "all: '1'\n%s: '2'\n%s-%s: '3'\n" % (VM_U24, VM_U24, VM_ARCH_X86_64))
The vm definitions are read from the repository this test is part of, so only the revisions are written here."""
file_write(os.path.join(path, "test/container.yaml"), container)
file_write(
os.path.join(path, "test/certificate/pgbackrest-test-ca.crt"),
"-----BEGIN CERTIFICATE-----\nQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9w\n-----END CERTIFICATE-----\n",
@@ -210,8 +216,8 @@ def test_container_script():
with tempfile.TemporaryDirectory() as path:
_repo_write(path)
assert_equal(script_base(VM_U24, VM_ARCH_X86_64), SCRIPT_BASE_U24)
assert_equal(_collapse(script_test(VM_U24, path)), _expect(SCRIPT_TEST_U24))
assert_equal(script_base("u24", VM_ARCH_X86_64), SCRIPT_BASE_U24)
assert_equal(_collapse(script_test("u24", path)), _expect(SCRIPT_TEST_U24))
# The script is wrapped in a Dockerfile that runs it as a single layer
assert_equal(
@@ -228,7 +234,7 @@ def test_container_script_vm():
"""Each vm installs what its os base and PostgreSQL repository need."""
# RHEL 8 has no valgrind or coverage package and needs a python that PyYAML is packaged for
script = script_base(VM_RH8, VM_ARCH_X86_64)
script = script_base("rh8", VM_ARCH_X86_64)
assert_true("--set-enabled powertools" in script)
assert_true("valgrind" not in script)
@@ -242,49 +248,57 @@ def test_container_script_vm():
# Every RHEL vm tunes dnf to fail over from a slow mirror
assert_true("echo fastestmirror=True; } >> /etc/dnf/dnf.conf" in script)
# RHEL 9 packages a coverage too old to report the branch detail the report needs, so it comes from pip
script = script_base(VM_RH9, VM_ARCH_X86_64)
# RHEL 9 packages a coverage too old to report the branch detail the report needs, so it comes from pip. It still has dnf
# modularity, so the module that would shadow the PostgreSQL repo packages is disabled.
script = script_base("rh9", VM_ARCH_X86_64)
assert_true("python3-pip" in script)
assert_true("pip3 install --no-cache-dir 'coverage>=6.5'" in script)
assert_true("RPM-GPG-KEY-PGDG" in script)
assert_true("reporpms/EL-9-%s/" % VM_ARCH_X86_64 in script)
assert_true("reporpms/EL-9-%s/pgdg-redhat-repo-latest" % VM_ARCH_X86_64 in script)
assert_true("dnf -qy module disable postgresql" in script)
assert_true("ENV PATH=/usr/pgsql-18/bin:$PATH" in script)
# RHEL 10 cannot check the PostgreSQL repo signature so it skips the key and disables the check
script = script_base(VM_RH10, VM_ARCH_X86_64)
script = script_base("rh10", VM_ARCH_X86_64)
assert_true("python3-coverage" in script)
assert_true("RPM-GPG-KEY-PGDG" not in script)
assert_true("s/gpgcheck=1/gpgcheck=0/g" in script)
assert_true("dnf -qy module disable postgresql" not in script)
# Fedora installs from the Fedora build of the PostgreSQL repo
assert_true("reporpms/F-44-%s/" % VM_ARCH_X86_64 in script_base(VM_F44, VM_ARCH_X86_64))
# Fedora installs from the Fedora build of the PostgreSQL repo, which is named for the distribution rather than for RHEL
script = script_base("f44", VM_ARCH_X86_64)
assert_true("reporpms/F-44-%s/pgdg-fedora-repo-latest" % VM_ARCH_X86_64 in script)
# Fedora ships the packages that RHEL puts in EPEL and CRB
assert_true("epel-release" not in script)
# Debian 12 installs PostgreSQL from the distribution and runs neither valgrind nor coverage
script = script_base(VM_D12, VM_ARCH_X86_64)
script = script_base("d12", VM_ARCH_X86_64)
assert_true("valgrind" not in script)
assert_true("python3-coverage" not in script)
assert_true("apt.postgresql.org.sh" not in script)
# Ubuntu 22.04 is the last release the python tests do not run on
assert_true("python3-coverage" not in script_base(VM_U22, VM_ARCH_X86_64))
assert_true("python3-coverage" not in script_base("u22", VM_ARCH_X86_64))
# Alpine installs everything from one package manager and needs no PostgreSQL repo
script = script_base(VM_A321, VM_ARCH_X86_64)
script = script_base("a321", VM_ARCH_X86_64)
assert_true("apk add --no-cache postgresql15 postgresql16 postgresql17" in script)
assert_true("Install PostgreSQL packages" not in script)
# An architecture the PostgreSQL repo does not build for installs one version for the smoke test
script = script_base(VM_U24, VM_ARCH_PPC64LE)
script = script_base("u24", VM_ARCH_PPC64LE)
assert_true("apt.postgresql.org.sh -y &&" in script)
assert_true("Install a single PostgreSQL version for the smoke test" in script)
# There is nothing to install for alpine on that architecture, since it has no smoke test
assert_true("Install PostgreSQL" not in script_base(VM_A324, VM_ARCH_PPC64LE))
assert_true("Install PostgreSQL" not in script_base("a324", VM_ARCH_PPC64LE))
####################################################################################################################################
@@ -298,7 +312,7 @@ def test_container_script_test_vm():
# architecture is the host architecture rather than the one being built for, so it is set here to get the same script
# whichever machine the tests are run on.
with patch("command.vm.build.host_arch", return_value=VM_ARCH_X86_64):
script = script_test(VM_RH9, path)
script = script_test("rh9", path)
assert_true("/etc/pki/ca-trust/source/anchors/pgbackrest-test-ca.crt" in script)
assert_true("update-ca-trust extract" in script)
@@ -310,55 +324,68 @@ def test_container_script_test_vm():
assert_true("rm /etc/profile.d/lang.sh" not in script)
with patch("command.vm.build.host_arch", return_value=VM_ARCH_AARCH64):
assert_true("rm /etc/profile.d/lang.sh" in script_test(VM_RH9, path))
assert_true("rm /etc/profile.d/lang.sh" in script_test("rh9", path))
# Alpine creates the group differently, needs a password for the test user, and starts ssh without a service
script = script_test(VM_A321, path)
script = script_test("a321", path)
assert_true("getent group %s >/dev/null || addgroup" % group_name() in script)
assert_true("passwd -d '%s' %s" % (user_name(), user_name()) in script)
assert_true("ENTRYPOINT /usr/sbin/sshd -D" in script)
# Only the releases with an ssh that rejects the test key by default need the algorithms added back
assert_true("PubkeyAcceptedAlgorithms" not in script_test(VM_D12, path))
assert_true("PubkeyAcceptedAlgorithms" not in script_test("d12", path))
####################################################################################################################################
def _revision(section):
"""The revisions in a declaration, written as they are in test/container.yaml."""
return revision_check(yaml_load(section, "test/container.yaml"))
####################################################################################################################################
def test_container_revision():
"""A revision must be readable and name a vm that exists, else a rebuild would silently never happen."""
assert_is_none(revision_check(REVISION))
assert_equal(_revision("all: '1'\nu24: '2'\nu24-x86_64: '3'\n"), REVISION)
with assert_raises(ToolError) as error:
revision_check(["1"])
_revision("- '1'\n")
assert_equal(str(error.exception), "the 'revision' section in test/container.yaml must be a map")
# A repeated key is kept by the loader, so a revision that was written twice is reported rather than silently taking the last
with assert_raises(ToolError) as error:
_revision("all: '1'\nall: '2'\n")
assert_equal(str(error.exception), "the 'revision' section in test/container.yaml has duplicate key 'all'")
with assert_raises(ToolError) as error:
_revision("u24: '1'\n")
assert_equal(str(error.exception), "the 'all' revision is required in test/container.yaml")
with assert_raises(ToolError) as error:
revision_check({VM_U24: "1"})
_revision("all: '1'\nu24:\n")
assert_equal(str(error.exception), "the 'all' revision is required in test/container.yaml")
assert_equal(str(error.exception), "revision 'u24' in test/container.yaml must be set to a value")
with assert_raises(ToolError) as error:
revision_check({VM_ALL: "1", VM_U24: None})
assert_equal(str(error.exception), "revision '%s' in test/container.yaml must be set to a value" % VM_U24)
with assert_raises(ToolError) as error:
revision_check({VM_ALL: "1", "bogus": "1"})
_revision("all: '1'\nbogus: '1'\n")
assert_equal(str(error.exception), "revision 'bogus' in test/container.yaml has invalid vm 'bogus'")
# There is no container for none so a revision for it could never be used
with assert_raises(ToolError) as error:
revision_check({VM_ALL: "1", "none": "1"})
_revision("all: '1'\nnone: '1'\n")
assert_equal(str(error.exception), "revision 'none' in test/container.yaml has invalid vm 'none'")
with assert_raises(ToolError) as error:
revision_check({VM_ALL: "1", "%s-bogus" % VM_U24: "1"})
_revision("all: '1'\nu24-bogus: '1'\n")
assert_equal(str(error.exception), "revision '%s-bogus' in test/container.yaml has invalid architecture 'bogus'" % VM_U24)
assert_equal(str(error.exception), "revision 'u24-bogus' in test/container.yaml has invalid architecture 'bogus'")
####################################################################################################################################
@@ -368,8 +395,8 @@ def test_vm_build():
with tempfile.TemporaryDirectory() as path:
_repo_write(path)
image_base = "%s:%s-base-%s" % (container_repo(), VM_U24, VM_ARCH_X86_64)
image_test = "%s:%s-test-%s" % (container_repo(), VM_U24, VM_ARCH_X86_64)
image_base = "%s:%s-base-%s" % (container_repo(), "u24", VM_ARCH_X86_64)
image_test = "%s:%s-test-%s" % (container_repo(), "u24", VM_ARCH_X86_64)
# The base image is in the cache so only the test image is built. The revision for the vm and architecture is the most
# specific one so it is the one in the cache tag.
@@ -377,7 +404,7 @@ def test_vm_build():
with patch("command.vm.build.exec_one", docker.one):
with patch("command.vm.build.exec_status", docker.status_get):
cmd_vm_build(_Config(path, VM_U24))
cmd_vm_build(_Config(path, "u24"))
assert_equal(len(docker.match("^docker pull .*-base-.*-3-")), 1)
assert_equal(docker.match("^docker tag"), ["docker tag %s-3-%s %s" % (image_base, _hash(docker), image_base)])
@@ -385,7 +412,7 @@ def test_vm_build():
assert_true(docker.match("^docker build")[0].endswith("-t %s %s" % (image_test, os.path.join(path, "test"))))
# The Dockerfile is written where the build can find it and is the script with the trailing separator removed
assert_true(file_read(os.path.join(path, "test/result/docker/%s-test-%s" % (VM_U24, VM_ARCH_X86_64))).endswith("bash\n"))
assert_true(file_read(os.path.join(path, "test/result/docker/%s-test-%s" % ("u24", VM_ARCH_X86_64))).endswith("bash\n"))
# Nothing is in the cache so the base image is built and pushed. A fork pushes to its own cache since it cannot write to
# the pgbackrest one.
@@ -394,7 +421,7 @@ def test_vm_build():
with patch("command.vm.build.exec_one", docker.one):
with patch("command.vm.build.exec_status", docker.status_get):
with patch.dict(os.environ, {"GITHUB_REPOSITORY_OWNER": "Someone"}):
cmd_vm_build(_Config(path, VM_U24, log_level=DETAIL))
cmd_vm_build(_Config(path, "u24", log_level=DETAIL))
assert_equal(len(docker.match("^docker pull %s:" % container_repo())), 1)
assert_equal(len(docker.match("^docker pull %s:" % container_repo("someone"))), 1)
@@ -406,7 +433,7 @@ def test_vm_build():
with patch("command.vm.build.exec_one", docker.one):
with patch("command.vm.build.exec_status", docker.status_get):
cmd_vm_build(_Config(path, VM_D12))
cmd_vm_build(_Config(path, "d12"))
assert_equal(len(docker.match("^docker push %s:" % container_repo())), 1)
@@ -416,7 +443,7 @@ def test_vm_build():
with patch("command.vm.build.exec_one", docker.one):
with patch("command.vm.build.exec_status", docker.status_get):
cmd_vm_build(_Config(path, VM_U24, cache=False))
cmd_vm_build(_Config(path, "u24", cache=False))
assert_equal(len(docker.match("^docker pull")), 0)
assert_equal(len(docker.match("--no-cache")), 2)
@@ -441,6 +468,16 @@ def test_vm_build():
assert_equal(str(error.exception), "no definition for vm 'bogus'")
# The revisions come from the repository being built, so a declaration without them is reported rather than every image
# silently being keyed on nothing
with tempfile.TemporaryDirectory() as path:
_repo_write(path, container="vm:\n u24:\n os-base: debian\n")
with assert_raises(ToolError) as error:
cmd_vm_build(_Config(path, "u24"))
assert_equal(str(error.exception), "the 'revision' section is required in test/container.yaml")
####################################################################################################################################
def _hash(docker):

View File

@@ -1,7 +1,8 @@
"""Test Virtual Machine Definitions.
The definitions in the module are checked when it is imported, so a broken definition would fail before any test here runs. The
checks are exercised again with definitions built here, since the real ones can only ever be valid."""
The definitions in test/container.yaml are read and checked when the module is imported, so a broken declaration would fail before
any test here runs. The parsing and the checks are exercised again with declarations written here, since the real one can only ever
be valid."""
####################################################################################################################################
from harness.test import *
@@ -9,6 +10,18 @@ from harness.test import *
from common.error import *
from common.vm import *
# A declaration with one vm in it, which is enough for the keys that are read one vm at a time
VM_ONE = """
vm:
u24:
default: true
os-base: debian
image: ubuntu:24.04
db: [17, 18]
db-test: [18]
coverage-c: true
"""
####################################################################################################################################
def _vm_map(db_test_list=None, coverage_c=True):
@@ -16,10 +29,15 @@ def _vm_map(db_test_list=None, coverage_c=True):
Every supported version is tested on the first vm unless the caller says otherwise."""
result = {vm: Vm(VM_OS_BASE_DEBIAN, []) for vm in VM_LIST}
result[VM_LIST[0]] = Vm(
VM_OS_BASE_DEBIAN, list(PG_VERSION_LIST) if db_test_list is None else db_test_list, coverage_c=coverage_c
)
result = {}
for name in VM_LIST:
vm = Vm(name)
vm.default = True
result[name] = vm
result[VM_LIST[0]].db_test_list = list(PG_VERSION_LIST) if db_test_list is None else db_test_list
result[VM_LIST[0]].coverage_c = coverage_c
return result
@@ -28,25 +46,33 @@ def _vm_map(db_test_list=None, coverage_c=True):
def test_vm_get():
"""A vm definition is found by name and a name with no definition is an error."""
assert_true(vm_valid(VM_U24))
assert_true(vm_valid("u24"))
assert_false(vm_valid("bogus"))
vm = vm_get(VM_U24)
vm = vm_get("u24")
assert_equal(vm.name, "u24")
assert_equal(vm.os_base, VM_OS_BASE_DEBIAN)
assert_equal(vm.image, "ubuntu:24.04")
assert_true(vm.pg_repo)
assert_true(vm.coverage_c)
# A vm that does not install from the PostgreSQL repo and tests every version it installs
vm = vm_get(VM_D12)
vm = vm_get("d12")
assert_false(vm.pg_repo)
assert_false(vm.coverage_c)
assert_equal(vm.db_list, vm.db_test_list)
# There is no container for none, since it runs on the host
assert_is_none(vm_get(VM_NONE).image)
# There is no container for none, since it runs on the host, and it is not a vm --vm=all selects. What would be installed in a
# container is not declared for it, and neither is an os base since the os is whatever the host is.
vm = vm_get(VM_NONE)
assert_is_none(vm.image)
assert_is_none(vm.os_base)
assert_equal(vm.db_list, [])
assert_true(vm.coverage_c)
assert_true(VM_NONE not in VM_LIST)
with assert_raises(ToolError) as error:
vm_get("bogus")
@@ -54,6 +80,123 @@ def test_vm_get():
assert_equal(str(error.exception), "no definition for vm 'bogus'")
####################################################################################################################################
def _parse(key):
"""Parse a vm that has a container, since that is what all but a couple of the keys are for."""
return vm_parse("vm:\n u24:\n image: ubuntu:24.04\n%s" % key)["u24"]
####################################################################################################################################
def test_vm_parse():
"""A definition is read as it is declared and what it does not declare gets a default."""
vm = vm_parse(VM_ONE)["u24"]
assert_true(vm.default)
assert_equal(vm.os_base, VM_OS_BASE_DEBIAN)
assert_equal(vm.image, "ubuntu:24.04")
assert_equal(vm.db_list, ["17", "18"])
assert_equal(vm.db_test_list, ["18"])
assert_true(vm.coverage_c)
# Everything that was not declared, which is what a vm that needs nothing installed or configured gets
assert_true(vm.pg_repo)
assert_true(vm.pg_repo_key)
assert_is_none(vm.pg_repo_release)
assert_false(vm.dnf_module)
assert_is_none(vm.coverage_python)
assert_true(vm.valgrind)
assert_false(vm.epel)
assert_false(vm.powertools)
assert_is_none(vm.python)
assert_false(vm.ssh_rsa)
# Integration tests run against every version installed when the vm does not say which
vm = _parse(" os-base: debian\n db: [17, 18]\n")
assert_equal(vm.db_test_list, ["17", "18"])
assert_false(vm.default)
# An unreleased version is only in the beta repository, so the container build is told which installed version needs it
assert_is_none(vm.pg_beta)
assert_true(vm_get("u24").pg_beta in vm_get("u24").db_list)
# A vm with no image has no container, so it declares only whether it is a default and whether it collects C coverage
vm = vm_parse("vm:\n none:\n coverage-c: true\n")["none"]
assert_is_none(vm.image)
assert_is_none(vm.os_base)
assert_equal(vm.db_list, [])
assert_equal(vm.db_test_list, [])
assert_true(vm.coverage_c)
####################################################################################################################################
def test_vm_parse_error():
"""A declaration that is not one is reported, since a key that is ignored would silently change what is installed."""
with assert_raises(ToolError) as error:
vm_parse("bogus:\n")
assert_equal(str(error.exception), "the 'vm' section is required in test/container.yaml")
with assert_raises(ToolError) as error:
vm_parse("vm:\n - u24\n")
assert_equal(str(error.exception), "the 'vm' section in test/container.yaml must be a map")
with assert_raises(ToolError) as error:
vm_parse("vm:\n u24: bogus\n")
assert_equal(str(error.exception), "vm 'u24' must be a map")
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n bogus: true\n")
assert_equal(str(error.exception), "unknown key 'bogus' in vm 'u24'")
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n default: yes\n")
assert_equal(str(error.exception), "invalid boolean 'yes' for vm 'u24' key 'default'")
# A container has to be built on something, so an os base that is not one of them, or none at all, is reported the same way
with assert_raises(ToolError) as error:
_parse(" os-base: bogus\n")
assert_equal(str(error.exception), "vm 'u24' must declare one of these os bases: alpine, debian, rhel")
# A vm with no container is not built, so a key that says how one would be built could never be used
with assert_raises(ToolError) as error:
vm_parse("vm:\n none:\n os-base: debian\n")
assert_equal(str(error.exception), "vm 'none' has no container so it may not declare 'os-base'")
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n coverage-python: bogus\n")
assert_equal(str(error.exception), "invalid python coverage 'bogus' in vm 'u24'")
# A version written on its own is a string, which would be read as a list of the characters in it
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n db: 17\n")
assert_equal(str(error.exception), "vm 'u24' key 'db' must be a list")
# A version no interface is generated for could never be tested, so it is a typo rather than a version
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n db: [17, 99]\n")
assert_equal(str(error.exception), "PostgreSQL 99 in vm 'u24' is not a supported version")
# A version that is tested but not installed would be selected for an integration test that could never run
with assert_raises(ToolError) as error:
_parse(" os-base: debian\n db: [17]\n db-test: [18]\n")
assert_equal(str(error.exception), "PostgreSQL 18 tested on vm 'u24' is not installed there")
####################################################################################################################################
def test_vm_check():
"""Definitions that leave a version or C coverage untested are an error."""
@@ -68,7 +211,7 @@ def test_vm_check():
# A version tested on more than one vm is an error since the extra run is just slower with nothing added
vm_map = _vm_map()
vm_map[VM_LIST[1]] = Vm(VM_OS_BASE_DEBIAN, [PG_VERSION_LIST[0]])
vm_map[VM_LIST[1]].db_test_list = [PG_VERSION_LIST[0]]
with assert_raises(ToolError) as error:
vm_check(vm_map)