// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/ios/xcresult.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; import 'xcresult_test_data.dart'; void main() { // Creates a FakeCommand for the xcresult get call to build the app // in the given configuration. FakeCommand setUpFakeXCResultCommand({ required String stdout, required String tempResultPath, required Xcode xcode, required Version? xcodeVersion, int exitCode = 0, String stderr = '', }) { final bool useNewCommand = xcodeVersion != null && xcodeVersion >= Version(16, 0, 0); final List command; if (useNewCommand) { command = [ ...xcode.xcrunCommand(), 'xcresulttool', 'get', 'build-results', '--path', tempResultPath, '--format', 'json', ]; } else { command = [ ...xcode.xcrunCommand(), 'xcresulttool', 'get', '--path', tempResultPath, '--format', 'json', ]; } return FakeCommand( command: command, stdout: stdout, stderr: stderr, exitCode: exitCode, onRun: (_) {}, ); } const kWhichSysctlCommand = FakeCommand(command: ['which', 'sysctl']); const kx64CheckCommand = FakeCommand( command: ['sysctl', 'hw.optional.arm64'], exitCode: 1, ); XCResultGenerator setupGenerator({ required String resultJson, int exitCode = 0, String stderr = '', // Default to an pre-Xcode 16 version of Xcode. // This ensures that tests which don't explicitly set an Xcode version still cover // the logic for pre-Xcode 16 platforms. Version? xcodeVersion = const Version.withText(15, 0, 0, '15.0'), }) { final fakeProcessManager = FakeProcessManager.list([ kWhichSysctlCommand, kx64CheckCommand, ]); final xcode = Xcode.test( processManager: fakeProcessManager, xcodeProjectInterpreter: XcodeProjectInterpreter.test( processManager: fakeProcessManager, version: xcodeVersion, ), ); fakeProcessManager.addCommands([ setUpFakeXCResultCommand( stdout: resultJson, tempResultPath: _tempResultPath, xcode: xcode, exitCode: exitCode, stderr: stderr, xcodeVersion: xcodeVersion, ), ]); final processUtils = ProcessUtils( processManager: fakeProcessManager, logger: BufferLogger.test(), ); return XCResultGenerator(resultPath: _tempResultPath, xcode: xcode, processUtils: processUtils); } testWithoutContext('correctly parses new format (Xcode 16+) JSON with issues', () async { final XCResultGenerator generator = setupGenerator( resultJson: kNewFormatResultJsonWithIssues, xcodeVersion: Version(16, 0, 0), ); final XCResult result = await generator.generate(); expect(result.issues.length, 2); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); final XCResultIssue error = result.issues.firstWhere( (issue) => issue.type == XCResultIssueType.error, ); expect(error.subType, 'Swift Compiler Error'); expect(error.message, "consecutive statements on a line must be separated by ';'"); expect(error.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.swift:11:82'); final XCResultIssue warning = result.issues.firstWhere( (issue) => issue.type == XCResultIssueType.warning, ); expect(warning.subType, 'Deprecation'); expect(warning.message, "'openURL' was deprecated in iOS 10.0"); expect(warning.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.swift:15:20'); }); testWithoutContext('correctly handles new format (Xcode 16+) with invalid sourceURL', () async { final XCResultGenerator generator = setupGenerator( resultJson: kNewFormatResultJsonWithInvalidUrl, xcodeVersion: Version(16, 0, 0), ); final XCResult result = await generator.generate(); expect(result.issues.length, 1); final XCResultIssue error = result.issues.first; expect(error.location, isNull); expect(error.warnings.first, contains('failed to be parsed')); }); testWithoutContext('correctly parse sample result json when there are issues.', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final XCResult result = await generator.generate(); expect(result.issues.length, 2); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Semantic Issue'); expect(result.issues.first.message, "Use of undeclared identifier 'asdas'"); expect( result.issues.first.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56', ); expect(result.issues.last.type, XCResultIssueType.warning); expect(result.issues.last.subType, 'Warning'); expect( result.issues.last.message, "The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.", ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext( 'correctly parse sample result json when there are issues but invalid url.', () async { final XCResultGenerator generator = setupGenerator( resultJson: kSampleResultJsonWithIssuesAndInvalidUrl, ); final XCResult result = await generator.generate(); expect(result.issues.length, 2); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Semantic Issue'); expect(result.issues.first.message, "Use of undeclared identifier 'asdas'"); expect(result.issues.first.location, isNull); expect( result.issues.first.warnings.first, '(XCResult) The `url` exists but it was failed to be parsed. url: 3:00', ); expect(result.issues.last.type, XCResultIssueType.warning); expect(result.issues.last.subType, 'Warning'); expect( result.issues.last.message, "The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.", ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }, ); testWithoutContext('correctly parse sample result json and discard all warnings', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final discarder = XCResultIssueDiscarder(typeMatcher: XCResultIssueType.warning); final XCResult result = await generator.generate( issueDiscarders: [discarder], ); expect(result.issues.length, 1); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Semantic Issue'); expect(result.issues.first.message, "Use of undeclared identifier 'asdas'"); expect( result.issues.first.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56', ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json and discard base on subType', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final discarder = XCResultIssueDiscarder(subTypeMatcher: RegExp(r'^Warning$')); final XCResult result = await generator.generate( issueDiscarders: [discarder], ); expect(result.issues.length, 1); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Semantic Issue'); expect(result.issues.first.message, "Use of undeclared identifier 'asdas'"); expect( result.issues.first.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56', ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json and discard base on message', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final discarder = XCResultIssueDiscarder( messageMatcher: RegExp( r"^The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.$", ), ); final XCResult result = await generator.generate( issueDiscarders: [discarder], ); expect(result.issues.length, 1); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Semantic Issue'); expect(result.issues.first.message, "Use of undeclared identifier 'asdas'"); expect( result.issues.first.location, '/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56', ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json and discard base on location', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final discarder = XCResultIssueDiscarder( locationMatcher: RegExp(r'/Users/m/Projects/test_create/ios/Runner/AppDelegate.m'), ); final XCResult result = await generator.generate( issueDiscarders: [discarder], ); expect(result.issues.length, 1); expect(result.issues.first.type, XCResultIssueType.warning); expect(result.issues.first.subType, 'Warning'); expect( result.issues.first.message, "The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.", ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json with multiple discarders.', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonWithIssues); final discardWarnings = XCResultIssueDiscarder(typeMatcher: XCResultIssueType.warning); final discardSemanticIssues = XCResultIssueDiscarder( subTypeMatcher: RegExp(r'^Semantic Issue$'), ); final XCResult result = await generator.generate( issueDiscarders: [discardWarnings, discardSemanticIssues], ); expect(result.issues, isEmpty); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json when no issues.', () async { final XCResultGenerator generator = setupGenerator(resultJson: kSampleResultJsonNoIssues); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result json with action issues.', () async { final XCResultGenerator generator = setupGenerator( resultJson: kSampleResultJsonWithActionIssues, ); final discarder = XCResultIssueDiscarder(typeMatcher: XCResultIssueType.warning); final XCResult result = await generator.generate( issueDiscarders: [discarder], ); expect(result.issues.length, 1); expect(result.issues.first.type, XCResultIssueType.error); expect(result.issues.first.subType, 'Uncategorized'); expect( result.issues.first.message, contains('Unable to find a destination matching the provided destination specifier'), ); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext('correctly parse sample result on < Xcode 16.', () async { final XCResultGenerator generator = setupGenerator( resultJson: kSampleResultJsonNoIssues, xcodeVersion: Version(15, 0, 0), ); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, isTrue); expect(result.parsingErrorMessage, isNull); }); testWithoutContext( 'error: `xcresulttool get` process fail should return an `XCResult` with stderr as `parsingErrorMessage`.', () async { const fakeStderr = 'Fake: fail to parse result json.'; final XCResultGenerator generator = setupGenerator( resultJson: '', exitCode: 1, stderr: fakeStderr, ); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, false); expect(result.parsingErrorMessage, fakeStderr); }, ); testWithoutContext('error: `xcresulttool get` no stdout', () async { final XCResultGenerator generator = setupGenerator(resultJson: ''); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, false); expect(result.parsingErrorMessage, 'xcresult parser: Unrecognized top level json format.'); }); testWithoutContext('error: wrong top level json format.', () async { final XCResultGenerator generator = setupGenerator(resultJson: '[]'); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, false); expect(result.parsingErrorMessage, 'xcresult parser: Unrecognized top level json format.'); }); testWithoutContext('error: fail to parse issue map', () async { final XCResultGenerator generator = setupGenerator(resultJson: '{}'); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, false); expect(result.parsingErrorMessage, 'xcresult parser: Failed to parse the issues map.'); }); testWithoutContext('error: invalid issue map', () async { final XCResultGenerator generator = setupGenerator( resultJson: kSampleResultJsonInvalidIssuesMap, ); final XCResult result = await generator.generate(); expect(result.issues.length, 0); expect(result.parseSuccess, false); expect(result.parsingErrorMessage, 'xcresult parser: Failed to parse the issues map.'); }); } const _tempResultPath = 'temp';