1eeffdb10STomasz Sapetaimport spawnAsync from '@expo/spawn-async';
2eeffdb10STomasz Sapetaimport chalk from 'chalk';
34ff2cc49SŁukasz Kosmatyimport path from 'path';
4eeffdb10STomasz Sapeta
5eeffdb10STomasz Sapetaimport * as Directories from '../Directories';
6eeffdb10STomasz Sapetaimport * as Packages from '../Packages';
7782a85d5SEric Samelsonimport { filterAsync } from '../Utils';
8eeffdb10STomasz Sapeta
9eeffdb10STomasz Sapetaconst ANDROID_DIR = Directories.getAndroidDir();
10eeffdb10STomasz Sapeta
114ff2cc49SŁukasz Kosmatyconst BARE_EXPO_DIR = path.join(Directories.getAppsDir(), 'bare-expo', 'android');
124ff2cc49SŁukasz Kosmaty
13eeffdb10STomasz Sapetaconst excludedInTests = [
14eeffdb10STomasz Sapeta  'expo-module-template',
15ab91dd02Saleqsio  'expo-module-template-local',
16eeffdb10STomasz Sapeta  'expo-notifications',
17eeffdb10STomasz Sapeta  'expo-in-app-purchases',
18eeffdb10STomasz Sapeta  'expo-splash-screen',
19e229380eSŁukasz Kosmaty  'expo-modules-test-core',
202acc3378SŁukasz Kosmaty  'expo-dev-client',
21eeffdb10STomasz Sapeta];
22eeffdb10STomasz Sapeta
234ff2cc49SŁukasz Kosmatyconst packagesNeedToBeTestedUsingBareExpo = [
244ff2cc49SŁukasz Kosmaty  'expo-dev-launcher',
254ff2cc49SŁukasz Kosmaty  'expo-dev-menu-interface',
26*61b15591SŁukasz Kosmaty  'expo-dev-menu',
27*61b15591SŁukasz Kosmaty  'expo-modules-core',
284ff2cc49SŁukasz Kosmaty];
294ff2cc49SŁukasz Kosmaty
30eeffdb10STomasz Sapetatype TestType = 'local' | 'instrumented';
31eeffdb10STomasz Sapeta
324ff2cc49SŁukasz Kosmatyfunction consoleErrorOutput(output: string, label: string, colorifyLine: (string) => string): void {
334ff2cc49SŁukasz Kosmaty  const lines = output.trim().split(/\r\n?|\n/g);
344ff2cc49SŁukasz Kosmaty  console.error(lines.map((line) => `${chalk.gray(label)} ${colorifyLine(line)}`).join('\n'));
354ff2cc49SŁukasz Kosmaty}
364ff2cc49SŁukasz Kosmaty
37782a85d5SEric Samelsonexport async function androidNativeUnitTests({
38782a85d5SEric Samelson  type,
39782a85d5SEric Samelson  packages,
40782a85d5SEric Samelson}: {
41782a85d5SEric Samelson  type: TestType;
42782a85d5SEric Samelson  packages?: string;
43782a85d5SEric Samelson}) {
44eeffdb10STomasz Sapeta  if (!type) {
45eeffdb10STomasz Sapeta    throw new Error(
46eeffdb10STomasz Sapeta      'Must specify which type of unit test to run with `--type local` or `--type instrumented`.'
47eeffdb10STomasz Sapeta    );
48eeffdb10STomasz Sapeta  }
49eeffdb10STomasz Sapeta  if (type !== 'local' && type !== 'instrumented') {
50eeffdb10STomasz Sapeta    throw new Error('Invalid type specified. Must use `--type local` or `--type instrumented`.');
51eeffdb10STomasz Sapeta  }
52eeffdb10STomasz Sapeta
53782a85d5SEric Samelson  const allPackages = await Packages.getListOfPackagesAsync();
54782a85d5SEric Samelson  const packageNamesFilter = packages ? packages.split(',') : [];
55eeffdb10STomasz Sapeta
56782a85d5SEric Samelson  const androidPackages = await filterAsync(allPackages, async (pkg) => {
57782a85d5SEric Samelson    if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) {
58782a85d5SEric Samelson      return false;
59782a85d5SEric Samelson    }
60782a85d5SEric Samelson
61782a85d5SEric Samelson    let includesTests;
62fc12ed2dSBartosz Kaszubowski    if (pkg.isSupportedOnPlatform('android') && !excludedInTests.includes(pkg.packageSlug)) {
63eeffdb10STomasz Sapeta      if (type === 'instrumented') {
64fc12ed2dSBartosz Kaszubowski        includesTests = await pkg.hasNativeInstrumentationTestsAsync('android');
65eeffdb10STomasz Sapeta      } else {
66fc12ed2dSBartosz Kaszubowski        includesTests = await pkg.hasNativeTestsAsync('android');
67fc12ed2dSBartosz Kaszubowski      }
68782a85d5SEric Samelson    }
69782a85d5SEric Samelson
70782a85d5SEric Samelson    if (!includesTests && packageNamesFilter.includes(pkg.packageName)) {
71782a85d5SEric Samelson      throw new Error(
72782a85d5SEric Samelson        `The package ${pkg.packageName} does not include Android ${type} unit tests.`
73eeffdb10STomasz Sapeta      );
74eeffdb10STomasz Sapeta    }
75782a85d5SEric Samelson
76782a85d5SEric Samelson    return includesTests;
77eeffdb10STomasz Sapeta  });
78eeffdb10STomasz Sapeta
79eeffdb10STomasz Sapeta  console.log(chalk.green('Packages to test: '));
80eeffdb10STomasz Sapeta  androidPackages.forEach((pkg) => {
81eeffdb10STomasz Sapeta    console.log(chalk.yellow(pkg.packageSlug));
82eeffdb10STomasz Sapeta  });
83eeffdb10STomasz Sapeta
844ff2cc49SŁukasz Kosmaty  const partition = <T>(arr: T[], condition: (T) => boolean) => {
854ff2cc49SŁukasz Kosmaty    const trues = arr.filter((el) => condition(el));
864ff2cc49SŁukasz Kosmaty    const falses = arr.filter((el) => !condition(el));
874ff2cc49SŁukasz Kosmaty    return [trues, falses];
884ff2cc49SŁukasz Kosmaty  };
894ff2cc49SŁukasz Kosmaty
90a272999eSBartosz Kaszubowski  const [androidPackagesTestedUsingBareProject, androidPackagesTestedUsingExpoProject] = partition(
91a272999eSBartosz Kaszubowski    androidPackages,
92a272999eSBartosz Kaszubowski    (element) => packagesNeedToBeTestedUsingBareExpo.includes(element.packageName)
934ff2cc49SŁukasz Kosmaty  );
944ff2cc49SŁukasz Kosmaty
9575be7bc1SKudo Chien  if (type === 'instrumented') {
96e0f520f5SKudo Chien    const testCommand = 'connectedAndroidTest';
9775be7bc1SKudo Chien    const uninstallTestCommand = 'uninstallDebugAndroidTest';
98e0f520f5SKudo Chien
99e0f520f5SKudo Chien    // TODO: remove this once avd cache saved to storage
10075be7bc1SKudo Chien    await runGradlew(androidPackagesTestedUsingExpoProject, uninstallTestCommand, ANDROID_DIR);
10175be7bc1SKudo Chien    await runGradlew(androidPackagesTestedUsingBareProject, uninstallTestCommand, BARE_EXPO_DIR);
10275be7bc1SKudo Chien
103e0f520f5SKudo Chien    // We should build and test expo-modules-core first
104e0f520f5SKudo Chien    // that to make the `isExpoModulesCoreTests` in _expo-modules-core/android/build.gradle_ working.
105e0f520f5SKudo Chien    // Otherwise, the `./gradlew :expo-modules-core:connectedAndroidTest :expo-eas-client:connectedAndroidTest`
106e0f520f5SKudo Chien    // will have duplicated fbjni.so when building expo-eas-client.
107e0f520f5SKudo Chien    const isExpoModulesCore = (pkg: Packages.Package) => pkg.packageName === 'expo-modules-core';
108e0f520f5SKudo Chien    const isNotExpoModulesCore = (pkg: Packages.Package) => pkg.packageName !== 'expo-modules-core';
109*61b15591SŁukasz Kosmaty    await runGradlew(androidPackages.filter(isExpoModulesCore), testCommand, BARE_EXPO_DIR);
110e0f520f5SKudo Chien
111e0f520f5SKudo Chien    await runGradlew(
112e0f520f5SKudo Chien      androidPackagesTestedUsingExpoProject.filter(isNotExpoModulesCore),
113e0f520f5SKudo Chien      testCommand,
114e0f520f5SKudo Chien      ANDROID_DIR
115e0f520f5SKudo Chien    );
116e0f520f5SKudo Chien    await runGradlew(
117e0f520f5SKudo Chien      androidPackagesTestedUsingBareProject.filter(isNotExpoModulesCore),
118e0f520f5SKudo Chien      testCommand,
119e0f520f5SKudo Chien      BARE_EXPO_DIR
120e0f520f5SKudo Chien    );
121e0f520f5SKudo Chien
122e0f520f5SKudo Chien    // Cleanup installed test app
123e0f520f5SKudo Chien    await runGradlew(androidPackagesTestedUsingExpoProject, uninstallTestCommand, ANDROID_DIR);
124e0f520f5SKudo Chien    await runGradlew(androidPackagesTestedUsingBareProject, uninstallTestCommand, BARE_EXPO_DIR);
125e0f520f5SKudo Chien  } else {
126e0f520f5SKudo Chien    const testCommand = 'testDebugUnitTest';
1274ff2cc49SŁukasz Kosmaty    await runGradlew(androidPackagesTestedUsingExpoProject, testCommand, ANDROID_DIR);
1284ff2cc49SŁukasz Kosmaty    await runGradlew(androidPackagesTestedUsingBareProject, testCommand, BARE_EXPO_DIR);
129e0f520f5SKudo Chien  }
130e0f520f5SKudo Chien
1314ff2cc49SŁukasz Kosmaty  console.log(chalk.green('Finished android unit tests successfully.'));
1324ff2cc49SŁukasz Kosmaty}
1334ff2cc49SŁukasz Kosmaty
1344ff2cc49SŁukasz Kosmatyasync function runGradlew(packages: Packages.Package[], testCommand: string, cwd: string) {
1354ff2cc49SŁukasz Kosmaty  if (!packages.length) {
1364ff2cc49SŁukasz Kosmaty    return;
1374ff2cc49SŁukasz Kosmaty  }
1384ff2cc49SŁukasz Kosmaty
139eeffdb10STomasz Sapeta  try {
140eeffdb10STomasz Sapeta    await spawnAsync(
141eeffdb10STomasz Sapeta      './gradlew',
1424ff2cc49SŁukasz Kosmaty      packages.map((pkg) => `:${pkg.packageSlug}:${testCommand}`),
143eeffdb10STomasz Sapeta      {
1444ff2cc49SŁukasz Kosmaty        cwd,
145eeffdb10STomasz Sapeta        stdio: 'inherit',
146eeffdb10STomasz Sapeta        env: { ...process.env },
147eeffdb10STomasz Sapeta      }
148eeffdb10STomasz Sapeta    );
149eeffdb10STomasz Sapeta  } catch (error) {
150eeffdb10STomasz Sapeta    console.error('Failed while executing android unit tests');
151eeffdb10STomasz Sapeta    consoleErrorOutput(error.stdout, 'stdout >', chalk.reset);
152eeffdb10STomasz Sapeta    consoleErrorOutput(error.stderr, 'stderr >', chalk.red);
153eeffdb10STomasz Sapeta    throw error;
154eeffdb10STomasz Sapeta  }
155eeffdb10STomasz Sapeta}
156eeffdb10STomasz Sapeta
157eeffdb10STomasz Sapetaexport default (program: any) => {
158eeffdb10STomasz Sapeta  program
159eeffdb10STomasz Sapeta    .command('android-native-unit-tests')
160eeffdb10STomasz Sapeta    .option('-t, --type <string>', 'Type of unit test to run: local or instrumented')
161782a85d5SEric Samelson    .option(
162782a85d5SEric Samelson      '--packages <string>',
163782a85d5SEric Samelson      '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.'
164782a85d5SEric Samelson    )
165eeffdb10STomasz Sapeta    .description('Runs Android native unit tests for each package that provides them.')
166eeffdb10STomasz Sapeta    .asyncAction(androidNativeUnitTests);
167eeffdb10STomasz Sapeta};
168