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