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