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 'unimodules-test-core', 19]; 20 21const packagesNeedToBeTestedUsingBareExpo = [ 22 'expo-dev-menu', 23 'expo-dev-launcher', 24 'expo-dev-menu-interface', 25]; 26 27type TestType = 'local' | 'instrumented'; 28 29function consoleErrorOutput(output: string, label: string, colorifyLine: (string) => string): void { 30 const lines = output.trim().split(/\r\n?|\n/g); 31 console.error(lines.map((line) => `${chalk.gray(label)} ${colorifyLine(line)}`).join('\n')); 32} 33 34export async function androidNativeUnitTests({ 35 type, 36 packages, 37}: { 38 type: TestType; 39 packages?: string; 40}) { 41 if (!type) { 42 throw new Error( 43 'Must specify which type of unit test to run with `--type local` or `--type instrumented`.' 44 ); 45 } 46 if (type !== 'local' && type !== 'instrumented') { 47 throw new Error('Invalid type specified. Must use `--type local` or `--type instrumented`.'); 48 } 49 50 const allPackages = await Packages.getListOfPackagesAsync(); 51 const packageNamesFilter = packages ? packages.split(',') : []; 52 53 const androidPackages = await filterAsync(allPackages, async (pkg) => { 54 const pkgSlug = pkg.packageSlug; 55 56 if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) { 57 return false; 58 } 59 60 let includesTests; 61 if (type === 'instrumented') { 62 includesTests = 63 pkg.isSupportedOnPlatform('android') && 64 (await pkg.hasNativeInstrumentationTestsAsync('android')) && 65 !excludedInTests.includes(pkgSlug); 66 } else { 67 includesTests = 68 pkg.isSupportedOnPlatform('android') && 69 (await pkg.hasNativeTestsAsync('android')) && 70 !excludedInTests.includes(pkgSlug); 71 } 72 73 if (!includesTests && packageNamesFilter.includes(pkg.packageName)) { 74 throw new Error( 75 `The package ${pkg.packageName} does not include Android ${type} unit tests.` 76 ); 77 } 78 79 return includesTests; 80 }); 81 82 console.log(chalk.green('Packages to test: ')); 83 androidPackages.forEach((pkg) => { 84 console.log(chalk.yellow(pkg.packageSlug)); 85 }); 86 87 const testCommand = type === 'instrumented' ? 'connectedAndroidTest' : 'test'; 88 89 const partition = <T>(arr: T[], condition: (T) => boolean) => { 90 const trues = arr.filter((el) => condition(el)); 91 const falses = arr.filter((el) => !condition(el)); 92 return [trues, falses]; 93 }; 94 95 const [ 96 androidPackagesTestedUsingBareProject, 97 androidPackagesTestedUsingExpoProject, 98 ] = partition(androidPackages, (element) => 99 packagesNeedToBeTestedUsingBareExpo.includes(element.packageName) 100 ); 101 102 await runGradlew(androidPackagesTestedUsingExpoProject, testCommand, ANDROID_DIR); 103 await runGradlew(androidPackagesTestedUsingBareProject, testCommand, BARE_EXPO_DIR); 104 console.log(chalk.green('Finished android unit tests successfully.')); 105} 106 107async function runGradlew(packages: Packages.Package[], testCommand: string, cwd: string) { 108 if (!packages.length) { 109 return; 110 } 111 112 try { 113 await spawnAsync( 114 './gradlew', 115 packages.map((pkg) => `:${pkg.packageSlug}:${testCommand}`), 116 { 117 cwd, 118 stdio: 'inherit', 119 env: { ...process.env }, 120 } 121 ); 122 } catch (error) { 123 console.error('Failed while executing android unit tests'); 124 consoleErrorOutput(error.stdout, 'stdout >', chalk.reset); 125 consoleErrorOutput(error.stderr, 'stderr >', chalk.red); 126 throw error; 127 } 128} 129 130export default (program: any) => { 131 program 132 .command('android-native-unit-tests') 133 .option('-t, --type <string>', 'Type of unit test to run: local or instrumented') 134 .option( 135 '--packages <string>', 136 '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.' 137 ) 138 .description('Runs Android native unit tests for each package that provides them.') 139 .asyncAction(androidNativeUnitTests); 140}; 141