From d466ab69f8c66344d288f55a319dc9cecf6a7a0c Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Wed, 7 Sep 2022 20:02:18 +1000 Subject: [PATCH] fix: fix bug on utimes, futimes, add support of lutimes lutimes is available on Nodejs v14.5.0+ and v12.19.0, our CI matrix covers both versions. closes #365 --- lib/binding.js | 53 ++- lib/filesystem.js | 48 ++- lib/index.js | 4 +- test/lib/filesystem.spec.js | 11 +- test/lib/fs.utimes-futimes.spec.js | 196 ---------- test/lib/fs.utimes-lutimes-futimes.spec.js | 435 +++++++++++++++++++++ 6 files changed, 535 insertions(+), 212 deletions(-) delete mode 100644 test/lib/fs.utimes-futimes.spec.js create mode 100644 test/lib/fs.utimes-lutimes-futimes.spec.js diff --git a/lib/binding.js b/lib/binding.js index 6801f95..cea5405 100644 --- a/lib/binding.js +++ b/lib/binding.js @@ -7,7 +7,7 @@ const Directory = require('./directory.js'); const SymbolicLink = require('./symlink.js'); const {FSError} = require('./error.js'); const constants = require('constants'); -const getPathParts = require('./filesystem.js').getPathParts; +const {getPathParts, getRealPath} = require('./filesystem.js'); const MODE_TO_KTYPE = { [constants.S_IFREG]: constants.UV_DIRENT_FILE, @@ -260,10 +260,8 @@ Binding.prototype.realpath = function (filepath, encoding, callback, ctx) { throw new FSError('ENOENT', filepath); } - if (process.platform === 'win32' && realPath.startsWith('\\\\?\\')) { - // Remove win32 file namespace prefix \\?\ - realPath = realPath.slice(4); - } + // Remove win32 file namespace prefix \\?\ + realPath = getRealPath(realPath); if (encoding === 'buffer') { realPath = Buffer.from(realPath); @@ -1073,12 +1071,45 @@ Binding.prototype.unlink = function (pathname, callback, ctx) { Binding.prototype.utimes = function (pathname, atime, mtime, callback, ctx) { markSyscall(ctx, 'utimes'); + return maybeCallback(normalizeCallback(callback), ctx, this, function () { + let filepath = deBuffer(pathname); + let item = this._system.getItem(filepath); + let links = 0; + while (item instanceof SymbolicLink) { + if (links > MAX_LINKS) { + throw new FSError('ELOOP', filepath); + } + filepath = path.resolve(path.dirname(filepath), item.getPath()); + item = this._system.getItem(filepath); + ++links; + } + if (!item) { + throw new FSError('ENOENT', pathname); + } + item.setATime(new Date(atime * 1000)); + item.setMTime(new Date(mtime * 1000)); + }); +}; + +/** + * Update timestamps. + * @param {string} pathname Path to item. + * @param {number} atime Access time (in seconds). + * @param {number} mtime Modification time (in seconds). + * @param {function(Error)} callback Optional callback. + * @param {object} ctx Context object (optional), only for nodejs v10+. + * @return {*} The return if no callback is provided. + */ +Binding.prototype.lutimes = function (pathname, atime, mtime, callback, ctx) { + markSyscall(ctx, 'utimes'); + return maybeCallback(normalizeCallback(callback), ctx, this, function () { pathname = deBuffer(pathname); const item = this._system.getItem(pathname); if (!item) { throw new FSError('ENOENT', pathname); } + // lutimes doesn't follow symlink item.setATime(new Date(atime * 1000)); item.setMTime(new Date(mtime * 1000)); }); @@ -1098,7 +1129,17 @@ Binding.prototype.futimes = function (fd, atime, mtime, callback, ctx) { return maybeCallback(normalizeCallback(callback), ctx, this, function () { const descriptor = this.getDescriptorById(fd); - const item = descriptor.getItem(); + let item = descriptor.getItem(); + let filepath = this._system.getFilepath(item); + let links = 0; + while (item instanceof SymbolicLink) { + if (links > MAX_LINKS) { + throw new FSError('ELOOP', filepath); + } + filepath = path.resolve(path.dirname(filepath), item.getPath()); + item = this._system.getItem(filepath); + ++links; + } item.setATime(new Date(atime * 1000)); item.setMTime(new Date(mtime * 1000)); }); diff --git a/lib/filesystem.js b/lib/filesystem.js index 95d7c0e..dd0b431 100644 --- a/lib/filesystem.js +++ b/lib/filesystem.js @@ -9,19 +9,26 @@ const SymbolicLink = require('./symlink.js'); const isWindows = process.platform === 'win32'; -function toNamespacedPath(filePath) { - return path.toNamespacedPath - ? path.toNamespacedPath(filePath) - : path._makeLong(filePath); +// on Win32, change filepath from \\?\c:\a\b to C:\a\b +function getRealPath(filepath) { + if (isWindows && filepath.startsWith('\\\\?\\')) { + // Remove win32 file namespace prefix \\?\ + return filepath[4].toUpperCase() + filepath.slice(5); + } + return filepath; } function getPathParts(filepath) { - const parts = toNamespacedPath(path.resolve(filepath)).split(path.sep); + // path.toNamespacedPath is only for Win32 system. + // on other platform, it returns the path unmodified. + const parts = path.toNamespacedPath(path.resolve(filepath)).split(path.sep); parts.shift(); if (isWindows) { // parts currently looks like ['', '?', 'c:', ...] parts.shift(); const q = parts.shift(); // should be '?' + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#win32-file-namespaces + // Win32 File Namespaces prefix \\?\ const base = '\\\\' + q + '\\' + parts.shift().toLowerCase(); parts.unshift(base); } @@ -130,6 +137,35 @@ FileSystem.prototype.getItem = function (filepath) { return item; }; +function _getFilepath(item, itemPath, wanted) { + if (item === wanted) { + return itemPath; + } + if (item instanceof Directory) { + for (const name of item.list()) { + const got = _getFilepath( + item.getItem(name), + path.join(itemPath, name), + wanted + ); + if (got) { + return got; + } + } + } + return null; +} + +/** + * Get file path from a file system item. + * @param {Item} item a file system item. + * @return {string} file path for the item (or null if not found). + */ +FileSystem.prototype.getFilepath = function (item) { + const namespacedPath = _getFilepath(this._root, isWindows ? '' : '/', item); + return getRealPath(namespacedPath); +}; + /** * Populate a directory with an item. * @param {Directory} directory The directory to populate. @@ -329,4 +365,4 @@ FileSystem.directory = function (config) { module.exports = FileSystem; exports = module.exports; exports.getPathParts = getPathParts; -exports.toNamespacedPath = toNamespacedPath; +exports.getRealPath = getRealPath; diff --git a/lib/index.js b/lib/index.js index 68e92b3..fa3fca8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,8 +13,6 @@ const { } = require('./readfilecontext.js'); const fs = require('fs'); -const toNamespacedPath = FileSystem.toNamespacedPath; - const realProcessProps = { cwd: process.cwd, chdir: process.chdir, @@ -153,7 +151,7 @@ module.exports = function mock(config, options = {}) { }, function chdir(directory) { if (realBinding._mockedBinding) { - if (!fs.statSync(toNamespacedPath(directory)).isDirectory()) { + if (!fs.statSync(path.toNamespacedPath(directory)).isDirectory()) { throw new FSError('ENOTDIR'); } currentPath = path.resolve(currentPath, directory); diff --git a/test/lib/filesystem.spec.js b/test/lib/filesystem.spec.js index 66cfa49..837057d 100644 --- a/test/lib/filesystem.spec.js +++ b/test/lib/filesystem.spec.js @@ -40,7 +40,7 @@ describe('FileSystem', function () { }); }); - describe('#getItem()', function () { + describe('#getItem() #getFilepath()', function () { it('gets an item', function () { const system = FileSystem.create({ 'one/two/three.js': 'contents', @@ -49,6 +49,7 @@ describe('FileSystem', function () { const filepath = path.join('one', 'two', 'three.js'); const item = system.getItem(filepath); assert.instanceOf(item, File); + assert.equal(system.getFilepath(item), path.resolve(filepath)); }); it('returns null if not found', function () { @@ -79,10 +80,18 @@ describe('FileSystem', function () { }); const file = system.getItem(path.join('dir-link', 'a')); assert.instanceOf(file, File); + assert.equal( + system.getFilepath(file), + path.resolve(path.join('b', 'c', 'dir', 'a')) + ); const dir = system.getItem(path.join('dir-link', 'b')); assert.instanceOf(dir, Directory); assert.deepEqual(dir.list().sort(), ['c', 'd']); + assert.equal( + system.getFilepath(dir), + path.resolve(path.join('b', 'c', 'dir', 'b')) + ); }); }); }); diff --git a/test/lib/fs.utimes-futimes.spec.js b/test/lib/fs.utimes-futimes.spec.js deleted file mode 100644 index 464fce6..0000000 --- a/test/lib/fs.utimes-futimes.spec.js +++ /dev/null @@ -1,196 +0,0 @@ -'use strict'; - -const helper = require('../helper.js'); -const fs = require('fs'); -const mock = require('../../lib/index.js'); - -const assert = helper.assert; - -describe('fs.utimes(path, atime, mtime, callback)', function () { - beforeEach(function () { - mock({ - dir: {}, - 'file.txt': 'content', - }); - }); - afterEach(mock.restore); - - it('updates timestamps for a file', function (done) { - fs.utimes('file.txt', new Date(100), new Date(200), function (err) { - if (err) { - return done(err); - } - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - done(); - }); - }); - - it('supports Buffer input', function (done) { - fs.utimes( - Buffer.from('file.txt'), - new Date(100), - new Date(200), - function (err) { - if (err) { - return done(err); - } - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - done(); - } - ); - }); - - it('promise updates timestamps for a file', function (done) { - fs.promises - .utimes('file.txt', new Date(100), new Date(200)) - .then(function () { - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - done(); - }, done); - }); - - it('updates timestamps for a directory', function (done) { - fs.utimes('dir', new Date(300), new Date(400), function (err) { - if (err) { - return done(err); - } - const stats = fs.statSync('dir'); - assert.equal(stats.atime.getTime(), 300); - assert.equal(stats.mtime.getTime(), 400); - done(); - }); - }); - - it('promise updates timestamps for a directory', function (done) { - fs.promises.utimes('dir', new Date(300), new Date(400)).then(function () { - const stats = fs.statSync('dir'); - assert.equal(stats.atime.getTime(), 300); - assert.equal(stats.mtime.getTime(), 400); - done(); - }, done); - }); - - it('fails for a bogus path', function (done) { - fs.utimes('bogus.txt', new Date(100), new Date(200), function (err) { - assert.instanceOf(err, Error); - assert.equal(err.code, 'ENOENT'); - done(); - }); - }); - - it('promise fails for a bogus path', function (done) { - fs.promises.utimes('bogus.txt', new Date(100), new Date(200)).then( - function () { - done(new Error('should not succeed.')); - }, - function (err) { - assert.instanceOf(err, Error); - assert.equal(err.code, 'ENOENT'); - done(); - } - ); - }); -}); - -describe('fs.utimesSync(path, atime, mtime)', function () { - beforeEach(function () { - mock({ - 'file.txt': 'content', - }); - }); - afterEach(mock.restore); - - it('updates timestamps for a file', function () { - fs.utimesSync('file.txt', new Date(100), new Date(200)); - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - }); -}); - -describe('fs.futimes(fd, atime, mtime, callback)', function () { - beforeEach(function () { - mock({ - dir: {}, - 'file.txt': 'content', - }); - }); - afterEach(mock.restore); - - it('updates timestamps for a file', function (done) { - const fd = fs.openSync('file.txt', 'r'); - fs.futimes(fd, new Date(100), new Date(200), function (err) { - if (err) { - return done(err); - } - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - done(); - }); - }); - - it('promise updates timestamps for a file', function (done) { - fs.promises - .open('file.txt', 'r') - .then(function (fd) { - return fd.utimes(new Date(100), new Date(200)); - }) - .then(function () { - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - done(); - }, done); - }); - - it('updates timestamps for a directory', function (done) { - const fd = fs.openSync('dir', 'r'); - fs.futimes(fd, new Date(300), new Date(400), function (err) { - if (err) { - return done(err); - } - const stats = fs.statSync('dir'); - assert.equal(stats.atime.getTime(), 300); - assert.equal(stats.mtime.getTime(), 400); - done(); - }); - }); - - it('promise updates timestamps for a directory', function (done) { - fs.promises - .open('dir', 'r') - .then(function (fd) { - return fd.utimes(new Date(300), new Date(400)); - }) - .then(function () { - const stats = fs.statSync('dir'); - assert.equal(stats.atime.getTime(), 300); - assert.equal(stats.mtime.getTime(), 400); - done(); - }, done); - }); -}); - -describe('fs.futimesSync(path, atime, mtime)', function () { - beforeEach(function () { - mock({ - 'file.txt': 'content', - }); - }); - afterEach(mock.restore); - - it('updates timestamps for a file', function () { - const fd = fs.openSync('file.txt', 'r'); - fs.futimesSync(fd, new Date(100), new Date(200)); - const stats = fs.statSync('file.txt'); - assert.equal(stats.atime.getTime(), 100); - assert.equal(stats.mtime.getTime(), 200); - }); -}); diff --git a/test/lib/fs.utimes-lutimes-futimes.spec.js b/test/lib/fs.utimes-lutimes-futimes.spec.js new file mode 100644 index 0000000..0168bc4 --- /dev/null +++ b/test/lib/fs.utimes-lutimes-futimes.spec.js @@ -0,0 +1,435 @@ +'use strict'; + +const helper = require('../helper.js'); +const fs = require('fs'); +const mock = require('../../lib/index.js'); + +const assert = helper.assert; + +describe('fs.utimes(path, atime, mtime, callback)', function () { + beforeEach(function () { + mock({ + dir: {}, + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function (done) { + fs.utimes('file.txt', new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }); + }); + + it('updates timestamps for a file following symlink', function (done) { + fs.utimes('link', new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 2); + assert.equal(stats2.mtime.getTime(), 2); + done(); + }); + }); + + it('supports Buffer input', function (done) { + fs.utimes( + Buffer.from('file.txt'), + new Date(100), + new Date(200), + function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + } + ); + }); + + it('promise updates timestamps for a file', function (done) { + fs.promises + .utimes('file.txt', new Date(100), new Date(200)) + .then(function () { + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }, done); + }); + + it('updates timestamps for a directory', function (done) { + fs.utimes('dir', new Date(300), new Date(400), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }); + }); + + it('promise updates timestamps for a directory', function (done) { + fs.promises.utimes('dir', new Date(300), new Date(400)).then(function () { + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }, done); + }); + + it('fails for a bogus path', function (done) { + fs.utimes('bogus.txt', new Date(100), new Date(200), function (err) { + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOENT'); + done(); + }); + }); + + it('promise fails for a bogus path', function (done) { + fs.promises.utimes('bogus.txt', new Date(100), new Date(200)).then( + function () { + done(new Error('should not succeed.')); + }, + function (err) { + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOENT'); + done(); + } + ); + }); +}); + +describe('fs.lutimes(path, atime, mtime, callback)', function () { + beforeEach(function () { + mock({ + dir: {}, + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function (done) { + fs.lutimes('file.txt', new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }); + }); + + it('updates timestamps for a file but not following symlink', function (done) { + fs.lutimes('link', new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 1); + assert.equal(stats.mtime.getTime(), 1); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 100); + assert.equal(stats2.mtime.getTime(), 200); + done(); + }); + }); + + it('supports Buffer input', function (done) { + fs.lutimes( + Buffer.from('file.txt'), + new Date(100), + new Date(200), + function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + } + ); + }); + + it('promise updates timestamps for a file', function (done) { + fs.promises + .lutimes('file.txt', new Date(100), new Date(200)) + .then(function () { + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }, done); + }); + + it('updates timestamps for a directory', function (done) { + fs.lutimes('dir', new Date(300), new Date(400), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }); + }); + + it('promise updates timestamps for a directory', function (done) { + fs.promises.lutimes('dir', new Date(300), new Date(400)).then(function () { + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }, done); + }); + + it('fails for a bogus path', function (done) { + fs.lutimes('bogus.txt', new Date(100), new Date(200), function (err) { + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOENT'); + done(); + }); + }); + + it('promise fails for a bogus path', function (done) { + fs.promises.lutimes('bogus.txt', new Date(100), new Date(200)).then( + function () { + done(new Error('should not succeed.')); + }, + function (err) { + assert.instanceOf(err, Error); + assert.equal(err.code, 'ENOENT'); + done(); + } + ); + }); +}); + +describe('fs.utimesSync(path, atime, mtime)', function () { + beforeEach(function () { + mock({ + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function () { + fs.utimesSync('file.txt', new Date(100), new Date(200)); + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + }); + + it('updates timestamps for a file following symlink', function () { + fs.utimesSync('link', new Date(100), new Date(200)); + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 2); + assert.equal(stats2.mtime.getTime(), 2); + }); +}); + +describe('fs.lutimesSync(path, atime, mtime)', function () { + beforeEach(function () { + mock({ + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function () { + fs.lutimesSync('file.txt', new Date(100), new Date(200)); + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + }); + + it('updates timestamps for a file but not following symlink', function () { + fs.lutimesSync('link', new Date(100), new Date(200)); + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 1); + assert.equal(stats.mtime.getTime(), 1); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 100); + assert.equal(stats2.mtime.getTime(), 200); + }); +}); + +describe('fs.futimes(fd, atime, mtime, callback)', function () { + beforeEach(function () { + mock({ + dir: {}, + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function (done) { + const fd = fs.openSync('file.txt', 'r'); + fs.futimes(fd, new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }); + }); + + it('updates timestamps for a file following symlink', function (done) { + const fd = fs.openSync('link', 'r'); + fs.futimes(fd, new Date(100), new Date(200), function (err) { + if (err) { + return done(err); + } + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 2); + assert.equal(stats2.mtime.getTime(), 2); + done(); + }); + }); + + it('promise updates timestamps for a file', function (done) { + fs.promises + .open('file.txt', 'r') + .then(function (fd) { + return fd.utimes(new Date(100), new Date(200)); + }) + .then(function () { + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + done(); + }, done); + }); + + it('updates timestamps for a directory', function (done) { + const fd = fs.openSync('dir', 'r'); + fs.futimes(fd, new Date(300), new Date(400), function (err) { + if (err) { + return done(err); + } + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }); + }); + + it('promise updates timestamps for a directory', function (done) { + fs.promises + .open('dir', 'r') + .then(function (fd) { + return fd.utimes(new Date(300), new Date(400)); + }) + .then(function () { + const stats = fs.statSync('dir'); + assert.equal(stats.atime.getTime(), 300); + assert.equal(stats.mtime.getTime(), 400); + done(); + }, done); + }); +}); + +describe('fs.futimesSync(path, atime, mtime)', function () { + beforeEach(function () { + mock({ + 'file.txt': mock.file({ + content: 'content', + atime: new Date(1), + mtime: new Date(1), + }), + link: mock.symlink({ + path: './file.txt', + atime: new Date(2), + mtime: new Date(2), + }), + }); + }); + afterEach(mock.restore); + + it('updates timestamps for a file', function () { + const fd = fs.openSync('file.txt', 'r'); + fs.futimesSync(fd, new Date(100), new Date(200)); + const stats = fs.statSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + }); + + it('updates timestamps for a file following symlink', function () { + const fd = fs.openSync('link', 'r'); + fs.futimesSync(fd, new Date(100), new Date(200)); + const stats = fs.lstatSync('file.txt'); + assert.equal(stats.atime.getTime(), 100); + assert.equal(stats.mtime.getTime(), 200); + const stats2 = fs.lstatSync('link'); + assert.equal(stats2.atime.getTime(), 2); + assert.equal(stats2.mtime.getTime(), 2); + }); +});