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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ jobs:
run: node common/scripts/install-run-rush.js retest --verbose --production
working-directory: repo-a

- name: Run package manager integration tests
run: npm run test
working-directory: repo-a/build-tests/rush-package-manager-integration-test

- name: Ensure repo README is up-to-date
run: node repo-scripts/repo-toolbox/lib/start.js readme --verify
working-directory: repo-a
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/rush-amazon-s3-build-cache-plugin-integration-test](./build-tests/rush-amazon-s3-build-cache-plugin-integration-test/) | Tests connecting to an amazon S3 endpoint |
| [/build-tests/rush-lib-declaration-paths-test](./build-tests/rush-lib-declaration-paths-test/) | This project ensures all of the paths in rush-lib/lib/... have imports that resolve correctly. If this project builds, all `lib/**/*.d.ts` files in the `@microsoft/rush-lib` package are valid. |
| [/build-tests/rush-mcp-example-plugin](./build-tests/rush-mcp-example-plugin/) | Example showing how to create a plugin for @rushstack/mcp-server |
| [/build-tests/rush-package-manager-integration-test](./build-tests/rush-package-manager-integration-test/) | Integration tests for non-pnpm package managers in Rush. |
| [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to |
| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server |
| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin |
Expand Down
97 changes: 97 additions & 0 deletions build-tests/rush-package-manager-integration-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Rush Package Manager Integration Tests

This directory contains integration tests for verifying Rush works correctly with different package managers after the tar 7.x upgrade.

## Background

Rush's npm and yarn modes use temp project tarballs (stored in `common/temp/projects/`) to simulate package installations. The tar library is used to:
1. **Create** tarballs from temp project folders (`TempProjectHelper.createTempProjectTarball`)
2. **Extract** tarballs during the linking process (`NpmLinkManager._linkProjectAsync`)

These tests ensure the tar 7.x upgrade works correctly with these workflows.

## Tests

The test suite is written in TypeScript using `@rushstack/node-core-library` for cross-platform compatibility.

### testNpmMode.ts
Tests Rush npm mode by:
- Initializing a Rush repo with `npmVersion` configured
- Creating two projects with dependencies
- Running `rush update`
- Running `rush install`
- Running `rush build` (verifies everything works end-to-end)

### testYarnMode.ts
Tests Rush yarn mode by:
- Initializing a Rush repo with `yarnVersion` configured
- Creating two projects with dependencies
- Running `rush update`
- Running `rush install`
- Running `rush build` (verifies everything works end-to-end)

## Prerequisites

Before running these tests:
1. Build Rush locally: `rush build --to rush`
2. Build this test project: `rush build --to rush-package-manager-integration-test`
3. Ensure you have Node.js 18+ installed

## Running the Tests

```bash
# Build the test project first
cd build-tests/rush-package-manager-integration-test
rush build

# Run all tests
npm run test
```

Or from the root of the repo:
```bash
rush build --to rush-package-manager-integration-test
cd build-tests/rush-package-manager-integration-test
npm run test
```

## What Gets Tested

These integration tests verify:
- ✓ Temp project tarballs are created correctly using tar 7.x
- ✓ Tarballs are extracted correctly during `rush install`
- ✓ File permissions are preserved (tar filter function works)
- ✓ Dependencies are linked properly between projects
- ✓ The complete workflow (update → install → build) succeeds
- ✓ Built code executes correctly

## Test Output

Each test creates a temporary Rush repository in `/tmp/rush-package-manager-test/`:
- `/tmp/rush-package-manager-test/npm-test-repo/` - npm mode test repository
- `/tmp/rush-package-manager-test/yarn-test-repo/` - yarn mode test repository

These directories are cleaned up at the start of each test run.

## Implementation

The tests use:
- **TypeScript** for type safety and better IDE support
- **@rushstack/node-core-library** for cross-platform file operations and process execution
- **TestHelper class** to encapsulate common test operations
- Modular test functions that can be run independently or together

## Related Code

The tar library is used in:
- `libraries/rush-lib/src/logic/TempProjectHelper.ts` - Creates tarballs
- `libraries/rush-lib/src/logic/npm/NpmLinkManager.ts` - Extracts tarballs

## Troubleshooting

If tests fail:
1. Check that Rush built successfully: `rush build --to rush`
2. Check that the test project built: `rush build --to rush-package-manager-integration-test`
3. Verify Node.js version: `node --version` (should be 18+)
4. Look for error messages in the test output
5. Inspect the temp test repo: `ls -la temp/npm-test-repo/common/temp/projects/`
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",

"rigPackageName": "local-node-rig"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

const nodeProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node');

module.exports = [
...nodeProfile,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
tsconfigRootDir: __dirname
}
}
}
];
20 changes: 20 additions & 0 deletions build-tests/rush-package-manager-integration-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "rush-package-manager-integration-test",
"version": "1.0.0",
"private": true,
"description": "Integration tests for non-pnpm package managers in Rush.",
"license": "MIT",
"scripts": {
"_phase:build": "heft build --clean",
"build": "heft build --clean",
"test": "node lib/runTests.js"
},
"devDependencies": {
"@microsoft/rush": "workspace:*",
"@microsoft/rush-lib": "workspace:*",
"@rushstack/heft": "workspace:*",
"@rushstack/node-core-library": "workspace:*",
"@rushstack/terminal": "workspace:*",
"local-node-rig": "workspace:*"
}
}
205 changes: 205 additions & 0 deletions build-tests/rush-package-manager-integration-test/src/TestHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import * as path from 'node:path';
import type * as child_process from 'node:child_process';

