blob: 9cd4a7c9704e660adf7d99f81b2201fb8f17b2a4 [file] [edit]
// 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
//
// https://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 windows
package resource
import (
"context"
"fmt"
"os"
"syscall"
"testing"
"unsafe"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/run"
"golang.org/x/sys/windows"
)
// constraintTestOpts contains options for setting up the test. This is primarily used for
// error injection.
type constraintTestOpts struct {
// overrideAll indicates whether to override all functions with mock variants.
overrideAll bool
// createJobObjErr indicates whether to return an error when creating job object.
createJobObjErr bool
// overrideCreateJob indicates whether to override the createJob function.
overrideCreateJob bool
// openProcessErr indicates whether to return an error when opening process.
openProcessErr bool
// overrideOpenProcess indicates whether to override the openProcess function.
overrideOpenProcess bool
// assignProccessErr indicates whether to return an error when assigning process to job object.
assignProccessErr bool
// overrideAssignProcess indicates whether to override the assignProcessToJobObj function.
overrideAssignProcess bool
// setInfoErr indicates whether to return an error when setting information job object.
setInfoErr bool
// overrideSetInfo indicates whether to override the setInformationJobObj function.
overrideSetInfo bool
}
func constraintTestSetup(t *testing.T, opts constraintTestOpts) *windowsClient {
t.Helper()
client := &windowsClient{
createJobObject: windows.CreateJobObject,
getProcessHandle: windows.OpenProcess,
assignProcessToJobObject: windows.AssignProcessToJobObject,
setInformationJobObject: windows.SetInformationJobObject,
handles: map[string]windows.Handle{},
}
if opts.overrideAll || opts.overrideCreateJob {
client.createJobObject = func(_ *windows.SecurityAttributes, _ *uint16) (windows.Handle, error) {
if opts.createJobObjErr {
return 0, fmt.Errorf("mock fail createJobObj")
}
return 1, nil
}
}
if opts.overrideAll || opts.overrideOpenProcess {
client.getProcessHandle = func(_ uint32, _ bool, _ uint32) (windows.Handle, error) {
if opts.openProcessErr {
return 0, fmt.Errorf("mock fail openProcess")
}
return 1, nil
}
}
if opts.overrideAll || opts.overrideAssignProcess {
client.assignProcessToJobObject = func(_ windows.Handle, _ windows.Handle) error {
if opts.assignProccessErr {
return fmt.Errorf("mock fail assignProcessToJobObj")
}
return nil
}
}
if opts.overrideAll || opts.overrideSetInfo {
client.setInformationJobObject = func(_ windows.Handle, _ uint32, _ uintptr, _ uint32) (ret int, err error) {
if opts.setInfoErr {
return 0, fmt.Errorf("mock fail setInformationJobObj")
}
return 1, nil
}
}
t.Cleanup(func() {
// Make sure to close all the handles.
for _, handle := range client.handles {
windows.CloseHandle(handle)
}
})
return client
}
// TestApplyResourceConstraintsError tests the error handling of applyResourceConstraints().
func TestApplyResourceConstraintsError(t *testing.T) {
test := []struct {
// name is the name of the test.
name string
// pages is the number of pages for setting workingsetsize.
pages int
// opts is the options for setting up the test.
opts constraintTestOpts
}{
{
name: "create-job-error",
opts: constraintTestOpts{
overrideAll: true,
createJobObjErr: true,
},
},
{
name: "get-process-error",
opts: constraintTestOpts{
overrideAll: true,
openProcessErr: true,
},
},
{
name: "assign-process-error",
opts: constraintTestOpts{
overrideAll: true,
assignProccessErr: true,
},
},
{
name: "memory-too-small",
pages: 1,
opts: constraintTestOpts{
overrideAll: true,
},
},
{
name: "set-info-error",
opts: constraintTestOpts{
overrideAll: true,
setInfoErr: true,
},
},
}
for _, test := range test {
t.Run(test.name, func(t *testing.T) {
c := constraintTestSetup(t, test.opts)
var maxMemory int64
if test.pages > 0 {
maxMemory = int64(test.pages * os.Getpagesize())
} else {
maxMemory = int64(21 * os.Getpagesize())
}
err := c.Apply(Constraint{Name: "testPlugin_A", MaxMemoryUsage: maxMemory, MaxCPUUsage: 10})
if err == nil {
t.Errorf("Apply() returned no error, error expected")
}
})
}
}
// TestApplyResourceConstraints tests if resource constraints are successfully applied to the
// created JobObject.
func TestApplyResourceConstraints(t *testing.T) {
c := constraintTestSetup(t, constraintTestOpts{})
testName := "testPlugin_A"
// Start a process for adding to the JobObject.
options := run.Options{
Name: "powershell",
Args: []string{"-NoProfile", "Start-Sleep -s 30"},
ExecMode: run.ExecModeAsync,
OutputType: run.OutputNone,
}
res, err := run.WithContext(context.Background(), options)
if err != nil {
t.Fatalf("failed to start up test program: %v", err)
}
t.Cleanup(func() {
// Kill the test process if it's still running.
process, _ := os.FindProcess(res.Pid)
if process != nil {
process.Kill()
}
})
testConstraint := Constraint{
PID: res.Pid,
Name: testName,
MaxMemoryUsage: int64(21 * os.Getpagesize()),
MaxCPUUsage: 50,
}
if err := c.Apply(testConstraint); err != nil {
t.Fatalf("Apply(%+v) returned unexpected error: %v", testConstraint, err)
}
// Get the handle.
handle, found := c.handles[testName]
if !found {
t.Fatalf("failed to find job object handle for %s", testName)
}
defer windows.CloseHandle(handle)
// Check that the memory limits are set correctly.
var memoryLimitInfo windows.JOBOBJECT_BASIC_LIMIT_INFORMATION
if err := windows.QueryInformationJobObject(
handle,
windows.JobObjectBasicLimitInformation,
uintptr(unsafe.Pointer(&memoryLimitInfo)),
uint32(unsafe.Sizeof(memoryLimitInfo)),
nil,
); err != nil {
t.Fatalf("failed to query basic limit information: %v", err)
}
// Since the value pointed to by the pointer reside in the Plugin struct, this should be safe.
actual := int64(memoryLimitInfo.MaximumWorkingSetSize)
if actual != testConstraint.MaxMemoryUsage {
t.Errorf("Apply(%+v) set MaximumWorkingSetSize %d, expected %d", testConstraint, actual, testConstraint.MaxMemoryUsage)
}
// Check that the CPU limits are set correctly.
var cpuLimitInfo jobObjectCPURateControlInfo
if err := windows.QueryInformationJobObject(
handle,
windows.JobObjectCpuRateControlInformation,
uintptr(unsafe.Pointer(&cpuLimitInfo)),
uint32(unsafe.Sizeof(cpuLimitInfo)),
nil,
); err != nil {
t.Fatalf("failed to query CPU rate control information: %v", err)
}
expectedCPULimit := int(testConstraint.MaxCPUUsage * 100)
if int(cpuLimitInfo.Value) != expectedCPULimit {
t.Errorf("Apply(%+v) set CPU rate control value %d, expected %d", testConstraint, cpuLimitInfo.Value, expectedCPULimit)
}
}
func TestRemoveConstraint(t *testing.T) {
c := constraintTestSetup(t, constraintTestOpts{
overrideOpenProcess: true,
overrideAssignProcess: true,
overrideSetInfo: true,
})
testName := "testPlugin_B"
testConstraint := Constraint{
PID: 4,
Name: testName,
MaxMemoryUsage: int64(21 * os.Getpagesize()),
MaxCPUUsage: 50,
}
if err := c.Apply(testConstraint); err != nil {
t.Fatalf("Apply(ctx, %+v) returned unexpected error: %v", testConstraint, err)
}
// Attempt to close it. It should not error.
if err := c.RemoveConstraint(context.Background(), testName); err != nil {
t.Fatalf("RemoveConstraint(ctx, %s) returned unexpected error: %v", testName, err)
}
// Check if the handle was successfully deleted from the map.
_, found := c.handles[testName]
if found {
t.Fatalf("RemoveConstraint(ctx, %s) did not remove job object handle", testName)
}
// Attempt to find the JobObject. It should no longer exist.
modKernel32 := windows.NewLazyDLL("kernel32.dll")
procOpenJobObject := modKernel32.NewProc("OpenJobObjectW")
name, err := windows.UTF16PtrFromString(testName)
if err != nil {
t.Fatalf("failed to parse job object name: %v", err)
}
h, _, _ := syscall.SyscallN(procOpenJobObject.Addr(), uintptr(0x1F001F), 0, uintptr(unsafe.Pointer(name)))
if h != 0 {
t.Fatalf("JobObject still exists")
}
}