diff --git a/.gitignore b/.gitignore index f1c181e..38322d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,4 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE +client/client +server/server *.out +*.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..72451ad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM golang:1.11-alpine3.8 + +EXPOSE 9090 + +# Preparation stage. +RUN apk add build-base + +RUN apk update && apk upgrade && apk add git + +RUN go get golang.org/x/tools/cmd/goimports + +RUN go get -u golang.org/x/lint/golint + +RUN go get github.com/stretchr/testify + +ADD . /go/src/NonRelDB/ + +# Check stage. +WORKDIR /go/src/NonRelDB + +RUN go vet ./... + +RUN goimports ./ + +RUN golint ./... + +# Build stage. +WORKDIR /go/src/NonRelDB/server + +RUN go build server.go + +WORKDIR /go/src/NonRelDB/client + +RUN go build client.go + +# Entrypoint bind. +ENTRYPOINT [ "/go/src/NonRelDB/server/server" ] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf4486a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 777777miSSU7777777 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..922a304 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +CURRENT_DIR = $(shell pwd) + +check: + go vet ./... + + goimports ./ + + golint ./... + +clean-binaries: + rm server/server && rm client/client + +build-server: + go build -o server/server $(CURRENT_DIR)/server/server.go + +build-client: + go build -o client/client $(CURRENT_DIR)/client/client.go + +clean: + sudo docker system prune + +build: clean + sudo docker build -t "nonreldb" . + +run: clean + sudo docker run -d --net=host nonreldb + +test: + echo "Running unit & integration tests" + + go test ./... -coverprofile coverage.out + + go tool cover -html=coverage.out + + + + diff --git a/Project.pdf b/Project.pdf new file mode 100644 index 0000000..53f81b5 Binary files /dev/null and b/Project.pdf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2307fe1 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +## NonRelDB + +NonRelDB is an in-memory database that persists on disk. The data model is key-value. Written on pure golang. + +## Installation +First of all you need to install [git](https://git-scm.com/) and [docker](https://www.docker.com/) +Then you need to clone repository on your pc from github + + git clone https://github.com/777777miSSU7777777/NonRelDB.git + +In cloned repository you will find **Makefile** with following targets: + + - **build-server** - builds server's executable binary file. + - **build-client** - builds client's executable binary file. + - **clean-binaries** - removes server's & client's binaries on local machine. + - **build** - copies server's & client's & dependencies src, adds go vet, goimports and golint. Runs checks and if no errors were occured, builds server's & client's binaries. Entrypoint is server with default configuration. + - **clean** - cleans docker's unused containers, networks, volumes and dangling images. + - **check** - runs subsequently go vet, goimports and golint on the project. Fails if any error occurs. + - **test** - runs unit & integration tests. Fails if any test don't pass. + - **run** - runs built docker container in detached mode. +## Usage +### Server's flags + - **-host -h** - defines host ip (default is 127.0.0.1) + - **-port -p** - defines host port (default is 9090) + - **-mode -m** - defines storage location (default is "memory"). Possible options are "memory" and "disk". + - **-location -l** - defines storage location on disk (default is "storage.json"). +### Client's flags + - **-host -h** - defines host ip (default is 127.0.0.1) + - **-port -p** - defines host port (default is 9090) + - **--dump** - requests full database dump in json format on stdout. + Usage example + + ./client --dump > dump.json + + - **--restore** - restores database from stdin. + Usage example + + + ./client --restore < dump.json + +### Commands +Commands can be entered only in one register (**GET** and **get** but not **Get**). + + **List of supported commands** + - **GET** - returns the value if existing, otherwise message "Value with this key not found". + Example + + + GET 123 + + + - **SET** - set the value if existing, otherwise creates new. Also returns message "Value has changed". + **Value must be in double quotes.* + Example + + + SET 123 "123" + + + - **DEL** - deletes value from storage and returns it's value if existing, otherwise message "Value with this key not found". + Example + + + DEL 123 + +- **KEYS** - returns all keys matching to entered regexp pattern, otherwise message "Keys with this pattern not found" or "Pattern is incorrect". +**Regex pattern must be in double quotes.* +Example + + KEYS "/*" + +- **SUBSCRIBE** - subscribes the client on specified channel. +Example + + SUBSCRIBE redis + +- **UNSUBSCRIBE** - unsubscribes the client from specified channel. +**Cannot use from client because after subscribe client turns into listening state.* +Example + + UNSUBSCRIBE redis + +- **PUBLISH** - sends the message to specified channel. +*\*Message must be in double quotes.* +Example + + PUBLISH redis "Hello world" + +## Project requirements + + - There no verbose mode and flag because logger any way logs full user's request. + - Not implemented TAB completion of the commands in cli. + + ## Notice + - Server & client will crash with non-existing flag's values. + - Not implented -help flag. diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..8be1a7a --- /dev/null +++ b/client/client.go @@ -0,0 +1,63 @@ +package main + +import ( + "NonRelDB/client/handler" + "NonRelDB/log" + "bufio" + "flag" + "fmt" + "net" + "os" +) + +var host string +var port string +var dump bool +var restore bool +var location string + +func init() { + flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.BoolVar(&dump, "dump", false, "Requests db dump in json format from server") + flag.BoolVar(&restore, "restore", false, "Restores db from dumped file") + flag.Parse() +} + +// main entry point for client. +func main() { + c, err := net.Dial("tcp", host+":"+port) + defer c.Close() + + if err != nil { + log.Error.Panicln(err.Error()) + } + + if dump { + fmt.Fprintf(c, "dump\n") + dbDump, err := bufio.NewReader(c).ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Println(dbDump) + return + } + + if restore { + fmt.Fprintf(c, "restore\n") + dbRestore, err := bufio.NewReader(os.Stdin).ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Fprintf(c, dbRestore) + return + } + + handler.HandleConnection(c) +} diff --git a/client/handler/handle_connection.go b/client/handler/handle_connection.go new file mode 100644 index 0000000..63982f1 --- /dev/null +++ b/client/handler/handle_connection.go @@ -0,0 +1,65 @@ +package handler + +import ( + "NonRelDB/log" + "NonRelDB/util/regex" + "bufio" + "fmt" + "net" + "os" + "strings" +) + +// SendRequest sends request to specified connection. +func SendRequest(req string, c net.Conn) { + fmt.Fprintf(c, req+"\n") +} + +// HandleConnection handling communication with server. +func HandleConnection(c net.Conn) { + consoleReader := bufio.NewReader(os.Stdin) + netReader := bufio.NewReader(c) + for { + fmt.Printf("%s> ", c.RemoteAddr().String()) + req, err := consoleReader.ReadString('\n') + req = strings.Trim(req, "\n") + + if err != nil { + log.Error.Panicln(err.Error()) + } + + if regex.QueryReg.MatchString(req) { + SendRequest(req, c) + resp, err := netReader.ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Println(resp) + + } else if regex.ExitReg.MatchString(req) { + fmt.Println("Good bye") + SendRequest(req, c) + return + + } else if regex.TopicReg.MatchString(req) { + reqParts := strings.Split(req, " ") + + if len(reqParts) == 2 { + if strings.ToLower(reqParts[0]) == "subscribe" { + SendRequest(req, c) + HandleTopic(c, *netReader, reqParts[1]) + } + } else if len(reqParts) >= 3 { + if strings.ToLower(reqParts[0]) == "publish" { + SendRequest(req, c) + } + } + + } else { + fmt.Println("Bad request") + continue + } + } +} diff --git a/client/handler/handle_topic.go b/client/handler/handle_topic.go new file mode 100644 index 0000000..5c6b986 --- /dev/null +++ b/client/handler/handle_topic.go @@ -0,0 +1,38 @@ +package handler + +import ( + "NonRelDB/log" + "bufio" + "fmt" + "net" + "os" + "os/signal" + "syscall" +) + +// quit Handling Ctrl + C press. +func quit(c net.Conn, topic string) { + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + fmt.Println("Ctrl+C pressed in Terminal") + fmt.Fprintf(c, "unsubscribe "+topic+"\n") + os.Exit(0) + }() +} + +// HandleTopic listening messages from specified topic. +func HandleTopic(c net.Conn, r bufio.Reader, topic string) { + quit(c, topic) + fmt.Printf("Reading messages from %s... (press Ctrl + C to stop)\n", topic) + for { + msg, err := r.ReadString('\n') + + if err != nil { + log.Error.Panicln(err.Error()) + } + + fmt.Print(msg) + } +} diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..3d93de9 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,22 @@ +package log + +import ( + "io/ioutil" + "log" + "os" +) + +// Presents 4 logging levels: trace, info, warning and error. +var ( + Trace *log.Logger + Info *log.Logger + Warning *log.Logger + Error *log.Logger +) + +func init() { + Trace = log.New(ioutil.Discard, "[TRACE] ", log.Ldate|log.Ltime|log.Lshortfile) + Info = log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime|log.Lshortfile) + Warning = log.New(os.Stdout, "[WARNING] ", log.Ldate|log.Ltime|log.Lshortfile) + Error = log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile) +} diff --git a/server/handler/handle_connection.go b/server/handler/handle_connection.go new file mode 100644 index 0000000..39e11bd --- /dev/null +++ b/server/handler/handle_connection.go @@ -0,0 +1,88 @@ +package handler + +import ( + "NonRelDB/log" + "NonRelDB/server/storage/inmemory" + "NonRelDB/server/topic" + "NonRelDB/util/json" + "NonRelDB/util/regex" + "bufio" + "fmt" + "net" + "strings" +) + +// SendResponse sends response to specified connection. +func SendResponse(resp string, c net.Conn) { + fmt.Fprintf(c, resp+"\n") + log.Info.Printf("Sent response to %s -> %s", c.RemoteAddr().String(), resp) +} + +// HandleConnection handling communication with client. +func HandleConnection(c net.Conn) { + defer c.Close() + + netReader := bufio.NewReader(c) + + for { + req, err := netReader.ReadString('\n') + req = strings.TrimSuffix(req, "\n") + + if err != nil { + log.Error.Println(err.Error()) + return + } + + log.Info.Printf("Received request from %s -> %s", c.RemoteAddr().String(), req) + + if regex.QueryReg.MatchString(req) { + resp := HandleQuery(req, inmemory.GetStorage()) + SendResponse(resp, c) + + } else if regex.ExitReg.MatchString(req) { + log.Info.Printf("%s disconnected from server", c.RemoteAddr().String()) + return + + } else if regex.DumpReg.MatchString(req) { + dbDump := string(json.PackMapToJSON((*inmemory.GetStorage().GetMap()))) + fmt.Fprintf(c, dbDump+"\n") + log.Info.Printf("Sent db dump to %s", c.RemoteAddr().String()) + return + + } else if regex.RestoreReg.MatchString(req) { + dbDump, err := netReader.ReadString('\n') + + if err != nil { + log.Warning.Println(err.Error()) + } + + inmemory.RestoreDBFromDump([]byte(dbDump)) + log.Info.Printf("Successfully restored dump from %s", c.RemoteAddr().String()) + return + + } else if regex.TopicReg.MatchString(req) { + reqParts := strings.Split(req, " ") + + if len(reqParts) == 2 { + if strings.ToLower(reqParts[0]) == "subscribe" { + topic.Subscribe(reqParts[1], c) + + } else if strings.ToLower(reqParts[0]) == "unsubscribe" { + topic.Unsubscribe(reqParts[1], c) + return + + } + + } else if len(reqParts) >= 3 { + if strings.ToLower(reqParts[0]) == "publish" { + msg := regex.DoubleQuoteReg.FindString(req) + topic.Publish(reqParts[1], msg) + } + + } + + } else { + SendResponse("Bad request", c) + } + } +} diff --git a/server/handler/handle_listener.go b/server/handler/handle_listener.go new file mode 100644 index 0000000..ec6c503 --- /dev/null +++ b/server/handler/handle_listener.go @@ -0,0 +1,20 @@ +package handler + +import ( + "NonRelDB/log" + "net" +) + +// HandleListener accepts clients and runs their handlers. +func HandleListener(l net.Listener) { + defer l.Close() + for { + c, err := l.Accept() + if err != nil { + log.Warning.Printf("Failed connection from %s", c.RemoteAddr().String()) + c.Close() + } + log.Info.Printf("%s was connected to server", c.RemoteAddr().String()) + go HandleConnection(c) + } +} diff --git a/server/handler/handle_query.go b/server/handler/handle_query.go new file mode 100644 index 0000000..ba3cb8b --- /dev/null +++ b/server/handler/handle_query.go @@ -0,0 +1,37 @@ +package handler + +import ( + "NonRelDB/util/regex" + "NonRelDB/util/sync" + "strings" +) + +// HandleQuery handling queries to db. +func HandleQuery(query string, syncMap *sync.Map) string { + queryParts := strings.Split(query, " ") + + if len(queryParts) >= 2 { + switch queryCtx := strings.ToLower(queryParts[0]); queryCtx { + case "get": + { + return syncMap.Get(queryParts[1]) + } + case "set": + { + value := strings.Trim(regex.DoubleQuoteReg.FindString(query), "\"") + return syncMap.Set(queryParts[1], value) + } + case "del": + { + return syncMap.Del(queryParts[1]) + } + case "keys": + { + pattern := strings.Trim(regex.DoubleQuoteReg.FindString(query), "\"") + return syncMap.Keys(pattern) + } + } + } + + return "Undefined query" +} diff --git a/server/handler/handle_query_test.go b/server/handler/handle_query_test.go new file mode 100644 index 0000000..40daad5 --- /dev/null +++ b/server/handler/handle_query_test.go @@ -0,0 +1,63 @@ +package handler + +import ( + "NonRelDB/util/sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +var testSyncMap *sync.Map + +// Used as setup in testing. +func init() { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + testSyncMap = &sync.Map{} + testSyncMap.SetMap(&testMap) +} + +func TestHandleQuery__GetExisting__Found(t *testing.T) { + assert.Equal(t, "one", HandleQuery("get 1", testSyncMap)) +} + +func TestHandleQuery__GetExisting__NotFound(t *testing.T) { + assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) +} + +func TestHandleQuery__SetExisting__Changed(t *testing.T) { + assert.Equal(t, "two", HandleQuery("get 2", testSyncMap)) + assert.Equal(t, "Value has changed", HandleQuery("set 2 \"2\"", testSyncMap)) + assert.Equal(t, "2", HandleQuery("get 2", testSyncMap)) +} + +func TestHandleQuery__SetNonExistring__Created(t *testing.T) { + assert.Equal(t, "Value with this key not found", HandleQuery("get 4", testSyncMap)) + assert.Equal(t, "Value has changed", HandleQuery("set 4 \"four\"", testSyncMap)) + assert.Equal(t, "four", HandleQuery("get 4", testSyncMap)) +} + +func TestHandleQuery__DelNonExisting__NotFound(t *testing.T) { + assert.Equal(t, "Value with this key not found", HandleQuery("del 5", testSyncMap)) +} + +func TestHandleQuery__DelExisting__Deleted(t *testing.T) { + assert.Equal(t, "three", HandleQuery("del 3", testSyncMap)) + assert.Equal(t, "Value with this key not found", HandleQuery("get 3", testSyncMap)) +} + +func TestHandleQuery__KeysExistingCorrectWildcard__Found(t *testing.T) { + querySet := HandleQuery("keys \"/*\"", testSyncMap) + assert.True(t, "1,2,4" == querySet || "1,4,2" == querySet || "2,1,4" == querySet || "2,4,1" == querySet || "4,1,2" == querySet || "4,2,1" == querySet) +} + +func TestHandleQuery__KeysNotExisting__NotFound(t *testing.T) { + assert.Equal(t, "Keys with this pattern not found", HandleQuery("keys \"123\"", testSyncMap)) +} + +func TestHandleQuery__KeysIncorrectWildcard__IncorrectPattern(t *testing.T) { + assert.Equal(t, "Pattern is incorrect", HandleQuery("keys \"*\"", testSyncMap)) +} + +func TestHandleQuery__123__UndefinedQuery(t *testing.T) { + assert.Equal(t, "Undefined query", HandleQuery("123", testSyncMap)) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..e932c7c --- /dev/null +++ b/server/server.go @@ -0,0 +1,67 @@ +package main + +import ( + "NonRelDB/log" + "NonRelDB/server/handler" + "NonRelDB/server/storage/inmemory" + "flag" + "net" + "os" + "os/signal" + "syscall" +) + +var host string +var port string +var mode string +var location string + +func init() { + flag.StringVar(&host, "host", "127.0.0.1", "Defines host ip") + flag.StringVar(&host, "h", "127.0.0.1", "Defines host ip") + flag.StringVar(&port, "port", "9090", "Defines host port") + flag.StringVar(&port, "p", "9090", "Defines host port") + flag.StringVar(&mode, "mode", "memory", "Defines storage location") + flag.StringVar(&mode, "m", "memory", "Defines storage location") + flag.StringVar(&location, "location", "storage.json", "Defines storage location on disk") + flag.StringVar(&location, "l", "storage.json", "Defines storage location on disk") + flag.Parse() +} + +// storageInit init of storage. +func storageInit() { + if mode == "memory" { + inmemory.InitDBInMemory() + } else if mode == "disk" { + inmemory.InitDBFromStorage(location) + } +} + +// cleanup storage cleanup. +func cleanup() { + sign := make(chan os.Signal, 2) + signal.Notify(sign, os.Interrupt, syscall.SIGTERM) + go func() { + <-sign + log.Info.Println("Ctrl+C pressed in Terminal") + if mode == "disk" { + inmemory.SaveDBToStorage(location) + } + os.Exit(0) + }() +} + +// main entry point for server. +func main() { + storageInit() + cleanup() + + l, err := net.Listen("tcp", host+":"+port) + + if err != nil { + log.Error.Panicln(err.Error()) + } + + log.Info.Printf("Server started listening on %s", l.Addr().String()) + handler.HandleListener(l) +} diff --git a/server/storage/inmemory/inmemory_service.go b/server/storage/inmemory/inmemory_service.go new file mode 100644 index 0000000..3a9befe --- /dev/null +++ b/server/storage/inmemory/inmemory_service.go @@ -0,0 +1,67 @@ +package inmemory + +import ( + "NonRelDB/log" + "NonRelDB/util/file" + "NonRelDB/util/json" + "NonRelDB/util/sync" + "os" +) + +// Global variable for kv storage. +var storage *sync.Map + +// GetStorage getter for storage. +func GetStorage() *sync.Map { + return storage +} + +// SetStorage setter for storage +func SetStorage(syncMap *sync.Map) { + storage = syncMap +} + +// InitDBInMemory init kv db in memory. +func InitDBInMemory() { + storage = &sync.Map{} + syncMap := make(map[string]string) + storage.SetMap(&syncMap) + log.Info.Println("DB successfully created in-memory") +} + +// InitDBFromStorage receives filename and load its content to inmemory storage. +func InitDBFromStorage(filename string) { + storage = &sync.Map{} + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + log.Warning.Println(err.Error()) + log.Warning.Printf("Storage doesnt exist. Will be created new with name %s", filename) + + f, err := os.Create(filename) + + if err != nil { + log.Error.Panicln(err.Error()) + } + f.Close() + } + + jsonString := file.OpenAndReadString(filename) + jsonBytes := []byte(jsonString) + storage.SetMap(json.UnpackFromJSON(jsonBytes)) + log.Info.Printf("DB successfully initialized from %s", filename) +} + +// RestoreDBFromDump restores db from received dump. +func RestoreDBFromDump(dump []byte) { + storage.SetMap(json.UnpackFromJSON(dump)) +} + +// SaveDBToStorage receives file name and saves inmemory storage to it. +func SaveDBToStorage(filename string) { + jsonBytes := json.PackMapToJSON((*storage.GetMap())) + jsonString := string(jsonBytes) + + file.CreateAndWriteString(filename, jsonString) + log.Info.Printf("DB successfully saved to %s", filename) +} diff --git a/server/storage/inmemory/inmemory_service_test.go b/server/storage/inmemory/inmemory_service_test.go new file mode 100644 index 0000000..29d03d3 --- /dev/null +++ b/server/storage/inmemory/inmemory_service_test.go @@ -0,0 +1,78 @@ +package inmemory + +import ( + "NonRelDB/util/file" + "NonRelDB/util/json" + "NonRelDB/util/sync" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetStorage__Empty__Fail(t *testing.T) { + var syncMap *sync.Map + assert.Equal(t, syncMap, GetStorage()) +} + +func TestGetStorage__Existing__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + storage = &syncMap + assert.Equal(t, &syncMap, GetStorage()) +} + +func TestSetStorage__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + SetStorage(&syncMap) + assert.Equal(t, &syncMap, GetStorage()) +} + +func TestSetStorage__Empty__Fail(t *testing.T) { + emptyMap := sync.Map{} + SetStorage(&emptyMap) + assert.Equal(t, &emptyMap, GetStorage()) +} + +func TestInitDBInMemory__Success(t *testing.T) { + assert.NotPanics(t, func() { + InitDBInMemory() + }) +} + +func TestInitDBFromStorage__Existing__Success(t *testing.T) { + file.CreateAndWriteString("test.json", "{\"123\":\"123\"}") + InitDBFromStorage("test.json") + testMap := map[string]string{"123": "123"} + assert.Equal(t, testMap, *(GetStorage().GetMap())) + + os.Remove("test.json") +} + +func TestInitDBFromStorage__NotExisting__Success(t *testing.T) { + InitDBFromStorage("test.json") + testMap := map[string]string{} + assert.Equal(t, testMap, *(GetStorage().GetMap())) + + os.Remove("test.json") +} + +func TestRestoreDBFromDump__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + RestoreDBFromDump(json.PackMapToJSON(testMap)) + assert.Equal(t, testMap, *(GetStorage().GetMap())) +} + +func TestSaveDBToStorage__NonEmpty__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := sync.Map{} + syncMap.SetMap(&testMap) + SetStorage(&syncMap) + SaveDBToStorage("test.json") + assert.Equal(t, "{\"1\":\"one\",\"2\":\"two\",\"3\":\"three\"}", file.OpenAndReadString("test.json")) + + os.Remove("test.json") +} diff --git a/server/topic/topic_service.go b/server/topic/topic_service.go new file mode 100644 index 0000000..0375b82 --- /dev/null +++ b/server/topic/topic_service.go @@ -0,0 +1,48 @@ +package topic + +import ( + "NonRelDB/log" + "NonRelDB/util/collection" + "fmt" + "net" +) + +// Storage for topic. +var topics map[string][]net.Conn + +func init() { + topics = make(map[string][]net.Conn) +} + +// Subscribe adding client to specified topic. +func Subscribe(name string, c net.Conn) { + if topics[name] == nil { + topics[name] = make([]net.Conn, 10) + } + topics[name] = append(topics[name], c) + log.Info.Printf("%s just subscribed %s", c.RemoteAddr().String(), name) +} + +// Unsubscribe removing client from specified topic. +func Unsubscribe(name string, c net.Conn) { + if topics[name] != nil || len(topics) != 0 { + index := collection.ConnIndex(topics[name], c) + if index != -1 { + topics[name][index] = nil + log.Info.Printf("%s just unsubscribed %s", c.RemoteAddr().String(), name) + } + } +} + +// Publish publishes message to specified topic. +func Publish(name string, msg string) { + if topics[name] != nil || len(topics) != 0 { + log.Info.Printf("%s just published in %s", msg, name) + for _, listener := range topics[name] { + str := fmt.Sprintf("[%s]: %s", name, msg) + if listener != nil { + fmt.Fprintf(listener, str+"\n") + } + } + } +} diff --git a/util/collection/slice_util.go b/util/collection/slice_util.go new file mode 100644 index 0000000..d295979 --- /dev/null +++ b/util/collection/slice_util.go @@ -0,0 +1,15 @@ +package collection + +import ( + "net" +) + +// ConnIndex returns index of neccessary element in net.Conn slice if found, otherwise -1. +func ConnIndex(slice []net.Conn, value net.Conn) int { + for index, sliceValue := range slice { + if sliceValue == value { + return index + } + } + return -1 +} diff --git a/util/collection/slice_util_test.go b/util/collection/slice_util_test.go new file mode 100644 index 0000000..c80e1a7 --- /dev/null +++ b/util/collection/slice_util_test.go @@ -0,0 +1,39 @@ +package collection + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testConn struct { + net.Conn + id int +} + +func TestConIndex__Contains__Success(t *testing.T) { + testSlice := make([]net.Conn, 10, 10) + + neededConn := testConn{id: 123} + + for i := 0; i < len(testSlice); i++ { + testSlice[i] = testConn{id: i} + } + + testSlice[4] = neededConn + + assert.Equal(t, 4, ConnIndex(testSlice, neededConn)) +} + +func TestConIndex__DoesntContain__Fail(t *testing.T) { + testSlice := make([]net.Conn, 10, 10) + + neededConn := testConn{id: 123} + + for i := 0; i < len(testSlice); i++ { + testSlice[i] = testConn{id: i} + } + + assert.Equal(t, -1, ConnIndex(testSlice, neededConn)) +} diff --git a/util/file/file_read.go b/util/file/file_read.go new file mode 100644 index 0000000..84dfdd9 --- /dev/null +++ b/util/file/file_read.go @@ -0,0 +1,28 @@ +package file + +import ( + "NonRelDB/log" + "io/ioutil" +) + +// OpenAndReadString receives file name, reads this file and returns its string content. +func OpenAndReadString(name string) string { + bytes, err := ioutil.ReadFile(name) + + if err != nil { + log.Error.Panicln(err.Error()) + } + + return string(bytes) +} + +// OpenAndRead receives file name, reads this file and returns byte array from it. +func OpenAndRead(name string) []byte { + bytes, err := ioutil.ReadFile(name) + + if err != nil { + log.Error.Panicln(err.Error()) + } + + return bytes +} diff --git a/util/file/file_read_test.go b/util/file/file_read_test.go new file mode 100644 index 0000000..cd8b503 --- /dev/null +++ b/util/file/file_read_test.go @@ -0,0 +1,68 @@ +package file + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpenAndReadString__SameString__Success(t *testing.T) { + file, _ := os.Create("test.txt") + + file.WriteString("123") + + file.Close() + + assert.Equal(t, "123", OpenAndReadString("test.txt")) + + os.Remove("test.txt") +} + +func TestOpenAndReadString__OtherString__Fail(t *testing.T) { + file, _ := os.Create("test.txt") + + file.WriteString("123") + + file.Close() + + assert.NotEqual(t, "124", OpenAndReadString("test.txt")) + + os.Remove("test.txt") +} + +func TestOpenAndReadString__FileNotExist__Fail(t *testing.T) { + assert.Panics(t, func() { + OpenAndReadString("test.txt") + }) +} + +func TestOpenAndRead__SameByteArray__Success(t *testing.T) { + file, _ := os.Create("test.bin") + + file.Write([]byte("123")) + + file.Close() + + assert.Equal(t, []byte("123"), OpenAndRead("test.bin")) + + os.Remove("test.bin") +} + +func TestOpenAndRead__DifferentByteArray__Success(t *testing.T) { + file, _ := os.Create("test.bin") + + file.Write([]byte("123")) + + file.Close() + + assert.NotEqual(t, []byte("124"), OpenAndRead("test.bin")) + + os.Remove("test.bin") +} + +func TestOpenAndRead__FileNotExist__Fail(t *testing.T) { + assert.Panics(t, func() { + OpenAndRead("test.bin") + }) +} diff --git a/util/file/file_write.go b/util/file/file_write.go new file mode 100644 index 0000000..f96d6d0 --- /dev/null +++ b/util/file/file_write.go @@ -0,0 +1,38 @@ +package file + +import ( + "NonRelDB/log" + "os" +) + +// CreateAndWriteString creates file and writes string to it. +func CreateAndWriteString(name string, value string) { + file, err := os.Create(name) + + defer file.Close() + + if err != nil { + log.Error.Panicln(err.Error()) + } + + _, err = file.WriteString(value) + if err != nil { + log.Error.Panicln(err.Error()) + } +} + +// CreateAndWrite creates file and writes byte array to it. +func CreateAndWrite(name string, value []byte) { + file, err := os.Create(name) + + defer file.Close() + + if err != nil { + log.Error.Panicln(err.Error()) + } + + _, err = file.Write(value) + if err != nil { + log.Error.Panicln(err.Error()) + } +} diff --git a/util/file/file_write_test.go b/util/file/file_write_test.go new file mode 100644 index 0000000..fa54bfb --- /dev/null +++ b/util/file/file_write_test.go @@ -0,0 +1,32 @@ +package file + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateAndWriteString__SameString__Success(t *testing.T) { + CreateAndWriteString("test.txt", "123") + assert.Equal(t, "123", OpenAndReadString("test.txt")) + os.Remove("test.txt") +} + +func TestCreateAndWriteString__OtherString__Fail(t *testing.T) { + CreateAndWriteString("test.txt", "123") + assert.NotEqual(t, "124", OpenAndReadString("test.txt")) + os.Remove("test.txt") +} + +func TestCreateAndWrite__SameByteArray__Success(t *testing.T) { + CreateAndWrite("test.bin", []byte("123")) + assert.Equal(t, []byte("123"), OpenAndRead("test.bin")) + os.Remove("test.bin") +} + +func TestCreateAndWrite__OtherByteArray__Success(t *testing.T) { + CreateAndWrite("test.bin", []byte("123")) + assert.NotEqual(t, []byte("124"), OpenAndRead("test.bin")) + os.Remove("test.bin") +} diff --git a/util/json/json_decode.go b/util/json/json_decode.go new file mode 100644 index 0000000..05459da --- /dev/null +++ b/util/json/json_decode.go @@ -0,0 +1,17 @@ +package json + +import ( + "NonRelDB/log" + "encoding/json" +) + +// UnpackFromJSON receives json bytes and returns map pointer. +func UnpackFromJSON(bytes []byte) *map[string]string { + kvMap := make(map[string]string) + err := json.Unmarshal(bytes, &kvMap) + if err != nil { + log.Warning.Println(err.Error()) + log.Warning.Println("No bytes. Will be returned zero map") + } + return &kvMap +} diff --git a/util/json/json_decode_test.go b/util/json/json_decode_test.go new file mode 100644 index 0000000..f4c3ca2 --- /dev/null +++ b/util/json/json_decode_test.go @@ -0,0 +1,34 @@ +package json + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnpackMapFromJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + mapBytes, _ := json.Marshal(testMap) + assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + mapBytes, _ := json.Marshal(testMap) + testMap["1"] = "1" + assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__SameMapWithIndent__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + mapBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, testMap, *UnpackFromJSON(mapBytes)) +} + +func TestUnpackMapFromJSON__DiffMapWithIndent__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + mapBytes, _ := json.MarshalIndent(testMap, "", " ") + testMap["1"] = "1" + assert.NotEqual(t, testMap, *UnpackFromJSON(mapBytes)) +} diff --git a/util/json/json_encode.go b/util/json/json_encode.go new file mode 100644 index 0000000..979471a --- /dev/null +++ b/util/json/json_encode.go @@ -0,0 +1,44 @@ +package json + +import ( + "NonRelDB/log" + "encoding/json" +) + +// PackToJSON receives string key and interface value and returns json bytes. +func PackToJSON(key string, value string) []byte { + kvMap := map[string]string{key: value} + bytes, err := json.Marshal(kvMap) + if err != nil { + log.Error.Println(err.Error()) + } + return bytes +} + +// PackToJSONIndent receives string key and interface value and returns json bytes with indent. +func PackToJSONIndent(key string, value string) []byte { + kvMap := map[string]string{key: value} + bytes, err := json.MarshalIndent(kvMap, "", " ") + if err != nil { + log.Error.Println(err.Error()) + } + return bytes +} + +// PackMapToJSON receives map and returns json bytes. +func PackMapToJSON(kvMap map[string]string) []byte { + bytes, err := json.Marshal(kvMap) + if err != nil { + log.Error.Println(err.Error()) + } + return bytes +} + +// PackMapToJSONIndent receives map and returns json bytes with indent. +func PackMapToJSONIndent(kvMap map[string]string) []byte { + bytes, err := json.MarshalIndent(kvMap, "", " ") + if err != nil { + log.Error.Println(err.Error()) + } + return bytes +} diff --git a/util/json/json_encode_test.go b/util/json/json_encode_test.go new file mode 100644 index 0000000..ee591de --- /dev/null +++ b/util/json/json_encode_test.go @@ -0,0 +1,58 @@ +package json + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPackToJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} + expBytes, _ := json.Marshal(testMap) + assert.Equal(t, expBytes, PackToJSON("1", testMap["1"])) +} + +func TestPackToJSON__OtherMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} + expBytes, _ := json.Marshal(testMap) + assert.NotEqual(t, expBytes, PackToJSON("1", "two")) +} + +func TestPackToJSONIndent__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, expBytes, PackToJSONIndent("1", testMap["1"])) +} + +func TestPackToJSONIndent__OtherMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.NotEqual(t, expBytes, PackToJSONIndent("1", "two")) +} + +func TestPackMapToJSON__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + expBytes, _ := json.Marshal(testMap) + assert.Equal(t, expBytes, PackMapToJSON(testMap)) +} + +func TestPackMapToJSON__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + expBytes, _ := json.Marshal(testMap) + testMap["1"] = "1" + assert.NotEqual(t, expBytes, PackMapToJSON(testMap)) +} + +func TestPackMapToJSONIndent__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + assert.Equal(t, expBytes, PackMapToJSONIndent(testMap)) +} + +func TestPackMapToJSONIndent__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + expBytes, _ := json.MarshalIndent(testMap, "", " ") + testMap["1"] = "1" + assert.NotEqual(t, expBytes, PackMapToJSONIndent(testMap)) +} diff --git a/util/regex/regex_util.go b/util/regex/regex_util.go new file mode 100644 index 0000000..575e886 --- /dev/null +++ b/util/regex/regex_util.go @@ -0,0 +1,29 @@ +package regex + +import ( + "regexp" +) + +var ( + // DoubleQuoteReg regexp for values inside double quotes. + DoubleQuoteReg *regexp.Regexp + // QueryReg regexp which checks is this query to db. + QueryReg *regexp.Regexp + // TopicReg regexp which checks is this topic's command. + TopicReg *regexp.Regexp + // ExitReg regexp which checks is this exit command. + ExitReg *regexp.Regexp + // DumpReg regexp which checks is this dump command. + DumpReg *regexp.Regexp + // RestoreReg regexp which check is this restore command. + RestoreReg *regexp.Regexp +) + +func init() { + DoubleQuoteReg = regexp.MustCompile("\"(.*)\"") + QueryReg = regexp.MustCompile("^(get|GET|set|SET|del|DEL|keys|KEYS)") + TopicReg = regexp.MustCompile("^(subscribe|SUBSCRIBE|publish|PUBLISH|unsubscribe|UNSUBSCRIBE)") + ExitReg = regexp.MustCompile("^(exit|EXIT)$") + DumpReg = regexp.MustCompile("^(dump|DUMP)$") + RestoreReg = regexp.MustCompile("^(restore|RESTORE)$") +} diff --git a/util/regex/regex_util_test.go b/util/regex/regex_util_test.go new file mode 100644 index 0000000..bec2817 --- /dev/null +++ b/util/regex/regex_util_test.go @@ -0,0 +1,83 @@ +package regex + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDoubleQuoteReg__Matching__Success(t *testing.T) { + assert.True(t, DoubleQuoteReg.MatchString("\"123\"")) +} + +func TestDoublQuoteReg__NotMatching__Fail(t *testing.T) { + assert.False(t, DoubleQuoteReg.MatchString("123")) +} + +func TestDoubleQuoteReg__Parse123__Success(t *testing.T) { + assert.Equal(t, "\"123\"", DoubleQuoteReg.FindString("\"123\"")) +} + +func TestDoubleQuoteReg__Parse123WithoutQuotes__Fail(t *testing.T) { + assert.NotEqual(t, "\"123\"", DoubleQuoteReg.FindString("123")) +} + +func TestQueryReg__Get__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123")) +} + +func TestQueryReg__Set__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123 \"123\"")) +} + +func TestQueryReg__Del__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("get 123")) +} + +func TestQueryReg__Keys__Success(t *testing.T) { + assert.True(t, QueryReg.MatchString("keys \"\\*\"")) +} + +func TestQueryReg__123__Fail(t *testing.T) { + assert.False(t, QueryReg.MatchString("123")) +} + +func TestTopicReg__Subscribe__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("subscribe redis")) +} + +func TestTopicReg__Unsubscribe__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("unsubscribe redis")) +} + +func TestTopicReg__Publish__Success(t *testing.T) { + assert.True(t, TopicReg.MatchString("publish redis \"Hello World\"")) +} + +func TestTopicReg__123__Fail(t *testing.T) { + assert.False(t, TopicReg.MatchString("123")) +} + +func TestExitReg__Exit__Success(t *testing.T) { + assert.True(t, ExitReg.MatchString("exit")) +} + +func TestExitReg__ExitWithSpaces__Fail(t *testing.T) { + assert.False(t, ExitReg.MatchString(" exit ")) +} + +func TestDumpReg__Dump__Success(t *testing.T) { + assert.True(t, DumpReg.MatchString("dump")) +} + +func TestDumpReg__DumpWithSpaces__Fail(t *testing.T) { + assert.False(t, DumpReg.MatchString(" dump ")) +} + +func TestRestoreReg__Restore__Success(t *testing.T) { + assert.True(t, RestoreReg.MatchString("restore")) +} + +func TestRestoreReg__RestoreWithSpaces__Fail(t *testing.T) { + assert.False(t, RestoreReg.MatchString(" restore ")) +} diff --git a/util/sync/sync_map.go b/util/sync/sync_map.go new file mode 100644 index 0000000..5b2bd5f --- /dev/null +++ b/util/sync/sync_map.go @@ -0,0 +1,81 @@ +package sync + +import ( + "NonRelDB/log" + "regexp" + "strings" + "sync" +) + +// Map map synchronized with mutex. +type Map struct { + sync.Mutex + storage *map[string]string +} + +// GetMap getter for map. +func (syncMap *Map) GetMap() *map[string]string { + return syncMap.storage +} + +// SetMap setter for map. +func (syncMap *Map) SetMap(storage *map[string]string) { + syncMap.storage = storage +} + +// Get receives and key and returns value according its key. +func (syncMap *Map) Get(key string) string { + syncMap.Lock() + defer syncMap.Unlock() + v := (*syncMap.storage)[key] + if v != "" { + return v + } + return "Value with this key not found" +} + +// Set set value according to key. +func (syncMap *Map) Set(key string, value string) string { + syncMap.Lock() + defer syncMap.Unlock() + (*syncMap.storage)[key] = value + return "Value has changed" +} + +// Del del value according to key. +func (syncMap *Map) Del(key string) string { + syncMap.Lock() + defer syncMap.Unlock() + v := (*syncMap.storage)[key] + if v != "" { + delete((*syncMap.storage), key) + return v + } + return "Value with this key not found" +} + +// Keys returns keys which match to pattern. +func (syncMap *Map) Keys(pattern string) string { + syncMap.Lock() + defer syncMap.Unlock() + + var keys []string + + regex, err := regexp.Compile(pattern) + + if err != nil { + log.Warning.Println(err.Error()) + return "Pattern is incorrect" + } + + for key := range *syncMap.storage { + if regex.MatchString(key) { + keys = append(keys, key) + } + } + + if keys != nil { + return strings.Join(keys, ",") + } + return "Keys with this pattern not found" +} diff --git a/util/sync/sync_map_test.go b/util/sync/sync_map_test.go new file mode 100644 index 0000000..6c0cd4f --- /dev/null +++ b/util/sync/sync_map_test.go @@ -0,0 +1,97 @@ +package sync + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMap__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} + syncMap := Map{} + syncMap.storage = &testMap + assert.Equal(t, &testMap, syncMap.GetMap()) +} + +func TestGetMap__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} + syncMap := Map{} + syncMap.storage = &testMap + diffMap := map[string]string{"2": "two"} + assert.NotEqual(t, &diffMap, syncMap.GetMap()) +} + +func TestSetMap__SameMap__Success(t *testing.T) { + testMap := map[string]string{"1": "one"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, &testMap, syncMap.GetMap()) +} + +func TestSetMap__DiffMap__Fail(t *testing.T) { + testMap := map[string]string{"1": "one"} + syncMap := Map{} + syncMap.SetMap(&testMap) + diffMap := map[string]string{"2": "two"} + assert.NotEqual(t, &diffMap, syncMap.GetMap()) +} + +func TestGet__Existing__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "one", syncMap.Get("1")) +} + +func TestGet__NotExisting__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Value with this key not found", syncMap.Get("4")) +} + +func TestSet__Changed__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + syncMap.Set("1", "123") + assert.Equal(t, "123", syncMap.Get("1")) +} + +func TestDel__DeletedAndReturned__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "one", syncMap.Del("1")) +} + +func TestDel__DeletedAndNotFound__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + syncMap.Del("1") + assert.Equal(t, "Value with this key not found", syncMap.Del("1")) +} + +func TestKeys__FoundWildcard__Success(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + querySet := syncMap.Keys("/*") + assert.True(t, "1,2,3" == querySet || "1,3,2" == querySet || "2,1,3" == querySet || "2,3,1" == querySet || "3,2,1" == querySet || "3,1,2" == querySet) + +} + +func TestKeys__IncorrectWildcard__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Pattern is incorrect", syncMap.Keys("*")) +} + +func TestKeys__NotFound__Fail(t *testing.T) { + testMap := map[string]string{"1": "one", "2": "two", "3": "three"} + syncMap := Map{} + syncMap.SetMap(&testMap) + assert.Equal(t, "Keys with this pattern not found", syncMap.Keys("123")) +}