Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task/APPENG-2738: NIM Cypress E2E tests #3298

Merged
merged 36 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
82dc694
test: added mocks and interceptors for nim cypress tests
TomerFi Oct 2, 2024
777fbf1
Merging the base test cases of Daniel and Lokesh's code to the Tomer'…
lokeshrangineni Oct 2, 2024
88fe97b
Reverting the mock changes which are not needed.
lokeshrangineni Oct 2, 2024
dddb3fc
nim-specific classes and functions, partial rework of deployment test…
dmartinol Oct 2, 2024
683bf47
Checking in the cypress test covers to validate all the scenarios to …
lokeshrangineni Oct 3, 2024
353ae78
Added mockNimServingRuntimeTemplate mock function
dmartinol Oct 3, 2024
f44825c
mocking deploy respurces (WIP)
dmartinol Oct 3, 2024
534222b
Completed deployment tests
dmartinol Oct 3, 2024
da51470
Added new test cases to validate if the nim is not enabled
lokeshrangineni Oct 3, 2024
1a0b155
test: added test cases for deleting model from the project models tab
TomerFi Oct 3, 2024
a7826d2
* Added new test cases to validate the model serving menu item when t…
lokeshrangineni Oct 3, 2024
5dc4c2c
test: cleanups
TomerFi Oct 4, 2024
5a87a50
adding const for modal dialog title
dmartinol Oct 4, 2024
963d077
test cases for list of models in different pages/tabs
dmartinol Oct 4, 2024
ff3d67b
linting fixes
dmartinol Oct 4, 2024
c34551f
removed experimentalStudio option
dmartinol Oct 4, 2024
bbd1688
test: split nim tests per pages (#13)
TomerFi Oct 4, 2024
394820e
testing more buttons in modal dialog (#14)
dmartinol Oct 4, 2024
4d62576
Added a test case to address there is a failure in loading Nvidia Nim…
lokeshrangineni Oct 4, 2024
3858254
Added a test case to address there is a failure in loading Nvidia Nim…
lokeshrangineni Oct 4, 2024
a5d6858
test: fix linting errors
TomerFi Oct 4, 2024
35f9c15
test: fix review change requests 1 - see body
TomerFi Oct 7, 2024
917d012
test: fix review change requests 2 - see body
TomerFi Oct 7, 2024
3e2b3c2
test: create page objects for verifying nim model tabels
TomerFi Oct 7, 2024
a955014
Update frontend/src/__tests__/cypress/cypress/tests/mocked/projects/m…
TomerFi Oct 7, 2024
0c519b9
test: fix linting errors
TomerFi Oct 7, 2024
19d20f6
test: rewrite test case for global model list nim
TomerFi Oct 8, 2024
26195c1
test: fix linting errors
TomerFi Oct 8, 2024
98eabbe
test: rewrite test case for project overview list nim
TomerFi Oct 8, 2024
abdd860
test: rewrite test case for project models tab list nim
TomerFi Oct 8, 2024
bd9fa01
test: cleanups
TomerFi Oct 8, 2024
b01fe1b
test: rewrite test cases nim enablement
TomerFi Oct 9, 2024
06baa5e
test: final touchups
TomerFi Oct 9, 2024
9dea313
test: fixing 1 of 2 failing tests after rebase
TomerFi Oct 29, 2024
80af75e
test: fix mockInfereceService should take labels
TomerFi Oct 30, 2024
a9007d5
test: fix review suggestion for global model edit button
TomerFi Oct 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/__mocks__/mockDashboardConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DashboardConfigKind, KnownLabels } from '~/k8sTypes';
import { NotebookSize } from '~/types';

type MockDashboardConfigType = {
export type MockDashboardConfigType = {
disableInfo?: boolean;
disableSupport?: boolean;
disableClusterManager?: boolean;
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/__mocks__/mockInferenceServiceK8sResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MockResourceConfigType = {
kserveInternalUrl?: string;
statusPredictor?: Record<string, string>;
kserveInternalLabel?: boolean;
additionalLabels?: Record<string, string>;
};

type InferenceServicek8sError = K8sStatus & {
Expand Down Expand Up @@ -80,6 +81,7 @@ export const mockInferenceServiceK8sResource = ({
statusPredictor = undefined,
kserveInternalUrl = '',
kserveInternalLabel = false,
additionalLabels = {},
}: MockResourceConfigType): InferenceServiceKind => ({
apiVersion: 'serving.kserve.io/v1beta1',
kind: 'InferenceService',
Expand All @@ -99,8 +101,7 @@ export const mockInferenceServiceK8sResource = ({
generation: 1,
labels: {
name,
[KnownLabels.REGISTERED_MODEL_ID]: '1',
[KnownLabels.MODEL_VERSION_ID]: '3',
...additionalLabels,
[KnownLabels.DASHBOARD_RESOURCE]: 'true',
...(kserveInternalLabel && { 'networking.knative.dev/visibility': 'cluster-local' }),
},
Expand Down
145 changes: 145 additions & 0 deletions frontend/src/__mocks__/mockNimResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
ConfigMapKind,
InferenceServiceKind,
PersistentVolumeClaimKind,
ProjectKind,
SecretKind,
ServingRuntimeKind,
TemplateKind,
} from '~/k8sTypes';
import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types';
import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource';
import { NimServingResponse } from '~/__tests__/cypress/cypress/types';
import { mockConfigMap } from './mockConfigMap';
import { mockServingRuntimeK8sResource } from './mockServingRuntimeK8sResource';
import { mockInferenceServiceK8sResource } from './mockInferenceServiceK8sResource';
import { mockServingRuntimeTemplateK8sResource } from './mockServingRuntimeTemplateK8sResource';
import { mockSecretK8sResource } from './mockSecretK8sResource';
import { mockPVCK8sResource } from './mockPVCK8sResource';

export const mockNimImages = (): ConfigMapKind =>
mockConfigMap({
name: 'nvidia-nim-images-data',
namespace: 'opendatahub',
data: {
alphafold2: JSON.stringify({
name: 'alphafold2',
displayName: 'AlphaFold2',
shortDescription:
'A widely used model for predicting the 3D structures of proteins from their amino acid sequences.',
namespace: 'nim/deepmind',
tags: ['1.0.0'],
latestTag: '1.0.0',
updatedDate: '2024-08-27T01:51:55.642Z',
}),
'arctic-embed-l': JSON.stringify({
name: 'arctic-embed-l',
displayName: 'Snowflake Arctic Embed Large Embedding',
shortDescription:
'NVIDIA NIM for GPU accelerated Snowflake Arctic Embed Large Embedding inference',
namespace: 'nim/snowflake',
tags: ['1.0.1', '1.0.0'],
latestTag: '1.0.1',
updatedDate: '2024-07-27T00:38:40.927Z',
}),
},
});

export const mockNimInferenceService = (): InferenceServiceKind => {
const inferenceService = mockInferenceServiceK8sResource({
name: 'test-name',
modelName: 'test-name',
displayName: 'Test Name',
kserveInternalLabel: true,
resources: {
limits: { cpu: '2', memory: '8Gi' },
requests: { cpu: '1', memory: '4Gi' },
},
});
delete inferenceService.metadata.labels?.name;
delete inferenceService.metadata.creationTimestamp;
delete inferenceService.metadata.generation;
delete inferenceService.metadata.resourceVersion;
delete inferenceService.metadata.uid;
if (inferenceService.spec.predictor.model?.modelFormat) {
inferenceService.spec.predictor.model.modelFormat.name = 'arctic-embed-l';
}
delete inferenceService.spec.predictor.model?.modelFormat?.version;
delete inferenceService.spec.predictor.model?.storage;
delete inferenceService.status;

return inferenceService;
};

export const mockNimServingRuntime = (): ServingRuntimeKind => {
const servingRuntime = mockServingRuntimeK8sResource({
name: 'test-name',
displayName: 'Test Name',
});
if (servingRuntime.metadata.annotations) {
servingRuntime.metadata.annotations['opendatahub.io/template-display-name'] = 'NVIDIA NIM';
servingRuntime.metadata.annotations['opendatahub.io/template-name'] = 'nvidia-nim-runtime';
}

return servingRuntime;
};

export const mockNimServingRuntimeTemplate = (): TemplateKind => {
const templateMock = mockServingRuntimeTemplateK8sResource({
name: 'nvidia-nim-serving-template',
displayName: 'NVIDIA NIM',
platforms: [ServingRuntimePlatform.SINGLE],
apiProtocol: ServingRuntimeAPIProtocol.REST,
namespace: 'opendatahub',
});
if (templateMock.metadata.annotations != null) {
templateMock.metadata.annotations['opendatahub.io/dashboard'] = 'true';
}

return templateMock;
};

export const mockNvidiaNimAccessSecret = (): SecretKind => {
const secret = mockSecretK8sResource({
name: 'nvidia-nim-access',
});
delete secret.data;
secret.data = {};
secret.data.api_key = 'api-key'; // eslint-disable-line camelcase
secret.data.configMapName = 'bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA==';

return secret;
};

export const mockNvidiaNimImagePullSecret = (): SecretKind => {
const secret = mockSecretK8sResource({
name: 'nvidia-nim-image-pull',
});
delete secret.data;
secret.data = {};
secret.data['.dockerconfigjson'] = 'ZG9ja2VyY29uZmlnCg==';

return secret;
};

export const mockNimProject = (hasAllModels: boolean): ProjectKind => {
const project = mockProjectK8sResource({
hasAnnotations: true,
enableModelMesh: hasAllModels ? undefined : false,
});
if (project.metadata.annotations != null) {
project.metadata.annotations['opendatahub.io/nim-support'] = 'true';
}
return project;
};

export const mockNimModelPVC = (): PersistentVolumeClaimKind => {
const pvc = mockPVCK8sResource({
name: 'nim-pvc',
});
return pvc;
};

export const mockNimServingResource = (
resource: ConfigMapKind | SecretKind,
): NimServingResponse => ({ body: { body: resource } });
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal';

class NIMDeployModal extends Modal {
constructor(private edit = false) {
super(`${edit ? 'Edit' : 'Deploy'} model with NVIDIA NIM`);
}

findSubmitButton() {
return this.findFooter().findByTestId('modal-submit-button');
}

findModelNameInput() {
return this.find().findByTestId('model-deployment-name-section');
}

findNIMToDeploy() {
return this.find().findByTestId('nim-model-list-selection');
}

findNimStorageSizeInput() {
return cy.get('[data-testid="pvc-size"] input');
andrewballantyne marked this conversation as resolved.
Show resolved Hide resolved
}

findStorageSizeMinusButton() {
return this.find().findByTestId('pvc-size').findByRole('button', { name: 'Minus' });
}

findStorageSizePlusButton() {
return this.find().findByTestId('pvc-size').findByRole('button', { name: 'Plus' });
}

findNimModelReplicas() {
return cy.get('[id="model-server-replicas"]');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here -- another gap we already had.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll log a bug for this... this is a common component we failed to properly annotate, shouldn't be one you folks have to make use of. Leave as-is.

}

findNimModelReplicasMinusButton() {
return this.find().find('button[aria-label="Minus"]').eq(1);
}

findNimModelReplicasPlusButton() {
return this.find().find('button[aria-label="Plus"]').eq(1);
}
Comment on lines +36 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these have a chance of conflicting with the StorageSize +/-

Hopefully in the improvement issues I'll log we can make sure this is covered too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll log a bug for this... this is a common component we failed to properly annotate, shouldn't be one you folks have to make use of. Leave as-is.


shouldDisplayError(msg: string): void {
this.find().should('contain.text', msg);
}
}

export const nimDeployModal = new NIMDeployModal();
8 changes: 8 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/modelServing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,14 @@ class InferenceServiceRow extends TableRow {
findExternalServicePopover() {
return cy.findByTestId('external-service-popover');
}

findServingRuntime() {
return this.find().find(`[data-label="Serving Runtime"]`);
}

findProject() {
return this.find().find(`[data-label=Project]`);
}
Comment on lines +323 to +329
Copy link
Member

@andrewballantyne andrewballantyne Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although these are not likely to change, they feel like they might be brittle based on how the selectors are written.

No action needed atm, but I wonder if we can't log something to improve these gaps in the future.

}
class ServingPlatformCard extends Contextual<HTMLElement> {
findDeployModelButton() {
Expand Down
56 changes: 54 additions & 2 deletions frontend/src/__tests__/cypress/cypress/pages/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class ProjectDetails {
return this.findDataConnectionTable().find('thead').findByRole('button', { name });
}

private findModelServingPlatform(name: string) {
findModelServingPlatform(name: string) {
return this.findComponent('model-server').findByTestId(`${name}-serving-platform-card`);
}

Expand Down Expand Up @@ -307,13 +307,23 @@ class ProjectDetails {
return cy.findByTestId('unsupported-pipeline-version-alert');
}

private findKserveModelsTable() {
findKserveModelsTable() {
return cy.findByTestId('kserve-inference-service-table');
}

getKserveModelMetricLink(name: string) {
return this.findKserveModelsTable().findByTestId(`metrics-link-${name}`);
}

getKserveTableRow(name: string) {
return new KserveTableRow(() =>
this.findKserveModelsTable()
.find('tbody')
.find('[data-label="Name"]')
.contains(name)
.closest('tr'),
);
}
}

class ProjectDetailsSettingsTab extends ProjectDetails {
Expand All @@ -326,9 +336,51 @@ class ProjectDetailsSettingsTab extends ProjectDetails {
}
}

class ProjectDetailsOverviewTab extends ProjectDetails {
visit(project: string) {
super.visitSection(project, 'overview');
}

findDeployedModelServingRuntime(name: string) {
return cy
.findByTestId('section-overview')
.get('div')
.contains(name)
.parents('.odh-type-bordered-card .model-server')
.get('dd');
}
Comment on lines +344 to +351
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will select anything -- I can't find section-overview test id in the code outside of this selector.

Looking for classes and going through divs is definitely a fast-track to brittle tests. If you're trying to select an element specifically on a page, we need a better data-testid on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I does selects, the test works:

it('should list the deployed model in Overview tab', () => {
      initInterceptsToEnableNim({ hasAllModels: false });
      cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()]));
      cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()]));

      projectDetails.visit('test-project');

      // Card is visible
      projectDetailsOverviewTab
        .findDeployedModelServingRuntime('Test Name')
        .should('have.text', 'NVIDIA NIM');
    });

I couldn't find a testid on the deployed model card, using the parent based on the calsses was the only way I was able to select the card. Can you suggest something else please?


findModelServingPlatform(name: string) {
return cy.findByTestId(`${name}-platform-card`);
}
}

class KserveTableRow extends TableRow {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like it doesn't belong here -- KServe Specific stuff in a projects file? Hmmm... I wonder if this doesn't belong better in the modelServing.ts page component file?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we named it incorrectly, but we use it to select deployed model records from the models table in the Models tab. See test called should list the deployed model in Models tab.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewballantyne there's a decent amount of model serving related duplication between the projects page tests and the model serving page tests, since we have some of the same functionality in 3 places (project details overview tab, project details models tab, model serving page). It maybe should be cleaned up at the same time as some of the refactoring @lucferbux and @emilys314 have mentioned doing as part of the KServe Raw Deployment work.

findAPIProtocol() {
return this.find().find(`[data-label="API protocol"]`);
}

findServiceRuntime() {
return this.find().find(`[data-label="Serving Runtime"]`);
}

findDetailsTriggerButton() {
return this.find().findByTestId('kserve-model-row-item').find('button');
}

private findDetailsCell() {
return this.find().next('tr').find('td').eq(1);
}

findInfoValueFor(label: string) {
return this.findDetailsCell().find('dt').contains(label).closest('div').find('dd');
}
}

export const projectListPage = new ProjectListPage();
export const createProjectModal = new CreateEditProjectModal();
export const editProjectModal = new CreateEditProjectModal(true);
export const deleteProjectModal = new DeleteModal();
export const projectDetails = new ProjectDetails();
export const projectDetailsSettingsTab = new ProjectDetailsSettingsTab();
export const projectDetailsOverviewTab = new ProjectDetailsOverviewTab();
15 changes: 15 additions & 0 deletions frontend/src/__tests__/cypress/cypress/support/commands/odh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { StatusResponse } from '~/redux/types';
import type {
BYONImage,
ClusterSettingsType,
DetectedAccelerators,
ImageInfo,
OdhApplication,
OdhDocument,
Expand All @@ -54,6 +55,7 @@ import type { GrpcResponse } from '~/__mocks__/mlmd/utils';
import type { BuildMockPipelinveVersionsType } from '~/__mocks__';
import type { ArtifactStorage } from '~/concepts/pipelines/types';
import type { ConnectionTypeConfigMap } from '~/concepts/connectionTypes/types';
import type { NimServingResponse } from '~/__tests__/cypress/cypress/types';

type SuccessErrorResponse = {
success: boolean;
Expand Down Expand Up @@ -649,6 +651,19 @@ declare global {
path: { serviceName: string; apiVersion: string; artifactId: string };
},
response: OdhResponse<ModelArtifact>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/accelerators',
response: OdhResponse<DetectedAccelerators>,
) => Cypress.Chainable<null>) &
((
type: 'GET /api/nim-serving/:resource',
options: {
path: {
resource: 'nvidia-nim-images-data' | 'nvidia-nim-access' | 'nvidia-nim-image-pull';
};
},
response: OdhResponse<NimServingResponse>,
) => Cypress.Chainable<null>);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '~/__tests__/cypress/cypress/pages/modelRegistry/modelVersionArchive';
import { labelModal, modelRegistry } from '~/__tests__/cypress/cypress/pages/modelRegistry';
import { mockModelRegistryService } from '~/__mocks__/mockModelRegistryService';
import { KnownLabels } from '~/k8sTypes';

const MODEL_REGISTRY_API_VERSION = 'v1alpha3';

Expand Down Expand Up @@ -323,7 +324,14 @@ describe('Archiving version', () => {
cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockProjectK8sResource({})]));
cy.interceptK8sList(
InferenceServiceModel,
mockK8sResourceList([mockInferenceServiceK8sResource({})]),
mockK8sResourceList([
mockInferenceServiceK8sResource({
additionalLabels: {
[KnownLabels.REGISTERED_MODEL_ID]: '1',
[KnownLabels.MODEL_VERSION_ID]: '3',
},
}),
]),
);
initIntercepts({});

Expand Down
Loading
Loading