|
|
@@ -1,6 +1,7 @@
|
|
|
package customcost
|
|
|
|
|
|
import (
|
|
|
+ "fmt"
|
|
|
"os/exec"
|
|
|
"runtime"
|
|
|
"sync"
|
|
|
@@ -8,6 +9,7 @@ import (
|
|
|
"time"
|
|
|
|
|
|
"github.com/hashicorp/go-plugin"
|
|
|
+ "github.com/opencost/opencost/core/pkg/opencost"
|
|
|
)
|
|
|
|
|
|
func TestIngestor_Stop_KillsPluginProcesses(t *testing.T) {
|
|
|
@@ -25,7 +27,7 @@ func TestIngestor_Stop_KillsPluginProcesses(t *testing.T) {
|
|
|
_, _ = client.Client()
|
|
|
|
|
|
ingestor := &CustomCostIngestor{
|
|
|
- plugins: map[string]*plugin.Client{
|
|
|
+ plugins: map[string]pluginConnector{
|
|
|
"test-plugin": client,
|
|
|
},
|
|
|
}
|
|
|
@@ -37,6 +39,7 @@ func TestIngestor_Stop_KillsPluginProcesses(t *testing.T) {
|
|
|
}
|
|
|
|
|
|
func TestIngestor_Stop_MultiplePlugins(t *testing.T) {
|
|
|
+ connectors := make(map[string]pluginConnector)
|
|
|
clients := make(map[string]*plugin.Client)
|
|
|
for _, name := range []string{"plugin-a", "plugin-b", "plugin-c"} {
|
|
|
cmd := exec.Command("sleep", "60")
|
|
|
@@ -50,10 +53,11 @@ func TestIngestor_Stop_MultiplePlugins(t *testing.T) {
|
|
|
StartTimeout: 2 * time.Second,
|
|
|
})
|
|
|
_, _ = client.Client()
|
|
|
+ connectors[name] = client
|
|
|
clients[name] = client
|
|
|
}
|
|
|
|
|
|
- ingestor := &CustomCostIngestor{plugins: clients}
|
|
|
+ ingestor := &CustomCostIngestor{plugins: connectors}
|
|
|
ingestor.Stop()
|
|
|
|
|
|
for name, client := range clients {
|
|
|
@@ -65,7 +69,7 @@ func TestIngestor_Stop_MultiplePlugins(t *testing.T) {
|
|
|
|
|
|
func TestIngestor_Stop_EmptyPluginsMap(t *testing.T) {
|
|
|
ingestor := &CustomCostIngestor{
|
|
|
- plugins: map[string]*plugin.Client{},
|
|
|
+ plugins: map[string]pluginConnector{},
|
|
|
}
|
|
|
ingestor.Stop() // covers lock path with 0 iterations
|
|
|
}
|
|
|
@@ -77,7 +81,7 @@ func TestIngestor_Stop_NilPluginsMap(t *testing.T) {
|
|
|
|
|
|
func TestIngestor_Stop_AlreadyStopping(t *testing.T) {
|
|
|
ingestor := &CustomCostIngestor{
|
|
|
- plugins: map[string]*plugin.Client{},
|
|
|
+ plugins: map[string]pluginConnector{},
|
|
|
}
|
|
|
ingestor.isStopping.Store(true) // atomic.Bool must use Store()!
|
|
|
ingestor.Stop() // should return immediately
|
|
|
@@ -85,7 +89,7 @@ func TestIngestor_Stop_AlreadyStopping(t *testing.T) {
|
|
|
|
|
|
func TestIngestor_Stop_ConcurrentCalls(t *testing.T) {
|
|
|
ingestor := &CustomCostIngestor{
|
|
|
- plugins: map[string]*plugin.Client{},
|
|
|
+ plugins: map[string]pluginConnector{},
|
|
|
}
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
@@ -187,3 +191,89 @@ func TestIngestor_BuildWindow_WithPlugin(t *testing.T) {
|
|
|
// BuildWindow and buildSingleDomain; client.Client() fails fast (false exits)
|
|
|
ingestor.BuildWindow(now.Add(-time.Hour), now)
|
|
|
}
|
|
|
+
|
|
|
+// mockClientProtocol implements plugin.ClientProtocol for testing.
|
|
|
+type mockClientProtocol struct {
|
|
|
+ dispenseResult interface{}
|
|
|
+ dispenseErr error
|
|
|
+}
|
|
|
+
|
|
|
+func (m *mockClientProtocol) Dispense(string) (interface{}, error) {
|
|
|
+ return m.dispenseResult, m.dispenseErr
|
|
|
+}
|
|
|
+func (m *mockClientProtocol) Ping() error { return nil }
|
|
|
+func (m *mockClientProtocol) Close() error { return nil }
|
|
|
+
|
|
|
+// mockPluginConnector implements pluginConnector for testing.
|
|
|
+type mockPluginConnector struct {
|
|
|
+ protocol plugin.ClientProtocol
|
|
|
+ clientErr error
|
|
|
+ killed bool
|
|
|
+}
|
|
|
+
|
|
|
+func (m *mockPluginConnector) Client() (plugin.ClientProtocol, error) {
|
|
|
+ if m.clientErr != nil {
|
|
|
+ return nil, m.clientErr
|
|
|
+ }
|
|
|
+ return m.protocol, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *mockPluginConnector) Kill() { m.killed = true }
|
|
|
+
|
|
|
+func TestBuildSingleDomain_InvalidPluginType_NoPanic(t *testing.T) {
|
|
|
+ mock := &mockPluginConnector{
|
|
|
+ protocol: &mockClientProtocol{
|
|
|
+ dispenseResult: "not a CustomCostSource", // wrong type
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ repo := NewMemoryRepository()
|
|
|
+ ingestor := &CustomCostIngestor{
|
|
|
+ plugins: map[string]pluginConnector{"bad-plugin": mock},
|
|
|
+ resolution: time.Hour,
|
|
|
+ repo: repo,
|
|
|
+ coverage: map[string]opencost.Window{},
|
|
|
+ }
|
|
|
+
|
|
|
+ now := time.Now().UTC()
|
|
|
+ // Before the fix this would panic; now it should log an error and return.
|
|
|
+ ingestor.BuildWindow(now.Add(-time.Hour), now)
|
|
|
+}
|
|
|
+
|
|
|
+func TestBuildSingleDomain_DispenseError(t *testing.T) {
|
|
|
+ mock := &mockPluginConnector{
|
|
|
+ protocol: &mockClientProtocol{
|
|
|
+ dispenseErr: fmt.Errorf("dispense failed"),
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ repo := NewMemoryRepository()
|
|
|
+ ingestor := &CustomCostIngestor{
|
|
|
+ plugins: map[string]pluginConnector{"err-plugin": mock},
|
|
|
+ resolution: time.Hour,
|
|
|
+ repo: repo,
|
|
|
+ coverage: map[string]opencost.Window{},
|
|
|
+ }
|
|
|
+
|
|
|
+ now := time.Now().UTC()
|
|
|
+ // Should handle the error gracefully without panic.
|
|
|
+ ingestor.BuildWindow(now.Add(-time.Hour), now)
|
|
|
+}
|
|
|
+
|
|
|
+func TestBuildSingleDomain_ClientError(t *testing.T) {
|
|
|
+ mock := &mockPluginConnector{
|
|
|
+ clientErr: fmt.Errorf("connection failed"),
|
|
|
+ }
|
|
|
+
|
|
|
+ repo := NewMemoryRepository()
|
|
|
+ ingestor := &CustomCostIngestor{
|
|
|
+ plugins: map[string]pluginConnector{"fail-plugin": mock},
|
|
|
+ resolution: time.Hour,
|
|
|
+ repo: repo,
|
|
|
+ coverage: map[string]opencost.Window{},
|
|
|
+ }
|
|
|
+
|
|
|
+ now := time.Now().UTC()
|
|
|
+ // Should handle the error gracefully without panic.
|
|
|
+ ingestor.BuildWindow(now.Add(-time.Hour), now)
|
|
|
+}
|