Skip to content
Merged
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
13 changes: 12 additions & 1 deletion .github/workflows/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ runs:
with:
node-version: ${{ inputs.node_version}}
- name: Install
uses: dtolnay/rust-toolchain@1.79.0
uses: dtolnay/rust-toolchain@1.88.0
- name: Cache cargo-http-registry
id: cargo-http-registry-cache
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-http-registry
key: ${{ runner.os }}-cargo-http-registry-0.1.8
- name: Install cargo-http-registry
# Local cargo registry used by the rust-e2e release/publish test.
if: steps.cargo-http-registry-cache.outputs.cache-hit != 'true'
shell: bash
run: cargo install cargo-http-registry --version 0.1.8 --locked
- uses: actions/cache@v4
id: workspace-cache
with:
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ Thumbs.db
/target

.nx/cache
.nx/workspace-data
.nx/workspace-data
.claude/worktrees
.claude/settings.local.json
4 changes: 2 additions & 2 deletions e2e/rust-e2e/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "e2e/rust-e2e/src",
"implicitDependencies": ["rust"],
"targets": {
"e2e": {
"executor": "@nx/jest:jest",
Expand All @@ -17,6 +18,5 @@
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
},
"implicitDependencies": ["rust"]
}
}
93 changes: 93 additions & 0 deletions e2e/rust-e2e/src/cargo-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { execSync, spawn } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

const REGISTRY_BIN = 'cargo-http-registry';

export interface LocalCargoRegistry {
/**
* Absolute path to the registry root. Doubles as the `file://` index that
* cargo reads, and is where published `<name>-<version>.crate` tarballs land.
*/
registryRoot: string;
/** Stops the background registry process. */
stop: () => void;
}

/**
* Starts a local cargo registry (cargo-http-registry) for e2e publish testing.
*
* The binary must already be installed. Developers install it once with
* `cargo install cargo-http-registry`; CI installs it via
* `.github/workflows/setup`.
*/
export async function startCargoRegistry(
registryRoot: string
): Promise<LocalCargoRegistry> {
const bin = resolveBinary();
mkdirSync(registryRoot, { recursive: true });

const proc = spawn(bin, [registryRoot], { stdio: 'inherit' });
proc.on('error', (err) => {
throw err;
});

// The registry writes config.json (recording its api/dl URLs) only once it has
// bound its socket and is ready to accept publishes. Poll for that file rather
// than guessing readiness with a fixed sleep.
await waitForFile(join(registryRoot, 'config.json'), 10_000);

return {
registryRoot,
stop: () => {
proc.kill();
},
};
}

/**
* Resolves the registry binary. It is usually on the PATH, but cargo installs
* to `$CARGO_HOME/bin` (default `~/.cargo/bin`), which is not always on the PATH
* of the node process that runs the tests — so fall back to that location.
*/
function resolveBinary(): string {
// Prefer the canonical install location — it's an absolute path, so it works
// even when the test runner's PATH doesn't include the cargo bin dir. (Note:
// we can't probe with `<bin> --version`; cargo-http-registry prints its
// version but exits non-zero, so that would look like a failure.)
const cargoBin = join(
process.env.CARGO_HOME ?? join(homedir(), '.cargo'),
'bin',
REGISTRY_BIN
);
if (existsSync(cargoBin)) {
return cargoBin;
}
// Otherwise fall back to PATH resolution.
try {
execSync(`command -v ${REGISTRY_BIN}`, { stdio: 'ignore' });
return REGISTRY_BIN;
} catch {
throw new Error(
`"${REGISTRY_BIN}" was not found on the PATH or in ${cargoBin}. The ` +
`rust-e2e release test needs it to stand up a local cargo registry.\n\n` +
`Install it with:\n\n` +
` cargo install cargo-http-registry\n\n` +
`(CI installs it automatically via .github/workflows/setup.)`
);
}
}

async function waitForFile(path: string, timeoutMs: number): Promise<void> {
const start = Date.now();
while (!existsSync(path)) {
if (Date.now() - start > timeoutMs) {
throw new Error(
`Timed out after ${timeoutMs}ms waiting for the cargo registry to ` +
`become ready (${path} never appeared).`
);
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
35 changes: 20 additions & 15 deletions e2e/rust-e2e/src/napi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { execSync } from 'child_process';
import { createTestProject, runNxCommand } from './utils';
import {
crateRoot,
createTestProject,
installPlugin,
runNxCommand,
} from './utils';
import { rmSync } from 'fs';
import { join } from 'path';
import { listFiles, readFile, updateFile } from '@nx/plugin/testing';

describe('napi', () => {
let projectDirectory: string;
beforeAll(() => {
projectDirectory = createTestProject('napi');

// The plugin has been built and published to a local registry in the jest globalSetup
// Install the plugin built with the latest source code into the test repo
execSync(`yarn add -D @monodon/rust@e2e`, {
cwd: projectDirectory,
stdio: 'inherit',
env: process.env,
});
installPlugin(projectDirectory);
});

afterAll(() => {
Expand All @@ -31,7 +30,15 @@ describe('napi', () => {
projectDirectory
);

const projectConfigPath = `test-project-napi/napi_proj/project.json`;
// Crates are generated under a layout-dependent base dir (e.g. packages/).
// The @nx/plugin/testing helpers resolve paths relative to the tmp proj
// root, so prefix the crate root with the workspace folder name.
const napiProjDir = join(
'test-project-napi',
crateRoot(projectDirectory, 'napi_proj')
);

const projectConfigPath = join(napiProjDir, 'project.json');
const projectFile = JSON.parse(readFile(projectConfigPath));
projectFile['targets']['build']['options'] = {
...projectFile['targets']['build']['options'],
Expand All @@ -40,15 +47,13 @@ describe('napi', () => {
};
updateFile(projectConfigPath, JSON.stringify(projectFile, null, 2));

expect(listFiles(`test-project-napi/napi_proj/npm`).length).toBeGreaterThan(
0
);
expect(listFiles(join(napiProjDir, 'npm')).length).toBeGreaterThan(0);

expect(() =>
runNxCommand(`build napi_proj`, projectDirectory)
).not.toThrow();

const files = listFiles(`test-project-napi/napi_proj`);
const files = listFiles(napiProjDir);
expect(files.some((file) => file.endsWith('native.js'))).toBeTruthy();
expect(files.some((file) => file.endsWith('native.d.ts'))).toBeTruthy();
expect(files.some((file) => file.endsWith('.node'))).toBeTruthy();
Expand All @@ -59,7 +64,7 @@ describe('napi', () => {
projectDirectory
)
).not.toThrow();
const files2 = listFiles(`test-project-napi/napi_proj`);
const files2 = listFiles(napiProjDir);
expect(
files2.some((file) => file.endsWith('wasm32-wasi.wasm'))
).toBeTruthy();
Expand Down
Loading
Loading