Skip to content

Commit 9e04da1

Browse files
authored
Merge pull request #9 from redhat-developer/add-tests
chore(ci): add e2e tests
2 parents 0d05cdc + ef5c181 commit 9e04da1

File tree

5 files changed

+882
-34
lines changed

5 files changed

+882
-34
lines changed

.github/workflows/pr.yaml

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI
1+
name: PR Checks and Tests
22

33
on:
44
pull_request:
@@ -8,12 +8,9 @@ concurrency:
88
cancel-in-progress: true
99

1010
jobs:
11-
ci:
12-
name: CI
11+
checks:
12+
name: Checks
1313
runs-on: ubuntu-latest
14-
permissions:
15-
contents: write
16-
pull-requests: write
1714
env:
1815
CI: true
1916

@@ -57,3 +54,36 @@ jobs:
5754
git diff
5855
exit 1
5956
fi
57+
58+
tests:
59+
name: Tests
60+
runs-on: ubuntu-latest
61+
env:
62+
CI: true
63+
COMMUNITY_PLUGINS_REPO_ARCHIVE: /tmp/community-plugins-repo.tar.gz
64+
65+
steps:
66+
- name: Checkout
67+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v
68+
69+
- name: Set up Node
70+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
71+
with:
72+
cache: 'yarn'
73+
node-version-file: .nvmrc
74+
75+
- name: yarn install
76+
run: yarn install --immutable
77+
78+
- name: run unit tests
79+
run: yarn test
80+
81+
- name: Cache Backstage Community Repo
82+
id: backstage-community-repo-cache
83+
uses: actions/cache@v4
84+
with:
85+
path: ${{ env.COMMUNITY_PLUGINS_REPO_ARCHIVE }}
86+
key: ${{ runner.os }}-backstage-community-repo
87+
88+
- name: run e2e tests
89+
run: yarn test:e2e
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import fs from 'fs-extra';
2+
import os from 'os';
3+
import path from 'path';
4+
import { promisify } from 'util';
5+
import * as tar from 'tar';
6+
import axios from 'axios';
7+
8+
const exec = promisify(require('child_process').exec);
9+
10+
const CONTAINER_TOOL = process.env.CONTAINER_TOOL || 'podman';
11+
12+
async function downloadFile(url: string, file: string): Promise<void> {
13+
console.log(`Downloading file from ${url} to ${file}`);
14+
const response = await axios({
15+
method: 'GET',
16+
url: url,
17+
responseType: 'stream',
18+
});
19+
20+
const fileStream = fs.createWriteStream(file);
21+
response.data.pipe(fileStream);
22+
23+
return new Promise((resolve, reject) => {
24+
fileStream.on('finish', resolve);
25+
fileStream.on('error', reject);
26+
response.data.on('error', reject);
27+
});
28+
}
29+
30+
async function runCommand(
31+
command: string,
32+
options: { cwd?: string } = {},
33+
): Promise<{ stdout: string; stderr: string }> {
34+
console.log(
35+
`Executing command: ${command}, in directory: ${options.cwd || process.cwd()}`,
36+
);
37+
38+
const { err, stdout, stderr } = await exec(command, {
39+
shell: true,
40+
...options,
41+
});
42+
console.log(`Command output: ${stdout}`);
43+
console.log(`Command error output: ${stderr}`);
44+
if (err) {
45+
console.error(`Error executing command: ${command}`);
46+
console.error(stderr);
47+
console.error(stdout);
48+
throw err;
49+
}
50+
return { stdout, stderr };
51+
}
52+
53+
async function getDynamicPluginAnnotation(image: string): Promise<object[]> {
54+
const { stdout } = await runCommand(`${CONTAINER_TOOL} inspect ${image}`);
55+
const imageInfo = JSON.parse(stdout)[0];
56+
const dynamicPackagesAnnotation =
57+
imageInfo.Annotations['io.backstage.dynamic-packages'];
58+
return JSON.parse(
59+
Buffer.from(dynamicPackagesAnnotation, 'base64').toString('utf-8'),
60+
);
61+
}
62+
63+
// you can use COMMUNITY_PLUGINS_REPO_ARCHIVE env variable to specify a path existing local archive of the community plugins repository
64+
// this is useful to avoid downloading the archive every time
65+
// e.g. COMMUNITY_PLUGINS_REPO_ARCHIVE=/path/to/archive.tar.gz
66+
// if not set, it will download the archive from the specified REPO_URL
67+
describe('export and package backstage-community plugin', () => {
68+
const TEST_TIMEOUT = 5 * 60 * 1000;
69+
const RHDH_CLI = path.resolve(__dirname, '../bin/rhdh-cli');
70+
const REPO_URL =
71+
'https://github.com/backstage/community-plugins/archive/refs/heads/main.tar.gz';
72+
73+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rhdh-cli-e2e-'));
74+
const getClonedRepoPath = () => path.join(tmpDir, 'community-plugins-main');
75+
76+
jest.setTimeout(TEST_TIMEOUT);
77+
78+
beforeAll(async () => {
79+
console.log(`Using rhdh-cli at: ${RHDH_CLI}`);
80+
console.log(`Test workspace: ${tmpDir}`);
81+
console.log(`Container tool: ${CONTAINER_TOOL}`);
82+
83+
let communityPluginsArchivePath = path.join(
84+
tmpDir,
85+
'community-plugins.tar.gz',
86+
);
87+
88+
if (process.env.COMMUNITY_PLUGINS_REPO_ARCHIVE) {
89+
communityPluginsArchivePath = process.env.COMMUNITY_PLUGINS_REPO_ARCHIVE;
90+
console.log(
91+
`Using community plugins repo archive: ${communityPluginsArchivePath}`,
92+
);
93+
}
94+
95+
if (!fs.existsSync(communityPluginsArchivePath)) {
96+
console.log(`Downloading community plugins archive from: ${REPO_URL}`);
97+
await downloadFile(REPO_URL, communityPluginsArchivePath);
98+
console.log(
99+
`Downloaded community plugins archive to: ${communityPluginsArchivePath}`,
100+
);
101+
} else {
102+
console.log(
103+
`Using existing community plugins archive: ${communityPluginsArchivePath}`,
104+
);
105+
}
106+
107+
console.log(
108+
`Extracting community plugins archive to: ${getClonedRepoPath()}`,
109+
);
110+
fs.mkdirSync(getClonedRepoPath(), { recursive: true });
111+
await tar.x({
112+
file: communityPluginsArchivePath,
113+
cwd: getClonedRepoPath(),
114+
strip: 1,
115+
sync: true,
116+
});
117+
});
118+
119+
afterAll(async () => {
120+
if (tmpDir && fs.existsSync(tmpDir)) {
121+
fs.removeSync(tmpDir);
122+
}
123+
});
124+
125+
describe.each([
126+
[
127+
'workspaces/tech-radar/plugins/tech-radar',
128+
`rhdh-test-tech-radar-frontend:${Date.now()}`,
129+
],
130+
[
131+
'workspaces/tech-radar/plugins/tech-radar-backend',
132+
`rhdh-test-tech-radar-backend:${Date.now()}`,
133+
],
134+
])('plugin in %s directory', (pluginPath, imageTag) => {
135+
const getFullPluginPath = () => path.join(getClonedRepoPath(), pluginPath);
136+
137+
beforeAll(async () => {
138+
console.log(`Installing dependencies in ${getFullPluginPath()}`);
139+
await runCommand(`yarn install`, {
140+
cwd: getFullPluginPath(),
141+
});
142+
console.log(`Compiling TypeScript in ${getFullPluginPath()}`);
143+
await runCommand(`npx tsc`, {
144+
cwd: getFullPluginPath(),
145+
});
146+
});
147+
148+
afterAll(async () => {
149+
console.log(`Cleaning up image: ${imageTag}`);
150+
await runCommand(`${CONTAINER_TOOL} rmi -f ${imageTag}`);
151+
});
152+
153+
test('should export the plugin', async () => {
154+
await runCommand(`${RHDH_CLI} plugin export`, {
155+
cwd: getFullPluginPath(),
156+
});
157+
158+
expect(
159+
fs.existsSync(
160+
path.join(getFullPluginPath(), 'dist-dynamic/package.json'),
161+
),
162+
).toEqual(true);
163+
164+
const packageJsonPath = path.join(getFullPluginPath(), 'package.json');
165+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
166+
const role = packageJson.backstage?.role;
167+
if (role === 'frontend-plugin') {
168+
// eslint-disable-next-line jest/no-conditional-expect
169+
expect(
170+
fs.existsSync(
171+
path.join(
172+
getFullPluginPath(),
173+
'dist-dynamic/dist-scalprum/plugin-manifest.json',
174+
),
175+
),
176+
).toEqual(true);
177+
}
178+
});
179+
180+
test('should package the plugin', async () => {
181+
await runCommand(`${RHDH_CLI} plugin package --tag ${imageTag}`, {
182+
cwd: getFullPluginPath(),
183+
});
184+
185+
const annotation = await getDynamicPluginAnnotation(imageTag);
186+
expect(annotation).not.toBeNull();
187+
console.log(`Plugin annotation: ${JSON.stringify(annotation)}`);
188+
189+
expect(annotation.length).toBe(1);
190+
expect(Object.keys(annotation[0]).length).toBe(1);
191+
192+
const key = Object.keys(annotation[0])[0];
193+
const pluginInfo = annotation[0][key];
194+
195+
const pluginJson = JSON.parse(
196+
fs.readFileSync(
197+
path.join(getFullPluginPath(), 'dist-dynamic', 'package.json'),
198+
'utf-8',
199+
),
200+
);
201+
expect(pluginInfo.name).toEqual(pluginJson.name);
202+
expect(pluginInfo.version).toEqual(pluginJson.version);
203+
expect(pluginInfo.backstage).toEqual(pluginJson.backstage);
204+
205+
const { stdout } = await runCommand(
206+
`${CONTAINER_TOOL} create --workdir / ${imageTag} 'false'`,
207+
);
208+
const containerId = stdout.trim();
209+
const imageContentDir = path.join(getFullPluginPath(), imageTag);
210+
fs.mkdirSync(imageContentDir);
211+
await runCommand(
212+
`${CONTAINER_TOOL} cp ${containerId}:/ ${imageContentDir}`,
213+
);
214+
await runCommand(`${CONTAINER_TOOL} rm ${containerId}`);
215+
216+
await runCommand(`ls -lah ${path.join(imageContentDir, key)}`);
217+
await runCommand(
218+
`ls -lah ${path.join(getFullPluginPath(), 'dist-dynamic')}`,
219+
);
220+
221+
const filesInImage = fs.readdirSync(path.join(imageContentDir, key));
222+
const filesInDerivedPackage = fs.readdirSync(
223+
path.join(getFullPluginPath(), 'dist-dynamic'),
224+
);
225+
expect(filesInImage.length).toEqual(filesInDerivedPackage.length);
226+
227+
const indexJson = JSON.parse(
228+
fs.readFileSync(path.join(imageContentDir, 'index.json'), 'utf-8'),
229+
);
230+
console.log(`Index JSON from image: ${JSON.stringify(indexJson)}`);
231+
console.log(`Annotation JSON: ${JSON.stringify(annotation)}`);
232+
expect(indexJson).toEqual(annotation);
233+
});
234+
});
235+
});

