From 48c5e9606808aad41fad6f4f03f4285bf18ec4c3 Mon Sep 17 00:00:00 2001 From: Tanmay Rustagi Date: Tue, 13 Aug 2024 13:57:24 +0200 Subject: [PATCH] Added Support for Unit Tests for Terraform Plugin Framework --- pluginframework/resource_quality_monitor.go | 2 + .../resource_quality_monitor_test.go | 304 +++++++++++++++--- pluginframework/testing.go | 120 ++++++- qa/test_credentials_provider.go | 10 +- qa/testing.go | 10 +- 5 files changed, 389 insertions(+), 57 deletions(-) diff --git a/pluginframework/resource_quality_monitor.go b/pluginframework/resource_quality_monitor.go index 26e575556b..0905a67536 100644 --- a/pluginframework/resource_quality_monitor.go +++ b/pluginframework/resource_quality_monitor.go @@ -98,6 +98,8 @@ func (r *QualityMonitorResource) Create(ctx context.Context, req resource.Create return } var monitorInfoTfSDK MonitorInfoExtended + + // "mismatch between struct and object: Struct defines fields not found in object: baseline_table_name, time_series, monitor_version, status, warehouse_id, dashboard_id, data_classification_config, drift_metrics_table_name, snapshot, skip_builtin_dashboard, notifications, schedule, table_name, custom_metrics, latest_monitor_failure_msg, inference_log, profile_metrics_table_name, and slicing_exprs." diags := req.Plan.Get(ctx, &monitorInfoTfSDK) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/pluginframework/resource_quality_monitor_test.go b/pluginframework/resource_quality_monitor_test.go index e43f4b3249..ad98c4a15e 100644 --- a/pluginframework/resource_quality_monitor_test.go +++ b/pluginframework/resource_quality_monitor_test.go @@ -6,68 +6,284 @@ import ( "github.com/databricks/databricks-sdk-go/experimental/mocks" "github.com/databricks/databricks-sdk-go/service/catalog" - "github.com/databricks/terraform-provider-databricks/common" pluginFrameworkResource "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestQualityMonitorCreateTimeseriesPluginFramework(t *testing.T) { - // Setup mock workspace or account client whichever is applicable - // This will be use to mock the databricks-sdk-go calls - MockWorkspaceClientFunc := func(w *mocks.MockWorkspaceClient) { - e := w.GetMockQualityMonitorsAPI().EXPECT() - e.Create(mock.Anything, catalog.CreateMonitor{ - TableName: "test_table", - OutputSchemaName: "output.schema", - AssetsDir: "sample.dir", - TimeSeries: &catalog.MonitorTimeSeries{ - Granularities: []string{"1 day"}, - TimestampCol: "timestamp", - }, - }).Return(&catalog.MonitorInfo{ - AssetsDir: "sample.dir", - OutputSchemaName: "output.schema", - TableName: "test_table", - Status: catalog.MonitorInfoStatusMonitorStatusPending, - DriftMetricsTableName: "test_table_drift", - }, nil) - e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ - TableName: "test_table", - Status: catalog.MonitorInfoStatusMonitorStatusActive, - AssetsDir: "sample.dir", - OutputSchemaName: "output.schema", - DriftMetricsTableName: "test_table_drift", - }, nil) + ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.Create(mock.Anything, catalog.CreateMonitor{ + TableName: "test_table", + OutputSchemaName: "output.schema", + AssetsDir: "sample.dir", + TimeSeries: &catalog.MonitorTimeSeries{ + Granularities: []string{"1 day"}, + TimestampCol: "timestamp", + }, + }).Return(&catalog.MonitorInfo{ + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusPending, + DriftMetricsTableName: "test_table_drift", + }, nil) + e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + DriftMetricsTableName: "test_table_drift", + }, nil) + }, + }.Start(t) + assert.NoError(t, err) + + qualityMonitorResource := QualityMonitorResource{ + Client: client, + } + + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) + createRequest := pluginFrameworkResource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: MapToTfTypesValue(map[string]any{ + "table_name": "test_table", + "assets_dir": "sample.dir", + "output_schema_name": "output.schema", + "time_series": `{"granularities":["1 day"],"timestamp_col":"timestamp"}`, + }), + Schema: schema.Schema, + }, } - _ = MockWorkspaceClientFunc + createResponse := pluginFrameworkResource.CreateResponse{} + + qualityMonitorResource.Create(ctx, createRequest, &createResponse) + + assert.False(t, createResponse.Diagnostics.HasError()) +} - // Start the server - // tanmaytodo +func TestQualityMonitorCreateInferencePluginFramework(t *testing.T) { + ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.Create(mock.Anything, catalog.CreateMonitor{ + TableName: "test_table", + OutputSchemaName: "output.schema", + AssetsDir: "sample.dir", + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 day"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + ProblemType: catalog.MonitorInferenceLogProblemTypeProblemTypeRegression, + }, + }).Return(&catalog.MonitorInfo{ + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + }, nil) + e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 day"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + ProblemType: catalog.MonitorInferenceLogProblemTypeProblemTypeRegression, + }, + }, nil) + }, + }.Start(t) + assert.NoError(t, err) - // Create resource using the above mock workspace client qualityMonitorResource := QualityMonitorResource{ - Client: &common.DatabricksClient{}, + Client: client, } - // Create request and response for the respective CRUD operation and pass the config + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) createRequest := pluginFrameworkResource.CreateRequest{ - Config: GetPluginFrameworkConfig(` - table_name = "test_table", - assets_dir = "sample.dir", - output_schema_name = "output.schema", - time_series = { - granularities = ["1 day"], - timestamp_col = "timestamp" - } - `), + Plan: tfsdk.Plan{ + Raw: MapToTfTypesValue(map[string]any{ + "table_name": "test_table", + "assets_dir": "sample.dir", + "output_schema_name": "output.schema", + "inference_log": `{ + granularities: ["1 day"], + timestamp_col: "timestamp", + prediction_col: "prediction", + model_id_col: "model_id", + problem_type: "PROBLEM_TYPE_REGRESSION" + }`, + }), + Schema: schema.Schema, + }, } createResponse := pluginFrameworkResource.CreateResponse{} + + qualityMonitorResource.Create(ctx, createRequest, &createResponse) + + assert.False(t, createResponse.Diagnostics.HasError()) +} + +func TestQualityMonitorCreateSnapshot(t *testing.T) { ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.Create(mock.Anything, catalog.CreateMonitor{ + TableName: "test_table", + OutputSchemaName: "output.schema", + AssetsDir: "sample.dir", + Snapshot: &catalog.MonitorSnapshot{}, + }).Return(&catalog.MonitorInfo{ + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + }, nil) + e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + AssetsDir: "sample.dir", + OutputSchemaName: "output.schema", + Snapshot: &catalog.MonitorSnapshot{}, + }, nil) + }, + }.Start(t) + assert.NoError(t, err) + + qualityMonitorResource := QualityMonitorResource{ + Client: client, + } + + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) + + createRequest := pluginFrameworkResource.CreateRequest{ + Plan: tfsdk.Plan{ + Raw: MapToTfTypesValue(map[string]any{ + "table_name": "test_table", + "assets_dir": "sample.dir", + "output_schema_name": "output.schema", + "snapshot": `{}`, + }), + Schema: schema.Schema, + }, + } + createResponse := pluginFrameworkResource.CreateResponse{} - // Call the method to unit test qualityMonitorResource.Create(ctx, createRequest, &createResponse) - // Add assertions assert.False(t, createResponse.Diagnostics.HasError()) } + +func TestQualityMonitorGet(t *testing.T) { + ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + AssetsDir: "new_assets.dir", + OutputSchemaName: "output.schema", + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 week"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + }, + DriftMetricsTableName: "test_table_drift"}, nil) + }, + }.Start(t) + assert.NoError(t, err) + + qualityMonitorResource := QualityMonitorResource{ + Client: client, + } + + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) +} + +func TestQualityMonitorUpdate(t *testing.T) { + ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.Update(mock.Anything, catalog.UpdateMonitor{ + TableName: "test_table", + OutputSchemaName: "output.schema", + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 week"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + ProblemType: catalog.MonitorInferenceLogProblemTypeProblemTypeRegression, + }, + }).Return(&catalog.MonitorInfo{ + AssetsDir: "new_assets.dir", + OutputSchemaName: "output.schema", + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 week"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + }, + DriftMetricsTableName: "test_table_drift", + }, nil) + e.GetByTableName(mock.Anything, "test_table").Return(&catalog.MonitorInfo{ + TableName: "test_table", + Status: catalog.MonitorInfoStatusMonitorStatusActive, + AssetsDir: "new_assets.dir", + OutputSchemaName: "output.schema", + InferenceLog: &catalog.MonitorInferenceLog{ + Granularities: []string{"1 week"}, + TimestampCol: "timestamp", + PredictionCol: "prediction", + ModelIdCol: "model_id", + }, + DriftMetricsTableName: "test_table_drift", + }, nil) + }, + }.Start(t) + assert.NoError(t, err) + + qualityMonitorResource := QualityMonitorResource{ + Client: client, + } + + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) +} + +func TestQualityMonitorDelete(t *testing.T) { + ctx := context.Background() + client, err := ResourceFixturePluginFramework{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockQualityMonitorsAPI().EXPECT() + e.DeleteByTableName(mock.Anything, "test_table").Return(nil) + }, + }.Start(t) + assert.NoError(t, err) + + qualityMonitorResource := QualityMonitorResource{ + Client: client, + } + + schema := pluginFrameworkResource.SchemaResponse{} + qualityMonitorResource.Schema(ctx, pluginFrameworkResource.SchemaRequest{}, &schema) +} diff --git a/pluginframework/testing.go b/pluginframework/testing.go index 9a10fc3862..3854f66f23 100644 --- a/pluginframework/testing.go +++ b/pluginframework/testing.go @@ -1,9 +1,123 @@ package pluginframework import ( - "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "fmt" + "reflect" + "testing" + + "github.com/databricks/databricks-sdk-go/client" + "github.com/databricks/databricks-sdk-go/config" + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/qa" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) -func GetPluginFrameworkConfig(s string) tfsdk.Config { - return tfsdk.Config{} // tanmaytodo +type ResourceFixturePluginFramework struct { + Fixtures []qa.HTTPFixture + MockWorkspaceClientFunc func(*mocks.MockWorkspaceClient) + MockAccountClientFunc func(*mocks.MockAccountClient) + Token string +} + +func (f ResourceFixturePluginFramework) validateMocks() error { + isMockConfigured := f.MockAccountClientFunc != nil || f.MockWorkspaceClientFunc != nil + isFixtureConfigured := f.Fixtures != nil + if isFixtureConfigured && isMockConfigured { + return fmt.Errorf("either (MockWorkspaceClientFunc, MockAccountClientFunc) or Fixtures may be set, not both") + } + return nil +} + +func (f ResourceFixturePluginFramework) Start(t *testing.T) (*common.DatabricksClient, error) { + err := f.validateMocks() + if err != nil { + return nil, err + } + client, server, err := f.setupClient(t) + if err != nil { + return nil, err + } + defer server.Close() + return client, nil +} + +func (f ResourceFixturePluginFramework) setupClient(t *testing.T) (*common.DatabricksClient, qa.Server, error) { + token := "..." + if f.Token != "" { + token = f.Token + } + if f.Fixtures != nil { + client, s, err := qa.HttpFixtureClientWithToken(t, f.Fixtures, token) + ss := qa.Server{ + Close: s.Close, + URL: s.URL, + } + return client, ss, err + } + mw := mocks.NewMockWorkspaceClient(t) + ma := mocks.NewMockAccountClient(t) + if f.MockWorkspaceClientFunc != nil { + f.MockWorkspaceClientFunc(mw) + } + if f.MockAccountClientFunc != nil { + f.MockAccountClientFunc(ma) + } + c := &common.DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{}, + }, + } + c.SetWorkspaceClient(mw.WorkspaceClient) + c.SetAccountClient(ma.AccountClient) + c.Config.Credentials = qa.TestCredentialsProvider{Token: token} + return c, qa.Server{ + Close: func() {}, + URL: "does-not-matter", + }, nil +} + +func StructToTfTypesValue(s interface{}) tftypes.Value { + v := reflect.ValueOf(s) + if v.Kind() != reflect.Struct { + panic("provided input is not a struct") + } + + result := make(map[string]interface{}) + t := v.Type() + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldName := t.Field(i).Name + if field.CanInterface() { + result[fieldName] = field.Interface() + } + } + return MapToTfTypesValue(result) +} + +func MapToTfTypesValue(configMap map[string]any) tftypes.Value { + configTypeMap := map[string]tftypes.Type{} + rawConfigValueMap := map[string]tftypes.Value{} + for k, v := range configMap { + switch v := v.(type) { + case string: + configTypeMap[k] = tftypes.String + rawConfigValueMap[k] = tftypes.NewValue(tftypes.String, v) + case int: + configTypeMap[k] = tftypes.Number + rawConfigValueMap[k] = tftypes.NewValue(tftypes.Bool, int(v)) + case bool: + configTypeMap[k] = tftypes.Bool + rawConfigValueMap[k] = tftypes.NewValue(tftypes.Bool, v) + default: + configTypeMap[k] = tftypes.String // tfypes.Object{}? + rawConfigValueMap[k] = tftypes.NewValue(tftypes.String, fmt.Sprintf("%v", v)) + } + } + rawConfigType := tftypes.Object{ + AttributeTypes: configTypeMap, + } + rawConfigValue := tftypes.NewValue(rawConfigType, rawConfigValueMap) + return rawConfigValue } diff --git a/qa/test_credentials_provider.go b/qa/test_credentials_provider.go index 9d5741d6a8..81bf331cce 100644 --- a/qa/test_credentials_provider.go +++ b/qa/test_credentials_provider.go @@ -9,17 +9,17 @@ import ( "github.com/databricks/databricks-sdk-go/credentials" ) -type testCredentialsProvider struct { - token string +type TestCredentialsProvider struct { + Token string } -func (testCredentialsProvider) Name() string { +func (TestCredentialsProvider) Name() string { return "test" } -func (t testCredentialsProvider) Configure(ctx context.Context, cfg *config.Config) (credentials.CredentialsProvider, error) { +func (t TestCredentialsProvider) Configure(ctx context.Context, cfg *config.Config) (credentials.CredentialsProvider, error) { fun := func(r *http.Request) error { - r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.token)) + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.Token)) return nil } return credentials.NewCredentialsProvider(fun), nil diff --git a/qa/testing.go b/qa/testing.go index 00d8c05597..82e5e3db37 100644 --- a/qa/testing.go +++ b/qa/testing.go @@ -210,19 +210,19 @@ func (f ResourceFixture) validateMocks() error { return nil } -type server struct { +type Server struct { Close func() URL string } -func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, server, error) { +func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, Server, error) { token := "..." if f.Token != "" { token = f.Token } if f.Fixtures != nil { client, s, err := HttpFixtureClientWithToken(t, f.Fixtures, token) - ss := server{ + ss := Server{ Close: s.Close, URL: s.URL, } @@ -243,8 +243,8 @@ func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, se } c.SetWorkspaceClient(mw.WorkspaceClient) c.SetAccountClient(ma.AccountClient) - c.Config.Credentials = testCredentialsProvider{token: token} - return c, server{ + c.Config.Credentials = TestCredentialsProvider{Token: token} + return c, Server{ Close: func() {}, URL: "does-not-matter", }, nil