mirror of
https://github.com/protocolbuffers/protobuf
synced 2026-08-26 02:23:14 -04:00
Add Windows conformance runner and testee support (#28130)
Adds Windows support for the conformance runner and C++ conformance testee. For the conformance runner, this splits `ForkPipeRunner` into POSIX and Win32 implementations. The testee is switched to portable `FILE*` I/O, with binary mode on Windows.
Also makes two related changes:
* `waitpid(..., WEXITED)` is changed to `waitpid(..., 0)` because `WEXITED` is a `waitid()` flag. `waitpid(..., 0)` already waits for the child to terminate.
* Adds `protobuf_configure_target` to the CMake conformance targets so they get the necessary target flags, e.g. `/utf-8`, which is important on Windows.
Otherwise wires the change into CI so the Windows target functionally mirrors the existing macOS target.
Resolves this existing Windows TODO in `fork_pipe_runner.cc`:
```
// TODO: make this work on Windows, instead of using these
// UNIX-specific APIs.
//
// There is a platform-agnostic API in
// src/google/protobuf/compiler/subprocess.h
//
// However that API only supports sending a single message to the subprocess.
// We really want to be able to send messages and receive responses one at a
// time:
//
// 1. Spawning a new process for each test would take way too long for thousands
// of tests and subprocesses like java that can take 100ms or more to start
// up.
//
// 2. Sending all the tests in one big message and receiving all results in one
// big message would take away our visibility about which test(s) caused a
// crash or other fatal error. It would also give us only a single failure
// instead of all of them.
```
Closes #28130
FUTURE_COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/28130 from dcodeIO:windows 4872b82d60
PiperOrigin-RevId: 968704895
This commit is contained in:
parent
ff9440f2ee
commit
5cc3eb9d6d
10 changed files with 569 additions and 220 deletions
16
.github/workflows/test_cpp.yml
vendored
16
.github/workflows/test_cpp.yml
vendored
|
|
@ -462,15 +462,15 @@ jobs:
|
|||
- name: Windows Bazel
|
||||
os: windows-2022
|
||||
cache_key: windows-2022-msvc-cl
|
||||
bazel: test //src/... --config=msvc-cl --test_tag_filters=-conformance --build_tag_filters=-conformance
|
||||
bazel: test //src/... //third_party/utf8_range/... //conformance:conformance_framework_tests --config=msvc-cl
|
||||
- name: Windows Bazel C++20
|
||||
os: windows-2022
|
||||
cache_key: windows-2022-msvc-cl
|
||||
bazel: test //src/... --config=msvc-cl --cxxopt="-std:c++20" --test_tag_filters=-conformance --build_tag_filters=-conformance
|
||||
bazel: test //src/... //third_party/utf8_range/... //conformance:conformance_framework_tests --config=msvc-cl --cxxopt="-std:c++20"
|
||||
- name: Windows Bazel clang-cl
|
||||
os: windows-2022
|
||||
cache_key: windows-2022-clang-cl
|
||||
bazel: test //src/... --test_tag_filters=-conformance --build_tag_filters=-conformance
|
||||
bazel: test //src/... //third_party/utf8_range/... //conformance:conformance_framework_tests
|
||||
name: ${{ matrix.continuous-only && inputs.continuous-prefix || '' }} ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
|
@ -500,7 +500,7 @@ jobs:
|
|||
- name: Windows CMake
|
||||
os: windows-2022
|
||||
flags: >-
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF -Dprotobuf_BUILD_CONFORMANCE=OFF
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF
|
||||
-Dprotobuf_BUILD_SHARED_LIBS=OFF
|
||||
-Dprotobuf_BUILD_EXAMPLES=ON
|
||||
vsversion: '2022'
|
||||
|
|
@ -508,7 +508,7 @@ jobs:
|
|||
- name: Windows CMake 32-bit
|
||||
os: windows-2022
|
||||
flags: >-
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF -Dprotobuf_BUILD_CONFORMANCE=OFF
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF
|
||||
vsversion: '2022'
|
||||
windows-arch: 'win32'
|
||||
cache-prefix: windows-2022-win32-cmake
|
||||
|
|
@ -516,16 +516,16 @@ jobs:
|
|||
- name: Windows CMake Shared
|
||||
os: windows-2022
|
||||
flags: >-
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF -Dprotobuf_BUILD_CONFORMANCE=OFF
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF
|
||||
-Dprotobuf_BUILD_SHARED_LIBS=ON
|
||||
vsversion: '2022'
|
||||
cache-prefix: windows-2022-cmake
|
||||
- name: Windows CMake Install
|
||||
os: windows-2022
|
||||
install-flags: >-
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF -Dprotobuf_BUILD_CONFORMANCE=OFF
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF
|
||||
flags: >-
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF -Dprotobuf_BUILD_CONFORMANCE=OFF
|
||||
-G Ninja -Dprotobuf_WITH_ZLIB=OFF
|
||||
-Dprotobuf_REMOVE_INSTALLED_HEADERS=ON
|
||||
-Dprotobuf_BUILD_PROTOBUF_BINARIES=OFF
|
||||
vsversion: '2022'
|
||||
|
|
|
|||
|
|
@ -113,11 +113,13 @@ add_executable(conformance_test_runner
|
|||
${conformance_runner_srcs}
|
||||
${conformance_runner_hdrs}
|
||||
)
|
||||
protobuf_configure_target(conformance_test_runner)
|
||||
|
||||
add_executable(conformance_cpp
|
||||
${conformance_testee_srcs}
|
||||
${conformance_testee_hdrs}
|
||||
)
|
||||
protobuf_configure_target(conformance_cpp)
|
||||
|
||||
target_include_directories(
|
||||
conformance_test_runner
|
||||
|
|
|
|||
|
|
@ -187,7 +187,11 @@ cc_library(
|
|||
|
||||
cc_library(
|
||||
name = "fork_pipe_runner",
|
||||
srcs = ["fork_pipe_runner.cc"],
|
||||
srcs = [
|
||||
"fork_pipe_runner.cc",
|
||||
"fork_pipe_runner_posix.cc",
|
||||
"fork_pipe_runner_win32.cc",
|
||||
],
|
||||
hdrs = ["fork_pipe_runner.h"],
|
||||
deps = [
|
||||
":conformance_cc_proto",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
|
@ -16,6 +15,11 @@
|
|||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#endif
|
||||
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/api.pb.h"
|
||||
#include "google/protobuf/duration.pb.h"
|
||||
|
|
@ -68,15 +72,14 @@ using TestAllTypesProto2Editions =
|
|||
using TestAllTypesProto3Editions =
|
||||
::protobuf_test_messages::editions::proto3::TestAllTypesProto3;
|
||||
|
||||
absl::Status ReadFd(int fd, char* buf, size_t len) {
|
||||
absl::Status ReadFile(FILE* file, char* buf, size_t len) {
|
||||
while (len > 0) {
|
||||
ssize_t bytes_read = read(fd, buf, len);
|
||||
size_t bytes_read = fread(buf, 1, len, file);
|
||||
|
||||
if (bytes_read == 0) {
|
||||
return absl::DataLossError("unexpected EOF");
|
||||
}
|
||||
|
||||
if (bytes_read < 0) {
|
||||
if (feof(file)) {
|
||||
return absl::DataLossError("unexpected EOF");
|
||||
}
|
||||
return absl::ErrnoToStatus(errno, "error reading from test runner");
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +89,9 @@ absl::Status ReadFd(int fd, char* buf, size_t len) {
|
|||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status WriteFd(int fd, const void* buf, size_t len) {
|
||||
if (static_cast<size_t>(write(fd, buf, len)) != len) {
|
||||
return absl::ErrnoToStatus(errno, "error reading to test runner");
|
||||
absl::Status WriteFile(FILE* file, const void* buf, size_t len) {
|
||||
if (fwrite(buf, 1, len, file) != len) {
|
||||
return absl::ErrnoToStatus(errno, "error writing to test runner");
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
|
@ -224,8 +227,7 @@ absl::StatusOr<ConformanceResponse> Harness::RunTest(
|
|||
|
||||
absl::StatusOr<bool> Harness::ServeConformanceRequest() {
|
||||
uint32_t in_len;
|
||||
if (!ReadFd(STDIN_FILENO, reinterpret_cast<char*>(&in_len), sizeof(in_len))
|
||||
.ok()) {
|
||||
if (!ReadFile(stdin, reinterpret_cast<char*>(&in_len), sizeof(in_len)).ok()) {
|
||||
// EOF means we're done.
|
||||
return true;
|
||||
}
|
||||
|
|
@ -233,7 +235,7 @@ absl::StatusOr<bool> Harness::ServeConformanceRequest() {
|
|||
|
||||
std::string serialized_input;
|
||||
serialized_input.resize(in_len);
|
||||
RETURN_IF_ERROR(ReadFd(STDIN_FILENO, &serialized_input[0], in_len));
|
||||
RETURN_IF_ERROR(ReadFile(stdin, &serialized_input[0], in_len));
|
||||
|
||||
ConformanceRequest request;
|
||||
ABSL_CHECK(request.ParseFromString(serialized_input));
|
||||
|
|
@ -248,9 +250,12 @@ absl::StatusOr<bool> Harness::ServeConformanceRequest() {
|
|||
uint32_t out_len = internal::little_endian::FromHost(
|
||||
static_cast<uint32_t>(serialized_output.size()));
|
||||
|
||||
RETURN_IF_ERROR(WriteFd(STDOUT_FILENO, &out_len, sizeof(out_len)));
|
||||
RETURN_IF_ERROR(WriteFd(STDOUT_FILENO, serialized_output.data(),
|
||||
serialized_output.size()));
|
||||
RETURN_IF_ERROR(WriteFile(stdout, &out_len, sizeof(out_len)));
|
||||
RETURN_IF_ERROR(
|
||||
WriteFile(stdout, serialized_output.data(), serialized_output.size()));
|
||||
if (fflush(stdout) != 0) {
|
||||
return absl::ErrnoToStatus(errno, "error flushing to test runner");
|
||||
}
|
||||
|
||||
if (verbose_) {
|
||||
ABSL_LOG(INFO) << "conformance-cpp: request="
|
||||
|
|
@ -264,6 +269,10 @@ absl::StatusOr<bool> Harness::ServeConformanceRequest() {
|
|||
} // namespace google
|
||||
|
||||
int main() {
|
||||
#ifdef _WIN32
|
||||
_setmode(_fileno(stdin), _O_BINARY);
|
||||
_setmode(_fileno(stdout), _O_BINARY);
|
||||
#endif
|
||||
google::protobuf::Harness harness;
|
||||
int total_runs = 0;
|
||||
while (true) {
|
||||
|
|
|
|||
|
|
@ -30,11 +30,13 @@
|
|||
// 3. testee sends 4-byte length M (little endian)
|
||||
// 4. testee sends M bytes representing a ConformanceResponse proto
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#ifndef _WIN32
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
|
|
|||
|
|
@ -32,43 +32,20 @@
|
|||
|
||||
#include "fork_pipe_runner.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono> // NOLINT(build/c++11)
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <future> // NOLINT(build/c++11)
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/log/absl_log.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "conformance/conformance.pb.h"
|
||||
#include "google/protobuf/endian.h"
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define TOSTRING(x) STRINGIFY(x)
|
||||
#define CHECK_SYSCALL(call) \
|
||||
if (call < 0) { \
|
||||
perror(#call " " __FILE__ ":" TOSTRING(__LINE__)); \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
namespace google {
|
||||
namespace protobuf {
|
||||
|
||||
std::string ForkPipeRunner::RunTest(absl::string_view test_name,
|
||||
absl::string_view request) {
|
||||
if (child_pid_ < 0) {
|
||||
if (!IsTestProgramRunning()) {
|
||||
SpawnTestProgram();
|
||||
}
|
||||
current_test_name_ = std::string(test_name);
|
||||
|
|
@ -76,37 +53,20 @@ std::string ForkPipeRunner::RunTest(absl::string_view test_name,
|
|||
uint32_t len =
|
||||
internal::little_endian::FromHost(static_cast<uint32_t>(request.size()));
|
||||
|
||||
CheckedWrite(write_fd_, &len, sizeof(uint32_t));
|
||||
CheckedWrite(write_fd_, request.data(), request.size());
|
||||
CheckedWrite(&len, sizeof(uint32_t));
|
||||
CheckedWrite(request.data(), request.size());
|
||||
|
||||
std::string response;
|
||||
if (!TryRead(read_fd_, &len, sizeof(uint32_t))) {
|
||||
// We failed to read from the child, assume a crash and try to reap.
|
||||
ABSL_LOG(INFO) << "Trying to reap child, pid=" << child_pid_;
|
||||
|
||||
int status = 0;
|
||||
waitpid(child_pid_, &status, WEXITED);
|
||||
|
||||
std::string error_msg;
|
||||
bool timed_out = false;
|
||||
if (!TryRead(&len, sizeof(uint32_t), &timed_out)) {
|
||||
conformance::ConformanceResponse response_obj;
|
||||
if (WIFEXITED(status)) {
|
||||
if (WEXITSTATUS(status) == 0) {
|
||||
absl::StrAppendFormat(&error_msg,
|
||||
"child timed out, killed by signal %d",
|
||||
WTERMSIG(status));
|
||||
response_obj.set_timeout_error(error_msg);
|
||||
} else {
|
||||
absl::StrAppendFormat(&error_msg, "child exited, status=%d",
|
||||
WEXITSTATUS(status));
|
||||
response_obj.set_runtime_error(error_msg);
|
||||
}
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
absl::StrAppendFormat(&error_msg, "child killed by signal %d",
|
||||
WTERMSIG(status));
|
||||
}
|
||||
std::string error_msg = GetTestProgramFailure(timed_out);
|
||||
ABSL_LOG(INFO) << error_msg;
|
||||
child_pid_ = -1;
|
||||
|
||||
if (timed_out) {
|
||||
response_obj.set_timeout_error(error_msg);
|
||||
} else {
|
||||
response_obj.set_runtime_error(error_msg);
|
||||
}
|
||||
// TODO: Remove this suppression.
|
||||
(void)response_obj.SerializeToString(&response);
|
||||
return response;
|
||||
|
|
@ -114,138 +74,9 @@ std::string ForkPipeRunner::RunTest(absl::string_view test_name,
|
|||
|
||||
len = internal::little_endian::ToHost(len);
|
||||
response.resize(len);
|
||||
CheckedRead(read_fd_, (void *)response.c_str(), len);
|
||||
CheckedRead((void*)response.c_str(), len);
|
||||
return response;
|
||||
}
|
||||
|
||||
// TODO: make this work on Windows, instead of using these
|
||||
// UNIX-specific APIs.
|
||||
//
|
||||
// There is a platform-agnostic API in
|
||||
// src/google/protobuf/compiler/subprocess.h
|
||||
//
|
||||
// However that API only supports sending a single message to the subprocess.
|
||||
// We really want to be able to send messages and receive responses one at a
|
||||
// time:
|
||||
//
|
||||
// 1. Spawning a new process for each test would take way too long for thousands
|
||||
// of tests and subprocesses like java that can take 100ms or more to start
|
||||
// up.
|
||||
//
|
||||
// 2. Sending all the tests in one big message and receiving all results in one
|
||||
// big message would take away our visibility about which test(s) caused a
|
||||
// crash or other fatal error. It would also give us only a single failure
|
||||
// instead of all of them.
|
||||
void ForkPipeRunner::SpawnTestProgram() {
|
||||
int toproc_pipe_fd[2];
|
||||
int fromproc_pipe_fd[2];
|
||||
if (pipe(toproc_pipe_fd) < 0 || pipe(fromproc_pipe_fd) < 0) {
|
||||
perror("pipe");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
// Parent.
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[0]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[1]));
|
||||
write_fd_ = toproc_pipe_fd[1];
|
||||
read_fd_ = fromproc_pipe_fd[0];
|
||||
child_pid_ = pid;
|
||||
} else {
|
||||
// Child.
|
||||
CHECK_SYSCALL(close(STDIN_FILENO));
|
||||
CHECK_SYSCALL(close(STDOUT_FILENO));
|
||||
CHECK_SYSCALL(dup2(toproc_pipe_fd[0], STDIN_FILENO));
|
||||
CHECK_SYSCALL(dup2(fromproc_pipe_fd[1], STDOUT_FILENO));
|
||||
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[0]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[1]));
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[1]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[0]));
|
||||
|
||||
std::unique_ptr<char[]> executable(new char[executable_.size() + 1]);
|
||||
memcpy(executable.get(), executable_.c_str(), executable_.size());
|
||||
executable[executable_.size()] = '\0';
|
||||
|
||||
std::vector<const char *> argv;
|
||||
argv.push_back(executable.get());
|
||||
ABSL_LOG(INFO) << argv[0];
|
||||
for (size_t i = 0; i < executable_args_.size(); ++i) {
|
||||
argv.push_back(executable_args_[i].c_str());
|
||||
ABSL_LOG(INFO) << executable_args_[i];
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
// Never returns.
|
||||
CHECK_SYSCALL(execv(executable.get(), const_cast<char **>(argv.data())));
|
||||
}
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedWrite(int fd, const void *buf, size_t len) {
|
||||
if (static_cast<size_t>(write(fd, buf, len)) != len) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error writing to test program: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
|
||||
bool ForkPipeRunner::TryRead(int fd, void *buf, size_t len) {
|
||||
size_t ofs = 0;
|
||||
while (len > 0) {
|
||||
std::future<ssize_t> future = std::async(
|
||||
std::launch::async,
|
||||
[](int fd, void *buf, size_t ofs, size_t len) {
|
||||
return read(fd, (char *)buf + ofs, len);
|
||||
},
|
||||
fd, buf, ofs, len);
|
||||
std::future_status status = future.wait_for(std::chrono::seconds(30));
|
||||
if (status == std::future_status::timeout) {
|
||||
ABSL_LOG(ERROR) << current_test_name_ << ": timeout from test program";
|
||||
kill(child_pid_, SIGQUIT);
|
||||
// TODO: Only log in flag-guarded mode, since reading output
|
||||
// from SIGQUIT is slow and verbose.
|
||||
std::vector<char> err;
|
||||
err.resize(5000);
|
||||
ssize_t err_bytes_read;
|
||||
size_t err_ofs = 0;
|
||||
do {
|
||||
err_bytes_read = read(fd, (void *)&err[err_ofs], err.size() - err_ofs);
|
||||
err_ofs += static_cast<size_t>(err_bytes_read);
|
||||
} while (err_bytes_read > 0 && err_ofs < err.size());
|
||||
ABSL_LOG(ERROR) << "child_pid_=" << child_pid_ << " SIGQUIT: \n"
|
||||
<< &err[0];
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t bytes_read = future.get();
|
||||
if (bytes_read == 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": unexpected EOF from test program";
|
||||
return false;
|
||||
} else if (bytes_read < 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": error reading from test program: "
|
||||
<< strerror(errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
len -= static_cast<size_t>(bytes_read);
|
||||
ofs += static_cast<size_t>(bytes_read);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedRead(int fd, void *buf, size_t len) {
|
||||
if (!TryRead(fd, buf, len)) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error reading from test program: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace protobuf
|
||||
} // namespace google
|
||||
|
|
|
|||
|
|
@ -26,32 +26,36 @@ namespace protobuf {
|
|||
class ForkPipeRunner : public ConformanceTestRunner {
|
||||
public:
|
||||
ForkPipeRunner(absl::string_view executable,
|
||||
absl::Span<const std::string> executable_args)
|
||||
: child_pid_(-1),
|
||||
executable_(executable),
|
||||
executable_args_(executable_args.begin(), executable_args.end()) {}
|
||||
absl::Span<const std::string> executable_args);
|
||||
|
||||
explicit ForkPipeRunner(const std::string& executable)
|
||||
: child_pid_(-1), executable_(executable) {}
|
||||
explicit ForkPipeRunner(const std::string& executable);
|
||||
|
||||
~ForkPipeRunner() override = default;
|
||||
~ForkPipeRunner() override;
|
||||
|
||||
ForkPipeRunner(const ForkPipeRunner&) = delete;
|
||||
ForkPipeRunner& operator=(const ForkPipeRunner&) = delete;
|
||||
|
||||
std::string RunTest(absl::string_view test_name,
|
||||
absl::string_view request) override;
|
||||
|
||||
private:
|
||||
// Platform-specific process and pipe handles live in the implementation
|
||||
// files.
|
||||
struct State;
|
||||
|
||||
void SpawnTestProgram();
|
||||
|
||||
void CheckedWrite(int fd, const void* buf, size_t len);
|
||||
bool TryRead(int fd, void* buf, size_t len);
|
||||
void CheckedRead(int fd, void* buf, size_t len);
|
||||
bool IsTestProgramRunning() const;
|
||||
void CloseTestProgram();
|
||||
std::string GetTestProgramFailure(bool timed_out);
|
||||
void CheckedWrite(const void* buf, size_t len);
|
||||
bool TryRead(void* buf, size_t len, bool* timed_out);
|
||||
void CheckedRead(void* buf, size_t len);
|
||||
|
||||
int write_fd_;
|
||||
int read_fd_;
|
||||
pid_t child_pid_;
|
||||
std::string executable_;
|
||||
const std::vector<std::string> executable_args_;
|
||||
std::string current_test_name_;
|
||||
State* state_;
|
||||
};
|
||||
|
||||
} // namespace protobuf
|
||||
|
|
|
|||
228
conformance/fork_pipe_runner_posix.cc
Normal file
228
conformance/fork_pipe_runner_posix.cc
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
#include "fork_pipe_runner.h"
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono> // NOLINT(build/c++11)
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <future> // NOLINT(build/c++11)
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/log/absl_log.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define TOSTRING(x) STRINGIFY(x)
|
||||
#define CHECK_SYSCALL(call) \
|
||||
if (call < 0) { \
|
||||
perror(#call " " __FILE__ ":" TOSTRING(__LINE__)); \
|
||||
exit(1); \
|
||||
}
|
||||
|
||||
namespace google {
|
||||
namespace protobuf {
|
||||
|
||||
struct ForkPipeRunner::State {
|
||||
int write_fd = -1;
|
||||
int read_fd = -1;
|
||||
pid_t child_pid = -1;
|
||||
};
|
||||
|
||||
ForkPipeRunner::ForkPipeRunner(absl::string_view executable,
|
||||
absl::Span<const std::string> executable_args)
|
||||
: executable_(executable),
|
||||
executable_args_(executable_args.begin(), executable_args.end()),
|
||||
state_(new State) {}
|
||||
|
||||
ForkPipeRunner::ForkPipeRunner(const std::string& executable)
|
||||
: executable_(executable), state_(new State) {}
|
||||
|
||||
ForkPipeRunner::~ForkPipeRunner() {
|
||||
CloseTestProgram();
|
||||
delete state_;
|
||||
}
|
||||
|
||||
bool ForkPipeRunner::IsTestProgramRunning() const {
|
||||
return state_->child_pid >= 0;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::SpawnTestProgram() {
|
||||
int toproc_pipe_fd[2];
|
||||
int fromproc_pipe_fd[2];
|
||||
if (pipe(toproc_pipe_fd) < 0 || pipe(fromproc_pipe_fd) < 0) {
|
||||
perror("pipe");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
// Parent.
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[0]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[1]));
|
||||
state_->write_fd = toproc_pipe_fd[1];
|
||||
state_->read_fd = fromproc_pipe_fd[0];
|
||||
state_->child_pid = pid;
|
||||
} else {
|
||||
// Child.
|
||||
CHECK_SYSCALL(close(STDIN_FILENO));
|
||||
CHECK_SYSCALL(close(STDOUT_FILENO));
|
||||
CHECK_SYSCALL(dup2(toproc_pipe_fd[0], STDIN_FILENO));
|
||||
CHECK_SYSCALL(dup2(fromproc_pipe_fd[1], STDOUT_FILENO));
|
||||
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[0]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[1]));
|
||||
CHECK_SYSCALL(close(toproc_pipe_fd[1]));
|
||||
CHECK_SYSCALL(close(fromproc_pipe_fd[0]));
|
||||
|
||||
std::unique_ptr<char[]> executable(new char[executable_.size() + 1]);
|
||||
memcpy(executable.get(), executable_.c_str(), executable_.size());
|
||||
executable[executable_.size()] = '\0';
|
||||
|
||||
std::vector<const char*> argv;
|
||||
argv.reserve(executable_args_.size() + 2);
|
||||
argv.push_back(executable.get());
|
||||
ABSL_LOG(INFO) << argv[0];
|
||||
for (size_t i = 0; i < executable_args_.size(); ++i) {
|
||||
argv.push_back(executable_args_[i].c_str());
|
||||
ABSL_LOG(INFO) << executable_args_[i];
|
||||
}
|
||||
argv.push_back(nullptr);
|
||||
// Never returns.
|
||||
CHECK_SYSCALL(execv(executable.get(), const_cast<char**>(argv.data())));
|
||||
}
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedWrite(const void* buf, size_t len) {
|
||||
if (static_cast<size_t>(write(state_->write_fd, buf, len)) != len) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error writing to test program: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
|
||||
bool ForkPipeRunner::TryRead(void* buf, size_t len, bool* timed_out) {
|
||||
*timed_out = false;
|
||||
size_t ofs = 0;
|
||||
while (len > 0) {
|
||||
std::future<ssize_t> future = std::async(
|
||||
std::launch::async,
|
||||
[](int read_fd, void* buf, size_t ofs, size_t len) {
|
||||
return read(read_fd, (char*)buf + ofs, len);
|
||||
},
|
||||
state_->read_fd, buf, ofs, len);
|
||||
std::future_status status = future.wait_for(std::chrono::seconds(30));
|
||||
if (status == std::future_status::timeout) {
|
||||
ABSL_LOG(ERROR) << current_test_name_ << ": timeout from test program";
|
||||
*timed_out = true;
|
||||
kill(state_->child_pid, SIGQUIT);
|
||||
// TODO: Only log in flag-guarded mode, since reading output
|
||||
// from SIGQUIT is slow and verbose.
|
||||
std::vector<char> err;
|
||||
err.resize(5000);
|
||||
ssize_t err_bytes_read;
|
||||
size_t err_ofs = 0;
|
||||
do {
|
||||
err_bytes_read =
|
||||
read(state_->read_fd, (void*)&err[err_ofs], err.size() - err_ofs);
|
||||
if (err_bytes_read > 0) {
|
||||
err_ofs += static_cast<size_t>(err_bytes_read);
|
||||
}
|
||||
} while (err_bytes_read > 0 && err_ofs < err.size());
|
||||
ABSL_LOG(ERROR) << "child_pid_=" << state_->child_pid << " SIGQUIT: \n"
|
||||
<< &err[0];
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t bytes_read = future.get();
|
||||
if (bytes_read == 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": unexpected EOF from test program";
|
||||
return false;
|
||||
} else if (bytes_read < 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": error reading from test program: "
|
||||
<< strerror(errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
len -= static_cast<size_t>(bytes_read);
|
||||
ofs += static_cast<size_t>(bytes_read);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedRead(void* buf, size_t len) {
|
||||
bool timed_out = false;
|
||||
if (!TryRead(buf, len, &timed_out)) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error reading from test program: " << strerror(errno);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ForkPipeRunner::GetTestProgramFailure(bool timed_out) {
|
||||
// We failed to read from the child, assume a crash and try to reap.
|
||||
ABSL_LOG(INFO) << "Trying to reap child, pid=" << state_->child_pid;
|
||||
|
||||
int status = 0;
|
||||
waitpid(state_->child_pid, &status, 0);
|
||||
|
||||
std::string error_msg;
|
||||
if (timed_out) {
|
||||
if (WIFSIGNALED(status)) {
|
||||
absl::StrAppendFormat(&error_msg, "child timed out, killed by signal %d",
|
||||
WTERMSIG(status));
|
||||
} else {
|
||||
error_msg = "child timed out";
|
||||
}
|
||||
} else if (WIFEXITED(status)) {
|
||||
absl::StrAppendFormat(&error_msg, "child exited, status=%d",
|
||||
WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
absl::StrAppendFormat(&error_msg, "child killed by signal %d",
|
||||
WTERMSIG(status));
|
||||
}
|
||||
CloseTestProgram();
|
||||
return error_msg;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CloseTestProgram() {
|
||||
if (state_->write_fd >= 0) {
|
||||
close(state_->write_fd);
|
||||
state_->write_fd = -1;
|
||||
}
|
||||
if (state_->read_fd >= 0) {
|
||||
close(state_->read_fd);
|
||||
state_->read_fd = -1;
|
||||
}
|
||||
state_->child_pid = -1;
|
||||
}
|
||||
|
||||
} // namespace protobuf
|
||||
} // namespace google
|
||||
|
||||
#endif // !_WIN32
|
||||
267
conformance/fork_pipe_runner_win32.cc
Normal file
267
conformance/fork_pipe_runner_win32.cc
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2026 Google Inc. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include "fork_pipe_runner.h"
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono> // NOLINT(build/c++11)
|
||||
#include <cstddef>
|
||||
#include <future> // NOLINT(build/c++11)
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "absl/log/absl_log.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
|
||||
namespace google {
|
||||
namespace protobuf {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string WindowsErrorMessage(DWORD error) {
|
||||
std::ostringstream oss;
|
||||
oss << "Windows error " << error;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string LastSystemError() { return WindowsErrorMessage(GetLastError()); }
|
||||
|
||||
std::string QuoteCommandLineArg(absl::string_view arg) {
|
||||
if (arg.empty()) {
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
bool needs_quotes = false;
|
||||
for (char c : arg) {
|
||||
if (c == ' ' || c == '\t' || c == '"' || c == '\\') {
|
||||
needs_quotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!needs_quotes) {
|
||||
return std::string(arg);
|
||||
}
|
||||
|
||||
std::string quoted = "\"";
|
||||
size_t backslashes = 0;
|
||||
for (char c : arg) {
|
||||
if (c == '\\') {
|
||||
++backslashes;
|
||||
} else if (c == '"') {
|
||||
quoted.append(backslashes * 2 + 1, '\\');
|
||||
quoted.push_back(c);
|
||||
backslashes = 0;
|
||||
} else {
|
||||
quoted.append(backslashes, '\\');
|
||||
backslashes = 0;
|
||||
quoted.push_back(c);
|
||||
}
|
||||
}
|
||||
quoted.append(backslashes * 2, '\\');
|
||||
quoted.push_back('"');
|
||||
return quoted;
|
||||
}
|
||||
|
||||
struct ReadChunk {
|
||||
std::ptrdiff_t bytes_read;
|
||||
DWORD error_code;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ForkPipeRunner::State {
|
||||
HANDLE child_process = NULL;
|
||||
HANDLE write_handle = NULL;
|
||||
HANDLE read_handle = NULL;
|
||||
};
|
||||
|
||||
ForkPipeRunner::ForkPipeRunner(absl::string_view executable,
|
||||
absl::Span<const std::string> executable_args)
|
||||
: executable_(executable),
|
||||
executable_args_(executable_args.begin(), executable_args.end()),
|
||||
state_(new State) {}
|
||||
|
||||
ForkPipeRunner::ForkPipeRunner(const std::string& executable)
|
||||
: executable_(executable), state_(new State) {}
|
||||
|
||||
ForkPipeRunner::~ForkPipeRunner() {
|
||||
CloseTestProgram();
|
||||
delete state_;
|
||||
}
|
||||
|
||||
bool ForkPipeRunner::IsTestProgramRunning() const {
|
||||
return state_->child_process != NULL;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::SpawnTestProgram() {
|
||||
SECURITY_ATTRIBUTES security_attributes;
|
||||
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
|
||||
security_attributes.bInheritHandle = TRUE;
|
||||
security_attributes.lpSecurityDescriptor = NULL;
|
||||
|
||||
HANDLE child_stdin_read = NULL;
|
||||
HANDLE child_stdin_write = NULL;
|
||||
HANDLE child_stdout_read = NULL;
|
||||
HANDLE child_stdout_write = NULL;
|
||||
|
||||
if (!CreatePipe(&child_stdin_read, &child_stdin_write, &security_attributes,
|
||||
0) ||
|
||||
!SetHandleInformation(child_stdin_write, HANDLE_FLAG_INHERIT, 0) ||
|
||||
!CreatePipe(&child_stdout_read, &child_stdout_write, &security_attributes,
|
||||
0) ||
|
||||
!SetHandleInformation(child_stdout_read, HANDLE_FLAG_INHERIT, 0)) {
|
||||
ABSL_LOG(FATAL) << "pipe setup failed: " << LastSystemError();
|
||||
}
|
||||
|
||||
std::string command_line = QuoteCommandLineArg(executable_);
|
||||
ABSL_LOG(INFO) << executable_;
|
||||
for (const std::string& arg : executable_args_) {
|
||||
command_line.push_back(' ');
|
||||
command_line.append(QuoteCommandLineArg(arg));
|
||||
ABSL_LOG(INFO) << arg;
|
||||
}
|
||||
|
||||
STARTUPINFOA startup_info;
|
||||
ZeroMemory(&startup_info, sizeof(startup_info));
|
||||
startup_info.cb = sizeof(startup_info);
|
||||
startup_info.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup_info.hStdInput = child_stdin_read;
|
||||
startup_info.hStdOutput = child_stdout_write;
|
||||
startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
|
||||
PROCESS_INFORMATION process_info;
|
||||
ZeroMemory(&process_info, sizeof(process_info));
|
||||
if (!CreateProcessA(NULL, &command_line[0], NULL, NULL, TRUE, 0, NULL, NULL,
|
||||
&startup_info, &process_info)) {
|
||||
ABSL_LOG(FATAL) << "CreateProcess failed: " << LastSystemError();
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(child_stdin_read);
|
||||
CloseHandle(child_stdout_write);
|
||||
state_->write_handle = child_stdin_write;
|
||||
state_->read_handle = child_stdout_read;
|
||||
state_->child_process = process_info.hProcess;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedWrite(const void* buf, size_t len) {
|
||||
DWORD bytes_written = 0;
|
||||
if (len > static_cast<size_t>(std::numeric_limits<DWORD>::max()) ||
|
||||
!WriteFile(state_->write_handle, buf, static_cast<DWORD>(len),
|
||||
&bytes_written, NULL) ||
|
||||
static_cast<size_t>(bytes_written) != len) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error writing to test program: " << LastSystemError();
|
||||
}
|
||||
}
|
||||
|
||||
bool ForkPipeRunner::TryRead(void* buf, size_t len, bool* timed_out) {
|
||||
*timed_out = false;
|
||||
size_t offset = 0;
|
||||
while (len > 0) {
|
||||
std::future<ReadChunk> future = std::async(
|
||||
std::launch::async,
|
||||
[](HANDLE read_handle, void* buf, size_t offset, size_t len) {
|
||||
DWORD bytes_read = 0;
|
||||
if (len > static_cast<size_t>(std::numeric_limits<DWORD>::max())) {
|
||||
len = static_cast<size_t>(std::numeric_limits<DWORD>::max());
|
||||
}
|
||||
if (!ReadFile(read_handle, static_cast<char*>(buf) + offset,
|
||||
static_cast<DWORD>(len), &bytes_read, NULL)) {
|
||||
return ReadChunk{-1, GetLastError()};
|
||||
}
|
||||
return ReadChunk{static_cast<std::ptrdiff_t>(bytes_read), 0};
|
||||
},
|
||||
state_->read_handle, buf, offset, len);
|
||||
std::future_status status = future.wait_for(std::chrono::seconds(30));
|
||||
if (status == std::future_status::timeout) {
|
||||
ABSL_LOG(ERROR) << current_test_name_ << ": timeout from test program";
|
||||
*timed_out = true;
|
||||
TerminateProcess(state_->child_process, 1);
|
||||
WaitForSingleObject(state_->child_process, 5000);
|
||||
return false;
|
||||
}
|
||||
|
||||
ReadChunk chunk = future.get();
|
||||
if (chunk.bytes_read == 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": unexpected EOF from test program";
|
||||
return false;
|
||||
} else if (chunk.bytes_read < 0) {
|
||||
ABSL_LOG(ERROR) << current_test_name_
|
||||
<< ": error reading from test program: "
|
||||
<< WindowsErrorMessage(chunk.error_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
len -= static_cast<size_t>(chunk.bytes_read);
|
||||
offset += static_cast<size_t>(chunk.bytes_read);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CheckedRead(void* buf, size_t len) {
|
||||
bool timed_out = false;
|
||||
if (!TryRead(buf, len, &timed_out)) {
|
||||
ABSL_LOG(FATAL) << current_test_name_
|
||||
<< ": error reading from test program: "
|
||||
<< LastSystemError();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ForkPipeRunner::GetTestProgramFailure(bool timed_out) {
|
||||
std::string error_msg;
|
||||
if (timed_out) {
|
||||
CloseTestProgram();
|
||||
return "child timed out";
|
||||
}
|
||||
|
||||
DWORD status = 0;
|
||||
if (state_->child_process != NULL) {
|
||||
WaitForSingleObject(state_->child_process, 5000);
|
||||
}
|
||||
if (state_->child_process != NULL &&
|
||||
GetExitCodeProcess(state_->child_process, &status)) {
|
||||
if (status == STILL_ACTIVE) {
|
||||
error_msg = "child failed while still active";
|
||||
} else {
|
||||
absl::StrAppendFormat(&error_msg, "child exited, status=%d", status);
|
||||
}
|
||||
} else {
|
||||
absl::StrAppendFormat(&error_msg, "child failed: %s", LastSystemError());
|
||||
}
|
||||
CloseTestProgram();
|
||||
return error_msg;
|
||||
}
|
||||
|
||||
void ForkPipeRunner::CloseTestProgram() {
|
||||
if (state_->write_handle != NULL) {
|
||||
CloseHandle(state_->write_handle);
|
||||
state_->write_handle = NULL;
|
||||
}
|
||||
if (state_->read_handle != NULL) {
|
||||
CloseHandle(state_->read_handle);
|
||||
state_->read_handle = NULL;
|
||||
}
|
||||
if (state_->child_process != NULL) {
|
||||
CloseHandle(state_->child_process);
|
||||
state_->child_process = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace protobuf
|
||||
} // namespace google
|
||||
|
||||
#endif // _WIN32
|
||||
|
|
@ -1176,6 +1176,8 @@ set(conformance_runner_srcs
|
|||
${protobuf_SOURCE_DIR}/conformance/conformance_test_runner.cc
|
||||
${protobuf_SOURCE_DIR}/conformance/failure_list_trie_node.cc
|
||||
${protobuf_SOURCE_DIR}/conformance/fork_pipe_runner.cc
|
||||
${protobuf_SOURCE_DIR}/conformance/fork_pipe_runner_posix.cc
|
||||
${protobuf_SOURCE_DIR}/conformance/fork_pipe_runner_win32.cc
|
||||
${protobuf_SOURCE_DIR}/conformance/text_format_conformance_suite.cc
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue