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.
Files changed (181) hide show
  1. pg_maker/__init__.py +3 -0
  2. pg_maker/__main__.py +3 -0
  3. pg_maker/api.py +594 -0
  4. pg_maker/artifacts/__init__.py +4 -0
  5. pg_maker/artifacts/catalog.py +67 -0
  6. pg_maker/artifacts/manifest.py +301 -0
  7. pg_maker/assets.py +93 -0
  8. pg_maker/builders/__init__.py +4 -0
  9. pg_maker/builders/autotools.py +430 -0
  10. pg_maker/builders/components.py +359 -0
  11. pg_maker/bundled/PROJECT.md +57 -0
  12. pg_maker/bundled/build.yaml +29 -0
  13. pg_maker/bundled/components/pg-stat-kcache.yaml +30 -0
  14. pg_maker/bundled/components/pg-stat-statements.yaml +24 -0
  15. pg_maker/bundled/components/pg-wait-sampling.yaml +30 -0
  16. pg_maker/bundled/config/defaults.yaml +80 -0
  17. pg_maker/bundled/config/documentation.yaml +24 -0
  18. pg_maker/bundled/config/features/my-feature.yaml +17 -0
  19. pg_maker/bundled/config/vendor.yaml +13 -0
  20. pg_maker/bundled/documentation/README.md +20 -0
  21. pg_maker/bundled/documentation/pg-stat-kcache.sgml +99 -0
  22. pg_maker/bundled/documentation/pg-stat-kcache.yaml +15 -0
  23. pg_maker/bundled/documentation/pg-wait-sampling.sgml +122 -0
  24. pg_maker/bundled/documentation/pg-wait-sampling.yaml +15 -0
  25. pg_maker/bundled/patches/unix-socket-directory.patch +5 -0
  26. pg_maker/changes/__init__.py +1 -0
  27. pg_maker/changes/apply.py +298 -0
  28. pg_maker/changes/loader.py +135 -0
  29. pg_maker/changes/workspace.py +221 -0
  30. pg_maker/cli.py +546 -0
  31. pg_maker/components/__init__.py +4 -0
  32. pg_maker/config.py +514 -0
  33. pg_maker/development/__init__.py +1 -0
  34. pg_maker/development/cli.py +104 -0
  35. pg_maker/development/git.py +362 -0
  36. pg_maker/development/session.py +735 -0
  37. pg_maker/documentation/__init__.py +4 -0
  38. pg_maker/documentation/builder.py +467 -0
  39. pg_maker/documentation/cli.py +24 -0
  40. pg_maker/documentation/config.py +127 -0
  41. pg_maker/documentation/install.py +79 -0
  42. pg_maker/documentation/site.py +166 -0
  43. pg_maker/documentation/sphinx_ext.py +150 -0
  44. pg_maker/documentation/theme/_static/pg-maker.css +26 -0
  45. pg_maker/documentation/theme/_static/pg-maker.js +35 -0
  46. pg_maker/documentation/theme/_templates/footer_en.html +5 -0
  47. pg_maker/documentation/theme/_templates/footer_metadata.html +13 -0
  48. pg_maker/documentation/theme/_templates/footer_ru.html +5 -0
  49. pg_maker/documentation/translation.py +341 -0
  50. pg_maker/documentation/validation.py +132 -0
  51. pg_maker/documentation/worker.py +62 -0
  52. pg_maker/documentation/writer.py +324 -0
  53. pg_maker/documentation/xml.py +210 -0
  54. pg_maker/documents.py +118 -0
  55. pg_maker/errors.py +50 -0
  56. pg_maker/execution/__init__.py +1 -0
  57. pg_maker/execution/build.py +502 -0
  58. pg_maker/execution/cache.py +287 -0
  59. pg_maker/execution/cancellation.py +60 -0
  60. pg_maker/execution/checkpoints.py +218 -0
  61. pg_maker/execution/cleanup.py +122 -0
  62. pg_maker/execution/component_cache.py +164 -0
  63. pg_maker/execution/development.py +109 -0
  64. pg_maker/execution/engine.py +171 -0
  65. pg_maker/execution/events.py +65 -0
  66. pg_maker/execution/incremental.py +178 -0
  67. pg_maker/execution/preparation.py +130 -0
  68. pg_maker/execution/process.py +155 -0
  69. pg_maker/execution/resources.py +41 -0
  70. pg_maker/execution/state.py +82 -0
  71. pg_maker/execution/worker.py +45 -0
  72. pg_maker/identity.py +116 -0
  73. pg_maker/integrations/__init__.py +1 -0
  74. pg_maker/integrations/machine.py +163 -0
  75. pg_maker/integrations/runtime.py +387 -0
  76. pg_maker/models.py +245 -0
  77. pg_maker/packaging/__init__.py +4 -0
  78. pg_maker/packaging/deb.py +337 -0
  79. pg_maker/packaging/service.py +165 -0
  80. pg_maker/packaging/spec.py +64 -0
  81. pg_maker/packaging/templates/deb-control.in +8 -0
  82. pg_maker/packaging/tree.py +239 -0
  83. pg_maker/planning/__init__.py +1 -0
  84. pg_maker/planning/documents.py +65 -0
  85. pg_maker/planning/environments.py +318 -0
  86. pg_maker/planning/graph.py +23 -0
  87. pg_maker/planning/plan.py +418 -0
  88. pg_maker/planning/resolve.py +117 -0
  89. pg_maker/planning/reuse.py +80 -0
  90. pg_maker/planning/runtime_repository.py +90 -0
  91. pg_maker/postgresql.py +59 -0
  92. pg_maker/progress.py +64 -0
  93. pg_maker/publishing/__init__.py +4 -0
  94. pg_maker/py.typed +0 -0
  95. pg_maker/recipes/README.md +13 -0
  96. pg_maker/recipes/platforms/debian-13.yaml +125 -0
  97. pg_maker/recipes/postgresql.yaml +114 -0
  98. pg_maker/runners/__init__.py +1 -0
  99. pg_maker/runners/base.py +29 -0
  100. pg_maker/runners/docker.py +312 -0
  101. pg_maker/schemas/build.schema.json +5 -0
  102. pg_maker/schemas/change-set.schema.json +5 -0
  103. pg_maker/schemas/component-envelope.schema.json +69 -0
  104. pg_maker/schemas/component.schema.json +5 -0
  105. pg_maker/schemas/defaults.schema.json +5 -0
  106. pg_maker/schemas/doc-rules.schema.json +5 -0
  107. pg_maker/schemas/documentation.schema.json +5 -0
  108. pg_maker/schemas/environment.schema.json +228 -0
  109. pg_maker/schemas/feature.schema.json +5 -0
  110. pg_maker/schemas/insertions.schema.json +5 -0
  111. pg_maker/schemas/lock.schema.json +3963 -0
  112. pg_maker/schemas/operation-input.schema.json +306 -0
  113. pg_maker/schemas/operation-state.schema.json +127 -0
  114. pg_maker/schemas/plan.schema.json +284 -0
  115. pg_maker/schemas/planning-common.schema.json +8192 -0
  116. pg_maker/schemas/platform.schema.json +5 -0
  117. pg_maker/schemas/postgresql-catalog.schema.json +5 -0
  118. pg_maker/schemas/run-event.schema.json +72 -0
  119. pg_maker/schemas/translations.schema.json +5 -0
  120. pg_maker/schemas/vendor.schema.json +5 -0
  121. pg_maker/security.py +73 -0
  122. pg_maker/sources/__init__.py +1 -0
  123. pg_maker/sources/resolver.py +238 -0
  124. pg_maker/sources/store.py +279 -0
  125. pg_maker/storage.py +127 -0
  126. pg_maker/validation/__init__.py +4 -0
  127. pg_maker/validation/installed.py +667 -0
  128. pg_maker/validation/packages.py +309 -0
  129. pg_maker/validation/systemctl.py +185 -0
  130. pg_maker/yamlio.py +54 -0
  131. pg_maker-0.1.0.data/data/share/doc/pg-maker/CHANGELOG.md +22 -0
  132. pg_maker-0.1.0.data/data/share/doc/pg-maker/CONTRIBUTING.md +63 -0
  133. pg_maker-0.1.0.data/data/share/doc/pg-maker/LICENSE +21 -0
  134. pg_maker-0.1.0.data/data/share/doc/pg-maker/README.md +163 -0
  135. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/architecture.md +55 -0
  136. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development-storage.md +72 -0
  137. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development.md +86 -0
  138. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/README.md +32 -0
  139. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/api.md +75 -0
  140. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/build.md +112 -0
  141. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/changes.md +58 -0
  142. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/cleanup.md +17 -0
  143. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/components.md +39 -0
  144. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/configuration.md +83 -0
  145. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/development.md +88 -0
  146. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documentation.md +241 -0
  147. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documents.md +42 -0
  148. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/environments.md +40 -0
  149. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/execution.md +49 -0
  150. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/machine.md +31 -0
  151. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/observability.md +28 -0
  152. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/packaging.md +107 -0
  153. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/planning.md +63 -0
  154. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/postgresql.md +79 -0
  155. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/publication.md +20 -0
  156. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/recovery.md +83 -0
  157. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/runtime.md +31 -0
  158. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/sources.md +48 -0
  159. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/state.md +49 -0
  160. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/storage.md +24 -0
  161. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/releasing.md +98 -0
  162. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/reuse.md +24 -0
  163. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/roadmap.md +54 -0
  164. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/default-components.md +56 -0
  165. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/development-release.md +49 -0
  166. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/documentation-theme.md +45 -0
  167. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/integration.md +49 -0
  168. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/postgresql-majors.md +107 -0
  169. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/review-fixes.md +68 -0
  170. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage4.md +65 -0
  171. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage56.md +66 -0
  172. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage7.md +72 -0
  173. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/guc/README.md +11 -0
  174. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage4/README.md +31 -0
  175. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage7/README.md +16 -0
  176. pg_maker-0.1.0.dist-info/METADATA +200 -0
  177. pg_maker-0.1.0.dist-info/RECORD +181 -0
  178. pg_maker-0.1.0.dist-info/WHEEL +5 -0
  179. pg_maker-0.1.0.dist-info/entry_points.txt +4 -0
  180. pg_maker-0.1.0.dist-info/licenses/LICENSE +21 -0
  181. 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)