dsfx/internal/sim/system.go

220 lines
5.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sim
import (
"errors"
"math/rand"
"sync"
"time"
"git.numenor-labs.us/dsfx/internal/lib/disk"
"git.numenor-labs.us/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)
}
*/