Skip to content

Commit b75cc53

Browse files
committed
chore: add tests on resource operations
1 parent 643736c commit b75cc53

File tree

3 files changed

+299
-12
lines changed

3 files changed

+299
-12
lines changed

lib/adapters/apply-edit-adapter.ts

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
DocumentUri
1313
} from "../languageclient"
1414
import { TextBuffer, TextEditor } from "atom"
15-
import * as fs from 'fs';
15+
import * as fs from "fs"
16+
import * as rimraf from "rimraf"
1617

1718
/** Public: Adapts workspace/applyEdit commands to editors. */
1819
export default class ApplyEditAdapter {
@@ -49,14 +50,16 @@ export default class ApplyEditAdapter {
4950

5051
public static async apply(workspaceEdit: WorkspaceEdit): Promise<ApplyWorkspaceEditResponse> {
5152
ApplyEditAdapter.normalize(workspaceEdit)
52-
53+
5354
// Keep checkpoints from all successful buffer edits
5455
const checkpoints: Array<{ buffer: TextBuffer; checkpoint: number }> = []
5556

5657
const promises = (workspaceEdit.documentChanges || []).map(async (edit): Promise<void> => {
5758
if (!TextDocumentEdit.is(edit)) {
58-
return ApplyEditAdapter.handleResourceOperation(edit)
59-
}
59+
return ApplyEditAdapter.handleResourceOperation(edit).catch((err) => {
60+
throw Error(`Error during ${edit.kind} resource operation: ${err.message}`)
61+
})
62+
}
6063
const path = Convert.uriToPath(edit.textDocument.uri)
6164
const editor = (await atom.workspace.open(path, {
6265
searchAllPanes: true,
@@ -87,23 +90,72 @@ export default class ApplyEditAdapter {
8790
return { applied }
8891
}
8992

90-
private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise<void>
91-
{
93+
private static async handleResourceOperation(edit: (CreateFile | RenameFile | DeleteFile)): Promise<void> {
9294
if (DeleteFile.is(edit)) {
93-
return fs.promises.unlink(Convert.uriToPath(edit.uri))
95+
const path = Convert.uriToPath(edit.uri)
96+
const exists = fs.existsSync(path)
97+
const ignoreIfNotExists = edit.options?.ignoreIfNotExists
98+
99+
if (!exists) {
100+
if (ignoreIfNotExists) {
101+
return
102+
}
103+
throw Error(`Target doesn't exist.`)
104+
}
105+
106+
const isDirectory = fs.lstatSync(path).isDirectory()
107+
108+
if (isDirectory) {
109+
if (edit.options?.recursive) {
110+
return new Promise((resolve, reject) => {
111+
rimraf(path, { glob: false }, (err) => {
112+
if (err) {
113+
reject(err)
114+
}
115+
resolve()
116+
})
117+
})
118+
}
119+
return fs.promises.rmdir(path, { recursive: edit.options?.recursive })
120+
}
121+
122+
return fs.promises.unlink(path)
94123
}
95124
if (RenameFile.is(edit)) {
96-
return fs.promises.rename(Convert.uriToPath(edit.oldUri), Convert.uriToPath(edit.newUri))
125+
const oldPath = Convert.uriToPath(edit.oldUri)
126+
const newPath = Convert.uriToPath(edit.newUri)
127+
const exists = fs.existsSync(newPath)
128+
const ignoreIfExists = edit.options?.ignoreIfExists
129+
const overwrite = edit.options?.overwrite
130+
131+
if (exists && ignoreIfExists && !overwrite) {
132+
return
133+
}
134+
135+
if (exists && !ignoreIfExists && !overwrite) {
136+
throw Error(`Target exists.`)
137+
}
138+
139+
return fs.promises.rename(oldPath, newPath)
97140
}
98141
if (CreateFile.is(edit)) {
99-
return fs.promises.writeFile(edit.uri, '')
142+
const path = Convert.uriToPath(edit.uri)
143+
const exists = fs.existsSync(path)
144+
const ignoreIfExists = edit.options?.ignoreIfExists
145+
const overwrite = edit.options?.overwrite
146+
147+
if (exists && ignoreIfExists && !overwrite) {
148+
return
149+
}
150+
151+
return fs.promises.writeFile(path, '')
100152
}
101-
}
153+
}
102154

103155
private static normalize(workspaceEdit: WorkspaceEdit): void {
104156
const documentChanges = workspaceEdit.documentChanges || []
105157

106-
if (!workspaceEdit.hasOwnProperty('documentChanges') && workspaceEdit.hasOwnProperty('changes')) {
158+
if (!('documentChanges' in workspaceEdit) && ('changes' in workspaceEdit)) {
107159
Object.keys(workspaceEdit.changes || []).forEach((uri: DocumentUri) => {
108160
documentChanges.push({
109161
textDocument: {
@@ -114,7 +166,7 @@ export default class ApplyEditAdapter {
114166
})
115167
})
116168
}
117-
169+
118170
workspaceEdit.documentChanges = documentChanges
119171
}
120172

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
},
2626
"atomTestRunner": "./test/runner",
2727
"dependencies": {
28+
"@types/rimraf": "^3.0.0",
2829
"atom-ide-base": "^2.4.0",
30+
"rimraf": "^3.0.2",
2931
"vscode-jsonrpc": "6.0.0",
3032
"vscode-languageserver-protocol": "3.16.0",
3133
"vscode-languageserver-types": "3.16.0",

test/adapters/apply-edit-adapter.test.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { expect } from "chai"
22
import * as path from "path"
3+
import * as os from "os"
4+
import * as fs from "fs"
35
import * as sinon from "sinon"
46
import ApplyEditAdapter from "../../lib/adapters/apply-edit-adapter"
57
import Convert from "../../lib/convert"
@@ -9,6 +11,7 @@ const TEST_PATH1 = normalizeDriveLetterName(path.join(__dirname, "test.txt"))
911
const TEST_PATH2 = normalizeDriveLetterName(path.join(__dirname, "test2.txt"))
1012
const TEST_PATH3 = normalizeDriveLetterName(path.join(__dirname, "test3.txt"))
1113
const TEST_PATH4 = normalizeDriveLetterName(path.join(__dirname, "test4.txt"))
14+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atom-languageclient-tests'))
1215

1316
function normalizeDriveLetterName(filePath: string): string {
1417
if (process.platform === "win32") {
@@ -186,5 +189,235 @@ describe("ApplyEditAdapter", () => {
186189
expect(errorCalls.length).to.equal(1)
187190
expect(errorCalls[0].args[1].detail).to.equal(`Out of range edit on ${TEST_PATH4}:1:2`)
188191
})
192+
193+
it("handles rename resource operations", async () => {
194+
const directory = fs.mkdtempSync(tempDir)
195+
const oldUri = path.join(directory, "test.txt")
196+
const newUri = path.join(directory, "test-renamed.txt")
197+
fs.writeFileSync(oldUri, 'abcd')
198+
199+
const result = await ApplyEditAdapter.onApplyEdit({
200+
edit: {
201+
documentChanges: [
202+
{
203+
kind: "rename",
204+
oldUri: oldUri,
205+
newUri: newUri,
206+
}
207+
]
208+
},
209+
})
210+
211+
expect(result.applied).to.equal(true)
212+
expect(fs.existsSync(newUri)).to.equal(true)
213+
expect(fs.readFileSync(newUri).toString()).to.equal('abcd')
214+
expect(fs.existsSync(oldUri)).to.equal(false)
215+
})
216+
217+
it("handles rename operation with ignoreIfExists option", async () => {
218+
const directory = fs.mkdtempSync(tempDir)
219+
const oldUri = path.join(directory, "test.txt")
220+
const newUri = path.join(directory, "test-renamed.txt")
221+
fs.writeFileSync(oldUri, 'abcd')
222+
fs.writeFileSync(newUri, 'efgh')
223+
224+
const result = await ApplyEditAdapter.onApplyEdit({
225+
edit: {
226+
documentChanges: [
227+
{
228+
kind: "rename",
229+
oldUri: oldUri,
230+
newUri: newUri,
231+
options: {
232+
ignoreIfExists: true
233+
}
234+
}
235+
]
236+
},
237+
})
238+
239+
expect(result.applied).to.equal(true)
240+
expect(fs.existsSync(oldUri)).to.equal(true)
241+
expect(fs.readFileSync(newUri).toString()).to.equal('efgh')
242+
})
243+
244+
it("handles rename operation with overwrite option", async () => {
245+
const directory = fs.mkdtempSync(tempDir)
246+
const oldUri = path.join(directory, "test.txt")
247+
const newUri = path.join(directory, "test-renamed.txt")
248+
fs.writeFileSync(oldUri, 'abcd')
249+
fs.writeFileSync(newUri, 'efgh')
250+
251+
const result = await ApplyEditAdapter.onApplyEdit({
252+
edit: {
253+
documentChanges: [
254+
{
255+
kind: "rename",
256+
oldUri: oldUri,
257+
newUri: newUri,
258+
options: {
259+
overwrite: true,
260+
ignoreIfExists: true // Overwrite wins over ignoreIfExists
261+
}
262+
}
263+
]
264+
},
265+
})
266+
267+
expect(result.applied).to.equal(true)
268+
expect(fs.existsSync(oldUri)).to.equal(false)
269+
expect(fs.readFileSync(newUri).toString()).to.equal('abcd')
270+
})
271+
272+
it("throws an error on rename operation if target exists", async () => {
273+
const directory = fs.mkdtempSync(tempDir)
274+
const oldUri = path.join(directory, "test.txt")
275+
const newUri = path.join(directory, "test-renamed.txt")
276+
fs.writeFileSync(oldUri, 'abcd')
277+
fs.writeFileSync(newUri, 'efgh')
278+
279+
const result = await ApplyEditAdapter.onApplyEdit({
280+
edit: {
281+
documentChanges: [
282+
{
283+
kind: "rename",
284+
oldUri: oldUri,
285+
newUri: newUri,
286+
}
287+
]
288+
},
289+
})
290+
291+
expect(result.applied).to.equal(false)
292+
expect(fs.existsSync(oldUri)).to.equal(true)
293+
expect(fs.readFileSync(oldUri).toString()).to.equal('abcd')
294+
expect(fs.existsSync(newUri)).to.equal(true)
295+
expect(fs.readFileSync(newUri).toString()).to.equal('efgh')
296+
297+
expect(
298+
(atom as any).notifications.addError.calledWith("workspace/applyEdits failed", {
299+
description: "Failed to apply edits.",
300+
detail: "Error during rename resource operation: Target exists.",
301+
})
302+
).to.equal(true)
303+
})
304+
305+
it("handles delete resource operations on files", async () => {
306+
const directory = fs.mkdtempSync(tempDir)
307+
const uri = path.join(directory, "test.txt")
308+
fs.writeFileSync(uri, 'abcd')
309+
310+
const result = await ApplyEditAdapter.onApplyEdit({
311+
edit: {
312+
documentChanges: [
313+
{
314+
kind: "delete",
315+
uri: uri
316+
}
317+
]
318+
},
319+
})
320+
321+
expect(result.applied).to.equal(true)
322+
expect(fs.existsSync(uri)).to.equal(false)
323+
})
324+
325+
it("handles delete resource operations on directories", async () => {
326+
const directory = fs.mkdtempSync(tempDir)
327+
const file1 = path.join(directory, '1.txt')
328+
const file2 = path.join(directory, '2.txt')
329+
fs.writeFileSync(file1, '1')
330+
fs.writeFileSync(file2, '2')
331+
332+
const result = await ApplyEditAdapter.onApplyEdit({
333+
edit: {
334+
documentChanges: [
335+
{
336+
kind: "delete",
337+
uri: directory,
338+
options: {
339+
recursive: true
340+
}
341+
}
342+
]
343+
},
344+
})
345+
346+
expect(result.applied).to.equal(true)
347+
expect(fs.existsSync(directory)).to.equal(false)
348+
expect(fs.existsSync(file1)).to.equal(false)
349+
expect(fs.existsSync(file2)).to.equal(false)
350+
})
351+
352+
it("throws an error when deleting a non-empty directory without recursive option", async () => {
353+
const directory = fs.mkdtempSync(tempDir)
354+
const file1 = path.join(directory, '1.txt')
355+
const file2 = path.join(directory, '2.txt')
356+
fs.writeFileSync(file1, '1')
357+
fs.writeFileSync(file2, '2')
358+
359+
const result = await ApplyEditAdapter.onApplyEdit({
360+
edit: {
361+
documentChanges: [
362+
{
363+
kind: "delete",
364+
uri: directory,
365+
options: {
366+
recursive: false
367+
}
368+
}
369+
]
370+
},
371+
})
372+
373+
expect(result.applied).to.equal(false)
374+
expect(fs.existsSync(directory)).to.equal(true)
375+
expect(fs.existsSync(file1)).to.equal(true)
376+
expect(fs.existsSync(file2)).to.equal(true)
377+
const errorCalls = (atom as any).notifications.addError.getCalls()
378+
expect(errorCalls.length).to.equal(1)
379+
expect(errorCalls[0].args[1].detail).to.match(/Error during delete resource operation: (.*)/)
380+
})
381+
382+
383+
it("throws an error on delete operation if target doesnt exist", async () => {
384+
const result = await ApplyEditAdapter.onApplyEdit({
385+
edit: {
386+
documentChanges: [
387+
{
388+
kind: "delete",
389+
uri: path.join(tempDir, "unexisting.txt"),
390+
}
391+
]
392+
},
393+
})
394+
//
395+
expect(result.applied).to.equal(false)
396+
expect(
397+
(atom as any).notifications.addError.calledWith("workspace/applyEdits failed", {
398+
description: "Failed to apply edits.",
399+
detail: "Error during delete resource operation: Target doesn't exist.",
400+
})
401+
).to.equal(true)
402+
})
403+
404+
it("handles create resource operations", async () => {
405+
const directory = fs.mkdtempSync(tempDir)
406+
const uri = path.join(directory, "test.txt")
407+
408+
const result = await ApplyEditAdapter.onApplyEdit({
409+
edit: {
410+
documentChanges: [
411+
{
412+
kind: "create",
413+
uri: uri
414+
}
415+
]
416+
},
417+
})
418+
419+
expect(result.applied).to.equal(true)
420+
expect(fs.existsSync(uri)).to.equal(true)
421+
})
189422
})
190423
})

0 commit comments

Comments
 (0)