Skip to content

Commit

Permalink
feat: Api methods to list test and test suites with executions (#1167)
Browse files Browse the repository at this point in the history
* feat: Api methods to list test and test suites with executions

* fix: tune descriptions

* fix: use single mongo query for execution search

* fix: don't query mongodb for empty conditions
  • Loading branch information
vsukhin authored Mar 30, 2022
1 parent 3cce680 commit d2e13c6
Show file tree
Hide file tree
Showing 14 changed files with 476 additions and 46 deletions.
62 changes: 60 additions & 2 deletions api/v1/testkube.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ paths:
items:
$ref: "#/components/schemas/Problem"

/test-suite-with-executions:
get:
tags:
- test-suites
- api
summary: "Get all test suite with executions"
description: "Returns array of test suite with executions"
operationId: listTestSuiteWithExecutions
parameters:
- $ref: "#/components/parameters/Selector"
- $ref: "#/components/parameters/TextSearch"
responses:
200:
description: successful operation
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TestSuiteWithExecution"
500:
description: "problem with getting test suite executions from storage"
content:
application/problem+json:
schema:
type: array
items:
$ref: "#/components/schemas/Problem"

/test-suite-with-executions/{id}:
get:
parameters:
Expand Down Expand Up @@ -173,7 +202,7 @@ paths:
schema:
$ref: "#/components/schemas/TestSuiteExecutionsResult"
500:
description: "problem with getting test suite executions from storage"
description: "problem with getting test suite with executions from storage"
content:
application/problem+json:
schema:
Expand Down Expand Up @@ -204,7 +233,7 @@ paths:
schema:
$ref: "#/components/schemas/TestSuiteExecution"
500:
description: "problem with getting test suite executions from storage"
description: "problem with getting test suite with execution from storage"
content:
application/problem+json:
schema:
Expand Down Expand Up @@ -533,6 +562,35 @@ paths:
items:
$ref: "#/components/schemas/Problem"

/test-with-executions:
get:
tags:
- tests
- api
summary: "List test with executions"
description: "List available test with executions"
operationId: listTestWithExecutions
parameters:
- $ref: "#/components/parameters/Selector"
- $ref: "#/components/parameters/TextSearch"
responses:
200:
description: "successful operation"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TestWithExecution"
502:
description: "problem with read information from kubernetes cluster"
content:
application/problem+json:
schema:
type: array
items:
$ref: "#/components/schemas/Problem"

/test-with-executions/{id}:
get:
tags:
Expand Down
16 changes: 9 additions & 7 deletions cmd/kubectl-testkube/commands/tests/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/common/render"
"github.com/kubeshop/testkube/cmd/kubectl-testkube/commands/tests/renderer"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/ui"
"github.com/spf13/cobra"
)
Expand All @@ -28,8 +27,6 @@ func NewGetTestsCmd() *cobra.Command {
client, _ := common.GetClient(cmd)

var name string
var tests testkube.Tests
var err error

if len(args) > 0 {
name = args[0]
Expand All @@ -49,11 +46,16 @@ func NewGetTestsCmd() *cobra.Command {
}

} else {
tests, err = client.ListTests(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting all tests in namespace "+namespace, err)
render.List(cmd, tests, os.Stdout)
if noExecution {
tests, err := client.ListTests(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting all tests in namespace "+namespace, err)
render.List(cmd, tests, os.Stdout)
} else {
tests, err := client.ListTestWithExecutions(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting all test with executions in namespace "+namespace, err)
render.List(cmd, tests, os.Stdout)
}
}

},
}
cmd.Flags().StringSliceVarP(&selectors, "label", "l", nil, "label key value pair: --label key1=value1")
Expand Down
15 changes: 11 additions & 4 deletions cmd/kubectl-testkube/commands/testsuites/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ func NewGetTestSuiteCmd() *cobra.Command {
ui.ExitOnError("rendering obj", err)
}
} else {
testSuites, err := client.ListTestSuites(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting test suites", err)
err = render.List(cmd, testSuites, os.Stdout)
ui.ExitOnError("rendering list", err)
if noExecution {
testSuites, err := client.ListTestSuites(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting test suites", err)
err = render.List(cmd, testSuites, os.Stdout)
ui.ExitOnError("rendering list", err)
} else {
testSuites, err := client.ListTestSuiteWithExecutions(namespace, strings.Join(selectors, ","))
ui.ExitOnError("getting test suite with executions", err)
err = render.List(cmd, testSuites, os.Stdout)
ui.ExitOnError("rendering list", err)
}
}

},
Expand Down
2 changes: 2 additions & 0 deletions internal/app/api/v1/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func (s TestkubeAPI) Init() {
tests.Delete("/:id/executions/:executionID", s.AbortExecutionHandler())

testWithExecutions := s.Routes.Group("/test-with-executions")
testWithExecutions.Get("/", s.ListTestWithExecutionsHandler())
testWithExecutions.Get("/:id", s.GetTestWithExecutionHandler())

testsuites := s.Routes.Group("/test-suites")
Expand All @@ -203,6 +204,7 @@ func (s TestkubeAPI) Init() {
testExecutions.Get("/:executionID", s.GetTestSuiteExecutionHandler())

testSuiteWithExecutions := s.Routes.Group("/test-suite-with-executions")
testSuiteWithExecutions.Get("/", s.ListTestSuiteWithExecutionsHandler())
testSuiteWithExecutions.Get("/:id", s.GetTestSuiteWithExecutionHandler())

labels := s.Routes.Group("/labels")
Expand Down
89 changes: 68 additions & 21 deletions internal/app/api/v1/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/gofiber/fiber/v2"
testsv2 "github.com/kubeshop/testkube-operator/apis/tests/v2"
"github.com/kubeshop/testkube/pkg/api/v1/testkube"
"github.com/kubeshop/testkube/pkg/cronjob"
testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests"
Expand Down Expand Up @@ -67,39 +68,85 @@ func (s TestkubeAPI) GetTestWithExecutionHandler() fiber.Handler {
}
}

func (s TestkubeAPI) getFilteredTestList(c *fiber.Ctx) (*testsv2.TestList, error) {
namespace := c.Query("namespace", "testkube")
// TODO filters looks messy need to introduce some common Filter object for Kubernetes query for List like objects
crTests, err := s.TestsClient.List(namespace, c.Query("selector"))
if err != nil {
return nil, err
}

search := c.Query("textSearch")
if search != "" {
// filter items array
for i := len(crTests.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTests.Items[i].Name, search) {
crTests.Items = append(crTests.Items[:i], crTests.Items[i+1:]...)
}
}
}

testType := c.Query("type")
if testType != "" {
// filter items array
for i := len(crTests.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTests.Items[i].Spec.Type_, testType) {
crTests.Items = append(crTests.Items[:i], crTests.Items[i+1:]...)
}
}
}

return crTests, nil
}

// ListTestsHandler is a method for getting list of all available tests
func (s TestkubeAPI) ListTestsHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
namespace := c.Query("namespace", "testkube")
// TODO filters looks messy need to introduce some common Filter object for Kubernetes query for List like objects
crTests, err := s.TestsClient.List(namespace, c.Query("selector"))
crTests, err := s.getFilteredTestList(c)
if err != nil {
return s.Error(c, http.StatusBadGateway, err)
}

search := c.Query("textSearch")
if search != "" {
// filter items array
for i := len(crTests.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTests.Items[i].Name, search) {
crTests.Items = append(crTests.Items[:i], crTests.Items[i+1:]...)
}
}
}
tests := testsmapper.MapTestListKubeToAPI(*crTests)

