Skip to content

Commit 4735a96

Browse files
committed
Add unit tests
1 parent d0f0e43 commit 4735a96

File tree

17 files changed

+6043
-536
lines changed

17 files changed

+6043
-536
lines changed

.github/workflows/compile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: Compile with ncc
1616
run: |
1717
npm install
18-
npx ncc build index.ts -o dist
18+
npx ncc build src/index.ts -o dist
1919
- name: Push
2020
env:
2121
DIR: dist

.github/workflows/test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ name: Test
33
on: [push, pull_request]
44

55
jobs:
6+
unit-tests:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-node@v4
11+
with:
12+
node-version: 20
13+
- name: Test
14+
run: |
15+
npm install
16+
npm test
17+
618
tests-with-token:
719
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == 'axel-op'
820
strategy:

__tests__/exec.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { expect, jest, test } from '@jest/globals';
2+
import { ExecOptions } from '@actions/exec';
3+
import { wrapExecutor } from '../src/exec';
4+
5+
function mockWrappedExec(returnCode: number, std: { stdOut: string, stdErr: string }) {
6+
return jest.fn((command: string, args?: string[], options?: ExecOptions) => {
7+
options?.listeners?.stdout?.(Buffer.from(std.stdOut));
8+
options?.listeners?.stderr?.(Buffer.from(std.stdErr));
9+
return Promise.resolve(returnCode);
10+
});
11+
}
12+
13+
test('test executing command with no error', async () => {
14+
const stdOut = 'hello world';
15+
const stdErr = '';
16+
const exitCode = 0;
17+
const mockedWrapped = mockWrappedExec(exitCode, { stdOut, stdErr });
18+
const execute = wrapExecutor(mockedWrapped);
19+
const command = 'echo';
20+
const args = ['hello world'];
21+
const result = await execute(command, args);
22+
expect(result).toEqual({ exitCode, stdOut, stdErr });
23+
expect(mockedWrapped).toHaveBeenCalledTimes(1);
24+
});
25+
26+
test('test executing command with error and ignoring return code', async () => {
27+
const stdOut = 'hello world';
28+
const stdErr = '';
29+
const exitCode = 1;
30+
const mockedWrapped = mockWrappedExec(exitCode, { stdOut, stdErr });
31+
const execute = wrapExecutor(mockedWrapped);
32+
const command = 'echo';
33+
const args = ['hello world'];
34+
const result = await execute(command, args);
35+
expect(result).toEqual({ exitCode, stdOut, stdErr });
36+
expect(mockedWrapped).toHaveBeenCalledTimes(1);
37+
});
38+
39+
test('test executing command with error and not ignoring return code', async () => {
40+
const stdOut = 'hello world';
41+
const stdErr = '';
42+
const exitCode = 1;
43+
const mockedWrapped = mockWrappedExec(exitCode, { stdOut, stdErr });
44+
const execute = wrapExecutor(mockedWrapped);
45+
const command = 'echo';
46+
const args = ['hello world'];
47+
const fn = async () => await execute(command, args, { ignoreReturnCode: false });
48+
await expect(fn).rejects.toThrow(new Error("Command 'echo hello world' failed with exit code 1"));
49+
expect(mockedWrapped).toHaveBeenCalledTimes(1);
50+
});

