1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import path from 'path'; 4 5import * as Directories from '../Directories'; 6import * as Packages from '../Packages'; 7import { filterAsync } from '../Utils'; 8 9const ANDROID_DIR = Directories.getAndroidDir(); 10 11const BARE_EXPO_DIR = path.join(Directories.getAppsDir(), 'bare-expo', 'android'); 12 13const excludedInTests = [ 14 'expo-module-template', 15 'expo-notifications', 16 'expo-in-app-purchases', 17 'expo-splash-screen', 18 'expo-modules-test-core', 19 'expo-dev-client', 20]; 21 22const packagesNeedToBeTestedUsingBareExpo = [ 23 'expo-dev-menu', 24 'expo-dev-launcher', 25 'expo-dev-menu-interface', 26]; 27 28type TestType = 'local' | 'instrumented'; 29 30function consoleErrorOutput(output: string, label: string, colorifyLine: (string) => string): void { 31 const lines = output.trim().split(/\r\n?|\n/g); 32 console.error(lines.map((line) => `${chalk.gray(label)} ${colorifyLine(line)}`).join('\n')); 33} 34 35export async function androidNativeUnitTests({ 36 type, 37 packages, 38}: { 39 type: TestType; 40 packages?: string; 41}) { 42 if (!type) { 43 throw new Error( 44 'Must specify which type of unit test to run with `--type local` or `--type instrumented`.' 45 ); 46 } 47 if (type !== 'local' && type !== 'instrumented') { 48 throw new Error('Invalid type specified. Must use `--type local` or `--type instrumented`.'); 49 } 50 51 const allPackages = await Packages.getListOfPackagesAsync(); 52 const packageNamesFilter = packages ? packages.split(',') : []; 53 54 const androidPackages = await filterAsync(allPackages, async (pkg) => { 55 if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) { 56 return false; 57 } 58 59 let includesTests; 60 if (pkg.isSupportedOnPlatform('android') && !excludedInTests.includes(pkg.packageSlug)) { 61 if (type === 'instrumented') { 62 includesTests = await pkg.hasNativeInstrumentationTestsAsync('android'); 63 } else { 64 includesTests = await pkg.hasNativeTestsAsync('android'); 65 } 66 } 67 68 if (!includesTests && packageNamesFilter.includes(pkg.packageName)) { 69 throw new Error( 70 `The package ${pkg.packageName} does not include Android ${type} unit tests.` 71 ); 72 } 73 74 return includesTests; 75 }); 76 77 console.log(chalk.green('Packages to test: ')); 78 androidPackages.forEach((pkg) => { 79 console.log(chalk.yellow(pkg.packageSlug)); 80 }); 81 82 const partition = <T>(arr: T[], condition: (T) => boolean) => { 83 const trues = arr.filter((el) => condition(el)); 84 const falses = arr.filter((el) => !condition(el)); 85 return [trues, falses]; 86 }; 87 88 const [androidPackagesTestedUsingBareProject, androidPackagesTestedUsingExpoProject] = partition( 89 androidPackages, 90 (element) => packagesNeedToBeTestedUsingBareExpo.includes(element.packageName) 91 ); 92 93 if (type === 'instrumented') { 94 const testCommand = 'connectedAndroidTest'; 95 const uninstallTestCommand = 'uninstallDebugAndroidTest'; 96 97 // TODO: remove this once avd cache saved to storage 98 await runGradlew(androidPackagesTestedUsingExpoProject, uninstallTestCommand, ANDROID_DIR); 99 await runGradlew(androidPackagesTestedUsingBareProject, uninstallTestCommand, BARE_EXPO_DIR); 100 101 // We should build and test expo-modules-core first 102 // that to make the `isExpoModulesCoreTests` in _expo-modules-core/android/build.gradle_ working. 103 // Otherwise, the `./gradlew :expo-modules-core:connectedAndroidTest :expo-eas-client:connectedAndroidTest` 104 // will have duplicated fbjni.so when building expo-eas-client. 105 const isExpoModulesCore = (pkg: Packages.Package) => pkg.packageName === 'expo-modules-core'; 106 const isNotExpoModulesCore = (pkg: Packages.Package) => pkg.packageName !== 'expo-modules-core'; 107 await runGradlew(androidPackages.filter(isExpoModulesCore), testCommand, ANDROID_DIR); 108 109 await runGradlew( 110 androidPackagesTestedUsingExpoProject.filter(isNotExpoModulesCore), 111 testCommand, 112 ANDROID_DIR 113 ); 114 await runGradlew( 115 androidPackagesTestedUsingBareProject.filter(isNotExpoModulesCore), 116 testCommand, 117 BARE_EXPO_DIR 118 ); 119 120 // Cleanup installed test app 121 await runGradlew(androidPackagesTestedUsingExpoProject, uninstallTestCommand, ANDROID_DIR); 122 await runGradlew(androidPackagesTestedUsingBareProject, uninstallTestCommand, BARE_EXPO_DIR); 123 } else { 124 const testCommand = 'testDebugUnitTest'; 125 await runGradlew(androidPackagesTestedUsingExpoProject, testCommand, ANDROID_DIR); 126 await runGradlew(androidPackagesTestedUsingBareProject, testCommand, BARE_EXPO_DIR); 127 } 128 129 console.log(chalk.green('Finished android unit tests successfully.')); 130} 131 132async function runGradlew(packages: Packages.Package[], testCommand: string, cwd: string) { 133 if (!packages.length) { 134 return; 135 } 136 137 try { 138 await spawnAsync( 139 './gradlew', 140 packages.map((pkg) => `:${pkg.packageSlug}:${testCommand}`), 141 { 142 cwd, 143 stdio: 'inherit', 144 env: { ...process.env }, 145 } 146 ); 147 } catch (error) { 148 console.error('Failed while executing android unit tests'); 149 consoleErrorOutput(error.stdout, 'stdout >', chalk.reset); 150 consoleErrorOutput(error.stderr, 'stderr >', chalk.red); 151 throw error; 152 } 153} 154 155export default (program: any) => { 156 program 157 .command('android-native-unit-tests') 158 .option('-t, --type <string>', 'Type of unit test to run: local or instrumented') 159 .option( 160 '--packages <string>', 161 '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.' 162 ) 163 .description('Runs Android native unit tests for each package that provides them.') 164 .asyncAction(androidNativeUnitTests); 165}; 166