forked from electron/electron
-
Notifications
You must be signed in to change notification settings - Fork 0
/
release-notes-spec.ts
214 lines (183 loc) · 8.64 KB
/
release-notes-spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
import { GitProcess, IGitExecutionOptions, IGitResult } from 'dugite';
import { expect } from 'chai';
import * as notes from '../script/release/notes/notes.js';
import * as path from 'node:path';
import * as sinon from 'sinon';
/* Fake a Dugite GitProcess that only returns the specific
commits that we want to test */
class Commit {
sha1: string;
subject: string;
constructor (sha1: string, subject: string) {
this.sha1 = sha1;
this.subject = subject;
}
}
class GitFake {
branches: {
[key: string]: Commit[],
};
constructor () {
this.branches = {};
}
setBranch (name: string, commits: Array<Commit>): void {
this.branches[name] = commits;
}
// find the newest shared commit between branches a and b
mergeBase (a: string, b:string): string {
for (const commit of [...this.branches[a].reverse()]) {
if (this.branches[b].map((commit: Commit) => commit.sha1).includes(commit.sha1)) {
return commit.sha1;
}
}
console.error('test error: branches not related');
return '';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exec (args: string[], path: string, options?: IGitExecutionOptions | undefined): Promise<IGitResult> {
let stdout = '';
const stderr = '';
const exitCode = 0;
if (args.length === 3 && args[0] === 'merge-base') {
// expected form: `git merge-base branchName1 branchName2`
const a: string = args[1]!;
const b: string = args[2]!;
stdout = this.mergeBase(a, b);
} else if (args.length === 3 && args[0] === 'log' && args[1] === '--format=%H') {
// expected form: `git log --format=%H branchName
const branch: string = args[2]!;
stdout = this.branches[branch].map((commit: Commit) => commit.sha1).join('\n');
} else if (args.length > 1 && args[0] === 'log' && args.includes('--format=%H,%s')) {
// expected form: `git log --format=%H,%s sha1..branchName
const [start, branch] = args[args.length - 1].split('..');
const lines : string[] = [];
let started = false;
for (const commit of this.branches[branch]) {
started = started || commit.sha1 === start;
if (started) {
lines.push(`${commit.sha1},${commit.subject}` /* %H,%s */);
}
}
stdout = lines.join('\n');
} else if (args.length === 6 &&
args[0] === 'branch' &&
args[1] === '--all' &&
args[2] === '--contains' &&
args[3].endsWith('-x-y')) {
// "what branch is this tag in?"
// git branch --all --contains ${ref} --sort version:refname
stdout = args[3];
} else {
console.error('unhandled GitProcess.exec():', args);
}
return Promise.resolve({ exitCode, stdout, stderr });
}
}
describe('release notes', () => {
const sandbox = sinon.createSandbox();
const gitFake = new GitFake();
const oldBranch = '8-x-y';
const newBranch = '9-x-y';
// commits shared by both oldBranch and newBranch
const sharedHistory = [
new Commit('2abea22b4bffa1626a521711bacec7cd51425818', "fix: explicitly cancel redirects when mode is 'error' (#20686)"),
new Commit('467409458e716c68b35fa935d556050ca6bed1c4', 'build: add support for automated minor releases (#20620)') // merge-base
];
// these commits came after newBranch was created
const newBreaking = new Commit('2fad53e66b1a2cb6f7dad88fe9bb62d7a461fe98', 'refactor: use v8 serialization for ipc (#20214)');
const newFeat = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: allow GUID parameter to avoid systray demotion on Windows (#21891)');
const newFix = new Commit('0600420bac25439fc2067d51c6aaa4ee11770577', "fix: don't allow window to go behind menu bar on mac (#22828)");
const oldFix = new Commit('f77bd19a70ac2d708d17ddbe4dc12745ca3a8577', 'fix: prevent menu gc during popup (#20785)');
// a bug that's fixed in both branches by separate PRs
const newTropFix = new Commit('a6ff42c190cb5caf8f3e217748e49183a951491b', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22750)');
const oldTropFix = new Commit('8751f485c5a6c8c78990bfd55a4350700f81f8cd', 'fix: workaround for hang when preventDefault-ing nativeWindowOpen (#22749)');
// a PR that has unusual note formatting
const sublist = new Commit('61dc1c88fd34a3e8fff80c80ed79d0455970e610', 'fix: client area inset calculation when maximized for frameless windows (#25052) (#25216)');
before(() => {
// location of release-notes' octokit reply cache
const fixtureDir = path.resolve(__dirname, 'fixtures', 'release-notes');
process.env.NOTES_CACHE_PATH = path.resolve(fixtureDir, 'cache');
});
beforeEach(() => {
const wrapper = (args: string[], path: string, options?: IGitExecutionOptions | undefined) => gitFake.exec(args, path, options);
sandbox.replace(GitProcess, 'exec', wrapper);
gitFake.setBranch(oldBranch, [...sharedHistory, oldFix]);
});
afterEach(() => {
sandbox.restore();
});
describe('trop annotations', () => {
it('shows sibling branches', async function () {
const version = 'v9.0.0';
gitFake.setBranch(oldBranch, [...sharedHistory, oldTropFix]);
gitFake.setBranch(newBranch, [...sharedHistory, newTropFix]);
const results: any = await notes.get(oldBranch, newBranch, version);
expect(results.fix).to.have.lengthOf(1);
console.log(results.fix);
expect(results.fix[0].trops).to.have.keys('8-x-y', '9-x-y');
});
});
// use case: A malicious contributor could edit the text of their 'Notes:'
// in the PR body after a PR's been merged and the maintainers have moved on.
// So instead always use the release-clerk PR comment
it('uses the release-clerk text', async function () {
// realText source: ${fixtureDir}/electron-electron-issue-21891-comments
const realText = 'Added GUID parameter to Tray API to avoid system tray icon demotion on Windows';
const testCommit = new Commit('89eb309d0b22bd4aec058ffaf983e81e56a5c378', 'feat: lole u got troled hard (#21891)');
const version = 'v9.0.0';
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
expect(results.feat).to.have.lengthOf(1);
expect(results.feat[0].hash).to.equal(testCommit.sha1);
expect(results.feat[0].note).to.equal(realText);
});
describe('rendering', () => {
it('removes redundant bullet points', async function () {
const testCommit = sublist;
const version = 'v10.1.1';
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
const rendered: any = await notes.render(results);
expect(rendered).to.not.include('* *');
});
it('indents sublists', async function () {
const testCommit = sublist;
const version = 'v10.1.1';
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
const rendered: any = await notes.render(results);
expect(rendered).to.include([
'* Fixed the following issues for frameless when maximized on Windows:',
' * fix unreachable task bar when auto hidden with position top',
' * fix 1px extending to secondary monitor',
' * fix 1px overflowing into taskbar at certain resolutions',
' * fix white line on top of window under 4k resolutions. [#25216]'].join('\n'));
});
});
// test that when you feed in different semantic commit types,
// the parser returns them in the results' correct category
describe('semantic commit', () => {
const version = 'v9.0.0';
it("honors 'feat' type", async function () {
const testCommit = newFeat;
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
expect(results.feat).to.have.lengthOf(1);
expect(results.feat[0].hash).to.equal(testCommit.sha1);
});
it("honors 'fix' type", async function () {
const testCommit = newFix;
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
expect(results.fix).to.have.lengthOf(1);
expect(results.fix[0].hash).to.equal(testCommit.sha1);
});
it("honors 'BREAKING CHANGE' message", async function () {
const testCommit = newBreaking;
gitFake.setBranch(newBranch, [...sharedHistory, testCommit]);
const results: any = await notes.get(oldBranch, newBranch, version);
expect(results.breaking).to.have.lengthOf(1);
expect(results.breaking[0].hash).to.equal(testCommit.sha1);
});
});
});