1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3 4import * as Directories from '../Directories'; 5import * as Packages from '../Packages'; 6import { filterAsync } from '../Utils'; 7 8const ANDROID_DIR = Directories.getAndroidDir(); 9 10const excludedInTests = [ 11 'expo-module-template', 12 'expo-notifications', 13 'expo-in-app-purchases', 14 'expo-splash-screen', 15 'unimodules-test-core', 16]; 17 18type TestType = 'local' | 'instrumented'; 19 20export async function androidNativeUnitTests({ 21 type, 22 packages, 23}: { 24 type: TestType; 25 packages?: string; 26}) { 27 if (!type) { 28 throw new Error( 29 'Must specify which type of unit test to run with `--type local` or `--type instrumented`.' 30 ); 31 } 32 if (type !== 'local' && type !== 'instrumented') { 33 throw new Error('Invalid type specified. Must use `--type local` or `--type instrumented`.'); 34 } 35 36 const allPackages = await Packages.getListOfPackagesAsync(); 37 const packageNamesFilter = packages ? packages.split(',') : []; 38 39 function consoleErrorOutput( 40 output: string, 41 label: string, 42 colorifyLine: (string) => string 43 ): void { 44 const lines = output.trim().split(/\r\n?|\n/g); 45 console.error(lines.map((line) => `${chalk.gray(label)} ${colorifyLine(line)}`).join('\n')); 46 } 47 48 const androidPackages = await filterAsync(allPackages, async (pkg) => { 49 const pkgSlug = pkg.packageSlug; 50 51 if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) { 52 return false; 53 } 54 55 let includesTests; 56 if (type === 'instrumented') { 57 includesTests = 58 pkg.isSupportedOnPlatform('android') && 59 (await pkg.hasNativeInstrumentationTestsAsync('android')) && 60 !excludedInTests.includes(pkgSlug); 61 } else { 62 includesTests = 63 pkg.isSupportedOnPlatform('android') && 64 (await pkg.hasNativeTestsAsync('android')) && 65 !excludedInTests.includes(pkgSlug); 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 testCommand = type === 'instrumented' ? 'connectedAndroidTest' : 'test'; 83 try { 84 await spawnAsync( 85 './gradlew', 86 androidPackages.map((pkg) => `:${pkg.packageSlug}:${testCommand}`), 87 { 88 cwd: ANDROID_DIR, 89 stdio: 'inherit', 90 env: { ...process.env }, 91 } 92 ); 93 } catch (error) { 94 console.error('Failed while executing android unit tests'); 95 consoleErrorOutput(error.stdout, 'stdout >', chalk.reset); 96 consoleErrorOutput(error.stderr, 'stderr >', chalk.red); 97 throw error; 98 } 99 console.log(chalk.green('Finished android unit tests successfully.')); 100} 101 102export default (program: any) => { 103 program 104 .command('android-native-unit-tests') 105 .option('-t, --type <string>', 'Type of unit test to run: local or instrumented') 106 .option( 107 '--packages <string>', 108 '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.' 109 ) 110 .description('Runs Android native unit tests for each package that provides them.') 111 .asyncAction(androidNativeUnitTests); 112}; 113