// Tests for libmonet. Plain asserts, no framework, no real desktop needed: every // source is exercised through a fake Context. #include #include #include #include #include #include #include #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 env; std::map programs; // argv[0] -> standard output std::vector called; fs::path configHome; Context context() { Context c; c.getenv = [this](const std::string &k) -> std::optional { auto it = env.find(k); return it == env.end() ? std::nullopt : std::optional(it->second); }; c.run = [this](const std::vector &argv, int) -> std::optional { called.push_back(argv[0]); auto it = programs.find(argv[0]); return it == programs.end() ? std::nullopt : std::optional(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': , 'color-scheme': , '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(state >> 24); }; std::vector 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{0x3D, 0xAE, 0xE9})); CHECK(parse::hexColor("3daee9") == std::optional(Rgb{0x3D, 0xAE, 0xE9})); CHECK(parse::hexColor(" #fff ") == std::optional(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{61, 174, 233})); CHECK(parse::kdeRgb(" 61, 174, 233 ") == std::optional(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::Dark)); CHECK(parse::gnomeScheme("'prefer-light'") == std::optional(Mode::Light)); CHECK(!parse::gnomeScheme("'default'") && !parse::gnomeScheme("") && !parse::gnomeScheme("garbage")); CHECK(parse::gnomeAccent("'pink'\n") == std::optional(Rgb{0xd5, 0x61, 0x99})); CHECK(parse::gnomeAccent("'blue'") == std::optional(Rgb{0x35, 0x84, 0xe4})); CHECK(parse::gnomeAccent("'Slate'") == std::optional(Rgb{0x6f, 0x83, 0x96})); CHECK(!parse::gnomeAccent("'magenta'") && !parse::gnomeAccent("")); // the real output of this machine CHECK(parse::portalScheme(kBusctl) == std::optional(Mode::Light)); CHECK(parse::portalAccent(kBusctl) == std::optional(Rgb{61, 174, 233})); CHECK(parse::portalScheme(kBusctlDark) == std::optional(Mode::Dark)); CHECK(parse::portalAccent(kBusctlDark) == std::optional(Rgb{128, 64, 255})); // gdbus prints it differently CHECK(parse::portalScheme(kGdbus) == std::optional(Mode::Dark)); CHECK(parse::portalAccent(kGdbus) == std::optional(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{26, 51, 77})); CHECK(parse::portalScheme("\"accent-color\" (ddd) 0.1 0.2 0.3 \"color-scheme\" u 1") == std::optional(Mode::Dark)); // the 32 of uint32 and the 3 of (ddd) are not values CHECK(parse::portalScheme("'color-scheme': ") == std::optional(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{61, 174, 233}) && a.mode == std::optional(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{100, 50, 200}) && a.mode == std::optional(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{1, 2, 3})); // the scheme name when there are no colours CHECK(parse::kdeGlobals("[General]\nColorScheme=BreezeDark\n").mode == std::optional(Mode::Dark)); CHECK(parse::kdeGlobals("[General]\nColorScheme=Foo\n").mode == std::optional(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::Dark)); CHECK(parse::gtkSettings("gtk-theme-name=Orchis-Dark\n") == std::optional(Mode::Dark)); CHECK(parse::gtkSettings("gtk-theme-name=Adwaita\n") == std::optional(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{61, 174, 233}) && a.accentSource == "portal"); CHECK(a.mode == std::optional(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{10, 200, 30}) && a.accentSource == "kde"); CHECK(a.mode == std::optional(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::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{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{0x11, 0x22, 0x33}) && a.accentSource == "override"); CHECK(a.mode == std::optional(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::Dark) && a.modeSource == "portal"); CHECK(a.accent == std::optional(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(kDefaultSeed) && a.accentSource == "fallback"); CHECK(a.mode == std::optional(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(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::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{128, 64, 255}) && a.accentSource == "portal" && a.mode == std::optional(Mode::Dark)); } fs::remove_all(dir); } // --- running real programs ------------------------------------------------- { auto ms = [](auto from) { return std::chrono::duration_cast(std::chrono::steady_clock::now() - from).count(); }; CHECK(detail::runProgram({"echo", "hi"}, 2000) == std::optional("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("")); // 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(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(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(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(ms(t))); } std::printf(failures ? "%d check(s) failed\n" : "libmonet: all checks passed\n", failures); return failures ? 1 : 0; }