Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 9 commits into from
Mar 19, 2024
4 changes: 4 additions & 0 deletions cli/provider/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ 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("clearUnusedMocks", false, "Clear the unused mocks for the passed test-sets")
charankamarapu marked this conversation as resolved.
Show resolved Hide resolved
cmd.Flags().Lookup("clearUnusedMocks").NoOptDefVal = "true"
charankamarapu marked this conversation as resolved.
Show resolved Hide resolved
} else {
cmd.Flags().Uint64("recordTimer", 0, "User provided time to record its application")
}
Expand Down Expand Up @@ -260,6 +262,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"`
ClearUnusedMocks bool `json:"clearUnusedMocks" yaml:"clearUnusedMocks" mapstructure:"clearUnusedMocks"`
}

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: ""
clearUnusedMocks: false
record:
recordTimer: 0s
filters: []
Expand Down
22 changes: 11 additions & 11 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 {
logger *zap.Logger
id utils.AutoInc
apps sync.Map
hook Hooks
proxy Proxy
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
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 @@
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

Check failure on line 53 in pkg/core/proxy/integrations/integrations.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofmt`-ed with `-s` (gofmt)
}
2 changes: 2 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,8 @@ 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
mockDb.FlagMockAsUsed(configMocks[bestMatchIndex])
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/mysql/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
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:]...)
mockDb.FlagMockAsUsed(configMocks[matchedIndex])

Check failure on line 77 in pkg/core/proxy/integrations/mysql/decode.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)

Check failure on line 77 in pkg/core/proxy/integrations/mysql/decode.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)
}
//h.SetConfigMocks(configMocks)
firstLoop = false
Expand Down Expand Up @@ -162,7 +163,7 @@
}
//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
13 changes: 12 additions & 1 deletion pkg/core/proxy/integrations/mysql/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"fmt"

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

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,11 @@
}
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:]...)
mockDb.FlagMockAsUsed(configMocks[matchedIndex])

Check failure on line 59 in pkg/core/proxy/integrations/mysql/match.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)

Check failure on line 59 in pkg/core/proxy/integrations/mysql/match.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)
// deleteConfigMock
}
//h.SetConfigMocks(configMocks)
} else {
realIndex := matchedIndex - len(configMocks)
Expand All @@ -61,6 +67,11 @@
}
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:]...)
mockDb.FlagMockAsUsed(tcsMocks[realIndex])

Check failure on line 72 in pkg/core/proxy/integrations/mysql/match.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)

Check failure on line 72 in pkg/core/proxy/integrations/mysql/match.go

View workflow job for this annotation

GitHub Actions / lint

Error return value of `mockDb.FlagMockAsUsed` is not checked (errcheck)
// 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
76 changes: 76 additions & 0 deletions pkg/core/proxy/mockmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@ 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
charankamarapu marked this conversation as resolved.
Show resolved Hide resolved
}

func NewMockManager(filtered, unfiltered *TreeDb) *MockManager {
usedMap := make(map[string][]string)
return &MockManager{
filtered: filtered,
unfiltered: unfiltered,
usedMocks: usedMap,
gouravkrosx marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -64,15 +79,76 @@ 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)
println("printing the update mocks for name: ", old.Name, "and isUpdated: ", updated)
charankamarapu marked this conversation as resolved.
Show resolved Hide resolved
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 (ctx 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 (ctx 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
16 changes: 3 additions & 13 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
}
}
2 changes: 2 additions & 0 deletions pkg/core/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type Proxy interface {
Record(ctx context.Context, id uint64, mocks chan<- *models.Mock, opts models.OutgoingOptions) error
Mock(ctx context.Context, id uint64, opts models.OutgoingOptions) error
SetMocks(ctx context.Context, id uint64, filtered []*models.Mock, unFiltered []*models.Mock) error
GetConsumedFilteredMocks (ctx context.Context, id uint64) ([]string, error)
GetConsumedMocks (ctx context.Context, id uint64) (map[string][]string, error)
}

type ProxyOptions struct {
Expand Down
Loading
Loading