1 | /* |
2 | * SPDX-FileCopyrightText: 2014-2015 David Rosca <nowrep@gmail.com> |
3 | * |
4 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
5 | */ |
6 | |
7 | #include "adaptertest.h" |
8 | #include "autotests.h" |
9 | #include "device.h" |
10 | #include "initmanagerjob.h" |
11 | #include "pendingcall.h" |
12 | |
13 | #include <QSignalSpy> |
14 | #include <QTest> |
15 | |
16 | namespace BluezQt |
17 | { |
18 | extern void bluezqt_initFakeBluezTestRun(); |
19 | } |
20 | |
21 | using namespace BluezQt; |
22 | |
23 | AdapterTest::AdapterTest() |
24 | : m_manager(nullptr) |
25 | { |
26 | Autotests::registerMetatypes(); |
27 | } |
28 | |
29 | void AdapterTest::initTestCase() |
30 | { |
31 | QDBusConnection connection = QDBusConnection::sessionBus(); |
32 | QString service = QStringLiteral("org.kde.bluezqt.fakebluez" ); |
33 | |
34 | bluezqt_initFakeBluezTestRun(); |
35 | |
36 | FakeBluez::start(); |
37 | FakeBluez::runTest(QStringLiteral("bluez-standard" )); |
38 | |
39 | // Create adapters |
40 | QDBusObjectPath adapter1path = QDBusObjectPath(QStringLiteral("/org/bluez/hci0" )); |
41 | QVariantMap adapterProps; |
42 | adapterProps[QStringLiteral("Path" )] = QVariant::fromValue(adapter1path); |
43 | adapterProps[QStringLiteral("Address" )] = QStringLiteral("1C:E5:C3:BC:94:7E" ); |
44 | adapterProps[QStringLiteral("Name" )] = QStringLiteral("TestAdapter" ); |
45 | adapterProps[QStringLiteral("Alias" )] = QStringLiteral("TestAlias" ); |
46 | adapterProps[QStringLiteral("Class" )] = QVariant::fromValue(quint32(101)); |
47 | adapterProps[QStringLiteral("Powered" )] = false; |
48 | adapterProps[QStringLiteral("Discoverable" )] = false; |
49 | adapterProps[QStringLiteral("Pairable" )] = false; |
50 | adapterProps[QStringLiteral("PairableTimeout" )] = QVariant::fromValue(quint32(0)); |
51 | adapterProps[QStringLiteral("DiscoverableTimeout" )] = QVariant::fromValue(quint32(0)); |
52 | adapterProps[QStringLiteral("Discovering" )] = false; |
53 | adapterProps[QStringLiteral("UUIDs" )] = QStringList(QStringLiteral("00001200-0000-1000-8000-00805f9b34fb" )); |
54 | adapterProps[QStringLiteral("Modalias" )] = QStringLiteral("usb:v2D6Bp1236d0215" ); |
55 | FakeBluez::runAction(QStringLiteral("devicemanager" ), QStringLiteral("create-adapter" ), adapterProps); |
56 | |
57 | QDBusObjectPath adapter2path = QDBusObjectPath(QStringLiteral("/org/bluez/hci1" )); |
58 | adapterProps[QStringLiteral("Path" )] = QVariant::fromValue(adapter2path); |
59 | adapterProps[QStringLiteral("Address" )] = QStringLiteral("2E:3A:C3:BC:85:7C" ); |
60 | adapterProps[QStringLiteral("Name" )] = QStringLiteral("TestAdapter2" ); |
61 | adapterProps[QStringLiteral("Alias" )] = QStringLiteral("TestAlias2" ); |
62 | adapterProps[QStringLiteral("Class" )] = QVariant::fromValue(quint32(201)); |
63 | adapterProps[QStringLiteral("Powered" )] = true; |
64 | adapterProps[QStringLiteral("Discoverable" )] = true; |
65 | adapterProps[QStringLiteral("Pairable" )] = true; |
66 | adapterProps[QStringLiteral("PairableTimeout" )] = QVariant::fromValue(quint32(150)); |
67 | adapterProps[QStringLiteral("DiscoverableTimeout" )] = QVariant::fromValue(quint32(120)); |
68 | adapterProps[QStringLiteral("Discovering" )] = false; |
69 | adapterProps[QStringLiteral("UUIDs" )] = QStringList(QStringLiteral("0000110c-0000-1000-8000-00805f9b34fb" )); |
70 | adapterProps[QStringLiteral("Modalias" )] = QStringLiteral("usb:v1D3Bp1134d0214" ); |
71 | FakeBluez::runAction(QStringLiteral("devicemanager" ), QStringLiteral("create-adapter" ), adapterProps); |
72 | |
73 | // Create devices |
74 | QVariantMap deviceProps; |
75 | deviceProps[QStringLiteral("Path" )] = QVariant::fromValue(QDBusObjectPath("/org/bluez/hci0/dev_40_79_6A_0C_39_75" )); |
76 | deviceProps[QStringLiteral("Adapter" )] = QVariant::fromValue(adapter1path); |
77 | deviceProps[QStringLiteral("Address" )] = QStringLiteral("40:79:6A:0C:39:75" ); |
78 | deviceProps[QStringLiteral("Name" )] = QStringLiteral("TestDevice" ); |
79 | FakeBluez::runAction(QStringLiteral("devicemanager" ), QStringLiteral("create-device" ), deviceProps); |
80 | |
81 | deviceProps[QStringLiteral("Path" )] = QVariant::fromValue(QDBusObjectPath("/org/bluez/hci1/dev_50_79_6A_0C_39_75" )); |
82 | deviceProps[QStringLiteral("Adapter" )] = QVariant::fromValue(adapter2path); |
83 | deviceProps[QStringLiteral("Address" )] = QStringLiteral("50:79:6A:0C:39:75" ); |
84 | deviceProps[QStringLiteral("Name" )] = QStringLiteral("TestDevice2" ); |
85 | FakeBluez::runAction(QStringLiteral("devicemanager" ), QStringLiteral("create-device" ), deviceProps); |
86 | |
87 | m_manager = new Manager(); |
88 | InitManagerJob *initJob = m_manager->init(); |
89 | initJob->exec(); |
90 | QVERIFY(!initJob->error()); |
91 | |
92 | for (AdapterPtr adapter : m_manager->adapters()) { |
93 | QVERIFY(!adapter->ubi().isEmpty()); |
94 | |
95 | AdapterUnit u; |
96 | u.adapter = adapter; |
97 | u.dbusAdapter = new org::bluez::Adapter1(service, adapter->ubi(), connection, this); |
98 | u.dbusProperties = new org::freedesktop::DBus::Properties(service, adapter->ubi(), connection, this); |
99 | m_units.append(u); |
100 | } |
101 | |
102 | QCOMPARE(m_manager->adapters().count(), 2); |
103 | } |
104 | |
105 | void AdapterTest::cleanupTestCase() |
106 | { |
107 | for (const AdapterUnit &unit : m_units) { |
108 | delete unit.dbusAdapter; |
109 | delete unit.dbusProperties; |
110 | } |
111 | |
112 | delete m_manager; |
113 | |
114 | FakeBluez::stop(); |
115 | } |
116 | |
117 | static void compareUuids(const QStringList &actual, const QStringList &expected) |
118 | { |
119 | QCOMPARE(actual.size(), expected.size()); |
120 | |
121 | for (int i = 0; i < actual.size(); ++i) { |
122 | QCOMPARE(actual.at(i).toUpper(), expected.at(i).toUpper()); |
123 | } |
124 | } |
125 | |
126 | void AdapterTest::getPropertiesTest() |
127 | { |
128 | for (const AdapterUnit &unit : m_units) { |
129 | QCOMPARE(unit.adapter->ubi(), unit.dbusAdapter->path()); |
130 | QCOMPARE(unit.adapter->address(), unit.dbusAdapter->address()); |
131 | QCOMPARE(unit.adapter->name(), unit.dbusAdapter->alias()); |
132 | QCOMPARE(unit.adapter->systemName(), unit.dbusAdapter->name()); |
133 | QCOMPARE(unit.adapter->adapterClass(), unit.dbusAdapter->adapterClass()); |
134 | QCOMPARE(unit.adapter->isPowered(), unit.dbusAdapter->powered()); |
135 | QCOMPARE(unit.adapter->isDiscoverable(), unit.dbusAdapter->discoverable()); |
136 | QCOMPARE(unit.adapter->discoverableTimeout(), unit.dbusAdapter->discoverableTimeout()); |
137 | QCOMPARE(unit.adapter->isPairable(), unit.dbusAdapter->pairable()); |
138 | QCOMPARE(unit.adapter->pairableTimeout(), unit.dbusAdapter->pairableTimeout()); |
139 | QCOMPARE(unit.adapter->isDiscovering(), unit.dbusAdapter->discovering()); |
140 | QCOMPARE(unit.adapter->modalias(), unit.dbusAdapter->modalias()); |
141 | |
142 | compareUuids(unit.adapter->uuids(), unit.dbusAdapter->uUIDs()); |
143 | } |
144 | } |
145 | |
146 | void AdapterTest::setAliasTest() |
147 | { |
148 | for (const AdapterUnit &unit : m_units) { |
149 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(nameChanged(QString))); |
150 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
151 | |
152 | QString value = unit.adapter->name() + QLatin1String("_tst_alias" ); |
153 | |
154 | unit.adapter->setName(value); |
155 | QTRY_COMPARE(adapterSpy.count(), 1); |
156 | |
157 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
158 | QCOMPARE(adapterArguments.at(0).toString(), value); |
159 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Alias" ), value); |
160 | |
161 | QCOMPARE(unit.adapter->name(), value); |
162 | QCOMPARE(unit.dbusAdapter->alias(), value); |
163 | } |
164 | } |
165 | |
166 | void AdapterTest::setPoweredTest() |
167 | { |
168 | for (const AdapterUnit &unit : m_units) { |
169 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(poweredChanged(bool))); |
170 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
171 | |
172 | bool value = !unit.adapter->isPowered(); |
173 | |
174 | unit.adapter->setPowered(value); |
175 | QTRY_COMPARE(adapterSpy.count(), 1); |
176 | |
177 | QVERIFY(dbusSpy.count() >= 1); |
178 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Powered" ), value); |
179 | |
180 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
181 | QCOMPARE(adapterArguments.at(0).toBool(), value); |
182 | |
183 | QCOMPARE(unit.adapter->isPowered(), value); |
184 | QCOMPARE(unit.dbusAdapter->powered(), value); |
185 | } |
186 | } |
187 | |
188 | void AdapterTest::setDiscoverableTest() |
189 | { |
190 | // Discoverable cannot be changed when Adapter is off |
191 | |
192 | for (const AdapterUnit &unit : m_units) { |
193 | unit.adapter->setPowered(true)->waitForFinished(); |
194 | |
195 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(discoverableChanged(bool))); |
196 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
197 | |
198 | bool value = !unit.adapter->isDiscoverable(); |
199 | |
200 | unit.adapter->setDiscoverable(value); |
201 | QTRY_COMPARE(adapterSpy.count(), 1); |
202 | |
203 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
204 | QCOMPARE(adapterArguments.at(0).toBool(), value); |
205 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Discoverable" ), value); |
206 | |
207 | QCOMPARE(unit.adapter->isDiscoverable(), value); |
208 | QCOMPARE(unit.dbusAdapter->discoverable(), value); |
209 | } |
210 | } |
211 | |
212 | void AdapterTest::setDiscoverableTimeoutTest() |
213 | { |
214 | for (const AdapterUnit &unit : m_units) { |
215 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(discoverableTimeoutChanged(quint32))); |
216 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
217 | |
218 | quint32 value = unit.adapter->discoverableTimeout() + 1; |
219 | |
220 | unit.adapter->setDiscoverableTimeout(value); |
221 | QTRY_COMPARE(adapterSpy.count(), 1); |
222 | |
223 | QVERIFY(dbusSpy.count() >= 1); |
224 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("DiscoverableTimeout" ), value); |
225 | |
226 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
227 | QCOMPARE(adapterArguments.at(0).toUInt(), value); |
228 | |
229 | QCOMPARE(unit.adapter->discoverableTimeout(), value); |
230 | QCOMPARE(unit.dbusAdapter->discoverableTimeout(), value); |
231 | } |
232 | } |
233 | |
234 | void AdapterTest::setPairableTest() |
235 | { |
236 | for (const AdapterUnit &unit : m_units) { |
237 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(pairableChanged(bool))); |
238 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
239 | |
240 | bool value = !unit.adapter->isPairable(); |
241 | |
242 | unit.adapter->setPairable(value); |
243 | QTRY_COMPARE(adapterSpy.count(), 1); |
244 | |
245 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
246 | QCOMPARE(adapterArguments.at(0).toBool(), value); |
247 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Pairable" ), value); |
248 | |
249 | QCOMPARE(unit.adapter->isPairable(), value); |
250 | QCOMPARE(unit.dbusAdapter->pairable(), value); |
251 | } |
252 | } |
253 | |
254 | void AdapterTest::setPairableTimeoutTest() |
255 | { |
256 | for (const AdapterUnit &unit : m_units) { |
257 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(pairableTimeoutChanged(quint32))); |
258 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
259 | |
260 | quint32 value = unit.adapter->pairableTimeout() + 1; |
261 | |
262 | unit.adapter->setPairableTimeout(value); |
263 | QTRY_COMPARE(adapterSpy.count(), 1); |
264 | |
265 | QVERIFY(dbusSpy.count() >= 1); |
266 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("PairableTimeout" ), value); |
267 | |
268 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
269 | QCOMPARE(adapterArguments.at(0).toUInt(), value); |
270 | |
271 | QCOMPARE(unit.adapter->pairableTimeout(), value); |
272 | QCOMPARE(unit.dbusAdapter->pairableTimeout(), value); |
273 | } |
274 | } |
275 | |
276 | void AdapterTest::discoveryTest() |
277 | { |
278 | // Discovery needs Adapter powered on |
279 | |
280 | for (const AdapterUnit &unit : m_units) { |
281 | // Make sure the Adapter is powered on and not discovering |
282 | unit.adapter->setPowered(true)->waitForFinished(); |
283 | if (unit.adapter->isDiscovering()) { |
284 | unit.adapter->stopDiscovery()->waitForFinished(); |
285 | } |
286 | |
287 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(discoveringChanged(bool))); |
288 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
289 | |
290 | // Start Discovery |
291 | unit.adapter->startDiscovery(); |
292 | QTRY_COMPARE(adapterSpy.count(), 1); |
293 | |
294 | QVERIFY(dbusSpy.count() >= 1); |
295 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Discovering" ), true); |
296 | |
297 | QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
298 | QCOMPARE(adapterArguments.at(0).toBool(), true); |
299 | |
300 | QCOMPARE(unit.adapter->isDiscovering(), true); |
301 | QCOMPARE(unit.dbusAdapter->discovering(), true); |
302 | |
303 | adapterSpy.clear(); |
304 | dbusSpy.clear(); |
305 | |
306 | // Stop Discovery |
307 | unit.adapter->stopDiscovery(); |
308 | QTRY_COMPARE(adapterSpy.count(), 1); |
309 | |
310 | QVERIFY(dbusSpy.count() >= 1); |
311 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Discovering" ), false); |
312 | |
313 | adapterArguments = adapterSpy.takeFirst(); |
314 | QCOMPARE(adapterArguments.at(0).toBool(), false); |
315 | |
316 | QCOMPARE(unit.adapter->isDiscovering(), false); |
317 | QCOMPARE(unit.dbusAdapter->discovering(), false); |
318 | } |
319 | } |
320 | |
321 | void AdapterTest::removeDeviceTest() |
322 | { |
323 | for (const AdapterUnit &unit : m_units) { |
324 | while (!unit.adapter->devices().isEmpty()) { |
325 | DevicePtr device = unit.adapter->devices().first(); |
326 | |
327 | QSignalSpy managerSpy(m_manager, SIGNAL(deviceRemoved(DevicePtr))); |
328 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(deviceRemoved(DevicePtr))); |
329 | QSignalSpy deviceSpy(device.data(), SIGNAL(deviceRemoved(DevicePtr))); |
330 | |
331 | unit.adapter->removeDevice(device); |
332 | |
333 | QTRY_COMPARE(managerSpy.count(), 1); |
334 | QTRY_COMPARE(adapterSpy.count(), 1); |
335 | QTRY_COMPARE(deviceSpy.count(), 1); |
336 | |
337 | QCOMPARE(managerSpy.at(0).at(0).value<DevicePtr>(), device); |
338 | QCOMPARE(adapterSpy.at(0).at(0).value<DevicePtr>(), device); |
339 | QCOMPARE(deviceSpy.at(0).at(0).value<DevicePtr>(), device); |
340 | } |
341 | } |
342 | } |
343 | |
344 | static bool isFilterValid(const QStringList &filters, const QVariantMap &filter) |
345 | { |
346 | auto i = filter.constBegin(); |
347 | while (i != filter.constEnd()) { |
348 | if (!filters.contains(i.key())) { |
349 | return false; |
350 | } |
351 | i++; |
352 | } |
353 | return true; |
354 | } |
355 | |
356 | void AdapterTest::discoveryFilterTest_data() { |
357 | QTest::addColumn<QVariantMap>("filter" ); |
358 | QTest::addColumn<bool>("isValid" ); |
359 | QTest::addColumn<bool>("shouldBeDiscoverable" ); |
360 | |
361 | |
362 | QTest::newRow("valid-non-discoverable" ) << QVariantMap({ |
363 | { QStringLiteral("UUIDs" ), QStringList() }, |
364 | { QStringLiteral("RSSI" ), QVariant::fromValue(qint16(-100)) }, |
365 | { QStringLiteral("Transport" ), QStringLiteral("auto" ) }, |
366 | { QStringLiteral("DuplicateData" ), true }, |
367 | { QStringLiteral("Discoverable" ), false }, |
368 | { QStringLiteral("Pattern" ), QLatin1String("" )} |
369 | }) << true << false; |
370 | |
371 | QTest::newRow("valid-discoverable" ) << QVariantMap({ |
372 | { QStringLiteral("UUIDs" ), QStringList() }, |
373 | { QStringLiteral("RSSI" ), QVariant::fromValue(qint16(-100)) }, |
374 | { QStringLiteral("Transport" ), QStringLiteral("auto" ) }, |
375 | { QStringLiteral("DuplicateData" ), true }, |
376 | { QStringLiteral("Discoverable" ), true }, |
377 | { QStringLiteral("Pattern" ), QLatin1String("" )} |
378 | }) << true << true; |
379 | |
380 | QTest::newRow("invalid" ) << QVariantMap({ |
381 | { QStringLiteral("SomeKey" ), QStringList() } |
382 | }) << false << false; |
383 | } |
384 | |
385 | void AdapterTest::discoveryFilterTest() |
386 | { |
387 | QFETCH(QVariantMap, filter); |
388 | QFETCH(bool, isValid); |
389 | QFETCH(bool, shouldBeDiscoverable); |
390 | |
391 | for (const AdapterUnit &unit : m_units) { |
392 | |
393 | // Get available discovery filters |
394 | PendingCall* p = unit.adapter->getDiscoveryFilters(); p->waitForFinished(); |
395 | Q_ASSERT(p->isFinished()); |
396 | QCOMPARE(p->error(), PendingCall::NoError); |
397 | const QStringList filters = p->value().toStringList(); |
398 | |
399 | // Verify filter |
400 | QCOMPARE(isFilterValid(filters, filter), isValid); |
401 | |
402 | // Make sure adapter is powered and not discoverable |
403 | unit.adapter->setPowered(true)->waitForFinished(); |
404 | unit.adapter->setDiscoverable(false); |
405 | QTRY_COMPARE(unit.adapter->isDiscoverable(), false); |
406 | QTRY_COMPARE(unit.dbusAdapter->discoverable(), false); |
407 | |
408 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(discoverableChanged(bool))); |
409 | QSignalSpy dbusSpy(unit.dbusProperties, SIGNAL(PropertiesChanged(QString, QVariantMap, QStringList))); |
410 | |
411 | // Set discovery filter |
412 | unit.adapter->setDiscoveryFilter(filter); |
413 | |
414 | if (shouldBeDiscoverable) { |
415 | // Check if adapter became discoverable |
416 | QTRY_COMPARE(adapterSpy.count(), 1); |
417 | |
418 | const QList<QVariant> adapterArguments = adapterSpy.takeFirst(); |
419 | QCOMPARE(adapterArguments.at(0).toBool(), true); |
420 | Autotests::verifyPropertiesChangedSignal(dbusSpy, QStringLiteral("Discoverable" ), true); |
421 | |
422 | QCOMPARE(unit.adapter->isDiscoverable(), true); |
423 | QCOMPARE(unit.dbusAdapter->discoverable(), true); |
424 | } |
425 | } |
426 | } |
427 | |
428 | void AdapterTest::adapterRemovedTest() |
429 | { |
430 | for (const AdapterUnit &unit : m_units) { |
431 | QSignalSpy managerSpy(m_manager, SIGNAL(adapterRemoved(AdapterPtr))); |
432 | QSignalSpy adapterSpy(unit.adapter.data(), SIGNAL(adapterRemoved(AdapterPtr))); |
433 | |
434 | QVariantMap properties; |
435 | properties[QStringLiteral("Path" )] = QVariant::fromValue(QDBusObjectPath(unit.adapter->ubi())); |
436 | FakeBluez::runAction(QStringLiteral("devicemanager" ), QStringLiteral("remove-adapter" ), properties); |
437 | |
438 | QTRY_COMPARE(managerSpy.count(), 1); |
439 | QTRY_COMPARE(adapterSpy.count(), 1); |
440 | |
441 | QCOMPARE(managerSpy.at(0).at(0).value<AdapterPtr>(), unit.adapter); |
442 | QCOMPARE(adapterSpy.at(0).at(0).value<AdapterPtr>(), unit.adapter); |
443 | } |
444 | } |
445 | |
446 | QTEST_MAIN(AdapterTest) |
447 | |
448 | #include "moc_adaptertest.cpp" |
449 | |