blob: 66ba692766596d751dd661fa8bc47b01fd93205c [file] [log] [blame] [edit]
/*
* 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);
}
}