| 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 (StatT const& st) { return st.st_mtim; } |
| 109 | TimeSpec (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>(d: ft.time_since_epoch()); |
| 119 | auto nsecs = duration_cast<NanoSec>(d: 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>(d: Sec(ts.tv_sec)); |
| 139 | if (duration_cast<Sec>(d: secs_part).count() != ts.tv_sec) |
| 140 | return false; |
| 141 | auto subsecs = duration_cast<file_time_type::duration>(d: 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(ts&: 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(file: p.string().c_str(), buf: &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 {.access: extract_atime(st), .write: 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(file: p.string().c_str(), buf: &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(p: file); |
| 214 | file_time_type tp(seconds(-5)); |
| 215 | fs::last_write_time(p: file, new_time: tp, ec&: ec); |
| 216 | new_write_time = LastWriteTime(p: 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(ts&: max_ts, ft: 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(p: file); |
| 234 | file_time_type tp = file_time_type::max(); |
| 235 | fs::last_write_time(p: file, new_time: tp, ec&: ec); |
| 236 | new_write_time = LastWriteTime(p: 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(ts&: min_ts, ft: 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(p: file); |
| 252 | file_time_type tp = file_time_type::min(); |
| 253 | fs::last_write_time(p: file, new_time: tp, ec&: ec); |
| 254 | new_write_time = LastWriteTime(p: 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: p, new_time: 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: p, new_time: 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, ft: file_time_type::min())) |
| 296 | return false; |
| 297 | file_time_type min_val = {}; |
| 298 | if (!ConvertFromTimeSpec(ft&: 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(ts: t1, ts2: t2); |
| 308 | if (t1.tv_sec != t2.tv_sec) |
| 309 | return false; |
| 310 | |
| 311 | auto diff = std::abs(i: t1.tv_nsec - t2.tv_nsec); |
| 312 | if (WorkaroundStatTruncatesToSeconds) |
| 313 | return diff < duration_cast<NanoSec>(d: Sec(1)).count(); |
| 314 | return diff < duration_cast<NanoSec>(d: MicroSec(1)).count(); |
| 315 | } |
| 316 | |
| 317 | static bool CompareTime(file_time_type t1, TimeSpec t2) { |
| 318 | TimeSpec ts1 = {}; |
| 319 | if (!ConvertToTimeSpec(ts&: ts1, ft: t1)) |
| 320 | return false; |
| 321 | return CompareTime(t1: ts1, t2); |
| 322 | } |
| 323 | |
| 324 | static bool CompareTime(TimeSpec t1, file_time_type t2) { |
| 325 | return CompareTime(t1: t2, t2: t1); |
| 326 | } |
| 327 | |
| 328 | static bool CompareTime(file_time_type t1, file_time_type t2) { |
| 329 | auto min_secs = duration_cast<Sec>(d: 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>(d: t1.time_since_epoch()) == |
| 337 | duration_cast<Sec>(d: 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>(d: dur).count() == 0; |
| 346 | return duration_cast<MicroSec>(d: 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, ft: 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: p)); |
| 391 | ASSERT_NOT_NOEXCEPT(last_write_time(p: p, new_time: t)); |
| 392 | ASSERT_NOEXCEPT(last_write_time(p: p, ec&: ec)); |
| 393 | ASSERT_NOEXCEPT(last_write_time(p: p, new_time: t, ec&: 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(t1: ret, t2: 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(t1: ret, t2: 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(p: file); |
| 436 | const TimeSpec file_write_time = file_times.write; |
| 437 | const auto dir_times = GetTimes(p: dir); |
| 438 | const TimeSpec dir_write_time = dir_times.write; |
| 439 | |
| 440 | file_time_type ftime = last_write_time(p: file); |
| 441 | assert(CompareTime(t1: ftime, t2: file_write_time)); |
| 442 | |
| 443 | file_time_type dtime = last_write_time(p: dir); |
| 444 | assert(CompareTime(t1: dtime, t2: 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(filename: file.string().c_str(), modes: "a" ); |
| 450 | std::fwrite(ptr: "hello" , size: 1, n: sizeof("hello" ), s: of); |
| 451 | std::fclose(stream: of); |
| 452 | env.create_file("dir/file1" , 1); |
| 453 | |
| 454 | file_time_type ftime2 = last_write_time(p: file); |
| 455 | file_time_type dtime2 = last_write_time(p: dir); |
| 456 | |
| 457 | assert(ftime2 > ftime); |
| 458 | assert(dtime2 > dtime); |
| 459 | assert(CompareTime(t1: LastWriteTime(p: file), t2: ftime2)); |
| 460 | assert(CompareTime(t1: LastWriteTime(p: dir), t2: 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 | {.case_name: "file, epoch_time" , .p: file, .new_time: epoch_time}, |
| 495 | {.case_name: "dir, epoch_time" , .p: dir, .new_time: epoch_time}, |
| 496 | {.case_name: "file, future_time" , .p: file, .new_time: future_time}, |
| 497 | {.case_name: "dir, future_time" , .p: dir, .new_time: future_time}, |
| 498 | {.case_name: "file, past_time" , .p: file, .new_time: past_time}, |
| 499 | {.case_name: "dir, past_time" , .p: dir, .new_time: 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 | {.case_name: "file, before_epoch_time" , .p: file, .new_time: before_epoch_time}, |
| 506 | {.case_name: "dir, before_epoch_time" , .p: dir, .new_time: before_epoch_time}, |
| 507 | {.case_name: "file, just_before_epoch_time" , .p: file, .new_time: just_before_epoch_time}, |
| 508 | {.case_name: "dir, just_before_epoch_time" , .p: dir, .new_time: 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(p: sym); |
| 545 | const auto old_sym_times = GetSymlinkTimes(p: sym); |
| 546 | |
| 547 | std::error_code ec = GetTestEC(); |
| 548 | last_write_time(p: sym, new_time: new_time, ec&: ec); |
| 549 | assert(!ec); |
| 550 | |
| 551 | file_time_type got_time = last_write_time(p: sym); |
| 552 | assert(!CompareTime(got_time, old_times.write)); |
| 553 | if (!WorkaroundStatTruncatesToSeconds) { |
| 554 | assert(got_time == new_time); |
| 555 | } else { |
| 556 | assert(CompareTime(t1: got_time, t2: new_time)); |
| 557 | } |
| 558 | |
| 559 | assert(CompareTime(t1: LastWriteTime(p: file), t2: new_time)); |
| 560 | assert(CompareTime(LastAccessTime(p: sym), old_times.access)); |
| 561 | Times sym_times = GetSymlinkTimes(p: 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: p); |
| 571 | file_time_type new_time = file_time_type::min(); |
| 572 | |
| 573 | std::error_code ec = GetTestEC(); |
| 574 | last_write_time(p: p, new_time: new_time, ec&: ec); |
| 575 | file_time_type tt = last_write_time(p: p); |
| 576 | |
| 577 | if (TimeIsRepresentableByFilesystem(tp: new_time)) { |
| 578 | assert(!ec); |
| 579 | assert(CompareTime(t1: tt, t2: new_time)); |
| 580 | |
| 581 | last_write_time(p: p, new_time: old_time); |
| 582 | new_time = file_time_type::min() + SubSec(1); |
| 583 | |
| 584 | ec = GetTestEC(); |
| 585 | last_write_time(p: p, new_time: new_time, ec&: ec); |
| 586 | tt = last_write_time(p: p); |
| 587 | |
| 588 | if (TimeIsRepresentableByFilesystem(tp: new_time)) { |
| 589 | assert(!ec); |
| 590 | assert(CompareTime(t1: tt, t2: 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: p); |
| 605 | file_time_type new_time = file_time_type::max(); |
| 606 | |
| 607 | std::error_code ec = GetTestEC(); |
| 608 | last_write_time(p: p, new_time: new_time, ec&: ec); |
| 609 | file_time_type tt = last_write_time(p: p); |
| 610 | |
| 611 | if (TimeIsRepresentableByFilesystem(tp: new_time)) { |
| 612 | assert(!ec); |
| 613 | assert(CompareTime(t1: tt, t2: 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: p, ec&: 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(p: dir, prms: perms::none); |
| 638 | |
| 639 | std::error_code ec = GetTestEC(); |
| 640 | assert(last_write_time(p: file, ec&: 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 | |