• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //===----------------------------------------------------------------------===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is dual licensed under the MIT and the University of Illinois Open
6 // Source Licenses. See LICENSE.TXT for details.
7 //
8 //===----------------------------------------------------------------------===//
9 
10 // UNSUPPORTED: c++98, c++03
11 
12 // <filesystem>
13 
14 // file_time_type last_write_time(const path& p);
15 // file_time_type last_write_time(const path& p, std::error_code& ec) noexcept;
16 // void last_write_time(const path& p, file_time_type new_time);
17 // void last_write_time(const path& p, file_time_type new_type,
18 //                      std::error_code& ec) noexcept;
19 
20 #include "filesystem_include.hpp"
21 #include <type_traits>
22 #include <chrono>
23 #include <fstream>
24 #include <cstdlib>
25 
26 #include "test_macros.h"
27 #include "rapid-cxx-test.hpp"
28 #include "filesystem_test_helper.hpp"
29 
30 #include <sys/stat.h>
31 #include <iostream>
32 
33 #include <fcntl.h>
34 #include <sys/time.h>
35 
36 using namespace fs;
37 
38 using TimeSpec = struct ::timespec;
39 using StatT = struct ::stat;
40 
41 using Sec = std::chrono::duration<file_time_type::rep>;
42 using Hours = std::chrono::hours;
43 using Minutes = std::chrono::minutes;
44 using MicroSec = std::chrono::duration<file_time_type::rep, std::micro>;
45 using NanoSec = std::chrono::duration<file_time_type::rep, std::nano>;
46 using std::chrono::duration_cast;
47 
48 #if defined(__APPLE__)
extract_mtime(StatT const & st)49 TimeSpec extract_mtime(StatT const& st) { return st.st_mtimespec; }
extract_atime(StatT const & st)50 TimeSpec extract_atime(StatT const& st) { return st.st_atimespec; }
51 #else
extract_mtime(StatT const & st)52 TimeSpec extract_mtime(StatT const& st) { return st.st_mtim; }
extract_atime(StatT const & st)53 TimeSpec extract_atime(StatT const& st) { return st.st_atim; }
54 #endif
55 
ConvertToTimeSpec(TimeSpec & ts,file_time_type ft)56 bool ConvertToTimeSpec(TimeSpec& ts, file_time_type ft) {
57   using SecFieldT = decltype(TimeSpec::tv_sec);
58   using NSecFieldT = decltype(TimeSpec::tv_nsec);
59   using SecLim = std::numeric_limits<SecFieldT>;
60   using NSecLim = std::numeric_limits<NSecFieldT>;
61 
62   auto secs = duration_cast<Sec>(ft.time_since_epoch());
63   auto nsecs = duration_cast<NanoSec>(ft.time_since_epoch() - secs);
64   if (nsecs.count() < 0) {
65     if (Sec::min().count() > SecLim::min()) {
66       secs += Sec(1);
67       nsecs -= Sec(1);
68     } else {
69       nsecs = NanoSec(0);
70     }
71   }
72   if (SecLim::max() < secs.count() || SecLim::min() > secs.count())
73     return false;
74   if (NSecLim::max() < nsecs.count() || NSecLim::min() > nsecs.count())
75     return false;
76   ts.tv_sec = secs.count();
77   ts.tv_nsec = nsecs.count();
78   return true;
79 }
80 
ConvertFromTimeSpec(file_time_type & ft,TimeSpec ts)81 bool ConvertFromTimeSpec(file_time_type& ft, TimeSpec ts) {
82   auto secs_part = duration_cast<file_time_type::duration>(Sec(ts.tv_sec));
83   if (duration_cast<Sec>(secs_part).count() != ts.tv_sec)
84     return false;
85   auto subsecs = duration_cast<file_time_type::duration>(NanoSec(ts.tv_nsec));
86   auto dur = secs_part + subsecs;
87   if (dur < secs_part && subsecs.count() >= 0)
88     return false;
89   ft = file_time_type(dur);
90   return true;
91 }
92 
CompareTimeExact(TimeSpec ts,TimeSpec ts2)93 bool CompareTimeExact(TimeSpec ts, TimeSpec ts2) {
94   return ts2.tv_sec == ts.tv_sec && ts2.tv_nsec == ts.tv_nsec;
95 }
CompareTimeExact(file_time_type ft,TimeSpec ts)96 bool CompareTimeExact(file_time_type ft, TimeSpec ts) {
97   TimeSpec ts2 = {};
98   if (!ConvertToTimeSpec(ts2, ft))
99     return false;
100   return CompareTimeExact(ts, ts2);
101 }
CompareTimeExact(TimeSpec ts,file_time_type ft)102 bool CompareTimeExact(TimeSpec ts, file_time_type ft) {
103   return CompareTimeExact(ft, ts);
104 }
105 
106 struct Times {
107   TimeSpec access, write;
108 };
109 
GetTimes(path const & p)110 Times GetTimes(path const& p) {
111     StatT st;
112     if (::stat(p.c_str(), &st) == -1) {
113         std::error_code ec(errno, std::generic_category());
114 #ifndef TEST_HAS_NO_EXCEPTIONS
115         throw ec;
116 #else
117         std::cerr << ec.message() << std::endl;
118         std::exit(EXIT_FAILURE);
119 #endif
120     }
121     return {extract_atime(st), extract_mtime(st)};
122 }
123 
LastAccessTime(path const & p)124 TimeSpec LastAccessTime(path const& p) { return GetTimes(p).access; }
125 
LastWriteTime(path const & p)126 TimeSpec LastWriteTime(path const& p) { return GetTimes(p).write; }
127 
GetSymlinkTimes(path const & p)128 Times GetSymlinkTimes(path const& p) {
129   StatT st;
130   if (::lstat(p.c_str(), &st) == -1) {
131     std::error_code ec(errno, std::generic_category());
132 #ifndef TEST_HAS_NO_EXCEPTIONS
133         throw ec;
134 #else
135         std::cerr << ec.message() << std::endl;
136         std::exit(EXIT_FAILURE);
137 #endif
138     }
139     Times res;
140     res.access = extract_atime(st);
141     res.write = extract_mtime(st);
142     return res;
143 }
144 
145 namespace {
146 
147 // In some configurations, the comparison is tautological and the test is valid.
148 // We disable the warning so that we can actually test it regardless. Also, that
149 // diagnostic is pretty new, so also don't fail if old clang does not support it
150 #if defined(__clang__)
151 #pragma clang diagnostic push
152 #pragma clang diagnostic ignored "-Wunknown-warning-option"
153 #pragma clang diagnostic ignored "-Wunknown-pragmas"
154 #pragma clang diagnostic ignored "-Wtautological-constant-compare"
155 #endif
156 
__anonae5559740202null157 static const bool SupportsNegativeTimes = [] {
158   using namespace std::chrono;
159   std::error_code ec;
160   TimeSpec old_write_time, new_write_time;
161   { // WARNING: Do not assert in this scope.
162     scoped_test_env env;
163     const path file = env.create_file("file", 42);
164     old_write_time = LastWriteTime(file);
165     file_time_type tp(seconds(-5));
166     fs::last_write_time(file, tp, ec);
167     new_write_time = LastWriteTime(file);
168   }
169 
170   return !ec && new_write_time.tv_sec < 0;
171 }();
172 
__anonae5559740302null173 static const bool SupportsMaxTime = [] {
174   using namespace std::chrono;
175   TimeSpec max_ts = {};
176   if (!ConvertToTimeSpec(max_ts, file_time_type::max()))
177     return false;
178 
179   std::error_code ec;
180   TimeSpec old_write_time, new_write_time;
181   { // WARNING: Do not assert in this scope.
182     scoped_test_env env;
183     const path file = env.create_file("file", 42);
184     old_write_time = LastWriteTime(file);
185     file_time_type tp = file_time_type::max();
186     fs::last_write_time(file, tp, ec);
187     new_write_time = LastWriteTime(file);
188   }
189   return !ec && new_write_time.tv_sec > max_ts.tv_sec - 1;
190 }();
191 
__anonae5559740402null192 static const bool SupportsMinTime = [] {
193   using namespace std::chrono;
194   TimeSpec min_ts = {};
195   if (!ConvertToTimeSpec(min_ts, file_time_type::min()))
196     return false;
197   std::error_code ec;
198   TimeSpec old_write_time, new_write_time;
199   { // WARNING: Do not assert in this scope.
200     scoped_test_env env;
201     const path file = env.create_file("file", 42);
202     old_write_time = LastWriteTime(file);
203     file_time_type tp = file_time_type::min();
204     fs::last_write_time(file, tp, ec);
205     new_write_time = LastWriteTime(file);
206   }
207   return !ec && new_write_time.tv_sec < min_ts.tv_sec + 1;
208 }();
209 
__anonae5559740502null210 static const bool SupportsNanosecondRoundTrip = [] {
211   NanoSec ns(3);
212   static_assert(std::is_same<file_time_type::period, std::nano>::value, "");
213 
214   // Test that the system call we use to set the times also supports nanosecond
215   // resolution. (utimes does not)
216   file_time_type ft(ns);
217   {
218     scoped_test_env env;
219     const path p = env.create_file("file", 42);
220     last_write_time(p, ft);
221     return last_write_time(p) == ft;
222   }
223 }();
224 
225 // The HFS+ filesystem (used by default before macOS 10.13) stores timestamps at
226 // a 1-second granularity, and APFS (now the default) at a 1 nanosecond granularity.
227 // 1-second granularity is also the norm on many of the supported filesystems
228 // on Linux as well.
__anonae5559740602null229 static const bool WorkaroundStatTruncatesToSeconds = [] {
230   MicroSec micros(3);
231   static_assert(std::is_same<file_time_type::period, std::nano>::value, "");
232 
233   file_time_type ft(micros);
234   {
235     scoped_test_env env;
236     const path p = env.create_file("file", 42);
237     if (LastWriteTime(p).tv_nsec != 0)
238       return false;
239     last_write_time(p, ft);
240     return last_write_time(p) != ft && LastWriteTime(p).tv_nsec == 0;
241   }
242 }();
243 
__anonae5559740702null244 static const bool SupportsMinRoundTrip = [] {
245   TimeSpec ts = {};
246   if (!ConvertToTimeSpec(ts, file_time_type::min()))
247     return false;
248   file_time_type min_val = {};
249   if (!ConvertFromTimeSpec(min_val, ts))
250     return false;
251   return min_val == file_time_type::min();
252 }();
253 
254 } // end namespace
255 
CompareTime(TimeSpec t1,TimeSpec t2)256 static bool CompareTime(TimeSpec t1, TimeSpec t2) {
257   if (SupportsNanosecondRoundTrip)
258     return CompareTimeExact(t1, t2);
259   if (t1.tv_sec != t2.tv_sec)
260     return false;
261 
262   auto diff = std::abs(t1.tv_nsec - t2.tv_nsec);
263   if (WorkaroundStatTruncatesToSeconds)
264    return diff < duration_cast<NanoSec>(Sec(1)).count();
265   return diff < duration_cast<NanoSec>(MicroSec(1)).count();
266 }
267 
CompareTime(file_time_type t1,TimeSpec t2)268 static bool CompareTime(file_time_type t1, TimeSpec t2) {
269   TimeSpec ts1 = {};
270   if (!ConvertToTimeSpec(ts1, t1))
271     return false;
272   return CompareTime(ts1, t2);
273 }
274 
CompareTime(TimeSpec t1,file_time_type t2)275 static bool CompareTime(TimeSpec t1, file_time_type t2) {
276   return CompareTime(t2, t1);
277 }
278 
CompareTime(file_time_type t1,file_time_type t2)279 static bool CompareTime(file_time_type t1, file_time_type t2) {
280   auto min_secs = duration_cast<Sec>(file_time_type::min().time_since_epoch());
281   bool IsMin =
282       t1.time_since_epoch() < min_secs || t2.time_since_epoch() < min_secs;
283 
284   if (SupportsNanosecondRoundTrip && (!IsMin || SupportsMinRoundTrip))
285     return t1 == t2;
286   if (IsMin) {
287     return duration_cast<Sec>(t1.time_since_epoch()) ==
288            duration_cast<Sec>(t2.time_since_epoch());
289   }
290   file_time_type::duration dur;
291   if (t1 > t2)
292     dur = t1 - t2;
293   else
294     dur = t2 - t1;
295   if (WorkaroundStatTruncatesToSeconds)
296     return duration_cast<Sec>(dur).count() == 0;
297   return duration_cast<MicroSec>(dur).count() == 0;
298 }
299 
300 // Check if a time point is representable on a given filesystem. Check that:
301 // (A) 'tp' is representable as a time_t
302 // (B) 'tp' is non-negative or the filesystem supports negative times.
303 // (C) 'tp' is not 'file_time_type::max()' or the filesystem supports the max
304 //     value.
305 // (D) 'tp' is not 'file_time_type::min()' or the filesystem supports the min
306 //     value.
TimeIsRepresentableByFilesystem(file_time_type tp)307 inline bool TimeIsRepresentableByFilesystem(file_time_type tp) {
308   TimeSpec ts = {};
309   if (!ConvertToTimeSpec(ts, tp))
310     return false;
311   else if (tp.time_since_epoch().count() < 0 && !SupportsNegativeTimes)
312     return false;
313   else if (tp == file_time_type::max() && !SupportsMaxTime)
314     return false;
315   else if (tp == file_time_type::min() && !SupportsMinTime)
316     return false;
317   return true;
318 }
319 
320 #if defined(__clang__)
321 #pragma clang diagnostic pop
322 #endif
323 
324 // Create a sub-second duration using the smallest period the filesystem supports.
SubSec(long long val)325 file_time_type::duration SubSec(long long val) {
326   using SubSecT = file_time_type::duration;
327   if (SupportsNanosecondRoundTrip) {
328     return duration_cast<SubSecT>(NanoSec(val));
329   } else {
330     return duration_cast<SubSecT>(MicroSec(val));
331   }
332 }
333 
334 TEST_SUITE(last_write_time_test_suite)
335 
TEST_CASE(signature_test)336 TEST_CASE(signature_test)
337 {
338     const file_time_type t;
339     const path p; ((void)p);
340     std::error_code ec; ((void)ec);
341     ASSERT_SAME_TYPE(decltype(last_write_time(p)), file_time_type);
342     ASSERT_SAME_TYPE(decltype(last_write_time(p, ec)), file_time_type);
343     ASSERT_SAME_TYPE(decltype(last_write_time(p, t)), void);
344     ASSERT_SAME_TYPE(decltype(last_write_time(p, t, ec)), void);
345     ASSERT_NOT_NOEXCEPT(last_write_time(p));
346     ASSERT_NOT_NOEXCEPT(last_write_time(p, t));
347     ASSERT_NOEXCEPT(last_write_time(p, ec));
348     ASSERT_NOEXCEPT(last_write_time(p, t, ec));
349 }
350 
TEST_CASE(read_last_write_time_static_env_test)351 TEST_CASE(read_last_write_time_static_env_test)
352 {
353     using C = file_time_type::clock;
354     file_time_type min = file_time_type::min();
355     {
356         file_time_type ret = last_write_time(StaticEnv::File);
357         TEST_CHECK(ret != min);
358         TEST_CHECK(ret < C::now());
359         TEST_CHECK(CompareTime(ret, LastWriteTime(StaticEnv::File)));
360 
361         file_time_type ret2 = last_write_time(StaticEnv::SymlinkToFile);
362         TEST_CHECK(CompareTime(ret, ret2));
363         TEST_CHECK(CompareTime(ret2, LastWriteTime(StaticEnv::SymlinkToFile)));
364     }
365     {
366         file_time_type ret = last_write_time(StaticEnv::Dir);
367         TEST_CHECK(ret != min);
368         TEST_CHECK(ret < C::now());
369         TEST_CHECK(CompareTime(ret, LastWriteTime(StaticEnv::Dir)));
370 
371         file_time_type ret2 = last_write_time(StaticEnv::SymlinkToDir);
372         TEST_CHECK(CompareTime(ret, ret2));
373         TEST_CHECK(CompareTime(ret2, LastWriteTime(StaticEnv::SymlinkToDir)));
374     }
375 }
376 
TEST_CASE(get_last_write_time_dynamic_env_test)377 TEST_CASE(get_last_write_time_dynamic_env_test)
378 {
379     using Clock = file_time_type::clock;
380     using Sec = std::chrono::seconds;
381     scoped_test_env env;
382 
383     const path file = env.create_file("file", 42);
384     const path dir = env.create_dir("dir");
385 
386     const auto file_times = GetTimes(file);
387     const TimeSpec file_write_time = file_times.write;
388     const auto dir_times = GetTimes(dir);
389     const TimeSpec dir_write_time = dir_times.write;
390 
391     file_time_type ftime = last_write_time(file);
392     TEST_CHECK(Clock::to_time_t(ftime) == file_write_time.tv_sec);
393     TEST_CHECK(CompareTime(ftime, file_write_time));
394 
395     file_time_type dtime = last_write_time(dir);
396     TEST_CHECK(Clock::to_time_t(dtime) == dir_write_time.tv_sec);
397     TEST_CHECK(CompareTime(dtime, dir_write_time));
398 
399     SleepFor(Sec(2));
400 
401     // update file and add a file to the directory. Make sure the times increase.
402     std::ofstream of(file, std::ofstream::app);
403     of << "hello";
404     of.close();
405     env.create_file("dir/file1", 1);
406 
407     file_time_type ftime2 = last_write_time(file);
408     file_time_type dtime2 = last_write_time(dir);
409 
410     TEST_CHECK(ftime2 > ftime);
411     TEST_CHECK(dtime2 > dtime);
412     TEST_CHECK(CompareTime(LastWriteTime(file), ftime2));
413     TEST_CHECK(CompareTime(LastWriteTime(dir), dtime2));
414 }
415 
416 
TEST_CASE(set_last_write_time_dynamic_env_test)417 TEST_CASE(set_last_write_time_dynamic_env_test)
418 {
419     using Clock = file_time_type::clock;
420     scoped_test_env env;
421 
422     const path file = env.create_file("file", 42);
423     const path dir = env.create_dir("dir");
424     const auto now = Clock::now();
425     const file_time_type epoch_time = now - now.time_since_epoch();
426 
427     const file_time_type future_time = now + Hours(3) + Sec(42) + SubSec(17);
428     const file_time_type past_time = now - Minutes(3) - Sec(42) - SubSec(17);
429     const file_time_type before_epoch_time =
430         epoch_time - Minutes(3) - Sec(42) - SubSec(17);
431     // FreeBSD has a bug in their utimes implementation where the time is not update
432     // when the number of seconds is '-1'.
433 #if defined(__FreeBSD__) || defined(__NetBSD__)
434     const file_time_type just_before_epoch_time =
435         epoch_time - Sec(2) - SubSec(17);
436 #else
437     const file_time_type just_before_epoch_time = epoch_time - SubSec(17);
438 #endif
439 
440     struct TestCase {
441       const char * case_name;
442       path p;
443       file_time_type new_time;
444     } cases[] = {
445         {"file, epoch_time", file, epoch_time},
446         {"dir, epoch_time", dir, epoch_time},
447         {"file, future_time", file, future_time},
448         {"dir, future_time", dir, future_time},
449         {"file, past_time", file, past_time},
450         {"dir, past_time", dir, past_time},
451         {"file, before_epoch_time", file, before_epoch_time},
452         {"dir, before_epoch_time", dir, before_epoch_time},
453         {"file, just_before_epoch_time", file, just_before_epoch_time},
454         {"dir, just_before_epoch_time", dir, just_before_epoch_time}
455     };
456     for (const auto& TC : cases) {
457         std::cerr << "Test Case = " << TC.case_name << "\n";
458         const auto old_times = GetTimes(TC.p);
459         file_time_type old_time;
460         TEST_REQUIRE(ConvertFromTimeSpec(old_time, old_times.write));
461 
462         std::error_code ec = GetTestEC();
463         last_write_time(TC.p, TC.new_time, ec);
464         TEST_CHECK(!ec);
465 
466         ec = GetTestEC();
467         file_time_type  got_time = last_write_time(TC.p, ec);
468         TEST_REQUIRE(!ec);
469 
470         if (TimeIsRepresentableByFilesystem(TC.new_time)) {
471             TEST_CHECK(got_time != old_time);
472             TEST_CHECK(CompareTime(got_time, TC.new_time));
473             TEST_CHECK(CompareTime(LastAccessTime(TC.p), old_times.access));
474         }
475     }
476 }
477 
TEST_CASE(last_write_time_symlink_test)478 TEST_CASE(last_write_time_symlink_test)
479 {
480     using Clock = file_time_type::clock;
481 
482     scoped_test_env env;
483 
484     const path file = env.create_file("file", 42);
485     const path sym = env.create_symlink("file", "sym");
486 
487     const file_time_type new_time = Clock::now() + Hours(3);
488 
489     const auto old_times = GetTimes(sym);
490     const auto old_sym_times = GetSymlinkTimes(sym);
491 
492     std::error_code ec = GetTestEC();
493     last_write_time(sym, new_time, ec);
494     TEST_CHECK(!ec);
495 
496     file_time_type  got_time = last_write_time(sym);
497     TEST_CHECK(!CompareTime(got_time, old_times.write));
498     if (!WorkaroundStatTruncatesToSeconds) {
499       TEST_CHECK(got_time == new_time);
500     } else {
501       TEST_CHECK(CompareTime(got_time, new_time));
502     }
503 
504     TEST_CHECK(CompareTime(LastWriteTime(file), new_time));
505     TEST_CHECK(CompareTime(LastAccessTime(sym), old_times.access));
506     Times sym_times = GetSymlinkTimes(sym);
507     TEST_CHECK(CompareTime(sym_times.write, old_sym_times.write));
508 }
509 
510 
TEST_CASE(test_write_min_time)511 TEST_CASE(test_write_min_time)
512 {
513     scoped_test_env env;
514     const path p = env.create_file("file", 42);
515     const file_time_type old_time = last_write_time(p);
516     file_time_type new_time = file_time_type::min();
517 
518     std::error_code ec = GetTestEC();
519     last_write_time(p, new_time, ec);
520     file_time_type tt = last_write_time(p);
521 
522     if (TimeIsRepresentableByFilesystem(new_time)) {
523         TEST_CHECK(!ec);
524         TEST_CHECK(CompareTime(tt, new_time));
525 
526         last_write_time(p, old_time);
527         new_time = file_time_type::min() + SubSec(1);
528 
529         ec = GetTestEC();
530         last_write_time(p, new_time, ec);
531         tt = last_write_time(p);
532 
533         if (TimeIsRepresentableByFilesystem(new_time)) {
534             TEST_CHECK(!ec);
535             TEST_CHECK(CompareTime(tt, new_time));
536         } else {
537           TEST_CHECK(ErrorIs(ec, std::errc::value_too_large));
538           TEST_CHECK(tt == old_time);
539         }
540     } else {
541       TEST_CHECK(ErrorIs(ec, std::errc::value_too_large));
542       TEST_CHECK(tt == old_time);
543     }
544 }
545 
TEST_CASE(test_write_max_time)546 TEST_CASE(test_write_max_time) {
547   scoped_test_env env;
548   const path p = env.create_file("file", 42);
549   const file_time_type old_time = last_write_time(p);
550   file_time_type new_time = file_time_type::max();
551 
552   std::error_code ec = GetTestEC();
553   last_write_time(p, new_time, ec);
554   file_time_type tt = last_write_time(p);
555 
556   if (TimeIsRepresentableByFilesystem(new_time)) {
557     TEST_CHECK(!ec);
558     TEST_CHECK(CompareTime(tt, new_time));
559   } else {
560     TEST_CHECK(ErrorIs(ec, std::errc::value_too_large));
561     TEST_CHECK(tt == old_time);
562   }
563 }
564 
TEST_CASE(test_value_on_failure)565 TEST_CASE(test_value_on_failure)
566 {
567     const path p = StaticEnv::DNE;
568     std::error_code ec = GetTestEC();
569     TEST_CHECK(last_write_time(p, ec) == file_time_type::min());
570     TEST_CHECK(ErrorIs(ec, std::errc::no_such_file_or_directory));
571 }
572 
TEST_CASE(test_exists_fails)573 TEST_CASE(test_exists_fails)
574 {
575     scoped_test_env env;
576     const path dir = env.create_dir("dir");
577     const path file = env.create_file("dir/file", 42);
578     permissions(dir, perms::none);
579 
580     std::error_code ec = GetTestEC();
581     TEST_CHECK(last_write_time(file, ec) == file_time_type::min());
582     TEST_CHECK(ErrorIs(ec, std::errc::permission_denied));
583 
584     ExceptionChecker Checker(file, std::errc::permission_denied,
585                              "last_write_time");
586     TEST_CHECK_THROW_RESULT(filesystem_error, Checker, last_write_time(file));
587 }
588 
589 TEST_SUITE_END()
590