1import * as common from '../common/index.mjs';
2
3// Test timestamps returned by fsPromises.stat and fs.statSync
4
5import fs from 'node:fs';
6import fsPromises from 'node:fs/promises';
7import path from 'node:path';
8import assert from 'node:assert';
9import tmpdir from '../common/tmpdir.js';
10
11// On some platforms (for example, ppc64) boundaries are tighter
12// than usual. If we catch these errors, skip corresponding test.
13const ignoredErrors = new Set(['EINVAL', 'EOVERFLOW']);
14
15tmpdir.refresh();
16const filepath = path.resolve(tmpdir.path, 'timestamp');
17
18await (await fsPromises.open(filepath, 'w')).close();
19
20// Perform a trivial check to determine if filesystem supports setting
21// and retrieving atime and mtime. If it doesn't, skip the test.
22await fsPromises.utimes(filepath, 2, 2);
23const { atimeMs, mtimeMs } = await fsPromises.stat(filepath);
24if (atimeMs !== 2000 || mtimeMs !== 2000) {
25  common.skip(`Unsupported filesystem (atime=${atimeMs}, mtime=${mtimeMs})`);
26}
27
28// Date might round down timestamp
29function closeEnough(actual, expected, margin) {
30  // On ppc64, value is rounded to seconds
31  if (process.arch === 'ppc64') {
32    margin += 1000;
33  }
34
35  // Filesystems without support for timestamps before 1970-01-01, such as NFSv3,
36  // should return 0 for negative numbers. Do not treat it as error.
37  if (actual === 0 && expected < 0) {
38    console.log(`ignored 0 while expecting ${expected}`);
39    return;
40  }
41
42  assert.ok(Math.abs(Number(actual - expected)) < margin,
43            `expected ${expected} ± ${margin}, got ${actual}`);
44}
45
46async function runTest(atime, mtime, margin = 0) {
47  margin += Number.EPSILON;
48  try {
49    await fsPromises.utimes(filepath, new Date(atime), new Date(mtime));
50  } catch (e) {
51    if (ignoredErrors.has(e.code)) return;
52    throw e;
53  }
54
55  const stats = await fsPromises.stat(filepath);
56  closeEnough(stats.atimeMs, atime, margin);
57  closeEnough(stats.mtimeMs, mtime, margin);
58  closeEnough(stats.atime.getTime(), new Date(atime).getTime(), margin);
59  closeEnough(stats.mtime.getTime(), new Date(mtime).getTime(), margin);
60
61  const statsBigint = await fsPromises.stat(filepath, { bigint: true });
62  closeEnough(statsBigint.atimeMs, BigInt(atime), margin);
63  closeEnough(statsBigint.mtimeMs, BigInt(mtime), margin);
64  closeEnough(statsBigint.atime.getTime(), new Date(atime).getTime(), margin);
65  closeEnough(statsBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
66
67  const statsSync = fs.statSync(filepath);
68  closeEnough(statsSync.atimeMs, atime, margin);
69  closeEnough(statsSync.mtimeMs, mtime, margin);
70  closeEnough(statsSync.atime.getTime(), new Date(atime).getTime(), margin);
71  closeEnough(statsSync.mtime.getTime(), new Date(mtime).getTime(), margin);
72
73  const statsSyncBigint = fs.statSync(filepath, { bigint: true });
74  closeEnough(statsSyncBigint.atimeMs, BigInt(atime), margin);
75  closeEnough(statsSyncBigint.mtimeMs, BigInt(mtime), margin);
76  closeEnough(statsSyncBigint.atime.getTime(), new Date(atime).getTime(), margin);
77  closeEnough(statsSyncBigint.mtime.getTime(), new Date(mtime).getTime(), margin);
78}
79
80// Too high/low numbers produce too different results on different platforms
81{
82  // TODO(LiviaMedeiros): investigate outdated stat time on FreeBSD.
83  // On Windows, filetime is stored and handled differently. Supporting dates
84  // after Y2038 is preferred over supporting dates before 1970-01-01.
85  if (!common.isFreeBSD && !common.isWindows) {
86    await runTest(-40691, -355, 1); // Potential precision loss on 32bit
87    await runTest(-355, -40691, 1);  // Potential precision loss on 32bit
88    await runTest(-1, -1);
89  }
90  await runTest(0, 0);
91  await runTest(1, 1);
92  await runTest(355, 40691, 1); // Precision loss on 32bit
93  await runTest(40691, 355, 1); // Precision loss on 32bit
94  await runTest(1713037251360, 1713037251360, 1); // Precision loss
95}
96