Skip to content

Commit d4f279a

Browse files
committed
jobserver: add unit tests
Implement proper testing of the MAKEFLAGS parsing, and the token acquire/release logic in the jobserver class.
1 parent 551ac60 commit d4f279a

File tree

2 files changed

+346
-0
lines changed

2 files changed

+346
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ if(BUILD_TESTING)
272272
src/edit_distance_test.cc
273273
src/explanations_test.cc
274274
src/graph_test.cc
275+
src/jobserver_test.cc
275276
src/json_test.cc
276277
src/lexer_test.cc
277278
src/manifest_parser_test.cc

src/jobserver_test.cc

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
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+
#ifdef _WIN32
18+
#include <windows.h>
19+
#else
20+
#include <fcntl.h>
21+
#include <sys/stat.h>
22+
#include <sys/types.h>
23+
#include <unistd.h>
24+
#endif
25+
26+
#include <cassert>
27+
#include <cstdlib>
28+
#include <vector>
29+
30+
#include <gtest/gtest.h>
31+
32+
/// Wrapper class to provide access to protected members of the Jobserver class.
33+
struct JobserverTest : public Jobserver {
34+
/// Forwards calls to the protected Jobserver::ParseJobserverAuth() method.
35+
bool ParseJobserverAuth(const char* type) {
36+
return Jobserver::ParseJobserverAuth(type);
37+
}
38+
39+
/// Provides access to the protected Jobserver::jobserver_name_ member.
40+
const char* JobserverName() const {
41+
return jobserver_name_.c_str();
42+
}
43+
};
44+
45+
/// Jobserver state class that provides helpers to create, configure, and remove
46+
/// "external" jobserver pools.
47+
struct JobserverTestState : public testing::Test {
48+
/// Save the initial MAKEFLAGS environment variable value to allow restoring
49+
/// it upon destruction.
50+
JobserverTestState();
51+
52+
/// Restores the MAKEFLAGS environment variable value recorded upon
53+
/// construction.
54+
~JobserverTestState();
55+
56+
/// Configure the --jobserver-auth=<type>:<name> argument in the MAKEFLAGS
57+
/// environment value.
58+
void ServerConfigure(const char* name);
59+
60+
/// Creates an external token pool with the given \a name and \a count number
61+
/// of tokens. Also configures the MAKEFLAGS environment variable with the
62+
/// correct --jobserver-auth argument to make the Jobserver class use the
63+
/// created external token pool.
64+
void ServerCreate(const char* name, size_t count);
65+
66+
/// Return the number of tokens currently available in the external token
67+
/// pool.
68+
size_t ServerCount();
69+
70+
/// Remove/close the handle to external token pool.
71+
void ServerRemove();
72+
73+
/// Wrapped jobserver object to test on.
74+
JobserverTest jobserver_;
75+
76+
/// Stored makeflags read before starting tests.
77+
const char* makeflags_ = nullptr;
78+
79+
/// Name of created external jobserver token pool.
80+
const char* name_ = nullptr;
81+
82+
#ifdef _WIN32
83+
/// Implementation of posix setenv() for windows that forwards calls to
84+
/// _putenv().
85+
int setenv(const char* name, const char* value, int _) {
86+
std::string envstring;
87+
88+
// _putenv() requires a single <name>=<value> string as argument.
89+
envstring += name;
90+
envstring += '=';
91+
envstring += value;
92+
93+
return _putenv(envstring.c_str());
94+
};
95+
96+
/// Implementation of posix unsetenv() for windows that forwards calls to
97+
/// _putenv().
98+
int unsetenv(const char *name) {
99+
/// Call _putenv() with <name>="" to unset the env variable.
100+
return setenv(name, "", 0);
101+
}
102+
103+
/// Handle of the semaphore used as external token pool.
104+
HANDLE sem_ = INVALID_HANDLE_VALUE;
105+
#else
106+
/// File descriptor of the fifo used as external token pool.
107+
int fd_ = -1;
108+
#endif
109+
};
110+
111+
JobserverTestState::JobserverTestState() {
112+
makeflags_ = getenv("MAKEFLAGS");
113+
unsetenv("MAKEFLAGS");
114+
}
115+
116+
JobserverTestState::~JobserverTestState() {
117+
if (name_ != nullptr) {
118+
ServerRemove();
119+
}
120+
121+
if (makeflags_ != nullptr) {
122+
setenv("MAKEFLAGS", makeflags_, 1);
123+
} else {
124+
unsetenv("MAKEFLAGS");
125+
}
126+
}
127+
128+
void JobserverTestState::ServerConfigure(const char* name)
129+
{
130+
std::string makeflags("--jobserver-auth=");
131+
#ifdef _WIN32
132+
makeflags += "sem:";
133+
#else
134+
makeflags += "fifo:";
135+
#endif
136+
makeflags += name;
137+
138+
assert(setenv("MAKEFLAGS", makeflags.c_str(), 1) == 0);
139+
140+
}
141+
142+
#ifdef _WIN32
143+
void JobserverTestState::ServerCreate(const char* name, size_t count) {
144+
assert(name_ == nullptr);
145+
ServerConfigure(name);
146+
name_ = name;
147+
148+
// One cannot create a semaphore with a max value of 0 on windows
149+
sem_ = CreateSemaphoreA(nullptr, count, count ? : 1, name);
150+
assert(sem_ != NULL);
151+
}
152+
153+
size_t JobserverTestState::ServerCount() {
154+
assert(name_ != nullptr);
155+
size_t count = 0;
156+
157+
// First acquire all the available tokens to count them
158+
while (WaitForSingleObject(sem_, 0) == WAIT_OBJECT_0) {
159+
count++;
160+
}
161+
162+
// Then return the acquired tokens to let the client continue
163+
ReleaseSemaphore(sem_, count, nullptr);
164+
165+
return count;
166+
}
167+
168+
void JobserverTestState::ServerRemove() {
169+
assert(name_ != nullptr);
170+
CloseHandle(sem_);
171+
name_ = nullptr;
172+
}
173+
174+
#else // _WIN32
175+
176+
void JobserverTestState::ServerCreate(const char* name, size_t count) {
177+
assert(name_ == nullptr);
178+
ServerConfigure(name);
179+
name_ = name;
180+
181+
// Create and open the fifo
182+
assert(mkfifo(name, S_IWUSR | S_IRUSR) == 0);
183+
fd_ = open(name, O_RDWR | O_NONBLOCK);
184+
assert(fd_ >= 0);
185+
186+
// Fill the fifo the requested number of tokens
187+
std::vector<char> tokens(count, '+');
188+
assert(write(fd_, tokens.data(), count) == count);
189+
}
190+
191+
size_t JobserverTestState::ServerCount() {
192+
assert(name_ != nullptr);
193+
size_t count = 0;
194+
char token;
195+
196+
// First acquire all the available tokens to count them
197+
while (read(fd_, &token, 1) == 1) {
198+
count++;
199+
}
200+
201+
// Then return the acquired tokens to let the client continue
202+
std::vector<char> tokens(count, '+');
203+
assert(write(fd_, tokens.data(), tokens.size()) == tokens.size());
204+
205+
return count;
206+
}
207+
208+
void JobserverTestState::ServerRemove() {
209+
assert(name_ != nullptr);
210+
close(fd_);
211+
fd_ = -1;
212+
unlink(name_);
213+
name_ = nullptr;
214+
}
215+
216+
#endif // _WIN32
217+
218+
TEST_F(JobserverTestState, MakeFlags) {
219+
// Test with no make flags configured
220+
ASSERT_FALSE(unsetenv("MAKEFLAGS"));
221+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
222+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem"));
223+
224+
// Test with no --jobserver-auth in make flags
225+
ASSERT_FALSE(setenv("MAKEFLAGS", "--other-arg=val", 0));
226+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
227+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem"));
228+
229+
// Test fifo type
230+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:jobserver-1.fifo", 1));
231+
ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo"));
232+
ASSERT_STREQ(jobserver_.JobserverName(), "jobserver-1.fifo");
233+
234+
// Test sem type
235+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=sem:jobserver-2-sem", 1));
236+
ASSERT_TRUE(jobserver_.ParseJobserverAuth("sem"));
237+
ASSERT_STREQ(jobserver_.JobserverName(), "jobserver-2-sem");
238+
239+
// Test preceding arguments
240+
ASSERT_FALSE(setenv("MAKEFLAGS", "--other=val --jobserver-auth=fifo:jobserver-3.fifo", 1));
241+
ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo"));
242+
ASSERT_STREQ(jobserver_.JobserverName(), "jobserver-3.fifo");
243+
244+
// Test following arguments
245+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:jobserver-4.fifo", 1));
246+
ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo"));
247+
ASSERT_STREQ(jobserver_.JobserverName(), "jobserver-4.fifo");
248+
249+
// Test surrounding arguments
250+
ASSERT_FALSE(setenv("MAKEFLAGS", "--preceeding-arg=val --jobserver-auth=fifo:jobserver-5.fifo --following-arg=val", 1));
251+
ASSERT_TRUE(jobserver_.ParseJobserverAuth("fifo"));
252+
ASSERT_STREQ(jobserver_.JobserverName(), "jobserver-5.fifo");
253+
254+
// Test invalid type
255+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=bad:jobserver-6", 1));
256+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
257+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem"));
258+
259+
// Test missing type
260+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=", 1));
261+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
262+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("sem"));
263+
264+
// Test missing colon
265+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo", 1));
266+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
267+
268+
// Test missing colon following by another argument
269+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo --other-arg=val", 1));
270+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
271+
272+
// Test missing colon following by another argument with a colon
273+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo --other-arg=val0:val1", 1));
274+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
275+
276+
// Test missing value
277+
ASSERT_FALSE(setenv("MAKEFLAGS", "--jobserver-auth=fifo:", 1));
278+
ASSERT_FALSE(jobserver_.ParseJobserverAuth("fifo"));
279+
}
280+
281+
TEST_F(JobserverTestState, InitNoServer) {
282+
// Verify that the jobserver isn't enabled when no configuration is given
283+
jobserver_.Init();
284+
ASSERT_FALSE(jobserver_.Enabled());
285+
}
286+
287+
TEST_F(JobserverTestState, InitServer) {
288+
// Verify that the jobserver is enabled when a (valid) configuration is given
289+
ServerCreate("jobserver-init", 0);
290+
jobserver_.Init();
291+
ASSERT_TRUE(jobserver_.Enabled());
292+
}
293+
294+
TEST_F(JobserverTestState, InitFail) {
295+
// Verify that the jobserver exits with an error if a non-existing jobserver
296+
// is configured
297+
ServerConfigure("jobserver-missing");
298+
ASSERT_DEATH(jobserver_.Init(), "ninja: fatal: ");
299+
}
300+
301+
TEST_F(JobserverTestState, NoTokens) {
302+
// Verify that an empty token pool does in fact provide a "default" token
303+
ServerCreate("jobserver-empty", 0);
304+
305+
jobserver_.Init();
306+
ASSERT_TRUE(jobserver_.Acquire());
307+
ASSERT_FALSE(jobserver_.Acquire());
308+
jobserver_.Release();
309+
}
310+
311+
TEST_F(JobserverTestState, OneToken) {
312+
// Verify that a token pool with exactly one token allows acquisition of one
313+
// "default" token and one "external" token
314+
ServerCreate("jobserver-one", 1);
315+
jobserver_.Init();
316+
317+
for (int i = 0; i < 2; i++) {
318+
ASSERT_TRUE(jobserver_.Acquire());
319+
}
320+
321+
ASSERT_FALSE(jobserver_.Acquire());
322+
323+
for (int i = 0; i < 2; i++) {
324+
jobserver_.Release();
325+
}
326+
}
327+
328+
TEST_F(JobserverTestState, AcquireRelease) {
329+
// Verify that Acquire() takes a token from the external pool, and that
330+
// Release() returns it again.
331+
ServerCreate("jobserver-acquire-release", 5);
332+
jobserver_.Init();
333+
334+
ASSERT_TRUE(jobserver_.Acquire());
335+
ASSERT_EQ(ServerCount(), 5);
336+
337+
ASSERT_TRUE(jobserver_.Acquire());
338+
ASSERT_EQ(ServerCount(), 4);
339+
340+
jobserver_.Release();
341+
ASSERT_EQ(ServerCount(), 5);
342+
343+
jobserver_.Release();
344+
ASSERT_EQ(ServerCount(), 5);
345+
}

0 commit comments

Comments
 (0)