Repository: snowflakedb/gosnowflake
Pull request #924 (2023-10-11 09:56:45)

SNOW-856228 easy logging parser
### Description
Please explain the changes you made here.

### Checklist
- [x] Code compiles correctly
- [x] Run ``make fmt`` to fix inconsistent formats
- [x] Run ``make lint`` to get lint errors and fix all of them
- [x] Created tests which fail without the change (if possible)
- [x] All tests passing
- [x] Extended the README / documentation, if necessary


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/924?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) Report
> Merging [#924](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/924?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) (04082c1) into [master](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/3a295547f478bda564c3943b5e1f5d61a525437b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) (3a29554) will **decrease** coverage by `1.38%`.
> The diff coverage is `72.09%`.

```diff
@@            Coverage Diff             @@
##           master     #924      +/-   ##
==========================================
- Coverage   78.24%   76.86%   -1.38%     
==========================================
  Files          48       48              
  Lines        9244     9285      +41     
==========================================
- Hits         7233     7137      -96     
- Misses       1551     1695     +144     
+ Partials      460      453       -7     
```


| [Files](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/924?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) | Coverage Δ | |
|---|---|---|
| [client\_configuration.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/924?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-Y2xpZW50X2NvbmZpZ3VyYXRpb24uZ28=) | `72.09% <72.09%> (ø)` | |

... and [5 files with indirect coverage changes](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/924/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb)

sfc-gh-dheyman-1: test

Patch:
diff --git a/client_configuration.go b/client_configuration.go
new file mode 100644
index 0000000..381ed8d
--- /dev/null
+++ b/client_configuration.go
@@ -0,0 +1,87 @@
+// Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
+
+package gosnowflake
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+)
+
+// log levels for easy logging
+const (
+	levelOff   string = "OFF"   // log level for logging switched off
+	levelError string = "ERROR" // error log level
+	levelWarn  string = "WARN"  // warn log level
+	levelInfo  string = "INFO"  // info log level
+	levelDebug string = "DEBUG" // debug log level
+	levelTrace string = "TRACE" // trace log level
+)
+
+// ClientConfig config root
+type ClientConfig struct {
+	Common *ClientConfigCommonProps `json:"common"`
+}
+
+// ClientConfigCommonProps properties from "common" section
+type ClientConfigCommonProps struct {
+	LogLevel string `json:"log_level,omitempty"`
+	LogPath  string `json:"log_path,omitempty"`
+}
+
+func parseClientConfiguration(filePath string) (*ClientConfig, error) {
+	if filePath == "" {
+		return nil, nil
+	}
+	fileContents, err := os.ReadFile(filePath)
+	if err != nil {
+		return nil, parsingClientConfigError(err)
+	}
+	var clientConfig ClientConfig
+	err = json.Unmarshal(fileContents, &clientConfig)
+	if err != nil {
+		return nil, parsingClientConfigError(err)
+	}
+	err = validateClientConfiguration(&clientConfig)
+	if err != nil {
+		return nil, parsingClientConfigError(err)
+	}
+	return &clientConfig, nil
+}
+
+func parsingClientConfigError(err error) error {
+	return fmt.Errorf("parsing client config failed: %w", err)
+}
+
+func validateClientConfiguration(clientConfig *ClientConfig) error {
+	if clientConfig == nil {
+		return errors.New("client config not found")
+	}
+	if clientConfig.Common == nil {
+		return errors.New("common section in client config not found")
+	}
+	return validateLogLevel(*clientConfig)
+}
+
+func validateLogLevel(clientConfig ClientConfig) error {
+	var logLevel = clientConfig.Common.LogLevel
+	if logLevel != "" {
+		_, error := toLogLevel(logLevel)
+		if error != nil {
+			return error
+		}
+	}
+	return nil
+}
+
+func toLogLevel(logLevelString string) (string, error) {
+	var logLevel = strings.ToUpper(logLevelString)
+	switch logLevel {
+	case levelOff, levelError, levelWarn, levelInfo, levelDebug, levelTrace:
+		return logLevel, nil
+	default:
+		return "", errors.New("unknown log level: " + logLevelString)
+	}
+}


