From 6d87895ffd0b4dfa53bd993a45c7d7350c692add Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 6 May 2021 12:30:35 -0700 Subject: [PATCH] test(dev-infra): add testing for integration of ReleaseNotes in publishing tooling (#41967) Add testing of the ReleaseNotes integration with the release publishing tooling. PR Close #41967 --- dev-infra/ng-dev.js | 32 +++++++++----- dev-infra/release/config/index.ts | 2 - dev-infra/release/publish/actions.ts | 3 +- .../publish/release-notes/release-notes.ts | 35 ++++++++++----- dev-infra/release/publish/test/common.spec.ts | 15 +++---- .../publish/test/github-api-testing.ts | 7 --- .../move-next-into-feature-freeze.spec.ts | 3 +- .../test/release-notes/release-notes-utils.ts | 43 +++++++++++++++++++ dev-infra/release/publish/test/test-utils.ts | 11 +---- 9 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 dev-infra/release/publish/test/release-notes/release-notes-utils.ts diff --git a/dev-infra/ng-dev.js b/dev-infra/ng-dev.js index 9778612953..b486b57518 100755 --- a/dev-infra/ng-dev.js +++ b/dev-infra/ng-dev.js @@ -5855,18 +5855,20 @@ function getLocalChangelogFilePath(projectDir) { } /** Release note generation. */ class ReleaseNotes { - constructor(version, config) { + constructor(version, startingRef, endingRef) { this.version = version; - this.config = config; + this.startingRef = startingRef; + this.endingRef = endingRef; /** An instance of GitClient. */ this.git = GitClient.getInstance(); /** A promise resolving to a list of Commits since the latest semver tag on the branch. */ - this.commits = getCommitsInRange(this.git.getLatestSemverTag().format(), 'HEAD'); + this.commits = this.getCommitsInRange(this.startingRef, this.endingRef); + /** The configuration for release notes. */ + this.config = this.getReleaseConfig().releaseNotes; } - /** Construct a release note generation instance. */ - static fromLatestTagToHead(version, config) { + static fromRange(version, startingRef, endingRef) { return tslib.__awaiter(this, void 0, void 0, function* () { - return new ReleaseNotes(version, config); + return new ReleaseNotes(version, startingRef, endingRef); }); } /** Retrieve the release note generated for a Github Release. */ @@ -5888,7 +5890,7 @@ class ReleaseNotes { promptForReleaseTitle() { return tslib.__awaiter(this, void 0, void 0, function* () { if (this.title === undefined) { - if (this.config.releaseNotes.useReleaseTitle) { + if (this.config.useReleaseTitle) { this.title = yield promptInput('Please provide a title for the release:'); } else { @@ -5906,14 +5908,24 @@ class ReleaseNotes { commits: yield this.commits, github: this.git.remoteConfig, version: this.version.format(), - groupOrder: this.config.releaseNotes.groupOrder, - hiddenScopes: this.config.releaseNotes.hiddenScopes, + groupOrder: this.config.groupOrder, + hiddenScopes: this.config.hiddenScopes, title: yield this.promptForReleaseTitle(), }); } return this.renderContext; }); } + // These methods are used for access to the utility functions while allowing them to be + // overwritten in subclasses during testing. + getCommitsInRange(from, to) { + return tslib.__awaiter(this, void 0, void 0, function* () { + return getCommitsInRange(from, to); + }); + } + getReleaseConfig(config) { + return getReleaseConfig(config); + } } /** @@ -6194,7 +6206,7 @@ class ReleaseAction { */ stageVersionForBranchAndCreatePullRequest(newVersion, pullRequestBaseBranch) { return tslib.__awaiter(this, void 0, void 0, function* () { - const releaseNotes = yield ReleaseNotes.fromLatestTagToHead(newVersion, this.config); + const releaseNotes = yield ReleaseNotes.fromRange(newVersion, this.git.getLatestSemverTag().format(), 'HEAD'); yield this.updateProjectVersion(newVersion); yield this.prependReleaseNotesToChangelog(releaseNotes); yield this.waitForEditsAndCreateReleaseCommit(newVersion); diff --git a/dev-infra/release/config/index.ts b/dev-infra/release/config/index.ts index 368e942dda..fb87ce3e6e 100644 --- a/dev-infra/release/config/index.ts +++ b/dev-infra/release/config/index.ts @@ -6,8 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import * as semver from 'semver'; - import {assertNoErrors, getConfig, NgDevConfig} from '../../utils/config'; /** Interface describing a built package. */ diff --git a/dev-infra/release/publish/actions.ts b/dev-infra/release/publish/actions.ts index 95aa24eaf2..6d067e93a1 100644 --- a/dev-infra/release/publish/actions.ts +++ b/dev-infra/release/publish/actions.ts @@ -350,7 +350,8 @@ export abstract class ReleaseAction { protected async stageVersionForBranchAndCreatePullRequest( newVersion: semver.SemVer, pullRequestBaseBranch: string): Promise<{releaseNotes: ReleaseNotes, pullRequest: PullRequest}> { - const releaseNotes = await ReleaseNotes.fromLatestTagToHead(newVersion, this.config); + const releaseNotes = + await ReleaseNotes.fromRange(newVersion, this.git.getLatestSemverTag().format(), 'HEAD'); await this.updateProjectVersion(newVersion); await this.prependReleaseNotesToChangelog(releaseNotes); await this.waitForEditsAndCreateReleaseCommit(newVersion); diff --git a/dev-infra/release/publish/release-notes/release-notes.ts b/dev-infra/release/publish/release-notes/release-notes.ts index edbad43238..5b2e6aff55 100644 --- a/dev-infra/release/publish/release-notes/release-notes.ts +++ b/dev-infra/release/publish/release-notes/release-notes.ts @@ -8,11 +8,12 @@ import {renderFile} from 'ejs'; import {join} from 'path'; import * as semver from 'semver'; +import {CommitFromGitLog} from '../../../commit-message/parse'; import {getCommitsInRange} from '../../../commit-message/utils'; import {promptInput} from '../../../utils/console'; import {GitClient} from '../../../utils/git/index'; -import {ReleaseConfig} from '../../config/index'; +import {DevInfraReleaseConfig, getReleaseConfig, ReleaseNotesConfig} from '../../config/index'; import {changelogPath} from '../constants'; import {RenderContext} from './context'; @@ -21,13 +22,10 @@ export function getLocalChangelogFilePath(projectDir: string): string { return join(projectDir, changelogPath); } - /** Release note generation. */ export class ReleaseNotes { - /** Construct a release note generation instance. */ - static async fromLatestTagToHead(version: semver.SemVer, config: ReleaseConfig): - Promise { - return new ReleaseNotes(version, config); + static async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) { + return new ReleaseNotes(version, startingRef, endingRef); } /** An instance of GitClient. */ @@ -37,9 +35,13 @@ export class ReleaseNotes { /** The title to use for the release. */ private title: string|false|undefined; /** A promise resolving to a list of Commits since the latest semver tag on the branch. */ - private commits = getCommitsInRange(this.git.getLatestSemverTag().format(), 'HEAD'); + private commits: Promise = + this.getCommitsInRange(this.startingRef, this.endingRef); + /** The configuration for release notes. */ + private config: ReleaseNotesConfig = this.getReleaseConfig().releaseNotes; - private constructor(public readonly version: semver.SemVer, private config: ReleaseConfig) {} + protected constructor( + public version: semver.SemVer, private startingRef: string, private endingRef: string) {} /** Retrieve the release note generated for a Github Release. */ async getGithubReleaseEntry(): Promise { @@ -61,7 +63,7 @@ export class ReleaseNotes { */ async promptForReleaseTitle() { if (this.title === undefined) { - if (this.config.releaseNotes.useReleaseTitle) { + if (this.config.useReleaseTitle) { this.title = await promptInput('Please provide a title for the release:'); } else { this.title = false; @@ -77,11 +79,22 @@ export class ReleaseNotes { commits: await this.commits, github: this.git.remoteConfig, version: this.version.format(), - groupOrder: this.config.releaseNotes.groupOrder, - hiddenScopes: this.config.releaseNotes.hiddenScopes, + groupOrder: this.config.groupOrder, + hiddenScopes: this.config.hiddenScopes, title: await this.promptForReleaseTitle(), }); } return this.renderContext; } + + + // These methods are used for access to the utility functions while allowing them to be + // overwritten in subclasses during testing. + protected async getCommitsInRange(from: string, to?: string) { + return getCommitsInRange(from, to); + } + + protected getReleaseConfig(config?: Partial) { + return getReleaseConfig(config); + } } diff --git a/dev-infra/release/publish/test/common.spec.ts b/dev-infra/release/publish/test/common.spec.ts index 8ceefcd851..28b6044c04 100644 --- a/dev-infra/release/publish/test/common.spec.ts +++ b/dev-infra/release/publish/test/common.spec.ts @@ -20,7 +20,7 @@ import {actions} from '../actions/index'; import {changelogPath} from '../constants'; import {ReleaseNotes} from '../release-notes/release-notes'; -import {fakeNpmPackageQueryRequest, getChangelogForVersion, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; +import {fakeNpmPackageQueryRequest, getTestingMocksForReleaseAction, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; describe('common release action logic', () => { const baseReleaseTrains: ActiveReleaseTrains = { @@ -87,7 +87,6 @@ describe('common release action logic', () => { describe('changelog cherry-picking', () => { const {version, branchName} = baseReleaseTrains.latest; - const fakeReleaseNotes = getChangelogForVersion(version.format()); const forkBranchName = `changelog-cherry-pick-${version}`; it('should prepend the changelog to the next branch', async () => { @@ -96,8 +95,7 @@ describe('common release action logic', () => { // Expect the changelog to be fetched and return a fake changelog to test that // it is properly appended. Also expect a pull request to be created in the fork. - repo.expectChangelogFetch(branchName, fakeReleaseNotes) - .expectFindForkRequest(fork) + repo.expectFindForkRequest(fork) .expectPullRequestToBeCreated('master', fork, forkBranchName, 200) .expectPullRequestWait(200); @@ -107,7 +105,7 @@ describe('common release action logic', () => { await instance.testCherryPickWithPullRequest(version, branchName); const changelogContent = readFileSync(join(testTmpDir, changelogPath), 'utf8'); - expect(changelogContent).toEqual(`${fakeReleaseNotes}Existing changelog`); + expect(changelogContent).toEqual(`Changelog Entry for 10.0.1\n\nExisting changelog`); }); it('should push changes to a fork for creating a pull request', async () => { @@ -116,8 +114,7 @@ describe('common release action logic', () => { // Expect the changelog to be fetched and return a fake changelog to test that // it is properly appended. Also expect a pull request to be created in the fork. - repo.expectChangelogFetch(branchName, fakeReleaseNotes) - .expectFindForkRequest(fork) + repo.expectFindForkRequest(fork) .expectPullRequestToBeCreated('master', fork, forkBranchName, 200) .expectPullRequestWait(200); @@ -155,12 +152,12 @@ class TestAction extends ReleaseAction { } async testBuildAndPublish(version: semver.SemVer, publishBranch: string, distTag: string) { - const releaseNotes = await ReleaseNotes.fromLatestTagToHead(version, this.config); + const releaseNotes = await ReleaseNotes.fromRange(version, '', ''); await this.buildAndPublish(releaseNotes, publishBranch, distTag); } async testCherryPickWithPullRequest(version: semver.SemVer, branch: string) { - const releaseNotes = await ReleaseNotes.fromLatestTagToHead(version, this.config); + const releaseNotes = await ReleaseNotes.fromRange(version, '', ''); await this.cherryPickChangelogIntoNextBranch(releaseNotes, branch); } } diff --git a/dev-infra/release/publish/test/github-api-testing.ts b/dev-infra/release/publish/test/github-api-testing.ts index c03295bd88..084b9097dc 100644 --- a/dev-infra/release/publish/test/github-api-testing.ts +++ b/dev-infra/release/publish/test/github-api-testing.ts @@ -60,13 +60,6 @@ export class GithubTestingRepo { return this; } - expectChangelogFetch(branch: string, content: string): this { - nock(this.repoApiUrl).get(`/contents/%2FCHANGELOG.md`).query(p => p.ref === branch).reply(200, { - content: new Buffer(content).toString('base64') - }); - return this; - } - expectCommitRequest(sha: string, message: string): this { nock(this.repoApiUrl).get(`/commits/${sha}`).reply(200, {commit: {message}}); return this; diff --git a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts index 78309754f9..fe61b4a975 100644 --- a/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts +++ b/dev-infra/release/publish/test/move-next-into-feature-freeze.spec.ts @@ -13,7 +13,7 @@ import {ReleaseTrain} from '../../versioning/release-trains'; import {MoveNextIntoFeatureFreezeAction} from '../actions/move-next-into-feature-freeze'; import * as externalCommands from '../external-commands'; -import {getChangelogForVersion, parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; +import {parse, setupReleaseActionForTesting, testTmpDir} from './test-utils'; describe('move next into feature-freeze action', () => { it('should not activate if a feature-freeze release-train is active', async () => { @@ -84,7 +84,6 @@ describe('move next into feature-freeze action', () => { 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) - .expectChangelogFetch(expectedNewBranch, getChangelogForVersion(expectedVersion)) .expectPullRequestToBeCreated('master', fork, expectedNextUpdateBranch, 100); // In the fork, we make the following branches appear as non-existent, diff --git a/dev-infra/release/publish/test/release-notes/release-notes-utils.ts b/dev-infra/release/publish/test/release-notes/release-notes-utils.ts new file mode 100644 index 0000000000..98424d7bc4 --- /dev/null +++ b/dev-infra/release/publish/test/release-notes/release-notes-utils.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as semver from 'semver'; + +import {DevInfraReleaseConfig, ReleaseConfig} from '../../../config'; +import {ReleaseNotes} from '../../release-notes/release-notes'; + +/** + * Mock version of the ReleaseNotes for testing, preventing actual calls to git for commits and + * returning versioned entry strings. + */ +class MockReleaseNotes extends ReleaseNotes { + static async fromRange(version: semver.SemVer, startingRef: string, endingRef: string) { + return new MockReleaseNotes(version, startingRef, endingRef); + } + + async getChangelogEntry() { + return `Changelog Entry for ${this.version}`; + } + + async getGithubReleaseEntry() { + return `Github Release Entry for ${this.version}`; + } + + // Overrides of utility functions which call out to other tools and are unused in this mock. + protected async getCommitsInRange(from: string, to?: string) { + return []; + } + protected getReleaseConfig(config?: Partial) { + return {} as ReleaseConfig; + } +} + +/** Replace the ReleaseNotes static builder function with the MockReleaseNotes builder function. */ +export function installMockReleaseNotes() { + spyOn(ReleaseNotes, 'fromRange').and.callFake(MockReleaseNotes.fromRange); +} diff --git a/dev-infra/release/publish/test/test-utils.ts b/dev-infra/release/publish/test/test-utils.ts index 77dd64410f..f463c635da 100644 --- a/dev-infra/release/publish/test/test-utils.ts +++ b/dev-infra/release/publish/test/test-utils.ts @@ -11,7 +11,6 @@ import * as nock from 'nock'; import {join} from 'path'; import * as semver from 'semver'; -import * as commitMessageUtils from '../../../commit-message/utils'; import {GithubConfig} from '../../../utils/config'; import * as console from '../../../utils/console'; import {getBranchPushMatcher, installVirtualGitClientSpies, VirtualGitClient} from '../../../utils/testing'; @@ -22,9 +21,9 @@ import {_npmPackageInfoCache, NpmPackageInfo} from '../../versioning/npm-registr import {ReleaseAction, ReleaseActionConstructor} from '../actions'; import * as constants from '../constants'; import * as externalCommands from '../external-commands'; -import {buildDateStamp} from '../release-notes/context'; import {GithubTestingRepo} from './github-api-testing'; +import {installMockReleaseNotes} from './release-notes/release-notes-utils'; /** * Temporary directory which will be used as project directory in tests. Note that @@ -70,7 +69,7 @@ export function setupReleaseActionForTesting( actionCtor: ReleaseActionConstructor, active: ActiveReleaseTrains, isNextPublishedToNpm = true): TestReleaseAction { installVirtualGitClientSpies(); - spyOn(commitMessageUtils, 'getCommitsInRange').and.returnValue(Promise.resolve([])); + installMockReleaseNotes(); // Reset existing HTTP interceptors. nock.cleanAll(); @@ -123,11 +122,6 @@ export function parse(version: string): semver.SemVer { return semver.parse(version)!; } -/** Gets a changelog for the specified version. */ -export function getChangelogForVersion(version: string): string { - return `\n# ${version} (${buildDateStamp()})\n\n\n`; -} - export async function expectStagingAndPublishWithoutCherryPick( action: TestReleaseAction, expectedBranch: string, expectedVersion: string, expectedNpmDistTag: string) { @@ -197,7 +191,6 @@ export async function expectStagingAndPublishWithCherryPick( 'STAGING_COMMIT_SHA', `release: cut the v${expectedVersion} release\n\nPR Close #200.`) .expectTagToBeCreated(expectedTagName, 'STAGING_COMMIT_SHA') .expectReleaseToBeCreated(`v${expectedVersion}`, expectedTagName) - .expectChangelogFetch(expectedBranch, getChangelogForVersion(expectedVersion)) .expectPullRequestToBeCreated('master', fork, expectedCherryPickForkBranch, 300) .expectPullRequestWait(300);