-
Notifications
You must be signed in to change notification settings - Fork 53
Setting Up
- Version Requirement
- References
- Building
- Integrating Library
- Other I/O
- Setting up custom Coroutines Interface
- Processing Results
It is expected that updates to imgui and imgui_test_engine are synchronized to similar dates. Even though we'll put some effort at supporting it, old versions of imgui_test_engine may not work with newer versions of Dear ImGui, and vice-versa.
Dear ImGui 1.87+ is required and your backend needs to be submitting IO events via the new API introduced in 1.87 (see #4921). If you use a custom backend, updating for it to use the io.AddXXXEvent()
functions should be straightforward.
imgui_test_suite/: before anything, build and run and play around with the interactive test suite application to get a feel of things.
app_minimal/ is a simple application showcasing how to integrate the test engine in your project.
- Add to your app/project all source files in
imgui_test_engine/
. - In your imconfig file used to compile the main imgui library, add
#include "imgui_te_imconfig.h"
or add configuration directives listed in that file. Basically imgui needs to be compiled with at least#define IMGUI_ENABLE_TEST_ENGINE
, and maybe other options listed in that file. - You need to provide an implementation for
ImGuiTestCoroutineInterface
or you may#define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1
in your configuration file to use a default implementation usingstd::thread
. We usestd::thread
as convenience because it is highly portable nowadays (more so than coroutine implementation). See Setting up coroutines interface for details. - Optionally: You may redirect your assert to
IM_TEST_ENGINE_ASSERT()
: we use custom logging + debug break here to facilitate recovery in a debugger. Seeimgui_test_suite_imconfig.h
for reference of what we do in the test suite. - Optionally:
#define IMGUI_TEST_ENGINE_ENABLE_IMPLOT 1
is in your configuration file to add plotting of performance tests. You need to provide your own copy of implot (imgui_test_suite/ embeds one for our own needs, but imgui_test_engine/ itself does not embed it.). - Other configuration options are runtime, by poking in the ImGuiTestEngineIO structure.
- If you have building issues, you may Open an Issue.
// Initialize Dear ImGui and related backends
ImGui::CreateContext();
[...]
// Initialize Test Engine
ImGuiTestEngine* engine = ImGuiTestEngine_CreateContext();
ImGuiTestEngineIO& test_io = ImGuiTestEngine_GetIO(engine);
test_io.ConfigVerboseLevel = ImGuiTestVerboseLevel_Info;
test_io.ConfigVerboseLevelOnError = ImGuiTestVerboseLevel_Debug;
[...]
// Register your Tests
RegisterMyTests(engine); // will call IM_REGISTER_TEST() etc.
// Start test engine
ImGuiTestEngine_Start(engine, ImGui::GetCurrentContext());
// Optional: use default crash handler. You may use your own crash handler and call ImGuiTestEngine_CrashHandler() from it.
ImGuiTestEngine_InstallCrashHandler();
// May block until TestFunc thread/coroutine joins
ImGuiTestEngine_Stop(engine);
// We shutdown the Dear ImGui context _before_ the test engine context, so .ini data may be saved.
ImGui::DestroyContext();
ImGuiTestEngine_ShutdownContext(engine);
while (app_running)
{
[...]
// This will automatically call ImGuiTestEngine_PreNewFrame() to override user inputs when automation is active.
// This will automatically call ImGuiTestEngine_PostNewFrame() to new GuiFunc() and TestFunc() when active.
ImGui::NewFrame();
[...]
// Optionally: show test engine UI to browse/run test from the UI
ImGuiTestEngine_ShowTestEngineWindows();
// Rendering
my_rendering_backend_swap();
// Call after your rendering. This is mostly to support screen/video capturing features.
ImGuiTestEngine_PostSwap(engine);
}
Because the test engine has registered most hooks, its internal functions will be called by ImGui functions.
E.g. ImGui::NewFrame()
will automatically call internal ImGuiTestEngine_PostNewFrame()
, which the most important function at it will dispatch calls to both GuiFunc() and TestFunc(). For reference this is what it does:
// [Internal] Called automatically by NewFrame()
void ImGuiTestEngine_PostNewFrame()
{
// [...] Various internal stuff
// Call user GuiFunc() if used
ImGuiTestEngine_RunGuiFunc(engine);
// Execute one chunk of user TestFunc() until it yields
engine->IO.CoroutineFuncs->RunFunc(...);
// [...] Various internal stuff
}
When automation is running, a few selected lightweight hooks are enabled in the core library. Some queries are intently designed in a way to not make the library meaningful slower when automation is running (e.g. some queries are done over multiple frames so the low-level levels can early out with a single compare and without need to do scanning or searches operations).
void ImGuiTestEngine_QueueTest(ImGuiTestEngine* engine, ImGuiTest* test, ImGuiTestRunFlags run_flags = 0);
void ImGuiTestEngine_QueueTests(ImGuiTestEngine* engine, ImGuiTestGroup group, const char* filter = NULL, ImGuiTestRunFlags run_flags = 0);
bool ImGuiTestEngine_TryAbortEngine(ImGuiTestEngine* engine);
void ImGuiTestEngine_AbortCurrentTest(ImGuiTestEngine* engine);
// Status Queries
bool ImGuiTestEngine_IsTestQueueEmpty(ImGuiTestEngine* engine);
bool ImGuiTestEngine_IsUsingSimulatedInputs(ImGuiTestEngine* engine);
void ImGuiTestEngine_GetResult(ImGuiTestEngine* engine, int& count_tested, int& success_count);
// Crash Handling
void ImGuiTestEngine_InstallDefaultCrashHandler(); // Install default crash handler
void ImGuiTestEngine_CrashHandler(); // Default crash handler, should be called from a custom crash handler if such exists
When running automation in "fast" mode, the system will use techniques such as teleporting mouse cursor to desired locations, reducing the amount of time to achieve actions. This is in contrast with "normal" and "cinematic" modes which are designed for human watching a test running, or for video capture.
You may further reduce the time it takes to run in "fast" mode by polling the test_io.IsRequestingMaxAppSpeed
output. Based on this flag, you may: skip waiting for vsync, skip swapping, or skip rendering altogether. The Dear ImGui Test Suite uses this technique to disable vsync.
Some applications may choose to the hide system mouse cursor while displaying the simulated mouse cursor.
Some applications may choose to display the system mouse cursor (even though it is unused) together with the simulated mouse cursor.
You may want to render the simulated mouse cursor in a way which is clearly different from the system cursor:
if (ImGuiTestEngine_IsUsingSimulatedInputs(engine) && !test_io.ConfigMouseDrawCursor && !test_io.IsCapturing)
ImGui::RenderMouseCursor(ImGui::GetMousePos(), 1.2f, ImGui::GetMouseCursor(),
IM_COL32(255, 255, 120, 255), IM_COL32(0, 0, 0, 255), IM_COL32(0, 0, 0, 60)); // Custom yellow cursor
The reason we test for !test_io.IsCapturing
here is that during screen/video capture you'll probably want to display a regular cursor rather than an obnoxious yellow cursor.
We will later provide an opt-in full-featured overlay helper to display mouse and keyboard interactions on the screen.
The use of coroutine makes test functions easier to write code for: code can be written in sequence while calling "blocking" functions (e.g. a ctx->MouseMove()
call with typically takes multiple Dear ImGui frames to complete).
ImGuiTest* t = IM_REGISTER_TEST(e, "demo_tests", "test1");
t->TestFunc = [](ImGuiTestContext* ctx)
{
ctx->SetRef("Dear ImGui Demo Window");
printf("foobar\n"); // User code runs until Yield() happen.
ctx->MouseMove("Widgets"); // Takes multiple frames (internally call Yield between each step)
ctx->Yield(); // Takes one frame
};
The coroutine will NEVER run in parallel with your main thread: they'll be always waiting each others using a mutex. The coroutine generally only runs a few lines of user code before returning execution to the main thread.
Our system is designed to be easily portable: there is no parallelism involved, and regular threads can be used to implement our coroutine interface. There is no need or advantage to using a real low-level coroutine mechanism.
You need to provide an implementation for ImGuiTestCoroutineInterface
:
// An arbitrary handle used internally to represent coroutines (NULL indicates no handle)
typedef void* ImGuiTestCoroutineHandle;
// A coroutine main function
typedef void (ImGuiTestCoroutineMainFunc)(void* data);
// Coroutine support interface
struct ImGuiTestCoroutineInterface
{
// Create a new coroutine
ImGuiTestCoroutineHandle (*CreateFunc)(ImGuiTestCoroutineMainFunc* func, const char* name, void* data);
// Destroy a coroutine (which must have completed first)
void (*DestroyFunc)(ImGuiTestCoroutineHandle handle);
// Run a coroutine until it yields or finishes, returning false if finished
bool (*RunFunc)(ImGuiTestCoroutineHandle handle);
// Yield from a coroutine back to the caller, preserving coroutine state
void (*YieldFunc)();
};
You may #define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1
in your configuration file to use a default implementation using std::thread
. We use std::thread
as convenience because it is highly portable nowadays.
See imgui_te_coroutine.h for interface and imgui_te_coroutine.cpp for our suggested implementation using std::thread. If you are new to setting up the Test Engine, we advise that you start by using #define IMGUI_TEST_ENGINE_ENABLE_COROUTINE_STDTHREAD_IMPL 1
and then once it work you may replace it with your threading functions if you prefer.
When running the coroutine what generally happens is:
- Main thread release a mutex.
- The coroutine thread wakes up, take the mutex, resume executing TestFunc().
- After one or a few lines in the TestFunc(), an action generally requires a yield.
- The coroutine thread release the mutex.
- Main thread wakes up.
TL;DR; Main App Thread and TestFunc Coroutine Thread ARE NEVER RUNNING IN PARALLEL. You don't need to worry about concurrency issues between them.
-
If you run tests in a GUI application, you may use the Test Engine UI (
ImGuiTestEngine_ShowTestEngineWindows()
function) to browse and run tests, inspect their log and setup variety of options. -
If you run tests in a command-line application (no visual screen), a typical application running tests may use our helpers:
if (!aborted)
{
int count_tested = 0;
int count_success = 0;
ImGuiTestEngine_GetResult(engine, count_tested, count_success);
ImGuiTestEngine_PrintResultSummary(engine);
if (count_tested != count_success)
return (1); // Error
}
return (0); // OK
- You may export tests results in a standardized format. See Exporting Results.