Skip to content

Commit

Permalink
feat: test InstructLab in a container
Browse files Browse the repository at this point in the history
Signed-off-by: Jeff MAURY <[email protected]>
  • Loading branch information
jeffmaury committed Dec 24, 2024
1 parent 472de48 commit 69f6988
Show file tree
Hide file tree
Showing 16 changed files with 777 additions and 19 deletions.
3 changes: 3 additions & 0 deletions packages/backend/src/assets/instructlab-images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"default": "docker.io/redhat/instructlab@sha256:c6b2ecb4547b1f43b5539ee99bdbf5c9ae40599fabe1c740622295d9721b91c4"
}
14 changes: 14 additions & 0 deletions packages/backend/src/instructlab-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,25 @@
import type { InstructlabAPI } from '@shared/src/InstructlabAPI';
import type { InstructlabManager } from './managers/instructlab/instructlabManager';
import type { InstructlabSession } from '@shared/src/models/instructlab/IInstructlabSession';
import type { InstructlabContainerConfiguration } from '@shared/src/models/instructlab/IInstructlabContainerConfiguration';
import { navigation } from '@podman-desktop/api';

export class InstructlabApiImpl implements InstructlabAPI {
constructor(private instructlabManager: InstructlabManager) {}

async getIsntructlabSessions(): Promise<InstructlabSession[]> {
return this.instructlabManager.getSessions();
}

requestCreateInstructlabContainer(config: InstructlabContainerConfiguration): Promise<string> {
return this.instructlabManager.requestCreateInstructlabContainer(config);
}

routeToInstructLabContainerTerminal(containerId: string): Promise<void> {
return navigation.navigateToContainerTerminal(containerId);
}

getInstructlabContainerId(): Promise<string | undefined> {
return this.instructlabManager.getInstructLabContainer();
}
}
4 changes: 2 additions & 2 deletions packages/backend/src/managers/inference/inferenceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { InferenceServer, InferenceServerStatus, InferenceType } from '@sha
import type { PodmanConnection, PodmanConnectionEvent } from '../podmanConnection';
import { containerEngine, Disposable } from '@podman-desktop/api';
import type { ContainerInfo, TelemetryLogger, Webview, ContainerProviderConnection } from '@podman-desktop/api';
import type { ContainerRegistry, ContainerStart } from '../../registries/ContainerRegistry';
import type { ContainerRegistry, ContainerEvent } from '../../registries/ContainerRegistry';
import { getInferenceType, isTransitioning, LABEL_INFERENCE_SERVER } from '../../utils/inferenceUtils';
import { Publisher } from '../../utils/Publisher';
import { Messages } from '@shared/Messages';
Expand Down Expand Up @@ -318,7 +318,7 @@ export class InferenceManager extends Publisher<InferenceServer[]> implements Di
* Listener for container start events
* @param event the event containing the id of the container
*/
private watchContainerStart(event: ContainerStart): void {
private watchContainerStart(event: ContainerEvent): void {
// We might have a start event for an inference server we already know about
if (this.#servers.has(event.id)) return;

Expand Down
156 changes: 156 additions & 0 deletions packages/backend/src/managers/instructlab/instructlabManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import { TaskRegistry } from '../../registries/TaskRegistry';
import { beforeAll, beforeEach, expect, test, vi } from 'vitest';
import type { ContainerCreateResult, ContainerInfo, ImageInfo, Webview } from '@podman-desktop/api';
import { containerEngine, EventEmitter } from '@podman-desktop/api';
import type { PodmanConnection } from '../podmanConnection';
import { INSTRUCTLAB_CONTAINER_LABEL, InstructlabManager } from './instructlabManager';
import { ContainerRegistry } from '../../registries/ContainerRegistry';
import { TestEventEmitter } from '../../tests/utils';
import { VMType } from '@shared/src/models/IPodman';
import type { Task } from '@shared/src/models/ITask';
import instructlab_images from '../../assets/instructlab-images.json';

vi.mock('@podman-desktop/api', () => {
return {
EventEmitter: vi.fn(),
containerEngine: {
listContainers: vi.fn(),
listImages: vi.fn(),
createContainer: vi.fn(),
onEvent: vi.fn(),
},
};
});

const taskRegistry = new TaskRegistry({ postMessage: vi.fn().mockResolvedValue(undefined) } as unknown as Webview);

const podmanConnection: PodmanConnection = {
onPodmanConnectionEvent: vi.fn(),
findRunningContainerProviderConnection: vi.fn(),
} as unknown as PodmanConnection;

let instructlabManager: InstructlabManager;

beforeAll(() => {
vi.mocked(EventEmitter).mockImplementation(() => new TestEventEmitter() as unknown as EventEmitter<unknown>);
});

beforeEach(() => {
const containerRegistry = new ContainerRegistry();
containerRegistry.init();
instructlabManager = new InstructlabManager('', taskRegistry, podmanConnection, containerRegistry);
});

test('getInstructLabContainer should return undefined if no containers', async () => {
vi.mocked(containerEngine.listContainers).mockResolvedValue([]);
const containerId = await instructlabManager.getInstructLabContainer();
expect(containerId).toBeUndefined();
});

test('getInstructLabContainer should return undefined if no instructlab container', async () => {
vi.mocked(containerEngine.listContainers).mockResolvedValue([{ Id: 'dummyId' } as unknown as ContainerInfo]);
const containerId = await instructlabManager.getInstructLabContainer();
expect(containerId).toBeUndefined();
});

test('getInstructLabContainer should return id if instructlab container', async () => {
vi.mocked(containerEngine.listContainers).mockResolvedValue([
{
Id: 'dummyId',
State: 'running',
Labels: { [`${INSTRUCTLAB_CONTAINER_LABEL}`]: 'dummyLabel' },
} as unknown as ContainerInfo,
]);
const containerId = await instructlabManager.getInstructLabContainer();
expect(containerId).toBe('dummyId');
});

test('requestCreateInstructlabContainer throws error if no podman connection', async () => {
const containerIdPromise = instructlabManager.requestCreateInstructlabContainer({});
await expect(containerIdPromise).rejects.toBeInstanceOf(Error);
});

async function waitTasks(containerId: string, nb: number): Promise<Task[]> {
return vi.waitFor(() => {
const tasks = taskRegistry.getTasksByLabels({ trackingId: containerId });
if (tasks.length !== nb) {
throw new Error('not completed');
}
return tasks;
});
}

test('requestCreateInstructlabContainer returns id and error if listImage returns error', async () => {
vi.mocked(podmanConnection.findRunningContainerProviderConnection).mockReturnValue({
name: 'Podman Machine',
vmType: VMType.UNKNOWN,
type: 'podman',
status: () => 'started',
endpoint: {
socketPath: 'socket.sock',
},
});
vi.mocked(containerEngine.listImages).mockRejectedValue(new Error());
const containerId = await instructlabManager.requestCreateInstructlabContainer({});
expect(containerId).toBeDefined();
const tasks = await waitTasks(containerId, 2);
expect(tasks.some(task => task.state === 'error')).toBeTruthy();
});

test('requestCreateInstructlabContainer returns id and error if listImage returns image', async () => {
vi.mocked(podmanConnection.findRunningContainerProviderConnection).mockReturnValue({
name: 'Podman Machine',
vmType: VMType.UNKNOWN,
type: 'podman',
status: () => 'started',
endpoint: {
socketPath: 'socket.sock',
},
});
vi.mocked(containerEngine.listImages).mockResolvedValue([
{ RepoTags: [instructlab_images.default] } as unknown as ImageInfo,
]);
const containerId = await instructlabManager.requestCreateInstructlabContainer({});
expect(containerId).toBeDefined();
const tasks = await waitTasks(containerId, 3);
expect(tasks.some(task => task.state === 'error')).toBeTruthy();
});

test('requestCreateInstructlabContainer returns id and no error if createContainer returns id', async () => {
vi.mocked(podmanConnection.findRunningContainerProviderConnection).mockReturnValue({
name: 'Podman Machine',
vmType: VMType.UNKNOWN,
type: 'podman',
status: () => 'started',
endpoint: {
socketPath: 'socket.sock',
},
});
vi.mocked(containerEngine.listImages).mockResolvedValue([
{ RepoTags: [instructlab_images.default] } as unknown as ImageInfo,
]);
vi.mocked(containerEngine.createContainer).mockResolvedValue({
id: 'containerId',
} as unknown as ContainerCreateResult);
const containerId = await instructlabManager.requestCreateInstructlabContainer({});
expect(containerId).toBeDefined();
const tasks = await waitTasks(containerId, 3);
expect(tasks.some(task => task.state === 'error')).toBeFalsy();
});
Loading

0 comments on commit 69f6988

Please sign in to comment.