__tests__/main.test.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
2+
import { getInput, Main } from '../src/main';
3+
import { CommandExecutor } from '../src/exec';
4+
import { ReleaseData, Releases } from '../src/releases';
5+
import { GitOperations } from '../src/git';
6+
7+
const mockGetLatestReleaseData = jest.fn<InstanceType<typeof Releases>['getLatestReleaseData']>();
8+
const mockGetReleaseDataByName = jest.fn<InstanceType<typeof Releases>['getReleaseDataByName']>();
9+
jest.mock('../src/releases', () => {
10+
return {
11+
Releases: jest.fn().mockImplementation(() => {
12+
return {
13+
getLatestReleaseData: mockGetLatestReleaseData,
14+
getReleaseDataByName: mockGetReleaseDataByName,
15+
}
16+
}),
17+
};
18+
});
19+
20+
const mockHasChanges = jest.fn<InstanceType<typeof GitOperations>['hasChanges']>();
21+
const mockCommitAll = jest.fn<InstanceType<typeof GitOperations>['commitAll']>();
22+
const mockPush = jest.fn<InstanceType<typeof GitOperations>['push']>();
23+
jest.mock('../src/git', () => {
24+
return {
25+
GitOperations: jest.fn().mockImplementation(() => {
26+
return {
27+
configureGit: () => { },
28+
commitAll: mockCommitAll,
29+
hasChanges: mockHasChanges,
30+
push: mockPush,
31+
}
32+
}),
33+
};
34+
});
35+
36+
const executor = jest.fn<CommandExecutor>();
37+
38+
beforeEach(() => {
39+
executor.mockReset();
40+
});
41+
42+
const dummyReleaseData: ReleaseData = {
43+
url: "",
44+
html_url: "",
45+
assets_url: "",
46+
upload_url: "",
47+
tarball_url: null,
48+
zipball_url: null,
49+
id: 0,
50+
node_id: "",
51+
tag_name: "",
52+
target_commitish: "",
53+
name: "dummy-release-data",
54+
draft: false,
55+
prerelease: false,
56+
created_at: "",
57+
published_at: null,
58+
author: {
59+
name: undefined,
60+
email: undefined,
61+
login: "",
62+
id: 0,
63+
node_id: "",
64+
avatar_url: "",
65+
gravatar_id: null,
66+
url: "",
67+
html_url: "",
68+
followers_url: "",
69+
following_url: "",
70+
gists_url: "",
71+
starred_url: "",
72+
subscriptions_url: "",
73+
organizations_url: "",
74+
repos_url: "",
75+
events_url: "",
76+
received_events_url: "",
77+
type: "",
78+
site_admin: false,
79+
starred_at: undefined
80+
},
81+
assets: []
82+
};
83+
84+
function defineInput(inputName: string, inputValue?: string) {
85+
process.env[`INPUT_${inputName.toUpperCase()}`] = inputValue;
86+
}
87+
88+
describe('test getting inputs', () => {
89+
const existingInputName = "my-input";
90+
const existingInputValue = "my-input-value";
91+
92+
beforeEach(() => {
93+
defineInput(existingInputName, existingInputValue);
94+
});
95+
96+
afterEach(() => {
97+
defineInput(existingInputName, undefined);
98+
});
99+
100+
test('test getting existing input value', () => {
101+
const value = getInput([existingInputName, "non-existing-input"]);
102+
expect(value).toBe(existingInputValue);
103+
});
104+
105+
test('test getting existing input value with fallback input name', () => {
106+
const value = getInput(["input_name_one", existingInputName]);
107+
expect(value).toBe(existingInputValue);
108+
});
109+
110+
test('test getting non existing input value', () => {
111+
const inputName = "my-non-existing-input";
112+
const value = getInput([inputName]);
113+
expect(value).toBeUndefined();
114+
});
115+
});
116+
117+
describe('test getting Java version', () => {
118+
const main = new Main(executor);
119+
120+
test('test invalid return code', () => {
121+
const error = new Error("Command 'java -version' failed with exit code 1");
122+
executor.mockReturnValueOnce(Promise.reject(error));
123+
expect(() => main.getJavaVersion())
124+
.rejects
125+
.toThrow(error);
126+
expect(executor).lastCalledWith('java', ['-version'], { ignoreReturnCode: false, silent: true });
127+
});
128+
129+
test('test no output', () => {
130+
executor.mockReturnValueOnce(Promise.resolve({
131+
exitCode: 0,
132+
stdErr: '',
133+
stdOut: '',
134+
}));
135+
expect(() => main.getJavaVersion())
136+
.rejects
137+
.toThrow(new Error("Cannot find Java version number"));
138+
expect(executor).lastCalledWith('java', ['-version'], { ignoreReturnCode: false, silent: true });
139+
});
140+
141+
test('test version <= JDK 8', () => {
142+
executor.mockReturnValueOnce(Promise.resolve({
143+
exitCode: 0,
144+
stdErr: 'java version "1.8.0_211"',
145+
stdOut: '',
146+
}));
147+
expect(main.getJavaVersion())
148+
.resolves
149+
.toBe(8);
150+
expect(executor).lastCalledWith('java', ['-version'], { ignoreReturnCode: false, silent: true });
151+
});
152+
153+
test('test version > JDK 8', () => {
154+
executor.mockReturnValueOnce(Promise.resolve({
155+
exitCode: 0,
156+
stdErr: 'openjdk version "21.0.6" 2025-01-21',
157+
stdOut: '',
158+
}));
159+
expect(main.getJavaVersion())
160+
.resolves
161+
.toBe(21);
162+
expect(executor).lastCalledWith('java', ['-version'], { ignoreReturnCode: false, silent: true });
163+
});
164+
});
165+
166+
describe('test getting release data', () => {
167+
test('if no release name and java version >= 11, then return latest release', async () => {
168+
const main = new Main(executor);
169+
mockGetLatestReleaseData.mockReturnValueOnce(Promise.resolve(dummyReleaseData));
170+
const releaseData = await main.getReleaseData(11, undefined);
171+
expect(releaseData).toEqual(dummyReleaseData);
172+
});
173+
174+
test('if no release name and java version < 11, then return release 1.7', async () => {
175+
const main = new Main(executor);
176+
mockGetReleaseDataByName.mockReturnValueOnce(Promise.resolve(dummyReleaseData));
177+
const releaseData = await main.getReleaseData(8, undefined);
178+
expect(releaseData).toEqual(dummyReleaseData);
179+
expect(mockGetReleaseDataByName).lastCalledWith('1.7');
180+
});
181+
182+
test('if release name, then return it', async () => {
183+
const releaseName = 'my-release';
184+
const main = new Main(executor);
185+
mockGetReleaseDataByName.mockReturnValueOnce(Promise.resolve(dummyReleaseData));
186+
const releaseData = await main.getReleaseData(11, releaseName);
187+
expect(releaseData).toEqual(dummyReleaseData);
188+
expect(mockGetReleaseDataByName).lastCalledWith(releaseName);
189+
});
190+
191+
test('if release not found, then throw', async () => {
192+
const releaseName = 'my-release';
193+
defineInput('version', releaseName);
194+
const main = new Main(executor);
195+
mockGetReleaseDataByName.mockReturnValueOnce(Promise.resolve(undefined));
196+
expect(() => main.getReleaseData(11, releaseName))
197+
.rejects
198+
.toThrow(new Error(`Cannot find release id of Google Java Format ${releaseName}`));
199+
expect(mockGetReleaseDataByName).lastCalledWith(releaseName);
200+
});
201+
});
202+
203+
describe('test getting args', () => {
204+
const main = new Main(executor);
205+
206+
test('if input has args, then output has them too', async () => {
207+
const inputs = { args: ['--replace'], files: '**/*.java', filesExcluded: undefined };
208+
const output = await main.getGJFArgs(inputs);
209+
expect(output).toEqual(['--replace']);
210+
});
211+
212+
test('files matching glob are appended to output', async () => {
213+
const inputs = { args: ['--replace'], files: '*.md', filesExcluded: undefined };
214+
const output = await main.getGJFArgs(inputs);
215+
expect(output).toHaveLength(2);
216+
expect(output[0]).toEqual('--replace');
217+
expect(output[1]).toMatch(/^.*README\.md$/);
218+
});
219+
220+
test('if input has exclusion glob, then output excludes matching files', async () => {
221+
const inputs = { args: ['--replace'], files: '*.md', filesExcluded: '*.md' };
222+
const output = await main.getGJFArgs(inputs);
223+
expect(output).toEqual(['--replace']);
224+
})
225+
});
226+
227+
describe('commit changes', () => {
228+
const githubToken = '***';
229+
defineInput('github-token', githubToken);
230+
const main = new Main(executor);
231+
232+
test('if there is no change, then skip commit', async () => {
233+
mockHasChanges.mockReturnValueOnce(Promise.resolve(false));
234+
await main.commitChanges({ githubActor: '', repository: '', commitMessage: undefined, });
235+
expect(mockCommitAll).not.toBeCalled();
236+
expect(mockPush).not.toBeCalled();
237+
});
238+
239+
test('if there are changes, but no commit message, then commit with default message', async () => {
240+
mockHasChanges.mockReturnValueOnce(Promise.resolve(true));
241+
const githubActor = "actor";
242+
const repository = "actor/repo";
243+
await main.commitChanges({ githubActor, repository, commitMessage: undefined });
244+
expect(mockCommitAll).toHaveBeenCalledWith('Google Java Format');
245+
expect(mockPush).toBeCalledWith({ githubActor, repository, githubToken });
246+
});
247+
248+
test('if there are changes, and a commit message, then commit with message', async () => {
249+
mockHasChanges.mockReturnValueOnce(Promise.resolve(true));
250+
const githubActor = "actor";
251+
const repository = "actor/repo";
252+
const commitMessage = "my message";
253+
await main.commitChanges({ githubActor, repository, commitMessage });
254+
expect(mockCommitAll).toHaveBeenCalledWith(commitMessage);
255+
expect(mockPush).toBeCalledWith({ githubActor, repository, githubToken });
256+
});
257+
});
258+
259+
describe('execute Google Java Format', () => {
260+
const executablePath = 'google-java-format.jar';
261+
const main = new Main(executor, executablePath);
262+
263+
test('when running GJF, then pass user args to command', async () => {
264+
const mockedResult = { exitCode: 0, stdErr: '', stdOut: '' };
265+
executor.mockReturnValueOnce(Promise.resolve(mockedResult));
266+
const args = ['a', 'b', 'c'];
267+
const result = await main.executeGJF(8, args);
268+
expect(result).toEqual(mockedResult);
269+
expect(executor).lastCalledWith('java', ['-jar', executablePath, 'a', 'b', 'c'], { ignoreReturnCode: false });
270+
});
271+
272+
test('when running GJF with java version >= 11, then exports required jdk modules', async () => {
273+
const mockedResult = { exitCode: 0, stdErr: '', stdOut: '' };
274+
executor.mockReturnValueOnce(Promise.resolve(mockedResult));
275+
const args = ['a', 'b', 'c'];
276+
const result = await main.executeGJF(11, args);
277+
expect(result).toEqual(mockedResult);
278+
expect(executor).lastCalledWith(
279+
'java',
280+
[
281+
'--add-exports',
282+
'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
283+
'--add-exports',
284+
'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
285+
'--add-exports',
286+
'jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
287+
'--add-exports',
288+
'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
289+
'--add-exports',
290+
'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
291+
'-jar',
292+
executablePath,
293+
'a',
294+
'b',
295+
'c',
296+
],
297+
{ ignoreReturnCode: false }
298+
);
299+
});
300+
});

0 commit comments

Comments
 (0)