Skip to content

Commit 704b06a

Browse files
committed
git.ts: extract spawn logic into a wrapper and add tests
Currently, it's a bit hard to ensure that the code in git.exe actually does what we expect it to do, especially considering that we're calling things like please.sh and assume that some executables are available on the PATH. Let's ensure we add some tests for these code paths, which we can easily extend with more tests if needed. Signed-off-by: Dennis Ameling <[email protected]>
1 parent 8310ae7 commit 704b06a

File tree

5 files changed

+140
-35
lines changed

5 files changed

+140
-35
lines changed

.jest/setEnvVars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/*
2+
This ensures that the PATH of the machine that the tests are running on,
3+
doesn't leak into our tests.
4+
*/
5+
process.env.PATH = ''

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
clearMocks: true,
33
moduleFileExtensions: ['js', 'ts'],
4+
setupFiles: ["<rootDir>/.jest/setEnvVars.ts"],
45
testEnvironment: 'node',
56
testMatch: ['**/*.test.ts'],
67
testRunner: 'jest-circus/runner',

src/__tests__/git.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {mkdirSync, rmdirSync, existsSync} from 'fs'
2+
import * as git from '../git'
3+
import * as spawn from '../spawn'
4+
import * as core from '@actions/core'
5+
6+
describe('git', () => {
7+
// We don't want to _actually_ spawn external commands, so we mock the function
8+
let spawnSpy: jest.SpyInstance
9+
// Capture the startGroup calls
10+
let coreSpy: jest.SpyInstance
11+
12+
beforeEach(() => {
13+
coreSpy = jest.spyOn(core, 'startGroup')
14+
spawnSpy = jest.spyOn(spawn, 'spawn').mockResolvedValue({
15+
// 0 is the exit code for success
16+
exitCode: 0
17+
})
18+
// We don't want to _actually_ clone the repo, so we mock the function
19+
jest.spyOn(git, 'clone').mockResolvedValue()
20+
21+
// The script removes the .tmp folder at the end, so let's create it
22+
mkdirSync('.tmp')
23+
})
24+
25+
afterEach(() => {
26+
// Clean up the .tmp folder if the script didn't already do it
27+
if (existsSync('.tmp')) {
28+
rmdirSync('.tmp', {recursive: true})
29+
}
30+
})
31+
32+
test('getViaGit build-installers x86_64', async () => {
33+
const flavor = 'build-installers'
34+
const architecture = 'x86_64'
35+
const outputDirectory = 'outputDirectory'
36+
const {artifactName, download} = await git.getViaGit(flavor, architecture)
37+
38+
expect(artifactName).toEqual('git-sdk-64-build-installers')
39+
40+
await download(outputDirectory, true)
41+
42+
expect(coreSpy).toHaveBeenCalledWith(`Creating ${flavor} artifact`)
43+
expect(spawnSpy).toHaveBeenCalledWith(
44+
expect.stringContaining('/bash.exe'),
45+
expect.arrayContaining([
46+
'.tmp/build-extra/please.sh',
47+
'create-sdk-artifact',
48+
`--architecture=${architecture}`,
49+
`--out=${outputDirectory}`
50+
]),
51+
expect.objectContaining({
52+
env: expect.objectContaining({
53+
// We want to ensure that the correct /bin folders are in the PATH,
54+
// so that please.sh can find git.exe
55+
// https://github.com/git-for-windows/setup-git-for-windows-sdk/issues/951
56+
PATH:
57+
expect.stringContaining('/clangarm64/bin') &&
58+
expect.stringContaining('/mingw64/bin')
59+
})
60+
})
61+
)
62+
})
63+
64+
test('getViaGit full x86_64', async () => {
65+
const flavor = 'full'
66+
const architecture = 'x86_64'
67+
const outputDirectory = 'outputDirectory'
68+
const {artifactName, download} = await git.getViaGit(flavor, architecture)
69+
70+
expect(artifactName).toEqual('git-sdk-64-full')
71+
72+
await download(outputDirectory, true)
73+
74+
expect(coreSpy).toHaveBeenCalledWith(`Checking out git-sdk-64`)
75+
expect(spawnSpy).toHaveBeenCalledWith(
76+
expect.stringContaining('/git.exe'),
77+
expect.arrayContaining([
78+
'--git-dir=.tmp',
79+
'worktree',
80+
'add',
81+
outputDirectory
82+
]),
83+
expect.any(Object)
84+
)
85+
})
86+
})

