fiddle-core/tests/installer.test.ts

445 строки
14 KiB
TypeScript

import extract from 'extract-zip';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import nock, { Scope } from 'nock';
import {
ElectronBinary,
InstallStateEvent,
Installer,
Paths,
InstallState,
} from '../src/index';
jest.mock('extract-zip');
const extractZip = jest.requireActual<typeof extract>('extract-zip');
describe('Installer', () => {
let tmpdir: string;
let paths: Pick<Paths, 'electronDownloads' | 'electronInstall'>;
let nockScope: Scope;
let installer: Installer;
const { missing, downloading, downloaded, installing, installed } =
InstallState;
const version12 = '12.0.15' as const;
const version13 = '13.1.7' as const;
const version = version13;
const fixture = (name: string) => path.join(__dirname, 'fixtures', name);
beforeEach(async () => {
jest
.mocked(extract)
.mockImplementation(async (zipPath: string, opts: extract.Options) => {
await extractZip(zipPath, opts);
});
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), 'fiddle-core-'));
paths = {
electronDownloads: path.join(tmpdir, 'downloads'),
electronInstall: path.join(tmpdir, 'install'),
};
installer = new Installer(paths);
nock.disableNetConnect();
nockScope = nock('https://github.com:443');
nockScope
.persist()
.get(/electron-v13.1.7-.*\.zip$/)
.replyWithFile(200, fixture('electron-v13.1.7.zip'), {
'Content-Type': 'application/zip',
})
.get(/electron-v12.0.15-.*\.zip$/)
.replyWithFile(200, fixture('electron-v12.0.15.zip'), {
'Content-Type': 'application/zip',
})
.get(/SHASUMS256\.txt$/)
.replyWithFile(200, fixture('SHASUMS256.txt'), {
'Content-Type': 'text/plain;charset=UTF-8',
});
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
fs.removeSync(tmpdir);
});
// test helpers
async function listenWhile(
installer: Installer,
func: () => Promise<unknown>,
) {
const events: InstallStateEvent[] = [];
const listener = (ev: InstallStateEvent) => events.push(ev);
const event = 'state-changed';
installer.on(event, listener);
const result = await func();
installer.removeListener(event, listener);
return { events, result };
}
async function doRemove(installer: Installer, version: string) {
const func = () => installer.remove(version);
const { events } = await listenWhile(installer, func);
expect(installer.state(version)).toBe(missing);
return { events };
}
async function doInstall(installer: Installer, version: string) {
let isDownloaded = false;
const progressCallback = () => {
isDownloaded = true;
};
// Version is already downloaded and present in local
if (installer.state(version) !== missing) {
isDownloaded = true;
}
const func = () => installer.install(version, { progressCallback });
const { events, result } = await listenWhile(installer, func);
const exec = result as string;
const installedVersion = fs
.readFileSync(path.join(paths.electronInstall, 'version'), 'utf-8')
.trim();
expect(isDownloaded).toBe(true);
expect(installer.state(version)).toBe(installed);
expect(installer.installedVersion).toBe(version);
expect(installedVersion).toBe(version);
return { events, exec };
}
async function doDownload(installer: Installer, version: string) {
let isDownloaded = false;
const progressCallback = () => {
isDownloaded = true;
};
// Version is already downloaded and present in local
if (installer.state(version) !== missing) {
isDownloaded = true;
}
const func = () =>
installer.ensureDownloaded(version, {
progressCallback,
});
const { events, result } = await listenWhile(installer, func);
const binaryConfig = result as ElectronBinary;
const { path: zipfile } = binaryConfig;
expect(isDownloaded).toBe(true);
expect(fs.existsSync(zipfile)).toBe(true);
expect(installer.state(version)).toBe(downloaded);
return { events, binaryConfig };
}
async function unZipBinary(): Promise<string> {
const extractDir = path.join(paths.electronDownloads, version);
fs.mkdirSync(extractDir, { recursive: true });
await extract(fixture('electron-v13.1.7.zip'), {
dir: extractDir,
});
return extractDir;
}
// tests
describe('getExecPath()', () => {
it.each([
['Linux', 'linux', 'electron'],
['Windows', 'win32', 'electron.exe'],
['macOS', 'darwin', 'Electron.app/Contents/MacOS/Electron'],
])(
'returns the right path on %s',
(_, platform: string, expected: string) => {
const subpath = Installer.execSubpath(platform);
expect(subpath).toBe(expected);
},
);
});
describe('ensureDownloaded()', () => {
it('downloads the version if needed', async () => {
// setup: version is not installed
expect(installer.state(version)).toBe(missing);
// test that the zipfile was downloaded
const { events, binaryConfig } = await doDownload(installer, version);
expect(events).toStrictEqual([
{ version, state: downloading },
{ version, state: downloaded },
]);
expect(binaryConfig).toHaveProperty('alreadyExtracted', false);
});
it('does nothing if the version is already downloaded', async () => {
// setup: version is already installed
const { binaryConfig: config1 } = await doDownload(installer, version);
const { path: zip1 } = config1;
const { ctimeMs } = await fs.stat(zip1);
// test that ensureDownloaded() did nothing:
const { events, binaryConfig: config2 } = await doDownload(
installer,
version,
);
const { path: zip2 } = config2;
expect(zip2).toEqual(zip1);
expect((await fs.stat(zip2)).ctimeMs).toEqual(ctimeMs);
expect(events).toStrictEqual([]);
expect(config1).toStrictEqual({
path: config2.path,
alreadyExtracted: false,
});
});
it('makes use of the preinstalled electron versions', async () => {
const extractDir = await unZipBinary();
const {
binaryConfig: { path: zipFile },
} = await doDownload(installer, version);
// Purposely remove the downloaded zip file
fs.removeSync(zipFile);
const { binaryConfig } = await doDownload(installer, version);
expect(binaryConfig).toStrictEqual({
path: extractDir,
alreadyExtracted: true,
});
expect(installer.state(version)).toBe(downloaded);
});
it('downloads the version if the zip file is missing', async () => {
const {
binaryConfig: { path: zipFile },
} = await doDownload(installer, version);
// Purposely remove the downloaded zip file
fs.removeSync(zipFile);
expect(installer.state(version)).toBe(downloaded);
// test that the zipfile was downloaded
const { events, binaryConfig } = await doDownload(installer, version);
expect(events).toStrictEqual([
{ version, state: downloading },
{ version, state: downloaded },
]);
expect(binaryConfig).toHaveProperty('alreadyExtracted', false);
expect(nockScope.isDone());
});
it('resets install state on error', async () => {
// setup: version is not installed
expect(installer.state(version)).toBe(missing);
nock.cleanAll();
nockScope.get(/.*/).replyWithError('Server Error');
await expect(doDownload(installer, version)).rejects.toThrow(Error);
expect(installer.state(version)).toBe(missing);
expect(nockScope.isDone());
});
});
describe('remove()', () => {
it('removes a download', async () => {
// setup: version is already installed
await doDownload(installer, version);
const { events } = await doRemove(installer, version);
expect(events).toStrictEqual([{ version, state: missing }]);
});
it('does nothing if the version is missing', async () => {
// setup: version is not installed
expect(installer.state(version)).toBe(missing);
const { events } = await doRemove(installer, version);
expect(events).toStrictEqual([]);
});
it('uninstalls the version if it is installed', async () => {
// setup: version is installed
await doInstall(installer, version);
const { events } = await doRemove(installer, version);
expect(events).toStrictEqual([{ version, state: missing }]);
expect(installer.installedVersion).toBe(undefined);
});
it('removes the preinstalled electron versions', async () => {
const extractDir = await unZipBinary();
const {
binaryConfig: { path: zipFile },
} = await doDownload(installer, version);
// Purposely remove the downloaded zip file
fs.removeSync(zipFile);
expect(installer.state(version)).toBe(downloaded);
const { events } = await doRemove(installer, version);
expect(fs.existsSync(extractDir)).toBe(false);
expect(events).toStrictEqual([{ version, state: missing }]);
});
});
describe('install()', () => {
it('downloads a version if necessary', async () => {
// setup: version is not downloaded
expect(installer.state(version)).toBe(missing);
expect(installer.installedVersion).toBe(undefined);
const { events } = await doInstall(installer, version);
expect(events).toStrictEqual([
{ version, state: downloading },
{ version, state: downloaded },
{ version, state: installing },
{ version, state: installed },
]);
});
it('unzips a version if necessary', async () => {
// setup: version is downloaded but not installed
await doDownload(installer, version);
expect(installer.state(version)).toBe(downloaded);
const { events } = await doInstall(installer, version);
expect(events).toStrictEqual([
{ version, state: installing },
{ version, state: installed },
]);
});
it('does nothing if already installed', async () => {
await doInstall(installer, version);
const { events } = await doInstall(installer, version);
expect(events).toStrictEqual([]);
});
it('replaces the previous installation', async () => {
await doInstall(installer, version12);
const { events } = await doInstall(installer, version13);
expect(events).toStrictEqual([
{ version: version13, state: downloading },
{ version: version13, state: downloaded },
{ version: version13, state: installing },
{ version: version12, state: downloaded },
{ version: version13, state: installed },
]);
});
it('installs the already extracted electron version', async () => {
await unZipBinary();
const {
binaryConfig: { path: zipFile },
} = await doDownload(installer, version);
// Purposely remove the downloaded zip file
fs.removeSync(zipFile);
expect(installer.state(version)).toBe(downloaded);
const { events } = await doInstall(installer, version);
expect(events).toStrictEqual([
{ version: '13.1.7', state: 'installing' },
{ version: '13.1.7', state: 'installed' },
]);
});
it('throws error if already installing', async () => {
const promise = doInstall(installer, version);
try {
await expect(doInstall(installer, version)).rejects.toThrow(
'Currently installing',
);
} finally {
await promise;
}
});
it('leaves a valid state after an error', async () => {
// setup: version is not installed
expect(installer.state(version)).toBe(missing);
const spy = jest
.spyOn(installer, 'ensureDownloaded')
.mockRejectedValueOnce(new Error('Download failed'));
await expect(doInstall(installer, version)).rejects.toThrow(Error);
expect(installer.state(version)).toBe(missing);
spy.mockRestore();
const { events } = await doInstall(installer, version);
expect(events).toStrictEqual([
{ version, state: downloading },
{ version, state: downloaded },
{ version, state: installing },
{ version, state: installed },
]);
});
it('resets install state on error', async () => {
// setup: version is downloaded but not installed
await doDownload(installer, version);
expect(installer.state(version)).toBe(downloaded);
jest.mocked(extract).mockRejectedValue(new Error('Extract error'));
await expect(doInstall(installer, version)).rejects.toThrow(Error);
expect(installer.state(version)).toBe(downloaded);
});
});
describe('installedVersion', () => {
it('returns undefined if no version is installed', () => {
expect(installer.installedVersion).toBe(undefined);
});
it('returns the installed version', async () => {
expect(installer.installedVersion).toBe(undefined);
await doInstall(installer, version);
expect(installer.installedVersion).toBe(version);
});
});
describe('state()', () => {
it("returns 'installed' if the version is installed", async () => {
await doInstall(installer, version);
expect(installer.state(version)).toBe(installed);
});
it("returns 'downloaded' if the version is downloaded", async () => {
await doDownload(installer, version);
expect(installer.state(version)).toBe(downloaded);
});
it("returns 'missing' if the version is not downloaded", () => {
expect(installer.state(version)).toBe(missing);
});
it("returns 'downloaded' if the version is kept extracted", async () => {
expect(installer.state(version)).toBe(missing);
await unZipBinary();
const {
binaryConfig: { path: zipFile },
} = await doDownload(installer, version);
// Purposely remove the downloaded zip file
fs.removeSync(zipFile);
await doDownload(installer, version);
expect(installer.state(version)).toBe(downloaded);
});
});
});