Skip to content

Commit

Permalink
feat(test): adds clearUnusedMocks flag to remove the unused mocks of …
Browse files Browse the repository at this point in the history
…a test-set (keploy#1713)

* feat(test): adds clearUnusedMocks flag to remove the unused mocks of test-set

Signed-off-by: re-Tick <[email protected]>

---------

Signed-off-by: re-Tick <[email protected]>
  • Loading branch information
re-Tick authored Mar 19, 2024
1 parent 1f973c9 commit 895f699
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 35 deletions.
3 changes: 3 additions & 0 deletions cli/provider/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func (c *CmdConfigurator) AddFlags(cmd *cobra.Command, cfg *config.Config) error
cmd.Flags().StringP("language", "l", cfg.Test.Language, "application programming language")
cmd.Flags().Bool("ignoreOrdering", cfg.Test.IgnoreOrdering, "Ignore ordering of array in response")
cmd.Flags().Bool("coverage", cfg.Test.Coverage, "Enable coverage reporting for the testcases. for golang please set language flag to golang, ref https://keploy.io/docs/server/sdk-installation/go/")
cmd.Flags().Bool("removeUnusedMocks", false, "Clear the unused mocks for the passed test-sets")
} else {
cmd.Flags().Uint64("recordTimer", 0, "User provided time to record its application")
}
Expand Down Expand Up @@ -260,6 +261,8 @@ func (c CmdConfigurator) ValidateFlags(ctx context.Context, cmd *cobra.Command,
return errors.New(errMsg)
}
}
c.logger.Debug("config has been initialised", zap.Any("for cmd", cmd.Name()), zap.Any("config", cfg))

switch cmd.Name() {
case "record", "test":
bypassPorts, err := cmd.Flags().GetUintSlice("passThroughPorts")
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Test struct {
IgnoreOrdering bool `json:"ignoreOrdering" yaml:"ignoreOrdering" mapstructure:"ignoreOrdering"`
MongoPassword string `json:"mongoPassword" yaml:"mongoPassword" mapstructure:"mongoPassword"`
Language string `json:"language" yaml:"language" mapstructure:"language"`
RemoveUnusedMocks bool `json:"removeUnusedMocks" yaml:"removeUnusedMocks" mapstructure:"removeUnusedMocks"`
}

type Globalnoise struct {
Expand Down
1 change: 1 addition & 0 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test:
ignoreOrdering: true
mongoPassword: "default@123"
language: ""
removeUnusedMocks: false
record:
recordTimer: 0s
filters: []
Expand Down
16 changes: 8 additions & 8 deletions pkg/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import (
)

type Core struct {
Proxy // embedding the Proxy interface to transfer the proxy methods to the core object
Hooks // embedding the Hooks interface to transfer the hooks methods to the core object
logger *zap.Logger
id utils.AutoInc
apps sync.Map
hook Hooks
proxy Proxy
proxyStarted bool
hostConfigStr string // hosts string in the nsswitch.conf of linux system. To restore the system hosts configuration after completion of test
}

func New(logger *zap.Logger, hook Hooks, proxy Proxy) *Core {
return &Core{
logger: logger,
hook: hook,
proxy: proxy,
Hooks: hook,
Proxy: proxy,
}
}

Expand Down Expand Up @@ -131,7 +131,7 @@ func (c *Core) Hook(ctx context.Context, id uint64, opts models.HookOptions) err
})

