1
0
Fork 0
toolkit/packages/io/__tests__/io.test.ts

1595 lines
51 KiB
TypeScript

import * as child from 'child_process'
import {promises as fs} from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as io from '../src/io'
import * as ioUtil from '../src/io-util'
describe('cp', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('copies file with no flags', async () => {
const root = path.join(getTestTemp(), 'cp_with_no_flags')
const sourceFile = path.join(root, 'cp_source')
const targetFile = path.join(root, 'cp_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.cp(sourceFile, targetFile)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('copies file using -f', async () => {
const root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f')
const sourceFile: string = path.join(root, 'cp_source')
const targetFile: string = path.join(root, 'cp_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content')
await io.cp(sourceFile, targetFile, {recursive: false, force: true})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('copies file into directory', async () => {
const root: string = path.join(
path.join(__dirname, '_temp'),
'cp_file_to_directory'
)
const sourceFile: string = path.join(root, 'cp_source')
const targetDirectory: string = path.join(root, 'cp_target')
const targetFile: string = path.join(targetDirectory, 'cp_source')
await io.mkdirP(targetDirectory)
await fs.writeFile(sourceFile, 'test file content')
await io.cp(sourceFile, targetDirectory, {recursive: false, force: true})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('try copying to existing file with -n', async () => {
const root: string = path.join(getTestTemp(), 'cp_to_existing')
const sourceFile: string = path.join(root, 'cp_source')
const targetFile: string = path.join(root, 'cp_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await fs.writeFile(targetFile, 'correct content', {encoding: 'utf8'})
await io.cp(sourceFile, targetFile, {recursive: false, force: false})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'correct content'
)
})
it('copies directory into existing destination with -r', async () => {
const root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest')
const sourceFolder: string = path.join(root, 'cp_source')
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(
targetFolder,
'cp_source',
'cp_source_file'
)
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mkdirP(targetFolder)
await io.cp(sourceFolder, targetFolder, {recursive: true})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('copies directory into existing destination with -r without copying source directory', async () => {
const root: string = path.join(
getTestTemp(),
'cp_with_-r_existing_dest_no_source_dir'
)
const sourceFolder: string = path.join(root, 'cp_source')
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(targetFolder, 'cp_source_file')
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mkdirP(targetFolder)
await io.cp(sourceFolder, targetFolder, {
recursive: true,
copySourceDirectory: false
})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('copies directory into non-existing destination with -r', async () => {
const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexistent_dest')
const sourceFolder: string = path.join(root, 'cp_source')
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(targetFolder, 'cp_source_file')
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.cp(sourceFolder, targetFolder, {recursive: true})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
})
it('tries to copy directory without -r', async () => {
const root: string = path.join(getTestTemp(), 'cp_without_-r')
const sourceFolder: string = path.join(root, 'cp_source')
const sourceFile: string = path.join(sourceFolder, 'cp_source_file')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(
targetFolder,
'cp_source',
'cp_source_file'
)
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
let thrown = false
try {
await io.cp(sourceFolder, targetFolder)
} catch (err) {
thrown = true
}
expect(thrown).toBe(true)
await assertNotExists(targetFile)
})
it('Copies symlinks correctly', async () => {
// create the following layout
// sourceFolder
// sourceFolder/nested
// sourceFolder/nested/sourceFile
// sourceFolder/symlinkDirectory -> sourceFile
const root: string = path.join(getTestTemp(), 'cp_with_-r_symlinks')
const sourceFolder: string = path.join(root, 'cp_source')
const nestedFolder: string = path.join(sourceFolder, 'nested')
const sourceFile: string = path.join(nestedFolder, 'cp_source_file')
const symlinkDirectory: string = path.join(sourceFolder, 'symlinkDirectory')
const targetFolder: string = path.join(root, 'cp_target')
const targetFile: string = path.join(
targetFolder,
'nested',
'cp_source_file'
)
const symlinkTargetPath: string = path.join(
targetFolder,
'symlinkDirectory',
'cp_source_file'
)
await io.mkdirP(sourceFolder)
await io.mkdirP(nestedFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await createSymlinkDir(nestedFolder, symlinkDirectory)
await io.cp(sourceFolder, targetFolder, {recursive: true})
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
expect(await fs.readFile(symlinkTargetPath, {encoding: 'utf8'})).toBe(
'test file content'
)
})
})
describe('mv', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('moves file with no flags', async () => {
const root = path.join(getTestTemp(), ' mv_with_no_flags')
const sourceFile = path.join(root, ' mv_source')
const targetFile = path.join(root, ' mv_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mv(sourceFile, targetFile)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await assertNotExists(sourceFile)
})
it('moves file using -f', async () => {
const root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f')
const sourceFile: string = path.join(root, ' mv_source')
const targetFile: string = path.join(root, ' mv_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content')
await io.mv(sourceFile, targetFile)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await assertNotExists(sourceFile)
})
it('try moving to existing file with -n', async () => {
const root: string = path.join(getTestTemp(), ' mv_to_existing')
const sourceFile: string = path.join(root, ' mv_source')
const targetFile: string = path.join(root, ' mv_target')
await io.mkdirP(root)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await fs.writeFile(targetFile, 'correct content', {encoding: 'utf8'})
let failed = false
try {
await io.mv(sourceFile, targetFile, {force: false})
} catch {
failed = true
}
expect(failed).toBe(true)
expect(await fs.readFile(sourceFile, {encoding: 'utf8'})).toBe(
'test file content'
)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'correct content'
)
})
it('moves directory into existing destination', async () => {
const root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest')
const sourceFolder: string = path.join(root, ' mv_source')
const sourceFile: string = path.join(sourceFolder, ' mv_source_file')
const targetFolder: string = path.join(root, ' mv_target')
const targetFile: string = path.join(
targetFolder,
' mv_source',
' mv_source_file'
)
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mkdirP(targetFolder)
await io.mv(sourceFolder, targetFolder)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await assertNotExists(sourceFile)
})
it('moves directory into non-existing destination', async () => {
const root: string = path.join(
getTestTemp(),
' mv_with_-r_nonexistent_dest'
)
const sourceFolder: string = path.join(root, ' mv_source')
const sourceFile: string = path.join(sourceFolder, ' mv_source_file')
const targetFolder: string = path.join(root, ' mv_target')
const targetFile: string = path.join(targetFolder, ' mv_source_file')
await io.mkdirP(sourceFolder)
await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'})
await io.mv(sourceFolder, targetFolder)
expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await assertNotExists(sourceFile)
})
describe('ioUtil.rename', () => {
beforeAll(() => {
jest.spyOn(fs, 'rename').mockImplementation(() => {
throw Object.assign(new Error('cross-device link not permitted'), {
code: 'EXDEV'
})
})
})
afterEach(() => {
jest.restoreAllMocks()
})
it('fallbacks to `fs.cp` and `fs.rm` on `EXDEV` exception', async () => {
const root: string = path.join(getTestTemp(), 'rename_fallback_test')
const source: string = path.join(root, 'realfile1')
const dest: string = path.join(root, 'realfile2')
await io.mkdirP(root)
await fs.writeFile(source, 'test file content', {encoding: 'utf8'})
await io.mv(source, dest)
expect(await fs.readFile(dest, {encoding: 'utf8'})).toBe(
'test file content'
)
})
})
})
describe('rmRF', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('removes single folder with rmRF', async () => {
const testPath = path.join(getTestTemp(), 'testFolder')
await io.mkdirP(testPath)
await assertExists(testPath)
await io.rmRF(testPath)
await assertNotExists(testPath)
})
it('removes recursive folders with rmRF', async () => {
const testPath = path.join(getTestTemp(), 'testDir1')
const testPath2 = path.join(testPath, 'testDir2')
await io.mkdirP(testPath2)
await assertExists(testPath)
await assertExists(testPath2)
await io.rmRF(testPath)
await assertNotExists(testPath)
await assertNotExists(testPath2)
})
it('removes folder with locked file with rmRF', async () => {
const testPath = path.join(getTestTemp(), 'testFolder')
await io.mkdirP(testPath)
await assertExists(testPath)
// can't remove folder with locked file on windows
const filePath = path.join(testPath, 'file.txt')
await fs.appendFile(filePath, 'some data')
await assertExists(filePath)
// For windows we need to explicitly set an exclusive lock flag, because by default Node will open the file with the 'Delete' FileShare flag.
// See the exclusive lock windows flag definition:
// https://github.com/nodejs/node/blob/c2e4b1fa9ad0b744616c4e4c13a5017772a630c4/deps/uv/src/win/fs.c#L499-L513
const fd = await fs.open(
filePath,
fs.constants.O_RDONLY | ioUtil.UV_FS_O_EXLOCK
)
if (ioUtil.IS_WINDOWS) {
// On Windows, we expect an error due to an lstat call implementation in the underlying libuv code.
// See https://github.com/libuv/libuv/issues/3267 is resolved
await expect(async () => io.rmRF(testPath)).rejects.toThrow('EBUSY')
} else {
await io.rmRF(testPath)
await assertNotExists(testPath)
}
await fd.close()
await io.rmRF(testPath)
await assertNotExists(testPath)
})
it('removes folder that does not exist with rmRF', async () => {
const testPath = path.join(getTestTemp(), 'testFolder')
await assertNotExists(testPath)
await io.rmRF(testPath)
await assertNotExists(testPath)
})
it('removes file with rmRF', async () => {
const file: string = path.join(getTestTemp(), 'rmRF_file')
await fs.writeFile(file, 'test file content')
await assertExists(file)
await io.rmRF(file)
await assertNotExists(file)
})
it('removes hidden folder with rmRF', async () => {
const directory: string = path.join(getTestTemp(), '.rmRF_directory')
await createHiddenDirectory(directory)
await assertExists(directory)
await io.rmRF(directory)
await assertNotExists(directory)
})
it('removes hidden file with rmRF', async () => {
const file: string = path.join(getTestTemp(), '.rmRF_file')
await fs.writeFile(file, 'test file content')
await assertExists(file)
await io.rmRF(file)
await assertNotExists(file)
})
// creating a symlink to a file on Windows requires elevated
if (os.platform() !== 'win32') {
it('removes symlink file with rmRF', async () => {
// create the following layout:
// real_file
// symlink_file -> real_file
const root: string = path.join(getTestTemp(), 'rmRF_sym_file_test')
const realFile: string = path.join(root, 'real_file')
const symlinkFile: string = path.join(root, 'symlink_file')
await io.mkdirP(root)
await fs.writeFile(realFile, 'test file content')
await fs.symlink(realFile, symlinkFile)
expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await io.rmRF(symlinkFile)
await assertExists(realFile)
await assertNotExists(symlinkFile)
})
it('removes symlink file with missing source using rmRF', async () => {
// create the following layout:
// real_file
// symlink_file -> real_file
const root: string = path.join(
getTestTemp(),
'rmRF_sym_file_missing_source_test'
)
const realFile: string = path.join(root, 'real_file')
const symlinkFile: string = path.join(root, 'symlink_file')
await io.mkdirP(root)
await fs.writeFile(realFile, 'test file content')
await fs.symlink(realFile, symlinkFile)
expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe(
'test file content'
)
// remove the real file
await fs.unlink(realFile)
expect((await fs.lstat(symlinkFile)).isSymbolicLink()).toBe(true)
// remove the symlink file
await io.rmRF(symlinkFile)
let errcode = ''
try {
await fs.lstat(symlinkFile)
} catch (err) {
errcode = err.code
}
expect(errcode).toBe('ENOENT')
})
it('removes symlink level 2 file with rmRF', async () => {
// create the following layout:
// real_file
// symlink_file -> real_file
// symlink_level_2_file -> symlink_file
const root: string = path.join(
getTestTemp(),
'rmRF_sym_level_2_file_test'
)
const realFile: string = path.join(root, 'real_file')
const symlinkFile: string = path.join(root, 'symlink_file')
const symlinkLevel2File: string = path.join(root, 'symlink_level_2_file')
await io.mkdirP(root)
await fs.writeFile(realFile, 'test file content')
await fs.symlink(realFile, symlinkFile)
await fs.symlink(symlinkFile, symlinkLevel2File)
expect(await fs.readFile(symlinkLevel2File, {encoding: 'utf8'})).toBe(
'test file content'
)
await io.rmRF(symlinkLevel2File)
await assertExists(realFile)
await assertExists(symlinkFile)
await assertNotExists(symlinkLevel2File)
})
it('removes nested symlink file with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// outer_directory
// outer_directory/symlink_file -> real_file
const root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test')
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const outerDirectory: string = path.join(root, 'outer_directory')
const symlinkFile: string = path.join(
root,
'outer_directory',
'symlink_file'
)
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await io.mkdirP(outerDirectory)
await fs.symlink(realFile, symlinkFile)
expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await io.rmRF(outerDirectory)
await assertExists(realDirectory)
await assertExists(realFile)
await assertNotExists(symlinkFile)
await assertNotExists(outerDirectory)
})
it('removes deeply nested symlink file with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// outer_directory
// outer_directory/nested_directory
// outer_directory/nested_directory/symlink_file -> real_file
const root: string = path.join(
getTestTemp(),
'rmRF_sym_deep_nest_file_test'
)
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const outerDirectory: string = path.join(root, 'outer_directory')
const nestedDirectory: string = path.join(
root,
'outer_directory',
'nested_directory'
)
const symlinkFile: string = path.join(
root,
'outer_directory',
'nested_directory',
'symlink_file'
)
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await io.mkdirP(nestedDirectory)
await fs.symlink(realFile, symlinkFile)
expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe(
'test file content'
)
await io.rmRF(outerDirectory)
await assertExists(realDirectory)
await assertExists(realFile)
await assertNotExists(symlinkFile)
await assertNotExists(outerDirectory)
})
} else {
it('correctly escapes % on windows', async () => {
const root: string = path.join(getTestTemp(), 'rmRF_escape_test_win')
const directory: string = path.join(root, '%test%')
await io.mkdirP(root)
await io.mkdirP(directory)
const oldEnv = process.env['test']
process.env['test'] = 'thisshouldnotresolve'
await io.rmRF(directory)
await assertNotExists(directory)
process.env['test'] = oldEnv
})
it('Should throw for invalid characters', async () => {
const root: string = path.join(getTestTemp(), 'rmRF_invalidChar_Windows')
const errorString =
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
await expect(io.rmRF(path.join(root, '"'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '<'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '>'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '|'))).rejects.toHaveProperty(
'message',
errorString
)
await expect(io.rmRF(path.join(root, '*'))).rejects.toHaveProperty(
'message',
errorString
)
})
}
it('removes symlink folder with missing source using rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// symlink_directory -> real_directory
const root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test')
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const symlinkDirectory: string = path.join(root, 'symlink_directory')
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await createSymlinkDir(realDirectory, symlinkDirectory)
await assertExists(symlinkDirectory)
// remove the real directory
await fs.unlink(realFile)
await fs.rmdir(realDirectory)
let errcode = ''
try {
await fs.stat(symlinkDirectory)
} catch (err) {
errcode = err.code
}
expect(errcode).toBe('ENOENT')
// lstat shouldn't throw
await fs.lstat(symlinkDirectory)
// remove the symlink directory
await io.rmRF(symlinkDirectory)
errcode = ''
try {
await fs.lstat(symlinkDirectory)
} catch (err) {
errcode = err.code
}
expect(errcode).toBe('ENOENT')
})
it('removes symlink level 2 folder with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// symlink_directory -> real_directory
// symlink_level_2_directory -> symlink_directory
const root: string = path.join(
getTestTemp(),
'rmRF_sym_level_2_directory_test'
)
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(realDirectory, 'real_file')
const symlinkDirectory: string = path.join(root, 'symlink_directory')
const symlinkLevel2Directory: string = path.join(
root,
'symlink_level_2_directory'
)
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await createSymlinkDir(realDirectory, symlinkDirectory)
await createSymlinkDir(symlinkDirectory, symlinkLevel2Directory)
expect(
await fs.readFile(path.join(symlinkDirectory, 'real_file'), {
encoding: 'utf8'
})
).toBe('test file content')
if (os.platform() === 'win32') {
expect(await fs.readlink(symlinkLevel2Directory)).toBe(
`${symlinkDirectory}\\`
)
} else {
expect(await fs.readlink(symlinkLevel2Directory)).toBe(symlinkDirectory)
}
await io.rmRF(symlinkLevel2Directory)
await assertExists(path.join(symlinkDirectory, 'real_file'))
await assertNotExists(symlinkLevel2Directory)
})
it('removes nested symlink folder with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// outer_directory
// outer_directory/symlink_directory -> real_directory
const root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test')
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const outerDirectory: string = path.join(root, 'outer_directory')
const symlinkDirectory: string = path.join(
root,
'outer_directory',
'symlink_directory'
)
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await io.mkdirP(outerDirectory)
await createSymlinkDir(realDirectory, symlinkDirectory)
await assertExists(path.join(symlinkDirectory, 'real_file'))
await io.rmRF(outerDirectory)
await assertExists(realDirectory)
await assertExists(realFile)
await assertNotExists(symlinkDirectory)
await assertNotExists(outerDirectory)
})
it('removes deeply nested symlink folder with rmRF', async () => {
// create the following layout:
// real_directory
// real_directory/real_file
// outer_directory
// outer_directory/nested_directory
// outer_directory/nested_directory/symlink_directory -> real_directory
const root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test')
const realDirectory: string = path.join(root, 'real_directory')
const realFile: string = path.join(root, 'real_directory', 'real_file')
const outerDirectory: string = path.join(root, 'outer_directory')
const nestedDirectory: string = path.join(
root,
'outer_directory',
'nested_directory'
)
const symlinkDirectory: string = path.join(
root,
'outer_directory',
'nested_directory',
'symlink_directory'
)
await io.mkdirP(realDirectory)
await fs.writeFile(realFile, 'test file content')
await io.mkdirP(nestedDirectory)
await createSymlinkDir(realDirectory, symlinkDirectory)
await assertExists(path.join(symlinkDirectory, 'real_file'))
await io.rmRF(outerDirectory)
await assertExists(realDirectory)
await assertExists(realFile)
await assertNotExists(symlinkDirectory)
await assertNotExists(outerDirectory)
})
it('removes hidden file with rmRF', async () => {
const file: string = path.join(getTestTemp(), '.rmRF_file')
await io.mkdirP(path.dirname(file))
await createHiddenFile(file, 'test file content')
await assertExists(file)
await io.rmRF(file)
await assertNotExists(file)
})
})
describe('mkdirP', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('fails when called with an empty path', async () => {
expect.assertions(1)
try {
await io.mkdirP('')
} catch (err) {
expect(err.message).toEqual('a path argument must be provided')
}
})
it('creates folder', async () => {
const testPath = path.join(getTestTemp(), 'mkdirTest')
await io.mkdirP(testPath)
await assertExists(testPath)
})
it('creates nested folders with mkdirP', async () => {
const testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2')
await io.mkdirP(testPath)
await assertExists(testPath)
})
it('fails if mkdirP with illegal chars', async () => {
const testPath = path.join(getTestTemp(), 'mkdir\0')
let worked = false
try {
await io.mkdirP(testPath)
worked = true
} catch (err) {
await expect(fs.stat(testPath)).rejects.toHaveProperty(
'code',
'ERR_INVALID_ARG_VALUE'
)
}
expect(worked).toBe(false)
})
it('fails if mkdirP with conflicting file path', async () => {
const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path')
await io.mkdirP(getTestTemp())
await fs.writeFile(testPath, '')
let worked: boolean
try {
await io.mkdirP(testPath)
worked = true
} catch (err) {
worked = false
}
expect(worked).toBe(false)
})
it('fails if mkdirP with conflicting parent file path', async () => {
const testPath = path.join(
getTestTemp(),
'mkdirP_conflicting_parent_file_path',
'dir'
)
await io.mkdirP(getTestTemp())
await fs.writeFile(path.dirname(testPath), '')
let worked: boolean
try {
await io.mkdirP(testPath)
worked = true
} catch (err) {
worked = false
}
expect(worked).toBe(false)
})
it('no-ops if mkdirP directory exists', async () => {
const testPath = path.join(getTestTemp(), 'mkdirP_dir_exists')
await io.mkdirP(testPath)
await assertExists(testPath)
// Calling again shouldn't throw
await io.mkdirP(testPath)
await assertExists(testPath)
})
it('no-ops if mkdirP with symlink directory', async () => {
// create the following layout:
// real_dir
// real_dir/file.txt
// symlink_dir -> real_dir
const rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir')
const realDirPath = path.join(rootPath, 'real_dir')
const realFilePath = path.join(realDirPath, 'file.txt')
const symlinkDirPath = path.join(rootPath, 'symlink_dir')
await io.mkdirP(getTestTemp())
await fs.mkdir(rootPath)
await fs.mkdir(realDirPath)
await fs.writeFile(realFilePath, 'test real_dir/file.txt content')
await createSymlinkDir(realDirPath, symlinkDirPath)
await io.mkdirP(symlinkDirPath)
// the file in the real directory should still be accessible via the symlink
expect((await fs.lstat(symlinkDirPath)).isSymbolicLink()).toBe(true)
expect(
(await fs.stat(path.join(symlinkDirPath, 'file.txt'))).isFile()
).toBe(true)
})
it('no-ops if mkdirP with parent symlink directory', async () => {
// create the following layout:
// real_dir
// real_dir/file.txt
// symlink_dir -> real_dir
const rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir')
const realDirPath = path.join(rootPath, 'real_dir')
const realFilePath = path.join(realDirPath, 'file.txt')
const symlinkDirPath = path.join(rootPath, 'symlink_dir')
await io.mkdirP(getTestTemp())
await fs.mkdir(rootPath)
await fs.mkdir(realDirPath)
await fs.writeFile(realFilePath, 'test real_dir/file.txt content')
await createSymlinkDir(realDirPath, symlinkDirPath)
const subDirPath = path.join(symlinkDirPath, 'sub_dir')
await io.mkdirP(subDirPath)
// the subdirectory should be accessible via the real directory
expect(
(await fs.lstat(path.join(realDirPath, 'sub_dir'))).isDirectory()
).toBe(true)
})
})
describe('which', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('which() finds file name', async () => {
// create a executable file
const testPath = path.join(getTestTemp(), 'which-finds-file-name')
await io.mkdirP(testPath)
let fileName = 'Which-Test-File'
if (process.platform === 'win32') {
fileName += '.exe'
}
const filePath = path.join(testPath, fileName)
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
const originalPath = process.env['PATH']
try {
// update the PATH
process.env['PATH'] = `${process.env['PATH']}${path.delimiter}${testPath}`
// exact file name
expect(await io.which(fileName)).toBe(filePath)
expect(await io.which(fileName, false)).toBe(filePath)
expect(await io.which(fileName, true)).toBe(filePath)
if (process.platform === 'win32') {
// not case sensitive on windows
expect(await io.which('which-test-file.exe')).toBe(
path.join(testPath, 'which-test-file.exe')
)
expect(await io.which('WHICH-TEST-FILE.EXE')).toBe(
path.join(testPath, 'WHICH-TEST-FILE.EXE')
)
expect(await io.which('WHICH-TEST-FILE.EXE', false)).toBe(
path.join(testPath, 'WHICH-TEST-FILE.EXE')
)
expect(await io.which('WHICH-TEST-FILE.EXE', true)).toBe(
path.join(testPath, 'WHICH-TEST-FILE.EXE')
)
// without extension
expect(await io.which('which-test-file')).toBe(filePath)
expect(await io.which('which-test-file', false)).toBe(filePath)
expect(await io.which('which-test-file', true)).toBe(filePath)
} else if (process.platform === 'darwin') {
// not case sensitive on Mac
expect(await io.which(fileName.toUpperCase())).toBe(
path.join(testPath, fileName.toUpperCase())
)
expect(await io.which(fileName.toUpperCase(), false)).toBe(
path.join(testPath, fileName.toUpperCase())
)
expect(await io.which(fileName.toUpperCase(), true)).toBe(
path.join(testPath, fileName.toUpperCase())
)
} else {
// case sensitive on Linux
expect(await io.which(fileName.toUpperCase())).toBe('')
}
} finally {
process.env['PATH'] = originalPath
}
})
it('which() not found', async () => {
expect(await io.which('which-test-no-such-file')).toBe('')
expect(await io.which('which-test-no-such-file', false)).toBe('')
await expect(
io.which('which-test-no-such-file', true)
).rejects.toBeDefined()
})
it('which() searches path in order', async () => {
// create a chcp.com/bash override file
const testPath = path.join(getTestTemp(), 'which-searches-path-in-order')
await io.mkdirP(testPath)
let fileName
if (process.platform === 'win32') {
fileName = 'chcp.com'
} else {
fileName = 'bash'
}
const filePath = path.join(testPath, fileName)
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
const originalPath = process.env['PATH']
try {
// sanity - regular chcp.com/bash should be found
const originalWhich = await io.which(fileName)
expect(!!(originalWhich || '')).toBe(true)
// modify PATH
process.env['PATH'] = [testPath, process.env.PATH].join(path.delimiter)
// override chcp.com/bash should be found
expect(await io.which(fileName)).toBe(filePath)
} finally {
process.env['PATH'] = originalPath
}
})
it('which() requires executable', async () => {
// create a non-executable file
// on Windows, should not end in valid PATHEXT
// on Mac/Linux should not have executable bit
const testPath = path.join(getTestTemp(), 'which-requires-executable')
await io.mkdirP(testPath)
let fileName = 'Which-Test-File'
if (process.platform === 'win32') {
fileName += '.abc' // not a valid PATHEXT
}
const filePath = path.join(testPath, fileName)
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '-x')
}
const originalPath = process.env['PATH']
try {
// modify PATH
process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter)
// should not be found
expect(await io.which(fileName)).toBe('')
} finally {
process.env['PATH'] = originalPath
}
})
// which permissions tests
it('which() finds executable with different permissions', async () => {
await findsExecutableWithScopedPermissions('u=rwx,g=r,o=r')
await findsExecutableWithScopedPermissions('u=rw,g=rx,o=r')
await findsExecutableWithScopedPermissions('u=rw,g=r,o=rx')
})
it('which() ignores directory match', async () => {
// create a directory
const testPath = path.join(getTestTemp(), 'which-ignores-directory-match')
let dirPath = path.join(testPath, 'Which-Test-Dir')
if (process.platform === 'win32') {
dirPath += '.exe'
}
await io.mkdirP(dirPath)
if (process.platform !== 'win32') {
chmod(dirPath, '+x')
}
const originalPath = process.env['PATH']
try {
// modify PATH
process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter)
// should not be found
expect(await io.which(path.basename(dirPath))).toBe('')
} finally {
process.env['PATH'] = originalPath
}
})
it('which() allows rooted path', async () => {
// create an executable file
const testPath = path.join(getTestTemp(), 'which-allows-rooted-path')
await io.mkdirP(testPath)
let filePath = path.join(testPath, 'Which-Test-File')
if (process.platform === 'win32') {
filePath += '.exe'
}
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
// which the full path
expect(await io.which(filePath)).toBe(filePath)
expect(await io.which(filePath, false)).toBe(filePath)
expect(await io.which(filePath, true)).toBe(filePath)
})
it('which() requires rooted path to be executable', async () => {
// create a non-executable file
// on Windows, should not end in valid PATHEXT
// on Mac/Linux, should not have executable bit
const testPath = path.join(
getTestTemp(),
'which-requires-rooted-path-to-be-executable'
)
await io.mkdirP(testPath)
let filePath = path.join(testPath, 'Which-Test-File')
if (process.platform === 'win32') {
filePath += '.abc' // not a valid PATHEXT
}
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '-x')
}
// should not be found
expect(await io.which(filePath)).toBe('')
expect(await io.which(filePath, false)).toBe('')
let failed = false
try {
await io.which(filePath, true)
} catch (err) {
failed = true
}
expect(failed).toBe(true)
})
it('which() requires rooted path to be a file', async () => {
// create a dir
const testPath = path.join(
getTestTemp(),
'which-requires-rooted-path-to-be-executable'
)
let dirPath = path.join(testPath, 'Which-Test-Dir')
if (process.platform === 'win32') {
dirPath += '.exe'
}
await io.mkdirP(dirPath)
if (process.platform !== 'win32') {
chmod(dirPath, '+x')
}
// should not be found
expect(await io.which(dirPath)).toBe('')
expect(await io.which(dirPath, false)).toBe('')
let failed = false
try {
await io.which(dirPath, true)
} catch (err) {
failed = true
}
expect(failed).toBe(true)
})
it('which() requires rooted path to exist', async () => {
let filePath = path.join(__dirname, 'no-such-file')
if (process.platform === 'win32') {
filePath += '.exe'
}
expect(await io.which(filePath)).toBe('')
expect(await io.which(filePath, false)).toBe('')
let failed = false
try {
await io.which(filePath, true)
} catch (err) {
failed = true
}
expect(failed).toBe(true)
})
it('which() does not allow separators', async () => {
// create an executable file
const testDirName = 'which-does-not-allow-separators'
const testPath = path.join(getTestTemp(), testDirName)
await io.mkdirP(testPath)
let fileName = 'Which-Test-File'
if (process.platform === 'win32') {
fileName += '.exe'
}
const filePath = path.join(testPath, fileName)
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
const originalPath = process.env['PATH']
try {
// modify PATH
process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter)
// which "dir/file", should not be found
expect(await io.which(`${testDirName}/${fileName}`)).toBe('')
// on Windows, also try "dir\file"
if (process.platform === 'win32') {
expect(await io.which(`${testDirName}\\${fileName}`)).toBe('')
}
} finally {
process.env['PATH'] = originalPath
}
})
if (process.platform === 'win32') {
it('which() resolves actual case file name when extension is applied', async () => {
const comspec: string = process.env['ComSpec'] || ''
expect(!!comspec).toBe(true)
expect(await io.which('CmD.eXe')).toBe(
path.join(path.dirname(comspec), 'CmD.eXe')
)
expect(await io.which('CmD')).toBe(comspec)
})
it('which() appends ext on windows', async () => {
// create executable files
const testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows')
await io.mkdirP(testPath)
// PATHEXT=.COM;.EXE;.BAT;.CMD...
const files: {[key: string]: string} = {
'which-test-file-1': path.join(testPath, 'which-test-file-1.com'),
'which-test-file-2': path.join(testPath, 'which-test-file-2.exe'),
'which-test-file-3': path.join(testPath, 'which-test-file-3.bat'),
'which-test-file-4': path.join(testPath, 'which-test-file-4.cmd'),
'which-test-file-5.txt': path.join(
testPath,
'which-test-file-5.txt.com'
)
}
for (const fileName of Object.keys(files)) {
await fs.writeFile(files[fileName], '')
}
const originalPath = process.env['PATH']
try {
// modify PATH
process.env[
'PATH'
] = `${process.env['PATH']}${path.delimiter}${testPath}`
// find each file
for (const fileName of Object.keys(files)) {
expect(await io.which(fileName)).toBe(files[fileName])
}
} finally {
process.env['PATH'] = originalPath
}
})
it('which() appends ext on windows when rooted', async () => {
// create executable files
const testPath = path.join(
getTestTemp(),
'which-appends-ext-on-windows-when-rooted'
)
await io.mkdirP(testPath)
// PATHEXT=.COM;.EXE;.BAT;.CMD...
const files: {[key: string]: string} = {}
files[path.join(testPath, 'which-test-file-1')] = path.join(
testPath,
'which-test-file-1.com'
)
files[path.join(testPath, 'which-test-file-2')] = path.join(
testPath,
'which-test-file-2.exe'
)
files[path.join(testPath, 'which-test-file-3')] = path.join(
testPath,
'which-test-file-3.bat'
)
files[path.join(testPath, 'which-test-file-4')] = path.join(
testPath,
'which-test-file-4.cmd'
)
files[path.join(testPath, 'which-test-file-5.txt')] = path.join(
testPath,
'which-test-file-5.txt.com'
)
for (const fileName of Object.keys(files)) {
await fs.writeFile(files[fileName], '')
}
// find each file
for (const fileName of Object.keys(files)) {
expect(await io.which(fileName)).toBe(files[fileName])
}
})
it('which() prefer exact match on windows', async () => {
// create two executable files:
// which-test-file.bat
// which-test-file.bat.exe
//
// verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe"
//
// preference, within the same dir, should be given to the exact match (even though
// .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...)
const testPath = path.join(
getTestTemp(),
'which-prefer-exact-match-on-windows'
)
await io.mkdirP(testPath)
const fileName = 'which-test-file.bat'
const expectedFilePath = path.join(testPath, fileName)
const notExpectedFilePath = path.join(testPath, `${fileName}.exe`)
await fs.writeFile(expectedFilePath, '')
await fs.writeFile(notExpectedFilePath, '')
const originalPath = process.env['PATH']
try {
process.env[
'PATH'
] = `${process.env['PATH']}${path.delimiter}${testPath}`
expect(await io.which(fileName)).toBe(expectedFilePath)
} finally {
process.env['PATH'] = originalPath
}
})
it('which() prefer exact match on windows when rooted', async () => {
// create two executable files:
// which-test-file.bat
// which-test-file.bat.exe
//
// verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe"
//
// preference, within the same dir, should be given to the exact match (even though
// .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...)
const testPath = path.join(
getTestTemp(),
'which-prefer-exact-match-on-windows-when-rooted'
)
await io.mkdirP(testPath)
const fileName = 'which-test-file.bat'
const expectedFilePath = path.join(testPath, fileName)
const notExpectedFilePath = path.join(testPath, `${fileName}.exe`)
await fs.writeFile(expectedFilePath, '')
await fs.writeFile(notExpectedFilePath, '')
expect(await io.which(path.join(testPath, fileName))).toBe(
expectedFilePath
)
})
it('which() searches ext in order', async () => {
const testPath = path.join(getTestTemp(), 'which-searches-ext-in-order')
// create a directory for testing .COM order preference
// PATHEXT=.COM;.EXE;.BAT;.CMD...
const fileNameWithoutExtension = 'which-test-file'
const comTestPath = path.join(testPath, 'com-test')
await io.mkdirP(comTestPath)
await fs.writeFile(
path.join(comTestPath, `${fileNameWithoutExtension}.com`),
''
)
await fs.writeFile(
path.join(comTestPath, `${fileNameWithoutExtension}.exe`),
''
)
await fs.writeFile(
path.join(comTestPath, `${fileNameWithoutExtension}.bat`),
''
)
await fs.writeFile(
path.join(comTestPath, `${fileNameWithoutExtension}.cmd`),
''
)
// create a directory for testing .EXE order preference
// PATHEXT=.COM;.EXE;.BAT;.CMD...
const exeTestPath = path.join(testPath, 'exe-test')
await io.mkdirP(exeTestPath)
await fs.writeFile(
path.join(exeTestPath, `${fileNameWithoutExtension}.exe`),
''
)
await fs.writeFile(
path.join(exeTestPath, `${fileNameWithoutExtension}.bat`),
''
)
await fs.writeFile(
path.join(exeTestPath, `${fileNameWithoutExtension}.cmd`),
''
)
// create a directory for testing .BAT order preference
// PATHEXT=.COM;.EXE;.BAT;.CMD...
const batTestPath = path.join(testPath, 'bat-test')
await io.mkdirP(batTestPath)
await fs.writeFile(
path.join(batTestPath, `${fileNameWithoutExtension}.bat`),
''
)
await fs.writeFile(
path.join(batTestPath, `${fileNameWithoutExtension}.cmd`),
''
)
// create a directory for testing .CMD
const cmdTestPath = path.join(testPath, 'cmd-test')
await io.mkdirP(cmdTestPath)
const cmdFilePath = path.join(
cmdTestPath,
`${fileNameWithoutExtension}.cmd`
)
await fs.writeFile(cmdFilePath, '')
const originalPath = process.env['PATH']
try {
// test .COM
process.env['PATH'] = `${comTestPath}${path.delimiter}${originalPath}`
expect(await io.which(fileNameWithoutExtension)).toBe(
path.join(comTestPath, `${fileNameWithoutExtension}.com`)
)
// test .EXE
process.env['PATH'] = `${exeTestPath}${path.delimiter}${originalPath}`
expect(await io.which(fileNameWithoutExtension)).toBe(
path.join(exeTestPath, `${fileNameWithoutExtension}.exe`)
)
// test .BAT
process.env['PATH'] = `${batTestPath}${path.delimiter}${originalPath}`
expect(await io.which(fileNameWithoutExtension)).toBe(
path.join(batTestPath, `${fileNameWithoutExtension}.bat`)
)
// test .CMD
process.env['PATH'] = `${cmdTestPath}${path.delimiter}${originalPath}`
expect(await io.which(fileNameWithoutExtension)).toBe(
path.join(cmdTestPath, `${fileNameWithoutExtension}.cmd`)
)
} finally {
process.env['PATH'] = originalPath
}
})
}
})
describe('findInPath', () => {
beforeAll(async () => {
await io.rmRF(getTestTemp())
})
it('findInPath() not found', async () => {
expect(await io.findInPath('findInPath-test-no-such-file')).toEqual([])
})
it('findInPath() finds file names', async () => {
// create executable files
let fileName = 'FindInPath-Test-File'
if (process.platform === 'win32') {
fileName += '.exe'
}
const testPaths = ['1', '2', '3'].map(count =>
path.join(getTestTemp(), `findInPath-finds-file-names-${count}`)
)
for (const testPath of testPaths) {
await io.mkdirP(testPath)
}
const filePaths = testPaths.map(testPath => path.join(testPath, fileName))
for (const filePath of filePaths) {
await fs.writeFile(filePath, '')
if (process.platform !== 'win32') {
chmod(filePath, '+x')
}
}
const originalPath = process.env['PATH']
try {
// update the PATH
for (const testPath of testPaths) {
process.env[
'PATH'
] = `${process.env['PATH']}${path.delimiter}${testPath}`
}
// exact file names
expect(await io.findInPath(fileName)).toEqual(filePaths)
} finally {
process.env['PATH'] = originalPath
}
})
})
async function findsExecutableWithScopedPermissions(
chmodOptions: string
): Promise<void> {
// create a executable file
const testPath = path.join(getTestTemp(), 'which-finds-file-name')
await io.mkdirP(testPath)
const fileName = 'Which-Test-File'
if (process.platform === 'win32') {
return
}
const filePath = path.join(testPath, fileName)
await fs.writeFile(filePath, '')
chmod(filePath, chmodOptions)
const originalPath = process.env['PATH']
try {
// update the PATH
process.env['PATH'] = `${process.env['PATH']}${path.delimiter}${testPath}`
// exact file name
expect(await io.which(fileName)).toBe(filePath)
expect(await io.which(fileName, false)).toBe(filePath)
expect(await io.which(fileName, true)).toBe(filePath)
if (process.platform === 'darwin') {
// not case sensitive on Mac
expect(await io.which(fileName.toUpperCase())).toBe(
path.join(testPath, fileName.toUpperCase())
)
expect(await io.which(fileName.toUpperCase(), false)).toBe(
path.join(testPath, fileName.toUpperCase())
)
expect(await io.which(fileName.toUpperCase(), true)).toBe(
path.join(testPath, fileName.toUpperCase())
)
} else {
// case sensitive on Linux
expect(await io.which(fileName.toUpperCase())).toBe('')
}
} finally {
process.env['PATH'] = originalPath
}
}
// Assert that a file exists
async function assertExists(filePath: string): Promise<void> {
expect(await fs.stat(filePath)).toBeDefined()
}
// Assert that reading a file raises an ENOENT error (does not exist)
async function assertNotExists(filePath: string): Promise<void> {
await expect(fs.stat(filePath)).rejects.toHaveProperty('code', 'ENOENT')
}
function chmod(file: string, mode: string): void {
const result = child.spawnSync('chmod', [mode, file])
if (result.status !== 0) {
const message: string = (result.output || []).join(' ').trim()
throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`)
}
}
async function createHiddenDirectory(dir: string): Promise<void> {
if (!path.basename(dir).match(/^\./)) {
throw new Error(`Expected dir '${dir}' to start with '.'.`)
}
await io.mkdirP(dir)
if (os.platform() === 'win32') {
const result = child.spawnSync('attrib.exe', ['+H', dir])
if (result.status !== 0) {
const message: string = (result.output || []).join(' ').trim()
throw new Error(
`Failed to set hidden attribute for directory '${dir}'. ${message}`
)
}
}
}
async function createHiddenFile(file: string, content: string): Promise<void> {
if (!path.basename(file).match(/^\./)) {
throw new Error(`Expected dir '${file}' to start with '.'.`)
}
await io.mkdirP(path.dirname(file))
await fs.writeFile(file, content)
if (os.platform() === 'win32') {
const result = child.spawnSync('attrib.exe', ['+H', file])
if (result.status !== 0) {
const message: string = (result.output || []).join(' ').trim()
throw new Error(
`Failed to set hidden attribute for file '${file}'. ${message}`
)
}
}
}
function getTestTemp(): string {
return path.join(__dirname, '_temp')
}
/**
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
* A symlink directory is not created on Windows since it requires an elevated context.
*/
async function createSymlinkDir(real: string, link: string): Promise<void> {
if (os.platform() === 'win32') {
await fs.symlink(real, link, 'junction')
} else {
await fs.symlink(real, link)
}
}