add testing and plan milestones
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
#include "hot_reload.h"
|
||||
#include "desktop_platform.h"
|
||||
#include "testing/ui_inspector.h"
|
||||
#include "testing/action_recorder.h"
|
||||
#include "testing/action_player.h"
|
||||
|
||||
// Command-line options
|
||||
struct Options {
|
||||
@@ -31,6 +33,8 @@ struct Options {
|
||||
std::string output_dir = "dump";
|
||||
std::string log_file; // If set, write logs to this file
|
||||
std::string hierarchy_file; // If set, dump UI hierarchy to this file each frame
|
||||
std::string record_file; // If set, record actions to JSON file
|
||||
std::string playback_file; // If set, play back actions from JSON file
|
||||
};
|
||||
|
||||
// Global log file stream
|
||||
@@ -89,6 +93,43 @@ static mosis::IKernel* g_kernel = nullptr;
|
||||
static std::filesystem::path g_assets_path;
|
||||
static mosis::testing::UIInspector g_ui_inspector;
|
||||
|
||||
// Recording/playback state
|
||||
static std::unique_ptr<mosis::testing::ActionRecorder> g_recorder;
|
||||
static std::unique_ptr<mosis::testing::ActionPlayer> g_player;
|
||||
static std::string g_record_file_path;
|
||||
|
||||
// Key callback for F5 (recording control)
|
||||
bool HandleKeyDown(Rml::Context* context, Rml::Input::KeyIdentifier key, int key_modifier, float native_dp_ratio, bool priority) {
|
||||
// F5: Toggle recording / Save recording
|
||||
if (key == Rml::Input::KI_F5 && g_recorder) {
|
||||
if (g_recorder->IsRecording()) {
|
||||
g_recorder->StopRecording();
|
||||
if (g_recorder->SaveToFile(g_record_file_path)) {
|
||||
LogMessage("Recording saved to: " + g_record_file_path);
|
||||
} else {
|
||||
LogMessage("ERROR: Failed to save recording to: " + g_record_file_path);
|
||||
}
|
||||
} else {
|
||||
g_recorder->StartRecording();
|
||||
LogMessage("Recording started (press F5 to stop and save)");
|
||||
}
|
||||
return true; // Consumed
|
||||
}
|
||||
|
||||
// F6: Pause/resume playback
|
||||
if (key == Rml::Input::KI_F6 && g_player) {
|
||||
if (g_player->IsPlaying()) {
|
||||
g_player->Pause();
|
||||
LogMessage("Playback paused");
|
||||
} else if (g_player->IsPaused()) {
|
||||
g_player->Resume();
|
||||
LogMessage("Playback resumed");
|
||||
}
|
||||
return true; // Consumed
|
||||
}
|
||||
|
||||
return false; // Not consumed, let RmlUi handle it
|
||||
}
|
||||
|
||||
int main(int argc, const char* argv[])
|
||||
{
|
||||
@@ -206,6 +247,25 @@ int main(int argc, const char* argv[])
|
||||
// Start kernel
|
||||
kernel->Start();
|
||||
|
||||
// Initialize recording if enabled
|
||||
if (!opts.record_file.empty()) {
|
||||
g_recorder = std::make_unique<mosis::testing::ActionRecorder>(opts.width, opts.height);
|
||||
g_record_file_path = opts.record_file;
|
||||
LogMessage("Recording mode enabled. Press F5 to start recording.");
|
||||
}
|
||||
|
||||
// Initialize playback if enabled
|
||||
if (!opts.playback_file.empty()) {
|
||||
g_player = std::make_unique<mosis::testing::ActionPlayer>(context);
|
||||
if (g_player->LoadFromFile(opts.playback_file)) {
|
||||
LogMessage("Loaded playback file: " + opts.playback_file);
|
||||
g_player->Start();
|
||||
} else {
|
||||
LogMessage("ERROR: Failed to load playback file: " + opts.playback_file);
|
||||
g_player.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup hot-reload
|
||||
std::unique_ptr<mosis::desktop::HotReload> hot_reload;
|
||||
if (!opts.dump_mode) {
|
||||
@@ -228,8 +288,16 @@ int main(int argc, const char* argv[])
|
||||
hot_reload->CheckForChanges();
|
||||
}
|
||||
|
||||
// Process events and update
|
||||
running = Backend::ProcessEvents(context);
|
||||
// Process events and update (with key callback for F5/F6 control)
|
||||
running = Backend::ProcessEvents(context, HandleKeyDown);
|
||||
|
||||
// Update playback if active
|
||||
if (g_player && g_player->IsPlaying()) {
|
||||
g_player->Update();
|
||||
if (g_player->IsFinished()) {
|
||||
LogMessage("Playback complete");
|
||||
}
|
||||
}
|
||||
|
||||
// Update kernel (processes tasks, updates time, etc.)
|
||||
kernel->Update();
|
||||
@@ -249,6 +317,16 @@ int main(int argc, const char* argv[])
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
// Stop and save recording if still active
|
||||
if (g_recorder && g_recorder->IsRecording()) {
|
||||
g_recorder->StopRecording();
|
||||
if (g_recorder->SaveToFile(g_record_file_path)) {
|
||||
LogMessage("Recording saved on exit to: " + g_record_file_path);
|
||||
}
|
||||
}
|
||||
g_recorder.reset();
|
||||
g_player.reset();
|
||||
|
||||
kernel->Stop();
|
||||
kernel.reset();
|
||||
g_kernel = nullptr;
|
||||
@@ -282,11 +360,19 @@ void PrintUsage(const char* program)
|
||||
std::cout << " --output DIR Output directory for dump mode (default: dump)" << std::endl;
|
||||
std::cout << " --log FILE Write log output to file (for automated testing)" << std::endl;
|
||||
std::cout << " --hierarchy FILE Continuously dump UI hierarchy to JSON file" << std::endl;
|
||||
std::cout << " --record FILE Record actions to JSON file (F5 to start/stop)" << std::endl;
|
||||
std::cout << " --playback FILE Play back recorded actions from JSON file" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Recording Controls:" << std::endl;
|
||||
std::cout << " F5 Start/stop recording (when --record is enabled)" << std::endl;
|
||||
std::cout << " F6 Pause/resume playback (when --playback is enabled)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Examples:" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --resolution 720x1280" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --dump" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --record test.json" << std::endl;
|
||||
std::cout << " " << program << " assets/apps/home/home.rml --playback test.json" << std::endl;
|
||||
}
|
||||
|
||||
Options ParseOptions(int argc, const char* argv[])
|
||||
@@ -313,6 +399,10 @@ Options ParseOptions(int argc, const char* argv[])
|
||||
opts.log_file = argv[++i];
|
||||
} else if (arg == "--hierarchy" && i + 1 < argc) {
|
||||
opts.hierarchy_file = argv[++i];
|
||||
} else if (arg == "--record" && i + 1 < argc) {
|
||||
opts.record_file = argv[++i];
|
||||
} else if (arg == "--playback" && i + 1 < argc) {
|
||||
opts.playback_file = argv[++i];
|
||||
} else if (arg[0] != '-') {
|
||||
opts.document_path = arg;
|
||||
}
|
||||
|
||||
@@ -84,35 +84,132 @@ bool VisualCapture::CaptureScreenshot(const std::string& path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper struct for loaded PNG data
|
||||
struct PNGImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> pixels; // RGBA format
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// Load a PNG file into memory
|
||||
static PNGImage LoadPNG(const std::string& path) {
|
||||
PNGImage image;
|
||||
|
||||
FILE* fp = fopen(path.c_str(), "rb");
|
||||
if (!fp) return image;
|
||||
|
||||
// Check PNG signature
|
||||
uint8_t header[8];
|
||||
if (fread(header, 1, 8, fp) != 8 || png_sig_cmp(header, 0, 8)) {
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
||||
if (!png) {
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
png_infop info = png_create_info_struct(png);
|
||||
if (!info) {
|
||||
png_destroy_read_struct(&png, nullptr, nullptr);
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png))) {
|
||||
png_destroy_read_struct(&png, &info, nullptr);
|
||||
fclose(fp);
|
||||
return image;
|
||||
}
|
||||
|
||||
png_init_io(png, fp);
|
||||
png_set_sig_bytes(png, 8);
|
||||
png_read_info(png, info);
|
||||
|
||||
image.width = png_get_image_width(png, info);
|
||||
image.height = png_get_image_height(png, info);
|
||||
int color_type = png_get_color_type(png, info);
|
||||
int bit_depth = png_get_bit_depth(png, info);
|
||||
|
||||
// Convert to RGBA if necessary
|
||||
if (bit_depth == 16)
|
||||
png_set_strip_16(png);
|
||||
if (color_type == PNG_COLOR_TYPE_PALETTE)
|
||||
png_set_palette_to_rgb(png);
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
|
||||
png_set_expand_gray_1_2_4_to_8(png);
|
||||
if (png_get_valid(png, info, PNG_INFO_tRNS))
|
||||
png_set_tRNS_to_alpha(png);
|
||||
if (color_type == PNG_COLOR_TYPE_RGB ||
|
||||
color_type == PNG_COLOR_TYPE_GRAY ||
|
||||
color_type == PNG_COLOR_TYPE_PALETTE)
|
||||
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
|
||||
if (color_type == PNG_COLOR_TYPE_GRAY ||
|
||||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
|
||||
png_set_gray_to_rgb(png);
|
||||
|
||||
png_read_update_info(png, info);
|
||||
|
||||
// Allocate memory and read image
|
||||
image.pixels.resize(image.width * image.height * 4);
|
||||
std::vector<png_bytep> row_pointers(image.height);
|
||||
for (int y = 0; y < image.height; y++) {
|
||||
row_pointers[y] = image.pixels.data() + y * image.width * 4;
|
||||
}
|
||||
|
||||
png_read_image(png, row_pointers.data());
|
||||
png_read_end(png, nullptr);
|
||||
|
||||
png_destroy_read_struct(&png, &info, nullptr);
|
||||
fclose(fp);
|
||||
|
||||
image.valid = true;
|
||||
return image;
|
||||
}
|
||||
|
||||
// Compare two pixels with tolerance
|
||||
static bool PixelsMatch(const uint8_t* p1, const uint8_t* p2, int tolerance = 2) {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
if (std::abs(static_cast<int>(p1[c]) - static_cast<int>(p2[c])) > tolerance) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
float VisualCapture::CompareImages(const std::string& path1, const std::string& path2) {
|
||||
// Load both images and compare pixel by pixel
|
||||
// For simplicity, just check if files exist for now
|
||||
// A full implementation would load PNGs and compute difference
|
||||
|
||||
FILE* fp1 = fopen(path1.c_str(), "rb");
|
||||
FILE* fp2 = fopen(path2.c_str(), "rb");
|
||||
|
||||
if (!fp1 || !fp2) {
|
||||
if (fp1) fclose(fp1);
|
||||
if (fp2) fclose(fp2);
|
||||
return 1.0f; // Can't compare, assume different
|
||||
// Load both images
|
||||
PNGImage img1 = LoadPNG(path1);
|
||||
PNGImage img2 = LoadPNG(path2);
|
||||
|
||||
// Check if both loaded successfully
|
||||
if (!img1.valid || !img2.valid) {
|
||||
return 1.0f; // Can't compare, assume completely different
|
||||
}
|
||||
|
||||
// Read and compare file sizes as quick check
|
||||
fseek(fp1, 0, SEEK_END);
|
||||
fseek(fp2, 0, SEEK_END);
|
||||
long size1 = ftell(fp1);
|
||||
long size2 = ftell(fp2);
|
||||
|
||||
fclose(fp1);
|
||||
fclose(fp2);
|
||||
|
||||
// If sizes differ significantly, images are different
|
||||
if (std::abs(size1 - size2) > 1000) {
|
||||
return 0.5f; // Moderately different
|
||||
|
||||
// Check dimensions match
|
||||
if (img1.width != img2.width || img1.height != img2.height) {
|
||||
return 1.0f; // Different dimensions = completely different
|
||||
}
|
||||
|
||||
return 0.0f; // Assume similar if sizes match
|
||||
|
||||
// Count differing pixels
|
||||
int total_pixels = img1.width * img1.height;
|
||||
int diff_pixels = 0;
|
||||
|
||||
for (int i = 0; i < total_pixels; i++) {
|
||||
const uint8_t* p1 = img1.pixels.data() + i * 4;
|
||||
const uint8_t* p2 = img2.pixels.data() + i * 4;
|
||||
|
||||
if (!PixelsMatch(p1, p2)) {
|
||||
diff_pixels++;
|
||||
}
|
||||
}
|
||||
|
||||
// Return difference ratio (0.0 = identical, 1.0 = completely different)
|
||||
return static_cast<float>(diff_pixels) / static_cast<float>(total_pixels);
|
||||
}
|
||||
|
||||
} // namespace mosis::testing
|
||||
|
||||
Reference in New Issue
Block a user