import { FileSystem, Executable, JsonFile, type JsonObject } from '@rushstack/node-core-library';
import type { ITerminal } from '@rushstack/terminal';

/**
* Helper class for running integration tests with Rush package managers
*/
export class TestHelper {
private readonly _rushBinPath: string;
private readonly _terminal: ITerminal;

public constructor(terminal: ITerminal) {
this._terminal = terminal;
// Resolve rush bin path from @microsoft/rush dependency
this._rushBinPath = require.resolve('@microsoft/rush/lib/start-dev');
}

/**
* Execute a Rush command using the locally-built Rush
*/
public async executeRushAsync(args: string[], workingDirectory: string): Promise<void> {
this._terminal.writeLine(`Executing: ${process.argv0} ${this._rushBinPath} ${args.join(' ')}`);

const childProcess: child_process.ChildProcess = Executable.spawn(
process.argv0,
[this._rushBinPath, ...args],
{
currentWorkingDirectory: workingDirectory,
stdio: 'inherit'
}
);

await Executable.waitForExitAsync(childProcess, {
throwOnNonZeroExitCode: true
});
}

/**
* Create a test Rush repository with the specified package manager
*/
public async createTestRepoAsync(
testRepoPath: string,
packageManagerType: 'npm' | 'yarn',
packageManagerVersion: string
): Promise<void> {
// Clean up previous test run and create empty test repo directory
this._terminal.writeLine(`Creating test repository at ${testRepoPath}...`);
await FileSystem.ensureEmptyFolderAsync(testRepoPath);

// Initialize Rush repo
this._terminal.writeLine('Initializing Rush repo...');
await this.executeRushAsync(['init'], testRepoPath);

// Configure rush.json for the specified package manager
this._terminal.writeLine(`Configuring rush.json for ${packageManagerType} mode...`);
const rushJsonPath: string = path.join(testRepoPath, 'rush.json');
const rushJson: JsonObject = await JsonFile.loadAsync(rushJsonPath);

// Update package manager configuration
if (packageManagerType === 'npm') {
delete rushJson.pnpmVersion;
delete rushJson.yarnVersion;
rushJson.npmVersion = packageManagerVersion;
} else if (packageManagerType === 'yarn') {
delete rushJson.pnpmVersion;
delete rushJson.npmVersion;
rushJson.yarnVersion = packageManagerVersion;
}

// Add test projects
rushJson.projects = [
{
packageName: 'test-project-a',
projectFolder: 'projects/test-project-a'
},
{
packageName: 'test-project-b',
projectFolder: 'projects/test-project-b'
}
];

// Update nodeSupportedVersionRange to match current environment
rushJson.nodeSupportedVersionRange = '>=18.0.0';

await JsonFile.saveAsync(rushJson, rushJsonPath, { updateExistingFile: true });
}

/**
* Create a test project with the specified configuration
*/
public async createTestProjectAsync(
testRepoPath: string,
projectName: string,
version: string,
dependencies: Record<string, string>,
buildScript: string
): Promise<void> {
const projectPath: string = path.join(testRepoPath, 'projects', projectName);
await FileSystem.ensureFolderAsync(projectPath);

const packageJson: JsonObject = {
name: projectName,
version: version,
main: 'lib/index.js',
scripts: {
build: buildScript
},
dependencies: dependencies
};

await JsonFile.saveAsync(packageJson, path.join(projectPath, 'package.json'));
}

/**
* Verify that temp project tarballs were created
*/
public async verifyTempTarballsAsync(testRepoPath: string, projectNames: string[]): Promise<void> {
this._terminal.writeLine('\nVerifying temp project tarballs were created...');
for (const projectName of projectNames) {
const tarballPath: string = path.join(testRepoPath, 'common/temp/projects', `${projectName}.tgz`);
if (!(await FileSystem.existsAsync(tarballPath))) {
throw new Error(`ERROR: ${projectName}.tgz was not created!`);
}
}
this._terminal.writeLine('✓ Temp project tarballs created successfully');
}

/**
* Verify that dependencies are installed correctly
*/
public async verifyDependenciesAsync(
testRepoPath: string,
projectName: string,
expectedDependencies: string[]
): Promise<void> {
this._terminal.writeLine('\nVerifying node_modules structure...');
const projectPath: string = path.join(testRepoPath, 'projects', projectName);
const projectNodeModules: string = path.join(projectPath, 'node_modules');

for (const dep of expectedDependencies) {
const depPath: string = path.join(projectNodeModules, dep);
if (!(await FileSystem.existsAsync(depPath))) {
throw new Error(`ERROR: ${dep} not found in ${projectName}!`);
}

// Verify symlinks resolve correctly for local dependencies
if (dep.startsWith('test-project-')) {
const depRealPath: string = await FileSystem.getRealPathAsync(depPath);
const expectedRealPath: string = path.join(testRepoPath, 'projects', dep);
if (depRealPath !== expectedRealPath) {
throw new Error(
`ERROR: Symlink for ${dep} does not resolve correctly!\n` +
`Expected: ${expectedRealPath}\n` +
`Actual: ${depRealPath}`
);
}
}
}
this._terminal.writeLine('✓ Dependencies installed correctly');
}

/**
* Verify that build outputs were created
*/
public async verifyBuildOutputsAsync(testRepoPath: string, projectNames: string[]): Promise<void> {
this._terminal.writeLine('\nVerifying build outputs...');
for (const projectName of projectNames) {
const outputPath: string = path.join(testRepoPath, 'projects', projectName, 'lib/index.js');
if (!(await FileSystem.existsAsync(outputPath))) {
throw new Error(`ERROR: ${projectName} build output not found!`);
}
}
this._terminal.writeLine('✓ Build completed successfully');
}

/**
* Test that the built code executes correctly
*/
public async testBuiltCodeAsync(testRepoPath: string, projectName: string): Promise<void> {
this._terminal.writeLine('\nTesting built code...');
const projectLib: string = path.join(testRepoPath, 'projects', projectName, 'lib/index.js');

// Use forward slashes for require() path on all platforms
const projectLibPosix: string = projectLib.split(path.sep).join(path.posix.sep);

// Use Executable.spawnSync to capture output
const result: string = Executable.spawnSync(
process.argv0,
['-e', `const b = require('${projectLibPosix}'); console.log(b.test());`],
{
currentWorkingDirectory: testRepoPath
}
).stdout.toString();

if (!result.includes('Using: Hello from A')) {
throw new Error('ERROR: Built code did not execute as expected!');
}
this._terminal.writeLine('✓ Built code executes correctly');
}
}
Loading