From a0ad67e75a108184795471fc28a45ac6811200e4 Mon Sep 17 00:00:00 2001 From: Dave Storey Date: Mon, 15 Jun 2020 12:58:14 +0100 Subject: [PATCH 1/4] fix issue causing mount to error on read --- databricks/mounts.go | 24 +++++++++++------------- databricks/mounts_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 databricks/mounts_test.go diff --git a/databricks/mounts.go b/databricks/mounts.go index 8e546afae..2e0da8f1b 100644 --- a/databricks/mounts.go +++ b/databricks/mounts.go @@ -3,10 +3,11 @@ package databricks import ( "errors" "fmt" - "github.com/databrickslabs/databricks-terraform/client/service" "log" "net/url" "strings" + + "github.com/databrickslabs/databricks-terraform/client/service" ) // Mount interface describes the functionality of any mount which is create, read and delete @@ -164,11 +165,10 @@ dbutils.notebook.exit("success") // Read verifies a azure blob storage mount given a cluster id func (m AzureBlobMount) Read(client *service.DBApiClient, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` -dbutils.fs.ls("/mnt/%s") for mount in dbutils.fs.mounts(): - if mount.mountPoint == "/mnt/%s": - dbutils.notebook.exit(mount.source) -`, m.MountName, m.MountName) + if mount.mountPoint == "/mnt/%s": + dbutils.notebook.exit(mount.source) +`, m.MountName) resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err @@ -266,11 +266,10 @@ dbutils.notebook.exit("success") // Read verifies the azure datalake gen 1 storage mount given a cluster id func (m AzureADLSGen1Mount) Read(client *service.DBApiClient, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` -dbutils.fs.ls("/mnt/%s") for mount in dbutils.fs.mounts(): - if mount.mountPoint == "/mnt/%s": - dbutils.notebook.exit(mount.source) -`, m.MountName, m.MountName) + if mount.mountPoint == "/mnt/%s": + dbutils.notebook.exit(mount.source) +`, m.MountName) resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err @@ -373,11 +372,10 @@ dbutils.notebook.exit("success") // Read verifies the azure datalake gen 2 storage mount func (m AzureADLSGen2Mount) Read(client *service.DBApiClient, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` -dbutils.fs.ls("/mnt/%s") for mount in dbutils.fs.mounts(): - if mount.mountPoint == "/mnt/%s": - dbutils.notebook.exit(mount.source) -`, m.MountName, m.MountName) + if mount.mountPoint == "/mnt/%s": + dbutils.notebook.exit(mount.source) +`, m.MountName) resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err diff --git a/databricks/mounts_test.go b/databricks/mounts_test.go new file mode 100644 index 000000000..78f439861 --- /dev/null +++ b/databricks/mounts_test.go @@ -0,0 +1,37 @@ +package databricks + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProcessAzureWasbAbfssUrisCorrectlySplitsURI(t *testing.T) { + testCases := []struct { + URI string + ExpectedContainer string + ExpectedStorageAcc string + ExpectedDirectory string + }{ + { + URI: "abfss://wibble@mystorage.dfs.core.windows.net/wobble", + ExpectedContainer: "wibble", + ExpectedStorageAcc: "mystorage", + ExpectedDirectory: "/wobble", + }, + { + URI: "abfss://wibble@mystorage.dfs.core.windows.net", + ExpectedContainer: "wibble", + ExpectedStorageAcc: "mystorage", + ExpectedDirectory: "", + }, + } + + for _, tc := range testCases { + container, storageAcc, dir, err := ProcessAzureWasbAbfssUris(tc.URI) + assert.Equal(t, tc.ExpectedContainer, container) + assert.Equal(t, tc.ExpectedStorageAcc, storageAcc) + assert.Equal(t, tc.ExpectedDirectory, dir) + assert.Nil(t, err) + } +} From e948e6abee672b7b91a762185c5a3d1994432f67 Mon Sep 17 00:00:00 2001 From: Dave Storey Date: Mon, 15 Jun 2020 21:11:12 +0000 Subject: [PATCH 2/4] added extra tests and fixed linting issues --- client/service/commands.go | 8 +- databricks/mounts.go | 48 ++--- databricks/mounts_test.go | 181 ++++++++++++++++++ .../resource_databricks_aws_s3_mount.go | 6 +- ...source_databricks_azure_adls_gen1_mount.go | 6 +- ...source_databricks_azure_adls_gen2_mount.go | 6 +- .../resource_databricks_azure_blob_mount.go | 6 +- ...source_databricks_azure_blob_mount_test.go | 4 +- .../resource_databricks_job_aws_test.go | 84 ++++---- .../resource_databricks_job_azure_test.go | 52 ++--- 10 files changed, 294 insertions(+), 107 deletions(-) diff --git a/client/service/commands.go b/client/service/commands.go index 75330c45e..a69787438 100644 --- a/client/service/commands.go +++ b/client/service/commands.go @@ -4,10 +4,11 @@ import ( "encoding/json" "errors" "fmt" - "github.com/databrickslabs/databricks-terraform/client/model" "log" "net/http" "time" + + "github.com/databrickslabs/databricks-terraform/client/model" ) // CommandsAPI exposes the Context & Commands API @@ -15,6 +16,11 @@ type CommandsAPI struct { Client *DBApiClient } +// CommandExecutor creates a spark context and executes a command and then closes context +type CommandExecutor interface { + Execute(clusterID, langauge, commandStr string) (model.Command, error) +} + // Execute creates a spark context and executes a command and then closes context func (a CommandsAPI) Execute(clusterID, langauge, commandStr string) (model.Command, error) { var resp model.Command diff --git a/databricks/mounts.go b/databricks/mounts.go index 2e0da8f1b..c6c90d2ba 100644 --- a/databricks/mounts.go +++ b/databricks/mounts.go @@ -30,13 +30,13 @@ func NewAWSIamMount(s3BucketName string, mountName string) *AWSIamMount { } // Create creates an aws iam mount given a cluster ID -func (m AWSIamMount) Create(client *service.DBApiClient, clusterID string) error { +func (m AWSIamMount) Create(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` dbutils.fs.mount("s3a://%s", "/mnt/%s") dbutils.fs.ls("/mnt/%s") dbutils.notebook.exit("success") `, m.S3BucketName, m.MountName, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -48,13 +48,13 @@ dbutils.notebook.exit("success") } // Delete deletes an aws iam mount given a cluster ID -func (m AWSIamMount) Delete(client *service.DBApiClient, clusterID string) error { +func (m AWSIamMount) Delete(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` dbutils.fs.unmount("/mnt/%s") dbutils.fs.refreshMounts() dbutils.notebook.exit("success") `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -66,14 +66,14 @@ dbutils.notebook.exit("success") } // Read verifies an aws iam mount given a cluster ID -func (m AWSIamMount) Read(client *service.DBApiClient, clusterID string) (string, error) { +func (m AWSIamMount) Read(exec service.CommandExecutor, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` dbutils.fs.ls("/mnt/%s") for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%s": dbutils.notebook.exit(mount.source) `, m.MountName, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err } @@ -109,7 +109,7 @@ func NewAzureBlobMount(containerName string, storageAccountName string, director } // Create creates a azure blob storage mount given a cluster id -func (m AzureBlobMount) Create(client *service.DBApiClient, clusterID string) error { +func (m AzureBlobMount) Create(exec service.CommandExecutor, clusterID string) error { var confKey string if m.AuthType == "SAS" { @@ -133,7 +133,7 @@ except Exception as e: raise e dbutils.notebook.exit("success") `, m.ContainerName, m.StorageAccountName, m.Directory, m.MountName, confKey, m.SecretScope, m.SecretKey) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -145,13 +145,13 @@ dbutils.notebook.exit("success") } // Delete deletes a azure blob storage mount given a cluster id -func (m AzureBlobMount) Delete(client *service.DBApiClient, clusterID string) error { +func (m AzureBlobMount) Delete(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` dbutils.fs.unmount("/mnt/%s") dbutils.fs.refreshMounts() dbutils.notebook.exit("success") `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -163,13 +163,13 @@ dbutils.notebook.exit("success") } // Read verifies a azure blob storage mount given a cluster id -func (m AzureBlobMount) Read(client *service.DBApiClient, clusterID string) (string, error) { +func (m AzureBlobMount) Read(exec service.CommandExecutor, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%s": dbutils.notebook.exit(mount.source) `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err } @@ -213,7 +213,7 @@ func NewAzureADLSGen1Mount(storageResource string, directory string, mountName s } // Create creates a azure datalake gen 1 storage mount given a cluster id -func (m AzureADLSGen1Mount) Create(client *service.DBApiClient, clusterID string) error { +func (m AzureADLSGen1Mount) Create(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%[8]s" and mount.source=="adl://%[6]s.azuredatalakestore.net%[7]s": @@ -234,7 +234,7 @@ except Exception as e: raise e dbutils.notebook.exit("success") `, m.PrefixType, m.ClientID, m.SecretScope, m.SecretKey, m.TenantID, m.StorageResource, m.Directory, m.MountName, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -246,13 +246,13 @@ dbutils.notebook.exit("success") } // Delete deletes a azure datalake gen 1 storage mount given a cluster id -func (m AzureADLSGen1Mount) Delete(client *service.DBApiClient, clusterID string) error { +func (m AzureADLSGen1Mount) Delete(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` dbutils.fs.unmount("/mnt/%s") dbutils.fs.refreshMounts() dbutils.notebook.exit("success") `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -264,13 +264,13 @@ dbutils.notebook.exit("success") } // Read verifies the azure datalake gen 1 storage mount given a cluster id -func (m AzureADLSGen1Mount) Read(client *service.DBApiClient, clusterID string) (string, error) { +func (m AzureADLSGen1Mount) Read(exec service.CommandExecutor, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%s": dbutils.notebook.exit(mount.source) `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err } @@ -314,7 +314,7 @@ func NewAzureADLSGen2Mount(containerName string, storageAccountName string, dire } // Create creates a azure datalake gen 2 storage mount -func (m AzureADLSGen2Mount) Create(client *service.DBApiClient, clusterID string) error { +func (m AzureADLSGen2Mount) Create(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%[9]s" and mount.source=="abfss://%[6]s@%[7]s.dfs.core.windows.net%[8]s": @@ -340,7 +340,7 @@ except Exception as e: raise e dbutils.notebook.exit("success") `, m.ClientID, m.SecretScope, m.SecretKey, m.TenantID, m.InitializeFileSystem, m.ContainerName, m.StorageAccountName, m.Directory, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -352,13 +352,13 @@ dbutils.notebook.exit("success") } // Delete deletes a azure datalake gen 2 storage mount -func (m AzureADLSGen2Mount) Delete(client *service.DBApiClient, clusterID string) error { +func (m AzureADLSGen2Mount) Delete(exec service.CommandExecutor, clusterID string) error { iamMountCommand := fmt.Sprintf(` dbutils.fs.unmount("/mnt/%s") dbutils.fs.refreshMounts() dbutils.notebook.exit("success") `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return err } @@ -370,13 +370,13 @@ dbutils.notebook.exit("success") } // Read verifies the azure datalake gen 2 storage mount -func (m AzureADLSGen2Mount) Read(client *service.DBApiClient, clusterID string) (string, error) { +func (m AzureADLSGen2Mount) Read(exec service.CommandExecutor, clusterID string) (string, error) { iamMountCommand := fmt.Sprintf(` for mount in dbutils.fs.mounts(): if mount.mountPoint == "/mnt/%s": dbutils.notebook.exit(mount.source) `, m.MountName) - resp, err := client.Commands().Execute(clusterID, "python", iamMountCommand) + resp, err := exec.Execute(clusterID, "python", iamMountCommand) if err != nil { return "", err } diff --git a/databricks/mounts_test.go b/databricks/mounts_test.go index 78f439861..489002ca7 100644 --- a/databricks/mounts_test.go +++ b/databricks/mounts_test.go @@ -1,11 +1,192 @@ package databricks import ( + "errors" + "fmt" "testing" + "github.com/databrickslabs/databricks-terraform/client/model" "github.com/stretchr/testify/assert" ) +var executeMock func(clusterID, langauge, commandStr string) (model.Command, error) + +type commandExecutorMock struct{} + +func (a commandExecutorMock) Execute(clusterID, langauge, commandStr string) (model.Command, error) { + return executeMock(clusterID, langauge, commandStr) +} + +func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { + const cn = "mycontainer" + const sacc = "mystorage" + const dir = "mydirectory" + + testCases := []struct { + ExpectedResult string + ExpectedError error + CommandResult *model.CommandResults + }{ + { + ExpectedResult: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), + }, + }, + { + ExpectedError: errors.New("unable to find mount point"), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "", + }, + }, + { + ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != abfss://x@y.dfs.core.windows.net/z!", cn, sacc), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "abfss://x@y.dfs.core.windows.net/z", + }, + }, + { + ExpectedError: errors.New("out of wibble error"), + CommandResult: &model.CommandResults{ + ResultType: "error", + Summary: "out of wibble error", + }, + }, + } + + for _, tc := range testCases { + executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} + + blobMount := NewAzureBlobMount(cn, sacc, dir, "mount", "", "", "") + + result, err := blobMount.Read(executorMock, "wibble") + + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + } +} + +func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { + const sacc = "mystorage" + const dir = "mydirectory" + + testCases := []struct { + ExpectedResult string + ExpectedError error + CommandResult *model.CommandResults + }{ + { + ExpectedResult: fmt.Sprintf("adl://%s.azuredatalakestore.net/%s", sacc, dir), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: fmt.Sprintf("adl://%s.azuredatalakestore.net/%s", sacc, dir), + }, + }, + { + ExpectedError: errors.New("unable to find mount point"), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "", + }, + }, + { + ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != adl://x.azuredatalakestore.net/z!", sacc, dir), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "adl://x.azuredatalakestore.net/z", + }, + }, + { + ExpectedError: errors.New("out of wibble error"), + CommandResult: &model.CommandResults{ + ResultType: "error", + Summary: "out of wibble error", + }, + }, + } + + for _, tc := range testCases { + executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} + + adlsGen2Mount := NewAzureADLSGen1Mount(sacc, dir, "mount", "", "", "", "", "") + + result, err := adlsGen2Mount.Read(executorMock, "wibble") + + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + } +} + +func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { + const cn = "mycontainer" + const sacc = "mystorage" + const dir = "mydirectory" + + testCases := []struct { + ExpectedResult string + ExpectedError error + CommandResult *model.CommandResults + }{ + { + ExpectedResult: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), + }, + }, + { + ExpectedError: errors.New("unable to find mount point"), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "", + }, + }, + { + ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != abfss://x@y.dfs.core.windows.net/z!", cn, sacc), + CommandResult: &model.CommandResults{ + ResultType: "text", + Data: "abfss://x@y.dfs.core.windows.net/z", + }, + }, + { + ExpectedError: errors.New("out of wibble error"), + CommandResult: &model.CommandResults{ + ResultType: "error", + Summary: "out of wibble error", + }, + }, + } + + for _, tc := range testCases { + executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} + + adlsGen2Mount := NewAzureADLSGen2Mount(cn, sacc, dir, "mount", "", "", "", "", true) + + result, err := adlsGen2Mount.Read(executorMock, "wibble") + + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + } +} + func TestProcessAzureWasbAbfssUrisCorrectlySplitsURI(t *testing.T) { testCases := []struct { URI string diff --git a/databricks/resource_databricks_aws_s3_mount.go b/databricks/resource_databricks_aws_s3_mount.go index f7361d532..eead9e396 100644 --- a/databricks/resource_databricks_aws_s3_mount.go +++ b/databricks/resource_databricks_aws_s3_mount.go @@ -43,7 +43,7 @@ func resourceAWSS3Create(d *schema.ResourceData, m interface{}) error { s3BucketMount := NewAWSIamMount(s3BucketName, mountName) - err = s3BucketMount.Create(client, clusterID) + err = s3BucketMount.Create(client.Commands(), clusterID) if err != nil { return err } @@ -74,7 +74,7 @@ func resourceAWSS3Read(d *schema.ResourceData, m interface{}) error { s3BucketMount := NewAWSIamMount(s3BucketName, mountName) - s3BucketNameMounted, err := s3BucketMount.Read(client, clusterID) + s3BucketNameMounted, err := s3BucketMount.Read(client.Commands(), clusterID) if err != nil { return err } @@ -93,5 +93,5 @@ func resourceAWSS3Delete(d *schema.ResourceData, m interface{}) error { s3BucketName := d.Get("s3_bucket_name").(string) mountName := d.Get("mount_name").(string) s3BucketMount := NewAWSIamMount(s3BucketName, mountName) - return s3BucketMount.Delete(client, clusterID) + return s3BucketMount.Delete(client.Commands(), clusterID) } diff --git a/databricks/resource_databricks_azure_adls_gen1_mount.go b/databricks/resource_databricks_azure_adls_gen1_mount.go index 71032003f..626106d4c 100644 --- a/databricks/resource_databricks_azure_adls_gen1_mount.go +++ b/databricks/resource_databricks_azure_adls_gen1_mount.go @@ -97,7 +97,7 @@ func resourceAzureAdlsGen1Create(d *schema.ResourceData, m interface{}) error { adlsGen1Mount := NewAzureADLSGen1Mount(storageResourceName, directory, mountName, sparkConfPrefix, clientID, tenantID, clientSecretScope, clientSecretKey) - err = adlsGen1Mount.Create(client, clusterID) + err = adlsGen1Mount.Create(client.Commands(), clusterID) if err != nil { return err } @@ -158,7 +158,7 @@ func resourceAzureAdlsGen1Read(d *schema.ResourceData, m interface{}) error { adlsGen1Mount := NewAzureADLSGen1Mount(storageResourceName, directory, mountName, sparkConfPrefix, clientID, tenantID, clientSecretScope, clientSecretKey) - url, err := adlsGen1Mount.Read(client, clusterID) + url, err := adlsGen1Mount.Read(client.Commands(), clusterID) if err != nil { //Reset id in case of inability to find mount if strings.Contains(err.Error(), "Unable to find mount point!") || @@ -198,5 +198,5 @@ func resourceAzureAdlsGen1Delete(d *schema.ResourceData, m interface{}) error { adlsGen1Mount := NewAzureADLSGen1Mount(storageResourceName, directory, mountName, sparkConfPrefix, clientID, tenantID, clientSecretScope, clientSecretKey) - return adlsGen1Mount.Delete(client, clusterID) + return adlsGen1Mount.Delete(client.Commands(), clusterID) } diff --git a/databricks/resource_databricks_azure_adls_gen2_mount.go b/databricks/resource_databricks_azure_adls_gen2_mount.go index fc574fa1e..d2216ac7c 100644 --- a/databricks/resource_databricks_azure_adls_gen2_mount.go +++ b/databricks/resource_databricks_azure_adls_gen2_mount.go @@ -91,7 +91,7 @@ func resourceAzureAdlsGen2Create(d *schema.ResourceData, m interface{}) error { adlsGen2Mount := NewAzureADLSGen2Mount(containerName, storageAccountName, directory, mountName, clientID, tenantID, clientSecretScope, clientSecretKey, initializeFileSystem) - err = adlsGen2Mount.Create(client, clusterID) + err = adlsGen2Mount.Create(client.Commands(), clusterID) if err != nil { return err } @@ -154,7 +154,7 @@ func resourceAzureAdlsGen2Read(d *schema.ResourceData, m interface{}) error { adlsGen2Mount := NewAzureADLSGen2Mount(containerName, storageAccountName, directory, mountName, clientID, tenantID, clientSecretScope, clientSecretKey, initializeFileSystem) - url, err := adlsGen2Mount.Read(client, clusterID) + url, err := adlsGen2Mount.Read(client.Commands(), clusterID) if err != nil { //Reset id in case of inability to find mount if strings.Contains(err.Error(), "Unable to find mount point!") || @@ -199,5 +199,5 @@ func resourceAzureAdlsGen2Delete(d *schema.ResourceData, m interface{}) error { adlsGen2Mount := NewAzureADLSGen2Mount(containerName, storageAccountName, directory, mountName, clientID, tenantID, clientSecretScope, clientSecretKey, initializeFileSystem) - return adlsGen2Mount.Delete(client, clusterID) + return adlsGen2Mount.Delete(client.Commands(), clusterID) } diff --git a/databricks/resource_databricks_azure_blob_mount.go b/databricks/resource_databricks_azure_blob_mount.go index d09ea33cd..80105cc59 100644 --- a/databricks/resource_databricks_azure_blob_mount.go +++ b/databricks/resource_databricks_azure_blob_mount.go @@ -90,7 +90,7 @@ func resourceAzureBlobMountCreate(d *schema.ResourceData, m interface{}) error { blobMount := NewAzureBlobMount(containerName, storageAccountName, directory, mountName, authType, tokenSecretScope, tokenSecretKey) - err = blobMount.Create(client, clusterID) + err = blobMount.Create(client.Commands(), clusterID) if err != nil { return err } @@ -144,7 +144,7 @@ func resourceAzureBlobMountRead(d *schema.ResourceData, m interface{}) error { blobMount := NewAzureBlobMount(containerName, storageAccountName, directory, mountName, authType, tokenSecretScope, tokenSecretKey) - url, err := blobMount.Read(client, clusterID) + url, err := blobMount.Read(client.Commands(), clusterID) if err != nil { //Reset id in case of inability to find mount if strings.Contains(err.Error(), "Unable to find mount point!") || @@ -187,5 +187,5 @@ func resourceAzureBlobMountDelete(d *schema.ResourceData, m interface{}) error { blobMount := NewAzureBlobMount(containerName, storageAccountName, directory, mountName, authType, tokenSecretScope, tokenSecretKey) - return blobMount.Delete(client, clusterID) + return blobMount.Delete(client.Commands(), clusterID) } diff --git a/databricks/resource_databricks_azure_blob_mount_test.go b/databricks/resource_databricks_azure_blob_mount_test.go index 6940569cd..656916546 100644 --- a/databricks/resource_databricks_azure_blob_mount_test.go +++ b/databricks/resource_databricks_azure_blob_mount_test.go @@ -30,7 +30,7 @@ func TestAccAzureBlobMount_correctly_mounts(t *testing.T) { { PreConfig: func() { client := testAccProvider.Meta().(*service.DBApiClient) - err := azureBlobMount.Delete(client, clusterInfo.ClusterID) + err := azureBlobMount.Delete(client.Commands(), clusterInfo.ClusterID) assert.NoError(t, err, "TestAccAzureBlobMount_correctly_mounts: Failed to remove the mount.") }, Config: terraformToApply, @@ -115,7 +115,7 @@ func testAccAzureBlobMount_mount_exists(n string, azureBlobMount *AzureBlobMount client := testAccProvider.Meta().(*service.DBApiClient) cluster_id := clusterInfo.ClusterID - message, err := blobMount.Read(client, cluster_id) + message, err := blobMount.Read(client.Commands(), cluster_id) if err != nil { return fmt.Errorf("Error reading the mount %s: error %s", message, err) } diff --git a/databricks/resource_databricks_job_aws_test.go b/databricks/resource_databricks_job_aws_test.go index 011b89db7..884348135 100644 --- a/databricks/resource_databricks_job_aws_test.go +++ b/databricks/resource_databricks_job_aws_test.go @@ -3,13 +3,14 @@ package databricks import ( "errors" "fmt" + "strconv" + "testing" + "github.com/databrickslabs/databricks-terraform/client/model" "github.com/databrickslabs/databricks-terraform/client/service" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" "github.com/stretchr/testify/assert" - "strconv" - "testing" ) func TestAccAwsJobResource(t *testing.T) { @@ -95,49 +96,48 @@ func testAwsJobValuesNewCluster(t *testing.T, job *model.Job) resource.TestCheck assert.NotNil(t, job.Settings.NotebookTask) assert.Equal(t, 2, int(job.Settings.NewCluster.Autoscale.MinWorkers)) assert.Equal(t, 3, int(job.Settings.NewCluster.Autoscale.MaxWorkers)) - assert.Equal(t, "6.4.x-scala2.11", job.Settings.NewCluster.SparkVersion) - assert.Equal(t, model.AwsAvailability(model.AwsAvailabilitySpot), job.Settings.NewCluster.AwsAttributes.Availability) - assert.Equal(t, "us-east-1a", job.Settings.NewCluster.AwsAttributes.ZoneID) - assert.Equal(t, 100, int(job.Settings.NewCluster.AwsAttributes.SpotBidPricePercent)) - assert.Equal(t, 1, int(job.Settings.NewCluster.AwsAttributes.FirstOnDemand)) - assert.Equal(t, model.EbsVolumeType(model.EbsVolumeTypeGeneralPurposeSsd), job.Settings.NewCluster.AwsAttributes.EbsVolumeType) - assert.Equal(t, 1, int(job.Settings.NewCluster.AwsAttributes.EbsVolumeCount)) - assert.Equal(t, 32, int(job.Settings.NewCluster.AwsAttributes.EbsVolumeSize)) - assert.Equal(t, "r3.xlarge", job.Settings.NewCluster.NodeTypeID) - assert.Equal(t, "/Users/jane.doe@databricks.com/my-demo-notebook", job.Settings.NotebookTask.NotebookPath) - assert.Equal(t, "my-demo-notebook", job.Settings.Name) - assert.Equal(t, 3600, int(job.Settings.TimeoutSeconds)) - assert.Equal(t, 1, int(job.Settings.MaxRetries)) - assert.Equal(t, 1, int(job.Settings.MaxConcurrentRuns)) + assert.Equal(t, "6.4.x-scala2.11", job.Settings.NewCluster.SparkVersion) + assert.Equal(t, model.AwsAvailability(model.AwsAvailabilitySpot), job.Settings.NewCluster.AwsAttributes.Availability) + assert.Equal(t, "us-east-1a", job.Settings.NewCluster.AwsAttributes.ZoneID) + assert.Equal(t, 100, int(job.Settings.NewCluster.AwsAttributes.SpotBidPricePercent)) + assert.Equal(t, 1, int(job.Settings.NewCluster.AwsAttributes.FirstOnDemand)) + assert.Equal(t, model.EbsVolumeType(model.EbsVolumeTypeGeneralPurposeSsd), job.Settings.NewCluster.AwsAttributes.EbsVolumeType) + assert.Equal(t, 1, int(job.Settings.NewCluster.AwsAttributes.EbsVolumeCount)) + assert.Equal(t, 32, int(job.Settings.NewCluster.AwsAttributes.EbsVolumeSize)) + assert.Equal(t, "r3.xlarge", job.Settings.NewCluster.NodeTypeID) + assert.Equal(t, "/Users/jane.doe@databricks.com/my-demo-notebook", job.Settings.NotebookTask.NotebookPath) + assert.Equal(t, "my-demo-notebook", job.Settings.Name) + assert.Equal(t, 3600, int(job.Settings.TimeoutSeconds)) + assert.Equal(t, 1, int(job.Settings.MaxRetries)) + assert.Equal(t, 1, int(job.Settings.MaxConcurrentRuns)) return nil } } func testAwsJobResourceNewCluster() string { - return fmt.Sprintf(` - resource "databricks_job" "my_job" { - new_cluster { - autoscale { - min_workers = 2 - max_workers = 3 - } - spark_version = "6.4.x-scala2.11" - aws_attributes { - availability = "SPOT" - zone_id = "us-east-1a" - spot_bid_price_percent = "100" - first_on_demand = 1 - ebs_volume_type = "GENERAL_PURPOSE_SSD" - ebs_volume_count = 1 - ebs_volume_size = 32 - } - node_type_id = "r3.xlarge" - } - notebook_path = "/Users/jane.doe@databricks.com/my-demo-notebook" - name = "my-demo-notebook" - timeout_seconds = 3600 - max_retries = 1 - max_concurrent_runs = 1 - } - `) + return ` + resource "databricks_job" "my_job" { + new_cluster { + autoscale { + min_workers = 2 + max_workers = 3 + } + spark_version = "6.4.x-scala2.11" + aws_attributes { + availability = "SPOT" + zone_id = "us-east-1a" + spot_bid_price_percent = "100" + first_on_demand = 1 + ebs_volume_type = "GENERAL_PURPOSE_SSD" + ebs_volume_count = 1 + ebs_volume_size = 32 + } + node_type_id = "r3.xlarge" + } + notebook_path = "/Users/jane.doe@databricks.com/my-demo-notebook" + name = "my-demo-notebook" + timeout_seconds = 3600 + max_retries = 1 + max_concurrent_runs = 1 + }` } diff --git a/databricks/resource_databricks_job_azure_test.go b/databricks/resource_databricks_job_azure_test.go index 5347063fd..73cc75cab 100644 --- a/databricks/resource_databricks_job_azure_test.go +++ b/databricks/resource_databricks_job_azure_test.go @@ -3,13 +3,14 @@ package databricks import ( "errors" "fmt" + "strconv" + "testing" + "github.com/databrickslabs/databricks-terraform/client/model" "github.com/databrickslabs/databricks-terraform/client/service" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/terraform" "github.com/stretchr/testify/assert" - "strconv" - "testing" ) func TestAccAzureJobResource(t *testing.T) { @@ -94,33 +95,32 @@ func testAzureJobValuesNewCluster(t *testing.T, job *model.Job) resource.TestChe assert.NotNil(t, job.Settings.NotebookTask) assert.Equal(t, 2, int(job.Settings.NewCluster.Autoscale.MinWorkers)) assert.Equal(t, 3, int(job.Settings.NewCluster.Autoscale.MaxWorkers)) - assert.Equal(t, "6.4.x-scala2.11", job.Settings.NewCluster.SparkVersion) - assert.Equal(t, "Standard_DS3_v2", job.Settings.NewCluster.NodeTypeID) - assert.Equal(t, "/Users/jane.doe@databricks.com/my-demo-notebook", job.Settings.NotebookTask.NotebookPath) - assert.Equal(t, "my-demo-notebook", job.Settings.Name) - assert.Equal(t, 3600, int(job.Settings.TimeoutSeconds)) - assert.Equal(t, 1, int(job.Settings.MaxRetries)) - assert.Equal(t, 1, int(job.Settings.MaxConcurrentRuns)) + assert.Equal(t, "6.4.x-scala2.11", job.Settings.NewCluster.SparkVersion) + assert.Equal(t, "Standard_DS3_v2", job.Settings.NewCluster.NodeTypeID) + assert.Equal(t, "/Users/jane.doe@databricks.com/my-demo-notebook", job.Settings.NotebookTask.NotebookPath) + assert.Equal(t, "my-demo-notebook", job.Settings.Name) + assert.Equal(t, 3600, int(job.Settings.TimeoutSeconds)) + assert.Equal(t, 1, int(job.Settings.MaxRetries)) + assert.Equal(t, 1, int(job.Settings.MaxConcurrentRuns)) return nil } } func testAzureJobResourceNewCluster() string { - return fmt.Sprintf(` - resource "databricks_job" "my_job" { - new_cluster { - autoscale { - min_workers = 2 - max_workers = 3 - } - spark_version = "6.4.x-scala2.11" - node_type_id = "Standard_DS3_v2" - } - notebook_path = "/Users/jane.doe@databricks.com/my-demo-notebook" - name = "my-demo-notebook" - timeout_seconds = 3600 - max_retries = 1 - max_concurrent_runs = 1 - } - `) + return ` + resource "databricks_job" "my_job" { + new_cluster { + autoscale { + min_workers = 2 + max_workers = 3 + } + spark_version = "6.4.x-scala2.11" + node_type_id = "Standard_DS3_v2" + } + notebook_path = "/Users/jane.doe@databricks.com/my-demo-notebook" + name = "my-demo-notebook" + timeout_seconds = 3600 + max_retries = 1 + max_concurrent_runs = 1 + }` } From 9c81f69230a73e01cd0666bb89fd9768f28e00fe Mon Sep 17 00:00:00 2001 From: Dave Storey Date: Mon, 15 Jun 2020 21:31:51 +0000 Subject: [PATCH 3/4] fixing typo and formatting issues --- client/service/commands.go | 2 +- databricks/mounts_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/service/commands.go b/client/service/commands.go index 37614f98c..827ce524a 100644 --- a/client/service/commands.go +++ b/client/service/commands.go @@ -18,7 +18,7 @@ type CommandsAPI struct { // CommandExecutor creates a spark context and executes a command and then closes context type CommandExecutor interface { - Execute(clusterID, langauge, commandStr string) (model.Command, error) + Execute(clusterID, language, commandStr string) (model.Command, error) } // Execute creates a spark context and executes a command and then closes context diff --git a/databricks/mounts_test.go b/databricks/mounts_test.go index e91cad73c..fc767a690 100644 --- a/databricks/mounts_test.go +++ b/databricks/mounts_test.go @@ -9,12 +9,12 @@ import ( "github.com/stretchr/testify/assert" ) -var executeMock func(clusterID, langauge, commandStr string) (model.Command, error) +var executeMock func(clusterID, language, commandStr string) (model.Command, error) type commandExecutorMock struct{} -func (a commandExecutorMock) Execute(clusterID, langauge, commandStr string) (model.Command, error) { - return executeMock(clusterID, langauge, commandStr) +func (a commandExecutorMock) Execute(clusterID, language, commandStr string) (model.Command, error) { + return executeMock(clusterID, language, commandStr) } func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { @@ -58,7 +58,7 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { return model.Command{ Results: tc.CommandResult, }, nil @@ -114,7 +114,7 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { return model.Command{ Results: tc.CommandResult, }, nil @@ -171,7 +171,7 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, langauge, commandStr string) (model.Command, error) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { return model.Command{ Results: tc.CommandResult, }, nil @@ -214,7 +214,7 @@ func TestProcessAzureWasbAbfssUrisCorrectlySplitsURI(t *testing.T) { assert.Equal(t, tc.ExpectedStorageAcc, storageAcc) assert.Equal(t, tc.ExpectedDirectory, dir) assert.Nil(t, err) - } + } } func TestValidateMountDirectory(t *testing.T) { From cf84787bf11d06ba7c010122a64f58b1de9132dc Mon Sep 17 00:00:00 2001 From: Dave Storey Date: Tue, 16 Jun 2020 15:35:59 +0000 Subject: [PATCH 4/4] added test run command PR feedback to verify interface --- client/service/commands_test.go | 3 ++ databricks/mounts_test.go | 81 +++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/client/service/commands_test.go b/client/service/commands_test.go index 6481e7487..673625275 100644 --- a/client/service/commands_test.go +++ b/client/service/commands_test.go @@ -7,6 +7,9 @@ import ( "github.com/databrickslabs/databricks-terraform/client/model" ) +// Test interface compliance +var _ CommandExecutor = (*CommandsAPI)(nil) + func TestCommandsAPI_Execute(t *testing.T) { type context struct { Language string `json:"language,omitempty"` diff --git a/databricks/mounts_test.go b/databricks/mounts_test.go index fc767a690..173e7f578 100644 --- a/databricks/mounts_test.go +++ b/databricks/mounts_test.go @@ -23,11 +23,13 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { const dir = "mydirectory" testCases := []struct { + Name string ExpectedResult string ExpectedError error CommandResult *model.CommandResults }{ { + Name: "Mount found successfully", ExpectedResult: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), CommandResult: &model.CommandResults{ ResultType: "text", @@ -35,6 +37,7 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount not found - no match in db", ExpectedError: errors.New("unable to find mount point"), CommandResult: &model.CommandResults{ ResultType: "text", @@ -42,6 +45,7 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount found - but does not match configuration", ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != abfss://x@y.dfs.core.windows.net/z!", cn, sacc), CommandResult: &model.CommandResults{ ResultType: "text", @@ -49,6 +53,7 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Error - db returns error", ExpectedError: errors.New("out of wibble error"), CommandResult: &model.CommandResults{ ResultType: "error", @@ -58,19 +63,21 @@ func TestAzureBlobMountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, language, commandStr string) (model.Command, error) { - return model.Command{ - Results: tc.CommandResult, - }, nil - } - executorMock := commandExecutorMock{} + t.Run(tc.Name, func(t *testing.T) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} - blobMount := NewAzureBlobMount(cn, sacc, dir, "mount", "", "", "") + blobMount := NewAzureBlobMount(cn, sacc, dir, "mount", "", "", "") - result, err := blobMount.Read(executorMock, "wibble") + result, err := blobMount.Read(executorMock, "wibble") - assert.Equal(t, tc.ExpectedResult, result) - assert.Equal(t, tc.ExpectedError, err) + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + }) } } @@ -79,11 +86,13 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { const dir = "mydirectory" testCases := []struct { + Name string ExpectedResult string ExpectedError error CommandResult *model.CommandResults }{ { + Name: "Mount found successfully", ExpectedResult: fmt.Sprintf("adl://%s.azuredatalakestore.net/%s", sacc, dir), CommandResult: &model.CommandResults{ ResultType: "text", @@ -91,6 +100,7 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount not found - no match in db", ExpectedError: errors.New("unable to find mount point"), CommandResult: &model.CommandResults{ ResultType: "text", @@ -98,6 +108,7 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount found - but does not match configuration", ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != adl://x.azuredatalakestore.net/z!", sacc, dir), CommandResult: &model.CommandResults{ ResultType: "text", @@ -105,6 +116,7 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Error - db returns error", ExpectedError: errors.New("out of wibble error"), CommandResult: &model.CommandResults{ ResultType: "error", @@ -114,19 +126,21 @@ func TestAzureADLSGen1MountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, language, commandStr string) (model.Command, error) { - return model.Command{ - Results: tc.CommandResult, - }, nil - } - executorMock := commandExecutorMock{} + t.Run(tc.Name, func(t *testing.T) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} - adlsGen2Mount := NewAzureADLSGen1Mount(sacc, dir, "mount", "", "", "", "", "") + adlsGen2Mount := NewAzureADLSGen1Mount(sacc, dir, "mount", "", "", "", "", "") - result, err := adlsGen2Mount.Read(executorMock, "wibble") + result, err := adlsGen2Mount.Read(executorMock, "wibble") - assert.Equal(t, tc.ExpectedResult, result) - assert.Equal(t, tc.ExpectedError, err) + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + }) } } @@ -136,11 +150,13 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { const dir = "mydirectory" testCases := []struct { + Name string ExpectedResult string ExpectedError error CommandResult *model.CommandResults }{ { + Name: "Mount found successfully", ExpectedResult: fmt.Sprintf("abfss://%s@%s.dfs.core.windows.net/%s", cn, sacc, dir), CommandResult: &model.CommandResults{ ResultType: "text", @@ -148,6 +164,7 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount not found - no match in db", ExpectedError: errors.New("unable to find mount point"), CommandResult: &model.CommandResults{ ResultType: "text", @@ -155,6 +172,7 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Mount found - but does not match configuration", ExpectedError: fmt.Errorf("does not match uri with storage account and container values %s@%s != abfss://x@y.dfs.core.windows.net/z!", cn, sacc), CommandResult: &model.CommandResults{ ResultType: "text", @@ -162,6 +180,7 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { }, }, { + Name: "Error - db returns error", ExpectedError: errors.New("out of wibble error"), CommandResult: &model.CommandResults{ ResultType: "error", @@ -171,19 +190,21 @@ func TestAzureADLSGen2MountReadRetrievesMountInformation(t *testing.T) { } for _, tc := range testCases { - executeMock = func(clusterID, language, commandStr string) (model.Command, error) { - return model.Command{ - Results: tc.CommandResult, - }, nil - } - executorMock := commandExecutorMock{} + t.Run(tc.Name, func(t *testing.T) { + executeMock = func(clusterID, language, commandStr string) (model.Command, error) { + return model.Command{ + Results: tc.CommandResult, + }, nil + } + executorMock := commandExecutorMock{} - adlsGen2Mount := NewAzureADLSGen2Mount(cn, sacc, dir, "mount", "", "", "", "", true) + adlsGen2Mount := NewAzureADLSGen2Mount(cn, sacc, dir, "mount", "", "", "", "", true) - result, err := adlsGen2Mount.Read(executorMock, "wibble") + result, err := adlsGen2Mount.Read(executorMock, "wibble") - assert.Equal(t, tc.ExpectedResult, result) - assert.Equal(t, tc.ExpectedError, err) + assert.Equal(t, tc.ExpectedResult, result) + assert.Equal(t, tc.ExpectedError, err) + }) } }