Download Tests/PromptGarageCoreTests/RealDatabaseMigrationTests.swift from smlflg/PromtGarageMac: direct link, hf CLI and curl.
- Browser
- Download file 9.78 kB
-
https://huggingface.co/smlflg/PromtGarageMac/resolve/main/Tests/PromptGarageCoreTests/RealDatabaseMigrationTests.swift
- Command line
-
hf download hf://smlflg/PromtGarageMac/Tests/PromptGarageCoreTests/RealDatabaseMigrationTests.swift
-
curl -L -o RealDatabaseMigrationTests.swift https://huggingface.co/smlflg/PromtGarageMac/resolve/main/Tests/PromptGarageCoreTests/RealDatabaseMigrationTests.swift
9.78 kB
| import CryptoKit | |
| import Foundation | |
| import GRDB | |
| import XCTest | |
| @testable import PromptGarageCore | |
| /// Opt-in integration coverage for Samuel's real legacy database. | |
| /// | |
| /// Run on the Mac with: | |
| /// `PROMPTGARAGE_SOURCE_DB=/absolute/path/to/prompts.db swift test --filter RealDatabaseMigrationTests` | |
| /// | |
| /// The source is hashed before and after, and all SQLite connections opened by | |
| /// the production bootstrap service are read-only. Only a disposable copy below | |
| /// XCTest's temporary directory is migrated. | |
| final class RealDatabaseMigrationTests: XCTestCase { | |
| func testRealSourceInstallsToTemporaryDestinationWithoutSourceMutationOrBackfill() throws { | |
| let environment = ProcessInfo.processInfo.environment | |
| guard let sourcePath = environment["PROMPTGARAGE_SOURCE_DB"]? | |
| .trimmingCharacters(in: .whitespacesAndNewlines), | |
| !sourcePath.isEmpty | |
| else { | |
| throw XCTSkip("Set PROMPTGARAGE_SOURCE_DB to opt in to the real legacy DB migration test.") | |
| } | |
| let sourceURL = URL(fileURLWithPath: sourcePath).standardizedFileURL | |
| guard FileManager.default.fileExists(atPath: sourceURL.path) else { | |
| XCTFail("PROMPTGARAGE_SOURCE_DB does not exist: \(sourceURL.path)") | |
| return | |
| } | |
| let sourceFamilyBefore = try databaseFamilySnapshot(at: sourceURL) | |
| let sourceQueue = try readOnlyQueue( | |
| at: sourceURL, | |
| label: "PromptGarageTests.source.readonly" | |
| ) | |
| let sourceLegacySnapshot: LegacyRowsSnapshot | |
| do { | |
| sourceLegacySnapshot = try legacyRowsSnapshot(in: sourceQueue) | |
| try sourceQueue.close() | |
| } catch { | |
| try? sourceQueue.close() | |
| throw error | |
| } | |
| let root = FileManager.default.temporaryDirectory | |
| .appendingPathComponent("PromptGarageRealMigration-\(UUID().uuidString)", isDirectory: true) | |
| try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) | |
| addTeardownBlock { try? FileManager.default.removeItem(at: root) } | |
| let service = try DatabaseBootstrapService( | |
| applicationSupportRoot: root.appendingPathComponent("ApplicationSupport") | |
| ) | |
| let sourceReport = try service.validateSource(at: sourceURL) | |
| XCTAssertFalse( | |
| sourceReport.hasPromptedAt, | |
| "The opt-in source must be the untouched legacy DB, not an already migrated app copy." | |
| ) | |
| XCTAssertGreaterThan(sourceReport.promptCount, 0) | |
| let repository = try service.installDatabase(from: sourceURL) | |
| let destinationQueue = try readOnlyQueue( | |
| at: service.databaseURL, | |
| label: "PromptGarageTests.destination.readonly" | |
| ) | |
| defer { try? destinationQueue.close() } | |
| let destinationReport = try DatabaseSchema.validateReadOnly(destinationQueue) | |
| let destinationLegacySnapshot = try legacyRowsSnapshot(in: destinationQueue) | |
| let fourParityCounts = [ | |
| sourceReport.promptCount, | |
| sourceReport.ftsCount, | |
| destinationReport.promptCount, | |
| destinationReport.ftsCount, | |
| ] | |
| XCTAssertEqual( | |
| Set(fourParityCounts).count, | |
| 1, | |
| "source prompts/source FTS/destination prompts/destination FTS must stay dynamically equal: \(fourParityCounts)" | |
| ) | |
| XCTAssertTrue(destinationReport.hasPromptedAt) | |
| XCTAssertEqual(try repository.count(), sourceReport.promptCount) | |
| XCTAssertEqual(try repository.legacyUndatedCount(), sourceReport.promptCount) | |
| XCTAssertEqual(sourceLegacySnapshot.rowCount, sourceReport.promptCount) | |
| XCTAssertEqual(destinationLegacySnapshot.rowCount, destinationReport.promptCount) | |
| XCTAssertEqual( | |
| destinationLegacySnapshot.columnDigests, | |
| sourceLegacySnapshot.columnDigests, | |
| "Every value in every legacy column must survive the copy exactly." | |
| ) | |
| XCTAssertEqual( | |
| destinationLegacySnapshot.orderedRowsDigest, | |
| sourceLegacySnapshot.orderedRowsDigest, | |
| "Legacy row/column associations must remain byte-for-byte equivalent in deterministic id order." | |
| ) | |
| let destinationLegacyNullCount = try destinationQueue.read { db in | |
| try Int.fetchOne( | |
| db, | |
| sql: "SELECT COUNT(*) FROM prompts WHERE prompted_at IS NULL" | |
| ) ?? -1 | |
| } | |
| XCTAssertEqual( | |
| destinationLegacyNullCount, | |
| sourceReport.promptCount, | |
| "Adding prompted_at to the app copy must not infer or backfill dates for any legacy row." | |
| ) | |
| XCTAssertEqual( | |
| try databaseFamilySnapshot(at: sourceURL), | |
| sourceFamilyBefore, | |
| "The selected source DB family (main, WAL, SHM, or journal) changed during a supposedly read-only migration." | |
| ) | |
| } | |
| private let legacyColumns = [ | |
| "id", "title", "content", "source", "is_starred", "tags", "model", | |
| "rating", "use_count", "created_at", "updated_at", | |
| ] | |
| private func readOnlyQueue(at url: URL, label: String) throws -> DatabaseQueue { | |
| var configuration = Configuration() | |
| configuration.label = label | |
| configuration.readonly = true | |
| configuration.busyMode = .timeout(5) | |
| return try DatabaseQueue(path: url.path, configuration: configuration) | |
| } | |
| /// Hashes every SQLite value with an explicit storage-class marker and | |
| /// length framing. Per-column digests prove column preservation; the full | |
| /// digest additionally proves that values stayed attached to the same row | |
| /// in deterministic primary-key order. | |
| private func legacyRowsSnapshot(in queue: DatabaseQueue) throws -> LegacyRowsSnapshot { | |
| try queue.read { db in | |
| let selectedColumns = legacyColumns | |
| .map { "\"\($0)\"" } | |
| .joined(separator: ", ") | |
| let cursor = try Row.fetchCursor( | |
| db, | |
| sql: "SELECT \(selectedColumns) FROM prompts ORDER BY id ASC" | |
| ) | |
| var rowCount = 0 | |
| var orderedRowsHasher = SHA256() | |
| var columnHashers = legacyColumns.map { _ in SHA256() } | |
| while let row = try cursor.next() { | |
| rowCount += 1 | |
| update(&orderedRowsHasher, withUnsignedInteger: UInt64(rowCount)) | |
| for (index, column) in legacyColumns.enumerated() { | |
| let value: DatabaseValue = row[column] | |
| update(&orderedRowsHasher, with: value) | |
| update(&columnHashers[index], with: value) | |
| } | |
| } | |
| let columnDigests = Dictionary(uniqueKeysWithValues: zip( | |
| legacyColumns, | |
| columnHashers.map { hasher in | |
| var hasher = hasher | |
| return hexadecimal(hasher.finalize()) | |
| } | |
| )) | |
| return LegacyRowsSnapshot( | |
| rowCount: rowCount, | |
| orderedRowsDigest: hexadecimal(orderedRowsHasher.finalize()), | |
| columnDigests: columnDigests | |
| ) | |
| } | |
| } | |
| private func update(_ hasher: inout SHA256, with value: DatabaseValue) { | |
| switch value.storage { | |
| case .null: | |
| hasher.update(data: Data([0])) | |
| case let .int64(value): | |
| hasher.update(data: Data([1])) | |
| update(&hasher, withUnsignedInteger: UInt64(bitPattern: value)) | |
| case let .double(value): | |
| hasher.update(data: Data([2])) | |
| update(&hasher, withUnsignedInteger: value.bitPattern) | |
| case let .string(value): | |
| hasher.update(data: Data([3])) | |
| update(&hasher, withLengthPrefixed: Data(value.utf8)) | |
| case let .blob(value): | |
| hasher.update(data: Data([4])) | |
| update(&hasher, withLengthPrefixed: value) | |
| } | |
| } | |
| private func update(_ hasher: inout SHA256, withLengthPrefixed data: Data) { | |
| update(&hasher, withUnsignedInteger: UInt64(data.count)) | |
| hasher.update(data: data) | |
| } | |
| private func update(_ hasher: inout SHA256, withUnsignedInteger value: UInt64) { | |
| var bigEndian = value.bigEndian | |
| withUnsafeBytes(of: &bigEndian) { bytes in | |
| hasher.update(data: Data(bytes)) | |
| } | |
| } | |
| private func databaseFamilySnapshot(at mainURL: URL) throws -> [DatabaseFileSnapshot] { | |
| try ["", "-wal", "-shm", "-journal"].map { suffix in | |
| let url = URL(fileURLWithPath: mainURL.path + suffix) | |
| guard FileManager.default.fileExists(atPath: url.path) else { | |
| return DatabaseFileSnapshot(suffix: suffix, byteCount: nil, sha256: nil) | |
| } | |
| let attributes = try FileManager.default.attributesOfItem(atPath: url.path) | |
| let byteCount = (attributes[.size] as? NSNumber)?.uint64Value | |
| return DatabaseFileSnapshot( | |
| suffix: suffix, | |
| byteCount: byteCount, | |
| sha256: try sha256(of: url) | |
| ) | |
| } | |
| } | |
| private func sha256(of url: URL) throws -> String { | |
| let data = try Data(contentsOf: url, options: [.mappedIfSafe]) | |
| return hexadecimal(SHA256.hash(data: data)) | |
| } | |
| private func hexadecimal<D: Sequence>(_ digest: D) -> String where D.Element == UInt8 { | |
| digest.map { String(format: "%02x", $0) }.joined() | |
| } | |
| } | |
| private struct LegacyRowsSnapshot: Equatable { | |
| let rowCount: Int | |
| let orderedRowsDigest: String | |
| let columnDigests: [String: String] | |
| } | |
| private struct DatabaseFileSnapshot: Equatable { | |
| let suffix: String | |
| let byteCount: UInt64? | |
| let sha256: String? | |
| } | |