From 9947f960511824a85a719bdc5716a0802d19e318 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Wed, 2 Oct 2024 12:21:40 -0400 Subject: [PATCH 01/33] test: added mocks and interceptors for nim cypress tests Signed-off-by: Tomer Figenblat --- .../mocked/modelServing/modelServingNim.cy.ts | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts new file mode 100644 index 0000000000..dc983a6591 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -0,0 +1,191 @@ +import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; +import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; +import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; +import { + AcceleratorProfileModel, + ConfigMapModel, + NotebookModel, + PodModel, + ProjectModel, + PVCModel, + RoleBindingModel, + RouteModel, + SecretModel, + StorageClassModel, + TemplateModel, +} from '~/__tests__/cypress/cypress/utils/models'; +import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; +import { mockConfigMap } from '~/__mocks__/mockConfigMap'; +import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; +import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; +import { mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; +import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; +import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; +import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; +import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; +import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; + +const initIntercepts = () => { + // not all interceptions here are required for the test to succeed + // some are here to eliminate (not-blocking) error responses to ease with debugging + + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { kserve: true, 'model-mesh': true }, + }), + ); + + cy.interceptOdh('GET /api/dsci/status', mockDsciStatus({})); + + cy.interceptOdh('GET /api/builds', {}); + + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }), + ); + + cy.interceptK8sList(StorageClassModel, mockK8sResourceList(mockStorageClasses)); + + cy.interceptOdh('GET /api/console-links', mockConsoleLinks()); + + cy.interceptOdh('GET /api/quickstarts', mockQuickStarts()); + + cy.interceptOdh('GET /api/segment-key', {}); + + const project = mockProjectK8sResource({ hasAnnotations: true, enableModelMesh: false }); + if (project.metadata.annotations != null) { + project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; + } + cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); + + cy.interceptK8sList( + NotebookModel, + mockK8sResourceList([mockNotebookK8sResource({ namespace: 'test-project' })]), + ); + + cy.interceptK8sList(PVCModel, mockK8sResourceList([mockPVCK8sResource({})])); + + cy.interceptK8sList(SecretModel, mockK8sResourceList([mockSecretK8sResource({})])); + + cy.interceptK8sList( + SecretModel, + mockK8sResourceList([mockSecretK8sResource({ namespace: 'test-project' })]), + ); + + cy.interceptK8sList(RoleBindingModel, mockK8sResourceList([mockRoleBindingK8sResource({})])); + + 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'; + } + + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + + cy.interceptK8sList(PodModel, mockK8sResourceList([mockPodK8sResource({})])); + + cy.interceptK8sList( + AcceleratorProfileModel, + mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), + ); + + // TODO not required but eliminates not-blocking error response + // cy.interceptK8sList( + // ServingRuntimeModel, + // mockK8sResourceList([ + // mockServingRuntimeK8sResource({ + // name: 'nvidia-nim-runtime', + // disableModelMeshAnnotations: true, + // disableResources: true, + // acceleratorName: 'nvidia.com/gpu', + // displayName: 'NVIDIA NIM', + // }), + // ]), + // ); + + // TODO not required but eliminates not-blocking error response + // cy.interceptK8sList( + // InferenceServiceModel, + // mockK8sResourceList([mockInferenceServiceK8sResource({})]) + // ); + + cy.interceptK8s(RouteModel, mockRouteK8sResource({})); + + cy.interceptOdh('GET /api/accelerators', { + configured: true, + available: { 'nvidia.com/gpu': 1 }, + }); + + // TODO do we need to mock this? + // cy.interceptK8s( + // ConfigMapModel, + // mockConfigMap({ + // data: { + // validation_result: 'true', + // }, + // namespace: 'opendatahub', + // name: 'nvidia-nim-validation-result', + // }), + // ); + + cy.interceptK8s( + ConfigMapModel, + mockConfigMap({ + name: 'nvidia-nim-images-data', + namespace: 'opendatahub', + data: { + alphafold2: + '{' + + ' "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': + '{' + + ' "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"' + + ' }', + }, + }), + ); +}; + +describe('Model Serving NIM', () => { + it('should do something', () => { + initIntercepts(); + projectDetails.visitSection('test-project', 'model-server'); + // modelServingSection + // .getServingPlatformCard('nvidia-nim-platform-card') + // .findDeployModelButton() + // .click(); + }); +}); From ba0f98ce09f8b67d0e58c376024225e19ba33312 Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:52:19 -0400 Subject: [PATCH 02/33] Merging the base test cases of Daniel and Lokesh's code to the Tomer's standalone cypress test suite. --- .../mockInferenceServiceK8sResource.ts | 8 +- .../src/__mocks__/mockProjectK8sResource.ts | 4 + .../mockServingRuntimeK8sResource.ts | 8 +- .../src/__tests__/cypress/cypress.config.ts | 1 + .../cypress/cypress/pages/modelServing.ts | 19 ++ .../cypress/cypress/pages/projects.ts | 12 +- .../mocked/modelServing/modelServingNim.cy.ts | 196 +++++++++++++++++- 7 files changed, 238 insertions(+), 10 deletions(-) diff --git a/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts b/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts index f878a01375..85388dabdb 100644 --- a/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts +++ b/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts @@ -8,6 +8,7 @@ type MockResourceConfigType = { namespace?: string; displayName?: string; modelName?: string; + runtimeName?: string; secretName?: string; deleted?: boolean; isModelMesh?: boolean; @@ -65,7 +66,8 @@ export const mockInferenceServiceK8sResource = ({ name = 'test-inference-service', namespace = 'test-project', displayName = 'Test Inference Service', - modelName = 'test-model', + modelName = 'onnx', + runtimeName = 'test-model', secretName = 'test-secret', deleted = false, isModelMesh = false, @@ -113,7 +115,7 @@ export const mockInferenceServiceK8sResource = ({ maxReplicas, model: { modelFormat: { - name: 'onnx', + name: modelName, version: '1', }, ...(acceleratorIdentifier !== '' @@ -129,7 +131,7 @@ export const mockInferenceServiceK8sResource = ({ } : {}), ...(resources && { resources }), - runtime: modelName, + runtime: runtimeName, storage: { key: secretName, path, diff --git a/frontend/src/__mocks__/mockProjectK8sResource.ts b/frontend/src/__mocks__/mockProjectK8sResource.ts index 4ea79943e6..7f054cd95e 100644 --- a/frontend/src/__mocks__/mockProjectK8sResource.ts +++ b/frontend/src/__mocks__/mockProjectK8sResource.ts @@ -11,6 +11,7 @@ type MockResourceConfigType = { creationTimestamp?: string; enableModelMesh?: boolean; isDSProject?: boolean; + disableNIMModelServing?: boolean; phase?: 'Active' | 'Terminating'; }; @@ -24,6 +25,7 @@ export const mockProjectK8sResource = ({ description = '', isDSProject = true, phase = 'Active', + disableNIMModelServing = true, }: MockResourceConfigType): ProjectKind => ({ kind: 'Project', apiVersion: 'project.openshift.io/v1', @@ -37,9 +39,11 @@ export const mockProjectK8sResource = ({ [KnownLabels.MODEL_SERVING_PROJECT]: enableModelMesh ? 'true' : 'false', }), ...(isDSProject && { [KnownLabels.DASHBOARD_RESOURCE]: 'true' }), + ...(!disableNIMModelServing && { 'modelmesh-enabled': 'false' }), }, ...(hasAnnotations && { annotations: { + ...(!disableNIMModelServing && { 'opendatahub.io/nim-support': 'true' }), ...(description && { 'openshift.io/description': description }), ...(displayName && { 'openshift.io/display-name': displayName }), ...(username && { 'openshift.io/requester': username }), diff --git a/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts b/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts index 3becb782fc..845c8ee8e7 100644 --- a/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts +++ b/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts @@ -4,6 +4,8 @@ import { ServingRuntimeAPIProtocol, ContainerResources } from '~/types'; type MockResourceConfigType = { name?: string; displayName?: string; + templateName?: string; + templateDisplayName?: string; namespace?: string; replicas?: number; auth?: boolean; @@ -97,6 +99,8 @@ export const mockServingRuntimeK8sResource = ({ auth = false, route = false, displayName = 'OVMS Model Serving', + templateName = 'ovms', + templateDisplayName = 'OpenVINO Serving Runtime (Supports GPUs)', acceleratorName = '', apiProtocol = ServingRuntimeAPIProtocol.REST, resources = { @@ -122,9 +126,9 @@ export const mockServingRuntimeK8sResource = ({ [KnownLabels.DASHBOARD_RESOURCE]: 'true', }, annotations: { - 'opendatahub.io/template-display-name': 'OpenVINO Serving Runtime (Supports GPUs)', + 'opendatahub.io/template-display-name': templateDisplayName, 'opendatahub.io/accelerator-name': acceleratorName, - 'opendatahub.io/template-name': 'ovms', + 'opendatahub.io/template-name': templateName, 'openshift.io/display-name': displayName, 'opendatahub.io/apiProtocol': apiProtocol, ...(!disableModelMeshAnnotations && { diff --git a/frontend/src/__tests__/cypress/cypress.config.ts b/frontend/src/__tests__/cypress/cypress.config.ts index cb1bcf733e..493f9fdcb8 100644 --- a/frontend/src/__tests__/cypress/cypress.config.ts +++ b/frontend/src/__tests__/cypress/cypress.config.ts @@ -16,6 +16,7 @@ import { env, cypressEnv, BASE_URL } from '~/__tests__/cypress/cypress/utils/tes const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`; export default defineConfig({ + experimentalStudio: true, experimentalMemoryManagement: true, // Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406 reporter: '../../../node_modules/cypress-multi-reporters', diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts index 50cc4f2fd8..1d7f062521 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts @@ -160,6 +160,24 @@ class InferenceServiceModal extends 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'); + } +} + class ServingRuntimeModal extends Modal { constructor(private edit = false) { super(`${edit ? 'Edit' : 'Add'} model server`); @@ -392,3 +410,4 @@ export const createServingRuntimeModal = new ServingRuntimeModal(false); export const editServingRuntimeModal = new ServingRuntimeModal(true); export const kserveModal = new KServeModal(); export const kserveModalEdit = new KServeModal(true); +export const nimDeployModal = new NIMDeployModal(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 458ad42b8a..a3b41e42ad 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -235,6 +235,14 @@ class ProjectDetails { return this.findModelServingPlatform('multi').findByTestId('multi-serving-add-server-button'); } + findNimModelDeployButton() { + return this.findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); + } + + findNimModelServingPlatformCard() { + return cy.findByTestId('nvidia-nim-model-serving-platform-card'); + } + findDeployModelTooltip() { return cy.findByTestId('model-serving-action-tooltip'); } @@ -244,8 +252,8 @@ class ProjectDetails { return this; } - findServingPlatformLabel() { - return cy.findByTestId('serving-platform-label'); + findServingPlatformLabel(label = 'serving-platform-label') { + return cy.findByTestId(label); } findComponent(componentName: string) { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index dc983a6591..1a1e5ea03c 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -6,14 +6,14 @@ import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { AcceleratorProfileModel, - ConfigMapModel, + ConfigMapModel, InferenceServiceModel, NotebookModel, PodModel, ProjectModel, PVCModel, RoleBindingModel, RouteModel, - SecretModel, + SecretModel, ServingRuntimeModel, StorageClassModel, TemplateModel, } from '~/__tests__/cypress/cypress/utils/models'; @@ -22,12 +22,21 @@ import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { mockConfigMap } from '~/__mocks__/mockConfigMap'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; +import { + mockInferenceServiceK8sResource, + mockNotebookK8sResource, + mockRouteK8sResource, mockServingRuntimeK8sResource, + mockStorageClasses +} from '~/__mocks__'; import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; +import { + nimDeployModal, +} from '~/__tests__/cypress/cypress/pages/modelServing'; +import {getNIMResource} from "~/pages/modelServing/screens/projects/nimUtils"; const initIntercepts = () => { // not all interceptions here are required for the test to succeed @@ -180,6 +189,9 @@ const initIntercepts = () => { }; describe('Model Serving NIM', () => { + + + it('should do something', () => { initIntercepts(); projectDetails.visitSection('test-project', 'model-server'); @@ -188,4 +200,182 @@ describe('Model Serving NIM', () => { // .findDeployModelButton() // .click(); }); + + //TODO: Work by Daniel + it('Deploy NIM model', () => { + //TODO: abstract the dependency from projectDetails.cy.ts + // initInterceptsForNIM({ + // templates: true, + // disableKServeConfig: false, + // disableModelConfig: false, + // disableNIMModelServing: false, + // }); + + initIntercepts(); + cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); + cy.interceptK8s( + 'POST', + InferenceServiceModel, + mockInferenceServiceK8sResource({ + name: 'test-name', + path: 'test-model/', + modelName: 'llama-2-13b-chat', + runtimeName: 'test-name', + displayName: 'Test Name', + isModelMesh: true, + }), + ).as('createInferenceService'); + + cy.interceptK8s( + 'POST', + ServingRuntimeModel, + mockServingRuntimeK8sResource({ + templateDisplayName: 'NVIDIA NIM', + templateName: 'nvidia-nim-runtime', + }), + ).as('createServingRuntime'); + + cy.interceptK8s( + ConfigMapModel, + mockConfigMap({ + data: { + alphafold2: + '{\n "name": "alphafold2",\n "displayName": "AlphaFold2",\n "shortDescription": "A widely used model for predicting the 3D structures of proteins from their amino acid sequences.",\n "namespace": "nim/deepmind",\n "tags": [\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-27T01:51:55.642Z"\n}', + 'arctic-embed-l': + '{\n "name": "arctic-embed-l",\n "displayName": "Snowflake Arctic Embed Large Embedding",\n "shortDescription": "NVIDIA NIM for GPU accelerated Snowflake Arctic Embed Large Embedding inference",\n "namespace": "nim/snowflake",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:38:40.927Z"\n}', + diffdock: + '{\n "name": "diffdock",\n "displayName": "DiffDock",\n "shortDescription": "Diffdock predicts the 3D structure of the interaction between a molecule and a protein.",\n "namespace": "nim/mit",\n "tags": [\n "1.2.0"\n ],\n "latestTag": "1.2.0",\n "updatedDate": "2024-07-31T01:07:09.680Z"\n}', + 'fastpitch-hifigan-tts': + '{\n "name": "fastpitch-hifigan-tts",\n "displayName": "TTS FastPitch HifiGAN Riva",\n "shortDescription": "RIVA TTS NIM provide easy access to state-of-the-art text to speech models, capable of synthesizing English speech from text",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-06T16:18:02.346Z"\n}', + 'llama-2-13b-chat': + '{\n "name": "llama-2-13b-chat",\n "displayName": "meta-llama-2-13b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 13B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1",\n "1.0",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:10:56.252Z"\n}', + 'llama-2-70b-chat': + '{\n "name": "llama-2-70b-chat",\n "displayName": "meta-llama-2-70b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:09:42.812Z"\n}', + 'llama-2-7b-chat': + '{\n "name": "llama-2-7b-chat",\n "displayName": "meta-llama-2-7b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 7B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:09:01.084Z"\n}', + 'llama-3-taiwan-70b-instruct': + '{\n "name": "llama-3-taiwan-70b-instruct",\n "displayName": "Llama-3-Taiwan-70B-Instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama-3-Taiwan-70B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/yentinglin",\n "tags": [\n "latest",\n "1.1.2",\n "1.1",\n "1"\n ],\n "latestTag": "latest",\n "updatedDate": "2024-08-28T03:24:13.146Z"\n}', + 'llama-3.1-405b-instruct': + '{\n "name": "llama-3.1-405b-instruct",\n "displayName": "Llama-3.1-405b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 405B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.2.0",\n "1.2",\n "1.1.2",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.2.0",\n "updatedDate": "2024-09-11T03:47:21.693Z"\n}', + 'llama-3.1-70b-instruct': + '{\n "name": "llama-3.1-70b-instruct",\n "displayName": "Llama-3.1-70b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.2.1",\n "1.2",\n "1.1.2",\n "1.1.1",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.2",\n "updatedDate": "2024-09-20T22:01:11.799Z"\n}', + 'llama-3.1-8b-base': + '{\n "name": "llama-3.1-8b-base",\n "displayName": "Llama-3.1-8b-base",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 8B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.1.2",\n "1.1.1",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.1.2",\n "updatedDate": "2024-08-21T15:54:19.302Z"\n}', + 'llama3-70b-instruct': + '{\n "name": "llama3-70b-instruct",\n "displayName": "Meta/Llama3-70b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "1.0.1",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-09-23T18:15:48.418Z"\n}', + 'llama3-8b-instruct': + '{\n "name": "llama3-8b-instruct",\n "displayName": "Meta/Llama3-8b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3 8B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-03T21:47:11.384Z"\n}', + 'megatron-1b-nmt': + '{\n "name": "megatron-1b-nmt",\n "displayName": "NMT Megatron Riva 1b",\n "shortDescription": "Riva NMT NIM provide easy access to state-of-the-art neural machine translation (NMT) models, capable of translating text from one language to another with exceptional accuracy.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-06T01:30:36.789Z"\n}', + 'mistral-7b-instruct-v0.3': + '{\n "name": "mistral-7b-instruct-v0.3",\n "displayName": "Mistral-7B-Instruct-v0.3",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mistral-7B-Instruct-v0.3 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.1",\n "1.1.2",\n "1",\n "latest"\n ],\n "latestTag": "latest",\n "updatedDate": "2024-09-10T16:21:20.547Z"\n}', + 'mistral-nemo-12b-instruct': + '{\n "name": "mistral-nemo-12b-instruct",\n "displayName": "Mistral-Nemo-12B-Instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mistral-NeMo-12B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/nv-mistralai",\n "tags": [\n "1.2",\n "1.2.2",\n "1",\n "latest"\n ],\n "latestTag": "1.2",\n "updatedDate": "2024-09-26T19:10:40.237Z"\n}', + 'mixtral-8x22b-instruct-v01': + '{\n "name": "mixtral-8x22b-instruct-v01",\n "displayName": "Mixtral-8x22B-Instruct-v0.1",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mixtral-8x22B-Instruct-v0.1 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.2.2",\n "1",\n "1.2",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.2.2",\n "updatedDate": "2024-09-26T23:51:49.835Z"\n}', + 'mixtral-8x7b-instruct-v01': + '{\n "name": "mixtral-8x7b-instruct-v01",\n "displayName": "Mixtral-8x7B-Instruct-v0.1",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mixtral-8x7B-Instruct-v0.1 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.2.1",\n "1",\n "1.2",\n "1.0.0",\n "latest"\n ],\n "latestTag": "1.2.1",\n "updatedDate": "2024-09-20T21:57:44.596Z"\n}', + molmim: + '{\n "name": "molmim",\n "displayName": "MolMIM",\n "shortDescription": "MolMIM is a transformer-based model developed by NVIDIA for controlled small molecule generation.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-09-23T18:19:56.199Z"\n}', + 'nemotron-4-340b-instruct': + '{\n "name": "nemotron-4-340b-instruct",\n "displayName": "nemotron-4-340b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Nemotron-4-340B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/nvidia",\n "tags": [\n "latest",\n "1.1.2",\n "1.1",\n "1"\n ],\n "latestTag": "1.1.2",\n "updatedDate": "2024-08-29T18:33:09.615Z"\n}', + 'nv-embedqa-e5-v5': + '{\n "name": "nv-embedqa-e5-v5",\n "displayName": "NVIDIA Retrieval QA E5 Embedding v5",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA E5 Embedding v5 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:37:35.717Z"\n}', + 'nv-embedqa-mistral-7b-v2': + '{\n "name": "nv-embedqa-mistral-7b-v2",\n "displayName": "NVIDIA Retrieval QA Mistral 7B Embedding v2",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA Mistral 7B Embedding v2 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:38:04.698Z"\n}', + 'nv-rerankqa-mistral-4b-v3': + '{\n "name": "nv-rerankqa-mistral-4b-v3",\n "displayName": "NVIDIA Retrieval QA Mistral 4B Reranking v3",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA Mistral 4B Reranking v3 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.2",\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.2",\n "updatedDate": "2024-08-06T21:57:16.255Z"\n}', + 'parakeet-ctc-1.1b-asr': + '{\n "name": "parakeet-ctc-1.1b-asr",\n "displayName": "ASR Parakeet CTC Riva 1.1b",\n "shortDescription": "RIVA ASR NIM delivers accurate English speech-to-text transcription and enables easy-to-use optimized ASR inference for large scale deployments.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-07T05:12:53.897Z"\n}', + proteinmpnn: + '{\n "name": "proteinmpnn",\n "displayName": "ProteinMPNN",\n "shortDescription": "Predicts amino acid sequences from 3D structure of proteins.",\n "namespace": "nim/ipd",\n "tags": [\n "1",\n "1.0",\n "1.0.0"\n ],\n "latestTag": "1",\n "updatedDate": "2024-08-27T01:52:07.955Z"\n}', + }, + namespace: 'opendatahub', + name: 'nvidia-nim-images-data', + }), + ); + projectDetails.visitSection('test-project', 'model-server'); + //projectDetails.findNimModelDeployButton().click(); + cy.findByTestId('deploy-button').click(); + //cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + + // test that you can not submit on empty + nimDeployModal.shouldBeOpen(); + nimDeployModal.findSubmitButton().should('be.disabled'); + + // test filling in minimum required fields + nimDeployModal.findModelNameInput().type('Test Name'); + nimDeployModal.findNIMToDeploy().findSelectOption('meta-llama-2-13b-chat - latest').click(); + nimDeployModal.findSubmitButton().should('be.enabled'); + + //nimDeployModal.findSubmitButton().click(); + + //dry run request + // cy.wait('@createInferenceService').then((interception) => { + // expect(interception.request.url).to.include('?dryRun=All'); + // expect(interception.request.body).to.eql({ + // apiVersion: 'serving.kserve.io/v1beta1', + // kind: 'InferenceService', + // metadata: { + // name: 'test-name', + // namespace: 'test-project', + // labels: { 'opendatahub.io/dashboard': 'true' }, + // annotations: { + // 'openshift.io/display-name': 'Test Name', + // }, + // }, + // spec: { + // predictor: { + // model: { + // modelFormat: { name: 'llama-2-13b-chat' }, + // runtime: 'test-name', + // storage: { key: 'test-secret', path: 'test-model/' }, + // }, + // resources: { + // limits: { cpu: '2', memory: '8Gi' }, + // requests: { cpu: '1', memory: '4Gi' }, + // }, + // }, + // }, + // }); + // }); + // + // // Actual request + // cy.wait('@createInferenceService').then((interception) => { + // expect(interception.request.url).not.to.include('?dryRun=All'); + // }); + // + // cy.get('@createInferenceService.all').then((interceptions) => { + // expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request + // }); + }); + +//TODO: This work is done by Lokesh + it('Check if the Nim Model UI is enabled', () => { + // initIntercepts({ + // templates: true, + // disableKServeConfig: false, + // disableModelConfig: false, + // disableNIMModelServing: false, + // }); + + initIntercepts(); + + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + + + projectDetails.findServingPlatformLabel().should('exist'); + cy.findByTestId('empty-state-title').should('exist'); + cy.findByTestId('deploy-button').should('exist'); + + // projectDetails + // .findNimModelServingPlatformCard() + // .contains('Models are deployed using NVIDIA NIM microservices.'); + // projectDetails + // .findNimModelServingPlatformCard() + // .contains('NVIDIA NIM model serving platform'); + }); + + }); From d1032478f46bd8f53f31686a76458fa2070d3d4a Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:01:12 -0400 Subject: [PATCH 03/33] Reverting the mock changes which are not needed. --- frontend/src/__mocks__/mockInferenceServiceK8sResource.ts | 8 +++----- frontend/src/__mocks__/mockProjectK8sResource.ts | 4 ---- frontend/src/__mocks__/mockServingRuntimeK8sResource.ts | 8 ++------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts b/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts index 85388dabdb..f878a01375 100644 --- a/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts +++ b/frontend/src/__mocks__/mockInferenceServiceK8sResource.ts @@ -8,7 +8,6 @@ type MockResourceConfigType = { namespace?: string; displayName?: string; modelName?: string; - runtimeName?: string; secretName?: string; deleted?: boolean; isModelMesh?: boolean; @@ -66,8 +65,7 @@ export const mockInferenceServiceK8sResource = ({ name = 'test-inference-service', namespace = 'test-project', displayName = 'Test Inference Service', - modelName = 'onnx', - runtimeName = 'test-model', + modelName = 'test-model', secretName = 'test-secret', deleted = false, isModelMesh = false, @@ -115,7 +113,7 @@ export const mockInferenceServiceK8sResource = ({ maxReplicas, model: { modelFormat: { - name: modelName, + name: 'onnx', version: '1', }, ...(acceleratorIdentifier !== '' @@ -131,7 +129,7 @@ export const mockInferenceServiceK8sResource = ({ } : {}), ...(resources && { resources }), - runtime: runtimeName, + runtime: modelName, storage: { key: secretName, path, diff --git a/frontend/src/__mocks__/mockProjectK8sResource.ts b/frontend/src/__mocks__/mockProjectK8sResource.ts index 7f054cd95e..4ea79943e6 100644 --- a/frontend/src/__mocks__/mockProjectK8sResource.ts +++ b/frontend/src/__mocks__/mockProjectK8sResource.ts @@ -11,7 +11,6 @@ type MockResourceConfigType = { creationTimestamp?: string; enableModelMesh?: boolean; isDSProject?: boolean; - disableNIMModelServing?: boolean; phase?: 'Active' | 'Terminating'; }; @@ -25,7 +24,6 @@ export const mockProjectK8sResource = ({ description = '', isDSProject = true, phase = 'Active', - disableNIMModelServing = true, }: MockResourceConfigType): ProjectKind => ({ kind: 'Project', apiVersion: 'project.openshift.io/v1', @@ -39,11 +37,9 @@ export const mockProjectK8sResource = ({ [KnownLabels.MODEL_SERVING_PROJECT]: enableModelMesh ? 'true' : 'false', }), ...(isDSProject && { [KnownLabels.DASHBOARD_RESOURCE]: 'true' }), - ...(!disableNIMModelServing && { 'modelmesh-enabled': 'false' }), }, ...(hasAnnotations && { annotations: { - ...(!disableNIMModelServing && { 'opendatahub.io/nim-support': 'true' }), ...(description && { 'openshift.io/description': description }), ...(displayName && { 'openshift.io/display-name': displayName }), ...(username && { 'openshift.io/requester': username }), diff --git a/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts b/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts index 845c8ee8e7..3becb782fc 100644 --- a/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts +++ b/frontend/src/__mocks__/mockServingRuntimeK8sResource.ts @@ -4,8 +4,6 @@ import { ServingRuntimeAPIProtocol, ContainerResources } from '~/types'; type MockResourceConfigType = { name?: string; displayName?: string; - templateName?: string; - templateDisplayName?: string; namespace?: string; replicas?: number; auth?: boolean; @@ -99,8 +97,6 @@ export const mockServingRuntimeK8sResource = ({ auth = false, route = false, displayName = 'OVMS Model Serving', - templateName = 'ovms', - templateDisplayName = 'OpenVINO Serving Runtime (Supports GPUs)', acceleratorName = '', apiProtocol = ServingRuntimeAPIProtocol.REST, resources = { @@ -126,9 +122,9 @@ export const mockServingRuntimeK8sResource = ({ [KnownLabels.DASHBOARD_RESOURCE]: 'true', }, annotations: { - 'opendatahub.io/template-display-name': templateDisplayName, + 'opendatahub.io/template-display-name': 'OpenVINO Serving Runtime (Supports GPUs)', 'opendatahub.io/accelerator-name': acceleratorName, - 'opendatahub.io/template-name': templateName, + 'opendatahub.io/template-name': 'ovms', 'openshift.io/display-name': displayName, 'opendatahub.io/apiProtocol': apiProtocol, ...(!disableModelMeshAnnotations && { From b1e48d3aefba677c169c9d61cfaee786fe9b1159 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:55:18 +0200 Subject: [PATCH 04/33] nim-specific classes and functions, partial rework of deployment test (#2) Signed-off-by: Daniele Martinoli --- frontend/src/__mocks__/mockNimResource.ts | 60 ++++ .../cypress/cypress/pages/modelServing.ts | 19 -- .../cypress/cypress/pages/nimModelDialog.ts | 21 ++ .../cypress/cypress/pages/projects.ts | 12 +- .../mocked/modelServing/modelServingNim.cy.ts | 262 +++++------------- .../cypress/cypress/utils/nimUtils.ts | 11 + 6 files changed, 167 insertions(+), 218 deletions(-) create mode 100644 frontend/src/__mocks__/mockNimResource.ts create mode 100644 frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts new file mode 100644 index 0000000000..1809c27c60 --- /dev/null +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -0,0 +1,60 @@ +import { ConfigMapKind, InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; +import { mockConfigMap } from './mockConfigMap'; +import { mockServingRuntimeK8sResource } from './mockServingRuntimeK8sResource'; +import { mockInferenceServiceK8sResource } from './mockInferenceServiceK8sResource'; + +export const mockNimImages = (): ConfigMapKind => + mockConfigMap({ + name: 'nvidia-nim-images-data', + namespace: 'opendatahub', + data: { + alphafold2: + '{' + + ' "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': + '{' + + ' "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: 'alphafold2', + displayName: 'Test Name', + isModelMesh: true, + }); + 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; +}; diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts index 1d7f062521..50cc4f2fd8 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts @@ -160,24 +160,6 @@ class InferenceServiceModal extends 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'); - } -} - class ServingRuntimeModal extends Modal { constructor(private edit = false) { super(`${edit ? 'Edit' : 'Add'} model server`); @@ -410,4 +392,3 @@ export const createServingRuntimeModal = new ServingRuntimeModal(false); export const editServingRuntimeModal = new ServingRuntimeModal(true); export const kserveModal = new KServeModal(); export const kserveModalEdit = new KServeModal(true); -export const nimDeployModal = new NIMDeployModal(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts b/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts new file mode 100644 index 0000000000..23bdc73717 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts @@ -0,0 +1,21 @@ +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'); + } +} + +export const nimDeployModal = new NIMDeployModal(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index a3b41e42ad..458ad42b8a 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -235,14 +235,6 @@ class ProjectDetails { return this.findModelServingPlatform('multi').findByTestId('multi-serving-add-server-button'); } - findNimModelDeployButton() { - return this.findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); - } - - findNimModelServingPlatformCard() { - return cy.findByTestId('nvidia-nim-model-serving-platform-card'); - } - findDeployModelTooltip() { return cy.findByTestId('model-serving-action-tooltip'); } @@ -252,8 +244,8 @@ class ProjectDetails { return this; } - findServingPlatformLabel(label = 'serving-platform-label') { - return cy.findByTestId(label); + findServingPlatformLabel() { + return cy.findByTestId('serving-platform-label'); } findComponent(componentName: string) { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 1a1e5ea03c..12c474e929 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -1,44 +1,43 @@ import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockDscStatus } from '~/__mocks__/mockDscStatus'; import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { + mockNimImages, + mockNimInferenceService, + mockNimServingRuntime, +} from '~/__mocks__/mockNimResource'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { AcceleratorProfileModel, - ConfigMapModel, InferenceServiceModel, + ConfigMapModel, + InferenceServiceModel, NotebookModel, PodModel, ProjectModel, PVCModel, RoleBindingModel, RouteModel, - SecretModel, ServingRuntimeModel, + SecretModel, + ServingRuntimeModel, StorageClassModel, TemplateModel, } from '~/__tests__/cypress/cypress/utils/models'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; -import { mockConfigMap } from '~/__mocks__/mockConfigMap'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { - mockInferenceServiceK8sResource, - mockNotebookK8sResource, - mockRouteK8sResource, mockServingRuntimeK8sResource, - mockStorageClasses -} from '~/__mocks__'; +import { mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; -import { - nimDeployModal, -} from '~/__tests__/cypress/cypress/pages/modelServing'; -import {getNIMResource} from "~/pages/modelServing/screens/projects/nimUtils"; +import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; +import { findServingPlatformLabel } from '~/__tests__/cypress/cypress/utils/nimUtils'; -const initIntercepts = () => { +const initInterceptsToEnableNim = () => { // not all interceptions here are required for the test to succeed // some are here to eliminate (not-blocking) error responses to ease with debugging @@ -151,49 +150,21 @@ const initIntercepts = () => { // name: 'nvidia-nim-validation-result', // }), // ); +}; - cy.interceptK8s( - ConfigMapModel, - mockConfigMap({ - name: 'nvidia-nim-images-data', - namespace: 'opendatahub', - data: { - alphafold2: - '{' + - ' "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': - '{' + - ' "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"' + - ' }', - }, - }), +const initInterceptsToDeployModel = () => { + cy.interceptK8s(ConfigMapModel, mockNimImages()); + cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); + cy.interceptK8s('POST', InferenceServiceModel, mockNimInferenceService()).as( + 'createInferenceService', ); + + cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); }; describe('Model Serving NIM', () => { - - - it('should do something', () => { - initIntercepts(); + initInterceptsToEnableNim(); projectDetails.visitSection('test-project', 'model-server'); // modelServingSection // .getServingPlatformCard('nvidia-nim-platform-card') @@ -201,103 +172,16 @@ describe('Model Serving NIM', () => { // .click(); }); - //TODO: Work by Daniel it('Deploy NIM model', () => { - //TODO: abstract the dependency from projectDetails.cy.ts - // initInterceptsForNIM({ - // templates: true, - // disableKServeConfig: false, - // disableModelConfig: false, - // disableNIMModelServing: false, - // }); + initInterceptsToEnableNim(); + initInterceptsToDeployModel(); - initIntercepts(); - cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); - cy.interceptK8s( - 'POST', - InferenceServiceModel, - mockInferenceServiceK8sResource({ - name: 'test-name', - path: 'test-model/', - modelName: 'llama-2-13b-chat', - runtimeName: 'test-name', - displayName: 'Test Name', - isModelMesh: true, - }), - ).as('createInferenceService'); - - cy.interceptK8s( - 'POST', - ServingRuntimeModel, - mockServingRuntimeK8sResource({ - templateDisplayName: 'NVIDIA NIM', - templateName: 'nvidia-nim-runtime', - }), - ).as('createServingRuntime'); - - cy.interceptK8s( - ConfigMapModel, - mockConfigMap({ - data: { - alphafold2: - '{\n "name": "alphafold2",\n "displayName": "AlphaFold2",\n "shortDescription": "A widely used model for predicting the 3D structures of proteins from their amino acid sequences.",\n "namespace": "nim/deepmind",\n "tags": [\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-27T01:51:55.642Z"\n}', - 'arctic-embed-l': - '{\n "name": "arctic-embed-l",\n "displayName": "Snowflake Arctic Embed Large Embedding",\n "shortDescription": "NVIDIA NIM for GPU accelerated Snowflake Arctic Embed Large Embedding inference",\n "namespace": "nim/snowflake",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:38:40.927Z"\n}', - diffdock: - '{\n "name": "diffdock",\n "displayName": "DiffDock",\n "shortDescription": "Diffdock predicts the 3D structure of the interaction between a molecule and a protein.",\n "namespace": "nim/mit",\n "tags": [\n "1.2.0"\n ],\n "latestTag": "1.2.0",\n "updatedDate": "2024-07-31T01:07:09.680Z"\n}', - 'fastpitch-hifigan-tts': - '{\n "name": "fastpitch-hifigan-tts",\n "displayName": "TTS FastPitch HifiGAN Riva",\n "shortDescription": "RIVA TTS NIM provide easy access to state-of-the-art text to speech models, capable of synthesizing English speech from text",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-06T16:18:02.346Z"\n}', - 'llama-2-13b-chat': - '{\n "name": "llama-2-13b-chat",\n "displayName": "meta-llama-2-13b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 13B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1",\n "1.0",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:10:56.252Z"\n}', - 'llama-2-70b-chat': - '{\n "name": "llama-2-70b-chat",\n "displayName": "meta-llama-2-70b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:09:42.812Z"\n}', - 'llama-2-7b-chat': - '{\n "name": "llama-2-7b-chat",\n "displayName": "meta-llama-2-7b-chat",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 2 7B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-21T16:09:01.084Z"\n}', - 'llama-3-taiwan-70b-instruct': - '{\n "name": "llama-3-taiwan-70b-instruct",\n "displayName": "Llama-3-Taiwan-70B-Instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama-3-Taiwan-70B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/yentinglin",\n "tags": [\n "latest",\n "1.1.2",\n "1.1",\n "1"\n ],\n "latestTag": "latest",\n "updatedDate": "2024-08-28T03:24:13.146Z"\n}', - 'llama-3.1-405b-instruct': - '{\n "name": "llama-3.1-405b-instruct",\n "displayName": "Llama-3.1-405b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 405B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.2.0",\n "1.2",\n "1.1.2",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.2.0",\n "updatedDate": "2024-09-11T03:47:21.693Z"\n}', - 'llama-3.1-70b-instruct': - '{\n "name": "llama-3.1-70b-instruct",\n "displayName": "Llama-3.1-70b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.2.1",\n "1.2",\n "1.1.2",\n "1.1.1",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.2",\n "updatedDate": "2024-09-20T22:01:11.799Z"\n}', - 'llama-3.1-8b-base': - '{\n "name": "llama-3.1-8b-base",\n "displayName": "Llama-3.1-8b-base",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3.1 8B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.1.2",\n "1.1.1",\n "1.1",\n "1",\n "latest",\n "1.1.0"\n ],\n "latestTag": "1.1.2",\n "updatedDate": "2024-08-21T15:54:19.302Z"\n}', - 'llama3-70b-instruct': - '{\n "name": "llama3-70b-instruct",\n "displayName": "Meta/Llama3-70b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3 70B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "1.0.1",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-09-23T18:15:48.418Z"\n}', - 'llama3-8b-instruct': - '{\n "name": "llama3-8b-instruct",\n "displayName": "Meta/Llama3-8b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Llama 3 8B inference through OpenAI compatible APIs",\n "namespace": "nim/meta",\n "tags": [\n "1.0.3",\n "1.0",\n "1",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.3",\n "updatedDate": "2024-08-03T21:47:11.384Z"\n}', - 'megatron-1b-nmt': - '{\n "name": "megatron-1b-nmt",\n "displayName": "NMT Megatron Riva 1b",\n "shortDescription": "Riva NMT NIM provide easy access to state-of-the-art neural machine translation (NMT) models, capable of translating text from one language to another with exceptional accuracy.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-06T01:30:36.789Z"\n}', - 'mistral-7b-instruct-v0.3': - '{\n "name": "mistral-7b-instruct-v0.3",\n "displayName": "Mistral-7B-Instruct-v0.3",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mistral-7B-Instruct-v0.3 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.1",\n "1.1.2",\n "1",\n "latest"\n ],\n "latestTag": "latest",\n "updatedDate": "2024-09-10T16:21:20.547Z"\n}', - 'mistral-nemo-12b-instruct': - '{\n "name": "mistral-nemo-12b-instruct",\n "displayName": "Mistral-Nemo-12B-Instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mistral-NeMo-12B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/nv-mistralai",\n "tags": [\n "1.2",\n "1.2.2",\n "1",\n "latest"\n ],\n "latestTag": "1.2",\n "updatedDate": "2024-09-26T19:10:40.237Z"\n}', - 'mixtral-8x22b-instruct-v01': - '{\n "name": "mixtral-8x22b-instruct-v01",\n "displayName": "Mixtral-8x22B-Instruct-v0.1",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mixtral-8x22B-Instruct-v0.1 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.2.2",\n "1",\n "1.2",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.2.2",\n "updatedDate": "2024-09-26T23:51:49.835Z"\n}', - 'mixtral-8x7b-instruct-v01': - '{\n "name": "mixtral-8x7b-instruct-v01",\n "displayName": "Mixtral-8x7B-Instruct-v0.1",\n "shortDescription": "NVIDIA NIM for GPU accelerated Mixtral-8x7B-Instruct-v0.1 inference through OpenAI compatible APIs",\n "namespace": "nim/mistralai",\n "tags": [\n "1.2.1",\n "1",\n "1.2",\n "1.0.0",\n "latest"\n ],\n "latestTag": "1.2.1",\n "updatedDate": "2024-09-20T21:57:44.596Z"\n}', - molmim: - '{\n "name": "molmim",\n "displayName": "MolMIM",\n "shortDescription": "MolMIM is a transformer-based model developed by NVIDIA for controlled small molecule generation.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-09-23T18:19:56.199Z"\n}', - 'nemotron-4-340b-instruct': - '{\n "name": "nemotron-4-340b-instruct",\n "displayName": "nemotron-4-340b-instruct",\n "shortDescription": "NVIDIA NIM for GPU accelerated Nemotron-4-340B-Instruct inference through OpenAI compatible APIs",\n "namespace": "nim/nvidia",\n "tags": [\n "latest",\n "1.1.2",\n "1.1",\n "1"\n ],\n "latestTag": "1.1.2",\n "updatedDate": "2024-08-29T18:33:09.615Z"\n}', - 'nv-embedqa-e5-v5': - '{\n "name": "nv-embedqa-e5-v5",\n "displayName": "NVIDIA Retrieval QA E5 Embedding v5",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA E5 Embedding v5 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:37:35.717Z"\n}', - 'nv-embedqa-mistral-7b-v2': - '{\n "name": "nv-embedqa-mistral-7b-v2",\n "displayName": "NVIDIA Retrieval QA Mistral 7B Embedding v2",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA Mistral 7B Embedding v2 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.1",\n "updatedDate": "2024-07-27T00:38:04.698Z"\n}', - 'nv-rerankqa-mistral-4b-v3': - '{\n "name": "nv-rerankqa-mistral-4b-v3",\n "displayName": "NVIDIA Retrieval QA Mistral 4B Reranking v3",\n "shortDescription": "NVIDIA NIM for GPU accelerated NVIDIA Retrieval QA Mistral 4B Reranking v3 inference",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0.2",\n "1.0.1",\n "1.0.0"\n ],\n "latestTag": "1.0.2",\n "updatedDate": "2024-08-06T21:57:16.255Z"\n}', - 'parakeet-ctc-1.1b-asr': - '{\n "name": "parakeet-ctc-1.1b-asr",\n "displayName": "ASR Parakeet CTC Riva 1.1b",\n "shortDescription": "RIVA ASR NIM delivers accurate English speech-to-text transcription and enables easy-to-use optimized ASR inference for large scale deployments.",\n "namespace": "nim/nvidia",\n "tags": [\n "1.0",\n "latest",\n "1.0.0"\n ],\n "latestTag": "1.0.0",\n "updatedDate": "2024-08-07T05:12:53.897Z"\n}', - proteinmpnn: - '{\n "name": "proteinmpnn",\n "displayName": "ProteinMPNN",\n "shortDescription": "Predicts amino acid sequences from 3D structure of proteins.",\n "namespace": "nim/ipd",\n "tags": [\n "1",\n "1.0",\n "1.0.0"\n ],\n "latestTag": "1",\n "updatedDate": "2024-08-27T01:52:07.955Z"\n}', - }, - namespace: 'opendatahub', - name: 'nvidia-nim-images-data', - }), - ); projectDetails.visitSection('test-project', 'model-server'); - //projectDetails.findNimModelDeployButton().click(); + // For multiple cards use case + // findNimModelDeployButton().click(); + cy.findByTestId('deploy-button').should('exist'); cy.findByTestId('deploy-button').click(); - //cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); // test that you can not submit on empty nimDeployModal.shouldBeOpen(); @@ -305,52 +189,55 @@ describe('Model Serving NIM', () => { // test filling in minimum required fields nimDeployModal.findModelNameInput().type('Test Name'); - nimDeployModal.findNIMToDeploy().findSelectOption('meta-llama-2-13b-chat - latest').click(); + nimDeployModal + .findNIMToDeploy() + .findSelectOption('Snowflake Arctic Embed Large Embedding - 1.0.0') + .click(); nimDeployModal.findSubmitButton().should('be.enabled'); - //nimDeployModal.findSubmitButton().click(); + nimDeployModal.findSubmitButton().click(); //dry run request - // cy.wait('@createInferenceService').then((interception) => { - // expect(interception.request.url).to.include('?dryRun=All'); - // expect(interception.request.body).to.eql({ - // apiVersion: 'serving.kserve.io/v1beta1', - // kind: 'InferenceService', - // metadata: { - // name: 'test-name', - // namespace: 'test-project', - // labels: { 'opendatahub.io/dashboard': 'true' }, - // annotations: { - // 'openshift.io/display-name': 'Test Name', - // }, - // }, - // spec: { - // predictor: { - // model: { - // modelFormat: { name: 'llama-2-13b-chat' }, - // runtime: 'test-name', - // storage: { key: 'test-secret', path: 'test-model/' }, - // }, - // resources: { - // limits: { cpu: '2', memory: '8Gi' }, - // requests: { cpu: '1', memory: '4Gi' }, - // }, - // }, - // }, - // }); - // }); - // - // // Actual request - // cy.wait('@createInferenceService').then((interception) => { - // expect(interception.request.url).not.to.include('?dryRun=All'); - // }); - // - // cy.get('@createInferenceService.all').then((interceptions) => { - // expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request - // }); + cy.wait('@createInferenceService').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body).to.eql({ + apiVersion: 'serving.kserve.io/v1beta1', + kind: 'InferenceService', + metadata: { + name: 'test-name', + namespace: 'test-project', + labels: { 'opendatahub.io/dashboard': 'true' }, + annotations: { + 'openshift.io/display-name': 'Test Name', + }, + }, + spec: { + predictor: { + model: { + modelFormat: { name: 'llama-2-13b-chat' }, + runtime: 'test-name', + storage: { key: 'test-secret', path: 'test-model/' }, + }, + resources: { + limits: { cpu: '2', memory: '8Gi' }, + requests: { cpu: '1', memory: '4Gi' }, + }, + }, + }, + }); + }); + + // Actual request + cy.wait('@createInferenceService').then((interception) => { + expect(interception.request.url).not.to.include('?dryRun=All'); + }); + + cy.get('@createInferenceService.all').then((interceptions) => { + expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request + }); }); -//TODO: This work is done by Lokesh + //TODO: This work is done by Lokesh it('Check if the Nim Model UI is enabled', () => { // initIntercepts({ // templates: true, @@ -359,13 +246,12 @@ describe('Model Serving NIM', () => { // disableNIMModelServing: false, // }); - initIntercepts(); + initInterceptsToEnableNim(); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); - - projectDetails.findServingPlatformLabel().should('exist'); + findServingPlatformLabel().should('exist'); cy.findByTestId('empty-state-title').should('exist'); cy.findByTestId('deploy-button').should('exist'); @@ -376,6 +262,4 @@ describe('Model Serving NIM', () => { // .findNimModelServingPlatformCard() // .contains('NVIDIA NIM model serving platform'); }); - - }); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts new file mode 100644 index 0000000000..bd13913b1a --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -0,0 +1,11 @@ +export function findNimModelDeployButton(): Cypress.Chainable { + return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); +} + +export function findNimModelServingPlatformCard(): Cypress.Chainable { + return cy.findByTestId('nvidia-nim-model-serving-platform-card'); +} + +export function findServingPlatformLabel(): Cypress.Chainable { + return cy.findByTestId('serving-platform-label'); +} From 2c7152d572e3341b7b5cacf2f8e75f309751576b Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Thu, 3 Oct 2024 01:28:33 -0400 Subject: [PATCH 05/33] Checking in the cypress test covers to validate all the scenarios to render nim and all other cards on overview and model tab when there are no ServingRuntimes configured. --- .../mocked/modelServing/modelServingNim.cy.ts | 141 +++++++++++++++--- 1 file changed, 123 insertions(+), 18 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 12c474e929..49c0bf419e 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -35,7 +35,39 @@ import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; -import { findServingPlatformLabel } from '~/__tests__/cypress/cypress/utils/nimUtils'; +import { + findNimModelDeployButton, + findNimModelServingPlatformCard, +} from '~/__tests__/cypress/cypress/utils/nimUtils'; + +const constructInterceptorsWithoutModelSelection = () => { + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }), + ); + + 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'; + } + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + + cy.interceptK8sList( + ProjectModel, + mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), + ); +}; const initInterceptsToEnableNim = () => { // not all interceptions here are required for the test to succeed @@ -237,29 +269,102 @@ describe('Model Serving NIM', () => { }); }); - //TODO: This work is done by Lokesh - it('Check if the Nim Model UI is enabled', () => { - // initIntercepts({ - // templates: true, - // disableKServeConfig: false, - // disableModelConfig: false, - // disableNIMModelServing: false, - // }); + it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { + initInterceptsToEnableNim(); + const componentName = 'overview'; + projectDetails.visitSection('test-project', componentName); + const overviewComponent = projectDetails.findComponent(componentName); + overviewComponent.should('exist'); + const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); + deployModelButton.should('exist'); + validateNvidiaNimModel(deployModelButton); + }); + it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { initInterceptsToEnableNim(); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('exist'); + + cy.contains('Start by adding a model server'); + cy.contains( + 'Model servers are used to deploy models and to allow apps to send requests to your models. Configuring a model server includes specifying the number of replicas being deployed, the server size, the token authentication, the serving runtime, and how the project that the model server belongs to is accessed.', + ); + + const deployButton = projectDetails.findComponent('model-server').findByTestId('deploy-button'); + validateNvidiaNimModel(deployButton); + }); + + it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { + constructInterceptorsWithoutModelSelection(); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); - findServingPlatformLabel().should('exist'); - cy.findByTestId('empty-state-title').should('exist'); - cy.findByTestId('deploy-button').should('exist'); + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard().contains( + 'Models are deployed using NVIDIA NIM microservices.', + ); + findNimModelServingPlatformCard().contains('NVIDIA NIM model serving platform'); + + validateNvidiaNimModel(findNimModelDeployButton()); + }); - // projectDetails - // .findNimModelServingPlatformCard() - // .contains('Models are deployed using NVIDIA NIM microservices.'); - // projectDetails - // .findNimModelServingPlatformCard() - // .contains('NVIDIA NIM model serving platform'); + it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { + constructInterceptorsWithoutModelSelection(); + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .findByTestId('nvidia-nim-platform-card') + .contains('NVIDIA NIM model serving platform'); + projectDetails + .findComponent('overview') + .findByTestId('nvidia-nim-platform-card') + .contains('Models are deployed using NVIDIA NIM microservices.'); + + validateNvidiaNimModel( + projectDetails + .findComponent('overview') + .findByTestId('nvidia-nim-platform-card') + .findByTestId('model-serving-platform-button'), + ); }); }); + +//TODO: move below methods to some test util file. +function validateNvidiaNimModel(deployButtonElement) { + deployButtonElement.click(); + cy.contains('Deploy model with NVIDIA NIM'); + cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); + + //find the form label Project with value as the Test Project + cy.contains('label', 'Project').parent().next().find('p').should('have.text', 'Test Project'); + + //close the model window + cy.get('div[role="dialog"]').get('button[aria-label="Close"]').click(); + + // now the nvidia nim window should not be visible. + cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); + + deployButtonElement.click(); + //validate model submit button is disabled without entering form data + cy.findByTestId('modal-submit-button').should('be.disabled'); + //validate nim modal cancel button + cy.findByTestId('modal-cancel-button').click(); + cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); +} From 2424ab3eb12e9c6725ab07def11f02f8b57a32e7 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 3 Oct 2024 09:25:32 +0200 Subject: [PATCH 06/33] Added mockNimServingRuntimeTemplate mock function Signed-off-by: Daniele Martinoli --- frontend/src/__mocks__/mockNimResource.ts | 19 ++++++++++++++- .../mocked/modelServing/modelServingNim.cy.ts | 24 +++---------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index 1809c27c60..ee9967ed83 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -1,7 +1,9 @@ -import { ConfigMapKind, InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; +import { ConfigMapKind, InferenceServiceKind, ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; +import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import { mockConfigMap } from './mockConfigMap'; import { mockServingRuntimeK8sResource } from './mockServingRuntimeK8sResource'; import { mockInferenceServiceK8sResource } from './mockInferenceServiceK8sResource'; +import { mockServingRuntimeTemplateK8sResource } from './mockServingRuntimeTemplateK8sResource'; export const mockNimImages = (): ConfigMapKind => mockConfigMap({ @@ -58,3 +60,18 @@ export const mockNimServingRuntime = (): ServingRuntimeKind => { 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; +}; diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 49c0bf419e..0efd16da3a 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -5,6 +5,7 @@ import { mockNimImages, mockNimInferenceService, mockNimServingRuntime, + mockNimServingRuntimeTemplate, } from '~/__mocks__/mockNimResource'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; @@ -50,16 +51,7 @@ const constructInterceptorsWithoutModelSelection = () => { }), ); - 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'; - } + const templateMock = mockNimServingRuntimeTemplate(); cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); cy.interceptK8s(TemplateModel, templateMock); @@ -123,17 +115,7 @@ const initInterceptsToEnableNim = () => { cy.interceptK8sList(RoleBindingModel, mockK8sResourceList([mockRoleBindingK8sResource({})])); - 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'; - } - + const templateMock = mockNimServingRuntimeTemplate(); cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); cy.interceptK8s(TemplateModel, templateMock); From 8175aa675dccb6584bc2b6dc780a01c6827901a8 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 3 Oct 2024 15:49:27 +0200 Subject: [PATCH 07/33] mocking deploy respurces (WIP) Signed-off-by: Daniele Martinoli --- frontend/src/__mocks__/mockNimResource.ts | 54 ++++++++- .../mocked/modelServing/modelServingNim.cy.ts | 112 +++++++++++------- .../modelServing/screens/projects/nimUtils.ts | 3 +- 3 files changed, 120 insertions(+), 49 deletions(-) diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index ee9967ed83..1757d9b44d 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -1,9 +1,11 @@ -import { ConfigMapKind, InferenceServiceKind, ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; +import { ConfigMapKind, InferenceServiceKind, PersistentVolumeClaimKind, SecretKind, ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/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({ @@ -41,10 +43,26 @@ export const mockNimImages = (): ConfigMapKind => export const mockNimInferenceService = (): InferenceServiceKind => { const inferenceService = mockInferenceServiceK8sResource({ name: 'test-name', - modelName: 'alphafold2', + modelName: 'test-name', displayName: 'Test Name', - isModelMesh: true, + 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; }; @@ -75,3 +93,33 @@ export const mockNimServingRuntimeTemplate = (): TemplateKind => { return templateMock; }; + +export const mockNvidiaNimAccessSecret = (): SecretKind => { + let secret = mockSecretK8sResource({ + name: 'nvidia-nim-access', + }) + delete secret.data; + secret.data = secret.data || {}; + secret.data["api_key"] = "api-key"; + secret.data["configMapName"] = "bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA==" + + return secret; +} + +export const mockNvidiaNimImagePullSecret = (): SecretKind => { + let secret = mockSecretK8sResource({ + name: 'nvidia-nim-image-pull', + }) + delete secret.data; + secret.data = secret.data || {}; + secret.data[".dockerconfigjson"] = "ZG9ja2VyY29uZmlnCg=="; + + return secret; +} + +export const mockNimModelPVC = (): PersistentVolumeClaimKind => { + let pvc = mockPVCK8sResource({ + name: 'nim-pvc', + }) + return pvc; +} \ No newline at end of file diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 0efd16da3a..a373cc1383 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -4,12 +4,14 @@ import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import { mockNimImages, mockNimInferenceService, + mockNimModelPVC, mockNimServingRuntime, mockNimServingRuntimeTemplate, + mockNvidiaNimAccessSecret, + mockNvidiaNimImagePullSecret, } from '~/__mocks__/mockNimResource'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; -import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { AcceleratorProfileModel, ConfigMapModel, @@ -25,7 +27,6 @@ import { StorageClassModel, TemplateModel, } from '~/__tests__/cypress/cypress/utils/models'; -import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; @@ -40,6 +41,7 @@ import { findNimModelDeployButton, findNimModelServingPlatformCard, } from '~/__tests__/cypress/cypress/utils/nimUtils'; +import type { InferenceServiceKind } from '~/k8sTypes'; const constructInterceptorsWithoutModelSelection = () => { cy.interceptOdh( @@ -61,14 +63,22 @@ const constructInterceptorsWithoutModelSelection = () => { ); }; -const initInterceptsToEnableNim = () => { +type EnableNimConfigType = { + hasAllModels?: boolean; +}; + +const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType) => { // not all interceptions here are required for the test to succeed // some are here to eliminate (not-blocking) error responses to ease with debugging cy.interceptOdh( 'GET /api/dsc/status', mockDscStatus({ - installedComponents: { kserve: true, 'model-mesh': true }, + installedComponents: { + 'data-science-pipelines-operator': true, + kserve: true, + 'model-mesh': true, + }, }), ); @@ -93,7 +103,10 @@ const initInterceptsToEnableNim = () => { cy.interceptOdh('GET /api/segment-key', {}); - const project = mockProjectK8sResource({ hasAnnotations: true, enableModelMesh: false }); + const project = mockProjectK8sResource({ + hasAnnotations: true, + enableModelMesh: hasAllModels ? undefined : false, + }); if (project.metadata.annotations != null) { project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; } @@ -166,19 +179,33 @@ const initInterceptsToEnableNim = () => { // ); }; -const initInterceptsToDeployModel = () => { +const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) => { cy.interceptK8s(ConfigMapModel, mockNimImages()); cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); - cy.interceptK8s('POST', InferenceServiceModel, mockNimInferenceService()).as( - 'createInferenceService', - ); + cy.interceptK8s('POST', InferenceServiceModel, nimInferenceService).as('createInferenceService'); cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); + + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, + { + response: { + status: 200, + body: mockNvidiaNimAccessSecret(), + } + }); + cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', + { + response: { + status: 200, + body: mockNvidiaNimImagePullSecret(), + }}); + cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); }; describe('Model Serving NIM', () => { it('should do something', () => { - initInterceptsToEnableNim(); + initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); // modelServingSection // .getServingPlatformCard('nvidia-nim-platform-card') @@ -186,13 +213,27 @@ describe('Model Serving NIM', () => { // .click(); }); - it('Deploy NIM model', () => { - initInterceptsToEnableNim(); - initInterceptsToDeployModel(); + it('Deploy NIM model when all model cards are available', () => { + initInterceptsToEnableNim({ hasAllModels: true }); projectDetails.visitSection('test-project', 'model-server'); // For multiple cards use case - // findNimModelDeployButton().click(); + findNimModelDeployButton().click(); + cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + + // test that you can not submit on empty + nimDeployModal.shouldBeOpen(); + nimDeployModal.findSubmitButton().should('be.disabled'); + + // Actual dialog tests in the next test case + }); + + it('Deploy NIM model when no cards are available', () => { + initInterceptsToEnableNim({}); + const nimInferenceService = mockNimInferenceService(); + initInterceptsToDeployModel(nimInferenceService); + + projectDetails.visitSection('test-project', 'model-server'); cy.findByTestId('deploy-button').should('exist'); cy.findByTestId('deploy-button').click(); cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); @@ -212,33 +253,12 @@ describe('Model Serving NIM', () => { nimDeployModal.findSubmitButton().click(); //dry run request + if (nimInferenceService.status) { + delete nimInferenceService.status; + } cy.wait('@createInferenceService').then((interception) => { expect(interception.request.url).to.include('?dryRun=All'); - expect(interception.request.body).to.eql({ - apiVersion: 'serving.kserve.io/v1beta1', - kind: 'InferenceService', - metadata: { - name: 'test-name', - namespace: 'test-project', - labels: { 'opendatahub.io/dashboard': 'true' }, - annotations: { - 'openshift.io/display-name': 'Test Name', - }, - }, - spec: { - predictor: { - model: { - modelFormat: { name: 'llama-2-13b-chat' }, - runtime: 'test-name', - storage: { key: 'test-secret', path: 'test-model/' }, - }, - resources: { - limits: { cpu: '2', memory: '8Gi' }, - requests: { cpu: '1', memory: '4Gi' }, - }, - }, - }, - }); + expect(interception.request.body).to.eql(nimInferenceService); }); // Actual request @@ -249,10 +269,12 @@ describe('Model Serving NIM', () => { cy.get('@createInferenceService.all').then((interceptions) => { expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request }); + + // nimDeployModal.shouldBeOpen(false); }); - it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { - initInterceptsToEnableNim(); + it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { + initInterceptsToEnableNim({}); const componentName = 'overview'; projectDetails.visitSection('test-project', componentName); const overviewComponent = projectDetails.findComponent(componentName); @@ -262,8 +284,8 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(deployModelButton); }); - it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { - initInterceptsToEnableNim(); + it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { + initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); projectDetails.findServingPlatformLabel().should('exist'); @@ -277,7 +299,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(deployButton); }); - it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { + it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { constructInterceptorsWithoutModelSelection(); projectDetails.visitSection('test-project', 'model-server'); @@ -295,7 +317,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(findNimModelDeployButton()); }); - it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { + it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { constructInterceptorsWithoutModelSelection(); projectDetails.visitSection('test-project', 'overview'); diff --git a/frontend/src/pages/modelServing/screens/projects/nimUtils.ts b/frontend/src/pages/modelServing/screens/projects/nimUtils.ts index a22869194e..3af7d7ddc3 100644 --- a/frontend/src/pages/modelServing/screens/projects/nimUtils.ts +++ b/frontend/src/pages/modelServing/screens/projects/nimUtils.ts @@ -24,7 +24,8 @@ export const getNIMResource = async (resourceName: string): Promise const resourceData = await response.json(); return resourceData.body; } catch (error) { - throw new Error(`Failed to fetch the resource: ${resourceName}.`); + console.log('ERROR ', error); + throw new Error(`Failed to fetch the resource: ${resourceName}. ` + error); } }; export const getNIMData = async (isNGC: boolean): Promise | undefined> => { From 1d80e394a47c221c5117f939cc27a372886eac94 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Thu, 3 Oct 2024 18:22:04 +0200 Subject: [PATCH 08/33] Completed deployment tests Signed-off-by: Daniele Martinoli --- frontend/src/__mocks__/mockNimResource.ts | 33 +++++++++++------- .../mocked/modelServing/modelServingNim.cy.ts | 34 +++++++++---------- .../modelServing/screens/projects/nimUtils.ts | 3 +- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index 1757d9b44d..e1265f87bd 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -1,4 +1,11 @@ -import { ConfigMapKind, InferenceServiceKind, PersistentVolumeClaimKind, SecretKind, ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; +import { + ConfigMapKind, + InferenceServiceKind, + PersistentVolumeClaimKind, + SecretKind, + ServingRuntimeKind, + TemplateKind, +} from '~/k8sTypes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import { mockConfigMap } from './mockConfigMap'; import { mockServingRuntimeK8sResource } from './mockServingRuntimeK8sResource'; @@ -95,31 +102,31 @@ export const mockNimServingRuntimeTemplate = (): TemplateKind => { }; export const mockNvidiaNimAccessSecret = (): SecretKind => { - let secret = mockSecretK8sResource({ + const secret = mockSecretK8sResource({ name: 'nvidia-nim-access', - }) + }); delete secret.data; secret.data = secret.data || {}; - secret.data["api_key"] = "api-key"; - secret.data["configMapName"] = "bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA==" + secret.data.api_key = 'api-key'; + secret.data.configMapName = 'bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA=='; return secret; -} +}; export const mockNvidiaNimImagePullSecret = (): SecretKind => { - let secret = mockSecretK8sResource({ + const secret = mockSecretK8sResource({ name: 'nvidia-nim-image-pull', - }) + }); delete secret.data; secret.data = secret.data || {}; - secret.data[".dockerconfigjson"] = "ZG9ja2VyY29uZmlnCg=="; + secret.data['.dockerconfigjson'] = 'ZG9ja2VyY29uZmlnCg=='; return secret; -} +}; export const mockNimModelPVC = (): PersistentVolumeClaimKind => { - let pvc = mockPVCK8sResource({ + const pvc = mockPVCK8sResource({ name: 'nim-pvc', - }) + }); return pvc; -} \ No newline at end of file +}; diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index a373cc1383..eb46b28130 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -186,20 +186,20 @@ const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); + // NOTES: `body` field is needed! cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, - { - response: { - status: 200, - body: mockNvidiaNimAccessSecret(), - } - }); - cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, { - response: { - status: 200, - body: mockNvidiaNimImagePullSecret(), - }}); + body: { body: mockNimImages() }, + }, + ); + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, + { body: { body: mockNvidiaNimAccessSecret() } }, + ); + cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { + body: { body: mockNvidiaNimImagePullSecret() }, + }); cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); }; @@ -270,10 +270,10 @@ describe('Model Serving NIM', () => { expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request }); - // nimDeployModal.shouldBeOpen(false); + nimDeployModal.shouldBeOpen(false); }); - it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { + it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { initInterceptsToEnableNim({}); const componentName = 'overview'; projectDetails.visitSection('test-project', componentName); @@ -284,7 +284,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(deployModelButton); }); - it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { + it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); @@ -299,7 +299,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(deployButton); }); - it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { + it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { constructInterceptorsWithoutModelSelection(); projectDetails.visitSection('test-project', 'model-server'); @@ -317,7 +317,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(findNimModelDeployButton()); }); - it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { + it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { constructInterceptorsWithoutModelSelection(); projectDetails.visitSection('test-project', 'overview'); diff --git a/frontend/src/pages/modelServing/screens/projects/nimUtils.ts b/frontend/src/pages/modelServing/screens/projects/nimUtils.ts index 3af7d7ddc3..a22869194e 100644 --- a/frontend/src/pages/modelServing/screens/projects/nimUtils.ts +++ b/frontend/src/pages/modelServing/screens/projects/nimUtils.ts @@ -24,8 +24,7 @@ export const getNIMResource = async (resourceName: string): Promise const resourceData = await response.json(); return resourceData.body; } catch (error) { - console.log('ERROR ', error); - throw new Error(`Failed to fetch the resource: ${resourceName}. ` + error); + throw new Error(`Failed to fetch the resource: ${resourceName}.`); } }; export const getNIMData = async (isNGC: boolean): Promise | undefined> => { From b662f9f6eccfd0f5d712c207b0421ca72c803eea Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:47:08 -0400 Subject: [PATCH 09/33] Added new test cases to validate if the nim is not enabled * when combination of configurations to enable nim models are not configured properly. --- .../mocked/modelServing/modelServingNim.cy.ts | 196 ++++++++++++------ 1 file changed, 129 insertions(+), 67 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index eb46b28130..e088eac591 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -43,19 +43,18 @@ import { } from '~/__tests__/cypress/cypress/utils/nimUtils'; import type { InferenceServiceKind } from '~/k8sTypes'; -const constructInterceptorsWithoutModelSelection = () => { - cy.interceptOdh( - 'GET /api/config', - mockDashboardConfig({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }), - ); - - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); +// this will intercept all the APIs to create a new project without selecting the model runtime from available models run times. +const initInterceptorsForNewProjectWithoutModelSelection = ( + dashboardConfig: MockDashboardConfigType, + disableServingRuntime = false, +) => { + cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); + + if (!disableServingRuntime) { + const templateMock = mockNimServingRuntimeTemplate(); + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + } cy.interceptK8sList( ProjectModel, @@ -139,44 +138,12 @@ const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), ); - // TODO not required but eliminates not-blocking error response - // cy.interceptK8sList( - // ServingRuntimeModel, - // mockK8sResourceList([ - // mockServingRuntimeK8sResource({ - // name: 'nvidia-nim-runtime', - // disableModelMeshAnnotations: true, - // disableResources: true, - // acceleratorName: 'nvidia.com/gpu', - // displayName: 'NVIDIA NIM', - // }), - // ]), - // ); - - // TODO not required but eliminates not-blocking error response - // cy.interceptK8sList( - // InferenceServiceModel, - // mockK8sResourceList([mockInferenceServiceK8sResource({})]) - // ); - cy.interceptK8s(RouteModel, mockRouteK8sResource({})); cy.interceptOdh('GET /api/accelerators', { configured: true, available: { 'nvidia.com/gpu': 1 }, }); - - // TODO do we need to mock this? - // cy.interceptK8s( - // ConfigMapModel, - // mockConfigMap({ - // data: { - // validation_result: 'true', - // }, - // namespace: 'opendatahub', - // name: 'nvidia-nim-validation-result', - // }), - // ); }; const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) => { @@ -204,15 +171,6 @@ const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) }; describe('Model Serving NIM', () => { - it('should do something', () => { - initInterceptsToEnableNim({}); - projectDetails.visitSection('test-project', 'model-server'); - // modelServingSection - // .getServingPlatformCard('nvidia-nim-platform-card') - // .findDeployModelButton() - // .click(); - }); - it('Deploy NIM model when all model cards are available', () => { initInterceptsToEnableNim({ hasAllModels: true }); @@ -273,7 +231,7 @@ describe('Model Serving NIM', () => { nimDeployModal.shouldBeOpen(false); }); - it('Check if the Nim model UI enabled on Overview tab when model server platform for the project is nim', () => { + it('Check if the Nim model UI enabled on Overview tab when all the configuration enabled to display nim', () => { initInterceptsToEnableNim({}); const componentName = 'overview'; projectDetails.visitSection('test-project', componentName); @@ -284,7 +242,7 @@ describe('Model Serving NIM', () => { validateNvidiaNimModel(deployModelButton); }); - it('Check if the Nim model UI enabled on models tab when model server platform for the project is nim', () => { + it('Check if the Nim model UI enabled on models tab when all the configuration enabled to display nim', () => { initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); @@ -300,7 +258,11 @@ describe('Model Serving NIM', () => { }); it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { - constructInterceptorsWithoutModelSelection(); + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }); projectDetails.visitSection('test-project', 'model-server'); projectDetails.shouldBeEmptyState('Models', 'model-server', true); @@ -309,16 +271,19 @@ describe('Model Serving NIM', () => { projectDetails.findSingleModelDeployButton().should('exist'); projectDetails.findMultiModelButton().should('exist'); - findNimModelServingPlatformCard().contains( - 'Models are deployed using NVIDIA NIM microservices.', - ); - findNimModelServingPlatformCard().contains('NVIDIA NIM model serving platform'); + findNimModelServingPlatformCard() + .should('contain', 'Models are deployed using NVIDIA NIM microservices.') + .and('contain', 'NVIDIA NIM model serving platform'); validateNvidiaNimModel(findNimModelDeployButton()); }); it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { - constructInterceptorsWithoutModelSelection(); + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }); projectDetails.visitSection('test-project', 'overview'); projectDetails @@ -335,11 +300,8 @@ describe('Model Serving NIM', () => { projectDetails .findComponent('overview') .findByTestId('nvidia-nim-platform-card') - .contains('NVIDIA NIM model serving platform'); - projectDetails - .findComponent('overview') - .findByTestId('nvidia-nim-platform-card') - .contains('Models are deployed using NVIDIA NIM microservices.'); + .should('contain', 'NVIDIA NIM model serving platform') + .and('contain', 'Models are deployed using NVIDIA NIM microservices.'); validateNvidiaNimModel( projectDetails @@ -348,6 +310,106 @@ describe('Model Serving NIM', () => { .findByTestId('model-serving-platform-button'), ); }); + + it('Check if the Nim model UI disabled on overview tab when dashboard config disableNIMModelServing is true', () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: true, + }); + + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .find('[data-testid="nvidia-nim-platform-card"]') + .should('not.exist'); + + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + + it('Check if the Nim model UI disabled on models tab when dashboard config disableNIMModelServing is true', () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: true, + }); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); + + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard().should('not.exist'); + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + + it('Check if the Nim model UI disabled on overview tab when no service runtime not configured', () => { + initInterceptorsForNewProjectWithoutModelSelection( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .find('[data-testid="nvidia-nim-platform-card"]') + .should('not.exist'); + + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + + it('Check if the Nim model UI disabled on models tab when no service runtime not configured', () => { + initInterceptorsForNewProjectWithoutModelSelection( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); + + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard().should('not.exist'); + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); }); //TODO: move below methods to some test util file. From 47306a145865c542ab1c544f8bb3f94e3d3d5808 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Thu, 3 Oct 2024 16:57:08 -0400 Subject: [PATCH 10/33] test: added test cases for deleting model from the project models tab Signed-off-by: Tomer Figenblat --- .../mocked/modelServing/modelServingNim.cy.ts | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index e088eac591..8f3dc77aaa 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -30,7 +30,7 @@ import { import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; +import { mock200Status, mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; @@ -170,6 +170,34 @@ const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); }; +const initInterceptsForDeleteModel = () => { + // create initial inference and runtime + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + // intercept delete inference request + cy.interceptK8s( + 'DELETE', + { + model: InferenceServiceModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteInference'); + + // intercept delete runtime request + cy.interceptK8s( + 'DELETE', + { + model: ServingRuntimeModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteRuntime'); +}; + describe('Model Serving NIM', () => { it('Deploy NIM model when all model cards are available', () => { initInterceptsToEnableNim({ hasAllModels: true }); @@ -410,6 +438,39 @@ describe('Model Serving NIM', () => { cy.contains('NVIDIA NIM model serving platform').should('not.exist'); cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); }); + + describe('Delete existing model', () => { + it("should only allow deletion the project's models tab", () => { + initInterceptsToEnableNim({}); + initInterceptsForDeleteModel(); + + // go the Models tab in the created project + projectDetails.visitSection('test-project', 'model-server'); + // grab the deployed models table and click the kebab menu + cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); + cy.get('ul[role="menu"]').should('have.length', 1); + }); + + it("should be able to delete from project's models tab", () => { + initInterceptsToEnableNim({}); + initInterceptsForDeleteModel(); + + // go the Models tab in the created project + projectDetails.visitSection('test-project', 'model-server'); + // grab the deployed models table and click the kebab menu + cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); + // grab the delete menu and click it + cy.get('button').contains('Delete').click(); + // grab the delete menu window and put in the project name + cy.get('input[id="delete-modal-input"]').fill('Test Name'); + // grab the delete button and click it + cy.get('button').contains('Delete deployed model').click(); + + // verify the model was deleted + cy.wait('@deleteInference'); + cy.wait('@deleteRuntime'); + }); + }); }); //TODO: move below methods to some test util file. From 34ab9dfde5c32add5685098c2176de76d7818d0a Mon Sep 17 00:00:00 2001 From: lokeshrangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:38:07 -0400 Subject: [PATCH 11/33] * Added new test cases to validate the model serving menu item when there are deployments and no deployments. (#9) * Moved the utility method to nimUtils.ts --- .../mocked/modelServing/modelServingNim.cy.ts | 59 +++++++++++-------- .../cypress/cypress/utils/nimUtils.ts | 22 +++++++ 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 8f3dc77aaa..4e6b059dcf 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -30,7 +30,12 @@ import { import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { mock200Status, mockNotebookK8sResource, mockRouteK8sResource, mockStorageClasses } from '~/__mocks__'; +import { + mock200Status, + mockNotebookK8sResource, + mockRouteK8sResource, + mockStorageClasses, +} from '~/__mocks__'; import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; @@ -40,8 +45,13 @@ import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog import { findNimModelDeployButton, findNimModelServingPlatformCard, + validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import type { InferenceServiceKind } from '~/k8sTypes'; +import { + modelServingGlobal, + modelServingSection, +} from '~/__tests__/cypress/cypress/pages/modelServing'; // this will intercept all the APIs to create a new project without selecting the model runtime from available models run times. const initInterceptorsForNewProjectWithoutModelSelection = ( @@ -471,27 +481,30 @@ describe('Model Serving NIM', () => { cy.wait('@deleteRuntime'); }); }); -}); - -//TODO: move below methods to some test util file. -function validateNvidiaNimModel(deployButtonElement) { - deployButtonElement.click(); - cy.contains('Deploy model with NVIDIA NIM'); - cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); - - //find the form label Project with value as the Test Project - cy.contains('label', 'Project').parent().next().find('p').should('have.text', 'Test Project'); - - //close the model window - cy.get('div[role="dialog"]').get('button[aria-label="Close"]').click(); - // now the nvidia nim window should not be visible. - cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); + it('When the project model as nim and no deployments then model serving page should show No Deployed model with link to project', () => { + initInterceptsToEnableNim({}); + modelServingGlobal.visit('test-project'); + modelServingGlobal.shouldBeEmpty(); + modelServingGlobal.findGoToProjectButton().click(); + }); - deployButtonElement.click(); - //validate model submit button is disabled without entering form data - cy.findByTestId('modal-submit-button').should('be.disabled'); - //validate nim modal cancel button - cy.findByTestId('modal-cancel-button').click(); - cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); -} + it('When the project model is Nim then model serving page should show deployments of the project', () => { + initInterceptsToEnableNim({}); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + modelServingGlobal.visit('test-project'); + modelServingGlobal.getModelRow('Test Name').get('button[aria-label="Kebab toggle"]').click(); + + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .should('have.length', 1); + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .contains('Delete') + .should('exist'); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index bd13913b1a..af22a92a0e 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -9,3 +9,25 @@ export function findNimModelServingPlatformCard(): Cypress.Chainable { export function findServingPlatformLabel(): Cypress.Chainable { return cy.findByTestId('serving-platform-label'); } + +export function validateNvidiaNimModel(deployButtonElement): void { + deployButtonElement.click(); + cy.contains('Deploy model with NVIDIA NIM'); + cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); + + //find the form label Project with value as the Test Project + cy.contains('label', 'Project').parent().next().find('p').should('have.text', 'Test Project'); + + //close the model window + cy.get('div[role="dialog"]').get('button[aria-label="Close"]').click(); + + // now the nvidia nim window should not be visible. + cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); + + deployButtonElement.click(); + //validate model submit button is disabled without entering form data + cy.findByTestId('modal-submit-button').should('be.disabled'); + //validate nim modal cancel button + cy.findByTestId('modal-cancel-button').click(); + cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); +} From e57074a0f436035656838150018f96046c9ef335 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Thu, 3 Oct 2024 20:46:14 -0400 Subject: [PATCH 12/33] test: cleanups Signed-off-by: Tomer Figenblat --- frontend/src/__mocks__/mockDashboardConfig.ts | 2 +- frontend/src/__mocks__/mockNimResource.ts | 6 +- .../mocked/modelServing/modelServingNim.cy.ts | 510 ------------------ .../mocked/projects/modelServingNim.cy.ts | 467 ++++++++++++++++ .../cypress/cypress/utils/nimUtils.ts | 4 +- 5 files changed, 474 insertions(+), 515 deletions(-) delete mode 100644 frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts create mode 100644 frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index 5096817893..63788271ad 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -1,7 +1,7 @@ import { DashboardConfigKind, KnownLabels } from '~/k8sTypes'; import { NotebookSize } from '~/types'; -type MockDashboardConfigType = { +export type MockDashboardConfigType = { disableInfo?: boolean; disableSupport?: boolean; disableClusterManager?: boolean; diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index e1265f87bd..92d3163f5c 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -106,8 +106,8 @@ export const mockNvidiaNimAccessSecret = (): SecretKind => { name: 'nvidia-nim-access', }); delete secret.data; - secret.data = secret.data || {}; - secret.data.api_key = 'api-key'; + secret.data = {}; + secret.data.api_key = 'api-key'; // eslint-disable-line camelcase secret.data.configMapName = 'bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA=='; return secret; @@ -118,7 +118,7 @@ export const mockNvidiaNimImagePullSecret = (): SecretKind => { name: 'nvidia-nim-image-pull', }); delete secret.data; - secret.data = secret.data || {}; + secret.data = {}; secret.data['.dockerconfigjson'] = 'ZG9ja2VyY29uZmlnCg=='; return secret; diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts deleted file mode 100644 index 4e6b059dcf..0000000000 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ /dev/null @@ -1,510 +0,0 @@ -import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; -import { mockDscStatus } from '~/__mocks__/mockDscStatus'; -import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; -import { - mockNimImages, - mockNimInferenceService, - mockNimModelPVC, - mockNimServingRuntime, - mockNimServingRuntimeTemplate, - mockNvidiaNimAccessSecret, - mockNvidiaNimImagePullSecret, -} from '~/__mocks__/mockNimResource'; -import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; -import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; -import { - AcceleratorProfileModel, - ConfigMapModel, - InferenceServiceModel, - NotebookModel, - PodModel, - ProjectModel, - PVCModel, - RoleBindingModel, - RouteModel, - SecretModel, - ServingRuntimeModel, - StorageClassModel, - TemplateModel, -} from '~/__tests__/cypress/cypress/utils/models'; -import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; -import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; -import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { - mock200Status, - mockNotebookK8sResource, - mockRouteK8sResource, - mockStorageClasses, -} from '~/__mocks__'; -import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; -import { mockConsoleLinks } from '~/__mocks__/mockConsoleLinks'; -import { mockQuickStarts } from '~/__mocks__/mockQuickStarts'; -import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; -import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; -import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; -import { - findNimModelDeployButton, - findNimModelServingPlatformCard, - validateNvidiaNimModel, -} from '~/__tests__/cypress/cypress/utils/nimUtils'; -import type { InferenceServiceKind } from '~/k8sTypes'; -import { - modelServingGlobal, - modelServingSection, -} from '~/__tests__/cypress/cypress/pages/modelServing'; - -// this will intercept all the APIs to create a new project without selecting the model runtime from available models run times. -const initInterceptorsForNewProjectWithoutModelSelection = ( - dashboardConfig: MockDashboardConfigType, - disableServingRuntime = false, -) => { - cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); - - if (!disableServingRuntime) { - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); - } - - cy.interceptK8sList( - ProjectModel, - mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), - ); -}; - -type EnableNimConfigType = { - hasAllModels?: boolean; -}; - -const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType) => { - // not all interceptions here are required for the test to succeed - // some are here to eliminate (not-blocking) error responses to ease with debugging - - cy.interceptOdh( - 'GET /api/dsc/status', - mockDscStatus({ - installedComponents: { - 'data-science-pipelines-operator': true, - kserve: true, - 'model-mesh': true, - }, - }), - ); - - cy.interceptOdh('GET /api/dsci/status', mockDsciStatus({})); - - cy.interceptOdh('GET /api/builds', {}); - - cy.interceptOdh( - 'GET /api/config', - mockDashboardConfig({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }), - ); - - cy.interceptK8sList(StorageClassModel, mockK8sResourceList(mockStorageClasses)); - - cy.interceptOdh('GET /api/console-links', mockConsoleLinks()); - - cy.interceptOdh('GET /api/quickstarts', mockQuickStarts()); - - cy.interceptOdh('GET /api/segment-key', {}); - - const project = mockProjectK8sResource({ - hasAnnotations: true, - enableModelMesh: hasAllModels ? undefined : false, - }); - if (project.metadata.annotations != null) { - project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; - } - cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); - - cy.interceptK8sList( - NotebookModel, - mockK8sResourceList([mockNotebookK8sResource({ namespace: 'test-project' })]), - ); - - cy.interceptK8sList(PVCModel, mockK8sResourceList([mockPVCK8sResource({})])); - - cy.interceptK8sList(SecretModel, mockK8sResourceList([mockSecretK8sResource({})])); - - cy.interceptK8sList( - SecretModel, - mockK8sResourceList([mockSecretK8sResource({ namespace: 'test-project' })]), - ); - - cy.interceptK8sList(RoleBindingModel, mockK8sResourceList([mockRoleBindingK8sResource({})])); - - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); - - cy.interceptK8sList(PodModel, mockK8sResourceList([mockPodK8sResource({})])); - - cy.interceptK8sList( - AcceleratorProfileModel, - mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), - ); - - cy.interceptK8s(RouteModel, mockRouteK8sResource({})); - - cy.interceptOdh('GET /api/accelerators', { - configured: true, - available: { 'nvidia.com/gpu': 1 }, - }); -}; - -const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) => { - cy.interceptK8s(ConfigMapModel, mockNimImages()); - cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); - cy.interceptK8s('POST', InferenceServiceModel, nimInferenceService).as('createInferenceService'); - - cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); - - // NOTES: `body` field is needed! - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, - { - body: { body: mockNimImages() }, - }, - ); - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, - { body: { body: mockNvidiaNimAccessSecret() } }, - ); - cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { - body: { body: mockNvidiaNimImagePullSecret() }, - }); - cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); -}; - -const initInterceptsForDeleteModel = () => { - // create initial inference and runtime - cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); - cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - - // intercept delete inference request - cy.interceptK8s( - 'DELETE', - { - model: InferenceServiceModel, - ns: 'test-project', - name: 'test-name', - }, - mock200Status({}), - ).as('deleteInference'); - - // intercept delete runtime request - cy.interceptK8s( - 'DELETE', - { - model: ServingRuntimeModel, - ns: 'test-project', - name: 'test-name', - }, - mock200Status({}), - ).as('deleteRuntime'); -}; - -describe('Model Serving NIM', () => { - it('Deploy NIM model when all model cards are available', () => { - initInterceptsToEnableNim({ hasAllModels: true }); - - projectDetails.visitSection('test-project', 'model-server'); - // For multiple cards use case - findNimModelDeployButton().click(); - cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); - - // test that you can not submit on empty - nimDeployModal.shouldBeOpen(); - nimDeployModal.findSubmitButton().should('be.disabled'); - - // Actual dialog tests in the next test case - }); - - it('Deploy NIM model when no cards are available', () => { - initInterceptsToEnableNim({}); - const nimInferenceService = mockNimInferenceService(); - initInterceptsToDeployModel(nimInferenceService); - - projectDetails.visitSection('test-project', 'model-server'); - cy.findByTestId('deploy-button').should('exist'); - cy.findByTestId('deploy-button').click(); - cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); - - // test that you can not submit on empty - nimDeployModal.shouldBeOpen(); - nimDeployModal.findSubmitButton().should('be.disabled'); - - // test filling in minimum required fields - nimDeployModal.findModelNameInput().type('Test Name'); - nimDeployModal - .findNIMToDeploy() - .findSelectOption('Snowflake Arctic Embed Large Embedding - 1.0.0') - .click(); - nimDeployModal.findSubmitButton().should('be.enabled'); - - nimDeployModal.findSubmitButton().click(); - - //dry run request - if (nimInferenceService.status) { - delete nimInferenceService.status; - } - cy.wait('@createInferenceService').then((interception) => { - expect(interception.request.url).to.include('?dryRun=All'); - expect(interception.request.body).to.eql(nimInferenceService); - }); - - // Actual request - cy.wait('@createInferenceService').then((interception) => { - expect(interception.request.url).not.to.include('?dryRun=All'); - }); - - cy.get('@createInferenceService.all').then((interceptions) => { - expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request - }); - - nimDeployModal.shouldBeOpen(false); - }); - - it('Check if the Nim model UI enabled on Overview tab when all the configuration enabled to display nim', () => { - initInterceptsToEnableNim({}); - const componentName = 'overview'; - projectDetails.visitSection('test-project', componentName); - const overviewComponent = projectDetails.findComponent(componentName); - overviewComponent.should('exist'); - const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); - deployModelButton.should('exist'); - validateNvidiaNimModel(deployModelButton); - }); - - it('Check if the Nim model UI enabled on models tab when all the configuration enabled to display nim', () => { - initInterceptsToEnableNim({}); - projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('exist'); - - cy.contains('Start by adding a model server'); - cy.contains( - 'Model servers are used to deploy models and to allow apps to send requests to your models. Configuring a model server includes specifying the number of replicas being deployed, the server size, the token authentication, the serving runtime, and how the project that the model server belongs to is accessed.', - ); - - const deployButton = projectDetails.findComponent('model-server').findByTestId('deploy-button'); - validateNvidiaNimModel(deployButton); - }); - - it('Check if the Nim model UI enabled on models tab when model server platform for the project is not chosen', () => { - initInterceptorsForNewProjectWithoutModelSelection({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }); - - projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard() - .should('contain', 'Models are deployed using NVIDIA NIM microservices.') - .and('contain', 'NVIDIA NIM model serving platform'); - - validateNvidiaNimModel(findNimModelDeployButton()); - }); - - it('Check if the Nim model UI enabled on overview tab when model server platform for the project is not chosen', () => { - initInterceptorsForNewProjectWithoutModelSelection({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }); - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .findByTestId('nvidia-nim-platform-card') - .should('contain', 'NVIDIA NIM model serving platform') - .and('contain', 'Models are deployed using NVIDIA NIM microservices.'); - - validateNvidiaNimModel( - projectDetails - .findComponent('overview') - .findByTestId('nvidia-nim-platform-card') - .findByTestId('model-serving-platform-button'), - ); - }); - - it('Check if the Nim model UI disabled on overview tab when dashboard config disableNIMModelServing is true', () => { - initInterceptorsForNewProjectWithoutModelSelection({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: true, - }); - - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .find('[data-testid="nvidia-nim-platform-card"]') - .should('not.exist'); - - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); - }); - - it('Check if the Nim model UI disabled on models tab when dashboard config disableNIMModelServing is true', () => { - initInterceptorsForNewProjectWithoutModelSelection({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: true, - }); - projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard().should('not.exist'); - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); - }); - - it('Check if the Nim model UI disabled on overview tab when no service runtime not configured', () => { - initInterceptorsForNewProjectWithoutModelSelection( - { - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }, - true, - ); - - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .find('[data-testid="nvidia-nim-platform-card"]') - .should('not.exist'); - - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); - }); - - it('Check if the Nim model UI disabled on models tab when no service runtime not configured', () => { - initInterceptorsForNewProjectWithoutModelSelection( - { - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }, - true, - ); - projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard().should('not.exist'); - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); - }); - - describe('Delete existing model', () => { - it("should only allow deletion the project's models tab", () => { - initInterceptsToEnableNim({}); - initInterceptsForDeleteModel(); - - // go the Models tab in the created project - projectDetails.visitSection('test-project', 'model-server'); - // grab the deployed models table and click the kebab menu - cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); - cy.get('ul[role="menu"]').should('have.length', 1); - }); - - it("should be able to delete from project's models tab", () => { - initInterceptsToEnableNim({}); - initInterceptsForDeleteModel(); - - // go the Models tab in the created project - projectDetails.visitSection('test-project', 'model-server'); - // grab the deployed models table and click the kebab menu - cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); - // grab the delete menu and click it - cy.get('button').contains('Delete').click(); - // grab the delete menu window and put in the project name - cy.get('input[id="delete-modal-input"]').fill('Test Name'); - // grab the delete button and click it - cy.get('button').contains('Delete deployed model').click(); - - // verify the model was deleted - cy.wait('@deleteInference'); - cy.wait('@deleteRuntime'); - }); - }); - - it('When the project model as nim and no deployments then model serving page should show No Deployed model with link to project', () => { - initInterceptsToEnableNim({}); - modelServingGlobal.visit('test-project'); - modelServingGlobal.shouldBeEmpty(); - modelServingGlobal.findGoToProjectButton().click(); - }); - - it('When the project model is Nim then model serving page should show deployments of the project', () => { - initInterceptsToEnableNim({}); - cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); - cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - - modelServingGlobal.visit('test-project'); - modelServingGlobal.getModelRow('Test Name').get('button[aria-label="Kebab toggle"]').click(); - - modelServingGlobal - .getModelRow('Test Name') - .get('button[role="menuitem"]') - .should('have.length', 1); - modelServingGlobal - .getModelRow('Test Name') - .get('button[role="menuitem"]') - .contains('Delete') - .should('exist'); - }); -}); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts new file mode 100644 index 0000000000..a455ad366b --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -0,0 +1,467 @@ +import type { MockDashboardConfigType } from '~/__mocks__/mockDashboardConfig'; +import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { + mockNimImages, + mockNimInferenceService, + mockNimModelPVC, + mockNimServingRuntime, + mockNimServingRuntimeTemplate, + mockNvidiaNimAccessSecret, + mockNvidiaNimImagePullSecret, +} from '~/__mocks__/mockNimResource'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; +import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; +import { + AcceleratorProfileModel, + ConfigMapModel, + InferenceServiceModel, + ProjectModel, + PVCModel, + SecretModel, + ServingRuntimeModel, + TemplateModel, +} from '~/__tests__/cypress/cypress/utils/models'; +import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; +import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; +import { mock200Status } from '~/__mocks__'; +import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; +import { + findNimModelDeployButton, + findNimModelServingPlatformCard, + validateNvidiaNimModel, +} from '~/__tests__/cypress/cypress/utils/nimUtils'; +import type { InferenceServiceKind } from '~/k8sTypes'; +import { modelServingGlobal } from '~/__tests__/cypress/cypress/pages/modelServing'; +import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; + +type EnableNimConfigType = { + hasAllModels?: boolean; +}; +// intercept all APIs required for creating a new project without selecting the model runtime from available models run times. +const initInterceptorsForNewProjectWithoutModelSelection = ( + dashboardConfig: MockDashboardConfigType, + disableServingRuntime = false, +) => { + cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); + + if (!disableServingRuntime) { + const templateMock = mockNimServingRuntimeTemplate(); + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + } + + cy.interceptK8sList( + ProjectModel, + mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), + ); +}; + +// intercept all APIs required for enabling NIM +const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType) => { + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + kserve: true, + 'model-mesh': true, + }, + }), + ); + + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }), + ); + + const project = mockProjectK8sResource({ + hasAnnotations: true, + enableModelMesh: hasAllModels ? undefined : false, + }); + if (project.metadata.annotations != null) { + project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; + } + cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); + + const templateMock = mockNimServingRuntimeTemplate(); + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + + cy.interceptK8sList( + AcceleratorProfileModel, + mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), + ); + + cy.intercept('GET', '/api/accelerators', { + configured: true, + available: { 'nvidia.com/gpu': 1 }, + total: { 'nvidia.com/gpu': 1 }, + allocated: { 'nvidia.com/gpu': 1 }, + }); +}; + +// intercept all APIs required for deploying new models in existing projects +const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) => { + cy.interceptK8s(ConfigMapModel, mockNimImages()); + cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); + cy.interceptK8s('POST', InferenceServiceModel, nimInferenceService).as('createInferenceService'); + + cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); + + // NOTES: `body` field is needed! + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, + { + body: { body: mockNimImages() }, + }, + ); + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, + { body: { body: mockNvidiaNimAccessSecret() } }, + ); + cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { + body: { body: mockNvidiaNimImagePullSecret() }, + }); + cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); +}; + +// intercept all APIs required for deleting an existing model +const initInterceptsForDeleteModel = () => { + // create initial inference and runtime + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + // intercept delete inference request + cy.interceptK8s( + 'DELETE', + { + model: InferenceServiceModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteInference'); + + // intercept delete runtime request + cy.interceptK8s( + 'DELETE', + { + model: ServingRuntimeModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteRuntime'); +}; + +describe('NIM Model Serving', () => { + describe('Deploying a model from an existing Project', () => { + it('should be disabled if the card is empty', () => { + initInterceptsToEnableNim({ hasAllModels: true }); + + projectDetails.visitSection('test-project', 'model-server'); + // For multiple cards use case + findNimModelDeployButton().click(); + cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + + // test that you can not submit on empty + nimDeployModal.shouldBeOpen(); + nimDeployModal.findSubmitButton().should('be.disabled'); + }); + + it('should be enabled if the card has the minimal info', () => { + initInterceptsToEnableNim({}); + const nimInferenceService = mockNimInferenceService(); + initInterceptsToDeployModel(nimInferenceService); + + projectDetails.visitSection('test-project', 'model-server'); + cy.findByTestId('deploy-button').should('exist'); + cy.findByTestId('deploy-button').click(); + cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + + // test that you can not submit on empty + nimDeployModal.shouldBeOpen(); + nimDeployModal.findSubmitButton().should('be.disabled'); + + // test filling in minimum required fields + nimDeployModal.findModelNameInput().type('Test Name'); + nimDeployModal + .findNIMToDeploy() + .findSelectOption('Snowflake Arctic Embed Large Embedding - 1.0.0') + .click(); + nimDeployModal.findSubmitButton().should('be.enabled'); + + nimDeployModal.findSubmitButton().click(); + + //dry run request + if (nimInferenceService.status) { + delete nimInferenceService.status; + } + cy.wait('@createInferenceService').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body).to.eql(nimInferenceService); + }); + + // Actual request + cy.wait('@createInferenceService').then((interception) => { + expect(interception.request.url).not.to.include('?dryRun=All'); + }); + + cy.get('@createInferenceService.all').then((interceptions) => { + expect(interceptions).to.have.length(2); // 1 dry run request and 1 actual request + }); + + nimDeployModal.shouldBeOpen(false); + }); + }); + + describe('Enabling NIM', () => { + describe('When NIM feature is enabled', () => { + it("should allow deploying NIM from a Project's Overview tab when the only platform", () => { + initInterceptsToEnableNim({}); + const componentName = 'overview'; + projectDetails.visitSection('test-project', componentName); + const overviewComponent = projectDetails.findComponent(componentName); + overviewComponent.should('exist'); + const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); + deployModelButton.should('exist'); + validateNvidiaNimModel(deployModelButton); + }); + + it("should allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }); + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .findByTestId('nvidia-nim-platform-card') + .should('contain', 'NVIDIA NIM model serving platform') + .and('contain', 'Models are deployed using NVIDIA NIM microservices.'); + + validateNvidiaNimModel( + projectDetails + .findComponent('overview') + .findByTestId('nvidia-nim-platform-card') + .findByTestId('model-serving-platform-button'), + ); + }); + + it("should allow deploying NIM from a Project's Models tab when the only platform", () => { + initInterceptsToEnableNim({}); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('exist'); + + cy.contains('Start by adding a model server'); + cy.contains( + 'Model servers are used to deploy models and to allow apps to send requests to your models. Configuring a model server includes specifying the number of replicas being deployed, the server size, the token authentication, the serving runtime, and how the project that the model server belongs to is accessed.', + ); + + const deployButton = projectDetails + .findComponent('model-server') + .findByTestId('deploy-button'); + validateNvidiaNimModel(deployButton); + }); + + it("should allow deploying NIM from a Project's Models tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }); + + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); + + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard() + .should('contain', 'Models are deployed using NVIDIA NIM microservices.') + .and('contain', 'NVIDIA NIM model serving platform'); + + validateNvidiaNimModel(findNimModelDeployButton()); + }); + }); + + describe('When NIM feature is disabled', () => { + it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: true, + }); + + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .find('[data-testid="nvidia-nim-platform-card"]') + .should('not.exist'); + + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + + it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: true, + }); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); + + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard().should('not.exist'); + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + }); + + describe('When missing the Template', () => { + it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + + projectDetails.visitSection('test-project', 'overview'); + + projectDetails + .findComponent('overview') + .findByTestId('single-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + projectDetails + .findComponent('overview') + .findByTestId('multi-serving-platform-card') + .findByTestId('model-serving-platform-button') + .should('exist'); + + projectDetails + .findComponent('overview') + .find('[data-testid="nvidia-nim-platform-card"]') + .should('not.exist'); + + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + + it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { + initInterceptorsForNewProjectWithoutModelSelection( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + projectDetails.visitSection('test-project', 'model-server'); + projectDetails.shouldBeEmptyState('Models', 'model-server', true); + projectDetails.findServingPlatformLabel().should('not.exist'); + + projectDetails.findSingleModelDeployButton().should('exist'); + projectDetails.findMultiModelButton().should('exist'); + + findNimModelServingPlatformCard().should('not.exist'); + cy.contains('NVIDIA NIM model serving platform').should('not.exist'); + cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + }); + }); + }); + + describe('Deleting an existing model', () => { + it("should be the only option available from the Project's Models tab", () => { + initInterceptsToEnableNim({}); + initInterceptsForDeleteModel(); + + // go the Models tab in the created project + projectDetails.visitSection('test-project', 'model-server'); + // grab the deployed models table and click the kebab menu + cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); + cy.get('ul[role="menu"]').should('have.length', 1); + cy.get('button').contains('Delete').should('exist'); + }); + + // TODO this is the only test-case testing the global model serving section, the rest test projects. + // TODO should we move this one test-case to ../modelServing ? + it('should be the only option available for NIM Models in the Global Serving Models section', () => { + initInterceptsToEnableNim({}); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + modelServingGlobal.visit('test-project'); + modelServingGlobal.getModelRow('Test Name').get('button[aria-label="Kebab toggle"]').click(); + + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .should('have.length', 1); + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .contains('Delete') + .should('exist'); + }); + + it('should delete the underlying InferenceService and ServingRuntime', () => { + initInterceptsToEnableNim({}); + initInterceptsForDeleteModel(); + + // go the Models tab in the created project + projectDetails.visitSection('test-project', 'model-server'); + // grab the deployed models table and click the kebab menu + cy.findByTestId('kserve-model-row-item').get('button[aria-label="Kebab toggle"').click(); + // grab the delete menu and click it + cy.get('button').contains('Delete').click(); + // grab the delete menu window and put in the project name + deleteModal.findInput().type('Test Name'); + // grab the delete button and click it + deleteModal.findSubmitButton().click(); + + // verify the model was deleted + cy.wait('@deleteInference'); + cy.wait('@deleteRuntime'); + }); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index af22a92a0e..3692facb3d 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -10,7 +10,9 @@ export function findServingPlatformLabel(): Cypress.Chainable { return cy.findByTestId('serving-platform-label'); } -export function validateNvidiaNimModel(deployButtonElement): void { +export function validateNvidiaNimModel( + deployButtonElement: Cypress.Chainable>, +): void { deployButtonElement.click(); cy.contains('Deploy model with NVIDIA NIM'); cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); From b5c58a8b034f75ca11a3d91f367f31a53059f55b Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 4 Oct 2024 09:30:11 +0200 Subject: [PATCH 13/33] adding const for modal dialog title Signed-off-by: Daniele Martinoli --- .../cypress/tests/mocked/projects/modelServingNim.cy.ts | 5 +++-- frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index a455ad366b..fe6c9cda9d 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -30,6 +30,7 @@ import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog import { findNimModelDeployButton, findNimModelServingPlatformCard, + modalDialogTitle, validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import type { InferenceServiceKind } from '~/k8sTypes'; @@ -167,7 +168,7 @@ describe('NIM Model Serving', () => { projectDetails.visitSection('test-project', 'model-server'); // For multiple cards use case findNimModelDeployButton().click(); - cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + cy.contains(modalDialogTitle).should('be.visible'); // test that you can not submit on empty nimDeployModal.shouldBeOpen(); @@ -182,7 +183,7 @@ describe('NIM Model Serving', () => { projectDetails.visitSection('test-project', 'model-server'); cy.findByTestId('deploy-button').should('exist'); cy.findByTestId('deploy-button').click(); - cy.contains('Deploy model with NVIDIA NIM').should('be.visible'); + cy.contains(modalDialogTitle).should('be.visible'); // test that you can not submit on empty nimDeployModal.shouldBeOpen(); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 3692facb3d..20ff2bbbd2 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -10,11 +10,12 @@ export function findServingPlatformLabel(): Cypress.Chainable { return cy.findByTestId('serving-platform-label'); } +export const modalDialogTitle = 'Deploy model with NVIDIA NIM'; export function validateNvidiaNimModel( deployButtonElement: Cypress.Chainable>, ): void { deployButtonElement.click(); - cy.contains('Deploy model with NVIDIA NIM'); + cy.contains(modalDialogTitle); cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); //find the form label Project with value as the Test Project @@ -24,12 +25,12 @@ export function validateNvidiaNimModel( cy.get('div[role="dialog"]').get('button[aria-label="Close"]').click(); // now the nvidia nim window should not be visible. - cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); + cy.contains(modalDialogTitle).should('not.exist'); deployButtonElement.click(); //validate model submit button is disabled without entering form data cy.findByTestId('modal-submit-button').should('be.disabled'); //validate nim modal cancel button cy.findByTestId('modal-cancel-button').click(); - cy.contains('Deploy model with NVIDIA NIM').should('not.exist'); + cy.contains(modalDialogTitle).should('not.exist'); } From db769a117abc49112e6b7dcda8a5be7f2fa37030 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 4 Oct 2024 11:36:14 +0200 Subject: [PATCH 14/33] test cases for list of models in different pages/tabs Signed-off-by: Daniele Martinoli --- .../mocked/projects/modelServingNim.cy.ts | 35 ++++++++ .../cypress/cypress/utils/nimUtils.ts | 79 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index fe6c9cda9d..be86d30291 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -31,6 +31,9 @@ import { findNimModelDeployButton, findNimModelServingPlatformCard, modalDialogTitle, + validateNimInmferenceModelsTable, + validateNimModelsTable, + validateNimOverviewModelsTable, validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import type { InferenceServiceKind } from '~/k8sTypes'; @@ -219,6 +222,38 @@ describe('NIM Model Serving', () => { nimDeployModal.shouldBeOpen(false); }); + + it('should list the deployed model in Models tab', () => { + initInterceptsToEnableNim({ hasAllModels: false }); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + projectDetails.visitSection('test-project', 'model-server'); + + validateNimModelsTable(); + }); + + it('should list the deployed model in Overview tab', () => { + initInterceptsToEnableNim({ hasAllModels: false }); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + projectDetails.visitSection('test-project', 'overview'); + + validateNimOverviewModelsTable(); + validateNimModelsTable(); + projectDetails.visitSection('test-project', 'overview'); + }); + + it('should list the deployed model in Model Serving page', () => { + initInterceptsToEnableNim({ hasAllModels: false }); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + modelServingGlobal.visit('test-project'); + + validateNimInmferenceModelsTable(); + }); }); describe('Enabling NIM', () => { diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 20ff2bbbd2..0490b45ff4 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -34,3 +34,82 @@ export function validateNvidiaNimModel( cy.findByTestId('modal-cancel-button').click(); cy.contains(modalDialogTitle).should('not.exist'); } + +export function validateNimModelsTable(): void { + // Table is visible and has 2 rows (2nd is the hidden expandable row) + cy.get('[data-testid="kserve-inference-service-table"]') + .find('tbody') + .find('tr') + .should('have.length', 2); + + // First row matches the NIM inference service details + cy.get('[style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); + cy.get('[data-label="Serving Runtime"]').should('have.text', 'NVIDIA NIM'); + cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); + // Validate Internal Service tooltip and close it + cy.get('[data-testid="internal-service-button"]').click(); + cy.get('.pf-v5-c-popover__title-text').should( + 'have.text', + 'Internal Service can be accessed inside the cluster', + ); + cy.get('.pf-v5-c-popover__close > .pf-v5-c-button > .pf-v5-svg > path').click(); + // Open toggle to validate Model details + cy.get('.pf-v5-c-table__toggle-icon').click(); + cy.get( + ':nth-child(1) > .pf-v5-c-description-list > .pf-v5-c-description-list__group > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', + ).should('have.text', 'arctic-embed-l'); + cy.get( + ':nth-child(2) > .pf-v5-c-description-list > :nth-child(1) > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', + ).should('have.text', '1'); + cy.get('.pf-v5-c-list > :nth-child(1)').should('have.text', 'Small'); + cy.get('.pf-v5-c-list > :nth-child(2)').should('have.text', '1 CPUs, 4Gi Memory requested'); + cy.get('.pf-v5-c-list > :nth-child(3)').should('have.text', '2 CPUs, 8Gi Memory limit'); + cy.get( + ':nth-child(3) > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', + ).should('have.text', 'No accelerator selected'); + cy.get('.pf-v5-c-table__toggle-icon').click(); +} + +export function validateNimOverviewModelsTable(): void { + // Card is visible + cy.get('.pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(2) > .pf-v5-c-content > h3 > b').should('be.visible'); + cy.get('.pf-v5-l-gallery > :nth-child(1) > .pf-v5-c-card > .pf-v5-c-card__header > .pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(1)').should('be.visible'); + // Validate card details + cy.get(':nth-child(2) > [style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); + cy.get('dt').should('have.text', 'Serving runtime'); + cy.get('dd').should('have.text', 'NVIDIA NIM'); + cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); + cy.get('[data-testid="internal-service-button"]').click(); + cy.get('.pf-v5-c-popover__title-text').should('have.text', 'Internal Service can be accessed inside the cluster'); + // Opens the Models table + cy.get('.pf-m-gap-md > :nth-child(2) > .pf-v5-c-button').click(); +} + +export function validateNimInmferenceModelsTable(): void { + // Table is visible and has 1 row + cy.get('[data-testid="inference-service-table"]') + .find('tbody') + .find('tr') + .should('have.length', 1); + // First row matches the NIM inference service details + cy.get('[style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); + cy.get('[data-label="Project"]').should('contains.text', 'Test Project'); + cy.get( + '[data-label="Project"] > .pf-v5-c-label > .pf-v5-c-label__content > .pf-v5-c-label__text', + ).should('have.text', 'Single-model serving enabled'); + cy.get('[data-label="Serving Runtime"]').should('have.text', 'NVIDIA NIM'); + // Validate Internal Service tooltip and close it + cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); + cy.get('[data-testid="internal-service-button"]').click(); + cy.get('.pf-v5-c-popover__title-text').should( + 'have.text', + 'Internal Service can be accessed inside the cluster', + ); + cy.get('.pf-v5-c-popover__close > .pf-v5-c-button > .pf-v5-svg > path').click(); + cy.get( + '[data-label="API protocol"] > .pf-v5-c-label > .pf-v5-c-label__content > .pf-v5-c-label__text', + ).should('have.text', 'REST'); + cy.get('[data-testid="status-tooltip"] > .pf-v5-c-icon__content > .pf-v5-svg > path').should( + 'be.visible', + ); +} From 6d82381afb1d6592c40bc04f9bb18bd43be6782d Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 4 Oct 2024 12:10:38 +0200 Subject: [PATCH 15/33] linting fixes Signed-off-by: Daniele Martinoli --- .../cypress/cypress/utils/nimUtils.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 0490b45ff4..f989af333f 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -72,15 +72,25 @@ export function validateNimModelsTable(): void { export function validateNimOverviewModelsTable(): void { // Card is visible - cy.get('.pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(2) > .pf-v5-c-content > h3 > b').should('be.visible'); - cy.get('.pf-v5-l-gallery > :nth-child(1) > .pf-v5-c-card > .pf-v5-c-card__header > .pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(1)').should('be.visible'); + cy.get( + '.pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(2) > .pf-v5-c-content > h3 > b', + ).should('be.visible'); + cy.get( + '.pf-v5-l-gallery > :nth-child(1) > .pf-v5-c-card > .pf-v5-c-card__header > .pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(1)', + ).should('be.visible'); // Validate card details - cy.get(':nth-child(2) > [style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); + cy.get(':nth-child(2) > [style="display: block;"] > :nth-child(1)').should( + 'have.text', + 'Test Name', + ); cy.get('dt').should('have.text', 'Serving runtime'); cy.get('dd').should('have.text', 'NVIDIA NIM'); cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); cy.get('[data-testid="internal-service-button"]').click(); - cy.get('.pf-v5-c-popover__title-text').should('have.text', 'Internal Service can be accessed inside the cluster'); + cy.get('.pf-v5-c-popover__title-text').should( + 'have.text', + 'Internal Service can be accessed inside the cluster', + ); // Opens the Models table cy.get('.pf-m-gap-md > :nth-child(2) > .pf-v5-c-button').click(); } From 5de4b14963ceb019672f907aef391b26dc5cd0c4 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli Date: Fri, 4 Oct 2024 14:26:27 +0200 Subject: [PATCH 16/33] removed experimentalStudio option Signed-off-by: Daniele Martinoli --- frontend/src/__tests__/cypress/cypress.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/__tests__/cypress/cypress.config.ts b/frontend/src/__tests__/cypress/cypress.config.ts index 493f9fdcb8..cb1bcf733e 100644 --- a/frontend/src/__tests__/cypress/cypress.config.ts +++ b/frontend/src/__tests__/cypress/cypress.config.ts @@ -16,7 +16,6 @@ import { env, cypressEnv, BASE_URL } from '~/__tests__/cypress/cypress/utils/tes const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`; export default defineConfig({ - experimentalStudio: true, experimentalMemoryManagement: true, // Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406 reporter: '../../../node_modules/cypress-multi-reporters', From a765cb97f60946307ff5d4acadf69fb73565142e Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Fri, 4 Oct 2024 10:32:43 -0400 Subject: [PATCH 17/33] test: split nim tests per pages (#13) Signed-off-by: Tomer Figenblat --- .../mocked/modelServing/modelServingNim.cy.ts | 42 ++++ .../mocked/projects/modelServingNim.cy.ts | 196 +----------------- .../cypress/cypress/utils/nimUtils.ts | 160 ++++++++++++++ 3 files changed, 213 insertions(+), 185 deletions(-) create mode 100644 frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts new file mode 100644 index 0000000000..3c20f002f8 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -0,0 +1,42 @@ +import { + initInterceptsToEnableNim, + validateNimInmferenceModelsTable, +} from '~/__tests__/cypress/cypress/utils/nimUtils'; +import { mockNimInferenceService, mockNimServingRuntime } from '~/__mocks__/mockNimResource'; +import { + InferenceServiceModel, + ServingRuntimeModel, +} from '~/__tests__/cypress/cypress/utils/models'; +import { mockK8sResourceList } from '~/__mocks__'; +import { modelServingGlobal } from '~/__tests__/cypress/cypress/pages/modelServing'; + +describe('NIM Models Deployments', () => { + it('should be listed in the global models list', () => { + initInterceptsToEnableNim({ hasAllModels: false }); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + modelServingGlobal.visit('test-project'); + + validateNimInmferenceModelsTable(); + }); + + it('should only be allowed to be deleted, no edit', () => { + initInterceptsToEnableNim({}); + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + modelServingGlobal.visit('test-project'); + modelServingGlobal.getModelRow('Test Name').get('button[aria-label="Kebab toggle"]').click(); + + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .should('have.length', 1); + modelServingGlobal + .getModelRow('Test Name') + .get('button[role="menuitem"]') + .contains('Delete') + .should('exist'); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index be86d30291..64e4a8c80a 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -1,168 +1,25 @@ -import type { MockDashboardConfigType } from '~/__mocks__/mockDashboardConfig'; -import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; -import { mockDscStatus } from '~/__mocks__/mockDscStatus'; import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { mockNimInferenceService, mockNimServingRuntime } from '~/__mocks__/mockNimResource'; import { - mockNimImages, - mockNimInferenceService, - mockNimModelPVC, - mockNimServingRuntime, - mockNimServingRuntimeTemplate, - mockNvidiaNimAccessSecret, - mockNvidiaNimImagePullSecret, -} from '~/__mocks__/mockNimResource'; -import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; -import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; -import { - AcceleratorProfileModel, - ConfigMapModel, InferenceServiceModel, - ProjectModel, - PVCModel, - SecretModel, ServingRuntimeModel, - TemplateModel, } from '~/__tests__/cypress/cypress/utils/models'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; -import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; -import { mock200Status } from '~/__mocks__'; import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; import { findNimModelDeployButton, findNimModelServingPlatformCard, + initInterceptorsValidatingNimEnablement, + initInterceptsForDeleteModel, + initInterceptsToDeployModel, + initInterceptsToEnableNim, modalDialogTitle, - validateNimInmferenceModelsTable, validateNimModelsTable, validateNimOverviewModelsTable, validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; -import type { InferenceServiceKind } from '~/k8sTypes'; -import { modelServingGlobal } from '~/__tests__/cypress/cypress/pages/modelServing'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; -type EnableNimConfigType = { - hasAllModels?: boolean; -}; -// intercept all APIs required for creating a new project without selecting the model runtime from available models run times. -const initInterceptorsForNewProjectWithoutModelSelection = ( - dashboardConfig: MockDashboardConfigType, - disableServingRuntime = false, -) => { - cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); - - if (!disableServingRuntime) { - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); - } - - cy.interceptK8sList( - ProjectModel, - mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), - ); -}; - -// intercept all APIs required for enabling NIM -const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType) => { - cy.interceptOdh( - 'GET /api/dsc/status', - mockDscStatus({ - installedComponents: { - kserve: true, - 'model-mesh': true, - }, - }), - ); - - cy.interceptOdh( - 'GET /api/config', - mockDashboardConfig({ - disableKServe: false, - disableModelMesh: false, - disableNIMModelServing: false, - }), - ); - - const project = mockProjectK8sResource({ - hasAnnotations: true, - enableModelMesh: hasAllModels ? undefined : false, - }); - if (project.metadata.annotations != null) { - project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; - } - cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); - - const templateMock = mockNimServingRuntimeTemplate(); - cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); - cy.interceptK8s(TemplateModel, templateMock); - - cy.interceptK8sList( - AcceleratorProfileModel, - mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), - ); - - cy.intercept('GET', '/api/accelerators', { - configured: true, - available: { 'nvidia.com/gpu': 1 }, - total: { 'nvidia.com/gpu': 1 }, - allocated: { 'nvidia.com/gpu': 1 }, - }); -}; - -// intercept all APIs required for deploying new models in existing projects -const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind) => { - cy.interceptK8s(ConfigMapModel, mockNimImages()); - cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); - cy.interceptK8s('POST', InferenceServiceModel, nimInferenceService).as('createInferenceService'); - - cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); - - // NOTES: `body` field is needed! - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, - { - body: { body: mockNimImages() }, - }, - ); - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, - { body: { body: mockNvidiaNimAccessSecret() } }, - ); - cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { - body: { body: mockNvidiaNimImagePullSecret() }, - }); - cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); -}; - -// intercept all APIs required for deleting an existing model -const initInterceptsForDeleteModel = () => { - // create initial inference and runtime - cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); - cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - - // intercept delete inference request - cy.interceptK8s( - 'DELETE', - { - model: InferenceServiceModel, - ns: 'test-project', - name: 'test-name', - }, - mock200Status({}), - ).as('deleteInference'); - - // intercept delete runtime request - cy.interceptK8s( - 'DELETE', - { - model: ServingRuntimeModel, - ns: 'test-project', - name: 'test-name', - }, - mock200Status({}), - ).as('deleteRuntime'); -}; - describe('NIM Model Serving', () => { describe('Deploying a model from an existing Project', () => { it('should be disabled if the card is empty', () => { @@ -244,16 +101,6 @@ describe('NIM Model Serving', () => { validateNimModelsTable(); projectDetails.visitSection('test-project', 'overview'); }); - - it('should list the deployed model in Model Serving page', () => { - initInterceptsToEnableNim({ hasAllModels: false }); - cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); - cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - - modelServingGlobal.visit('test-project'); - - validateNimInmferenceModelsTable(); - }); }); describe('Enabling NIM', () => { @@ -270,7 +117,7 @@ describe('NIM Model Serving', () => { }); it("should allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection({ + initInterceptorsValidatingNimEnablement({ disableKServe: false, disableModelMesh: false, disableNIMModelServing: false, @@ -320,7 +167,7 @@ describe('NIM Model Serving', () => { }); it("should allow deploying NIM from a Project's Models tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection({ + initInterceptorsValidatingNimEnablement({ disableKServe: false, disableModelMesh: false, disableNIMModelServing: false, @@ -343,7 +190,7 @@ describe('NIM Model Serving', () => { describe('When NIM feature is disabled', () => { it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection({ + initInterceptorsValidatingNimEnablement({ disableKServe: false, disableModelMesh: false, disableNIMModelServing: true, @@ -372,7 +219,7 @@ describe('NIM Model Serving', () => { }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection({ + initInterceptorsValidatingNimEnablement({ disableKServe: false, disableModelMesh: false, disableNIMModelServing: true, @@ -392,7 +239,7 @@ describe('NIM Model Serving', () => { describe('When missing the Template', () => { it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection( + initInterceptorsValidatingNimEnablement( { disableKServe: false, disableModelMesh: false, @@ -424,7 +271,7 @@ describe('NIM Model Serving', () => { }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { - initInterceptorsForNewProjectWithoutModelSelection( + initInterceptorsValidatingNimEnablement( { disableKServe: false, disableModelMesh: false, @@ -459,27 +306,6 @@ describe('NIM Model Serving', () => { cy.get('button').contains('Delete').should('exist'); }); - // TODO this is the only test-case testing the global model serving section, the rest test projects. - // TODO should we move this one test-case to ../modelServing ? - it('should be the only option available for NIM Models in the Global Serving Models section', () => { - initInterceptsToEnableNim({}); - cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); - cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - - modelServingGlobal.visit('test-project'); - modelServingGlobal.getModelRow('Test Name').get('button[aria-label="Kebab toggle"]').click(); - - modelServingGlobal - .getModelRow('Test Name') - .get('button[role="menuitem"]') - .should('have.length', 1); - modelServingGlobal - .getModelRow('Test Name') - .get('button[role="menuitem"]') - .contains('Delete') - .should('exist'); - }); - it('should delete the underlying InferenceService and ServingRuntime', () => { initInterceptsToEnableNim({}); initInterceptsForDeleteModel(); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index f989af333f..6fc37ba538 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -1,3 +1,34 @@ +import type { MockDashboardConfigType } from '~/__mocks__'; +import { + mock200Status, + mockDashboardConfig, + mockDscStatus, + mockK8sResourceList, + mockProjectK8sResource, + mockSecretK8sResource, +} from '~/__mocks__'; +import { + AcceleratorProfileModel, + ConfigMapModel, + InferenceServiceModel, + ProjectModel, + PVCModel, + SecretModel, + ServingRuntimeModel, + TemplateModel, +} from '~/__tests__/cypress/cypress/utils/models'; +import { + mockNimImages, + mockNimInferenceService, + mockNimModelPVC, + mockNimServingRuntime, + mockNimServingRuntimeTemplate, + mockNvidiaNimAccessSecret, + mockNvidiaNimImagePullSecret, +} from '~/__mocks__/mockNimResource'; +import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; +import type { InferenceServiceKind } from '~/k8sTypes'; + export function findNimModelDeployButton(): Cypress.Chainable { return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); } @@ -11,6 +42,7 @@ export function findServingPlatformLabel(): Cypress.Chainable { } export const modalDialogTitle = 'Deploy model with NVIDIA NIM'; + export function validateNvidiaNimModel( deployButtonElement: Cypress.Chainable>, ): void { @@ -123,3 +155,131 @@ export function validateNimInmferenceModelsTable(): void { 'be.visible', ); } + +/* ################################################### + ###### Interception Initialization Utilities ###### + ################################################### */ + +type EnableNimConfigType = { + hasAllModels?: boolean; +}; + +// intercept all APIs required for enabling NIM +export const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimConfigType): void => { + cy.interceptOdh( + 'GET /api/dsc/status', + mockDscStatus({ + installedComponents: { + kserve: true, + 'model-mesh': true, + }, + }), + ); + + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }), + ); + + const project = mockProjectK8sResource({ + hasAnnotations: true, + enableModelMesh: hasAllModels ? undefined : false, + }); + if (project.metadata.annotations != null) { + project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; + } + cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); + + const templateMock = mockNimServingRuntimeTemplate(); + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + + cy.interceptK8sList( + AcceleratorProfileModel, + mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), + ); + + cy.intercept('GET', '/api/accelerators', { + configured: true, + available: { 'nvidia.com/gpu': 1 }, + total: { 'nvidia.com/gpu': 1 }, + allocated: { 'nvidia.com/gpu': 1 }, + }); +}; + +// intercept all APIs required for deploying new NIM models in existing projects +export const initInterceptsToDeployModel = (nimInferenceService: InferenceServiceKind): void => { + cy.interceptK8s(ConfigMapModel, mockNimImages()); + cy.interceptK8s('POST', SecretModel, mockSecretK8sResource({})); + cy.interceptK8s('POST', InferenceServiceModel, nimInferenceService).as('createInferenceService'); + + cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); + + // NOTES: `body` field is needed! + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, + { + body: { body: mockNimImages() }, + }, + ); + cy.intercept( + { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, + { body: { body: mockNvidiaNimAccessSecret() } }, + ); + cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { + body: { body: mockNvidiaNimImagePullSecret() }, + }); + cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); +}; + +// intercept all APIs required for deleting an existing NIM models +export const initInterceptsForDeleteModel = (): void => { + // create initial inference and runtime + cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); + cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); + + // intercept delete inference request + cy.interceptK8s( + 'DELETE', + { + model: InferenceServiceModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteInference'); + + // intercept delete runtime request + cy.interceptK8s( + 'DELETE', + { + model: ServingRuntimeModel, + ns: 'test-project', + name: 'test-name', + }, + mock200Status({}), + ).as('deleteRuntime'); +}; + +// intercept all APIs required for verifying NIM enablement +export const initInterceptorsValidatingNimEnablement = ( + dashboardConfig: MockDashboardConfigType, + disableServingRuntime = false, +): void => { + cy.interceptOdh('GET /api/config', mockDashboardConfig(dashboardConfig)); + + if (!disableServingRuntime) { + const templateMock = mockNimServingRuntimeTemplate(); + cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); + cy.interceptK8s(TemplateModel, templateMock); + } + + cy.interceptK8sList( + ProjectModel, + mockK8sResourceList([mockProjectK8sResource({ hasAnnotations: true })]), + ); +}; From e23d11aaed78baf2130159d862c777d03ded05b9 Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:48:25 +0200 Subject: [PATCH 18/33] testing more buttons in modal dialog (#14) Signed-off-by: Daniele Martinoli --- .../cypress/cypress/pages/nimModelDialog.ts | 24 +++++++++++++++++++ .../mocked/projects/modelServingNim.cy.ts | 12 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts b/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts index 23bdc73717..0e586b4fff 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts @@ -16,6 +16,30 @@ class NIMDeployModal extends Modal { findNIMToDeploy() { return this.find().findByTestId('nim-model-list-selection'); } + + findNimStorageSizeInput() { + return cy.get('[data-testid="pvc-size"] input'); + } + + 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"]'); + } + + findNimModelReplicasMinusButton() { + return this.find().find('button[aria-label="Minus"]').eq(1); + } + + findNimModelReplicasPlusButton() { + return this.find().find('button[aria-label="Plus"]').eq(1); + } } export const nimDeployModal = new NIMDeployModal(); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 64e4a8c80a..80e7ced94a 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -57,6 +57,18 @@ describe('NIM Model Serving', () => { .click(); nimDeployModal.findSubmitButton().should('be.enabled'); + nimDeployModal.findNimStorageSizeInput().should('have.value', '30'); + nimDeployModal.findStorageSizeMinusButton().click(); + nimDeployModal.findNimStorageSizeInput().should('have.value', '29'); + nimDeployModal.findStorageSizePlusButton().click(); + nimDeployModal.findNimStorageSizeInput().should('have.value', '30'); + + nimDeployModal.findNimModelReplicas().should('have.value', '1'); + nimDeployModal.findNimModelReplicasPlusButton().click(); + nimDeployModal.findNimModelReplicas().should('have.value', '2'); + nimDeployModal.findNimModelReplicasMinusButton().click(); + nimDeployModal.findNimModelReplicas().should('have.value', '1'); + nimDeployModal.findSubmitButton().click(); //dry run request From ca6ef78d01169b145f02f65989d0432bfc27fb96 Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:31:58 -0400 Subject: [PATCH 19/33] Added a test case to address there is a failure in loading Nvidia Nim model images then error message should be displayed. Added experimentalStudio flag to cypress.config.ts but disabled it by default. --- frontend/src/__tests__/cypress/cypress.config.ts | 1 + .../tests/mocked/projects/modelServingNim.cy.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/frontend/src/__tests__/cypress/cypress.config.ts b/frontend/src/__tests__/cypress/cypress.config.ts index cb1bcf733e..b27f0b6eaa 100644 --- a/frontend/src/__tests__/cypress/cypress.config.ts +++ b/frontend/src/__tests__/cypress/cypress.config.ts @@ -16,6 +16,7 @@ import { env, cypressEnv, BASE_URL } from '~/__tests__/cypress/cypress/utils/tes const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`; export default defineConfig({ + experimentalStudio: false, experimentalMemoryManagement: true, // Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406 reporter: '../../../node_modules/cypress-multi-reporters', diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 80e7ced94a..eab1687c89 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -198,6 +198,18 @@ describe('NIM Model Serving', () => { validateNvidiaNimModel(findNimModelDeployButton()); }); + + it("When there is a failure in loading Nvidia Nim model images then error message should be displayed.", () => { + initInterceptsToEnableNim({}); + const componentName = 'overview'; + projectDetails.visitSection('test-project', componentName); + const overviewComponent = projectDetails.findComponent(componentName); + overviewComponent.should('exist'); + const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); + deployModelButton.should('exist'); + deployModelButton.click() + cy.contains('There was a problem fetching the NIM models. Please try again later.') + }); }); describe('When NIM feature is disabled', () => { From b1c2492b855788c70e499e5518e93c4ed673e374 Mon Sep 17 00:00:00 2001 From: Lokesh Rangineni <19699092+lokeshrangineni@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:31:58 -0400 Subject: [PATCH 20/33] Added a test case to address there is a failure in loading Nvidia Nim model images then error message should be displayed. Added experimentalStudio flag to cypress.config.ts but disabled it by default. --- .../cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index eab1687c89..3350e21988 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -199,7 +199,7 @@ describe('NIM Model Serving', () => { validateNvidiaNimModel(findNimModelDeployButton()); }); - it("When there is a failure in loading Nvidia Nim model images then error message should be displayed.", () => { + it("should display an error when failed to fetch nim Nividia model list", () => { initInterceptsToEnableNim({}); const componentName = 'overview'; projectDetails.visitSection('test-project', componentName); From c480d4e4d79afd202888d9f78b3e4e9be77dbd92 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Fri, 4 Oct 2024 16:01:35 -0400 Subject: [PATCH 21/33] test: fix linting errors Signed-off-by: Tomer Figenblat --- .../cypress/tests/mocked/projects/modelServingNim.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 3350e21988..da5c6ce592 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -199,7 +199,7 @@ describe('NIM Model Serving', () => { validateNvidiaNimModel(findNimModelDeployButton()); }); - it("should display an error when failed to fetch nim Nividia model list", () => { + it('should display an error when failed to fetch NIM model list', () => { initInterceptsToEnableNim({}); const componentName = 'overview'; projectDetails.visitSection('test-project', componentName); @@ -207,8 +207,8 @@ describe('NIM Model Serving', () => { overviewComponent.should('exist'); const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); deployModelButton.should('exist'); - deployModelButton.click() - cy.contains('There was a problem fetching the NIM models. Please try again later.') + deployModelButton.click(); + cy.contains('There was a problem fetching the NIM models. Please try again later.'); }); }); From baf470e6a1464026e9fceed3d08ea4c163b8cdac Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Mon, 7 Oct 2024 09:49:20 -0400 Subject: [PATCH 22/33] test: fix review change requests 1 - see body - Use JSON object instead of string in mocking of NIM images configmap - Remove experiementalStudio cypres config param - Add mock for NIM project Co-authored-by: Daniele Martinoli Co-authored-by: lokeshrangineni Signed-off-by: Tomer Figenblat --- frontend/src/__mocks__/mockNimResource.ts | 58 +++++++++++-------- .../src/__tests__/cypress/cypress.config.ts | 1 - .../cypress/cypress/utils/nimUtils.ts | 10 +--- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index 92d3163f5c..baa2fa0f90 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -2,11 +2,13 @@ import { ConfigMapKind, InferenceServiceKind, PersistentVolumeClaimKind, + ProjectKind, SecretKind, ServingRuntimeKind, TemplateKind, } from '~/k8sTypes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { mockConfigMap } from './mockConfigMap'; import { mockServingRuntimeK8sResource } from './mockServingRuntimeK8sResource'; import { mockInferenceServiceK8sResource } from './mockInferenceServiceK8sResource'; @@ -19,31 +21,26 @@ export const mockNimImages = (): ConfigMapKind => name: 'nvidia-nim-images-data', namespace: 'opendatahub', data: { - alphafold2: - '{' + - ' "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': - '{' + - ' "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"' + - ' }', + 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', + }), }, }); @@ -124,6 +121,17 @@ export const mockNvidiaNimImagePullSecret = (): SecretKind => { 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', diff --git a/frontend/src/__tests__/cypress/cypress.config.ts b/frontend/src/__tests__/cypress/cypress.config.ts index b27f0b6eaa..cb1bcf733e 100644 --- a/frontend/src/__tests__/cypress/cypress.config.ts +++ b/frontend/src/__tests__/cypress/cypress.config.ts @@ -16,7 +16,6 @@ import { env, cypressEnv, BASE_URL } from '~/__tests__/cypress/cypress/utils/tes const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`; export default defineConfig({ - experimentalStudio: false, experimentalMemoryManagement: true, // Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406 reporter: '../../../node_modules/cypress-multi-reporters', diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 6fc37ba538..9073b9de7d 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -21,6 +21,7 @@ import { mockNimImages, mockNimInferenceService, mockNimModelPVC, + mockNimProject, mockNimServingRuntime, mockNimServingRuntimeTemplate, mockNvidiaNimAccessSecret, @@ -185,14 +186,7 @@ export const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimCon }), ); - const project = mockProjectK8sResource({ - hasAnnotations: true, - enableModelMesh: hasAllModels ? undefined : false, - }); - if (project.metadata.annotations != null) { - project.metadata.annotations['opendatahub.io/nim-support'] = 'true'; - } - cy.interceptK8sList(ProjectModel, mockK8sResourceList([project])); + cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockNimProject(hasAllModels)])); const templateMock = mockNimServingRuntimeTemplate(); cy.interceptK8sList(TemplateModel, mockK8sResourceList([templateMock])); From 7da5544955d17adfbb77d7687f0066304e51ae4e Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Mon, 7 Oct 2024 18:06:22 -0400 Subject: [PATCH 23/33] test: fix review change requests 2 - see body - Add nim-serving interception to odh intercept type safety mechanism - Add accelerators interception to odh intercept type safety mechanism Co-authored-by: Daniele Martinoli Co-authored-by: lokeshrangineni Signed-off-by: Tomer Figenblat --- frontend/src/__mocks__/mockNimResource.ts | 5 +++ .../cypress/cypress/support/commands/odh.ts | 24 +++++++++++--- .../src/__tests__/cypress/cypress/types.ts | 7 ++++ .../cypress/cypress/utils/nimUtils.ts | 32 +++++++++++-------- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/frontend/src/__mocks__/mockNimResource.ts b/frontend/src/__mocks__/mockNimResource.ts index baa2fa0f90..39b6684141 100644 --- a/frontend/src/__mocks__/mockNimResource.ts +++ b/frontend/src/__mocks__/mockNimResource.ts @@ -9,6 +9,7 @@ import { } 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'; @@ -138,3 +139,7 @@ export const mockNimModelPVC = (): PersistentVolumeClaimKind => { }); return pvc; }; + +export const mockNimServingResource = ( + resource: ConfigMapKind | SecretKind, +): NimServingResponse => ({ body: { body: resource } }); diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index d16745d4d5..73e73463c3 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -1,4 +1,4 @@ -import type { K8sResourceListResult, K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; +import { K8sResourceCommon, K8sResourceListResult, K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; import type { GenericStaticResponse, RouteHandlerController } from 'cypress/types/net-stubbing'; import type { BaseMetricCreationResponse, BaseMetricListResponse } from '~/api'; import type { @@ -9,7 +9,7 @@ import type { RegisteredModel, RegisteredModelList, } from '~/concepts/modelRegistry/types'; -import type { +import { DashboardConfigKind, DataScienceClusterInitializationKindStatus, DataScienceClusterKindStatus, @@ -19,16 +19,16 @@ import type { TemplateKind, NotebookKind, ModelRegistryKind, - ConsoleLinkKind, + ConsoleLinkKind, RoleBindingSubject, RoleBindingRoleRef, ConfigMapKind, SecretKind, } from '~/k8sTypes'; import type { StartNotebookData } from '~/pages/projects/types'; import type { AllowedUser } from '~/pages/notebookController/screens/admin/types'; import type { GroupsConfig } from '~/pages/groupSettings/groupTypes'; import type { StatusResponse } from '~/redux/types'; -import type { +import { BYONImage, - ClusterSettingsType, + ClusterSettingsType, DetectedAccelerators, ImageInfo, OdhApplication, OdhDocument, @@ -54,6 +54,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 { NimServingResponse } from '~/__tests__/cypress/cypress/types'; type SuccessErrorResponse = { success: boolean; @@ -637,6 +638,19 @@ declare global { (( type: 'POST /api/modelRegistryRoleBindings', response: OdhResponse, + ) => Cypress.Chainable) & + (( + type: 'GET /api/accelerators', + response: OdhResponse, + ) => Cypress.Chainable) & + (( + type: 'GET /api/nim-serving/:resource', + options: { + path: { + resource: 'nvidia-nim-images-data' | 'nvidia-nim-access' | 'nvidia-nim-image-pull'; + }; + }, + response: OdhResponse, ) => Cypress.Chainable); } } diff --git a/frontend/src/__tests__/cypress/cypress/types.ts b/frontend/src/__tests__/cypress/cypress/types.ts index 6b7297b9aa..c26889457f 100644 --- a/frontend/src/__tests__/cypress/cypress/types.ts +++ b/frontend/src/__tests__/cypress/cypress/types.ts @@ -1,4 +1,5 @@ import type { RouteMatcher } from 'cypress/types/net-stubbing'; +import { ConfigMapKind, SecretKind } from '~/k8sTypes'; export type Snapshot = { method: string; @@ -71,3 +72,9 @@ export type TestConfig = { OCP_ADMIN_USER: UserAuthConfig; S3: AWSS3Buckets; }; + +export type NimServingResponse = { + body: { + body: ConfigMapKind | SecretKind; + }; +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 9073b9de7d..7b1e665d41 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -21,7 +21,7 @@ import { mockNimImages, mockNimInferenceService, mockNimModelPVC, - mockNimProject, + mockNimProject, mockNimServingResource, mockNimServingRuntime, mockNimServingRuntimeTemplate, mockNvidiaNimAccessSecret, @@ -197,7 +197,7 @@ export const initInterceptsToEnableNim = ({ hasAllModels = false }: EnableNimCon mockK8sResourceList([mockAcceleratorProfile({ namespace: 'opendatahub' })]), ); - cy.intercept('GET', '/api/accelerators', { + cy.interceptOdh('GET /api/accelerators', { configured: true, available: { 'nvidia.com/gpu': 1 }, total: { 'nvidia.com/gpu': 1 }, @@ -213,20 +213,24 @@ export const initInterceptsToDeployModel = (nimInferenceService: InferenceServic cy.interceptK8s('POST', ServingRuntimeModel, mockNimServingRuntime()).as('createServingRuntime'); - // NOTES: `body` field is needed! - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-images-data' }, - { - body: { body: mockNimImages() }, - }, + cy.interceptOdh( + `GET /api/nim-serving/:resource`, + { path: { resource: 'nvidia-nim-images-data' } }, + mockNimServingResource(mockNimImages()), + ); + + cy.interceptOdh( + `GET /api/nim-serving/:resource`, + { path: { resource: 'nvidia-nim-access' } }, + mockNimServingResource(mockNvidiaNimAccessSecret()), ); - cy.intercept( - { method: 'GET', pathname: '/api/nim-serving/nvidia-nim-access' }, - { body: { body: mockNvidiaNimAccessSecret() } }, + + cy.interceptOdh( + `GET /api/nim-serving/:resource`, + { path: { resource: 'nvidia-nim-image-pull' } }, + mockNimServingResource(mockNvidiaNimImagePullSecret()), ); - cy.intercept('GET', 'api/nim-serving/nvidia-nim-image-pull', { - body: { body: mockNvidiaNimImagePullSecret() }, - }); + cy.interceptK8s('POST', PVCModel, mockNimModelPVC()); }; From eba0293afbde0a790c7f2303eb5dfd390ef58b64 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Mon, 7 Oct 2024 18:46:25 -0400 Subject: [PATCH 24/33] test: create page objects for verifying nim model tabels Co-authored-by: Daniele Martinoli Co-authored-by: lokeshrangineni Signed-off-by: Tomer Figenblat --- .../cypress/cypress/pages/projects.ts | 77 ++++++++++++++++++- .../cypress/cypress/utils/nimUtils.ts | 57 +++++++------- 2 files changed, 106 insertions(+), 28 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 458ad42b8a..96f053239f 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -286,13 +286,34 @@ 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'), + ); + } + + getKserveTableDetailsRow(name: string) { + return new KserveTableDetailsRow(() => + this.findKserveModelsTable() + .find('tbody') + .find('[data-label="Name"]') + .contains(name) + .closest('tr') + .next('tr'), + ); + } } class ProjectDetailsSettingsTab extends ProjectDetails { @@ -335,6 +356,60 @@ class TrustyAIUninstallModal extends DeleteModal { } } +class KserveTableDetailsRow extends TableRow { + private findDetailsCell() { + return this.find().find('td').eq(1); + } + + findValueFor(label: string) { + return this.findDetailsCell().find('dt').contains(label).closest('div').find('dd'); + } +} + +class KserveTableRow extends TableRow { + findColumn(name: string) { + return this.find().find(`[data-label="${name}"]`); + } + + findStatusTooltip() { + return this.find() + .findByTestId('status-tooltip') + .trigger('mouseenter') + .then(() => { + cy.findByTestId('model-status-tooltip'); + }); + } + + findStatusTooltipValue(msg: string) { + this.findStatusTooltip() + .invoke('text') + .should('contain', msg) + .then(() => { + this.findStatusTooltip().trigger('mouseleave'); + }); + } + + findAPIProtocol() { + return this.find().find(`[data-label="API protocol"]`); + } + + findInternalServiceButton() { + return this.find().findByTestId('internal-service-button'); + } + + findInternalServicePopover() { + return cy.findByTestId('internal-service-popover'); + } + + findInternalServicePopoverCloseButton() { + return this.findInternalServicePopover().find('button'); + } + + findDetailsTriggerButton() { + return this.find().findByTestId('kserve-model-row-item').find('button'); + } +} + export const projectListPage = new ProjectListPage(); export const createProjectModal = new CreateEditProjectModal(); export const editProjectModal = new CreateEditProjectModal(true); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 7b1e665d41..180db5d4da 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -29,6 +29,7 @@ import { } from '~/__mocks__/mockNimResource'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; +import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; export function findNimModelDeployButton(): Cypress.Chainable { return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); @@ -70,37 +71,39 @@ export function validateNvidiaNimModel( export function validateNimModelsTable(): void { // Table is visible and has 2 rows (2nd is the hidden expandable row) - cy.get('[data-testid="kserve-inference-service-table"]') - .find('tbody') - .find('tr') - .should('have.length', 2); + const kserveTable = projectDetails.findKserveModelsTable(); + kserveTable.find('tbody').find('tr').should('have.length', 2); // First row matches the NIM inference service details - cy.get('[style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); - cy.get('[data-label="Serving Runtime"]').should('have.text', 'NVIDIA NIM'); - cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); + const kserveTableRow = projectDetails.getKserveTableRow('Test Name'); + kserveTableRow.findColumn('Name').should('have.text', 'Test Name'); + kserveTableRow.findColumn('Serving Runtime').should('have.text', 'NVIDIA NIM'); + kserveTableRow.findColumn('Inference endpoint').should('have.text', 'Internal Service'); + kserveTableRow.findColumn('API protocol').should('have.text', 'REST'); + // Validate Internal Service tooltip and close it - cy.get('[data-testid="internal-service-button"]').click(); - cy.get('.pf-v5-c-popover__title-text').should( - 'have.text', - 'Internal Service can be accessed inside the cluster', - ); - cy.get('.pf-v5-c-popover__close > .pf-v5-c-button > .pf-v5-svg > path').click(); + kserveTableRow.findInternalServiceButton().click(); + kserveTableRow + .findInternalServicePopover() + .findByText('Internal Service can be accessed inside the cluster') + .should('exist'); + kserveTableRow.findInternalServicePopoverCloseButton().click(); + // Open toggle to validate Model details - cy.get('.pf-v5-c-table__toggle-icon').click(); - cy.get( - ':nth-child(1) > .pf-v5-c-description-list > .pf-v5-c-description-list__group > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', - ).should('have.text', 'arctic-embed-l'); - cy.get( - ':nth-child(2) > .pf-v5-c-description-list > :nth-child(1) > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', - ).should('have.text', '1'); - cy.get('.pf-v5-c-list > :nth-child(1)').should('have.text', 'Small'); - cy.get('.pf-v5-c-list > :nth-child(2)').should('have.text', '1 CPUs, 4Gi Memory requested'); - cy.get('.pf-v5-c-list > :nth-child(3)').should('have.text', '2 CPUs, 8Gi Memory limit'); - cy.get( - ':nth-child(3) > .pf-v5-c-description-list__description > .pf-v5-c-description-list__text', - ).should('have.text', 'No accelerator selected'); - cy.get('.pf-v5-c-table__toggle-icon').click(); + kserveTableRow.findDetailsTriggerButton().click(); + const kserveDetailsTableRow = projectDetails.getKserveTableDetailsRow('Test Name'); + kserveDetailsTableRow.findValueFor('Framework').should('have.text', 'arctic-embed-l'); + kserveDetailsTableRow.findValueFor('Model server replicas').should('have.text', '1'); + kserveDetailsTableRow.findValueFor('Model server size').should('contain.text', 'Small'); + kserveDetailsTableRow + .findValueFor('Model server size') + .should('contain.text', '1 CPUs, 4Gi Memory requested'); + kserveDetailsTableRow + .findValueFor('Model server size') + .should('contain.text', '2 CPUs, 8Gi Memory limit'); + kserveDetailsTableRow.findValueFor('Accelerator').should('have.text', 'No accelerator selected'); + + kserveTableRow.findDetailsTriggerButton().click(); } export function validateNimOverviewModelsTable(): void { From ec116f60de6a811b4e01b6a0324275fe4606d39e Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Mon, 7 Oct 2024 18:55:12 -0400 Subject: [PATCH 25/33] Update frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts Co-authored-by: Andrew Ballantyne <8126518+andrewballantyne@users.noreply.github.com> --- .../cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index da5c6ce592..243fd0b226 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -22,7 +22,7 @@ import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/Delete describe('NIM Model Serving', () => { describe('Deploying a model from an existing Project', () => { - it('should be disabled if the card is empty', () => { + it('should be disabled if the modal is empty', () => { initInterceptsToEnableNim({ hasAllModels: true }); projectDetails.visitSection('test-project', 'model-server'); From bdc58ab8c8a2ee400277e5ff9e5a325b5e6fca22 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Mon, 7 Oct 2024 19:02:51 -0400 Subject: [PATCH 26/33] test: fix linting errors Signed-off-by: Tomer Figenblat --- .../cypress/cypress/support/commands/odh.ts | 13 +++++++------ frontend/src/__tests__/cypress/cypress/types.ts | 2 +- .../src/__tests__/cypress/cypress/utils/nimUtils.ts | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index 73e73463c3..f14d23c7b3 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -1,4 +1,4 @@ -import { K8sResourceCommon, K8sResourceListResult, K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; +import type { K8sResourceListResult, K8sStatus } from '@openshift/dynamic-plugin-sdk-utils'; import type { GenericStaticResponse, RouteHandlerController } from 'cypress/types/net-stubbing'; import type { BaseMetricCreationResponse, BaseMetricListResponse } from '~/api'; import type { @@ -9,7 +9,7 @@ import type { RegisteredModel, RegisteredModelList, } from '~/concepts/modelRegistry/types'; -import { +import type { DashboardConfigKind, DataScienceClusterInitializationKindStatus, DataScienceClusterKindStatus, @@ -19,16 +19,17 @@ import { TemplateKind, NotebookKind, ModelRegistryKind, - ConsoleLinkKind, RoleBindingSubject, RoleBindingRoleRef, ConfigMapKind, SecretKind, + ConsoleLinkKind, } from '~/k8sTypes'; import type { StartNotebookData } from '~/pages/projects/types'; import type { AllowedUser } from '~/pages/notebookController/screens/admin/types'; import type { GroupsConfig } from '~/pages/groupSettings/groupTypes'; import type { StatusResponse } from '~/redux/types'; -import { +import type { BYONImage, - ClusterSettingsType, DetectedAccelerators, + ClusterSettingsType, + DetectedAccelerators, ImageInfo, OdhApplication, OdhDocument, @@ -54,7 +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 { NimServingResponse } from '~/__tests__/cypress/cypress/types'; +import type { NimServingResponse } from '~/__tests__/cypress/cypress/types'; type SuccessErrorResponse = { success: boolean; diff --git a/frontend/src/__tests__/cypress/cypress/types.ts b/frontend/src/__tests__/cypress/cypress/types.ts index c26889457f..ea00beb7ac 100644 --- a/frontend/src/__tests__/cypress/cypress/types.ts +++ b/frontend/src/__tests__/cypress/cypress/types.ts @@ -1,5 +1,5 @@ import type { RouteMatcher } from 'cypress/types/net-stubbing'; -import { ConfigMapKind, SecretKind } from '~/k8sTypes'; +import type { ConfigMapKind, SecretKind } from '~/k8sTypes'; export type Snapshot = { method: string; diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 180db5d4da..880180824f 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -21,7 +21,8 @@ import { mockNimImages, mockNimInferenceService, mockNimModelPVC, - mockNimProject, mockNimServingResource, + mockNimProject, + mockNimServingResource, mockNimServingRuntime, mockNimServingRuntimeTemplate, mockNvidiaNimAccessSecret, From eaa0e4709744979e6444eca9bdcb439f2c9d836f Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Tue, 8 Oct 2024 13:36:45 -0400 Subject: [PATCH 27/33] test: rewrite test case for global model list nim Signed-off-by: Tomer Figenblat --- .../cypress/cypress/pages/modelServing.ts | 8 ++++ .../mocked/modelServing/modelServingNim.cy.ts | 39 ++++++++++++++++--- .../cypress/cypress/utils/nimUtils.ts | 30 +------------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts index 50cc4f2fd8..d2dad593c7 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelServing.ts @@ -311,6 +311,14 @@ class InferenceServiceRow extends TableRow { findInternalServicePopover() { return cy.findByTestId('internal-service-popover'); } + + findServingRuntime() { + return this.find().find(`[data-label="Serving Runtime"]`); + } + + findProject() { + return this.find().find(`[data-label=Project]`); + } } class ServingPlatformCard extends Contextual { findDeployModelButton() { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index 3c20f002f8..ccc0d66780 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -1,14 +1,14 @@ -import { - initInterceptsToEnableNim, - validateNimInmferenceModelsTable, -} from '~/__tests__/cypress/cypress/utils/nimUtils'; +import { initInterceptsToEnableNim } from '~/__tests__/cypress/cypress/utils/nimUtils'; import { mockNimInferenceService, mockNimServingRuntime } from '~/__mocks__/mockNimResource'; import { InferenceServiceModel, ServingRuntimeModel, } from '~/__tests__/cypress/cypress/utils/models'; import { mockK8sResourceList } from '~/__mocks__'; -import { modelServingGlobal } from '~/__tests__/cypress/cypress/pages/modelServing'; +import { + modelServingGlobal, + modelServingSection, +} from '~/__tests__/cypress/cypress/pages/modelServing'; describe('NIM Models Deployments', () => { it('should be listed in the global models list', () => { @@ -18,7 +18,34 @@ describe('NIM Models Deployments', () => { modelServingGlobal.visit('test-project'); - validateNimInmferenceModelsTable(); + // Table is visible and has 1 row + modelServingSection.findInferenceServiceTable().should('have.length', 1); + + // First row matches the NIM inference service details + modelServingSection + .getInferenceServiceRow('Test Name') + .findProject() + .should('contains.text', 'Test Project'); + modelServingSection + .getInferenceServiceRow('Test Name') + .findProject() + .should('contains.text', 'Single-model serving enabled'); + modelServingSection + .getInferenceServiceRow('Test Name') + .findServingRuntime() + .should('have.text', 'NVIDIA NIM'); + modelServingSection + .getInferenceServiceRow('Test Name') + .findAPIProtocol() + .should('have.text', 'REST'); + + // Validate Internal Service tooltip and close it + modelServingSection.getInferenceServiceRow('Test Name').findInternalServiceButton().click(); + modelServingSection + .getInferenceServiceRow('Test Name') + .findInternalServicePopover() + .should('contain.text', 'Internal Service can be accessed inside the cluster') + .click(); }); it('should only be allowed to be deleted, no edit', () => { diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 880180824f..bee2edf49b 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -31,6 +31,7 @@ import { import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; +import { kserveModal, modelServingGlobal, modelServingSection } from '~/__tests__/cypress/cypress/pages/modelServing'; export function findNimModelDeployButton(): Cypress.Chainable { return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); @@ -132,35 +133,6 @@ export function validateNimOverviewModelsTable(): void { cy.get('.pf-m-gap-md > :nth-child(2) > .pf-v5-c-button').click(); } -export function validateNimInmferenceModelsTable(): void { - // Table is visible and has 1 row - cy.get('[data-testid="inference-service-table"]') - .find('tbody') - .find('tr') - .should('have.length', 1); - // First row matches the NIM inference service details - cy.get('[style="display: block;"] > :nth-child(1)').should('have.text', 'Test Name'); - cy.get('[data-label="Project"]').should('contains.text', 'Test Project'); - cy.get( - '[data-label="Project"] > .pf-v5-c-label > .pf-v5-c-label__content > .pf-v5-c-label__text', - ).should('have.text', 'Single-model serving enabled'); - cy.get('[data-label="Serving Runtime"]').should('have.text', 'NVIDIA NIM'); - // Validate Internal Service tooltip and close it - cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); - cy.get('[data-testid="internal-service-button"]').click(); - cy.get('.pf-v5-c-popover__title-text').should( - 'have.text', - 'Internal Service can be accessed inside the cluster', - ); - cy.get('.pf-v5-c-popover__close > .pf-v5-c-button > .pf-v5-svg > path').click(); - cy.get( - '[data-label="API protocol"] > .pf-v5-c-label > .pf-v5-c-label__content > .pf-v5-c-label__text', - ).should('have.text', 'REST'); - cy.get('[data-testid="status-tooltip"] > .pf-v5-c-icon__content > .pf-v5-svg > path').should( - 'be.visible', - ); -} - /* ################################################### ###### Interception Initialization Utilities ###### ################################################### */ From dd7ac09467216167c011fdac006bddda00845df8 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Tue, 8 Oct 2024 14:39:06 -0400 Subject: [PATCH 28/33] test: fix linting errors Signed-off-by: Tomer Figenblat --- frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index bee2edf49b..12ac960f2b 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -31,7 +31,6 @@ import { import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; -import { kserveModal, modelServingGlobal, modelServingSection } from '~/__tests__/cypress/cypress/pages/modelServing'; export function findNimModelDeployButton(): Cypress.Chainable { return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); From 7c06f11efdd6e0fdbe6dd4ed460796b7ea8202d5 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Tue, 8 Oct 2024 15:39:40 -0400 Subject: [PATCH 29/33] test: rewrite test case for project overview list nim Signed-off-by: Tomer Figenblat --- .../cypress/cypress/pages/projects.ts | 15 +++++++++++ .../mocked/projects/modelServingNim.cy.ts | 16 +++++++----- .../cypress/cypress/utils/nimUtils.ts | 25 ------------------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 96f053239f..eab631a488 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -410,9 +410,24 @@ class KserveTableRow extends TableRow { } } +class ProjectDetailsOverviewTab extends ProjectDetails { + visit(project: string) { + super.visitSection(project, 'overview'); + } + + findDeployedModel(name: string) { + return cy + .findByTestId('section-overview') + .get('div') + .contains(name) + .parents('.odh-type-bordered-card .model-server'); + } +} + 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(); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 243fd0b226..844d702357 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -4,7 +4,10 @@ import { InferenceServiceModel, ServingRuntimeModel, } from '~/__tests__/cypress/cypress/utils/models'; -import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; +import { + projectDetails, + projectDetailsOverviewTab, +} from '~/__tests__/cypress/cypress/pages/projects'; import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; import { findNimModelDeployButton, @@ -15,7 +18,6 @@ import { initInterceptsToEnableNim, modalDialogTitle, validateNimModelsTable, - validateNimOverviewModelsTable, validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; @@ -107,11 +109,13 @@ describe('NIM Model Serving', () => { cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([mockNimInferenceService()])); cy.interceptK8sList(ServingRuntimeModel, mockK8sResourceList([mockNimServingRuntime()])); - projectDetails.visitSection('test-project', 'overview'); + projectDetails.visit('test-project'); - validateNimOverviewModelsTable(); - validateNimModelsTable(); - projectDetails.visitSection('test-project', 'overview'); + // Card is visible + projectDetailsOverviewTab + .findDeployedModel('Test Name') + .get('dd') + .should('have.text', 'NVIDIA NIM'); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 12ac960f2b..abee1b9c4c 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -107,31 +107,6 @@ export function validateNimModelsTable(): void { kserveTableRow.findDetailsTriggerButton().click(); } -export function validateNimOverviewModelsTable(): void { - // Card is visible - cy.get( - '.pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(2) > .pf-v5-c-content > h3 > b', - ).should('be.visible'); - cy.get( - '.pf-v5-l-gallery > :nth-child(1) > .pf-v5-c-card > .pf-v5-c-card__header > .pf-v5-c-card__header-main > .pf-v5-l-flex > :nth-child(1)', - ).should('be.visible'); - // Validate card details - cy.get(':nth-child(2) > [style="display: block;"] > :nth-child(1)').should( - 'have.text', - 'Test Name', - ); - cy.get('dt').should('have.text', 'Serving runtime'); - cy.get('dd').should('have.text', 'NVIDIA NIM'); - cy.get('[data-testid="internal-service-button"]').should('have.text', 'Internal Service'); - cy.get('[data-testid="internal-service-button"]').click(); - cy.get('.pf-v5-c-popover__title-text').should( - 'have.text', - 'Internal Service can be accessed inside the cluster', - ); - // Opens the Models table - cy.get('.pf-m-gap-md > :nth-child(2) > .pf-v5-c-button').click(); -} - /* ################################################### ###### Interception Initialization Utilities ###### ################################################### */ From aed96854ef4cad842ff45f4230c44f777ab355ef Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Tue, 8 Oct 2024 19:23:10 -0400 Subject: [PATCH 30/33] test: rewrite test case for project models tab list nim Signed-off-by: Tomer Figenblat --- .../cypress/cypress/pages/projects.ts | 73 ++++--------------- .../mocked/projects/modelServingNim.cy.ts | 39 +++++++++- .../cypress/cypress/utils/nimUtils.ts | 38 ---------- 3 files changed, 52 insertions(+), 98 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index eab631a488..e961f4eafb 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -303,17 +303,6 @@ class ProjectDetails { .closest('tr'), ); } - - getKserveTableDetailsRow(name: string) { - return new KserveTableDetailsRow(() => - this.findKserveModelsTable() - .find('tbody') - .find('[data-label="Name"]') - .contains(name) - .closest('tr') - .next('tr'), - ); - } } class ProjectDetailsSettingsTab extends ProjectDetails { @@ -356,71 +345,39 @@ class TrustyAIUninstallModal extends DeleteModal { } } -class KserveTableDetailsRow extends TableRow { - private findDetailsCell() { - return this.find().find('td').eq(1); +class ProjectDetailsOverviewTab extends ProjectDetails { + visit(project: string) { + super.visitSection(project, 'overview'); } - findValueFor(label: string) { - return this.findDetailsCell().find('dt').contains(label).closest('div').find('dd'); + findDeployedModel(name: string) { + return cy + .findByTestId('section-overview') + .get('div') + .contains(name) + .parents('.odh-type-bordered-card .model-server'); } } class KserveTableRow extends TableRow { - findColumn(name: string) { - return this.find().find(`[data-label="${name}"]`); - } - - findStatusTooltip() { - return this.find() - .findByTestId('status-tooltip') - .trigger('mouseenter') - .then(() => { - cy.findByTestId('model-status-tooltip'); - }); - } - - findStatusTooltipValue(msg: string) { - this.findStatusTooltip() - .invoke('text') - .should('contain', msg) - .then(() => { - this.findStatusTooltip().trigger('mouseleave'); - }); - } - findAPIProtocol() { return this.find().find(`[data-label="API protocol"]`); } - findInternalServiceButton() { - return this.find().findByTestId('internal-service-button'); - } - - findInternalServicePopover() { - return cy.findByTestId('internal-service-popover'); - } - - findInternalServicePopoverCloseButton() { - return this.findInternalServicePopover().find('button'); + findServiceRuntime() { + return this.find().find(`[data-label="Serving Runtime"]`); } findDetailsTriggerButton() { return this.find().findByTestId('kserve-model-row-item').find('button'); } -} -class ProjectDetailsOverviewTab extends ProjectDetails { - visit(project: string) { - super.visitSection(project, 'overview'); + private findDetailsCell() { + return this.find().next('tr').find('td').eq(1); } - findDeployedModel(name: string) { - return cy - .findByTestId('section-overview') - .get('div') - .contains(name) - .parents('.odh-type-bordered-card .model-server'); + findInfoValueFor(label: string) { + return this.findDetailsCell().find('dt').contains(label).closest('div').find('dd'); } } diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 844d702357..b339a52a9f 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -17,7 +17,6 @@ import { initInterceptsToDeployModel, initInterceptsToEnableNim, modalDialogTitle, - validateNimModelsTable, validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; @@ -101,7 +100,43 @@ describe('NIM Model Serving', () => { projectDetails.visitSection('test-project', 'model-server'); - validateNimModelsTable(); + // Table is visible and has 1 row + projectDetails.findKserveModelsTable().should('have.length', 1); + + // First row matches the NIM inference service details + projectDetails + .getKserveTableRow('Test Name') + .findServiceRuntime() + .should('have.text', 'NVIDIA NIM'); + projectDetails.getKserveTableRow('Test Name').findAPIProtocol().should('have.text', 'REST'); + + // Open toggle to validate Model details + projectDetails.getKserveTableRow('Test Name').findDetailsTriggerButton().click(); + + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Framework') + .should('have.text', 'arctic-embed-l'); + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Model server replicas') + .should('have.text', '1'); + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Model server size') + .should('contain.text', 'Small'); + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Model server size') + .should('contain.text', '1 CPUs, 4Gi Memory requested'); + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Model server size') + .should('contain.text', '2 CPUs, 8Gi Memory limit'); + projectDetails + .getKserveTableRow('Test Name') + .findInfoValueFor('Accelerator') + .should('have.text', 'No accelerator selected'); }); it('should list the deployed model in Overview tab', () => { diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index abee1b9c4c..695eb5b03c 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -30,7 +30,6 @@ import { } from '~/__mocks__/mockNimResource'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; -import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; export function findNimModelDeployButton(): Cypress.Chainable { return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); @@ -70,43 +69,6 @@ export function validateNvidiaNimModel( cy.contains(modalDialogTitle).should('not.exist'); } -export function validateNimModelsTable(): void { - // Table is visible and has 2 rows (2nd is the hidden expandable row) - const kserveTable = projectDetails.findKserveModelsTable(); - kserveTable.find('tbody').find('tr').should('have.length', 2); - - // First row matches the NIM inference service details - const kserveTableRow = projectDetails.getKserveTableRow('Test Name'); - kserveTableRow.findColumn('Name').should('have.text', 'Test Name'); - kserveTableRow.findColumn('Serving Runtime').should('have.text', 'NVIDIA NIM'); - kserveTableRow.findColumn('Inference endpoint').should('have.text', 'Internal Service'); - kserveTableRow.findColumn('API protocol').should('have.text', 'REST'); - - // Validate Internal Service tooltip and close it - kserveTableRow.findInternalServiceButton().click(); - kserveTableRow - .findInternalServicePopover() - .findByText('Internal Service can be accessed inside the cluster') - .should('exist'); - kserveTableRow.findInternalServicePopoverCloseButton().click(); - - // Open toggle to validate Model details - kserveTableRow.findDetailsTriggerButton().click(); - const kserveDetailsTableRow = projectDetails.getKserveTableDetailsRow('Test Name'); - kserveDetailsTableRow.findValueFor('Framework').should('have.text', 'arctic-embed-l'); - kserveDetailsTableRow.findValueFor('Model server replicas').should('have.text', '1'); - kserveDetailsTableRow.findValueFor('Model server size').should('contain.text', 'Small'); - kserveDetailsTableRow - .findValueFor('Model server size') - .should('contain.text', '1 CPUs, 4Gi Memory requested'); - kserveDetailsTableRow - .findValueFor('Model server size') - .should('contain.text', '2 CPUs, 8Gi Memory limit'); - kserveDetailsTableRow.findValueFor('Accelerator').should('have.text', 'No accelerator selected'); - - kserveTableRow.findDetailsTriggerButton().click(); -} - /* ################################################### ###### Interception Initialization Utilities ###### ################################################### */ From 1cafc5dab179a3518160da7c6b50141aba99ca84 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Tue, 8 Oct 2024 19:31:15 -0400 Subject: [PATCH 31/33] test: cleanups Signed-off-by: Tomer Figenblat --- .../tests/mocked/modelServing/modelServingNim.cy.ts | 8 -------- frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts | 4 ---- 2 files changed, 12 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts index ccc0d66780..62933c8a8c 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingNim.cy.ts @@ -38,14 +38,6 @@ describe('NIM Models Deployments', () => { .getInferenceServiceRow('Test Name') .findAPIProtocol() .should('have.text', 'REST'); - - // Validate Internal Service tooltip and close it - modelServingSection.getInferenceServiceRow('Test Name').findInternalServiceButton().click(); - modelServingSection - .getInferenceServiceRow('Test Name') - .findInternalServicePopover() - .should('contain.text', 'Internal Service can be accessed inside the cluster') - .click(); }); it('should only be allowed to be deleted, no edit', () => { diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 695eb5b03c..271b7c9d10 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -39,10 +39,6 @@ export function findNimModelServingPlatformCard(): Cypress.Chainable { return cy.findByTestId('nvidia-nim-model-serving-platform-card'); } -export function findServingPlatformLabel(): Cypress.Chainable { - return cy.findByTestId('serving-platform-label'); -} - export const modalDialogTitle = 'Deploy model with NVIDIA NIM'; export function validateNvidiaNimModel( From cb01605f999808a72e3cbbc7810bdb80ef759277 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Wed, 9 Oct 2024 09:40:23 -0400 Subject: [PATCH 32/33] test: rewrite test cases nim enablement Signed-off-by: Tomer Figenblat --- .../NIMDeployModal.ts} | 4 + .../cypress/cypress/pages/projects.ts | 6 +- .../mocked/projects/modelServingNim.cy.ts | 211 +++++++----------- .../cypress/cypress/utils/nimUtils.ts | 24 -- 4 files changed, 91 insertions(+), 154 deletions(-) rename frontend/src/__tests__/cypress/cypress/pages/{nimModelDialog.ts => components/NIMDeployModal.ts} (92%) diff --git a/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts b/frontend/src/__tests__/cypress/cypress/pages/components/NIMDeployModal.ts similarity index 92% rename from frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts rename to frontend/src/__tests__/cypress/cypress/pages/components/NIMDeployModal.ts index 0e586b4fff..3bd40637e3 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/nimModelDialog.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/components/NIMDeployModal.ts @@ -40,6 +40,10 @@ class NIMDeployModal extends Modal { findNimModelReplicasPlusButton() { return this.find().find('button[aria-label="Plus"]').eq(1); } + + shouldDisplayError(msg: string): void { + this.find().should('contain.text', msg); + } } export const nimDeployModal = new NIMDeployModal(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index e961f4eafb..4f634d5464 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -181,7 +181,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`); } @@ -357,6 +357,10 @@ class ProjectDetailsOverviewTab extends ProjectDetails { .contains(name) .parents('.odh-type-bordered-card .model-server'); } + + findModelServingPlatform(name: string) { + return cy.findByTestId(`${name}-platform-card`); + } } class KserveTableRow extends TableRow { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index b339a52a9f..5e6ab2e185 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -8,16 +8,14 @@ import { projectDetails, projectDetailsOverviewTab, } from '~/__tests__/cypress/cypress/pages/projects'; -import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/nimModelDialog'; +import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/components/NIMDeployModal'; import { findNimModelDeployButton, - findNimModelServingPlatformCard, initInterceptorsValidatingNimEnablement, initInterceptsForDeleteModel, initInterceptsToDeployModel, initInterceptsToEnableNim, modalDialogTitle, - validateNvidiaNimModel, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; @@ -152,19 +150,25 @@ describe('NIM Model Serving', () => { .get('dd') .should('have.text', 'NVIDIA NIM'); }); + + it('should be blocked if failed to fetch NIM model list', () => { + initInterceptsToEnableNim({}); + projectDetailsOverviewTab.visit('test-project'); + cy.findByTestId('model-serving-platform-button').click(); + nimDeployModal.shouldDisplayError( + 'There was a problem fetching the NIM models. Please try again later.', + ); + nimDeployModal.findSubmitButton().should('be.disabled'); + }); }); describe('Enabling NIM', () => { describe('When NIM feature is enabled', () => { it("should allow deploying NIM from a Project's Overview tab when the only platform", () => { initInterceptsToEnableNim({}); - const componentName = 'overview'; - projectDetails.visitSection('test-project', componentName); - const overviewComponent = projectDetails.findComponent(componentName); - overviewComponent.should('exist'); - const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); - deployModelButton.should('exist'); - validateNvidiaNimModel(deployModelButton); + projectDetailsOverviewTab.visit('test-project'); + cy.findByTestId('model-serving-platform-button').click(); + nimDeployModal.shouldBeOpen(); }); it("should allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { @@ -173,48 +177,19 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }); - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') + projectDetailsOverviewTab.visit('test-project'); + projectDetailsOverviewTab + .findModelServingPlatform('nvidia-nim') .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .findByTestId('nvidia-nim-platform-card') - .should('contain', 'NVIDIA NIM model serving platform') - .and('contain', 'Models are deployed using NVIDIA NIM microservices.'); - - validateNvidiaNimModel( - projectDetails - .findComponent('overview') - .findByTestId('nvidia-nim-platform-card') - .findByTestId('model-serving-platform-button'), - ); + .click(); + nimDeployModal.shouldBeOpen(); }); it("should allow deploying NIM from a Project's Models tab when the only platform", () => { initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('exist'); - - cy.contains('Start by adding a model server'); - cy.contains( - 'Model servers are used to deploy models and to allow apps to send requests to your models. Configuring a model server includes specifying the number of replicas being deployed, the server size, the token authentication, the serving runtime, and how the project that the model server belongs to is accessed.', - ); - - const deployButton = projectDetails - .findComponent('model-server') - .findByTestId('deploy-button'); - validateNvidiaNimModel(deployButton); + cy.get('button').contains('Deploy model').click(); // TODO button has testid? + nimDeployModal.shouldBeOpen(); }); it("should allow deploying NIM from a Project's Models tab when multiple platforms exist", () => { @@ -223,62 +198,45 @@ describe('NIM Model Serving', () => { disableModelMesh: false, disableNIMModelServing: false, }); - projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard() - .should('contain', 'Models are deployed using NVIDIA NIM microservices.') - .and('contain', 'NVIDIA NIM model serving platform'); - - validateNvidiaNimModel(findNimModelDeployButton()); - }); - - it('should display an error when failed to fetch NIM model list', () => { - initInterceptsToEnableNim({}); - const componentName = 'overview'; - projectDetails.visitSection('test-project', componentName); - const overviewComponent = projectDetails.findComponent(componentName); - overviewComponent.should('exist'); - const deployModelButton = overviewComponent.findByTestId('model-serving-platform-button'); - deployModelButton.should('exist'); - deployModelButton.click(); - cy.contains('There was a problem fetching the NIM models. Please try again later.'); + projectDetails + .findModelServingPlatform('nvidia-nim-model') + .findByTestId('nim-serving-deploy-button') + .click(); + nimDeployModal.shouldBeOpen(); }); }); describe('When NIM feature is disabled', () => { + it("should NOT allow deploying NIM from a Project's Overview tab when the only platform", () => { + initInterceptorsValidatingNimEnablement({ + disableKServe: true, + disableModelMesh: true, + disableNIMModelServing: true, + }); + projectDetailsOverviewTab.visit('test-project'); + cy.findByTestId('model-serving-platform-button').should('not.exist'); + }); + it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { initInterceptorsValidatingNimEnablement({ disableKServe: false, disableModelMesh: false, disableNIMModelServing: true, }); + projectDetailsOverviewTab.visit('test-project'); + projectDetailsOverviewTab.findModelServingPlatform('nvidia-nim').should('not.exist'); + cy.findByTestId('model-serving-platform-button').should('not.exist'); + }); - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .find('[data-testid="nvidia-nim-platform-card"]') - .should('not.exist'); - - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + it("should NOT allow deploying NIM from a Project's Models tab when the only platform", () => { + initInterceptorsValidatingNimEnablement({ + disableKServe: true, + disableModelMesh: true, + disableNIMModelServing: true, + }); + projectDetails.visitSection('test-project', 'model-server'); + cy.get('button').contains('Deploy model').should('not.exist'); // TODO button has testid? }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { @@ -288,20 +246,13 @@ describe('NIM Model Serving', () => { disableNIMModelServing: true, }); projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard().should('not.exist'); - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + projectDetails.findModelServingPlatform('nvidia-nim-model').should('not.exist'); + cy.findByTestId('nim-serving-deploy-button').should('not.exist'); }); }); - describe('When missing the Template', () => { - it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { + describe('When the Template is missing', () => { + it("should NOT allow deploying NIM from a Project's Overview tab when the only platform", () => { initInterceptorsValidatingNimEnablement( { disableKServe: false, @@ -310,27 +261,36 @@ describe('NIM Model Serving', () => { }, true, ); + projectDetailsOverviewTab.visit('test-project'); + cy.findByTestId('model-serving-platform-button').should('not.exist'); + }); - projectDetails.visitSection('test-project', 'overview'); - - projectDetails - .findComponent('overview') - .findByTestId('single-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - projectDetails - .findComponent('overview') - .findByTestId('multi-serving-platform-card') - .findByTestId('model-serving-platform-button') - .should('exist'); - - projectDetails - .findComponent('overview') - .find('[data-testid="nvidia-nim-platform-card"]') - .should('not.exist'); + it("should NOT allow deploying NIM from a Project's Overview tab when multiple platforms exist", () => { + initInterceptorsValidatingNimEnablement( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + projectDetailsOverviewTab.visit('test-project'); + projectDetailsOverviewTab.findModelServingPlatform('nvidia-nim').should('not.exist'); + cy.findByTestId('model-serving-platform-button').should('not.exist'); + }); - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + it("should NOT allow deploying NIM from a Project's Models tab when the only platform", () => { + initInterceptorsValidatingNimEnablement( + { + disableKServe: false, + disableModelMesh: false, + disableNIMModelServing: false, + }, + true, + ); + projectDetails.visitSection('test-project', 'model-server'); + cy.get('button').contains('Deploy model').click(); // TODO button has testid? + nimDeployModal.shouldBeOpen(false); }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { @@ -343,15 +303,8 @@ describe('NIM Model Serving', () => { true, ); projectDetails.visitSection('test-project', 'model-server'); - projectDetails.shouldBeEmptyState('Models', 'model-server', true); - projectDetails.findServingPlatformLabel().should('not.exist'); - - projectDetails.findSingleModelDeployButton().should('exist'); - projectDetails.findMultiModelButton().should('exist'); - - findNimModelServingPlatformCard().should('not.exist'); - cy.contains('NVIDIA NIM model serving platform').should('not.exist'); - cy.contains('Models are deployed using NVIDIA NIM microservices.').should('not.exist'); + projectDetails.findModelServingPlatform('nvidia-nim-model').should('not.exist'); + cy.findByTestId('nim-serving-deploy-button').should('not.exist'); }); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index 271b7c9d10..d2e1c2bb2b 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -41,30 +41,6 @@ export function findNimModelServingPlatformCard(): Cypress.Chainable { export const modalDialogTitle = 'Deploy model with NVIDIA NIM'; -export function validateNvidiaNimModel( - deployButtonElement: Cypress.Chainable>, -): void { - deployButtonElement.click(); - cy.contains(modalDialogTitle); - cy.contains('Configure properties for deploying your model using an NVIDIA NIM.'); - - //find the form label Project with value as the Test Project - cy.contains('label', 'Project').parent().next().find('p').should('have.text', 'Test Project'); - - //close the model window - cy.get('div[role="dialog"]').get('button[aria-label="Close"]').click(); - - // now the nvidia nim window should not be visible. - cy.contains(modalDialogTitle).should('not.exist'); - - deployButtonElement.click(); - //validate model submit button is disabled without entering form data - cy.findByTestId('modal-submit-button').should('be.disabled'); - //validate nim modal cancel button - cy.findByTestId('modal-cancel-button').click(); - cy.contains(modalDialogTitle).should('not.exist'); -} - /* ################################################### ###### Interception Initialization Utilities ###### ################################################### */ From bd71dc4b5907b131a208c5f4ae634d57bf022a21 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Wed, 9 Oct 2024 10:52:22 -0400 Subject: [PATCH 33/33] test: final touchups Signed-off-by: Tomer Figenblat --- .../cypress/cypress/pages/projects.ts | 5 ++-- .../mocked/projects/modelServingNim.cy.ts | 24 ++++++++----------- .../cypress/cypress/utils/nimUtils.ts | 10 -------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 4f634d5464..5aded2111c 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -350,12 +350,13 @@ class ProjectDetailsOverviewTab extends ProjectDetails { super.visitSection(project, 'overview'); } - findDeployedModel(name: string) { + findDeployedModelServingRuntime(name: string) { return cy .findByTestId('section-overview') .get('div') .contains(name) - .parents('.odh-type-bordered-card .model-server'); + .parents('.odh-type-bordered-card .model-server') + .get('dd'); } findModelServingPlatform(name: string) { diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts index 5e6ab2e185..26c98337b4 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/modelServingNim.cy.ts @@ -10,12 +10,10 @@ import { } from '~/__tests__/cypress/cypress/pages/projects'; import { nimDeployModal } from '~/__tests__/cypress/cypress/pages/components/NIMDeployModal'; import { - findNimModelDeployButton, initInterceptorsValidatingNimEnablement, initInterceptsForDeleteModel, initInterceptsToDeployModel, initInterceptsToEnableNim, - modalDialogTitle, } from '~/__tests__/cypress/cypress/utils/nimUtils'; import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; @@ -26,23 +24,23 @@ describe('NIM Model Serving', () => { projectDetails.visitSection('test-project', 'model-server'); // For multiple cards use case - findNimModelDeployButton().click(); - cy.contains(modalDialogTitle).should('be.visible'); + projectDetails + .findModelServingPlatform('nvidia-nim-model') + .findByTestId('nim-serving-deploy-button') + .click(); // test that you can not submit on empty nimDeployModal.shouldBeOpen(); nimDeployModal.findSubmitButton().should('be.disabled'); }); - it('should be enabled if the card has the minimal info', () => { + it('should be enabled if the modal has the minimal info', () => { initInterceptsToEnableNim({}); const nimInferenceService = mockNimInferenceService(); initInterceptsToDeployModel(nimInferenceService); projectDetails.visitSection('test-project', 'model-server'); - cy.findByTestId('deploy-button').should('exist'); - cy.findByTestId('deploy-button').click(); - cy.contains(modalDialogTitle).should('be.visible'); + cy.get('button[data-testid=deploy-button]').click(); // test that you can not submit on empty nimDeployModal.shouldBeOpen(); @@ -146,8 +144,7 @@ describe('NIM Model Serving', () => { // Card is visible projectDetailsOverviewTab - .findDeployedModel('Test Name') - .get('dd') + .findDeployedModelServingRuntime('Test Name') .should('have.text', 'NVIDIA NIM'); }); @@ -188,7 +185,7 @@ describe('NIM Model Serving', () => { it("should allow deploying NIM from a Project's Models tab when the only platform", () => { initInterceptsToEnableNim({}); projectDetails.visitSection('test-project', 'model-server'); - cy.get('button').contains('Deploy model').click(); // TODO button has testid? + cy.get('button[data-testid=deploy-button]').click(); nimDeployModal.shouldBeOpen(); }); @@ -236,7 +233,7 @@ describe('NIM Model Serving', () => { disableNIMModelServing: true, }); projectDetails.visitSection('test-project', 'model-server'); - cy.get('button').contains('Deploy model').should('not.exist'); // TODO button has testid? + cy.get('button[data-testid=deploy-button]').should('not.exist'); }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { @@ -289,8 +286,7 @@ describe('NIM Model Serving', () => { true, ); projectDetails.visitSection('test-project', 'model-server'); - cy.get('button').contains('Deploy model').click(); // TODO button has testid? - nimDeployModal.shouldBeOpen(false); + cy.get('button[data-testid=deploy-button]').should('not.exist'); }); it("should NOT allow deploying NIM to a Project's Models tab when multiple platforms exist", () => { diff --git a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts index d2e1c2bb2b..e11cb1d7cb 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/nimUtils.ts @@ -31,16 +31,6 @@ import { import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import type { InferenceServiceKind } from '~/k8sTypes'; -export function findNimModelDeployButton(): Cypress.Chainable { - return findNimModelServingPlatformCard().findByTestId('nim-serving-deploy-button'); -} - -export function findNimModelServingPlatformCard(): Cypress.Chainable { - return cy.findByTestId('nvidia-nim-model-serving-platform-card'); -} - -export const modalDialogTitle = 'Deploy model with NVIDIA NIM'; - /* ################################################### ###### Interception Initialization Utilities ###### ################################################### */