244 lines
20 KiB
Python
244 lines
20 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.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"]
|
|
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"]
|
|
|
|
# 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"} <= 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"]
|
|
|
|
for package, expected in {
|
|
"copyx": {"TestCheckTypesAndEmptyCollections", "TestCheckRulesAndConverterBoundaries", "TestCopierCheckConcurrentAndOptionsIsolation"},
|
|
"logx": {"TestLoggerTimeZoneIsolation", "TestLoggerTimeZoneValidationAndDST", "TestLoggerTimeZoneConcurrentChildren"},
|
|
"cors": {"TestPolicyPreflightAndActual", "TestPolicyValidation", "TestPolicyCredentialsWildcardAndDuplicates", "TestPolicyPreservesHTTPAndSnapshot"},
|
|
"app": {"TestAppCORSRoutingAndErrors", "TestAppCORSConfigIsolation", "TestCORSTransports", "TestI18nRouteScopesEarlyFailuresAndReplay", "TestI18nExplicitProtocolsAndConcurrentApps", "TestI18nAdapterFailurePreservesBusiness"},
|
|
"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
|
|
checks += ["remotePreciseModuleDiagnostics", "remoteJSONSuccessStatuses", "remoteRejectedStatusBodyCleanup",
|
|
"remoteCopyTypePreflight", "remoteEmptyCollectionTypeRejection", "remoteLoggerTimeZoneIsolation",
|
|
"remoteCORSPolicyRace", "remoteCORSErrorResponses", "remoteCORSTransports", "generatedCORSConfigOnly"]
|
|
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"]
|
|
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()
|