Skip to content

Commit

Permalink
Adding bunch of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemanja Tesic committed Oct 15, 2024
1 parent a42ca36 commit 9d6d50b
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 31 deletions.
68 changes: 44 additions & 24 deletions src/__e2e__/publishE2E.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { publish } from '../commands/publish';
import { getDefaultOptions } from '../options/getDefaultOptions';
import { BeachballOptions } from '../types/BeachballOptions';
import { initNpmMock } from '../__fixtures__/mockNpm';
import os from 'os';

// Spawning actual npm to run commands against a fake registry is extremely slow, so mock it for
// this test (packagePublish covers the more complete npm registry scenario).
Expand All @@ -21,6 +22,7 @@ import { initNpmMock } from '../__fixtures__/mockNpm';
jest.mock('../packageManager/npm');

describe('publish command (e2e)', () => {
const concurrencyValues = [[1],[os.cpus().length]];
initNpmMock();

let repositoryFactory: RepositoryFactory | undefined;
Expand Down Expand Up @@ -55,11 +57,11 @@ describe('publish command (e2e)', () => {
repo = undefined;
});

it('can perform a successful npm publish', async () => {
it.each(concurrencyValues)('can perform a successful npm publish, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions();
const options = getOptions({ concurrency: concurrency });

generateChangeFiles(['foo'], options);
repo.push();
Expand All @@ -77,11 +79,14 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual(['foo_v1.1.0']);
});

it('can perform a successful npm publish in detached HEAD', async () => {
it.each(concurrencyValues)('can perform a successful npm publish in detached HEAD, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions({ push: false });
const options = getOptions({
push: false,
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
repo.push();
Expand All @@ -97,11 +102,11 @@ describe('publish command (e2e)', () => {
});
});

it('can perform a successful npm publish from a race condition', async () => {
it.each(concurrencyValues)('can perform a successful npm publish from a race condition, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions();
const options = getOptions({ concurrency: concurrency });

generateChangeFiles(['foo'], options);
repo.push();
Expand Down Expand Up @@ -138,11 +143,11 @@ describe('publish command (e2e)', () => {
expect(fetchCount).toBe(2);
});

it('can perform a successful npm publish from a race condition in the dependencies', async () => {
it.each(concurrencyValues)('can perform a successful npm publish from a race condition in the dependencies, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions();
const options = getOptions({ concurrency: concurrency });

generateChangeFiles(['foo'], options);
repo.push();
Expand Down Expand Up @@ -186,7 +191,7 @@ describe('publish command (e2e)', () => {
expect(contents.dependencies.baz).toBeUndefined();
});

it('can perform a successful npm publish without bump', async () => {
it.each(concurrencyValues)('can perform a successful npm publish without bump, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

Expand All @@ -209,11 +214,11 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual([]);
});

it('publishes only changed packages in a monorepo', async () => {
it.each(concurrencyValues)('publishes only changed packages in a monorepo, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();

const options = getOptions();
const options = getOptions({ concurrency: concurrency });

generateChangeFiles(['foo'], options);
repo.push();
Expand All @@ -233,11 +238,11 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual(['foo_v1.1.0']);
});

it('publishes dependent packages in a monorepo', async () => {
it.each(concurrencyValues)('publishes dependent packages in a monorepo, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();

const options = getOptions();
const options = getOptions({ concurrency: concurrency });

// bump baz => dependent bump bar => dependent bump foo
generateChangeFiles(['baz'], options);
Expand Down Expand Up @@ -272,7 +277,7 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual(['bar_v1.3.5', 'baz_v1.4.0', 'foo_v1.0.1']);
});

it('publishes new monorepo packages if requested', async () => {
it.each(concurrencyValues)('publishes new monorepo packages if requested, concurrency: %s', async (concurrency: number) => {
// use a slightly smaller fixture to only publish one extra package
repositoryFactory = new RepositoryFactory({
folders: {
Expand All @@ -281,7 +286,10 @@ describe('publish command (e2e)', () => {
});
repo = repositoryFactory.cloneRepository();

const options = getOptions({ new: true });
const options = getOptions({
new: true,
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
repo.push();
Expand All @@ -296,11 +304,14 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual(['bar_v1.3.4', 'foo_v1.1.0']);
});

it('should not perform npm publish on out-of-scope package', async () => {
it.each(concurrencyValues)('should not perform npm publish on out-of-scope package, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();

const options = getOptions({ scope: ['!packages/foo'] });
const options = getOptions({
scope: ['!packages/foo'],
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
generateChangeFiles(['bar'], options);
Expand All @@ -323,7 +334,7 @@ describe('publish command (e2e)', () => {
expect(repo.getCurrentTags()).toEqual(['bar_v1.4.0']);
});

it('should respect prepublish hooks', async () => {
it.each(concurrencyValues)('should respect prepublish hooks, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();

Expand All @@ -339,6 +350,7 @@ describe('publish command (e2e)', () => {
}
},
},
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
Expand All @@ -362,7 +374,7 @@ describe('publish command (e2e)', () => {
expect(fooPackageJson.onPublish.main).toBe('lib/index.js');
});

it('should respect postpublish hooks', async () => {
it.each(concurrencyValues)('should respect postpublish hooks, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();
let notified;
Expand All @@ -377,6 +389,7 @@ describe('publish command (e2e)', () => {
}
},
},
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
Expand All @@ -389,11 +402,14 @@ describe('publish command (e2e)', () => {
expect(notified).toBe(fooPackageJson.afterPublish.notify);
});

it('can perform a successful npm publish without fetch', async () => {
it.each(concurrencyValues)('can perform a successful npm publish without fetch, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions({ fetch: false });
const options = getOptions({
fetch: false,
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
repo.push();
Expand All @@ -418,11 +434,14 @@ describe('publish command (e2e)', () => {
expect(fetchCount).toBe(0);
});

it('should specify fetch depth when depth param is defined', async () => {
it.each(concurrencyValues)('should specify fetch depth when depth param is defined, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('single');
repo = repositoryFactory.cloneRepository();

const options = getOptions({ depth: 10 });
const options = getOptions({
depth: 10,
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);

Expand All @@ -447,7 +466,7 @@ describe('publish command (e2e)', () => {
expect(fetchCommand).toMatch('--depth=10');
});

it('calls precommit hook before committing changes', async () => {
it.each(concurrencyValues)('calls precommit hook before committing changes, concurrency: %s', async (concurrency: number) => {
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();

Expand All @@ -459,6 +478,7 @@ describe('publish command (e2e)', () => {
});
},
},
concurrency: concurrency,
});

generateChangeFiles(['foo'], options);
Expand Down
154 changes: 154 additions & 0 deletions src/__tests__/monorepo/getPackageGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { describe, expect, it } from '@jest/globals';
import { toposortPackages } from '../../publish/toposortPackages';
import { PackageInfo, PackageInfos } from '../../types/PackageInfo';
import { makePackageInfos } from '../../__fixtures__/packageInfos';
import { getPackageGraph } from '../../monorepo/getPackageGraph';

describe('getPackageGraph', () => {
/**
* @returns all package names in the package graph
*/
async function getPackageGraphPackageNames(affectedPackages: Iterable<string>, packageInfos: PackageInfos, runHook?: (packageInfo: PackageInfo) => Promise<void>): Promise<string[]> {
const visitedPackages: string[] = [];
const packageGraph = getPackageGraph(affectedPackages, packageInfos, async (packageInfo: PackageInfo) => {
visitedPackages.push(packageInfo.name);
if (runHook) {
await runHook(packageInfo);
}
});
await packageGraph.run({
concurrency: 1,
continue: false,
});

return visitedPackages;
}

/**
* Ensure that both `toposortPackages` and `getPackageGraph` are running the same logic for sorting packages.
*/
async function validateToposortPackagesAndPackageGraph(inputPackages: string[], packageInfos: PackageInfos, possibleSolutions: string[][]): Promise<void> {
const toposortPackagesOutput = (toposortPackages(inputPackages, packageInfos))
const getPackageGraphPackageNamesOutput = await getPackageGraphPackageNames(inputPackages, packageInfos);

expect(possibleSolutions).toContainEqual(toposortPackagesOutput);
expect(possibleSolutions).toContainEqual(getPackageGraphPackageNamesOutput);
}

it('sort packages which none of them has dependency', async () => {
const packageInfos: PackageInfos = makePackageInfos({ foo: {}, bar: {} });

await validateToposortPackagesAndPackageGraph(['foo', 'bar'], packageInfos, [
['foo', 'bar'],
['bar', 'foo'],
]);
});

it('sort packages with dependencies', async () => {
const packageInfos = makePackageInfos({
foo: {
dependencies: { foo3: '1.0.0', bar2: '1.0.0' },
},
foo3: { dependencies: { foo2: '1.0.0' } },
foo2: {},
});

await validateToposortPackagesAndPackageGraph(['foo', 'foo2', 'foo3'], packageInfos, [
['foo2', 'foo3', 'foo']
]);
});

it('sort packages with different kinds of dependencies', async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { foo3: '1.0.0' }, peerDependencies: { foo4: '1.0.0', bar: '1.0.0' } },
foo2: { dependencies: {} },
foo3: { dependencies: { foo2: '1.0.0' } },
foo4: { devDependencies: { foo2: '1.0.0' } },
});

await validateToposortPackagesAndPackageGraph(['foo', 'foo2', 'foo3', 'foo4'], packageInfos, [
['foo2', 'foo3', 'foo4', 'foo'],
['foo2', 'foo4', 'foo3', 'foo'],
]);
});

it('sort packages with all different kinds of dependencies', async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { foo3: '1.0.0' }, peerDependencies: { foo4: '1.0.0', bar: '1.0.0' } },
foo2: { dependencies: {} },
foo3: { optionalDependencies: { foo2: '1.0.0' } },
foo4: { devDependencies: { foo2: '1.0.0' } },
});

await validateToposortPackagesAndPackageGraph(['foo', 'foo2', 'foo3', 'foo4'], packageInfos, [
['foo2', 'foo3', 'foo4', 'foo']
]);
});

it('do not sort packages if it is not included', async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { foo3: '1.0.0', bar: '1.0.0' } },
foo2: {},
foo3: { dependencies: { foo2: '1.0.0' } },
});

await validateToposortPackagesAndPackageGraph(['foo', 'foo3'], packageInfos, [
['foo3', 'foo']
]);
});

it('do not sort packages if it is not included harder scenario', async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { foo3: '1.0.0', bar: '1.0.0' } },
foo2: { dependencies: { foo4: '1.0.0' } },
foo3: { dependencies: { foo2: '1.0.0' } },
foo4: { dependencies: {}},
bar: { dependencies: { foo: '1.0.0' } },
});

await validateToposortPackagesAndPackageGraph(['foo', 'foo3'], packageInfos, [
['foo3', 'foo']
]);
});

it('throws if contains circular dependencies inside affected packages', async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { bar: '1.0.0', } },
bar: { dependencies: { foo: '1.0.0' } },
});

expect(async () => {
await getPackageGraphPackageNames(['foo', 'bar'], packageInfos);
}).rejects.toThrow(/We could not find a node in the graph with no dependencies, this likely means there is a cycle including all nodes/);
});

it('throws if contains circular dependencies', () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { bar: '1.0.0', bar2: '1.0.0' } },
bar: { dependencies: { foo: '1.0.0' } },
});

expect(async () => {
await getPackageGraphPackageNames(['foo', 'bar', 'bar2'], packageInfos);
}).rejects.toThrow(/A cycle has been detected including the following nodes:\nfoo\nbar/);
});

it(`doesn't throws if graph contains circular dependencies outside affected packages`, async () => {
const packageInfos = makePackageInfos({
foo: { dependencies: { } },
bar: { dependencies: { } },
bar2: { dependencies: { bar3: '1.0.0' } },
bar3: { dependencies: { bar2: '1.0.0', bar: '1.0.0' } },
});

await getPackageGraphPackageNames(['foo', 'bar'], packageInfos)
});

it('throws if package info is missing', () => {
const packageInfos = {} as any as PackageInfos;

expect(async () => {
await getPackageGraphPackageNames(['foo', 'bar'], packageInfos)
}).rejects.toThrow(`Cannot read properties of undefined (reading 'name')`);
});
});
Loading

0 comments on commit 9d6d50b

Please sign in to comment.