#!/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.22.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", "TestCopyxThreeEndpointConsumer", "TestGeneratedConfigurationConsumer", "TestGeneratedCORSConsumer", "TestI18nGeneratedConsumer", "TestRecursiveResponseConsumer"] 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 and "/" not in e["Test"]] 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"] checks += ["generatedIncompleteHTTPDrainRetainsDependenciesAndLogger"] checks += ["recursiveResponseTypedHTTP", "recursiveResponseReferenceGraph", "recursiveResponseRegenerationAndDrift"] # Run tests from the downloaded module, not the framework checkout. http_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/httpc"], starter, env) http_events = [json.loads(line) for line in http_output.splitlines() if line.startswith("{")] http_passed = {e.get("Test") for e in http_events if e.get("Action") == "pass"} assert {"TestProxyRouting", "TestJSONHeadersConcurrentSnapshot", "TestPoolLimitsAndIdleExpiry", "TestErrorBodyPoolReuseAndDrainBound", "TestSlowKnownErrorBodyHasShortDrainBudget", "TestJSONSuccessStatuses", "TestJSONSuccessStatusConcurrentOptions", "TestJSONRejectedSuccessBodyRecovery"} <= http_passed example_output = run(["go", "run", MODULE + "/examples/httpClient"], starter, env) assert "authenticated request succeeded" in example_output checks += ["remoteHTTPClientRaceTests", "remoteHTTPProxyAndTimeouts", "remoteHTTPHeaderIsolation", "remoteHTTPPoolAndDrainBudgets", "remoteHTTPExample"] config_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/config"], starter, env) config_events = [json.loads(line) for line in config_output.splitlines() if line.startswith("{")] config_passed = {e.get("Test") for e in config_events if e.get("Action") == "pass"} assert {"TestExplicitReferencesAcrossFormats", "TestReferenceFallbackAndFixedValues", "TestReferenceDiagnosticsAndScalarLimits", "TestRemovedEnvTagsAndFieldSuggestion", "TestModuleFieldDiagnostics", "TestModuleDiagnosticsKeepReferenceAndCause", "TestHTTPBodyAndCancellationSettings"} <= config_passed run([cli, "api", "generate", "--check"], starter, env) run(["go", "run", ".", "-config", "etc/config.yaml", "-check-config"], starter, env) checks += ["remoteExplicitConfigurationRaceTests", "literalAndRequiredDefaultReferences", "remoteCheckConfigBeforeDependencies", "removedImplicitEnvironmentRules"] # The generated starter imports no cloud storage SDK. Resolve these # optional adapter dependencies before testing their downloaded packages; # lazy module loading otherwise leaves their sums out of starter/go.sum. run(["go", "get", MODULE + "/storage/oss@" + args.version, MODULE + "/storage/s3@" + args.version, MODULE + "/telemetry@" + args.version], starter, env) for package, expected in { "storage": {"TestReadOptions", "TestReadSignatureAndExpiry", "TestReadRangeStreamingAndCache"}, "storage/local": {"TestSignedPayloadRanges"}, "storage/oss": {"TestReadOptionsNativeSignatures"}, "storage/s3": {"TestReadOptionsS3AndR2"}, "examples/signedRead": {"TestSignedReadHTTP"}, "telemetry": {"TestConfiguredHeaderPropagation", "TestConfiguredHeaderPropagationPrefersW3CAndRejectsInvalidSetup"}, "examples/telemetryHeaders": {"TestTelemetryHeadersExample"}, "copyx": {"TestCheckTypesAndEmptyCollections", "TestCheckRulesAndConverterBoundaries", "TestCopierCheckConcurrentAndOptionsIsolation"}, "logx": {"TestLoggerTimeZoneIsolation", "TestLoggerTimeZoneValidationAndDST", "TestLoggerTimeZoneConcurrentChildren"}, "cors": {"TestPolicyPreflightAndActual", "TestPolicyValidation", "TestPolicyCredentialsWildcardAndDuplicates", "TestPolicyPreservesHTTPAndSnapshot"}, "app": {"TestAppCORSRoutingAndErrors", "TestAppCORSConfigIsolation", "TestCORSTransports", "TestI18nRouteScopesEarlyFailuresAndReplay", "TestI18nExplicitProtocolsAndConcurrentApps", "TestI18nAdapterFailurePreservesBusiness", "TestUploadPolicyAcrossTransports", "TestConfiguredBodyPolicyBeforeMiddleware", "TestBodyHardLimitConflictBeforeServing", "TestDefaultBodyLimitRegistrationPaths", "TestShutdownWaitsForCancellationCleanup", "TestExternalServingEntriesRetainActiveResources", "TestRequestAdmissionAndCloseRace", "TestConfiguredShutdownDoesNotWaitForUncooperativeHandler"}, "lifecycle": {"TestFinishRetainsDependenciesAfterIncompleteHTTPDrain"}, "transport/http2": {"TestShutdownWaitsForHandlerAfterClientDisconnect"}, "transport/native": {"TestApplicationRejectsUnreadBodyPromptly", "TestBodyRejectionWithBusinessHTTP200DoesNotDrain"}, "contract": {"TestBodyPolicyPropagatesOuterLimit", "TestJSONClosingDelimiterPreservesReaderCause"}, "i18n": {"TestIndependentImmutableBundlesAndFallback", "TestResourceAndParameterDiagnostics", "TestStrictLoadingAndExplicitOverlay", "TestPinnedCLDRSamples", "TestBrowserConformance", "TestSourceLocaleCanBeFallbackOnly", "TestTypeScriptConsumer"}, "i18n/httpi18n": {"TestNegotiationAndExplicitResolvers"}, "i18n/errori18n": {"TestCatalogAdapterIsExplicitAndDetached"}, "database": {"TestInTxStagesAndCauses", "TestInTxCommitOutcomeUnknown", "TestInTxCancellationAndOwnership", "TestInTxCancellationRace", "TestInTxSuccessPanicAndConcurrency", "TestInTxPublicErrorCompatibility"}, }.items(): output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/" + package], starter, env) events = [json.loads(line) for line in output.splitlines() if line.startswith("{")] passed = {event.get("Test") for event in events if event.get("Action") == "pass"} assert expected <= passed signed_read_output = run(["go", "run", MODULE + "/examples/signedRead", "-check"], starter, env) assert "PDF preview, Chinese filename, download, HEAD and Range passed" in signed_read_output checks += ["remoteSignedReadOptionsAndLegacyLinks", "remoteReadSignatureTamperingAndExpiry", "remoteLocalPayloadRangesAndHEAD", "remoteOSSNativeV1V4Signatures", "remoteS3R2ReadOptions", "remoteSignedReadHTTPExample"] header_output = run(["go", "run", MODULE + "/examples/telemetryHeaders"], starter, env) assert "traceId=11111111111111111111111111111111" in header_output checks += ["remoteConfiguredHeaderPropagation", "remoteHeaderW3CPrecedenceAndValidation", "remoteHeaderTelemetryExample"] checks += ["remotePreciseModuleDiagnostics", "remoteJSONSuccessStatuses", "remoteRejectedStatusBodyCleanup", "remoteCopyTypePreflight", "remoteEmptyCollectionTypeRejection", "remoteLoggerTimeZoneIsolation", "remoteCORSPolicyRace", "remoteCORSErrorResponses", "remoteCORSTransports", "generatedCORSConfigOnly"] run(["go", "build", "-o", temp / "httpSafety", MODULE + "/examples/httpSafety"], starter, env) checks += ["remoteUnifiedBodyPolicyTransports", "remoteBodyLimitsBeforeMiddleware", "remoteHardLimitConflictBeforeServing", "remoteCanceledHandlerCleanupBarrier", "remoteExternalServingResourceRetention", "remoteAdmissionCloseRace", "remoteRuntimeFinishRetainsHTTPDependencies", "remoteHTTP2DisconnectedHandlerDrain", "remoteNativeEarly413AndBusinessHTTP200", "remoteHTTPSafetyExampleBuild"] transaction_output = run(["go", "run", MODULE + "/examples/transactions"], starter, env) assert "commitAcknowledgementLost: stage=commit outcome=unknown stored=true cleanup=false" in transaction_output assert "workFailure: stage=work outcome=rolledBack stored=false cleanup=true" in transaction_output checks += ["remoteTransactionStages", "remoteTransactionCauseChains", "remoteCommitOutcomeUnknown", "remoteCancellationOutcomeUnknown", "remoteTransactionPanicAndConcurrency", "remoteTransactionPublicErrors", "remoteTransactionExample"] downloaded = Path(dependency["Dir"]) example = downloaded / "examples/i18n" resources = ["--config", example / "config.json", "--resource", example / "locales/ui.en.json", "--resource", example / "locales/ui.zh-Hans.json"] assert json.loads(run([cli, "i18n", "check", *resources], starter, env)) == [] pack = json.loads(run([cli, "i18n", "export", *resources, "--locale", "zh-Hans", "--namespace", "ui"], starter, env)) assert pack["locale"] == "zh-Hans" and pack["ruleVersion"] == "cldr-46.1" assert "UNICODE LICENSE" in pack["license"] assert {doc["namespace"] for doc in pack["documents"]} == {"ui"} types = run([cli, "i18n", "types", *resources, "--namespace", "ui"], starter, env) assert "export type Translate" in types and "bigint" in types extracted = json.loads(run([cli, "i18n", "extract", "--go", example / "main.go"], starter, env)) assert any(ref["namespace"] == "ui" and ref["key"] == "greeting" for ref in extracted["references"]) catalog_path = temp / "catalog.json" catalog_path.write_text(run(["go", "run", MODULE + "/examples/i18n", "-catalog"], starter, env)) source = json.loads(run([cli, "i18n", "extract", "--catalog", catalog_path, "--locale", "en", "--namespace", "errors"], starter, env)) assert source["messages"]["LIMIT_EXCEEDED"]["params"] == {"max": "integer"} browser_output = run(["node", "--test", downloaded / "i18n/browser/runtime.test.mjs"], starter, env) assert "# fail 0" in browser_output checks += ["remoteI18nRuntimeRace", "remoteI18nGeneratedConsumer", "remoteI18nHTTPBusinessInvariants", "remoteI18nSourceOnlyFallback", "remoteCLDRSamplesAndBrowserConformance", "remoteTypeScriptConsumer", "remoteI18nCLIExtractionCheckExportTypes", "remoteI18nNamespaceIsolation", "remoteBrowserRaceAndFailureRetention", "remoteJSONReaderCausePreservation"] checks += ["flatFieldComposition", "copyxThreeEndpointConsumer", "explicitNullAndMoneyPolicies", "fieldAdditionWithoutCopyRuleChanges", "middlewareFactoryInjectionAndCleanup", "optionalRepositoryPreservesBusiness"] checks += ["generatedUnifiedConfig", "jsonYamlXmlAndCustomDecoder", "configValidationBeforeDependencies", "configEnvProvenance", "configExtensionPreserved", "configLoaderDriftCheck"] 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/references.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")] sources += ["cmd/go-omni/apigen/" + name for name in ("composition.go", "generatorConfig.go", "copyConsumer_test.go", "generatorConfig_test.go", "middlewareFactory.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/composition").iterdir() if p.is_file()] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "copyx").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "config").glob("*.go")] sources += ["cmd/go-omni/apigen/configLoader_test.go", "cmd/go-omni/apigen/testdata/configuration/consumer_test.go", "cmd/go-omni/scaffold/templates/api-load.go.tmpl", "cmd/go-omni/scaffold/templates/api-loader.go.tmpl"] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "httpc").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/httpClient").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "logx").glob("*.go")] sources += ["internal/configdiag/error.go", "database/mysql/mysql.go", "database/connector.go", "database/database.go", "contract/policy.go"] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/developerTools").iterdir() if p.is_file()] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cors").glob("*.go")] sources += ["app/cors_test.go", "cmd/go-omni/apigen/corsConsumer_test.go", "cmd/go-omni/apigen/testdata/cors/consumer_test.go", "examples/cors/main.go", "examples/cors/config.yaml", "scripts/corsBrowser/main.go"] sources += ["database/transaction.go", "database/transaction_test.go", "database/transactionResponse_test.go", "database/mysqlConnector_test.go", "examples/transactions/main.go", "examples/transactions/demoDriver.go"] sources += [str(p.relative_to(ROOT)) for directory in ("i18n", "examples/i18n") for p in (ROOT / directory).rglob("*") if p.is_file()] sources += ["app/i18n_test.go", "errors/localization.go", "cmd/go-omni/i18n.go", "cmd/go-omni/apigen/i18nVersion.go", "cmd/go-omni/apigen/testdata/i18n/consumer_test.go", "contract/policy_test.go"] sources += ["app/body.go", "app/router.go", "app/requests.go", "app/bodyPolicy_test.go", "app/requests_test.go", "app/transport_test.go", "httpx/context.go", "internal/httplimits/limits.go", "lifecycle/runtime_test.go", "transport/http2/server.go", "transport/http2/lifecycle_test.go", "transport/native/server.go", "transport/native/earlyReject_test.go", "examples/httpSafety/main.go"] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "storage").rglob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/signedRead").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "telemetry").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/telemetryHeaders").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()