summaryrefslogtreecommitdiffhomepage
path: root/test/syscalls/linux/stat_times.cc
blob: 195c87ca5cbfc8c9cbad41244119eb1516ac66eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <fcntl.h>
#include <sys/stat.h>

#include <tuple>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "test/util/file_descriptor.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"

namespace gvisor {
namespace testing {

namespace {

using ::testing::IsEmpty;
using ::testing::Not;

std::tuple<absl::Time, absl::Time, absl::Time> GetTime(const TempPath& file) {
  struct stat statbuf = {};
  EXPECT_THAT(stat(file.path().c_str(), &statbuf), SyscallSucceeds());

  const auto atime = absl::TimeFromTimespec(statbuf.st_atim);
  const auto mtime = absl::TimeFromTimespec(statbuf.st_mtim);
  const auto ctime = absl::TimeFromTimespec(statbuf.st_ctim);
  return std::make_tuple(atime, mtime, ctime);
}

enum class AtimeEffect {
  Unchanged,
  Changed,
};

enum class MtimeEffect {
  Unchanged,
  Changed,
};

enum class CtimeEffect {
  Unchanged,
  Changed,
};

// Tests that fn modifies the atime/mtime/ctime of path as specified.
void CheckTimes(const TempPath& path, std::function<void()> fn,
                AtimeEffect atime_effect, MtimeEffect mtime_effect,
                CtimeEffect ctime_effect) {
  absl::Time atime, mtime, ctime;
  std::tie(atime, mtime, ctime) = GetTime(path);

  // FIXME(b/132819225): gVisor filesystem timestamps inconsistently use the
  // internal or host clock, which may diverge slightly. Allow some slack on
  // times to account for the difference.
  //
  // Here we sleep for 1s so that initial creation of path doesn't fall within
  // the before slack window.
  absl::SleepFor(absl::Seconds(1));

  const absl::Time before = absl::Now() - absl::Seconds(1);

  // Perform the op.
  fn();

  const absl::Time after = absl::Now() + absl::Seconds(1);

  absl::Time atime2, mtime2, ctime2;
  std::tie(atime2, mtime2, ctime2) = GetTime(path);

  if (atime_effect == AtimeEffect::Changed) {
    EXPECT_LE(before, atime2);
    EXPECT_GE(after, atime2);
    EXPECT_GT(atime2, atime);
  } else {
    EXPECT_EQ(atime2, atime);
  }

  if (mtime_effect == MtimeEffect::Changed) {
    EXPECT_LE(before, mtime2);
    EXPECT_GE(after, mtime2);
    EXPECT_GT(mtime2, mtime);
  } else {
    EXPECT_EQ(mtime2, mtime);
  }

  if (ctime_effect == CtimeEffect::Changed) {
    EXPECT_LE(before, ctime2);
    EXPECT_GE(after, ctime2);
    EXPECT_GT(ctime2, ctime);
  } else {
    EXPECT_EQ(ctime2, ctime);
  }
}

// File creation time is reflected in atime, mtime, and ctime.
TEST(StatTimesTest, FileCreation) {
  const DisableSave ds;  // Timing-related test.

  // Get a time for when the file is created.
  //
  // FIXME(b/132819225): See above.
  const absl::Time before = absl::Now() - absl::Seconds(1);
  const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
  const absl::Time after = absl::Now() + absl::Seconds(1);

  absl::Time atime, mtime, ctime;
  std::tie(atime, mtime, ctime) = GetTime(file);

  EXPECT_LE(before, atime);
  EXPECT_LE(before, mtime);
  EXPECT_LE(before, ctime);
  EXPECT_GE(after, atime);
  EXPECT_GE(after, mtime);
  EXPECT_GE(after, ctime);
}

// Calling chmod on a file changes ctime.
TEST(StatTimesTest, FileChmod) {
  TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());

