SPB Git

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%
5.2 KB · 143 lines swift
Raw Blame History
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