diff --git a/README.md b/README.md index 2a21b08..54c0411 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,99 @@ flowchart TD The plugin architecture allows custom scheduler implementations to override default behavior while maintaining compatibility with the core Gthulhu scheduler framework. +## Factory Pattern + +This repository implements a factory pattern for plugin creation, allowing dynamic plugin registration and instantiation without hardcoded switch-case logic in the Gthulhu main repository. + +### Quick Start + +```go +import ( + "github.com/Gthulhu/plugin/plugin" + // Import plugins to trigger registration + _ "github.com/Gthulhu/plugin/plugin/gthulhu" + _ "github.com/Gthulhu/plugin/plugin/simple" +) + +// Create a scheduler plugin using the factory +config := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000 * 1000, // 5ms + SliceNsMin: 500 * 1000, // 0.5ms +} + +scheduler, err := plugin.NewSchedulerPlugin(config) +if err != nil { + log.Fatalf("Failed to create plugin: %v", err) +} + +// Use the scheduler +scheduler.DrainQueuedTask(sched) +``` + +### Available Plugins + +| Mode | Description | +|------|-------------| +| `gthulhu` | Advanced scheduler with API integration and scheduling strategies | +| `simple` | Simple weighted vtime scheduler | +| `simple-fifo` | Simple FIFO scheduler | + +### Configuration + +The `SchedConfig` struct holds all configuration parameters: + +```go +type SchedConfig struct { + Mode string // Plugin mode to use + SliceNsDefault uint64 // Default time slice in nanoseconds + SliceNsMin uint64 // Minimum time slice in nanoseconds + FifoMode bool // Enable FIFO mode for simple plugin + + // Scheduler configuration (for Gthulhu plugin) + Scheduler struct { + SliceNsDefault uint64 + SliceNsMin uint64 + } + + // API configuration (for Gthulhu plugin) + APIConfig struct { + PublicKeyPath string + BaseURL string + } +} +``` + +### Creating New Plugins + +To add a new plugin: + +1. Implement the `CustomScheduler` interface +2. Register your plugin in `init()`: + +```go +func init() { + plugin.RegisterNewPlugin("myplugin", func(config *plugin.SchedConfig) (plugin.CustomScheduler, error) { + return NewMyPlugin(config), nil + }) +} +``` + +3. Import your plugin package to trigger registration + +## Testing + +Run tests with coverage: + +```bash +# Run all tests +go test ./... -v + +# Check coverage +go test ./... -coverprofile=coverage.out +go tool cover -func=coverage.out + +# View coverage in browser +go tool cover -html=coverage.out +``` + diff --git a/examples/usage_example.go b/examples/usage_example.go new file mode 100644 index 0000000..1bce55b --- /dev/null +++ b/examples/usage_example.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "log" + + "github.com/Gthulhu/plugin/plugin" + // Import plugin packages to trigger init() registration + _ "github.com/Gthulhu/plugin/plugin/gthulhu" + _ "github.com/Gthulhu/plugin/plugin/simple" +) + +// Example showing how to use this in Gthulhu main.go +func gthulhuMainExample() { + // Create plugin configuration + pluginConfig := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000 * 1000, + SliceNsMin: 500 * 1000, + } + pluginConfig.Scheduler.SliceNsDefault = 5000 * 1000 + pluginConfig.Scheduler.SliceNsMin = 500 * 1000 + + // Create the plugin using the factory + scheduler, err := plugin.NewSchedulerPlugin(pluginConfig) + if err != nil { + log.Fatalf("Failed to create scheduler plugin: %v", err) + } + + fmt.Printf("Created plugin successfully, pool count: %d\n", scheduler.GetPoolCount()) +} + +func main() { + fmt.Println("=== Gthulhu Plugin Factory Example ===") + + // List available plugins + modes := plugin.GetRegisteredModes() + fmt.Printf("Available plugin modes: %v\n\n", modes) + + // Create plugin instance + gthulhuMainExample() +} diff --git a/plugin/gthulhu/gthulhu.go b/plugin/gthulhu/gthulhu.go index 01f57da..0d68ba1 100644 --- a/plugin/gthulhu/gthulhu.go +++ b/plugin/gthulhu/gthulhu.go @@ -5,6 +5,43 @@ import ( "github.com/Gthulhu/plugin/plugin" ) +func init() { + // Register the gthulhu plugin with the factory + err := plugin.RegisterNewPlugin("gthulhu", func(config *plugin.SchedConfig) (plugin.CustomScheduler, error) { + // Use Scheduler config if available, otherwise use SimpleScheduler config + sliceNsDefault := config.Scheduler.SliceNsDefault + sliceNsMin := config.Scheduler.SliceNsMin + + if sliceNsDefault == 0 && config.SliceNsDefault > 0 { + sliceNsDefault = config.SliceNsDefault + } + if sliceNsMin == 0 && config.SliceNsMin > 0 { + sliceNsMin = config.SliceNsMin + } + + gthulhuPlugin := NewGthulhuPlugin(sliceNsDefault, sliceNsMin) + + // Initialize JWT client if API config is provided + if config.APIConfig.PublicKeyPath != "" && config.APIConfig.BaseURL != "" { + err := gthulhuPlugin.InitJWTClient(config.APIConfig.PublicKeyPath, config.APIConfig.BaseURL) + if err != nil { + return nil, err + } + + // Initialize metrics client + err = gthulhuPlugin.InitMetricsClient(config.APIConfig.BaseURL) + if err != nil { + return nil, err + } + } + + return gthulhuPlugin, nil + }) + if err != nil { + panic(err) + } +} + type GthulhuPlugin struct { // Scheduler configuration sliceNsDefault uint64 diff --git a/plugin/plugin.go b/plugin/plugin.go index d6aec31..f6a55ea 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,6 +1,11 @@ package plugin -import "github.com/Gthulhu/plugin/models" +import ( + "fmt" + "sync" + + "github.com/Gthulhu/plugin/models" +) type Sched interface { DequeueTask(task *models.QueuedTask) @@ -20,3 +25,88 @@ type CustomScheduler interface { // GetPoolCount will be called by the scheduler to notify the number of tasks waiting to be dispatched (NotifyComplete) GetPoolCount() uint64 } + +// SchedConfig holds the configuration parameters for creating a scheduler plugin +type SchedConfig struct { + // Mode specifies which scheduler plugin to use (e.g., "gthulhu", "simple", "simple-fifo") + Mode string `yaml:"mode"` + + // SimpleScheduler configuration + SliceNsDefault uint64 `yaml:"slice_ns_default"` + SliceNsMin uint64 `yaml:"slice_ns_min"` + FifoMode bool `yaml:"fifo_mode"` + + // Scheduler configuration (for Gthulhu plugin) + // These match the parameters that would be passed from the Gthulhu main repo + Scheduler struct { + SliceNsDefault uint64 `yaml:"slice_ns_default"` + SliceNsMin uint64 `yaml:"slice_ns_min"` + } `yaml:"scheduler"` + + // API configuration + APIConfig struct { + PublicKeyPath string `yaml:"public_key_path"` + BaseURL string `yaml:"base_url"` + } `yaml:"api_config"` +} + +// PluginFactory is a function type that creates a CustomScheduler instance +type PluginFactory func(config *SchedConfig) (CustomScheduler, error) + +var ( + // pluginRegistry stores registered plugin factories + pluginRegistry = make(map[string]PluginFactory) + registryMutex sync.RWMutex +) + +// RegisterNewPlugin registers a plugin factory for a specific mode +// This should be called in the init() function of each plugin implementation +func RegisterNewPlugin(mode string, factory PluginFactory) error { + registryMutex.Lock() + defer registryMutex.Unlock() + + if mode == "" { + return fmt.Errorf("plugin mode cannot be empty") + } + + if factory == nil { + return fmt.Errorf("plugin factory cannot be nil") + } + + if _, exists := pluginRegistry[mode]; exists { + return fmt.Errorf("plugin mode '%s' is already registered", mode) + } + + pluginRegistry[mode] = factory + return nil +} + +// NewSchedulerPlugin creates a new scheduler plugin based on the configuration +// This is the factory function that follows the simple factory pattern +func NewSchedulerPlugin(config *SchedConfig) (CustomScheduler, error) { + if config == nil { + return nil, fmt.Errorf("config cannot be nil") + } + + registryMutex.RLock() + factory, exists := pluginRegistry[config.Mode] + registryMutex.RUnlock() + + if !exists { + return nil, fmt.Errorf("unknown plugin mode: %s", config.Mode) + } + + return factory(config) +} + +// GetRegisteredModes returns a list of all registered plugin modes +func GetRegisteredModes() []string { + registryMutex.RLock() + defer registryMutex.RUnlock() + + modes := make([]string, 0, len(pluginRegistry)) + for mode := range pluginRegistry { + modes = append(modes, mode) + } + return modes +} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go new file mode 100644 index 0000000..f6c0c0d --- /dev/null +++ b/plugin/plugin_test.go @@ -0,0 +1,388 @@ +package plugin + +import ( + "testing" + + "github.com/Gthulhu/plugin/models" +) + +// TestRegisterNewPlugin tests the plugin registration functionality +func TestRegisterNewPlugin(t *testing.T) { + // Clear registry for testing + registryMutex.Lock() + originalRegistry := pluginRegistry + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + // Restore original registry after test + defer func() { + registryMutex.Lock() + pluginRegistry = originalRegistry + registryMutex.Unlock() + }() + + t.Run("SuccessfulRegistration", func(t *testing.T) { + // Clear for this subtest + registryMutex.Lock() + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{}, nil + } + + err := RegisterNewPlugin("test-mode", factory) + if err != nil { + t.Errorf("RegisterNewPlugin failed: %v", err) + } + + // Verify registration + modes := GetRegisteredModes() + if len(modes) != 1 || modes[0] != "test-mode" { + t.Errorf("Expected 1 mode 'test-mode', got %v", modes) + } + }) + + t.Run("EmptyModeError", func(t *testing.T) { + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{}, nil + } + + err := RegisterNewPlugin("", factory) + if err == nil { + t.Error("Expected error for empty mode, got nil") + } + if err.Error() != "plugin mode cannot be empty" { + t.Errorf("Expected 'plugin mode cannot be empty' error, got: %v", err) + } + }) + + t.Run("NilFactoryError", func(t *testing.T) { + err := RegisterNewPlugin("test-mode", nil) + if err == nil { + t.Error("Expected error for nil factory, got nil") + } + if err.Error() != "plugin factory cannot be nil" { + t.Errorf("Expected 'plugin factory cannot be nil' error, got: %v", err) + } + }) + + t.Run("DuplicateRegistrationError", func(t *testing.T) { + // Clear for this subtest + registryMutex.Lock() + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{}, nil + } + + // Register once + err := RegisterNewPlugin("duplicate-mode", factory) + if err != nil { + t.Errorf("First registration failed: %v", err) + } + + // Try to register again + err = RegisterNewPlugin("duplicate-mode", factory) + if err == nil { + t.Error("Expected error for duplicate registration, got nil") + } + if err.Error() != "plugin mode 'duplicate-mode' is already registered" { + t.Errorf("Expected duplicate registration error, got: %v", err) + } + }) +} + +// TestNewSchedulerPlugin tests the factory function +func TestNewSchedulerPlugin(t *testing.T) { + // Clear registry for testing + registryMutex.Lock() + originalRegistry := pluginRegistry + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + // Restore original registry after test + defer func() { + registryMutex.Lock() + pluginRegistry = originalRegistry + registryMutex.Unlock() + }() + + t.Run("NilConfigError", func(t *testing.T) { + _, err := NewSchedulerPlugin(nil) + if err == nil { + t.Error("Expected error for nil config, got nil") + } + if err.Error() != "config cannot be nil" { + t.Errorf("Expected 'config cannot be nil' error, got: %v", err) + } + }) + + t.Run("UnknownModeError", func(t *testing.T) { + config := &SchedConfig{Mode: "unknown-mode"} + _, err := NewSchedulerPlugin(config) + if err == nil { + t.Error("Expected error for unknown mode, got nil") + } + if err.Error() != "unknown plugin mode: unknown-mode" { + t.Errorf("Expected 'unknown plugin mode' error, got: %v", err) + } + }) + + t.Run("SuccessfulPluginCreation", func(t *testing.T) { + // Clear and register a test plugin + registryMutex.Lock() + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{mode: config.Mode}, nil + } + + err := RegisterNewPlugin("test-plugin", factory) + if err != nil { + t.Fatalf("Failed to register plugin: %v", err) + } + + config := &SchedConfig{Mode: "test-plugin"} + scheduler, err := NewSchedulerPlugin(config) + if err != nil { + t.Errorf("NewSchedulerPlugin failed: %v", err) + } + if scheduler == nil { + t.Error("Expected scheduler, got nil") + } + + mock, ok := scheduler.(*mockScheduler) + if !ok { + t.Error("Expected mockScheduler type") + } + if mock.mode != "test-plugin" { + t.Errorf("Expected mode 'test-plugin', got '%s'", mock.mode) + } + }) +} + +// TestGetRegisteredModes tests retrieving all registered modes +func TestGetRegisteredModes(t *testing.T) { + // Clear registry for testing + registryMutex.Lock() + originalRegistry := pluginRegistry + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + // Restore original registry after test + defer func() { + registryMutex.Lock() + pluginRegistry = originalRegistry + registryMutex.Unlock() + }() + + t.Run("EmptyRegistry", func(t *testing.T) { + // Clear registry + registryMutex.Lock() + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + modes := GetRegisteredModes() + if len(modes) != 0 { + t.Errorf("Expected 0 modes, got %d: %v", len(modes), modes) + } + }) + + t.Run("MultipleModes", func(t *testing.T) { + // Clear and register multiple plugins + registryMutex.Lock() + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{}, nil + } + + _ = RegisterNewPlugin("mode1", factory) + _ = RegisterNewPlugin("mode2", factory) + _ = RegisterNewPlugin("mode3", factory) + + modes := GetRegisteredModes() + if len(modes) != 3 { + t.Errorf("Expected 3 modes, got %d: %v", len(modes), modes) + } + + // Check all modes are present + modeMap := make(map[string]bool) + for _, mode := range modes { + modeMap[mode] = true + } + + expectedModes := []string{"mode1", "mode2", "mode3"} + for _, expected := range expectedModes { + if !modeMap[expected] { + t.Errorf("Expected mode '%s' not found in: %v", expected, modes) + } + } + }) +} + +// TestSchedConfigStructure tests the SchedConfig struct +func TestSchedConfigStructure(t *testing.T) { + t.Run("CompleteConfig", func(t *testing.T) { + config := &SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000000, + SliceNsMin: 500000, + FifoMode: false, + } + + config.Scheduler.SliceNsDefault = 10000000 + config.Scheduler.SliceNsMin = 1000000 + config.APIConfig.PublicKeyPath = "/path/to/key" + config.APIConfig.BaseURL = "https://api.example.com" + + if config.Mode != "gthulhu" { + t.Errorf("Expected mode 'gthulhu', got '%s'", config.Mode) + } + if config.SliceNsDefault != 5000000 { + t.Errorf("Expected SliceNsDefault 5000000, got %d", config.SliceNsDefault) + } + if config.Scheduler.SliceNsDefault != 10000000 { + t.Errorf("Expected Scheduler.SliceNsDefault 10000000, got %d", config.Scheduler.SliceNsDefault) + } + if config.APIConfig.BaseURL != "https://api.example.com" { + t.Errorf("Expected BaseURL 'https://api.example.com', got '%s'", config.APIConfig.BaseURL) + } + }) + + t.Run("MinimalConfig", func(t *testing.T) { + config := &SchedConfig{ + Mode: "simple", + } + + if config.Mode != "simple" { + t.Errorf("Expected mode 'simple', got '%s'", config.Mode) + } + if config.SliceNsDefault != 0 { + t.Errorf("Expected default SliceNsDefault 0, got %d", config.SliceNsDefault) + } + }) +} + +// TestConcurrentRegistration tests thread-safety of plugin registration +func TestConcurrentRegistration(t *testing.T) { + // Clear registry for testing + registryMutex.Lock() + originalRegistry := pluginRegistry + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + // Restore original registry after test + defer func() { + registryMutex.Lock() + pluginRegistry = originalRegistry + registryMutex.Unlock() + }() + + factory := func(config *SchedConfig) (CustomScheduler, error) { + return &mockScheduler{}, nil + } + + // Register plugins concurrently + done := make(chan bool, 10) + for i := 0; i < 10; i++ { + go func(id int) { + mode := "concurrent-mode-" + string(rune('0'+id)) + _ = RegisterNewPlugin(mode, factory) + done <- true + }(i) + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } + + // Check that some plugins were registered + modes := GetRegisteredModes() + if len(modes) == 0 { + t.Error("Expected at least some modes to be registered") + } +} + +// TestFactoryWithConfigParameters tests that config parameters are passed correctly +func TestFactoryWithConfigParameters(t *testing.T) { + // Clear registry for testing + registryMutex.Lock() + originalRegistry := pluginRegistry + pluginRegistry = make(map[string]PluginFactory) + registryMutex.Unlock() + + // Restore original registry after test + defer func() { + registryMutex.Lock() + pluginRegistry = originalRegistry + registryMutex.Unlock() + }() + + // Register a plugin that captures config + var capturedConfig *SchedConfig + factory := func(config *SchedConfig) (CustomScheduler, error) { + capturedConfig = config + return &mockScheduler{}, nil + } + + _ = RegisterNewPlugin("config-test", factory) + + config := &SchedConfig{ + Mode: "config-test", + SliceNsDefault: 12345, + SliceNsMin: 6789, + FifoMode: true, + } + + _, err := NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("NewSchedulerPlugin failed: %v", err) + } + + if capturedConfig == nil { + t.Fatal("Config was not passed to factory") + } + if capturedConfig.Mode != "config-test" { + t.Errorf("Expected mode 'config-test', got '%s'", capturedConfig.Mode) + } + if capturedConfig.SliceNsDefault != 12345 { + t.Errorf("Expected SliceNsDefault 12345, got %d", capturedConfig.SliceNsDefault) + } + if capturedConfig.SliceNsMin != 6789 { + t.Errorf("Expected SliceNsMin 6789, got %d", capturedConfig.SliceNsMin) + } + if !capturedConfig.FifoMode { + t.Error("Expected FifoMode true, got false") + } +} + +// mockScheduler is a mock implementation of CustomScheduler for testing +type mockScheduler struct { + mode string +} + +func (m *mockScheduler) DrainQueuedTask(s Sched) int { + return 0 +} + +func (m *mockScheduler) SelectQueuedTask(s Sched) *models.QueuedTask { + return nil +} + +func (m *mockScheduler) SelectCPU(s Sched, t *models.QueuedTask) (error, int32) { + return nil, 0 +} + +func (m *mockScheduler) DetermineTimeSlice(s Sched, t *models.QueuedTask) uint64 { + return 0 +} + +func (m *mockScheduler) GetPoolCount() uint64 { + return 0 +} diff --git a/plugin/simple/simple.go b/plugin/simple/simple.go index 0a4bf92..f8fe42c 100644 --- a/plugin/simple/simple.go +++ b/plugin/simple/simple.go @@ -5,6 +5,36 @@ import ( "github.com/Gthulhu/plugin/plugin" ) +func init() { + // Register the simple plugin with weighted vtime mode + err := plugin.RegisterNewPlugin("simple", func(config *plugin.SchedConfig) (plugin.CustomScheduler, error) { + simplePlugin := NewSimplePlugin(false) // weighted vtime mode + + if config.SliceNsDefault > 0 { + simplePlugin.SetSliceDefault(config.SliceNsDefault) + } + + return simplePlugin, nil + }) + if err != nil { + panic(err) + } + + // Register the simple plugin with FIFO mode + err = plugin.RegisterNewPlugin("simple-fifo", func(config *plugin.SchedConfig) (plugin.CustomScheduler, error) { + simplePlugin := NewSimplePlugin(true) // FIFO mode + + if config.SliceNsDefault > 0 { + simplePlugin.SetSliceDefault(config.SliceNsDefault) + } + + return simplePlugin, nil + }) + if err != nil { + panic(err) + } +} + // SimplePlugin implements a basic scheduler that can operate in two modes: // 1. Weighted vtime scheduling (default) // 2. FIFO scheduling diff --git a/tests/factory_integration_test.go b/tests/factory_integration_test.go new file mode 100644 index 0000000..3cc5ed3 --- /dev/null +++ b/tests/factory_integration_test.go @@ -0,0 +1,362 @@ +package tests + +import ( + "testing" + + "github.com/Gthulhu/plugin/models" + "github.com/Gthulhu/plugin/plugin" + + // Import plugin packages to trigger init() functions + _ "github.com/Gthulhu/plugin/plugin/gthulhu" + _ "github.com/Gthulhu/plugin/plugin/simple" +) + +// TestGthulhuPluginThroughFactory tests creating gthulhu plugin via factory +func TestGthulhuPluginThroughFactory(t *testing.T) { + t.Run("BasicCreation", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000 * 1000, + SliceNsMin: 500 * 1000, + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create gthulhu plugin: %v", err) + } + if scheduler == nil { + t.Fatal("Expected scheduler, got nil") + } + + // Verify it implements CustomScheduler interface + if scheduler.GetPoolCount() != 0 { + t.Errorf("Expected initial pool count 0, got %d", scheduler.GetPoolCount()) + } + }) + + t.Run("WithSchedulerConfig", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "gthulhu", + } + config.Scheduler.SliceNsDefault = 10000 * 1000 + config.Scheduler.SliceNsMin = 1000 * 1000 + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create gthulhu plugin: %v", err) + } + if scheduler == nil { + t.Fatal("Expected scheduler, got nil") + } + + // Test basic operations + if scheduler.GetPoolCount() != 0 { + t.Errorf("Expected pool count 0, got %d", scheduler.GetPoolCount()) + } + }) + + t.Run("FunctionalTest", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000 * 1000, + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create gthulhu plugin: %v", err) + } + + // Create a mock Sched + mockSched := &testSched{ + tasks: []*models.QueuedTask{ + {Pid: 100, Weight: 100, Vtime: 1000, Tgid: 100}, + {Pid: 200, Weight: 100, Vtime: 2000, Tgid: 200}, + }, + } + + // Drain tasks + drained := scheduler.DrainQueuedTask(mockSched) + if drained != 2 { + t.Errorf("Expected to drain 2 tasks, got %d", drained) + } + + // Check pool count + if scheduler.GetPoolCount() != 2 { + t.Errorf("Expected pool count 2, got %d", scheduler.GetPoolCount()) + } + + // Select a task + task := scheduler.SelectQueuedTask(mockSched) + if task == nil { + t.Fatal("Expected task, got nil") + } + + // Check pool count decreased + if scheduler.GetPoolCount() != 1 { + t.Errorf("Expected pool count 1 after select, got %d", scheduler.GetPoolCount()) + } + }) +} + +// TestSimplePluginThroughFactory tests creating simple plugin via factory +func TestSimplePluginThroughFactory(t *testing.T) { + t.Run("SimpleWeightedVtime", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "simple", + SliceNsDefault: 5000 * 100, + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create simple plugin: %v", err) + } + if scheduler == nil { + t.Fatal("Expected scheduler, got nil") + } + + // Verify initial state + if scheduler.GetPoolCount() != 0 { + t.Errorf("Expected initial pool count 0, got %d", scheduler.GetPoolCount()) + } + }) + + t.Run("SimpleFIFO", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "simple-fifo", + SliceNsDefault: 5000 * 100, + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create simple-fifo plugin: %v", err) + } + if scheduler == nil { + t.Fatal("Expected scheduler, got nil") + } + + // Verify initial state + if scheduler.GetPoolCount() != 0 { + t.Errorf("Expected initial pool count 0, got %d", scheduler.GetPoolCount()) + } + }) + + t.Run("FunctionalTest", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "simple", + SliceNsDefault: 5000 * 100, + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create simple plugin: %v", err) + } + + // Create a mock Sched + mockSched := &testSched{ + tasks: []*models.QueuedTask{ + {Pid: 100, Weight: 100, Vtime: 5000, Tgid: 100}, + {Pid: 200, Weight: 100, Vtime: 3000, Tgid: 200}, + {Pid: 300, Weight: 100, Vtime: 7000, Tgid: 300}, + }, + } + + // Drain tasks + drained := scheduler.DrainQueuedTask(mockSched) + if drained != 3 { + t.Errorf("Expected to drain 3 tasks, got %d", drained) + } + + // Check pool count + if scheduler.GetPoolCount() != 3 { + t.Errorf("Expected pool count 3, got %d", scheduler.GetPoolCount()) + } + + // Select tasks - should be in vtime order + task1 := scheduler.SelectQueuedTask(mockSched) + if task1 == nil { + t.Fatal("Expected first task, got nil") + } + if task1.Pid != 200 { // Task with lowest vtime (3000) + t.Errorf("Expected first task PID 200, got %d", task1.Pid) + } + + task2 := scheduler.SelectQueuedTask(mockSched) + if task2 == nil { + t.Fatal("Expected second task, got nil") + } + + // Check pool count + if scheduler.GetPoolCount() != 1 { + t.Errorf("Expected pool count 1, got %d", scheduler.GetPoolCount()) + } + }) + + t.Run("FIFOFunctionalTest", func(t *testing.T) { + config := &plugin.SchedConfig{ + Mode: "simple-fifo", + } + + scheduler, err := plugin.NewSchedulerPlugin(config) + if err != nil { + t.Fatalf("Failed to create simple-fifo plugin: %v", err) + } + + // Create a mock Sched with tasks in specific order + mockSched := &testSched{ + tasks: []*models.QueuedTask{ + {Pid: 100, Weight: 100, Vtime: 7000, Tgid: 100}, + {Pid: 200, Weight: 100, Vtime: 3000, Tgid: 200}, + {Pid: 300, Weight: 100, Vtime: 5000, Tgid: 300}, + }, + } + + // Drain tasks + drained := scheduler.DrainQueuedTask(mockSched) + if drained != 3 { + t.Errorf("Expected to drain 3 tasks, got %d", drained) + } + + // Select tasks - should be in FIFO order (100, 200, 300) + task1 := scheduler.SelectQueuedTask(mockSched) + if task1 == nil { + t.Fatal("Expected first task, got nil") + } + if task1.Pid != 100 { // First enqueued + t.Errorf("Expected first task PID 100, got %d", task1.Pid) + } + + task2 := scheduler.SelectQueuedTask(mockSched) + if task2 == nil { + t.Fatal("Expected second task, got nil") + } + if task2.Pid != 200 { // Second enqueued + t.Errorf("Expected second task PID 200, got %d", task2.Pid) + } + + task3 := scheduler.SelectQueuedTask(mockSched) + if task3 == nil { + t.Fatal("Expected third task, got nil") + } + if task3.Pid != 300 { // Third enqueued + t.Errorf("Expected third task PID 300, got %d", task3.Pid) + } + }) +} + +// TestMultiplePluginInstances tests creating multiple plugin instances +func TestMultiplePluginInstances(t *testing.T) { + t.Run("MultipleGthulhuInstances", func(t *testing.T) { + config1 := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 5000 * 1000, + } + config2 := &plugin.SchedConfig{ + Mode: "gthulhu", + SliceNsDefault: 10000 * 1000, + } + + scheduler1, err := plugin.NewSchedulerPlugin(config1) + if err != nil { + t.Fatalf("Failed to create first plugin: %v", err) + } + + scheduler2, err := plugin.NewSchedulerPlugin(config2) + if err != nil { + t.Fatalf("Failed to create second plugin: %v", err) + } + + // Verify they are independent + mockSched := &testSched{ + tasks: []*models.QueuedTask{ + {Pid: 100, Weight: 100, Vtime: 1000, Tgid: 100}, + }, + } + + scheduler1.DrainQueuedTask(mockSched) + if scheduler1.GetPoolCount() != 1 { + t.Errorf("Scheduler1 pool count = %d; want 1", scheduler1.GetPoolCount()) + } + if scheduler2.GetPoolCount() != 0 { + t.Errorf("Scheduler2 pool count = %d; want 0", scheduler2.GetPoolCount()) + } + }) + + t.Run("MixedPluginTypes", func(t *testing.T) { + gthulhuConfig := &plugin.SchedConfig{Mode: "gthulhu"} + simpleConfig := &plugin.SchedConfig{Mode: "simple"} + fifoConfig := &plugin.SchedConfig{Mode: "simple-fifo"} + + gthulhu, err := plugin.NewSchedulerPlugin(gthulhuConfig) + if err != nil { + t.Fatalf("Failed to create gthulhu plugin: %v", err) + } + + simple, err := plugin.NewSchedulerPlugin(simpleConfig) + if err != nil { + t.Fatalf("Failed to create simple plugin: %v", err) + } + + fifo, err := plugin.NewSchedulerPlugin(fifoConfig) + if err != nil { + t.Fatalf("Failed to create simple-fifo plugin: %v", err) + } + + // Verify all are independent + if gthulhu == nil || simple == nil || fifo == nil { + t.Fatal("One or more plugins are nil") + } + + // All should start with empty pools + if gthulhu.GetPoolCount() != 0 { + t.Errorf("Gthulhu pool count = %d; want 0", gthulhu.GetPoolCount()) + } + if simple.GetPoolCount() != 0 { + t.Errorf("Simple pool count = %d; want 0", simple.GetPoolCount()) + } + if fifo.GetPoolCount() != 0 { + t.Errorf("FIFO pool count = %d; want 0", fifo.GetPoolCount()) + } + }) +} + +// TestRegisteredModesIntegration tests that all expected modes are registered +func TestRegisteredModesIntegration(t *testing.T) { + modes := plugin.GetRegisteredModes() + + expectedModes := []string{"gthulhu", "simple", "simple-fifo"} + modeMap := make(map[string]bool) + for _, mode := range modes { + modeMap[mode] = true + } + + for _, expected := range expectedModes { + if !modeMap[expected] { + t.Errorf("Expected mode '%s' to be registered, but it's not", expected) + } + } + + if len(modes) < 3 { + t.Errorf("Expected at least 3 registered modes, got %d: %v", len(modes), modes) + } +} + +// testSched is a mock implementation of plugin.Sched interface for testing +type testSched struct { + tasks []*models.QueuedTask + index int +} + +var _ plugin.Sched = (*testSched)(nil) + +func (s *testSched) DequeueTask(task *models.QueuedTask) { + if s.index >= len(s.tasks) { + task.Pid = -1 + return + } + *task = *s.tasks[s.index] + s.index++ +} + +func (s *testSched) DefaultSelectCPU(t *models.QueuedTask) (error, int32) { + return nil, 0 +}