diff --git a/CHANGELOG.md b/CHANGELOG.md index 7868c124..613268d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Extension registry** — `StaticRegistryRepository` reads a static `index.json` published by [QueryaHub/extensions](https://github.com/QueryaHub/extensions) (GitHub Pages): yanked entries, non-GitHub-release URLs and extensions the running version is too old for are hidden, the index is cached, installs keep the SHA-256 / archive / sandbox checks. The Extensions → Marketplace tab now shows this catalog instead of the built-in demo one (which listed fake extensions and wrote placeholder folders into the real extensions directory, #1568); while the registry is empty or unreachable the tab says so, with a *Retry*. Also new for extension authors: the `ghcr.io/queryahub/features/querya-ext-tester` Dev Container Feature, `querya-testdb-*` test database images, and the [extension template](https://github.com/QueryaHub/extension-template). See [docs/extension-registry.md](docs/extension-registry.md). - **`manifestVersion` in `manifest.json`** — the version of the manifest format itself, apart from the extension's `version` and the host range `engines`. Absent means format 1, so nothing existing changes; `querya-ext-tester` rejects a format the emulated host does not read, and the app does not offer, install or load an extension in a format newer than it reads. See [docs/extension-registry.md](docs/extension-registry.md#versions-what-is-checked-against-what). +- **`querya-ext-tester --manifest-only`** — checks `manifest.json` (including `engines` and `manifestVersion`) against the emulated host and stops; the extension is not started, so no database or executable is needed. - **`engines.querya_desktop` accepts several comparators** — `>=0.4.0 <3.0.0` (`>=`, `>`, `<=`, `<`, `=`, all must hold), as the ClickHouse driver declares. The app used to read only `>=x.y.z` and `^x.y.z`, so such an extension was shown on every version. - **Release hooks** — after a Release the registry and the template are told about the new version (`repository_dispatch`); needs the `QUERYA_DISPATCH_TOKEN` secret. diff --git a/bin/querya_ext_tester.dart b/bin/querya_ext_tester.dart index 7fd0ddc3..a71a5f67 100644 --- a/bin/querya_ext_tester.dart +++ b/bin/querya_ext_tester.dart @@ -25,6 +25,9 @@ Options: --table also check db.getTableSchema for this table --database database passed to db.connect --sandbox run the plugin in the OS sandbox when available + --manifest-only check manifest.json against --target-version and + stop: the extension is not started (no database, + no executable needed) --strict treat warnings as failures (exit 1), for example a recommended method that errors or a sandbox that could not be applied @@ -51,6 +54,7 @@ Future _run(List args) async { String? database; var sandbox = false; var strict = false; + var manifestOnly = false; var timeoutMs = 5000; String? junit; String? tap; @@ -75,6 +79,8 @@ Future _run(List args) async { sandbox = true; case '--strict': strict = true; + case '--manifest-only': + manifestOnly = true; case '--no-color': color = false; case '--target-version' || @@ -167,6 +173,7 @@ Future _run(List args) async { database: database, ), sandbox: sandbox, + manifestOnly: manifestOnly, ); stdout.write(report.toConsole(color: color)); if (junit != null) File(junit).writeAsStringSync(report.toJUnitXml()); diff --git a/docs/extension-testing.md b/docs/extension-testing.md index 4ac3db1a..52ada48e 100644 --- a/docs/extension-testing.md +++ b/docs/extension-testing.md @@ -71,6 +71,7 @@ querya-ext-tester ./my-extension \ | `--table ` | also check `db.getTableSchema` | | `--database ` | database passed to `db.connect` | | `--sandbox` | run the plugin in the OS sandbox (`bwrap` on Linux, `sandbox-exec` on macOS) when available | +| `--manifest-only` | check `manifest.json` against `--target-version` and stop: the extension is not started, so no database and no executable are needed. For a quick lint, or a CI step with no database. Not in 0.4.19 or earlier | | `--strict` | warnings fail the run too (exit `1`): a recommended method that errors, or a sandbox that could not be applied. Not in 0.4.19 or earlier | | `--timeout-ms ` | per-request timeout (default 5000) | | `--junit ` / `--tap ` | write a JUnit XML / TAP report | diff --git a/lib/core/extensions/harness/extension_harness.dart b/lib/core/extensions/harness/extension_harness.dart index 66638e6c..7244abb8 100644 --- a/lib/core/extensions/harness/extension_harness.dart +++ b/lib/core/extensions/harness/extension_harness.dart @@ -187,11 +187,14 @@ class QueryaExtensionHarness { static const _connectionId = 1; + /// With [manifestOnly] the run ends after the `manifest` check: the + /// extension is not launched, so no database and no executable are needed. Future run({ required String extensionRoot, required String targetVersion, HarnessConnectionOptions connection = const HarnessConnectionOptions(), bool sandbox = false, + bool manifestOnly = false, }) async { final total = Stopwatch()..start(); final profile = ExtensionProtocolProfile.forTarget(targetVersion); @@ -286,6 +289,17 @@ class QueryaExtensionHarness { } } + // Only the manifest was asked for: nothing is started or connected. + if (manifestOnly) { + total.stop(); + return HarnessReport( + extensionId: extensionId, + targetVersion: profile.targetVersion, + checks: checks, + totalDuration: total.elapsed, + ); + } + // 2. Launch. HarnessPluginProcess? process; JsonRpcStdioClient? client; diff --git a/test/core/extensions/harness/extension_harness_test.dart b/test/core/extensions/harness/extension_harness_test.dart index 781e240a..c8b5fd95 100644 --- a/test/core/extensions/harness/extension_harness_test.dart +++ b/test/core/extensions/harness/extension_harness_test.dart @@ -295,6 +295,45 @@ void main() { expect(plugin!.calls, isNot(contains('db.getTableSchema'))); }); + group('manifest only', () { + Future manifestOnly(String target) { + var launched = false; + final harness = QueryaExtensionHarness( + launcher: (request) async { + launched = true; + throw StateError('must not start the plugin'); + }, + ); + return harness + .run( + extensionRoot: root.path, + targetVersion: target, + manifestOnly: true, + ) + .whenComplete(() => expect(launched, isFalse)); + } + + test('checks the manifest and starts nothing', () async { + writeExtension(manifest(), entry: false); // no executable at all + final report = await manifestOnly('0.5.0'); + expect(report.checks.map((c) => c.name), ['manifest']); + expect(report.isSuccess, isTrue, reason: report.toConsole()); + }); + + test('still fails on a manifest the emulated host cannot read', () async { + writeExtension(manifest()..['manifestVersion'] = 2, entry: false); + final report = await manifestOnly('0.5.0'); + expect(report.isSuccess, isFalse); + expect(check(report, 'manifest').message, contains('manifestVersion')); + }); + + test('a missing manifest.json is a failure', () async { + final report = await manifestOnly('0.5.0'); + expect(report.isSuccess, isFalse); + expect(check(report, 'manifest').status, HarnessCheckStatus.failed); + }); + }); + test('a missing recommended method is only a warning', () async { writeExtension(manifest()); final (report, _) = await run(_healthy()..remove('db.getCapabilities'));