fuzzyworld

template / commit

Adding tests, getting code coverage to be reasonable for most files (main.go is at 0%), and might be there for a bit.

9573ca022bf4fc61e69a07aa7c99da26190844e4 · Pavel Safronov · 2022-11-15 17:16:01 -0800 · parents 5e424f1b0adc

Adding tests, getting code coverage to be reasonable for most files (main.go is at 0%), and might be there for a bit.
 .vscode/launch.json   |  12 ++--
 .vscode/settings.json |   8 ++-
 config.go             |   4 +-
 config_test.go        |  76 +++++++++++++++++++-
 main.go               |  37 +++++++---
 monkey-patch.go       |   4 --
 profile.go            |  59 ++++++++-------
 profile_test.go       | 194 ++++++++++++++++++++++++++++++++++----------------
 8 files changed, 279 insertions(+), 115 deletions(-)

diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8602236..13d3f1a 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,6 +4,7 @@
     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "configurations": [
+        
         {
             "name": "Launch tests",
             "type": "go",
@@ -72,7 +73,7 @@
             ]
         },
         {
-            "name": "Run: template work test-project --use-defaults",
+            "name": "Run: template work test-project w/ in-memory defaults",
             "type": "go",
             "request": "launch",
             "mode": "auto",
@@ -80,8 +81,10 @@
             "args": [
                 "work",
                 "test-project",
-                "--use-defaults"
             ],
+            "env": {
+                "TEMPLATE_USE_DEFAULT_CONFIG": "true",
+            },
         },
         {
             "name": "Run: template --profile work --project testProj",
@@ -123,7 +126,6 @@
                 "--log-level",
                 "debug",
             ]
