fix: improve error output for failed shorebird preview on iOS (#2634)
This commit is contained in:
@@ -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
|
||||
)
|
||||
)
|
||||
)''',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user