You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libcutefish/audio/tests/audio_test.cpp

674 lines
27 KiB
C++

#include "audiomodel.h"
#include "backend.h"
#include "volumemonitor.h"
#include "fakeaudiodevice.h"
#include "teststream.h"
#include <QAbstractItemModelTester>
#include <QFile>
#include <QProcess>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickWindow>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <cmath>
#include <limits>
#include <memory>
#include <vector>
#include <spa/param/audio/raw.h>
#include <spa/param/profile.h>
#include <spa/param/props.h>
#include <spa/param/route.h>
#include <spa/pod/builder.h>
namespace CutefishAudio {
class AudioTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void volumeScale();
void parseProperties();
void parseProfilesAndRoutes();
void discoveryAndModels();
void volumeAndMute();
void externalChanges();
void defaultDevices();
void hotplug();
void hardwareRoutes();
void streamRouting();
void monitor();
void qmlModelsAndControls();
void settingsMultipleDevices();
void reconnect();
void cleanupTestCase();
void cleanup() { m_failed |= QTest::currentTestFailed(); }
private:
void startServers();
void stopServers();
void activateDevices();
AudioObject *find(const QString &name) const;
QByteArray command(const QString &program, const QStringList &args);
QTemporaryDir m_runtime;
QProcess m_server;
QProcess m_policy;
Backend *m_backend = nullptr;
std::unique_ptr<QQmlEngine> m_engine;
std::vector<std::unique_ptr<TestStream>> m_drivers;
bool m_failed = false;
};
void AudioTest::initTestCase()
{
QVERIFY(m_runtime.isValid());
const QByteArray path = m_runtime.path().toUtf8();
qputenv("XDG_RUNTIME_DIR", path);
qputenv("PIPEWIRE_RUNTIME_DIR", path);
qputenv("PIPEWIRE_REMOTE", "cutefish-audio-test");
qputenv("XDG_CONFIG_HOME", path + "/config");
qputenv("XDG_STATE_HOME", path + "/state");
qputenv("XDG_CACHE_HOME", path + "/cache");
qunsetenv("PIPEWIRE_CONFIG_NAME");
qunsetenv("PIPEWIRE_CONFIG_DIR");
qunsetenv("WIREPLUMBER_CONFIG_DIR");
m_backend = Backend::instance();
connect(m_backend, &Backend::disconnecting, this, [this] { m_drivers.clear(); });
connect(m_backend, &Backend::operationFailed, this, [](const QString &message) { qWarning() << message; });
QTest::qWait(50);
QVERIFY(!m_backend->isConnected());
startServers();
QTRY_VERIFY_WITH_TIMEOUT(m_backend->isConnected(), 10000);
QTRY_VERIFY_WITH_TIMEOUT(find("test-output-a") && find("test-output-b") && find("test-input"), 10000);
QTRY_VERIFY_WITH_TIMEOUT(m_backend->defaultNode(AudioObject::Sink), 10000);
activateDevices();
// The settings application registers this module from C++; the page
// under test only needs its Metrics singleton.
qmlRegisterSingletonType(QUrl::fromLocalFile(QStringLiteral(
AUDIO_SOURCE_ROOT "/settings/src/qml/Metrics.qml")),
"Cutefish.Settings", 1, 0, "Metrics");
qmlProtectModule("Cutefish.Settings", 1);
m_engine = std::make_unique<QQmlEngine>();
m_engine->addImportPath(AUDIO_IMPORT_PATH);
}
void AudioTest::startServers()
{
m_server.setProcessChannelMode(QProcess::MergedChannels);
m_server.start(PIPEWIRE_EXECUTABLE, {"-c", AUDIO_TEST_CONFIG});
QVERIFY2(m_server.waitForStarted(), qPrintable(m_server.errorString()));
QTRY_VERIFY_WITH_TIMEOUT(QFile::exists(m_runtime.path() + "/cutefish-audio-test"), 5000);
m_policy.setProcessChannelMode(QProcess::MergedChannels);
m_policy.start(WIREPLUMBER_EXECUTABLE, {"--profile=policy"});
QVERIFY2(m_policy.waitForStarted(), qPrintable(m_policy.errorString()));
}
void AudioTest::stopServers()
{
m_drivers.clear();
for (QProcess *process : {&m_policy, &m_server}) {
if (process->state() != QProcess::NotRunning) {
process->terminate();
if (!process->waitForFinished(3000)) {
process->kill();
process->waitForFinished(3000);
}
}
}
}
void AudioTest::activateDevices()
{
for (const QString &name : {QString("test-output-a"), QString("test-output-b"), QString("test-input")}) {
QTRY_VERIFY_WITH_TIMEOUT(find(name), 10000);
AudioObject *node = find(name);
auto stream = std::make_unique<TestStream>(m_backend->core(), "cutefish.internal.test-" + name.toUtf8(),
node->properties().value(PW_KEY_OBJECT_SERIAL).toString().toUtf8(), node->kind() == AudioObject::Source);
QVERIFY(stream->connected);
QTRY_VERIFY_WITH_TIMEOUT(stream->streaming, 5000);
m_drivers.push_back(std::move(stream));
}
}
AudioObject *AudioTest::find(const QString &name) const
{
for (AudioObject *object : m_backend->objects()) {
if (object->name() == name)
return object;
}
return nullptr;
}
QByteArray AudioTest::command(const QString &program, const QStringList &args)
{
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(program, args);
if (!process.waitForFinished(5000) || process.exitCode() != 0) {
QTest::qFail(qPrintable(QString::fromUtf8(process.readAll())), __FILE__, __LINE__);
return {};
}
return process.readAll();
}
void AudioTest::volumeScale()
{
QCOMPARE(AudioObject::fromLinear(0.125f), 0.5);
QCOMPARE(AudioObject::toLinear(0.5), 0.125f);
QCOMPARE(AudioObject::toLinear(-1.0), 0.0f);
QCOMPARE(AudioObject::toLinear(100.0), 3.375f);
QCOMPARE(AudioObject::toLinear(std::numeric_limits<double>::infinity()), 0.0f);
QCOMPARE(AudioObject::fromLinear(std::numeric_limits<float>::quiet_NaN()), 0.0);
for (double value : {0.0, 0.01, 0.25, 0.75, 1.0, 1.5})
QVERIFY(std::abs(AudioObject::fromLinear(AudioObject::toLinear(value)) - value) < 0.000001);
}
void AudioTest::parseProperties()
{
AudioObject object(nullptr, 42, AudioObject::Sink);
QVERIFY(!object.hasVolume());
QCOMPARE(object.activePortIndex(), -1);
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const float volumes[] = {0.125f, 1.0f};
const quint32 channels[] = {SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR};
const auto *param = static_cast<spa_pod *>(spa_pod_builder_add_object(&builder,
SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, 2, volumes),
SPA_PROP_channelMap, SPA_POD_Array(sizeof(quint32), SPA_TYPE_Id, 2, channels),
SPA_PROP_mute, SPA_POD_Bool(true)));
object.updateVolume(param);
QVERIFY(object.hasVolume());
QVERIFY(object.muted());
QCOMPARE(object.volume(), 1.0);
QCOMPARE(object.channelVolumes(), QVariantList({0.5, 1.0}));
QCOMPARE(object.channels(), QStringList({"FL", "FR"}));
object.setChannelVolume(-1, 0.5);
object.setChannelVolume(2, 0.5);
object.updateVolume(nullptr);
QCOMPARE(object.volume(), 1.0);
const spa_dict_item items[] = {{"node.name", "test"}, {"node.description", "Test speaker"}};
const spa_dict properties = SPA_DICT_INIT(items, 2);
object.updateProperties(&properties);
QCOMPARE(object.description(), QString("Test speaker"));
QCOMPARE(object.displayName(), QString("Test speaker"));
QCOMPARE(object.cardIndex(), AudioObject::InvalidId);
}
void AudioTest::parseProfilesAndRoutes()
{
AudioObject card(nullptr, 99, AudioObject::Card);
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const auto *profile = static_cast<spa_pod *>(spa_pod_builder_add_object(&builder,
SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_EnumProfile,
SPA_PARAM_PROFILE_index, SPA_POD_Int(7), SPA_PARAM_PROFILE_name, SPA_POD_String("stereo"),
SPA_PARAM_PROFILE_description, SPA_POD_String("Stereo"),
SPA_PARAM_PROFILE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_yes)));
card.updateProfile(profile, false);
card.updateProfile(profile, false);
card.updateProfile(profile, true);
QCOMPARE(card.profiles().size(), 1);
QCOMPARE(card.activeProfileIndex(), 0);
QCOMPARE(card.profiles().first().toMap().value("availability").toInt(), int(AudioObject::Available));
builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const int devices[] = {3};
const int profiles[] = {7};
const auto *route = static_cast<spa_pod *>(spa_pod_builder_add_object(&builder,
SPA_TYPE_OBJECT_ParamRoute, SPA_PARAM_EnumRoute,
SPA_PARAM_ROUTE_index, SPA_POD_Int(9), SPA_PARAM_ROUTE_device, SPA_POD_Int(3),
SPA_PARAM_ROUTE_direction, SPA_POD_Id(SPA_DIRECTION_OUTPUT),
SPA_PARAM_ROUTE_name, SPA_POD_String("headphones"),
SPA_PARAM_ROUTE_devices, SPA_POD_Array(sizeof(int), SPA_TYPE_Int, 1, devices),
SPA_PARAM_ROUTE_profiles, SPA_POD_Array(sizeof(int), SPA_TYPE_Int, 1, profiles),
SPA_PARAM_ROUTE_available, SPA_POD_Id(SPA_PARAM_AVAILABILITY_no)));
card.updateRoute(route, false);
card.updateRoute(route, true);
QCOMPARE(card.m_routes.size(), 1);
QCOMPARE(card.m_activeRoutes.value(3), 9);
QCOMPARE(card.m_routes.first().toMap().value("availability").toInt(), int(AudioObject::Unavailable));
}
void AudioTest::discoveryAndModels()
{
AudioModel sinks(AudioObject::Sink, m_backend);
AudioModel sources(AudioObject::Source, m_backend);
QAbstractItemModelTester tester(&sinks, QAbstractItemModelTester::FailureReportingMode::QtTest);
QCOMPARE(sinks.rowCount(), 2);
QCOMPARE(sources.rowCount(), 1);
QCOMPARE(sinks.rowCount(sinks.index(0, 0)), 0);
QVERIFY(sinks.role("AudioObject") > Qt::UserRole);
QVERIFY(sinks.role("Volume") > Qt::UserRole);
QVERIFY(sinks.role("Description") > Qt::UserRole);
QCOMPARE(sinks.role("MissingRole"), -1);
QVERIFY(!sinks.setData({}, 0.5, sinks.role("Volume")));
QVERIFY(!sinks.setData(sinks.index(0, 0), "bad", sinks.role("Name")));
QVERIFY(sinks.data(sinks.index(0, 0), sinks.role("AudioObject")).value<AudioObject *>());
QCOMPARE(sources.data(sources.index(0, 0), sources.role("Name")).toString(), QString("test-input"));
}
void AudioTest::volumeAndMute()
{
AudioObject *sink = find("test-output-a");
QTRY_VERIFY(sink->volumeWritable());
sink->setVolume(0.42);
QTRY_VERIFY(std::abs(sink->volume() - 0.42) < 0.0001);
sink->setMuted(true);
QTRY_VERIFY(sink->muted());
sink->setMuted(false);
QTRY_VERIFY(!sink->muted());
sink->setChannelVolumes({0.25, 0.5});
QTRY_VERIFY(std::abs(sink->channelVolumes().at(0).toDouble() - 0.25) < 0.0001);
sink->setVolume(1.0);
QTRY_VERIFY(std::abs(sink->volume() - 1.0) < 0.0001);
QVERIFY(std::abs(sink->channelVolumes().at(0).toDouble() - 0.5) < 0.0001);
sink->setVolume(std::numeric_limits<double>::quiet_NaN());
sink->setChannelVolumes({0.1});
sink->setChannelVolume(-1, 0.0);
QTest::qWait(30);
QVERIFY(std::abs(sink->volume() - 1.0) < 0.0001);
sink->setVolume(-1.0);
QTRY_COMPARE(sink->volume(), 0.0);
sink->setVolume(5.0);
QTRY_VERIFY(std::abs(sink->volume() - 1.5) < 0.0001);
sink->setVolume(0.5);
QTRY_VERIFY(std::abs(sink->volume() - 0.5) < 0.0001);
AudioObject *source = find("test-input");
QTRY_VERIFY(source->volumeWritable());
source->setMuted(true);
QTRY_VERIFY(source->muted());
source->setMuted(false);
QTRY_VERIFY(!source->muted());
}
void AudioTest::externalChanges()
{
AudioObject *sink = find("test-output-a");
command(PW_CLI_EXECUTABLE, {"set-param", QString::number(sink->index()), "Props",
"{ channelVolumes: [ 0.064 0.064 ], mute: true }"});
QTRY_VERIFY(std::abs(sink->volume() - 0.4) < 0.0001);
QTRY_VERIFY(sink->muted());
sink->setMuted(false);
QTRY_VERIFY(!sink->muted());
}
void AudioTest::defaultDevices()
{
AudioObject *first = find("test-output-a");
AudioObject *second = find("test-output-b");
second->setDefault(true);
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Sink), second);
QVERIFY(second->isDefault());
QVERIFY(!first->isDefault());
first->setDefault(true);
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Sink), first);
find("test-input")->setDefault(true);
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Source), find("test-input"));
}
void AudioTest::hotplug()
{
AudioModel sinks(AudioObject::Sink, m_backend);
QAbstractItemModelTester tester(&sinks, QAbstractItemModelTester::FailureReportingMode::QtTest);
auto *properties = pw_properties_new("factory.name", "support.null-audio-sink",
PW_KEY_NODE_NAME, "hotplug-output", PW_KEY_MEDIA_CLASS, "Audio/Sink",
"audio.position", "[ FL FR ]", nullptr);
auto *proxy = static_cast<pw_proxy *>(pw_core_create_object(m_backend->core(), "adapter",
PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
pw_properties_free(properties);
QVERIFY(proxy);
QTRY_COMPARE(sinks.rowCount(), 3);
QPointer<AudioObject> device = find("hotplug-output");
QVERIFY(device);
device->setDefault(true);
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Sink), device.data());
pw_proxy_destroy(proxy);
QTRY_COMPARE(sinks.rowCount(), 2);
QTRY_VERIFY(device.isNull());
QTRY_VERIFY(m_backend->defaultNode(AudioObject::Sink));
}
void AudioTest::hardwareRoutes()
{
FakeAudioDevice device;
QVERIFY(device.exportTo(m_backend->core()));
QTRY_VERIFY(find("test-card"));
AudioObject *card = find("test-card");
QTRY_COMPARE(card->profiles().size(), 2);
QTRY_VERIFY(card->activeProfileIndex() >= 0);
card->setActiveProfileIndex(1);
QTRY_COMPARE(device.profile, 8);
QTRY_COMPARE(card->activeProfileIndex(), 1);
QVERIFY(device.saved);
const QByteArray cardId = QByteArray::number(card->index());
auto *properties = pw_properties_new("factory.name", "support.null-audio-sink",
PW_KEY_NODE_NAME, "test-card-output", PW_KEY_MEDIA_CLASS, "Audio/Sink",
PW_KEY_DEVICE_ID, cardId.constData(), "card.profile.device", "3",
"audio.position", "[ FL FR ]", nullptr);
auto *proxy = static_cast<pw_proxy *>(pw_core_create_object(m_backend->core(), "adapter",
PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
pw_properties_free(properties);
QVERIFY(proxy);
QTRY_VERIFY(find("test-card-output"));
AudioObject *sink = find("test-card-output");
QTRY_VERIFY(sink->volumeWritable());
QTRY_COMPARE(sink->ports().size(), 2);
sink->setActivePortIndex(1);
QTRY_COMPARE(device.route, 10);
QTRY_COMPARE(sink->activePortIndex(), 1);
QCOMPARE(sink->displayName(), QString("Headphones"));
sink->setChannelVolumes({0.5, 0.5});
QTRY_COMPARE(device.volumes[0], 0.125f);
QTRY_COMPARE(device.volumes[1], 0.125f);
QTRY_VERIFY(std::abs(sink->volume() - 0.5) < 0.0001);
QVERIFY(device.saved);
sink->setVolume(0.25);
QTRY_COMPARE(device.volumes[0], 0.015625f);
QTRY_VERIFY(std::abs(sink->volume() - 0.25) < 0.0001);
sink->setMuted(true);
QTRY_VERIFY(device.muted);
QTRY_VERIFY(sink->muted());
const int writes = device.writes;
sink->setActivePortIndex(-1);
card->setActiveProfileIndex(999);
QTest::qWait(30);
QCOMPARE(device.writes, writes);
device.volumes[0] = 0.064f;
device.volumes[1] = 0.064f;
device.muted = false;
device.notify();
QTRY_VERIFY(std::abs(sink->volume() - 0.4) < 0.0001);
QTRY_VERIFY(!sink->muted());
pw_proxy_destroy(proxy);
QTRY_VERIFY(!find("test-card-output"));
}
void AudioTest::streamRouting()
{
AudioObject *first = find("test-output-a");
AudioObject *second = find("test-output-b");
TestStream playback(m_backend->core(), "test-playback",
first->properties().value(PW_KEY_OBJECT_SERIAL).toString().toUtf8());
QVERIFY(playback.connected);
QTRY_VERIFY(playback.streaming);
QTRY_VERIFY(find("test-playback"));
AudioObject *stream = find("test-playback");
QCOMPARE(stream->kind(), AudioObject::Playback);
QTRY_COMPARE(stream->deviceIndex(), first->index());
QTRY_VERIFY(stream->volumeWritable());
stream->setVolume(0.25);
QTRY_VERIFY(std::abs(stream->volume() - 0.25) < 0.0001);
stream->setMuted(true);
QTRY_VERIFY(stream->muted());
stream->setDeviceIndex(second->index());
QTRY_COMPARE(stream->deviceIndex(), second->index());
stream->setDeviceIndex(find("test-input")->index());
QTest::qWait(30);
QCOMPARE(stream->deviceIndex(), second->index());
first->switchStreams();
QTRY_COMPARE(stream->deviceIndex(), first->index());
TestStream recording(m_backend->core(), "test-recording",
find("test-input")->properties().value(PW_KEY_OBJECT_SERIAL).toString().toUtf8(), true);
QVERIFY(recording.connected);
QTRY_VERIFY(recording.streaming);
QTRY_VERIFY(find("test-recording"));
QCOMPARE(find("test-recording")->kind(), AudioObject::Capture);
QTRY_COMPARE(find("test-recording")->deviceIndex(), find("test-input")->index());
auto *properties = pw_properties_new("factory.name", "audiotestsrc",
PW_KEY_NODE_NAME, "second-input", PW_KEY_MEDIA_CLASS, "Audio/Source",
"audio.position", "[ FL FR ]", nullptr);
auto *proxy = static_cast<pw_proxy *>(pw_core_create_object(m_backend->core(), "adapter",
PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &properties->dict, 0));
pw_properties_free(properties);
QVERIFY(proxy);
QTRY_VERIFY(find("second-input"));
find("second-input")->setDefault(true);
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Source), find("second-input"));
find("test-recording")->setDeviceIndex(find("second-input")->index());
QTRY_COMPARE(find("test-recording")->deviceIndex(), find("second-input")->index());
find("test-recording")->setDeviceIndex(find("test-input")->index());
QTRY_COMPARE(find("test-recording")->deviceIndex(), find("test-input")->index());
pw_proxy_destroy(proxy);
QTRY_VERIFY(!find("second-input"));
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Source), find("test-input"));
}
void AudioTest::monitor()
{
AudioObject *sink = find("test-output-a");
sink->setVolume(1.0);
QTRY_VERIFY(std::abs(sink->volume() - 1.0) < 0.0001);
VolumeMonitor monitor;
monitor.setTarget(sink);
double peak = 0.0;
connect(&monitor, &VolumeMonitor::volumeChanged, this, [&] { peak = std::max(peak, monitor.volume()); });
TestStream tone(m_backend->core(), "test-tone",
sink->properties().value(PW_KEY_OBJECT_SERIAL).toString().toUtf8(), false, 0.5f);
QVERIFY(tone.connected);
QTRY_VERIFY_WITH_TIMEOUT(monitor.isAvailable(), 2000);
QTRY_VERIFY_WITH_TIMEOUT(peak > 0.01, 2000);
monitor.setTarget(nullptr);
QCOMPARE(monitor.volume(), 0.0);
QVERIFY(!monitor.isAvailable());
}
void AudioTest::qmlModelsAndControls()
{
QQmlEngine &engine = *m_engine;
QQmlComponent component(&engine);
component.setData(R"(
import QtQuick
import QtQuick.Controls
import FishUI 1.0 as FishUI
import Cutefish.Audio 1.0
Item {
width: 400
height: 400
property alias sinks: sinks
property alias slider: slider
SinkModel { id: sinks }
SourceModel { id: sources }
FishUI.Slider {
id: slider
from: PipeWire.MinimalVolume
to: PipeWire.NormalVolume
value: sinks.defaultSink ? sinks.defaultSink.volume : 0
onMoved: if (sinks.defaultSink) sinks.defaultSink.volume = value
}
Repeater {
model: sinks
delegate: Text { text: Description + ": " + Math.round(Volume * 100) }
}
VolumeMonitor {}
}
)", QUrl("file:///audio-test.qml"));
QTRY_VERIFY_WITH_TIMEOUT(component.status() != QQmlComponent::Loading, 3000);
QVERIFY2(component.isReady(), qPrintable(component.errorString()));
QScopedPointer<QObject> item(component.create());
QVERIFY2(item, qPrintable(component.errorString()));
QObject *slider = item->property("slider").value<QObject *>();
QVERIFY(slider);
QCOMPARE(slider->property("to").toDouble(), 1.0);
slider->setProperty("value", 0.63);
QVERIFY(QMetaObject::invokeMethod(slider, "moved"));
QTRY_VERIFY(std::abs(m_backend->defaultNode(AudioObject::Sink)->volume() - 0.63) < 0.0001);
}
void AudioTest::settingsMultipleDevices()
{
const QString pagePath = QStringLiteral(AUDIO_SOURCE_ROOT "/settings/src/qml/Sound/Main.qml");
// The page lives in the settings checkout next to this one, which a
// source tarball does not have.
if (!QFile::exists(pagePath)) {
QSKIP("the settings checkout is not next to this one");
}
auto playback = std::make_unique<TestStream>(m_backend->core(), "settings-playback",
find("test-output-a")->properties().value(PW_KEY_OBJECT_SERIAL).toString().toUtf8());
QTRY_VERIFY(playback->streaming);
QTRY_VERIFY(find("settings-playback"));
QQmlEngine &engine = *m_engine;
QSignalSpy warnings(&engine, &QQmlEngine::warnings);
QQmlComponent host(&engine);
host.setData(R"(
import QtQuick
import QtQuick.Controls
import FishUI 1.0 as FishUI
FishUI.ApplicationWindow {
property int headerInset: 40
width: 820
height: 780
visible: true
header: Item { height: 40 }
background: Rectangle { color: "#f6f6f6" }
}
)", QUrl());
QScopedPointer<QObject> window(host.create());
QVERIFY2(window, qPrintable(host.errorString()));
engine.rootContext()->setContextProperty("rootWindow", window.data());
QQmlComponent component(&engine, QUrl::fromLocalFile(pagePath));
QTRY_VERIFY_WITH_TIMEOUT(component.status() != QQmlComponent::Loading, 3000);
QVERIFY2(component.isReady(), qPrintable(component.errorString()));
QScopedPointer<QObject> page(component.create());
QVERIFY2(page, qPrintable(component.errorString()));
auto *item = qobject_cast<QQuickItem *>(page.data());
QVERIFY(item);
item->setParentItem(qobject_cast<QQuickWindow *>(window.data())->contentItem());
item->setWidth(820);
item->setHeight(740);
AudioObject *first = find("test-output-a");
AudioObject *second = find("test-output-b");
AudioObject *source = find("test-input");
auto visualChild = [&](const QString &name) -> QObject * {
QList<QQuickItem *> pending {item};
while (!pending.isEmpty()) {
QQuickItem *candidate = pending.takeLast();
if (candidate->objectName() == name)
return candidate;
pending.append(candidate->childItems());
}
return nullptr;
};
auto child = [&](const QString &prefix, AudioObject *device) {
return visualChild(prefix + QString::number(device->index()));
};
QTRY_VERIFY(child("device-", first));
QTRY_VERIFY(child("device-", second));
QVERIFY(!child("device-", source));
QTRY_VERIFY(child("device-", first)->property("width").toDouble() > 700);
QObject *secondRow = child("device-", second);
QVERIFY(QMetaObject::invokeMethod(secondRow, "clicked"));
QTRY_COMPARE(m_backend->defaultNode(AudioObject::Sink), second);
QTRY_VERIFY(secondRow->property("checked").toBool());
QVERIFY(!child("device-", first)->property("checked").toBool());
QTRY_VERIFY(child("volume-", second));
QVERIFY(!child("volume-", first));
const double originalVolume = first->volume();
QObject *slider = child("volume-", second);
QVERIFY(slider);
slider->setProperty("value", 0.57);
QVERIFY(QMetaObject::invokeMethod(slider, "moved"));
QTRY_VERIFY(std::abs(second->volume() - 0.57) < 0.0001);
QVERIFY(std::abs(first->volume() - originalVolume) < 0.0001);
QVERIFY(!visualChild("mute"));
second->setMuted(true);
QTRY_COMPARE(slider->property("value").toDouble(), 0.0);
slider->setProperty("value", 0.4);
QVERIFY(QMetaObject::invokeMethod(slider, "moved"));
QTRY_VERIFY(!second->muted());
QTRY_VERIFY(std::abs(second->volume() - 0.4) < 0.0001);
slider->setProperty("value", 0.0);
QVERIFY(QMetaObject::invokeMethod(slider, "moved"));
QTRY_VERIFY(second->muted());
QVERIFY(!first->muted());
QVERIFY(std::abs(first->volume() - originalVolume) < 0.0001);
visualChild("tabs")->setProperty("currentIndex", 1);
QTRY_VERIFY(child("device-", source));
QVERIFY(!child("device-", first));
QTRY_VERIFY(child("volume-", source));
AudioObject *stream = find("settings-playback");
QVERIFY(!child("stream-", stream));
QVERIFY(!child("volume-", stream));
QList<QQuickItem *> pending {item};
while (!pending.isEmpty()) {
QQuickItem *candidate = pending.takeLast();
const QString text = candidate->property("text").toString();
QVERIFY(text != "Applications" && text != "Test sound");
pending.append(candidate->childItems());
}
QTest::qWait(100);
QCOMPARE(warnings.count(), 0);
if (qEnvironmentVariableIsSet("CUTEFISH_AUDIO_SCREENSHOT")) {
const QImage screenshot = qobject_cast<QQuickWindow *>(window.data())->grabWindow();
QVERIFY(!screenshot.isNull());
QVERIFY(screenshot.save(qEnvironmentVariable("CUTEFISH_AUDIO_SCREENSHOT")));
}
visualChild("tabs")->setProperty("currentIndex", 0);
const QString firstCardName = "device-" + QString::number(first->index());
playback.reset();
QTRY_VERIFY(!find("settings-playback"));
stopServers();
QTRY_VERIFY(!m_backend->isConnected());
QTRY_VERIFY(!visualChild(firstCardName));
startServers();
QTRY_VERIFY_WITH_TIMEOUT(find("test-output-a") && find("test-output-b"), 10000);
activateDevices();
QTRY_VERIFY_WITH_TIMEOUT(child("device-", find("test-output-a")), 10000);
QCOMPARE(warnings.count(), 0);
}
void AudioTest::reconnect()
{
AudioModel sinks(AudioObject::Sink, m_backend);
QAbstractItemModelTester tester(&sinks, QAbstractItemModelTester::FailureReportingMode::QtTest);
QPointer<AudioObject> previous = find("test-output-a");
VolumeMonitor monitor;
monitor.setTarget(previous);
stopServers();
QTRY_VERIFY_WITH_TIMEOUT(!m_backend->isConnected(), 5000);
QCOMPARE(sinks.rowCount(), 0);
QVERIFY(!m_backend->defaultNode(AudioObject::Sink));
QTRY_VERIFY(previous.isNull());
QVERIFY(!monitor.target());
startServers();
QTRY_VERIFY_WITH_TIMEOUT(m_backend->isConnected(), 10000);
QTRY_COMPARE_WITH_TIMEOUT(sinks.rowCount(), 2, 10000);
QTRY_VERIFY_WITH_TIMEOUT(m_backend->defaultNode(AudioObject::Sink), 10000);
activateDevices();
AudioObject *sink = find("test-output-a");
QTRY_VERIFY(sink->volumeWritable());
sink->setVolume(0.3);
QTRY_VERIFY(std::abs(sink->volume() - 0.3) < 0.0001);
}
void AudioTest::cleanupTestCase()
{
stopServers();
const QByteArray serverLog = m_server.readAll();
const QByteArray policyLog = m_policy.readAll();
if (m_failed) {
qWarning().noquote() << serverLog;
qWarning().noquote() << policyLog;
}
}
}
QTEST_MAIN(CutefishAudio::AudioTest)
#include "audio_test.moc"