| /* |
| * Copyright 2024 Rive |
| */ |
| |
| #include "common/test_harness.hpp" |
| #include "common/stacktrace.hpp" |
| #include "rive/rive_types.hpp" |
| #include "rive/math/math_types.hpp" |
| #include "tcp_client.hpp" |
| #include "png.h" |
| #include "zlib.h" |
| #include <vector> |
| |
| #if defined(_WIN32) && !defined(NO_REDIRECT_OUTPUT) |
| #include <io.h> |
| static int pipe(int pipefd[2]) { return _pipe(pipefd, 65536, 0); } |
| #endif |
| |
| #ifdef RIVE_ANDROID |
| #include <android/log.h> |
| #endif |
| |
| constexpr static uint32_t REQUEST_TYPE_IMAGE_UPLOAD = 0; |
| constexpr static uint32_t REQUEST_TYPE_CLAIM_GM_TEST = 1; |
| constexpr static uint32_t REQUEST_TYPE_FETCH_RIV_FILE = 2; |
| constexpr static uint32_t REQUEST_TYPE_GET_INPUT = 3; |
| constexpr static uint32_t REQUEST_TYPE_CANCEL_INPUT = 4; |
| constexpr static uint32_t REQUEST_TYPE_PRINT_MESSAGE = 5; |
| constexpr static uint32_t REQUEST_TYPE_DISCONNECT = 6; |
| constexpr static uint32_t REQUEST_TYPE_APPLICATION_CRASH = 7; |
| |
| static void check_early_exit() |
| { |
| if (TestHarness::Instance().initialized()) |
| { |
| // The tool should have called shutdown() before exit. |
| TestHarness::Instance().onApplicationCrash("Early exit."); |
| } |
| } |
| |
| static void signal_wraper(const char* msg) |
| { |
| TestHarness::Instance().onApplicationCrash(msg); |
| } |
| |
| TestHarness& TestHarness::Instance() |
| { |
| static TestHarness* instance = new TestHarness(); |
| return *instance; |
| } |
| |
| TestHarness::TestHarness() |
| { |
| stacktrace::replace_signal_handlers(signal_wraper, check_early_exit); |
| } |
| |
| void TestHarness::init(std::unique_ptr<TCPClient> tcpClient, |
| size_t pngThreadCount) |
| { |
| assert(!m_initialized); |
| m_initialized = true; |
| |
| m_primaryTCPClient = std::move(tcpClient); |
| |
| initStdioThread(); |
| |
| // We don't compile with emscripten pthreads. |
| #ifndef __EMSCRIPTEN__ |
| for (size_t i = 0; i < pngThreadCount; ++i) |
| { |
| m_encodeThreads.emplace_back(EncodePNGThread, this); |
| } |
| #endif |
| } |
| |
| void TestHarness::init(std::filesystem::path outputDir, size_t pngThreadCount) |
| { |
| assert(!m_initialized); |
| m_initialized = true; |
| m_outputDir = outputDir; |
| std::filesystem::create_directories(m_outputDir); |
| |
| #ifdef RIVE_ANDROID |
| // Still pipe stdout and sterr to the android logs. |
| initStdioThread(); |
| #endif |
| |
| for (size_t i = 0; i < pngThreadCount; ++i) |
| { |
| m_encodeThreads.emplace_back(EncodePNGThread, this); |
| } |
| } |
| |
| void TestHarness::initStdioThread() |
| { |
| #if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__) |
| #ifndef _WIN32 |
| // Make stdout & stderr line buffered. (This is not supported on Windows.) |
| setvbuf(stdout, NULL, _IOLBF, 0); |
| setvbuf(stderr, NULL, _IOLBF, 0); |
| #endif |
| // Pipe stdout and sterr back to the server. |
| m_savedStdout = dup(1); |
| m_savedStderr = dup(2); |
| pipe(m_stdioPipe.data()); |
| dup2(m_stdioPipe[1], 1); |
| dup2(m_stdioPipe[1], 2); |
| m_stdioThread = std::thread(MonitorStdIOThread, this); |
| #endif |
| } |
| |
| void TestHarness::monitorStdIOThread() |
| { |
| #if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__) |
| assert(m_initialized); |
| |
| std::unique_ptr<TCPClient> threadTCPClient; |
| if (m_primaryTCPClient != nullptr) |
| { |
| threadTCPClient = m_primaryTCPClient->clone(); |
| } |
| |
| char buff[1024]; |
| size_t readSize; |
| while ((readSize = read(m_stdioPipe[0], buff, std::size(buff) - 1)) > 0) |
| { |
| buff[readSize] = '\0'; |
| if (threadTCPClient != nullptr) |
| { |
| threadTCPClient->send4(REQUEST_TYPE_PRINT_MESSAGE); |
| threadTCPClient->sendString(buff); |
| } |
| else if (FILE* f = nullptr/* |
| fopen((m_outputDir / "rive_log.png").string().c_str(), |
| "a")*/) |
| { |
| // Sometimes it can help to also save a log file (e.g., when |
| // ANDROID_LOG_DEBUG gets lost on browserstack). |
| // DANGER: Writing this file may also cause failures, so turn the |
| // if() back on with caution. |
| fwrite(buff, 1, strlen(buff), f); |
| fclose(f); |
| } |
| #ifdef RIVE_ANDROID |
| __android_log_write(ANDROID_LOG_DEBUG, "rive_android_tests", buff); |
| #endif |
| } |
| |
| if (threadTCPClient != nullptr) |
| { |
| threadTCPClient->send4(REQUEST_TYPE_DISCONNECT); |
| threadTCPClient->send4(false /* Don't shutdown the server yet */); |
| } |
| #endif |
| } |
| |
| void send_png_data_chunk(png_structp png, png_bytep data, png_size_t length) |
| { |
| auto tcpClient = reinterpret_cast<TCPClient*>(png_get_io_ptr(png)); |
| tcpClient->send4(rive::math::lossless_numeric_cast<uint32_t>(length)); |
| tcpClient->sendall(data, length); |
| } |
| |
| void flush_png_data(png_structp png) {} |
| |
| static void save_png_impl(ImageSaveArgs args, |
| const std::filesystem::path& outputDir, |
| PNGCompression pngCompression, |
| TCPClient* tcpClient) |
| { |
| assert(args.width > 0); |
| assert(args.height > 0); |
| std::string pngName = args.name + ".png"; |
| |
| if (tcpClient == nullptr) |
| { |
| // We aren't connect to a test harness. Just save a file. |
| auto destination = outputDir; |
| destination /= pngName; |
| destination.make_preferred(); |
| WritePNGFile(args.pixels.data(), |
| args.width, |
| args.height, |
| true, |
| destination.generic_string().c_str(), |
| pngCompression); |
| return; |
| } |
| |
| tcpClient->send4(REQUEST_TYPE_IMAGE_UPLOAD); |
| tcpClient->sendString(pngName); |
| |
| auto png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); |
| if (!png) |
| { |
| fprintf(stderr, "TestHarness: png_create_write_struct failed\n"); |
| abort(); |
| } |
| |
| // RLE with SUB gets best performance with our content. |
| png_set_compression_level(png, 6); |
| png_set_compression_strategy(png, Z_RLE); |
| png_set_compression_strategy(png, Z_RLE); |
| png_set_filter(png, 0, PNG_FILTER_SUB); |
| |
| auto info = png_create_info_struct(png); |
| if (!info) |
| { |
| fprintf(stderr, "TestHarness: png_create_info_struct failed\n"); |
| abort(); |
| } |
| |
| if (setjmp(png_jmpbuf(png))) |
| { |
| fprintf(stderr, "TestHarness: Error during init_io\n"); |
| abort(); |
| } |
| |
| png_set_write_fn(png, tcpClient, &send_png_data_chunk, &flush_png_data); |
| |
| // Write header. |
| if (setjmp(png_jmpbuf(png))) |
| { |
| fprintf(stderr, "TestHarness: Error during writing header\n"); |
| abort(); |
| } |
| |
| png_set_IHDR(png, |
| info, |
| args.width, |
| args.height, |
| 8, |
| PNG_COLOR_TYPE_RGB_ALPHA, |
| PNG_INTERLACE_NONE, |
| PNG_COMPRESSION_TYPE_BASE, |
| PNG_FILTER_TYPE_BASE); |
| |
| png_write_info(png, info); |
| |
| // Write bytes. |
| if (setjmp(png_jmpbuf(png))) |
| { |
| fprintf(stderr, "TestHarness: Error during writing bytes\n"); |
| abort(); |
| } |
| |
| std::vector<uint8_t*> rows(args.height); |
| for (uint32_t y = 0; y < args.height; ++y) |
| { |
| rows[y] = args.pixels.data() + (args.height - 1 - y) * args.width * 4; |
| } |
| png_write_image(png, rows.data()); |
| |
| // End write. |
| if (setjmp(png_jmpbuf(png))) |
| { |
| fprintf(stderr, "TestHarness: Error during end of write"); |
| abort(); |
| } |
| |
| png_write_end(png, NULL); |
| png_destroy_write_struct(&png, &info); |
| |
| tcpClient->sendHandshake(); |
| tcpClient->recvHandshake(); |
| } |
| |
| void TestHarness::savePNG(ImageSaveArgs args) |
| { |
| assert(m_initialized); |
| if (!m_encodeThreads.empty()) |
| { |
| m_encodeQueue.push(std::move(args)); |
| } |
| else |
| { |
| save_png_impl(std::move(args), |
| m_outputDir, |
| m_pngCompression, |
| m_primaryTCPClient.get()); |
| } |
| } |
| |
| void TestHarness::encodePNGThread() |
| { |
| assert(m_initialized); |
| |
| std::unique_ptr<TCPClient> threadTCPClient; |
| if (m_primaryTCPClient != nullptr) |
| { |
| threadTCPClient = m_primaryTCPClient->clone(); |
| } |
| |
| for (;;) |
| { |
| ImageSaveArgs args; |
| m_encodeQueue.pop(args); |
| if (args.quit) |
| { |
| break; |
| } |
| save_png_impl(std::move(args), |
| m_outputDir, |
| m_pngCompression, |
| threadTCPClient.get()); |
| } |
| |
| if (threadTCPClient != nullptr) |
| { |
| threadTCPClient->send4(REQUEST_TYPE_DISCONNECT); |
| threadTCPClient->send4(false /* Don't shutdown the server yet */); |
| } |
| } |
| |
| bool TestHarness::claimGMTest(const std::string& name) |
| { |
| if (m_primaryTCPClient != nullptr) |
| { |
| m_primaryTCPClient->send4(REQUEST_TYPE_CLAIM_GM_TEST); |
| m_primaryTCPClient->sendString(name); |
| return m_primaryTCPClient->recv4(); |
| } |
| return true; |
| } |
| |
| bool TestHarness::fetchRivFile(std::string& name, std::vector<uint8_t>& bytes) |
| { |
| if (m_primaryTCPClient == nullptr) |
| { |
| return false; |
| } |
| m_primaryTCPClient->send4(REQUEST_TYPE_FETCH_RIV_FILE); |
| uint32_t nameLength = m_primaryTCPClient->recv4(); |
| if (nameLength == TCPClient::SHUTDOWN_TOKEN) |
| { |
| return false; |
| } |
| name.resize(nameLength); |
| m_primaryTCPClient->recvall(name.data(), nameLength); |
| uint32_t fileSize = m_primaryTCPClient->recv4(); |
| bytes.resize(fileSize); |
| m_primaryTCPClient->recvall(bytes.data(), fileSize); |
| return true; |
| } |
| |
| void TestHarness::printMessageOnServer(const char* msg) |
| { |
| if (m_primaryTCPClient != nullptr) |
| { |
| m_primaryTCPClient->send4(REQUEST_TYPE_PRINT_MESSAGE); |
| m_primaryTCPClient->sendString(msg); |
| } |
| } |
| |
| void TestHarness::inputPumpThread() |
| { |
| assert(m_initialized); |
| |
| if (m_primaryTCPClient == nullptr) |
| { |
| return; |
| } |
| |
| std::unique_ptr<TCPClient> threadTCPClient = m_primaryTCPClient->clone(); |
| |
| for (std::vector<char> keys; m_initialized;) |
| { |
| threadTCPClient->send4(REQUEST_TYPE_GET_INPUT); |
| uint32_t len = threadTCPClient->recv4(); |
| if (len == TCPClient::SHUTDOWN_TOKEN) |
| { |
| return; |
| } |
| keys.resize(len); |
| threadTCPClient->recvall(keys.data(), len); |
| for (char key : keys) |
| { |
| m_inputQueue.push(char(key)); |
| } |
| } |
| |
| threadTCPClient->send4(REQUEST_TYPE_DISCONNECT); |
| threadTCPClient->send4(false /* Don't shutdown the server yet */); |
| } |
| |
| bool TestHarness::peekChar(char& key) |
| { |
| #ifdef __EMSCRIPTEN__ |
| // We don't compile with emscripten pthreads. |
| return false; |
| #endif |
| if (m_primaryTCPClient == nullptr) |
| { |
| return false; |
| } |
| if (!m_inputPumpThread.joinable()) |
| { |
| m_inputPumpThread = std::thread(InputPumpThread, this); |
| } |
| return m_inputQueue.try_pop(key); |
| } |
| |
| void TestHarness::shutdown() |
| { |
| if (!m_initialized) |
| { |
| return; |
| } |
| |
| for (std::thread& thread RIVE_MAYBE_UNUSED : m_encodeThreads) |
| { |
| m_encodeQueue.push({.quit = true}); |
| } |
| for (std::thread& thread : m_encodeThreads) |
| { |
| thread.join(); |
| } |
| m_encodeThreads.clear(); |
| |
| shutdownStdioThread(); |
| shutdownInputPumpThread(); |
| |
| if (m_primaryTCPClient != nullptr) |
| { |
| m_primaryTCPClient->send4(REQUEST_TYPE_DISCONNECT); |
| m_primaryTCPClient->send4(true /* Shutdown the server */); |
| m_primaryTCPClient = nullptr; |
| } |
| |
| m_initialized = false; |
| } |
| |
| void TestHarness::shutdownStdioThread() |
| { |
| #if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__) |
| if (m_savedStdout != 0 || m_savedStderr != 0) |
| { |
| // Restore stdout and stderr. |
| dup2(m_savedStdout, 1); |
| dup2(m_savedStderr, 2); |
| close(m_savedStdout); |
| close(m_savedStderr); |
| m_savedStdout = m_savedStderr = 0; |
| |
| // Shutdown the stdio-monitoring thread and pipe. |
| close(m_stdioPipe[1]); |
| m_stdioThread.join(); |
| close(m_stdioPipe[0]); |
| m_stdioPipe = {0, 0}; |
| } |
| #endif |
| } |
| |
| void TestHarness::shutdownInputPumpThread() |
| { |
| if (m_inputPumpThread.joinable()) |
| { |
| m_primaryTCPClient->send4(REQUEST_TYPE_CANCEL_INPUT); |
| m_inputPumpThread.join(); |
| } |
| } |
| |
| void TestHarness::onApplicationCrash(const char* message) |
| { |
| if (m_primaryTCPClient != nullptr) |
| { |
| // Buy monitorStdIOThread() some time to finish pumping any messages |
| // related to this abort. |
| |
| // std::this_thread::sleep_for causes weird link issues in unreal. just |
| // use sleep instead |
| #if defined(RIVE_UNREAL) && defined(_WIN32) |
| Sleep(100); |
| #else |
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| #endif |
| shutdownStdioThread(); |
| shutdownInputPumpThread(); |
| m_primaryTCPClient->send4(REQUEST_TYPE_APPLICATION_CRASH); |
| m_primaryTCPClient->sendString(message); |
| } |
| } |