diff --git a/packages/pigeon/tool/shared/test_suites.dart b/packages/pigeon/tool/shared/test_suites.dart index 1a5b853087cc..39250f816536 100644 --- a/packages/pigeon/tool/shared/test_suites.dart +++ b/packages/pigeon/tool/shared/test_suites.dart @@ -313,9 +313,30 @@ Future _runWindowsUnitTests() async { return compileCode; } - return runProcess( - '$examplePath/build/windows/plugins/test_plugin/Debug/test_plugin_test.exe', - []); + // Depending on the Flutter version, the build output path is different. To + // handle both master and stable, and to future-proof against the changes + // that will happen in https://github.com/flutter/flutter/issues/129807 + // - Try arm64, to future-proof against arm64 support. + // - Try x64, to cover pre-arm64 support on arm64 hosts, as well as x64 hosts + // running newer versions of Flutter. + // - Fall back to the pre-arch path, to support running against stable. + // TODO(stuartmorgan): Remove all this when these tests no longer need to + // support a version of Flutter without + // https://github.com/flutter/flutter/issues/129807, and just construct the + // version of the path with the current architecture. + const String buildDirBase = '$examplePath/build/windows'; + const String buildRelativeBinaryPath = + 'plugins/test_plugin/Debug/test_plugin_test.exe'; + const String arm64Path = '$buildDirBase/arm64/$buildRelativeBinaryPath'; + const String x64Path = '$buildDirBase/x64/$buildRelativeBinaryPath'; + const String oldPath = '$buildDirBase/$buildRelativeBinaryPath'; + if (File(arm64Path).existsSync()) { + return runProcess(arm64Path, []); + } else if (File(x64Path).existsSync()) { + return runProcess(x64Path, []); + } else { + return runProcess(oldPath, []); + } } Future _runWindowsIntegrationTests() async { diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart index 7d11039edd21..68533f6488f3 100644 --- a/script/tool/lib/src/common/cmake.dart +++ b/script/tool/lib/src/common/cmake.dart @@ -20,6 +20,7 @@ class CMakeProject { required this.buildMode, this.processRunner = const ProcessRunner(), this.platform = const LocalPlatform(), + this.arch, }); /// The directory of a Flutter project to run Gradle commands in. @@ -31,6 +32,11 @@ class CMakeProject { /// The platform that commands are being run on. final Platform platform; + /// The architecture subdirectory of the build. + // TODO(stuartmorgan): Make this non-nullable once Flutter 3.13 is no longer + // supported, since at that point there will always be a subdirectory. + final String? arch; + /// The build mode (e.g., Debug, Release). /// /// This is a constructor paramater because on Linux many properties depend @@ -46,14 +52,13 @@ class CMakeProject { Directory get buildDirectory { Directory buildDir = flutterProject.childDirectory('build').childDirectory(_platformDirName); + if (arch != null) { + buildDir = buildDir.childDirectory(arch!); + } if (platform.isLinux) { - buildDir = buildDir - // TODO(stuartmorgan): Support arm64 if that ever becomes a supported - // CI configuration for the repository. - .childDirectory('x64') - // Linux uses a single-config generator, so the base build directory - // includes the configuration. - .childDirectory(buildMode.toLowerCase()); + // Linux uses a single-config generator, so the base build directory + // includes the configuration. + buildDir = buildDir.childDirectory(buildMode.toLowerCase()); } return buildDir; } diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index e9fc3cc9664f..7fc88944ba90 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ffi'; + import 'package:file/file.dart'; import 'package:meta/meta.dart'; @@ -41,7 +43,9 @@ class NativeTestCommand extends PackageLoopingCommand { super.packagesDir, { super.processRunner, super.platform, - }) : _xcode = Xcode(processRunner: processRunner, log: true) { + Abi? abi, + }) : _abi = abi ?? Abi.current(), + _xcode = Xcode(processRunner: processRunner, log: true) { argParser.addOption( _iOSDestinationFlag, help: 'Specify the destination when running iOS tests.\n' @@ -63,6 +67,9 @@ class NativeTestCommand extends PackageLoopingCommand { help: 'Runs native integration (UI) tests', defaultsTo: true); } + // The ABI of the host. + final Abi _abi; + // The device destination flags for iOS tests. List _iOSDestinationFlags = []; @@ -548,9 +555,10 @@ this command. isTestBinary: isTestBinary); } - /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s - /// build directory for which [isTestBinary] is true, and runs all of them, - /// returning the overall result. + /// Finds every file in the relevant (based on [platformName], [buildMode], + /// and [arch]) subdirectory of [plugin]'s build directory for which + /// [isTestBinary] is true, and runs all of them, returning the overall + /// result. /// /// The binaries are assumed to be Google Test test binaries, thus returning /// zero for success and non-zero for failure. @@ -563,11 +571,45 @@ this command. final List testBinaries = []; bool hasMissingBuild = false; bool buildFailed = false; + String? arch; + const String x64DirName = 'x64'; + const String arm64DirName = 'arm64'; + if (platform.isWindows) { + arch = _abi == Abi.windowsX64 ? x64DirName : arm64DirName; + } else if (platform.isLinux) { + // TODO(stuartmorgan): Support arm64 if that ever becomes a supported + // CI configuration for the repository. + arch = 'x64'; + } for (final RepositoryPackage example in plugin.getExamples()) { - final CMakeProject project = CMakeProject(example.directory, + CMakeProject project = CMakeProject(example.directory, buildMode: buildMode, processRunner: processRunner, - platform: platform); + platform: platform, + arch: arch); + if (platform.isWindows) { + if (arch == arm64DirName && !project.isConfigured()) { + // Check for x64, to handle builds newer than 3.13, but that don't yet + // have https://github.com/flutter/flutter/issues/129807. + // TODO(stuartmorgan): Remove this when CI no longer supports a + // version of Flutter without the issue above fixed. + project = CMakeProject(example.directory, + buildMode: buildMode, + processRunner: processRunner, + platform: platform, + arch: x64DirName); + } + if (!project.isConfigured()) { + // Check again without the arch subdirectory, since 3.13 doesn't + // have it yet. + // TODO(stuartmorgan): Remove this when CI no longer supports Flutter + // 3.13. + project = CMakeProject(example.directory, + buildMode: buildMode, + processRunner: processRunner, + platform: platform); + } + } if (!project.isConfigured()) { printError('ERROR: Run "flutter build" on ${example.displayName}, ' 'or run this tool\'s "build-examples" command, for the target ' diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index a180c3cb09e1..993ee86d600c 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:convert'; +import 'dart:ffi'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; @@ -56,10 +57,13 @@ final Map _kDeviceListMap = { }; const String _fakeCmakeCommand = 'path/to/cmake'; +const String _archDirX64 = 'x64'; +const String _archDirArm64 = 'arm64'; -void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { +void _createFakeCMakeCache( + RepositoryPackage plugin, Platform platform, String? archDir) { final CMakeProject project = CMakeProject(getExampleDir(plugin), - platform: platform, buildMode: 'Release'); + platform: platform, buildMode: 'Release', arch: archDir); final File cache = project.buildDirectory.childFile('CMakeCache.txt'); cache.createSync(recursive: true); cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); @@ -1060,7 +1064,7 @@ public class FlutterActivityTest { ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1099,7 +1103,7 @@ public class FlutterActivityTest { ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); final File releaseTestBinary = childFileWithSubcomponents( plugin.directory, @@ -1159,7 +1163,7 @@ public class FlutterActivityTest { platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1194,7 +1198,7 @@ public class FlutterActivityTest { ], platformSupport: { platformLinux: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1760,25 +1764,22 @@ public class FlutterActivityTest { mockPlatform = MockPlatform(isWindows: true); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final NativeTestCommand command = NativeTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'native_test_command', 'Test for native_test_command'); - runner.addCommand(command); }); // Returns the ProcessCall to expect for build the Windows unit tests for // the given plugin. - ProcessCall getWindowsBuildCall(RepositoryPackage plugin) { + ProcessCall getWindowsBuildCall(RepositoryPackage plugin, String? arch) { + Directory projectDir = getExampleDir(plugin) + .childDirectory('build') + .childDirectory('windows'); + if (arch != null) { + projectDir = projectDir.childDirectory(arch); + } return ProcessCall( _fakeCmakeCommand, [ '--build', - getExampleDir(plugin) - .childDirectory('build') - .childDirectory('windows') - .path, + projectDir.path, '--target', 'unit_tests', '--config', @@ -1787,8 +1788,58 @@ public class FlutterActivityTest { null); } - group('Windows', () { + group('Windows x64', () { + setUp(() { + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, + platform: mockPlatform, + abi: Abi.windowsX64); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + test('runs unit tests', () async { + const String x64TestBinaryRelativePath = + 'build/windows/x64/Debug/bar/plugin_test.exe'; + const String arm64TestBinaryRelativePath = + 'build/windows/arm64/Debug/bar/plugin_test.exe'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$x64TestBinaryRelativePath', + 'example/$arm64TestBinaryRelativePath', + ], platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); + + final File testBinary = childFileWithSubcomponents(plugin.directory, + ['example', ...x64TestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + getWindowsBuildCall(plugin, _archDirX64), + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('runs unit tests with legacy build output', () async { const String testBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; final RepositoryPackage plugin = @@ -1797,7 +1848,7 @@ public class FlutterActivityTest { ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, null); final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1819,12 +1870,52 @@ public class FlutterActivityTest { expect( processRunner.recordedCalls, orderedEquals([ - getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin, null), ProcessCall(testBinary.path, const [], null), ])); }); test('only runs debug unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/windows/x64/Debug/bar/plugin_test.exe'; + const String releaseTestBinaryRelativePath = + 'build/windows/x64/Release/bar/plugin_test.exe'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); + + final File debugTestBinary = childFileWithSubcomponents( + plugin.directory, + ['example', ...debugTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + getWindowsBuildCall(plugin, _archDirX64), + ProcessCall(debugTestBinary.path, const [], null), + ])); + }); + + test('only runs debug unit tests with legacy build output', () async { const String debugTestBinaryRelativePath = 'build/windows/Debug/bar/plugin_test.exe'; const String releaseTestBinaryRelativePath = @@ -1836,7 +1927,7 @@ public class FlutterActivityTest { ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, null); final File debugTestBinary = childFileWithSubcomponents( plugin.directory, @@ -1859,7 +1950,7 @@ public class FlutterActivityTest { expect( processRunner.recordedCalls, orderedEquals([ - getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin, null), ProcessCall(debugTestBinary.path, const [], null), ])); }); @@ -1896,7 +1987,7 @@ public class FlutterActivityTest { platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1918,20 +2009,20 @@ public class FlutterActivityTest { expect( processRunner.recordedCalls, orderedEquals([ - getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin, _archDirX64), ])); }); test('fails if a unit test fails', () async { const String testBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; + 'build/windows/x64/Debug/bar/plugin_test.exe'; final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/$testBinaryRelativePath' ], platformSupport: { platformWindows: const PlatformDetails(PlatformSupport.inline), }); - _createFakeCMakeCache(plugin, mockPlatform); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); final File testBinary = childFileWithSubcomponents(plugin.directory, ['example', ...testBinaryRelativePath.split('/')]); @@ -1961,10 +2052,138 @@ public class FlutterActivityTest { expect( processRunner.recordedCalls, orderedEquals([ - getWindowsBuildCall(plugin), + getWindowsBuildCall(plugin, _archDirX64), ProcessCall(testBinary.path, const [], null), ])); }); }); + + group('Windows arm64', () { + setUp(() { + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, + platform: mockPlatform, + abi: Abi.windowsArm64); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + test('runs unit tests', () async { + const String x64TestBinaryRelativePath = + 'build/windows/x64/Debug/bar/plugin_test.exe'; + const String arm64TestBinaryRelativePath = + 'build/windows/arm64/Debug/bar/plugin_test.exe'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$x64TestBinaryRelativePath', + 'example/$arm64TestBinaryRelativePath', + ], platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform, _archDirArm64); + + final File testBinary = childFileWithSubcomponents(plugin.directory, + ['example', ...arm64TestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + getWindowsBuildCall(plugin, _archDirArm64), + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('falls back to x64 unit tests if arm64 is not built', () async { + const String x64TestBinaryRelativePath = + 'build/windows/x64/Debug/bar/plugin_test.exe'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$x64TestBinaryRelativePath', + ], platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform, _archDirX64); + + final File testBinary = childFileWithSubcomponents(plugin.directory, + ['example', ...x64TestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + getWindowsBuildCall(plugin, _archDirX64), + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs debug unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/windows/arm64/Debug/bar/plugin_test.exe'; + const String releaseTestBinaryRelativePath = + 'build/windows/arm64/Release/bar/plugin_test.exe'; + final RepositoryPackage plugin = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + platformWindows: const PlatformDetails(PlatformSupport.inline), + }); + _createFakeCMakeCache(plugin, mockPlatform, _archDirArm64); + + final File debugTestBinary = childFileWithSubcomponents( + plugin.directory, + ['example', ...debugTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + getWindowsBuildCall(plugin, _archDirArm64), + ProcessCall(debugTestBinary.path, const [], null), + ])); + }); + }); }); }