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
24namespace {
25
26#ifndef _WIN32
27struct 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
43TEST(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
81TEST(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();
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));
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();
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
183TEST(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));
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
227TEST(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.
243struct 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
255TEST(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
319TEST(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
372TEST(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