iOS: Move info parsing from devicectl to a function

And add a test to document what we expect from devicectl.

Change-Id: I395171bb5316c21b461a01dce5c9ec87d81fb0c4
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Eike Ziller
2024-01-29 15:55:18 +01:00
parent 0e531fa453
commit 15baf18911
4 changed files with 216 additions and 42 deletions

View File

@@ -5,9 +5,14 @@
#include "iostr.h"
#include <QJsonArray>
#include <QJsonDocument>
Utils::expected_str<QJsonValue> Ios::Internal::parseDevicectlResult(const QByteArray &rawOutput)
using namespace Utils;
namespace Ios::Internal {
expected_str<QJsonValue> parseDevicectlResult(const QByteArray &rawOutput)
{
// there can be crap (progress info) at front and/or end
const int firstCurly = rawOutput.indexOf('{');
@@ -18,7 +23,7 @@ Utils::expected_str<QJsonValue> Ios::Internal::parseDevicectlResult(const QByteA
auto jsonOutput = QJsonDocument::fromJson(rawOutput.sliced(start, end - start + 1), &parseError);
if (jsonOutput.isNull()) {
// parse error
return Utils::make_unexpected(
return make_unexpected(
Tr::tr("Failed to parse devicectl output: %1.").arg(parseError.errorString()));
}
const QJsonValue errorValue = jsonOutput["error"];
@@ -37,12 +42,45 @@ Utils::expected_str<QJsonValue> Ios::Internal::parseDevicectlResult(const QByteA
if (!v.isUndefined())
error += "\n" + v.toString();
}
return Utils::make_unexpected(error);
return make_unexpected(error);
}
const QJsonValue resultValue = jsonOutput["result"];
if (resultValue.isUndefined()) {
return Utils::make_unexpected(
Tr::tr("Failed to parse devicectl output: 'result' is missing"));
return make_unexpected(Tr::tr("Failed to parse devicectl output: 'result' is missing"));
}
return resultValue;
}
expected_str<QMap<QString, QString>> parseDeviceInfo(const QByteArray &rawOutput,
const QString &deviceUsbId)
{
const expected_str<QJsonValue> result = parseDevicectlResult(rawOutput);
if (!result)
return make_unexpected(result.error());
// find device
const QJsonArray deviceList = (*result)["devices"].toArray();
for (const QJsonValue &device : deviceList) {
const QString udid = device["hardwareProperties"]["udid"].toString();
// USB identifiers don't have dashes, but iOS device udids can. Remove.
if (QString(udid).remove('-') == deviceUsbId) {
// fill in the map that we use for the iostool data
QMap<QString, QString> info;
info[kDeviceName] = device["deviceProperties"]["name"].toString();
info[kDeveloperStatus] = QLatin1String(
device["deviceProperties"]["developerModeStatus"] == "enabled" ? vDevelopment
: vOff);
info[kDeviceConnected] = vYes; // that's the assumption
info[kOsVersion] = QLatin1String("%1 (%2)")
.arg(device["deviceProperties"]["osVersionNumber"].toString(),
device["deviceProperties"]["osBuildUpdate"].toString());
info[kCpuArchitecture] = device["hardwareProperties"]["cpuType"]["name"].toString();
info[kUniqueDeviceId] = udid;
return info;
}
}
// device not found, not handled by devicectl
// not translated, only internal logging
return make_unexpected(QLatin1String("Device is not handled by devicectl"));
}
} // namespace Ios::Internal

View File

@@ -9,6 +9,18 @@
namespace Ios::Internal {
const char kDeviceName[] = "deviceName";
const char kDeveloperStatus[] = "developerStatus";
const char kDeviceConnected[] = "deviceConnected";
const char kOsVersion[] = "osVersion";
const char kCpuArchitecture[] = "cpuArchitecture";
const char kUniqueDeviceId[] = "uniqueDeviceId";
const char vOff[] = "*off*";
const char vDevelopment[] = "Development";
const char vYes[] = "YES";
Utils::expected_str<QJsonValue> parseDevicectlResult(const QByteArray &rawOutput);
Utils::expected_str<QMap<QString, QString>> parseDeviceInfo(const QByteArray &rawOutput,
const QString &deviceUsbId);
} // namespace Ios::Internal

View File

@@ -3,6 +3,7 @@
#include "iosdevice.h"
#include "devicectlutils.h"
#include "iosconfigurations.h"
#include "iosconstants.h"
#include "iossimulator.h"
@@ -75,16 +76,6 @@ static QString CFStringRef2QString(CFStringRef s)
namespace Ios::Internal {
const char kDeviceName[] = "deviceName";
const char kDeveloperStatus[] = "developerStatus";
const char kDeviceConnected[] = "deviceConnected";
const char kOsVersion[] = "osVersion";
const char kCpuArchitecture[] = "cpuArchitecture";
const char kUniqueDeviceId[] = "uniqueDeviceId";
const char vOff[] = "*off*";
const char vDevelopment[] = "Development";
const char vYes[] = "YES";
const char kHandler[] = "Handler";
class IosDeviceInfoWidget : public IDeviceWidget
@@ -283,34 +274,14 @@ void IosDeviceManager::updateInfo(const QString &devId)
{"devicectl", "list", "devices", "--quiet", "--json-output", "-"}});
},
[this, devId](const Process &process) {
auto jsonOutput = QJsonDocument::fromJson(process.rawStdOut());
// find device
const QJsonArray deviceList = jsonOutput["result"]["devices"].toArray();
for (const QJsonValue &device : deviceList) {
const QString udid = device["hardwareProperties"]["udid"].toString();
// USB identifiers don't have dashes, but iOS device udids can. Remove.
if (QString(udid).remove('-') == devId) {
// fill in the map that we use for the iostool data
QMap<QString, QString> info;
info[kDeviceName] = device["deviceProperties"]["name"].toString();
info[kDeveloperStatus] = QLatin1String(
device["deviceProperties"]["developerModeStatus"] == "enabled"
? vDevelopment
: vOff);
info[kDeviceConnected] = vYes; // that's the assumption
info[kOsVersion]
= QLatin1String("%1 (%2)")
.arg(device["deviceProperties"]["osVersionNumber"].toString(),
device["deviceProperties"]["osBuildUpdate"].toString());
info[kCpuArchitecture]
= device["hardwareProperties"]["cpuType"]["name"].toString();
info[kUniqueDeviceId] = udid;
deviceInfo(devId, IosDevice::Handler::DeviceCtl, info);
return DoneResult::Success;
}
const expected_str<QMap<QString, QString>> result = parseDeviceInfo(process.rawStdOut(),
devId);
if (!result) {
qCDebug(detectLog) << result.error();
return DoneResult::Error;
}
// device not found, not handled by devicectl
return DoneResult::Error;
deviceInfo(devId, IosDevice::Handler::DeviceCtl, *result);
return DoneResult::Success;
},
CallDoneIf::Success);

