Skip to content

Commit b2210cc

Browse files
committed
Add State Restoration Tests, DB Recovery Steps
1 parent 13ed249 commit b2210cc

File tree

5 files changed

+119
-14
lines changed

5 files changed

+119
-14
lines changed

CodeEdit/Features/Editor/Models/Restoration/EditorStateRestoration.swift

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,25 @@ final class EditorStateRestoration {
6161
}
6262
}
6363

64-
private var databaseQueue: DatabaseQueue
65-
66-
private init() throws {
67-
let databaseURL: URL = FileManager.default
64+
private var databaseQueue: DatabaseQueue?
65+
private var databaseURL: URL
66+
67+
/// Create a new editor restoration object. Will connect to or create a SQLite db.
68+
/// - Parameter databaseURL: The database URL to use. Must point to a file, not a directory. If left `nil`, will
69+
/// create a new database named `editor-restoration.db` in the application support
70+
/// directory.
71+
init(_ databaseURL: URL? = nil) throws {
72+
self.databaseURL = databaseURL ?? FileManager.default
6873
.homeDirectoryForCurrentUser
6974
.appending(path: "Library/Application Support/CodeEdit", directoryHint: .isDirectory)
7075
.appending(path: "editor-restoration.db", directoryHint: .notDirectory)
71-
72-
self.databaseQueue = try DatabaseQueue(path: databaseURL.absolutePath, configuration: .init())
7376
try attemptMigration(retry: true)
7477
}
7578

76-
private func attemptMigration(retry: Bool) throws {
79+
func attemptMigration(retry: Bool) throws {
7780
do {
81+
let databaseQueue = try DatabaseQueue(path: self.databaseURL.absolutePath, configuration: .init())
82+
7883
var migrator = DatabaseMigrator()
7984

8085
migrator.registerMigration("Version 0") {
@@ -85,12 +90,11 @@ final class EditorStateRestoration {
8590
}
8691

8792
try migrator.migrate(databaseQueue)
93+
self.databaseQueue = databaseQueue
8894
} catch {
8995
if retry {
9096
// Try to delete the database on failure, might fix a corruption or version error.
9197
try? FileManager.default.removeItem(at: databaseURL)
92-
// This will recreate the db file if necessary
93-
self.databaseQueue = try DatabaseQueue(path: databaseURL.absolutePath, configuration: .init())
9498
try attemptMigration(retry: false)
9599

96100
return // Ignore the original error if we're retrying
@@ -108,7 +112,7 @@ final class EditorStateRestoration {
108112
do {
109113
let serializedData = try JSONEncoder().encode(data)
110114
let dbRow = StateRestorationRecord(uri: documentUrl.absolutePath, data: serializedData)
111-
try databaseQueue.write { try dbRow.upsert($0) }
115+
try databaseQueue?.write { try dbRow.upsert($0) }
112116
} catch {
113117
Self.logger.error("Failed to save editor state: \(error)")
114118
}
@@ -119,7 +123,7 @@ final class EditorStateRestoration {
119123
/// - Returns: Any data saved for this file.
120124
func restorationState(for documentUrl: URL) -> StateRestorationData? {
121125
do {
122-
guard let row = try databaseQueue.read({
126+
guard let row = try databaseQueue?.read({
123127
try StateRestorationRecord.fetchOne($0, key: documentUrl.absolutePath)
124128
}) else {
125129
return nil

CodeEdit/Features/Editor/Models/Restoration/UndoManagerRegistration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class UndoManagerRegistration: ObservableObject {
1919
private var managerMap: [CEWorkspaceFile.ID: CEUndoManager] = [:]
2020

2121
init() { }
22-
22+
2323
/// Find or create a new undo manager.
2424
/// - Parameter file: The file to create for.
2525
/// - Returns: The undo manager for the given file.

CodeEdit/Features/Editor/Views/CodeFileView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ struct CodeFileView: View {
5858
@Environment(\.colorScheme)
5959
private var colorScheme
6060

61-
@EnvironmentObject
62-
var undoRegistration: UndoManagerRegistration
61+
@EnvironmentObject var undoRegistration: UndoManagerRegistration
6362

6463
@ObservedObject private var themeModel: ThemeModel = .shared
6564

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// EditorStateRestorationTests.swift
3+
// CodeEditTests
4+
//
5+
// Created by Khan Winter on 7/3/25.
6+
//
7+
8+
import Testing
9+
import Foundation
10+
@testable import CodeEdit
11+
12+
@Suite(.serialized)
13+
struct EditorStateRestorationTests {
14+
@Test
15+
func createsDatabase() throws {
16+
try withTempDir { dir in
17+
let url = dir.appending(path: "database.db")
18+
_ = try EditorStateRestoration(url)
19+
#expect(FileManager.default.fileExists(atPath: url.path(percentEncoded: false)))
20+
}
21+
}
22+
23+
@Test
24+
func savesAndRetrievesState() throws {
25+
try withTempDir { dir in
26+
let url = dir.appending(path: "database.db")
27+
let restoration = try EditorStateRestoration(url)
28+
29+
// Update some state
30+
restoration.updateRestorationState(
31+
for: dir.appending(path: "file.txt"),
32+
data: .init(cursorPositions: [], scrollPosition: .zero)
33+
)
34+
35+
// Retrieve it
36+
#expect(restoration.restorationState(for: dir.appending(path: "file.txt")) != nil)
37+
}
38+
}
39+
40+
@Test
41+
func clearsCorruptedDatabase() throws {
42+
try withTempDir { dir in
43+
let url = dir.appending(path: "database.db")
44+
try "bad SQLITE data HAHAHA".write(to: url, atomically: true, encoding: .utf8)
45+
// This will throw if it can't connect to the database.
46+
_ = try EditorStateRestoration(url)
47+
}
48+
}
49+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// withTempDir.swift
3+
// CodeEditTests
4+
//
5+
// Created by Khan Winter on 7/3/25.
6+
//
7+
8+
import Foundation
9+
10+
func withTempDir(_ test: (URL) async throws -> Void) async throws {
11+
let tempDirURL = try createAndClearDir()
12+
do {
13+
try await test(tempDirURL)
14+
} catch {
15+
try clearDir(tempDirURL)
16+
throw error
17+
}
18+
try clearDir(tempDirURL)
19+
}
20+
21+
func withTempDir(_ test: (URL) throws -> Void) throws {
22+
let tempDirURL = try createAndClearDir()
23+
do {
24+
try test(tempDirURL)
25+
} catch {
26+
try clearDir(tempDirURL)
27+
throw error
28+
}
29+
try clearDir(tempDirURL)
30+
}
31+
32+
private func createAndClearDir() throws -> URL {
33+
let tempDirURL = try FileManager.default.url(
34+
for: .developerApplicationDirectory,
35+
in: .userDomainMask,
36+
appropriateFor: nil,
37+
create: true
38+
)
39+
.appending(path: "CodeEditTestDirectory", directoryHint: .isDirectory)
40+
41+
// If it exists, delete it before the test
42+
try clearDir(tempDirURL)
43+
44+
try FileManager.default.createDirectory(at: tempDirURL, withIntermediateDirectories: true)
45+
46+
return tempDirURL
47+
}
48+
49+
private func clearDir(_ url: URL) throws {
50+
if FileManager.default.fileExists(atPath: url.absoluteURL.path(percentEncoded: false)) {
51+
try FileManager.default.removeItem(at: url)
52+
}
53+
}

0 commit comments

Comments
 (0)