From 081a6e2a5fb680c2b09cca4cc73d2b1f562d6e71 Mon Sep 17 00:00:00 2001 From: Bryan Oltman Date: Tue, 11 Feb 2025 15:36:13 -0500 Subject: [PATCH] feat: better handle archiving failures (#2880) --- .../artifact_build_exception.dart | 29 +-- .../artifact_build_exception_test.dart | 170 +++++++++++------- 2 files changed, 122 insertions(+), 77 deletions(-) diff --git a/packages/shorebird_cli/lib/src/artifact_builder/artifact_build_exception.dart b/packages/shorebird_cli/lib/src/artifact_builder/artifact_build_exception.dart index 5af4bb20..384a5fee 100644 --- a/packages/shorebird_cli/lib/src/artifact_builder/artifact_build_exception.dart +++ b/packages/shorebird_cli/lib/src/artifact_builder/artifact_build_exception.dart @@ -54,14 +54,22 @@ class ArtifactBuildException implements Exception { late final String? fixRecommendation; List _errorMessageFromOutput(List output) { - final failureHeader = - RegExp(r'.*FAILURE: Build failed with an exception\..*'); - // This precedes a stack trace - final stackTraceHeader = RegExp(r'.*\* Exception is:.*'); + final failureHeaders = [ + RegExp(r'.*FAILURE: Build failed with an exception\..*'), + RegExp(r'.*Error \(Xcode\).*'), + ]; - // This precedes recommendations that are not applicable to us (e.g., "Get - // more help at https://help.gradle.org.") - final suggestionsHeader = RegExp(r'.*\* Try:.*'); + final failureFooters = [ + // This precedes a stack trace + RegExp(r'.*\* Exception is:.*'), + + // This precedes recommendations that are not applicable to us (e.g., "Get + // more help at https://help.gradle.org.") + RegExp(r'.*\* Try:.*'), + + // This precedes a stacktrace in the case of an Xcode error + RegExp('Encountered error while archiving for device'), + ]; String trimLine(String line) { return line.trim().replaceAll(RegExp(r'^\[.*\]'), ''); @@ -70,10 +78,9 @@ class ArtifactBuildException implements Exception { var inErrorOutput = false; final ret = []; for (final line in output) { - if (failureHeader.hasMatch(line)) { + if (failureHeaders.any((r) => r.hasMatch(line))) { inErrorOutput = true; - } else if (stackTraceHeader.hasMatch(line) || - suggestionsHeader.hasMatch(line)) { + } else if (failureFooters.any((r) => r.hasMatch(line))) { inErrorOutput = false; } @@ -93,6 +100,8 @@ class ArtifactBuildException implements Exception { [RegExp("Execution failed for task ':app:signReleaseBundle'")], _missingKeystoreFixSuggestion, ), + // Note: Xcode archive failures include suggestions from the flutter tool, + // so we don't need to duplicate them here. }; String? _recommendationFromOutput(List output) { diff --git a/packages/shorebird_cli/test/src/artifact_builder/artifact_build_exception_test.dart b/packages/shorebird_cli/test/src/artifact_builder/artifact_build_exception_test.dart index ca7b6a58..9802227b 100644 --- a/packages/shorebird_cli/test/src/artifact_builder/artifact_build_exception_test.dart +++ b/packages/shorebird_cli/test/src/artifact_builder/artifact_build_exception_test.dart @@ -34,83 +34,119 @@ void main() { }); }); - group('when an error is recognized but no fix recommendation is found', () { - test('has a Flutter error and no fix recommendation', () { - final exception = ArtifactBuildException( - 'message', - stderr: [ - 'some stderr output', - 'FAILURE: Build failed with an exception.', - '* Exception is:', - 'some stack trace', - '* Try:', - 'some recommendation', - ], - stdout: ['some stdout output'], - ); - expect( - exception.flutterError, - equals('FAILURE: Build failed with an exception.'), - ); - expect(exception.fixRecommendation, isNull); + group('gradle', () { + group('when an error is recognized but no fix recommendation is found', + () { + test('has a Flutter error and no fix recommendation', () { + final exception = ArtifactBuildException( + 'message', + stderr: [ + 'some stderr output', + 'FAILURE: Build failed with an exception.', + '* Exception is:', + 'some stack trace', + '* Try:', + 'some recommendation', + ], + stdout: ['some stdout output'], + ); + expect( + exception.flutterError, + equals('FAILURE: Build failed with an exception.'), + ); + expect(exception.fixRecommendation, isNull); + }); }); - }); - group('when a known error is recognized and a fix recommendation is found', - () { - test('has a Flutter error and a fix recommendation', () { - final exception = ArtifactBuildException( - 'message', - stderr: [ - 'some stderr output', - 'FAILURE: Build failed with an exception.', - '* What went wrong:', - "Execution failed for task ':app:signReleaseBundle'.", - r'''> A failure occurred while executing com.android.build.gradle.internal.tasks.FinalizeBundleTask$BundleToolRunnable''', - '> java.lang.NullPointerException (no error message)', - '* Exception is:', - 'some stack trace', - '* Try:', - 'some recommendation', - ], - stdout: ['some stdout output'], - ); - expect( - exception.flutterError, - equals(r''' + group( + 'when a known error is recognized and a fix recommendation is found', + () { + test('has a Flutter error and a fix recommendation', () { + final exception = ArtifactBuildException( + 'message', + stderr: [ + 'some stderr output', + 'FAILURE: Build failed with an exception.', + '* What went wrong:', + "Execution failed for task ':app:signReleaseBundle'.", + r'''> A failure occurred while executing com.android.build.gradle.internal.tasks.FinalizeBundleTask$BundleToolRunnable''', + '> java.lang.NullPointerException (no error message)', + '* Exception is:', + 'some stack trace', + '* Try:', + 'some recommendation', + ], + stdout: ['some stdout output'], + ); + expect( + exception.flutterError, + equals(r''' FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:signReleaseBundle'. > A failure occurred while executing com.android.build.gradle.internal.tasks.FinalizeBundleTask$BundleToolRunnable > java.lang.NullPointerException (no error message)'''), - ); - expect( - exception.fixRecommendation, - contains('This error is likely due to a missing keystore file'), - ); + ); + expect( + exception.fixRecommendation, + contains('This error is likely due to a missing keystore file'), + ); + }); + }); + + group('when a fix recommendation is provided', () { + test('does not read output to find fix recommendation', () { + final exception = ArtifactBuildException( + 'message', + fixRecommendation: 'some recommendation', + stderr: [ + 'some stderr output', + 'FAILURE: Build failed with an exception.', + '* Exception is:', + 'some stack trace', + '* Try:', + 'some recommendation', + ], + stdout: ['some stdout output'], + ); + expect( + exception.flutterError, + 'FAILURE: Build failed with an exception.', + ); + expect(exception.fixRecommendation, equals('some recommendation')); + }); }); }); - group('when a fix recommendation is provided', () { - test('does not read output to find fix recommendation', () { - final exception = ArtifactBuildException( - 'message', - fixRecommendation: 'some recommendation', - stderr: [ - 'some stderr output', - 'FAILURE: Build failed with an exception.', - '* Exception is:', - 'some stack trace', - '* Try:', - 'some recommendation', - ], - stdout: ['some stdout output'], - ); - expect( - exception.flutterError, - 'FAILURE: Build failed with an exception.', - ); - expect(exception.fixRecommendation, equals('some recommendation')); + group('xcode archiving', () { + group( + 'when a known error is recognized and a fix recommendation is found', + () { + test('has a Flutter error and a fix recommendation', () { + final exception = ArtifactBuildException( + 'message', + stderr: [ + 'some stderr output', + 'Error (Xcode):', + 'some error message', + 'some stack trace', + 'some recommendation', + 'Encountered error while archiving for device', + 'not part of the reported error', + ], + stdout: ['some stdout output'], + ); + expect( + exception.flutterError, + equals( + ''' +Error (Xcode): +some error message +some stack trace +some recommendation''', + ), + ); + }); }); }); });