fix: improve error output for failed shorebird preview on iOS (#2634)

This commit is contained in:
Bryan Oltman
2024-11-20 12:58:30 -05:00
committed by GitHub
parent fe4cbbd8c0
commit bb1807c35e
6 changed files with 334 additions and 13 deletions
@@ -301,6 +301,8 @@ class Devicectl {
}
return rootError.userInfo.localizedFailureReason?.string ??
rootError.userInfo.localizedDescription?.string ??
rootError.userInfo.description?.string ??
'unknown failure reason';
}
}
@@ -1,5 +1,6 @@
// ignore_for_file: public_member_api_docs
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
@@ -10,8 +11,8 @@ part 'nserror.g.dart';
///
/// See https://developer.apple.com/documentation/foundation/nserror.
/// {@endtemplate}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class NSError {
@JsonSerializable(fieldRename: FieldRename.none)
class NSError extends Equatable {
/// {@macro nserror}
const NSError({
required this.code,
@@ -25,16 +26,37 @@ class NSError {
/// Creates an [NSError] from JSON.
static NSError fromJson(Json json) => _$NSErrorFromJson(json);
Json toJson() => _$NSErrorToJson(this);
@override
String toString() => '''
NSError(
code: $code,
domain: $domain,
userInfo: $userInfo
)''';
@override
List<Object> get props => [
code,
domain,
userInfo,
];
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class UserInfo {
@JsonSerializable(fieldRename: FieldRename.none)
class UserInfo extends Equatable {
const UserInfo({
this.description,
this.localizedDescription,
this.localizedFailureReason,
this.underlyingError,
});
@JsonKey(name: 'NSDescription')
final StringContainer? description;
@JsonKey(name: 'NSLocalizedDescription')
final StringContainer? localizedDescription;
@@ -44,24 +66,64 @@ class UserInfo {
@JsonKey(name: 'NSUnderlyingError')
final NSUnderlyingError? underlyingError;
static const nullInfo = UserInfo();
static UserInfo fromJson(Json json) => _$UserInfoFromJson(json);
Json toJson() => _$UserInfoToJson(this);
@override
String toString() => '''
UserInfo(
description: $description,
localizedDescription: $localizedDescription,
localizedFailureReason: $localizedFailureReason,
underlyingError: $underlyingError
)''';
@override
List<Object?> get props => [
description,
localizedDescription,
localizedFailureReason,
underlyingError,
];
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class StringContainer {
const StringContainer({required this.string});
@JsonSerializable(fieldRename: FieldRename.none)
class StringContainer extends Equatable {
const StringContainer(this.string);
final String string;
static StringContainer fromJson(Json json) => _$StringContainerFromJson(json);
Json toJson() => _$StringContainerToJson(this);
@override
String toString() => string;
@override
List<Object> get props => [string];
}
@JsonSerializable(createToJson: false, fieldRename: FieldRename.none)
class NSUnderlyingError {
@JsonSerializable(fieldRename: FieldRename.none)
class NSUnderlyingError extends Equatable {
const NSUnderlyingError({required this.error});
final NSError? error;
static NSUnderlyingError fromJson(Json json) =>
_$NSUnderlyingErrorFromJson(json);
Json toJson() => _$NSUnderlyingErrorToJson(this);
@override
String toString() => '''
NSUnderlyingError(
$error
)''';
@override
List<Object?> get props => [error];
}
@@ -22,11 +22,22 @@ NSError _$NSErrorFromJson(Map<String, dynamic> json) => $checkedCreate(
},
);
Map<String, dynamic> _$NSErrorToJson(NSError instance) => <String, dynamic>{
'code': instance.code,
'domain': instance.domain,
'userInfo': instance.userInfo.toJson(),
};
UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => $checkedCreate(
'UserInfo',
json,
($checkedConvert) {
final val = UserInfo(
description: $checkedConvert(
'NSDescription',
(v) => v == null
? null
: StringContainer.fromJson(v as Map<String, dynamic>)),
localizedDescription: $checkedConvert(
'NSLocalizedDescription',
(v) => v == null
@@ -46,24 +57,37 @@ UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => $checkedCreate(
return val;
},
fieldKeyMap: const {
'description': 'NSDescription',
'localizedDescription': 'NSLocalizedDescription',
'localizedFailureReason': 'NSLocalizedFailureReason',
'underlyingError': 'NSUnderlyingError'
},
);
Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{
'NSDescription': instance.description?.toJson(),
'NSLocalizedDescription': instance.localizedDescription?.toJson(),
'NSLocalizedFailureReason': instance.localizedFailureReason?.toJson(),
'NSUnderlyingError': instance.underlyingError?.toJson(),
};
StringContainer _$StringContainerFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'StringContainer',
json,
($checkedConvert) {
final val = StringContainer(
string: $checkedConvert('string', (v) => v as String),
$checkedConvert('string', (v) => v as String),
);
return val;
},
);
Map<String, dynamic> _$StringContainerToJson(StringContainer instance) =>
<String, dynamic>{
'string': instance.string,
};
NSUnderlyingError _$NSUnderlyingErrorFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'NSUnderlyingError',
@@ -79,3 +103,8 @@ NSUnderlyingError _$NSUnderlyingErrorFromJson(Map<String, dynamic> json) =>
return val;
},
);
Map<String, dynamic> _$NSUnderlyingErrorToJson(NSUnderlyingError instance) =>
<String, dynamic>{
'error': instance.error?.toJson(),
};
@@ -0,0 +1,53 @@
{
"error": {
"code": 1,
"domain": "com.apple.CoreDevice.ControlChannelConnectionError",
"userInfo": {
"NSLocalizedDescription": {
"string": "Internal logic error: Connection was invalidated"
},
"NSUnderlyingError": {
"error": {
"code": 0,
"domain": "com.apple.CoreDevice.ControlChannelConnectionError",
"userInfo": {
"NSLocalizedDescription": {
"string": "Transport error"
},
"NSUnderlyingError": {
"error": {
"code": 60,
"domain": "Network.NWError",
"userInfo": {
"NSDescription": {
"string": "Operation timed out"
}
}
}
}
}
}
}
}
},
"info": {
"arguments": [
"devicectl",
"device",
"install",
"app",
"--device",
"00008110-000265A10E92801E",
"/Users/bryanoltman/shorebirdtech/_shorebird/shorebird/bin/cache/previews/3c2ca65a-6401-491a-8942-c9921ba43cec/ios_1.0.0+2_508316.app",
"--json-output",
"/var/folders/64/dj6krpq1093dmx08dy4r1cwh0000gn/T/AKL8Wl/devicectl.out.json"
],
"commandType": "devicectl.device.install.app",
"environment": {
"TERM": "xterm-256color"
},
"jsonVersion": 2,
"outcome": "failed",
"version": "397.24"
}
}
@@ -398,9 +398,9 @@ void main() {
deviceListJsonOutput = File(
'$fixturesPath/device_list_success.json',
).readAsStringSync();
// I was not able to get this command to fail, so just use an empty
// string as the output.
installJsonOutput = '';
installJsonOutput = File(
'$fixturesPath/install_failure.json',
).readAsStringSync();
});
test('returns exit code 70 ', () async {
@@ -413,6 +413,10 @@ void main() {
),
equals(ExitCode.software.code),
);
verify(
() => progress.fail(any(that: contains('Operation timed out'))),
).called(1);
});
});
@@ -437,6 +441,16 @@ void main() {
),
equals(ExitCode.software.code),
);
verify(
() => progress.fail(
any(
that: contains(
'''Unable to launch dev.shorebird.ios-test because the device was not, or could not be, unlocked.''',
),
),
),
).called(1);
});
});
@@ -0,0 +1,161 @@
import 'package:shorebird_cli/src/executables/devicectl/nserror.dart';
import 'package:test/test.dart';
void main() {
group(NSError, () {
test('(de)serialization', () {
const error = NSError(
code: 1,
domain: 'com.example',
userInfo: UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
),
);
final json = error.toJson();
final decoded = NSError.fromJson(json);
expect(decoded, error);
});
group('toString', () {
test('returns a string representation of the error', () {
const error = NSError(
code: 1,
domain: 'com.example',
userInfo: UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
),
);
expect(
error.toString(),
'''
NSError(
code: 1,
domain: com.example,
userInfo: UserInfo(
description: description,
localizedDescription: localizedDescription,
localizedFailureReason: localizedFailureReason,
underlyingError: null
)
)''',
);
});
});
});
group(UserInfo, () {
test('(de)serialization', () {
const userInfo = UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
);
final json = userInfo.toJson();
final decoded = UserInfo.fromJson(json);
expect(decoded, userInfo);
});
group('toString', () {
test('returns a string representation of the user info', () {
const userInfo = UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
);
expect(
userInfo.toString(),
'''
UserInfo(
description: description,
localizedDescription: localizedDescription,
localizedFailureReason: localizedFailureReason,
underlyingError: null
)''',
);
});
});
});
group(StringContainer, () {
test('(de)serialization', () {
const stringContainer = StringContainer('string');
final json = stringContainer.toJson();
final decoded = StringContainer.fromJson(json);
expect(decoded, stringContainer);
});
group('toString', () {
test('returns a string representation of the string container', () {
const stringContainer = StringContainer('string');
expect(stringContainer.toString(), 'string');
});
});
});
group(NSUnderlyingError, () {
test('(de)serialization', () {
const underlyingError = NSUnderlyingError(
error: NSError(
code: 1,
domain: 'com.example',
userInfo: UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
),
),
);
final json = underlyingError.toJson();
final decoded = NSUnderlyingError.fromJson(json);
expect(decoded, underlyingError);
});
group('toString', () {
test('returns a string representation of the underlying error', () {
const underlyingError = NSUnderlyingError(
error: NSError(
code: 1,
domain: 'com.example',
userInfo: UserInfo(
description: StringContainer('description'),
localizedDescription: StringContainer('localizedDescription'),
localizedFailureReason: StringContainer('localizedFailureReason'),
),
),
);
expect(
underlyingError.toString(),
'''
NSUnderlyingError(
NSError(
code: 1,
domain: com.example,
userInfo: UserInfo(
description: description,
localizedDescription: localizedDescription,
localizedFailureReason: localizedFailureReason,
underlyingError: null
)
)
)''',
);
});
});
});
}