|
| 1 | +#pragma once |
| 2 | + |
| 3 | +#include <gtest/gtest.h> |
| 4 | + |
| 5 | +#include <QBluetoothDeviceInfo> |
| 6 | +#include <QCoreApplication> |
| 7 | +#include <QDateTime> |
| 8 | +#include <QTimer> |
| 9 | + |
| 10 | +#include "Tools/testsettings.h" |
| 11 | +#include "devices/bluetooth.h" |
| 12 | +#include "devices/simulatedbike/simulatedbike.h" |
| 13 | +#include "qzsettings.h" |
| 14 | + |
| 15 | +namespace { |
| 16 | + |
| 17 | +/** |
| 18 | + * The bug this pins, in full, because it cost an install and a log to find and would be |
| 19 | + * invisible to every other test here. |
| 20 | + * |
| 21 | + * A real device is discovered from a radio callback. That happens once the event loop is |
| 22 | + * running, which is long after main() has built homeform and connected it to the two signals |
| 23 | + * that matter - deviceConnected(), which builds the session and clears the help label, and |
| 24 | + * bluetoothDeviceConnected(), which starts the template managers. |
| 25 | + * |
| 26 | + * The simulated bike is built in the bluetooth constructor instead, roughly a hundred lines |
| 27 | + * of main() before homeform exists. Announcing it there announced it to nobody: both signals |
| 28 | + * were emitted into an empty connection list, deviceConnected() was never emitted at all, and |
| 29 | + * the app came up with a working bike, a live DIRCON endpoint, a full metric stream - and no |
| 30 | + * tiles. The device log said it plainly: 219 metric points, and not one call to |
| 31 | + * homeform::deviceConnected(). |
| 32 | + * |
| 33 | + * So the announcement is deferred to a zero timer, and this test is the thing that says so. |
| 34 | + * It asserts what the constructor cannot: that by the time the event loop has turned once, |
| 35 | + * the signals homeform needs have actually been emitted. |
| 36 | + */ |
| 37 | +class SimulatedBikeAnnouncement : public ::testing::Test { |
| 38 | + protected: |
| 39 | + TestSettings testSettings{"Roberto Viola", "QZ Simulated Bike Test"}; |
| 40 | + |
| 41 | + void SetUp() override { |
| 42 | + // TestSettings does not activate itself, and an inactive one is a silent no-op: the |
| 43 | + // values land in its own file while the code under test goes on reading the default |
| 44 | + // one. The first version of this test failed for exactly that reason and looked like |
| 45 | + // a bug in the fix it was written to prove. |
| 46 | + testSettings.activate(); |
| 47 | + |
| 48 | + // Nothing here may touch a radio or a socket: this runs on a CI box with neither. |
| 49 | + testSettings.qsettings.setValue(QZSettings::virtual_device_enabled, false); |
| 50 | + testSettings.qsettings.setValue(QZSettings::virtual_device_bluetooth, false); |
| 51 | + testSettings.qsettings.setValue(QZSettings::dircon_yes, false); |
| 52 | + testSettings.qsettings.setValue(QZSettings::simulated_bike, true); |
| 53 | + testSettings.qsettings.setValue(QZSettings::simulated_bike_ride, QLatin1String("")); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Turn the event loop until the flag is set, or the deadline passes. Deliberately not |
| 58 | + * QSignalSpy: that lives in Qt's testlib, and this project does not link it - adding a Qt |
| 59 | + * module so one test can count signals would be paid for by every platform's build. |
| 60 | + */ |
| 61 | + static void spinUntil(const bool &flag, int ms = 2000) { |
| 62 | + const qint64 deadline = QDateTime::currentMSecsSinceEpoch() + ms; |
| 63 | + while (!flag && QDateTime::currentMSecsSinceEpoch() < deadline) |
| 64 | + QCoreApplication::processEvents(QEventLoop::AllEvents, 50); |
| 65 | + } |
| 66 | +}; |
| 67 | + |
| 68 | +TEST_F(SimulatedBikeAnnouncement, AnnouncesTheDeviceOnceTheEventLoopRuns) { |
| 69 | + bluetooth bt(false); |
| 70 | + int deviceConnected = 0; |
| 71 | + int bikeConnected = 0; |
| 72 | + QObject::connect(&bt, &bluetooth::deviceConnected, |
| 73 | + [&deviceConnected](QBluetoothDeviceInfo) { ++deviceConnected; }); |
| 74 | + QObject::connect(&bt, &bluetooth::bluetoothDeviceConnected, |
| 75 | + [&bikeConnected](bluetoothdevice *) { ++bikeConnected; }); |
| 76 | + |
| 77 | + // The device exists immediately - homeform::deviceConnected() returns early on a null |
| 78 | + // device, so being late with the object would break it just as thoroughly as being early |
| 79 | + // with the signal. |
| 80 | + ASSERT_NE(nullptr, bt.device()); |
| 81 | + EXPECT_NE(nullptr, dynamic_cast<simulatedbike *>(bt.device())); |
| 82 | + |
| 83 | + // ...but nothing has been announced yet, which is the whole point: a listener connected |
| 84 | + // after this constructor returns must still hear about it. |
| 85 | + EXPECT_EQ(0, deviceConnected); |
| 86 | + EXPECT_EQ(0, bikeConnected); |
| 87 | + |
| 88 | + bool announced = false; |
| 89 | + QObject::connect(&bt, &bluetooth::deviceConnected, [&announced](QBluetoothDeviceInfo) { announced = true; }); |
| 90 | + spinUntil(announced); |
| 91 | + |
| 92 | + EXPECT_EQ(1, deviceConnected) << "homeform would never build the session"; |
| 93 | + EXPECT_EQ(1, bikeConnected) << "the template managers would never start"; |
| 94 | +} |
| 95 | + |
| 96 | +TEST_F(SimulatedBikeAnnouncement, TheAnnouncedDeviceCarriesAUsableName) { |
| 97 | + bluetooth bt(false); |
| 98 | + bool announced = false; |
| 99 | + QBluetoothDeviceInfo info; |
| 100 | + QObject::connect(&bt, &bluetooth::deviceConnected, [&](QBluetoothDeviceInfo i) { |
| 101 | + info = i; |
| 102 | + announced = true; |
| 103 | + }); |
| 104 | + spinUntil(announced); |
| 105 | + |
| 106 | + ASSERT_TRUE(announced); |
| 107 | + // homeform passes this to deviceFound(), so an empty name is a blank device label. |
| 108 | + EXPECT_FALSE(info.name().isEmpty()); |
| 109 | + EXPECT_TRUE(info.isValid()); |
| 110 | +} |
| 111 | + |
| 112 | +TEST_F(SimulatedBikeAnnouncement, NoSimulatedBikeWhenTheSettingIsOff) { |
| 113 | + testSettings.qsettings.setValue(QZSettings::simulated_bike, false); |
| 114 | + |
| 115 | + // startDiscovery false, so this builds nothing and starts no scan - the point is only |
| 116 | + // that the simulated bike is not conjured up when it was not asked for. |
| 117 | + bluetooth bt(true, QLatin1String(""), false, false, 200, false, false, 4, 1.0, false); |
| 118 | + EXPECT_EQ(nullptr, bt.device()); |
| 119 | +} |
| 120 | + |
| 121 | +} // namespace |
0 commit comments