Skip to content

Commit 4b37f6b

Browse files
committed
Add sandboxing and refactored existing code
1 parent ea4869a commit 4b37f6b

File tree

12 files changed

+491
-360
lines changed

12 files changed

+491
-360
lines changed

packages/compass-e2e-tests/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
fixtures/*.csv
44
fixtures/*.json
55
hadron-build-info.json
6+
7+
# Ignoring sandboxes (created per test run)
8+
.smoke-sandboxes/
9+
# Cache of downloaded binaries
10+
.smoke-downloads/

packages/compass-e2e-tests/helpers/buildinfo.ts

Lines changed: 0 additions & 80 deletions
This file was deleted.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
import { handler as writeBuildInfo } from 'hadron-build/commands/info';
6+
7+
import { type PackageKind } from './packages';
8+
import { type SmokeTestsContext } from './context';
9+
import { pick } from 'lodash';
10+
11+
function assertObjectHasKeys(
12+
obj: unknown,
13+
name: string,
14+
keys: readonly string[]
15+
) {
16+
assert(
17+
typeof obj === 'object' && obj !== null,
18+
'Expected buildInfo to be an object'
19+
);
20+
21+
for (const key of keys) {
22+
assert(key in obj, `Expected '${name}' to have '${key}'`);
23+
}
24+
}
25+
26+
// subsets of the hadron-build info result
27+
28+
export const commonKeys = ['productName'] as const;
29+
export type CommonBuildInfo = Record<typeof commonKeys[number], string>;
30+
31+
export function assertCommonBuildInfo(
32+
buildInfo: unknown
33+
): asserts buildInfo is CommonBuildInfo {
34+
assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys);
35+
}
36+
37+
export const windowsFilenameKeys = [
38+
'windows_setup_filename',
39+
'windows_msi_filename',
40+
'windows_zip_filename',
41+
'windows_nupkg_full_filename',
42+
] as const;
43+
export type WindowsBuildInfo = CommonBuildInfo &
44+
Record<typeof windowsFilenameKeys[number], string>;
45+
46+
export function assertBuildInfoIsWindows(
47+
buildInfo: unknown
48+
): asserts buildInfo is WindowsBuildInfo {
49+
assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys);
50+
assertObjectHasKeys(buildInfo, 'buildInfo', windowsFilenameKeys);
51+
}
52+
53+
export const osxFilenameKeys = [
54+
'osx_dmg_filename',
55+
'osx_zip_filename',
56+
] as const;
57+
export type OSXBuildInfo = CommonBuildInfo &
58+
Record<typeof osxFilenameKeys[number], string>;
59+
60+
export function assertBuildInfoIsOSX(
61+
buildInfo: unknown
62+
): asserts buildInfo is OSXBuildInfo {
63+
assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys);
64+
assertObjectHasKeys(buildInfo, 'buildInfo', osxFilenameKeys);
65+
}
66+
67+
export const ubuntuFilenameKeys = [
68+
'linux_deb_filename',
69+
'linux_tar_filename',
70+
] as const;
71+
export type UbuntuBuildInfo = CommonBuildInfo &
72+
Record<typeof ubuntuFilenameKeys[number], string>;
73+
74+
export function assertBuildInfoIsUbuntu(
75+
buildInfo: unknown
76+
): asserts buildInfo is UbuntuBuildInfo {
77+
assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys);
78+
assertObjectHasKeys(buildInfo, 'buildInfo', ubuntuFilenameKeys);
79+
}
80+
81+
const rhelFilenameKeys = ['linux_rpm_filename', 'rhel_tar_filename'] as const;
82+
export type RHELBuildInfo = CommonBuildInfo &
83+
Record<typeof rhelFilenameKeys[number], string>;
84+
85+
export function assertBuildInfoIsRHEL(
86+
buildInfo: unknown
87+
): asserts buildInfo is RHELBuildInfo {
88+
assertObjectHasKeys(buildInfo, 'buildInfo', commonKeys);
89+
assertObjectHasKeys(buildInfo, 'buildInfo', rhelFilenameKeys);
90+
}
91+
92+
export type PackageDetails = {
93+
kind: PackageKind;
94+
filename: string;
95+
} & (
96+
| {
97+
kind: 'windows_setup' | 'windows_msi' | 'windows_zip';
98+
buildInfo: WindowsBuildInfo;
99+
}
100+
| {
101+
kind: 'osx_dmg' | 'osx_zip';
102+
buildInfo: OSXBuildInfo;
103+
}
104+
| {
105+
kind: 'linux_deb' | 'linux_tar';
106+
buildInfo: UbuntuBuildInfo;
107+
}
108+
| {
109+
kind: 'linux_rpm' | 'rhel_tar';
110+
buildInfo: RHELBuildInfo;
111+
}
112+
);
113+
114+
/**
115+
* Extracts the filename of the packaged app from the build info, specific to a kind of package.
116+
*/
117+
export function getPackageDetails(
118+
kind: PackageKind,
119+
buildInfo: unknown
120+
): PackageDetails {
121+
if (
122+
kind === 'windows_setup' ||
123+
kind === 'windows_msi' ||
124+
kind === 'windows_zip'
125+
) {
126+
assertBuildInfoIsWindows(buildInfo);
127+
return { kind, buildInfo, filename: buildInfo[`${kind}_filename`] };
128+
} else if (kind === 'osx_dmg' || kind === 'osx_zip') {
129+
assertBuildInfoIsOSX(buildInfo);
130+
return { kind, buildInfo, filename: buildInfo[`${kind}_filename`] };
131+
} else if (kind === 'linux_deb' || kind === 'linux_tar') {
132+
assertBuildInfoIsUbuntu(buildInfo);
133+
return { kind, buildInfo, filename: buildInfo[`${kind}_filename`] };
134+
} else if (kind === 'linux_rpm' || kind === 'rhel_tar') {
135+
assertBuildInfoIsRHEL(buildInfo);
136+
return { kind, buildInfo, filename: buildInfo[`${kind}_filename`] };
137+
} else {
138+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
139+
throw new Error(`Unsupported package kind: ${kind}`);
140+
}
141+
}
142+
143+
function readJson<T extends object>(...segments: string[]): T {
144+
const result = JSON.parse(
145+
fs.readFileSync(path.join(...segments), 'utf8')
146+
) as unknown;
147+
assert(typeof result === 'object' && result !== null, 'Expected an object');
148+
return result as T;
149+
}
150+
151+
export function readPackageDetails(
152+
kind: PackageKind,
153+
filePath: string
154+
): PackageDetails {
155+
const result = readJson(filePath);
156+
return getPackageDetails(kind, result);
157+
}
158+
159+
export function writeAndReadPackageDetails(
160+
context: SmokeTestsContext
161+
): PackageDetails {
162+
const compassDir = path.resolve(__dirname, '../../../compass');
163+
const infoArgs = {
164+
format: 'json',
165+
dir: compassDir,
166+
platform: context.platform,
167+
arch: context.arch,
168+
out: path.resolve(context.sandboxPath, 'target.json'),
169+
};
170+
console.log({ infoArgs });
171+
172+
// These are known environment variables that will affect the way
173+
// writeBuildInfo works. Log them as a reminder and for our own sanity
174+
console.log(
175+
'info env vars',
176+
pick(process.env, [
177+
'HADRON_DISTRIBUTION',
178+
'HADRON_APP_VERSION',
179+
'HADRON_PRODUCT',
180+
'HADRON_PRODUCT_NAME',
181+
'HADRON_READONLY',
182+
'HADRON_ISOLATED',
183+
'DEV_VERSION_IDENTIFIER',
184+
'IS_RHEL',
185+
])
186+
);
187+
writeBuildInfo(infoArgs);
188+
return readPackageDetails(context.package, infoArgs.out);
189+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type PackageKind } from './packages';
2+
3+
export type SmokeTestsContext = {
4+
bucketName?: string;
5+
bucketKeyPrefix?: string;
6+
platform: 'win32' | 'darwin' | 'linux';
7+
arch: 'x64' | 'arm64';
8+
package: PackageKind;
9+
forceDownload?: boolean;
10+
localPackage?: boolean;
11+
sandboxPath: string;
12+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert';
2+
import crypto from 'node:crypto';
3+
import fs from 'node:fs';
4+
import path from 'node:path';
5+
6+
function ensureSandboxesDirectory() {
7+
const sandboxesPath = path.resolve(__dirname, '../../.smoke-sandboxes');
8+
if (!fs.existsSync(sandboxesPath)) {
9+
fs.mkdirSync(sandboxesPath, { recursive: true });
10+
}
11+
return sandboxesPath;
12+
}
13+
14+
export function createSandbox() {
15+
const nonce = crypto.randomBytes(4).toString('hex');
16+
const sandboxPath = path.resolve(ensureSandboxesDirectory(), nonce);
17+
assert.equal(
18+
fs.existsSync(sandboxPath),
19+
false,
20+
`Failed to create sandbox at '${sandboxPath}' - it already exists`
21+
);
22+
fs.mkdirSync(sandboxPath);
23+
return sandboxPath;
24+
}
25+
26+
export function ensureDownloadsDirectory() {
27+
const downloadsPath = path.resolve(__dirname, '../../.smoke-downloads');
28+
if (!fs.existsSync(downloadsPath)) {
29+
fs.mkdirSync(downloadsPath, { recursive: true });
30+
}
31+
return downloadsPath;
32+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from 'node:assert';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import stream from 'node:stream';
5+
import { type fetch as undiciFetch } from 'undici-types';
6+
7+
// Hacking types here because the DOM types doesn't match the fetch implemented by Node.js
8+
const fetch = globalThis.fetch as unknown as typeof undiciFetch;
9+
10+
import { ensureDownloadsDirectory } from './directories';
11+
12+
type DownloadFileOptions = {
13+
url: string;
14+
targetFilename: string;
15+
clearCache?: boolean;
16+
};
17+
18+
export async function downloadFile({
19+
url,
20+
targetFilename,
21+
clearCache,
22+
}: DownloadFileOptions): Promise<string> {
23+
const response = await fetch(url);
24+
25+
const etag = response.headers.get('etag');
26+
assert(etag, 'Expected an ETag header');
27+
const cleanEtag = etag.match(/[0-9a-fA-F]/g)?.join('');
28+
assert(cleanEtag, 'Expected ETag to be cleanable');
29+
const cacheDirectoryPath = path.resolve(
30+
ensureDownloadsDirectory(),
31+
cleanEtag
32+
);
33+
const outputPath = path.resolve(cacheDirectoryPath, targetFilename);
34+
const cacheExists = fs.existsSync(outputPath);
35+
36+
if (cacheExists) {
37+
if (clearCache) {
38+
fs.rmSync(cacheDirectoryPath, { recursive: true, force: true });
39+
} else {
40+
console.log('Skipped downloading', url, '(cache existed)');
41+
return outputPath;
42+
}
43+
}
44+
45+
if (!fs.existsSync(cacheDirectoryPath)) {
46+
fs.mkdirSync(cacheDirectoryPath);
47+
}
48+
49+
// Write the response to file
50+
assert(response.body, 'Expected a response body');
51+
console.log('Downloading', url);
52+
await stream.promises.pipeline(
53+
response.body,
54+
fs.createWriteStream(outputPath)
55+
);
56+
57+
return outputPath;
58+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const SUPPORTED_PACKAGES = [
2+
'windows_setup',
3+
'windows_msi',
4+
'windows_zip',
5+
'osx_dmg',
6+
'osx_zip',
7+
'linux_deb',
8+
'linux_tar',
9+
'linux_rpm',
10+
'rhel_tar',
11+
] as const;
12+
13+
export type PackageKind = typeof SUPPORTED_PACKAGES[number];

0 commit comments

Comments
 (0)