99#include < stdexcept>
1010#include < thread>
1111
12- // How long to wait for the callback to be called
13- #define THREAD_WAIT_TIME_MS 50
12+ // A unit for small waits in test actions.
13+ constexpr int kThreadWaitTimeMs = 50 ;
14+ // Wait long enough to confirm no change occurred.
15+ constexpr int kNoChangeWaitMs = kThreadWaitTimeMs * 2 ;
16+ // Max wait to confirm a change occurred.
17+ // We don't have to wait this long due to kPollIntervalMs below,
18+ // So early stoppage on an action almost always make tests faster.
19+ constexpr int kCallbackMaxWaitMs = kThreadWaitTimeMs * 20 ;
20+ // A small delay between repeated file replacements.
21+ constexpr int kBetweenReplacementsWaitMs = kThreadWaitTimeMs ;
22+ // Wait long enough for safe-save rename callback to occur.
23+ constexpr int kSafeSaveWaitMs = kThreadWaitTimeMs * 4 ;
24+ // Polling avoids a fixed long sleep so tests can finish early when callbacks
25+ // are fast. eg. we dont need to wait for the full kCallbackMaxWaitMs
26+ // if action returns true early.
27+ constexpr int kPollIntervalMs = 5 ;
1428
1529// Helper function to simulate file modification
1630
@@ -47,8 +61,7 @@ void safeSaveFile(const std::string &path, const std::string &content) {
4761 ec.clear ();
4862 std::filesystem::rename (tempPath, original, ec);
4963 if (ec) {
50- throw std::runtime_error (" Failed to rename temp file: " +
51- ec.message ());
64+ throw std::runtime_error (" Failed to rename temp file: " + ec.message ());
5265 }
5366}
5467
@@ -71,90 +84,115 @@ class FileWatcherTest : public ::testing::Test {
7184};
7285
7386TEST_F (FileWatcherTest, NoChangeCallbackNotCalled) {
74- bool callbackCalled = false ;
75- auto callback = [&callbackCalled]() { callbackCalled = true ; };
87+ std::atomic< bool > callbackCalled{ false } ;
88+ auto callback = [&callbackCalled]() { callbackCalled. store ( true ) ; };
7689 createFile (testFilePath, " New content" );
7790 createFile (differentFilePath, " Different content" );
78- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
91+ std::this_thread::sleep_for (std::chrono::milliseconds (kNoChangeWaitMs ));
7992
8093 auto watcher = filewatcher_factory::createFileWatcher ();
8194 watcher->startWatching (testFilePath, callback);
8295 appendToFile (differentFilePath, " New content" );
83- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
96+ std::this_thread::sleep_for (std::chrono::milliseconds (kNoChangeWaitMs ));
8497 watcher->stopWatching ();
8598
86- EXPECT_FALSE (callbackCalled);
99+ EXPECT_FALSE (callbackCalled. load () );
87100}
88101
89102TEST_F (FileWatcherTest, FileModifiedCallbackCalled) {
90- bool callbackCalled = false ;
91- auto callback = [&callbackCalled]() { callbackCalled = true ; };
103+ std::atomic< bool > callbackCalled{ false } ;
104+ auto callback = [&callbackCalled]() { callbackCalled. store ( true ) ; };
92105 createFile (testFilePath, " New content" );
93106
94107 auto watcher = filewatcher_factory::createFileWatcher ();
95108 watcher->startWatching (testFilePath, callback);
96- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
109+ std::this_thread::sleep_for (std::chrono::milliseconds (kThreadWaitTimeMs ));
97110 appendToFile (testFilePath, " New content" );
98- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS));
111+
112+ // This one sometimes takes a bit longer to trigger eg. on Mac
113+ // so use kCallbackMaxWaitMs.
114+ // But usually it just finishes really early
115+ for (int waitedMs = 0 ;
116+ waitedMs < kCallbackMaxWaitMs && !callbackCalled.load ();
117+ waitedMs += kPollIntervalMs ) {
118+ std::this_thread::sleep_for (std::chrono::milliseconds (kPollIntervalMs ));
119+ }
99120 watcher->stopWatching ();
100121
101- EXPECT_TRUE (callbackCalled);
122+ EXPECT_TRUE (callbackCalled. load () );
102123}
103124
104125TEST_F (FileWatcherTest, FileDeletedAndReplacedCallbackCalled) {
105- bool callbackCalled = false ;
106- auto callback = [&callbackCalled]() { callbackCalled = true ; };
126+ std::atomic< bool > callbackCalled{ false } ;
127+ auto callback = [&callbackCalled]() { callbackCalled. store ( true ) ; };
107128 createFile (testFilePath, " New content" );
108129
109130 auto watcher = filewatcher_factory::createFileWatcher ();
110131 watcher->startWatching (testFilePath, callback);
111- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
132+ std::this_thread::sleep_for (std::chrono::milliseconds (kThreadWaitTimeMs ));
112133 replaceFile (testFilePath, " Replacement content" );
113- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS));
134+
135+ // This one sometimes takes a bit longer to trigger eg. on Mac
136+ // so use kCallbackMaxWaitMs.
137+ // But usually it just finishes really early
138+ for (int waitedMs = 0 ;
139+ waitedMs < kCallbackMaxWaitMs && !callbackCalled.load ();
140+ waitedMs += kPollIntervalMs ) {
141+ std::this_thread::sleep_for (std::chrono::milliseconds (kPollIntervalMs ));
142+ }
114143 watcher->stopWatching ();
115144
116- EXPECT_TRUE (callbackCalled);
145+ EXPECT_TRUE (callbackCalled. load () );
117146}
118147
119148TEST_F (FileWatcherTest, FileReplacedMultipleTimesCallbackCalled) {
120- int callbackCount = 0 ;
121- auto callback = [&callbackCount]() { callbackCount++ ; };
149+ std::atomic< int > callbackCount{ 0 } ;
150+ auto callback = [&callbackCount]() { callbackCount. fetch_add ( 1 ) ; };
122151
123152 createFile (testFilePath, " New content" );
124153 auto watcher = filewatcher_factory::createFileWatcher ();
125154 watcher->startWatching (testFilePath, callback);
126- std::this_thread::sleep_for (std::chrono::milliseconds (50 ));
155+ std::this_thread::sleep_for (std::chrono::milliseconds (kThreadWaitTimeMs ));
127156 for (int i = 0 ; i < 10 ; ++i) {
128157 replaceFile (testFilePath, " Content " + std::to_string (i));
129158 std::this_thread::sleep_for (
130- std::chrono::milliseconds (50 )); // Wait a bit between replacements
159+ std::chrono::milliseconds (kBetweenReplacementsWaitMs ));
160+ }
161+ for (int waitedMs = 0 ;
162+ waitedMs < kCallbackMaxWaitMs && callbackCount.load () < 10 ;
163+ waitedMs += kPollIntervalMs ) {
164+ std::this_thread::sleep_for (std::chrono::milliseconds (kPollIntervalMs ));
131165 }
132166 watcher->stopWatching ();
133167
134- EXPECT_GE (callbackCount, 10 ); // Ensure callback was called at least once
168+ EXPECT_GE (callbackCount.load (),
169+ 10 ); // Ensure callback was called at least once
135170}
136171
137- // This can be Windows only for now
172+ // Windows/Linux only for now.
138173// In any case the Shader Compiler will raise if it can't find the file
139- // So this doesn't seem to be too important eg. on Mac
140- #if defined(_WIN32)
174+ // So this doesn't seem to be too important eg. on Mac.
175+ // maybe macOS FSEvents stream doesn’t guarantee a clean “removed” flag for
176+ // every delete, and deletes are maybe reported as a rename (move to trash) or
177+ // maybe a metadata change before the remove flag appears??
178+ #if defined(_WIN32) || defined(__linux__)
141179TEST_F (FileWatcherTest, FileDeletedDoesNotTriggerCallback) {
142180 bool callbackCalled = false ;
143181 auto callback = [&callbackCalled]() { callbackCalled = true ; };
144182 createFile (testFilePath, " Initial content" );
145183
146184 auto watcher = filewatcher_factory::createFileWatcher ();
147185 watcher->startWatching (testFilePath, callback);
148- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
186+ std::this_thread::sleep_for (std::chrono::milliseconds (kThreadWaitTimeMs ));
149187 std::remove (testFilePath.c_str ());
150- std::this_thread::sleep_for (
151- std::chrono::milliseconds (THREAD_WAIT_TIME_MS * 2 ));
188+ std::this_thread::sleep_for (std::chrono::milliseconds (kNoChangeWaitMs ));
152189 watcher->stopWatching ();
153190
154191 EXPECT_FALSE (callbackCalled);
155192}
156193
157194TEST_F (FileWatcherTest, SafeSaveRenameCallbackSeesFile) {
195+ // expects at least one callback and no “file couldn’t open” failures
158196 std::atomic<int > callbackCount{0 };
159197 std::atomic<int > failedOpenCount{0 };
160198 auto callback = [&]() {
@@ -169,10 +207,14 @@ TEST_F(FileWatcherTest, SafeSaveRenameCallbackSeesFile) {
169207 createFile (testFilePath, " Initial content" );
170208 auto watcher = filewatcher_factory::createFileWatcher ();
171209 watcher->startWatching (testFilePath, callback);
172- std::this_thread::sleep_for (std::chrono::milliseconds (THREAD_WAIT_TIME_MS ));
210+ std::this_thread::sleep_for (std::chrono::milliseconds (kThreadWaitTimeMs ));
173211 safeSaveFile (testFilePath, " Updated content" );
174- std::this_thread::sleep_for (
175- std::chrono::milliseconds (THREAD_WAIT_TIME_MS * 4 ));
212+ for (int waitedMs = 0 ;
213+ waitedMs < kSafeSaveWaitMs && callbackCount.load () == 0 &&
214+ failedOpenCount.load () == 0 ;
215+ waitedMs += kPollIntervalMs ) {
216+ std::this_thread::sleep_for (std::chrono::milliseconds (kPollIntervalMs ));
217+ }
176218 watcher->stopWatching ();
177219
178220 EXPECT_EQ (failedOpenCount.load (), 0 );
0 commit comments