dsfx/internal/sim/system.go

220 lines
5.9 KiB
Go
Raw Normal View History

package sim
import (
"errors"
"math/rand"
"sync"
"time"
2025-03-25 15:24:26 -04:00
"numenor-labs.us/dsfx/dsfx/internal/lib/disk"
"numenor-labs.us/dsfx/dsfx/internal/lib/system"
)
// SimSystem is a simulated implementation of system.System.
// It allows the caller to set parameters for latency and failure chance (from tol)
// and provides inmemory values for commandline arguments, environment variables,
// and standard output/error.
type SimSystem struct {
// Simulation tolerance parameters (latency, failure chance, corruption chance).
tol Tolerance
// Simulated commandline arguments.
args []string
// Inmemory environment variables. Protected by mu.
mu sync.Mutex
env map[string]string
// Simulated directory paths.
homeDir string
configDir string
cacheDir string
tempDir string
// Simulated standard output and error.
stdout disk.File
stderr disk.File
}
// NewSimSystem returns a new simulated system that implements system.System.
// The caller provides a tolerance value and a slice of commandline arguments.
// Other fields (directories, environment) are prepopulated for simulation purposes.
func NewSimSystem(tol Tolerance, args []string) system.System {
s := &SimSystem{
tol: tol,
args: args,
env: make(map[string]string),
homeDir: "/home/simuser",
configDir: "/home/simuser/.config",
cacheDir: "/home/simuser/.cache",
tempDir: "/tmp",
}
// Create simulated stdout and stderr.
s.stdout = newSimOutput("stdout", tol)
s.stderr = newSimOutput("stderr", tol)
return s
}
// simulateOp applies the configured latency and possibly simulates an operation failure.
func (s *SimSystem) simulateOp() error {
time.Sleep(s.tol.Latency)
if rand.Float64() < s.tol.FailureChance {
return errors.New("simulated system operation failure")
}
return nil
}
// Args returns the simulated commandline arguments (skipping the program name).
func (s *SimSystem) Args() []string {
// Simulate latency even for argument access.
_ = s.simulateOp()
// Return a copy so that callers cannot modify the underlying slice.
cpy := make([]string, len(s.args))
copy(cpy, s.args)
return cpy
}
// Arg returns the simulated commandline argument at index i.
// If the index is outofrange, it returns an empty string.
func (s *SimSystem) Arg(i int) string {
_ = s.simulateOp()
if i < 0 || i >= len(s.args) {
return ""
}
return s.args[i]
}
// UserHomeDir returns the simulated home directory.
func (s *SimSystem) UserHomeDir() (string, error) {
if err := s.simulateOp(); err != nil {
return "", err
}
return s.homeDir, nil
}
// UserConfigDir returns the simulated configuration directory.
func (s *SimSystem) UserConfigDir() (string, error) {
if err := s.simulateOp(); err != nil {
return "", err
}
return s.configDir, nil
}
// UserCacheDir returns the simulated cache directory.
func (s *SimSystem) UserCacheDir() (string, error) {
if err := s.simulateOp(); err != nil {
return "", err
}
return s.cacheDir, nil
}
// TempDir returns the simulated temporary directory.
func (s *SimSystem) TempDir() string {
// We simulate latency even though TempDir cannot fail.
_ = s.simulateOp()
return s.tempDir
}
// Stdout returns a simulated disk.File representing standard output.
func (s *SimSystem) Stdout() disk.File {
// In a real simulation you might add latency/failure to writes on stdout.
_ = s.simulateOp()
return s.stdout
}
// Stderr returns a simulated disk.File representing standard error.
func (s *SimSystem) Stderr() disk.File {
_ = s.simulateOp()
return s.stderr
}
// Exit simulates terminating the program with the given exit code.
// As with many tests, we simulate exit by panicing with a special error.
// This allows tests to catch the panic and inspect the exit code.
func (s *SimSystem) Exit(code int) {
_ = s.simulateOp()
panic(&SimExitError{Code: code})
}
// GetEnv retrieves the simulated value for the environment variable named by key.
func (s *SimSystem) GetEnv(key string) string {
_ = s.simulateOp()
s.mu.Lock()
defer s.mu.Unlock()
return s.env[key]
}
// SetEnv sets the simulated environment variable named by key to value.
func (s *SimSystem) SetEnv(key, value string) error {
_ = s.simulateOp()
s.mu.Lock()
defer s.mu.Unlock()
s.env[key] = value
return nil
}
// SimExitError is the error value used to simulate a program exit.
type SimExitError struct {
Code int
}
// Error implements the error interface.
func (e *SimExitError) Error() string {
return "simulated program exit"
}
// --------------------------------------------------------------------------
// Helper functions and types for simulated standard output/error
// --------------------------------------------------------------------------
// newSimOutput creates a new simulated disk.File that acts as an output.
// Internally it creates a simulated file with an inmemory entry.
func newSimOutput(name string, tol Tolerance) disk.File {
entry := &simEntry{
name: name,
isDir: false,
perm: 0644,
modTime: time.Now(),
data: []byte{},
}
// Return a simFile which implements disk.File.
// (Note: simFile and simEntry are the types defined in dsfx/sim/disk.go.)
return &simFile{
entry: entry,
offset: 0,
readOnly: false,
tol: tol,
}
}
/*
Usage example:
// Define a tolerance for simulation.
tol := sim.Tolerance{
Latency: 10 * time.Millisecond,
FailureChance: 0.01,
CorruptionChance: 0.005,
}
// Simulate a system with custom commandline arguments.
sys := sim.NewSimSystem(tol, []string{"--verbose", "--config=sim.conf"})
// Calling system methods:
args := sys.Args()
home, err := sys.UserHomeDir()
// ... etc.
// To simulate an exit:
func run() {
defer func() {
if r := recover(); r != nil {
if exitErr, ok := r.(*sim.SimExitError); ok {
fmt.Printf("simulated exit(%d)\n", exitErr.Code)
}
}
}()
sys.Exit(3)
}
*/