| /* | |
| * Copyright 2024 Rive | |
| */ | |
| #include "common/test_harness.hpp" | |
| #include "common/report_fatal_error.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; | |
| #ifndef RIVE_REMOTE_ONLY | |
| std::filesystem::create_directories(m_outputDir); | |
| #endif | |
| #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 rive_tests::report_fatal_error(const char* serverAddress, | |
| const char* message) | |
| { | |
| fprintf(stderr, "%s\n", message); | |
| fflush(stderr); | |
| if (serverAddress == nullptr || serverAddress[0] == '\0') | |
| { | |
| return; | |
| } | |
| if (std::unique_ptr<TCPClient> tcpClient = | |
| TCPClient::Connect(serverAddress)) | |
| { | |
| tcpClient->send4(REQUEST_TYPE_APPLICATION_CRASH); | |
| tcpClient->sendString(message); | |
| } | |
| } | |
| 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); | |
| } | |
| } |