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 | |
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 '')
-rw-r--r-- | test/unit_tests/CMakeLists.txt | 18 | ||||
-rw-r--r-- | test/unit_tests/console.cpp | 40 | ||||
-rw-r--r-- | test/unit_tests/fixtures.hpp | 31 | ||||
-rw-r--r-- | test/unit_tests/process.cpp | 3 | ||||
-rw-r--r-- | test/unit_tests/process_worker.cpp | 278 | ||||
-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 | ||||
-rw-r--r-- | test/unit_tests/worker/worker.cpp | 118 |
11 files changed, 946 insertions, 11 deletions
diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt index fb87230..7311d75 100644 --- a/test/unit_tests/CMakeLists.txt +++ b/test/unit_tests/CMakeLists.txt @@ -1,5 +1,7 @@ +file(GLOB shared_src "shared/*.hpp") + file(GLOB unit_tests_src "*.cpp" "*.h" "*.hpp" "*.rc") -add_executable(unit_tests ${unit_tests_src}) +add_executable(unit_tests ${unit_tests_src} ${shared_src}) set_target_properties(unit_tests PROPERTIES OUTPUT_NAME winapi-common-unit-tests) target_link_libraries(unit_tests PRIVATE winapi_common winapi_utf8) @@ -11,3 +13,17 @@ install(TARGETS unit_tests RUNTIME DESTINATION bin) if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") install(FILES "$<TARGET_PDB_FILE:unit_tests>" DESTINATION bin OPTIONAL) endif() + +file(GLOB worker_src "worker/*.cpp") +add_executable(worker ${worker_src} ${shared_src}) +set_target_properties(worker PROPERTIES OUTPUT_NAME winapi-common-test-worker) + +target_link_libraries(worker PRIVATE winapi_common winapi_utf8) + +find_package(Boost REQUIRED) +target_link_libraries(worker PRIVATE Boost::disable_autolinking Boost::boost) + +install(TARGETS worker RUNTIME DESTINATION bin) +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + install(FILES "$<TARGET_PDB_FILE:worker>" DESTINATION bin OPTIONAL) +endif() diff --git a/test/unit_tests/console.cpp b/test/unit_tests/console.cpp new file mode 100644 index 0000000..9eb787c --- /dev/null +++ b/test/unit_tests/console.cpp @@ -0,0 +1,40 @@ +// 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. + +#include "shared/console.hpp" + +#include <boost/test/unit_test.hpp> + +#include <iostream> +#include <string> +#include <vector> + +BOOST_AUTO_TEST_SUITE(console_tests) + +BOOST_AUTO_TEST_CASE(read_last_lines) { + std::cout << "abc\ndef" << std::endl; + console::Buffer buffer; + const auto last = buffer.read_last_line(); + const auto last2 = buffer.read_last_lines(2); + const std::string expected{"def"}; + const std::vector<std::string> expected2{"abc", "def"}; + BOOST_TEST(last == expected); + BOOST_TEST(last2 == expected2); +} + +BOOST_AUTO_TEST_CASE(read_last_lines_overflow) { + console::Buffer buffer; + const std::string output(buffer.get_columns() + 5, 'X'); + std::cout << output << std::endl; + buffer.update(); + const auto last = buffer.read_last_lines(1); + const auto last2 = buffer.read_last_lines(2); + const std::vector<std::string> expected{"XXXXX"}; + const std::vector<std::string> expected2{std::string(buffer.get_columns(), 'X'), "XXXXX"}; + BOOST_TEST(last == expected); + BOOST_TEST(last2 == expected2); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/unit_tests/fixtures.hpp b/test/unit_tests/fixtures.hpp index 293ad35..aaf6571 100644 --- a/test/unit_tests/fixtures.hpp +++ b/test/unit_tests/fixtures.hpp @@ -5,6 +5,8 @@ #pragma once +#include "shared/command.hpp" + #include <winapi/cmd_line.hpp> #include <winapi/file.hpp> #include <winapi/path.hpp> @@ -34,18 +36,13 @@ private: RemoveFileGuard& operator=(const RemoveFileGuard&) = delete; }; -class WithEchoExe { +class WithParam { public: - WithEchoExe() : m_echo_exe(find_echo_exe()) {} + WithParam(const std::string& param_prefix) : m_value(find_param_value(param_prefix)) {} - const std::string& get_echo_exe() { return m_echo_exe; } + const std::string& get_value() { return m_value; } private: - static std::string find_echo_exe() { - static const std::string prefix{"--echo_exe="}; - return find_param_value(prefix); - } - static std::string find_param_value(const std::string& param_prefix) { const auto cmd_line = winapi::CommandLine::query(); const auto& args = cmd_line.get_args(); @@ -57,5 +54,21 @@ private: throw std::runtime_error{"couldn't find parameter " + param_prefix}; } - std::string m_echo_exe; + std::string m_value; +}; + +class WithEchoExe : public WithParam { +public: + WithEchoExe() : WithParam{"--echo_exe="} {} + + const std::string& get_echo_exe() { return get_value(); } +}; + +class WithWorkerExe : public WithParam { +public: + WithWorkerExe() : WithParam{"--worker_exe="}, m_cmd{worker::Command::create()} {} + + const std::string& get_worker_exe() { return get_value(); } + + worker::Command::Shared m_cmd; }; diff --git a/test/unit_tests/process.cpp b/test/unit_tests/process.cpp index cfc50f4..a048c84 100644 --- a/test/unit_tests/process.cpp +++ b/test/unit_tests/process.cpp @@ -86,7 +86,8 @@ BOOST_FIXTURE_TEST_CASE(echo_stdin_from_file, WithEchoExe) { BOOST_FIXTURE_TEST_CASE(echo_runas, WithEchoExe) { const CommandLine cmd_line{get_echo_exe(), {"foo", "bar"}}; - const auto process = Process::runas(cmd_line); + const auto params = ShellParameters::runas(cmd_line); + const auto process = Process::shell(params); process.wait(); BOOST_TEST(process.get_exit_code() == 0); } diff --git a/test/unit_tests/process_worker.cpp b/test/unit_tests/process_worker.cpp new file mode 100644 index 0000000..492ffff --- /dev/null +++ b/test/unit_tests/process_worker.cpp @@ -0,0 +1,278 @@ +// 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. + +#include "fixtures.hpp" +#include "shared/command.hpp" +#include "shared/console.hpp" +#include "shared/test_data.hpp" +#include "shared/worker.hpp" + +#include <winapi/buffer.hpp> +#include <winapi/cmd_line.hpp> +#include <winapi/handle.hpp> +#include <winapi/pipe.hpp> +#include <winapi/process.hpp> +#include <winapi/process_io.hpp> + +#include <boost/test/unit_test.hpp> + +#include <windows.h> + +#include <cstddef> +#include <iostream> +#include <sstream> +#include <string> +#include <utility> +#include <vector> + +using winapi::Buffer; +using winapi::CommandLine; +using winapi::Handle; +using winapi::Pipe; +using winapi::Process; +using winapi::ProcessParameters; +namespace process = winapi::process; + +using worker::StdHandles; +using worker::Worker; + +namespace { + +void check_window_same(Worker& worker) { + BOOST_TEST(::GetConsoleWindow()); + BOOST_TEST(worker.get_console_window() == ::GetConsoleWindow()); + BOOST_TEST(worker.is_window_visible()); +} + +void check_window_none(Worker& worker) { + BOOST_TEST(::GetConsoleWindow()); + BOOST_TEST(!worker.get_console_window()); + BOOST_TEST(!worker.is_window_visible()); +} + +void check_window_new(Worker& worker) { + BOOST_TEST(::GetConsoleWindow()); + BOOST_TEST(worker.get_console_window()); + BOOST_TEST(worker.get_console_window() != ::GetConsoleWindow()); + BOOST_TEST(worker.is_window_visible()); +} + +void check_std_handles(Worker& worker, const StdHandles& expected) { + const auto actual = worker.get_std_handles(); + BOOST_TEST(actual.in == expected.in); + BOOST_TEST(actual.out == expected.out); + BOOST_TEST(actual.err == expected.err); +} + +void check_std_handles_same(Worker& worker) { + check_std_handles(worker, + {::GetStdHandle(STD_INPUT_HANDLE), + ::GetStdHandle(STD_OUTPUT_HANDLE), + ::GetStdHandle(STD_ERROR_HANDLE)}); +} + +void check_std_handles_different(Worker& worker) { + const auto actual = worker.get_std_handles(); + BOOST_TEST(actual.in); + BOOST_TEST(actual.out); + BOOST_TEST(actual.err); + BOOST_TEST(actual.in != ::GetStdHandle(STD_INPUT_HANDLE)); + BOOST_TEST(actual.out != ::GetStdHandle(STD_OUTPUT_HANDLE)); + BOOST_TEST(actual.err != ::GetStdHandle(STD_ERROR_HANDLE)); +} + +void check_write(Worker& worker) { + const auto handles = worker.test_write(); + BOOST_TEST(!Handle::is_invalid(handles.in)); + BOOST_TEST(!Handle::is_invalid(handles.out)); + BOOST_TEST(!Handle::is_invalid(handles.err)); +} + +void check_redirected_output(const Buffer& buffer, const std::vector<std::string>& expected_lines) { + const auto actual = buffer.as_utf8(); + std::ostringstream oss; + for (const auto& line : expected_lines) { + oss << line << "\r\n"; + } + const std::string expected{oss.str()}; + BOOST_TEST(actual == expected); +} + +const std::string PLACEHOLDER{"This is a placeholder line."}; + +void check_console_buffer_inherit(Worker& worker, const std::vector<std::string>& expected) { + // Check that + // 1) the worker process writes the expected lines to its console window, + // 2) the lines appear in this process's window, since they're the same. + + const auto numof_lines = expected.size(); + // Write some lines in this process's window, they should be overwritten + // by worker's writes. + for (std::size_t i = 0; i < numof_lines; ++i) { + std::cout << PLACEHOLDER << std::endl; + } + // Order worker to write the lines: + const auto handles = worker.test_write(); + // Query worker process's window: + const auto worker_actual = worker.read_last_lines(numof_lines); + // Query this process's window: + const auto this_actual = console::Buffer{}.read_last_lines(numof_lines); + + // They should look the same: + const auto& worker_expected = expected; + const auto& this_expected = worker_expected; + + BOOST_TEST(worker_actual == worker_expected); + BOOST_TEST(this_actual == this_expected); +} + +void check_console_buffer_new(Worker& worker, const std::vector<std::string>& expected) { + // Check that + // 1) the worker process writes the expected lines in its console window, + // 2) the lines _do not_ appear in this process's window, since they're + // different windows. + + const auto numof_lines = expected.size(); + // Write some lines in this process's window, they _should not_ be + // overwritten by worker's writes. + for (std::size_t i = 0; i < numof_lines; ++i) { + std::cout << PLACEHOLDER << std::endl; + } + // Order worker to write the lines: + const auto handles = worker.test_write(); + // Query worker process's window: + const auto worker_actual = worker.read_last_lines(numof_lines); + // Query this process's window: + const auto this_actual = console::Buffer{}.read_last_lines(numof_lines); + + // Placeholder lines are still last in this process's window: + const auto& worker_expected = expected; + const std::vector<std::string> this_expected(numof_lines, PLACEHOLDER); + + BOOST_TEST(worker_actual == worker_expected); + BOOST_TEST(this_actual == this_expected); +} + +// CREATE_NO_WINDOW actually does create a new buffer, it's just invisible. +void check_console_buffer_none(Worker& worker, const std::vector<std::string>& expected) { + check_console_buffer_new(worker, expected); +} + +} // namespace + +BOOST_AUTO_TEST_SUITE(process_console_tests) + +BOOST_FIXTURE_TEST_CASE(create_inherit, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleInherit; + + Worker worker{Process::create(std::move(params))}; + check_window_same(worker); + check_std_handles_same(worker); + check_write(worker); + check_console_buffer_inherit(worker, {worker::test_data::out(), worker::test_data::err()}); + BOOST_TEST(worker.exit() == 0); +} + +BOOST_FIXTURE_TEST_CASE(create_inherit_override, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleInherit; + + Pipe stdin_pipe, stderr_pipe; + const StdHandles expected_handles{ + stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()}; + + process::IO io; + io.std_in = process::Stdin{stdin_pipe}; + io.std_err = process::Stderr{stderr_pipe}; + params.io = std::move(io); + + Worker worker{Process::create(std::move(params))}; + check_window_same(worker); + check_std_handles(worker, expected_handles); + check_write(worker); + check_console_buffer_inherit(worker, {worker::test_data::out()}); + BOOST_TEST(worker.exit() == 0); + check_redirected_output(stderr_pipe.read_end().read(), + {worker::test_data::err(), worker::test_data::err()}); +} + +BOOST_FIXTURE_TEST_CASE(create_none, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleNone; + + Worker worker{Process::create(std::move(params))}; + check_window_none(worker); + check_std_handles_different(worker); + check_write(worker); + check_console_buffer_none(worker, {worker::test_data::out(), worker::test_data::err()}); + BOOST_TEST(worker.exit() == 0); +} + +BOOST_FIXTURE_TEST_CASE(create_none_override, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleNone; + + Pipe stdin_pipe, stderr_pipe; + const StdHandles expected_handles{ + stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()}; + + process::IO io; + io.std_in = process::Stdin{stdin_pipe}; + io.std_err = process::Stderr{stderr_pipe}; + params.io = std::move(io); + + Worker worker{Process::create(std::move(params))}; + check_window_none(worker); + check_std_handles(worker, expected_handles); + check_write(worker); + check_console_buffer_none(worker, {worker::test_data::out()}); + BOOST_TEST(worker.exit() == 0); + check_redirected_output(stderr_pipe.read_end().read(), + {worker::test_data::err(), worker::test_data::err()}); +} + +BOOST_FIXTURE_TEST_CASE(create_new, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleNew; + + Worker worker{Process::create(std::move(params))}; + check_window_new(worker); + check_std_handles_different(worker); + check_write(worker); + check_console_buffer_new(worker, {worker::test_data::out(), worker::test_data::err()}); + BOOST_TEST(worker.exit() == 0); +} + +BOOST_FIXTURE_TEST_CASE(create_new_override, WithWorkerExe) { + const CommandLine cmd_line{get_worker_exe()}; + ProcessParameters params{cmd_line}; + params.console_mode = ProcessParameters::ConsoleNew; + + Pipe stdin_pipe, stderr_pipe; + const StdHandles expected_handles{ + stdin_pipe.read_end().get(), Handle::std_out().get(), stderr_pipe.write_end().get()}; + + process::IO io; + io.std_in = process::Stdin{stdin_pipe}; + io.std_err = process::Stderr{stderr_pipe}; + params.io = std::move(io); + + Worker worker{Process::create(std::move(params))}; + check_window_new(worker); + check_std_handles(worker, expected_handles); + check_write(worker); + check_console_buffer_new(worker, {worker::test_data::out()}); + BOOST_TEST(worker.exit() == 0); + check_redirected_output(stderr_pipe.read_end().read(), + {worker::test_data::err(), worker::test_data::err()}); +} + +BOOST_AUTO_TEST_SUITE_END() 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 diff --git a/test/unit_tests/worker/worker.cpp b/test/unit_tests/worker/worker.cpp new file mode 100644 index 0000000..9e48deb --- /dev/null +++ b/test/unit_tests/worker/worker.cpp @@ -0,0 +1,118 @@ +// 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. + +#include "../shared/command.hpp" +#include "../shared/console.hpp" +#include "../shared/test_data.hpp" + +#include <winapi/error.hpp> +#include <winapi/handle.hpp> + +#include <windows.h> + +#include <chrono> +#include <cstddef> +#include <exception> +#include <stdexcept> +#include <string> +#include <thread> +#include <vector> + +using worker::Command; + +namespace { + +bool is_window_visible() { + HWND window = ::GetConsoleWindow(); + + if (window == NULL) { + return false; + } + + const auto mask = ::GetWindowLongW(window, GWL_STYLE); + + if (!mask) { + throw winapi::error::windows(GetLastError(), "GetWindowLongW"); + } + + return mask & WS_VISIBLE; +} + +winapi::Handle write_to(winapi::Handle dest, const std::string& msg) { + try { + dest.write(msg + "\r\n"); + } catch (const std::exception& e) { + return winapi::Handle{}; + } + return dest; +} + +std::vector<std::string> get_console_buffer(std::size_t numof_lines) { + return console::Buffer{}.read_last_lines(numof_lines); +} + +void process_action(Command::Action action, const Command::Args& args, Command::Result& result) { + switch (action) { + case Command::EXIT: + break; + + case Command::GET_CONSOLE_WINDOW: + result.console_window = ::GetConsoleWindow(); + break; + + case Command::IS_WINDOW_VISIBLE: + result.is_window_visible = is_window_visible(); + break; + + case Command::GET_STD_HANDLES: + result.std_handles.in = ::GetStdHandle(STD_INPUT_HANDLE); + result.std_handles.out = ::GetStdHandle(STD_OUTPUT_HANDLE); + result.std_handles.err = ::GetStdHandle(STD_ERROR_HANDLE); + break; + + case Command::TEST_WRITE: + result.std_handles.in = winapi::Handle::std_in().get(); + result.std_handles.out = + write_to(winapi::Handle::std_out(), worker::test_data::out()).get(); + result.std_handles.err = + write_to(winapi::Handle::std_err(), worker::test_data::err()).get(); + break; + + case Command::GET_CONSOLE_BUFFER: + result.console_buffer = + result.console_buffer.convert(get_console_buffer(args.numof_lines)); + break; + + default: + throw std::runtime_error{"invalid worker command"}; + } +} + +int loop() { + auto exit_loop = false; + + const auto cmd = Command::open(); + + while (!exit_loop) { + cmd->process_action([&exit_loop](Command::Action action, + const Command::Args& args, + Command::Result& result) { + if (action == Command::EXIT) { + exit_loop = true; + } else { + process_action(action, args, result); + } + }); + } + return 0; +} + +} // namespace + +int main(int argc, char* argv[]) { + int ec = loop(); + std::this_thread::sleep_for(std::chrono::milliseconds{1000}); + return ec; +} |