1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/installer/mini_installer/delete_with_retry.h"
6
7 #include <windows.h>
8
9 #include <memory>
10
11 #include "base/files/file_path.h"
12 #include "base/files/file_util.h"
13 #include "base/files/memory_mapped_file.h"
14 #include "base/files/scoped_temp_dir.h"
15 #include "testing/gmock/include/gmock/gmock.h"
16 #include "testing/gtest/include/gtest/gtest.h"
17
18 namespace mini_installer {
19
20 namespace {
21
22 // A class for mocking DeleteWithRetry's sleep hook.
23 class MockSleepHook {
24 public:
25 MockSleepHook() = default;
26 MockSleepHook(const MockSleepHook&) = delete;
27 MockSleepHook& operator=(const MockSleepHook&) = delete;
28 virtual ~MockSleepHook() = default;
29
30 MOCK_METHOD(void, Sleep, ());
31 };
32
33 // A helper for temporarily connecting a specific MockSleepHook to
34 // DeleteWithRetry's sleep hook. Only one such instance may be alive at any
35 // given time.
36 class ScopedSleepHook {
37 public:
ScopedSleepHook(MockSleepHook * hook)38 explicit ScopedSleepHook(MockSleepHook* hook) : hook_(hook) {
39 EXPECT_EQ(SetRetrySleepHookForTesting(&ScopedSleepHook::SleepHook, this),
40 nullptr);
41 }
42 ScopedSleepHook(const ScopedSleepHook&) = delete;
43 ScopedSleepHook& operator=(const ScopedSleepHook&) = delete;
~ScopedSleepHook()44 ~ScopedSleepHook() {
45 EXPECT_EQ(SetRetrySleepHookForTesting(nullptr, nullptr),
46 &ScopedSleepHook::SleepHook);
47 }
48
49 private:
SleepHook(void * context)50 static void SleepHook(void* context) {
51 reinterpret_cast<ScopedSleepHook*>(context)->DoSleep();
52 }
DoSleep()53 void DoSleep() { hook_->Sleep(); }
54
55 MockSleepHook* hook_;
56 };
57
58 } // namespace
59
60 class DeleteWithRetryTest : public ::testing::Test {
61 protected:
62 DeleteWithRetryTest() = default;
63
64 // ::testing::Test:
SetUp()65 void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
TearDown()66 void TearDown() override { EXPECT_TRUE(temp_dir_.Delete()); }
67
TestDir() const68 const base::FilePath& TestDir() const { return temp_dir_.GetPath(); }
69
70 base::ScopedTempDir temp_dir_;
71 };
72
73 // Tests that deleting an item in a directory that doesn't exist succeeds.
TEST_F(DeleteWithRetryTest,DeleteNoDir)74 TEST_F(DeleteWithRetryTest, DeleteNoDir) {
75 int attempts = 0;
76 EXPECT_TRUE(DeleteWithRetry(TestDir()
77 .Append(FILE_PATH_LITERAL("nodir"))
78 .Append(FILE_PATH_LITERAL("noitem"))
79 .value()
80 .c_str(),
81 attempts));
82 EXPECT_EQ(attempts, 1);
83 }
84
85 // Tests that deleting an item that doesn't exist in a directory that does exist
86 // succeeds.
TEST_F(DeleteWithRetryTest,DeleteNoFile)87 TEST_F(DeleteWithRetryTest, DeleteNoFile) {
88 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("noitem"));
89 int attempts = 0;
90 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
91 EXPECT_EQ(attempts, 1);
92 }
93
94 // Tests that deleting a file succeeds.
TEST_F(DeleteWithRetryTest,DeleteFile)95 TEST_F(DeleteWithRetryTest, DeleteFile) {
96 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
97 ASSERT_TRUE(base::WriteFile(path, base::StringPiece()));
98 int attempts = 0;
99 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
100 EXPECT_GE(attempts, 1);
101 EXPECT_FALSE(base::PathExists(path));
102 }
103
104 // Tests that deleting a read-only file succeeds.
TEST_F(DeleteWithRetryTest,DeleteReadonlyFile)105 TEST_F(DeleteWithRetryTest, DeleteReadonlyFile) {
106 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
107 ASSERT_TRUE(base::WriteFile(path, base::StringPiece()));
108 DWORD attributes = ::GetFileAttributes(path.value().c_str());
109 ASSERT_NE(attributes, INVALID_FILE_ATTRIBUTES) << ::GetLastError();
110 ASSERT_NE(::SetFileAttributes(path.value().c_str(),
111 attributes | FILE_ATTRIBUTE_READONLY),
112 0)
113 << ::GetLastError();
114 int attempts = 0;
115 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
116 EXPECT_GE(attempts, 1);
117 EXPECT_FALSE(base::PathExists(path));
118 }
119
120 // Tests that deleting an empty directory succeeds.
TEST_F(DeleteWithRetryTest,DeleteEmptyDir)121 TEST_F(DeleteWithRetryTest, DeleteEmptyDir) {
122 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("dir"));
123 ASSERT_TRUE(base::CreateDirectory(path));
124 int attempts = 0;
125 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
126 EXPECT_GE(attempts, 1);
127 EXPECT_FALSE(base::PathExists(path));
128 }
129
130 // Tests that deleting a non-empty directory fails.
TEST_F(DeleteWithRetryTest,DeleteNonEmptyDir)131 TEST_F(DeleteWithRetryTest, DeleteNonEmptyDir) {
132 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("dir"));
133 ASSERT_TRUE(base::CreateDirectory(path));
134 ASSERT_TRUE(base::WriteFile(path.Append(FILE_PATH_LITERAL("file")),
135 base::StringPiece()));
136 {
137 ::testing::StrictMock<MockSleepHook> mock_hook;
138 ScopedSleepHook hook(&mock_hook);
139 int attempts = 0;
140 EXPECT_CALL(mock_hook, Sleep()).Times(99);
141 ASSERT_FALSE(DeleteWithRetry(path.value().c_str(), attempts));
142 EXPECT_EQ(attempts, 100);
143 }
144 EXPECT_TRUE(base::PathExists(path));
145 }
146
147 // Tests that deleting a non-empty directory succeeds once a file within is
148 // deleted.
TEST_F(DeleteWithRetryTest,DeleteDirThatEmpties)149 TEST_F(DeleteWithRetryTest, DeleteDirThatEmpties) {
150 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("dir"));
151 ASSERT_TRUE(base::CreateDirectory(path));
152 const base::FilePath file = path.Append(FILE_PATH_LITERAL("file"));
153 ASSERT_TRUE(base::WriteFile(file, base::StringPiece()));
154 {
155 ::testing::NiceMock<MockSleepHook> mock_hook;
156 ScopedSleepHook hook(&mock_hook);
157 int attempts = 0;
158 EXPECT_CALL(mock_hook, Sleep()).WillOnce([&file]() {
159 ::DeleteFile(file.value().c_str());
160 });
161 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
162 EXPECT_LT(attempts, 100);
163 }
164 EXPECT_FALSE(base::PathExists(path));
165 }
166
167 // Tests that deleting a file mapped into a process's address space triggers
168 // a retry that succeeds after the file is closed.
TEST_F(DeleteWithRetryTest,DeleteMappedFile)169 TEST_F(DeleteWithRetryTest, DeleteMappedFile) {
170 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
171 ASSERT_TRUE(base::WriteFile(path, base::StringPiece("i miss you")));
172
173 // Open the file for read-only access; allowing others to do anything.
174 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
175 base::File::FLAG_SHARE_DELETE);
176 ASSERT_TRUE(file.IsValid()) << file.error_details();
177
178 // Map the file into the process's address space, thereby preventing deletes.
179 auto mapped_file = std::make_unique<base::MemoryMappedFile>();
180 ASSERT_TRUE(mapped_file->Initialize(std::move(file)));
181
182 // Try to delete the file, expecting that a retry-induced sleep takes place.
183 // Unmap and close the file when that happens so that the retry succeeds.
184 {
185 ::testing::NiceMock<MockSleepHook> mock_hook;
186 EXPECT_CALL(mock_hook, Sleep()).WillOnce([&mapped_file]() {
187 mapped_file.reset();
188 });
189 ScopedSleepHook hook(&mock_hook);
190 int attempts = 0;
191 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
192 EXPECT_GE(attempts, 2);
193 }
194 EXPECT_FALSE(base::PathExists(path));
195 }
196
197 // Tests that deleting a file with an open handle succeeds after the file is
198 // closed.
TEST_F(DeleteWithRetryTest,DeleteInUseFile)199 TEST_F(DeleteWithRetryTest, DeleteInUseFile) {
200 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
201 ASSERT_TRUE(base::WriteFile(path, base::StringPiece("i miss you")));
202
203 // Open the file for read-only access; allowing others to do anything.
204 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
205 base::File::FLAG_SHARE_DELETE);
206 ASSERT_TRUE(file.IsValid()) << file.error_details();
207
208 // Try to delete the file, expecting that a retry-induced sleep takes place.
209 // Close the file when that happens so that the retry succeeds.
210 {
211 ::testing::NiceMock<MockSleepHook> mock_hook;
212 EXPECT_CALL(mock_hook, Sleep()).WillOnce([&file]() { file.Close(); });
213 ScopedSleepHook hook(&mock_hook);
214 int attempts = 0;
215 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
216 EXPECT_GE(attempts, 2);
217 }
218 EXPECT_FALSE(base::PathExists(path));
219 }
220
221 // Test that a read-only file that cannot be opened for deletion takes at least
222 // one retry.
TEST_F(DeleteWithRetryTest,DeleteReadOnlyNoSharing)223 TEST_F(DeleteWithRetryTest, DeleteReadOnlyNoSharing) {
224 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
225 ASSERT_TRUE(base::WriteFile(path, base::StringPiece("i miss you")));
226
227 // Make it read-only.
228 DWORD attributes = ::GetFileAttributes(path.value().c_str());
229 ASSERT_NE(attributes, INVALID_FILE_ATTRIBUTES) << ::GetLastError();
230 ASSERT_NE(::SetFileAttributes(path.value().c_str(),
231 attributes | FILE_ATTRIBUTE_READONLY),
232 0)
233 << ::GetLastError();
234
235 // Open the file for read-only access; allowing others to do anything.
236 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
237 base::File::FLAG_SHARE_DELETE);
238 ASSERT_TRUE(file.IsValid()) << file.error_details();
239
240 // Try to delete the file, expecting that a retry-induced sleep takes place.
241 // Close the file so that a retry succeeds.
242 {
243 ::testing::NiceMock<MockSleepHook> mock_hook;
244 EXPECT_CALL(mock_hook, Sleep()).WillOnce([&file]() { file.Close(); });
245 ScopedSleepHook hook(&mock_hook);
246 int attempts = 0;
247 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
248 EXPECT_GT(attempts, 1);
249 }
250 EXPECT_FALSE(base::PathExists(path));
251 }
252
253 // Tests that deleting fails after all retries are used up.
TEST_F(DeleteWithRetryTest,MaxRetries)254 TEST_F(DeleteWithRetryTest, MaxRetries) {
255 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
256 ASSERT_TRUE(base::WriteFile(path, base::StringPiece("i miss you")));
257
258 // Open the file for read-only access without allowing deletes.
259 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
260 ASSERT_TRUE(file.IsValid()) << file.error_details();
261
262 // Expect all 100 attempts to fail, with 99 sleeps betwixt them.
263 {
264 ::testing::StrictMock<MockSleepHook> mock_hook;
265 ScopedSleepHook hook(&mock_hook);
266 int attempts = 0;
267 EXPECT_CALL(mock_hook, Sleep()).Times(99);
268 ASSERT_FALSE(DeleteWithRetry(path.value().c_str(), attempts));
269 EXPECT_EQ(attempts, 100);
270 }
271 EXPECT_TRUE(base::PathExists(path));
272 }
273
274 // Test that success on the last retry is reported correctly.
TEST_F(DeleteWithRetryTest,LastRetrySucceeds)275 TEST_F(DeleteWithRetryTest, LastRetrySucceeds) {
276 const base::FilePath path = TestDir().Append(FILE_PATH_LITERAL("file"));
277 ASSERT_TRUE(base::WriteFile(path, base::StringPiece("i miss you")));
278
279 // Open the file for read-only access; allowing others to do anything.
280 base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
281 base::File::FLAG_SHARE_DELETE);
282 ASSERT_TRUE(file.IsValid()) << file.error_details();
283
284 // Try to delete the file, expecting that a retry-induced sleep takes place.
285 // Close the file on the 99th retry so that the last attempt succeeds.
286 {
287 ::testing::InSequence sequence;
288 ::testing::StrictMock<MockSleepHook> mock_hook;
289 EXPECT_CALL(mock_hook, Sleep()).Times(98);
290 EXPECT_CALL(mock_hook, Sleep()).WillOnce([&file]() { file.Close(); });
291 ScopedSleepHook hook(&mock_hook);
292 int attempts = 0;
293 ASSERT_TRUE(DeleteWithRetry(path.value().c_str(), attempts));
294 EXPECT_EQ(attempts, 100);
295 }
296 EXPECT_FALSE(base::PathExists(path));
297 }
298
299 } // namespace mini_installer
300