-        ]
-    }
-]
+        }
+    ]
 }
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ee0aba7..3e2495f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,12 @@
 {
   "cSpell.words": [
+    "bashdb",
+    "logrus",
     "pwsh",
     "runpostscriptonfailure",
     "scriptshell",
-    "stretchr"
+    "sirupsen",
+    "stretchr",
+    "urfave"
   ]
-}
+}
\ No newline at end of file
diff --git a/config.go b/config.go
index afd393b..7d6fd30 100644
--- a/config.go
+++ b/config.go
@@ -88,11 +88,11 @@ func saveConfig(path string, config Config) error {
 func mergeConfigs(a Config, b Config) (Config, error) {
 	err := validateConfig(&a)
 	if err != nil {
-		return Config{}, err
+		return Config{}, fmt.Errorf("error validating config a: %s", err)
 	}
 	err = validateConfig(&b)
 	if err != nil {
-		return Config{}, err
+		return Config{}, fmt.Errorf("error validating config b: %s", err)
 	}
 
 	merged := Config{
diff --git a/config_test.go b/config_test.go
index 2453a0e..aac86ff 100644
--- a/config_test.go
+++ b/config_test.go
@@ -8,6 +8,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const configPath string = "./test/test-config.yaml"
+
 // Compares YAML representation of two objects
 func compareObjectYaml(t *testing.T, expected, actual interface{}) {
 	expectedYaml, err := getYaml(expected)
@@ -21,7 +23,6 @@ func compareObjectYaml(t *testing.T, expected, actual interface{}) {
 func TestSaveLoadConfig(t *testing.T) {
 
 	config := defaultConfig()
-	configPath := "./test/test-config.yaml"
 
 	// save config
 	err := saveConfig(configPath, config)
@@ -35,7 +36,7 @@ func TestSaveLoadConfig(t *testing.T) {
 	compareObjectYaml(t, loadedConfig, config)
 
 	// test load non-existent config
-	_, err = loadConfig("non-existent-file.yaml")
+	_, err = loadConfig("./test/non-existent-file.yaml")
 	assert.NotNil(t, err, "Expected to fail to load config, but succeeded")
 
 	// test load invalid yaml
@@ -133,4 +134,75 @@ func TestValidateConfig(t *testing.T) {
 			},
 		},
 	}}, "multiple default profiles found")
+	testFailure(Config{Profiles: []Profile{
+		{
+			Name:         "daily",
+			IsDefault:    true,
+			NamePatterns: []ParameterizedText{"daily"},
+			Type:         FileType,
+			Path:         "foo",
+			File: File{
+				Template: "foo",
+			},
+		},
+		{
+			Name:         "daily",
+			IsDefault:    true,
+			NamePatterns: []ParameterizedText{"daily"},
+			Type:         FileType,
+			Path:         "foo",
+			File: File{
+				Template: "foo",
+			},
+		},
+	}}, "duplicate profile name 'daily'")
+}
+
+func TestLoadConfig(t *testing.T) {
+	bytes := []byte(`
+profiles:
+    - name:
+`)
+	os.WriteFile("./test/invalid-config.yaml", bytes, 0644)
+
+	// load config
+	_, err := loadConfig("./test/invalid-config.yaml")
+	assert.NotNil(t, err, "Expected to fail to load config, but succeeded")
+	assert.Contains(t, err.Error(), "error validating profile : Profile name is required")
+}
+
+func TestSaveConfig(t *testing.T) {
+	config := Config{Profiles: []Profile{}}
+	err := saveConfig("./test/invalid-config.yaml", config)
+	assert.NotNil(t, err, "Expected to fail to save config, but succeeded")
+	assert.Contains(t, err.Error(), "no profiles found")
+}
+
+func TestMergeConfigs(t *testing.T) {
+	defaultConfig := defaultConfig()
+	testSuccess := func(config1 Config, config2 Config) Config {
+		config, err := mergeConfigs(config1, config2)
+		// compareObjectYaml(t, mergedConfig, expected)
+		assert.Nil(t, err, "Expected to merge configs, but failed")
+		return config
+	}
+	testFailure := func(config1 Config, config2 Config, errorText string) {
+		_, err := mergeConfigs(config1, config2)
+		assert.NotNil(t, err, "Expected to fail to merge configs, but succeeded")
+		assert.Contains(t, err.Error(), errorText)
+	}
+	testFailure(Config{}, Config{}, "no profiles found")
+	testFailure(Config{Profiles: []Profile{Profile{}}}, Config{}, "error validating config a: error validating profile : Profile name is required")
+	testFailure(Config{Profiles: []Profile{defaultConfig.Profiles[0]}}, Config{}, "error validating config b: no profiles found")
+
+	profile1 := defaultConfig.Profiles[0]
+	profile2 := defaultConfig.Profiles[1]
+	testSuccess(Config{Profiles: []Profile{profile1}}, Config{Profiles: []Profile{profile2}})
+
+	profile1_updated := profile1
+	profile1_updated.Description += " This profile has been updated!"
+
+	result := testSuccess(Config{Profiles: []Profile{profile1}}, Config{Profiles: []Profile{profile1_updated}})
+	assert.Equal(t, 1, len(result.Profiles))
+	assert.Equal(t, profile1_updated.Name, result.Profiles[0].Name)
 }
diff --git a/main.go b/main.go
index b6aaf1f..852cf0a 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"os"
 	"os/user"
+	"strconv"
 	"strings"
 
 	log "github.com/sirupsen/logrus"
@@ -22,10 +23,13 @@ type ArgumentValues struct {
 	userConfigPath string
 	extrasFilePath string
 	logLevel       string
-	useDefaults    bool
+}
+type EnvVarValues struct {
+	useDefaultConfig bool
 }
 type Settings struct {
 	ArgumentValues
+	EnvVarValues
 	extras         map[string]string
 	fullConfigPath string
 	config         Config
@@ -88,13 +92,6 @@ func main() {
 				Value:       defaultLogLevel,
 				Destination: &argumentValues.logLevel,
 			},
-			&cli.BoolFlag{
-				Name:        "use-defaults",
-				Aliases:     []string{"d"},
-				Usage:       "use default values for config",
-				Value:       false,
-				Destination: &argumentValues.useDefaults,
-			},
 		},
 		Commands: []*cli.Command{
 			{
@@ -186,8 +183,13 @@ func createSettings(
 	argumentValues ArgumentValues,
 	context *cli.Context,
 ) (Settings, error) {
+	envVarValues, err := readEnvVars()
+	if err != nil {
+		return Settings{}, fmt.Errorf("error getting env var values: %w", err)
+	}
 	settings := Settings{
 		ArgumentValues: argumentValues,
+		EnvVarValues:   envVarValues,
 	}
 
 	// read args
@@ -231,7 +233,7 @@ func createSettings(
 	fmt.Println("Project name:", settings.projectName)
 
 	// extras strings
-	_, err := os.Stat(settings.extrasFilePath)
+	_, err = os.Stat(settings.extrasFilePath)
 	if err == nil {
 		extras, err := loadExtras(settings.extrasFilePath)
 		if err != nil {
@@ -252,7 +254,7 @@ func createSettings(
 	settings.fullConfigPath = fullConfigPath
 
 	var config Config
-	if settings.useDefaults {
+	if settings.useDefaultConfig {
 		config = defaultConfig()
 	} else {
 		config, err = loadConfig(fullConfigPath)
@@ -282,6 +284,21 @@ func createSettings(
 	return settings, nil
 }
 
+func readEnvVars() (EnvVarValues, error) {
+	envVarValues := EnvVarValues{}
+
+	useDefaultConfigText := os.Getenv("TEMPLATE_USE_DEFAULT_CONFIG")
+	if useDefaultConfigText != "" {
+		useDefaultConfig, err := strconv.ParseBool(useDefaultConfigText)
+		if err != nil {
+			return envVarValues, fmt.Errorf("error parsing env var TEMPLATE_USE_DEFAULT_CONFIG: \"%s\" %w", useDefaultConfigText, err)
+		}
+		envVarValues.useDefaultConfig = useDefaultConfig
+	}
+
+	return envVarValues, nil
+}
+
 func findProfile(settings Settings) (Profile, error) {
 	var profile Profile = Profile{}
 	var err error
diff --git a/monkey-patch.go b/monkey-patch.go
index 97c20d9..26a6ec9 100644
--- a/monkey-patch.go
+++ b/monkey-patch.go
@@ -8,10 +8,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-type monkeyOsDirEntry interface {
-	Name() string
-	IsDir() bool
-}
 type monkeyPatch struct {
 	ExecCommand func(name string, arg ...string) *exec.Cmd
 	OSWriteFile func(filename string, data []byte, perm os.FileMode) error
diff --git a/profile.go b/profile.go
index d04edae..705c66e 100644
--- a/profile.go
+++ b/profile.go
@@ -62,8 +62,7 @@ var forbiddenProfileNames = []string{
 
 func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User) []error {
 	if profile.IsDisabled {
-		fmt.Println("Profile is disabled, doing nothing")
-		return nil
+		return []error{fmt.Errorf("Profile is disabled, doing nothing")}
 	}
 
 	// decide if the profile should run on this OS
@@ -74,11 +73,9 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 		(profileOs == "Windows" && isWindows) ||
 		(profileOs == "Mac" && !isWindows)
 	if !shouldRun {
-		fmt.Println("This profile is not supported by this OS")
-		fmt.Println("Supported OS values: Windows, Mac, All")
-		fmt.Println("Current OS:", runtime.GOOS)
-		fmt.Println("Profile OS:", profileOs)
-		return nil
+		return []error{
+			fmt.Errorf("This profile is not supported on this OS, doing nothing. Profile OS: %s, system OS: %s. Supported OS values: Windows, Mac, All", profileOs, runtime.GOOS),
+		}
 	}
 
 	cwd, err := monkey.OSGetwd()
@@ -147,31 +144,33 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 	}
 
 	// execute PostScript, if present
-	postPatternData := createPostPatternData(fullPath, prePatternData)
-	postScripts, err := createPostScriptArrays(profile, postPatternData)
-	if err != nil {
-		errors = append(errors, fmt.Errorf("unable to create post scripts: %v", err))
-		return errors
-	}
-	for _, postScript := range postScripts {
-
-		shell, args := shellCommandAndArgs(postScript)
-		var out bytes.Buffer
-		fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(args, " "))
-		cmd := monkey.ExecCommand(shell, args...)
-		cmd.Stdout = &out
-		err := cmd.Run()
-		output := out.String()
-		if output != "" {
-			fmt.Printf("Post script output: %v\n", output)
-		} else {
-			fmt.Printf("Post script had no output\n")
-		}
+	if profile.PostScripts != nil && len(profile.PostScripts) > 0 {
+		postPatternData := createPostPatternData(fullPath, prePatternData)
+		postScripts, err := createPostScriptArrays(profile, postPatternData)
 		if err != nil {
-			// post scripts can fail (TODO: investigate why), so for now we'll just log the error
+			errors = append(errors, fmt.Errorf("unable to create post scripts: %v", err))
+			return errors
+		}
+		for _, postScript := range postScripts {
 
-			fmt.Printf("Error executing post script '%v': %v\n",
-				fmt.Sprintf("%v %v", shell, strings.Trim(fmt.Sprint(args), "[]")), err)
+			shell, args := shellCommandAndArgs(postScript)
+			var out bytes.Buffer
+			fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(args, " "))
+			cmd := monkey.ExecCommand(shell, args...)
+			cmd.Stdout = &out
+			err := cmd.Run()
+			output := out.String()
+			if output != "" {
+				fmt.Printf("Post script output: %v\n", output)
+			} else {
+				fmt.Printf("Post script had no output\n")
+			}
+			if err != nil {
+				// post scripts can fail (TODO: investigate why), so for now we'll just log the error
+
+				fmt.Printf("Error executing post script '%v': %v\n",
+					fmt.Sprintf("%v %v", shell, strings.Trim(fmt.Sprint(args), "[]")), err)
+			}
 		}
 	}
 
diff --git a/profile_test.go b/profile_test.go
index f75e0ac..d276b89 100644
--- a/profile_test.go
+++ b/profile_test.go
@@ -1,10 +1,12 @@
 package main
 
 import (
+	"io/fs"
 	"os"
 	"os/exec"
 	"os/user"
 	"path/filepath"
+	"runtime"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -17,11 +19,10 @@ func TestValidateProfile(t *testing.T) {
 			t.Errorf("Expected to validate profile '%v', but failed: %v", profile.Name, err)
 		}
 	}
-	testFailure := func(profile Profile, testName string) {
+	testFailure := func(profile Profile, expectedErrStr string) {
 		err := validateProfile(&profile)
-		if err == nil {
-			t.Errorf("%v - Expected to fail to validate profile '%v', but succeeded", testName, profile.Name)
-		}
+		assert.NotNil(t, err, "Expected to fail to validate profile '%v', but succeeded")
+		assert.Contains(t, err.Error(), expectedErrStr, "Expected error to contain '%v', but it didn't", expectedErrStr)
 	}
 
 	testSuccess(Profile{
@@ -42,22 +43,28 @@ func TestValidateProfile(t *testing.T) {
 
 	testFailure(Profile{
 		Name: "",
-	}, "empty name")
+	}, "Profile name is required")
 	testFailure(Profile{
 		Name: "list",
-	}, "illegal name")
+	}, "forbidden profile name 'list'")
 	testFailure(Profile{
 		Name: "get",
-	}, "illegal name")
+	}, "forbidden profile name 'get'")
 	testFailure(Profile{
 		Type: FileType,
 		Name: "daily",
-	}, "missing path")
+	}, "namePatterns is required")
 	testFailure(Profile{
-		Type: FileType,
-		Name: "daily",
-	}, "invalid path")
+		Type:         FileType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"blah"},
+	}, "path is not specified for profile daily, which has Type=file")
 
+	testFailure(Profile{
+		Type:         FolderType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"test"},
+	}, "path is required for profile daily, which has Type=folder")
 	testFailure(Profile{
 		Type:         FolderType,
 		Name:         "daily",
@@ -65,12 +72,22 @@ func TestValidateProfile(t *testing.T) {
 		File: File{
 			Template: "bar",
 		},
-	}, "file in folder")
+	}, "file template is not supported for profile daily, which has Type=folder")
 	testFailure(Profile{
 		Type:         ScriptType,
 		Name:         "daily",
 		NamePatterns: []ParameterizedText{"test", "test2"},
-	}, "multiple name patterns")
+	}, "multiple name patterns are not supported for profile daily, which has Type=script")
+	testFailure(Profile{
+		Type:         FileType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"test", "test2"},
+	}, "path is not specified for profile daily, which has Type=file")
+	testFailure(Profile{
+		Type:         FileType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"test", ""},
+	}, "name pattern is required for pattern index 1 of profile daily")
 
 	for _, profile := range defaultConfig().Profiles {
 		testSuccess(profile)
@@ -218,68 +235,125 @@ func TestCreateEntity(t *testing.T) {
 	testScript()
 }
 
+type testFile struct {
+	name  string
+	isDir bool
+}
+
+func (f testFile) Name() string               { return f.name }
+func (f testFile) IsDir() bool                { return f.isDir }
+func (f testFile) Type() fs.FileMode          { return 0 }
+func (f testFile) Info() (fs.FileInfo, error) { return nil, nil }
+
 func TestProcessProfile(t *testing.T) {
 	cwd := filepath.Clean("~/test/dir")
-	monkeyPatchCalls := applyTestMonkeyPatch(t, monkeyPatch{
-		ExecCommand: func(command string, args ...string) *exec.Cmd {
-			return exec.Command("echo", "foo")
-		},
-		OSChdir: func(dir string) error { return nil },
-		OSGetwd: func() (dir string, err error) { return cwd, nil },
-		OSReadDir: func(dirname string) ([]os.DirEntry, error) {
-			return []os.DirEntry{}, nil
-		},
-	})
+	createDirEntry := func(name string, isDir bool) os.DirEntry {
+		entry := testFile{name: name, isDir: isDir}
+		return entry
+	}
+	var monkeyPatchCalls *monkeyPatchCalls
+	configureMonkeyPatching := func() {
+		monkeyPatchCalls = applyTestMonkeyPatch(t, monkeyPatch{
+			ExecCommand: func(command string, args ...string) *exec.Cmd {
+				return exec.Command("echo")
+			},
+			OSChdir: func(dir string) error { return nil },
+			OSGetwd: func() (dir string, err error) { return cwd, nil },
+			OSReadDir: func(dirname string) ([]os.DirEntry, error) {
+				return []os.DirEntry{
+					createDirEntry("dir1", true),
+					createDirEntry("dir2", true),
+					createDirEntry("file1", false),
+					createDirEntry("file2", false),
+				}, nil
+			},
+			OSWriteFile: func(filename string, data []byte, perm os.FileMode) error {
+				return nil
+			},
+		})
+	}
 
 	projectName := "project-name"
 	extras := map[string]string{
 		"foo": "bar",
 	}
 
-	testSuccess := func() {
-		profile := Profile{
-			Path: cwd,
-			NamePatterns: []ParameterizedText{
-				"{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}",
-			},
-			RunPostscriptOnFailure: true,
-			PostScripts: []ParameterizedText{
-				"echo 'hi'",
-			},
-		}
+	usr, err := user.Current()
+	assert.Nil(t, err)
+	assert.NotNil(t, usr)
 
-		usr, err := user.Current()
-		assert.Nil(t, err)
-		assert.NotNil(t, usr)
+	testSuccess := func(profile Profile) {
+		configureMonkeyPatching()
 		errors := processProfile(profile, projectName, extras, *usr)
 		assert.Equal(t, 0, len(errors))
-		assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls))
-		assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls))
-		assert.Equal(t, 1, len(monkeyPatchCalls.OSGetwdCalls))
-		assert.Equal(t, 0, len(monkeyPatchCalls.OSReadDirCalls))
+		for _, error := range errors {
+			assert.Nil(t, error)
+		}
 	}
