Aurore
A Switch-like interface for your app, on every screen
Loading...
Searching...
No Matches
harness.hpp
Go to the documentation of this file.
1/*
2 Copyright 2026 JigSawFr
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15*/
16
17#pragma once
18
19// aurore::test::Harness runs an app's screens with no window: inputs given
20// through calls, a simulated clock (frames advance without waiting, the
21// same way every run), screenshots compared with references. Built with
22// -DAURORE_BUILD_TESTS=ON (target Aurore::test); docs/testing.md has a guide.
23//
24// aurore::test::Harness h; // Application::init + window
25// h.push(new MainActivity());
26// h.settle(); // animations done
27// h.tap(aurore::BUTTON_DOWN);
28// AURORE_CHECK(h.focusText() == "Settings");
29// AURORE_CHECK(h.matchesReference("main_settings"));
30//
31// One Harness per process: Aurore's Application is global.
32
35#include <aurore/core/input.hpp>
36#include <aurore/core/theme.hpp>
37#include <aurore/core/time.hpp>
38
39#include <cstdint>
40#include <functional>
41#include <string>
42#include <type_traits>
43#include <vector>
44
45namespace aurore
46{
47class HeadlessPlatform;
48class View;
49}
50
51namespace aurore::test
52{
53
54struct Options
55{
56 // The framebuffer's size; the UI scales as on a window this size
57 int width = 1280;
58 int height = 720;
59 // Simulated time per frame
60 double frameMs = 1000.0 / 60.0;
61 // The platform's locale and theme (Cadran's preferences, when they say
62 // "system", follow them)
63 std::string locale = "en-US";
65 // The wall clock at the start, in seconds since the epoch (the dates and
66 // times shown; it moves on with the simulated clock), in UTC: Monday
67 // 5 January 2026, 10:30. Negative: the system's clock and time zone.
68 int64_t wallTime = 1767609000;
69 // Window title (translated by the app if it wants)
70 std::string title = "test";
71 // Where matchesReference() reads references and writes what it got.
72 // Empty: AURORE_TEST_REFERENCES / AURORE_TEST_OUTPUT from the environment,
73 // else "references" and "test-output" in the working directory.
74 std::string referenceDir;
75 std::string outputDir;
76 // The system's fonts (Chinese, Japanese and Korean, colour emoji) looked
77 // up: off, the pictures are the same on every machine, with the app's
78 // fonts only
79 bool systemFonts = false;
80};
81
82// An RGBA picture, rows top-down
83struct Image
84{
85 int width = 0;
86 int height = 0;
87 std::vector<unsigned char> rgba;
88
89 bool empty() const { return rgba.empty(); }
90 // The pixel's channels, at x, y
91 const unsigned char* at(int x, int y) const { return rgba.data() + ((size_t)y * width + x) * 4; }
92 // The part of the picture in `x, y, w, h` (clipped to it)
93 Image crop(int x, int y, int w, int h) const;
94
95 bool savePng(const std::string& path) const;
96 static Image loadPng(const std::string& path);
97};
98
100{
101 bool sameSize = false;
102 size_t differingPixels = 0; // with a channel off by more than the tolerance
103 int maxDelta = 0; // the largest channel difference
104 double ratio = 0; // differingPixels / all pixels
105 // The pixels found different in red over a faded copy of `expected`
107};
108
109// Pixel by pixel: a channel off by more than `tolerance` (0-255) makes the
110// pixel differ
111ImageDiff compareImages(const Image& actual, const Image& expected, int tolerance = 0);
112
114{
115 public:
118
119 Harness(const Harness&) = delete;
120 Harness& operator=(const Harness&) = delete;
121
122 const Options& options() const { return opts; }
123 HeadlessPlatform* platform() { return headless; }
124
125 // Screens
126
127 // Application::pushActivity, then a frame
129 // The activity on top (nullptr without any)
131 // A view of the activity on top, by its XML id (nullptr when none)
132 View* find(const std::string& id);
133
134 // Time
135
136 // Runs `count` frames, the clock moving one frame's time before each
137 void step(int count = 1);
138 // Runs frames for `ms` of simulated time
139 void advance(double ms);
140 // Runs frames until nothing moves: no animation running, no task queued
141 // or running (sync/async), for 2 frames in a row; at most `maxMs` of
142 // simulated time. Returns false when it gave up.
143 bool settle(double maxMs = 5000);
144 // Microseconds on the simulated clock
145 Time now() const;
146 uint64_t frameCount() const { return frames; }
147
148 // Gamepad: press and release are kept until changed. tap() presses for
149 // one frame, releases and settles.
152 void tap(ControllerButton button, int times = 1);
153 // Holds the button for `ms` (repeats come as on a device), then releases
154 void hold(ControllerButton button, double ms);
155 void setAxis(ControllerAxis axis, float value);
156
157 // Keyboard (AURORE_KBD_KEY_*, GLFW's codes): Enter is A, Escape is B and
158 // the arrows are the D-pad, as on the desktop
161 void tapKey(AuroreKeyboardScancode key, int times = 1);
162
163 // Touch, in the views' coordinates (View::getFrame()). tapAt() and
164 // tapView() touch down, wait a frame, lift and settle.
165 void touchDown(float x, float y, int finger = 0);
166 void touchMove(float x, float y, int finger = 0);
167 void touchUp(int finger = 0);
168 void tapAt(float x, float y);
169 bool tapView(View* view);
170 // A drag from one point to another over `ms`, then settles
171 void swipe(float fromX, float fromY, float toX, float toY, double ms = 200);
172
173 // Mouse, in the views' coordinates
174 void mouseMove(float x, float y);
175 void mouseClick(float x, float y);
176 // Pressed and let go between two frames, as a quick click or a
177 // touchpad tap
178 void mouseQuickClick(float x, float y);
179 void mouseScroll(float dx, float dy);
180
181 // The next keyboard (ImeManager) opened gets `text`; without it, a
182 // keyboard opened is cancelled
183 void setKeyboardAnswer(const std::string& text);
184
185 // Focus
186
188 // The text of the focused view: its own if it is a label or a button,
189 // the title of a cell, else the first text found in it (empty: none)
190 std::string focusText();
191 // Every focus change since the start (or clearFocusLog()), as focusText()
192 const std::vector<std::string>& focusLog() const { return focusChanges; }
193 void clearFocusLog() { focusChanges.clear(); }
194 // Moves the focus with `button` until focusText() is `text`, at most
195 // `maxSteps` presses. Returns false when not reached.
196 bool focusByText(const std::string& text, ControllerButton button = BUTTON_DOWN, int maxSteps = 30);
197
198 // Text
199
200 // The labels shown on the activities drawn whose text the box cuts
201 // ("…", Label::isTruncated), their whole text. As with AURORE_CUT_TEXT, a
202 // label only clipped away by its parent (a folded sidebar) still counts.
203 std::vector<std::string> cutTexts();
204 // Every label's text shown (visible views of the activities drawn)
205 std::vector<std::string> texts();
206 // True when a label shown has exactly this text
207 bool hasText(const std::string& text);
208 // The first label shown with exactly this text (nullptr: none), to tap
209 View* findText(const std::string& text);
210
211 // Accessibility
212
213 // The views shown that take the focus themselves (not passing it on)
214 // with nothing to tell them apart: no accessibility label nor value
215 // (a role at most), as View::describe(). What the focus announcer was
216 // given last: Application::getLastAnnouncement().
217 std::vector<std::string> silentFocusables();
218
219 // Pictures
220
221 // The last frame drawn (a frame is drawn first if none was)
223 // Compares a screenshot (or the part of it in `area`, views'
224 // coordinates) with the reference `<referenceDir>/<name>.png`: a pixel
225 // differs when a channel is off by more than `tolerance`; it matches
226 // when at most `maxRatio` of the pixels differ. Writes
227 // `<outputDir>/<name>.png` (and `<name>.diff.png` when it differs).
228 // A missing reference, or AURORE_TEST_UPDATE=1, writes the reference
229 // instead and matches.
230 bool matchesReference(const std::string& name, int tolerance = 8, double maxRatio = 0.001);
231 bool matchesReference(const std::string& name, const Rect& area, int tolerance = 8, double maxRatio = 0.001);
232 // What the last matchesReference() found
233 const ImageDiff& lastDiff() const { return diff; }
234
235 // Window
236
237 // A new window size, then settles
238 void resize(int width, int height);
239
240 private:
241 void walkShown(const std::function<void(View*)>& visit);
242
243 Options opts;
244 HeadlessPlatform* headless = nullptr;
245 Time clock = 0;
246 uint64_t frames = 0;
247 std::vector<std::string> focusChanges;
248 ImageDiff diff;
249};
250
251// Text of a view as focusText() reads it
252std::string viewText(View* view);
253
254} // namespace aurore::test
255
256// A small test runner, for executables of test cases (tests/main.cpp):
257//
258// AURORE_TEST(focus_moves) { aurore::test::Harness h; ... AURORE_CHECK(...); }
259// int main(int argc, char** argv) { return aurore::test::runTests(argc, argv); }
260//
261// `program` runs every case, each in a process of its own; `program name`
262// runs one (what ctest calls, see aurore_add_tests in library/cmake/AuroreTesting.cmake);
263// `program --list` prints their names ("aurore-test: name" lines).
264namespace aurore::test
265{
266using TestFunction = void (*)();
267bool registerTest(const char* name, TestFunction function);
268void reportFailure(const char* file, int line, const std::string& what);
269int runTests(int argc, char** argv);
270} // namespace aurore::test
271
272#define AURORE_TEST(name) \
273 static void aurore_test_##name(); \
274 static const bool aurore_test_registered_##name = aurore::test::registerTest(#name, aurore_test_##name); \
275 static void aurore_test_##name()
276
277#define AURORE_CHECK(condition) \
278 do \
279 { \
280 if (!(condition)) \
281 aurore::test::reportFailure(__FILE__, __LINE__, "AURORE_CHECK(" #condition ")"); \
282 } while (0)
283
284#define AURORE_CHECK_EQ(actual, expected) \
285 do \
286 { \
287 auto aurore_actual_ = (actual); \
288 auto aurore_expected_ = (expected); \
289 if (!(aurore_actual_ == aurore_expected_)) \
290 aurore::test::reportFailure(__FILE__, __LINE__, \
291 std::string("AURORE_CHECK_EQ(" #actual ", " #expected "): got ") + aurore::test::describeValue(aurore_actual_) \
292 + ", expected " + aurore::test::describeValue(aurore_expected_)); \
293 } while (0)
294
295namespace aurore::test
296{
297inline std::string describeValue(const std::string& value) { return "\"" + value + "\""; }
298inline std::string describeValue(const char* value) { return "\"" + std::string(value) + "\""; }
299template <typename T>
300std::string describeValue(const T& value)
301{
302 if constexpr (std::is_arithmetic_v<T>)
303 return std::to_string(value);
304 else if constexpr (std::is_pointer_v<T>)
305 return value ? "a pointer" : "nullptr";
306 else
307 return "(value)";
308}
309} // namespace aurore::test
Definition activity.hpp:45
Definition headless_platform.hpp:116
Definition view.hpp:232
Definition harness.hpp:114
bool hasText(const std::string &text)
void advance(double ms)
void touchMove(float x, float y, int finger=0)
const std::vector< std::string > & focusLog() const
Definition harness.hpp:192
void tap(ControllerButton button, int times=1)
const ImageDiff & lastDiff() const
Definition harness.hpp:233
void mouseQuickClick(float x, float y)
std::vector< std::string > cutTexts()
void resize(int width, int height)
bool matchesReference(const std::string &name, int tolerance=8, double maxRatio=0.001)
HeadlessPlatform * platform()
Definition harness.hpp:123
void setKeyboardAnswer(const std::string &text)
void setAxis(ControllerAxis axis, float value)
View * find(const std::string &id)
Harness & operator=(const Harness &)=delete
View * findText(const std::string &text)
void pressKey(AuroreKeyboardScancode key)
void touchDown(float x, float y, int finger=0)
bool settle(double maxMs=5000)
void hold(ControllerButton button, double ms)
bool matchesReference(const std::string &name, const Rect &area, int tolerance=8, double maxRatio=0.001)
void push(Activity *activity, TransitionAnimation animation=TransitionAnimation::NONE)
bool focusByText(const std::string &text, ControllerButton button=BUTTON_DOWN, int maxSteps=30)
uint64_t frameCount() const
Definition harness.hpp:146
void clearFocusLog()
Definition harness.hpp:193
void mouseMove(float x, float y)
void release(ControllerButton button)
std::string focusText()
void swipe(float fromX, float fromY, float toX, float toY, double ms=200)
void press(ControllerButton button)
void tapKey(AuroreKeyboardScancode key, int times=1)
void step(int count=1)
const Options & options() const
Definition harness.hpp:122
std::vector< std::string > texts()
Harness(const Harness &)=delete
Harness(Options options=Options())
std::vector< std::string > silentFocusables()
void releaseKey(AuroreKeyboardScancode key)
void tapAt(float x, float y)
void mouseClick(float x, float y)
void touchUp(int finger=0)
void mouseScroll(float dx, float dy)
bool tapView(View *view)
Definition harness.hpp:52
std::string viewText(View *view)
ImageDiff compareImages(const Image &actual, const Image &expected, int tolerance=0)
Definition actions.hpp:25
AuroreKeyboardScancode
Definition input.hpp:44
TransitionAnimation
Definition view.hpp:153
ThemeVariant
The two sets of colours every theme has: light and dark.
Definition theme.hpp:37
ControllerButton
Definition input.hpp:179
@ BUTTON_DOWN
Definition input.hpp:187
retro_time_t Time
Definition time.hpp:31
ControllerAxis
Definition input.hpp:214
Definition text.hpp:13
Definition geometry.hpp:70
Definition harness.hpp:100
double ratio
Definition harness.hpp:104
Image map
Definition harness.hpp:106
size_t differingPixels
Definition harness.hpp:102
int maxDelta
Definition harness.hpp:103
bool sameSize
Definition harness.hpp:101
Definition harness.hpp:84
bool empty() const
Definition harness.hpp:89
bool savePng(const std::string &path) const
int width
Definition harness.hpp:85
int height
Definition harness.hpp:86
static Image loadPng(const std::string &path)
const unsigned char * at(int x, int y) const
Definition harness.hpp:91
std::vector< unsigned char > rgba
Definition harness.hpp:87
Image crop(int x, int y, int w, int h) const
Definition harness.hpp:55
bool systemFonts
Definition harness.hpp:79
int width
Definition harness.hpp:57
std::string locale
Definition harness.hpp:63
int height
Definition harness.hpp:58
std::string title
Definition harness.hpp:70
std::string outputDir
Definition harness.hpp:75
double frameMs
Definition harness.hpp:60
int64_t wallTime
Definition harness.hpp:68
ThemeVariant theme
Definition harness.hpp:64
std::string referenceDir
Definition harness.hpp:74