Skip to content

Commit

Permalink
Add some unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ricardobranco777 committed Dec 31, 2024
1 parent d81b2de commit 955a084
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 1 deletion.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ $(BIN): *.go
test:
@go vet
@staticcheck
@go test ./... -v

.PHONY: clean
clean:
Expand Down
5 changes: 4 additions & 1 deletion restartable.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"golang.org/x/sys/unix"
"io/fs"
"log"
"os"
"os/user"
Expand Down Expand Up @@ -37,6 +38,8 @@ type ProcPidFS interface {

// RealProcPidFS implements ProcPidFS for real /proc/<pid> filesystem
type RealProcPidFS struct {
ProcPidFS
fs fs.FS

Check failure on line 42 in restartable.go

View workflow job for this annotation

GitHub Actions / OS ubuntu-latest Go 1.22

field fs is unused (U1000)

Check failure on line 42 in restartable.go

View workflow job for this annotation

GitHub Actions / OS ubuntu-latest Go 1.23

field fs is unused (U1000)
dirFd int
pid int
}
Expand Down Expand Up @@ -193,7 +196,7 @@ func getCommand(fs ProcPidFS, fullPath bool, statusName string) (string, error)
command = strings.Split(command, " ")[0]
}
}
return command, nil
return strings.TrimSpace(command), nil
}

// parseStatusField extracts a field value from the status file given a key
Expand Down
242 changes: 242 additions & 0 deletions restartable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package main

import (
"fmt"
"io/fs"
"testing"
"testing/fstest"
)

// MockProcPidFS mimics the behavior of RealProcPidFS using MapFS.
type MockProcPidFS struct {
fs fs.FS
pid int
}

// ReadFile reads the mock file system.
func (p *MockProcPidFS) ReadFile(path string) ([]byte, error) {
return fs.ReadFile(p.fs, path) // Uses MapFS to read files
}

// ReadLink reads the mock file system.
func (p *MockProcPidFS) ReadLink(path string) (string, error) {
// Workaround for https://github.com/golang/go/issues/49580
file, err := p.fs.Open(path)
if err != nil {
return "", &fs.PathError{Op: "open", Path: path, Err: err}
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
return "", &fs.PathError{Op: "stat", Path: path, Err: err}
}

if stat.Mode()&fs.ModeSymlink == 0 {
return "", &fs.PathError{Op: "readlink", Path: path, Err: fmt.Errorf("not a symlink")}
}

// Since fstest.MapFS stores symlink target in `Data`, retrieve it directly
if mapFile, ok := p.fs.(fstest.MapFS)[path]; ok {
return string(mapFile.Data), nil
}
return "", &fs.PathError{Op: "readlink", Path: path, Err: fmt.Errorf("symlink target not found")}
}

func (p *MockProcPidFS) PID() int {
return -1
}

// Open is needed for fs.FS interface, we can mock it.
func (p *MockProcPidFS) Open(name string) (fs.File, error) {
return nil, fmt.Errorf("mock Open method")
}

// Close mimics closing a file, just return nil for the mock.
func (p *MockProcPidFS) Close() error {
return nil // No real file to close
}

// MockProcFS using fstest.MapFS with support for symlinks
func mockProcFS(pid int, files map[string]string, symlinks map[string]string) fs.FS {
mockFS := fstest.MapFS{}

// Add regular files to the mockFS
for path, content := range files {
mockFS[path] = &fstest.MapFile{
Data: []byte(content),
}
}

// Add symlinks to the mockFS
for path, target := range symlinks {
mockFS[path] = &fstest.MapFile{
Mode: fs.ModeSymlink,
Data: []byte(target),
}
}

return &MockProcPidFS{fs: mockFS, pid: pid} // Pass the correct pid here
}

// Test getDeleted
func TestGetDeleted(t *testing.T) {
mockFS := mockProcFS(1234, map[string]string{
"maps": "00400000-00452000 r-xp 00000000 fd:00 12345 /bin/bash (deleted)\n",
}, nil)

procFS := &MockProcPidFS{
fs: mockFS,
pid: 1234,
}

files, err := getDeleted(procFS)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(files) != 1 || files[0] != "/bin/bash" {
t.Errorf("Expected ['/bin/bash'], got %v", files)
}
}