src/git.ts

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as core from '@actions/core'
2-
import {ChildProcess, spawn} from 'child_process'
2+
import {spawn, SpawnReturnArgs} from './spawn'
33
import {Octokit} from '@octokit/rest'
44
import {delimiter} from 'path'
55
import * as fs from 'fs'
@@ -48,14 +48,14 @@ export function getArtifactMetadata(
4848
return {repo, artifactName}
4949
}
5050

51-
async function clone(
51+
export async function clone(
5252
url: string,
5353
destination: string,
5454
verbose: number | boolean,
5555
cloneExtraOptions: string[] = []
5656
): Promise<void> {
5757
if (verbose) core.info(`Cloning ${url} to ${destination}`)
58-
const child = spawn(
58+
const child = await spawn(
5959
gitExePath,
6060
[
6161
'clone',
@@ -73,22 +73,16 @@ async function clone(
7373
stdio: [undefined, 'inherit', 'inherit']
7474
}
7575
)
76-
return new Promise<void>((resolve, reject) => {
77-
child.on('close', code => {
78-
if (code === 0) {
79-
resolve()
80-
} else {
81-
reject(new Error(`git clone: exited with code ${code}`))
82-
}
83-
})
84-
})
76+
if (child.exitCode !== 0) {
77+
throw new Error(`git clone: exited with code ${child.exitCode}`)
78+
}
8579
}
8680

8781
async function updateHEAD(
8882
bareRepositoryPath: string,
8983
headSHA: string
9084
): Promise<void> {
91-
const child = spawn(
85+
const child = await spawn(
9286
gitExePath,
9387
['--git-dir', bareRepositoryPath, 'update-ref', 'HEAD', headSHA],
9488
{
@@ -98,15 +92,9 @@ async function updateHEAD(
9892
stdio: [undefined, 'inherit', 'inherit']
9993
}
10094
)
101-
return new Promise<void>((resolve, reject) => {
102-
child.on('close', code => {
103-
if (code === 0) {
104-
resolve()
105-
} else {
106-
reject(new Error(`git: exited with code ${code}`))
107-
}
108-
})
109-
})
95+
if (child.exitCode !== 0) {
96+
throw new Error(`git: exited with code ${child.exitCode}`)
97+
}
11098
}
11199

112100
export async function getViaGit(
@@ -175,10 +163,10 @@ export async function getViaGit(
175163
])
176164
core.endGroup()
177165

178-
let child: ChildProcess
166+
let child: SpawnReturnArgs
179167
if (flavor === 'full') {
180168
core.startGroup(`Checking out ${repo}`)
181-
child = spawn(
169+
child = await spawn(
182170
gitExePath,
183171
[`--git-dir=.tmp`, 'worktree', 'add', outputDirectory, head_sha],
184172
{
@@ -200,7 +188,7 @@ export async function getViaGit(
200188

201189
core.startGroup(`Creating ${flavor} artifact`)
202190
const traceArg = verbose ? ['-x'] : []
203-
child = spawn(
191+
child = await spawn(
204192
`${gitForWindowsUsrBinPath}/bash.exe`,
205193
[
206194
...traceArg,
@@ -226,16 +214,12 @@ export async function getViaGit(
226214
}
227215
)
228216
}
229-
return new Promise<void>((resolve, reject) => {
230-
child.on('close', code => {
231-
core.endGroup()
232-
if (code === 0) {
233-
fs.rm('.tmp', {recursive: true}, () => resolve())
234-
} else {
235-
reject(new Error(`process exited with code ${code}`))
236-
}
237-
})
238-
})
217+
core.endGroup()
218+
if (child.exitCode === 0) {
219+
fs.rmSync('.tmp', {recursive: true})
220+
} else {
221+
throw new Error(`process exited with code ${child.exitCode}`)
222+
}
239223
}
240224
}
241225
}

src/spawn.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
spawn as SpawnInternal,
3+
SpawnOptionsWithStdioTuple,
4+
StdioNull,
5+
StdioPipe
6+
} from 'child_process'
7+
8+
export type SpawnReturnArgs = {
9+
exitCode: number | null
10+
}
11+
12+
/**
13+
* Simple wrapper around NodeJS's "child_process.spawn" function.
14+
* Since we only use the exit code, we only expose that.
15+
*/
16+
export async function spawn(
17+
command: string,
18+
args: readonly string[],
19+
options: SpawnOptionsWithStdioTuple<StdioPipe, StdioNull, StdioNull>
20+
): Promise<SpawnReturnArgs> {
21+
const child = SpawnInternal(command, args, options)
22+
23+
return new Promise<SpawnReturnArgs>((resolve, reject) => {
24+
child.on('error', reject)
25+
child.on('close', code => {
26+
resolve({exitCode: code})
27+
})
28+
})
29+
}

0 commit comments

Comments
 (0)