1import spawnAsync from '@expo/spawn-async';
2import fs from 'fs-extra';
3import path from 'path';
4
5import * as Directories from '../Directories';
6import * as Packages from '../Packages';
7
8export async function iosNativeUnitTests({ packages }: { packages?: string }) {
9  const allPackages = await Packages.getListOfPackagesAsync();
10  const packageNamesFilter = packages ? packages.split(',') : [];
11  let packagesTested: string[] = [];
12  let errors: any[] = [];
13  for (const pkg of allPackages) {
14    if (!(await pkg.hasNativeTestsAsync('ios'))) {
15      if (packageNamesFilter.includes(pkg.packageName)) {
16        throw new Error(`The package ${pkg.packageName} does not include iOS unit tests.`);
17      }
18      continue;
19    }
20    if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) {
21      continue;
22    }
23
24    try {
25      await spawnAsync('fastlane', ['prepare_schemes', `pod:${pkg.podspecName}`], {
26        cwd: Directories.getExpoRepositoryRootDir(),
27        stdio: 'inherit',
28      });
29
30      // make schemes shared by moving them from xcodeproj/xcuserdata/runner.xcuserdatad/xcschemes
31      // to xcodeproj/xcshareddata/xcschemes
32      // otherwise they aren't visible to fastlane
33      const xcodeprojDir = path.join(
34        Directories.getIosDir(),
35        'Pods',
36        `${pkg.podspecName}.xcodeproj`
37      );
38      const destinationDir = path.join(xcodeprojDir, 'xcshareddata', 'xcschemes');
39      await fs.mkdirp(destinationDir);
40
41      // find user directory name, should be runner.xcuserdatad but depends on the OS username
42      const xcuserdataDirName = (await fs.readdir(path.join(xcodeprojDir, 'xcuserdata')))[0];
43
44      const xcschemesDir = path.join(xcodeprojDir, 'xcuserdata', xcuserdataDirName, 'xcschemes');
45      const xcschemesFiles = (await fs.readdir(xcschemesDir)).filter((file) =>
46        file.endsWith('.xcscheme')
47      );
48      if (!xcschemesFiles.length) {
49        throw new Error(`No scheme could be found to run tests for ${pkg.podspecName}`);
50      }
51      for (const file of xcschemesFiles) {
52        await fs.move(path.join(xcschemesDir, file), path.join(destinationDir, file), {
53          overwrite: true,
54        });
55      }
56
57      await spawnAsync('fastlane', ['test_module', `pod:${pkg.podspecName}`], {
58        cwd: Directories.getExpoRepositoryRootDir(),
59        stdio: 'inherit',
60      });
61
62      packagesTested.push(pkg.packageName);
63    } catch (error) {
64      errors.push(error);
65    }
66  }
67  if (errors.length) {
68    console.error('One or more iOS unit tests failed:');
69    for (const error of errors) {
70      console.error(error.message);
71      console.error('stdout >', error.stdout);
72      console.error('stderr >', error.stderr);
73      if (error.message.startsWith('fastlane exited')) {
74        console.warn(
75          "Did you add unit tests to a package that didn't have unit tests before? If so, make sure to add the correct subspec to ios/Podfile."
76        );
77      }
78    }
79    throw new Error('Unit tests failed');
80  } else {
81    console.log('✅ All unit tests passed for the following packages:', packagesTested.join(', '));
82  }
83}
84
85export default (program: any) => {
86  program
87    .command('ios-native-unit-tests')
88    .option(
89      '--packages <string>',
90      '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.'
91    )
92    .description('Runs iOS native unit tests for each package that provides them.')
93    .asyncAction(iosNativeUnitTests);
94};
95