Add tint::Command

Command is a helper used by tests for executing a process with a number of arguments and an optional stdin string, and then collecting and returning the process's stdout and stderr output as strings.

Will be used to invoke HLSL and MSL shader compilers to verify our test generated code actually compiles.

Change-Id: I5cd4ca63af9aaa29be7448bb4fa8422e6d42a8ce
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/41942
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
This commit is contained in:
Ben Clayton 2021-02-18 15:49:08 +00:00 committed by Commit Bot service account
parent a87fda9225
commit ce7e18e87c
7 changed files with 734 additions and 0 deletions

View File

@ -872,6 +872,8 @@ source_set("tint_unittests_core_src") {
"src/type/u32_type_test.cc",
"src/type/vector_type_test.cc",
"src/type_determiner_test.cc",
"src/utils/command_test.cc",
"src/utils/command.h",
"src/utils/tmpfile_test.cc",
"src/utils/tmpfile.h",
"src/utils/unique_vector_test.cc",
@ -885,10 +887,13 @@ source_set("tint_unittests_core_src") {
]
if (is_linux || is_mac) {
sources += [ "src/utils/command_posix.cc" ]
sources += [ "src/utils/tmpfile_posix.cc" ]
} else if (is_win) {
sources += [ "src/utils/command_windows.cc" ]
sources += [ "src/utils/tmpfile_windows.cc" ]
} else {
sources += [ "src/utils/command_other.cc" ]
sources += [ "src/utils/tmpfile_other.cc" ]
}

View File

@ -497,6 +497,8 @@ if(${TINT_BUILD_TESTS})
type/type_manager_test.cc
type/u32_type_test.cc
type/vector_type_test.cc
utils/command_test.cc
utils/command.h
utils/tmpfile_test.cc
utils/tmpfile.h
utils/unique_vector_test.cc
@ -512,9 +514,12 @@ if(${TINT_BUILD_TESTS})
if(UNIX OR APPLE)
list(APPEND TINT_TEST_SRCS utils/tmpfile_posix.cc)
list(APPEND TINT_TEST_SRCS utils/command_posix.cc)
elseif(WIN32)
list(APPEND TINT_TEST_SRCS utils/command_windows.cc)
list(APPEND TINT_TEST_SRCS utils/tmpfile_windows.cc)
else()
list(APPEND TINT_TEST_SRCS utils/command_other.cc)
list(APPEND TINT_TEST_SRCS utils/tmpfile_other.cc)
endif()

81
src/utils/command.h Normal file
View File

@ -0,0 +1,81 @@
// Copyright 2021 The Tint Authors.
//
// 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.
#ifndef SRC_UTILS_COMMAND_H_
#define SRC_UTILS_COMMAND_H_
#include <string>
#include <utility>
namespace tint {
namespace utils {
/// Command is a helper used by tests for executing a process with a number of
/// arguments and an optional stdin string, and then collecting and returning
/// the process's stdout and stderr output as strings.
class Command {
public:
/// Output holds the output of the process
struct Output {
/// stdout from the process
std::string out;
/// stderr from the process
std::string err;
/// process error code
int error_code = 0;
};
/// Constructor
/// @param path path to the executable
explicit Command(const std::string& path);
/// Looks for an executable with the given name in the current working
/// directory, and if not found there, in each of the directories in the
/// `PATH` environment variable.
/// @param executable the executable name
/// @returns a Command which will return true for Found() if the executable
/// was found.
static Command LookPath(const std::string& executable);
/// @return true if the executable exists at the path provided to the
/// constructor
bool Found() const;
/// Invokes the command with the given argument strings, blocking until the
/// process has returned.
/// @param args the string arguments to pass to the process
/// @returns the process output
template <typename... ARGS>
Output operator()(ARGS... args) const {
return Exec({std::forward<ARGS>(args)...});
}
/// Exec invokes the command with the given argument strings, blocking until
/// the process has returned.
/// @param args the string arguments to pass to the process
/// @returns the process output
Output Exec(std::initializer_list<std::string> args) const;
/// @param input the input data to pipe to the process's stdin
void SetInput(const std::string& input) { input_ = input; }
private:
std::string const path_;
std::string input_;
};
} // namespace utils
} // namespace tint
#endif // SRC_UTILS_COMMAND_H_

View File

@ -0,0 +1,38 @@
// Copyright 2021 The Tint Authors.
//
// 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 "src/utils/command.h"
namespace tint {
namespace utils {
Command::Command(const std::string&) {}
Command Command::LookPath(const std::string& executable) {
return Command("");
}
bool Command::Found() const {
return false;
}
Command::Output Command::Exec(
std::initializer_list<std::string> arguments) const {
Output out;
out.err = "Command not supported by this target";
return out;
}
} // namespace utils
} // namespace tint

271
src/utils/command_posix.cc Normal file
View File

@ -0,0 +1,271 @@
// Copyright 2021 The Tint Authors.
//
// 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 "src/utils/command.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sstream>
#include <vector>
namespace tint {
namespace utils {
namespace {
/// File is a simple wrapper around a POSIX file descriptor
class File {
constexpr static const int kClosed = -1;
public:
/// Constructor
File() : handle_(kClosed) {}
/// Constructor
explicit File(int handle) : handle_(handle) {}
/// Destructor
~File() { Close(); }
/// Move assignment operator
File& operator=(File&& rhs) {
Close();
handle_ = rhs.handle_;
rhs.handle_ = kClosed;
return *this;
}
/// Closes the file (if it wasn't already closed)
void Close() {
if (handle_ != kClosed) {
close(handle_);
}
handle_ = kClosed;
}
/// @returns the file handle
operator int() { return handle_; }
/// @returns true if the file is not closed
operator bool() { return handle_ != kClosed; }
private:
File(const File&) = delete;
File& operator=(const File&) = delete;
int handle_ = kClosed;
};
/// Pipe is a simple wrapper around a POSIX pipe() function
class Pipe {
public:
/// Constructs the pipe
Pipe() {
int pipes[2] = {};
if (pipe(pipes) == 0) {
read = File(pipes[0]);
write = File(pipes[1]);
}
}
/// Closes both the read and write files (if they're not already closed)
void Close() {
read.Close();
write.Close();
}
/// @returns true if the pipe has an open read or write file
operator bool() { return read || write; }
/// The reader end of the pipe
File read;
/// The writer end of the pipe
File write;
};
bool ExecutableExists(const std::string& path) {
struct stat s {};
if (stat(path.c_str(), &s) != 0) {
return false;
}
return s.st_mode & S_IXUSR;
}
std::string FindExecutable(const std::string& name) {
if (ExecutableExists(name)) {
return name;
}
if (name.find("/") == std::string::npos) {
auto* path_env = getenv("PATH");
if (!path_env) {
return "";
}
std::istringstream path{path_env};
std::string dir;
while (getline(path, dir, ':')) {
auto test = dir + "/" + name;
if (ExecutableExists(test)) {
return test;
}
}
}
return "";
}
} // namespace
Command::Command(const std::string& path) : path_(path) {}
Command Command::LookPath(const std::string& executable) {
return Command(FindExecutable(executable));
}
bool Command::Found() const {
return ExecutableExists(path_);
}
Command::Output Command::Exec(
std::initializer_list<std::string> arguments) const {
if (!Found()) {
Output out;
out.err = "Executable not found";
return out;
}
// Pipes used for piping std[in,out,err] to / from the target process.
Pipe stdin_pipe;
Pipe stdout_pipe;
Pipe stderr_pipe;
if (!stdin_pipe || !stdout_pipe || !stderr_pipe) {
Output output;
output.err = "Command::Exec(): Failed to create pipes";
return output;
}
// execv() and friends replace the current process image with the target
// process image. To keep process that called this function going, we need to
// fork() this process into a child and parent process.
//
// The child process is responsible for hooking up the pipes to
// std[in,out,err]_pipes to STD[IN,OUT,ERR]_FILENO and then calling execv() to
// run the target command.
//
// The parent process is responsible for feeding any input to the stdin_pipe
// and collectting output from the std[out,err]_pipes.
int child_id = fork();
if (child_id < 0) {
Output output;
output.err = "Command::Exec(): fork() failed";
return output;
}
if (child_id > 0) {
// fork() - parent
// Close the stdout and stderr writer pipes.
// This is required for getting poll() POLLHUP events.
stdout_pipe.write.Close();
stderr_pipe.write.Close();
// Write the input to the child process
if (!input_.empty()) {
ssize_t n = write(stdin_pipe.write, input_.data(), input_.size());
if (n != static_cast<ssize_t>(input_.size())) {
Output output;
output.err = "Command::Exec(): write() for stdin failed";
return output;
}
}
stdin_pipe.write.Close();
// Accumulate the stdout and stderr output from the child process
pollfd poll_fds[2];
poll_fds[0].fd = stdout_pipe.read;
poll_fds[0].events = POLLIN;
poll_fds[1].fd = stderr_pipe.read;
poll_fds[1].events = POLLIN;
Output output;
bool stdout_open = true;
bool stderr_open = true;
while (stdout_open || stderr_open) {
if (poll(poll_fds, 2, -1) < 0) {
break;
}
char buf[256];
if (poll_fds[0].revents & POLLIN) {
auto n = read(stdout_pipe.read, buf, sizeof(buf));
if (n > 0) {
output.out += std::string(buf, buf + n);
}
}
if (poll_fds[0].revents & POLLHUP) {
stdout_open = false;
}
if (poll_fds[1].revents & POLLIN) {
auto n = read(stderr_pipe.read, buf, sizeof(buf));
if (n > 0) {
output.err += std::string(buf, buf + n);
}
}
if (poll_fds[1].revents & POLLHUP) {
stderr_open = false;
}
}
// Get the resulting error code
waitpid(child_id, &output.error_code, 0);
return output;
} else {
// fork() - child
// Redirect the stdin, stdout, stderr pipes for the execv process
if ((dup2(stdin_pipe.read, STDIN_FILENO) == -1) ||
(dup2(stdout_pipe.write, STDOUT_FILENO) == -1) ||
(dup2(stderr_pipe.write, STDERR_FILENO) == -1)) {
fprintf(stderr, "Command::Exec(): Failed to redirect pipes");
exit(errno);
}
// Close the pipes, once redirected above, we're now done with them.
stdin_pipe.Close();
stdout_pipe.Close();
stderr_pipe.Close();
// Run target executable
std::vector<const char*> args;
args.emplace_back(path_.c_str());
for (auto& arg : arguments) {
args.emplace_back(arg.c_str());
}
args.emplace_back(nullptr);
auto res = execv(path_.c_str(), const_cast<char* const*>(args.data()));
exit(res);
}
}
} // namespace utils
} // namespace tint

92
src/utils/command_test.cc Normal file
View File

@ -0,0 +1,92 @@
// Copyright 2021 The Tint Authors.
//
// 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 "src/utils/command.h"
#include "gtest/gtest.h"
namespace tint {
namespace utils {
namespace {
#ifdef _WIN32
TEST(CommandTest, Echo) {
auto cmd = Command::LookPath("cmd");
if (!cmd.Found()) {
GTEST_SKIP() << "cmd not found on PATH";
}
auto res = cmd("/C", "echo", "hello world");
EXPECT_EQ(res.error_code, 0);
EXPECT_EQ(res.out, "hello world\r\n");
EXPECT_EQ(res.err, "");
}
#else
TEST(CommandTest, Echo) {
auto cmd = Command::LookPath("echo");
if (!cmd.Found()) {
GTEST_SKIP() << "echo not found on PATH";
}
auto res = cmd("hello world");
EXPECT_EQ(res.error_code, 0);
EXPECT_EQ(res.out, "hello world\n");
EXPECT_EQ(res.err, "");
}
TEST(CommandTest, Cat) {
auto cmd = Command::LookPath("cat");
if (!cmd.Found()) {
GTEST_SKIP() << "cat not found on PATH";
}
cmd.SetInput("hello world");
auto res = cmd();
EXPECT_EQ(res.error_code, 0);
EXPECT_EQ(res.out, "hello world");
EXPECT_EQ(res.err, "");
}
TEST(CommandTest, True) {
auto cmd = Command::LookPath("true");
if (!cmd.Found()) {
GTEST_SKIP() << "true not found on PATH";
}
auto res = cmd();
EXPECT_EQ(res.error_code, 0);
EXPECT_EQ(res.out, "");
EXPECT_EQ(res.err, "");
}
TEST(CommandTest, False) {
auto cmd = Command::LookPath("false");
if (!cmd.Found()) {
GTEST_SKIP() << "false not found on PATH";
}
auto res = cmd();
EXPECT_NE(res.error_code, 0);
EXPECT_EQ(res.out, "");
EXPECT_EQ(res.err, "");
}
#endif
} // namespace
} // namespace utils
} // namespace tint

View File

@ -0,0 +1,242 @@
// Copyright 2021 The Tint Authors.
//
// 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 "src/utils/command.h"
#define WIN32_LEAN_AND_MEAN 1
#include <Windows.h>
#include <sstream>
#include <string>
namespace tint {
namespace utils {
namespace {
/// Handle is a simple wrapper around the Win32 HANDLE
class Handle {
public:
/// Constructor
Handle() : handle_(nullptr) {}
/// Constructor
explicit Handle(HANDLE handle) : handle_(handle) {}
/// Destructor
~Handle() { Close(); }
/// Move assignment operator
Handle& operator=(Handle&& rhs) {
Close();
handle_ = rhs.handle_;
rhs.handle_ = nullptr;
return *this;
}
/// Closes the handle (if it wasn't already closed)
void Close() {
if (handle_) {
CloseHandle(handle_);
}
handle_ = nullptr;
}
/// @returns the handle
operator HANDLE() { return handle_; }
/// @returns true if the handle is not invalid
operator bool() { return handle_ != nullptr; }
private:
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
HANDLE handle_ = nullptr;
};
/// Pipe is a simple wrapper around a Win32 CreatePipe() function
class Pipe {
public:
/// Constructs the pipe
Pipe() {
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = nullptr;
HANDLE hread;
HANDLE hwrite;
if (CreatePipe(&hread, &hwrite, &sa, 0)) {
read = Handle(hread);
write = Handle(hwrite);
// Ensure the read handle to the pipe is not inherited
if (!SetHandleInformation(read, HANDLE_FLAG_INHERIT, 0)) {
read.Close();
write.Close();
}
}
}
/// @returns true if the pipe has an open read or write file
operator bool() { return read || write; }
/// The reader end of the pipe
Handle read;
/// The writer end of the pipe
Handle write;
};
bool ExecutableExists(const std::string& path) {
DWORD type = 0;
return GetBinaryTypeA(path.c_str(), &type);
}
std::string FindExecutable(const std::string& name) {
if (ExecutableExists(name)) {
return name;
}
if (ExecutableExists(name + ".exe")) {
return name + ".exe";
}
if (name.find("/") == std::string::npos &&
name.find("\\") == std::string::npos) {
char* path_env = nullptr;
size_t path_env_len = 0;
if (_dupenv_s(&path_env, &path_env_len, "PATH")) {
return "";
}
std::istringstream path{path_env};
free(path_env);
std::string dir;
while (getline(path, dir, ';')) {
auto test = dir + "\\" + name;
if (ExecutableExists(test)) {
return test;
}
if (ExecutableExists(test + ".exe")) {
return test + ".exe";
}
}
}
return "";
}
} // namespace
Command::Command(const std::string& path) : path_(path) {}
Command Command::LookPath(const std::string& executable) {
return Command(FindExecutable(executable));
}
bool Command::Found() const {
return ExecutableExists(path_);
}
Command::Output Command::Exec(
std::initializer_list<std::string> arguments) const {
Pipe stdout_pipe;
Pipe stderr_pipe;
Pipe stdin_pipe;
if (!stdin_pipe || !stdout_pipe || !stderr_pipe) {
Output output;
output.err = "Command::Exec(): Failed to create pipes";
return output;
}
if (!input_.empty()) {
if (!WriteFile(stdin_pipe.write, input_.data(), input_.size(), nullptr,
nullptr)) {
Output output;
output.err = "Command::Exec() Failed to write stdin";
return output;
}
}
stdin_pipe.write.Close();
STARTUPINFOA si{};
si.cb = sizeof(si);
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdOutput = stdout_pipe.write;
si.hStdError = stderr_pipe.write;
si.hStdInput = stdin_pipe.read;
std::stringstream args;
args << path_;
for (auto& arg : arguments) {
args << " " << arg;
}
PROCESS_INFORMATION pi{};
if (!CreateProcessA(nullptr, // No module name (use command line)
const_cast<LPSTR>(args.str().c_str()), // Command line
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
TRUE, // Handles are inherited
0, // No creation flags
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi)) { // Pointer to PROCESS_INFORMATION structure
Output out;
out.err = "Command::Exec() CreateProcess() failed";
return out;
}
stdout_pipe.write.Close();
stderr_pipe.write.Close();
Output output;
char buf[256];
HANDLE handles[] = {stdout_pipe.read, stderr_pipe.read};
bool stdout_open = true;
bool stderr_open = true;
while (stdout_open || stderr_open) {
auto res = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
switch (res) {
case WAIT_FAILED:
output.err = "Command::Exec() WaitForMultipleObjects() returned " +
std::to_string(res);
return output;
case WAIT_OBJECT_0: { // stdout
DWORD n = 0;
if (ReadFile(stdout_pipe.read, buf, sizeof(buf), &n, NULL)) {
output.out += std::string(buf, buf + n);
} else {
stdout_open = false;
}
break;
}
case WAIT_OBJECT_0 + 1: { // stderr
DWORD n = 0;
if (ReadFile(stderr_pipe.read, buf, sizeof(buf), &n, NULL)) {
output.err += std::string(buf, buf + n);
} else {
stderr_open = false;
}
break;
}
}
}
WaitForSingleObject(pi.hProcess, INFINITE);
return output;
}
} // namespace utils
} // namespace tint