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