| // Copyright 2024 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| //go:build linux |
| |
| package ps |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/cmpopts" |
| ) |
| |
| // Setup a fake process directory. |
| func setupProc(t *testing.T, entries []*Process) { |
| t.Helper() |
| procDir := path.Join(t.TempDir(), "proc") |
| Client = &linuxClient{ |
| procDir: procDir, |
| } |
| |
| if err := os.MkdirAll(procDir, 0755); err != nil { |
| t.Fatalf("failed to make mocked proc dir: %+v", err) |
| } |
| t.Logf("procDir: %s", procDir) |
| |
| if entries == nil { |
| return |
| } |
| |
| if err := os.WriteFile(path.Join(procDir, "uptime"), []byte("100 200"), 0755); err != nil { |
| t.Fatalf("os.WriteFile() failed to write mocked proc uptime file: %+v", err) |
| } |
| |
| for _, curr := range entries { |
| procDir := path.Join(procDir, fmt.Sprintf("%d", curr.PID)) |
| if err := os.MkdirAll(procDir, 0755); err != nil { |
| t.Fatalf("os.MkDirAll() failed to make process dir: %+v", err) |
| } |
| t.Logf("procDir entry: %s", procDir) |
| |
| // randomFilePath is the path of a random/unknown file in the processe's |
| // dir. |
| randomFilePath := path.Join(procDir, "random-file") |
| err := os.WriteFile(path.Join(randomFilePath), []byte("random\n"), 0644) |
| if err != nil { |
| t.Fatalf("os.WriteFile() failed to write random proc file: %+v", err) |
| } |
| |
| // randomDirPath is the path of a random/unknown dir in the processe's dir. |
| randomDirPath := path.Join(procDir, "random-dir") |
| if err := os.MkdirAll(randomDirPath, 0755); err != nil { |
| t.Fatalf("os.MkDirAll() failed to make random dir in the proc dir: %+v", err) |
| } |
| |
| // Write a stat file. |
| statFileContents := []byte("0 0 0 0 0 0 0 0 0 0 0 0 0 500 500 0 0 0 0 0 0 2000 0 0 0") |
| err = os.WriteFile(path.Join(procDir, "stat"), statFileContents, 0755) |
| if err != nil { |
| t.Fatalf("os.WriteFile() failed to write stat file: %+v", err) |
| } |
| |
| // Write a smaps_rollup file. |
| err = os.WriteFile(path.Join(procDir, "smaps_rollup"), []byte("Rss: 500 kB\nTSS: 200 kB"), 0755) |
| if err != nil { |
| t.Fatalf("os.WriteFile() failed to write smaps_rollup file: %+v", err) |
| } |
| |
| if curr.Exe != "" { |
| exeLinkPath := path.Join(procDir, "exe") |
| if err := os.Symlink(curr.Exe, exeLinkPath); err != nil { |
| t.Fatalf("os.Symlink() failed to create exe sym link: %+v", err) |
| } |
| } |
| |
| if len(curr.CommandLine) > 0 { |
| cmdlineFilePath := path.Join(procDir, "cmdline") |
| |
| var data []byte |
| for _, line := range curr.CommandLine { |
| data = append(data, []byte(line)...) |
| data = append(data, 0) |
| } |
| |
| if err := os.WriteFile(cmdlineFilePath, data, 0644); err != nil { |
| t.Fatalf("os.WriteFile() failed to write random proc file: %+v", err) |
| } |
| } |
| } |
| |
| // Cleanup after test is finished. |
| t.Cleanup(func() { |
| Client = &linuxClient{ |
| procDir: defaultLinuxProcDir, |
| } |
| }) |
| } |
| |
| // TestEmptyProcDir tests if FindRegex() correctly returns nil if the process |
| // directory is empty. |
| func TestEmptyProcDir(t *testing.T) { |
| setupProc(t, nil) |
| procs, err := FindRegex(".*dhclient.*") |
| if err != nil { |
| t.Fatalf("ps.Find() returned error: %+v, expected: nil", err) |
| } |
| |
| if procs != nil { |
| t.Fatalf("ps.Find() returned: %+v, expected: nil", procs) |
| } |
| } |
| |
| // TestMalformedExe tests if FindRegex() correctly returns nil if the process |
| // contains a bad exe. |
| func TestMalformedExe(t *testing.T) { |
| procs := []*Process{ |
| &Process{1, "", nil}, |
| } |
| setupProc(t, procs) |
| |
| res, err := FindRegex(".*dhclient.*") |
| if err != nil { |
| t.Fatalf("ps.Find() returned error: %+v, expected: nil", err) |
| } |
| |
| if res != nil { |
| t.Fatalf("ps.Find() returned: %+v, expected: nil", res) |
| } |
| } |
| |
| // TestFind tests whether FindRegex() can find existing processes, and that it |
| // can return a nil response if the process does not exist. |
| func TestFind(t *testing.T) { |
| tests := []struct { |
| name string |
| success bool |
| args []string |
| expr string |
| }{ |
| {"dhclient-exist", true, []string{"dhclient", "eth0"}, ".*dhclient.*"}, |
| {"google-guest-agent-exist", true, []string{"google_guest_agent"}, ".*google_guest_agent.*"}, |
| {"dhclient-not-exist", false, []string{}, ".*dhclientx.*"}, |
| {"google-guest-agent-not-exist", false, []string{}, ".*google_guest_agentx.*"}, |
| } |
| |
| procs := []*Process{ |
| &Process{1, "/usr/bin/dhclient", []string{"dhclient", "eth0"}}, |
| &Process{2, "/usr/bin/google_guest_agent", []string{"google_guest_agent"}}, |
| } |
| setupProc(t, procs) |
| |
| for _, curr := range tests { |
| t.Run(curr.name, func(t *testing.T) { |
| res, err := FindRegex(curr.expr) |
| if curr.success { |
| if err != nil { |
| t.Errorf("ps.Find() returned error: %+v, expected: nil", err) |
| } |
| |
| if res == nil { |
| t.Fatalf("ps.Find() returned: nil, expected: non-nil") |
| } |
| |
| if diff := cmp.Diff(res[0].CommandLine, curr.args, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" { |
| t.Errorf("ps.Find() returned diff for args (-want +got):\n%s", diff) |
| } |
| } else { |
| if res != nil { |
| t.Fatalf("ps.Find() returned: non-nil, expected: nil") |
| } |
| } |
| }) |
| } |
| } |
| |
| // TestMemory tests whether Memory() returns the correct memory usage. |
| func TestMemory(t *testing.T) { |
| procs := []*Process{ |
| &Process{1, "", nil}, |
| } |
| setupProc(t, procs) |
| |
| actual, err := Memory(1) |
| if err != nil { |
| t.Fatalf("ps.Memory() returned error: %+v, expected: nil", err) |
| } |
| |
| // This should successfully read the RSS value of 500 kB. |
| if actual != 500 { |
| t.Fatalf("ps.Memory() returned: %d, expected: 500", actual) |
| } |
| } |
| |
| // TestCPUUsage tests whether CPUUsage() returns the correct CPU usage. |
| func TestCPUUsage(t *testing.T) { |
| procs := []*Process{ |
| &Process{1, "", nil}, |
| } |
| setupProc(t, procs) |
| ctx := context.Background() |
| |
| actual, err := CPUUsage(ctx, 1) |
| if err != nil { |
| t.Fatalf("ps.CPUUsage() returned error: %+v, expected: nil", err) |
| } |
| |
| if actual != 0.125 { |
| t.Fatalf("ps.CPUUsage() returned: %f, expected: 0.125", actual) |
| } |
| } |