Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/version-plan-1784629893636.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: minor
---

Add an injectable filesystem context and in-memory filesystem helper for hermetic Harness integrations and tests.
23 changes: 23 additions & 0 deletions packages/github-action/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import baseConfig from '../../eslint.config.mjs';

export default [
...baseConfig,
{
files: ['src/shared/**/*.ts'],
ignores: ['src/shared/__tests__/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'node:fs',
message:
'Use getFs() from @react-native-harness/tools/harness-context instead.',
},
{
name: 'node:fs/promises',
message:
'Use getFs() from @react-native-harness/tools/harness-context instead.',
},
],
},
],
},
},
{
files: ['**/*.json'],
rules: {
Expand Down
3 changes: 2 additions & 1 deletion packages/github-action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dependencies": {
"@react-native-harness/cache": "workspace:*",
"@react-native-harness/config": "workspace:*",
"@react-native-harness/platform-android": "workspace:*"
"@react-native-harness/platform-android": "workspace:*",
"@react-native-harness/tools": "workspace:*"
},
"devDependencies": {
"tsup": "^8.5.1"
Expand Down
25 changes: 15 additions & 10 deletions packages/github-action/src/shared/__tests__/plan-restore.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
runWithHarnessContext,
type FileSystem,
} from '@react-native-harness/tools/harness-context';
import { createInMemoryFileSystem } from '@react-native-harness/tools/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { run } from '../plan-restore-run.js';

const mocks = vi.hoisted(() => ({
getConfig: vi.fn(),
Expand All @@ -23,16 +27,18 @@ vi.mock('@react-native-harness/cache', () => ({
describe('plan-restore', () => {
let projectRoot: string;
let githubOutputPath: string;
let fileSystem: FileSystem;
let planRestoreMock: ReturnType<typeof vi.fn>;
let writeKeysFileMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();

projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-restore-'));
fileSystem = createInMemoryFileSystem();
projectRoot = '/project';
githubOutputPath = path.join(projectRoot, 'github-output.txt');
fs.writeFileSync(githubOutputPath, '');
fileSystem.mkdirSync(projectRoot, { recursive: true });
fileSystem.writeFileSync(githubOutputPath, '');

process.env.INPUT_PROJECTROOT = projectRoot;
process.env.GITHUB_OUTPUT = githubOutputPath;
Expand Down Expand Up @@ -69,21 +75,20 @@ describe('plan-restore', () => {
});

afterEach(() => {
fs.rmSync(projectRoot, { recursive: true, force: true });
delete process.env.INPUT_PROJECTROOT;
delete process.env.GITHUB_OUTPUT;
delete process.env.RUNNER_OS;
vi.restoreAllMocks();
});

it('writes metroRestoreKey and metroRestorePrefixes (as JSON) to GITHUB_OUTPUT', async () => {
await import('../plan-restore.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
});

const output = fs.readFileSync(githubOutputPath, 'utf8');
const output = fileSystem.readFileSync(githubOutputPath, 'utf8');
expect(output).toContain('metroRestoreKey=harness-metro-linux-abc\n');
expect(output).toContain(
'metroRestorePrefixes=["harness-metro-linux-abc-"]\n'
Expand All @@ -94,7 +99,7 @@ describe('plan-restore', () => {
});

it('calls planRestore with the os and staticInputs assembled from the Metro key recipe', async () => {
await import('../plan-restore.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(planRestoreMock).toHaveBeenCalled();
Expand All @@ -111,7 +116,7 @@ describe('plan-restore', () => {
it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => {
delete process.env.GITHUB_OUTPUT;

await import('../plan-restore.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(process.exit).toHaveBeenCalledWith(1);
Expand Down
37 changes: 21 additions & 16 deletions packages/github-action/src/shared/__tests__/plan-save.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
runWithHarnessContext,
type FileSystem,
} from '@react-native-harness/tools/harness-context';
import { createInMemoryFileSystem } from '@react-native-harness/tools/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { run } from '../plan-save-run.js';

const mocks = vi.hoisted(() => ({
getConfig: vi.fn(),
Expand All @@ -21,16 +25,18 @@ vi.mock('@react-native-harness/cache', () => ({
describe('plan-save', () => {
let projectRoot: string;
let githubOutputPath: string;
let fileSystem: FileSystem;
let planSaveMock: ReturnType<typeof vi.fn>;
let writeKeysFileMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();

projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-save-'));
fileSystem = createInMemoryFileSystem();
projectRoot = '/project';
githubOutputPath = path.join(projectRoot, 'github-output.txt');
fs.writeFileSync(githubOutputPath, '');
fileSystem.mkdirSync(projectRoot, { recursive: true });
fileSystem.writeFileSync(githubOutputPath, '');

process.env.INPUT_PROJECTROOT = projectRoot;
process.env.GITHUB_OUTPUT = githubOutputPath;
Expand Down Expand Up @@ -69,7 +75,6 @@ describe('plan-save', () => {
});

afterEach(() => {
fs.rmSync(projectRoot, { recursive: true, force: true });
delete process.env.INPUT_PROJECTROOT;
delete process.env.GITHUB_OUTPUT;
delete process.env.RUNNER_OS;
Expand All @@ -80,21 +85,21 @@ describe('plan-save', () => {
});

it('writes metroShouldSave and metroSaveKey to GITHUB_OUTPUT', async () => {
await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
});

const output = fs.readFileSync(githubOutputPath, 'utf8');
const output = fileSystem.readFileSync(githubOutputPath, 'utf8');
expect(output).toContain('metroShouldSave=true\n');
expect(output).toContain(
'metroSaveKey=harness-metro-linux-abc-contenthash\n'
);
});

it('parses the metroSnapshot input and builds the SavePolicy from env/input', async () => {
await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(planSaveMock).toHaveBeenCalled();
Expand All @@ -110,7 +115,7 @@ describe('plan-save', () => {
it('treats an unparseable metroSnapshot input as an empty snapshot instead of throwing', async () => {
process.env.INPUT_METRO_SNAPSHOT = 'not json';

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(planSaveMock).toHaveBeenCalled();
Expand All @@ -127,7 +132,7 @@ describe('plan-save', () => {
it('defaults an unrecognized cacheSavePolicy input to "default-branch"', async () => {
process.env.INPUT_CACHESAVEPOLICY = 'bogus';

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(planSaveMock).toHaveBeenCalled();
Expand Down Expand Up @@ -156,7 +161,7 @@ describe('plan-save', () => {
},
});

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
Expand All @@ -179,7 +184,7 @@ describe('plan-save', () => {
},
});

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
Expand All @@ -203,7 +208,7 @@ describe('plan-save', () => {
},
});

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
Expand All @@ -227,7 +232,7 @@ describe('plan-save', () => {
},
});

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(writeKeysFileMock).toHaveBeenCalled();
Expand All @@ -241,7 +246,7 @@ describe('plan-save', () => {
it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => {
delete process.env.GITHUB_OUTPUT;

await import('../plan-save.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(process.exit).toHaveBeenCalledWith(1);
Expand Down
27 changes: 16 additions & 11 deletions packages/github-action/src/shared/__tests__/snapshot-metro.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
runWithHarnessContext,
type FileSystem,
} from '@react-native-harness/tools/harness-context';
import { createInMemoryFileSystem } from '@react-native-harness/tools/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { run } from '../snapshot-metro-run.js';

const mocks = vi.hoisted(() => ({
getConfig: vi.fn(),
Expand All @@ -19,15 +23,17 @@ vi.mock('@react-native-harness/cache', () => ({
describe('snapshot-metro', () => {
let projectRoot: string;
let githubOutputPath: string;
let fileSystem: FileSystem;
let snapshotMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();

projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-snapshot-metro-'));
fileSystem = createInMemoryFileSystem();
projectRoot = '/project';
githubOutputPath = path.join(projectRoot, 'github-output.txt');
fs.writeFileSync(githubOutputPath, '');
fileSystem.mkdirSync(projectRoot, { recursive: true });
fileSystem.writeFileSync(githubOutputPath, '');

process.env.INPUT_PROJECTROOT = projectRoot;
process.env.GITHUB_OUTPUT = githubOutputPath;
Expand All @@ -51,21 +57,20 @@ describe('snapshot-metro', () => {
});

afterEach(() => {
fs.rmSync(projectRoot, { recursive: true, force: true });
delete process.env.INPUT_PROJECTROOT;
delete process.env.GITHUB_OUTPUT;
delete process.env.METRO_RESTORED_KEY;
vi.restoreAllMocks();
});

it('writes the current cache snapshot to GITHUB_OUTPUT as metroSnapshot', async () => {
await import('../snapshot-metro.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(snapshotMock).toHaveBeenCalled();
});

const output = fs.readFileSync(githubOutputPath, 'utf8');
const output = fileSystem.readFileSync(githubOutputPath, 'utf8');
expect(output).toContain(
'metroSnapshot={"metro":[{"path":"metro/a","sizeBytes":3,"mtimeMs":1}]}\n'
);
Expand All @@ -80,7 +85,7 @@ describe('snapshot-metro', () => {
],
});

await import('../snapshot-metro.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(snapshotMock).toHaveBeenCalled();
Expand All @@ -94,7 +99,7 @@ describe('snapshot-metro', () => {
it('reports a cold start when no cache entry matched', async () => {
delete process.env.METRO_RESTORED_KEY;

await import('../snapshot-metro.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(snapshotMock).toHaveBeenCalled();
Expand All @@ -108,7 +113,7 @@ describe('snapshot-metro', () => {
it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => {
delete process.env.GITHUB_OUTPUT;

await import('../snapshot-metro.js');
await runWithHarnessContext({ fs: fileSystem }, run);

await vi.waitFor(() => {
expect(process.exit).toHaveBeenCalledWith(1);
Expand Down
10 changes: 7 additions & 3 deletions packages/github-action/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import {
getEmulatorCpuCores,
getHostAndroidSystemImageArch,
} from '@react-native-harness/platform-android';
import {
getFs,
getHarnessContext,
runWithHarnessContext,
} from '@react-native-harness/tools/harness-context';
import path from 'node:path';
import fs from 'node:fs';

const resolveAvdCachingEnabled = ({
snapshotEnabled,
Expand Down Expand Up @@ -122,7 +126,7 @@ const run = async (): Promise<void> => {
const output = `config=${JSON.stringify(
resolvedRunner
)}\nprojectRoot=${relativeProjectRoot}\n`;
fs.appendFileSync(githubOutput, output);
getFs().appendFileSync(githubOutput, output);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
Expand All @@ -134,4 +138,4 @@ const run = async (): Promise<void> => {
}
};

run();
runWithHarnessContext(getHarnessContext(), run);
8 changes: 5 additions & 3 deletions packages/github-action/src/shared/metro-cache-inputs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'node:fs';
import path from 'node:path';
import { computeMetroStaticInputs } from '@react-native-harness/cache';
import { getFs } from '@react-native-harness/tools/harness-context';

const FALLBACK_BUNDLER_METRO_VERSION = 'unknown';

Expand All @@ -16,7 +16,7 @@ export const resolveRepoRoot = (projectRoot: string): string => {
let dir = projectRoot;

while (true) {
if (fs.existsSync(path.join(dir, '.git'))) {
if (getFs().existsSync(path.join(dir, '.git'))) {
return dir;
}

Expand Down Expand Up @@ -65,7 +65,9 @@ const resolveBundlerMetroPackageJson = (projectRoot: string): string => {
export const resolveBundlerMetroVersion = (projectRoot: string): string => {
try {
const packageJsonPath = resolveBundlerMetroPackageJson(projectRoot);
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageJson = JSON.parse(
getFs().readFileSync(packageJsonPath, 'utf8')
);

if (typeof packageJson.version !== 'string') {
throw new Error('"version" field is missing or not a string');
Expand Down
Loading
Loading