Skip to content

Commit

Permalink
Provide unit tests for collecting runtime metrics for containerized a…
Browse files Browse the repository at this point in the history
…pplications (#48)

[#34] Provide unit tests for collecting runtime metrics for containerized applications

Added unit tests for the following services updated by the new metrics support implementation:
- containerd client
- container manager
- container network manager
- things

Signed-off-by: Trifonova Antonia <[email protected]>
Signed-off-by: Konstantina Gramatova <[email protected]>
  • Loading branch information
Antonia Trifonova authored Sep 16, 2022
1 parent 3370bc4 commit 61383e5
Show file tree
Hide file tree
Showing 6 changed files with 397 additions and 7 deletions.
148 changes: 148 additions & 0 deletions containerm/ctr/ctrd_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import (
"github.com/containerd/containerd/snapshots"
"github.com/containerd/imgcrypt"
"github.com/containerd/imgcrypt/images/encryption"
"github.com/containerd/typeurl"
"github.com/containers/ocicrypt/config"

statsV1 "github.com/containerd/cgroups/stats/v1"
containerdtypes "github.com/containerd/containerd/api/types"

"github.com/eclipse-kanto/container-management/containerm/containers/types"
"github.com/eclipse-kanto/container-management/containerm/log"
"github.com/eclipse-kanto/container-management/containerm/pkg/testutil"
Expand Down Expand Up @@ -1118,3 +1123,146 @@ func TestCtrdClientUpdateContainer(t *testing.T) {
})
}
}

