| // Copyright 2024 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "jobserver.h" |
| |
| #include "test.h" |
| |
| #ifndef _WIN32 |
| #include <fcntl.h> |
| #include <unistd.h> |
| #endif |
| |
| namespace { |
| |
| #ifndef _WIN32 |
| struct ScopedTestFd { |
| explicit ScopedTestFd(int fd) : fd_(fd) {} |
| |
| ~ScopedTestFd() { |
| if (IsValid()) |
| ::close(fd_); |
| } |
| |
| bool IsValid() const { return fd_ >= 0; } |
| |
| int fd_ = -1; |
| }; |
| #endif // !_WIN32 |
| |
| } // namespace |
| |
| TEST(Jobserver, SlotTest) { |
| // Default construction. |
| Jobserver::Slot slot; |
| EXPECT_FALSE(slot.IsValid()); |
| |
| // Construct implicit slot |
| Jobserver::Slot slot0 = Jobserver::Slot::CreateImplicit(); |
| EXPECT_TRUE(slot0.IsValid()); |
| EXPECT_TRUE(slot0.IsImplicit()); |
| EXPECT_FALSE(slot0.IsExplicit()); |
| |
| // Construct explicit slots |
| auto slot1 = Jobserver::Slot::CreateExplicit(10u); |
| EXPECT_TRUE(slot1.IsValid()); |
| EXPECT_FALSE(slot1.IsImplicit()); |
| EXPECT_TRUE(slot1.IsExplicit()); |
| EXPECT_EQ(10u, slot1.GetExplicitValue()); |
| |
| auto slot2 = Jobserver::Slot::CreateExplicit(42u); |
| EXPECT_TRUE(slot2.IsValid()); |
| EXPECT_FALSE(slot2.IsImplicit()); |
| EXPECT_TRUE(slot2.IsExplicit()); |
| EXPECT_EQ(42u, slot2.GetExplicitValue()); |
| |
| // Move operation. |
| slot2 = std::move(slot1); |
| EXPECT_FALSE(slot1.IsValid()); |
| EXPECT_TRUE(slot2.IsValid()); |
| EXPECT_TRUE(slot2.IsExplicit()); |
| ASSERT_EQ(10u, slot2.GetExplicitValue()); |
| |
| slot1 = std::move(slot0); |
| EXPECT_FALSE(slot0.IsValid()); |
| EXPECT_TRUE(slot1.IsValid()); |
| EXPECT_TRUE(slot1.IsImplicit()); |
| EXPECT_FALSE(slot1.IsExplicit()); |
| } |
| |
| TEST(Jobserver, ParseMakeFlagsValue) { |
| Jobserver::Config config; |
| std::string error; |
| |
| // Passing nullptr does not crash. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(nullptr, &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| // Passing an empty string does not crash. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("", &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| // Passing a string that only contains whitespace does not crash. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" \t", &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| // Passing an `n` in the first word reports no mode. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("kns --jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| // Passing "--jobserver-auth=fifo:<path>" works. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| EXPECT_EQ("foo", config.path); |
| |
| // Passing an initial " -j" or " -j<count>" works. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" -j --jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| EXPECT_EQ("foo", config.path); |
| |
| // Passing an initial " -j<count>" works. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue(" -j10 --jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| EXPECT_EQ("foo", config.path); |
| |
| // Passing an `n` in the first word _after_ a dash works though, i.e. |
| // It is not interpreted as GNU Make dry-run flag. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue( |
| "-one-flag --jobserver-auth=fifo:foo", &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=semaphore_name", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeWin32Semaphore, config.mode); |
| EXPECT_EQ("semaphore_name", config.path); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=10,42", &config, |
| &error)); |
| EXPECT_EQ(Jobserver::Config::kModePipe, config.mode); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=-1,42", &config, |
| &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue("--jobserver-auth=10,-42", &config, |
| &error)); |
| EXPECT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseMakeFlagsValue( |
| "--jobserver-auth=10,42 --jobserver-fds=12,44 " |
| "--jobserver-auth=fifo:/tmp/fifo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| EXPECT_EQ("/tmp/fifo", config.path); |
| |
| config = {}; |
| error.clear(); |
| ASSERT_FALSE( |
| Jobserver::ParseMakeFlagsValue("--jobserver-fds=10,", &config, &error)); |
| EXPECT_EQ("Invalid file descriptor pair [10,]", error); |
| } |
| |
| TEST(Jobserver, ParseNativeMakeFlagsValue) { |
| Jobserver::Config config; |
| std::string error; |
| |
| // --jobserver-auth=R,W is not supported. |
| config = {}; |
| error.clear(); |
| EXPECT_FALSE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=3,4", |
| &config, &error)); |
| EXPECT_EQ(error, "Pipe-based protocol is not supported!"); |
| |
| #ifdef _WIN32 |
| // --jobserver-auth=NAME works on Windows. |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseNativeMakeFlagsValue( |
| "--jobserver-auth=semaphore_name", &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModeWin32Semaphore, config.mode); |
| EXPECT_EQ("semaphore_name", config.path); |
| |
| // --jobserver-auth=fifo:PATH does not work on Windows. |
| config = {}; |
| error.clear(); |
| ASSERT_FALSE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(error, "FIFO mode is not supported on Windows!"); |
| #else // !_WIN32 |
| // --jobserver-auth=NAME does not work on Posix |
| config = {}; |
| error.clear(); |
| ASSERT_FALSE(Jobserver::ParseNativeMakeFlagsValue( |
| "--jobserver-auth=semaphore_name", &config, &error)); |
| EXPECT_EQ(error, "Semaphore mode is not supported on Posix!"); |
| |
| // --jobserver-auth=fifo:PATH works on Posix |
| config = {}; |
| error.clear(); |
| ASSERT_TRUE(Jobserver::ParseNativeMakeFlagsValue("--jobserver-auth=fifo:foo", |
| &config, &error)); |
| EXPECT_EQ(Jobserver::Config::kModePosixFifo, config.mode); |
| EXPECT_EQ("foo", config.path); |
| #endif // !_WIN32 |
| } |
| |
| TEST(Jobserver, NullJobserver) { |
| Jobserver::Config config; |
| ASSERT_EQ(Jobserver::Config::kModeNone, config.mode); |
| |
| std::string error; |
| std::unique_ptr<Jobserver::Client> client = |
| Jobserver::Client::Create(config, &error); |
| EXPECT_FALSE(client.get()); |
| EXPECT_EQ("Unsupported jobserver mode", error); |
| } |
| |
| #ifdef _WIN32 |
| |
| #include <windows.h> |
| |
| // Scoped HANDLE class for the semaphore. |
| struct ScopedSemaphoreHandle { |
| ScopedSemaphoreHandle(HANDLE handle) : handle_(handle) {} |
| ~ScopedSemaphoreHandle() { |
| if (handle_) |
| ::CloseHandle(handle_); |
| } |
| HANDLE get() const { return handle_; } |
| |
| private: |
| HANDLE handle_ = NULL; |
| }; |
| |
| TEST(Jobserver, Win32SemaphoreClient) { |
| // Create semaphore with initial token count. |
| const size_t kExplicitCount = 10; |
| const char kSemaphoreName[] = "ninja_test_jobserver_semaphore"; |
| ScopedSemaphoreHandle handle( |
| ::CreateSemaphoreA(NULL, static_cast<DWORD>(kExplicitCount), |
| static_cast<DWORD>(kExplicitCount), kSemaphoreName)); |
| ASSERT_TRUE(handle.get()) << GetLastErrorString(); |
| |
| // Create new client instance. |
| Jobserver::Config config; |
| config.mode = Jobserver::Config::kModeWin32Semaphore; |
| config.path = kSemaphoreName; |
| |
| std::string error; |
| std::unique_ptr<Jobserver::Client> client = |
| Jobserver::Client::Create(config, &error); |
| EXPECT_TRUE(client.get()) << error; |
| EXPECT_TRUE(error.empty()) << error; |
| |
| Jobserver::Slot slot; |
| std::vector<Jobserver::Slot> slots; |
| |
| // Read the implicit slot. |
| slot = client->TryAcquire(); |
| EXPECT_TRUE(slot.IsValid()); |
| EXPECT_TRUE(slot.IsImplicit()); |
| slots.push_back(std::move(slot)); |
| |
| // Read the explicit slots. |
| for (size_t n = 0; n < kExplicitCount; ++n) { |
| slot = client->TryAcquire(); |
| EXPECT_TRUE(slot.IsValid()); |
| EXPECT_TRUE(slot.IsExplicit()); |
| slots.push_back(std::move(slot)); |
| } |
| |
| // Pool should be empty now. |
| slot = client->TryAcquire(); |
| EXPECT_FALSE(slot.IsValid()); |
| |
| // Release the slots again. |
| while (!slots.empty()) { |
| client->Release(std::move(slots.back())); |
| slots.pop_back(); |
| } |
| |
| slot = client->TryAcquire(); |
| EXPECT_TRUE(slot.IsValid()); |
| EXPECT_TRUE(slot.IsImplicit()); |
| slots.push_back(std::move(slot)); |
| |
| for (size_t n = 0; n < kExplicitCount; ++n) { |
| slot = client->TryAcquire(); |
| EXPECT_TRUE(slot.IsValid()); |
| EXPECT_TRUE(slot.IsExplicit()) << n; |
| slots.push_back(std::move(slot)); |
| } |
| |
| // And the pool should be empty again. |
| slot = client->TryAcquire(); |
| EXPECT_FALSE(slot.IsValid()); |
| } |
| #else // !_WIN32 |
| TEST(Jobserver, PosixFifoClient) { |
| ScopedTempDir temp_dir; |
| temp_dir.CreateAndEnter("ninja_test_jobserver_fifo"); |
| |
| // Create the Fifo, then write kSlotCount slots into it. |
| std::string fifo_path = temp_dir.temp_dir_name_ + "fifo"; |
| int ret = mknod(fifo_path.c_str(), S_IFIFO | 0666, 0); |
| ASSERT_EQ(0, ret) << "Could not create FIFO at: " << fifo_path; |
| |
| const size_t kSlotCount = 5; |
| |
| ScopedTestFd write_fd(::open(fifo_path.c_str(), O_RDWR)); |
| ASSERT_TRUE(write_fd.IsValid()) << "Cannot open FIFO at: " << strerror(errno); |
| for (size_t n = 0; n < kSlotCount; ++n) { |
| uint8_t slot_byte = static_cast<uint8_t>('0' + n); |
| ::write(write_fd.fd_, &slot_byte, 1); |
| } |
| // Keep the file descriptor opened to ensure the fifo's content |
| // persists in kernel memory. |
| |
| // Create new client instance. |
| Jobserver::Config config; |
| config.mode = Jobserver::Config::kModePosixFifo; |
| config.path = fifo_path; |
| |
| std::string error; |
| std::unique_ptr<Jobserver::Client> client = |
| Jobserver::Client::Create(config, &error); |
| EXPECT_TRUE(client.get()); |
| EXPECT_TRUE(error.empty()) << error; |
| |
| // Read slots from the pool, and store them |
| std::vector<Jobserver::Slot> slots; |
| |
| // First slot is always implicit. |
| slots.push_back(client->TryAcquire()); |
| ASSERT_TRUE(slots.back().IsValid()); |
| EXPECT_TRUE(slots.back().IsImplicit()); |
| |
| // Then read kSlotCount slots from the pipe and verify their value. |
| for (size_t n = 0; n < kSlotCount; ++n) { |
| Jobserver::Slot slot = client->TryAcquire(); |
| ASSERT_TRUE(slot.IsValid()) << "Slot #" << n + 1; |
| EXPECT_EQ(static_cast<uint8_t>('0' + n), slot.GetExplicitValue()); |
| slots.push_back(std::move(slot)); |
| } |
| |
| // Pool should be empty now, so next TryAcquire() will fail. |
| Jobserver::Slot slot = client->TryAcquire(); |
| EXPECT_FALSE(slot.IsValid()); |
| } |
| |
| TEST(Jobserver, PosixFifoClientWithWrongPath) { |
| ScopedTempDir temp_dir; |
| temp_dir.CreateAndEnter("ninja_test_jobserver_fifo"); |
| |
| // Create a regular file. |
| std::string file_path = temp_dir.temp_dir_name_ + "not_a_fifo"; |
| int fd = ::open(file_path.c_str(), O_CREAT | O_RDWR, 0660); |
| ASSERT_GE(fd, 0) << "Could not create file: " << strerror(errno); |
| ::close(fd); |
| |
| // Create new client instance, passing the file path for the fifo. |
| Jobserver::Config config; |
| config.mode = Jobserver::Config::kModePosixFifo; |
| config.path = file_path; |
| |
| std::string error; |
| std::unique_ptr<Jobserver::Client> client = |
| Jobserver::Client::Create(config, &error); |
| EXPECT_FALSE(client.get()); |
| EXPECT_FALSE(error.empty()); |
| EXPECT_EQ("Not a fifo path: " + file_path, error); |
| |
| // Do the same with an empty file path. |
| error.clear(); |
| config.path.clear(); |
| client = Jobserver::Client::Create(config, &error); |
| EXPECT_FALSE(client.get()); |
| EXPECT_FALSE(error.empty()); |
| EXPECT_EQ("Empty fifo path", error); |
| } |
| #endif // !_WIN32 |