aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/test/unit_tests/shared
diff options
context:
space:
mode:
authorEgor Tensin <Egor.Tensin@gmail.com>2020-10-24 21:33:44 +0300
committerEgor Tensin <Egor.Tensin@gmail.com>2020-10-27 00:11:41 +0300
commita864099ba77157090c4cd12817245122c163ec24 (patch)
treef15b1f2801219fa9af6111fa28591858d87711ec /test/unit_tests/shared
parentProcess: add termination methods (diff)
downloadwinapi-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.hpp142
-rw-r--r--test/unit_tests/shared/console.hpp138
-rw-r--r--test/unit_tests/shared/fixed_size.hpp79
-rw-r--r--test/unit_tests/shared/test_data.hpp24
-rw-r--r--test/unit_tests/shared/worker.hpp86
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