| 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) | |
| } | |
| } | |
| } |