//load hooks
err = c.hook.Load(hookCtx, id, HookCfg{
err = c.Hooks.Load(hookCtx, id, HookCfg{
AppID: id,
Pid: 0,
IsDocker: isDocker,
Expand All @@ -156,8 +156,8 @@ func (c *Core) Hook(ctx context.Context, id uint64, opts models.HookOptions) err
// TODO: Hooks can be loaded multiple times but proxy should be started only once
// if there is another containerized app, then we need to pass new (ip:port) of proxy to the eBPF
// as the network namespace is different for each container and so is the keploy/proxy IP to communicate with the app.
//start proxy
err = c.proxy.StartProxy(proxyCtx, ProxyOptions{
// start proxy
err = c.Proxy.StartProxy(proxyCtx, ProxyOptions{
DNSIPv4Addr: a.KeployIPv4Addr(),
//DnsIPv6Addr: ""
})
Expand Down Expand Up @@ -207,7 +207,7 @@ func (c *Core) Run(ctx context.Context, id uint64, opts models.RunOptions) model
return nil
}
inode := <-inodeChan
err := c.hook.SendInode(ctx, id, inode)
err := c.Hooks.SendInode(ctx, id, inode)
if err != nil {
utils.LogError(c.logger, err, "")
inodeErrCh <- errors.New("failed to send inode to the kernel")
Expand Down
2 changes: 2 additions & 0 deletions pkg/core/proxy/integrations/integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ type MockMemDb interface {
UpdateUnFilteredMock(old *models.Mock, new *models.Mock) bool
DeleteFilteredMock(mock *models.Mock) bool
DeleteUnFilteredMock(mock *models.Mock) bool
// Flag the mock as used which matches the external request from application in test mode
FlagMockAsUsed(mock *models.Mock) error
}
7 changes: 7 additions & 0 deletions pkg/core/proxy/integrations/mongo/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ func decodeMongo(ctx context.Context, logger *zap.Logger, reqBuf []byte, clientC
logger.Debug("the mongo request do not matches with any config mocks", zap.Any("request", mongoRequests))
continue
}
// set the config as used in the mockManager
err = mockDb.FlagMockAsUsed(configMocks[bestMatchIndex])
if err != nil {
utils.LogError(logger, err, "failed to flag mock as used in mongo parser", zap.Any("for mock", configMocks[bestMatchIndex].Name))
errCh <- err
return
}
for _, mongoResponse := range configMocks[bestMatchIndex].Spec.MongoResponses {
switch mongoResponse.Header.Opcode {
case wiremessage.OpReply:
Expand Down
3 changes: 2 additions & 1 deletion pkg/core/proxy/integrations/mongo/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"io"
"net"
"time"

"golang.org/x/sync/errgroup"

"go.keploy.io/server/v2/pkg/core/proxy/util"
"go.keploy.io/server/v2/pkg/models"
"go.keploy.io/server/v2/utils"
Expand Down
8 changes: 7 additions & 1 deletion pkg/core/proxy/integrations/mysql/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ func decodeMySQL(ctx context.Context, logger *zap.Logger, clientConn net.Conn, d
configMocks[matchedIndex].Spec.MySQLResponses = append(configMocks[matchedIndex].Spec.MySQLResponses[:matchedReqIndex], configMocks[matchedIndex].Spec.MySQLResponses[matchedReqIndex+1:]...)
if len(configMocks[matchedIndex].Spec.MySQLResponses) == 0 {
configMocks = append(configMocks[:matchedIndex], configMocks[matchedIndex+1:]...)
err = mockDb.FlagMockAsUsed(configMocks[matchedIndex])
if err != nil {
utils.LogError(logger, err, "Failed to flag mock as used")
errCh <- err
return
}
}
//h.SetConfigMocks(configMocks)
firstLoop = false
Expand Down Expand Up @@ -162,7 +168,7 @@ func decodeMySQL(ctx context.Context, logger *zap.Logger, clientConn net.Conn, d
}
//TODO: both in case of no match or some other error, we are receiving the error.
// Due to this, there will be no passthrough in case of no match.
matchedResponse, matchedIndex, _, err := matchRequestWithMock(ctx, mysqlRequest, configMocks, tcsMocks)
matchedResponse, matchedIndex, _, err := matchRequestWithMock(ctx, mysqlRequest, configMocks, tcsMocks, mockDb)
if err != nil {
utils.LogError(logger, err, "Failed to match request with mock")
errCh <- err
Expand Down
19 changes: 18 additions & 1 deletion pkg/core/proxy/integrations/mysql/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"context"
"fmt"

"go.keploy.io/server/v2/pkg/core/proxy/integrations"
"go.keploy.io/server/v2/pkg/models"
)

func matchRequestWithMock(ctx context.Context, mysqlRequest models.MySQLRequest, configMocks, tcsMocks []*models.Mock) (*models.MySQLResponse, int, string, error) {
func matchRequestWithMock(ctx context.Context, mysqlRequest models.MySQLRequest, configMocks, tcsMocks []*models.Mock, mockDb integrations.MockMemDb) (*models.MySQLResponse, int, string, error) {
//TODO: any reason to write the similar code twice?
allMocks := append([]*models.Mock(nil), configMocks...)
allMocks = append(allMocks, tcsMocks...)
Expand Down Expand Up @@ -53,6 +54,14 @@ func matchRequestWithMock(ctx context.Context, mysqlRequest models.MySQLRequest,
}
configMocks[matchedIndex].Spec.MySQLRequests = append(configMocks[matchedIndex].Spec.MySQLRequests[:matchedReqIndex], configMocks[matchedIndex].Spec.MySQLRequests[matchedReqIndex+1:]...)
configMocks[matchedIndex].Spec.MySQLResponses = append(configMocks[matchedIndex].Spec.MySQLResponses[:matchedReqIndex], configMocks[matchedIndex].Spec.MySQLResponses[matchedReqIndex+1:]...)
if len(configMocks[matchedIndex].Spec.MySQLResponses) == 0 {
configMocks = append(configMocks[:matchedIndex], configMocks[matchedIndex+1:]...)
err := mockDb.FlagMockAsUsed(configMocks[matchedIndex])
if err != nil {
return nil, -1, "", fmt.Errorf("failed to flag mock as used: %v", err.Error())
}
// deleteConfigMock
}
//h.SetConfigMocks(configMocks)
} else {
realIndex := matchedIndex - len(configMocks)
Expand All @@ -61,6 +70,14 @@ func matchRequestWithMock(ctx context.Context, mysqlRequest models.MySQLRequest,
}
tcsMocks[realIndex].Spec.MySQLRequests = append(tcsMocks[realIndex].Spec.MySQLRequests[:matchedReqIndex], tcsMocks[realIndex].Spec.MySQLRequests[matchedReqIndex+1:]...)
tcsMocks[realIndex].Spec.MySQLResponses = append(tcsMocks[realIndex].Spec.MySQLResponses[:matchedReqIndex], tcsMocks[realIndex].Spec.MySQLResponses[matchedReqIndex+1:]...)
if len(tcsMocks[realIndex].Spec.MySQLResponses) == 0 {
tcsMocks = append(tcsMocks[:realIndex], tcsMocks[realIndex+1:]...)
err := mockDb.FlagMockAsUsed(tcsMocks[realIndex])
if err != nil {
return nil, -1, "", fmt.Errorf("failed to flag mock as used: %v", err.Error())
}
// deleteTcsMock
}
//h.SetTcsMocks(tcsMocks)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/core/proxy/integrations/postgres/v1/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@ func matchingReadablePG(ctx context.Context, logger *zap.Logger, requestBuffers
if matched {
logger.Debug("Matched mock", zap.String("mock", matchedMock.Name))
if matchedMock.TestModeInfo.IsFiltered {
originalMatchedMock := matchedMock
originalMatchedMock := *matchedMock
matchedMock.TestModeInfo.IsFiltered = false
matchedMock.TestModeInfo.SortOrder = math.MaxInt
updated := mockDb.UpdateUnFilteredMock(originalMatchedMock, matchedMock)
updated := mockDb.UpdateUnFilteredMock(&originalMatchedMock, matchedMock)
if !updated {
continue
}
Expand Down
74 changes: 74 additions & 0 deletions pkg/core/proxy/mockmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@ package proxy

import (
"fmt"
"sort"
"strconv"
"strings"

"go.keploy.io/server/v2/pkg/models"
)

const (
filteredMock = "filtered"
unFilteredMock = "unfiltered"
totalMock = "total"
)

type MockManager struct {
filtered *TreeDb
unfiltered *TreeDb
// usedMocks contains the name of the mocks as key which were used by the parsers during the test execution.
//
// value is an array that will contain the type of mock
usedMocks map[string][]string
}

func NewMockManager(filtered, unfiltered *TreeDb) *MockManager {
return &MockManager{
filtered: filtered,
unfiltered: unfiltered,
usedMocks: make(map[string][]string),
}
}

Expand Down Expand Up @@ -64,15 +78,75 @@ func (m *MockManager) GetUnFilteredMocks() ([]*models.Mock, error) {

func (m *MockManager) UpdateUnFilteredMock(old *models.Mock, new *models.Mock) bool {
updated := m.unfiltered.update(old.TestModeInfo, new.TestModeInfo, new)
if updated {
// mark the unfiltered mock as used for the current simulated test-case
m.usedMocks[old.Name] = []string{unFilteredMock, totalMock}
}
return updated
}

func (m *MockManager) FlagMockAsUsed(mock *models.Mock) error {
if mock == nil {
return fmt.Errorf("mock is empty")
}

if mockType, ok := mock.Spec.Metadata["type"]; ok && mockType == "config" {
// mark the unfiltered mock as used for the current simulated test-case
m.usedMocks[mock.Name] = []string{unFilteredMock, totalMock}
} else {
// mark the filtered mock as used for the current simulated test-case
m.usedMocks[mock.Name] = []string{filteredMock, totalMock}
}
return nil
}

func (m *MockManager) DeleteFilteredMock(mock *models.Mock) bool {
isDeleted := m.filtered.delete(mock.TestModeInfo)
if isDeleted {
// mark the unfiltered mock as used for the current simulated test-case
m.usedMocks[mock.Name] = []string{filteredMock, totalMock}
}
return isDeleted
}

func (m *MockManager) DeleteUnFilteredMock(mock *models.Mock) bool {
isDeleted := m.unfiltered.delete(mock.TestModeInfo)
return isDeleted
}

func (m *MockManager) GetConsumedFilteredMocks() []string {
var allNames []string
// Extract all names from the map
for mockName, typeList := range m.usedMocks {
for _, mockType := range typeList {
// add mock name which are consumed by the parsers during the test-case simulation.
// Since, test-case are simulated synchronously, so the order of the mock consumption is preserved.
if mockType == filteredMock || mockType == unFilteredMock {
allNames = append(allNames, mockName)
}
}
}

// Custom sorting function to sort names by sequence number
sort.Slice(allNames, func(i, j int) bool {
seqNo1, _ := strconv.Atoi(strings.Split(allNames[i], "-")[1])
seqNo2, _ := strconv.Atoi(strings.Split(allNames[j], "-")[1])
return seqNo1 < seqNo2
})

// add the consumed filtered mocks into the total consumed mocks
for mockName, typeList := range m.usedMocks {
for indx, mockType := range typeList {
// reset the consumed unfiltered slice for the test-case simulation.
if mockType == unFilteredMock || mockType == filteredMock {
m.usedMocks[mockName] = append(typeList[:indx], typeList[indx+1:]...)
}
}
}

return allNames
}

func (m *MockManager) GetConsumedMocks() map[string][]string {
return m.usedMocks
}
19 changes: 18 additions & 1 deletion pkg/core/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ func (p *Proxy) handleConnection(ctx context.Context, srcConn net.Conn) error {
addr := fmt.Sprintf("%v:%v", dstURL, destInfo.Port)
if rule.Mode != models.MODE_TEST {
dialer := &net.Dialer{
Timeout: 1 * time.Second,
Timeout: 4 * time.Second,
}
dstConn, err = tls.DialWithDialer(dialer, "tcp", addr, cfg)
if err != nil {
Expand Down Expand Up @@ -556,3 +556,20 @@ func (p *Proxy) SetMocks(_ context.Context, id uint64, filtered []*models.Mock,

return nil
}

// GetConsumedFilteredMocks returns the consumed filtered mocks for a given app id
func (p *Proxy) GetConsumedFilteredMocks(_ context.Context, id uint64) ([]string, error) {
m, ok := p.MockManagers.Load(id)
if !ok {
return nil, fmt.Errorf("mock manager not found to get consumed filtered mocks")
}
return m.(*MockManager).GetConsumedFilteredMocks(), nil
}

func (p *Proxy) GetConsumedMocks(_ context.Context, id uint64) (map[string][]string, error) {
m, ok := p.MockManagers.Load(id)
if !ok {
return nil, fmt.Errorf("mock manager not found to get consumed mocks")
}
return m.(*MockManager).GetConsumedMocks(), nil
}
6 changes: 3 additions & 3 deletions pkg/core/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ import (
)

func (c *Core) GetIncoming(ctx context.Context, id uint64, _ models.IncomingOptions) (<-chan *models.TestCase, error) {
return c.hook.Record(ctx, id)
return c.Hooks.Record(ctx, id)
}

func (c *Core) GetOutgoing(ctx context.Context, id uint64, opts models.OutgoingOptions) (<-chan *models.Mock, error) {
m := make(chan *models.Mock, 500)

ports := GetPortToSendToKernel(ctx, opts.Rules)
if len(ports) > 0 {
err := c.hook.PassThroughPortsInKernel(ctx, id, ports)
err := c.Hooks.PassThroughPortsInKernel(ctx, id, ports)
if err != nil {
return nil, err
}
}

err := c.proxy.Record(ctx, id, m, opts)
err := c.Proxy.Record(ctx, id, m, opts)
if err != nil {
return nil, err
}
Expand Down
14 changes: 2 additions & 12 deletions pkg/core/replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,21 @@ import (
"context"

"go.keploy.io/server/v2/pkg/models"
"go.keploy.io/server/v2/utils"
)

func (c *Core) MockOutgoing(ctx context.Context, id uint64, opts models.OutgoingOptions) error {
ports := GetPortToSendToKernel(ctx, opts.Rules)
if len(ports) > 0 {
err := c.hook.PassThroughPortsInKernel(ctx, id, ports)
err := c.Hooks.PassThroughPortsInKernel(ctx, id, ports)
if err != nil {
return err
}
}

err := c.proxy.Mock(ctx, id, opts)
err := c.Proxy.Mock(ctx, id, opts)
if err != nil {
return err
}

return nil
}

func (c *Core) SetMocks(ctx context.Context, id uint64, filtered []*models.Mock, unFiltered []*models.Mock) error {
err := c.proxy.SetMocks(ctx, id, filtered, unFiltered)
if err != nil {
utils.LogError(c.logger, nil, "failed to set mocks")
return err
}
return nil
}
Loading

0 comments on commit 895f699

Please sign in to comment.