Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Exercises the GTK user idle service. The service uses the
// org.gnome.Mutter.IdleMonitor DBus backend -- the cache-and-async-poll logic
// from bug 1962438 -- whenever that service is reachable on the session bus,
// i.e. on any GNOME session, X11 or Wayland, including the Wayland CI workers.
// Otherwise the backend is XScreenSaver and only the generic smoke assertions
// apply. The active backend cannot be queried directly, so we detect it from
// the service's own MOZ_LOG output rather than guessing from the desktop
// environment (XDG_CURRENT_DESKTOP does not survive into the test process under
// CI) or the window protocol (Mutter is used under XWayland too).
const { TestUtils } = ChromeUtils.importESModule(
);
add_task(async function () {
const idle = Cc["@mozilla.org/widget/useridleservice;1"].getService(
Ci.nsIUserIdleService
);
// Backend-agnostic smoke checks.
const first = idle.idleTime;
Assert.equal(typeof first, "number", "idleTime is a number");
Assert.greaterOrEqual(first, 0, "idleTime is non-negative");
// Route the idle service's MOZ_LOG module to a file (its output is not
// visible to the console service); logging.config.sync flushes each line so
// the readbacks below see it. Enabled up front so it captures the poll used
// to detect the active backend.
const logBase = PathUtils.join(
Services.dirsvc.get("ProfD", Ci.nsIFile).path,
"idle_mutter_test.log"
);
const logFile = `${logBase}-main.${Services.appinfo.processID}.moz_log`;
await SpecialPowers.pushPrefEnv({
set: [
["logging.config.sync", true],
["logging.config.LOG_FILE", logBase],
["logging.nsIUserIdleService", 5],
],
});
registerCleanupFunction(() =>
IOUtils.remove(logFile, { ignoreAbsent: true })
);
async function logIncludes(needle) {
if (!(await IOUtils.exists(logFile))) {
return false;
}
return (await IOUtils.readUTF8(logFile)).includes(needle);
}
// Detect the active backend from its log: a Mutter poll logs
// "PollIdleTime() request", whereas the XScreenSaver backend logs
// "UserIdleServiceX11::PollIdleTime". One idleTime read triggers a poll.
void idle.idleTime;
await TestUtils.waitForCondition(
async () =>
(await logIncludes("PollIdleTime() request")) ||
(await logIncludes("UserIdleServiceX11")),
"idle service served a poll we can attribute to a backend",
100,
100
);
if (!(await logIncludes("PollIdleTime() request"))) {
info("Idle backend is not Mutter (XScreenSaver); skipping Mutter checks.");
return;
}
info("Mutter idle backend active; exercising the cache path.");
// Within the fresh-cache window (kCacheFreshMs == 1000ms) reads are served
// from the cache without a new DBus poll. The cached value is advanced by
// the time elapsed since it was sampled (bug 2053041), so repeated reads must
// report a forward-moving value: returning a frozen value would make
// nsUserIdleService misread the stall as a return from idle and re-arm its
// timer in a tight loop.
//
// A background refresh re-samples mCachedIdleTime (logged as "Async handler
// got N, cached"), which drops the reported value back to a fresh real
// reading. Local user interaction can trigger that at any time, so fresh
// readings that straddle a cache update are not comparable. Only readings
// from a single cache sample -- an unbroken run of "returns cached (fresh)"
// lines with no cache update in between -- advance monotonically, so we
// collect the fresh readings split on each cache-update boundary and assert
// within each run rather than across the whole capture.
async function freshRuns() {
if (!(await IOUtils.exists(logFile))) {
return [];
}
const text = await IOUtils.readUTF8(logFile);
const runs = [];
let run = null;
for (const m of text.matchAll(
/returns cached \(fresh\) (\d+)|(Async handler got)/g
)) {
if (m[2]) {
run = null; // A cache update ends the current run.
} else {
if (!run) {
runs.push((run = []));
}
run.push(parseInt(m[1], 10));
}
}
return runs;
}
// Drive fresh reads until at least one within-sample run shows advancement.
await TestUtils.waitForCondition(
async () => {
void idle.idleTime;
return (await freshRuns()).some(r => r.length >= 2 && r.at(-1) > r[0]);
},
"fresh-cache idle time advances with the wall clock (bug 2053041)",
100,
100
);
for (const run of await freshRuns()) {
for (let i = 1; i < run.length; i++) {
Assert.greaterOrEqual(
run[i],
run[i - 1],
"fresh-cache idle time is non-decreasing within a cache sample"
);
}
}
// The fresh window must time out: once more than kCacheFreshMs has elapsed,
// the next read takes the stale branch, returns the cached value, and kicks
// off a background DBus refresh. Polling idleTime crosses that boundary
// without an arbitrary fixed delay.
await TestUtils.waitForCondition(
() => {
void idle.idleTime;
return logIncludes("returns cached (stale)");
},
"cache freshness timed out and took the stale branch",
100,
100
);
// The background refresh's async DBus reply lands and updates the cache.
await TestUtils.waitForCondition(
() => logIncludes("Async handler got"),
"background DBus refresh completed and re-cached the value",
100,
100
);
// Two paths are deliberately left untested here:
// - The cold "very stale" resync (cache older than kCacheStaleMs == 5000ms,
// which falls through to a synchronous wait). Every stale read above kicks
// a background refresh that re-stamps the cache, so reaching the very-stale
// state requires sitting >5s without touching idleTime -- i.e. an arbitrary
// fixed delay, which is both slow and flaky (see no-arbitrary-setTimeout).
// - The DBus poll timeout (kPollTimeoutMs, the "timed out" path). It only
// fires when the session bus stops replying, and there is no way to make a
// healthy Mutter IdleMonitor hang from a test.
// Both are covered by manual MOZ_LOG inspection rather than automation.
});