apify-test-tools 0.10.2-beta.13 → 0.10.2-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/cli/apify-test-tools.mjs +123 -128
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +1 -1
- package/.github/workflows/public_platform-tests.yaml +36 -22
- package/.github/workflows/public_pr-build-test.yaml +7 -6
- package/.github/workflows/public_push-build-latest.yaml +6 -4
- package/.github/workflows/public_stale-issues.yaml +51 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +9 -7
- package/README.md +11 -12
- package/bin/actor-ids.ts +38 -0
- package/bin/build.ts +1 -1
- package/bin/git.ts +20 -92
- package/bin/main.ts +87 -73
- package/bin/notify.ts +198 -0
- package/bin/run-tests.ts +141 -0
- package/bin/slack.ts +16 -87
- package/bin/stdin-input.ts +98 -7
- package/bin/test-report.ts +141 -140
- package/bin/types.ts +0 -9
- package/dist/bin/actor-ids.d.ts +16 -0
- package/dist/bin/actor-ids.d.ts.map +1 -0
- package/dist/bin/actor-ids.js +24 -0
- package/dist/bin/actor-ids.js.map +1 -0
- package/dist/bin/build.js +1 -1
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/git.d.ts +6 -16
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +15 -75
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +80 -51
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/notify.d.ts +35 -0
- package/dist/bin/notify.d.ts.map +1 -0
- package/dist/bin/notify.js +131 -0
- package/dist/bin/notify.js.map +1 -0
- package/dist/bin/run-tests.d.ts +21 -0
- package/dist/bin/run-tests.d.ts.map +1 -0
- package/dist/bin/run-tests.js +116 -0
- package/dist/bin/run-tests.js.map +1 -0
- package/dist/bin/slack.d.ts +2 -17
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +17 -58
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +71 -9
- package/dist/bin/stdin-input.d.ts.map +1 -1
- package/dist/bin/stdin-input.js +76 -6
- package/dist/bin/stdin-input.js.map +1 -1
- package/dist/bin/test-report.d.ts +69 -8
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +76 -67
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +0 -8
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/lib/lib.d.ts +0 -3
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +5 -10
- package/dist/lib/lib.js.map +1 -1
- package/dist/test/bundle/cli-bundle.test.js +40 -48
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/e2e/cli-bundle.e2e.test.js +15 -16
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/e2e/git-build-release.e2e.test.js +2 -2
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -1
- package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
- package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
- package/dist/test/unit/bin/actor-ids.test.js +32 -0
- package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +8 -3
- package/dist/test/unit/bin/build-cli.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +1 -1
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +4 -142
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/notify.test.d.ts +2 -0
- package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
- package/dist/test/unit/bin/notify.test.js +222 -0
- package/dist/test/unit/bin/notify.test.js.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +51 -9
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -1
- package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-cli.test.js +115 -0
- package/dist/test/unit/bin/test-cli.test.js.map +1 -0
- package/dist/test/unit/bin/test-report.test.d.ts +2 -0
- package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-report.test.js +100 -0
- package/dist/test/unit/bin/test-report.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/cli.md +131 -53
- package/docs/lib.md +21 -16
- package/docs/workflows.md +63 -23
- package/lib/lib.ts +5 -10
- package/package.json +1 -1
- package/test/bundle/cli-bundle.test.ts +47 -60
- package/test/e2e/cli-bundle.e2e.test.ts +18 -25
- package/test/e2e/git-build-release.e2e.test.ts +2 -2
- package/test/unit/bin/actor-ids.test.ts +43 -0
- package/test/unit/bin/build-cli.test.ts +9 -3
- package/test/unit/bin/diff-changes.test.ts +1 -1
- package/test/unit/bin/git.test.ts +3 -197
- package/test/unit/bin/notify.test.ts +299 -0
- package/test/unit/bin/stdin-input.test.ts +68 -9
- package/test/unit/bin/test-cli.test.ts +137 -0
- package/test/unit/bin/test-report.test.ts +118 -0
- package/dist/test/unit/bin/logging.test.d.ts +0 -2
- package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
- package/dist/test/unit/bin/logging.test.js +0 -37
- package/dist/test/unit/bin/logging.test.js.map +0 -1
- package/dist/test/unit/bin/slack.test.d.ts +0 -2
- package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
- package/dist/test/unit/bin/slack.test.js +0 -71
- package/dist/test/unit/bin/slack.test.js.map +0 -1
- package/test/unit/bin/logging.test.ts +0 -48
- package/test/unit/bin/slack.test.ts +0 -84
|
@@ -46,8 +46,6 @@ const runCliWithInput = (input: string, ...args: string[]) => {
|
|
|
46
46
|
|
|
47
47
|
const runCli = (...args: string[]) => runCliWithInput('', ...args);
|
|
48
48
|
|
|
49
|
-
const gitRange = ['--source-branch', 'feature', '--target-branch', 'main'];
|
|
50
|
-
|
|
51
49
|
describe('CLI bundle', () => {
|
|
52
50
|
// Two Actors on `main`; `feature` changes only tester/foo and the root README.
|
|
53
51
|
beforeAll(async () => {
|
|
@@ -91,14 +89,11 @@ describe('CLI bundle', () => {
|
|
|
91
89
|
const { status, stdout } = runCli('--help');
|
|
92
90
|
expect(status).toBe(0);
|
|
93
91
|
for (const command of [
|
|
94
|
-
'get-commits',
|
|
95
|
-
'get-latest-commit',
|
|
96
|
-
'get-changed-files',
|
|
97
92
|
'get-actor-configs',
|
|
98
93
|
'get-affected-actors',
|
|
99
|
-
'
|
|
94
|
+
'test [filters..]',
|
|
95
|
+
'notify',
|
|
100
96
|
'build',
|
|
101
|
-
'notify-release',
|
|
102
97
|
'build-from-local',
|
|
103
98
|
'delete-old-builds',
|
|
104
99
|
]) {
|
|
@@ -106,27 +101,6 @@ describe('CLI bundle', () => {
|
|
|
106
101
|
}
|
|
107
102
|
});
|
|
108
103
|
|
|
109
|
-
it('get-commits returns the commits on the source branch', () => {
|
|
110
|
-
const { status, stdout, stderr } = runCli('get-commits', ...gitRange);
|
|
111
|
-
expect(status, stderr).toBe(0);
|
|
112
|
-
expect(JSON.parse(stdout).map((commit: { message: string }) => commit.message)).toEqual([
|
|
113
|
-
'fix(foo): change foo',
|
|
114
|
-
'docs: readme',
|
|
115
|
-
]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('get-latest-commit returns the head of the source branch', () => {
|
|
119
|
-
const { status, stdout, stderr } = runCli('get-latest-commit', ...gitRange);
|
|
120
|
-
expect(status, stderr).toBe(0);
|
|
121
|
-
expect(JSON.parse(stdout)).toMatchObject({ sha: headSha, message: 'docs: readme' });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('get-changed-files returns the files changed on the source branch', () => {
|
|
125
|
-
const { status, stdout, stderr } = runCli('get-changed-files', ...gitRange);
|
|
126
|
-
expect(status, stderr).toBe(0);
|
|
127
|
-
expect(JSON.parse(stdout)).toEqual(['README.md', 'actors/foo/main.js']);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
104
|
it('get-actor-configs reads both Actors from the config file', () => {
|
|
131
105
|
const { status, stdout, stderr } = runCli('get-actor-configs');
|
|
132
106
|
expect(status, stderr).toBe(0);
|
|
@@ -149,6 +123,14 @@ describe('CLI bundle', () => {
|
|
|
149
123
|
});
|
|
150
124
|
});
|
|
151
125
|
|
|
126
|
+
it.each(['info', 'silent'])('keeps JSON results intact with --log-level %s', (level) => {
|
|
127
|
+
const { status, stdout, stderr } = runCli('get-affected-actors', '--base', 'main', '--log-level', level);
|
|
128
|
+
expect(status, stderr).toBe(0);
|
|
129
|
+
expect(stderr).toEqual(level === 'silent' ? '' : expect.stringContaining('Changed files'));
|
|
130
|
+
expect(JSON.parse(stdout)).toMatchObject({ head: headSha });
|
|
131
|
+
expect(stdout.endsWith('\n')).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
152
134
|
it('build --dry-run with nothing to build needs no token', () => {
|
|
153
135
|
const { status, stdout, stderr } = runCliWithInput(
|
|
154
136
|
JSON.stringify({ actors: [] }),
|
|
@@ -189,47 +171,52 @@ describe('CLI bundle', () => {
|
|
|
189
171
|
});
|
|
190
172
|
});
|
|
191
173
|
|
|
192
|
-
it('notify
|
|
174
|
+
it('notify announces no release, and needs no token, when there are no new commits', () => {
|
|
193
175
|
const affected = runCli('get-affected-actors', '--since', headSha).stdout;
|
|
194
|
-
const { status, stderr } = runCliWithInput(affected, 'notify
|
|
176
|
+
const { status, stderr } = runCliWithInput(affected, 'notify', '--slack-channel', '#releases');
|
|
195
177
|
expect(status, stderr).toBe(0);
|
|
196
178
|
expect(stderr).toContain('No new commits, so there is no release to announce');
|
|
197
179
|
});
|
|
198
180
|
|
|
199
|
-
it('notify
|
|
181
|
+
it('notify skips a release without a channel', () => {
|
|
200
182
|
const affected = runCli('get-affected-actors', '--since', baseSha).stdout;
|
|
201
|
-
const { status, stderr } = runCliWithInput(affected, 'notify
|
|
183
|
+
const { status, stderr } = runCliWithInput(affected, 'notify', '--dry-run');
|
|
202
184
|
expect(status, stderr).toBe(0);
|
|
203
|
-
expect(stderr).toContain('Skipping slack
|
|
185
|
+
expect(stderr).toContain('Skipping notification, no --slack-channel passed');
|
|
204
186
|
});
|
|
205
187
|
|
|
206
|
-
it('
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
meta: { runId, runLink: `https://console.apify.com/view/runs/${runId}`, actorId: 'tester/foo' },
|
|
214
|
-
});
|
|
215
|
-
await writeRepoFile(
|
|
216
|
-
'test-output.json',
|
|
217
|
-
JSON.stringify({
|
|
218
|
-
success: false,
|
|
219
|
-
testResults: [
|
|
220
|
-
{
|
|
221
|
-
name: 'test/platform/foo.test.ts',
|
|
222
|
-
status: 'failed',
|
|
223
|
-
assertionResults: [assertion('works', 'passed', 'run1'), assertion('fails', 'failed', 'run2')],
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
}),
|
|
227
|
-
);
|
|
188
|
+
it('test fails without TESTER_APIFY_TOKEN instead of passing with every test skipped', () => {
|
|
189
|
+
// Every Actor in the config, on its default build, as the scheduled tests pipe them in
|
|
190
|
+
const { status, stdout, stderr } = runCliWithInput(runCli('get-actor-configs').stdout, 'test');
|
|
191
|
+
expect(status).toBe(1);
|
|
192
|
+
expect(stderr).toContain('TESTER_APIFY_TOKEN is not set');
|
|
193
|
+
expect(stdout).toBe('');
|
|
194
|
+
});
|
|
228
195
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
196
|
+
it('notify needs the Slack token to post a failed report, even in a dry run', () => {
|
|
197
|
+
const report = {
|
|
198
|
+
kind: 'test-report',
|
|
199
|
+
version: 1,
|
|
200
|
+
success: false,
|
|
201
|
+
summary: { total: 1, passed: 0, failed: 1, skipped: 0 },
|
|
202
|
+
tests: [
|
|
203
|
+
{
|
|
204
|
+
name: 'tester/foo: fails',
|
|
205
|
+
file: 'test/platform/foo.test.ts',
|
|
206
|
+
status: 'failed',
|
|
207
|
+
durationMs: 1,
|
|
208
|
+
failureMessages: ['AssertionError: expected 1 to be 2'],
|
|
209
|
+
run: { actorId: 'tester/foo', runId: 'run2', runLink: 'https://console.apify.com/view/runs/run2' },
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
errors: [],
|
|
213
|
+
};
|
|
214
|
+
const withChannel = runCliWithInput(JSON.stringify(report), 'notify', '--slack-channel', '#notif', '--dry-run');
|
|
215
|
+
expect(withChannel.status).toBe(1);
|
|
216
|
+
expect(withChannel.stderr).toContain('SLACK_BOT_TOKEN not defined');
|
|
217
|
+
|
|
218
|
+
const withoutChannel = runCliWithInput(JSON.stringify(report), 'notify', '--dry-run');
|
|
219
|
+
expect(withoutChannel.status, withoutChannel.stderr).toBe(0);
|
|
220
|
+
expect(withoutChannel.stderr).toContain('Skipping notification');
|
|
234
221
|
});
|
|
235
222
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { mkdir, mkdtemp,
|
|
2
|
+
import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
@@ -9,6 +9,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
|
9
9
|
|
|
10
10
|
import type { ActorBuild } from './helpers.js';
|
|
11
11
|
import {
|
|
12
|
+
bundlePath,
|
|
12
13
|
cliEnv,
|
|
13
14
|
gitEnv,
|
|
14
15
|
projectRoot,
|
|
@@ -21,8 +22,8 @@ import {
|
|
|
21
22
|
|
|
22
23
|
// End-to-end run of the shipped CLI bundle against two dummy Actors in an isolated sandbox account:
|
|
23
24
|
// dry-run plans read the real Actors, `build-from-local` really builds them, the lib runs them
|
|
24
|
-
// through `testActor` on exactly those builds (
|
|
25
|
-
//
|
|
25
|
+
// through `testActor` on exactly those builds (`build-from-local | test`), `notify` reads the
|
|
26
|
+
// report, and `delete-old-builds` cleans up. Git builds of a real repository are in
|
|
26
27
|
// git-build-release.e2e.test.ts.
|
|
27
28
|
//
|
|
28
29
|
// Runs with `npm run test:e2e`, kept out of `npm test`. Skipped without the token, e.g. on fork PRs.
|
|
@@ -30,8 +31,6 @@ const ACTORS = ['test-tools-testing-sandbox/dummy-for-testing-1', 'test-tools-te
|
|
|
30
31
|
// Only used as the source URL in dry-run plans; nothing is cloned from it.
|
|
31
32
|
const REPO_URL = 'https://github.com/apify/apify-test-tools-e2e-fixture';
|
|
32
33
|
|
|
33
|
-
const vitestPath = path.join(projectRoot, 'node_modules/vitest/vitest.mjs');
|
|
34
|
-
|
|
35
34
|
let repoDir: string;
|
|
36
35
|
let baseSha: string;
|
|
37
36
|
|
|
@@ -166,34 +165,28 @@ describe('sandbox dummies', () => {${actorTests}
|
|
|
166
165
|
`,
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
expect(vitest.status, `${vitest.stdout}\n${vitest.stderr}`).toBe(0);
|
|
168
|
+
const tested = await runProcess(process.execPath, [bundlePath, 'test', 'test/platform'], {
|
|
169
|
+
cwd: repoDir,
|
|
170
|
+
env: { ...cliEnv, TESTER_APIFY_TOKEN: token! },
|
|
171
|
+
input: JSON.stringify(builds),
|
|
172
|
+
});
|
|
173
|
+
expect(tested.status, tested.stderr).toBe(0);
|
|
174
|
+
expect(tested.stderr).toContain('https://console.apify.com/view/runs/');
|
|
178
175
|
|
|
179
|
-
// Each run used the build
|
|
180
|
-
const report = JSON.parse(
|
|
181
|
-
|
|
182
|
-
(file: { assertionResults: { meta: { runId: string } }[] }) => file.assertionResults,
|
|
183
|
-
);
|
|
184
|
-
expect(assertions).toHaveLength(ACTORS.length);
|
|
176
|
+
// Each run used the build piped into `test`, not the Actor's default build.
|
|
177
|
+
const report = JSON.parse(tested.stdout);
|
|
178
|
+
expect(report).toMatchObject({ success: true, summary: { passed: ACTORS.length, failed: 0 } });
|
|
185
179
|
const client = new ApifyClient({ token });
|
|
186
180
|
const runBuildIds = await Promise.all(
|
|
187
|
-
|
|
188
|
-
async ({
|
|
181
|
+
report.tests.map(
|
|
182
|
+
async ({ run }: { run: { runId: string } }) => (await client.run(run.runId).get())?.buildId,
|
|
189
183
|
),
|
|
190
184
|
);
|
|
191
185
|
expect(runBuildIds.sort()).toEqual(builds.map((build) => build.buildId).sort());
|
|
192
186
|
|
|
193
|
-
const { status, stderr } = await
|
|
187
|
+
const { status, stderr } = await runCliWithInput(tested.stdout, 'notify', '--dry-run');
|
|
194
188
|
expect(status, stderr).toBe(0);
|
|
195
|
-
expect(stderr).toContain(
|
|
196
|
-
expect(stderr).toContain('https://console.apify.com/view/runs/');
|
|
189
|
+
expect(stderr).toContain('Skipping notification');
|
|
197
190
|
});
|
|
198
191
|
|
|
199
192
|
// Last, so builds from earlier runs of this suite don't pile up in the sandbox.
|
|
@@ -106,8 +106,8 @@ describe.runIf(token)('CLI bundle building a real repository', { timeout: 2 * 60
|
|
|
106
106
|
expect(await versionSource('0.0')).toBe(BUILD_SOURCE);
|
|
107
107
|
|
|
108
108
|
// Without a channel the announcement is skipped, so it needs no Slack token
|
|
109
|
-
const notify = await runCliWithInput(affected.stdout, 'notify
|
|
109
|
+
const notify = await runCliWithInput(affected.stdout, 'notify');
|
|
110
110
|
expect(notify.status, notify.stderr).toBe(0);
|
|
111
|
-
expect(notify.stderr).toContain('Skipping
|
|
111
|
+
expect(notify.stderr).toContain('Skipping notification');
|
|
112
112
|
});
|
|
113
113
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { withActorIds } from '../../../bin/actor-ids.js';
|
|
4
|
+
|
|
5
|
+
type Client = NonNullable<Parameters<typeof withActorIds>[1]>;
|
|
6
|
+
|
|
7
|
+
/** A client that knows one Actor, by its full name and by its raw ID, as the API does */
|
|
8
|
+
const fakeClient = () => {
|
|
9
|
+
const known = { id: 'rawA', username: 'owner', name: 'a' };
|
|
10
|
+
const actor = vi.fn((actorId: string) => ({
|
|
11
|
+
get: async () => (actorId === 'owner/a' || actorId === 'rawA' ? known : undefined),
|
|
12
|
+
}));
|
|
13
|
+
return { actor } as unknown as Client & { actor: typeof actor };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('withActorIds', () => {
|
|
17
|
+
it('adds the raw ID to an Actor listed by full name, as get-actor-configs lists them', async () => {
|
|
18
|
+
expect(await withActorIds([{ actorFullName: 'owner/a' }], fakeClient())).toEqual([
|
|
19
|
+
{ actorFullName: 'owner/a', actorRawId: 'rawA' },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('adds the full name to an Actor listed by raw ID', async () => {
|
|
24
|
+
const build = { actorRawId: 'rawA', buildId: 'b1', buildNumber: '0.99.1' };
|
|
25
|
+
expect(await withActorIds([build], fakeClient())).toEqual([{ ...build, actorFullName: 'owner/a' }]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps the IDs of a build and does not look them up', async () => {
|
|
29
|
+
const client = fakeClient();
|
|
30
|
+
const build = { actorFullName: 'owner/a', buildId: 'b1', actorRawId: 'raw1', buildNumber: '0.99.1' };
|
|
31
|
+
expect(await withActorIds([build], client)).toEqual([build]);
|
|
32
|
+
expect(client.actor).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('fails for an Actor the token cannot see, instead of skipping its tests', async () => {
|
|
36
|
+
await expect(withActorIds([{ actorFullName: 'owner/missing' }], fakeClient())).rejects.toThrow(
|
|
37
|
+
'Actor owner/missing not found',
|
|
38
|
+
);
|
|
39
|
+
await expect(withActorIds([{ actorRawId: 'rawMissing' }], fakeClient())).rejects.toThrow(
|
|
40
|
+
'Actor rawMissing not found',
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -12,7 +12,7 @@ const run = (command: string[], input: string, env: NodeJS.ProcessEnv = process.
|
|
|
12
12
|
spawnSync(cliPath, command, { encoding: 'utf8', input, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
13
13
|
const runBuild = (input: string, args: string[] = ['--version-number', '0.99']) => run(['build', ...args], input);
|
|
14
14
|
|
|
15
|
-
describe('build and notify
|
|
15
|
+
describe('build and notify CLI', () => {
|
|
16
16
|
beforeAll(async () => {
|
|
17
17
|
({ cliPath, cleanup } = await compileCli());
|
|
18
18
|
});
|
|
@@ -46,9 +46,15 @@ describe('build and notify-release CLI', () => {
|
|
|
46
46
|
expect(result.stderr).toContain('Missing required argument: version-number');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it('notify
|
|
49
|
+
it('notify takes one channel per call', () => {
|
|
50
|
+
const result = run(['notify', '--slack-channel', '#a', '--slack-channel', '#b'], '{}');
|
|
51
|
+
expect(result.status).toBe(1);
|
|
52
|
+
expect(result.stderr).toContain('--slack-channel takes one channel, call notify once for each channel');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('notify announces no release when there are no new commits', () => {
|
|
50
56
|
const document = { commits: [], changedFiles: [], changelog: null, functionallyChangedActors: [] };
|
|
51
|
-
const result = run(['notify-
|
|
57
|
+
const result = run(['notify', '--slack-channel', '#notif'], JSON.stringify(document));
|
|
52
58
|
expect(result.status, result.stderr).toBe(0);
|
|
53
59
|
expect(result.stderr).toContain('No new commits, so there is no release to announce');
|
|
54
60
|
});
|
|
@@ -508,7 +508,7 @@ describe('getChangedActors', () => {
|
|
|
508
508
|
expect(result).toEqual([]);
|
|
509
509
|
});
|
|
510
510
|
|
|
511
|
-
it('does not special-case code/ and shared/ prefixes
|
|
511
|
+
it('does not special-case code/ and shared/ prefixes — they must be declared via overrideActorContext', async () => {
|
|
512
512
|
const result = await changedActorsFor({
|
|
513
513
|
filepathsChanged: ['code/some/code.ts'],
|
|
514
514
|
actorConfigs: [miniActor],
|
|
@@ -1,206 +1,18 @@
|
|
|
1
|
-
import type { MockInstance } from 'vitest';
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
findCommitSha,
|
|
6
|
-
getChangedFiles,
|
|
7
5
|
getChangedFilesBetween,
|
|
8
|
-
getCommits,
|
|
9
6
|
getCommitsBetween,
|
|
10
7
|
getCurrentBranch,
|
|
11
8
|
getRepoName,
|
|
12
9
|
normalizeRepoUrl,
|
|
13
|
-
parseBaseCommit,
|
|
14
10
|
resolveCommitSha,
|
|
15
11
|
resolveReleaseBaseCommit,
|
|
16
12
|
resolveRepoUrl,
|
|
17
13
|
} from '../../../bin/git.js';
|
|
18
14
|
import * as Utils from '../../../bin/utils.js';
|
|
19
15
|
|
|
20
|
-
describe('getCommits', () => {
|
|
21
|
-
const sourceBranch = 'feature-branch';
|
|
22
|
-
const targetBranch = 'main';
|
|
23
|
-
|
|
24
|
-
const sha1 = '1'.repeat(40);
|
|
25
|
-
const sha2 = '2'.repeat(40);
|
|
26
|
-
const sha3 = '3'.repeat(40);
|
|
27
|
-
|
|
28
|
-
const commit1 = `${sha1}»¦«Author1»¦«Date1»¦«First Change On Feature`;
|
|
29
|
-
const commit2 = `${sha2}»¦«Author1»¦«Date2»¦«Second Change On Feature`;
|
|
30
|
-
const commit3 = `${sha3}»¦«Author1»¦«Date3»¦«Third Change On Feature`;
|
|
31
|
-
|
|
32
|
-
let gitCommandSpy: MockInstance;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(`${commit3}\n${commit2}\n${commit1}`);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return commits between source and target branches', async () => {
|
|
39
|
-
// Act
|
|
40
|
-
const commits = await getCommits({ sourceBranch, targetBranch });
|
|
41
|
-
|
|
42
|
-
// Assert
|
|
43
|
-
expect(commits).toStrictEqual([
|
|
44
|
-
{ sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
|
|
45
|
-
{ sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
|
|
46
|
-
{ sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
expect(gitCommandSpy).toHaveBeenCalledTimes(1);
|
|
50
|
-
expect(gitCommandSpy).toHaveBeenCalledWith(
|
|
51
|
-
['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
|
|
52
|
-
{ operation: 'list commits reachable from the source ref but absent from the target ref' },
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should return commits after the base commit if provided', async () => {
|
|
57
|
-
// Act
|
|
58
|
-
const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: sha1 });
|
|
59
|
-
|
|
60
|
-
// Assert
|
|
61
|
-
expect(commits).toStrictEqual([
|
|
62
|
-
{ sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
|
|
63
|
-
{ sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
expect(gitCommandSpy).toHaveBeenCalledTimes(1);
|
|
67
|
-
expect(gitCommandSpy).toHaveBeenCalledWith(
|
|
68
|
-
['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
|
|
69
|
-
{ operation: 'list commits reachable from the source ref but absent from the target ref' },
|
|
70
|
-
);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should ignore the base commit and return all commits when it is the branch HEAD (rerun or force push)', async () => {
|
|
74
|
-
// Act
|
|
75
|
-
const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: sha3 });
|
|
76
|
-
|
|
77
|
-
// Assert
|
|
78
|
-
expect(commits).toStrictEqual([
|
|
79
|
-
{ sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
|
|
80
|
-
{ sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
|
|
81
|
-
{ sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
|
|
82
|
-
]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should return all commits if base commit is not found', async () => {
|
|
86
|
-
// Act
|
|
87
|
-
const commits = await getCommits({ sourceBranch, targetBranch, baseCommit: 'a'.repeat(40) });
|
|
88
|
-
|
|
89
|
-
// Assert
|
|
90
|
-
expect(commits).toStrictEqual([
|
|
91
|
-
{ sha: sha1, author: 'Author1', date: 'Date1', message: 'First Change On Feature' },
|
|
92
|
-
{ sha: sha2, author: 'Author1', date: 'Date2', message: 'Second Change On Feature' },
|
|
93
|
-
{ sha: sha3, author: 'Author1', date: 'Date3', message: 'Third Change On Feature' },
|
|
94
|
-
]);
|
|
95
|
-
|
|
96
|
-
expect(gitCommandSpy).toHaveBeenCalledTimes(1);
|
|
97
|
-
expect(gitCommandSpy).toHaveBeenCalledWith(
|
|
98
|
-
['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'main..feature-branch'],
|
|
99
|
-
{ operation: 'list commits reachable from the source ref but absent from the target ref' },
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('getChangedFiles', () => {
|
|
105
|
-
let gitCommandSpy: MockInstance;
|
|
106
|
-
|
|
107
|
-
beforeEach(() => {
|
|
108
|
-
gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('file1.txt\nfolder/file2.txt');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should return changed files between commits', async () => {
|
|
112
|
-
// Arrange
|
|
113
|
-
const firstSha = '1'.repeat(40);
|
|
114
|
-
const lastSha = '3'.repeat(40);
|
|
115
|
-
const commits = [
|
|
116
|
-
{ sha: firstSha, author: '', date: '', message: '' },
|
|
117
|
-
{ sha: lastSha, author: '', date: '', message: '' },
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
// Act
|
|
121
|
-
const changedFiles = await getChangedFiles(commits);
|
|
122
|
-
|
|
123
|
-
// Assert
|
|
124
|
-
expect(changedFiles).toStrictEqual(['file1.txt', 'folder/file2.txt']);
|
|
125
|
-
|
|
126
|
-
expect(gitCommandSpy).toHaveBeenCalledTimes(1);
|
|
127
|
-
expect(gitCommandSpy).toHaveBeenCalledWith(
|
|
128
|
-
['diff', '--name-only', '--no-renames', `${firstSha}~..${lastSha}`],
|
|
129
|
-
{
|
|
130
|
-
operation: 'get the list of changed files between commits',
|
|
131
|
-
},
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should throw without running git when the commit list is empty', async () => {
|
|
136
|
-
// Act & Assert
|
|
137
|
-
await expect(getChangedFiles([])).rejects.toThrow('Cannot get changed files: the commit list is empty');
|
|
138
|
-
expect(gitCommandSpy).not.toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle only one commit', async () => {
|
|
142
|
-
// Arrange
|
|
143
|
-
const onlySha = '1'.repeat(40);
|
|
144
|
-
const commits = [{ sha: onlySha, author: '', date: '', message: '' }];
|
|
145
|
-
|
|
146
|
-
// Act
|
|
147
|
-
const changedFiles = await getChangedFiles(commits);
|
|
148
|
-
|
|
149
|
-
// Assert
|
|
150
|
-
expect(changedFiles).toStrictEqual(['file1.txt', 'folder/file2.txt']);
|
|
151
|
-
|
|
152
|
-
expect(gitCommandSpy).toHaveBeenCalledTimes(1);
|
|
153
|
-
expect(gitCommandSpy).toHaveBeenCalledWith(['diff', '--name-only', '--no-renames', `${onlySha}~..${onlySha}`], {
|
|
154
|
-
operation: 'get the list of changed files between commits',
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should return an empty list when the net diff is empty (e.g. a commit and its revert)', async () => {
|
|
159
|
-
// Arrange
|
|
160
|
-
gitCommandSpy.mockResolvedValue('');
|
|
161
|
-
const commits = [
|
|
162
|
-
{ sha: '1'.repeat(40), author: '', date: '', message: '' },
|
|
163
|
-
{ sha: '2'.repeat(40), author: '', date: '', message: '' },
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
// Act
|
|
167
|
-
const changedFiles = await getChangedFiles(commits);
|
|
168
|
-
|
|
169
|
-
// Assert
|
|
170
|
-
expect(changedFiles).toStrictEqual([]);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const VALID_SHA = 'a'.repeat(40);
|
|
175
|
-
const VALID_JSON = JSON.stringify({ sha: VALID_SHA, author: 'test', date: 'now', message: 'msg' });
|
|
176
|
-
|
|
177
|
-
describe('parseBaseCommit', () => {
|
|
178
|
-
it('should return undefined for undefined input', () => {
|
|
179
|
-
expect(parseBaseCommit(undefined)).toBeUndefined();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should return undefined for empty string', () => {
|
|
183
|
-
expect(parseBaseCommit('')).toBeUndefined();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('should accept a plain SHA string', () => {
|
|
187
|
-
expect(parseBaseCommit(VALID_SHA)).toBe(VALID_SHA);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should extract sha from a JSON commit object', () => {
|
|
191
|
-
expect(parseBaseCommit(VALID_JSON)).toBe(VALID_SHA);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should throw on an invalid SHA string', () => {
|
|
195
|
-
expect(() => parseBaseCommit('not-a-sha')).toThrow('Invalid base commit SHA');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should throw when JSON contains an invalid sha field', () => {
|
|
199
|
-
const badJson = JSON.stringify({ sha: 'bad', author: 'test', date: 'now', message: 'msg' });
|
|
200
|
-
expect(() => parseBaseCommit(badJson)).toThrow('Invalid base commit SHA');
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
16
|
describe('getCurrentBranch', () => {
|
|
205
17
|
it('should return the checked-out branch', async () => {
|
|
206
18
|
const gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('master');
|
|
@@ -279,7 +91,7 @@ describe('resolveReleaseBaseCommit', () => {
|
|
|
279
91
|
});
|
|
280
92
|
await expect(resolveReleaseBaseCommit(baseSha)).resolves.toBe(baseSha);
|
|
281
93
|
expect(gitCommandSpy).toHaveBeenCalledWith(['rev-parse', '--verify', '--quiet', `${baseSha}^{commit}`], {
|
|
282
|
-
operation:
|
|
94
|
+
operation: `look up ${baseSha} in local Git history`,
|
|
283
95
|
});
|
|
284
96
|
expect(gitCommandSpy).toHaveBeenCalledWith(['merge-base', baseSha, 'HEAD'], {
|
|
285
97
|
operation: 'check whether the release base commit is an ancestor of HEAD',
|
|
@@ -348,11 +160,11 @@ describe('resolveReleaseBaseCommit', () => {
|
|
|
348
160
|
new Utils.GitCommandError(
|
|
349
161
|
['rev-parse', '--verify', '--quiet', `${baseSha}^{commit}`],
|
|
350
162
|
Object.assign(new Error('Git failed'), { code: 128 }),
|
|
351
|
-
|
|
163
|
+
`look up ${baseSha} in local Git history`,
|
|
352
164
|
),
|
|
353
165
|
);
|
|
354
166
|
await expect(resolveReleaseBaseCommit(baseSha)).rejects.toThrow(
|
|
355
|
-
|
|
167
|
+
`Failed to look up ${baseSha} in local Git history.\n` +
|
|
356
168
|
`Command: git "rev-parse" "--verify" "--quiet" "${baseSha}^{commit}"\nGit error: Git failed`,
|
|
357
169
|
);
|
|
358
170
|
});
|
|
@@ -399,12 +211,6 @@ describe('empty commit ranges', () => {
|
|
|
399
211
|
it('getCommitsBetween returns no commits', async () => {
|
|
400
212
|
await expect(getCommitsBetween('HEAD', 'HEAD')).resolves.toStrictEqual([]);
|
|
401
213
|
});
|
|
402
|
-
|
|
403
|
-
it('getCommits fails clearly, since the commands built on it need at least one commit', async () => {
|
|
404
|
-
await expect(getCommits({ sourceBranch: 'HEAD', targetBranch: 'HEAD' })).rejects.toThrow(
|
|
405
|
-
'HEAD has no commits that are not on HEAD.',
|
|
406
|
-
);
|
|
407
|
-
});
|
|
408
214
|
});
|
|
409
215
|
|
|
410
216
|
describe('getChangedFilesBetween', () => {
|