/* * Copyright 2025 The RuleGo Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package streamsql // package streamsql 的白盒测试与基准(覆盖/性能/溢出策略/表格打印 + 端到端示例)。 // 访问非导出字段(performanceMode/customConfig/stream/fieldOrder)与非导出方法 // (printTableFormat),故必须在 package streamsql 内,不能迁 test/e2e。 // 纯公开 API 的集成测试见 test/e2e。 import ( "context" "fmt" "math/rand" "strings" "sync" "testing" "time" "github.com/rulego/streamsql/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ---------- coverage ---------- // TestStreamSQLPerformanceModesExtended 测试不同性能模式的配置 func TestStreamSQLPerformanceModesExtended(t *testing.T) { t.Run("default performance mode", func(t *testing.T) { ssql := New() assert.Equal(t, "default", ssql.performanceMode) assert.Nil(t, ssql.customConfig) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) assert.NotNil(t, ssql.stream) ssql.Stop() }) t.Run("high performance mode", func(t *testing.T) { ssql := New(WithHighPerformance()) assert.Equal(t, "high_performance", ssql.performanceMode) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) assert.NotNil(t, ssql.stream) ssql.Stop() }) t.Run("low latency mode", func(t *testing.T) { ssql := New(WithLowLatency()) assert.Equal(t, "low_latency", ssql.performanceMode) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) assert.NotNil(t, ssql.stream) ssql.Stop() }) t.Run("custom performance mode", func(t *testing.T) { customConfig := types.DefaultPerformanceConfig() customConfig.BufferConfig.DataChannelSize = 2000 ssql := New(WithCustomPerformance(customConfig)) assert.Equal(t, "custom", ssql.performanceMode) assert.NotNil(t, ssql.customConfig) assert.Equal(t, 2000, ssql.customConfig.BufferConfig.DataChannelSize) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) assert.NotNil(t, ssql.stream) ssql.Stop() }) t.Run("custom mode with nil config", func(t *testing.T) { ssql := New() ssql.performanceMode = "custom" ssql.customConfig = nil err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) assert.NotNil(t, ssql.stream) ssql.Stop() }) } // TestStreamSQLFieldOrder 测试字段顺序保持功能 func TestStreamSQLFieldOrder(t *testing.T) { t.Run("field order preservation", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT name, id, value FROM stream") require.NoError(t, err) // 验证字段顺序被正确保存 expectedOrder := []string{"name", "id", "value"} assert.Equal(t, expectedOrder, ssql.fieldOrder) ssql.Stop() }) t.Run("field order with aliases", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT name as device_name, id as device_id FROM stream") require.NoError(t, err) // 验证别名字段顺序 expectedOrder := []string{"device_name", "device_id"} assert.Equal(t, expectedOrder, ssql.fieldOrder) ssql.Stop() }) } // TestStreamSQLPrintTableFormat 测试表格打印功能 func TestStreamSQLPrintTableFormat(t *testing.T) { t.Run("print table format with data", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id, name FROM stream") require.NoError(t, err) // 测试 printTableFormat 方法 testResults := []map[string]any{ {"id": 1, "name": "test1"}, {"id": 2, "name": "test2"}, } // 这个方法主要是打印输出,我们确保它不会panic assert.NotPanics(t, func() { ssql.printTableFormat(testResults) }) ssql.Stop() }) t.Run("print table format with empty data", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) // 测试空数据 emptyResults := []map[string]any{} assert.NotPanics(t, func() { ssql.printTableFormat(emptyResults) }) ssql.Stop() }) t.Run("print table format with nil field order", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) // 清空字段顺序 ssql.fieldOrder = nil testResults := []map[string]any{ {"id": 1}, } assert.NotPanics(t, func() { ssql.printTableFormat(testResults) }) ssql.Stop() }) } // TestStreamSQLToChannel 测试通道功能 func TestStreamSQLToChannel(t *testing.T) { t.Run("to channel with aggregation query", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT COUNT(*) FROM stream GROUP BY TumblingWindow('1s')") require.NoError(t, err) // 获取结果通道 resultChan := ssql.ToChannel() assert.NotNil(t, resultChan) // 启动goroutine接收结果 var wg sync.WaitGroup wg.Add(1) var receivedResults [][]map[string]any go func() { defer wg.Done() timeout := time.After(3 * time.Second) for { select { case result := <-resultChan: if result != nil { receivedResults = append(receivedResults, result) return } case <-timeout: return } } }() // 发送一些数据 for i := 0; i < 5; i++ { ssql.Emit(map[string]any{"id": i}) } // 等待结果 wg.Wait() ssql.Stop() // 验证至少收到了一些结果 assert.GreaterOrEqual(t, len(receivedResults), 0) }) t.Run("to channel with non-aggregation query", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) resultChan := ssql.ToChannel() assert.NotNil(t, resultChan) ssql.Stop() }) } // TestStreamSQLMultipleOptions 测试多个配置选项组合 func TestStreamSQLMultipleOptions(t *testing.T) { t.Run("multiple options combination", func(t *testing.T) { // 组合多个配置选项 ssql := New( WithHighPerformance(), WithDiscardLog(), ) assert.Equal(t, "high_performance", ssql.performanceMode) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) ssql.Stop() }) t.Run("override performance mode", func(t *testing.T) { // 后面的选项应该覆盖前面的 ssql := New( WithHighPerformance(), WithLowLatency(), ) assert.Equal(t, "low_latency", ssql.performanceMode) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) ssql.Stop() }) } // TestStreamSQLExecuteErrorHandling 测试Execute方法的错误处理 func TestStreamSQLExecuteErrorHandling(t *testing.T) { t.Run("stream creation failure simulation", func(t *testing.T) { ssql := New() // 使用一个可能导致stream创建失败的SQL err := ssql.Execute("SELECT invalid_function() FROM test_stream") require.NotNil(t, err) require.Contains(t, err.Error(), "function") }) t.Run("filter registration failure", func(t *testing.T) { ssql := New() defer ssql.Stop() // 使用可能导致过滤器注册失败的SQL err := ssql.Execute("SELECT id FROM stream WHERE INVALID_CONDITION") if err != nil { // 如果有错误,应该包含相关信息 assert.True(t, strings.Contains(err.Error(), "SQL parsing failed") || strings.Contains(err.Error(), "failed to register filter condition") || strings.Contains(err.Error(), "failed to create stream processor")) } }) } // TestStreamSQLConcurrentAccess 测试并发访问安全性 func TestStreamSQLConcurrentAccess(t *testing.T) { t.Run("concurrent emit and stop", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) var wg sync.WaitGroup numWorkers := 10 // 启动多个goroutine并发发送数据 for i := 0; i < numWorkers; i++ { wg.Add(1) go func(workerID int) { defer wg.Done() for j := 0; j < 100; j++ { ssql.Emit(map[string]any{"id": workerID*100 + j}) } }(i) } // 等待一段时间后停止 time.Sleep(100 * time.Millisecond) ssql.Stop() wg.Wait() }) t.Run("concurrent method calls", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) var wg sync.WaitGroup numWorkers := 5 // 并发调用各种方法 for i := 0; i < numWorkers; i++ { wg.Add(1) go func() { defer wg.Done() // 这些方法调用应该是安全的 _ = ssql.GetStats() _ = ssql.GetDetailedStats() _ = ssql.IsAggregationQuery() _ = ssql.Stream() _ = ssql.ToChannel() ssql.AddSink(func(results []map[string]any) {}) }() } wg.Wait() ssql.Stop() }) } // TestStreamSQLEdgeCasesAdditional 测试额外的边界情况 func TestStreamSQLEdgeCasesAdditional(t *testing.T) { t.Run("execute with different performance modes after creation", func(t *testing.T) { ssql := New() // 先用默认模式执行 err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) ssql.Stop() // 改变性能模式后再次执行应该失败,因为已经执行过了 ssql.performanceMode = "high_performance" err = ssql.Execute("SELECT name FROM stream") require.Error(t, err) require.Contains(t, err.Error(), "Execute() has already been called") // 不需要再次调用Stop(),因为第二次Execute失败了 }) t.Run("field order with complex query", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT COUNT(*) as cnt, AVG(value) as avg_val, deviceId FROM stream GROUP BY deviceId") require.NoError(t, err) // 验证复杂查询的字段顺序 expectedOrder := []string{"cnt", "avg_val", "deviceId"} assert.Equal(t, expectedOrder, ssql.fieldOrder) ssql.Stop() }) t.Run("print table with field order", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT name, id, value FROM stream") require.NoError(t, err) // 设置字段顺序 ssql.fieldOrder = []string{"name", "id", "value"} // 测试PrintTable方法 assert.NotPanics(t, func() { ssql.PrintTable() }) ssql.Stop() }) } // TestStreamSQLEmitSync 测试EmitSync方法的各种情况 func TestStreamSQLEmitSync(t *testing.T) { t.Run("emit sync with uninitialized stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用EmitSync result, err := ssql.EmitSync(map[string]any{"id": 1}) require.Error(t, err) require.Nil(t, result) require.Contains(t, err.Error(), "stream not initialized") }) t.Run("emit sync with aggregation query", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT COUNT(*) FROM stream GROUP BY id") require.NoError(t, err) // 对聚合查询调用EmitSync应该返回错误 result, err := ssql.EmitSync(map[string]any{"id": 1}) require.Error(t, err) require.Nil(t, result) require.Contains(t, err.Error(), "synchronous mode only supports non-aggregation queries") ssql.Stop() }) t.Run("emit sync with non-aggregation query", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id, name FROM stream WHERE id > 0") require.NoError(t, err) // 对非聚合查询调用EmitSync data := map[string]any{"id": 1, "name": "test"} result, err := ssql.EmitSync(data) // 根据实际实现,这里可能成功或失败 if err != nil { t.Logf("EmitSync error (expected): %v", err) } else { t.Logf("EmitSync result: %v", result) } ssql.Stop() }) } // TestStreamSQLCustomPerformanceConfig 测试自定义性能配置 func TestStreamSQLCustomPerformanceConfig(t *testing.T) { t.Run("custom performance config with nil config", func(t *testing.T) { ssql := New() ssql.performanceMode = "custom" ssql.customConfig = nil // 设置为nil // 执行SQL时应该回退到默认配置 err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) ssql.Stop() }) t.Run("custom performance config with valid config", func(t *testing.T) { customConfig := types.PerformanceConfig{ BufferConfig: types.BufferConfig{ DataChannelSize: 1000, ResultChannelSize: 100, WindowOutputSize: 50, }, WorkerConfig: types.WorkerConfig{ SinkPoolSize: 4, SinkWorkerCount: 2, }, } ssql := New(WithCustomPerformance(customConfig)) err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) require.Equal(t, "custom", ssql.performanceMode) require.Equal(t, &customConfig, ssql.customConfig) ssql.Stop() }) } // TestStreamSQLStatsMethods 测试统计信息相关方法 func TestStreamSQLStatsMethods(t *testing.T) { t.Run("get stats with uninitialized stream", func(t *testing.T) { ssql := New() stats := ssql.GetStats() require.NotNil(t, stats) require.Equal(t, 0, len(stats)) }) t.Run("get detailed stats with uninitialized stream", func(t *testing.T) { ssql := New() detailedStats := ssql.GetDetailedStats() require.NotNil(t, detailedStats) require.Equal(t, 0, len(detailedStats)) }) t.Run("get stats with initialized stream", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) stats := ssql.GetStats() require.NotNil(t, stats) detailedStats := ssql.GetDetailedStats() require.NotNil(t, detailedStats) ssql.Stop() }) t.Run("is aggregation query method", func(t *testing.T) { // 测试未初始化的情况 ssql := New() require.False(t, ssql.IsAggregationQuery()) // 测试非聚合查询 err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) isAgg := ssql.IsAggregationQuery() t.Logf("Is aggregation query: %v", isAgg) ssql.Stop() // 测试聚合查询 ssql2 := New() err = ssql2.Execute("SELECT COUNT(*) FROM stream GROUP BY id") require.NoError(t, err) isAgg2 := ssql2.IsAggregationQuery() t.Logf("Is aggregation query (with GROUP BY): %v", isAgg2) ssql2.Stop() }) } // TestStreamSQLNilAndEdgeCases 测试空值和边界情况 func TestStreamSQLNilAndEdgeCases(t *testing.T) { t.Run("emit with nil stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用Emit assert.NotPanics(t, func() { ssql.Emit(map[string]any{"id": 1}) }) }) t.Run("add sink with nil stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用AddSink assert.NotPanics(t, func() { ssql.AddSink(func(results []map[string]any) { t.Log("Sink called") }) }) }) t.Run("to channel with nil stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用ToChannel resultChan := ssql.ToChannel() require.Nil(t, resultChan) }) t.Run("stream method with nil stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用Stream stream := ssql.Stream() require.Nil(t, stream) }) t.Run("stop with nil stream", func(t *testing.T) { ssql := New() // 在没有执行SQL的情况下调用Stop assert.NotPanics(t, func() { ssql.Stop() }) }) t.Run("print table format with empty results", func(t *testing.T) { ssql := New() ssql.fieldOrder = []string{"id", "name"} // 测试空结果的表格打印 assert.NotPanics(t, func() { ssql.printTableFormat([]map[string]any{}) }) }) t.Run("print table format with nil field order", func(t *testing.T) { ssql := New() ssql.fieldOrder = nil results := []map[string]any{ {"id": 1, "name": "test"}, } // 测试nil字段顺序的表格打印 assert.NotPanics(t, func() { ssql.printTableFormat(results) }) }) } // TestStreamSQLComplexScenarios 测试复杂场景 func TestStreamSQLComplexScenarios(t *testing.T) { t.Run("multiple execute calls", func(t *testing.T) { ssql := New() // 第一次执行 err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) ssql.Stop() // 第二次执行应该失败,因为已经执行过了 err = ssql.Execute("SELECT name FROM stream") require.Error(t, err) require.Contains(t, err.Error(), "Execute() has already been called") }) t.Run("performance mode switching", func(t *testing.T) { // 测试所有性能模式 modes := []string{"default", "high_performance", "low_latency", "zero_data_loss"} for _, mode := range modes { t.Run(fmt.Sprintf("mode_%s", mode), func(t *testing.T) { ssql := New() ssql.performanceMode = mode err := ssql.Execute("SELECT id FROM stream") require.NoError(t, err) require.Equal(t, mode, ssql.performanceMode) ssql.Stop() }) } }) t.Run("field order preservation", func(t *testing.T) { ssql := New() err := ssql.Execute("SELECT z, a, m, b FROM stream") require.NoError(t, err) // 验证字段顺序被正确保存 expectedOrder := []string{"z", "a", "m", "b"} require.Equal(t, expectedOrder, ssql.fieldOrder) ssql.Stop() }) } // ---------- perf benchmarks ---------- // Integration benchmarks exercising the full main path with realistic RSQL. // EmitSync processes each row synchronously end-to-end (the same path users // call), so ns/op is the true per-row latency through ProcessData -> field // evaluation -> result building. Aggregation queries are exercised separately // via the Emit-based benchmarks. func benchEmitSync(b *testing.B, sql string, row map[string]any) { b.Helper() ssql := New() defer ssql.Stop() if err := ssql.Execute(sql); err != nil { b.Fatalf("Execute: %v", err) } // Warm up compile/preprocess caches (do not measure). if _, err := ssql.EmitSync(row); err != nil { b.Fatalf("warmup EmitSync: %v", err) } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := ssql.EmitSync(row); err != nil { b.Fatalf("EmitSync: %v", err) } } b.StopTimer() } func BenchmarkMainPath_FilterProject(b *testing.B) { benchEmitSync(b, "SELECT deviceId, temperature FROM stream WHERE temperature > 20", map[string]any{"deviceId": "d1", "temperature": 25.5, "humidity": 60.0}, ) } func BenchmarkMainPath_MultiFieldFilter(b *testing.B) { benchEmitSync(b, "SELECT deviceId, temperature, humidity FROM stream WHERE temperature > 20 AND humidity < 80", map[string]any{"deviceId": "d1", "temperature": 25.5, "humidity": 60.0}, ) } func BenchmarkMainPath_ComputedFields(b *testing.B) { benchEmitSync(b, "SELECT deviceId, temperature * 2 + humidity AS score, abs(temperature - 100) AS dev FROM stream WHERE temperature > 20", map[string]any{"deviceId": "d1", "temperature": 25.5, "humidity": 60.0}, ) } func BenchmarkMainPath_StringConcat(b *testing.B) { benchEmitSync(b, "SELECT deviceId + '-' + location AS id FROM stream", map[string]any{"deviceId": "d1", "location": "roomA"}, ) } func BenchmarkMainPath_NoFilter(b *testing.B) { benchEmitSync(b, "SELECT deviceId, temperature, humidity FROM stream", map[string]any{"deviceId": "d1", "temperature": 25.5, "humidity": 60.0}, ) } // ---------- overflow strategy ---------- // TestSQLIntegration_StrategyBlock 测试 SQL 集成下的阻塞策略 func TestSQLIntegration_StrategyBlock(t *testing.T) { // 配置:输出缓冲为 1,阻塞策略,超时 100ms ssql := New(WithCustomPerformance(types.PerformanceConfig{ BufferConfig: types.BufferConfig{ DataChannelSize: 100, ResultChannelSize: 100, WindowOutputSize: 1, }, OverflowConfig: types.OverflowConfig{ Strategy: types.OverflowStrategyBlock, BlockTimeout: 100 * time.Millisecond, AllowDataLoss: true, }, WorkerConfig: types.WorkerConfig{ SinkPoolSize: 0, // 无缓冲任务队列 SinkWorkerCount: 1, // 1个 worker }, })) defer ssql.Stop() // SQL: 每条数据触发一次窗口 rsql := "SELECT deviceId FROM stream GROUP BY deviceId, CountingWindow(1)" err := ssql.Execute(rsql) require.NoError(t, err) // 添加同步 Sink 阻塞 Stream 处理,从而反压 Window // 注意:必须在 Execute 之后添加,因为 Execute 才会创建 stream ssql.AddSyncSink(func(results []map[string]any) { time.Sleep(500 * time.Millisecond) }) // 发送 5 条数据 // d1: Worker 处理中 (阻塞 500ms) // d2: Stream 尝试写入 WorkerPool -> 阻塞 (无缓冲) // d3: Window OutputChan (size 1) -> 填满 // d4: Window OutputChan 满 -> 尝试写入 -> 阻塞 (Window Add) -> 放入 TriggerChan (size=1) // d5: Window Add -> TriggerChan 满 -> 阻塞? No, Emit 是异步的? // Emit 往 dataChan 写. DataProcessor 读 dataChan -> Window.Add. // Window.Add 往 triggerChan 写. // // 修正分析: // Window.Add 是非阻塞的 (如果 triggerChan 不满). // CountingWindow triggerChan size = bufferSize = 1. // Worker 协程: 从 triggerChan 读 -> 处理 -> sendResult (到 OutputChan). // // d1: Worker读triggerChan -> OutputChan -> Stream -> WorkerPool -> Worker(busy). // d2: Worker读triggerChan -> OutputChan -> Stream -> Blocked on WorkerPool. // 此时 Stream 持有 d2. OutputChan 空. // Worker 协程 阻塞在 sendResult(d2)? No, Stream 取走了 d2, Stream 阻塞在 dispatch. // 所以 OutputChan 是空的! // Wait, Stream loop: // result := <-OutputChan. (Stream has d2). // handleResult(d2) -> Blocked. // So OutputChan is empty. // d3: Worker读triggerChan -> OutputChan (d3). Success. // OutputChan has d3. // d4: Worker读triggerChan -> OutputChan (d4). Blocked (OutputChan full). // Worker 协程 阻塞在 sendResult(d4). // d5: Add -> triggerChan (d5). Success (triggerChan size 1). // d6: Add -> triggerChan (d6). Blocked (triggerChan full). // Add blocks. DataProcessor blocks. Emit succeeds (dataChan). // // 所以 Window Worker 只有在 sendResult 阻塞时才触发 Drop logic. // sendResult 只有在 OutputChan 满且超时时才 Drop. // // d4 阻塞在 sendResult. // 100ms 后超时 -> Drop d4. // Worker 继续. // // 所以 d4 应该是被 Drop 的那个. // Sent: d1, d2, d3. (d5 在 triggerChan, d6 在 dataChan). // Wait, d5 is in triggerChan, not processed yet. // So Sent = 3. Dropped = 1 (d4). for _, id := range []string{"d1", "d2", "d3", "d4", "d5"} { ssql.Emit(map[string]any{"deviceId": id}) time.Sleep(10 * time.Millisecond) } // 等待足够长的时间让 Stream 醒来并处理完,以及 Window 丢弃逻辑执行 time.Sleep(1000 * time.Millisecond) // 获取统计信息 // d1: Stream 处理完 // d2: Stream 处理完 (Worker 醒来后处理 d2) // d3: Dropped (Worker 阻塞 -> 超时) // d4: Dropped (Worker 阻塞 -> 超时) // d5: Dropped (Worker 阻塞 -> 超时) // Total Sent: 2 (d1, d2). // Dropped: 3 (d3, d4, d5). stats := ssql.stream.GetStats() assert.Equal(t, int64(3), stats["droppedCount"], "Should have 3 dropped window result due to overflow") assert.Equal(t, int64(2), stats["sentCount"], "Should have 2 sent window result") } // TestSQLIntegration_StrategyDrop 测试 SQL 集成下的丢弃策略 func TestSQLIntegration_StrategyDrop(t *testing.T) { // 配置:输出缓冲为 1,丢弃策略 ssql := New(WithCustomPerformance(types.PerformanceConfig{ BufferConfig: types.BufferConfig{ DataChannelSize: 100, ResultChannelSize: 100, WindowOutputSize: 1, }, OverflowConfig: types.OverflowConfig{ Strategy: types.OverflowStrategyDrop, }, })) defer ssql.Stop() // SQL: 每条数据触发一次窗口 rsql := "SELECT deviceId FROM stream GROUP BY deviceId, CountingWindow(1)" err := ssql.Execute(rsql) require.NoError(t, err) // 连续发送 3 条数据 ssql.Emit(map[string]any{"deviceId": "d1"}) ssql.Emit(map[string]any{"deviceId": "d2"}) ssql.Emit(map[string]any{"deviceId": "d3"}) // 等待处理完成 time.Sleep(200 * time.Millisecond) // 对于 StrategyDrop,它会挤掉旧数据,所以 sentCount 应该持续增加 stats := ssql.stream.GetStats() // d1, d2, d3 都会成功发送(虽然 d1, d2 可能被挤掉,但 sendResult 逻辑中挤掉旧的后写入新的算发送成功) assert.Equal(t, int64(3), stats["sentCount"]) // 验证最终留在缓冲区的是最后一条数据 (d3) // 注意:AddSink 会启动 worker 从 OutputChan 读。 // 为了验证,我们直接从 Window 的 OutputChan 读 select { case result := <-ssql.stream.Window.OutputChan(): assert.Equal(t, "d3", result[0].Data.(map[string]any)["deviceId"]) case <-time.After(100 * time.Millisecond): // 如果已经被 AddSink 的 worker 读走了也正常,但由于我们没加 Sink,所以应该在里面 } } // ---------- table print ---------- // TestPrintTable 测试PrintTable方法的基本功能 func TestPrintTable(t *testing.T) { // 创建StreamSQL实例并测试PrintTable ssql := New() defer ssql.Stop() err := ssql.Execute("SELECT device, AVG(temperature) as avg_temp FROM stream GROUP BY device, TumblingWindow('2s')") assert.NoError(t, err) // 使用PrintTable方法(不验证输出内容,只确保不会panic) assert.NotPanics(t, func() { ssql.PrintTable() }, "PrintTable方法不应该panic") // 发送测试数据 testData := []map[string]any{ {"device": "sensor1", "temperature": 25.0}, {"device": "sensor2", "temperature": 30.0}, } for _, data := range testData { ssql.Emit(data) } // 等待窗口触发 time.Sleep(3 * time.Second) } // TestPrintTableFormat 测试printTableFormat方法处理不同数据类型 func TestPrintTableFormat(t *testing.T) { ssql := New() // 测试不同类型的数据,确保不会panic assert.NotPanics(t, func() { // 测试空切片 ssql.printTableFormat([]map[string]any{}) }, "空切片不应该panic") } // ---------- end-to-end example ---------- func TestStreamData(t *testing.T) { // 步骤1: 创建 StreamSQL 实例 // StreamSQL 是流式 SQL 处理引擎的核心组件,负责管理整个流处理生命周期 ssql := New() // 确保测试结束时停止流处理,释放资源 defer ssql.Stop() // 步骤2: 定义流式 SQL 查询语句 // 这个 SQL 语句展示了 StreamSQL 的核心功能: // - SELECT: 选择要输出的字段和聚合函数 // - FROM stream: 指定数据源为流数据 // - WHERE: 过滤条件,排除 device3 的数据 // - GROUP BY: 按设备ID分组,配合滚动窗口进行聚合 // - TumblingWindow('5s'): 5秒滚动窗口,每5秒触发一次计算 // - avg(), min(): 聚合函数,计算平均值和最小值 // - window_start(), window_end(): 窗口函数,获取窗口的开始和结束时间 rsql := "SELECT deviceId,avg(temperature) as avg_temp,min(humidity) as min_humidity ," + "window_start() as start,window_end() as end FROM stream where deviceId!='device3' group by deviceId,TumblingWindow('5s')" // 步骤3: 执行 SQL 语句,启动流式分析任务 // Execute 方法会解析 SQL、构建执行计划、初始化窗口管理器和聚合器 err := ssql.Execute(rsql) if err != nil { panic(err) } // 步骤4: 设置测试环境和并发控制 var wg sync.WaitGroup wg.Add(1) // 设置30秒测试超时时间,防止测试无限运行 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 步骤5: 启动数据生产者协程 // 模拟实时数据流,持续向 StreamSQL 输入数据 go func() { defer wg.Done() // 创建定时器,每秒触发一次数据生成 ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: // 每秒生成10条随机测试数据,模拟高频数据流 // 这种数据密度可以测试 StreamSQL 的实时处理能力 for i := 0; i < 10; i++ { // 构造设备数据,包含设备ID、温度和湿度 randomData := map[string]any{ "deviceId": fmt.Sprintf("device%d", rand.Intn(3)+1), // 随机选择 device1, device2, device3 "temperature": 20.0 + rand.Float64()*10, // 温度范围: 20-30度 "humidity": 50.0 + rand.Float64()*20, // 湿度范围: 50-70% } // 将数据添加到流中,触发 StreamSQL 的实时处理 // Emit 会将数据分发到相应的窗口和聚合器中 ssql.Emit(randomData) } case <-ctx.Done(): // 超时或取消信号,停止数据生成 return } } }() // 步骤6: 设置结果处理管道 resultChan := make(chan any, 10) // 添加计算结果回调函数(Sink) // 当窗口触发计算时,结果会通过这个回调函数输出 ssql.stream.AddSink(func(result []map[string]any) { // 非阻塞发送,避免阻塞 sink worker select { case resultChan <- result: default: // Channel 已满,忽略(非阻塞发送) } }) // 步骤7: 启动结果消费者协程 // 记录收到的结果数量,用于验证测试效果 var resultCount int64 var countMutex sync.Mutex var consumerWg sync.WaitGroup consumerWg.Add(1) go func() { defer consumerWg.Done() for { select { case <-resultChan: // 每当收到一个窗口的计算结果时,计数器加1 // 注释掉的代码可以用于调试,打印每个结果的详细信息 //fmt.Printf("打印结果: [%s] %v\n", time.Now().Format("15:04:05.000"), result) countMutex.Lock() resultCount++ countMutex.Unlock() case <-ctx.Done(): // 测试超时,退出消费者 goroutine // 不关闭 channel,让主程序自动退出时清理 return } } }() // 步骤8: 等待测试完成 // 等待数据生产者协程结束(30秒超时或手动取消) wg.Wait() // 停止流处理,确保所有 goroutine 正确退出 ssql.Stop() // 等待一小段时间,确保所有 sink worker 完成当前任务 // 这样可以确保所有结果都被发送到 channel time.Sleep(100 * time.Millisecond) // 取消 context,通知消费者 goroutine 退出 cancel() // 等待消费者 goroutine 完成(处理完 channel 中剩余的数据或收到取消信号) consumerWg.Wait() // 步骤9: 验证测试结果 // 预期在30秒内应该收到5个窗口的计算结果(每5秒一个窗口) // 这验证了 StreamSQL 的窗口触发机制是否正常工作 countMutex.Lock() finalCount := resultCount countMutex.Unlock() assert.Equal(t, finalCount, int64(5)) }