Skip to content

Commit fdbbb2c

Browse files
authored
feat: lazily init macadam (#409)
* feat: lazily init macadam when binaries are present or at creation time * test: fix tests when binaries are installed * test: add tests when binaries are not available * test: add unit tests
1 parent 4ef083e commit fdbbb2c

4 files changed

Lines changed: 154 additions & 9 deletions

File tree

src/extension.spec.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ vi.mock('@crc-org/macadam.js', async () => {
4949
Macadam.prototype.stopVm = vi.fn();
5050
Macadam.prototype.removeVm = vi.fn();
5151
Macadam.prototype.executeCommand = vi.fn();
52+
Macadam.prototype.areBinariesAvailable = vi.fn();
5253
return { Macadam };
5354
});
5455
vi.mock('./macadam-machine-stream.js', async () => {
@@ -61,7 +62,7 @@ beforeEach(() => {
6162
vol.reset();
6263
});
6364

64-
describe('activate', () => {
65+
describe.each([true, false])('activate when binaries availability is %s', binariesAvailable => {
6566
const extensionContext: extensionApi.ExtensionContext = {
6667
subscriptions: {
6768
push: vi.fn(),
@@ -77,11 +78,12 @@ describe('activate', () => {
7778

7879
beforeEach(async () => {
7980
vi.mocked(extensionApi.provider.createProvider).mockReturnValue(provider);
81+
vi.mocked(macadamJSPackage.Macadam.prototype.areBinariesAvailable).mockReturnValue(binariesAvailable);
8082
});
8183

82-
test('macadam library is initialized', async () => {
84+
test('macadam library is initialized only if binaries are available', async () => {
8385
await activate(extensionContext);
84-
expect(macadamJSPackage.Macadam.prototype.init).toHaveBeenCalled();
86+
expect(macadamJSPackage.Macadam.prototype.init).toHaveBeenCalledTimes(binariesAvailable ? 1 : 0);
8587
});
8688

8789
test('createCliTool is called and its result is added to subscriptions', async () => {
@@ -384,14 +386,14 @@ bla bla
384386
]);
385387
});
386388

387-
test('listVms is called once', async () => {
389+
test('listVms is called once', { skip: !binariesAvailable }, async () => {
388390
await activate(extensionContext);
389391
await vi.waitFor(() => {
390392
expect(macadamJSPackage.Macadam.prototype.listVms).toHaveBeenCalledWith({ containerProvider: 'applehv' });
391393
});
392394
});
393395

394-
test('registerVmProviderConnection is called once', async () => {
396+
test('registerVmProviderConnection is called once', { skip: !binariesAvailable }, async () => {
395397
await activate(extensionContext);
396398
await vi.waitFor(() => {
397399
expect(provider.registerVmProviderConnection).toHaveBeenCalledOnce();
@@ -404,6 +406,18 @@ bla bla
404406
beforeEach(async () => {
405407
vi.mocked(provider.updateStatus).mockClear();
406408
await activate(extensionContext);
409+
410+
if (!binariesAvailable) {
411+
expect(provider.setVmProviderConnectionFactory).toHaveBeenCalledOnce();
412+
const call = vi.mocked(provider.setVmProviderConnectionFactory).mock.calls[0];
413+
assert(!!call[0].create);
414+
const create = call[0].create;
415+
await create({
416+
'rhel-vms.factory.machine.image': 'RHEL 10',
417+
'rhel-vms.factory.machine.register': false,
418+
});
419+
}
420+
407421
await vi.waitFor(() => {
408422
expect(provider.registerVmProviderConnection).toHaveBeenCalledOnce();
409423
});
@@ -520,6 +534,16 @@ bla bla
520534

521535
test('listVms is called for each provider', async () => {
522536
await activate(extensionContext);
537+
if (!binariesAvailable) {
538+
expect(provider.setVmProviderConnectionFactory).toHaveBeenCalledOnce();
539+
const call = vi.mocked(provider.setVmProviderConnectionFactory).mock.calls[0];
540+
assert(!!call[0].create);
541+
const create = call[0].create;
542+
await create({
543+
'rhel-vms.factory.machine.image': 'RHEL 10',
544+
'rhel-vms.factory.machine.register': false,
545+
});
546+
}
523547
await vi.waitFor(() => {
524548
expect(macadamJSPackage.Macadam.prototype.listVms).toHaveBeenCalledTimes(2);
525549
});
@@ -566,6 +590,8 @@ describe('register', () => {
566590
vi.mocked(extensionApi.env).isWindows = false;
567591
vi.mocked(authentication.initAuthentication).mockResolvedValue(authClient);
568592

593+
vi.mocked(macadamJSPackage.Macadam.prototype.areBinariesAvailable).mockReturnValue(true);
594+
569595
await activate(extensionContext);
570596
expect(provider.setVmProviderConnectionFactory).toHaveBeenCalledOnce();
571597
const call = vi.mocked(provider.setVmProviderConnectionFactory).mock.calls[0];

src/extension.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**********************************************************************
2-
* Copyright (C) 2025 Red Hat, Inc.
2+
* Copyright (C) 2025 - 2026 Red Hat, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import {
3030
} from './constants';
3131
import { getImageSha } from './images';
3232
import { LoggerDelegator } from './logger';
33+
import { MacadamInitializer } from './macadam-init';
3334
import { ProviderConnectionShellAccessImpl } from './macadam-machine-stream';
3435
import { getErrorMessage, pullImageFromRedHatRegistry, verifyContainerProivder } from './utils';
3536
import { isHyperVEnabled, isWSLEnabled } from './win/utils';
@@ -88,19 +89,26 @@ type MachineJSONListOutput = {
8889
};
8990

9091
export let macadam: macadamJSPackage.Macadam;
92+
export let macadamInitializer: MacadamInitializer;
9193

9294
export const macadamMachinesStatuses = new Map<string, extensionApi.ProviderConnectionStatus>();
9395

9496
export async function activate(extensionContext: extensionApi.ExtensionContext): Promise<void> {
9597
macadam = new macadamJSPackage.Macadam('rhel');
96-
await macadam.init();
98+
macadamInitializer = new MacadamInitializer(macadam);
9799

98100
const provider = await createProvider(extensionContext);
99101

100-
monitorMachines(provider, extensionContext).catch((error: unknown) => {
101-
console.error('Error while monitoring machines', error);
102+
macadamInitializer.onInitialized(() => {
103+
monitorMachines(provider, extensionContext).catch((error: unknown) => {
104+
console.error('Error while monitoring machines', error);
105+
});
102106
});
103107

108+
if (macadam.areBinariesAvailable()) {
109+
await macadamInitializer.init();
110+
}
111+
104112
// create cli tool for the cliTool page in desktop
105113
const macadamCli = extensionApi.cli.createCliTool({
106114
name: MACADAM_CLI_NAME,
@@ -464,6 +472,7 @@ async function createVM(
464472
logger?: extensionApi.Logger,
465473
token?: extensionApi.CancellationToken,
466474
): Promise<void> {
475+
await macadamInitializer.ensureInitialized();
467476
const telemetryRecords: Record<string, unknown> = {};
468477
if (extensionApi.env.isMac) {
469478
telemetryRecords.OS = 'mac';

src/macadam-init.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**********************************************************************
2+
* Copyright (C) 2026 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import type { Macadam } from '@crc-org/macadam.js';
20+
import { beforeEach, expect, test, vi } from 'vitest';
21+
22+
import { MacadamInitializer } from './macadam-init';
23+
24+
beforeEach(() => {
25+
vi.resetAllMocks();
26+
});
27+
28+
test('init should initialize the macadam library', async () => {
29+
const macadam: Macadam = {
30+
init: vi.fn(),
31+
} as unknown as Macadam;
32+
const initializer = new MacadamInitializer(macadam);
33+
expect(macadam.init).not.toHaveBeenCalled();
34+
await initializer.init();
35+
expect(macadam.init).toHaveBeenCalled();
36+
});
37+
38+
test('ensureInitialized should initialize the macadam library only it is not initialized', async () => {
39+
const macadam: Macadam = {
40+
init: vi.fn(),
41+
} as unknown as Macadam;
42+
const initializer = new MacadamInitializer(macadam);
43+
44+
expect(macadam.init).not.toHaveBeenCalled();
45+
await initializer.ensureInitialized();
46+
expect(macadam.init).toHaveBeenCalled();
47+
48+
// should not be called again
49+
vi.mocked(macadam.init).mockClear();
50+
expect(macadam.init).not.toHaveBeenCalled();
51+
await initializer.ensureInitialized();
52+
expect(macadam.init).not.toHaveBeenCalled();
53+
});
54+
55+
test('onInitialized should call the callback when the macadam library is initialized', async () => {
56+
const macadam: Macadam = {
57+
init: vi.fn(),
58+
} as unknown as Macadam;
59+
const initializer = new MacadamInitializer(macadam);
60+
const callback = vi.fn();
61+
initializer.onInitialized(callback);
62+
await initializer.init();
63+
expect(callback).toHaveBeenCalled();
64+
});

src/macadam-init.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**********************************************************************
2+
* Copyright (C) 2026 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { EventEmitter } from 'node:stream';
20+
21+
import type { Macadam } from '@crc-org/macadam.js';
22+
23+
const SIGNAL = 'emit';
24+
25+
export class MacadamInitializer {
26+
#onMacadamInit = new EventEmitter();
27+
#initialized = false;
28+
29+
constructor(private readonly macadam: Macadam) {}
30+
31+
async init(): Promise<void> {
32+
await this.macadam.init();
33+
this.#initialized = true;
34+
this.#onMacadamInit.emit(SIGNAL);
35+
}
36+
37+
async ensureInitialized(): Promise<void> {
38+
if (!this.#initialized) {
39+
await this.init();
40+
}
41+
}
42+
43+
onInitialized(callback: () => void): void {
44+
this.#onMacadamInit.on(SIGNAL, callback);
45+
}
46+
}

0 commit comments

Comments
 (0)