Files
libmonet/tests/test_monet.cpp
T
justmeandClaude Sonnet 5.5 b97a55ac75 Add libmonet: Material 3 style schemes and where to get the seed from
A header only C++20 library. One seed colour gives a complete Material 3 style
scheme, light or dark, with the tones, roles and contrast guarantees of the
baseline scheme, computed in OKLCH as a light weight stand in for HCT.

The seed colour and the light or dark preference come from a chain of sources:
an environment override, the XDG desktop portal (GNOME 47+ and KDE Plasma 6),
kdeglobals on KDE, gsettings on GNOME, GTK settings, and a fallback that is
always appended and cannot be left out. Desktop specific sources are only asked
on their own desktop, so a stale gsettings value on KDE never wins. Programs can
put sources of their own in front.

The tests run without a desktop through a fake Context. They cover colour
conversions, tone accuracy, the contrast of every role pair over 300 random
seeds in both modes, every parser against real portal output, the order of the
sources per desktop, the fallback and external program timeouts. The test suite
was checked by breaking the code 22 ways, all are caught.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 00:14:10 +08:00

420 lines
25 KiB
C++

// Tests for libmonet. Plain asserts, no framework, no real desktop needed: every
// source is exercised through a fake Context.
#include <sys/stat.h>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <map>
#include "monet/monet.h"
using namespace monet;
namespace fs = std::filesystem;
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); \
failures++; \
} \
} while (0)
#define CHECK_MSG(cond, ...) \
do { \
if (!(cond)) { \
std::printf("FAIL line %d: %s ", __LINE__, #cond); \
std::printf(__VA_ARGS__); \
std::printf("\n"); \
failures++; \
} \
} while (0)
static bool same(Rgb a, Rgb b, int tolerance = 0) {
return std::abs(a.r - b.r) <= tolerance && std::abs(a.g - b.g) <= tolerance && std::abs(a.b - b.b) <= tolerance;
}
// --- a fake system -----------------------------------------------------------
struct Fake {
std::map<std::string, std::string> env;
std::map<std::string, std::string> programs; // argv[0] -> standard output
std::vector<std::string> called;
fs::path configHome;
Context context() {
Context c;
c.getenv = [this](const std::string &k) -> std::optional<std::string> {
auto it = env.find(k);
return it == env.end() ? std::nullopt : std::optional<std::string>(it->second);
};
c.run = [this](const std::vector<std::string> &argv, int) -> std::optional<std::string> {
called.push_back(argv[0]);
auto it = programs.find(argv[0]);
return it == programs.end() ? std::nullopt : std::optional<std::string>(it->second);
};
c.configHome = configHome;
c.home = configHome;
return c;
}
};
static const char *kBusctl =
"a{sa{sv}} 1 \"org.freedesktop.appearance\" 4 \"contrast\" u 0 \"color-scheme\" u 2 \"reduced-motion\" u 0 "
"\"accent-color\" (ddd) 0.239216 0.682353 0.913725\n";
static const char *kBusctlDark =
"a{sa{sv}} 1 \"org.freedesktop.appearance\" 2 \"color-scheme\" u 1 \"accent-color\" (ddd) 0.5 0.25 1\n";
static const char *kGdbus =
"({'org.freedesktop.appearance': {'contrast': <uint32 0>, 'color-scheme': <uint32 1>, 'accent-color': <(0.5, 0.25, 1.0)>}},)\n";
static void writeFile(const fs::path &p, const std::string &text) {
fs::create_directories(p.parent_path());
std::ofstream(p) << text;
}
int main() {
// --- colour spaces -------------------------------------------------------
{
const Rgb samples[] = {{0, 0, 0}, {255, 255, 255}, {255, 0, 0}, {0, 255, 0}, {0, 0, 255},
{61, 174, 233}, {255, 176, 201}, {128, 128, 128}, {1, 2, 3}, {250, 250, 5}};
for (Rgb c : samples) {
CHECK_MSG(same(toRgb(toOklch(c)), c, 1), "roundtrip of %s gave %s", hex(c).c_str(), hex(toRgb(toOklch(c))).c_str());
}
const Oklch white = toOklch({255, 255, 255});
CHECK(std::fabs(white.L - 1.0) < 0.002 && white.C < 0.002);
const Oklch red = toOklch({255, 0, 0});
CHECK(std::fabs(red.L - 0.628) < 0.003 && std::fabs(red.C - 0.2577) < 0.003 && std::fabs(red.h - 29.23) < 0.3);
CHECK(toOklch({0, 0, 0}).L < 1e-6);
CHECK(hex({0x3D, 0xAE, 0xE9}) == "#3DAEE9");
CHECK(std::fabs(toneOf({255, 255, 255}) - 100) < 0.01 && toneOf({0, 0, 0}) < 0.01);
CHECK(std::fabs(contrast({0, 0, 0}, {255, 255, 255}) - 21.0) < 0.01);
CHECK(std::fabs(contrast({90, 90, 90}, {90, 90, 90}) - 1.0) < 1e-9);
}
// --- tones ---------------------------------------------------------------
{
const double hues[] = {0, 30, 90, 150, 210, 270, 330};
const double chromas[] = {0.0, 0.05, 0.12, 0.25};
for (double h : hues) {
for (double c : chromas) {
double lastLum = -1;
for (double tone = 5; tone <= 95; tone += 5) {
const Rgb col = toneColor(h, c, tone);
CHECK_MSG(std::fabs(toneOf(col) - tone) < 0.9, "hue %.0f chroma %.2f tone %.0f gave %.2f", h, c, tone, toneOf(col));
const double lum = luminance(col);
CHECK_MSG(lum >= lastLum, "tones are not increasing at hue %.0f chroma %.2f tone %.0f", h, c, tone);
lastLum = lum;
}
}
}
CHECK(toneColor(200, 0.2, 0) == (Rgb{0, 0, 0}));
CHECK(toneColor(200, 0.2, 100) == (Rgb{255, 255, 255}));
CHECK(toneColor(200, 0.2, -5) == (Rgb{0, 0, 0}));
CHECK(toneColor(200, 0.2, 250) == (Rgb{255, 255, 255}));
// a grey stays grey
const Rgb g = toneColor(123, 0.0, 50);
CHECK(g.r == g.g && g.g == g.b);
// the chroma is kept where the gamut allows it and the hue stays the seed's
const Oklch mid = toOklch(toneColor(250, 0.10, 50));
CHECK(std::fabs(mid.h - 250) < 3 && mid.C > 0.08);
}
// --- schemes -------------------------------------------------------------
{
unsigned state = 12345;
auto next = [&] { state = state * 1664525u + 1013904223u; return static_cast<std::uint8_t>(state >> 24); };
std::vector<Rgb> seeds = {{61, 174, 233}, {255, 176, 201}, {0, 0, 0}, {255, 255, 255}, {128, 128, 128},
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {0x67, 0x50, 0xA4}};
for (int i = 0; i < 300; i++) seeds.push_back({next(), next(), next()});
for (Rgb seed : seeds) {
for (Mode mode : {Mode::Light, Mode::Dark}) {
const Scheme s = generate(seed, mode);
const char *m = mode == Mode::Light ? "light" : "dark";
auto pair = [&](const char *name, Rgb bg, Rgb fg, double minimum) {
CHECK_MSG(contrast(bg, fg) >= minimum, "%s %s seed %s: contrast %.2f < %.1f", m, name, hex(seed).c_str(), contrast(bg, fg), minimum);
};
pair("primary", s.primary, s.onPrimary, 4.5);
pair("primaryContainer", s.primaryContainer, s.onPrimaryContainer, 7.0);
pair("secondary", s.secondary, s.onSecondary, 4.5);
pair("secondaryContainer", s.secondaryContainer, s.onSecondaryContainer, 7.0);
pair("tertiary", s.tertiary, s.onTertiary, 4.5);
pair("tertiaryContainer", s.tertiaryContainer, s.onTertiaryContainer, 7.0);
pair("error", s.error, s.onError, 4.5);
pair("errorContainer", s.errorContainer, s.onErrorContainer, 7.0);
pair("surface", s.surface, s.onSurface, 7.0);
pair("background", s.background, s.onBackground, 7.0);
pair("surfaceContainerHighest", s.surfaceContainerHighest, s.onSurface, 7.0);
pair("surfaceContainerLowest", s.surfaceContainerLowest, s.onSurface, 7.0);
pair("surfaceVariant", s.surfaceVariant, s.onSurfaceVariant, 4.5);
pair("inverse", s.inverseSurface, s.inverseOnSurface, 7.0);
pair("outline", s.surface, s.outline, 3.0);
pair("inversePrimary", s.inverseSurface, s.inversePrimary, 3.0);
// the surface ladder is ordered, towards the light in light mode and towards the dark in dark mode
const double l[] = {luminance(s.surfaceContainerLowest), luminance(s.surfaceContainerLow), luminance(s.surfaceContainer),
luminance(s.surfaceContainerHigh), luminance(s.surfaceContainerHighest)};
for (int k = 0; k < 4; k++) {
CHECK_MSG(mode == Mode::Light ? l[k] > l[k + 1] : l[k] < l[k + 1], "%s surface ladder step %d seed %s", m, k, hex(seed).c_str());
}
CHECK(s.seed == seed && s.mode == mode && s.dark() == (mode == Mode::Dark));
// same input, same output
const Scheme again = generate(seed, mode);
CHECK(same(again.primary, s.primary) && same(again.surface, s.surface) && same(again.outline, s.outline));
}
CHECK(luminance(generate(seed, Mode::Dark).surface) < luminance(generate(seed, Mode::Light).surface));
}
// the accent keeps the seed's hue
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{255, 176, 201}, Rgb{0x67, 0x50, 0xA4}, Rgb{200, 60, 20}}) {
const Oklch want = toOklch(seed);
for (Mode mode : {Mode::Light, Mode::Dark}) {
const Oklch got = toOklch(generate(seed, mode).primary);
CHECK_MSG(got.C < 0.04 || std::fabs(std::remainder(got.h - want.h, 360.0)) < 4.0, "hue of %s: seed %.1f primary %.1f", hex(seed).c_str(), want.h, got.h);
}
}
// the tertiary accent sits 60 degrees round the hue circle from the primary, the secondary does not
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{255, 176, 201}, Rgb{0x67, 0x50, 0xA4}, Rgb{200, 60, 20}, Rgb{30, 160, 60}}) {
for (Mode mode : {Mode::Light, Mode::Dark}) {
const Scheme s = generate(seed, mode);
const Oklch p = toOklch(s.primary), t = toOklch(s.tertiary), c = toOklch(s.secondary);
const double shift = std::remainder(t.h - p.h, 360.0);
CHECK_MSG(std::fabs(shift - 60.0) < 8.0, "tertiary of %s is %.1f degrees from the primary", hex(seed).c_str(), shift);
CHECK_MSG(c.C < 0.03 || std::fabs(std::remainder(c.h - p.h, 360.0)) < 8.0, "secondary of %s left the hue", hex(seed).c_str());
// and it is a real accent, not a neutral
CHECK(t.C > 0.03 && c.C > 0.015);
}
}
// the neutrals are almost grey, the error accent is red whatever the seed
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{30, 160, 60}, Rgb{255, 176, 201}}) {
const Scheme s = generate(seed, Mode::Light);
CHECK(toOklch(s.surface).C < 0.03 && toOklch(s.surfaceVariant).C < 0.04);
const Oklch e = toOklch(s.error);
CHECK_MSG(std::fabs(std::remainder(e.h - 29.0, 360.0)) < 12.0 && e.C > 0.12, "error accent of %s: hue %.1f chroma %.3f", hex(seed).c_str(), e.h, e.C);
}
// a grey seed gives a grey scheme, a colourful one does not
for (Rgb grey : {Rgb{128, 128, 128}, Rgb{0, 0, 0}, Rgb{255, 255, 255}, Rgb{40, 41, 40}}) {
const Scheme s = generate(grey, Mode::Light);
CHECK_MSG(toOklch(s.primary).C < 0.03, "grey seed %s gave chroma %.3f", hex(grey).c_str(), toOklch(s.primary).C);
}
CHECK(toOklch(generate({61, 174, 233}, Mode::Light).primary).C > 0.09);
}
// --- parsers -------------------------------------------------------------
{
CHECK(parse::hexColor("#3DAEE9") == std::optional<Rgb>(Rgb{0x3D, 0xAE, 0xE9}));
CHECK(parse::hexColor("3daee9") == std::optional<Rgb>(Rgb{0x3D, 0xAE, 0xE9}));
CHECK(parse::hexColor(" #fff ") == std::optional<Rgb>(Rgb{255, 255, 255}));
CHECK(!parse::hexColor("") && !parse::hexColor("#") && !parse::hexColor("#12") && !parse::hexColor("zzzzzz"));
CHECK(!parse::hexColor("#3DAEE9FF") && !parse::hexColor("banana"));
CHECK(parse::kdeRgb("61,174,233") == std::optional<Rgb>(Rgb{61, 174, 233}));
CHECK(parse::kdeRgb(" 61, 174, 233 ") == std::optional<Rgb>(Rgb{61, 174, 233}));
CHECK(!parse::kdeRgb("300,0,0") && !parse::kdeRgb("1,2") && !parse::kdeRgb("1,2,3,4") && !parse::kdeRgb("a,b,c"));
CHECK(!parse::kdeRgb("") && !parse::kdeRgb("-1,0,0") && !parse::kdeRgb("1 2 3"));
CHECK(parse::gnomeScheme("'prefer-dark'\n") == std::optional<Mode>(Mode::Dark));
CHECK(parse::gnomeScheme("'prefer-light'") == std::optional<Mode>(Mode::Light));
CHECK(!parse::gnomeScheme("'default'") && !parse::gnomeScheme("") && !parse::gnomeScheme("garbage"));
CHECK(parse::gnomeAccent("'pink'\n") == std::optional<Rgb>(Rgb{0xd5, 0x61, 0x99}));
CHECK(parse::gnomeAccent("'blue'") == std::optional<Rgb>(Rgb{0x35, 0x84, 0xe4}));
CHECK(parse::gnomeAccent("'Slate'") == std::optional<Rgb>(Rgb{0x6f, 0x83, 0x96}));
CHECK(!parse::gnomeAccent("'magenta'") && !parse::gnomeAccent(""));
// the real output of this machine
CHECK(parse::portalScheme(kBusctl) == std::optional<Mode>(Mode::Light));
CHECK(parse::portalAccent(kBusctl) == std::optional<Rgb>(Rgb{61, 174, 233}));
CHECK(parse::portalScheme(kBusctlDark) == std::optional<Mode>(Mode::Dark));
CHECK(parse::portalAccent(kBusctlDark) == std::optional<Rgb>(Rgb{128, 64, 255}));
// gdbus prints it differently
CHECK(parse::portalScheme(kGdbus) == std::optional<Mode>(Mode::Dark));
CHECK(parse::portalAccent(kGdbus) == std::optional<Rgb>(Rgb{128, 64, 255}));
// no preference, no accent, missing keys, a different order, nonsense
CHECK(!parse::portalScheme("\"color-scheme\" u 0 \"x\" u 1"));
CHECK(!parse::portalAccent("\"accent-color\" (ddd) -1 -1 -1"));
CHECK(!parse::portalAccent("\"accent-color\" (ddd) 2 0.5 0.5"));
CHECK(!parse::portalAccent("\"accent-color\" (ddd) 0.5 0.5"));
CHECK(!parse::portalScheme("") && !parse::portalAccent("") && !parse::portalScheme("nothing here"));
CHECK(parse::portalAccent("\"accent-color\" (ddd) 0.1 0.2 0.3 \"color-scheme\" u 1") == std::optional<Rgb>(Rgb{26, 51, 77}));
CHECK(parse::portalScheme("\"accent-color\" (ddd) 0.1 0.2 0.3 \"color-scheme\" u 1") == std::optional<Mode>(Mode::Dark));
// the 32 of uint32 and the 3 of (ddd) are not values
CHECK(parse::portalScheme("'color-scheme': <uint32 2>") == std::optional<Mode>(Mode::Light));
}
{
const std::string light =
"[General]\nAccentColor=61,174,233\nColorScheme=BreezeLight\n[Colors:Window]\nBackgroundNormal=239,240,241\n";
Appearance a = parse::kdeGlobals(light);
CHECK(a.accent == std::optional<Rgb>(Rgb{61, 174, 233}) && a.mode == std::optional<Mode>(Mode::Light));
// no accent set: the selection colour of the scheme
a = parse::kdeGlobals("[Colors:Selection]\nBackgroundNormal=100,50,200\n[Colors:Window]\nBackgroundNormal=35,38,41\n");
CHECK(a.accent == std::optional<Rgb>(Rgb{100, 50, 200}) && a.mode == std::optional<Mode>(Mode::Dark));
// a broken accent falls back to the selection colour
a = parse::kdeGlobals("[General]\nAccentColor=abc\n[Colors:Selection]\nBackgroundNormal=1,2,3\n");
CHECK(a.accent == std::optional<Rgb>(Rgb{1, 2, 3}));
// the scheme name when there are no colours
CHECK(parse::kdeGlobals("[General]\nColorScheme=BreezeDark\n").mode == std::optional<Mode>(Mode::Dark));
CHECK(parse::kdeGlobals("[General]\nColorScheme=Foo\n").mode == std::optional<Mode>(Mode::Light));
// comments, blank lines and garbage are ignored, keys of other sections are not mixed up
a = parse::kdeGlobals("# comment\n\nnot a line\n[Other]\nAccentColor=9,9,9\n[Colors:View]\nBackgroundNormal=0,0,0\n");
CHECK(!a.accent && !a.mode);
CHECK(!parse::kdeGlobals("").accent && !parse::kdeGlobals("").mode);
CHECK(parse::gtkSettings("[Settings]\ngtk-application-prefer-dark-theme=1\n") == std::optional<Mode>(Mode::Dark));
CHECK(parse::gtkSettings("gtk-theme-name=Orchis-Dark\n") == std::optional<Mode>(Mode::Dark));
CHECK(parse::gtkSettings("gtk-theme-name=Adwaita\n") == std::optional<Mode>(Mode::Light));
CHECK(!parse::gtkSettings("gtk-font-name=Sans 10\n"));
}
// --- the sources, through a fake system ------------------------------------
{
char tmpl[] = "/tmp/monet_test.XXXXXX";
const fs::path dir = mkdtemp(tmpl);
const std::string kdeFile =
"[General]\nAccentColor=10,200,30\n[Colors:Window]\nBackgroundNormal=30,30,30\n";
{ // KDE: the portal answers, it is used and kdeglobals is not needed
Fake f; f.configHome = dir / "a"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
f.programs["busctl"] = kBusctl;
writeFile(f.configHome / "kdeglobals", kdeFile);
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(Rgb{61, 174, 233}) && a.accentSource == "portal");
CHECK(a.mode == std::optional<Mode>(Mode::Light) && a.modeSource == "portal");
}
{ // KDE without a portal: the file
Fake f; f.configHome = dir / "b"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
writeFile(f.configHome / "kdeglobals", kdeFile);
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(Rgb{10, 200, 30}) && a.accentSource == "kde");
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "kde");
}
{ // GNOME without a portal: gsettings
Fake f; f.configHome = dir / "c"; f.env["XDG_CURRENT_DESKTOP"] = "ubuntu:GNOME";
f.programs["gsettings"] = "'prefer-dark'\n";
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.modeSource == "gnome" && a.mode == std::optional<Mode>(Mode::Dark));
}
{ // a leftover gsettings value on KDE must not beat the desktop's own answer
Fake f; f.configHome = dir / "d"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
f.programs["gsettings"] = "'pink'\n";
writeFile(f.configHome / "kdeglobals", kdeFile);
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(Rgb{10, 200, 30}) && a.accentSource == "kde");
CHECK(std::find(f.called.begin(), f.called.end(), "gsettings") == f.called.end());
}
{ // the override wins over everything
Fake f; f.configHome = dir / "e"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
f.env["MONET_SEED"] = "#112233"; f.env["MONET_MODE"] = "Light";
f.programs["busctl"] = kBusctlDark;
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(Rgb{0x11, 0x22, 0x33}) && a.accentSource == "override");
CHECK(a.mode == std::optional<Mode>(Mode::Light) && a.modeSource == "override");
}
{ // a broken override is ignored, not fatal
Fake f; f.configHome = dir / "f"; f.env["MONET_SEED"] = "banana"; f.env["MONET_MODE"] = "purple";
f.programs["busctl"] = kBusctlDark;
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accentSource == "portal" && a.modeSource == "portal");
}
{ // each value comes from the first source that has it
Fake f; f.configHome = dir / "g"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
f.programs["busctl"] = "\"color-scheme\" u 1";
writeFile(f.configHome / "kdeglobals", "[General]\nAccentColor=1,2,3\n[Colors:Window]\nBackgroundNormal=255,255,255\n");
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "portal");
CHECK(a.accent == std::optional<Rgb>(Rgb{1, 2, 3}) && a.accentSource == "kde");
}
{ // the fallback: nothing is available at all
Fake f; f.configHome = dir / "h";
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(kDefaultSeed) && a.accentSource == "fallback");
CHECK(a.mode == std::optional<Mode>(kDefaultMode) && a.modeSource == "fallback");
}
{ // and it is there even when the list of sources is empty or every source fails
Fake f; f.configHome = dir / "i";
Context c = f.context();
const Appearance a = detect(c, {});
CHECK(a.accent && a.mode && a.accentSource == "fallback");
Source broken{"broken", [](const Context &) { return Appearance{}; }};
const Appearance b = detect(c, {broken, broken});
CHECK(b.accent == std::optional<Rgb>(kDefaultSeed) && b.modeSource == "fallback");
const Scheme s = fromDesktop(c);
CHECK(s.seed == kDefaultSeed && s.mode == kDefaultMode);
}
{ // a source of the program's own is asked first
Fake f; f.configHome = dir / "j"; f.programs["busctl"] = kBusctl;
Context c = f.context();
Source mine{"mine", [](const Context &) { Appearance a; a.accent = Rgb{9, 8, 7}; return a; }};
const Scheme s = fromDesktop(c, {mine});
CHECK(s.seed == (Rgb{9, 8, 7}) && s.mode == Mode::Light);
}
{ // an unknown desktop does not consult the desktop specific sources
Fake f; f.configHome = dir / "k";
f.programs["gsettings"] = "'prefer-dark'";
Context c = f.context();
detect(c, defaultSources(c));
CHECK(std::find(f.called.begin(), f.called.end(), "gsettings") == f.called.end());
}
{ // the list of sources per desktop
auto names = [&](const char *desktop) {
Fake f; f.configHome = dir / "n";
if (desktop) f.env["XDG_CURRENT_DESKTOP"] = desktop;
std::string out;
for (const auto &s : defaultSources(f.context())) out += s.name + " ";
return out;
};
CHECK(names("KDE") == "override portal kde gtk ");
CHECK(names("GNOME") == "override portal gnome gtk ");
CHECK(names("X-Cinnamon") == "override portal gnome gtk ");
CHECK(names("sway") == "override portal gtk ");
CHECK(names(nullptr) == "override portal gtk ");
}
{ // gtk settings give the mode on a desktop with nothing better
Fake f; f.configHome = dir / "o";
writeFile(f.configHome / "gtk-3.0/settings.ini", "[Settings]\ngtk-application-prefer-dark-theme=1\n");
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "gtk");
CHECK(a.accentSource == "fallback");
}
{ // the portal is read through gdbus when there is no busctl
Fake f; f.configHome = dir / "p"; f.programs["gdbus"] = kGdbus;
Context c = f.context();
const Appearance a = detect(c, defaultSources(c));
CHECK(a.accent == std::optional<Rgb>(Rgb{128, 64, 255}) && a.accentSource == "portal" && a.mode == std::optional<Mode>(Mode::Dark));
}
fs::remove_all(dir);
}
// --- running real programs -------------------------------------------------
{
auto ms = [](auto from) { return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - from).count(); };
CHECK(detail::runProgram({"echo", "hi"}, 2000) == std::optional<std::string>("hi\n"));
CHECK(!detail::runProgram({"definitely-not-a-program-xyz"}, 2000));
CHECK(!detail::runProgram({"false"}, 2000));
CHECK(!detail::runProgram({}, 2000));
CHECK(detail::runProgram({"cat"}, 2000) == std::optional<std::string>("")); // stdin is closed
auto t = std::chrono::steady_clock::now();
CHECK(!detail::runProgram({"sleep", "5"}, 150));
CHECK_MSG(ms(t) < 1500, "timeout took %lld ms", static_cast<long long>(ms(t)));
t = std::chrono::steady_clock::now();
CHECK(!detail::runProgram({"head", "-c", "3000000", "/dev/zero"}, 5000)); // far more than an answer
CHECK_MSG(ms(t) < 3000, "oversized output took %lld ms", static_cast<long long>(ms(t)));
// the real system: the call must come back, with or without a desktop
t = std::chrono::steady_clock::now();
const Appearance real = detect(Context::system(), defaultSources(Context::system()));
CHECK(real.accent && real.mode && !real.accentSource.empty());
CHECK_MSG(ms(t) < 2500, "detecting on this machine took %lld ms", static_cast<long long>(ms(t)));
std::printf("this machine: accent %s from %s, %s from %s, %lld ms\n", hex(*real.accent).c_str(), real.accentSource.c_str(),
*real.mode == Mode::Dark ? "dark" : "light", real.modeSource.c_str(), static_cast<long long>(ms(t)));
}
std::printf(failures ? "%d check(s) failed\n" : "libmonet: all checks passed\n", failures);
return failures ? 1 : 0;
}