Skip to content
omar edited this page Sep 3, 2024 · 12 revisions

Index

Version Requirement

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.

References

Dear ImGui Test Suite application

imgui_test_suite/: before anything, build and run and play around with the interactive test suite application to get a feel of things.

Minimal application

app_minimal/ is a simple application showcasing how to integrate the test engine in your project.

Building

  • 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 using std::thread. We use std::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. See imgui_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.

Integrating Library

Initialization

// 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();

Shutdown

// 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);

Main loop

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);
}

What Happens Internally...

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).

Other I/O

Useful APIs

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

Running at "Maximum Speed"

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.

Mouse Cursor and Inputs Overlay

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.

Setting up custom Coroutines Interface

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.

Processing Results

  • 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
Clone this wiki locally