blob: c8eacdfeab62104c284ef35003ad510b5f693a2f [file] [log] [blame]
package cloudlogger
import (
"io/ioutil"
"testing"
"time"
"cloud.google.com/go/logging"
"cos.googlesource.com/cos/tools.git/src/pkg/nodeprofiler/profiler"
"cos.googlesource.com/cos/tools.git/src/pkg/nodeprofiler/utils"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
// fakeCPU is a struct that implements the Component interface.
type fakeCPU struct {
CPUName string
Metrics *profiler.USEMetrics
}
// CollectUtilization behavior with regards to type fakeCPU.
func (f *fakeCPU) CollectUtilization(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Utilization = float64(7) // an arbitrary number
return nil
}
// CollectSaturation behavior with regards to type fakeCPU.
func (f *fakeCPU) CollectSaturation(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Saturation = true
return nil
}
// CollectErrors behavior with regards to type fakeCPU.
func (f *fakeCPU) CollectErrors(outputs map[string]utils.ParsedOutput) error {
return nil
}
// USEMetrics behavior with regards to type fakeCPU.
func (f *fakeCPU) USEMetrics() *profiler.USEMetrics {
return f.Metrics
}
// Name behavior with regards to type CPU.
func (f *fakeCPU) Name() string {
return f.CPUName
}
// AdditionalInformation returns unique information for fakeCPU
func (f *fakeCPU) AdditionalInformation() string {
return ""
}
// fakeMemCap is a struct that implement the Component interface.
type fakeMemCap struct {
MemCapName string
Metrics *profiler.USEMetrics
}
// CollectUtilization behavior with regards to type fakeMemCap.
func (f *fakeMemCap) CollectUtilization(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Utilization = float64(7) // any arbitrary number
return nil
}
// CollectSaturation behavior with regards to type fakeMemCap.
func (f *fakeMemCap) CollectSaturation(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Saturation = true
return nil
}
// CollectErrors behavior with regards to type fakeMemCap.
func (f *fakeMemCap) CollectErrors(outputs map[string]utils.ParsedOutput) error {
return nil
}
// USEMetrics behavior with regards to type fakeMemCap.
func (f *fakeMemCap) USEMetrics() *profiler.USEMetrics {
return f.Metrics
}
// Name behavior with regards to type fakeMemCap.
func (f *fakeMemCap) Name() string {
return f.MemCapName
}
// AdditionalInformation behaviro with regards to fakeMemCap.
func (f *fakeMemCap) AdditionalInformation() string {
return ""
}
// fakeStorageDevIO is a struct that implement the Component interface.
type fakeStorageDevIO struct {
StorageDevIOName string
Metrics *profiler.USEMetrics
}
// CollectUtilization behavior with regards to type fakeStorageDevIO.
func (f *fakeStorageDevIO) CollectUtilization(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Utilization = float64(7) // any arbitrary number
return nil
}
// CollectSaturation behavior with regards to type fakeStorageDevIO.
func (f *fakeStorageDevIO) CollectSaturation(outputs map[string]utils.ParsedOutput) error {
f.Metrics.Saturation = true
return nil
}
// CollectErrors behavior with regards to type fakeStorageDevIO.
func (f *fakeStorageDevIO) CollectErrors(outputs map[string]utils.ParsedOutput) error {
return nil
}
// USEMetrics behavior with regards to type fakeStorageDevIO.
func (f *fakeStorageDevIO) USEMetrics() *profiler.USEMetrics {
return f.Metrics
}
// Name behavior with regards to type fakeStorageDevIO.
func (f *fakeStorageDevIO) Name() string {
return f.StorageDevIOName
}
func (f *fakeStorageDevIO) AdditionalInformation() string {
return ""
}
// fakeTextLogger is a struct that implements the TextLogger interface.
type fakeTextLogger struct {
logged string
}
// fakeStructuredLogger is a struct that implements the StructuredLogger
// interface.
type fakeStructuredLogger struct {
buffer []logging.Entry
logged []logging.Entry
}
// Printf behavior with regards to type fakeTextLogger.
func (f *fakeTextLogger) Printf(text string, a ...interface{}) {
f.logged = text
}
// Log behavior with regards to type fakeStructuredLogger.
func (f *fakeStructuredLogger) Log(entry logging.Entry) {
f.buffer = append(f.buffer, entry)
}
// Flush behavior with regards to type fakeStructuredLogger.
func (f *fakeStructuredLogger) Flush() error {
f.logged = append(f.logged, f.buffer...)
f.buffer = []logging.Entry{}
return nil
}
// generateFakeProfilerOpts initializes profiler components and commands
// and returns them.
func generateFakeProfilerOpts() ([]profiler.Component, []profiler.Command) {
fCPU := &fakeCPU{
CPUName: "fakeCPU",
Metrics: &profiler.USEMetrics{},
}
fMemCap := &fakeMemCap{
MemCapName: "fakeMemCap",
Metrics: &profiler.USEMetrics{},
}
fStorageDevIO := &fakeStorageDevIO{
StorageDevIOName: "fakeStorageDevIO",
Metrics: &profiler.USEMetrics{},
}
// populating fake components.
components := []profiler.Component{fCPU, fMemCap, fStorageDevIO}
// populating fake commands.
cmds := []profiler.Command{}
return components, cmds
}
// For every input, the logged string must be the expected output unless the
// input is an empty string. In that case, nothing gets logged.
func TestTableLogText(t *testing.T) {
var tests = []struct {
name string
input string
wantOutput string
wantErr bool
}{
{
name: "non-empty string log",
input: "Node Profiler",
wantOutput: "Node Profiler",
wantErr: false,
},
{
name: "empty string log",
input: "",
wantOutput: "",
wantErr: true,
},
}
for _, test := range tests {
var f *fakeTextLogger = &fakeTextLogger{}
err := LogText(f, test.input)
// err will not be nil if the user attempted to log an empty string.
// ignoring the case in which the user logged empty string.
if gotErr := err != nil; gotErr != test.wantErr {
t.Errorf("LogText(%v, %v) = %v, wantErr %t", f, test.input, err, test.wantErr)
}
if diff := cmp.Diff(f.logged, test.wantOutput); diff != "" {
t.Errorf("ran LogText(fakeTextLogger, %+v), but got mismatch between got and want (-got, +want): \n diff %s", test.input, diff)
}
}
}
func TestTableLogProfilerReport(t *testing.T) {
// Retrieving testing data.
inputFile1, inputFile2 := "testdata/testdata.txt", "testdata/testdata2.txt"
inputFileData1, err := ioutil.ReadFile(inputFile1)
if err != nil {
t.Errorf("failed to open testing input file %v: %v\n", inputFile1, err)
}
inputFileData2, err := ioutil.ReadFile(inputFile2)
if err != nil {
t.Errorf("failed to open testing input file %v: %v\n", inputFile2, err)
}
// Retrieving profiler components and commands.
components, cmds := generateFakeProfilerOpts()
useReport := profiler.USEReport{
Components: components,
}
var cInfos []componentInfo
expected := struct {
Metrics *profiler.USEMetrics
}{
Metrics: &profiler.USEMetrics{
Timestamp: time.Date(2021, time.July, 21, 9, 59, 30, 0, time.UTC),
Interval: time.Date(2021, time.July, 21, 10, 3, 0, 0, time.UTC).Sub(time.Date(2021, time.July, 21, 9, 59, 30, 0, time.UTC)),
Utilization: 7,
Saturation: true,
Errors: 0,
},
}
for _, c := range useReport.Components {
cInfos = append(cInfos, componentInfo{Name: c.Name(), Metrics: expected.Metrics, Additional: c.AdditionalInformation()})
}
var tests = []struct {
name string
input *LoggerOpts
wantOutput []logging.Entry
wantErr bool
}{
{
name: "valid logger options and non-empty json payload log.",
input: &LoggerOpts{
ProjID: "cos-interns-playground",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "bash testdata/testcmd.sh",
CmdCount: 1,
CmdInterval: 0 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 1,
ProfilerInterval: 0 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: []logging.Entry{
{
Payload: struct {
CommandName string
CommandOutput string
}{
CommandName: "bash testdata/testcmd.sh",
CommandOutput: string(inputFileData1),
},
Severity: logging.Debug,
},
{
Payload: struct {
Components []componentInfo
Analysis string
}{
Components: cInfos,
Analysis: useReport.Analysis,
},
Severity: logging.Debug,
}},
wantErr: false,
},
{
name: "multiple commands executions and multiple profiler runs non-empty json payload log.",
input: &LoggerOpts{
ProjID: "cos-interns-playground",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "bash testdata/testcmd.sh",
CmdCount: 1,
CmdInterval: 0 * time.Second,
CmdTimeOut: 3 * time.Second,
}, ShellCmdOpts{
Command: "bash testdata/testhello.sh",
CmdCount: 1,
CmdInterval: 0 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 2,
ProfilerInterval: 0 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: []logging.Entry{
{
Payload: struct {
CommandName string
CommandOutput string
}{
CommandName: "bash testdata/testcmd.sh",
CommandOutput: string(inputFileData1),
},
Severity: logging.Debug,
},
{
Payload: struct {
CommandName string
CommandOutput string
}{
CommandName: "bash testdata/testhello.sh",
CommandOutput: string(inputFileData2),
},
Severity: logging.Debug,
},
{
Payload: struct {
Components []componentInfo
Analysis string
}{
Components: cInfos,
Analysis: useReport.Analysis,
},
Severity: logging.Debug,
}, {
Payload: struct {
Components []componentInfo
Analysis string
}{
Components: cInfos,
Analysis: useReport.Analysis,
},
Severity: logging.Debug,
}},
wantErr: false,
},
{
name: "invalid logger options payload log: empty command with CmdCount and/or CmdInterval.",
input: &LoggerOpts{
ProjID: "cos-interns-playground",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "",
CmdCount: 1,
CmdInterval: 0 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 1,
ProfilerInterval: 0 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: nil,
wantErr: true,
},
{
name: "invalid logger options payload log: inconsistent CmdCount and CmdInterval.",
input: &LoggerOpts{
ProjID: "cos-interns-playground",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "bash testdata/testcmd.sh",
CmdCount: 0,
CmdInterval: 3 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 1,
ProfilerInterval: 0 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: nil,
wantErr: true,
},
{
name: "invalid logger options payload log: inconsistent ProfilerCount and ProfilerInterval.",
input: &LoggerOpts{
ProjID: "cos-interns-playground",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "bash testdata/testcmd.sh",
CmdCount: 0,
CmdInterval: 3 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 0,
ProfilerInterval: 4 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: nil,
wantErr: true,
},
{
name: "invalid logger options payload log: no project ID/invalid project ID.",
input: &LoggerOpts{
ProjID: "",
ShCmds: []ShellCmdOpts{
ShellCmdOpts{
Command: "bash testdata/testcmd.sh",
CmdCount: 0,
CmdInterval: 3 * time.Second,
CmdTimeOut: 3 * time.Second,
},
},
ProfilerCount: 1,
ProfilerInterval: 0 * time.Second,
Components: components,
ProfilerCmds: cmds,
},
wantOutput: nil,
wantErr: true,
},
}
for _, test := range tests {
var f *fakeStructuredLogger = &fakeStructuredLogger{}
err := LogProfilerReport(f, test.input)
if gotErr := err != nil; gotErr != test.wantErr {
t.Errorf("LogProfilerReport(%v, %v) = %v, wantErr %t", f, test.input, err, test.wantErr)
}
if diff := cmp.Diff(f.logged, test.wantOutput, cmpopts.IgnoreFields(profiler.USEMetrics{}, "Timestamp", "Interval")); diff != "" {
t.Errorf("ran LogProfilerReport(fakeStructuredLogger,%+v), but got mismatch between got and want (-got, +want): \n diff %s", test.input, diff)
}
}
}