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// DTATests.swift3// Metrika4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQData1314/// Native .dta reader/writer tests: haven-written fixtures (formats 11715/// and 118) must load identically to the CSV, and a write/read roundtrip16/// must be lossless. All loads go through the public ZQDataStore path.17@Suite("Stata .dta reader/writer")18struct DTATests {19 let store: ZQDataStore20 let csvFrame: ZQDataFrame21 let fixturesDirectory: URL2223 init() async throws {24 let fixtures = try Fixtures()25 self.store = try ZQDataStore()26 self.csvFrame = try await store.load(contentsOf: fixtures.datasetURL)27 self.fixturesDirectory = fixtures.datasetURL.deletingLastPathComponent()28 }2930 private func expectMatchesCSV(_ frame: ZQDataFrame) throws {31 #expect(frame.rowCount == 60)32 #expect(frame.columnNames == [33 "revenue", "price", "region", "firm_id", "purchase", "orders",34 "z1", "z2", "firm_name",35 ])3637 // Numeric columns: bit-identical to the CSV load (both sides parse38 // the same decimal text into the nearest double).39 for name in csvFrame.columnNames {40 let expected = try csvFrame.requireNumeric(name)41 let actual = try frame.requireNumeric(name)42 #expect(actual.missing == expected.missing, "missing mask of \(name)")43 for i in 0..<60 where !expected.missing[i] {44 #expect(actual.values[i] == expected.values[i], "\(name)[\(i)]")45 }46 }4748 // String column: haven wrote firm_<id>, with row 5 blank (missing).49 guard case .string(let names)? = frame.column(named: "firm_name")?.data else {50 Issue.record("firm_name is not a string column")51 return52 }53 #expect(names[0] == "firm_1")54 #expect(names[4] == nil) // "" is Stata's string missing55 #expect(names[59] == "firm_12")56 }5758 @Test("reads haven-written format 118")59 func readsFormat118() async throws {60 let url = fixturesDirectory.appendingPathComponent("regression_v118.dta")61 try expectMatchesCSV(try await store.load(contentsOf: url))62 }6364 @Test("reads haven-written format 117")65 func readsFormat117() async throws {66 let url = fixturesDirectory.appendingPathComponent("regression_v117.dta")67 try expectMatchesCSV(try await store.load(contentsOf: url))68 }6970 @Test("write/read roundtrip is lossless")71 func roundtrip() async throws {72 let url = fixturesDirectory.appendingPathComponent("regression_v118.dta")73 let original = try await store.load(contentsOf: url)7475 let out = FileManager.default.temporaryDirectory76 .appendingPathComponent("metrika_roundtrip_\(UUID().uuidString).dta")77 defer { try? FileManager.default.removeItem(at: out) }78 try await store.save(original, to: out)79 let reloaded = try await store.load(contentsOf: out)8081 #expect(reloaded == original)82 }8384 @Test("rejects pre-117 files with a clear message")85 func rejectsOldFormats() async throws {86 // Format 115 files start with the raw version byte 0x73.87 let url = try temporaryFile(Data([0x73, 0x02, 0x01, 0x00]))88 defer { try? FileManager.default.removeItem(at: url) }89 do {90 _ = try await store.load(contentsOf: url)91 Issue.record("expected a format error")92 } catch {93 #expect("\(error)".contains("older than 117"))94 }95 }9697 @Test("rejects non-dta bytes")98 func rejectsGarbage() async throws {99 let url = try temporaryFile(Data("not a dta file at all".utf8))100 defer { try? FileManager.default.removeItem(at: url) }101 await #expect(throws: (any Error).self) {102 _ = try await store.load(contentsOf: url)103 }104 }105106 private func temporaryFile(_ data: Data) throws -> URL {107 let url = FileManager.default.temporaryDirectory108 .appendingPathComponent("metrika_dta_\(UUID().uuidString).dta")109 try data.write(to: url)110 return url111 }112}113