Ninja
jobserver_test.cc
Go to the documentation of this file.
1 // Copyright 2024 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "jobserver.h"
16 
17 #include "test.h"
18 
19 #ifndef _WIN32
20 #include <fcntl.h>
21 #include <unistd.h>
22 #endif
23 
24 namespace {
25 
26 #ifndef _WIN32
27 struct ScopedTestFd {
28  explicit ScopedTestFd(int fd) : fd_(fd) {}
29 
30  ~ScopedTestFd() {
31  if (IsValid())
32  ::close(fd_);
33  }
34 
35  bool IsValid() const { return fd_ >= 0; }
36 
37  int fd_ = -1;
38 };
39 #endif // !_WIN32
40 
41 } // namespace
42 
43 TEST(Jobserver, SlotTest) {
44  // Default construction.
45  Jobserver::Slot slot;
46  EXPECT_FALSE(slot.IsValid());
47 
48  // Construct implicit slot
50  EXPECT_TRUE(slot0.IsValid());
51  EXPECT_TRUE(slot0.IsImplicit());
52  EXPECT_FALSE(slot0.IsExplicit());
53 
54  // Construct explicit slots
55  auto slot1 = Jobserver::Slot::CreateExplicit(10u);
56  EXPECT_TRUE(slot1.IsValid());
57  EXPECT_FALSE(slot1.IsImplicit());
58  EXPECT_TRUE(slot1.IsExplicit());
59  EXPECT_EQ(10u, slot1.GetExplicitValue());
60 
61  auto slot2 = Jobserver::Slot::CreateExplicit(42u);
62  EXPECT_TRUE(slot2.IsValid());
63  EXPECT_FALSE(slot2.IsImplicit());
64  EXPECT_TRUE(slot2.IsExplicit());
65  EXPECT_EQ(42u, slot2.GetExplicitValue());
66 
67  // Move operation.
68  slot2 = std::move(slot1);
69  EXPECT_FALSE(slot1.IsValid());
70  EXPECT_TRUE(slot2.IsValid());
71  EXPECT_TRUE(slot2.IsExplicit());
72  ASSERT_EQ(10u, slot2.GetExplicitValue());
73 
74  slot1 = std::move(slot0);
75  EXPECT_FALSE(slot0.IsValid());
76  EXPECT_TRUE(slot1.IsValid());
77  EXPECT_TRUE(slot1.IsImplicit());
78  EXPECT_FALSE(slot1.IsExplicit());
79 }
80 
81 TEST(Jobserver, ParseMakeFlagsValue) {
82  Jobserver::Config config;
83  std::string error;
84 
85  // Passing nullptr does not crash.
86  config = {};
87  error.clear();
88  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(nullptr, &config, &error));
89  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
90 
91  // Passing an empty string does not crash.
92  config = {};
93  error.clear();
94  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("", &config, &error));
95  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
96 
97  // Passing a string that only contains whitespace does not crash.
98  config = {};
99  error.clear();
100  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" \t", &config, &error));
101  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
102 
103  // Passing an `n` in the first word reports no mode.
104  config = {};
105  error.clear();
106  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("kns --jobserver-auth=fifo:foo",
107  &config, &error));
108  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
109 
110  // Passing "--jobserver-auth=fifo:<path>" works.
111  config = {};
112  error.clear();
113  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=fifo:foo",
114  &config, &error));
115  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
116  EXPECT_EQ("foo", config.path);
117 
118  // Passing an initial " -j" or " -j<count>" works.
119  config = {};
120  error.clear();
121  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" -j --jobserver-auth=fifo:foo",
122  &config, &error));
123  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
124  EXPECT_EQ("foo", config.path);
125 
126  // Passing an initial " -j<count>" works.
127  config = {};
128  error.clear();
129  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" -j10 --jobserver-auth=fifo:foo",
130  &config, &error));
131  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
132  EXPECT_EQ("foo", config.path);
133 
134  // Passing an `n` in the first word _after_ a dash works though, i.e.
135  // It is not interpreted as GNU Make dry-run flag.
136  config = {};
137  error.clear();
138  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(
139  "-one-flag --jobserver-auth=fifo:foo", &config, &error));
140  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
141 
142  config = {};
143  error.clear();
144  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=semaphore_name",
145  &config, &error));
146  EXPECT_EQ(Jobserver::Config::kModeWin32Semaphore, config.mode);
147  EXPECT_EQ("semaphore_name", config.path);
148 
149  config = {};
150  error.clear();
151  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=10,42", &config,
152  &error));
153  EXPECT_EQ(Jobserver::Config::kModePipe, config.mode);
154 
155  config = {};
156  error.clear();
157  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=-1,42", &config,
158  &error));
159  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
160 
161  config = {};
162  error.clear();
163  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=10,-42", &config,
164  &error));
165  EXPECT_EQ(Jobserver::Config::kModeNone, config.mode);
166 
167  config = {};
168  error.clear();
169  ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(
170  "--jobserver-auth=10,42 --jobserver-fds=12,44 "
171  "--jobserver-auth=fifo:/tmp/fifo",
172  &config, &error));
173  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
174  EXPECT_EQ("/tmp/fifo", config.path);
175 
176  config = {};
177  error.clear();
178  ASSERT_FALSE(
179  Jobserver::ParseMakeFlagsValue("--jobserver-fds=10,", &config, &error));
180  EXPECT_EQ("Invalid file descriptor pair [10,]", error);
181 }
182 
183 TEST(Jobserver, ParseNativeMakeFlagsValue) {
184  Jobserver::Config config;
185  std::string error;
186 
187  // --jobserver-auth=R,W is not supported.
188  config = {};
189  error.clear();
190  EXPECT_FALSE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=3,4",
191  &config, &error));
192  EXPECT_EQ(error, "Pipe-based protocol is not supported!");
193 
194 #ifdef _WIN32
195  // --jobserver-auth=NAME works on Windows.
196  config = {};
197  error.clear();
199  "--jobserver-auth=semaphore_name", &config, &error));
200  EXPECT_EQ(Jobserver::Config::kModeWin32Semaphore, config.mode);
201  EXPECT_EQ("semaphore_name", config.path);
202 
203  // --jobserver-auth=fifo:PATH does not work on Windows.
204  config = {};
205  error.clear();
206  ASSERT_FALSE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=fifo:foo",
207  &config, &error));
208  EXPECT_EQ(error, "FIFO mode is not supported on Windows!");
209 #else // !_WIN32
210  // --jobserver-auth=NAME does not work on Posix
211  config = {};
212  error.clear();
214  "--jobserver-auth=semaphore_name", &config, &error));
215  EXPECT_EQ(error, "Semaphore mode is not supported on Posix!");
216 
217  // --jobserver-auth=fifo:PATH works on Posix
218  config = {};
219  error.clear();
220  ASSERT_TRUE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=fifo:foo",
221  &config, &error));
222  EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode);
223  EXPECT_EQ("foo", config.path);
224 #endif // !_WIN32
225 }
226 
227 TEST(Jobserver, NullJobserver) {
228  Jobserver::Config config;
229  ASSERT_EQ(Jobserver::Config::kModeNone, config.mode);
230 
231  std::string error;
232  std::unique_ptr<Jobserver::Client> client =
233  Jobserver::Client::Create(config, &error);
234  EXPECT_FALSE(client.get());
235  EXPECT_EQ("Unsupported jobserver mode", error);
236 }
237 
238 #ifdef _WIN32
239 
240 #include <windows.h>
241 
242 // Scoped HANDLE class for the semaphore.
243 struct ScopedSemaphoreHandle {
244  ScopedSemaphoreHandle(HANDLE handle) : handle_(handle) {}
245  ~ScopedSemaphoreHandle() {
246  if (handle_)
247  ::CloseHandle(handle_);
248  }
249  HANDLE get() const { return handle_; }
250 
251  private:
252  HANDLE handle_ = NULL;
253 };
254 
255 TEST(Jobserver, Win32SemaphoreClient) {
256  // Create semaphore with initial token count.
257  const size_t kExplicitCount = 10;
258  const char kSemaphoreName[] = "ninja_test_jobserver_semaphore";
259  ScopedSemaphoreHandle handle(
260  ::CreateSemaphoreA(NULL, static_cast<DWORD>(kExplicitCount),
261  static_cast<DWORD>(kExplicitCount), kSemaphoreName));
262  ASSERT_TRUE(handle.get()) << GetLastErrorString();
263 
264  // Create new client instance.
265  Jobserver::Config config;
267  config.path = kSemaphoreName;
268 
269  std::string error;
270  std::unique_ptr<Jobserver::Client> client =
271  Jobserver::Client::Create(config, &error);
272  EXPECT_TRUE(client.get()) << error;
273  EXPECT_TRUE(error.empty()) << error;
274 
275  Jobserver::Slot slot;
276  std::vector<Jobserver::Slot> slots;
277 
278  // Read the implicit slot.
279  slot = client->TryAcquire();
280  EXPECT_TRUE(slot.IsValid());
281  EXPECT_TRUE(slot.IsImplicit());
282  slots.push_back(std::move(slot));
283 
284  // Read the explicit slots.
285  for (size_t n = 0; n < kExplicitCount; ++n) {
286  slot = client->TryAcquire();
287  EXPECT_TRUE(slot.IsValid());
288  EXPECT_TRUE(slot.IsExplicit());
289  slots.push_back(std::move(slot));
290  }
291 
292  // Pool should be empty now.
293  slot = client->TryAcquire();
294  EXPECT_FALSE(slot.IsValid());
295 
296  // Release the slots again.
297  while (!slots.empty()) {
298  client->Release(std::move(slots.back()));
299  slots.pop_back();
300  }
301 
302  slot = client->TryAcquire();
303  EXPECT_TRUE(slot.IsValid());
304  EXPECT_TRUE(slot.IsImplicit());
305  slots.push_back(std::move(slot));
306 
307  for (size_t n = 0; n < kExplicitCount; ++n) {
308  slot = client->TryAcquire();
309  EXPECT_TRUE(slot.IsValid());
310  EXPECT_TRUE(slot.IsExplicit()) << n;
311  slots.push_back(std::move(slot));
312  }
313 
314  // And the pool should be empty again.
315  slot = client->TryAcquire();
316  EXPECT_FALSE(slot.IsValid());
317 }
318 #else // !_WIN32
319 TEST(Jobserver, PosixFifoClient) {
320  ScopedTempDir temp_dir;
321  temp_dir.CreateAndEnter("ninja_test_jobserver_fifo");
322 
323  // Create the Fifo, then write kSlotCount slots into it.
324  std::string fifo_path = temp_dir.temp_dir_name_ + "fifo";
325  int ret = mknod(fifo_path.c_str(), S_IFIFO | 0666, 0);
326  ASSERT_EQ(0, ret) << "Could not create FIFO at: " << fifo_path;
327 
328  const size_t kSlotCount = 5;
329 
330  ScopedTestFd write_fd(::open(fifo_path.c_str(), O_RDWR));
331  ASSERT_TRUE(write_fd.IsValid()) << "Cannot open FIFO at: " << strerror(errno);
332  for (size_t n = 0; n < kSlotCount; ++n) {
333  uint8_t slot_byte = static_cast<uint8_t>('0' + n);
334  ssize_t ret = ::write(write_fd.fd_, &slot_byte, 1);
335  (void)ret; // make compiler happy
336  }
337  // Keep the file descriptor opened to ensure the fifo's content
338  // persists in kernel memory.
339 
340  // Create new client instance.
341  Jobserver::Config config;
343  config.path = fifo_path;
344 
345  std::string error;
346  std::unique_ptr<Jobserver::Client> client =
347  Jobserver::Client::Create(config, &error);
348  EXPECT_TRUE(client.get());
349  EXPECT_TRUE(error.empty()) << error;
350 
351  // Read slots from the pool, and store them
352  std::vector<Jobserver::Slot> slots;
353 
354  // First slot is always implicit.
355  slots.push_back(client->TryAcquire());
356  ASSERT_TRUE(slots.back().IsValid());
357  EXPECT_TRUE(slots.back().IsImplicit());
358 
359  // Then read kSlotCount slots from the pipe and verify their value.
360  for (size_t n = 0; n < kSlotCount; ++n) {
361  Jobserver::Slot slot = client->TryAcquire();
362  ASSERT_TRUE(slot.IsValid()) << "Slot #" << n + 1;
363  EXPECT_EQ(static_cast<uint8_t>('0' + n), slot.GetExplicitValue());
364  slots.push_back(std::move(slot));
365  }
366 
367  // Pool should be empty now, so next TryAcquire() will fail.
368  Jobserver::Slot slot = client->TryAcquire();
369  EXPECT_FALSE(slot.IsValid());
370 }
371 
372 TEST(Jobserver, PosixFifoClientWithWrongPath) {
373  ScopedTempDir temp_dir;
374  temp_dir.CreateAndEnter("ninja_test_jobserver_fifo");
375 
376  // Create a regular file.
377  std::string file_path = temp_dir.temp_dir_name_ + "not_a_fifo";
378  int fd = ::open(file_path.c_str(), O_CREAT | O_RDWR, 0660);
379  ASSERT_GE(fd, 0) << "Could not create file: " << strerror(errno);
380  ::close(fd);
381 
382  // Create new client instance, passing the file path for the fifo.
383  Jobserver::Config config;
385  config.path = file_path;
386 
387  std::string error;
388  std::unique_ptr<Jobserver::Client> client =
389  Jobserver::Client::Create(config, &error);
390  EXPECT_FALSE(client.get());
391  EXPECT_FALSE(error.empty());
392  EXPECT_EQ("Not a fifo path: " + file_path, error);
393 
394  // Do the same with an empty file path.
395  error.clear();
396  config.path.clear();
397  client = Jobserver::Client::Create(config, &error);
398  EXPECT_FALSE(client.get());
399  EXPECT_FALSE(error.empty());
400  EXPECT_EQ("Empty fifo path", error);
401 }
402 #endif // !_WIN32
static std::unique_ptr< Client > Create(const Config &, std::string *error)
Create a new Client instance from a given configuration.
TEST(Jobserver, SlotTest)
A Jobserver::Config models how to access or implement a GNU jobserver implementation.
Definition: jobserver.h:106
std::string path
For kModeFifo, this is the path to the Unix FIFO to use.
Definition: jobserver.h:146
Mode mode
Implementation mode for the pool.
Definition: jobserver.h:142
A Jobserver::Slot models a single job slot that can be acquired from.
Definition: jobserver.h:55
uint8_t GetExplicitValue() const
Return value of an explicit slot.
Definition: jobserver.cc:64
bool IsExplicit() const
Return true if this instance represents an explicit job slot.
Definition: jobserver.h:82
static Slot CreateImplicit()
Create instance for the implicit value.
Definition: jobserver.h:94
static Slot CreateExplicit(uint8_t value)
Create instance for explicit byte value.
Definition: jobserver.h:89
bool IsImplicit() const
Return true if this instance represents an implicit job slot.
Definition: jobserver.h:79
bool IsValid() const
Return true if this instance is valid, i.e.
Definition: jobserver.h:76
Jobserver provides types related to managing a pool of "job slots" using the GNU Make jobserver ptoco...
Definition: jobserver.h:28
static bool ParseNativeMakeFlagsValue(const char *makeflags_env, Config *config, std::string *error)
A variant of ParseMakeFlagsValue() that will return an error if the parsed result is not compatible w...
Definition: jobserver.cc:186
static bool ParseMakeFlagsValue(const char *makeflags_env, Config *config, std::string *error)
Parse the value of a MAKEFLAGS environment variable.
Definition: jobserver.cc:69
void CreateAndEnter(const std::string &name)
Create a temporary directory and chdir into it.
Definition: test.cc:199
std::string temp_dir_name_
The subdirectory name for our dir, or empty if it hasn't been set up.
Definition: test.h:100