Files
go-omni/scripts/verifyManagedRelease.py

153 lines
11 KiB
Python

#!/usr/bin/env python3
"""Accept a fixed managed-project release using a fresh remote module cache."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import signal
import tempfile
import time
from verifyErrorInheritanceRelease import run
from verifyManagedProject import serve, verify_contract_changes
ROOT = Path(__file__).resolve().parents[1]
MODULE = "git.yuntuo.hk/k2safe/go-omni"
def verify_shared_catalog(cli, temp, env, version):
workspace = temp / "workspace"
(workspace / "internal/apierrors").mkdir(parents=True)
(workspace / "contracts").mkdir()
(workspace / "go.mod").write_text(f"module example.com/releaseWorkspace\n\ngo 1.24.0\nrequire {MODULE} {version}\n")
provider = workspace / "internal/apierrors/catalog.go"
provider.write_text('package apierrors\nimport errors "' + MODULE + '/errors"\n'
'func Catalog() errors.Catalog {return errors.Catalog{DefaultStatus:400,Definitions:map[string]errors.Definition{'
'"INVALID":{Code:"INVALID",Message:"invalid"},"MISSING":{Status:404,Code:"MISSING",Message:"missing"}}}}\n')
run(["go", "mod", "tidy"], workspace, env)
for name in ("api", "id"):
service = workspace / "services" / name
run([cli, "new", "--module", "example.com/" + name, service], workspace, env)
(service / "api/omni.imports.json").write_text(json.dumps({
"roots": {"common": {"path": "../../../contracts"}},
"errors": {"source": "@common/errors.json", "include": ["INVALID"],
"routes": {"GET /api/greet/:name": ["MISSING"]}}}))
manifest = workspace / "omni.workspace.json"
manifest.write_text(json.dumps({"errorCatalogs": [{"dir": ".", "package": "example.com/releaseWorkspace/internal/apierrors",
"function": "Catalog", "output": "contracts/errors.json"}],
"services": [{"dir": "services/api"}, {"dir": "services/id"}]}))
for command in ("generate", "format", "generate", "check"):
run([cli, "workspace", command, "--file", manifest], workspace, env)
for name in ("api", "id"):
service = workspace / "services" / name
assert not (service / "api/omni.errors.json").exists()
assert not (service / "internal/apierrors/errorsGen.go").exists()
doc = json.loads((service / "docs/openapi.json").read_text())
responses = doc["paths"]["/api/greet/{name}"]["get"]["responses"]
assert "404" in responses
run(["go", "test", "-mod=mod", "./..."], service, env)
snapshot = lambda: {str(p.relative_to(workspace)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in workspace.rglob("*") if p.is_file()}
before = snapshot()
run([cli, "workspace", "check", "--file", manifest], workspace, env)
assert before == snapshot(), "workspace check changed project files"
provider.write_text(provider.read_text().replace('Message:"invalid"', 'Message:"new message"'))
before = snapshot()
try:
run([cli, "workspace", "check", "--file", manifest], workspace, env)
except RuntimeError as err:
assert "catalog drift" in str(err)
else:
raise AssertionError("stale catalog accepted")
assert before == snapshot(), "drift check changed project files"
run([cli, "workspace", "generate", "--file", manifest], workspace, env)
run([cli, "workspace", "check", "--file", manifest], workspace, env)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument("--report", type=Path, default=ROOT / "performance/managedProjectRemote.json")
args = parser.parse_args()
if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", args.version):
parser.error("a fixed release version is required")
env = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk")
started = time.monotonic()
checks = ["remoteCLIInstall", "freshModuleCache", "fixedVersionNoReplace"]
with tempfile.TemporaryDirectory(prefix="goOmniManagedRelease-") as directory:
temp = Path(directory)
(temp / "bin").mkdir()
env.update(GOBIN=str(temp / "bin"), GOMODCACHE=str(temp / "modcache"))
print(f"Installing remote CLI {args.version} in an empty module cache", flush=True)
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env)
cli = temp / "bin/go-omni"
# Do not pass --framework-version: verify that the installed CLI pins
# its own downloaded version, without a source-directory replacement.
starter = temp / "starter"
run([cli, "new", "--module", "example.com/releaseStarter", starter], temp, env)
run(["go", "mod", "tidy"], starter, env)
dependency = json.loads(run(["go", "list", "-m", "-json", MODULE], starter, env))
assert dependency["Version"] == args.version and dependency.get("Sum") and not dependency.get("Replace")
verify_contract_changes(cli, starter, env)
checks += ["contractChangePreview", "persistentLogicSignatureFailure", "businessSourcePreserved", "fixedConsumerBuild"]
serve(["go", "run", "."], starter, env, "nethttp", signal.SIGTERM, False, logs=temp)
for transport in ("nethttp", "native"):
for sig in (signal.SIGINT, signal.SIGTERM):
serve([str(starter / "releaseStarter")], starter, env, transport, sig, True, logs=temp)
checks += ["rootGoRun", "nethttpAndNativeSignals", "listenerReleased"]
# Only project-owned business implementations come from the example.
# Main, handlers, DTOs and startup are emitted by the downloaded CLI.
managed = temp / "managed"
run([cli, "new", "--api", ROOT / "examples/contracts/managed.api", "--module", "example.com/managed", managed], temp, env)
example = ROOT / "examples/managedService"
sources = list((example / "internal/logic/demo").glob("*.go"))
sources += [example / "internal/svc/options.go", example / "internal/apierrors/catalog.go"]
for source in sources:
target = managed / source.relative_to(example)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
run([cli, "api", "generate", "--check"], managed, env)
run(["go", "mod", "tidy"], managed, env)
run(["go", "build", "-o", temp / "managedServer", "."], managed, env)
managed_dependency = json.loads(run(["go", "list", "-m", "-json", MODULE], managed, env))
assert managed_dependency["Sum"] == dependency["Sum"] and not managed_dependency.get("Replace")
for transport in ("nethttp", "native"):
serve([str(temp / "managedServer")], managed, env, transport, signal.SIGTERM, True, contract=True, logs=temp)
checks += ["default404And405", "goOwnedErrorCatalog", "unwrappedDownloads", "rawCallback", "multipart400And413", "nethttpAndNativeTransfers"]
verify_shared_catalog(cli, temp, env, args.version)
checks += ["sharedGoCatalogExport", "documentOnlyErrorSelection", "workspaceCheckNoWrites", "staleCatalogRejected", "twoIndependentServiceBuilds"]
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject", "TestPrimitiveArrayConsumer"]
print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True)
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env)
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e]
assert sorted(passed) == sorted(tests), output
evidence = "".join(e.get("Output", "") for e in events)
sums = re.findall(r"checksum (h1:\S+); no local replace", evidence)
assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch"
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "primitiveAndDynamicArrays", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"]
sources = ["lifecycle/runtime.go", "lifecycle/phases.go", "config/lifecycle_test.go", "task/task.go", "cmd/go-omni/scaffold/templates/api-runtime.go.tmpl", "cmd/go-omni/apigen/templates.go", "config/project.go", "contract/bind.go", "contract/policy.go", "contract/multipart.go", "contract/file.go", "cmd/go-omni/apigen/business_test.go", "cmd/go-omni/apigen/generate.go", "lifecycle/controller.go", "config/endpoints.go", "health/health.go", "httpx/response.go", "app/app.go", "app/transport.go", "scripts/verifyManagedRelease.py", "scripts/verifyManagedProject.py", "cmd/go-omni/apigen/managed_test.go"]
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/managed").glob("*.go")]
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/business").glob("*.go")]
sources += ["cmd/go-omni/apigen/responseShape.go", "cmd/go-omni/apigen/documentRefs.go", "cmd/go-omni/apigen/catalogDocument.go", "cmd/go-omni/apigen/workspaceCatalog.go", "config/env.go", "cmd/go-omni/apigen/arrayResponse_test.go"]
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/arrays").glob("*.go")]
report = {"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
"freshModuleCache": True, "noLocalReplace": True,
"durationSeconds": round(time.monotonic() - started, 2), "checks": checks,
"sourceHashes": {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in sources}}
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
print(f"PASS remote managed project release {args.version}; temporary files and processes removed", flush=True)
if __name__ == "__main__":
main()