  auto fn = [&] {
    EXPECT_THAT(chmod(file.path().c_str(), 0666), SyscallSucceeds());
  };
  CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
             CtimeEffect::Changed);
}

// Renaming a file changes ctime.
TEST(StatTimesTest, FileRename) {
  TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());

  const std::string newpath = NewTempAbsPath();

  auto fn = [&] {
    ASSERT_THAT(rename(file.release().c_str(), newpath.c_str()),
                SyscallSucceeds());
    file.reset(newpath);
  };
  CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
             CtimeEffect::Changed);
}

// Renaming a file changes ctime, even with an open FD.
//
// NOTE(b/132732387): This is a regression test for fs/gofer failing to update
// cached ctime.
TEST(StatTimesTest, FileRenameOpenFD) {
  TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());

  // Holding an FD shouldn't affect behavior.
  const FileDescriptor fd =
      ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY));

  const std::string newpath = NewTempAbsPath();

  // FIXME(b/132814682): Restore fails with an uncached gofer and an open FD
  // across rename.
  //
  // N.B. The logic here looks backwards because it isn't possible to
  // conditionally disable save, only conditionally re-enable it.
  DisableSave ds;
  if (!getenv("GVISOR_GOFER_UNCACHED")) {
    ds.reset();
  }

  auto fn = [&] {
    ASSERT_THAT(rename(file.release().c_str(), newpath.c_str()),
                SyscallSucceeds());
    file.reset(newpath);
  };
  CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Unchanged,
             CtimeEffect::Changed);
}

// Calling utimes on a file changes ctime and the time that we ask to change
// (atime to now in this case).
TEST(StatTimesTest, FileUtimes) {
  TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());

  auto fn = [&] {
    const struct timespec ts[2] = {{0, UTIME_NOW}, {0, UTIME_OMIT}};
    ASSERT_THAT(utimensat(AT_FDCWD, file.path().c_str(), ts, 0),
                SyscallSucceeds());
  };
  CheckTimes(file, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
             CtimeEffect::Changed);
}

// Truncating a file changes mtime and ctime.
TEST(StatTimesTest, FileTruncate) {
  const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
      TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), "yaaass", 0666));

  auto fn = [&] {
    EXPECT_THAT(truncate(file.path().c_str(), 0), SyscallSucceeds());
  };
  CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Changed,
             CtimeEffect::Changed);
}

// Writing a file changes mtime and ctime.
TEST(StatTimesTest, FileWrite) {
  const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
      TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), "yaaass", 0666));

  const FileDescriptor fd =
      ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0));

  auto fn = [&] {
    const std::string contents = "all the single dollars";
    EXPECT_THAT(WriteFd(fd.get(), contents.data(), contents.size()),
                SyscallSucceeds());
  };
  CheckTimes(file, fn, AtimeEffect::Unchanged, MtimeEffect::Changed,
             CtimeEffect::Changed);
}

// Reading a file changes atime.
TEST(StatTimesTest, FileRead) {
  const std::string contents = "bills bills bills";
  const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(
      TempPath::CreateFileWith(GetAbsoluteTestTmpdir(), contents, 0666));

  const FileDescriptor fd =
      ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY, 0));

  auto fn = [&] {
    char buf[20];
    ASSERT_THAT(ReadFd(fd.get(), buf, sizeof(buf)),
                SyscallSucceedsWithValue(contents.size()));
  };
  CheckTimes(file, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
             CtimeEffect::Unchanged);
}

// Listing files in a directory changes atime.
TEST(StatTimesTest, DirList) {
  const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
  const TempPath file =
      ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path()));

  auto fn = [&] {
    const auto contents = ASSERT_NO_ERRNO_AND_VALUE(ListDir(dir.path(), false));
    EXPECT_THAT(contents, Not(IsEmpty()));
  };
  CheckTimes(dir, fn, AtimeEffect::Changed, MtimeEffect::Unchanged,
             CtimeEffect::Unchanged);
}

}  // namespace

}  // namespace testing
}  // namespace gvisor