e2e-tests/jest.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const { createDefaultPreset } = require('ts-jest');
2+
3+
const tsJestTransformCfg = createDefaultPreset().transform;
4+
5+
/** @type {import("jest").Config} **/
6+
module.exports = {
7+
testEnvironment: 'node',
8+
transform: {
9+
...tsJestTransformCfg,
10+
},
11+
};

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"prettier:check": "prettier --ignore-unknown --check .",
2929
"prettier:fix": "prettier --ignore-unknown --write .",
3030
"test": "backstage-cli package test --passWithNoTests --coverage",
31+
"test:e2e": "jest --config e2e-tests/jest.config.js",
3132
"clean": "backstage-cli package clean"
3233
},
3334
"bin": "bin/rhdh-cli",
@@ -85,16 +86,23 @@
8586
"@backstage/cli": "0.29.5",
8687
"@backstage/core-plugin-api": "1.10.3",
8788
"@backstage/repo-tools": "^0.13.3",
89+
"@jest/globals": "^30.0.0-beta.3",
8890
"@spotify/prettier-config": "15.0.0",
8991
"@types/fs-extra": "9.0.13",
92+
"@types/jest": "^29.5.14",
9093
"@types/mock-fs": "4.13.4",
9194
"@types/node": "18.19.34",
9295
"@types/npm-packlist": "3.0.0",
9396
"@types/recursive-readdir": "2.2.4",
97+
"@types/tar": "^6.1.1",
9498
"@types/yarnpkg__lockfile": "1.1.9",
99+
"axios": "^1.9.0",
100+
"jest": "^29.7.0",
95101
"mock-fs": "5.2.0",
96102
"nodemon": "3.1.3",
97103
"prettier": "3.3.3",
104+
"tar": "^6.2.0",
105+
"ts-jest": "^29.3.4",
98106
"ts-node": "10.9.2",
99107
"type-fest": "4.20.1",
100108
"typescript": "5.4.5"

0 commit comments

Comments
 (0)