// Test getService
func TestGetService(t *testing.T) {
mockFS := mockProcFS(1234, map[string]string{
"cgroup": "/proc/777/cgroup:0::/system.slice/sshd.service\n",
}, nil)

procFS := &MockProcPidFS{
pid: 1234,
fs: mockFS,
}

service := getService(procFS, false)
if service != "sshd" {
t.Errorf("Expected 'sshd', got '%s'", service)
}
}

// Test getCommand
func TestGetCommand(t *testing.T) {
mockFS := mockProcFS(1234, map[string]string{
"cmdline": "/bin/bash\x00--version\x00\x00",
}, nil)

procFS := &MockProcPidFS{
pid: 1234,
fs: mockFS,
}

command, err := getCommand(procFS, true, "bash")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
expected := "/bin/bash --version"
if command != expected {
t.Errorf("Expected '%s', got '%s'", expected, command)
}
}

// Test parseStatusField
func TestParseStatusField(t *testing.T) {
tests := []struct {
name string
data string
field string
expected string
}{
{
name: "Valid Uid Field",
data: "Name:\tbash\nPPid:\t1\nUid:\t1000\t1001\t1002\t1003\n",
field: "Uid",
expected: "1000\t1001\t1002\t1003",
},
{
name: "Valid Name Field",
data: "Name:\tbash\nPPid:\t1\nUid:\t1000\t1000\t1000\t1000\n",
field: "Name",
expected: "bash",
},
{
name: "Valid PPid Field",
data: "Name:\tbash\nPPid:\t1\nUid:\t1000\t1000\t1000\t1000\n",
field: "PPid",
expected: "1",
},
{
name: "Missing Field",
data: "Name:\tbash\nPPid:\t1\nUid:\t1000\t1000\t1000\t1000\n",
field: "Gid",
expected: "",
},
{
name: "Empty Data",
data: "",
field: "Uid",
expected: "",
},
{
name: "Malformed Data",
data: "Name\nPPid:\t1\nUid:\t1000\n",
field: "Uid",
expected: "1000",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
field := parseStatusField(tt.data, tt.field)
if field != tt.expected {
t.Errorf("For field '%s': expected '%s', got '%s'", tt.field, tt.expected, field)
}
})
}
}

// Test getProcessInfo
func TestGetProcessInfo(t *testing.T) {
mockFS := mockProcFS(1234, map[string]string{
"cgroup": "/proc/777/cgroup:0::/system.slice/sshd.service\n",
"cmdline": "/bin/bash\x00--version\x00\x00",
"maps": "00400000-00452000 r-xp 00000000 fd:00 12345 /bin/bash (deleted)\n",
"status": "Name:\tbash\nPPid:\t1\nUid:\t1000\t1000\t1000\t1000\n",
}, nil)

procFS := &MockProcPidFS{
pid: 1234,
fs: mockFS,
}

info, err := getProcessInfo(procFS, true, false)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if info.Pid != 1234 {
t.Errorf("Expected PID 1234, got %d", info.Pid)
}
if info.Ppid != 1 {
t.Errorf("Expected PPID 1, got %d", info.Ppid)
}
if info.Uid != 1000 {
t.Errorf("Expected UID 1000, got %d", info.Uid)
}
if info.Command != "/bin/bash --version" {
t.Errorf("Expected command '/bin/bash --version', got '%s'", info.Command)
}
if len(info.Deleted) != 1 || info.Deleted[0] != "/bin/bash" {
t.Errorf("Expected Deleted ['/bin/bash'], got %v", info.Deleted)
}
}

// Test getUser
func TestGetUser(t *testing.T) {
user := getUser(0)
if user != "root" {
t.Errorf("Expected 'root', got '%s'", user)
}

user = getUser(99999) // Invalid UID
if user != "-" {
t.Errorf("Expected '-', got '%s'", user)
}
}

0 comments on commit 955a084

Please sign in to comment.