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
pull/18/head 0.0.1
Paulo Truta 2021-06-13 12:03:36 +02:00 committed by GitHub
parent 92ac9c2b03
commit 9339e6cf09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 483 additions and 46 deletions

View File

@ -42,15 +42,16 @@
<!-- 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)
- [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

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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)

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)