func TestGetContainerStats(t *testing.T) {

tests := map[string]struct {
arg *types.Container
mockExec func(context context.Context, mockTask *containerdMocks.MockTask) (*types.CPUStats, *types.MemoryStats, *types.IOStats, uint64, time.Time, error)
}{
"test_container_not_exists": {
arg: &types.Container{
ID: "non-existing-test-container-id",
},
mockExec: func(context context.Context, mockTask *containerdMocks.MockTask) (*types.CPUStats, *types.MemoryStats, *types.IOStats, uint64, time.Time, error) {
return nil, nil, nil, 0, time.Time{}, log.NewErrorf("missing container with ID = non-existing-test-container-id")
},
},
"test_metrics_error": {
arg: &types.Container{
ID: testContainerID,
},
mockExec: func(context context.Context, mockTask *containerdMocks.MockTask) (*types.CPUStats, *types.MemoryStats, *types.IOStats, uint64, time.Time, error) {
err := log.NewErrorf("metrics error")
mockTask.EXPECT().Metrics(context).Return(nil, err)
return nil, nil, nil, 0, time.Time{}, err
},
},
"test_metrics_invalid_type": {
arg: &types.Container{
ID: testContainerID,
},
mockExec: func(context context.Context, mockTask *containerdMocks.MockTask) (*types.CPUStats, *types.MemoryStats, *types.IOStats, uint64, time.Time, error) {
invalidMetricsObj := &statsV1.MemoryStat{
Usage: &statsV1.MemoryEntry{},
}
b, mErr := typeurl.MarshalAny(invalidMetricsObj)
testutil.AssertNil(t, mErr)
invalidMetrics := &containerdtypes.Metric{
Data: b,
}
err := log.NewErrorf("unexpected metrics type = %T for container with ID = %s", invalidMetricsObj, testContainerID)
mockTask.EXPECT().Metrics(context).Return(invalidMetrics, nil)
return nil, nil, nil, 0, time.Time{}, err
},
},
"test_metrics": {
arg: &types.Container{
ID: testContainerID,
},
mockExec: func(context context.Context, mockTask *containerdMocks.MockTask) (*types.CPUStats, *types.MemoryStats, *types.IOStats, uint64, time.Time, error) {
ctrdMetrics := &statsV1.Metrics{
Blkio: &statsV1.BlkIOStat{
IoServiceBytesRecursive: []*statsV1.BlkIOEntry{
{
Op: "read",
Value: 1,
},
{
Op: "read",
Value: 1,
},
{
Op: "write",
Value: 1,
},
{
Op: "write",
Value: 11,
},
},
},
Pids: &statsV1.PidsStat{
Current: 11,
},
CPU: &statsV1.CPUStat{
Usage: &statsV1.CPUUsage{
Total: 1,
},
},
Memory: &statsV1.MemoryStat{
Usage: &statsV1.MemoryEntry{
Usage: 11,
},
TotalInactiveFile: 1,
},
}

eBytes, marshalErr := typeurl.MarshalAny(ctrdMetrics)
testutil.AssertNil(t, marshalErr)

ctrdMetricsRaw := &containerdtypes.Metric{
Data: eBytes,
Timestamp: time.Now(),
}
mockTask.EXPECT().Metrics(context).Return(ctrdMetricsRaw, nil)
return &types.CPUStats{Used: ctrdMetrics.CPU.Usage.Total}, &types.MemoryStats{Used: ctrdMetrics.Memory.Usage.Usage - ctrdMetrics.Memory.TotalInactiveFile}, &types.IOStats{Read: 2, Write: 12}, ctrdMetrics.Pids.Current, ctrdMetricsRaw.Timestamp, nil
},
},
}

for testName, testCase := range tests {
t.Run(testName, func(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockTask := containerdMocks.NewMockTask(mockCtrl)

testClient := &containerdClient{
ctrdCache: &containerInfoCache{
cache: map[string]*containerInfo{
testContainerID: {
c: &types.Container{
ID: testContainerID,
},
task: mockTask,
},
},
},
}

ctx := context.Background()

cpuStats, memStats, ioStats, pidStats, timestamp, expectedError := testCase.mockExec(ctx, mockTask)
cpu, mem, io, pids, tstamp, err := testClient.GetContainerStats(ctx, testCase.arg)
testutil.AssertError(t, expectedError, err)
if expectedError != nil {
testutil.AssertNil(t, cpu)
testutil.AssertNil(t, mem)
testutil.AssertNil(t, io)
testutil.AssertEqual(t, uint64(0), pids)
testutil.AssertEqual(t, time.Time{}, tstamp)
} else {
testutil.AssertNotNil(t, cpu)
testutil.AssertEqual(t, cpuStats.Used, cpu.Used)
testutil.AssertNotNil(t, mem)
testutil.AssertEqual(t, memStats.Used, mem.Used)
testutil.AssertNotNil(t, io)
testutil.AssertEqual(t, ioStats.Read, io.Read)
testutil.AssertEqual(t, ioStats.Write, io.Write)
testutil.AssertEqual(t, pidStats, pids)
testutil.AssertEqual(t, timestamp, tstamp)
}
})
}
}
163 changes: 161 additions & 2 deletions containerm/mgr/mgr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ import (
eventsMock "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/events"
mgrMock "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/mgr"
"github.com/eclipse-kanto/container-management/containerm/streams"
errorUtil "github.com/eclipse-kanto/container-management/containerm/util/error"
"github.com/sirupsen/logrus/hooks/test"

"context"
"path/filepath"
"sync"
"time"

"context"
"testing"

networkMock "github.com/eclipse-kanto/container-management/containerm/pkg/testutil/mocks/network"
Expand Down Expand Up @@ -1057,6 +1059,163 @@ func TestAttach(t *testing.T) {
testutil.AssertNil(t, err)
}

func TestMetrics(t *testing.T) {
const testCtrID = "test-ctr-id"
metricsReportFull := &types.Metrics{
CPU: &types.CPUStats{
Used: 15000,
Total: 150000,
},
Memory: &types.MemoryStats{
Used: 1024 * 1024 * 1024,
Total: 8 * 1024 * 1024 * 1024,
},
IO: &types.IOStats{
Read: 1024,
Write: 2028,
},
Network: &types.IOStats{
Read: 2048,
Write: 4096,
},
PIDs: 5,
Timestamp: time.Now(),
}

tests := map[string]struct {
ctr *types.Container
ctrStatsFailOnly bool
addCtrToCache bool
mockExec func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error)
}{
"test_missing_in_cache": {
ctr: &types.Container{
ID: testCtrID,
},
addCtrToCache: false,
mockExec: func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error) {
return nil, log.NewErrorf(noSuchContainerErrorMsg, testCtrID)
},
},
"test_exited_container": {
ctr: &types.Container{
ID: testCtrID,
State: &types.State{
Exited: true,
Paused: false,
Running: false,
},
},
addCtrToCache: true,
mockExec: func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error) {
return nil, nil
},
},
"test_ctr_stats_error": {
ctrStatsFailOnly: true,
addCtrToCache: true,
ctr: &types.Container{
ID: testCtrID,
State: &types.State{
Running: true,
},
},
mockExec: func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error) {
err := log.NewError("test error")
client.EXPECT().GetContainerStats(ctx, ctr).Return(nil, nil, nil, uint64(0), time.Time{}, err)
manager.EXPECT().Stats(ctx, ctr).Return(metricsReportFull.Network, nil)
return &types.Metrics{
Network: metricsReportFull.Network,
Timestamp: time.Now(),
}, nil
},
},
"test_net_stats_error": {
ctr: &types.Container{
ID: testCtrID,
State: &types.State{
Running: true,
},
},
addCtrToCache: true,
mockExec: func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error) {
err := log.NewError("test error")
client.EXPECT().GetContainerStats(ctx, ctr).Return(metricsReportFull.CPU, metricsReportFull.Memory, metricsReportFull.IO, uint64(metricsReportFull.PIDs), metricsReportFull.Timestamp, nil)
manager.EXPECT().Stats(ctx, ctr).Return(nil, err)
return &types.Metrics{
CPU: metricsReportFull.CPU,
Memory: metricsReportFull.Memory,
IO: metricsReportFull.IO,
Network: nil,
Timestamp: metricsReportFull.Timestamp,
PIDs: metricsReportFull.PIDs,
}, nil
},
},
"test_ctr_net_stats_error": {
ctr: &types.Container{
ID: testCtrID,
State: &types.State{
Running: true,
},
},
addCtrToCache: true,
mockExec: func(ctx context.Context, ctr *types.Container, client *ctrMock.MockContainerAPIClient, manager *networkMock.MockContainerNetworkManager) (*types.Metrics, error) {
err := log.NewError("test error")
client.EXPECT().GetContainerStats(ctx, ctr).Return(nil, nil, nil, uint64(0), time.Time{}, err)
manager.EXPECT().Stats(ctx, ctr).Return(nil, err)
errs := &errorUtil.CompoundError{}
errs.Append(err, err)
return nil, errs
},
},
}
// run tests
for testName, testCase := range tests {
t.Run(testName, func(t *testing.T) {
t.Log(testName)

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

// init mocks
mockCtrClient := ctrMock.NewMockContainerAPIClient(mockCtrl)
mockNetworkManager := networkMock.NewMockContainerNetworkManager(mockCtrl)

testMgr := &containerMgr{
ctrClient: mockCtrClient,
netMgr: mockNetworkManager,
containers: make(map[string]*types.Container),
}
if testCase.addCtrToCache {
testMgr.containers[testCase.ctr.ID] = testCase.ctr
}
ctx := context.Background()
expectedMetrics, expectedErr := testCase.mockExec(ctx, testCase.ctr, mockCtrClient, mockNetworkManager)
metrics, err := testMgr.Metrics(ctx, testCase.ctr.ID)

testutil.AssertError(t, expectedErr, err)
if expectedErr != nil {
testutil.AssertNil(t, metrics)
} else if expectedMetrics == nil {
testutil.AssertNil(t, metrics)
} else {
testutil.AssertEqual(t, expectedMetrics.CPU, metrics.CPU)
testutil.AssertEqual(t, expectedMetrics.Memory, metrics.Memory)
testutil.AssertEqual(t, expectedMetrics.IO, metrics.IO)
testutil.AssertEqual(t, expectedMetrics.PIDs, metrics.PIDs)
testutil.AssertEqual(t, expectedMetrics.Network, metrics.Network)

if testCase.ctrStatsFailOnly {
testutil.AssertNotNil(t, metrics.Timestamp)
} else {
testutil.AssertEqual(t, expectedMetrics.Timestamp, metrics.Timestamp)
}
}
})
}
}

func getDeadContainer() (string, *types.Container) {
containerID := "dead-container"
pathToContatiner := filepath.Join("../pkg/testutil/metapath/valid/containers/", containerID, "/config.json")
Expand Down
Loading

0 comments on commit 61383e5

Please sign in to comment.