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
8async function runTests(testTargets: string[]) {
9  await spawnAsync('fastlane', ['ios', 'unit_tests', `targets:${testTargets.join(',')}`], {
10    cwd: Directories.getExpoRepositoryRootDir(),
11    stdio: 'inherit',
12  });
13}
14
15function getTestSpecNames(pkg: Packages.Package): string[] {
16  const podspec = fs.readFileSync(path.join(pkg.path, pkg.podspecPath!), 'utf8');
17  const regex = new RegExp("test_spec\\s'([^']*)'", 'g');
18  const testSpecNames: string[] = [];
19  let match: RegExpExecArray | null;
20  while ((match = regex.exec(podspec)) !== null) {
21    testSpecNames.push(match[1]);
22  }
23  return testSpecNames;
24}
25
26export async function iosNativeUnitTests({ packages }: { packages?: string }) {
27  const allPackages = await Packages.getListOfPackagesAsync();
28  const packageNamesFilter = packages ? packages.split(',') : [];
29
30  const targetsToTest: string[] = [];
31  const packagesToTest: string[] = [];
32  for (const pkg of allPackages) {
33    if (!pkg.podspecName || !pkg.podspecPath || !(await pkg.hasNativeTestsAsync('ios'))) {
34      if (packageNamesFilter.includes(pkg.packageName)) {
35        throw new Error(`The package ${pkg.packageName} does not include iOS unit tests.`);
36      }
37      continue;
38    }
39
40    if (packageNamesFilter.length > 0 && !packageNamesFilter.includes(pkg.packageName)) {
41      continue;
42    }
43
44    const testSpecNames = getTestSpecNames(pkg);
45    if (!testSpecNames.length) {
46      throw new Error(
47        `Failed to test package ${pkg.packageName}: no test specs were found in podspec file.`
48      );
49    }
50
51    for (const testSpecName of testSpecNames) {
52      targetsToTest.push(`${pkg.podspecName}-Unit-${testSpecName}`);
53    }
54    packagesToTest.push(pkg.packageName);
55  }
56
57  if (packageNamesFilter.length && !targetsToTest.length) {
58    throw new Error(
59      `No packages were found with the specified names: ${packageNamesFilter.join(', ')}`
60    );
61  }
62
63  try {
64    await runTests(targetsToTest);
65  } catch (error) {
66    console.error('iOS unit tests failed:');
67    console.error('stdout >', error.stdout);
68    console.error('stderr >', error.stderr);
69    throw new Error('iOS Unit tests failed');
70  }
71  console.log('✅ All unit tests passed for the following packages:', packagesToTest.join(', '));
72}
73
74export default (program: any) => {
75  program
76    .command('ios-native-unit-tests')
77    .option(
78      '--packages <string>',
79      '[optional] Comma-separated list of package names to run unit tests for. Defaults to all packages with unit tests.'
80    )
81    .description('Runs iOS native unit tests for each package that provides them.')
82    .asyncAction(iosNativeUnitTests);
83};
84