Skip to content

Commit c9a2cc7

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

File tree

3 files changed

+294
-8
lines changed

3 files changed

+294
-8
lines changed

lib/adapters/apply-edit-adapter.ts

Lines changed: 60 additions & 8 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 {
@@ -55,7 +56,9 @@ export default class ApplyEditAdapter {
5556

5657
const promises = (workspaceEdit.documentChanges || []).map(async (edit): Promise<void> => {
5758
if (!TextDocumentEdit.is(edit)) {
58-
return ApplyEditAdapter.handleResourceOperation(edit)
59+
return ApplyEditAdapter.handleResourceOperation(edit).catch((err) => {
60+
throw Error(`Error during ${edit.kind} resource operation: ${err.message}`)
61+
})
5962
}
6063
const path = Convert.uriToPath(edit.textDocument.uri)
6164
const editor = (await atom.workspace.open(path, {
@@ -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
}
101153
}
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: {

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

0 commit comments

Comments
 (0)