testType := c.Query("type")
if testType != "" {
// filter items array
for i := len(crTests.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTests.Items[i].Spec.Type_, testType) {
crTests.Items = append(crTests.Items[:i], crTests.Items[i+1:]...)
}
}
return c.JSON(tests)
}
}

// ListTestWithExecutionsHandler is a method for getting list of all available test with latest executions
func (s TestkubeAPI) ListTestWithExecutionsHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
crTests, err := s.getFilteredTestList(c)
if err != nil {
return s.Error(c, http.StatusBadGateway, err)
}

tests := testsmapper.MapTestListKubeToAPI(*crTests)
ctx := c.Context()
testWithExecutions := make([]testkube.TestWithExecution, len(tests))
testNames := make([]string, len(tests))
for i := range tests {
testNames[i] = tests[i].Name
}

return c.JSON(tests)
executions, err := s.ExecutionResults.GetLatestByTests(ctx, testNames)
if err != nil && err != mongo.ErrNoDocuments {
return s.Error(c, http.StatusInternalServerError, err)
}

executionMap := make(map[string]testkube.Execution, len(executions))
for i := range executions {
executionMap[executions[i].TestName] = executions[i]
}

for i := range tests {
testWithExecutions[i].Test = &tests[i]
if execution, ok := executionMap[tests[i].Name]; ok {
testWithExecutions[i].LatestExecution = &execution
}
}

