Initial implementation of SwiftDBAI
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>
This commit is contained in:
254
Tests/SwiftDBAITests/BinarySizeTests.swift
Normal file
254
Tests/SwiftDBAITests/BinarySizeTests.swift
Normal file
@@ -0,0 +1,254 @@
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user