Test patch:
diff --git a/client_configuration_test.go b/client_configuration_test.go
new file mode 100644
index 0000000..b7ecdd2
--- /dev/null
+++ b/client_configuration_test.go
@@ -0,0 +1,175 @@
+// Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
+
+package gosnowflake
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"strings"
+	"testing"
+)
+
+func TestParseConfiguration(t *testing.T) {
+	dir := t.TempDir()
+	testCases := []struct {
+		testName         string
+		fileName         string
+		fileContents     string
+		expectedLogLevel string
+		expectedLogPath  string
+	}{
+		{
+			testName: "TestWithLogLevelUpperCase",
+			fileName: "config_1.json",
+			fileContents: `{
+				"common": {
+					"log_level" : "INFO",
+					"log_path" : "/some-path/some-directory"
+				}
+			}`,
+			expectedLogLevel: "INFO",
+			expectedLogPath:  "/some-path/some-directory",
+		},
+		{
+			testName: "TestWithLogLevelLowerCase",
+			fileName: "config_2.json",
+			fileContents: `{
+				"common": {
+					"log_level" : "info",
+					"log_path" : "/some-path/some-directory"
+				}
+			}`,
+			expectedLogLevel: "info",
+			expectedLogPath:  "/some-path/some-directory",
+		},
+		{
+			testName: "TestWithMissingValues",
+			fileName: "config_3.json",
+			fileContents: `{
+				"common": {}
+			}`,
+			expectedLogLevel: "",
+			expectedLogPath:  "",
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.testName, func(t *testing.T) {
+			fileName := createFile(t, tc.fileName, tc.fileContents, dir)
+
+			config, err := parseClientConfiguration(fileName)
+
+			if err != nil {
+				t.Fatalf("Error should be nil but was %s", err)
+			}
+			if config.Common.LogLevel != tc.expectedLogLevel {
+				t.Errorf("Log level should be %s but was %s", tc.expectedLogLevel, config.Common.LogLevel)
+			}
+			if config.Common.LogPath != tc.expectedLogPath {
+				t.Errorf("Log path should be %s but was %s", tc.expectedLogPath, config.Common.LogPath)
+			}
+		})
+	}
+}
+
+func TestParseAllLogLevels(t *testing.T) {
+	dir := t.TempDir()
+	for _, logLevel := range []string{"OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"} {
+		t.Run(logLevel, func(t *testing.T) {
+			fileContents := fmt.Sprintf(`{
+				"common": {
+					"log_level" : "%s",
+					"log_path" : "/some-path/some-directory"
+				}
+			}`, logLevel)
+			fileName := createFile(t, fmt.Sprintf("config_%s.json", logLevel), fileContents, dir)
+
+			config, err := parseClientConfiguration(fileName)
+
+			if err != nil {
+				t.Fatalf("Error should be nil but was: %s", err)
+			}
+			if config.Common.LogLevel != logLevel {
+				t.Errorf("Log level should be %s but was %s", logLevel, config.Common.LogLevel)
+			}
+		})
+	}
+}
+
+func TestParseConfigurationFails(t *testing.T) {
+	dir := t.TempDir()
+	testCases := []struct {
+		testName                      string
+		fileName                      string
+		FileContents                  string
+		expectedErrorMessageToContain string
+	}{
+		{
+			testName: "TestWithWrongLogLevel",
+			fileName: "config_1.json",
+			FileContents: `{
+				"common": {
+					"log_level" : "something weird",
+					"log_path" : "/some-path/some-directory"
+				}
+			}`,
+			expectedErrorMessageToContain: "unknown log level",
+		},
+		{
+			testName: "TestWithWrongTypeOfLogLevel",
+			fileName: "config_2.json",
+			FileContents: `{
+				"common": {
+					"log_level" : 15,
+					"log_path" : "/some-path/some-directory"
+				}
+			}`,
+			expectedErrorMessageToContain: "ClientConfigCommonProps.common.log_level",
+		},
+		{
+			testName: "TestWithWrongTypeOfLogPath",
+			fileName: "config_3.json",
+			FileContents: `{
+				"common": {
+					"log_level" : "INFO",
+					"log_path" : true
+				}
+			}`,
+			expectedErrorMessageToContain: "ClientConfigCommonProps.common.log_path",
+		},
+		{
+			testName:                      "TestWithoutCommon",
+			fileName:                      "config_4.json",
+			FileContents:                  "{}",
+			expectedErrorMessageToContain: "common section in client config not found",
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.testName, func(t *testing.T) {
+			fileName := createFile(t, tc.fileName, tc.FileContents, dir)
+
+			_, err := parseClientConfiguration(fileName)
+
+			if err == nil {
+				t.Fatal("Error should not be nil but was nil")
+			}
+			errMessage := fmt.Sprint(err)
+			expectedPrefix := "parsing client config failed"
+			if !strings.HasPrefix(errMessage, expectedPrefix) {
+				t.Errorf("Error message: \"%s\" should start with prefix: \"%s\"", errMessage, expectedPrefix)
+			}
+			if !strings.Contains(errMessage, tc.expectedErrorMessageToContain) {
+				t.Errorf("Error message: \"%s\" should contain given phrase: \"%s\"", errMessage, tc.expectedErrorMessageToContain)
+			}
+		})
+	}
+}
+
+func createFile(t *testing.T, fileName string, fileContents string, directory string) string {
+	fullFileName := path.Join(directory, fileName)
+	err := os.WriteFile(fullFileName, []byte(fileContents), 0644)
+	if err != nil {
+		t.Fatal("Could not create file")
+	}
+	return fullFileName
+}