return c.JSON(testWithExecutions)
}
}

Expand Down
71 changes: 60 additions & 11 deletions internal/app/api/v1/testsuites.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,29 +183,78 @@ func (s TestkubeAPI) DeleteTestSuitesHandler() fiber.Handler {
}
}

func (s TestkubeAPI) getFilteredTestSuitesList(c *fiber.Ctx) (*testsuitesv1.TestSuiteList, error) {
namespace := c.Query("namespace", "testkube")
crTestSuites, err := s.TestsSuitesClient.List(namespace, c.Query("selector"))
if err != nil {
return nil, err
}

search := c.Query("textSearch")
if search != "" {
// filter items array
for i := len(crTestSuites.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTestSuites.Items[i].Name, search) {
crTestSuites.Items = append(crTestSuites.Items[:i], crTestSuites.Items[i+1:]...)
}
}
}

return crTestSuites, nil
}

// ListTestSuitesHandler for getting list of all available TestSuites
func (s TestkubeAPI) ListTestSuitesHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
namespace := c.Query("namespace", "testkube")
crTests, err := s.TestsSuitesClient.List(namespace, c.Query("selector"))
crTestSuites, err := s.getFilteredTestSuitesList(c)
if err != nil {
return s.Error(c, http.StatusInternalServerError, err)
}

testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites)

return c.JSON(testSuites)
}
}

// ListTestSuiteWithExecutionsHandler for getting list of all available TestSuite with latest executions
func (s TestkubeAPI) ListTestSuiteWithExecutionsHandler() fiber.Handler {
return func(c *fiber.Ctx) error {
crTestSuites, err := s.getFilteredTestSuitesList(c)
if err != nil {
return s.Error(c, http.StatusInternalServerError, err)
}

search := c.Query("textSearch")
if search != "" {
// filter items array
for i := len(crTests.Items) - 1; i >= 0; i-- {
if !strings.Contains(crTests.Items[i].Name, search) {
crTests.Items = append(crTests.Items[:i], crTests.Items[i+1:]...)
}
ctx := c.Context()
testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites)
testSuiteWithExecutions := make([]testkube.TestSuiteWithExecution, len(testSuites))
testNames := make([]string, len(testSuites))
for i := range testSuites {
testNames[i] = testSuites[i].Name
}

executions, err := s.TestExecutionResults.GetLatestByTests(ctx, testNames)
if err != nil && err != mongo.ErrNoDocuments {
return s.Error(c, http.StatusInternalServerError, err)
}

executionMap := make(map[string]testkube.TestSuiteExecution, len(executions))
for i := range executions {
if executions[i].TestSuite == nil {
continue
}

executionMap[executions[i].TestSuite.Name] = executions[i]
}

tests := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTests)
for i := range testSuites {
testSuiteWithExecutions[i].TestSuite = &testSuites[i]
if execution, ok := executionMap[testSuites[i].Name]; ok {
testSuiteWithExecutions[i].LatestExecution = &execution
}
}

return c.JSON(tests)
return c.JSON(testSuiteWithExecutions)
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/api/repository/result/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Repository interface {
GetByNameAndTest(ctx context.Context, name, testName string) (testkube.Execution, error)
// GetLatestByTest gets latest execution result by test
GetLatestByTest(ctx context.Context, testName string) (testkube.Execution, error)
// GetLatestByTests gets latest execution results by test names
GetLatestByTests(ctx context.Context, testNames []string) (executions []testkube.Execution, err error)
// GetExecutions gets executions using a filter, use filter with no data for all
GetExecutions(ctx context.Context, filter Filter) ([]testkube.Execution, error)
// GetExecutionTotals gets the statistics on number of executions using a filter, but without paging
Expand Down
Loading

0 comments on commit d2e13c6

Please sign in to comment.