Files
qbz/scripts/test_qt_exclusive_gate.mjs
T
vicrodh e58ec85eb2 fix(qt): restore the Exclusive mode row on macOS (#748)
System default on macOS is the CoreAudio exclusive backend: with
`exclusive_mode` set, qbz-audio takes Hog Mode, pins the device and switches
its nominal rate to the track (PR #391, shipped in 1.2.11 and never removed).
The Tauri settings gated the toggle on `ALSA || (macos && SystemDefault)`;
the Slint port copied only the ALSA half, and fc0d35e29 then hid the row on
macOS on the premise that no backend there could honour it. So 2.0.x showed
a grey row and 2.1.0 none, while the DB flag still engaged Hog Mode.

- SettingsDoc publishes `backendIsCoreAudio` (macOS && System default); the
  row is visible and enabled on it. Linux and Windows are unchanged.
- The backend-switch cascade goes through one predicate
  (`output_labels::backend_honours_exclusive`), so re-selecting Auto or
  System default on macOS no longer clears the flag.
- The output LED reads EXCL on macOS System default + exclusive_mode instead
  of DEFAULT while the device is hogged.
- Node test `scripts/test_qt_exclusive_gate.mjs` evaluates the row's real
  gate expressions across platforms; added to qt-runtime-gate.sh. Rust tests
  cover the predicate, the LED and the serde name QML reads.

Verified by smoke on the Mac mini with a USB DAC: the row appears, Hog Mode
is acquired, and browser audio moves to the built-in speakers while QBZ
plays. qbz-qt suite 782/782 on Linux; 13/13 with cfg(macos) on the Mac.

cc
2026-09-09 22:44:16 -06:00

96 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
// Executes the REAL gate expressions of the Settings > Audio "Exclusive mode"
// row (visible / rowEnabled / toggle enabled) against a platform x backend
// matrix, the way test_qt_release_sort.mjs evaluates LibraryView bindings.
//
// #748: the row must be reachable on macOS, whose only backend (System
// default) honours `exclusive_mode` through CoreAudio Hog Mode (PR #391,
// qbz-audio backend.rs `create_output_stream_with_exclusive_guard`). It was
// hidden there by fc0d35e29 on the wrong premise that no macOS backend could
// honour it. Linux and Windows must keep their 2026-08-27 behaviour.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const qml = fs.readFileSync(
new URL('../crates/qbz-qt/qml/settings/AudioSettings.qml', import.meta.url),
'utf8',
);
// Locate the SettingRow that carries the "Exclusive mode" label and return
// its full block (balanced braces), with // comments stripped so a comment
// can never satisfy a binding match.
function exclusiveRowBlock(source) {
const label = source.indexOf('QbzSession.tr("Exclusive mode"');
assert.notEqual(label, -1, 'Exclusive mode row must exist');
const start = source.lastIndexOf('SettingRow {', label);
assert.notEqual(start, -1, 'Exclusive mode label must sit inside a SettingRow');
let end = source.indexOf('{', start) + 1;
let depth = 1;
while (depth && end < source.length) {
if (source[end] === '{') ++depth;
if (source[end] === '}') --depth;
++end;
}
assert.equal(depth, 0, 'SettingRow block must be balanced');
return source
.slice(start, end)
.split('\n')
.map(line => line.replace(/\/\/.*$/, ''))
.join('\n');
}
const block = exclusiveRowBlock(qml);
const binding = (text, name) => {
const m = text.match(new RegExp(`(?:^|[\\s;{])${name}:\\s*([^\\n]+)`));
assert.ok(m, `binding "${name}" must be present`);
return m[1].trim();
};
const toggleStart = block.indexOf('QbzToggle {');
assert.notEqual(toggleStart, -1, 'row must contain a QbzToggle');
const rowPart = block.slice(0, toggleStart);
const togglePart = block.slice(toggleStart);
const visibleExpr = binding(rowPart, 'visible');
const rowEnabledExpr = binding(rowPart, 'rowEnabled');
const toggleEnabledExpr = binding(togglePart, 'enabled');
// Evaluate a binding as QML would: an absent document field reads as
// undefined, so `undefined === true` is false and the row stays hidden.
function evaluate(expr, platform, doc) {
const QbzShell = {
isLinux: platform === 'linux',
isMacos: platform === 'macos',
isWindows: platform === 'windows',
};
return Boolean(vm.runInNewContext(expr, { QbzShell, root: { doc } }));
}
const cases = [
// Linux: unchanged since 2026-08-27 — always visible, enabled only on ALSA.
{ platform: 'linux', doc: { backendIsAlsa: true }, visible: true, enabled: true },
{ platform: 'linux', doc: { backendIsPipewire: true }, visible: true, enabled: false },
{ platform: 'linux', doc: {}, visible: true, enabled: false },
// macOS: System default IS the CoreAudio exclusive backend (Hog Mode).
{ platform: 'macos', doc: { backendIsCoreAudio: true }, visible: true, enabled: true },
// macOS with the flag absent (older document): stays hidden, never a grey row.
{ platform: 'macos', doc: {}, visible: false, enabled: false },
// Windows: hidden on purpose (5ba49a739) — the exclusive path is the
// backend dropdown, the toggle changes nothing there.
{ platform: 'windows', doc: {}, visible: false, enabled: false },
{ platform: 'windows', doc: { backendIsWasapi: true }, visible: false, enabled: true },
];
for (const c of cases) {
const tag = `${c.platform} ${JSON.stringify(c.doc)}`;
assert.equal(evaluate(visibleExpr, c.platform, c.doc), c.visible, `visible on ${tag}`);
assert.equal(evaluate(rowEnabledExpr, c.platform, c.doc), c.enabled, `rowEnabled on ${tag}`);
assert.equal(evaluate(toggleEnabledExpr, c.platform, c.doc), c.enabled, `toggle enabled on ${tag}`);
}
// The toggle must keep writing the same settings key the bridge dispatches
// ("exclusive-mode" -> AudioSettingsStore::set_exclusive_mode, Apply::Reinit).
assert.match(togglePart, /QbzBridge\.settingsBool\("exclusive-mode", v\)/);
console.log('Exclusive mode row gates: Linux unchanged, macOS CoreAudio reachable, Windows hidden: passed');