|
1 | 1 | import './vscode_mock_setup'; |
2 | | -import { describe, it, before, after, beforeEach } from 'node:test'; |
| 2 | +import { describe, it, before, after, beforeEach, afterEach } from 'node:test'; |
3 | 3 | import assert from 'node:assert'; |
4 | 4 | import { mockVscode } from './mocks/vscode'; |
5 | 5 |
|
@@ -118,3 +118,345 @@ describe('isAutoCommitEnabled', () => { |
118 | 118 | await setupEnvironmentAndTest(undefined, 'ssh-remote', 2 /* Workspace */, false); |
119 | 119 | }); |
120 | 120 | }); |
| 121 | + |
| 122 | + |
| 123 | + |
| 124 | + |
| 125 | +describe('DatabaseDocument save/saveAs fallback', () => { |
| 126 | + let DatabaseDocument: any; |
| 127 | + |
| 128 | + beforeEach(() => { |
| 129 | + // Clear module cache to ensure we get a fresh instance that uses our mocked workerFactory |
| 130 | + const moduleCache = require('module')._cache; |
| 131 | + Object.keys(moduleCache).forEach(key => { |
| 132 | + if (key.includes('src/') && !key.includes('workerFactory') && !key.includes('mocks')) { |
| 133 | + delete moduleCache[key]; |
| 134 | + } |
| 135 | + }); |
| 136 | + |
| 137 | + const workerFactoryPath = require.resolve('../../src/workerFactory'); |
| 138 | + moduleCache[workerFactoryPath] = { |
| 139 | + id: workerFactoryPath, |
| 140 | + filename: workerFactoryPath, |
| 141 | + loaded: true, |
| 142 | + exports: { |
| 143 | + createDatabaseConnection: async () => ({ |
| 144 | + establishConnection: async () => ({ |
| 145 | + databaseOps: { |
| 146 | + engineKind: Promise.resolve('wasm'), |
| 147 | + writeToFile: async () => { throw new Error('Simulated write failure'); }, |
| 148 | + serializeDatabase: async () => new Uint8Array([1, 2, 3]), |
| 149 | + applyModifications: async () => {} |
| 150 | + }, |
| 151 | + isReadOnly: false |
| 152 | + }), |
| 153 | + workerMethods: { |
| 154 | + [Symbol.dispose]: () => {} |
| 155 | + } |
| 156 | + }) |
| 157 | + } |
| 158 | + }; |
| 159 | + |
| 160 | + const dbModel = require('../../src/databaseModel'); |
| 161 | + DatabaseDocument = dbModel.DatabaseDocument; |
| 162 | + }); |
| 163 | + |
| 164 | + const createFileUri = (path: string) => { |
| 165 | + return { |
| 166 | + scheme: 'file', |
| 167 | + authority: '', |
| 168 | + path: path, |
| 169 | + query: '', |
| 170 | + fragment: '', |
| 171 | + fsPath: path, |
| 172 | + with: () => ({}), |
| 173 | + toJSON: () => ({}) |
| 174 | + }; |
| 175 | + }; |
| 176 | + |
| 177 | + const createDocBypassingFactory = (dbOps: any) => { |
| 178 | + const mockViewerProvider = { |
| 179 | + reporter: {}, |
| 180 | + isVerified: true, |
| 181 | + context: { extensionUri: createFileUri('/ext') }, |
| 182 | + forceReadOnly: false, |
| 183 | + outputChannel: undefined |
| 184 | + }; |
| 185 | + const fileUri = createFileUri('/test/db.sqlite'); |
| 186 | + |
| 187 | + return new (DatabaseDocument as any)( |
| 188 | + mockViewerProvider, |
| 189 | + fileUri, |
| 190 | + null, // tracker |
| 191 | + false, // autoCommitEnabled |
| 192 | + { databaseOps: dbOps, isReadOnly: false }, |
| 193 | + { [Symbol.dispose]: () => {} }, // workerMethods |
| 194 | + async () => {}, // establishConnection |
| 195 | + {} // reporter |
| 196 | + ); |
| 197 | + }; |
| 198 | + |
| 199 | + it('saveAs: falls back to buffer transfer when writeToFile fails for file URI', async () => { |
| 200 | + let serialized = false; |
| 201 | + const dbOps = { |
| 202 | + engineKind: Promise.resolve('wasm'), |
| 203 | + writeToFile: async () => { throw new Error('Simulated write failure'); }, |
| 204 | + serializeDatabase: async () => { |
| 205 | + serialized = true; |
| 206 | + return new Uint8Array([1, 2, 3]); |
| 207 | + } |
| 208 | + }; |
| 209 | + |
| 210 | + const doc = createDocBypassingFactory(dbOps); |
| 211 | + |
| 212 | + const targetUri = createFileUri('/test/target.sqlite'); |
| 213 | + |
| 214 | + let statCalled = false; |
| 215 | + let writeFileCalled = false; |
| 216 | + |
| 217 | + const originalFs = mockVscode.workspace.fs; |
| 218 | + mockVscode.workspace.fs = { |
| 219 | + ...originalFs, |
| 220 | + stat: async () => { |
| 221 | + statCalled = true; |
| 222 | + return { size: 100 }; // Less than getFileSizeLimit() |
| 223 | + }, |
| 224 | + writeFile: async (uri: any, content: any) => { |
| 225 | + writeFileCalled = true; |
| 226 | + assert.deepStrictEqual(content, new Uint8Array([1, 2, 3])); |
| 227 | + }, |
| 228 | + readFile: async () => new Uint8Array([]) |
| 229 | + } as any; |
| 230 | + |
| 231 | + const originalConsoleWarn = console.warn; |
| 232 | + let consoleWarnCalled = false; |
| 233 | + console.warn = (msg: string, err: any) => { |
| 234 | + if (msg && msg.includes('Direct write failed')) { |
| 235 | + consoleWarnCalled = true; |
| 236 | + } else { |
| 237 | + originalConsoleWarn(msg, err); |
| 238 | + } |
| 239 | + }; |
| 240 | + |
| 241 | + try { |
| 242 | + await doc.saveAs(targetUri, undefined); |
| 243 | + |
| 244 | + assert.strictEqual(consoleWarnCalled, true, 'console.warn should be called'); |
| 245 | + assert.strictEqual(statCalled, true, 'fs.stat should be called'); |
| 246 | + assert.strictEqual(serialized, true, 'serializeDatabase should be called'); |
| 247 | + assert.strictEqual(writeFileCalled, true, 'fs.writeFile should be called'); |
| 248 | + } finally { |
| 249 | + mockVscode.workspace.fs = originalFs; |
| 250 | + console.warn = originalConsoleWarn; |
| 251 | + } |
| 252 | + }); |
| 253 | + |
| 254 | + it('save: falls back to buffer transfer when writeToFile fails for file URI', async () => { |
| 255 | + let serialized = false; |
| 256 | + const dbOps = { |
| 257 | + engineKind: Promise.resolve('wasm'), |
| 258 | + writeToFile: async () => { throw new Error('Simulated write failure'); }, |
| 259 | + serializeDatabase: async () => { |
| 260 | + serialized = true; |
| 261 | + return new Uint8Array([1, 2, 3]); |
| 262 | + } |
| 263 | + }; |
| 264 | + |
| 265 | + const doc = createDocBypassingFactory(dbOps); |
| 266 | + |
| 267 | + let writeFileCalled = false; |
| 268 | + |
| 269 | + const originalFs = mockVscode.workspace.fs; |
| 270 | + mockVscode.workspace.fs = { |
| 271 | + ...originalFs, |
| 272 | + stat: async () => { |
| 273 | + return { size: 100 }; |
| 274 | + }, |
| 275 | + writeFile: async (uri: any, content: any) => { |
| 276 | + writeFileCalled = true; |
| 277 | + assert.deepStrictEqual(content, new Uint8Array([1, 2, 3])); |
| 278 | + }, |
| 279 | + readFile: async () => new Uint8Array([]) |
| 280 | + } as any; |
| 281 | + |
| 282 | + const originalConsoleWarn = console.warn; |
| 283 | + let consoleWarnCalled = false; |
| 284 | + console.warn = (msg: string, err: any) => { |
| 285 | + if (msg && msg.includes('Direct write failed')) { |
| 286 | + consoleWarnCalled = true; |
| 287 | + } else { |
| 288 | + originalConsoleWarn(msg, err); |
| 289 | + } |
| 290 | + }; |
| 291 | + |
| 292 | + try { |
| 293 | + await doc.save(); |
| 294 | + |
| 295 | + assert.strictEqual(consoleWarnCalled, true, 'console.warn should be called'); |
| 296 | + assert.strictEqual(serialized, true, 'serializeDatabase should be called'); |
| 297 | + assert.strictEqual(writeFileCalled, true, 'fs.writeFile should be called'); |
| 298 | + } finally { |
| 299 | + mockVscode.workspace.fs = originalFs; |
| 300 | + console.warn = originalConsoleWarn; |
| 301 | + } |
| 302 | + }); |
| 303 | +}); |
| 304 | + |
| 305 | + |
| 306 | +describe('DatabaseDocument undo/redo error handling', () => { |
| 307 | + let mockViewerProvider: any; |
| 308 | + let mockOpenContext: any; |
| 309 | + let originalShowErrorMessage: any; |
| 310 | + let originalL10n: any; |
| 311 | + |
| 312 | + beforeEach(() => { |
| 313 | + originalShowErrorMessage = mockVscode.window.showErrorMessage; |
| 314 | + originalL10n = mockVscode.l10n.t; |
| 315 | + |
| 316 | + mockVscode.l10n.t = (key: string, ...args: any[]) => { |
| 317 | + let res = key; |
| 318 | + args.forEach((arg, i) => { |
| 319 | + res = res.replace(`{${i}}`, String(arg)); |
| 320 | + }); |
| 321 | + return res; |
| 322 | + }; |
| 323 | + |
| 324 | + mockViewerProvider = { |
| 325 | + reporter: undefined, |
| 326 | + isVerified: true, |
| 327 | + context: { extensionUri: mockVscode.Uri.parse('file:///ext') }, |
| 328 | + forceReadOnly: false |
| 329 | + }; |
| 330 | + |
| 331 | + mockOpenContext = { |
| 332 | + backupId: undefined |
| 333 | + }; |
| 334 | + |
| 335 | + const moduleCache = require('module')._cache; |
| 336 | + const workerFactoryPath = require.resolve('../../src/workerFactory'); |
| 337 | + moduleCache[workerFactoryPath] = { |
| 338 | + id: workerFactoryPath, |
| 339 | + filename: workerFactoryPath, |
| 340 | + loaded: true, |
| 341 | + exports: { |
| 342 | + createDatabaseConnection: () => { |
| 343 | + return Promise.resolve({ |
| 344 | + workerMethods: { |
| 345 | + open: () => Promise.resolve({ isReadOnly: false, bufferInfo: {} }), |
| 346 | + exec: () => Promise.resolve(), |
| 347 | + }, |
| 348 | + establishConnection: () => Promise.resolve({ isReadOnly: false }), |
| 349 | + databaseOps: { |
| 350 | + undoModification: () => Promise.resolve(), |
| 351 | + redoModification: () => Promise.resolve(), |
| 352 | + close: () => Promise.resolve(), |
| 353 | + getSchema: () => Promise.resolve([]), |
| 354 | + query: () => Promise.resolve([]) |
| 355 | + } |
| 356 | + }); |
| 357 | + } |
| 358 | + } |
| 359 | + }; |
| 360 | + |
| 361 | + }); |
| 362 | + |
| 363 | + afterEach(() => { |
| 364 | + mockVscode.window.showErrorMessage = originalShowErrorMessage; |
| 365 | + mockVscode.l10n.t = originalL10n; |
| 366 | + }); |
| 367 | + |
| 368 | + it('should show error message when undoModification fails', async () => { |
| 369 | + let errorMessageShown = false; |
| 370 | + mockVscode.window.showErrorMessage = async (msg?: string) => { |
| 371 | + if (msg?.includes('Test Undo Error')) { |
| 372 | + errorMessageShown = true; |
| 373 | + } |
| 374 | + }; |
| 375 | + |
| 376 | + // We clear module cache of databaseModel so it uses the mocked workerFactory |
| 377 | + delete require('module')._cache[require.resolve('../../src/databaseModel')]; |
| 378 | + const { DatabaseDocument } = require('../../src/databaseModel'); |
| 379 | + |
| 380 | + const uri = mockVscode.Uri.parse('file:///test.db'); |
| 381 | + const doc = await DatabaseDocument.create(mockViewerProvider, uri, mockOpenContext); |
| 382 | + |
| 383 | + // Mock database operations to throw error on undo |
| 384 | + const dbOps = doc.databaseOperations; |
| 385 | + if(dbOps) { |
| 386 | + dbOps.undoModification = () => Promise.reject(new Error("Test Undo Error")); |
| 387 | + } else { |
| 388 | + // force override via any |
| 389 | + (doc as any).connectionState = { |
| 390 | + databaseOps: { |
| 391 | + undoModification: () => Promise.reject(new Error("Test Undo Error")), |
| 392 | + redoModification: () => Promise.resolve() |
| 393 | + } |
| 394 | + }; |
| 395 | + } |
| 396 | + |
| 397 | + let undoAction: (() => Promise<void>) | undefined; |
| 398 | + doc.onDidChange((modification: any) => { |
| 399 | + undoAction = modification.undo; |
| 400 | + }); |
| 401 | + |
| 402 | + doc.recordModification({ |
| 403 | + label: 'Test Mod', |
| 404 | + action: 'Test Mod Action', |
| 405 | + sql: [], |
| 406 | + inverseSql: [] |
| 407 | + }); |
| 408 | + |
| 409 | + assert.ok(undoAction, 'Undo action should be emitted'); |
| 410 | + |
| 411 | + await undoAction(); |
| 412 | + |
| 413 | + assert.strictEqual(errorMessageShown, true, 'Error message should be shown for failed undo'); |
| 414 | + }); |
| 415 | + |
| 416 | + it('should show error message when redoModification fails', async () => { |
| 417 | + let errorMessageShown = false; |
| 418 | + mockVscode.window.showErrorMessage = async (msg?: string) => { |
| 419 | + if (msg?.includes('Test Redo Error')) { |
| 420 | + errorMessageShown = true; |
| 421 | + } |
| 422 | + }; |
| 423 | + |
| 424 | + const { DatabaseDocument } = require('../../src/databaseModel'); |
| 425 | + const uri = mockVscode.Uri.parse('file:///test.db'); |
| 426 | + const doc = await DatabaseDocument.create(mockViewerProvider, uri, mockOpenContext); |
| 427 | + |
| 428 | + // Mock database operations to throw error on redo |
| 429 | + const dbOps = doc.databaseOperations; |
| 430 | + if(dbOps) { |
| 431 | + dbOps.redoModification = () => Promise.reject(new Error("Test Redo Error")); |
| 432 | + } else { |
| 433 | + (doc as any).connectionState = { |
| 434 | + databaseOps: { |
| 435 | + undoModification: () => Promise.resolve(), |
| 436 | + redoModification: () => Promise.reject(new Error("Test Redo Error")) |
| 437 | + } |
| 438 | + }; |
| 439 | + } |
| 440 | + |
| 441 | + let redoAction: (() => Promise<void>) | undefined; |
| 442 | + let undoAction: (() => Promise<void>) | undefined; |
| 443 | + doc.onDidChange((modification: any) => { |
| 444 | + undoAction = modification.undo; |
| 445 | + redoAction = modification.redo; |
| 446 | + }); |
| 447 | + |
| 448 | + doc.recordModification({ |
| 449 | + label: 'Test Mod', |
| 450 | + action: 'Test Mod Action', |
| 451 | + sql: [], |
| 452 | + inverseSql: [] |
| 453 | + }); |
| 454 | + |
| 455 | + assert.ok(redoAction, 'Redo action should be emitted'); |
| 456 | + |
| 457 | + await undoAction!(); |
| 458 | + await redoAction(); |
| 459 | + |
| 460 | + assert.strictEqual(errorMessageShown, true, 'Error message should be shown for failed redo'); |
| 461 | + }); |
| 462 | +}); |
0 commit comments