Chat with any SQLite database using natural language. Built on AnyLanguageModel (HuggingFace) for LLM-agnostic provider support and GRDB for SQLite access. Core features: - Auto schema introspection from sqlite_master (zero config) - NL → SQL generation via any AnyLanguageModel provider - Three rendering modes: text summary, data table, Swift Charts - Drop-in DataChatView (SwiftUI) and headless ChatEngine - Operation allowlist with read-only default - Mutation policy with per-table control - ToolExecutionDelegate for destructive operation confirmation - Multi-turn conversation context - 352 tests across 24 suites, all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
10 KiB
Swift
255 lines
10 KiB
Swift
// BinarySizeTests.swift
|
||
// SwiftDBAI
|
||
//
|
||
// Validates that the SwiftDBAI package stays within its 2 MB binary size budget.
|
||
// This test suite uses source-level heuristics since we can't measure the actual
|
||
// compiled binary size in a unit test. The constraints ensure the package remains
|
||
// lightweight by checking:
|
||
// 1. Total source code size (proxy for compiled size)
|
||
// 2. No embedded binary assets or large resources
|
||
// 3. No unnecessary heavy dependencies
|
||
// 4. File count stays reasonable (no code bloat)
|
||
|
||
import Foundation
|
||
import Testing
|
||
|
||
@Suite("Binary Size Budget")
|
||
struct BinarySizeTests {
|
||
|
||
/// The maximum allowed total source code size in bytes.
|
||
/// At typical Swift optimized compilation ratios (2-4x), 500 KB of source
|
||
/// compiles to roughly 1-2 MB of binary. We set the source budget at 500 KB
|
||
/// to keep the compiled output well under 2 MB.
|
||
private static let maxSourceSizeBytes: Int = 500_000 // 500 KB
|
||
|
||
/// Maximum number of Swift source files allowed.
|
||
/// More files generally means more code and larger binaries.
|
||
private static let maxSourceFileCount: Int = 60
|
||
|
||
/// Maximum size for any single source file in bytes.
|
||
/// Large individual files often indicate code that should be split or
|
||
/// contains embedded data that bloats the binary.
|
||
private static let maxSingleFileSizeBytes: Int = 50_000 // 50 KB
|
||
|
||
/// Disallowed file extensions in the Sources directory that would bloat the binary.
|
||
private static let disallowedExtensions: Set<String> = [
|
||
"png", "jpg", "jpeg", "gif", "bmp", "tiff",
|
||
"mp3", "mp4", "wav", "mov",
|
||
"mlmodel", "mlmodelc", "mlpackage",
|
||
"sqlite", "db",
|
||
"zip", "tar", "gz",
|
||
"bin", "dat",
|
||
"framework", "dylib", "a"
|
||
]
|
||
|
||
// MARK: - Helper
|
||
|
||
/// Recursively finds all files in the Sources/SwiftDBAI directory.
|
||
private func findSourceFiles() throws -> [URL] {
|
||
let sourcesDir = findSourcesDirectory()
|
||
guard let sourcesDir else {
|
||
Issue.record("Could not locate Sources/SwiftDBAI directory")
|
||
return []
|
||
}
|
||
|
||
let fileManager = FileManager.default
|
||
guard let enumerator = fileManager.enumerator(
|
||
at: sourcesDir,
|
||
includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey],
|
||
options: [.skipsHiddenFiles]
|
||
) else {
|
||
Issue.record("Could not enumerate Sources/SwiftDBAI directory")
|
||
return []
|
||
}
|
||
|
||
var files: [URL] = []
|
||
for case let fileURL as URL in enumerator {
|
||
let resourceValues = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
|
||
if resourceValues.isRegularFile == true {
|
||
files.append(fileURL)
|
||
}
|
||
}
|
||
return files
|
||
}
|
||
|
||
/// Locates the Sources/SwiftDBAI directory by walking up from the test bundle.
|
||
private func findSourcesDirectory() -> URL? {
|
||
// Try common locations relative to the build directory
|
||
let fileManager = FileManager.default
|
||
|
||
// In SPM test runs, we can find the package root by checking known paths
|
||
var candidateURL = URL(fileURLWithPath: #filePath)
|
||
// Walk up from Tests/SwiftDBAITests/BinarySizeTests.swift to package root
|
||
for _ in 0..<3 {
|
||
candidateURL = candidateURL.deletingLastPathComponent()
|
||
}
|
||
let sourcesDir = candidateURL.appendingPathComponent("Sources/SwiftDBAI")
|
||
if fileManager.fileExists(atPath: sourcesDir.path) {
|
||
return sourcesDir
|
||
}
|
||
|
||
// Fallback: check current working directory
|
||
let cwdSources = URL(fileURLWithPath: fileManager.currentDirectoryPath)
|
||
.appendingPathComponent("Sources/SwiftDBAI")
|
||
if fileManager.fileExists(atPath: cwdSources.path) {
|
||
return cwdSources
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// MARK: - Tests
|
||
|
||
@Test("Total source code size stays under 500 KB budget")
|
||
func totalSourceCodeSizeUnderBudget() throws {
|
||
let files = try findSourceFiles()
|
||
let swiftFiles = files.filter { $0.pathExtension == "swift" }
|
||
|
||
var totalSize: Int = 0
|
||
for file in swiftFiles {
|
||
let attributes = try FileManager.default.attributesOfItem(atPath: file.path)
|
||
let fileSize = attributes[.size] as? Int ?? 0
|
||
totalSize += fileSize
|
||
}
|
||
|
||
#expect(totalSize < Self.maxSourceSizeBytes,
|
||
"""
|
||
Total Swift source size (\(totalSize) bytes) exceeds \(Self.maxSourceSizeBytes) byte budget.
|
||
At typical 2-4x compilation ratio, this would produce a binary larger than 2 MB.
|
||
Consider removing unused code or splitting into optional sub-targets.
|
||
""")
|
||
|
||
// Log the actual size for visibility
|
||
let sizeKB = Double(totalSize) / 1024.0
|
||
let budgetKB = Double(Self.maxSourceSizeBytes) / 1024.0
|
||
print("📦 SwiftDBAI source size: \(String(format: "%.1f", sizeKB)) KB / \(String(format: "%.0f", budgetKB)) KB budget (\(String(format: "%.0f", (sizeKB / budgetKB) * 100))% used)")
|
||
}
|
||
|
||
@Test("Source file count stays reasonable")
|
||
func sourceFileCountUnderLimit() throws {
|
||
let files = try findSourceFiles()
|
||
let swiftFiles = files.filter { $0.pathExtension == "swift" }
|
||
|
||
#expect(swiftFiles.count <= Self.maxSourceFileCount,
|
||
"""
|
||
Swift source file count (\(swiftFiles.count)) exceeds limit of \(Self.maxSourceFileCount).
|
||
More files generally means more code and larger binaries.
|
||
""")
|
||
|
||
print("📦 SwiftDBAI file count: \(swiftFiles.count) / \(Self.maxSourceFileCount) max")
|
||
}
|
||
|
||
@Test("No individual source file exceeds 50 KB")
|
||
func noOversizedSourceFiles() throws {
|
||
let files = try findSourceFiles()
|
||
let swiftFiles = files.filter { $0.pathExtension == "swift" }
|
||
|
||
for file in swiftFiles {
|
||
let attributes = try FileManager.default.attributesOfItem(atPath: file.path)
|
||
let fileSize = attributes[.size] as? Int ?? 0
|
||
|
||
#expect(fileSize < Self.maxSingleFileSizeBytes,
|
||
"""
|
||
File \(file.lastPathComponent) is \(fileSize) bytes, exceeding the \(Self.maxSingleFileSizeBytes) byte limit.
|
||
Large files may contain embedded data or code that should be split.
|
||
""")
|
||
}
|
||
}
|
||
|
||
@Test("No binary assets or heavy resources in Sources directory")
|
||
func noBinaryAssetsInSources() throws {
|
||
let files = try findSourceFiles()
|
||
|
||
let disallowedFiles = files.filter { file in
|
||
Self.disallowedExtensions.contains(file.pathExtension.lowercased())
|
||
}
|
||
|
||
#expect(disallowedFiles.isEmpty,
|
||
"""
|
||
Found \(disallowedFiles.count) disallowed file(s) in Sources directory:
|
||
\(disallowedFiles.map(\.lastPathComponent).joined(separator: "\n"))
|
||
These file types bloat the binary. Remove them or move to a separate resource bundle.
|
||
""")
|
||
}
|
||
|
||
@Test("Package has no resource bundles that could bloat binary")
|
||
func noResourceBundles() throws {
|
||
let files = try findSourceFiles()
|
||
|
||
let resourceFiles = files.filter { file in
|
||
let ext = file.pathExtension.lowercased()
|
||
return ["xcassets", "storyboard", "xib", "nib", "xcdatamodeld"].contains(ext)
|
||
}
|
||
|
||
#expect(resourceFiles.isEmpty,
|
||
"""
|
||
Found resource bundle files that could bloat the binary:
|
||
\(resourceFiles.map(\.lastPathComponent).joined(separator: "\n"))
|
||
SwiftDBAI should be pure code — no bundled resources.
|
||
""")
|
||
}
|
||
|
||
@Test("Only expected dependencies declared (GRDB + AnyLanguageModel)")
|
||
func minimalDependencies() throws {
|
||
// Read Package.swift to verify we only have the expected dependencies
|
||
var packageURL = URL(fileURLWithPath: #filePath)
|
||
for _ in 0..<3 {
|
||
packageURL = packageURL.deletingLastPathComponent()
|
||
}
|
||
let packageSwiftURL = packageURL.appendingPathComponent("Package.swift")
|
||
|
||
guard FileManager.default.fileExists(atPath: packageSwiftURL.path) else {
|
||
// Skip if we can't find Package.swift (CI environments etc.)
|
||
return
|
||
}
|
||
|
||
let packageContents = try String(contentsOf: packageSwiftURL, encoding: .utf8)
|
||
|
||
// Count .package() declarations (dependencies)
|
||
let packageDeclarations = packageContents.components(separatedBy: ".package(")
|
||
.count - 1 // subtract 1 because the first segment is before any .package(
|
||
|
||
#expect(packageDeclarations <= 2,
|
||
"""
|
||
Found \(packageDeclarations) package dependencies, expected at most 2 (GRDB + AnyLanguageModel).
|
||
Additional dependencies increase binary size. Evaluate if they're truly needed.
|
||
""")
|
||
|
||
// Verify the expected dependencies are present
|
||
#expect(packageContents.contains("GRDB"), "Expected GRDB dependency")
|
||
#expect(packageContents.contains("AnyLanguageModel"), "Expected AnyLanguageModel dependency")
|
||
|
||
print("📦 SwiftDBAI dependencies: \(packageDeclarations) (GRDB + AnyLanguageModel)")
|
||
}
|
||
|
||
@Test("Estimated binary size under 2 MB")
|
||
func estimatedBinarySizeUnderLimit() throws {
|
||
let files = try findSourceFiles()
|
||
let swiftFiles = files.filter { $0.pathExtension == "swift" }
|
||
|
||
var totalSize: Int = 0
|
||
for file in swiftFiles {
|
||
let attributes = try FileManager.default.attributesOfItem(atPath: file.path)
|
||
let fileSize = attributes[.size] as? Int ?? 0
|
||
totalSize += fileSize
|
||
}
|
||
|
||
// Conservative estimate: optimized Swift binary is typically 2-4x source size.
|
||
// Use 4x as worst case multiplier for safety margin.
|
||
let worstCaseMultiplier = 4.0
|
||
let estimatedBinarySize = Double(totalSize) * worstCaseMultiplier
|
||
let maxBinarySize: Double = 2.0 * 1024.0 * 1024.0 // 2 MB
|
||
|
||
#expect(estimatedBinarySize < maxBinarySize,
|
||
"""
|
||
Estimated binary size (\(String(format: "%.1f", estimatedBinarySize / 1024.0)) KB) exceeds 2 MB limit.
|
||
Source: \(totalSize) bytes × \(worstCaseMultiplier)x multiplier = \(String(format: "%.1f", estimatedBinarySize / 1024.0)) KB
|
||
Note: This is the SwiftDBAI module only — excludes GRDB and AnyLanguageModel
|
||
which are existing dependencies the developer already includes.
|
||
""")
|
||
|
||
let estimatedMB = estimatedBinarySize / (1024.0 * 1024.0)
|
||
print("📦 Estimated SwiftDBAI binary size: \(String(format: "%.2f", estimatedMB)) MB / 2.00 MB limit (worst case \(worstCaseMultiplier)x)")
|
||
}
|
||
}
|