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