Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions bin/querya_ext_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Options:
--table <name> also check db.getTableSchema for this table
--database <name> 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
Expand All @@ -51,6 +54,7 @@ Future<int> _run(List<String> args) async {
String? database;
var sandbox = false;
var strict = false;
var manifestOnly = false;
var timeoutMs = 5000;
String? junit;
String? tap;
Expand All @@ -75,6 +79,8 @@ Future<int> _run(List<String> args) async {
sandbox = true;
case '--strict':
strict = true;
case '--manifest-only':
manifestOnly = true;
case '--no-color':
color = false;
case '--target-version' ||
Expand Down Expand Up @@ -167,6 +173,7 @@ Future<int> _run(List<String> args) async {
database: database,
),
sandbox: sandbox,
manifestOnly: manifestOnly,
);
stdout.write(report.toConsole(color: color));
if (junit != null) File(junit).writeAsStringSync(report.toJUnitXml());
Expand Down
1 change: 1 addition & 0 deletions docs/extension-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ querya-ext-tester ./my-extension \
| `--table <name>` | also check `db.getTableSchema` |
| `--database <name>` | 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 <n>` | per-request timeout (default 5000) |
| `--junit <path>` / `--tap <path>` | write a JUnit XML / TAP report |
Expand Down
14 changes: 14 additions & 0 deletions lib/core/extensions/harness/extension_harness.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<HarnessReport> 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);
Expand Down Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions test/core/extensions/harness/extension_harness_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,45 @@ void main() {
expect(plugin!.calls, isNot(contains('db.getTableSchema')));
});

group('manifest only', () {
Future<HarnessReport> 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'));
Expand Down
Loading