diff --git a/src/browser/provider/built-in/chrome/create-temp-profile.js b/src/browser/provider/built-in/chrome/create-temp-profile.js index 63efba2d6b3..073a82a10ce 100644 --- a/src/browser/provider/built-in/chrome/create-temp-profile.js +++ b/src/browser/provider/built-in/chrome/create-temp-profile.js @@ -1,13 +1,11 @@ import path from 'path'; -import tmp from 'tmp'; +import TempDirectory from '../../../../utils/temp-directory'; import { writeFile, ensureDir } from '../../../../utils/promisified-functions'; export default async function (proxyHostName) { - tmp.setGracefulCleanup(); - - const tempDir = tmp.dirSync({ unsafeCleanup: true }); - const profileDirName = path.join(tempDir.name, 'Default'); + const tempDir = await TempDirectory.createDirectory('chrome-profile'); + const profileDirName = path.join(tempDir.path, 'Default'); await ensureDir(profileDirName); @@ -39,7 +37,7 @@ export default async function (proxyHostName) { }; await writeFile(path.join(profileDirName, 'Preferences'), JSON.stringify(preferences)); - await writeFile(path.join(tempDir.name, 'First Run'), ''); + await writeFile(path.join(tempDir.path, 'First Run'), ''); return tempDir; } diff --git a/src/browser/provider/built-in/chrome/index.js b/src/browser/provider/built-in/chrome/index.js index 12a4ca4c620..cfd0cc3ed3b 100644 --- a/src/browser/provider/built-in/chrome/index.js +++ b/src/browser/provider/built-in/chrome/index.js @@ -46,6 +46,9 @@ export default { if (OS.mac || runtimeInfo.config.headless) await stopLocalChrome(runtimeInfo); + if (runtimeInfo.tempProfileDir) + await runtimeInfo.tempProfileDir.dispose(); + delete this.openedBrowsers[browserId]; }, diff --git a/src/browser/provider/built-in/chrome/local-chrome.js b/src/browser/provider/built-in/chrome/local-chrome.js index 62649cbf8ad..9a652375c6d 100644 --- a/src/browser/provider/built-in/chrome/local-chrome.js +++ b/src/browser/provider/built-in/chrome/local-chrome.js @@ -1,5 +1,5 @@ import browserTools from 'testcafe-browser-tools'; -import killBrowserProcess from '../../utils/kill-browser-process'; +import killBrowserProcess from '../../../../utils/kill-browser-process'; import BrowserStarter from '../../utils/browser-starter'; @@ -9,7 +9,7 @@ function buildChromeArgs (config, cdpPort, platformArgs, profileDir) { return [] .concat( cdpPort ? [`--remote-debugging-port=${cdpPort}`] : [], - !config.userProfile ? [`--user-data-dir=${profileDir.name}`] : [], + !config.userProfile ? [`--user-data-dir=${profileDir.path}`] : [], config.headless ? ['--headless'] : [], config.userArgs ? [config.userArgs] : [], platformArgs ? [platformArgs] : [] diff --git a/src/browser/provider/built-in/chrome/runtime-info.js b/src/browser/provider/built-in/chrome/runtime-info.js index ab291b099f1..cb68ac36940 100644 --- a/src/browser/provider/built-in/chrome/runtime-info.js +++ b/src/browser/provider/built-in/chrome/runtime-info.js @@ -3,26 +3,10 @@ import getConfig from './config'; import createTempProfile from './create-temp-profile'; -var commonTempProfile = null; - -async function getTempProfile (proxyHostName, config) { - - var tempProfile = commonTempProfile; - var shouldUseCommonProfile = !config.headless && !config.emulation; - - if (!shouldUseCommonProfile || !commonTempProfile) - tempProfile = await createTempProfile(proxyHostName); - - if (shouldUseCommonProfile && !commonTempProfile) - commonTempProfile = tempProfile; - - return tempProfile; -} - export default async function (proxyHostName, configString) { - var config = getConfig(configString); - var tempProfileDir = !config.userProfile ? await getTempProfile(proxyHostName, config) : null; - var cdpPort = config.cdpPort || (!config.userProfile ? await getFreePort() : null); + const config = getConfig(configString); + const tempProfileDir = !config.userProfile ? await createTempProfile(proxyHostName, config) : null; + const cdpPort = config.cdpPort || (!config.userProfile ? await getFreePort() : null); return { config, cdpPort, tempProfileDir }; } diff --git a/src/browser/provider/built-in/firefox/create-temp-profile.js b/src/browser/provider/built-in/firefox/create-temp-profile.js index c30e7f885a2..5d5283b961b 100644 --- a/src/browser/provider/built-in/firefox/create-temp-profile.js +++ b/src/browser/provider/built-in/firefox/create-temp-profile.js @@ -1,5 +1,5 @@ import path from 'path'; -import tmp from 'tmp'; +import TempDirectory from '../../../../utils/temp-directory'; import { writeFile } from '../../../../utils/promisified-functions'; @@ -54,11 +54,9 @@ async function generatePreferences (profileDir, { marionettePort, config }) { } export default async function (runtimeInfo) { - tmp.setGracefulCleanup(); + const tmpDir = await TempDirectory.createDirectory('firefox-profile'); - const tmpDir = tmp.dirSync({ unsafeCleanup: true }); - - await generatePreferences(tmpDir.name, runtimeInfo); + await generatePreferences(tmpDir.path, runtimeInfo); return tmpDir; } diff --git a/src/browser/provider/built-in/firefox/index.js b/src/browser/provider/built-in/firefox/index.js index bbb3aa1c654..92fed99c99d 100644 --- a/src/browser/provider/built-in/firefox/index.js +++ b/src/browser/provider/built-in/firefox/index.js @@ -53,6 +53,9 @@ export default { if (OS.mac && !config.headless) await stopLocalFirefox(runtimeInfo); + if (runtimeInfo.tempProfileDir) + await runtimeInfo.tempProfileDir.dispose(); + delete this.openedBrowsers[browserId]; }, diff --git a/src/browser/provider/built-in/firefox/local-firefox.js b/src/browser/provider/built-in/firefox/local-firefox.js index 75aff2cb823..a480fcdce11 100644 --- a/src/browser/provider/built-in/firefox/local-firefox.js +++ b/src/browser/provider/built-in/firefox/local-firefox.js @@ -1,6 +1,6 @@ import OS from 'os-family'; import browserTools from 'testcafe-browser-tools'; -import killBrowserProcess from '../../utils/kill-browser-process'; +import killBrowserProcess from '../../../../utils/kill-browser-process'; import BrowserStarter from '../../utils/browser-starter'; @@ -18,7 +18,7 @@ function buildFirefoxArgs (config, platformArgs, { marionettePort, tempProfileDi return [] .concat( marionettePort ? ['-marionette'] : [], - !config.userProfile ? ['-no-remote', '-new-instance', `-profile "${tempProfileDir.name}"`] : [], + !config.userProfile ? ['-no-remote', '-new-instance', `-profile "${tempProfileDir.path}"`] : [], config.headless ? ['-headless'] : [], config.userArgs ? [config.userArgs] : [], platformArgs ? [platformArgs] : [] diff --git a/src/browser/provider/utils/kill-browser-process.js b/src/browser/provider/utils/kill-browser-process.js deleted file mode 100644 index 563d02317e7..00000000000 --- a/src/browser/provider/utils/kill-browser-process.js +++ /dev/null @@ -1,38 +0,0 @@ -import OS from 'os-family'; -import { findProcess, killProcess, exec } from '../../../utils/promisified-functions'; - - -const BROWSER_CLOSING_TIMEOUT = 5; - -async function findProcessWin (processOptions) { - var cmd = `wmic process where "commandline like '%${processOptions.arguments}%' and name <> 'cmd.exe' and name <> 'wmic.exe'" get processid`; - var wmicOutput = await exec(cmd); - var processList = wmicOutput.split(/\s*\n/); - - processList = processList - // NOTE: remove list's header and empty last element, caused by trailing newline - .slice(1, -1) - .map(pid => ({ pid: Number(pid) })); - - return processList; -} - -export default async function (browserId) { - var processOptions = { arguments: browserId, psargs: 'aux' }; - var processList = OS.win ? await findProcessWin(processOptions) : await findProcess(processOptions); - - if (!processList.length) - return true; - - try { - if (OS.win) - process.kill(processList[0].pid); - else - await killProcess(processList[0].pid, { timeout: BROWSER_CLOSING_TIMEOUT }); - - return true; - } - catch (e) { - return false; - } -} diff --git a/src/utils/kill-browser-process.js b/src/utils/kill-browser-process.js new file mode 100644 index 00000000000..d1171619237 --- /dev/null +++ b/src/utils/kill-browser-process.js @@ -0,0 +1,63 @@ +import { spawn } from 'child_process'; +import OS from 'os-family'; +import promisifyEvent from 'promisify-event'; +import Promise from 'pinkie'; +import { findProcess, killProcess } from './promisified-functions'; + + +const BROWSER_CLOSING_TIMEOUT = 5; + +async function runWMIC (args) { + const wmicProcess = spawn('wmic.exe', args, { detached: true }); + + let wmicOutput = ''; + + wmicProcess.stdout.on('data', data => { + wmicOutput += data.toString(); + }); + + try { + await Promise.race([ + promisifyEvent(wmicProcess.stdout, 'end'), + promisifyEvent(wmicProcess, 'error') + ]); + + return wmicOutput; + } + catch (e) { + return ''; + } +} + +async function findProcessWin (processOptions) { + var wmicArgs = ['process', 'where', `commandline like '%${processOptions.arguments}%' and name <> 'cmd.exe' and name <> 'wmic.exe'`, 'get', 'processid']; + var wmicOutput = await runWMIC(wmicArgs); + var processList = wmicOutput.split(/\s*\n/); + + processList = processList + // NOTE: remove list's header and empty last element, caused by trailing newline + .slice(1, -1) + .map(pid => ({ pid: Number(pid) })); + + return processList; +} + +export default async function (browserId) { + var processOptions = { arguments: browserId, psargs: '-ef' }; + var processList = OS.win ? await findProcessWin(processOptions) : await findProcess(processOptions); + + if (!processList.length) + return true; + + try { + if (OS.win) + process.kill(processList[0].pid); + else + await killProcess(processList[0].pid, { timeout: BROWSER_CLOSING_TIMEOUT }); + + return true; + } + catch (e) { + return false; + } +} diff --git a/src/utils/promisified-functions.js b/src/utils/promisified-functions.js index a6aa010a921..5f0095a5281 100644 --- a/src/utils/promisified-functions.js +++ b/src/utils/promisified-functions.js @@ -6,6 +6,7 @@ import promisify from './promisify'; export const ensureDir = promisify(mkdirp); +export const readDir = promisify(fs.readdir); export const stat = promisify(fs.stat); export const writeFile = promisify(fs.writeFile); export const readFile = promisify(fs.readFile); @@ -15,3 +16,5 @@ export const findProcess = promisify(psNode.lookup); export const killProcess = promisify(psNode.kill); export const exec = promisify(childProcess.exec); + +export const sendMessageToChildProcess = promisify((process, ...args) => process.send(...args)); diff --git a/src/utils/temp-directory/cleanup-process/commands.js b/src/utils/temp-directory/cleanup-process/commands.js new file mode 100644 index 00000000000..619f22f5443 --- /dev/null +++ b/src/utils/temp-directory/cleanup-process/commands.js @@ -0,0 +1,5 @@ +export default { + init: 'init', + add: 'add', + remove: 'remove' +}; diff --git a/src/utils/temp-directory/cleanup-process/index.js b/src/utils/temp-directory/cleanup-process/index.js new file mode 100644 index 00000000000..a7b30893b78 --- /dev/null +++ b/src/utils/temp-directory/cleanup-process/index.js @@ -0,0 +1,136 @@ +import { spawn } from 'child_process'; +import debug from 'debug'; +import promisifyEvent from 'promisify-event'; +import Promise from 'pinkie'; +import { sendMessageToChildProcess } from '../../promisified-functions'; +import COMMANDS from './commands'; + + +const WORKER_PATH = require.resolve('./worker'); +const WORKER_STDIO_CONFIG = ['ignore', 'ignore', 'ignore', 'ipc']; + +const DEBUG_LOGGER = debug('testcafe:utils:temp-directory:cleanup-process'); + +class CleanupProcess { + constructor () { + this.worker = null; + this.initialized = false; + this.initPromise = Promise.resolve(void 0); + + this.messageCounter = 0; + + this.pendingResponses = {}; + } + + _sendMessage (id, msg) { + return sendMessageToChildProcess(this.worker, { id, ...msg }); + } + + _onResponse (response) { + const pendingResponse = this.pendingResponses[response.id]; + + if (response.error) { + if (pendingResponse) + pendingResponse.control.reject(response.error); + else + this.pendingResponses[response.id] = Promise.reject(response.error); + } + else if (pendingResponse) + pendingResponse.control.resolve(); + else + this.pendingResponses[response.id] = Promise.resolve(); + } + + async _waitResponse (id) { + if (!this.pendingResponses[id]) { + const promiseControl = {}; + + this.pendingResponses[id] = new Promise((resolve, reject) => { + Object.assign(promiseControl, { resolve, reject }); + }); + + this.pendingResponses[id].control = promiseControl; + } + + try { + await this.pendingResponses[id]; + } + finally { + delete this.pendingResponses[id]; + } + } + + async _waitResponseForMessage (msg) { + const currentId = this.messageCounter; + + this.messageCounter++; + + await this._sendMessage(currentId, msg); + await this._waitResponse(currentId); + } + + init () { + this.initPromise = this.initPromise + .then(async initialized => { + if (initialized !== void 0) + return initialized; + + this.worker = spawn(process.argv[0], [WORKER_PATH], { detached: true, stdio: WORKER_STDIO_CONFIG }); + + this.worker.on('message', message => this._onResponse(message)); + + this.worker.unref(); + + try { + await Promise.race([ + this._waitResponseForMessage({ command: COMMANDS.init }), + promisifyEvent(this.worker, 'error') + ]); + + const channel = this.worker.channel || this.worker._channel; + + channel.unref(); + + this.initialized = true; + } + catch (e) { + DEBUG_LOGGER('Failed to start cleanup process'); + DEBUG_LOGGER(e); + + this.initialized = false; + } + + return this.initialized; + }); + + return this.initPromise; + } + + async addDirectory (path) { + if (!this.initialized) + return; + + try { + await this._waitResponseForMessage({ command: COMMANDS.add, path }); + } + catch (e) { + DEBUG_LOGGER(`Failed to add the ${path} directory to cleanup process`); + DEBUG_LOGGER(e); + } + } + + async removeDirectory (path) { + if (!this.initialized) + return; + + try { + await this._waitResponseForMessage({ command: COMMANDS.remove, path }); + } + catch (e) { + DEBUG_LOGGER(`Failed to remove the ${path} directory in cleanup process`); + DEBUG_LOGGER(e); + } + } +} + +export default new CleanupProcess(); diff --git a/src/utils/temp-directory/cleanup-process/worker.js b/src/utils/temp-directory/cleanup-process/worker.js new file mode 100644 index 00000000000..987eb1191fc --- /dev/null +++ b/src/utils/temp-directory/cleanup-process/worker.js @@ -0,0 +1,72 @@ +import path from 'path'; +import { inspect } from 'util'; +import del from 'del'; +import Promise from 'pinkie'; +import { noop } from 'lodash'; +import killBrowserProcess from '../../kill-browser-process'; +import COMMANDS from './commands'; + + +const DIRECTORIES_TO_CLEANUP = {}; + +function addDirectory (dirPath) { + if (!DIRECTORIES_TO_CLEANUP[dirPath]) + DIRECTORIES_TO_CLEANUP[dirPath] = {}; +} + +async function removeDirectory (dirPath) { + if (!DIRECTORIES_TO_CLEANUP[dirPath]) + return; + + let delPromise = DIRECTORIES_TO_CLEANUP[dirPath].delPromise; + + if (!delPromise) { + delPromise = killBrowserProcess(path.basename(dirPath)) + .then(() => del(dirPath, { force: true })); + + DIRECTORIES_TO_CLEANUP[dirPath].delPromise = delPromise; + } + + await DIRECTORIES_TO_CLEANUP[dirPath].delPromise; + + delete DIRECTORIES_TO_CLEANUP[dirPath].delPromise; +} + +async function dispatchCommand (message) { + switch (message.command) { + case COMMANDS.init: + return; + case COMMANDS.add: + addDirectory(message.path); + return; + case COMMANDS.remove: + addDirectory(message.path); + await removeDirectory(message.path); + return; + } +} + +process.on('message', async message => { + let error = ''; + + try { + await dispatchCommand(message); + } + catch (e) { + error = inspect(e); + } + + process.send({ id: message.id, error }); +}); + +process.on('disconnect', async () => { + const removePromises = Object + .keys(DIRECTORIES_TO_CLEANUP) + .map(dirPath => removeDirectory(dirPath).catch(noop)); + + await Promise.all(removePromises); + + process.exit(0); //eslint-disable-line no-process-exit +}); + + diff --git a/src/utils/temp-directory/index.js b/src/utils/temp-directory/index.js new file mode 100644 index 00000000000..0df94003965 --- /dev/null +++ b/src/utils/temp-directory/index.js @@ -0,0 +1,116 @@ +import debug from 'debug'; +import os from 'os'; +import path from 'path'; +import setupExitHook from 'async-exit-hook'; +import tmp from 'tmp'; +import LockFile from './lockfile'; +import cleanupProcess from './cleanup-process'; +import { ensureDir, readDir } from '../../utils/promisified-functions'; + + +// NOTE: mutable for testing purposes +const TESTCAFE_TMP_DIRS_ROOT = path.join(os.tmpdir(), 'testcafe'); +const DEFAULT_NAME_PREFIX = 'tmp'; +const USED_TEMP_DIRS = {}; +const DEBUG_LOGGER = debug('testcafe:utils:temp-directory'); + +export default class TempDirectory { + constructor (namePrefix) { + this.namePrefix = namePrefix || DEFAULT_NAME_PREFIX; + + this.path = ''; + this.lockFile = null; + } + + async _getTmpDirsList () { + const tmpDirNames = await readDir(TempDirectory.TEMP_DIRECTORIES_ROOT); + + return tmpDirNames + .filter(tmpDir => !USED_TEMP_DIRS[tmpDir]) + .filter(tmpDir => path.basename(tmpDir).startsWith(this.namePrefix)); + } + + async _findFreeTmpDir (tmpDirNames) { + for (const tmpDirName of tmpDirNames) { + const tmpDirPath = path.join(TempDirectory.TEMP_DIRECTORIES_ROOT, tmpDirName); + + const lockFile = new LockFile(tmpDirPath); + + if (lockFile.init()) { + this.path = tmpDirPath; + this.lockFile = lockFile; + + return true; + } + } + + return false; + } + + async _createNewTmpDir () { + this.path = tmp.tmpNameSync({ dir: TempDirectory.TEMP_DIRECTORIES_ROOT, prefix: this.namePrefix + '-' }); + + await ensureDir(this.path); + + this.lockFile = new LockFile(this.path); + + this.lockFile.init(); + } + + _disposeSync () { + if (!USED_TEMP_DIRS[this.path]) + return; + + this.lockFile.dispose(); + + delete USED_TEMP_DIRS[this.path]; + } + + static async createDirectory (prefix) { + const tmpDir = new TempDirectory(prefix); + + await tmpDir.init(); + + return tmpDir; + } + + static disposeDirectoriesSync () { + Object.values(USED_TEMP_DIRS).forEach(tmpDir => tmpDir._disposeSync()); + } + + async init () { + await ensureDir(TempDirectory.TEMP_DIRECTORIES_ROOT); + + const tmpDirNames = await this._getTmpDirsList(this.namePrefix); + + DEBUG_LOGGER('Found temp directories:', tmpDirNames); + + const existingTmpDirFound = await this._findFreeTmpDir(tmpDirNames); + + if (!existingTmpDirFound) + await this._createNewTmpDir(); + + DEBUG_LOGGER('Temp directory path: ', this.path); + + await cleanupProcess.init(); + await cleanupProcess.addDirectory(this.path); + + USED_TEMP_DIRS[this.path] = this; + } + + async dispose () { + if (!USED_TEMP_DIRS[this.path]) + return; + + this.lockFile.dispose(); + + await cleanupProcess.removeDirectory(this.path); + + delete USED_TEMP_DIRS[this.path]; + } +} + +// NOTE: exposed for testing purposes +TempDirectory.TEMP_DIRECTORIES_ROOT = TESTCAFE_TMP_DIRS_ROOT; + +setupExitHook(TempDirectory.disposeDirectoriesSync); diff --git a/src/utils/temp-directory/lockfile.js b/src/utils/temp-directory/lockfile.js new file mode 100644 index 00000000000..fb9857a20ec --- /dev/null +++ b/src/utils/temp-directory/lockfile.js @@ -0,0 +1,64 @@ +import path from 'path'; +import debug from 'debug'; +import fs from 'fs'; + + +const LOCKFILE_NAME = '.testcafe-lockfile'; +const STALE_LOCKFILE_AGE = 2 * 24 * 60 * 60 * 1000; +const DEBUG_LOGGER = debug('testcafe:utils:temp-directory:lockfile'); + +export default class LockFile { + constructor (dirPath) { + this.path = path.join(dirPath, LOCKFILE_NAME); + } + + _open ({ force = false } = {}) { + try { + fs.writeFileSync(this.path, '', { flag: force ? 'w' : 'wx' }); + + return true; + } + catch (e) { + DEBUG_LOGGER('Failed to init lockfile ' + this.path); + DEBUG_LOGGER(e); + + return false; + } + } + + _isStale () { + const currentMs = Date.now(); + + try { + const { mtimeMs } = fs.statSync(this.path); + + return currentMs - mtimeMs > STALE_LOCKFILE_AGE; + } + catch (e) { + DEBUG_LOGGER('Failed to check status of lockfile ' + this.path); + DEBUG_LOGGER(e); + + return false; + } + } + + init () { + if (this._open()) + return true; + + if (this._isStale()) + return this._open({ force: true }); + + return false; + } + + dispose () { + try { + fs.unlinkSync(this.path); + } + catch (e) { + DEBUG_LOGGER('Failed to dispose lockfile ' + this.path); + DEBUG_LOGGER(e); + } + } +} diff --git a/test/functional/assertion-helper.js b/test/functional/assertion-helper.js index 439c31b6e52..5b6291c1e70 100644 --- a/test/functional/assertion-helper.js +++ b/test/functional/assertion-helper.js @@ -311,7 +311,9 @@ exports.isScreenshotsEqual = function (customPath, referenceImagePathGetter) { exports.removeScreenshotDir = function () { if (isDirExists(SCREENSHOTS_PATH)) - del(SCREENSHOTS_PATH); + return del(SCREENSHOTS_PATH); + + return Promise.resolve(); }; exports.SCREENSHOTS_PATH = SCREENSHOTS_PATH; diff --git a/test/server/util-test.js b/test/server/util-test.js index 66138f4f852..097e9ce788e 100644 --- a/test/server/util-test.js +++ b/test/server/util-test.js @@ -1,8 +1,12 @@ const path = require('path'); +const fs = require('fs'); +const del = require('del'); const expect = require('chai').expect; const correctFilePath = require('../../lib/utils/correct-file-path'); const escapeUserAgent = require('../../lib/utils/escape-user-agent'); const parseFileList = require('../../lib/utils/parse-file-list'); +const TempDirectory = require('../../lib/utils/temp-directory'); + describe('Utils', () => { it('Correct File Path', () => { @@ -62,4 +66,58 @@ describe('Utils', () => { }); }); }); + + describe('Temp Directory', function () { + const TMP_ROOT = path.join(process.cwd(), '__tmp__'); + + const savedTmpRoot = TempDirectory.TEMP_DIRECTORIES_ROOT; + + beforeEach(() => { + TempDirectory.TEMP_DIRECTORIES_ROOT = TMP_ROOT; + + return del(TMP_ROOT); + }); + + afterEach(() => { + TempDirectory.TEMP_DIRECTORIES_ROOT = savedTmpRoot; + + return del(TMP_ROOT); + }); + + it('Should reuse existing temp directories after synchronous disposal', function () { + const tempDir1 = new TempDirectory(); + const tempDir2 = new TempDirectory(); + const tempDir3 = new TempDirectory(); + + return tempDir1 + .init() + .then(() => tempDir2.init()) + .then(() => tempDir1._disposeSync()) + .then(() => tempDir3.init()) + .then(() => { + const subDirs = fs.readdirSync(TempDirectory.TEMP_DIRECTORIES_ROOT); + + expect(subDirs.length).eql(2); + expect(tempDir3.path).eql(tempDir1.path); + }); + }); + + it('Should remove temp directories after asynchronous disposal', function () { + const tempDir = new TempDirectory(); + + return tempDir + .init() + .then(() => { + const subDirs = fs.readdirSync(TempDirectory.TEMP_DIRECTORIES_ROOT); + + expect(subDirs.length).eql(1); + }) + .then(() => tempDir.dispose()) + .then(() => { + const subDirs = fs.readdirSync(TempDirectory.TEMP_DIRECTORIES_ROOT); + + expect(subDirs.length).eql(0); + }); + }); + }); });