312 lines
12 KiB
Go
312 lines
12 KiB
Go
package core_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kubeflow/model-registry/internal/apiutils"
|
|
"github.com/kubeflow/model-registry/internal/core"
|
|
"github.com/kubeflow/model-registry/pkg/openapi"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestPartialUpdateBehavior verifies that partial updates only modify provided fields
|
|
func TestPartialUpdateBehavior(t *testing.T) {
|
|
service, cleanup := SetupModelRegistryService(t)
|
|
defer cleanup()
|
|
|
|
t.Run("Experiment partial update preserves existing fields", func(t *testing.T) {
|
|
testExperimentPartialUpdate(t, service)
|
|
})
|
|
|
|
t.Run("RegisteredModel partial update preserves existing fields", func(t *testing.T) {
|
|
testRegisteredModelPartialUpdate(t, service)
|
|
})
|
|
|
|
t.Run("ModelVersion partial update preserves existing fields", func(t *testing.T) {
|
|
testModelVersionPartialUpdate(t, service)
|
|
})
|
|
|
|
t.Run("Artifact partial update preserves existing fields", func(t *testing.T) {
|
|
testArtifactPartialUpdate(t, service)
|
|
})
|
|
}
|
|
|
|
func testExperimentPartialUpdate(t *testing.T, service *core.ModelRegistryService) {
|
|
// Create experiment with all fields populated
|
|
experiment := &openapi.Experiment{
|
|
Name: "test-partial-experiment",
|
|
Description: apiutils.Of("Original description"),
|
|
Owner: apiutils.Of("original-owner"),
|
|
ExternalId: apiutils.Of("original-ext-id"),
|
|
State: (*openapi.ExperimentState)(apiutils.Of(string(openapi.EXPERIMENTSTATE_LIVE))),
|
|
CustomProperties: &map[string]openapi.MetadataValue{
|
|
"team": {
|
|
MetadataStringValue: &openapi.MetadataStringValue{
|
|
StringValue: "data-science",
|
|
MetadataType: "MetadataStringValue",
|
|
},
|
|
},
|
|
"priority": {
|
|
MetadataStringValue: &openapi.MetadataStringValue{
|
|
StringValue: "high",
|
|
MetadataType: "MetadataStringValue",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
created, err := service.UpsertExperiment(experiment)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("update only description - preserves all other fields", func(t *testing.T) {
|
|
// Update only description
|
|
partialUpdate := &openapi.Experiment{
|
|
Id: created.Id,
|
|
Description: apiutils.Of("Updated description only"),
|
|
}
|
|
|
|
updated, err := service.UpsertExperiment(partialUpdate)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only description changed
|
|
assert.Equal(t, "Updated description only", *updated.Description)
|
|
|
|
// Verify all other fields preserved
|
|
assert.Equal(t, "original-owner", *updated.Owner, "Owner should be preserved")
|
|
assert.Equal(t, "original-ext-id", *updated.ExternalId, "ExternalId should be preserved")
|
|
assert.Equal(t, openapi.EXPERIMENTSTATE_LIVE, *updated.State, "State should be preserved")
|
|
|
|
// Verify custom properties preserved
|
|
assert.Contains(t, *updated.CustomProperties, "team", "Custom property 'team' should be preserved")
|
|
assert.Contains(t, *updated.CustomProperties, "priority", "Custom property 'priority' should be preserved")
|
|
assert.Equal(t, "data-science", (*updated.CustomProperties)["team"].MetadataStringValue.StringValue)
|
|
assert.Equal(t, "high", (*updated.CustomProperties)["priority"].MetadataStringValue.StringValue)
|
|
})
|
|
|
|
t.Run("update only custom properties - preserves all other fields", func(t *testing.T) {
|
|
// Update only custom properties
|
|
partialUpdate := &openapi.Experiment{
|
|
Id: created.Id,
|
|
CustomProperties: &map[string]openapi.MetadataValue{
|
|
"environment": {
|
|
MetadataStringValue: &openapi.MetadataStringValue{
|
|
StringValue: "production",
|
|
MetadataType: "MetadataStringValue",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
updated, err := service.UpsertExperiment(partialUpdate)
|
|
require.NoError(t, err)
|
|
|
|
// Verify custom properties completely replaced (this is expected behavior for maps)
|
|
assert.Contains(t, *updated.CustomProperties, "environment")
|
|
assert.Equal(t, "production", (*updated.CustomProperties)["environment"].MetadataStringValue.StringValue)
|
|
|
|
// For complete replacement of CustomProperties map, previous properties should be gone
|
|
// This is the expected behavior when you provide a CustomProperties field
|
|
|
|
// Verify all other fields preserved
|
|
assert.Equal(t, "Updated description only", *updated.Description, "Description should be preserved from previous update")
|
|
assert.Equal(t, "original-owner", *updated.Owner, "Owner should be preserved")
|
|
assert.Equal(t, "original-ext-id", *updated.ExternalId, "ExternalId should be preserved")
|
|
})
|
|
|
|
t.Run("nil/empty fields are ignored", func(t *testing.T) {
|
|
// Update with some nil fields - they should be ignored
|
|
partialUpdate := &openapi.Experiment{
|
|
Id: created.Id,
|
|
Owner: apiutils.Of("new-owner"),
|
|
Description: nil, // This should be ignored
|
|
ExternalId: nil, // This should be ignored
|
|
}
|
|
|
|
updated, err := service.UpsertExperiment(partialUpdate)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only non-nil fields updated
|
|
assert.Equal(t, "new-owner", *updated.Owner, "Owner should be updated")
|
|
|
|
// Verify nil fields were ignored (preserved from previous state)
|
|
assert.Equal(t, "Updated description only", *updated.Description, "Description should be preserved (nil ignored)")
|
|
assert.Equal(t, "original-ext-id", *updated.ExternalId, "ExternalId should be preserved (nil ignored)")
|
|
})
|
|
}
|
|
|
|
func testRegisteredModelPartialUpdate(t *testing.T, service *core.ModelRegistryService) {
|
|
// Create registered model with all fields
|
|
model := &openapi.RegisteredModel{
|
|
Name: "test-partial-model",
|
|
Description: apiutils.Of("Original model description"),
|
|
Owner: apiutils.Of("original-model-owner"),
|
|
ExternalId: apiutils.Of("original-model-ext-id"),
|
|
State: (*openapi.RegisteredModelState)(apiutils.Of(string(openapi.REGISTEREDMODELSTATE_LIVE))),
|
|
CustomProperties: &map[string]openapi.MetadataValue{
|
|
"framework": {
|
|
MetadataStringValue: &openapi.MetadataStringValue{
|
|
StringValue: "tensorflow",
|
|
MetadataType: "MetadataStringValue",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
created, err := service.UpsertRegisteredModel(model)
|
|
require.NoError(t, err)
|
|
|
|
// Test partial update - only description
|
|
partialUpdate := &openapi.RegisteredModel{
|
|
Id: created.Id,
|
|
Description: apiutils.Of("Updated model description"),
|
|
}
|
|
|
|
updated, err := service.UpsertRegisteredModel(partialUpdate)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only description changed
|
|
assert.Equal(t, "Updated model description", *updated.Description)
|
|
|
|
// Verify all other fields preserved
|
|
assert.Equal(t, "original-model-owner", *updated.Owner, "Owner should be preserved")
|
|
assert.Equal(t, "original-model-ext-id", *updated.ExternalId, "ExternalId should be preserved")
|
|
assert.Equal(t, openapi.REGISTEREDMODELSTATE_LIVE, *updated.State, "State should be preserved")
|
|
assert.Contains(t, *updated.CustomProperties, "framework", "CustomProperties should be preserved")
|
|
}
|
|
|
|
func testModelVersionPartialUpdate(t *testing.T, service *core.ModelRegistryService) {
|
|
// Create parent registered model
|
|
rm := &openapi.RegisteredModel{Name: "test-mv-partial-parent"}
|
|
createdRM, err := service.UpsertRegisteredModel(rm)
|
|
require.NoError(t, err)
|
|
|
|
// Create model version with all fields
|
|
mv := &openapi.ModelVersion{
|
|
Name: "v1.0",
|
|
Description: apiutils.Of("Original version description"),
|
|
Author: apiutils.Of("original-author"),
|
|
ExternalId: apiutils.Of("original-mv-ext-id"),
|
|
State: (*openapi.ModelVersionState)(apiutils.Of(string(openapi.MODELVERSIONSTATE_LIVE))),
|
|
CustomProperties: &map[string]openapi.MetadataValue{
|
|
"accuracy": {
|
|
MetadataDoubleValue: &openapi.MetadataDoubleValue{
|
|
DoubleValue: 0.95,
|
|
MetadataType: "MetadataDoubleValue",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
created, err := service.UpsertModelVersion(mv, createdRM.Id)
|
|
require.NoError(t, err)
|
|
|
|
// Test partial update - only author
|
|
partialUpdate := &openapi.ModelVersion{
|
|
Id: created.Id,
|
|
Author: apiutils.Of("updated-author"),
|
|
}
|
|
|
|
updated, err := service.UpsertModelVersion(partialUpdate, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only author changed
|
|
assert.Equal(t, "updated-author", *updated.Author)
|
|
|
|
// Verify all other fields preserved
|
|
assert.Equal(t, "Original version description", *updated.Description, "Description should be preserved")
|
|
assert.Equal(t, "original-mv-ext-id", *updated.ExternalId, "ExternalId should be preserved")
|
|
assert.Equal(t, openapi.MODELVERSIONSTATE_LIVE, *updated.State, "State should be preserved")
|
|
assert.Contains(t, *updated.CustomProperties, "accuracy", "CustomProperties should be preserved")
|
|
}
|
|
|
|
func testArtifactPartialUpdate(t *testing.T, service *core.ModelRegistryService) {
|
|
// Create model artifact with all fields
|
|
artifact := &openapi.Artifact{
|
|
ModelArtifact: &openapi.ModelArtifact{
|
|
Name: apiutils.Of("test-partial-artifact"),
|
|
Description: apiutils.Of("Original artifact description"),
|
|
Uri: apiutils.Of("s3://original/path"),
|
|
ExternalId: apiutils.Of("original-artifact-ext-id"),
|
|
State: (*openapi.ArtifactState)(apiutils.Of(string(openapi.ARTIFACTSTATE_LIVE))),
|
|
CustomProperties: &map[string]openapi.MetadataValue{
|
|
"size": {
|
|
MetadataIntValue: &openapi.MetadataIntValue{
|
|
IntValue: "1024",
|
|
MetadataType: "MetadataIntValue",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
created, err := service.UpsertArtifact(artifact)
|
|
require.NoError(t, err)
|
|
|
|
// Test partial update - only URI
|
|
partialUpdate := &openapi.Artifact{
|
|
ModelArtifact: &openapi.ModelArtifact{
|
|
Id: created.ModelArtifact.Id,
|
|
Uri: apiutils.Of("s3://updated/path"),
|
|
},
|
|
}
|
|
|
|
updated, err := service.UpsertArtifact(partialUpdate)
|
|
require.NoError(t, err)
|
|
|
|
// Verify only URI changed
|
|
assert.Equal(t, "s3://updated/path", *updated.ModelArtifact.Uri)
|
|
|
|
// Verify all other fields preserved
|
|
assert.Equal(t, "Original artifact description", *updated.ModelArtifact.Description, "Description should be preserved")
|
|
assert.Equal(t, "original-artifact-ext-id", *updated.ModelArtifact.ExternalId, "ExternalId should be preserved")
|
|
assert.Equal(t, openapi.ARTIFACTSTATE_LIVE, *updated.ModelArtifact.State, "State should be preserved")
|
|
assert.Contains(t, *updated.ModelArtifact.CustomProperties, "size", "CustomProperties should be preserved")
|
|
assert.Equal(t, "1024", (*updated.ModelArtifact.CustomProperties)["size"].MetadataIntValue.IntValue)
|
|
}
|
|
|
|
// TestEmptyStringVsNilBehavior verifies the difference between empty strings and nil pointers
|
|
func TestEmptyStringVsNilBehavior(t *testing.T) {
|
|
service, cleanup := SetupModelRegistryService(t)
|
|
defer cleanup()
|
|
|
|
// Create experiment with description
|
|
experiment := &openapi.Experiment{
|
|
Name: "test-empty-vs-nil",
|
|
Description: apiutils.Of("Original description"),
|
|
Owner: apiutils.Of("original-owner"),
|
|
}
|
|
|
|
created, err := service.UpsertExperiment(experiment)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("nil description field is ignored", func(t *testing.T) {
|
|
// Update with nil description - should be ignored
|
|
updateWithNil := &openapi.Experiment{
|
|
Id: created.Id,
|
|
Description: nil, // This should be ignored
|
|
Owner: apiutils.Of("updated-owner"),
|
|
}
|
|
|
|
updated, err := service.UpsertExperiment(updateWithNil)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "Original description", *updated.Description, "Nil description should be ignored")
|
|
assert.Equal(t, "updated-owner", *updated.Owner, "Owner should be updated")
|
|
})
|
|
|
|
t.Run("empty string description field updates to empty", func(t *testing.T) {
|
|
// Update with empty string - should actually update to empty
|
|
updateWithEmpty := &openapi.Experiment{
|
|
Id: created.Id,
|
|
Description: apiutils.Of(""), // This should update to empty string
|
|
}
|
|
|
|
updated, err := service.UpsertExperiment(updateWithEmpty)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "", *updated.Description, "Empty string should update the field")
|
|
assert.Equal(t, "updated-owner", *updated.Owner, "Owner should be preserved")
|
|
})
|
|
}
|