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%
9.9 KB · 245 lines swift
Raw Blame History
1//2//  EngineTests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Testing11import ZQEngine1213/// End-to-end tests: full command lines through the session, checking the14/// returned scalar results against the R fixtures.15@Suite("ZQEngine end-to-end", .serialized)16struct EngineTests {17    let fixtures: Fixtures18    let session: ZQSession1920    init() async throws {21        self.fixtures = try Fixtures()22        self.session = try ZQSession(discoverUserCommands: false)23        _ = try await session.execute("use \(fixtures.datasetURL.path)")24        _ = try await session.execute("gen log_rev = ln(revenue)")25    }2627    @Test("use reports dataset dimensions")28    func loadedDimensions() async throws {29        let result = try await session.execute("count")30        #expect(result.scalars["N"] == 60)31    }3233    @Test("sysuse loads bundled samples and lists them")34    func sysuse() async throws {35        let fresh = try ZQSession(discoverUserCommands: false)36        let listing = try await fresh.execute("sysuse")37        #expect(listing.text.contains("sales"))38        let sales = try await fresh.execute("sysuse sales")39        #expect(sales.scalars["N"] == 1000)40        let reg = try await fresh.execute("reg revenue price")41        #expect(reg.scalars["N"] == 1000)42        let cars = try await fresh.execute("sysuse mtcars")43        #expect(cars.scalars["N"] == 32)44        await #expect(throws: ZQEngineError.self) {45            _ = try await fresh.execute("sysuse nosuchsample")46        }47    }4849    @Test("regress with listwise deletion matches R")50    func regress() async throws {51        let result = try await session.execute("reg log_rev price")52        #expect(result.scalars["N"] == fixtures["ols_n"])53        expectClose(try #require(result.scalars["b_price"]), fixtures["ols_b_price"], "b[price]")54        expectClose(try #require(result.scalars["b__cons"]), fixtures["ols_b_cons"], "b[_cons]")55        expectClose(56            try #require(result.scalars["se_price"]),57            fixtures["ols_se_classical_price"], "se[price]"58        )59        expectClose(try #require(result.scalars["r2"]), fixtures["ols_r2"], "R²")60        #expect(result.text.contains("observations dropped due to missing values"))61    }6263    @Test("regress, robust matches R HC1")64    func regressRobust() async throws {65        let result = try await session.execute("reg log_rev price, robust")66        expectClose(67            try #require(result.scalars["se_price"]),68            fixtures["ols_se_hc1_price"], "robust se[price]"69        )70    }7172    @Test("regress, cluster matches R with Stata small-sample factor")73    func regressCluster() async throws {74        let result = try await session.execute("reg log_rev price, cluster(firm_id)")75        #expect(result.scalars["N_clust"] == fixtures["ols_G"])76        expectClose(77            try #require(result.scalars["se_price"]),78            fixtures["ols_se_cluster_price"], "cluster se[price]"79        )80    }8182    @Test("factor variable expansion i.region matches R factor()")83    func regressFactor() async throws {84        let result = try await session.execute("reg log_rev price i.region")85        expectClose(try #require(result.scalars["b_price"]), fixtures["ols2_b_price"], "b[price]")86        expectClose(87            try #require(result.scalars["b_2.region"]),88            fixtures["ols2_b_region2"], "b[2.region]"89        )90        expectClose(91            try #require(result.scalars["b_3.region"]),92            fixtures["ols2_b_region3"], "b[3.region]"93        )94        expectClose(try #require(result.scalars["b__cons"]), fixtures["ols2_b_cons"], "b[_cons]")95        expectClose(try #require(result.scalars["r2"]), fixtures["ols2_r2"], "R²")96    }9798    @Test("summarize scalars match R")99    func summarize() async throws {100        let result = try await session.execute("summarize revenue")101        expectClose(102            try #require(result.scalars["mean"]),103            fixtures["sum_revenue_mean"], "mean"104        )105        expectClose(try #require(result.scalars["sd"]), fixtures["sum_revenue_sd"], "sd")106    }107108    @Test("if qualifier and count")109    func conditionalCount() async throws {110        let all = try await session.execute("count")111        let some = try await session.execute("count if missing(price)")112        #expect(all.scalars["N"] == 60)113        #expect(some.scalars["N"] == fixtures["ols_dropped"])114    }115116    @Test("bootstrap is reproducible for a fixed seed")117    func bootstrapReproducible() async throws {118        let first = try await session.execute(119            "bootstrap, reps(200) seed(42): reg log_rev price"120        )121        let second = try await session.execute(122            "bootstrap, reps(200) seed(42): reg log_rev price"123        )124        #expect(first.scalars["se_price"] == second.scalars["se_price"])125        // Bootstrap SE should be in the neighborhood of the analytic one.126        let analytic = fixtures["ols_se_classical_price"]127        let bootstrap = try #require(first.scalars["se_price"])128        #expect(bootstrap > analytic / 3 && bootstrap < analytic * 3)129    }130131    @Test("generate honors if, replace counts changes")132    func generateReplace() async throws {133        _ = try await session.execute("gen flag = 1 if price > 10")134        let count = try await session.execute("count if flag == 1")135        let expected = try await session.execute("count if price > 10")136        #expect(count.scalars["N"] == expected.scalars["N"])137        let replaced = try await session.execute("replace flag = 0 if missing(flag)")138        #expect(replaced.text.contains("real changes"))139        _ = try await session.execute("drop flag")140    }141142    @Test("graph scatter produces a plot spec")143    func graphScatter() async throws {144        _ = try await session.execute("graph scatter log_rev price, by(region)")145        let plot = try #require(await session.lastPlot)146        #expect(plot.kind == .scatter)147        #expect(plot.series.count == 3)  // three regions148        #expect(plot.xLabel == "price")149    }150151    @Test("logit through the console matches R")152    func logitCommand() async throws {153        let result = try await session.execute("logit purchase price")154        expectClose(155            try #require(result.scalars["b_price"]), fixtures["logit_b_price"], "b[price]"156        )157        expectClose(try #require(result.scalars["ll"]), fixtures["logit_ll"], "ll")158        #expect(result.text.contains("Logistic regression"))159        #expect(result.text.contains("Pseudo R2"))160    }161162    @Test("poisson with robust SE through the console")163    func poissonCommand() async throws {164        let result = try await session.execute("poisson orders price, robust")165        expectClose(166            try #require(result.scalars["b_price"]), fixtures["pois_b_price"], "b[price]"167        )168        expectClose(169            try #require(result.scalars["se_price"]),170            fixtures["pois_se_hc0_price"], "robust se[price]"171        )172    }173174    @Test("tabulate one-way counts region levels")175    func tabulateOneWay() async throws {176        let result = try await session.execute("tab region")177        #expect(result.scalars["N"] == 60)178        #expect(result.scalars["r"] == 3)179        #expect(result.text.contains("Total"))180        // region cycles 1,2,3 over 60 rows → 20 each.181        #expect(result.text.contains("20"))182    }183184    @Test("tabulate two-way region by purchase")185    func tabulateTwoWay() async throws {186        let result = try await session.execute("tabulate region purchase")187        #expect(result.scalars["r"] == 3)188        #expect(result.scalars["c"] == 2)189        #expect(result.scalars["N"] == 60)190    }191192    @Test("correlate matches R and reports listwise obs")193    func correlateCommand() async throws {194        let result = try await session.execute("correlate revenue price")195        expectClose(196            try #require(result.scalars["rho"]),197            fixtures["corr_rev_price"], "cor(revenue, price)"198        )199        #expect(result.scalars["N"] == fixtures["ols_n"])200    }201202    @Test("help lists every category; help <verb> shows the entry")203    func help() async throws {204        let index = try await session.execute("help")205        for category in ZQCommandReference.categories {206            #expect(index.text.contains(category), Comment(rawValue: category))207        }208        let entry = try await session.execute("help regress")209        #expect(entry.text.contains("cluster(var)"))210        #expect(entry.text.contains("reg log_rev price, robust"))211        let byAbbreviation = try await session.execute("help reg")212        #expect(byAbbreviation.text.contains("Ordinary least squares"))213        let unknown = try await session.execute("help nosuchthing")214        #expect(unknown.text.contains("no entry"))215    }216217    @Test("every dispatched estimation and data verb has a manual entry")218    func manualCoverage() {219        let documented = Set(ZQCommandReference.all.map(\.verb))220        for verb in [221            "use", "sysuse", "save", "clear", "describe", "list", "count", "generate",222            "replace", "drop", "keep", "summarize", "tabulate", "correlate",223            "regress", "logit", "probit", "poisson", "ivregress", "xtreg",224            "xtset", "lasso", "elasticnet", "boost", "bootstrap", "permute", "predict",225            "margins", "bayes", "scatter", "histogram", "kdensity", "graph", "display",226            "set", "log", "help", "zscore",227        ] {228            #expect(documented.contains(verb), Comment(rawValue: "missing manual entry: \(verb)"))229        }230    }231232    @Test("display evaluates scalar expressions")233    func display() async throws {234        let result = try await session.execute("display 2 + 2 * 3")235        #expect(result.text == "8")236    }237238    @Test("unknown variable yields a helpful error")239    func unknownVariable() async throws {240        await #expect(throws: (any Error).self) {241            _ = try await session.execute("summarize nonexistent_var")242        }243    }244}245