View File

@@ -14,6 +14,9 @@ class tst_Devicectlutils : public QObject
private slots:
void parseError_data();
void parseError();
void parseDeviceInfo_data();
void parseDeviceInfo();
};
void tst_Devicectlutils::parseError_data()
@@ -152,6 +155,156 @@ void tst_Devicectlutils::parseError()
}
}
void tst_Devicectlutils::parseDeviceInfo_data()
{
QTest::addColumn<QByteArray>("data");
QTest::addColumn<QString>("usbId");
QTest::addColumn<QString>("error");
QTest::addColumn<QMap<QString, QString>>("info");
const QByteArray data(R"raw(
{
"info" : {
"arguments" : [
"devicectl",
"list",
"devices",
"--quiet",
"--json-output",
"-"
],
"commandType" : "devicectl.list.devices",
"environment" : {
"TERM" : "xterm-256color"
},
"jsonVersion" : 2,
"outcome" : "success",
"version" : "355.7.7"
},
"result" : {
"devices" : [
{
"capabilities" : [
{
"featureIdentifier" : "com.apple.coredevice.feature.connectdevice",
"name" : "Connect to Device"
},
{
"featureIdentifier" : "com.apple.coredevice.feature.acquireusageassertion",
"name" : "Acquire Usage Assertion"
},
{
"featureIdentifier" : "com.apple.coredevice.feature.unpairdevice",
"name" : "Unpair Device"
}
],
"connectionProperties" : {
"authenticationType" : "manualPairing",
"isMobileDeviceOnly" : false,
"lastConnectionDate" : "2024-01-29T08:49:25.179Z",
"pairingState" : "paired",
"potentialHostnames" : [
"00000000-0000000000000000.coredevice.local",
"00000000-0000-0000-0000-000000000000.coredevice.local"
],
"transportType" : "wired",
"tunnelState" : "disconnected",
"tunnelTransportProtocol" : "tcp"
},
"deviceProperties" : {
"bootedFromSnapshot" : true,
"bootedSnapshotName" : "com.apple.os.update-0000",
"ddiServicesAvailable" : false,
"developerModeStatus" : "enabled",
"hasInternalOSBuild" : false,
"name" : "Some iOS device",
"osBuildUpdate" : "21D50",
"osVersionNumber" : "17.3",
"rootFileSystemIsWritable" : false
},
"hardwareProperties" : {
"cpuType" : {
"name" : "arm64e",
"subType" : 2,
"type" : 16777228
},
"deviceType" : "iPad",
"ecid" : 0,
"hardwareModel" : "J211AP",
"internalStorageCapacity" : 64000000000,
"isProductionFused" : true,
"marketingName" : "iPad mini (5th generation)",
"platform" : "iOS",
"productType" : "iPad11,2",
"reality" : "physical",
"serialNumber" : "000000000000",
"supportedCPUTypes" : [
{
"name" : "arm64e",
"subType" : 2,
"type" : 16777228
},
{
"name" : "arm64",
"subType" : 0,
"type" : 16777228
},
{
"name" : "arm64",
"subType" : 1,
"type" : 16777228
},
{
"name" : "arm64_32",
"subType" : 1,
"type" : 33554444
}
],
"supportedDeviceFamilies" : [
1,
2
],
"thinningProductType" : "iPad11,2",
"udid" : "00000000-0000000000000000"
},
"identifier" : "00000000-0000-0000-0000-000000000000",
"visibilityClass" : "default"
}
]
}
})raw");
QTest::addRow("handled device")
<< data << QString("000000000000000000000000") << QString()
<< QMap<QString, QString>({{"cpuArchitecture", "arm64e"},
{"developerStatus", "Development"},
{"deviceConnected", "YES"},
{"deviceName", "Some iOS device"},
{"osVersion", "17.3 (21D50)"},
{"uniqueDeviceId", "00000000-0000000000000000"}});
QTest::addRow("unhandled device")
<< data << QString("000000000000000000000001")
<< QString("Device is not handled by devicectl") << QMap<QString, QString>({});
}
void tst_Devicectlutils::parseDeviceInfo()
{
using InfoMap = QMap<QString, QString>;
QFETCH(QByteArray, data);
QFETCH(QString, usbId);
QFETCH(QString, error);
QFETCH(InfoMap, info);
const Utils::expected_str<InfoMap> result = Ios::Internal::parseDeviceInfo(data, usbId);
if (error.isEmpty()) {
QVERIFY(result);
QCOMPARE(*result, info);
} else {
QVERIFY(!result);
QCOMPARE(result.error(), error);
}
}
QTEST_GUILESS_MAIN(tst_Devicectlutils)
#include "tst_devicectlutils.moc"