1 //===----------------------------------------------------------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 // REQUIRES: can-create-symlinks
10 // UNSUPPORTED: c++03, c++11, c++14
11 // UNSUPPORTED: no-filesystem
12 // UNSUPPORTED: availability-filesystem-missing
13 
14 // The string reported on errors changed, which makes those tests fail when run
15 // against a built library that doesn't contain 0aa637b2037d.
16 // XFAIL: using-built-library-before-llvm-13
17 
18 // <filesystem>
19 
20 // file_time_type last_write_time(const path& p);
21 // file_time_type last_write_time(const path& p, std::error_code& ec) noexcept;
22 // void last_write_time(const path& p, file_time_type new_time);
23 // void last_write_time(const path& p, file_time_type new_type,
24 //                      std::error_code& ec) noexcept;
25 
26 #include <filesystem>
27 #include <chrono>
28 #include <cstdio>
29 #include <cstdlib>
30 #include <ctime>
31 #include <ratio>
32 #include <type_traits>
33 
34 #include "test_macros.h"
35 #include "filesystem_test_helper.h"
36 
37 #include <fcntl.h>
38 #ifdef _WIN32
39 #include <windows.h>
40 #else
41 #include <sys/time.h>
42 #include <sys/stat.h>
43 #endif
44 namespace fs = std::filesystem;
45 using namespace fs;
46 
47 using Sec = std::chrono::duration<file_time_type::rep>;
48 using Hours = std::chrono::hours;
49 using Minutes = std::chrono::minutes;
50 using MilliSec = std::chrono::duration<file_time_type::rep, std::milli>;
51 using MicroSec = std::chrono::duration<file_time_type::rep, std::micro>;
52 using NanoSec = std::chrono::duration<file_time_type::rep, std::nano>;
53 using std::chrono::duration_cast;
54 
55 #ifdef _WIN32
56 struct TimeSpec {
57   std::int64_t tv_sec;
58   std::int64_t tv_nsec;
59 };
60 struct StatT {
61   TimeSpec st_atim;
62   TimeSpec st_mtim;
63 };
64 // There were 369 years and 89 leap days from the Windows epoch
65 // (1601) to the Unix epoch (1970).
66 #define FILE_TIME_OFFSET_SECS (std::uint64_t(369 * 365 + 89) * (24 * 60 * 60))
67 static TimeSpec filetime_to_timespec(LARGE_INTEGER li) {
68   TimeSpec ret;
69   ret.tv_sec = li.QuadPart / 10000000 - FILE_TIME_OFFSET_SECS;
70   ret.tv_nsec = (li.QuadPart % 10000000) * 100;
71   return ret;
72 }
73 static int stat_file(const char *path, StatT *buf, int flags) {
74   HANDLE h = CreateFileA(path, FILE_READ_ATTRIBUTES,
75                          FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
76                          nullptr, OPEN_EXISTING,
77                          FILE_FLAG_BACKUP_SEMANTICS | flags, nullptr);
78   if (h == INVALID_HANDLE_VALUE)
79     return -1;
80   int ret = -1;
81   FILE_BASIC_INFO basic;
82   if (GetFileInformationByHandleEx(h, FileBasicInfo, &basic, sizeof(basic))) {
83     buf->st_mtim = filetime_to_timespec(basic.LastWriteTime);
84     buf->st_atim = filetime_to_timespec(basic.LastAccessTime);
85     ret = 0;
86   }
87   CloseHandle(h);
88   return ret;
89 }
90 static int stat(const char *path, StatT *buf) {
91   return stat_file(path, buf, 0);
92 }
93 static int lstat(const char *path, StatT *buf) {
94   return stat_file(path, buf, FILE_FLAG_OPEN_REPARSE_POINT);
95 }
96 #elif defined(_AIX)
97 using TimeSpec = st_timespec_t;
98 using StatT = struct stat;
99 #else
100 using TimeSpec = timespec;
101 using StatT = struct stat;
102 #endif
103 
104 #if defined(__APPLE__)
105 TimeSpec extract_mtime(StatT const& st) { return st.st_mtimespec; }
106 TimeSpec extract_atime(StatT const& st) { return st.st_atimespec; }
107 #else
108 TimeSpec extract_mtime(StatT const& st) { return st.st_mtim; }
109 TimeSpec extract_atime(StatT const& st) { return st.st_atim; }
110 #endif
111 
112 bool ConvertToTimeSpec(TimeSpec& ts, file_time_type ft) {
113   using SecFieldT = decltype(TimeSpec::tv_sec);
114   using NSecFieldT = decltype(TimeSpec::tv_nsec);
115   using SecLim = std::numeric_limits<SecFieldT>;
116   using NSecLim = std::numeric_limits<NSecFieldT>;
117 
118   auto secs = duration_cast<Sec>(ft.time_since_epoch());
119   auto nsecs = duration_cast<NanoSec>(ft.time_since_epoch() - secs);
120   if (nsecs.count() < 0) {
121     if (Sec::min().count() > SecLim::min()) {
122       secs += Sec(1);
123       nsecs -= Sec(1);
124     } else {
125       nsecs = NanoSec(0);
126     }
127   }
128   if (SecLim::max() < secs.count() || SecLim::min() > secs.count())
129     return false;
130   if (NSecLim::max() < nsecs.count() || NSecLim::min() > nsecs.count())
131     return false;
132   ts.tv_sec = secs.count();
133   ts.tv_nsec = nsecs.count();
134   return true;
135 }
136 
137 bool ConvertFromTimeSpec(file_time_type& ft, TimeSpec ts) {
138   auto secs_part = duration_cast<file_time_type::duration>(Sec(ts.tv_sec));
139   if (duration_cast<Sec>(secs_part).count() != ts.tv_sec)
140     return false;
141   auto subsecs = duration_cast<file_time_type::duration>(NanoSec(ts.tv_nsec));
142   auto dur = secs_part + subsecs;
143   if (dur < secs_part && subsecs.count() >= 0)
144     return false;
145   ft = file_time_type(dur);
146   return true;
147 }
148 
149 bool CompareTimeExact(TimeSpec ts, TimeSpec ts2) {
150   return ts2.tv_sec == ts.tv_sec && ts2.tv_nsec == ts.tv_nsec;
151 }
152 bool CompareTimeExact(file_time_type ft, TimeSpec ts) {
153   TimeSpec ts2 = {};
154   if (!ConvertToTimeSpec(ts2, ft))
155     return false;
156   return CompareTimeExact(ts, ts2);
157 }
158 bool CompareTimeExact(TimeSpec ts, file_time_type ft) {
159   return CompareTimeExact(ft, ts);
160 }
161 
162 struct Times {
163   TimeSpec access, write;
164 };
165 
166 Times GetTimes(path const& p) {
167     StatT st;
168     if (::stat(p.string().c_str(), &st) == -1) {
169         std::error_code ec(errno, std::generic_category());
170 #ifndef TEST_HAS_NO_EXCEPTIONS
171         throw ec;
172 #else
173         std::exit(EXIT_FAILURE);
174 #endif
175     }
176     return {extract_atime(st), extract_mtime(st)};
177 }
178 
179 TimeSpec LastAccessTime(path const& p) { return GetTimes(p).access; }
180 
181 TimeSpec LastWriteTime(path const& p) { return GetTimes(p).write; }
182 
183 Times GetSymlinkTimes(path const& p) {
184   StatT st;
185   if (::lstat(p.string().c_str(), &st) == -1) {
186     std::error_code ec(errno, std::generic_category());
187 #ifndef TEST_HAS_NO_EXCEPTIONS
188         throw ec;
189 #else
190         std::exit(EXIT_FAILURE);
191 #endif
192     }
193     Times res;
194     res.access = extract_atime(st);
195     res.write = extract_mtime(st);
196     return res;
197 }
198 
199 namespace {
200 
201 // In some configurations, the comparison is tautological and the test is valid.
202 // We disable the warning so that we can actually test it regardless.
203 TEST_DIAGNOSTIC_PUSH
204 TEST_CLANG_DIAGNOSTIC_IGNORED("-Wtautological-constant-compare")
205 
206 static const bool SupportsNegativeTimes = [] {
207   using namespace std::chrono;
208   std::error_code ec;
209   TimeSpec old_write_time, new_write_time;
210   { // WARNING: Do not assert in this scope.
211     scoped_test_env env;
212     const path file = env.create_file("file", 42);
213     old_write_time = LastWriteTime(file);
214     file_time_type tp(seconds(-5));
215     fs::last_write_time(file, tp, ec);
216     new_write_time = LastWriteTime(file);
217   }
218 
219   return !ec && new_write_time.tv_sec < 0;
220 }();
221 
222 static const bool SupportsMaxTime = [] {
223   using namespace std::chrono;
224   TimeSpec max_ts = {};
225   if (!ConvertToTimeSpec(max_ts, file_time_type::max()))
226     return false;
227 
228   std::error_code ec;
229   TimeSpec old_write_time, new_write_time;
230   { // WARNING: Do not assert in this scope.
231     scoped_test_env env;
232     const path file = env.create_file("file", 42);
233     old_write_time = LastWriteTime(file);
234     file_time_type tp = file_time_type::max();
235     fs::last_write_time(file, tp, ec);
236     new_write_time = LastWriteTime(file);
237   }
238   return !ec && new_write_time.tv_sec > max_ts.tv_sec - 1;
239 }();
240 
241 static const bool SupportsMinTime = [] {
242   using namespace std::chrono;
243   TimeSpec min_ts = {};
244   if (!ConvertToTimeSpec(min_ts, file_time_type::min()))
245     return false;
246   std::error_code ec;
247   TimeSpec old_write_time, new_write_time;
248   { // WARNING: Do not assert in this scope.
249     scoped_test_env env;
250     const path file = env.create_file("file", 42);
251     old_write_time = LastWriteTime(file);
252     file_time_type tp = file_time_type::min();
253     fs::last_write_time(file, tp, ec);
254     new_write_time = LastWriteTime(file);
255   }
256   return !ec && new_write_time.tv_sec < min_ts.tv_sec + 1;
257 }();
258 
259 static const bool SupportsNanosecondRoundTrip = [] {
260   NanoSec ns(3);
261   static_assert(std::is_same<file_time_type::period, std::nano>::value, "");
262 
263   // Test that the system call we use to set the times also supports nanosecond
264   // resolution. (utimes does not)
265   file_time_type ft(ns);
266   {
267     scoped_test_env env;
268     const path p = env.create_file("file", 42);
269     last_write_time(p, ft);
270     return last_write_time(p) == ft;
271   }
272 }();
273 
274 // The HFS+ filesystem (used by default before macOS 10.13) stores timestamps at
275 // a 1-second granularity, and APFS (now the default) at a 1 nanosecond granularity.
276 // 1-second granularity is also the norm on many of the supported filesystems
277 // on Linux as well.
278 static const bool WorkaroundStatTruncatesToSeconds = [] {
279   MicroSec micros(3);
280   static_assert(std::is_same<file_time_type::period, std::nano>::value, "");
281 
282   file_time_type ft(micros);
283   {
284     scoped_test_env env;
285     const path p = env.create_file("file", 42);
286     if (LastWriteTime(p).tv_nsec != 0)
287       return false;
288     last_write_time(p, ft);
289     return last_write_time(p) != ft && LastWriteTime(p).tv_nsec == 0;
290   }
291 }();
292 
293 static const bool SupportsMinRoundTrip = [] {
294   TimeSpec ts = {};
295   if (!ConvertToTimeSpec(ts, file_time_type::min()))
296     return false;
297   file_time_type min_val = {};
298   if (!ConvertFromTimeSpec(min_val, ts))
299     return false;
300   return min_val == file_time_type::min();
301 }();
302 
303 } // namespace
304 
305 static bool CompareTime(TimeSpec t1, TimeSpec t2) {
306   if (SupportsNanosecondRoundTrip)
307     return CompareTimeExact(t1, t2);
308   if (t1.tv_sec != t2.tv_sec)
309     return false;
310 
311   auto diff = std::abs(t1.tv_nsec - t2.tv_nsec);
312   if (WorkaroundStatTruncatesToSeconds)
313    return diff < duration_cast<NanoSec>(Sec(1)).count();
314   return diff < duration_cast<NanoSec>(MicroSec(1)).count();
315 }
316 
317 static bool CompareTime(file_time_type t1, TimeSpec t2) {
318   TimeSpec ts1 = {};
319   if (!ConvertToTimeSpec(ts1, t1))
320     return false;
321   return CompareTime(ts1, t2);
322 }
323 
324 static bool CompareTime(TimeSpec t1, file_time_type t2) {
325   return CompareTime(t2, t1);
326 }
327 
328 static bool CompareTime(file_time_type t1, file_time_type t2) {
329   auto min_secs = duration_cast<Sec>(file_time_type::min().time_since_epoch());
330   bool IsMin =
331       t1.time_since_epoch() < min_secs || t2.time_since_epoch() < min_secs;
332 
333   if (SupportsNanosecondRoundTrip && (!IsMin || SupportsMinRoundTrip))
334     return t1 == t2;
335   if (IsMin) {
336     return duration_cast<Sec>(t1.time_since_epoch()) ==
337            duration_cast<Sec>(t2.time_since_epoch());
338   }
339   file_time_type::duration dur;
340   if (t1 > t2)
341     dur = t1 - t2;
342   else
343     dur = t2 - t1;
344   if (WorkaroundStatTruncatesToSeconds)
345     return duration_cast<Sec>(dur).count() == 0;
346   return duration_cast<MicroSec>(dur).count() == 0;
347 }
348 
349 // Check if a time point is representable on a given filesystem. Check that:
350 // (A) 'tp' is representable as a time_t
351 // (B) 'tp' is non-negative or the filesystem supports negative times.
352 // (C) 'tp' is not 'file_time_type::max()' or the filesystem supports the max
353 //     value.
354 // (D) 'tp' is not 'file_time_type::min()' or the filesystem supports the min
355 //     value.
356 inline bool TimeIsRepresentableByFilesystem(file_time_type tp) {
357   TimeSpec ts = {};
358   if (!ConvertToTimeSpec(ts, tp))
359     return false;
360   else if (tp.time_since_epoch().count() < 0 && !SupportsNegativeTimes)
361     return false;
362   else if (tp == file_time_type::max() && !SupportsMaxTime)
363     return false;
364   else if (tp == file_time_type::min() && !SupportsMinTime)
365     return false;
366   return true;
367 }
368 
369 TEST_DIAGNOSTIC_POP
370 
371 // Create a sub-second duration using the smallest period the filesystem supports.
372 file_time_type::duration SubSec(long long val) {
373   using SubSecT = file_time_type::duration;
374   if (SupportsNanosecondRoundTrip) {
375     return duration_cast<SubSecT>(NanoSec(val));
376   } else {
377     return duration_cast<SubSecT>(MicroSec(val));
378   }
379 }
380 
381 static void signature_test()
382 {
383     const file_time_type t;
384     const path p; ((void)p);
385     std::error_code ec; ((void)ec);
386     ASSERT_SAME_TYPE(decltype(last_write_time(p)), file_time_type);
387     ASSERT_SAME_TYPE(decltype(last_write_time(p, ec)), file_time_type);
388     ASSERT_SAME_TYPE(decltype(last_write_time(p, t)), void);
389     ASSERT_SAME_TYPE(decltype(last_write_time(p, t, ec)), void);
390     ASSERT_NOT_NOEXCEPT(last_write_time(p));
391     ASSERT_NOT_NOEXCEPT(last_write_time(p, t));
392     ASSERT_NOEXCEPT(last_write_time(p, ec));
393     ASSERT_NOEXCEPT(last_write_time(p, t, ec));
394 }
395 
396 static void read_last_write_time_static_env_test()
397 {
398     static_test_env static_env;
399     using C = file_time_type::clock;
400     file_time_type min = file_time_type::min();
401     // Sleep a little to make sure that static_env.File created above is
402     // strictly older than C::now() even with a coarser clock granularity
403     // in C::now(). (GetSystemTimeAsFileTime on windows has a fairly coarse
404     // granularity.)
405     SleepFor(MilliSec(30));
406     {
407         file_time_type ret = last_write_time(static_env.File);
408         assert(ret != min);
409         assert(ret < C::now());
410         assert(CompareTime(ret, LastWriteTime(static_env.File)));
411 
412         file_time_type ret2 = last_write_time(static_env.SymlinkToFile);
413         assert(CompareTime(ret, ret2));
414         assert(CompareTime(ret2, LastWriteTime(static_env.SymlinkToFile)));
415     }
416     {
417         file_time_type ret = last_write_time(static_env.Dir);
418         assert(ret != min);
419         assert(ret < C::now());
420         assert(CompareTime(ret, LastWriteTime(static_env.Dir)));
421 
422         file_time_type ret2 = last_write_time(static_env.SymlinkToDir);
423         assert(CompareTime(ret, ret2));
424         assert(CompareTime(ret2, LastWriteTime(static_env.SymlinkToDir)));
425     }
426 }
427 
428 static void get_last_write_time_dynamic_env_test()
429 {
430     scoped_test_env env;
431 
432     const path file = env.create_file("file", 42);
433     const path dir = env.create_dir("dir");
434 
435     const auto file_times = GetTimes(file);
436     const TimeSpec file_write_time = file_times.write;
437     const auto dir_times = GetTimes(dir);
438     const TimeSpec dir_write_time = dir_times.write;
439 
440     file_time_type ftime = last_write_time(file);
441     assert(CompareTime(ftime, file_write_time));
442 
443     file_time_type dtime = last_write_time(dir);
444     assert(CompareTime(dtime, dir_write_time));
445 
446     SleepFor(std::chrono::seconds(2));
447 
448     // update file and add a file to the directory. Make sure the times increase.
449     std::FILE* of = std::fopen(file.string().c_str(), "a");
450     std::fwrite("hello", 1, sizeof("hello"), of);
451     std::fclose(of);
452     env.create_file("dir/file1", 1);
453 
454     file_time_type ftime2 = last_write_time(file);
455     file_time_type dtime2 = last_write_time(dir);
456 
457     assert(ftime2 > ftime);
458     assert(dtime2 > dtime);
459     assert(CompareTime(LastWriteTime(file), ftime2));
460     assert(CompareTime(LastWriteTime(dir), dtime2));
461 }
462 
463 
464 static void set_last_write_time_dynamic_env_test()
465 {
466     using Clock = file_time_type::clock;
467     scoped_test_env env;
468 
469     const path file = env.create_file("file", 42);
470     const path dir = env.create_dir("dir");
471     const auto now = Clock::now();
472     const file_time_type epoch_time = now - now.time_since_epoch();
473 
474     const file_time_type future_time = now + Hours(3) + Sec(42) + SubSec(17);
475     const file_time_type past_time = now - Minutes(3) - Sec(42) - SubSec(17);
476     const file_time_type before_epoch_time =
477         epoch_time - Minutes(3) - Sec(42) - SubSec(17);
478     (void)before_epoch_time;
479     // FreeBSD has a bug in their utimes implementation where the time is not update
480     // when the number of seconds is '-1'.
481 #if defined(__FreeBSD__) || defined(__NetBSD__)
482     const file_time_type just_before_epoch_time =
483         epoch_time - Sec(2) - SubSec(17);
484 #else
485     const file_time_type just_before_epoch_time = epoch_time - SubSec(17);
486     (void)just_before_epoch_time;
487 #endif
488 
489     struct TestCase {
490       const char * case_name;
491       path p;
492       file_time_type new_time;
493     } cases[] = {
494         {"file, epoch_time", file, epoch_time},
495         {"dir, epoch_time", dir, epoch_time},
496         {"file, future_time", file, future_time},
497         {"dir, future_time", dir, future_time},
498         {"file, past_time", file, past_time},
499         {"dir, past_time", dir, past_time}
500         // Exclude file time types of before epoch time from testing on AIX
501         // because AIX system call utimensat() does not accept the times
502         // parameter having a negative tv_sec or tv_nsec field.
503 #if !defined(_AIX)
504         ,
505         {"file, before_epoch_time", file, before_epoch_time},
506         {"dir, before_epoch_time", dir, before_epoch_time},
507         {"file, just_before_epoch_time", file, just_before_epoch_time},
508         {"dir, just_before_epoch_time", dir, just_before_epoch_time}
509 #endif
510     };
511 
512     for (const auto& TC : cases) {
513         const auto old_times = GetTimes(TC.p);
514         file_time_type old_time;
515         assert(ConvertFromTimeSpec(old_time, old_times.write));
516 
517         std::error_code ec = GetTestEC();
518         last_write_time(TC.p, TC.new_time, ec);
519         assert(!ec);
520 
521         ec = GetTestEC();
522         file_time_type  got_time = last_write_time(TC.p, ec);
523         assert(!ec);
524 
525         if (TimeIsRepresentableByFilesystem(TC.new_time)) {
526             assert(got_time != old_time);
527             assert(CompareTime(got_time, TC.new_time));
528             assert(CompareTime(LastAccessTime(TC.p), old_times.access));
529         }
530     }
531 }
532 
533 static void last_write_time_symlink_test()
534 {
535     using Clock = file_time_type::clock;
536 
537     scoped_test_env env;
538 
539     const path file = env.create_file("file", 42);
540     const path sym = env.create_symlink("file", "sym");
541 
542     const file_time_type new_time = Clock::now() + Hours(3);
543 
544     const auto old_times = GetTimes(sym);
545     const auto old_sym_times = GetSymlinkTimes(sym);
546 
547     std::error_code ec = GetTestEC();
548     last_write_time(sym, new_time, ec);
549     assert(!ec);
550 
551     file_time_type  got_time = last_write_time(sym);
552     assert(!CompareTime(got_time, old_times.write));
553     if (!WorkaroundStatTruncatesToSeconds) {
554       assert(got_time == new_time);
555     } else {
556       assert(CompareTime(got_time, new_time));
557     }
558 
559     assert(CompareTime(LastWriteTime(file), new_time));
560     assert(CompareTime(LastAccessTime(sym), old_times.access));
561     Times sym_times = GetSymlinkTimes(sym);
562     assert(CompareTime(sym_times.write, old_sym_times.write));
563 }
564 
565 
566 static void test_write_min_time()
567 {
568     scoped_test_env env;
569     const path p = env.create_file("file", 42);
570     const file_time_type old_time = last_write_time(p);
571     file_time_type new_time = file_time_type::min();
572 
573     std::error_code ec = GetTestEC();
574     last_write_time(p, new_time, ec);
575     file_time_type tt = last_write_time(p);
576 
577     if (TimeIsRepresentableByFilesystem(new_time)) {
578         assert(!ec);
579         assert(CompareTime(tt, new_time));
580 
581         last_write_time(p, old_time);
582         new_time = file_time_type::min() + SubSec(1);
583 
584         ec = GetTestEC();
585         last_write_time(p, new_time, ec);
586         tt = last_write_time(p);
587 
588         if (TimeIsRepresentableByFilesystem(new_time)) {
589             assert(!ec);
590             assert(CompareTime(tt, new_time));
591         } else {
592           assert(ErrorIs(ec, std::errc::value_too_large));
593           assert(tt == old_time);
594         }
595     } else {
596       assert(ErrorIs(ec, std::errc::value_too_large));
597       assert(tt == old_time);
598     }
599 }
600 
601 static void test_write_max_time() {
602   scoped_test_env env;
603   const path p = env.create_file("file", 42);
604   const file_time_type old_time = last_write_time(p);
605   file_time_type new_time = file_time_type::max();
606 
607   std::error_code ec = GetTestEC();
608   last_write_time(p, new_time, ec);
609   file_time_type tt = last_write_time(p);
610 
611   if (TimeIsRepresentableByFilesystem(new_time)) {
612     assert(!ec);
613     assert(CompareTime(tt, new_time));
614   } else {
615     assert(ErrorIs(ec, std::errc::value_too_large));
616     assert(tt == old_time);
617   }
618 }
619 
620 static void test_value_on_failure()
621 {
622     static_test_env static_env;
623     const path p = static_env.DNE;
624     std::error_code ec = GetTestEC();
625     assert(last_write_time(p, ec) == file_time_type::min());
626     assert(ErrorIs(ec, std::errc::no_such_file_or_directory));
627 }
628 
629 // Windows doesn't support setting perms::none to trigger failures
630 // reading directories.
631 #ifndef TEST_WIN_NO_FILESYSTEM_PERMS_NONE
632 static void test_exists_fails()
633 {
634     scoped_test_env env;
635     const path dir = env.create_dir("dir");
636     const path file = env.create_file("dir/file", 42);
637     permissions(dir, perms::none);
638 
639     std::error_code ec = GetTestEC();
640     assert(last_write_time(file, ec) == file_time_type::min());
641     assert(ErrorIs(ec, std::errc::permission_denied));
642 
643     ExceptionChecker Checker(file, std::errc::permission_denied,
644                              "last_write_time");
645     TEST_VALIDATE_EXCEPTION(filesystem_error, Checker, last_write_time(file));
646 }
647 #endif // TEST_WIN_NO_FILESYSTEM_PERMS_NONE
648 
649 int main(int, char**) {
650     signature_test();
651     read_last_write_time_static_env_test();
652     get_last_write_time_dynamic_env_test();
653     set_last_write_time_dynamic_env_test();
654     last_write_time_symlink_test();
655     test_write_min_time();
656     test_write_max_time();
657     test_value_on_failure();
658 #ifndef TEST_WIN_NO_FILESYSTEM_PERMS_NONE
659     test_exists_fails();
660 #endif
661   return 0;
662 }
663