spb/metrika Public
Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.
Swift 92.4%
HTML 3.3%
R 3%
Shell 1.3%
1//2// PluginTests.swift3// Metrika4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQData13import ZQEngine14import ZQParser15import ZQPlugins1617/// User-command tests: `.zyq` script commands with `args` substitution,18/// and native ZQCommandPlugin registration, validation, and mutation19/// gating.20@Suite("Plugin protocol", .serialized)21struct PluginTests {22 let fixtures: Fixtures23 let commandsDirectory: URL2425 init() throws {26 self.fixtures = try Fixtures()27 self.commandsDirectory = FileManager.default.temporaryDirectory28 .appendingPathComponent("metrika_commands_\(UUID().uuidString)")29 try FileManager.default.createDirectory(30 at: commandsDirectory, withIntermediateDirectories: true31 )32 let script = """33 // robust regression of a logged response — ado-file analog34 args response predictor35 gen __log_response = ln(`response')36 reg __log_response `predictor', robust37 drop __log_response38 """39 try Data(script.utf8).write(40 to: commandsDirectory.appendingPathComponent("logreg.zyq")41 )42 }4344 private func makeSession(plugins: [any ZQCommandPlugin] = []) async throws -> ZQSession {45 let session = try ZQSession(46 discoverUserCommands: true,47 commandsDirectory: commandsDirectory,48 plugins: plugins49 )50 _ = try await session.execute("use \(fixtures.datasetURL.path)")51 return session52 }5354 @Test("script command with args substitution matches the built-in path")55 func scriptCommand() async throws {56 let session = try await makeSession()57 let result = try await session.execute("logreg revenue price")58 // Same estimate as running the commands directly.59 expectClose(60 try #require(result.scalars["b_price"]),61 fixtures["ols_b_price"], "b[price] via script command"62 )63 // The scratch variable was dropped by the script.64 let count = try await session.execute("count")65 #expect(count.scalars["N"] == 60)66 await #expect(throws: (any Error).self) {67 _ = try await session.execute("summarize __log_response")68 }69 }7071 @Test("args parsing strips the declaration and names parameters")72 func argsDeclaration() throws {73 let script = ZQScriptCommand(74 verb: "demo",75 fileURL: URL(fileURLWithPath: "/tmp/demo.zyq"),76 lines: ["* comment first", "args a b", "reg `a' `b'", "count if `a' > `2'"]77 )78 #expect(script.parameterNames == ["a", "b"])79 #expect(script.lines == ["* comment first", "reg `a' `b'", "count if `a' > `2'"])80 #expect(script.expandedLines(arguments: "y x") == [81 "* comment first", "reg y x", "count if y > x",82 ])83 }8485 @Test("native plugin executes and its mutation is installed")86 func nativePlugin() async throws {87 let session = try await makeSession(plugins: [ZScorePlugin()])88 let result = try await session.execute("zscore revenue")89 #expect(result.text.contains("z_revenue"))9091 // The generated column exists and is standardized.92 let summary = try await session.execute("summarize z_revenue")93 #expect(abs(try #require(summary.scalars["mean"])) < 1e-12)94 expectClose(try #require(summary.scalars["sd"]), 1, "sd = 1")95 }9697 @Test("plugin syntax validation rejects undeclared qualifiers")98 func syntaxValidation() async throws {99 let session = try await makeSession(plugins: [ZScorePlugin()])100 // ZScorePlugin declares acceptsCondition: false.101 await #expect(throws: ZQEngineError.self) {102 _ = try await session.execute("zscore revenue if price > 10")103 }104 }105106 @Test("a plugin shadowing a built-in verb is rejected at startup")107 func shadowingRejected() {108 struct Impostor: ZQCommandPlugin {109 static let verb = "regress"110 static let syntax = ZQSyntaxSpec()111 func execute(_ ctx: ZQContext) async throws -> ZQResult {112 ZQResult(text: "hijacked")113 }114 }115 #expect(throws: ZQPluginRegistry.ShadowingError.self) {116 _ = try ZQSession(117 discoverUserCommands: false, plugins: [Impostor()]118 )119 }120 }121122 @Test("non-mutating plugin cannot smuggle a replacement frame")123 func mutationGating() async throws {124 struct Sneaky: ZQCommandPlugin {125 static let verb = "sneaky"126 static let syntax = ZQSyntaxSpec(mutates: false)127 func execute(_ ctx: ZQContext) async throws -> ZQResult {128 ZQResult(129 text: "",130 replacementFrame: try ZQDataFrame(columns: [])131 )132 }133 }134 let session = try await makeSession(plugins: [Sneaky()])135 await #expect(throws: ZQEngineError.self) {136 _ = try await session.execute("sneaky")137 }138 // Working dataset untouched.139 let count = try await session.execute("count")140 #expect(count.scalars["N"] == 60)141 }142}143