From dc919a0e619f265e64e741e1bac87a8f4e4fb2af Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Thu, 4 May 2023 12:45:05 -0700 Subject: [PATCH] Reuse repository fixtures to speed up tests --- ...-e19233fc-2764-4e87-8f58-d6315c281d2d.json | 7 + jest.config.js | 1 + scripts/jestSetup.js | 7 + src/__e2e__/bump.test.ts | 192 ++++++++---------- src/__e2e__/change.test.ts | 90 ++++---- src/__e2e__/getChangedPackages.test.ts | 30 +-- src/__e2e__/publishE2E.test.ts | 68 ++++--- src/__e2e__/publishGit.test.ts | 26 ++- src/__e2e__/publishRegistry.test.ts | 44 ++-- src/__e2e__/syncE2E.test.ts | 90 ++++---- src/__fixtures__/registry.ts | 25 ++- src/__fixtures__/repository.ts | 48 ++++- src/__fixtures__/repositoryFactory.ts | 101 +++++++-- src/__fixtures__/verdaccio.js | 5 +- src/__fixtures__/verdaccioConstants.js | 5 + src/__fixtures__/verdaccioUser.js | 5 - .../changefile/readChangeFiles.test.ts | 38 ++-- .../changefile/writeChangeFiles.test.ts | 23 +-- .../changelog/writeChangelog.test.ts | 43 ++-- .../monorepo/getPackageInfos.test.ts | 44 ++-- .../monorepo/getScopedPackages.test.ts | 18 +- src/__functional__/options/getOptions.test.ts | 17 +- .../validation/areChangeFilesDeleted.test.ts | 20 +- .../validation/isChangeFileNeeded.test.ts | 15 +- src/options/getRepoOptions.ts | 2 +- src/packageManager/listPackageVersions.ts | 10 +- src/packageManager/packagePublish.ts | 3 +- yarn.lock | 6 +- 28 files changed, 559 insertions(+), 424 deletions(-) create mode 100644 change/beachball-e19233fc-2764-4e87-8f58-d6315c281d2d.json create mode 100644 scripts/jestSetup.js create mode 100644 src/__fixtures__/verdaccioConstants.js delete mode 100644 src/__fixtures__/verdaccioUser.js diff --git a/change/beachball-e19233fc-2764-4e87-8f58-d6315c281d2d.json b/change/beachball-e19233fc-2764-4e87-8f58-d6315c281d2d.json new file mode 100644 index 000000000..88cc0b2e4 --- /dev/null +++ b/change/beachball-e19233fc-2764-4e87-8f58-d6315c281d2d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Add an internal mechanism to disable caching for tests", + "packageName": "beachball", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/jest.config.js b/jest.config.js index dc2852b59..616443d6c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,7 @@ /** @type {import('@jest/types').Config.InitialProjectOptions} */ const commonOptions = { roots: ['/src'], + setupFilesAfterEnv: ['/scripts/jestSetup.js'], transform: { '^.+\\.tsx?$': 'ts-jest', }, diff --git a/scripts/jestSetup.js b/scripts/jestSetup.js new file mode 100644 index 000000000..b786ba540 --- /dev/null +++ b/scripts/jestSetup.js @@ -0,0 +1,7 @@ +// @ts-check +// Disable caching in workspace-tools to prevent interference with tests that reuse directories +// with potentially different contents. +require('workspace-tools').setCachingEnabled(false); + +// Disable caching in beachball +process.env.BEACHBALL_DISABLE_CACHE = '1'; diff --git a/src/__e2e__/bump.test.ts b/src/__e2e__/bump.test.ts index 5a8cd5f73..4b981536d 100644 --- a/src/__e2e__/bump.test.ts +++ b/src/__e2e__/bump.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, afterEach } from '@jest/globals'; +import { describe, expect, it, afterEach, beforeAll, afterAll } from '@jest/globals'; import fs from 'fs-extra'; import path from 'path'; import { generateChangeFiles, getChangeFiles } from '../__fixtures__/changeFiles'; @@ -10,28 +10,48 @@ import { getPackageInfos } from '../monorepo/getPackageInfos'; import { BeachballOptions } from '../types/BeachballOptions'; describe('version bumping', () => { - let repositoryFactory: RepositoryFactory | undefined; + /** Factories used in multiple tests */ + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory('monorepo'), + multiWorkspace: new RepositoryFactory('multi-workspace'), + monorepoSinglePackage: new RepositoryFactory({ + folders: { + packages: { 'pkg-1': { version: '1.0.0' } }, + }, + }), + monorepoMultiDepTypes: new RepositoryFactory({ + folders: { + packages: { + 'pkg-1': { version: '1.0.0' }, + 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, + 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, + 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, + }, + }, + }), + }; + let factory: RepositoryFactory | undefined; initMockLogs(); + beforeAll(() => { + RepositoryFactory.initAll(factories); + }); + afterEach(() => { - if (repositoryFactory) { - repositoryFactory.cleanUp(); - repositoryFactory = undefined; - } + RepositoryFactory.resetOrCleanUp(factory, factories); + factory = undefined; + }); + + afterAll(() => { + RepositoryFactory.cleanUpAll(factories); }); it('bumps only packages with change files', async () => { - const monorepo: RepoFixture['folders'] = { - packages: { - 'pkg-1': { version: '1.0.0' }, - 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, - 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, - 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, - }, - }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoMultiDepTypes; + const monorepo = factory.fixture.folders!; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -55,9 +75,9 @@ describe('version bumping', () => { }); it('for multi-workspace (multi-monorepo), only bumps packages in the current workspace', async () => { - repositoryFactory = new RepositoryFactory('multi-workspace'); - expect(Object.keys(repositoryFactory.fixtures)).toEqual(['workspace-a', 'workspace-b']); - const repo = repositoryFactory.cloneRepository(); + factory = factories.multiWorkspace; + expect(Object.keys(factory.fixtures)).toEqual(['workspace-a', 'workspace-b']); + const repo = factory.defaultRepo; const workspaceARoot = repo.pathTo('workspace-a'); const workspaceBRoot = repo.pathTo('workspace-b'); @@ -88,8 +108,9 @@ describe('version bumping', () => { 'pkg-3': { version: '1.0.0' }, }, }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory({ folders: monorepo }); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -117,16 +138,8 @@ describe('version bumping', () => { }); it('bumps all dependent packages with `bumpDeps` flag', async () => { - const monorepo: RepoFixture['folders'] = { - packages: { - 'pkg-1': { version: '1.0.0' }, - 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, - 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, - 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, - }, - }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoMultiDepTypes; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -161,8 +174,9 @@ describe('version bumping', () => { 'pkg-4': { version: '1.0.0' }, }, }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory({ folders: monorepo }); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -199,8 +213,9 @@ describe('version bumping', () => { unrelated: { version: '1.0.0' }, }, }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory({ folders: monorepo }); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles([{ packageName: 'commonlib', dependentChangeType: 'minor' }], repo.rootPath); @@ -227,9 +242,9 @@ describe('version bumping', () => { }); it('should not bump out-of-scope package even if package has change', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const monorepo = repositoryFactory.fixture.folders!; - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepo; + const monorepo = factory.fixture.folders!; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -250,9 +265,9 @@ describe('version bumping', () => { }); it('should not bump out-of-scope package and its dependencies even if dependency of the package has change', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const monorepo = repositoryFactory.fixture.folders!; - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepo; + const monorepo = factory.fixture.folders!; + const repo = factory.defaultRepo; generateChangeFiles([{ packageName: 'bar', type: 'patch' }], repo.rootPath); @@ -274,16 +289,9 @@ describe('version bumping', () => { }); it('bumps all packages and keeps change files with `keep-change-files` flag', async () => { - const monorepo: RepoFixture['folders'] = { - packages: { - 'pkg-1': { version: '1.0.0' }, - 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, - 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, - 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, - }, - }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoMultiDepTypes; + const monorepo = factory.fixture.folders!; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -311,16 +319,8 @@ describe('version bumping', () => { }); it('bumps all packages and uses prefix in the version', async () => { - const monorepo: RepoFixture['folders'] = { - packages: { - 'pkg-1': { version: '1.0.0' }, - 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, - 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, - 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, - }, - }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoMultiDepTypes; + const repo = factory.defaultRepo; generateChangeFiles([{ packageName: 'pkg-1', type: 'prerelease' }], repo.rootPath); @@ -349,16 +349,8 @@ describe('version bumping', () => { }); it('bumps all packages and uses prefixed versions in dependents', async () => { - const monorepo: RepoFixture['folders'] = { - packages: { - 'pkg-1': { version: '1.0.0' }, - 'pkg-2': { version: '1.0.0', dependencies: { 'pkg-1': '1.0.0' } }, - 'pkg-3': { version: '1.0.0', devDependencies: { 'pkg-2': '1.0.0' } }, - 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, - }, - }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoMultiDepTypes; + const repo = factory.defaultRepo; generateChangeFiles( [{ packageName: 'pkg-1', type: 'prerelease', dependentChangeType: 'prerelease' }], @@ -398,8 +390,9 @@ describe('version bumping', () => { 'pkg-4': { version: '1.0.0', peerDependencies: { 'pkg-3': '1.0.0' } }, }, }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory({ folders: monorepo }); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles( [{ packageName: 'pkg-1', type: 'prerelease', dependentChangeType: 'prerelease' }], @@ -438,8 +431,9 @@ describe('version bumping', () => { package2: { version: '0.0.1', dependencies: { package1: '^0.0.1' } }, }, }; - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory({ folders: monorepo }); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles( [ @@ -470,12 +464,8 @@ describe('version bumping', () => { }); it('calls sync prebump hook before packages are bumped', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -503,12 +493,8 @@ describe('version bumping', () => { }); it('calls async prebump hook before packages are bumped', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -536,12 +522,8 @@ describe('version bumping', () => { }); it('propagates prebump hook exceptions', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -561,12 +543,8 @@ describe('version bumping', () => { }); it('calls sync postbump hook before packages are bumped', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -594,12 +572,8 @@ describe('version bumping', () => { }); it('calls async postbump hook before packages are bumped', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); @@ -627,12 +601,8 @@ describe('version bumping', () => { }); it('propagates postbump hook exceptions', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { 'pkg-1': { version: '1.0.0' } }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepoSinglePackage; + const repo = factory.defaultRepo; generateChangeFiles(['pkg-1'], repo.rootPath); diff --git a/src/__e2e__/change.test.ts b/src/__e2e__/change.test.ts index 714e0f8e0..a0de76706 100644 --- a/src/__e2e__/change.test.ts +++ b/src/__e2e__/change.test.ts @@ -1,16 +1,15 @@ -import { describe, expect, it, afterEach, jest, beforeEach } from '@jest/globals'; +import { describe, expect, it, afterEach, jest, beforeEach, beforeAll, afterAll } from '@jest/globals'; import fs from 'fs-extra'; import type prompts from 'prompts'; import { getChangeFiles } from '../__fixtures__/changeFiles'; import { initMockLogs } from '../__fixtures__/mockLogs'; -import { RepoFixture, RepositoryFactory } from '../__fixtures__/repositoryFactory'; +import { RepositoryFactory } from '../__fixtures__/repositoryFactory'; import { change } from '../commands/change'; import { BeachballOptions } from '../types/BeachballOptions'; import { defaultBranchName } from '../__fixtures__/gitDefaults'; import { MockStdout } from '../__fixtures__/mockStdout'; import { MockStdin } from '../__fixtures__/mockStdin'; import { ChangeFileInfo } from '../types/ChangeInfo'; -import { Repository } from '../__fixtures__/repository'; // prompts writes to stdout (not console) in a way that can't really be mocked with spies, // so instead we inject a custom mock stdout stream, as well as stdin for entering answers @@ -42,23 +41,31 @@ jest.mock('../options/getDefaultOptions', () => ({ /** Wait for the prompt to finish rendering (simulates real user input) */ const waitForPrompt = () => new Promise(resolve => process.nextTick(resolve)); -const monorepo: RepoFixture['folders'] = { - packages: { 'pkg-1': { version: '1.0.0' }, 'pkg-2': { version: '1.0.0' }, 'pkg-3': { version: '1.0.0' } }, -}; - -function makeMonorepoChanges(repo: Repository) { - repo.checkout('-b', 'test'); - repo.stageChange('packages/pkg-1/file.js'); - repo.commitAll('commit 1'); - repo.stageChange('packages/pkg-2/file.js'); - repo.commitAll('commit 2'); -} - describe('change command', () => { - let repositoryFactory: RepositoryFactory | undefined; - + /** Factories used in multiple tests. */ + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory({ + folders: { + packages: { 'pkg-1': { version: '1.0.0' }, 'pkg-2': { version: '1.0.0' }, 'pkg-3': { version: '1.0.0' } }, + }, + }), + }; + let factory: RepositoryFactory | undefined; const logs = initMockLogs(); + function makeMonorepoChanges() { + factories.monorepo.defaultRepo.checkout('-b', 'test'); + factories.monorepo.defaultRepo.stageChange('packages/pkg-1/file.js'); + factories.monorepo.defaultRepo.commitAll('commit 1'); + factories.monorepo.defaultRepo.stageChange('packages/pkg-2/file.js'); + factories.monorepo.defaultRepo.commitAll('commit 2'); + } + + beforeAll(() => { + RepositoryFactory.initAll(factories); + }); + beforeEach(() => { stdin = new MockStdin(); stdout = new MockStdout({ replace: 'prompts' }); @@ -67,14 +74,18 @@ describe('change command', () => { afterEach(() => { stdin.destroy(); stdout.destroy(); - repositoryFactory?.cleanUp(); - repositoryFactory = undefined; + RepositoryFactory.resetOrCleanUp(factory, factories); + factory = undefined; mockBeachballOptions = undefined; }); + afterAll(() => { + RepositoryFactory.cleanUpAll(factories); + }); + it('does not create change files when there are no changes', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; await change({ path: repo.rootPath, branch: defaultBranchName } as BeachballOptions); @@ -82,8 +93,8 @@ describe('change command', () => { }); it('creates and stages a change file', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; repo.checkout('-b', 'test'); repo.commitChange('file.js'); @@ -115,8 +126,8 @@ describe('change command', () => { }); it('creates and commits a change file', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; repo.checkout('-b', 'test'); repo.commitChange('file.js'); @@ -141,11 +152,11 @@ describe('change command', () => { }); it('creates a change file when there are no changes but package name is provided', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; const changePromise = change({ - package: repositoryFactory.fixture.rootPackage!.name, + package: factory.fixture.rootPackage!.name, path: repo.rootPath, branch: defaultBranchName, commit: false, @@ -164,11 +175,12 @@ describe('change command', () => { }); it('creates and commits change files for multiple packages', async () => { - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); - makeMonorepoChanges(repo); + factory = factories.monorepo; + makeMonorepoChanges(); + const repo = factory.defaultRepo; const changePromise = change({ path: repo.rootPath, branch: defaultBranchName } as BeachballOptions); + await waitForPrompt(); // use custom values for first package expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1'); @@ -203,15 +215,16 @@ describe('change command', () => { }); it('creates and commits grouped change file for multiple packages', async () => { - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); - makeMonorepoChanges(repo); + factory = factories.monorepo; + makeMonorepoChanges(); + const repo = factory.defaultRepo; const changePromise = change({ path: repo.rootPath, branch: defaultBranchName, groupChanges: true, } as BeachballOptions); + await waitForPrompt(); // use custom values for first package expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1'); @@ -238,10 +251,11 @@ describe('change command', () => { }); it('uses custom per-package prompt', async () => { - repositoryFactory = new RepositoryFactory({ folders: monorepo }); - const repo = repositoryFactory.cloneRepository(); - makeMonorepoChanges(repo); + factory = factories.monorepo; + makeMonorepoChanges(); + const repo = factory.defaultRepo; + // custom prompt for different packages (only truly doable here because elsewhere it uses combinedOptions) mockBeachballOptions = { changeFilePrompt: { changePrompt: (defaultPrompt, pkg) => { @@ -284,6 +298,4 @@ describe('change command', () => { expect.objectContaining({ packageName: 'pkg-2', type: 'patch', comment: 'commit 2', custom: 'stuff' }), ]); }); - - // custom prompt for different packages (only truly doable here because elsewhere it uses combinedOptions) }); diff --git a/src/__e2e__/getChangedPackages.test.ts b/src/__e2e__/getChangedPackages.test.ts index be303215d..26ccd0289 100644 --- a/src/__e2e__/getChangedPackages.test.ts +++ b/src/__e2e__/getChangedPackages.test.ts @@ -6,18 +6,19 @@ import { BeachballOptions } from '../types/BeachballOptions'; import { getChangedPackages } from '../changefile/getChangedPackages'; describe('getChangedPackages', () => { - let repositoryFactory: RepositoryFactory | undefined; + let factory: RepositoryFactory | undefined; afterEach(() => { - if (repositoryFactory) { - repositoryFactory.cleanUp(); - repositoryFactory = undefined; + if (factory) { + factory.cleanUp(); + factory = undefined; } }); it('detects changed files in single repo', () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('single'); + factory.init(); + const repo = factory.defaultRepo; const options = { fetch: false, path: repo.rootPath, branch: defaultBranchName } as BeachballOptions; const packageInfos = getPackageInfos(repo.rootPath); @@ -28,8 +29,9 @@ describe('getChangedPackages', () => { }); it('respects ignorePatterns option', () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('single'); + factory.init(); + const repo = factory.defaultRepo; const options = { fetch: false, path: repo.rootPath, @@ -46,8 +48,9 @@ describe('getChangedPackages', () => { }); it('detects changed files in monorepo', () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('monorepo'); + factory.init(); + const repo = factory.defaultRepo; const options = { fetch: false, path: repo.rootPath, branch: defaultBranchName } as BeachballOptions; const packageInfos = getPackageInfos(repo.rootPath); @@ -58,10 +61,11 @@ describe('getChangedPackages', () => { }); it('detects changed files in multi-monorepo (multi-workspace) repo', () => { - repositoryFactory = new RepositoryFactory('multi-workspace'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('multi-workspace'); + factory.init(); + const repo = factory.defaultRepo; const rootOptions = { fetch: false, branch: defaultBranchName, path: repo.rootPath } as BeachballOptions; - expect(Object.keys(repositoryFactory.fixtures)).toEqual(['workspace-a', 'workspace-b']); + expect(Object.keys(factory.fixtures)).toEqual(['workspace-a', 'workspace-b']); const workspaceARoot = repo.pathTo('workspace-a'); const workspaceBRoot = repo.pathTo('workspace-b'); diff --git a/src/__e2e__/publishE2E.test.ts b/src/__e2e__/publishE2E.test.ts index aa5efe0ef..1060d5b6f 100644 --- a/src/__e2e__/publishE2E.test.ts +++ b/src/__e2e__/publishE2E.test.ts @@ -15,10 +15,15 @@ import { BeachballOptions } from '../types/BeachballOptions'; describe('publish command (e2e)', () => { let registry: Registry; - let repositoryFactory: RepositoryFactory | undefined; + /** Factories used in multiple tests. */ + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory('monorepo'), + }; + let factory: RepositoryFactory | undefined; // show error logs for these tests - initMockLogs(['error']); + initMockLogs({ alsoLog: ['error'] }); function getOptions(repo: Repository, overrides?: Partial): BeachballOptions { return { @@ -38,10 +43,12 @@ describe('publish command (e2e)', () => { beforeAll(() => { registry = new Registry(__filename); jest.setTimeout(30000); + RepositoryFactory.initAll(factories); }); afterAll(() => { registry.stop(); + RepositoryFactory.cleanUpAll(factories); }); beforeEach(async () => { @@ -51,15 +58,13 @@ describe('publish command (e2e)', () => { afterEach(() => { clearGitObservers(); - if (repositoryFactory) { - repositoryFactory.cleanUp(); - repositoryFactory = undefined; - } + RepositoryFactory.resetOrCleanUp(factory, factories); + factory = undefined; }); it('can perform a successful npm publish', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -80,8 +85,8 @@ describe('publish command (e2e)', () => { }); it('can perform a successful npm publish in detached HEAD', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -100,8 +105,8 @@ describe('publish command (e2e)', () => { }); it('can perform a successful npm publish from a race condition', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -113,7 +118,7 @@ describe('publish command (e2e)', () => { addGitObserver((args, output) => { if (args[0] === 'fetch') { if (fetchCount === 0) { - const anotherRepo = repositoryFactory!.cloneRepository(); + const anotherRepo = factory!.cloneRepository(); // inject a checkin anotherRepo.updateJsonFile('package.json', { version: '1.0.2' }); anotherRepo.push(); @@ -141,8 +146,8 @@ describe('publish command (e2e)', () => { }); it('can perform a successful npm publish from a race condition in the dependencies', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -154,7 +159,7 @@ describe('publish command (e2e)', () => { addGitObserver((args, output) => { if (args[0] === 'fetch') { if (fetchCount === 0) { - const anotherRepo = repositoryFactory!.cloneRepository(); + const anotherRepo = factory!.cloneRepository(); // inject a checkin const packageJsonFile = anotherRepo.pathTo('package.json'); const contents = fs.readJSONSync(packageJsonFile, 'utf-8'); @@ -189,8 +194,8 @@ describe('publish command (e2e)', () => { }); it('can perform a successful npm publish without bump', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -211,8 +216,9 @@ describe('publish command (e2e)', () => { }); it('should not perform npm publish on out-of-scope package', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('monorepo'); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); generateChangeFiles(['bar'], repo.rootPath); @@ -238,8 +244,9 @@ describe('publish command (e2e)', () => { }); it('should respect prepublish hooks', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('monorepo'); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -279,8 +286,9 @@ describe('publish command (e2e)', () => { }); it('should respect postpublish hooks', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('monorepo'); + factory.init(); + const repo = factory.defaultRepo; let notified; generateChangeFiles(['foo'], repo.rootPath); @@ -308,8 +316,8 @@ describe('publish command (e2e)', () => { }); it('can perform a successful npm publish without fetch', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -338,8 +346,8 @@ describe('publish command (e2e)', () => { }); it('should specify fetch depth when depth param is defined', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -368,8 +376,8 @@ describe('publish command (e2e)', () => { }); it('calls precommit hook before committing changes', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = factories.monorepo; + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); diff --git a/src/__e2e__/publishGit.test.ts b/src/__e2e__/publishGit.test.ts index 3d48e1d6f..462420bf6 100644 --- a/src/__e2e__/publishGit.test.ts +++ b/src/__e2e__/publishGit.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, beforeEach, afterEach, jest } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterEach, jest, afterAll } from '@jest/globals'; import fs from 'fs-extra'; import { defaultRemoteBranchName } from '../__fixtures__/gitDefaults'; import { generateChangeFiles, getChangeFiles } from '../__fixtures__/changeFiles'; @@ -32,7 +32,7 @@ function getOptions(repo: Repository, overrides?: Partial): Be } describe('publish command (git)', () => { - let repositoryFactory: RepositoryFactory; + const factory = new RepositoryFactory('single'); initMockLogs(); @@ -40,16 +40,20 @@ describe('publish command (git)', () => { jest.setTimeout(30000); }); - beforeEach(() => { - repositoryFactory = new RepositoryFactory('single'); + beforeAll(() => { + factory.init(); }); afterEach(() => { - repositoryFactory.cleanUp(); + factory.reset(); + }); + + afterAll(() => { + factory.cleanUp(); }); it('can perform a successful git push', async () => { - const repo = repositoryFactory.cloneRepository(); + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -57,7 +61,7 @@ describe('publish command (git)', () => { await publish(getOptions(repo)); - const newRepo = repositoryFactory.cloneRepository(); + const newRepo = factory.cloneRepository(); const packageJson = fs.readJSONSync(newRepo.pathTo('package.json')); @@ -65,8 +69,8 @@ describe('publish command (git)', () => { }); it('can handle a merge when there are change files present', async () => { - // 1. clone a new repo1, write a change file in repo1 - const repo1 = repositoryFactory.cloneRepository(); + // 1. in the default repo, create a change file and push it + const repo1 = factory.defaultRepo; generateChangeFiles(['foo'], repo1.rootPath); repo1.push(); @@ -79,7 +83,7 @@ describe('publish command (git)', () => { const bumpInfo = gatherBumpInfo(options, getPackageInfos(repo1.rootPath)); // 3. Meanwhile, in repo2, also create a new change file - const repo2 = repositoryFactory.cloneRepository(); + const repo2 = factory.cloneRepository(); generateChangeFiles(['foo2'], repo2.rootPath); repo2.push(); @@ -87,7 +91,7 @@ describe('publish command (git)', () => { await bumpAndPush(bumpInfo, publishBranch, options); // 5. In a brand new cloned repo, make assertions - const newRepo = repositoryFactory.cloneRepository(); + const newRepo = factory.cloneRepository(); const changeFiles = getChangeFiles(newRepo.rootPath); expect(changeFiles).toHaveLength(1); const changeFileContent: ChangeFileInfo = fs.readJSONSync(changeFiles[0]); diff --git a/src/__e2e__/publishRegistry.test.ts b/src/__e2e__/publishRegistry.test.ts index eb780a669..15d1ab575 100644 --- a/src/__e2e__/publishRegistry.test.ts +++ b/src/__e2e__/publishRegistry.test.ts @@ -12,10 +12,10 @@ import { BeachballOptions } from '../types/BeachballOptions'; describe('publish command (registry)', () => { let registry: Registry; - let repositoryFactory: RepositoryFactory | undefined; + let factory: RepositoryFactory | undefined; // show error logs for these tests - const logs = initMockLogs(['error']); + const logs = initMockLogs({ alsoLog: ['error'] }); function getOptions(repo: Repository, overrides: Partial): BeachballOptions { return { @@ -49,15 +49,14 @@ describe('publish command (registry)', () => { }); afterEach(() => { - if (repositoryFactory) { - repositoryFactory.cleanUp(); - repositoryFactory = undefined; - } + factory?.cleanUp(); + factory = undefined; }); it('can perform a successful npm publish', async () => { - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('single'); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -71,7 +70,7 @@ describe('publish command (registry)', () => { }); it('can perform a successful npm publish even with private packages', async () => { - repositoryFactory = new RepositoryFactory({ + factory = new RepositoryFactory({ folders: { packages: { foopkg: { version: '1.0.0', private: true }, @@ -79,7 +78,8 @@ describe('publish command (registry)', () => { }, }, }); - const repo = repositoryFactory.cloneRepository(); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foopkg'], repo.rootPath); @@ -91,7 +91,7 @@ describe('publish command (registry)', () => { }); it('can perform a successful npm publish when multiple packages changed at same time', async () => { - repositoryFactory = new RepositoryFactory({ + factory = new RepositoryFactory({ folders: { packages: { foopkg: { version: '1.0.0', dependencies: { barpkg: '^1.0.0' } }, @@ -99,7 +99,8 @@ describe('publish command (registry)', () => { }, }, }); - const repo = repositoryFactory.cloneRepository(); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foopkg', 'barpkg'], repo.rootPath); @@ -115,7 +116,7 @@ describe('publish command (registry)', () => { }); it('can perform a successful npm publish even with a non-existent package listed in the change file', async () => { - repositoryFactory = new RepositoryFactory({ + factory = new RepositoryFactory({ folders: { packages: { foopkg: { version: '1.0.0' }, @@ -123,7 +124,8 @@ describe('publish command (registry)', () => { }, }, }); - const repo = repositoryFactory.cloneRepository(); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['badname'], repo.rootPath); @@ -135,8 +137,9 @@ describe('publish command (registry)', () => { }); it('should exit publishing early if only invalid change files exist', async () => { - repositoryFactory = new RepositoryFactory('monorepo'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('monorepo'); + factory.init(); + const repo = factory.defaultRepo; repo.updateJsonFile('packages/bar/package.json', { private: true }); @@ -161,8 +164,9 @@ describe('publish command (registry)', () => { // hide the errors for this test--it's supposed to have errors, and showing them is misleading logs.init(false); - repositoryFactory = new RepositoryFactory('single'); - const repo = repositoryFactory.cloneRepository(); + factory = new RepositoryFactory('single'); + factory.init(); + const repo = factory.defaultRepo; generateChangeFiles(['foo'], repo.rootPath); @@ -177,8 +181,6 @@ describe('publish command (registry)', () => { ); await expect(publishPromise).rejects.toThrow(); - expect( - logs.mocks.log.mock.calls.some(([arg0]) => typeof arg0 === 'string' && arg0.includes('Retrying... (3/3)')) - ).toBeTruthy(); + expect(logs.getMockLines('log')).toMatch('Retrying... (3/3)'); }); }); diff --git a/src/__e2e__/syncE2E.test.ts b/src/__e2e__/syncE2E.test.ts index 20923c208..f7c908521 100644 --- a/src/__e2e__/syncE2E.test.ts +++ b/src/__e2e__/syncE2E.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, afterAll, beforeEach, afterEach, jest } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterAll, afterEach, jest, beforeEach } from '@jest/globals'; import fs from 'fs-extra'; import path from 'path'; import { defaultRemoteBranchName } from '../__fixtures__/gitDefaults'; @@ -15,7 +15,15 @@ import { getDefaultOptions } from '../options/getDefaultOptions'; import { BeachballOptions } from '../types/BeachballOptions'; describe('sync command (e2e)', () => { - let repositoryFactory: RepositoryFactory | undefined; + const factory = new RepositoryFactory({ + folders: { + packages: { + foopkg: { version: '1.0.0' }, + barpkg: { version: '2.2.0' }, + bazpkg: { version: '3.0.0' }, + }, + }, + }); let registry: Registry; const tempDirs: string[] = []; @@ -60,36 +68,26 @@ describe('sync command (e2e)', () => { beforeAll(() => { registry = new Registry(__filename); jest.setTimeout(30000); + factory.init(); }); afterAll(() => { registry.stop(); + factory.cleanUp(); }); beforeEach(async () => { await registry.reset(); }); - afterEach(() => { - if (repositoryFactory) { - repositoryFactory.cleanUp(); - repositoryFactory = undefined; - } + afterEach(async () => { + factory.reset(); tempDirs.forEach(dir => fs.removeSync(dir)); tempDirs.splice(0, tempDirs.length); }); it('can perform a successful sync', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { - foopkg: { version: '1.0.0' }, - barpkg: { version: '2.2.0' }, - bazpkg: { version: '3.0.0' }, - }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + const repo = factory.defaultRepo; const packageInfosBeforeSync = getPackageInfos(repo.rootPath); @@ -112,24 +110,15 @@ describe('sync command (e2e)', () => { }); it('can perform a successful sync using dist tag', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { - apkg: { version: '1.0.0' }, - bpkg: { version: '2.2.0' }, - cpkg: { version: '3.0.0' }, - }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + const repo = factory.defaultRepo; const packageInfosBeforeSync = getPackageInfos(repo.rootPath); - expect((await packagePublish(packageInfosBeforeSync['apkg'], registry.getUrl(), '', '')).success).toBeTruthy(); - expect((await packagePublish(packageInfosBeforeSync['bpkg'], registry.getUrl(), '', '')).success).toBeTruthy(); + expect((await packagePublish(packageInfosBeforeSync['foopkg'], registry.getUrl(), '', '')).success).toBeTruthy(); + expect((await packagePublish(packageInfosBeforeSync['barpkg'], registry.getUrl(), '', '')).success).toBeTruthy(); - const newFooInfo = createTempPackage('apkg', '2.0.0', 'beta'); - const newBarInfo = createTempPackage('bpkg', '3.0.0', 'latest'); + const newFooInfo = createTempPackage('foopkg', '2.0.0', 'beta'); + const newBarInfo = createTempPackage('barpkg', '3.0.0', 'latest'); expect((await packagePublish(newFooInfo, registry.getUrl(), '', '')).success).toBeTruthy(); expect((await packagePublish(newBarInfo, registry.getUrl(), '', '')).success).toBeTruthy(); @@ -138,36 +127,27 @@ describe('sync command (e2e)', () => { const packageInfosAfterSync = getPackageInfos(repo.rootPath); - expect(packageInfosAfterSync['apkg'].version).toEqual('2.0.0'); - expect(packageInfosAfterSync['bpkg'].version).toEqual('2.2.0'); - expect(packageInfosAfterSync['cpkg'].version).toEqual('3.0.0'); + expect(packageInfosAfterSync['foopkg'].version).toEqual('2.0.0'); + expect(packageInfosAfterSync['barpkg'].version).toEqual('2.2.0'); + expect(packageInfosAfterSync['bazpkg'].version).toEqual('3.0.0'); }); it('can perform a successful sync by forcing dist tag version', async () => { - repositoryFactory = new RepositoryFactory({ - folders: { - packages: { - epkg: { version: '1.0.0' }, - fpkg: { version: '2.2.0' }, - gpkg: { version: '3.0.0' }, - }, - }, - }); - const repo = repositoryFactory.cloneRepository(); + const repo = factory.defaultRepo; const packageInfosBeforeSync = getPackageInfos(repo.rootPath); - const epkg = packageInfosBeforeSync['epkg']; - const fpkg = packageInfosBeforeSync['fpkg']; + const foopkg = packageInfosBeforeSync['foopkg']; + const barpkg = packageInfosBeforeSync['barpkg']; - epkg.combinedOptions.tag = 'latest'; - fpkg.combinedOptions.tag = 'latest'; + foopkg.combinedOptions.tag = 'latest'; + barpkg.combinedOptions.tag = 'latest'; - expect((await packagePublish(epkg, registry.getUrl(), '', '')).success).toBeTruthy(); - expect((await packagePublish(fpkg, registry.getUrl(), '', '')).success).toBeTruthy(); + expect((await packagePublish(foopkg, registry.getUrl(), '', '')).success).toBeTruthy(); + expect((await packagePublish(barpkg, registry.getUrl(), '', '')).success).toBeTruthy(); - const newFooInfo = createTempPackage('epkg', '1.0.0-1'); - const newBarInfo = createTempPackage('fpkg', '3.0.0'); + const newFooInfo = createTempPackage('foopkg', '1.0.0-1'); + const newBarInfo = createTempPackage('barpkg', '3.0.0'); newFooInfo.combinedOptions.tag = 'prerelease'; newBarInfo.combinedOptions.tag = 'latest'; @@ -179,8 +159,8 @@ describe('sync command (e2e)', () => { const packageInfosAfterSync = getPackageInfos(repo.rootPath); - expect(packageInfosAfterSync['epkg'].version).toEqual('1.0.0-1'); - expect(packageInfosAfterSync['fpkg'].version).toEqual('2.2.0'); - expect(packageInfosAfterSync['gpkg'].version).toEqual('3.0.0'); + expect(packageInfosAfterSync['foopkg'].version).toEqual('1.0.0-1'); + expect(packageInfosAfterSync['barpkg'].version).toEqual('2.2.0'); + expect(packageInfosAfterSync['bazpkg'].version).toEqual('3.0.0'); }); }); diff --git a/src/__fixtures__/registry.ts b/src/__fixtures__/registry.ts index 804b86752..baa2251ba 100644 --- a/src/__fixtures__/registry.ts +++ b/src/__fixtures__/registry.ts @@ -2,7 +2,7 @@ import execa from 'execa'; import path from 'path'; // @ts-ignore import fp from 'find-free-port'; -import verdaccioUser from './verdaccioUser'; +import { fakeUser, runningMsg } from './verdaccioConstants'; const verdaccioApi = require.resolve('./verdaccio.js'); @@ -22,14 +22,14 @@ export class Registry { private server?: execa.ExecaChildProcess = undefined; private port?: number = undefined; private startPort: number; - private testName: string; + private testFilename: string; constructor(filename: string) { - this.testName = path.basename(filename, '.test.ts'); - if (!knownTests.includes(this.testName)) { - throw new Error(`Please add ${this.testName} to knownTests in registry.ts`); + this.testFilename = path.basename(filename, '.test.ts'); + if (!knownTests.includes(this.testFilename)) { + throw new Error(`Please add ${this.testFilename} to knownTests in registry.ts`); } - this.startPort = 4873 + knownTests.indexOf(this.testName) * portRange; + this.startPort = 4873 + knownTests.indexOf(this.testFilename) * portRange; } async start() { @@ -62,9 +62,9 @@ export class Registry { npm.stdout!.on('data', chunk => { chunk = String(chunk); if (chunk.includes('Username:')) { - npm.stdin!.write(verdaccioUser.username + '\r\n'); + npm.stdin!.write(fakeUser.username + '\r\n'); } else if (chunk.includes('Password:')) { - npm.stdin!.write(verdaccioUser.password + '\r\n'); + npm.stdin!.write(fakeUser.password + '\r\n'); } else if (chunk.includes('Email:')) { npm.stdin!.write('fake@example.com\r\n'); } @@ -93,14 +93,17 @@ export class Registry { } this.server.stdout.on('data', data => { - if (data.includes('verdaccio running')) { - console.log(`Started registry for ${this.testName} on port ${port}`); + if (data.includes(runningMsg)) { + console.log(`Started registry for ${this.testFilename} on port ${port}`); resolve(port); } }); this.server.stderr.on('data', data => { - reject(data?.toString()); + data = String(data || ''); + if (!data.includes('Debugger attached')) { + reject(data); + } }); this.server.on('error', data => { diff --git a/src/__fixtures__/repository.ts b/src/__fixtures__/repository.ts index 1cc661c94..498c2752a 100644 --- a/src/__fixtures__/repository.ts +++ b/src/__fixtures__/repository.ts @@ -1,7 +1,9 @@ +import realConsole from 'console'; +import execa from 'execa'; import path from 'path'; import * as fs from 'fs-extra'; -import { tmpdir } from './tmpdir'; import { git } from 'workspace-tools'; +import { tmpdir } from './tmpdir'; import { defaultBranchName, defaultRemoteName, setDefaultBranchName } from './gitDefaults'; /** @@ -16,12 +18,15 @@ import { defaultBranchName, defaultRemoteName, setDefaultBranchName } from './gi export class Repository { /** Root temp directory for the repo */ #root?: string; + /** Cleanup function passed in to the repo */ + #onCleanUp?: () => void; /** * Clone the given remote repo into a temp directory and configure settings that are needed * by certain tests (user name+email and default branch). */ - constructor(clonePath: string, tempDescription: string = 'repository') { + constructor(clonePath: string, tempDescription: string = 'repository', onCleanUp?: () => void) { + this.#onCleanUp = onCleanUp; this.#root = tmpdir({ prefix: `beachball-${tempDescription}-cloned-` }); this.git(['clone', clonePath, '.']); @@ -72,6 +77,13 @@ ${gitResult.stderr.toString()}`); return gitResult; } + /** Log the results of a git command to stdio (for debugging) */ + async gitConsoleLog(args: string[]) { + realConsole.log(`$ git ${args.join(' ')}`); + const res = await execa('git', args, { cwd: this.rootPath, all: true, reject: false }); + realConsole.log(res.all); + } + /** * Create (or update) and stage a file, creating the intermediate directories if necessary. * Automatically uses root path; do not pass absolute paths here. @@ -151,9 +163,34 @@ ${gitResult.stderr.toString()}`); this.git(['pull', defaultRemoteName, `HEAD:${defaultBranchName}`]); } - /** Push to the default remote and branch. */ - push() { - this.git(['push', defaultRemoteName, `HEAD:${defaultBranchName}`]); + /** Push to either the specified or default branch to the default remote. */ + push(branch?: string, force?: boolean) { + const forceArg = force ? ['-f'] : []; + this.git(['push', ...forceArg, defaultRemoteName, branch ?? `HEAD:${defaultBranchName}`]); + } + + /** Call `git clean -fdx` */ + clean() { + this.git(['clean', '-fdx']); + } + + /** + * Remove any local changes (`git clean -fdx`), check out the given or default branch if needed + * (and delete the previous branch), and reset its state to match the remote branch. + */ + resetFromOrigin(branch: string = defaultBranchName) { + this.clean(); + const currentBranch = this.git(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim(); + if (currentBranch === 'HEAD') { + // detached head + this.checkout(defaultBranchName); + } else if (currentBranch !== branch) { + // non-default branch + this.checkout(branch); + this.git(['branch', '-D', currentBranch]); + } + this.git(['fetch', defaultRemoteName]); + this.git(['reset', '--hard', `${defaultRemoteName}/${branch}`]); } /** @@ -172,6 +209,7 @@ ${gitResult.stderr.toString()}`); // This is non-fatal since the temp dir will eventually be cleaned up automatically console.warn('Could not clean up repository: ' + err); } + this.#onCleanUp?.(); this.#root = undefined; } } diff --git a/src/__fixtures__/repositoryFactory.ts b/src/__fixtures__/repositoryFactory.ts index 1f680633e..9e642b640 100644 --- a/src/__fixtures__/repositoryFactory.ts +++ b/src/__fixtures__/repositoryFactory.ts @@ -5,7 +5,7 @@ import { Repository } from './repository'; import { BeachballOptions } from '../types/BeachballOptions'; import { tmpdir } from './tmpdir'; import { gitFailFast } from 'workspace-tools'; -import { setDefaultBranchName } from './gitDefaults'; +import { defaultBranchName, defaultRemoteName, setDefaultBranchName } from './gitDefaults'; /** * Standard fixture options. See {@link getSinglePackageFixture}, {@link getMonorepoFixture} and @@ -120,6 +120,30 @@ function getMultiWorkspaceFixture() { /** Provides setup, cloning, and teardown for repository factories */ export class RepositoryFactory { + /** Call `init()` on all the factories. */ + public static initAll(reusableFactories: Record) { + Object.values(reusableFactories).forEach(factory => factory.init()); + } + + /** If `factory` is in `reusableFactories`, reset it. Otherwise clean it up. */ + public static resetOrCleanUp( + factory: RepositoryFactory | undefined, + reusableFactories: Record + ) { + if (factory) { + if (Object.values(reusableFactories).includes(factory)) { + factory.reset(); + } else { + factory.cleanUp(); + } + } + } + + /** Call `cleanUp()` on all the factories. */ + public static cleanUpAll(reusableFactories: Record) { + Object.values(reusableFactories).forEach(factory => factory.cleanUp()); + } + /** * Primary fixture for the test *(do not use for multi-workspace)*. * This is public to potentially reduce hardcoded values (such as versions) in tests. @@ -133,20 +157,36 @@ export class RepositoryFactory { */ public readonly fixtures: { [parentFolder: string]: RepoFixture }; + /** + * Default clone for this repository factory. Tests can use this for most operations that don't + * involve changing the remote URL, provided that they call `repositoryFactory.reset()` after + * each test. + */ + public get defaultRepo(): Repository { + if (!this.#defaultRepo) { + throw new Error('RepositoryFactory not initialized or already cleaned up'); + } + return this.#defaultRepo; + } + /** Root directory hosting the origin repository */ #root?: string; + /** Default clone of the repository */ + #defaultRepo?: Repository; /** Description to use in temp directory names */ #tempDescription: string; /** Cloned child repos, tracked so we can clean them up */ #childRepos: Repository[] = []; + #initialCommit: string = ''; /** - * Create the "origin" repo and create+commit fixture files. + * Create a repository factory. **You must call `factory.init()` before using the factory.** + * (This allows factories to be created as consts in the test file and initialized during `beforeAll`.) + * * If `fixture` is a string, the corresponding default fixture is used. * - * (Note that there's currently no way to create a custom multi-workspace fixture, - * because that hasn't been needed so far.) + * (There's currently no way to create a custom multi-workspace fixture, since that hasn't been needed.) */ constructor(fixture: FixtureType | RepoFixture) { this.fixtures = {}; @@ -160,6 +200,11 @@ export class RepositoryFactory { } this.#tempDescription = typeof fixture === 'string' ? fixture : fixture.tempDescription || 'custom'; + } + + /** Create the "origin" repo and create+commit fixture files. */ + init() { + if (this.#root) throw new Error('RepositoryFactory was already initialized'); // Init the "origin" repo. This repo must be "bare" (has .git but no working directory) because // we'll be pushing to and pulling from it, which would cause the working directory and the @@ -171,8 +216,8 @@ export class RepositoryFactory { // Initialize the repo contents by cloning the "origin" repo, committing the fixture files, // and pushing changes back. - const tmpRepo = new Repository(this.root); - tmpRepo.commitChange('README'); + this.#defaultRepo = this.cloneRepository(); + this.#defaultRepo.commitChange('README'); // Create the fixture files. // The files are committed all together at the end to speed things up. @@ -184,7 +229,7 @@ export class RepositoryFactory { throw new Error('`fixtures` must define `rootPackage` and/or `folders`'); } - fs.ensureDirSync(tmpRepo.pathTo(parentFolder)); + fs.ensureDirSync(this.#defaultRepo.pathTo(parentFolder)); // create the root package.json let finalRootPackage = rootPackage; @@ -197,36 +242,61 @@ export class RepositoryFactory { // these paths are relative to THIS workspace and should not include the parent folder finalRootPackage.workspaces = Object.keys(folders).map(folder => `${folder}/*`); } - fs.writeJSONSync(tmpRepo.pathTo(parentFolder, 'package.json'), finalRootPackage, jsonOptions); + fs.writeJSONSync(this.#defaultRepo.pathTo(parentFolder, 'package.json'), finalRootPackage, jsonOptions); // create the lock file // (an option could be added to disable or customize this in the future if needed) - fs.writeFileSync(tmpRepo.pathTo(parentFolder, 'yarn.lock'), ''); + fs.writeFileSync(this.#defaultRepo.pathTo(parentFolder, 'yarn.lock'), ''); // create the packages for (const [folder, contents] of Object.entries(folders || {})) { for (const [name, packageJson] of Object.entries(contents)) { - const pkgFolder = tmpRepo.pathTo(parentFolder, folder, name); + const pkgFolder = this.#defaultRepo.pathTo(parentFolder, folder, name); fs.ensureDirSync(pkgFolder); fs.writeJSONSync(path.join(pkgFolder, 'package.json'), { name, ...packageJson }, jsonOptions); } } - tmpRepo.commitAll(`committing fixture ${parentFolder}`); + this.#defaultRepo.commitAll(`committing fixture ${parentFolder}`); } - tmpRepo.push(); - tmpRepo.cleanUp(); + this.#initialCommit = this.#defaultRepo.getCurrentHash(); + this.#defaultRepo.push(); } cloneRepository(): Repository { - if (!this.#root) throw new Error('Factory was already cleaned up'); + if (!this.#root) throw new Error('RepositoryFactory not initialized or already cleaned up'); - const newRepo = new Repository(this.#root, this.#tempDescription); + const newRepo = new Repository(this.#root, this.#tempDescription, () => { + const repoIndex = this.#childRepos.indexOf(newRepo); + if (repoIndex !== -1) this.#childRepos.splice(repoIndex, 1); + }); this.#childRepos.push(newRepo); return newRepo; } + /** + * Resets the default branch of the origin repo to the initial commit after fixture files were + * created, then updates the default branch in the default clone (`this.defaultRepo`) to match. + * If the default clone is on a non-default branch, that branch will be deleted. + * + * Does NOT clean up child repos, tags, or other branches (could be added in the future if needed). + */ + reset() { + if (!this.#root) throw new Error('RepositoryFactory not initialized or already cleaned up'); + + const repoOrigin = this.defaultRepo.git(['remote', 'get-url', defaultRemoteName]).stdout.toString().trim(); + if (repoOrigin === this.#root) { + this.defaultRepo.push(`${this.#initialCommit}:${defaultBranchName}`, true /*force*/); + this.defaultRepo.resetFromOrigin(defaultBranchName); + } else { + throw new Error( + 'A test has changed the remote URL for the defaultRepo. ' + + 'Tests making major config changes should use repositoryFactory.cloneRepository() instead.' + ); + } + } + /** * Clean up the factory and its repos IF this is a local build. * @@ -248,5 +318,6 @@ export class RepositoryFactory { repo.cleanUp(); } this.#childRepos = []; + this.#defaultRepo = undefined; } } diff --git a/src/__fixtures__/verdaccio.js b/src/__fixtures__/verdaccio.js index cd16b2fe3..6a2d95555 100644 --- a/src/__fixtures__/verdaccio.js +++ b/src/__fixtures__/verdaccio.js @@ -4,6 +4,7 @@ // @ts-ignore const startServer = require('verdaccio').default; const store = require('verdaccio-memory').default; +const { fakeUser, runningMsg } = require('./verdaccioConstants'); const [port, logFile] = process.argv.slice(2); if (!port) { @@ -18,7 +19,7 @@ const config = { auth: { 'auth-memory': { users: { - fake: require('./verdaccioUser'), + fake: fakeUser, }, }, }, @@ -56,7 +57,7 @@ const addr = { startServer(config, port, store, '1.0.0', 'verdaccio', (webServer, addrs) => { webServer.listen(addr.port || addr.path, addr.host, () => { // This is logged to tell whoever spawns us that we're ready. - console.log('verdaccio running'); + console.log(runningMsg); console.dir({ addrs }); }); }); diff --git a/src/__fixtures__/verdaccioConstants.js b/src/__fixtures__/verdaccioConstants.js new file mode 100644 index 000000000..7f59d2b7c --- /dev/null +++ b/src/__fixtures__/verdaccioConstants.js @@ -0,0 +1,5 @@ +// constants used by verdaccio.js and registry.ts +module.exports = { + fakeUser: { username: 'fake', password: 'fake' }, + runningMsg: 'verdaccio running', +}; diff --git a/src/__fixtures__/verdaccioUser.js b/src/__fixtures__/verdaccioUser.js deleted file mode 100644 index 83cc06647..000000000 --- a/src/__fixtures__/verdaccioUser.js +++ /dev/null @@ -1,5 +0,0 @@ -// constants used by verdaccio.js and registry.ts -module.exports = { - username: 'fake', - password: 'fake', -}; diff --git a/src/__functional__/changefile/readChangeFiles.test.ts b/src/__functional__/changefile/readChangeFiles.test.ts index ddbea7272..6e447560f 100644 --- a/src/__functional__/changefile/readChangeFiles.test.ts +++ b/src/__functional__/changefile/readChangeFiles.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals'; import _ from 'lodash'; import { generateChangeFiles } from '../../__fixtures__/changeFiles'; @@ -10,25 +10,31 @@ import { readChangeFiles } from '../../changefile/readChangeFiles'; import { BeachballOptions } from '../../types/BeachballOptions'; describe('readChangeFiles', () => { - let repositoryFactory: RepositoryFactory; - let monoRepoFactory: RepositoryFactory; + /** Factories used in multiple tests. */ + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory('monorepo'), + }; + let factory: RepositoryFactory | undefined; const logs = initMockLogs(); beforeAll(() => { - // These tests can share the same repo factories because they don't push to origin - // (the actual tests run against a clone) - repositoryFactory = new RepositoryFactory('single'); - monoRepoFactory = new RepositoryFactory('monorepo'); + RepositoryFactory.initAll(factories); + }); + + afterEach(() => { + RepositoryFactory.resetOrCleanUp(factory, factories); + factory = undefined; }); afterAll(() => { - repositoryFactory.cleanUp(); - monoRepoFactory.cleanUp(); + RepositoryFactory.cleanUpAll(factories); }); it('does not add commit hash', () => { - const repository = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repository = factory.defaultRepo; repository.commitChange('foo'); generateChangeFiles(['foo'], repository.rootPath); @@ -39,7 +45,8 @@ describe('readChangeFiles', () => { }); it('excludes invalid change files', () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.updateJsonFile('packages/bar/package.json', { private: true }); // fake doesn't exist, bar is private, foo is okay generateChangeFiles(['fake', 'bar', 'foo'], monoRepo.rootPath); @@ -55,7 +62,8 @@ describe('readChangeFiles', () => { }); it('excludes invalid changes from grouped change file', () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.updateJsonFile('packages/bar/package.json', { private: true }); // fake doesn't exist, bar is private, foo is okay generateChangeFiles(['fake', 'bar', 'foo'], monoRepo.rootPath, true /*groupChanges*/); @@ -74,7 +82,8 @@ describe('readChangeFiles', () => { }); it('excludes out of scope change files', () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; generateChangeFiles(['bar', 'foo'], monoRepo.rootPath); const packageInfos = getPackageInfos(monoRepo.rootPath); @@ -87,7 +96,8 @@ describe('readChangeFiles', () => { }); it('excludes out of scope changes from grouped change file', () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; generateChangeFiles(['bar', 'foo'], monoRepo.rootPath, true /*groupChanges*/); const packageInfos = getPackageInfos(monoRepo.rootPath); diff --git a/src/__functional__/changefile/writeChangeFiles.test.ts b/src/__functional__/changefile/writeChangeFiles.test.ts index 263b5bc3e..72f640c67 100644 --- a/src/__functional__/changefile/writeChangeFiles.test.ts +++ b/src/__functional__/changefile/writeChangeFiles.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals'; import fs from 'fs-extra'; import { initMockLogs } from '../../__fixtures__/mockLogs'; import { RepositoryFactory } from '../../__fixtures__/repositoryFactory'; @@ -19,25 +19,24 @@ function cleanChangeFilePaths(root: string, changeFiles: string[]) { } describe('writeChangeFiles', () => { - let repositoryFactory: RepositoryFactory; - let monorepoFactory: RepositoryFactory; + const factory = new RepositoryFactory('monorepo'); initMockLogs(); beforeAll(() => { - // These tests can share the same repo factories because they don't push to origin - // (the actual tests run against a clone) - repositoryFactory = new RepositoryFactory('single'); - monorepoFactory = new RepositoryFactory('monorepo'); + factory.init(); + }); + + afterEach(() => { + factory.reset(); }); afterAll(() => { - repositoryFactory.cleanUp(); - monorepoFactory.cleanUp(); + factory.cleanUp(); }); it('writes individual change files', () => { - const repo = monorepoFactory.cloneRepository(); + const repo = factory.defaultRepo; const previousHead = repo.getCurrentHash(); writeChangeFiles({ @@ -64,7 +63,7 @@ describe('writeChangeFiles', () => { }); it('respects commitChangeFiles=false', () => { - const repo = monorepoFactory.cloneRepository(); + const repo = factory.defaultRepo; const previousHead = repo.getCurrentHash(); writeChangeFiles({ @@ -88,7 +87,7 @@ describe('writeChangeFiles', () => { }); it('writes grouped change files', () => { - const repo = monorepoFactory.cloneRepository(); + const repo = factory.defaultRepo; writeChangeFiles({ changes: [{ packageName: 'foo' }, { packageName: 'bar' }] as ChangeFileInfo[], diff --git a/src/__functional__/changelog/writeChangelog.test.ts b/src/__functional__/changelog/writeChangelog.test.ts index 7e6061fe0..00eb2ac05 100644 --- a/src/__functional__/changelog/writeChangelog.test.ts +++ b/src/__functional__/changelog/writeChangelog.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals'; import { generateChangeFiles } from '../../__fixtures__/changeFiles'; import { cleanChangelogJson, readChangelogJson, readChangelogMd } from '../../__fixtures__/changelog'; import { initMockLogs } from '../../__fixtures__/mockLogs'; @@ -21,25 +21,30 @@ function getChange(packageName: string, comment: string): ChangeFileInfo { } describe('writeChangelog', () => { - let repositoryFactory: RepositoryFactory; - let monoRepoFactory: RepositoryFactory; + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory('monorepo'), + }; + let factory: RepositoryFactory | undefined; initMockLogs(); beforeAll(() => { - // These tests can share the same repo factories because they don't push to origin - // (the actual tests run against a clone) - repositoryFactory = new RepositoryFactory('single'); - monoRepoFactory = new RepositoryFactory('monorepo'); + RepositoryFactory.initAll(factories); + }); + + afterEach(() => { + factory?.reset(); + factory = undefined; }); afterAll(() => { - repositoryFactory.cleanUp(); - monoRepoFactory.cleanUp(); + RepositoryFactory.cleanUpAll(factories); }); it('generates correct changelog', async () => { - const repository = repositoryFactory.cloneRepository(); + factory = factories.singlePackage; + const repository = factory.defaultRepo; repository.commitChange('foo'); generateChangeFiles([getChange('foo', 'additional comment 2')], repository.rootPath); generateChangeFiles([getChange('foo', 'additional comment 1')], repository.rootPath); @@ -69,7 +74,8 @@ describe('writeChangelog', () => { }); it('generates correct changelog in monorepo with groupChanges (grouped change FILES)', async () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('foo'); const params = [monoRepo.rootPath, true /*groupChanges*/] as const; generateChangeFiles( @@ -106,7 +112,8 @@ describe('writeChangelog', () => { }); it('generates correct grouped changelog', async () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('foo'); generateChangeFiles([getChange('foo', 'comment 1')], monoRepo.rootPath); @@ -141,7 +148,8 @@ describe('writeChangelog', () => { }); it('generates grouped changelog without dependent change entries', async () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('baz'); generateChangeFiles([getChange('baz', 'comment 1')], monoRepo.rootPath); @@ -185,7 +193,8 @@ describe('writeChangelog', () => { }); it('generates grouped changelog without dependent change entries where packages have normal changes and dependency changes', async () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('baz'); generateChangeFiles([getChange('baz', 'comment 1')], monoRepo.rootPath); generateChangeFiles([getChange('bar', 'comment 1')], monoRepo.rootPath); @@ -223,7 +232,8 @@ describe('writeChangelog', () => { }); it('generates correct grouped changelog when grouped change log is saved to the same dir as a regular changelog', async () => { - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('foo'); generateChangeFiles([getChange('foo', 'comment 1')], monoRepo.rootPath); @@ -257,7 +267,8 @@ describe('writeChangelog', () => { it('Verify that the changeFile transform functions are run, if provided', async () => { const editedComment: string = 'Edited comment for testing'; - const monoRepo = monoRepoFactory.cloneRepository(); + factory = factories.monorepo; + const monoRepo = factory.defaultRepo; monoRepo.commitChange('foo'); generateChangeFiles([getChange('foo', 'comment 1')], monoRepo.rootPath); diff --git a/src/__functional__/monorepo/getPackageInfos.test.ts b/src/__functional__/monorepo/getPackageInfos.test.ts index 2702623f3..a2f67efb4 100644 --- a/src/__functional__/monorepo/getPackageInfos.test.ts +++ b/src/__functional__/monorepo/getPackageInfos.test.ts @@ -51,27 +51,28 @@ function getPackageNamesAndPaths(root: string, packageInfos: PackageInfos) { } describe('getPackageInfos', () => { - // factories can be reused between these tests because none of them push changes - let singleFactory: RepositoryFactory; - let monorepoFactory: RepositoryFactory; - let multiWorkspaceFactory: RepositoryFactory; + /** Factories used in multiple tests */ + const factories = { + singlePackage: new RepositoryFactory('single'), + monorepo: new RepositoryFactory('monorepo'), + multiWorkspace: new RepositoryFactory('multi-workspace'), + }; + let factory: RepositoryFactory | undefined; let tempDir: string | undefined; beforeAll(() => { - singleFactory = new RepositoryFactory('single'); - monorepoFactory = new RepositoryFactory('monorepo'); - multiWorkspaceFactory = new RepositoryFactory('multi-workspace'); + RepositoryFactory.initAll(factories); }); afterEach(() => { tempDir && fs.removeSync(tempDir); tempDir = undefined; + RepositoryFactory.resetOrCleanUp(factory, factories); + factory = undefined; }); afterAll(() => { - singleFactory.cleanUp(); - monorepoFactory.cleanUp(); - multiWorkspaceFactory.cleanUp(); + RepositoryFactory.cleanUpAll(factories); }); it('throws if run outside a git repo', () => { @@ -86,7 +87,8 @@ describe('getPackageInfos', () => { }); it('works in single-package repo', () => { - const repo = singleFactory.cloneRepository(); + factory = factories.singlePackage; + const repo = factory.defaultRepo; let packageInfos = getPackageInfos(repo.rootPath); packageInfos = cleanPackageInfos(repo.rootPath, packageInfos); expect(packageInfos).toMatchInlineSnapshot(` @@ -107,7 +109,8 @@ describe('getPackageInfos', () => { // both yarn and npm define "workspaces" in package.json it('works in yarn/npm monorepo', () => { - const repo = monorepoFactory.cloneRepository(); + factory = factories.monorepo; + const repo = factory.defaultRepo; let packageInfos = getPackageInfos(repo.rootPath); packageInfos = cleanPackageInfos(repo.rootPath, packageInfos); expect(packageInfos).toMatchInlineSnapshot(` @@ -153,7 +156,8 @@ describe('getPackageInfos', () => { }); it('works in pnpm monorepo', () => { - const repo = monorepoFactory.cloneRepository(); + factory = factories.monorepo; + const repo = factory.defaultRepo; fs.writeJSONSync(repo.pathTo('package.json'), { name: 'pnpm-monorepo', version: '1.0.0', private: true }); fs.writeFileSync(repo.pathTo('pnpm-lock.yaml'), ''); fs.writeFileSync(repo.pathTo('pnpm-workspace.yaml'), 'packages: ["packages/*", "packages/grouped/*"]'); @@ -172,7 +176,8 @@ describe('getPackageInfos', () => { }); it('works in rush monorepo', () => { - const repo = monorepoFactory.cloneRepository(); + factory = factories.monorepo; + const repo = factory.defaultRepo; fs.writeJSONSync(repo.pathTo('package.json'), { name: 'rush-monorepo', version: '1.0.0', private: true }); fs.writeJSONSync(repo.pathTo('rush.json'), { projects: [{ projectFolder: 'packages' }, { projectFolder: 'packages/grouped' }], @@ -192,7 +197,10 @@ describe('getPackageInfos', () => { }); it('works in lerna monorepo', () => { - const repo = monorepoFactory.cloneRepository(); + // factory = new RepositoryFactory('monorepo'); + // factory.init(); + factory = factories.monorepo; + const repo = factory.defaultRepo; fs.writeJSONSync(repo.pathTo('package.json'), { name: 'lerna-monorepo', version: '1.0.0', private: true }); fs.writeJSONSync(repo.pathTo('lerna.json'), { packages: ['packages/*', 'packages/grouped/*'] }); @@ -209,7 +217,8 @@ describe('getPackageInfos', () => { }); it('works multi-workspace monorepo', () => { - const repo = multiWorkspaceFactory.cloneRepository(); + factory = factories.multiWorkspace; + const repo = factory.defaultRepo; // For this test, only snapshot the package names and paths const rootPackageInfos = getPackageInfos(repo.rootPath); @@ -259,7 +268,8 @@ describe('getPackageInfos', () => { // If there are multiple workspaces in a monorepo, it's possible that two packages in different // workspaces could share the same name, which causes problems for beachball. // (This is only known to have been an issue with the test fixture, but is worth testing.) - const repo = multiWorkspaceFactory.cloneRepository(); + factory = factories.multiWorkspace; + const repo = factory.defaultRepo; repo.updateJsonFile('workspace-a/packages/foo/package.json', { name: 'foo' }); repo.updateJsonFile('workspace-b/packages/foo/package.json', { name: 'foo' }); expect(() => getPackageInfos(repo.rootPath)).toThrow(); diff --git a/src/__functional__/monorepo/getScopedPackages.test.ts b/src/__functional__/monorepo/getScopedPackages.test.ts index 8fe0617a3..99426cf3d 100644 --- a/src/__functional__/monorepo/getScopedPackages.test.ts +++ b/src/__functional__/monorepo/getScopedPackages.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; -import { Repository } from '../../__fixtures__/repository'; import { RepositoryFactory } from '../../__fixtures__/repositoryFactory'; import { getScopedPackages } from '../../monorepo/getScopedPackages'; import { BeachballOptions } from '../../types/BeachballOptions'; @@ -7,23 +6,22 @@ import { PackageInfos } from '../../types/PackageInfo'; import { getPackageInfos } from '../../monorepo/getPackageInfos'; describe('getScopedPackages', () => { - let repoFactory: RepositoryFactory; - let repo: Repository; + // These tests don't make local changes, so cleaning up after each test is unnecessary. + const factory = new RepositoryFactory('monorepo'); let packageInfos: PackageInfos; beforeAll(() => { - repoFactory = new RepositoryFactory('monorepo'); - repo = repoFactory.cloneRepository(); - packageInfos = getPackageInfos(repo.rootPath); + factory.init(); + packageInfos = getPackageInfos(factory.defaultRepo.rootPath); }); afterAll(() => { - repoFactory.cleanUp(); + factory.cleanUp(); }); it('can scope packages', () => { const scopedPackages = getScopedPackages( { - path: repo.rootPath, + path: factory.defaultRepo.rootPath, scope: ['packages/grouped/*'], } as BeachballOptions, packageInfos @@ -35,7 +33,7 @@ describe('getScopedPackages', () => { it('can scope with excluded packages', () => { const scopedPackages = getScopedPackages( { - path: repo.rootPath, + path: factory.defaultRepo.rootPath, scope: ['!packages/grouped/*'], } as BeachballOptions, packageInfos @@ -47,7 +45,7 @@ describe('getScopedPackages', () => { it('can mix and match with excluded packages', () => { const scopedPackages = getScopedPackages( { - path: repo.rootPath, + path: factory.defaultRepo.rootPath, scope: ['packages/b*', '!packages/grouped/*'], } as BeachballOptions, packageInfos diff --git a/src/__functional__/options/getOptions.test.ts b/src/__functional__/options/getOptions.test.ts index 2af4151ff..0977c820c 100644 --- a/src/__functional__/options/getOptions.test.ts +++ b/src/__functional__/options/getOptions.test.ts @@ -1,30 +1,29 @@ import { describe, expect, it, beforeAll, afterEach, afterAll } from '@jest/globals'; import fs from 'fs-extra'; -import { Repository } from '../../__fixtures__/repository'; import { RepositoryFactory } from '../../__fixtures__/repositoryFactory'; import { getOptions } from '../../options/getOptions'; const baseArgv = ['node.exe', 'bin.js']; describe('getOptions', () => { - let repositoryFactory: RepositoryFactory; - let repo: Repository; + const factory = new RepositoryFactory('single'); + let repoRoot: string; beforeAll(() => { - repositoryFactory = new RepositoryFactory('single'); - repo = repositoryFactory.cloneRepository(); + factory.init(); + repoRoot = factory.defaultRepo.rootPath; }); afterEach(() => { - repo.git(['clean', '-fdx']); + factory.reset(); }); afterAll(() => { - repositoryFactory.cleanUp(); + factory.cleanUp(); }); it('uses the branch name defined in beachball.config.js', () => { - const config = inDirectory(repo.rootPath, () => { + const config = inDirectory(repoRoot, () => { fs.writeFileSync('beachball.config.js', 'module.exports = { branch: "origin/foo" };'); return getOptions(baseArgv); }); @@ -32,7 +31,7 @@ describe('getOptions', () => { }); it('--config overrides configuration path', () => { - const config = inDirectory(repo.rootPath, () => { + const config = inDirectory(repoRoot, () => { fs.writeFileSync('beachball.config.js', 'module.exports = { branch: "origin/main" };'); fs.writeFileSync('alternate.config.js', 'module.exports = { branch: "origin/foo" };'); return getOptions([...baseArgv, '--config', 'alternate.config.js']); diff --git a/src/__functional__/validation/areChangeFilesDeleted.test.ts b/src/__functional__/validation/areChangeFilesDeleted.test.ts index 52b10f509..cac92cddd 100644 --- a/src/__functional__/validation/areChangeFilesDeleted.test.ts +++ b/src/__functional__/validation/areChangeFilesDeleted.test.ts @@ -1,34 +1,33 @@ -import { describe, expect, it, beforeAll, beforeEach, afterAll } from '@jest/globals'; +import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals'; import fs from 'fs-extra'; import { generateChangeFiles } from '../../__fixtures__/changeFiles'; import { defaultRemoteBranchName } from '../../__fixtures__/gitDefaults'; import { initMockLogs } from '../../__fixtures__/mockLogs'; -import { Repository } from '../../__fixtures__/repository'; import { RepositoryFactory } from '../../__fixtures__/repositoryFactory'; import { BeachballOptions } from '../../types/BeachballOptions'; import { areChangeFilesDeleted } from '../../validation/areChangeFilesDeleted'; import { getChangePath } from '../../paths'; describe('areChangeFilesDeleted', () => { - let repositoryFactory: RepositoryFactory; - let repository: Repository; + const factory = new RepositoryFactory('single'); initMockLogs(); beforeAll(() => { - repositoryFactory = new RepositoryFactory('single'); + factory.init(); + generateChangeFiles(['pkg-1'], factory.defaultRepo.rootPath); + factory.defaultRepo.push(); }); - beforeEach(() => { - repository = repositoryFactory.cloneRepository(); - generateChangeFiles(['pkg-1'], repository.rootPath); - repository.push(); + afterEach(() => { + factory.defaultRepo.resetFromOrigin(); }); afterAll(() => { - repositoryFactory.cleanUp(); + factory.cleanUp(); }); it('is false when no change files are deleted', () => { + const repository = factory.defaultRepo; repository.checkout('-b', 'feature-0'); const result = areChangeFilesDeleted({ @@ -39,6 +38,7 @@ describe('areChangeFilesDeleted', () => { }); it('is true when change files are deleted', () => { + const repository = factory.defaultRepo; repository.checkout('-b', 'feature-0'); const changeDirPath = getChangePath(repository.rootPath); diff --git a/src/__functional__/validation/isChangeFileNeeded.test.ts b/src/__functional__/validation/isChangeFileNeeded.test.ts index b8d61dc7c..826807892 100644 --- a/src/__functional__/validation/isChangeFileNeeded.test.ts +++ b/src/__functional__/validation/isChangeFileNeeded.test.ts @@ -1,30 +1,29 @@ import { describe, expect, it, beforeAll, beforeEach, afterAll } from '@jest/globals'; import { defaultRemoteBranchName, defaultRemoteName } from '../../__fixtures__/gitDefaults'; import { initMockLogs } from '../../__fixtures__/mockLogs'; -import { Repository } from '../../__fixtures__/repository'; import { RepositoryFactory } from '../../__fixtures__/repositoryFactory'; import { isChangeFileNeeded } from '../../validation/isChangeFileNeeded'; import { BeachballOptions } from '../../types/BeachballOptions'; import { getPackageInfos } from '../../monorepo/getPackageInfos'; describe('isChangeFileNeeded', () => { - let repositoryFactory: RepositoryFactory; - let repository: Repository; + const factory = new RepositoryFactory('single'); initMockLogs(); beforeAll(() => { - repositoryFactory = new RepositoryFactory('single'); + factory.init(); }); beforeEach(() => { - repository = repositoryFactory.cloneRepository(); + factory.reset(); }); afterAll(() => { - repositoryFactory.cleanUp(); + factory.cleanUp(); }); it('is false when no changes have been made', () => { + const repository = factory.defaultRepo; const result = isChangeFileNeeded( { branch: defaultRemoteBranchName, @@ -37,6 +36,7 @@ describe('isChangeFileNeeded', () => { }); it('is true when changes exist in a new branch', () => { + const repository = factory.defaultRepo; repository.checkout('-b', 'feature-0'); repository.commitChange('myFilename'); const result = isChangeFileNeeded( @@ -51,6 +51,7 @@ describe('isChangeFileNeeded', () => { }); it('is false when changes are CHANGELOG files', () => { + const repository = factory.defaultRepo; repository.checkout('-b', 'feature-0'); repository.commitChange('CHANGELOG.md'); const result = isChangeFileNeeded( @@ -66,7 +67,7 @@ describe('isChangeFileNeeded', () => { it('throws if the remote is invalid', () => { // make a separate clone due to messing with the remote - const repo = repositoryFactory.cloneRepository(); + const repo = factory.cloneRepository(); repo.git(['remote', 'set-url', defaultRemoteName, 'file:///__nonexistent']); repo.checkout('-b', 'feature-0'); repo.commitChange('CHANGELOG.md'); diff --git a/src/options/getRepoOptions.ts b/src/options/getRepoOptions.ts index b2d343ac5..ab2de3bdc 100644 --- a/src/options/getRepoOptions.ts +++ b/src/options/getRepoOptions.ts @@ -6,7 +6,7 @@ let cachedRepoOptions = new Map(); export function getRepoOptions(cliOptions: CliOptions): RepoOptions { const { configPath, path: cwd, branch } = cliOptions; - if (cachedRepoOptions.has(cliOptions)) { + if (!process.env.BEACHBALL_DISABLE_CACHE && cachedRepoOptions.has(cliOptions)) { return cachedRepoOptions.get(cliOptions)!; } diff --git a/src/packageManager/listPackageVersions.ts b/src/packageManager/listPackageVersions.ts index 8645093d6..500bcbe4f 100644 --- a/src/packageManager/listPackageVersions.ts +++ b/src/packageManager/listPackageVersions.ts @@ -3,24 +3,24 @@ import pLimit from 'p-limit'; import { PackageInfo } from '../types/PackageInfo'; import { AuthType } from '../types/Auth'; -const packageVersions: { [pkgName: string]: any } = {}; +const packageVersionsCache: { [pkgName: string]: any } = {}; const NPM_CONCURRENCY = 5; export async function getNpmPackageInfo(packageName: string, registry: string, token?: string, authType?: AuthType) { - if (!packageVersions[packageName]) { + if (process.env.BEACHBALL_DISABLE_CACHE || !packageVersionsCache[packageName]) { const args = ['show', '--registry', registry, '--json', packageName, ...getNpmAuthArgs(registry, token, authType)]; const showResult = await npmAsync(args); if (showResult.success && showResult.stdout !== '') { const packageInfo = JSON.parse(showResult.stdout); - packageVersions[packageName] = packageInfo; + packageVersionsCache[packageName] = packageInfo; } else { - packageVersions[packageName] = {}; + packageVersionsCache[packageName] = {}; } } - return packageVersions[packageName]; + return packageVersionsCache[packageName]; } export async function listPackageVersionsByTag( diff --git a/src/packageManager/packagePublish.ts b/src/packageManager/packagePublish.ts index b11de99f6..1aa7188c2 100644 --- a/src/packageManager/packagePublish.ts +++ b/src/packageManager/packagePublish.ts @@ -9,8 +9,7 @@ export function packagePublish( token: string, access: string, authType?: AuthType, - timeout?: number | undefined, - gitTimeout?: number | undefined + timeout?: number | undefined ) { const packageOptions = packageInfo.combinedOptions; const packagePath = path.dirname(packageInfo.packageJsonPath); diff --git a/yarn.lock b/yarn.lock index 27ecef024..c3c23fbbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11599,9 +11599,9 @@ worker-farm@^1.7.0: errno "~0.1.7" workspace-tools@^0.34.0: - version "0.34.0" - resolved "https://registry.yarnpkg.com/workspace-tools/-/workspace-tools-0.34.0.tgz#cfe096d8d55c8c3ec7c7977b84bbe2bbdfb05e17" - integrity sha512-grYUZvWs7SXJzXCPx0jdIkErFkGKVt74jZKYC2GxIEX2IZgEYYjwLmWDIPnZAtXpQ2jNuZiXxh9Kglyb5LUpnQ== + version "0.34.1" + resolved "https://registry.yarnpkg.com/workspace-tools/-/workspace-tools-0.34.1.tgz#a4682c17161712eb2bc0361c848f06d454b27704" + integrity sha512-Dhaonm9PJ00P6O+R7+wHcQh0U6+85xwk8QDVEXl9dFNbiQa9pbyhe9JUM1aSy68vND8JrHimnDBpIDr7NOhcFQ== dependencies: "@yarnpkg/lockfile" "^1.1.0" git-url-parse "^13.0.0"