mirror of
https://github.com/gnif/LookingGlass.git
synced 2026-08-09 00:31:31 +00:00
736 lines
15 KiB
C
736 lines
15 KiB
C
/**
|
|
* Looking Glass
|
|
* Copyright © 2017-2026 The Looking Glass Authors
|
|
* https://looking-glass.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation; either version 2 of the License, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#include "app.h"
|
|
#include "core.h"
|
|
#include "main.h"
|
|
#include "test.h"
|
|
#include "util.h"
|
|
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
enum EvType
|
|
{
|
|
EV_GRAB,
|
|
EV_UNGRAB,
|
|
EV_CAPTURE,
|
|
EV_UNCAPTURE,
|
|
EV_WARP,
|
|
EV_GUEST,
|
|
EV_MOTION,
|
|
EV_VALID,
|
|
};
|
|
|
|
struct Ev
|
|
{
|
|
enum EvType type;
|
|
double x;
|
|
double y;
|
|
bool exit;
|
|
bool conf;
|
|
bool lock;
|
|
};
|
|
|
|
struct Trap
|
|
{
|
|
bool req;
|
|
bool on;
|
|
bool hold;
|
|
};
|
|
|
|
static struct
|
|
{
|
|
enum LG_DSWarpSupport support;
|
|
struct Trap conf;
|
|
struct Trap lock;
|
|
bool valid;
|
|
struct Ev ev[256];
|
|
unsigned int count;
|
|
}
|
|
m;
|
|
|
|
struct AppState g_state;
|
|
struct CursorState g_cursor;
|
|
struct AppParams g_params;
|
|
|
|
static void push(enum EvType type, double x, double y, bool leaving)
|
|
{
|
|
CHECK(m.count < sizeof(m.ev) / sizeof(m.ev[0]));
|
|
m.ev[m.count++] = (struct Ev) {
|
|
.type = type,
|
|
.x = x,
|
|
.y = y,
|
|
.exit = leaving,
|
|
.conf = m.conf.on,
|
|
.lock = m.lock.on,
|
|
};
|
|
}
|
|
|
|
static unsigned int count(enum EvType type)
|
|
{
|
|
unsigned int result = 0;
|
|
for (unsigned int i = 0; i < m.count; ++i)
|
|
if (m.ev[i].type == type)
|
|
++result;
|
|
return result;
|
|
}
|
|
|
|
static int first(enum EvType type)
|
|
{
|
|
for (unsigned int i = 0; i < m.count; ++i)
|
|
if (m.ev[i].type == type)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
static void guest(double x, double y, double localX, double localY)
|
|
{
|
|
(void)x;
|
|
(void)y;
|
|
push(EV_GUEST, localX, localY, false);
|
|
}
|
|
|
|
static void setPointer(LG_DSPointer pointer)
|
|
{
|
|
(void)pointer;
|
|
}
|
|
|
|
static void keyNoop(void)
|
|
{
|
|
}
|
|
|
|
static void grab(void)
|
|
{
|
|
m.conf.req = true;
|
|
push(EV_GRAB, 0, 0, false);
|
|
}
|
|
|
|
static void ungrab(void)
|
|
{
|
|
push(EV_UNGRAB, 0, 0, false);
|
|
m.conf.req = false;
|
|
if (!m.conf.hold)
|
|
{
|
|
m.conf.on = false;
|
|
core_handleGrabEvent(false);
|
|
}
|
|
}
|
|
|
|
static bool grabbed(void)
|
|
{
|
|
return m.conf.on;
|
|
}
|
|
|
|
static void capture(void)
|
|
{
|
|
if (m.support == LG_DS_WARP_NONE)
|
|
{
|
|
grab();
|
|
return;
|
|
}
|
|
|
|
m.conf.req = false;
|
|
if (!m.conf.hold)
|
|
m.conf.on = false;
|
|
m.lock.req = true;
|
|
push(EV_CAPTURE, 0, 0, false);
|
|
}
|
|
|
|
static void uncapture(void)
|
|
{
|
|
if (m.support == LG_DS_WARP_NONE)
|
|
{
|
|
ungrab();
|
|
return;
|
|
}
|
|
|
|
push(EV_UNCAPTURE, 0, 0, false);
|
|
m.lock.req = false;
|
|
if (!m.lock.hold)
|
|
m.lock.on = false;
|
|
}
|
|
|
|
static bool captured(void)
|
|
{
|
|
return m.support == LG_DS_WARP_NONE ? m.conf.on : m.lock.on;
|
|
}
|
|
|
|
static void warp(int x, int y, bool exiting)
|
|
{
|
|
push(EV_WARP, x, y, exiting);
|
|
}
|
|
|
|
static bool valid(int x, int y)
|
|
{
|
|
push(EV_VALID, x, y, false);
|
|
return m.valid;
|
|
}
|
|
|
|
static struct LG_DisplayServerOps ds = {
|
|
.name = "test",
|
|
.guestPointerUpdated = guest,
|
|
.setPointer = setPointer,
|
|
.grabKeyboard = keyNoop,
|
|
.ungrabKeyboard = keyNoop,
|
|
.grabPointer = grab,
|
|
.ungrabPointer = ungrab,
|
|
.isPointerGrabbed = grabbed,
|
|
.capturePointer = capture,
|
|
.uncapturePointer = uncapture,
|
|
.isPointerCaptured = captured,
|
|
.warpPointer = warp,
|
|
.isValidPointerPos = valid,
|
|
};
|
|
|
|
bool app_getProp(LG_DSProperty prop, void * ret)
|
|
{
|
|
if (prop != LG_DS_WARP_SUPPORT)
|
|
return false;
|
|
|
|
*(enum LG_DSWarpSupport *)ret = m.support;
|
|
return true;
|
|
}
|
|
|
|
bool app_guestIsWindows(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool app_isOverlayMode(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool app_isRunning(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint64_t app_mouseSeq(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
void app_mouseTrace(const char * file, unsigned int line,
|
|
const char * function, uint64_t seq, const char * format, ...)
|
|
{
|
|
(void)file;
|
|
(void)line;
|
|
(void)function;
|
|
(void)seq;
|
|
(void)format;
|
|
}
|
|
|
|
bool purespice_mouseMotion(int32_t x, int32_t y)
|
|
{
|
|
push(EV_MOTION, x, y, false);
|
|
return true;
|
|
}
|
|
|
|
static void reset(void)
|
|
{
|
|
memset(&m, 0, sizeof(m));
|
|
memset(&g_state, 0, sizeof(g_state));
|
|
memset(&g_cursor, 0, sizeof(g_cursor));
|
|
memset(&g_params, 0, sizeof(g_params));
|
|
|
|
m.support = LG_DS_WARP_SURFACE;
|
|
m.valid = true;
|
|
m.conf.req = true;
|
|
m.conf.on = true;
|
|
|
|
g_state.ds = &ds;
|
|
g_state.focused = true;
|
|
g_state.haveSrcSize = true;
|
|
g_state.posInfoValid = true;
|
|
g_state.formatValid = true;
|
|
g_state.windowW = 300;
|
|
g_state.windowH = 200;
|
|
g_state.srcSize = (struct Point) { 100, 80 };
|
|
g_state.dstRect = (LG_RendererRect) {
|
|
.valid = true,
|
|
.x = 10,
|
|
.y = 20,
|
|
.w = 100,
|
|
.h = 80,
|
|
};
|
|
|
|
g_params.useSpiceInput = true;
|
|
g_params.scaleMouseInput = true;
|
|
|
|
g_cursor.inWindow = true;
|
|
g_cursor.inView = true;
|
|
g_cursor.viewReq = true;
|
|
g_cursor.warpState = WARP_STATE_ON;
|
|
g_cursor.guest.valid = true;
|
|
g_cursor.scale = (struct DoublePoint) { 1.0, 1.0 };
|
|
}
|
|
|
|
static void setLocal(double x, double y)
|
|
{
|
|
g_cursor.pos = (struct DoublePoint) { x, y };
|
|
|
|
struct DoublePoint guest;
|
|
util_localCurToGuest(&guest);
|
|
g_cursor.guest.x = lround(guest.x);
|
|
g_cursor.guest.y = lround(guest.y);
|
|
}
|
|
|
|
static bool near(double a, double b)
|
|
{
|
|
return fabs(a - b) < 0.000001;
|
|
}
|
|
|
|
static void testInsetExit(void)
|
|
{
|
|
reset();
|
|
setLocal(10, 20);
|
|
g_cursor.pos = (struct DoublePoint) { 9.75, 19.75 };
|
|
|
|
core_handleMouseNormal(-0.25, -0.25);
|
|
|
|
const int drop = first(EV_UNGRAB);
|
|
const int move = first(EV_WARP);
|
|
CHECK(drop >= 0);
|
|
CHECK(count(EV_WARP) == 1);
|
|
CHECK(move > drop);
|
|
CHECK(m.ev[move].x == 9);
|
|
CHECK(m.ev[move].y == 19);
|
|
CHECK(m.ev[move].exit);
|
|
CHECK(!g_cursor.inView);
|
|
|
|
core_handleGuestMouseUpdate();
|
|
CHECK(count(EV_GUEST) == 0);
|
|
}
|
|
|
|
static void startExit(void)
|
|
{
|
|
reset();
|
|
setLocal(109, 50);
|
|
m.conf.hold = true;
|
|
|
|
core_handleMouseNormal(2, 0);
|
|
}
|
|
|
|
static void testExitWait(void)
|
|
{
|
|
startExit();
|
|
const int drop = first(EV_UNGRAB);
|
|
CHECK(drop >= 0);
|
|
CHECK(count(EV_WARP) == 0);
|
|
CHECK(g_cursor.inView);
|
|
}
|
|
|
|
static void testExitGuest(void)
|
|
{
|
|
startExit();
|
|
g_cursor.guest.x = 99;
|
|
core_handleGuestMouseUpdate();
|
|
CHECK(count(EV_GUEST) == 0);
|
|
}
|
|
|
|
static void startConf(void)
|
|
{
|
|
reset();
|
|
setLocal(50, 50);
|
|
m.conf.req = false;
|
|
m.conf.on = false;
|
|
g_cursor.inView = false;
|
|
g_cursor.viewReq = false;
|
|
|
|
core_handleMouseNormal(0, 0);
|
|
}
|
|
|
|
static void setConf(bool active)
|
|
{
|
|
m.conf.on = active;
|
|
core_handleGrabEvent(active);
|
|
}
|
|
|
|
static void testConfWait(void)
|
|
{
|
|
startConf();
|
|
CHECK(m.conf.req);
|
|
CHECK(!m.conf.on);
|
|
CHECK(!g_cursor.inView);
|
|
|
|
setConf(true);
|
|
CHECK(g_cursor.inView);
|
|
}
|
|
|
|
static void testConfGuest(void)
|
|
{
|
|
startConf();
|
|
core_handleGuestMouseUpdate();
|
|
CHECK(m.conf.req);
|
|
CHECK(count(EV_GUEST) == 0);
|
|
|
|
setConf(true);
|
|
core_handleGuestMouseUpdate();
|
|
CHECK(count(EV_GUEST) == 1);
|
|
}
|
|
|
|
static void testCapWait(void)
|
|
{
|
|
reset();
|
|
setLocal(50, 50);
|
|
g_params.captureInputOnly = true;
|
|
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 0);
|
|
|
|
core_setGrabQuiet(true);
|
|
CHECK(g_cursor.grab);
|
|
CHECK(m.lock.req);
|
|
CHECK(!m.lock.on);
|
|
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 0);
|
|
}
|
|
|
|
static void testCapRevoke(void)
|
|
{
|
|
reset();
|
|
setLocal(50, 50);
|
|
g_params.captureInputOnly = true;
|
|
core_setGrabQuiet(true);
|
|
|
|
m.lock.on = true;
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
CHECK(m.ev[first(EV_MOTION)].lock);
|
|
|
|
m.lock.on = false;
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(g_cursor.grab);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
}
|
|
|
|
static void testCapRelease(void)
|
|
{
|
|
reset();
|
|
setLocal(50, 50);
|
|
g_params.captureInputOnly = true;
|
|
core_setGrabQuiet(true);
|
|
|
|
m.lock.on = true;
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
|
|
m.lock.hold = true;
|
|
core_setGrabQuiet(false);
|
|
CHECK(!g_cursor.grab);
|
|
CHECK(!m.lock.req);
|
|
CHECK(m.lock.on);
|
|
|
|
const int drop = first(EV_UNCAPTURE);
|
|
CHECK(drop >= 0);
|
|
CHECK(m.ev[drop].lock);
|
|
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
}
|
|
|
|
static void testCapFallback(void)
|
|
{
|
|
reset();
|
|
setLocal(50, 50);
|
|
m.support = LG_DS_WARP_NONE;
|
|
g_params.captureInputOnly = true;
|
|
core_setGrabQuiet(true);
|
|
|
|
m.conf.on = true;
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
|
|
m.conf.on = false;
|
|
core_handleMouseGrabbed(1, 0);
|
|
CHECK(count(EV_MOTION) == 1);
|
|
}
|
|
|
|
static void testRotateScale(void)
|
|
{
|
|
reset();
|
|
g_state.dstRect = (LG_RendererRect) {
|
|
.valid = true,
|
|
.x = 10,
|
|
.y = 20,
|
|
.w = 200,
|
|
.h = 100,
|
|
};
|
|
g_cursor.scale = (struct DoublePoint) { 2, 4 };
|
|
g_cursor.guest.x = 40;
|
|
g_cursor.guest.y = 20;
|
|
|
|
for (int rot = 0; rot < LG_ROTATE_MAX; ++rot)
|
|
{
|
|
g_state.rotate = rot;
|
|
|
|
struct DoublePoint local;
|
|
CHECK(util_guestCurToLocal(&local));
|
|
g_cursor.pos = local;
|
|
|
|
struct DoublePoint guest;
|
|
util_localCurToGuest(&guest);
|
|
CHECK(near(guest.x, 40));
|
|
CHECK(near(guest.y, 20));
|
|
}
|
|
}
|
|
|
|
static void testGeometry(void)
|
|
{
|
|
static const struct DoublePoint expected[] = {
|
|
{ 30, 25 },
|
|
{ 205, 40 },
|
|
{ 190, 115 },
|
|
{ 15, 100 },
|
|
};
|
|
static const struct DoublePoint rotated[] = {
|
|
{ 1, 2 },
|
|
{ 2, -1 },
|
|
{ -1, -2 },
|
|
{ -2, 1 },
|
|
};
|
|
|
|
reset();
|
|
g_state.dstRect = (LG_RendererRect) {
|
|
.valid = true,
|
|
.x = 10,
|
|
.y = 20,
|
|
.w = 200,
|
|
.h = 100,
|
|
};
|
|
g_cursor.scale = (struct DoublePoint) { 2, 4 };
|
|
g_cursor.guest.x = 40;
|
|
g_cursor.guest.y = 20;
|
|
|
|
for (int rot = 0; rot < LG_ROTATE_MAX; ++rot)
|
|
{
|
|
g_state.rotate = rot;
|
|
|
|
struct DoublePoint local;
|
|
CHECK(util_guestCurToLocal(&local));
|
|
CHECK(near(local.x, expected[rot].x));
|
|
CHECK(near(local.y, expected[rot].y));
|
|
|
|
struct DoublePoint delta = { 1, 2 };
|
|
util_rotatePoint(&delta);
|
|
CHECK(near(delta.x, rotated[rot].x));
|
|
CHECK(near(delta.y, rotated[rot].y));
|
|
}
|
|
|
|
static const double scales[] = { 0.5, 1.0, 2.0 };
|
|
for (unsigned int i = 0; i < sizeof(scales) / sizeof(scales[0]); ++i)
|
|
for (int rot = 0; rot < LG_ROTATE_MAX; ++rot)
|
|
{
|
|
g_state.rotate = rot;
|
|
g_cursor.scale.x = scales[i];
|
|
g_cursor.scale.y = scales[i];
|
|
g_cursor.guest.x = 40;
|
|
g_cursor.guest.y = 20;
|
|
|
|
struct DoublePoint local;
|
|
CHECK(util_guestCurToLocal(&local));
|
|
g_cursor.pos = local;
|
|
|
|
struct DoublePoint round;
|
|
util_localCurToGuest(&round);
|
|
CHECK(near(round.x, 40));
|
|
CHECK(near(round.y, 20));
|
|
}
|
|
|
|
g_cursor.guest.valid = false;
|
|
struct DoublePoint local = { -1, -2 };
|
|
CHECK(!util_guestCurToLocal(&local));
|
|
CHECK(near(local.x, -1));
|
|
CHECK(near(local.y, -2));
|
|
}
|
|
|
|
static struct DoublePoint inputFor(int rot, double x, double y)
|
|
{
|
|
switch (rot)
|
|
{
|
|
case LG_ROTATE_0:
|
|
return (struct DoublePoint) { x, y };
|
|
case LG_ROTATE_90:
|
|
return (struct DoublePoint) { -y, x };
|
|
case LG_ROTATE_180:
|
|
return (struct DoublePoint) { -x, -y };
|
|
case LG_ROTATE_270:
|
|
return (struct DoublePoint) { y, -x };
|
|
}
|
|
|
|
CHECK(false);
|
|
return (struct DoublePoint) { 0, 0 };
|
|
}
|
|
|
|
static void testEdges(void)
|
|
{
|
|
static const struct
|
|
{
|
|
struct Point pos;
|
|
struct Point delta;
|
|
struct Point target;
|
|
}
|
|
cases[] = {
|
|
{ { 10, 60 }, { -1, 0 }, { 9, 60 } },
|
|
{ { 109, 60 }, { 1, 0 }, { 110, 60 } },
|
|
{ { 60, 20 }, { 0, -1 }, { 60, 19 } },
|
|
{ { 60, 99 }, { 0, 1 }, { 60, 100 } },
|
|
{ { 10, 20 }, { -1, -1 }, { 9, 19 } },
|
|
{ { 109, 20 }, { 1, -1 }, { 110, 19 } },
|
|
{ { 10, 99 }, { -1, 1 }, { 9, 100 } },
|
|
{ { 109, 99 }, { 1, 1 }, { 110, 100 } },
|
|
};
|
|
|
|
for (int rot = 0; rot < LG_ROTATE_MAX; ++rot)
|
|
for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i)
|
|
{
|
|
reset();
|
|
g_state.rotate = rot;
|
|
setLocal(cases[i].pos.x, cases[i].pos.y);
|
|
const struct DoublePoint input = inputFor(rot,
|
|
cases[i].delta.x, cases[i].delta.y);
|
|
|
|
core_handleMouseNormal(input.x, input.y);
|
|
|
|
const int index = first(EV_WARP);
|
|
CHECK(index >= 0);
|
|
CHECK(m.ev[index].x == cases[i].target.x);
|
|
CHECK(m.ev[index].y == cases[i].target.y);
|
|
CHECK(m.ev[index].exit);
|
|
}
|
|
}
|
|
|
|
static void testScales(void)
|
|
{
|
|
static const struct
|
|
{
|
|
double scale;
|
|
double pos;
|
|
double delta;
|
|
int target;
|
|
}
|
|
cases[] = {
|
|
{ 0.5, 108, 4, 110 },
|
|
{ 1.0, 109, 1, 110 },
|
|
{ 2.0, 109, 1, 111 },
|
|
};
|
|
|
|
for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i)
|
|
{
|
|
reset();
|
|
g_cursor.useScale = true;
|
|
g_cursor.scale.x = cases[i].scale;
|
|
g_cursor.scale.y = cases[i].scale;
|
|
setLocal(cases[i].pos, 50);
|
|
|
|
core_handleMouseNormal(cases[i].delta, 0);
|
|
|
|
const int index = first(EV_WARP);
|
|
CHECK(index >= 0);
|
|
CHECK(m.ev[index].x == cases[i].target);
|
|
CHECK(m.ev[index].y == 50);
|
|
}
|
|
}
|
|
|
|
static void testBorderExit(void)
|
|
{
|
|
static const struct
|
|
{
|
|
struct Border border;
|
|
struct Point valid;
|
|
}
|
|
cases[] = {
|
|
{ { 8, 24, 8, 8 }, { 219, 274 } },
|
|
{ { 0, 0, 0, 0 }, { 211, 250 } },
|
|
};
|
|
|
|
for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i)
|
|
{
|
|
reset();
|
|
m.support = LG_DS_WARP_SCREEN;
|
|
g_state.windowPos = (struct Point) { 100, 200 };
|
|
g_state.border = cases[i].border;
|
|
setLocal(109, 50);
|
|
|
|
core_handleMouseNormal(2, 0);
|
|
|
|
const int check = first(EV_VALID);
|
|
const int move = first(EV_WARP);
|
|
CHECK(check >= 0);
|
|
CHECK(move > check);
|
|
CHECK(m.ev[check].x == cases[i].valid.x);
|
|
CHECK(m.ev[check].y == cases[i].valid.y);
|
|
CHECK(m.ev[move].x == 111);
|
|
CHECK(m.ev[move].y == 50);
|
|
CHECK(m.ev[move].exit);
|
|
}
|
|
}
|
|
|
|
struct Test
|
|
{
|
|
const char * name;
|
|
void (*run)(void);
|
|
};
|
|
|
|
static const struct Test tests[] = {
|
|
{ "inset-exit" , testInsetExit },
|
|
{ "exit-delay" , testExitWait },
|
|
{ "exit-guest" , testExitGuest },
|
|
{ "confine-pending" , testConfWait },
|
|
{ "confine-guest" , testConfGuest },
|
|
{ "capture-pending" , testCapWait },
|
|
{ "capture-revoked" , testCapRevoke },
|
|
{ "capture-release" , testCapRelease },
|
|
{ "capture-fallback", testCapFallback },
|
|
{ "rotate-scale" , testRotateScale },
|
|
{ "geometry" , testGeometry },
|
|
{ "edges" , testEdges },
|
|
{ "scales" , testScales },
|
|
{ "border-exit" , testBorderExit },
|
|
};
|
|
|
|
int main(int argc, char ** argv)
|
|
{
|
|
if (argc == 2)
|
|
{
|
|
for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i)
|
|
if (strcmp(argv[1], tests[i].name) == 0)
|
|
{
|
|
tests[i].run();
|
|
return 0;
|
|
}
|
|
|
|
fprintf(stderr, "unknown test: %s\n", argv[1]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (argc != 1)
|
|
return EXIT_FAILURE;
|
|
|
|
for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i)
|
|
tests[i].run();
|
|
return 0;
|
|
}
|