-
Notifications
You must be signed in to change notification settings - Fork 92
Expand file tree
/
Copy pathreadChangeFiles.test.ts
More file actions
330 lines (273 loc) · 13.3 KB
/
readChangeFiles.test.ts
File metadata and controls
330 lines (273 loc) · 13.3 KB
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import { describe, expect, it, beforeAll, afterAll, afterEach } from '@jest/globals';
import fs from 'fs';
import { generateChangeFiles, getChangeFiles } from '../../__fixtures__/changeFiles';
import { initMockLogs } from '../../__fixtures__/mockLogs';
import { RepositoryFactory } from '../../__fixtures__/repositoryFactory';
import { getPackageInfos } from '../../monorepo/getPackageInfos';
import { readChangeFiles } from '../../changefile/readChangeFiles';
import type { RepoOptions } from '../../types/BeachballOptions';
import type { Repository } from '../../__fixtures__/repository';
import type { ChangeInfo, ChangeSet } from '../../types/ChangeInfo';
import { defaultRemoteBranchName } from '../../__fixtures__/gitDefaults';
import { getParsedOptions } from '../../options/getOptions';
import { removeTempDir } from '../../__fixtures__/tmpdir';
import path from 'path';
import { createTestFileStructureType } from '../../__fixtures__/createTestFileStructure';
import { readJson } from '../../object/readJson';
import { writeJson } from '../../object/writeJson';
import { getScopedPackages } from '../../monorepo/getScopedPackages';
import { getChangePath } from '../../paths';
// The tests for fromRef that use git are in a nested describe block
describe('readChangeFiles', () => {
/** Non-git temp directory root, for tests that don't need git */
let tempRoot: string | undefined;
const logs = initMockLogs();
function getOptionsAndPackages(repoOptions?: Partial<RepoOptions>) {
const cwd = repoOptions?.path || tempRoot;
expect(cwd).toBeTruthy();
const parsedOptions = getParsedOptions({
cwd: cwd!,
argv: ['node', 'beachball', 'change'],
testRepoOptions: { branch: defaultRemoteBranchName, ...repoOptions },
});
const packageInfos = getPackageInfos(parsedOptions);
const scopedPackages = getScopedPackages(parsedOptions.options, packageInfos);
return { packageInfos, options: parsedOptions.options, parsedOptions, scopedPackages };
}
function updateJsonFile(relativePath: string, json: Record<string, unknown>) {
const fullPath = path.join(tempRoot!, relativePath);
const diskJson = readJson<Record<string, unknown>>(fullPath);
Object.assign(diskJson, json);
writeJson(fullPath, diskJson);
}
/** Get the sorted package names from a list of changes */
function getPackages(changes: ChangeSet) {
return changes.map(changeEntry => changeEntry.change.packageName).sort();
}
afterEach(() => {
tempRoot && removeTempDir(tempRoot);
tempRoot = undefined;
});
it('reads change files and returns in reverse chronological order', async () => {
// this test doesn't need git
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages();
generateChangeFiles(['bar'], options);
// Wait slightly to ensure the mtime is different for sorting
await new Promise(resolve => setTimeout(resolve, 5));
generateChangeFiles(['foo'], options);
// Include a basic check reading from disk to verify generateChangeFiles worked
expect(getChangeFiles(options)).toHaveLength(2);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(changeSet).toHaveLength(2);
expect(changeSet).toEqual([
// foo will be first since it's newer
{
change: {
comment: 'foo comment',
dependentChangeType: 'patch',
email: 'test@test.com',
packageName: 'foo',
type: 'minor',
},
changeFile: expect.stringMatching(/^foo-[\w-]+\.json$/),
},
{
change: {
comment: 'bar comment',
dependentChangeType: 'patch',
email: 'test@test.com',
packageName: 'bar',
type: 'minor',
},
changeFile: expect.stringMatching(/^bar-[\w-]+\.json$/),
},
]);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('reads from a custom changeDir', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({ changeDir: 'changeDir' });
generateChangeFiles(['foo'], options);
expect(getChangeFiles(options)).toHaveLength(1);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['foo']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('reads a grouped change file', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({ groupChanges: true });
generateChangeFiles(['foo', 'bar'], options);
expect(getChangeFiles(options)).toHaveLength(1);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['bar', 'foo']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('excludes invalid change files', () => {
tempRoot = createTestFileStructureType('monorepo');
updateJsonFile('packages/bar/package.json', { private: true });
const { options, packageInfos, scopedPackages } = getOptionsAndPackages();
// fake doesn't exist, bar is private, foo is okay
generateChangeFiles(['fake', 'bar', 'foo'], options);
logs.clear();
fs.writeFileSync(path.join(getChangePath(options), 'not-change.json'), '{}');
expect(getChangeFiles(options)).toHaveLength(4);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['foo']);
expect(logs.getMockLines('warn', { root: tempRoot, sanitize: true, sort: true })).toMatchInlineSnapshot(`
"<root>/change/not-change.json does not appear to be a change file
Change detected for nonexistent package fake; delete this file: <root>/change/fake-<guid>.json
Change detected for private package bar; delete this file: <root>/change/bar-<guid>.json"
`);
});
it('excludes invalid changes from grouped change file in monorepo', () => {
tempRoot = createTestFileStructureType('monorepo');
updateJsonFile('packages/bar/package.json', { private: true });
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({ groupChanges: true });
// fake doesn't exist, bar is private, foo is okay
generateChangeFiles(['fake', 'bar', 'foo'], options);
expect(getChangeFiles(options)).toHaveLength(1);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['foo']);
expect(logs.getMockLines('warn', { root: tempRoot, sanitize: true, sort: true })).toMatchInlineSnapshot(`
"Change detected for nonexistent package fake; remove the entry from this file: <root>/change/change-<guid>.json
Change detected for private package bar; remove the entry from this file: <root>/change/change-<guid>.json"
`);
});
it('excludes out of scope change files in monorepo', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({ scope: ['packages/foo'] });
generateChangeFiles(['bar', 'foo'], options);
expect(getChangeFiles(options)).toHaveLength(2);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['foo']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('excludes out of scope changes from grouped change file in monorepo', () => {
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({
scope: ['packages/foo'],
groupChanges: true,
});
generateChangeFiles(['bar', 'foo'], options);
expect(getChangeFiles(options)).toHaveLength(1);
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['foo']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('runs transform.changeFiles functions if provided', () => {
const editedComment = 'Edited comment for testing';
tempRoot = createTestFileStructureType('monorepo');
const { options, packageInfos, scopedPackages } = getOptionsAndPackages({
transform: {
changeFiles: (changeFile, changeFilePath, { command }) => {
// For test, we will be changing the comment based on the package name
if ((changeFile as ChangeInfo).packageName === 'foo') {
(changeFile as ChangeInfo).comment = editedComment;
(changeFile as ChangeInfo).command = command;
}
return changeFile as ChangeInfo;
},
},
changelog: {
groups: [
{
mainPackageName: 'foo',
changelogPath: path.join(tempRoot, 'packages/foo'),
include: ['packages/foo', 'packages/bar'],
},
],
},
});
generateChangeFiles([{ packageName: 'foo', comment: 'comment 1' }], options);
generateChangeFiles([{ packageName: 'bar', comment: 'comment 2' }], options);
const changes = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changes)).toEqual(['bar', 'foo']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
// Verify that the comment of only the intended change file is changed
for (const { change, changeFile } of changes) {
if (changeFile.startsWith('foo')) {
expect(change.comment).toBe(editedComment);
expect(change.command).toEqual('change');
} else {
expect(change.comment).toBe('comment 2');
}
}
});
describe('fromRef option', () => {
let repositoryFactory: RepositoryFactory;
let repo: Repository;
function getRepoOptionsAndPackages(repoOptions?: Partial<RepoOptions>) {
return getOptionsAndPackages({ ...repoOptions, path: repo.rootPath });
}
beforeAll(() => {
// These tests can share a single factory and repo because they don't push to the remote,
// and the repo is reset after each test (which is faster than making new clones).
// Also, readChangeFiles doesn't directly care about single package vs monorepo, so we can
// use the monorepo fixture for all tests that need git (not all the tests need git).
repositoryFactory = new RepositoryFactory('monorepo');
repo = repositoryFactory.cloneRepository();
});
afterEach(() => {
repo.resetAndClean();
});
afterAll(() => {
repositoryFactory.cleanUp();
});
it('filters change files to only those modified since fromRef', () => {
const { options: initialOptions } = getRepoOptionsAndPackages({ commit: true });
// Create an initial change file and commit it
repo.commitChange('file1');
generateChangeFiles(['foo'], initialOptions);
const firstCommit = repo.getCurrentHash();
// Create another change file after the reference point
repo.commitChange('file2');
generateChangeFiles(['bar', 'baz'], initialOptions);
expect(getChangeFiles(initialOptions)).toHaveLength(3);
// Read change files with fromRef set to the first commit
const {
options: optionsFromRef,
packageInfos,
scopedPackages,
} = getRepoOptionsAndPackages({ fromRef: firstCommit });
const changeSet = readChangeFiles(optionsFromRef, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['bar', 'baz']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('returns empty set when no change files exist since fromRef', () => {
const { options: initialOptions } = getRepoOptionsAndPackages({ commit: true });
// Create change files and commit them
repo.commitChange('file1');
generateChangeFiles(['foo'], initialOptions);
const changeCommit = repo.getCurrentHash();
// Make another commit without change files
repo.commitChange('file2');
// Read change files from after the change file commit
const { options, packageInfos, scopedPackages } = getRepoOptionsAndPackages({ fromRef: changeCommit });
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual([]);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
it('excludes deleted change files when using fromRef', () => {
const { options: initialOptions } = getRepoOptionsAndPackages({ commit: true });
// Create two change files
repo.commitChange('file1');
generateChangeFiles(['foo', 'bar'], initialOptions);
const firstCommit = repo.getCurrentHash();
// Delete the bar change file
const changeFiles = fs.readdirSync(repo.pathTo('change'));
const barChangeFile = changeFiles.find(file => file.includes('bar'));
expect(barChangeFile).toBeTruthy();
repo.git(['rm', repo.pathTo('change', barChangeFile!)]);
repo.commitChange('delete bar change file');
// Add another change file
repo.commitChange('file2');
generateChangeFiles(['baz'], initialOptions);
// Read change files with fromRef - should only include foo (bar was deleted)
const { options, packageInfos, scopedPackages } = getRepoOptionsAndPackages({ fromRef: firstCommit });
const changeSet = readChangeFiles(options, packageInfos, scopedPackages);
expect(getPackages(changeSet)).toEqual(['baz']);
expect(logs.mocks.warn).not.toHaveBeenCalled();
});
});
});