diff options
author | Egor Tensin <Egor.Tensin@gmail.com> | 2020-10-24 21:33:44 +0300 |
---|---|---|
committer | Egor Tensin <Egor.Tensin@gmail.com> | 2020-10-27 00:11:41 +0300 |
commit | a864099ba77157090c4cd12817245122c163ec24 (patch) | |
tree | f15b1f2801219fa9af6111fa28591858d87711ec /test/unit_tests/shared | |
parent | Process: add termination methods (diff) | |
download | winapi-common-a864099ba77157090c4cd12817245122c163ec24.tar.gz winapi-common-a864099ba77157090c4cd12817245122c163ec24.zip |
rework Process API & tests
* Add separate classes ProcessParameters & ShellParameters for
Process::create() and Process::shell() methods.
* Add a separate "worker" executable. It's used in tests via a fairly
complicated scheme, receiving orders to execute via a shared memory
region.
* Add tests that utilize the Console API, reading console window's
screen buffer directly, making for more reliable tests & broader
coverage.
Diffstat (limited to 'test/unit_tests/shared')
-rw-r--r-- | test/unit_tests/shared/command.hpp | 142 | ||||
-rw-r--r-- | test/unit_tests/shared/console.hpp | 138 | ||||
-rw-r--r-- | test/unit_tests/shared/fixed_size.hpp | 79 | ||||
-rw-r--r-- | test/unit_tests/shared/test_data.hpp | 24 | ||||
-rw-r--r-- | test/unit_tests/shared/worker.hpp | 86 |
5 files changed, 469 insertions, 0 deletions
diff --git a/test/unit_tests/shared/command.hpp b/test/unit_tests/shared/command.hpp new file mode 100644 index 0000000..de7c704 --- /dev/null +++ b/test/unit_tests/shared/command.hpp @@ -0,0 +1,142 @@ +// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include "fixed_size.hpp" + +#include <winapi/shmem.hpp> + +#include <boost/config.hpp> +#include <boost/interprocess/sync/interprocess_condition.hpp> +#include <boost/interprocess/sync/interprocess_mutex.hpp> +#include <boost/interprocess/sync/scoped_lock.hpp> + +#include <windows.h> + +#include <cstddef> +#include <exception> +#include <functional> +#include <stdexcept> + +namespace worker { + +struct StdHandles { + HANDLE in; + HANDLE out; + HANDLE err; +}; + +class Command { +public: + BOOST_STATIC_CONSTEXPR auto SHMEM_NAME = "shmem-test-cmd"; + + typedef winapi::SharedObject<Command> Shared; + + static Shared create() { return Shared::create(SHMEM_NAME); } + static Shared open() { return Shared::open(SHMEM_NAME); } + + typedef boost::interprocess::interprocess_mutex mutex; + typedef boost::interprocess::interprocess_condition condition_variable; + typedef boost::interprocess::scoped_lock<mutex> lock; + + enum Action { + EXIT = 1, + GET_CONSOLE_WINDOW, + IS_WINDOW_VISIBLE, + GET_STD_HANDLES, + TEST_WRITE, + GET_CONSOLE_BUFFER, + }; + + union Args { + std::size_t numof_lines; + }; + + union Result { + HWND console_window; + bool is_window_visible; + StdHandles std_handles; + fixed_size::StringList<> console_buffer; + }; + + typedef std::function<void(Args&)> SetArgs; + typedef std::function<void(const Result&)> ReadResult; + + void get_result(Action action, const SetArgs& set_args, const ReadResult& read_result) { + { + lock lck{m_mtx}; + m_action = action; + set_args(m_args); + + m_action_requested = true; + m_result_ready = false; + m_error_occured = false; + } + m_cv.notify_all(); + + lock lck{m_mtx}; + m_cv.wait(lck, [this]() { return m_error_occured || m_result_ready; }); + + const auto error = m_error_occured; + + m_action_requested = false; + m_result_ready = false; + m_error_occured = false; + + if (error) { + throw std::runtime_error{"Worker error: " + m_error.extract()}; + } + + read_result(m_result); + } + + void get_result(Action action, const ReadResult& read_result) { + return get_result( + action, [](Args&) {}, read_result); + } + + void get_result(Action action) { + return get_result(action, [](const Result&) {}); + } + + typedef std::function<void(Action, const Args&, Result&)> ProcessAction; + + void process_action(const ProcessAction& callback) { + { + lock lck{m_mtx}; + m_cv.wait(lck, [this]() { return m_action_requested; }); + + m_action_requested = false; + m_result_ready = false; + m_error_occured = false; + + try { + callback(m_action, m_args, m_result); + m_result_ready = true; + } catch (const std::exception& e) { + m_error_occured = true; + m_error = m_error.convert(e.what()); + } + } + m_cv.notify_all(); + } + +private: + Action m_action; + Args m_args; + bool m_action_requested = false; + + Result m_result; + bool m_result_ready = false; + + bool m_error_occured = false; + fixed_size::String<> m_error; + + mutex m_mtx; + condition_variable m_cv; +}; + +} // namespace worker diff --git a/test/unit_tests/shared/console.hpp b/test/unit_tests/shared/console.hpp new file mode 100644 index 0000000..0a415e5 --- /dev/null +++ b/test/unit_tests/shared/console.hpp @@ -0,0 +1,138 @@ +// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include <winapi/error.hpp> +#include <winapi/handle.hpp> +#include <winapi/utf8.hpp> + +#include <boost/algorithm/string.hpp> + +#include <windows.h> + +#include <cstddef> +#include <cstring> +#include <sstream> +#include <string> +#include <utility> +#include <vector> + +namespace console { + +class Buffer { +public: + typedef CONSOLE_SCREEN_BUFFER_INFO Info; + + Buffer() : m_handle{winapi::Handle::std_out()}, m_info{get_info(m_handle)} {} + + Buffer(winapi::Handle&& handle) : m_handle{std::move(handle)}, m_info{get_info(m_handle)} {} + + std::size_t get_columns() const { return m_info.dwSize.X; } + + std::size_t get_lines() const { return m_info.dwSize.Y; } + + std::size_t get_cursor_column() const { return m_info.dwCursorPosition.X; } + + std::size_t get_cursor_line() const { return m_info.dwCursorPosition.Y; } + + void update() { m_info = get_info(m_handle); } + + /* + * This is a stupid little function to read the console screen buffer. + * It's fragile and will break whenever anything happens (like, if the + * console screen is resized). + * + * The screen buffer: + * 1) doesn't preserve line breaks, + * 2) pads text lines with spaces (ASCII 0x20) for storage. + * + * Hence, the "lines" read are one-dimensional arrays, right-trimmed, and + * there's no way to learn whether the whole line printed by the user was + * read back in its entirety. + * + * For example, let the console window be 80 columns wide, and the user + * prints 85 consecutive characters 'a' using + * + * std::cout << std::string(85, 'a') << '\n'; + * + * The following holds: + * 1) read_lines(-1, -1) == {"aaaaa"}, + * 2) read_lines(-2, -2) == {std::string(80, 'a'), "aaaaa"}. + * + * I also don't know how it interacts with tab characters '\t', encodings, + * etc. It sucks, don't use it. + */ + std::vector<std::string> read_lines(int top, int bottom) const { + if (top < 0) { + top = get_cursor_line() + top; + } + if (bottom < 0) { + bottom = get_cursor_line() + bottom; + } + if (top > bottom) { + std::swap(top, bottom); + } + int numof_lines = bottom - top + 1; + + COORD buffer_size; + buffer_size.X = get_columns(); + buffer_size.Y = numof_lines; + + COORD buffer_coord; + buffer_coord.X = 0; + buffer_coord.Y = 0; + + std::vector<CHAR_INFO> buffer; + buffer.resize(buffer_size.X * buffer_size.Y); + + SMALL_RECT read_region; + read_region.Top = top; + read_region.Left = 0; + read_region.Bottom = bottom; + read_region.Right = buffer_size.X - 1; + + if (!::ReadConsoleOutputW( + m_handle.get(), buffer.data(), buffer_size, buffer_coord, &read_region)) { + throw winapi::error::windows(GetLastError(), "ReadConsoleOutputW"); + } + + std::vector<std::string> result; + for (std::size_t i = 0; i < numof_lines; ++i) { + std::wostringstream oss; + for (std::size_t c = 0; c < buffer_size.X; ++c) { + oss << buffer[i * buffer_size.X + c].Char.UnicodeChar; + } + result.emplace_back(boost::trim_right_copy(winapi::narrow(oss.str()))); + } + + return result; + } + + std::vector<std::string> read_last_lines(int numof_lines = 1) const { + return read_lines(-numof_lines, -1); + } + + std::string read_last_line() const { return read_lines(-1, -1)[0]; } + + std::string read_line(int n) const { return read_lines(n, n)[0]; } + +private: + static Info get_info(const winapi::Handle& handle) { + Info dest; + std::memset(&dest, 0, sizeof(dest)); + + if (!::GetConsoleScreenBufferInfo(static_cast<HANDLE>(handle), &dest)) { + throw winapi::error::windows(GetLastError(), "GetConsoleScreenBufferInfo"); + } + + return dest; + } + + winapi::Handle m_handle; + Info m_info; +}; + +} // namespace console diff --git a/test/unit_tests/shared/fixed_size.hpp b/test/unit_tests/shared/fixed_size.hpp new file mode 100644 index 0000000..e4b11c7 --- /dev/null +++ b/test/unit_tests/shared/fixed_size.hpp @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include <array> +#include <cstddef> +#include <cstring> +#include <string> +#include <vector> + +namespace fixed_size { + +/* + * These are fixed-size classes, to be used as part of an interprocess shared + * memory region. + */ + +// 120 characters is an arbitrary limit, strings are cut to this many +// characters for storage. +template <std::size_t Length = 120> +class String : public std::array<char, Length> { +public: + static String convert(const std::string& src) { + String dest; + std::size_t nch = dest.size() - 1; + if (src.size() < nch) { + nch = src.size(); + } + std::memcpy(dest.data(), src.c_str(), nch); + dest[nch] = '\0'; + return dest; + } + + std::string extract() const { + // Lines are null-terminated, and don't store their lenghts, so... + return data(); + } + +private: +}; + +// 5 lines to store is also arbitrary, set it higher if needed. +template <std::size_t Length = 5, std::size_t StringLength = 120> +struct StringList : public std::array<String<StringLength>, Length> { + static StringList convert(const std::vector<std::string>& src) { + // If src.size() > Length, only the last Length lines from the source + // list are stored. + + StringList dest; + std::memset(&dest, 0, sizeof(dest)); + + std::size_t src_offset = 0; + if (src.size() > dest.size()) { + src_offset = src.size() - dest.size(); + } + + for (std::size_t i = 0, j = src_offset; i < dest.size() && j < src.size(); + ++i, ++j, ++dest.numof_lines) { + dest[i] = dest[i].convert(src[j]); + } + + return dest; + } + + std::vector<std::string> extract() const { + std::vector<std::string> dest; + for (std::size_t i = 0; i < numof_lines; ++i) { + dest.emplace_back(at(i).extract()); + } + return dest; + } + + std::size_t numof_lines; +}; + +} // namespace fixed_size diff --git a/test/unit_tests/shared/test_data.hpp b/test/unit_tests/shared/test_data.hpp new file mode 100644 index 0000000..0094f0b --- /dev/null +++ b/test/unit_tests/shared/test_data.hpp @@ -0,0 +1,24 @@ +// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include <string> + +namespace worker { +namespace test_data { + +BOOST_STATIC_CONSTEXPR auto str = "Test output."; + +inline std::string out() { + return "stdout: " + std::string{str}; +} + +inline std::string err() { + return "stderr: " + std::string{str}; +} + +} // namespace test_data +} // namespace worker diff --git a/test/unit_tests/shared/worker.hpp b/test/unit_tests/shared/worker.hpp new file mode 100644 index 0000000..320a924 --- /dev/null +++ b/test/unit_tests/shared/worker.hpp @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Egor Tensin <Egor.Tensin@gmail.com> +// This file is part of the "winapi-common" project. +// For details, see https://github.com/egor-tensin/winapi-common. +// Distributed under the MIT License. + +#pragma once + +#include "command.hpp" + +#include <winapi/process.hpp> + +#include <windows.h> + +#include <exception> +#include <string> +#include <utility> +#include <vector> + +namespace worker { + +class Worker { +public: + Worker(winapi::Process&& process) : m_cmd{Command::create()}, m_process{std::move(process)} {} + + ~Worker() { + try { + if (m_process.is_running()) { + exit(); + } + } catch (const std::exception&) { + } + } + + HWND get_console_window() { + HWND ret = NULL; + m_cmd->get_result(Command::GET_CONSOLE_WINDOW, + [&ret](const Command::Result& result) { ret = result.console_window; }); + return ret; + } + + bool is_window_visible() { + bool ret = false; + m_cmd->get_result(Command::IS_WINDOW_VISIBLE, [&ret](const Command::Result& result) { + ret = result.is_window_visible; + }); + return ret; + } + + StdHandles get_std_handles() { + StdHandles ret; + m_cmd->get_result(Command::GET_STD_HANDLES, + [&ret](const Command::Result& result) { ret = result.std_handles; }); + return ret; + } + + StdHandles test_write() { + StdHandles ret; + m_cmd->get_result(Command::TEST_WRITE, + [&ret](const Command::Result& result) { ret = result.std_handles; }); + return ret; + } + + std::vector<std::string> read_last_lines(std::size_t numof_lines) { + std::vector<std::string> ret; + const auto set_args = [numof_lines](Command::Args& args) { + args.numof_lines = numof_lines; + }; + const auto read_result = [&ret](const Command::Result& result) { + ret = result.console_buffer.extract(); + }; + m_cmd->get_result(Command::GET_CONSOLE_BUFFER, set_args, read_result); + return ret; + } + + int exit() { + m_cmd->get_result(Command::EXIT); + m_process.wait(); + return m_process.get_exit_code(); + } + +private: + Command::Shared m_cmd; + winapi::Process m_process; +}; + +} // namespace worker |