Skip to content

Commit 477b6bf

Browse files
committed
test: add tests for statement highlighting, theme colors, and located scanner
- CurrentStatementHighlighterTests: 9 tests for statement detection logic (multi-statement, single statement, strings, comments, edge cases) - ThemeDefinitionTests: 7 tests for currentStatementHighlight color (defaults, round-trip, backward compatibility) - SQLStatementScannerLocatedTests: 13 tests for locatedStatementAtCursor (offsets, comments, backticks, large input, edge cases) - Fix pre-existing TabDiskActorTests compile error (save() now throws)
1 parent 1759dcf commit 477b6bf

File tree

4 files changed

+457
-27
lines changed

4 files changed

+457
-27
lines changed

TableProTests/Core/Storage/TabDiskActorTests.swift

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ struct TabDiskActorTests {
3636
// MARK: - save / load round-trip
3737

3838
@Test("Save then load round-trips correctly")
39-
func saveAndLoadRoundTrip() async {
39+
func saveAndLoadRoundTrip() async throws {
4040
let connectionId = UUID()
4141
let tabId = UUID()
4242
let tab = makeTab(id: tabId, title: "My Tab", query: "SELECT * FROM users")
4343

44-
await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId)
44+
try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId)
4545
let state = await actor.load(connectionId: connectionId)
4646

4747
#expect(state != nil)
@@ -58,21 +58,21 @@ struct TabDiskActorTests {
5858
// MARK: - load returns nil for unknown connectionId
5959

6060
@Test("Load returns nil for unknown connectionId")
61-
func loadReturnsNilForUnknown() async {
61+
func loadReturnsNilForUnknown() async throws {
6262
let result = await actor.load(connectionId: UUID())
6363
#expect(result == nil)
6464
}
6565

6666
// MARK: - save overwrites previous state
6767

6868
@Test("Save overwrites previous state")
69-
func saveOverwritesPreviousState() async {
69+
func saveOverwritesPreviousState() async throws {
7070
let connectionId = UUID()
7171
let tab1 = makeTab(title: "First")
7272
let tab2 = makeTab(title: "Second")
7373

74-
await actor.save(connectionId: connectionId, tabs: [tab1], selectedTabId: tab1.id)
75-
await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: tab2.id)
74+
try await actor.save(connectionId: connectionId, tabs: [tab1], selectedTabId: tab1.id)
75+
try await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: tab2.id)
7676

7777
let state = await actor.load(connectionId: connectionId)
7878

@@ -86,11 +86,11 @@ struct TabDiskActorTests {
8686
// MARK: - clear removes saved state
8787

8888
@Test("Clear removes saved state")
89-
func clearRemovesSavedState() async {
89+
func clearRemovesSavedState() async throws {
9090
let connectionId = UUID()
9191
let tab = makeTab()
9292

93-
await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tab.id)
93+
try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tab.id)
9494
await actor.clear(connectionId: connectionId)
9595

9696
let state = await actor.load(connectionId: connectionId)
@@ -100,21 +100,21 @@ struct TabDiskActorTests {
100100
// MARK: - clear on non-existent connectionId does not crash
101101

102102
@Test("Clear on non-existent connectionId does not crash")
103-
func clearNonExistentDoesNotCrash() async {
103+
func clearNonExistentDoesNotCrash() async throws {
104104
await actor.clear(connectionId: UUID())
105105
}
106106

107107
// MARK: - Multiple connections are independent
108108

109109
@Test("Multiple connections are independent")
110-
func multipleConnectionsAreIndependent() async {
110+
func multipleConnectionsAreIndependent() async throws {
111111
let connA = UUID()
112112
let connB = UUID()
113113
let tabA = makeTab(title: "Tab A")
114114
let tabB = makeTab(title: "Tab B")
115115

116-
await actor.save(connectionId: connA, tabs: [tabA], selectedTabId: tabA.id)
117-
await actor.save(connectionId: connB, tabs: [tabB], selectedTabId: tabB.id)
116+
try await actor.save(connectionId: connA, tabs: [tabA], selectedTabId: tabA.id)
117+
try await actor.save(connectionId: connB, tabs: [tabB], selectedTabId: tabB.id)
118118

119119
let stateA = await actor.load(connectionId: connA)
120120
let stateB = await actor.load(connectionId: connB)
@@ -135,18 +135,18 @@ struct TabDiskActorTests {
135135
// MARK: - selectedTabId preservation
136136

137137
@Test("selectedTabId is preserved correctly including nil")
138-
func selectedTabIdPreserved() async {
138+
func selectedTabIdPreserved() async throws {
139139
let connectionId = UUID()
140140
let tab = makeTab()
141141

142-
await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: nil)
142+
try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: nil)
143143
let stateNil = await actor.load(connectionId: connectionId)
144144
#expect(stateNil?.selectedTabId == nil)
145145
#expect(stateNil?.tabs.count == 1)
146146

147147
let specificId = UUID()
148148
let tab2 = makeTab(id: specificId)
149-
await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: specificId)
149+
try await actor.save(connectionId: connectionId, tabs: [tab2], selectedTabId: specificId)
150150
let stateWithId = await actor.load(connectionId: connectionId)
151151
#expect(stateWithId?.selectedTabId == specificId)
152152

@@ -156,7 +156,7 @@ struct TabDiskActorTests {
156156
// MARK: - saveLastQuery / loadLastQuery round-trip
157157

158158
@Test("saveLastQuery then loadLastQuery round-trips")
159-
func lastQueryRoundTrip() async {
159+
func lastQueryRoundTrip() async throws {
160160
let connectionId = UUID()
161161
let query = "SELECT * FROM products WHERE active = true"
162162

@@ -171,15 +171,15 @@ struct TabDiskActorTests {
171171
// MARK: - loadLastQuery returns nil for unknown connectionId
172172

173173
@Test("loadLastQuery returns nil for unknown connectionId")
174-
func loadLastQueryReturnsNilForUnknown() async {
174+
func loadLastQueryReturnsNilForUnknown() async throws {
175175
let result = await actor.loadLastQuery(for: UUID())
176176
#expect(result == nil)
177177
}
178178

179179
// MARK: - saveLastQuery with empty string removes the file
180180

181181
@Test("saveLastQuery with empty string removes the file")
182-
func saveLastQueryEmptyRemovesFile() async {
182+
func saveLastQueryEmptyRemovesFile() async throws {
183183
let connectionId = UUID()
184184

185185
await actor.saveLastQuery("SELECT 1", for: connectionId)
@@ -193,7 +193,7 @@ struct TabDiskActorTests {
193193
// MARK: - saveLastQuery with whitespace-only string removes the file
194194

195195
@Test("saveLastQuery with whitespace-only string removes the file")
196-
func saveLastQueryWhitespaceOnlyRemovesFile() async {
196+
func saveLastQueryWhitespaceOnlyRemovesFile() async throws {
197197
let connectionId = UUID()
198198

199199
await actor.saveLastQuery("SELECT 1", for: connectionId)
@@ -206,7 +206,7 @@ struct TabDiskActorTests {
206206
// MARK: - saveLastQuery skips queries exceeding 500KB
207207

208208
@Test("saveLastQuery skips queries exceeding 500KB")
209-
func saveLastQuerySkipsLargeQueries() async {
209+
func saveLastQuerySkipsLargeQueries() async throws {
210210
let connectionId = UUID()
211211
let smallQuery = "SELECT 1"
212212

@@ -225,7 +225,7 @@ struct TabDiskActorTests {
225225
// MARK: - Tab with all fields round-trips
226226

227227
@Test("Tab with all fields including isView and databaseName round-trips")
228-
func tabWithAllFieldsRoundTrips() async {
228+
func tabWithAllFieldsRoundTrips() async throws {
229229
let connectionId = UUID()
230230
let tabId = UUID()
231231
let tab = makeTab(
@@ -238,7 +238,7 @@ struct TabDiskActorTests {
238238
databaseName: "production"
239239
)
240240

241-
await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId)
241+
try await actor.save(connectionId: connectionId, tabs: [tab], selectedTabId: tabId)
242242
let state = await actor.load(connectionId: connectionId)
243243

244244
#expect(state != nil)
@@ -257,13 +257,13 @@ struct TabDiskActorTests {
257257
// MARK: - Multiple tabs in single save
258258

259259
@Test("Multiple tabs in a single save round-trip correctly")
260-
func multipleTabsRoundTrip() async {
260+
func multipleTabsRoundTrip() async throws {
261261
let connectionId = UUID()
262262
let tab1 = makeTab(title: "Tab 1", tabType: .query)
263263
let tab2 = makeTab(title: "Tab 2", tabType: .table, tableName: "orders")
264264
let tab3 = makeTab(title: "Tab 3", tabType: .query)
265265

266-
await actor.save(connectionId: connectionId, tabs: [tab1, tab2, tab3], selectedTabId: tab2.id)
266+
try await actor.save(connectionId: connectionId, tabs: [tab1, tab2, tab3], selectedTabId: tab2.id)
267267
let state = await actor.load(connectionId: connectionId)
268268

269269
#expect(state?.tabs.count == 3)
@@ -278,7 +278,7 @@ struct TabDiskActorTests {
278278
// MARK: - saveSync writes data readable by load
279279

280280
@Test("saveSync writes data that load can read back")
281-
func saveSyncWritesReadableData() async {
281+
func saveSyncWritesReadableData() async throws {
282282
let connectionId = UUID()
283283
let tabId = UUID()
284284
let tab = makeTab(id: tabId, title: "Sync Tab", query: "SELECT 42", tabType: .table, tableName: "orders")
@@ -301,10 +301,10 @@ struct TabDiskActorTests {
301301
// MARK: - Empty tabs array
302302

303303
@Test("Saving empty tabs array round-trips")
304-
func emptyTabsArrayRoundTrips() async {
304+
func emptyTabsArrayRoundTrips() async throws {
305305
let connectionId = UUID()
306306

307-
await actor.save(connectionId: connectionId, tabs: [], selectedTabId: nil)
307+
try await actor.save(connectionId: connectionId, tabs: [], selectedTabId: nil)
308308
let state = await actor.load(connectionId: connectionId)
309309

310310
#expect(state != nil)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//
2+
// SQLStatementScannerLocatedTests.swift
3+
// TableProTests
4+
//
5+
// Focused tests on locatedStatementAtCursor, the key function
6+
// powering the current statement highlighter.
7+
//
8+
9+
import Foundation
10+
import Testing
11+
@testable import TablePro
12+
13+
@Suite("SQL Statement Scanner — locatedStatementAtCursor")
14+
struct SQLStatementScannerLocatedTests {
15+
16+
// MARK: - Offset correctness
17+
18+
@Test("Returns correct offset for each statement in multi-statement string")
19+
func correctOffsetsForMultipleStatements() {
20+
let sql = "SELECT 1; UPDATE t SET x=1; DELETE FROM t"
21+
// 0123456789...
22+
23+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
24+
#expect(first.offset == 0)
25+
#expect(first.sql == "SELECT 1;")
26+
27+
let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 12)
28+
#expect(second.offset == 9)
29+
#expect(second.sql == " UPDATE t SET x=1;")
30+
31+
let third = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 30)
32+
#expect(third.offset == 27)
33+
#expect(third.sql == " DELETE FROM t")
34+
}
35+
36+
@Test("offset + sql.count covers the full statement range")
37+
func offsetPlusSqlLengthCoversRange() {
38+
let sql = "INSERT INTO t VALUES(1); SELECT * FROM t; DROP TABLE t"
39+
40+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5)
41+
let firstEnd = first.offset + (first.sql as NSString).length
42+
#expect(firstEnd == 24) // "INSERT INTO t VALUES(1);" is 24 chars
43+
44+
let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 30)
45+
let secondEnd = second.offset + (second.sql as NSString).length
46+
#expect(secondEnd == 41) // up to and including the second semicolon
47+
48+
let third = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 50)
49+
let thirdEnd = third.offset + (third.sql as NSString).length
50+
#expect(thirdEnd == (sql as NSString).length)
51+
}
52+
53+
// MARK: - Leading whitespace handling
54+
55+
@Test("Offset accounts for leading whitespace between statements")
56+
func leadingWhitespaceIncludedInOffset() {
57+
let sql = "SELECT 1; SELECT 2"
58+
// ^ offset 9, then " SELECT 2" starts at 9
59+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 15)
60+
#expect(located.offset == 9)
61+
// The raw SQL includes the leading spaces
62+
#expect(located.sql.hasPrefix(" "))
63+
}
64+
65+
@Test("Offset accounts for newlines between statements")
66+
func newlinesBetweenStatements() {
67+
let sql = "SELECT 1;\n\nSELECT 2"
68+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 15)
69+
#expect(located.offset == 9)
70+
#expect(located.sql == "\n\nSELECT 2")
71+
}
72+
73+
// MARK: - Trailing whitespace handling
74+
75+
@Test("Trailing whitespace before semicolon is included in statement")
76+
func trailingWhitespaceBeforeSemicolon() {
77+
let sql = "SELECT 1 ; SELECT 2"
78+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5)
79+
#expect(located.sql == "SELECT 1 ;")
80+
}
81+
82+
// MARK: - Comment styles
83+
84+
@Test("Works with line comments containing semicolons")
85+
func lineCommentWithSemicolon() {
86+
let sql = "SELECT 1 -- drop; table\n; SELECT 2"
87+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
88+
// The semicolon in the comment is not a delimiter
89+
#expect(first.sql == "SELECT 1 -- drop; table\n;")
90+
#expect(first.offset == 0)
91+
}
92+
93+
@Test("Works with block comments containing semicolons")
94+
func blockCommentWithSemicolon() {
95+
let sql = "SELECT /* ; */ 1; SELECT 2"
96+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
97+
#expect(first.sql == "SELECT /* ; */ 1;")
98+
#expect(first.offset == 0)
99+
}
100+
101+
@Test("Works with mixed comment styles")
102+
func mixedComments() {
103+
// Semicolons inside comments are ignored; real delimiter is at pos 31
104+
let sql = "SELECT 1 /* block; */ -- line;\n; SELECT 2"
105+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
106+
#expect(first.offset == 0)
107+
#expect(first.sql.contains("SELECT 1"))
108+
109+
let second = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 38)
110+
#expect(second.sql.contains("SELECT 2"))
111+
}
112+
113+
// MARK: - Backtick-quoted identifiers
114+
115+
@Test("Backtick-quoted identifiers containing semicolons do not split")
116+
func backtickWithSemicolon() {
117+
let sql = "SELECT `col;name`; SELECT 2"
118+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 5)
119+
#expect(first.sql == "SELECT `col;name`;")
120+
#expect(first.offset == 0)
121+
}
122+
123+
// MARK: - Edge cases
124+
125+
@Test("Cursor at exact semicolon position belongs to current statement")
126+
func cursorAtSemicolon() {
127+
let sql = "SELECT 1; SELECT 2"
128+
// Position 8 is the semicolon character
129+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 8)
130+
#expect(located.offset == 0)
131+
#expect(located.sql == "SELECT 1;")
132+
}
133+
134+
@Test("Cursor beyond end of string is clamped")
135+
func cursorBeyondEnd() {
136+
let sql = "SELECT 1; SELECT 2"
137+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 9999)
138+
#expect(located.offset == 9)
139+
#expect(located.sql == " SELECT 2")
140+
}
141+
142+
@Test("Handles very large input without crashing")
143+
func largeInput() {
144+
var parts: [String] = []
145+
for i in 0..<200 {
146+
parts.append("SELECT \(i) FROM very_long_table_name_for_testing;")
147+
}
148+
let sql = parts.joined(separator: " ")
149+
let nsSQL = sql as NSString
150+
#expect(nsSQL.length > 10_000)
151+
152+
let midpoint = nsSQL.length / 2
153+
let located = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: midpoint)
154+
#expect(!located.sql.isEmpty)
155+
#expect(located.offset >= 0)
156+
#expect(located.offset < nsSQL.length)
157+
}
158+
159+
@Test("Multiple consecutive semicolons produce empty-ish segments")
160+
func consecutiveSemicolons() {
161+
let sql = "SELECT 1;;; SELECT 2"
162+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
163+
#expect(first.sql == "SELECT 1;")
164+
#expect(first.offset == 0)
165+
}
166+
167+
@Test("Escaped quote inside string does not break parsing")
168+
func escapedQuoteInString() {
169+
let sql = "SELECT 'it\\'s here'; SELECT 2"
170+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
171+
#expect(first.sql == "SELECT 'it\\'s here';")
172+
#expect(first.offset == 0)
173+
}
174+
175+
@Test("Doubled quote escape inside string does not break parsing")
176+
func doubledQuoteInString() {
177+
let sql = "SELECT 'it''s here'; SELECT 2"
178+
let first = SQLStatementScanner.locatedStatementAtCursor(in: sql, cursorPosition: 0)
179+
#expect(first.sql == "SELECT 'it''s here';")
180+
#expect(first.offset == 0)
181+
}
182+
}

0 commit comments

Comments
 (0)