From 5adaeac2389844e2d486c282326eda77bf96a018 Mon Sep 17 00:00:00 2001 From: re-Tick Date: Fri, 8 Dec 2023 07:12:37 +0000 Subject: [PATCH] feat: adds integration for the go-test coverage Signed-off-by: re-Tick --- README.md | 61 ++++++++- keploy/keploy.go | 336 ++--------------------------------------------- 2 files changed, 70 insertions(+), 327 deletions(-) diff --git a/README.md b/README.md index 09956be..c55f104 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This is the client SDK for the [Keploy](https://github.com/keploy/keploy) testin 1. [Installation](#installation) 2. [Usage](#usage) 3. [Mocking/Stubbing for unit tests](#mockingstubbing-for-unit-tests) +4. [Code coverage by the API tests](#code-coverage-by-the-api-tests) ## Installation @@ -16,6 +17,10 @@ go get -u github.com/keploy/go-sdk/v2 ## Usage +### Get coverage for keploy automated tests +The code coverage for the keploy API tests using the `go-test` integration. +Keploy can be integrated in your CI pipeline which can add the coverage of your keploy test. + ### Create mocks/stubs for your unit-test These mocks/stubs are realistic and frees you up from writing them manually. Keploy creates `readable/editable` mocks/stubs yaml files which can be referenced in any of your unit-tests tests. An example is mentioned in [Mocking/Stubbing for unit tests](#mockingstubbing-for-unit-tests) section @@ -32,7 +37,7 @@ import( ... err := keploy.New(keploy.Config{ Mode: keploy.MODE_RECORD, // It can be MODE_TEST or MODE_OFF. Default is MODE_TEST. Default MODE_TEST - Name: "" // TestSuite name to record the mock or test the mocks + Name: "" // TestSuite name to record the mock or test the mocks Path: "", // optional. It can be relative(./internals) or absolute(/users/xyz/...) MuteKeployLogs: false, // optional. It can be true or false. If it is true keploy logs will be not shown in the unit test terminal. Default: false delay: 10, // by default it is 5 . This delay is for running keploy @@ -142,3 +147,57 @@ func TestPutURL(t *testing.T) { } } ``` + +## Code coverage by the API tests + +The percentage of code covered by the recorded tests is logged if the test cmd is ran with the go binary and `withCoverage` flag. The conditions for the coverage is: +1. The go binary should be built with `-cover` flag. +2. The application should have a graceful shutdown to stop the API server on `SIGTERM` or `SIGINT` signals. Or if not call the **GracefulShutdown** from the main function of your go program. Ex: +```go +func main() { + + port := "8080" + + r := gin.Default() + + r.GET("/:param", getURL) + r.POST("/url", putURL) + // should be called before starting the API server from main() + keploy.GracefulShutdown() + + r.Run() +} +``` +The keploy test cmd will look like: +```sh +keploy test -c "PATH_TO_GO_COVER_BIANRY" --withCoverage +``` +The coverage files will be stored in the directory. +``` +keploy +├── coverage-reports +│ ├── covcounters.befc2fe88a620bbd45d85aa09517b5e7.305756.1701767439933176870 +│ ├── covmeta.befc2fe88a620bbd45d85aa09517b5e7 +│ └── total-coverage.txt +├── test-set-0 +│ ├── mocks.yaml +│ └── tests +│ ├── test-1.yaml +│ ├── test-2.yaml +│ ├── test-3.yaml +│ └── test-4.yaml +``` +Coverage percentage log in the cmd will be: +```sh +🐰 Keploy: 2023-12-07T08:53:14Z INFO test/test.go:261 + test-app-url-shortener coverage: 78.4% of statements +``` + +Also the go-test coverage can be merged along the recorded tests coverage by following the steps: +```sh +go test -cover ./... -args -test.gocoverdir="PATH_TO_UNIT_COVERAGE_FILES" + +go tool covdata textfmt -i="PATH_TO_UNIT_COVERAGE_FILES","./keploy/coverage-reports" -o coverage-profile + +go tool cover -func coverage-profile +``` \ No newline at end of file diff --git a/keploy/keploy.go b/keploy/keploy.go index 984406e..9f1e428 100644 --- a/keploy/keploy.go +++ b/keploy/keploy.go @@ -1,338 +1,22 @@ package keploy import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" "os" - "os/exec" "os/signal" - "strconv" - "strings" "syscall" - "time" - - "go.uber.org/zap" -) - -const ( - GraphQLEndpoint = "/query" - Host = "http://localhost:" -) - -var ( - // serverPort is the port on which the keploy GraphQL will be running. - serverPort = 6789 - // process which is running the keploy GraphQL server. - kProcess *exec.Cmd - // Create an buffered channel for stopping the user app. - shutdownChan = make(chan os.Signal, 1) -) - -// Define a custom signal to trigger shutdown event -const shutdownSignal = syscall.SIGUSR1 - -func init() { - // Notify the channel when the shutdown signal is received for user app - signal.Notify(shutdownChan, shutdownSignal) - - logger, _ = zap.NewDevelopment() - defer func() { - _ = logger.Sync() - }() -} - -type GraphQLResponse struct { - Data ResponseData -} - -type ResponseData struct { - TestSets []string - TestSetStatus TestSetStatus - RunTestSet RunTestSetResponse -} - -type TestSetStatus struct { - Status string -} - -type RunTestSetResponse struct { - Success bool - TestRunId string - Message string -} - -type TestRunStatus string - -const ( - Running TestRunStatus = "RUNNING" - Passed TestRunStatus = "PASSED" - Failed TestRunStatus = "FAILED" ) -// LaunchShutdown sends a custom signal to request the application to -// shut down gracefully. -func LaunchShutdown() { - pid := os.Getpid() - logger.Info(fmt.Sprintf("Sending custom signal %s to PID %d...", shutdownSignal, pid)) - err := syscall.Kill(pid, shutdownSignal) - if err != nil { - logger.Info("Failed to send custom signal:", zap.Error(err)) - } -} - -// AddShutdownListener listens for the custom signal and initiate shutdown by -// executing stopper function from the parameter. -func AddShutdownListener(stopper func()) { +// GracefulShutdown is used to signal the user application to exit when SIGTERM is triggered +// from keploy test cmd. This function call can be used when the go application have not employed +// a graceful shutdown mechanism. +func GracefulShutdown() { + stopper := make(chan os.Signal, 1) + // listens for interrupt and SIGTERM signal + signal.Notify(stopper, os.Interrupt, os.Kill, syscall.SIGKILL, syscall.SIGTERM) go func() { - sig := <-shutdownChan - fmt.Println("Received custom signal:", sig) - stopper() - }() -} - -// RunKeployServer starts the Keploy server with specified parameters. -func RunKeployServer(pid int64, delay int, testPath string, port int) error { - defer func() { - if r := recover(); r != nil { - logger.Info("Recovered in RunKeployServer", zap.Any("message", r)) + select { + case <-stopper: + os.Exit(0) } }() - - if port != 0 { - serverPort = port - } - - cmd := exec.Command( - "sudo", - "/usr/local/bin/keploy", - "serve", - fmt.Sprintf("--pid=%d", pid), - fmt.Sprintf("-p=%s", testPath), - fmt.Sprintf("-d=%d", delay), - fmt.Sprintf("--port=%d", port), - "--language=go", - ) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - logger.Error("failed to start the keploy serve cmd", zap.Error(err)) - return err - } - kProcess = cmd - // delay to start the proxy and graphql server - time.Sleep(10 * time.Second) - return nil -} - -// setHttpClient returns a HTTP client and request. -func setHttpClient() (*http.Client, *http.Request, error) { - client := &http.Client{ - Timeout: 10 * time.Second, - } - req, err := http.NewRequest("POST", Host+fmt.Sprintf("%d", serverPort)+GraphQLEndpoint, nil) - if err != nil { - return nil, nil, err - } - - req.Header.Set("Content-Type", "application/json; charset=UTF-8") - req.Header.Set("Accept", "application/json") - - // Set a context with a timeout for reading the response - ctx, _ := context.WithTimeout(req.Context(), 15*time.Second) - - req = req.WithContext(ctx) - - return client, req, nil -} - -// FetchTestSets fetches the recorded test sets from the keploy GraphQL server. -func FetchTestSets() ([]string, error) { - client, req, err := setHttpClient() - if err != nil { - return nil, err - } - - payload := []byte(`{ "query": "{ testSets }" }`) - req.Body = io.NopCloser(bytes.NewBuffer(payload)) - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - bodyBytes, _ := io.ReadAll(resp.Body) - var response GraphQLResponse - if err := json.Unmarshal(bodyBytes, &response); err != nil { - return nil, err - } - - return response.Data.TestSets, nil - } - - return nil, fmt.Errorf("Error fetching test sets") -} - -// FetchTestSetStatus fetches test set status based on the running testRunId. -func FetchTestSetStatus(testRunId string) (TestRunStatus, error) { - client, req, err := setHttpClient() - if err != nil { - return "", err - } - - payloadStr := fmt.Sprintf(`{ "query": "{ testSetStatus(testRunId: \"%s\") { status } }" }`, testRunId) - req.Body = io.NopCloser(bytes.NewBufferString(payloadStr)) - - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - bodyBytes, _ := io.ReadAll(resp.Body) - var response GraphQLResponse - if err := json.Unmarshal(bodyBytes, &response); err != nil { - return "", err - } - - switch response.Data.TestSetStatus.Status { - case "RUNNING": - return Running, nil - case "PASSED": - return Passed, nil - case "FAILED": - return Failed, nil - default: - return "", fmt.Errorf("Unknown status: %s", response.Data.TestSetStatus.Status) - } - } - - return "", fmt.Errorf("Error fetching test set status") -} - -// RunTestSet runs a test set. -func RunTestSet(testSetName string) (string, error) { - client, req, err := setHttpClient() - if err != nil { - return "", err - } - - payloadStr := fmt.Sprintf(`{ "query": "mutation { runTestSet(testSet: \"%s\") { success testRunId message } }" }`, testSetName) - req.Body = io.NopCloser(bytes.NewBufferString(payloadStr)) - - resp, err := client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - bodyBytes, _ := io.ReadAll(resp.Body) - var response GraphQLResponse - if err := json.Unmarshal(bodyBytes, &response); err != nil { - return "", err - } - - return response.Data.RunTestSet.TestRunId, nil - } - - return "", fmt.Errorf("Error running test set") -} - -// isSuccessfulResponse checks if an HTTP response is successful. -func isSuccessfulResponse(resp *http.Response) bool { - return resp.StatusCode >= 200 && resp.StatusCode < 300 -} - -// getResponseBody fetches the response body from an HTTP response. -func getResponseBody(conn *http.Response) (string, error) { - defer conn.Body.Close() - bodyBytes, err := io.ReadAll(conn.Body) - if err != nil { - return "", err - } - return string(bodyBytes), nil -} - -// StopKeployServer stops the Keploy GraphQL server. -func StopKeployServer() { - killProcessOnPort(serverPort) -} - -// killProcessOnPort kills the processes and its children listening on the specified port. -func killProcessOnPort(port int) { - cmdStr := fmt.Sprintf("lsof -t -i:%d", port) - processIDs, err := exec.Command("sh", "-c", cmdStr).Output() - if err != nil { - logger.Error("failed to fetch the proces ID of user application", zap.Error(err), zap.Any("on port", port)) - return - } - - pids := strings.Split(string(processIDs), "\n") - for _, pidStr := range pids { - if pidStr != "" { - pid, err := strconv.Atoi(pidStr) - if err != nil { - logger.Error("failed to convert pid from string to integer") - } - killProcessesAndTheirChildren(pid) - } - } -} - -// killProcessesAndTheirChildren recursively kills child processes and their descendants of the parentPID. -func killProcessesAndTheirChildren(parentPID int) { - - pids := []int{} - - findAndCollectChildProcesses(fmt.Sprintf("%d", parentPID), &pids) - - for _, childPID := range pids { - if os.Getpid() != childPID { - // Use the `sudo` command to execute the `kill` command with elevated privileges. - cmd := exec.Command("sudo", "kill", "-9", fmt.Sprint(childPID)) - - // Run the `sudo kill` command. - err := cmd.Run() - if err != nil { - fmt.Printf("Failed to kill child process %d: %s\n", childPID, err) - } else { - fmt.Printf("Killed child process %d\n", childPID) - } - } - - } -} - -// findAndCollectChildProcesses find and collect child processes of a parent process. -func findAndCollectChildProcesses(parentPID string, pids *[]int) { - cmd := exec.Command("pgrep", "-P", parentPID) - parentIDint, err := strconv.Atoi(parentPID) - if err != nil { - logger.Error("failed to convert parent PID to int", zap.Any("error converting parent PID to int", err.Error())) - } - - *pids = append(*pids, parentIDint) - - output, err := cmd.Output() - if err != nil { - return - } - - outputStr := string(output) - childPIDs := strings.Split(outputStr, "\n") - childPIDs = childPIDs[:len(childPIDs)-1] - - for _, childPID := range childPIDs { - if childPID != "" { - findAndCollectChildProcesses(childPID, pids) - } - } }