Skip to content

Commit

Permalink
Initial implementation of Jest test runner for RN integration tests
Browse files Browse the repository at this point in the history
Differential Revision: D65661701
  • Loading branch information
rubennorte authored and facebook-github-bot committed Nov 14, 2024
1 parent 6a12063 commit f106eb9
Show file tree
Hide file tree
Showing 7 changed files with 485 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,6 @@ vendor/

# CircleCI
.circleci/generated_config.yml

# Jest Integration
/jest/integration/build/
27 changes: 27 additions & 0 deletions jest/integration/config/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

'use strict';

const baseConfig = require('../../../jest.config');
const path = require('path');

module.exports = {
rootDir: path.resolve(__dirname, '../../..'),
roots: [
'<rootDir>/packages/react-native',
'<rootDir>/jest/integration/runtime',
],
// This allows running Meta-internal tests with the `-test.fb.js` suffix.
testRegex: '/__tests__/.*-itest(\\.fb)?\\.js$',
testPathIgnorePatterns: baseConfig.testPathIgnorePatterns,
transformIgnorePatterns: ['.*'],
testRunner: './jest/integration/runner/index.js',
watchPathIgnorePatterns: ['<rootDir>/jest/integration/build/'],
};
13 changes: 13 additions & 0 deletions jest/integration/config/metro-babel-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

'use strict';

require('../../../scripts/build/babel-register').registerForMonorepo();
module.exports = require('@react-native/metro-babel-transformer');
40 changes: 40 additions & 0 deletions jest/integration/config/metro.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

'use strict';

const {getDefaultConfig} = require('@react-native/metro-config');
const {mergeConfig} = require('metro-config');
const path = require('path');

const rnTesterConfig = getDefaultConfig(
path.resolve('../../../packages/rn-tester'),
);

const config = {
projectRoot: path.resolve(__dirname, '../../..'),
reporter: {
update: () => {},
},
resolver: {
blockList: null,
sourceExts: [...rnTesterConfig.resolver.sourceExts, 'fb.js'],
nodeModulesPaths: process.env.JS_DIR
? [path.join(process.env.JS_DIR, 'public', 'node_modules')]
: [],
},
transformer: {
// We need to wrap the default transformer so we can run it from source
// using babel-register.
babelTransformerPath: path.resolve(__dirname, 'metro-babel-transformer.js'),
},
watchFolders: process.env.JS_DIR ? [process.env.JS_DIR] : [],
};

module.exports = mergeConfig(rnTesterConfig, config);
28 changes: 28 additions & 0 deletions jest/integration/runner/entrypoint-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

module.exports = function entrypointTemplate({testPath, setupModulePath}) {
return `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* ${'@'}generated
* @noformat
* @noflow
* @oncall react_native
*/
import {registerTest} from '${setupModulePath}';
registerTest(() => require('${testPath}'));
`;
};
194 changes: 194 additions & 0 deletions jest/integration/runner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

'use strict';

const entrypointTemplate = require('./entrypoint-template');
const {spawnSync} = require('child_process');
const fs = require('fs');
const {formatResultsErrors} = require('jest-message-util');
const Metro = require('metro');
const nullthrows = require('nullthrows');
const os = require('os');
const path = require('path');
const crypto = require('crypto');

const BUILD_OUTPUT_PATH = path.resolve(__dirname, '..', 'build');

function parseRNTesterCommandResult(commandArgs, result) {
const stdout = result.stdout.toString();

const outputArray = stdout.trim().split('\n');

// Remove AppRegistry logs at the end
while (
outputArray.length > 0 &&
outputArray[outputArray.length - 1].startsWith('Running "')
) {
outputArray.pop();
}

// The last line should be the test output in JSON format
const testResultJSON = outputArray.pop();

let testResult;
try {
testResult = JSON.parse(nullthrows(testResultJSON));
} catch (error) {
throw new Error(
[
'Failed to parse test results from RN tester binary result. Full output:',
'buck2 ' + commandArgs.join(' '),
'stdout:',
stdout,
'stderr:',
result.stderr.toString(),
].join('\n'),
);
}

return {logs: outputArray.join('\n'), testResult};
}

function getBuckModeForPlatform() {
switch (os.platform()) {
case 'linux':
return '@//arvr/mode/linux/dev';
case 'darwin':
return os.arch() === 'arm64'
? '@//arvr/mode/mac-arm/dev'
: '@//arvr/mode/mac/dev';
case 'win32':
return '@//arvr/mode/win/dev';
default:
throw new Error(`Unsupported platform: ${os.platform()}`);
}
}

function getShortHash(contents) {
return crypto.createHash('md5').update(contents).digest('hex').slice(0, 8);
}

module.exports = async function runTest(
globalConfig,
config,
environment,
runtime,
testPath,
sendMessageToJest,
) {
const startTime = Date.now();

const metroConfig = await Metro.loadConfig({
config: require.resolve('../config/metro.config.js'),
});

const setupModulePath = path.resolve(__dirname, '../runtime/setup.js');

const entrypointContents = entrypointTemplate({
testPath: `.${path.sep}${path.relative(BUILD_OUTPUT_PATH, testPath)}`,
setupModulePath: `.${path.sep}${path.relative(BUILD_OUTPUT_PATH, setupModulePath)}`,
});

const entrypointPath = path.join(
BUILD_OUTPUT_PATH,
`${getShortHash(entrypointContents)}-${path.basename(testPath)}`,
);
const testBundlePath = entrypointPath + '.bundle';

fs.mkdirSync(path.dirname(entrypointPath), {recursive: true});
fs.writeFileSync(entrypointPath, entrypointContents, 'utf8');

await Metro.runBuild(metroConfig, {
entry: entrypointPath,
out: testBundlePath,
platform: 'android',
minify: false,
dev: true,
});

const rnTesterCommandArgs = [
'run',
getBuckModeForPlatform(),
'//xplat/ReactNative/react-native-cxx/samples/tester:tester',
'--',
`--bundlePath=${testBundlePath}`,
];
const rnTesterCommandResult = spawnSync('buck2', rnTesterCommandArgs, {
encoding: 'utf8',
});

if (rnTesterCommandResult.status !== 0) {
throw new Error(
`Failed to run test: ${rnTesterCommandResult.stderr.toString()}`,
);
}

const rnTesterParsedOutput = parseRNTesterCommandResult(
rnTesterCommandArgs,
rnTesterCommandResult,
);

const testResultError = rnTesterParsedOutput.testResult.error;
if (testResultError) {
const error = new Error(testResultError.message);
error.stack = testResultError.stack;
throw error;
}

const endTime = Date.now();

console.log(rnTesterParsedOutput.logs);

const testResults =
nullthrows(rnTesterParsedOutput.testResult.testResults).map(testResult => ({
ancestorTitles: [],
failureDetails: [],
testFilePath: testPath,
...testResult,
})) ?? [];

return {
testFilePath: testPath,
failureMessage: formatResultsErrors(
testResults,
config,
globalConfig,
testPath,
),
leaks: false,
openHandles: [],
perfStats: {
start: startTime,
end: endTime,
duration: endTime - startTime,
runtime: endTime - startTime,
slow: false,
},
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0,
},
numTotalTests: testResults.length,
numPassingTests: testResults.filter(test => test.status === 'passed')
.length,
numFailingTests: testResults.filter(test => test.status === 'failed')
.length,
numPendingTests: 0,
numTodoTests: 0,
skipped: false,
testResults,
};
};
Loading

0 comments on commit f106eb9

Please sign in to comment.