-	testFailure := func() {
-		profile := Profile{
-			Path: cwd,
-			NamePatterns: []ParameterizedText{
-				"{{.ProjectName",
-			},
-			// RunPostscriptOnFailure: true,
-			PostScripts: []ParameterizedText{
-				"echo 'hi'",
-			},
-			Type: FileType,
+	testFailure := func(profile Profile, errorCount int, errorStr string) {
+		configureMonkeyPatching()
+		errors := processProfile(profile, projectName, extras, *usr)
+		assert.Equal(t, errorCount, len(errors))
+		allErrorsText := ""
+		for _, err := range errors {
+			allErrorsText += err.Error()
 		}
+		assert.Contains(t, allErrorsText, errorStr)
+	}
+	testSingleFailure := func(profile Profile, errorStr string) {
+		testFailure(profile, 1, errorStr)
+	}
 
-		usr, err := user.Current()
-		assert.Nil(t, err)
-		assert.NotNil(t, usr)
-
-		errors := processProfile(profile, projectName, extras, *usr)
-		assert.Equal(t, 1, len(errors))
+	// this profile will be built up from failing to eventually passing
+	currentOS := getCurrentOs()
+	profile := Profile{
+		IsDisabled: true,
 	}
+	testSingleFailure(profile, "Profile is disabled, doing nothing")
+	profile.IsDisabled = false
+	profile.OS = getOtherOs(currentOS)
+	testSingleFailure(profile, "This profile is not supported on this OS, doing nothing.")
+	profile.OS = currentOS
+	// testSingleFailure(profile, "unable to create post scripts: no post scripts specified")
 
-	testSuccess()
+	profile.NamePatterns = []ParameterizedText{}
+	profile.Type = FileType
+	testSingleFailure(profile, "couldn't find a good new name")
 
-	testFailure()
+	profile.NamePatterns = []ParameterizedText{"file1"}
+	profile.RunPostscriptOnFailure = true
+	testSingleFailure(profile, "couldn't find a good new name")
+
+	profile.NamePatterns = []ParameterizedText{"file3"}
+	profile.PostScripts = []ParameterizedText{"echo {{.ProjectName}}-{{.Year}}"}
+	profile.Path = "~/test/dir"
+	testSuccess(profile)
+	assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls), "calls to ExecCommand")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls), "calls to OSChdir")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSGetwdCalls), "calls to OSGetwd")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSReadDirCalls), "calls to OSReadDir")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls), "calls to OSWriteFile")
+
+	profile.PostScripts = []ParameterizedText{"echo ''"}
+	testSuccess(profile)
+	assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls), "calls to ExecCommand")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls), "calls to OSChdir")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSGetwdCalls), "calls to OSGetwd")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSReadDirCalls), "calls to OSReadDir")
+	assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls), "calls to OSWriteFile")
+}
+
+func getCurrentOs() OSType {
+	os := runtime.GOOS
+	if os == "windows" {
+		return "Windows"
+	} else {
+		return "Mac"
+	}
+}
+func getOtherOs(currentOs OSType) OSType {
+	if currentOs == "windows" {
+		return "Mac"
+	}
+	return "Windows"
 }