pg-maker 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pg_maker/__init__.py +3 -0
- pg_maker/__main__.py +3 -0
- pg_maker/api.py +594 -0
- pg_maker/artifacts/__init__.py +4 -0
- pg_maker/artifacts/catalog.py +67 -0
- pg_maker/artifacts/manifest.py +301 -0
- pg_maker/assets.py +93 -0
- pg_maker/builders/__init__.py +4 -0
- pg_maker/builders/autotools.py +430 -0
- pg_maker/builders/components.py +359 -0
- pg_maker/bundled/PROJECT.md +57 -0
- pg_maker/bundled/build.yaml +29 -0
- pg_maker/bundled/components/pg-stat-kcache.yaml +30 -0
- pg_maker/bundled/components/pg-stat-statements.yaml +24 -0
- pg_maker/bundled/components/pg-wait-sampling.yaml +30 -0
- pg_maker/bundled/config/defaults.yaml +80 -0
- pg_maker/bundled/config/documentation.yaml +24 -0
- pg_maker/bundled/config/features/my-feature.yaml +17 -0
- pg_maker/bundled/config/vendor.yaml +13 -0
- pg_maker/bundled/documentation/README.md +20 -0
- pg_maker/bundled/documentation/pg-stat-kcache.sgml +99 -0
- pg_maker/bundled/documentation/pg-stat-kcache.yaml +15 -0
- pg_maker/bundled/documentation/pg-wait-sampling.sgml +122 -0
- pg_maker/bundled/documentation/pg-wait-sampling.yaml +15 -0
- pg_maker/bundled/patches/unix-socket-directory.patch +5 -0
- pg_maker/changes/__init__.py +1 -0
- pg_maker/changes/apply.py +298 -0
- pg_maker/changes/loader.py +135 -0
- pg_maker/changes/workspace.py +221 -0
- pg_maker/cli.py +546 -0
- pg_maker/components/__init__.py +4 -0
- pg_maker/config.py +514 -0
- pg_maker/development/__init__.py +1 -0
- pg_maker/development/cli.py +104 -0
- pg_maker/development/git.py +362 -0
- pg_maker/development/session.py +735 -0
- pg_maker/documentation/__init__.py +4 -0
- pg_maker/documentation/builder.py +467 -0
- pg_maker/documentation/cli.py +24 -0
- pg_maker/documentation/config.py +127 -0
- pg_maker/documentation/install.py +79 -0
- pg_maker/documentation/site.py +166 -0
- pg_maker/documentation/sphinx_ext.py +150 -0
- pg_maker/documentation/theme/_static/pg-maker.css +26 -0
- pg_maker/documentation/theme/_static/pg-maker.js +35 -0
- pg_maker/documentation/theme/_templates/footer_en.html +5 -0
- pg_maker/documentation/theme/_templates/footer_metadata.html +13 -0
- pg_maker/documentation/theme/_templates/footer_ru.html +5 -0
- pg_maker/documentation/translation.py +341 -0
- pg_maker/documentation/validation.py +132 -0
- pg_maker/documentation/worker.py +62 -0
- pg_maker/documentation/writer.py +324 -0
- pg_maker/documentation/xml.py +210 -0
- pg_maker/documents.py +118 -0
- pg_maker/errors.py +50 -0
- pg_maker/execution/__init__.py +1 -0
- pg_maker/execution/build.py +502 -0
- pg_maker/execution/cache.py +287 -0
- pg_maker/execution/cancellation.py +60 -0
- pg_maker/execution/checkpoints.py +218 -0
- pg_maker/execution/cleanup.py +122 -0
- pg_maker/execution/component_cache.py +164 -0
- pg_maker/execution/development.py +109 -0
- pg_maker/execution/engine.py +171 -0
- pg_maker/execution/events.py +65 -0
- pg_maker/execution/incremental.py +178 -0
- pg_maker/execution/preparation.py +130 -0
- pg_maker/execution/process.py +155 -0
- pg_maker/execution/resources.py +41 -0
- pg_maker/execution/state.py +82 -0
- pg_maker/execution/worker.py +45 -0
- pg_maker/identity.py +116 -0
- pg_maker/integrations/__init__.py +1 -0
- pg_maker/integrations/machine.py +163 -0
- pg_maker/integrations/runtime.py +387 -0
- pg_maker/models.py +245 -0
- pg_maker/packaging/__init__.py +4 -0
- pg_maker/packaging/deb.py +337 -0
- pg_maker/packaging/service.py +165 -0
- pg_maker/packaging/spec.py +64 -0
- pg_maker/packaging/templates/deb-control.in +8 -0
- pg_maker/packaging/tree.py +239 -0
- pg_maker/planning/__init__.py +1 -0
- pg_maker/planning/documents.py +65 -0
- pg_maker/planning/environments.py +318 -0
- pg_maker/planning/graph.py +23 -0
- pg_maker/planning/plan.py +418 -0
- pg_maker/planning/resolve.py +117 -0
- pg_maker/planning/reuse.py +80 -0
- pg_maker/planning/runtime_repository.py +90 -0
- pg_maker/postgresql.py +59 -0
- pg_maker/progress.py +64 -0
- pg_maker/publishing/__init__.py +4 -0
- pg_maker/py.typed +0 -0
- pg_maker/recipes/README.md +13 -0
- pg_maker/recipes/platforms/debian-13.yaml +125 -0
- pg_maker/recipes/postgresql.yaml +114 -0
- pg_maker/runners/__init__.py +1 -0
- pg_maker/runners/base.py +29 -0
- pg_maker/runners/docker.py +312 -0
- pg_maker/schemas/build.schema.json +5 -0
- pg_maker/schemas/change-set.schema.json +5 -0
- pg_maker/schemas/component-envelope.schema.json +69 -0
- pg_maker/schemas/component.schema.json +5 -0
- pg_maker/schemas/defaults.schema.json +5 -0
- pg_maker/schemas/doc-rules.schema.json +5 -0
- pg_maker/schemas/documentation.schema.json +5 -0
- pg_maker/schemas/environment.schema.json +228 -0
- pg_maker/schemas/feature.schema.json +5 -0
- pg_maker/schemas/insertions.schema.json +5 -0
- pg_maker/schemas/lock.schema.json +3963 -0
- pg_maker/schemas/operation-input.schema.json +306 -0
- pg_maker/schemas/operation-state.schema.json +127 -0
- pg_maker/schemas/plan.schema.json +284 -0
- pg_maker/schemas/planning-common.schema.json +8192 -0
- pg_maker/schemas/platform.schema.json +5 -0
- pg_maker/schemas/postgresql-catalog.schema.json +5 -0
- pg_maker/schemas/run-event.schema.json +72 -0
- pg_maker/schemas/translations.schema.json +5 -0
- pg_maker/schemas/vendor.schema.json +5 -0
- pg_maker/security.py +73 -0
- pg_maker/sources/__init__.py +1 -0
- pg_maker/sources/resolver.py +238 -0
- pg_maker/sources/store.py +279 -0
- pg_maker/storage.py +127 -0
- pg_maker/validation/__init__.py +4 -0
- pg_maker/validation/installed.py +667 -0
- pg_maker/validation/packages.py +309 -0
- pg_maker/validation/systemctl.py +185 -0
- pg_maker/yamlio.py +54 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/CHANGELOG.md +22 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/CONTRIBUTING.md +63 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/LICENSE +21 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/README.md +163 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/architecture.md +55 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development-storage.md +72 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development.md +86 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/README.md +32 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/api.md +75 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/build.md +112 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/changes.md +58 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/cleanup.md +17 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/components.md +39 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/configuration.md +83 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/development.md +88 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documentation.md +241 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documents.md +42 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/environments.md +40 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/execution.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/machine.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/observability.md +28 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/packaging.md +107 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/planning.md +63 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/postgresql.md +79 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/publication.md +20 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/recovery.md +83 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/runtime.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/sources.md +48 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/state.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/storage.md +24 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/releasing.md +98 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/reuse.md +24 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/roadmap.md +54 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/default-components.md +56 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/development-release.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/documentation-theme.md +45 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/integration.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/postgresql-majors.md +107 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/review-fixes.md +68 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage4.md +65 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage56.md +66 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage7.md +72 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/guc/README.md +11 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage4/README.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage7/README.md +16 -0
- pg_maker-0.1.0.dist-info/METADATA +200 -0
- pg_maker-0.1.0.dist-info/RECORD +181 -0
- pg_maker-0.1.0.dist-info/WHEEL +5 -0
- pg_maker-0.1.0.dist-info/entry_points.txt +4 -0
- pg_maker-0.1.0.dist-info/licenses/LICENSE +21 -0
- pg_maker-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Autotools execution policy and the PostgreSQL 18 DB-test evidence adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from pg_maker.errors import CommandError, PreconditionError
|
|
9
|
+
from pg_maker.storage import read_json, write_json
|
|
10
|
+
|
|
11
|
+
# Evaluated by make inside the build container, after configure. No controller-side make.
|
|
12
|
+
INVENTORY_MAKE = """
|
|
13
|
+
.PHONY: maker-inventory
|
|
14
|
+
maker-inventory:
|
|
15
|
+
\t@$(info PG_MAKER_SUBDIRS=$(SUBDIRS))true
|
|
16
|
+
\t@$(info PG_MAKER_REGRESS=$(REGRESS))true
|
|
17
|
+
\t@$(info PG_MAKER_ISOLATION=$(ISOLATION))true
|
|
18
|
+
\t@$(info PG_MAKER_REGRESS_OPTS=$(REGRESS_OPTS))true
|
|
19
|
+
\t@$(info PG_MAKER_TAP=$(TAP_TESTS))true
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# TAP::Parser supplies machine evidence, independent of make/prove exit status and console format.
|
|
23
|
+
TAP_FORMATTER = r"""
|
|
24
|
+
package PgMaker::Formatter;
|
|
25
|
+
use strict;
|
|
26
|
+
use warnings;
|
|
27
|
+
use parent 'TAP::Formatter::Console';
|
|
28
|
+
sub open_test {
|
|
29
|
+
my ($self, $name, $parser) = @_;
|
|
30
|
+
my $session = $self->SUPER::open_test($name, $parser);
|
|
31
|
+
bless $session, 'PgMaker::Session';
|
|
32
|
+
return $session;
|
|
33
|
+
}
|
|
34
|
+
package PgMaker::Session;
|
|
35
|
+
use parent 'TAP::Formatter::Console::Session';
|
|
36
|
+
use JSON::PP;
|
|
37
|
+
use Cwd qw(abs_path);
|
|
38
|
+
use Digest::SHA qw(sha256_hex);
|
|
39
|
+
sub close_test {
|
|
40
|
+
my ($self) = @_;
|
|
41
|
+
my $p = $self->parser;
|
|
42
|
+
my $name = abs_path($self->name);
|
|
43
|
+
my $root = $ENV{PG_MAKER_SOURCE};
|
|
44
|
+
die 'test outside source' unless index($name, "$root/") == 0;
|
|
45
|
+
my $rel = substr($name, length($root) + 1);
|
|
46
|
+
my $out = $ENV{PG_MAKER_TAP} . '/' . sha256_hex($rel) . '.json';
|
|
47
|
+
open(my $fh, '>', "$out.$$") or die $!;
|
|
48
|
+
print $fh JSON::PP->new->canonical->encode({
|
|
49
|
+
path => $rel, tests => 0 + $p->tests_run, planned => 0 + ($p->tests_planned // 0),
|
|
50
|
+
failed => [$p->failed], skipped => [$p->skipped], errors => [$p->parse_errors],
|
|
51
|
+
exit => 0 + $p->exit, wait => 0 + $p->wait, skip_all => ($p->skip_all // ''),
|
|
52
|
+
});
|
|
53
|
+
close($fh) or die $!;
|
|
54
|
+
rename("$out.$$", $out) or die $!;
|
|
55
|
+
$self->SUPER::close_test;
|
|
56
|
+
}
|
|
57
|
+
1;
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extra_tests(build: dict, recipe: dict) -> str:
|
|
62
|
+
# GSSAPI/LDAP/OAuth need additional service fixtures; refuse their configure options below.
|
|
63
|
+
return " ".join([*(["ssl"] if build["options"]["openssl"] else []), *recipe["extra_tests"]])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def verify_configure(argv, settings: str, header: str, output: str) -> dict:
|
|
67
|
+
"""Check the effective Autoconf result before any compilation."""
|
|
68
|
+
if re.search(r"unrecognized (?:options?|arguments?)", output, re.I):
|
|
69
|
+
raise PreconditionError("configure reported unrecognized options")
|
|
70
|
+
values = dict(re.findall(r"^([A-Za-z_]+)\s*:?=\s*(.*?)\s*$", settings, re.M))
|
|
71
|
+
macros = set(re.findall(r"^#define ([A-Z_0-9]+)\s+1$", header, re.M))
|
|
72
|
+
expected = {"enable_tap_tests": "yes"}
|
|
73
|
+
macro_options = {
|
|
74
|
+
"lz4": "USE_LZ4",
|
|
75
|
+
"zstd": "USE_ZSTD",
|
|
76
|
+
"pam": "USE_PAM",
|
|
77
|
+
"cassert": "USE_ASSERT_CHECKING",
|
|
78
|
+
}
|
|
79
|
+
for arg in argv[1:]:
|
|
80
|
+
if arg.startswith("--prefix="):
|
|
81
|
+
expected["prefix"] = arg.split("=", 1)[1]
|
|
82
|
+
match = re.fullmatch(r"--(with|without|enable|disable)-([a-z0-9-]+)(?:=(.*))?", arg)
|
|
83
|
+
if not match:
|
|
84
|
+
continue
|
|
85
|
+
action, name, value = match.groups()
|
|
86
|
+
enabled = action in {"with", "enable"} and value != "no"
|
|
87
|
+
if name in macro_options:
|
|
88
|
+
if (macro_options[name] in macros) != enabled:
|
|
89
|
+
raise PreconditionError(
|
|
90
|
+
"configured option differs from plan", details={"option": name}
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
key = ("with_" if action in {"with", "without"} else "enable_") + name.replace("-", "_")
|
|
94
|
+
if key in values or name in {
|
|
95
|
+
"icu",
|
|
96
|
+
"perl",
|
|
97
|
+
"python",
|
|
98
|
+
"tcl",
|
|
99
|
+
"ssl",
|
|
100
|
+
"readline",
|
|
101
|
+
"systemd",
|
|
102
|
+
"gssapi",
|
|
103
|
+
"ldap",
|
|
104
|
+
"libxml",
|
|
105
|
+
"libxslt",
|
|
106
|
+
"llvm",
|
|
107
|
+
"uuid",
|
|
108
|
+
"nls",
|
|
109
|
+
"tap-tests",
|
|
110
|
+
"debug",
|
|
111
|
+
}:
|
|
112
|
+
expected[key] = (value if value and name != "nls" else "yes") if enabled else "no"
|
|
113
|
+
mismatch = {
|
|
114
|
+
key: {"expected": value, "actual": values.get(key)}
|
|
115
|
+
for key, value in expected.items()
|
|
116
|
+
if values.get(key) != value
|
|
117
|
+
}
|
|
118
|
+
if mismatch:
|
|
119
|
+
raise PreconditionError("configured options differ from plan", details=mismatch)
|
|
120
|
+
return {"passed": True, "settings": expected}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_build(config: dict, goal: str, *, run_db_tests: bool = True) -> None:
|
|
124
|
+
from pg_maker.errors import UnsupportedError
|
|
125
|
+
|
|
126
|
+
if goal not in {
|
|
127
|
+
"source",
|
|
128
|
+
"build",
|
|
129
|
+
"package",
|
|
130
|
+
"verified",
|
|
131
|
+
"docs",
|
|
132
|
+
"translations",
|
|
133
|
+
"doc-source",
|
|
134
|
+
"translated-docs",
|
|
135
|
+
}:
|
|
136
|
+
raise UnsupportedError("unsupported build goal")
|
|
137
|
+
if (
|
|
138
|
+
run_db_tests
|
|
139
|
+
and goal != "source"
|
|
140
|
+
and config["postgresql_recipe"]["db_test_adapter"] != "pg18"
|
|
141
|
+
):
|
|
142
|
+
raise UnsupportedError(
|
|
143
|
+
"DB-test evidence adapter is not implemented for this PostgreSQL major; "
|
|
144
|
+
"build/package with DB tests disabled remain available"
|
|
145
|
+
)
|
|
146
|
+
if (
|
|
147
|
+
run_db_tests
|
|
148
|
+
and goal != "source"
|
|
149
|
+
and config["defaults"]["validation"]["profile"] == "release"
|
|
150
|
+
):
|
|
151
|
+
raise UnsupportedError("extended release test fixtures are not implemented")
|
|
152
|
+
for variant in config["variants"]:
|
|
153
|
+
if variant["build"]["backend"] != "autotools":
|
|
154
|
+
raise UnsupportedError("only Autotools execution is implemented")
|
|
155
|
+
for arg in variant["build"]["configure_args"]:
|
|
156
|
+
if run_db_tests and arg.startswith(
|
|
157
|
+
("--with-gssapi", "--with-ldap", "--with-oauth", "--with-curl")
|
|
158
|
+
):
|
|
159
|
+
raise UnsupportedError(
|
|
160
|
+
"this configure option requires additional mandatory test fixtures"
|
|
161
|
+
)
|
|
162
|
+
for feature in config["features"]:
|
|
163
|
+
for component in feature["spec"]["components"]:
|
|
164
|
+
spec = component["spec"]
|
|
165
|
+
if (
|
|
166
|
+
run_db_tests
|
|
167
|
+
and spec["type"] in {"pgxs", "utility", "library"}
|
|
168
|
+
and not spec["test_targets"]
|
|
169
|
+
):
|
|
170
|
+
raise PreconditionError("external component must declare native test targets")
|
|
171
|
+
if (
|
|
172
|
+
goal == "verified"
|
|
173
|
+
and spec["type"] in {"utility", "library"}
|
|
174
|
+
and not spec.get("installed_checks")
|
|
175
|
+
):
|
|
176
|
+
raise PreconditionError("installed utility/library checks must be declared")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def schedule_tests(path: Path) -> list[str]:
|
|
180
|
+
if not path.is_file():
|
|
181
|
+
raise PreconditionError(
|
|
182
|
+
"expected regression schedule is missing", details={"path": str(path)}
|
|
183
|
+
)
|
|
184
|
+
names = []
|
|
185
|
+
for line in path.read_text().splitlines():
|
|
186
|
+
if line.startswith("test:"):
|
|
187
|
+
names.extend(line.split(":", 1)[1].split())
|
|
188
|
+
if not names or len(names) != len(set(names)):
|
|
189
|
+
raise PreconditionError("empty or duplicate regression schedule")
|
|
190
|
+
return names
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def skip_policy(path: str, source: str, config: dict) -> str | None:
|
|
194
|
+
if path.endswith("/005_sspi.pl"):
|
|
195
|
+
return "SSPI tests require Windows (without PG_TEST_USE_UNIX_SOCKETS)"
|
|
196
|
+
if (
|
|
197
|
+
"Injection points not supported by this build" in source
|
|
198
|
+
and config.get("enable_injection_points") != "yes"
|
|
199
|
+
):
|
|
200
|
+
return "Injection points not supported by this build"
|
|
201
|
+
if path.startswith("src/test/modules/xid_wraparound/t/"):
|
|
202
|
+
return "test xid_wraparound not enabled in PG_TEST_EXTRA"
|
|
203
|
+
if path == "src/interfaces/libpq/t/004_load_balance_dns.pl":
|
|
204
|
+
return "Potentially unsafe test load_balance not enabled in PG_TEST_EXTRA"
|
|
205
|
+
if (
|
|
206
|
+
path.startswith("src/test/modules/oauth_validator/t/")
|
|
207
|
+
and config.get("with_oauth") != "curl"
|
|
208
|
+
):
|
|
209
|
+
return "Potentially unsafe test oauth not enabled in PG_TEST_EXTRA"
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def expected_inventory(source: Path, obj: Path, query) -> dict:
|
|
214
|
+
"""Follow configured SUBDIRS, rather than a guessed list of directories in the checkout."""
|
|
215
|
+
config = dict(
|
|
216
|
+
re.findall(
|
|
217
|
+
r"^([A-Za-z_]+)\s*=\s*(.*?)\s*$", (obj / "src/Makefile.global").read_text(), re.M
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
if config.get("enable_tap_tests") != "yes":
|
|
221
|
+
raise PreconditionError("configure did not enable mandatory TAP tests")
|
|
222
|
+
suites, visited = [], set()
|
|
223
|
+
queue = [
|
|
224
|
+
"src/test",
|
|
225
|
+
"src/pl",
|
|
226
|
+
"src/interfaces",
|
|
227
|
+
"src/interfaces/ecpg/test",
|
|
228
|
+
"contrib",
|
|
229
|
+
"src/bin",
|
|
230
|
+
"src/tools/pg_bsd_indent",
|
|
231
|
+
]
|
|
232
|
+
while queue:
|
|
233
|
+
directory = queue.pop(0)
|
|
234
|
+
if directory in visited:
|
|
235
|
+
continue
|
|
236
|
+
visited.add(directory)
|
|
237
|
+
makefile = source / directory / "Makefile"
|
|
238
|
+
if not makefile.is_file():
|
|
239
|
+
continue
|
|
240
|
+
values = query(directory)
|
|
241
|
+
for child in values["SUBDIRS"].split():
|
|
242
|
+
if child in {".", ".."} or "/" in child:
|
|
243
|
+
raise PreconditionError("unsupported configured SUBDIRS entry")
|
|
244
|
+
queue.append(directory + "/" + child)
|
|
245
|
+
regress = values["REGRESS"].split()
|
|
246
|
+
for schedule in re.findall(r"--schedule=(\S+)", values["REGRESS_OPTS"]):
|
|
247
|
+
path = Path(schedule)
|
|
248
|
+
# make expands $(srcdir) to the container source path.
|
|
249
|
+
if str(path).startswith("/work/build/"):
|
|
250
|
+
path = source / str(path).split("/source/", 1)[1]
|
|
251
|
+
elif not path.is_absolute():
|
|
252
|
+
path = source / directory / path
|
|
253
|
+
regress.extend(schedule_tests(path))
|
|
254
|
+
if directory == "src/test/regress":
|
|
255
|
+
regress = schedule_tests(source / directory / "parallel_schedule")
|
|
256
|
+
if directory == "src/test/isolation":
|
|
257
|
+
regress = schedule_tests(source / directory / "isolation_schedule")
|
|
258
|
+
if directory == "src/interfaces/ecpg/test":
|
|
259
|
+
regress = schedule_tests(source / directory / "ecpg_schedule") + ["sql/twophase"]
|
|
260
|
+
if regress:
|
|
261
|
+
suites.append(
|
|
262
|
+
{
|
|
263
|
+
"kind": "isolation" if directory == "src/test/isolation" else "regression",
|
|
264
|
+
"directory": directory,
|
|
265
|
+
"tests": regress,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
isolation = values["ISOLATION"].split()
|
|
269
|
+
if isolation:
|
|
270
|
+
suites.append({"kind": "isolation", "directory": directory, "tests": isolation})
|
|
271
|
+
if values["TAP"] == "1" or "$(prove_check)" in makefile.read_text():
|
|
272
|
+
scripts = sorted((source / directory / "t").glob("*.pl"))
|
|
273
|
+
if not scripts:
|
|
274
|
+
raise PreconditionError("configured TAP suite has no test scripts")
|
|
275
|
+
for script in scripts:
|
|
276
|
+
relative = script.relative_to(source).as_posix()
|
|
277
|
+
suites.append(
|
|
278
|
+
{
|
|
279
|
+
"kind": "tap",
|
|
280
|
+
"path": relative,
|
|
281
|
+
"allowed_skip": skip_policy(relative, script.read_text(), config),
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
if not any(s.get("directory") == "src/test/regress" for s in suites):
|
|
285
|
+
raise PreconditionError("core regression inventory is empty")
|
|
286
|
+
mandatory = {
|
|
287
|
+
"src/test/recovery",
|
|
288
|
+
"src/test/subscription",
|
|
289
|
+
"src/test/authentication",
|
|
290
|
+
"src/test/isolation",
|
|
291
|
+
"src/interfaces/libpq",
|
|
292
|
+
"contrib",
|
|
293
|
+
"src/bin",
|
|
294
|
+
}
|
|
295
|
+
if config.get("with_ssl") == "openssl":
|
|
296
|
+
mandatory.add("src/test/ssl")
|
|
297
|
+
if not mandatory <= visited:
|
|
298
|
+
raise PreconditionError("mandatory source test directories are missing")
|
|
299
|
+
if not any(s["kind"] == "tap" for s in suites):
|
|
300
|
+
raise PreconditionError("mandatory TAP inventory is empty")
|
|
301
|
+
return {
|
|
302
|
+
"api_version": "pg_maker/test-inventory/v1",
|
|
303
|
+
"suites": suites,
|
|
304
|
+
"extra_tests": config.get("PG_TEST_EXTRA", ""),
|
|
305
|
+
"not_applicable": {
|
|
306
|
+
key: "not configured"
|
|
307
|
+
for key in ("gssapi", "ldap", "oauth")
|
|
308
|
+
if config.get("with_" + key) not in {"yes", "curl"}
|
|
309
|
+
},
|
|
310
|
+
"extended_tests": (
|
|
311
|
+
"load_balance, wal_consistency_checking, xid_wraparound require release fixtures"
|
|
312
|
+
),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def regression_evidence(stdout: str) -> dict:
|
|
317
|
+
"""GNU make --output-sync=target keeps each test command's TAP block together.
|
|
318
|
+
|
|
319
|
+
pg_regress deletes regression.out on success. Preserve its streamed evidence,
|
|
320
|
+
using make's directory boundaries and PostgreSQL's suite markers.
|
|
321
|
+
"""
|
|
322
|
+
current, stack, observed = ("regression", ""), [], {}
|
|
323
|
+
for line in stdout.splitlines():
|
|
324
|
+
entering = re.search(r"Entering directory '.*?/obj(?:/(.*?))?'$", line)
|
|
325
|
+
leaving = re.search(r"Leaving directory '.*?/obj(?:/(.*?))?'$", line)
|
|
326
|
+
marker = re.match(
|
|
327
|
+
r"# \+\+\+ (regress|isolation) (?:check|install-check) in (\S*) \+\+\+", line
|
|
328
|
+
)
|
|
329
|
+
if entering:
|
|
330
|
+
directory = entering[1] or ""
|
|
331
|
+
stack.append(directory)
|
|
332
|
+
current = ("regression", directory)
|
|
333
|
+
elif leaving:
|
|
334
|
+
directory = leaving[1] or ""
|
|
335
|
+
if directory in stack:
|
|
336
|
+
stack = stack[: len(stack) - 1 - stack[::-1].index(directory)]
|
|
337
|
+
current = ("regression", stack[-1]) if stack else None
|
|
338
|
+
elif marker:
|
|
339
|
+
current = ("regression" if marker[1] == "regress" else "isolation", marker[2])
|
|
340
|
+
elif current:
|
|
341
|
+
match = re.match(r"(not )?ok\s+\d+\s+[-+]\s+(\S+)\s+\d+\s+ms$", line)
|
|
342
|
+
if match:
|
|
343
|
+
value = observed.setdefault(current, {"tests": [], "failed": [], "plans": []})
|
|
344
|
+
value["tests"].append(match[2])
|
|
345
|
+
if match[1]:
|
|
346
|
+
value["failed"].append(match[2])
|
|
347
|
+
elif re.fullmatch(r"1\.\.\d+", line) and current in observed:
|
|
348
|
+
observed[current]["plans"].append(int(line[3:]))
|
|
349
|
+
return observed
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def reconcile(
|
|
353
|
+
inventory: dict, obj: Path, tap: Path, *, core_only=False, stdout: str | None = None
|
|
354
|
+
) -> dict:
|
|
355
|
+
results, failures = [], []
|
|
356
|
+
observed = {r["path"]: r for p in sorted(tap.glob("*.json")) for r in [read_json(p)]}
|
|
357
|
+
regressions = regression_evidence(stdout) if stdout is not None else None
|
|
358
|
+
for suite in inventory["suites"]:
|
|
359
|
+
if core_only and suite.get("directory") != "src/test/regress":
|
|
360
|
+
continue
|
|
361
|
+
if suite["kind"] == "tap":
|
|
362
|
+
value = observed.get(suite["path"])
|
|
363
|
+
valid = bool(
|
|
364
|
+
value
|
|
365
|
+
and not value["failed"]
|
|
366
|
+
and not value["errors"]
|
|
367
|
+
and not value["exit"]
|
|
368
|
+
and not value["wait"]
|
|
369
|
+
)
|
|
370
|
+
if valid and value["skip_all"]:
|
|
371
|
+
valid = bool(suite["allowed_skip"] and suite["allowed_skip"] == value["skip_all"])
|
|
372
|
+
elif valid:
|
|
373
|
+
valid = (
|
|
374
|
+
value["tests"] > len(value["skipped"]) and value["planned"] == value["tests"]
|
|
375
|
+
)
|
|
376
|
+
results.append({"suite": suite, "observed": value, "passed": valid})
|
|
377
|
+
else:
|
|
378
|
+
directory = obj / suite["directory"]
|
|
379
|
+
if suite["kind"] == "isolation":
|
|
380
|
+
directory /= "output_iso"
|
|
381
|
+
output = directory / "regression.out"
|
|
382
|
+
text = output.read_text() if output.is_file() else ""
|
|
383
|
+
names = re.findall(r"^ok\s+\d+\s+[-+]\s+(\S+)", text, re.M)
|
|
384
|
+
valid = sorted(names) == sorted(suite["tests"]) and not re.search(
|
|
385
|
+
r"^not ok\b", text, re.M
|
|
386
|
+
)
|
|
387
|
+
if regressions is not None:
|
|
388
|
+
value = regressions.get((suite["kind"], suite["directory"]), {})
|
|
389
|
+
names = value.get("tests", [])
|
|
390
|
+
valid = (
|
|
391
|
+
sorted(names) == sorted(suite["tests"])
|
|
392
|
+
and value.get("plans") == [len(names)]
|
|
393
|
+
and not value.get("failed")
|
|
394
|
+
)
|
|
395
|
+
diffs = directory / "regression.diffs"
|
|
396
|
+
valid = valid and (not diffs.exists() or diffs.stat().st_size == 0)
|
|
397
|
+
results.append({"suite": suite, "observed": names, "passed": valid})
|
|
398
|
+
if not valid:
|
|
399
|
+
failures.append(suite.get("path", suite.get("directory")))
|
|
400
|
+
report = {"passed": not failures, "results": results, "failures": failures}
|
|
401
|
+
write_json(tap.parent / ("core-result.json" if core_only else "world-result.json"), report)
|
|
402
|
+
if failures:
|
|
403
|
+
raise CommandError("test inventory reconciliation failed", details={"suites": failures})
|
|
404
|
+
return report
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def verify_required_tests(
|
|
408
|
+
feature: dict, report: dict, world: dict, branch="candidate"
|
|
409
|
+
) -> list[str]:
|
|
410
|
+
from pg_maker.changes.loader import required_tests
|
|
411
|
+
|
|
412
|
+
evidence = {item["id"] for item in report.get("acceptance", []) if item["passed"]}
|
|
413
|
+
for component in report.get("components", []):
|
|
414
|
+
if component["passed"]:
|
|
415
|
+
evidence.update(
|
|
416
|
+
component["id"] + "." + target for target in component["native_targets"]
|
|
417
|
+
)
|
|
418
|
+
for result in world["results"]:
|
|
419
|
+
if not result["passed"]:
|
|
420
|
+
continue
|
|
421
|
+
suite = result["suite"]
|
|
422
|
+
if suite["kind"] == "tap":
|
|
423
|
+
if not result["observed"]["skip_all"]:
|
|
424
|
+
evidence.add(suite["path"])
|
|
425
|
+
else:
|
|
426
|
+
evidence.update(suite["directory"] + "/" + name for name in suite["tests"])
|
|
427
|
+
missing = sorted(set(required_tests(feature, branch)) - evidence)
|
|
428
|
+
if missing:
|
|
429
|
+
raise CommandError("required tests have no successful evidence", details={"tests": missing})
|
|
430
|
+
return sorted(evidence)
|