From 9339e6cf098218c0c5cd7e008d711fe2ad066d3e Mon Sep 17 00:00:00 2001 From: Paulo Truta Date: Sun, 13 Jun 2021 12:03:36 +0200 Subject: [PATCH] Storage Module Basis (#13) * Added storage module, base structs, getDevices() func, taskGetStorageDevices (ticks every 30) * GetDevices now returns a complete description w/ disk partitions * Added InUse flag to Device, fixed logic bugs * Added disk space usage for partitions in use by the system * Cleanup * Added UsageStat and UsageSplit filling for in use devices * Changed value of lsblk block usage output to bytes * Cleanup, small fixes * Added ExecAndGetLines util func * Added explicit detection of disk or part block type, ignoring non-suppoorted devices * Added command line tool dependencies for the project in README file * Added comment to func ExecAndGetLines * Removed GetMySQLDbConnectionDetails() * Added tests to utils module * Fix invalid argument type for Exec family of tests * Removed too much verbosity from utils.GetPath when no env file is available, added new argument (path) to Exec family of funcs * Added new path argument to utils.Exec... test * Fixed TestExec * Added spaces trim to Exec result * Trimming spaces and newlines from result of Exec * Better visibility for test result log of TestExec * Added ExampleExecAndStream test * Added Example tests with failure status for ExecAndStream * Added branch to test for valid key in GetPath * Added missing t.Fail() statements in utils_test.go * Added storage_test.go file, smoke test * TestGetDevices --- README.md | 27 ++- go.mod | 2 + go.sum | 4 + internal/edgeapps/edgeapps.go | 12 +- internal/storage/storage.go | 272 +++++++++++++++++++++++++++++++ internal/storage/storage_test.go | 31 ++++ internal/tasks/tasks.go | 43 ++++- internal/utils/utils.go | 48 +++--- internal/utils/utils_test.go | 90 ++++++++++ 9 files changed, 483 insertions(+), 46 deletions(-) create mode 100644 internal/storage/storage.go create mode 100644 internal/storage/storage_test.go diff --git a/README.md b/README.md index b49a510..b264102 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,16 @@ ## Table of Contents -* [About the Project](#about-the-project) - * [Built With](#built-with) -* [Getting Started](#getting-started) - * [Prerequisites](#prerequisites) - * [Installation](#installation) -* [Usage](#usage) -* [Roadmap](#roadmap) -* [Contributing](#contributing) -* [License](#license) +- [Table of Contents](#table-of-contents) +- [About The Project](#about-the-project) + - [Built With](#built-with) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) @@ -88,6 +89,14 @@ sudo apt-get install docker docker-compose Check the following links for more info on [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/). +Aditionally, edgeboxctl needs the following bash commands available wherever it runs: +* `sh` +* `rm` +* `systemctl` +* `lsblk` +* `yq` +* `tinc-boot` _(not mandatory)_ + ### Installation 1. Clone the repo diff --git a/go.mod b/go.mod index 175fc69..1645bc4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.15 require ( github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect + github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/go-ole/go-ole v1.2.5 // indirect github.com/go-sql-driver/mysql v1.5.0 github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index 3fe5772..c25bdef 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY= github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2 h1:5EPE4Uk7ucthLTJAZqZxu6LZluox5/AqXUxJDpzgJjg= +github.com/dariubs/percent v0.0.0-20200128140941-b7801cf1c7e2/go.mod h1:NDZpkezJ8QqyIW/510MywB5T2KdC8v/0oTlEoPcMsRM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= diff --git a/internal/edgeapps/edgeapps.go b/internal/edgeapps/edgeapps.go index ecace61..b5d99a7 100644 --- a/internal/edgeapps/edgeapps.go +++ b/internal/edgeapps/edgeapps.go @@ -239,14 +239,14 @@ func GetEdgeAppStatus(ID string) EdgeAppStatus { func GetEdgeAppServices(ID string) []EdgeAppService { cmdArgs := []string{"-r", ".services | keys[]", utils.GetPath("edgeAppsPath") + ID + configFilename} - servicesString := utils.Exec("yq", cmdArgs) + servicesString := utils.Exec(utils.GetPath("wsPath"), "yq", cmdArgs) serviceSlices := strings.Split(servicesString, "\n") serviceSlices = utils.DeleteEmptySlices(serviceSlices) var edgeAppServices []EdgeAppService for _, serviceID := range serviceSlices { cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "exec", "-T", serviceID, "echo", "'Service Check'"} - cmdResult := utils.Exec("docker-compose", cmdArgs) + cmdResult := utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) isRunning := false if cmdResult != "" { isRunning = true @@ -268,7 +268,7 @@ func RunEdgeApp(ID string) EdgeAppStatus { for _, service := range services { cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "start", service.ID} - utils.Exec("docker-compose", cmdArgs) + utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) } @@ -289,7 +289,7 @@ func StopEdgeApp(ID string) EdgeAppStatus { for _, service := range services { cmdArgs = []string{"-f", utils.GetPath("wsPath") + "/docker-compose.yml", "stop", service.ID} - utils.Exec("docker-compose", cmdArgs) + utils.Exec(utils.GetPath("wsPath"), "docker-compose", cmdArgs) } @@ -326,7 +326,7 @@ func DisableOnline(ID string) MaybeEdgeApp { log.Println("myedge.app environment file for " + ID + " not found. No need to delete.") } else { cmdArgs := []string{envFilePath} - utils.Exec("rm", cmdArgs) + utils.Exec(utils.GetPath("wsPath"), "rm", cmdArgs) } buildFrameworkContainers() @@ -338,7 +338,7 @@ func DisableOnline(ID string) MaybeEdgeApp { func buildFrameworkContainers() { cmdArgs := []string{utils.GetPath("wsPath") + "ws", "--build"} - utils.ExecAndStream("sh", cmdArgs) + utils.ExecAndStream(utils.GetPath("wsPath"), "sh", cmdArgs) time.Sleep(defaultContainerOperationSleepTime) diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..58628db --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,272 @@ +package storage + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/edgebox-iot/edgeboxctl/internal/utils" + "github.com/shirou/gopsutil/disk" +) + +// Device : Struct representing a storage device in the system +type Device struct { + ID string `json:"id"` + Name string `json:"name"` + Size string `json:"size"` + InUse bool `json:"in_use"` + MainDevice bool `json:"main_device"` + MAJ string `json:"maj"` + MIN string `json:"min"` + RM string `json:"rm"` + RO string `json:"ro"` + Partitions []Partition `json:"partitions"` + Status DeviceStatus `json:"status"` + UsageStat UsageStat `json:"usage_stat"` +} + +// DeviceStatus : Struct representing possible storage device statuses (code + description) +type DeviceStatus struct { + ID int `json:"id"` + Description string `json:"description"` +} + +// MaybeDevice : Boolean flag for validation of device existance +type MaybeDevice struct { + Device Device `json:"device"` + Valid bool `json:"valid"` +} + +type UsageStat struct { + Total uint64 `json:"total"` + Used uint64 `json:"used"` + Free uint64 `json:"free"` + Percent string `json:"percent"` + UsageSplit UsageSplit `json:"usage_split"` +} + +type UsageSplit struct { + OS uint64 `json:"os"` + EdgeApps uint64 `json:"edgeapps"` + Buckets uint64 `json:"buckets"` + Others uint64 `json:"others"` +} + +// Partition : Struct representing a partition / filesystem (Empty Mountpoint means it is not mounted) +type Partition struct { + ID string `json:"id"` + Size string `json:"size"` + MAJ string `json:"maj"` + MIN string `json:"min"` + RM string `json:"rm"` + RO string `json:"ro"` + Filesystem string `json:"filesystem"` + Mountpoint string `json:"mountpoint"` + UsageStat UsageStat `json:"usage_stat"` +} + +const mainDiskID = "mmcblk0" + +// GetDevices : Returns a list of all available sotrage devices in structs filled with information +func GetDevices() []Device { + + var devices []Device + + cmdArgs := []string{"--raw", "--bytes", "--noheadings"} + scanner := utils.ExecAndGetLines("/", "lsblk", cmdArgs) + + var currentDevice Device + var currentPartitions []Partition + + firstDevice := true + currentDeviceInUseFlag := false + + for scanner.Scan() { + // 1 Device is represented here. Extract words in order for filling a Device struct + // Example deviceRawInfo: "mmcblk0 179:0 0 29.7G 0 disk" + deviceRawInfo := strings.Fields(scanner.Text()) + majMin := strings.SplitN(deviceRawInfo[1], ":", 2) + + isDevice := false + isPartition := false + if deviceRawInfo[5] == "part" { + isDevice = false + isPartition = true + + } else if deviceRawInfo[5] == "disk" { + isDevice = true + isPartition = false + } + + if isDevice { + // Clean up on the latest device being prepared. Append all partitions found and delete the currentPartitions list afterwards. + // The first device found should not run the cleanup lines below + + if !firstDevice { + currentDevice.Partitions = currentPartitions + + if !currentDeviceInUseFlag { + currentDevice.Status.ID = 0 + currentDevice.Status.Description = "not configured" + } + + currentDevice.InUse = currentDeviceInUseFlag + currentDeviceInUseFlag = false + currentPartitions = []Partition{} + devices = append(devices, currentDevice) + } else { + firstDevice = false + } + + mainDevice := false + + device := Device{ + ID: deviceRawInfo[0], + Name: deviceRawInfo[0], + Size: deviceRawInfo[3], + MainDevice: mainDevice, + MAJ: majMin[0], + MIN: majMin[1], + RM: deviceRawInfo[2], + RO: deviceRawInfo[4], + Status: DeviceStatus{ID: 1, Description: "healthy"}, + } + + if device.ID == mainDiskID { + device.MainDevice = true + } + + currentDevice = device + + } else if isPartition { + + mountpoint := "" + if len(deviceRawInfo) >= 7 { + mountpoint = deviceRawInfo[6] + currentDeviceInUseFlag = true + } + + // It is a partition, part of the last device read. + partition := Partition{ + ID: deviceRawInfo[0], + Size: deviceRawInfo[3], + MAJ: majMin[0], + MIN: majMin[1], + RM: deviceRawInfo[2], + RO: deviceRawInfo[4], + Filesystem: "", + Mountpoint: mountpoint, + } + + currentPartitions = append(currentPartitions, partition) + + } else { + fmt.Println("Found device not compatible with Edgebox, ignoring.") + } + + } + + currentDevice.Partitions = currentPartitions + if !currentDeviceInUseFlag { + currentDevice.Status.ID = 0 + currentDevice.Status.Description = "Not configured" + } + currentDevice.InUse = currentDeviceInUseFlag + devices = append([]Device{currentDevice}, devices...) // Prepending the first device... + + devices = getDevicesSpaceUsage(devices) + + return devices +} + +func getDevicesSpaceUsage(devices []Device) []Device { + + for deviceIndex, device := range devices { + + if device.InUse { + + deviceUsageStat := UsageStat{} + + for partitionIndex, partition := range device.Partitions { + + if partition.Mountpoint != "" { + + s, _ := disk.Usage(partition.Mountpoint) + + if s.Total == 0 { + continue + } + + partitionUsagePercent := fmt.Sprintf("%2.f%%", s.UsedPercent) + osUsageSplit := (uint64)(0) + edgeappsUsageSplit := (uint64)(0) + bucketsUsageSplit := (uint64)(0) + othersUsageSplit := (uint64)(0) + + edgeappsDirSize, _ := getDirSize(utils.GetPath("edgeAppsPath")) + // TODO for later: Figure out to get correct paths for each partition... + wsAppDataDirSize, _ := getDirSize("/home/system/components/ws/appdata") + + if partition.Mountpoint == "/" { + edgeappsUsageSplit = edgeappsDirSize + wsAppDataDirSize + deviceUsageStat.UsageSplit.EdgeApps += edgeappsUsageSplit + } + + if device.MainDevice { + osUsageSplit = (s.Used - othersUsageSplit - bucketsUsageSplit - edgeappsUsageSplit) + } else { + othersUsageSplit = (s.Used - bucketsUsageSplit - edgeappsUsageSplit) + } + + partitionUsageSplit := UsageSplit{ + OS: osUsageSplit, + EdgeApps: edgeappsUsageSplit, + Buckets: bucketsUsageSplit, + Others: othersUsageSplit, + } + + deviceUsageStat.Total = deviceUsageStat.Total + s.Total + deviceUsageStat.Used = deviceUsageStat.Used + s.Used + deviceUsageStat.Free = deviceUsageStat.Free + s.Free + deviceUsageStat.UsageSplit.OS = deviceUsageStat.UsageSplit.OS + osUsageSplit + deviceUsageStat.UsageSplit.EdgeApps = deviceUsageStat.UsageSplit.EdgeApps + edgeappsUsageSplit + deviceUsageStat.UsageSplit.Buckets = deviceUsageStat.UsageSplit.Buckets + bucketsUsageSplit + deviceUsageStat.UsageSplit.Others = deviceUsageStat.UsageSplit.Others + othersUsageSplit + + devices[deviceIndex].Partitions[partitionIndex].UsageStat = UsageStat{ + Total: s.Total, + Used: s.Used, + Free: s.Free, + Percent: partitionUsagePercent, + UsageSplit: partitionUsageSplit, + } + + } + + } + + devices[deviceIndex].UsageStat = deviceUsageStat + totalDevicePercentUsage := fmt.Sprintf("%2.f%%", (float32(devices[deviceIndex].UsageStat.Used)/float32(devices[deviceIndex].UsageStat.Total))*100) + devices[deviceIndex].UsageStat.Percent = totalDevicePercentUsage + + } + + } + + return devices +} + +func getDirSize(path string) (uint64, error) { + var size uint64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + size += (uint64)(info.Size()) + } + return err + }) + return size, err +} diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go new file mode 100644 index 0000000..a0893bb --- /dev/null +++ b/internal/storage/storage_test.go @@ -0,0 +1,31 @@ +// +build unit + +package storage + +import ( + "testing" +) + +func TestGetDevices(t *testing.T) { + result := GetDevices() + + if len(result) == 0 { + t.Log("Expecting at least 1 block device, 0 elements found in slice") + t.Fail() + } + + foundDevice := false + + t.Log("Looking for a mmcblk0 or sda device") + for _, device := range result { + if device.ID == "mmcblk0" || device.ID == "sda" { + t.Log("Found target device", device.ID) + foundDevice = true + } + } + + if !foundDevice { + t.Log("Expected to find device mmcblk0 but did not. Devices:", result) + t.Fail() + } +} diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go index 064ffa2..f8e8978 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -10,6 +10,7 @@ import ( "github.com/edgebox-iot/edgeboxctl/internal/diagnostics" "github.com/edgebox-iot/edgeboxctl/internal/edgeapps" + "github.com/edgebox-iot/edgeboxctl/internal/storage" "github.com/edgebox-iot/edgeboxctl/internal/system" "github.com/edgebox-iot/edgeboxctl/internal/utils" _ "github.com/go-sql-driver/mysql" // Mysql Driver @@ -253,10 +254,16 @@ func ExecuteSchedules(tick int) { uptime := taskGetSystemUptime() log.Println("Uptime is " + uptime + " seconds (" + system.GetUptimeFormatted() + ")") + log.Println(taskGetStorageDevices()) log.Println(taskGetEdgeApps()) } + if tick%5 == 0 { + // Executing every 5 ticks + log.Println(taskGetStorageDevices()) + } + if tick%30 == 0 { // Executing every 30 ticks log.Println(taskGetEdgeApps()) @@ -277,13 +284,13 @@ func taskSetupTunnel(args taskSetupTunnelArgs) string { fmt.Println("Executing taskSetupTunnel") cmdargs := []string{"gen", "--name", args.NodeName, "--token", args.BootnodeToken, args.BootnodeAddress + ":8655", "--prefix", args.AssignedAddress} - utils.Exec("tinc-boot", cmdargs) + utils.Exec(utils.GetPath("wsPath"), "tinc-boot", cmdargs) cmdargs = []string{"start", "tinc@dnet"} - utils.Exec("systemctl", cmdargs) + utils.Exec(utils.GetPath("wsPath"), "systemctl", cmdargs) cmdargs = []string{"enable", "tinc@dnet"} - utils.Exec("systemctl", cmdargs) + utils.Exec(utils.GetPath("wsPath"), "systemctl", cmdargs) output := "OK" // Better check / logging of command execution result. return output @@ -435,3 +442,33 @@ func taskGetSystemUptime() string { return uptime } + +func taskGetStorageDevices() string { + fmt.Println("Executing taskGetStorageDevices") + + devices := storage.GetDevices() + devicesJSON, _ := json.Marshal(devices) + + db, err := sql.Open("sqlite3", utils.GetSQLiteDbConnectionDetails()) + + if err != nil { + log.Fatal(err.Error()) + } + + statement, err := db.Prepare("REPLACE into option (name, value, created, updated) VALUES (?, ?, ?, ?);") // Prepare SQL Statement + if err != nil { + log.Fatal(err.Error()) + } + + formatedDatetime := utils.GetSQLiteFormattedDateTime(time.Now()) + + _, err = statement.Exec("STORAGE_DEVICES_LIST", devicesJSON, formatedDatetime, formatedDatetime) // Execute SQL Statement + if err != nil { + log.Fatal(err.Error()) + } + + db.Close() + + return string(devicesJSON) + +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index ce9d032..996c0ea 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,31 +1,33 @@ package utils import ( + "bufio" "bytes" "fmt" "io" "log" "os" "os/exec" + "strings" "time" "github.com/joho/godotenv" ) // ExecAndStream : Runs a terminal command, but streams progress instead of outputting. Ideal for long lived process that need to be logged. -func ExecAndStream(command string, args []string) { +func ExecAndStream(path string, command string, args []string) { cmd := exec.Command(command, args...) var stdoutBuf, stderrBuf bytes.Buffer cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - cmd.Dir = GetPath("wsPath") + cmd.Dir = path err := cmd.Run() if err != nil { - log.Fatalf("cmd.Run() failed with %s\n", err) + fmt.Printf("cmd.Run() failed with %s\n", err) } outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes()) @@ -34,13 +36,13 @@ func ExecAndStream(command string, args []string) { } // Exec : Runs a terminal Command, catches and logs errors, returns the result. -func Exec(command string, args []string) string { +func Exec(path string, command string, args []string) string { cmd := exec.Command(command, args...) var out bytes.Buffer var stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr - cmd.Dir = GetPath("wsPath") + cmd.Dir = path err := cmd.Run() if err != nil { // TODO: Deal with possibility of error in command, allow explicit error handling and return proper formatted stderr @@ -49,10 +51,20 @@ func Exec(command string, args []string) string { // log.Println("Result: " + out.String()) // ... Silence ... - return out.String() + return strings.Trim(out.String(), " \n") } +// Exec : Runs a terminal Command, returns the result as a *bufio.Scanner type, split in lines and ready to parse. +func ExecAndGetLines(path string, command string, args []string) *bufio.Scanner { + cmdOutput := Exec(path, command, args) + cmdOutputReader := strings.NewReader(cmdOutput) + scanner := bufio.NewScanner(cmdOutputReader) + scanner.Split(bufio.ScanLines) + + return scanner +} + // DeleteEmptySlices : Given a string array, delete empty entries. func DeleteEmptySlices(s []string) []string { var r []string @@ -64,25 +76,6 @@ func DeleteEmptySlices(s []string) []string { return r } -// GetMySQLDbConnectionDetails : Returns the necessary string as connection info for SQL.db() -func GetMySQLDbConnectionDetails() string { - - var apiEnv map[string]string - apiEnv, err := godotenv.Read(GetPath("apiEnvFileLocation")) - - if err != nil { - log.Fatal("Error loading .env file") - } - - Dbhost := "127.0.0.1:" + apiEnv["HOST_MACHINE_MYSQL_PORT"] - Dbname := apiEnv["MYSQL_DATABASE"] - Dbuser := apiEnv["MYSQL_USER"] - Dbpass := apiEnv["MYSQL_PASSWORD"] - - return Dbuser + ":" + Dbpass + "@tcp(" + Dbhost + ")/" + Dbname - -} - // GetSQLiteDbConnectionDetails : Returns the necessary string as connection info for SQL.db() func GetSQLiteDbConnectionDetails() string { @@ -112,11 +105,10 @@ func GetPath(pathKey string) string { // Read whole of .env file to map. var env map[string]string env, err := godotenv.Read() - targetPath := "" + var targetPath string if err != nil { - // log.Println("Project .env file not found withing project root. Using only hardcoded path variables.") - // Do Nothing... + targetPath = "" } switch pathKey { diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index e09778e..eef26e2 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -7,6 +7,96 @@ import ( "time" ) +func TestExec(t *testing.T) { + testCommand := "echo" + testArguments := []string{"Hello World"} + + result := Exec("/", testCommand, testArguments) + + if result != "Hello World" { + t.Log("Expected 'Hello World' but got", "'"+result+"'") + t.Fail() + } +} + +func ExampleExecAndStream() { + ExecAndStream("/", "echo", []string{"Hello"}) + // Output: + // Hello + // + // out: + // Hello + // + // err: +} + +func ExampleExecAndStreamExecutableNotFound() { + ExecAndStream("/", "testcommand", []string{"Hello"}) + // Output: + // cmd.Run() failed with exec: "testcommand": executable file not found in $PATH + // + // out: + // + // err: +} + +func ExampleExecAndStreamError() { + ExecAndStream("/", "man", []string{"Hello"}) + // Output: + // cmd.Run() failed with exit status 16 + // + // out: + // + // err: + // No manual entry for Hello +} + +func TestExecAndGetLines(t *testing.T) { + testCommand := "echo" + testArguments := []string{"$'Line1\nLine2\nLine3'"} + var result []string + + scanner := ExecAndGetLines("/", testCommand, testArguments) + + for scanner.Scan() { + result = append(result, scanner.Text()) + } + + if len(result) != 3 { + t.Log("Expected 3 lines but got ", len(result)) + t.Fail() + } +} + +func TestDeleteEmptySlices(t *testing.T) { + testSlice := []string{"Line1", "", "Line3"} + + resultSlice := DeleteEmptySlices(testSlice) + + if (len(resultSlice)) != 2 { + t.Log("Expected 2 slices but got ", len(resultSlice)) + t.Fail() + } + +} + +func TestGetPath(t *testing.T) { + invalidPathKey := "test" + + result := GetPath(invalidPathKey) + if result != "" { + t.Log("Expected empty result but got ", result) + t.Fail() + } + + validPathKey := "wsPath" + result = GetPath(validPathKey) + if result != "/home/system/components/ws/" { + t.Log("Expected /home/system/components/ws/ but got", result) + t.Fail() + } +} + func TestGetSQLiteFormattedDateTime(t *testing.T) { datetime := time.Date(2021, time.Month(1), 01, 1, 30, 15, 0, time.UTC) result := GetSQLiteFormattedDateTime(datetime)