From f8d5b56171c813b7e013a39ddea6f2d00da4d767 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Mon, 23 Jun 2025 00:24:35 -0700 Subject: [PATCH] add complex translations test to engine --- internal/simulation/engine.go | 75 ++++++++++++------- internal/simulation/engine_test.go | 34 +++++++++ .../simulation/testdata/complex_design.json | 1 + 3 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 internal/simulation/testdata/complex_design.json diff --git a/internal/simulation/engine.go b/internal/simulation/engine.go index e0b6ec4..edee4a7 100644 --- a/internal/simulation/engine.go +++ b/internal/simulation/engine.go @@ -60,10 +60,10 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "cache": simNode = &CacheNode{ ID: n.ID, - Label: n.Props["label"].(string), - CacheTTL: int(n.Props["cacheTTL"].(float64)), - MaxEntries: int(n.Props["maxEntries"].(float64)), - EvictionPolicy: n.Props["evictionPolicy"].(string), + Label: asString(n.Props["label"]), + CacheTTL: int(asFloat64(n.Props["cacheTTL"])), + MaxEntries: int(asFloat64(n.Props["maxEntries"])), + EvictionPolicy: asString(n.Props["evictionPolicy"]), CurrentLoad: 0, Queue: []*Request{}, Cache: make(map[string]CacheEntry), @@ -72,20 +72,20 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "database": simNode = &DatabaseNode{ ID: n.ID, - Label: n.Props["label"].(string), - Replication: int(n.Props["replication"].(float64)), + Label: asString(n.Props["label"]), + Replication: int(asFloat64(n.Props["replication"])), Queue: []*Request{}, Alive: true, } case "cdn": simNode = &CDNNode{ ID: n.ID, - Label: n.Props["label"].(string), - TTL: int(n.Props["ttl"].(float64)), - GeoReplication: n.Props["geoReplication"].(string), - CachingStrategy: n.Props["cachingStrategy"].(string), - Compression: n.Props["compression"].(string), - HTTP2: n.Props["http2"].(string), + Label: asString(n.Props["label"]), + TTL: int(asFloat64(n.Props["ttl"])), + GeoReplication: asString(n.Props["geoReplication"]), + CachingStrategy: asString(n.Props["cachingStrategy"]), + Compression: asString(n.Props["compression"]), + HTTP2: asString(n.Props["http2"]), Queue: []*Request{}, Alive: true, output: []*Request{}, @@ -94,9 +94,9 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "messageQueue": simNode = &MessageQueueNode{ ID: n.ID, - Label: n.Props["label"].(string), - QueueSize: int(n.Props["maxSize"].(float64)), - MessageTTL: int(n.Props["retentionSeconds"].(float64)), + Label: asString(n.Props["label"]), + QueueSize: int(asFloat64(n.Props["maxSize"])), + MessageTTL: int(asFloat64(n.Props["retentionSeconds"])), DeadLetter: false, Queue: []*Request{}, Alive: true, @@ -104,9 +104,9 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "microservice": simNode = &MicroserviceNode{ ID: n.ID, - Label: n.Props["label"].(string), - APIEndpoint: n.Props["apiVersion"].(string), - RateLimit: int(n.Props["rpsCapacity"].(float64)), + Label: asString(n.Props["label"]), + APIEndpoint: asString(n.Props["apiVersion"]), + RateLimit: int(asFloat64(n.Props["rpsCapacity"])), CircuitBreaker: true, Queue: []*Request{}, CircuitState: "closed", @@ -115,9 +115,9 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "third party service": simNode = &ThirdPartyServiceNode{ ID: n.ID, - Label: n.Props["label"].(string), - APIEndpoint: n.Props["provider"].(string), - RateLimit: int(n.Props["latency"].(float64)), + Label: asString(n.Props["label"]), + APIEndpoint: asString(n.Props["provider"]), + RateLimit: int(asFloat64(n.Props["latency"])), RetryPolicy: "exponential", Queue: []*Request{}, Alive: true, @@ -125,20 +125,20 @@ func NewEngineFromDesign(design design.Design, duration int, tickMs int) *Engine case "data pipeline": simNode = &DataPipelineNode{ ID: n.ID, - Label: n.Props["label"].(string), - BatchSize: int(n.Props["batchSize"].(float64)), - Transformation: n.Props["transformation"].(string), + Label: asString(n.Props["label"]), + BatchSize: int(asFloat64(n.Props["batchSize"])), + Transformation: asString(n.Props["transformation"]), Queue: []*Request{}, Alive: true, } case "monitoring/alerting": simNode = &MonitoringNode{ ID: n.ID, - Label: n.Props["label"].(string), - Tool: n.Props["tool"].(string), - AlertMetric: n.Props["metric"].(string), - ThresholdValue: int(n.Props["threshold"].(float64)), - ThresholdUnit: n.Props["unit"].(string), + Label: asString(n.Props["label"]), + Tool: asString(n.Props["tool"]), + AlertMetric: asString(n.Props["metric"]), + ThresholdValue: int(asFloat64(n.Props["threshold"])), + ThresholdUnit: asString(n.Props["unit"]), Queue: []*Request{}, Alive: true, } @@ -243,3 +243,20 @@ func shouldInject(tick int) bool { func generateRequestID(tick int) string { return fmt.Sprintf("req-%d-%d", tick, rand.Intn(1000)) } + +func asFloat64(v interface{}) float64 { + if v == nil { + return 0 + } + + return v.(float64) +} + +func asString(v interface{}) string { + s, ok := v.(string) + if !ok { + return "" + } + + return s +} diff --git a/internal/simulation/engine_test.go b/internal/simulation/engine_test.go index 987679a..1f11cb9 100644 --- a/internal/simulation/engine_test.go +++ b/internal/simulation/engine_test.go @@ -1,8 +1,11 @@ package simulation import ( + "os" + "path/filepath" "testing" + "encoding/json" "systemdesigngame/internal/design" ) @@ -53,3 +56,34 @@ func TestNewEngineFromDesign(t *testing.T) { } } +func TestComplexSimulationRun(t *testing.T) { + filePath := filepath.Join("testdata", "complex_design.json") + data, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("Failed to read JSON file: %v", err) + } + + var d design.Design + if err := json.Unmarshal([]byte(data), &d); err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + + engine := NewEngineFromDesign(d, 10, 100) + if engine == nil { + t.Fatal("Engine should not be nil") + } + + engine.Run() + + if len(engine.Timeline) == 0 { + t.Fatal("Expected timeline snapshots after Run, got none") + } + + // Optional: check that some nodes received or emitted requests + for id, node := range engine.Nodes { + if len(node.Emit()) > 0 { + t.Logf("Node %s has activity", id) + } + } +} + diff --git a/internal/simulation/testdata/complex_design.json b/internal/simulation/testdata/complex_design.json new file mode 100644 index 0000000..fd0c1ec --- /dev/null +++ b/internal/simulation/testdata/complex_design.json @@ -0,0 +1 @@ +{"nodes":[{"id":"node-1","type":"loadBalancer","position":{"x":-4,"y":0},"props":{"label":"Load Balancer","algorithm":"round-robin"}},{"id":"node-2","type":"webserver","position":{"x":-1,"y":0},"props":{"label":"Web Server","instanceSize":"medium"}},{"id":"node-3","type":"database","position":{"x":177,"y":-176},"props":{"label":"Database","replication":1}},{"id":"node-4","type":"cache","position":{"x":204,"y":-78},"props":{"label":"Cache","cacheTTL":60,"maxEntries":100000,"evictionPolicy":"LRU"}},{"id":"node-5","type":"messageQueue","position":{"x":0,"y":0},"props":{"label":"MQ","maxSize":10000,"retentionSeconds":600}},{"id":"node-6","type":"cdn","position":{"x":20,"y":69},"props":{"label":"CDN","ttl":3600,"geoReplication":"global","cachingStrategy":"cache-first","compression":"brotli","http2":"enabled"}},{"id":"node-7","type":"microservice","position":{"x":0,"y":0},"props":{"label":"Service","instanceCount":3,"instanceSize":"medium","scalingStrategy":"auto","apiVersion":"v1"}},{"id":"node-8","type":"data pipeline","position":{"x":-453,"y":-121},"props":{"label":"pipeline","batchSize":500,"transformation":"map"}},{"id":"node-9","type":"monitoring/alerting","position":{"x":0,"y":0},"props":{"label":"monitor","tool":"Prometheus","alertThreshold":80}},{"id":"node-10","type":"third party service","position":{"x":0,"y":0},"props":{"label":"third party service","provider":"Stripe","latency":200}}],"connections":[{"source":"node-0","target":"node-1","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-1","target":"node-2","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-2","target":"node-3","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-3","target":"node-4","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-4","target":"node-5","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-5","target":"node-6","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-6","target":"node-7","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-8","target":"node-7","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-8","target":"node-10","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-10","target":"node-9","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000},{"source":"node-9","target":"node-0","label":"Read traffic","direction":"forward","protocol":"HTTP","tls":false,"capacity":1000}]}