-
Notifications
You must be signed in to change notification settings - Fork 162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Task/APPENG-2738: NIM Cypress E2E tests #3298
base: main
Are you sure you want to change the base?
Changes from 20 commits
9947f96
ba0f98c
d103247
b1e48d3
2c7152d
2424ab3
8175aa6
1d80e39
b662f9f
47306a1
34ab9df
e57074a
b5c58a8
db769a1
6d82381
5de4b14
a765cb9
e23d11a
ca6ef78
b1c2492
c480d4e
baf470e
7da5544
eba0293
ec116f6
bdc58ab
eaa0e47
dd7ac09
7c06f11
aed9685
1cafc5d
cb01605
bd71dc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
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({ | ||
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: 'test-name', | ||
displayName: 'Test Name', | ||
kserveInternalLabel: true, | ||
resources: { | ||
limits: { cpu: '2', memory: '8Gi' }, | ||
requests: { cpu: '1', memory: '4Gi' }, | ||
}, | ||
}); | ||
delete inferenceService.metadata.labels?.name; | ||
delete inferenceService.metadata.creationTimestamp; | ||
delete inferenceService.metadata.generation; | ||
delete inferenceService.metadata.resourceVersion; | ||
delete inferenceService.metadata.uid; | ||
if (inferenceService.spec.predictor.model?.modelFormat) { | ||
inferenceService.spec.predictor.model.modelFormat.name = 'arctic-embed-l'; | ||
} | ||
delete inferenceService.spec.predictor.model?.modelFormat?.version; | ||
delete inferenceService.spec.predictor.model?.storage; | ||
delete inferenceService.status; | ||
|
||
return inferenceService; | ||
}; | ||
|
||
export const mockNimServingRuntime = (): ServingRuntimeKind => { | ||
const servingRuntime = mockServingRuntimeK8sResource({ | ||
name: 'test-name', | ||
displayName: 'Test Name', | ||
}); | ||
if (servingRuntime.metadata.annotations) { | ||
servingRuntime.metadata.annotations['opendatahub.io/template-display-name'] = 'NVIDIA NIM'; | ||
servingRuntime.metadata.annotations['opendatahub.io/template-name'] = 'nvidia-nim-runtime'; | ||
} | ||
|
||
return servingRuntime; | ||
}; | ||
|
||
export const mockNimServingRuntimeTemplate = (): TemplateKind => { | ||
const templateMock = mockServingRuntimeTemplateK8sResource({ | ||
name: 'nvidia-nim-serving-template', | ||
displayName: 'NVIDIA NIM', | ||
platforms: [ServingRuntimePlatform.SINGLE], | ||
apiProtocol: ServingRuntimeAPIProtocol.REST, | ||
namespace: 'opendatahub', | ||
}); | ||
if (templateMock.metadata.annotations != null) { | ||
templateMock.metadata.annotations['opendatahub.io/dashboard'] = 'true'; | ||
} | ||
|
||
return templateMock; | ||
}; | ||
|
||
export const mockNvidiaNimAccessSecret = (): SecretKind => { | ||
const secret = mockSecretK8sResource({ | ||
name: 'nvidia-nim-access', | ||
}); | ||
delete secret.data; | ||
secret.data = {}; | ||
secret.data.api_key = 'api-key'; // eslint-disable-line camelcase | ||
secret.data.configMapName = 'bnZpZGlhLW5pbS12YWxpZGF0aW9uLXJlc3VsdA=='; | ||
|
||
return secret; | ||
}; | ||
|
||
export const mockNvidiaNimImagePullSecret = (): SecretKind => { | ||
const secret = mockSecretK8sResource({ | ||
name: 'nvidia-nim-image-pull', | ||
}); | ||
delete secret.data; | ||
secret.data = {}; | ||
secret.data['.dockerconfigjson'] = 'ZG9ja2VyY29uZmlnCg=='; | ||
|
||
return secret; | ||
}; | ||
|
||
export const mockNimModelPVC = (): PersistentVolumeClaimKind => { | ||
const pvc = mockPVCK8sResource({ | ||
name: 'nim-pvc', | ||
}); | ||
return pvc; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this needed? Changing base configs I imagine is not necessary here... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it was helpful to debug by running the one testcase and add some html code by enabling this flag. it is disabled now if you want I am happy to take it out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved here: baf470e |
||
experimentalMemoryManagement: true, | ||
// Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406 | ||
reporter: '../../../node_modules/cypress-multi-reporters', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal'; | ||
|
||
class NIMDeployModal extends Modal { | ||
constructor(private edit = false) { | ||
super(`${edit ? 'Edit' : 'Deploy'} model with NVIDIA NIM`); | ||
} | ||
|
||
findSubmitButton() { | ||
return this.findFooter().findByTestId('modal-submit-button'); | ||
} | ||
|
||
findModelNameInput() { | ||
return this.find().findByTestId('model-deployment-name-section'); | ||
} | ||
|
||
findNIMToDeploy() { | ||
return this.find().findByTestId('nim-model-list-selection'); | ||
} | ||
|
||
findNimStorageSizeInput() { | ||
return cy.get('[data-testid="pvc-size"] input'); | ||
} | ||
|
||
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(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps to make this easier to read just make it a normal JS object and do
JSON.stringify
if you need a single string value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved here: baf470e