Files
v-hono/tests/test_stream_sse_properties.v
T
meiseayoung 9ae9170bcb feat: add SSE streaming helper support
Add Server-Sent Events (SSE) and streaming response support with three
streaming functions:

- stream(): Basic binary streaming with chunked transfer encoding
- stream_text(): Text streaming with proper Content-Type headers
- stream_sse(): SSE event streaming with full event field support

Features:
- SSEEvent struct with data, event, id, retry fields
- StreamContext for write, writeln, write_sse, sleep, pipe operations
- Multi-line data handling in SSE events
- Custom error handler support
- Client disconnect detection via on_abort callback
- Integrated with both Picoev and uSockets backends

New files:
- streaming.v: Core streaming implementation
- examples/sse_demo.v: Comprehensive SSE demo with test page
- tests/test_stream_*.v: 62 property-based tests

Updated:
- picoev_server.v: PicoevStreamWriter integration
- usockets_server.v: UsocketsStreamWriter integration
- README.md: SSE streaming documentation
2026-01-04 15:13:28 +08:00

989 lines
28 KiB
V

// test_stream_sse_properties.v
// Property-Based Testing for stream_sse() function
// Feature: sse-streaming-helper
// Property 6: streamSSE() 设置正确的 HTTP 头
// Property 7: SSE 事件格式化正确性
// Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
module main
import rand
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_test(test_name string, test_func fn () bool) {
stats.total_tests++
print('🔬 ${test_name}... ')
if test_func() {
stats.passed_tests++
println('✅')
} else {
stats.failed_tests++
println('❌')
}
}
fn (stats PropertyTestStats) print_summary() {
println('\n=== stream_sse() 函数属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// ============================================================================
// Mock StreamWriter for Testing
// ============================================================================
// MockStreamWriter - A mock implementation of StreamWriter for testing
// Captures all written data for verification
struct MockStreamWriter {
mut:
written_data []u8
connected bool
closed bool
flush_count int
}
fn MockStreamWriter.new() &MockStreamWriter {
return &MockStreamWriter{
written_data: []u8{}
connected: true
closed: false
flush_count: 0
}
}
fn (mut w MockStreamWriter) write(data []u8) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := '${data.len:x}\r\n'
w.written_data << chunk_header.bytes()
w.written_data << data
w.written_data << '\r\n'.bytes()
}
fn (mut w MockStreamWriter) write_string(data string) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := '${data.len:x}\r\n'
w.written_data << chunk_header.bytes()
w.written_data << data.bytes()
w.written_data << '\r\n'.bytes()
}
fn (mut w MockStreamWriter) flush() ! {
w.flush_count++
}
fn (mut w MockStreamWriter) close() ! {
if w.connected {
// Write the final chunk (zero-length chunk) to signal end of stream
w.written_data << '0\r\n\r\n'.bytes()
w.connected = false
w.closed = true
}
}
fn (w MockStreamWriter) is_connected() bool {
return w.connected
}
fn (w MockStreamWriter) get_written_string() string {
return w.written_data.bytestr()
}
// extract_sse_content - Extract the actual SSE content from chunked encoding
// Removes the chunk size headers and CRLF markers to get the raw SSE data
fn (w MockStreamWriter) extract_sse_content() string {
raw := w.written_data.bytestr()
mut result := []u8{}
mut i := 0
for i < raw.len {
// Find the chunk size (hex number before \r\n)
mut size_end := i
for size_end < raw.len && raw[size_end] != `\r` {
size_end++
}
if size_end >= raw.len {
break
}
// Parse chunk size from hex string
size_str := raw[i..size_end]
chunk_size := parse_hex_int(size_str)
if chunk_size == 0 {
// Final chunk
break
}
// Skip \r\n after size
i = size_end + 2
// Extract chunk data
if i + chunk_size <= raw.len {
result << raw[i..i + chunk_size].bytes()
}
// Skip chunk data and trailing \r\n
i = i + chunk_size + 2
}
return result.bytestr()
}
// parse_hex_int - Parse a hex string to integer
fn parse_hex_int(s string) int {
mut result := 0
for c in s {
result *= 16
if c >= `0` && c <= `9` {
result += int(c - `0`)
} else if c >= `a` && c <= `f` {
result += int(c - `a` + 10)
} else if c >= `A` && c <= `F` {
result += int(c - `A` + 10)
}
}
return result
}
// ============================================================================
// SSEEvent Structure for Testing
// ============================================================================
// SSEEvent - Server-Sent Events event structure
// Mirrors the implementation in streaming.v
struct SSEEvent {
data string // Event data (required) - supports multi-line
event string // Event type name (optional)
id string // Event ID for client reconnection (optional)
retry int // Reconnection time in milliseconds (optional, 0 = not set)
}
// ============================================================================
// StreamContext for Testing
// ============================================================================
// TestStreamContext - A test implementation of StreamContext
// Provides write_sse method for testing SSE event formatting
struct TestStreamContext {
mut:
writer &MockStreamWriter
is_closed bool
}
fn TestStreamContext.new(writer &MockStreamWriter) TestStreamContext {
return TestStreamContext{
writer: unsafe { writer }
is_closed: false
}
}
fn (mut ctx TestStreamContext) write_string(data string) ! {
if ctx.is_closed {
return error('Stream is closed')
}
if !ctx.writer.is_connected() {
ctx.is_closed = true
return error('Connection lost')
}
ctx.writer.write_string(data)!
}
// write_sse - Write an SSE event to the stream
// Formats the event according to the SSE specification:
// - event: {type}\n (if event field is set)
// - data: {line}\n (for each line of data)
// - id: {id}\n (if id field is set)
// - retry: {ms}\n (if retry > 0)
// - \n (event separator)
fn (mut ctx TestStreamContext) write_sse(event SSEEvent) ! {
if ctx.is_closed {
return error('Stream is closed')
}
if !ctx.writer.is_connected() {
ctx.is_closed = true
return error('Connection lost')
}
// Write event type if specified
if event.event.len > 0 {
ctx.writer.write_string('event: ${event.event}\n')!
}
// Write data field - handle multi-line data
lines := event.data.split('\n')
for line in lines {
ctx.writer.write_string('data: ${line}\n')!
}
// Write id if specified
if event.id.len > 0 {
ctx.writer.write_string('id: ${event.id}\n')!
}
// Write retry if specified (must be > 0)
if event.retry > 0 {
ctx.writer.write_string('retry: ${event.retry}\n')!
}
// Write event separator (empty line)
ctx.writer.write_string('\n')!
// Flush to ensure event is sent immediately
ctx.writer.flush()!
}
fn (mut ctx TestStreamContext) close() {
if !ctx.is_closed {
ctx.is_closed = true
ctx.writer.close() or {}
}
}
// ============================================================================
// Random Data Generators for Property Testing
// ============================================================================
// generate_random_string - Generate a random alphanumeric string
fn generate_random_string(min_len int, max_len int) string {
len := rand.int_in_range(min_len, max_len + 1) or { min_len }
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
mut result := []u8{cap: len}
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result << chars[idx]
}
return result.bytestr()
}
// generate_random_multiline_string - Generate a random string with newlines
fn generate_random_multiline_string(min_lines int, max_lines int) string {
num_lines := rand.int_in_range(min_lines, max_lines + 1) or { min_lines }
mut lines := []string{cap: num_lines}
for _ in 0 .. num_lines {
lines << generate_random_string(1, 20)
}
return lines.join('\n')
}
// generate_random_sse_event - Generate a random SSE event
fn generate_random_sse_event() SSEEvent {
// Randomly decide which optional fields to include
include_event := rand.int_in_range(0, 2) or { 0 } == 1
include_id := rand.int_in_range(0, 2) or { 0 } == 1
include_retry := rand.int_in_range(0, 2) or { 0 } == 1
is_multiline := rand.int_in_range(0, 3) or { 0 } == 0 // 1/3 chance of multiline
mut data := ''
if is_multiline {
data = generate_random_multiline_string(2, 5)
} else {
data = generate_random_string(1, 50)
}
return SSEEvent{
data: data
event: if include_event { generate_random_string(3, 15) } else { '' }
id: if include_id { generate_random_string(1, 10) } else { '' }
retry: if include_retry { rand.int_in_range(100, 10000) or { 1000 } } else { 0 }
}
}
// ============================================================================
// Helper Functions
// ============================================================================
// get_stream_sse_headers - Get the headers required for SSE streaming
// This mirrors the implementation in streaming.v
fn get_stream_sse_headers() map[string]string {
return {
'Content-Type': 'text/event-stream'
'Cache-Control': 'no-cache'
'Connection': 'keep-alive'
'Transfer-Encoding': 'chunked'
}
}
// ============================================================================
// Property 6: streamSSE() 设置正确的 HTTP 头
// Feature: sse-streaming-helper, Property 6
// Validates: Requirements 3.1, 3.2, 3.3
//
// *For any* call to streamSSE(c, callback), the response SHALL contain:
// - Content-Type: text/event-stream
// - Cache-Control: no-cache
// - Connection: keep-alive
// ============================================================================
// Test 6a: Content-Type header is set correctly for SSE
fn test_property_6a_content_type_header() bool {
headers := get_stream_sse_headers()
// Verify Content-Type header exists
if 'Content-Type' !in headers {
println(' Content-Type header missing')
return false
}
// Verify value is 'text/event-stream' (Requirement 3.1)
if headers['Content-Type'] != 'text/event-stream' {
println(' Content-Type should be "text/event-stream", got "${headers['Content-Type']}"')
return false
}
return true
}
// Test 6b: Cache-Control header is set correctly for SSE
fn test_property_6b_cache_control_header() bool {
headers := get_stream_sse_headers()
// Verify Cache-Control header exists
if 'Cache-Control' !in headers {
println(' Cache-Control header missing')
return false
}
// Verify value is 'no-cache' (Requirement 3.2)
if headers['Cache-Control'] != 'no-cache' {
println(' Cache-Control should be "no-cache", got "${headers['Cache-Control']}"')
return false
}
return true
}
// Test 6c: Connection header is set correctly for SSE
fn test_property_6c_connection_header() bool {
headers := get_stream_sse_headers()
// Verify Connection header exists
if 'Connection' !in headers {
println(' Connection header missing')
return false
}
// Verify value is 'keep-alive' (Requirement 3.3)
if headers['Connection'] != 'keep-alive' {
println(' Connection should be "keep-alive", got "${headers['Connection']}"')
return false
}
return true
}
// Test 6d: Transfer-Encoding header is set for chunked transfer
fn test_property_6d_transfer_encoding_header() bool {
headers := get_stream_sse_headers()
// Verify Transfer-Encoding header exists
if 'Transfer-Encoding' !in headers {
println(' Transfer-Encoding header missing')
return false
}
// Verify value is 'chunked'
if headers['Transfer-Encoding'] != 'chunked' {
println(' Transfer-Encoding should be "chunked", got "${headers['Transfer-Encoding']}"')
return false
}
return true
}
// Test 6e: Headers are consistent across multiple calls
fn test_property_6e_headers_consistency() bool {
for _ in 0 .. test_iterations {
headers1 := get_stream_sse_headers()
headers2 := get_stream_sse_headers()
// Headers should be identical
if headers1['Content-Type'] != headers2['Content-Type'] {
println(' Content-Type inconsistent between calls')
return false
}
if headers1['Cache-Control'] != headers2['Cache-Control'] {
println(' Cache-Control inconsistent between calls')
return false
}
if headers1['Connection'] != headers2['Connection'] {
println(' Connection inconsistent between calls')
return false
}
if headers1['Transfer-Encoding'] != headers2['Transfer-Encoding'] {
println(' Transfer-Encoding inconsistent between calls')
return false
}
}
return true
}
// Test 6f: All required SSE headers are present
fn test_property_6f_all_required_headers_present() bool {
headers := get_stream_sse_headers()
required_headers := ['Content-Type', 'Cache-Control', 'Connection', 'Transfer-Encoding']
for header in required_headers {
if header !in headers {
println(' Required header "${header}" is missing')
return false
}
}
return true
}
// Test 6g: SSE headers differ from text streaming headers
fn test_property_6g_sse_headers_differ_from_text() bool {
sse_headers := get_stream_sse_headers()
// SSE should use text/event-stream, not text/plain
if sse_headers['Content-Type'] == 'text/plain; charset=utf-8' {
println(' SSE Content-Type should not be text/plain')
return false
}
// SSE should have Cache-Control: no-cache
if 'Cache-Control' !in sse_headers {
println(' SSE should have Cache-Control header')
return false
}
return true
}
// ============================================================================
// Property 7: SSE 事件格式化正确性
// Feature: sse-streaming-helper, Property 7
// Validates: Requirements 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
//
// *For any* SSEEvent with fields (data, event, id, retry), when stream.writeSSE(event) is called:
// - If event is non-empty, output SHALL start with event: {event}\n
// - Output SHALL contain data: {data}\n for each line of data
// - If id is non-empty, output SHALL contain id: {id}\n
// - If retry > 0, output SHALL contain retry: {retry}\n
// - Output SHALL end with an additional \n to separate events
// ============================================================================
// Test 7a: Data field is always written correctly (Requirement 3.5)
// *For any* SSE event with data, the output SHALL contain "data: {value}\n"
fn test_property_7a_data_field_formatting() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
// Generate random single-line data
data := generate_random_string(1, 50)
event := SSEEvent{
data: data
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
expected := 'data: ${data}\n'
if !content.contains(expected) {
println(' Iteration ${i}: Expected data line "${expected}" not found in output')
println(' Got: "${content}"')
return false
}
}
return true
}
// Test 7b: Multi-line data is split correctly (Requirement 3.5)
// *For any* SSE event with multi-line data, each line SHALL be prefixed with "data: "
fn test_property_7b_multiline_data_formatting() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
// Generate random multi-line data
data := generate_random_multiline_string(2, 5)
event := SSEEvent{
data: data
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
lines := data.split('\n')
// Each line of data should be prefixed with "data: "
for line in lines {
expected := 'data: ${line}\n'
if !content.contains(expected) {
println(' Iteration ${i}: Expected data line "${expected}" not found')
println(' Original data: "${data}"')
println(' Got: "${content}"')
return false
}
}
}
return true
}
// Test 7c: Event field is written before data (Requirement 3.6)
// *For any* SSE event with event field, output SHALL start with "event: {value}\n"
fn test_property_7c_event_field_before_data() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
event_type := generate_random_string(3, 15)
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
event: event_type
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
event_line := 'event: ${event_type}\n'
data_line := 'data: ${data}\n'
// Event line should exist
if !content.contains(event_line) {
println(' Iteration ${i}: Event line "${event_line}" not found')
return false
}
// Event line should come before data line
event_pos := content.index(event_line) or { -1 }
data_pos := content.index(data_line) or { -1 }
if event_pos >= data_pos {
println(' Iteration ${i}: Event line should come before data line')
println(' Event pos: ${event_pos}, Data pos: ${data_pos}')
return false
}
}
return true
}
// Test 7d: Event field is omitted when empty (Requirement 3.6)
// *For any* SSE event with empty event field, output SHALL NOT contain "event: "
fn test_property_7d_empty_event_field_omitted() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
event: '' // Empty event field
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Should not contain "event: " prefix
if content.contains('event: ') {
println(' Iteration ${i}: Empty event field should not produce "event: " line')
println(' Got: "${content}"')
return false
}
}
return true
}
// Test 7e: ID field is written after data (Requirement 3.7)
// *For any* SSE event with id field, output SHALL contain "id: {value}\n" after data
fn test_property_7e_id_field_after_data() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
id := generate_random_string(1, 10)
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
id: id
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
id_line := 'id: ${id}\n'
data_line := 'data: ${data}\n'
// ID line should exist
if !content.contains(id_line) {
println(' Iteration ${i}: ID line "${id_line}" not found')
return false
}
// ID line should come after data line
id_pos := content.index(id_line) or { -1 }
data_pos := content.index(data_line) or { -1 }
if id_pos <= data_pos {
println(' Iteration ${i}: ID line should come after data line')
println(' ID pos: ${id_pos}, Data pos: ${data_pos}')
return false
}
}
return true
}
// Test 7f: ID field is omitted when empty (Requirement 3.7)
// *For any* SSE event with empty id field, output SHALL NOT contain "id: "
fn test_property_7f_empty_id_field_omitted() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
id: '' // Empty id field
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Should not contain "id: " prefix
if content.contains('id: ') {
println(' Iteration ${i}: Empty id field should not produce "id: " line')
println(' Got: "${content}"')
return false
}
}
return true
}
// Test 7g: Retry field is written when positive (Requirement 3.8)
// *For any* SSE event with retry > 0, output SHALL contain "retry: {value}\n"
fn test_property_7g_retry_field_when_positive() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
retry := rand.int_in_range(100, 10000) or { 1000 }
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
retry: retry
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
retry_line := 'retry: ${retry}\n'
// Retry line should exist
if !content.contains(retry_line) {
println(' Iteration ${i}: Retry line "${retry_line}" not found')
println(' Got: "${content}"')
return false
}
}
return true
}
// Test 7h: Retry field is omitted when zero (Requirement 3.8)
// *For any* SSE event with retry = 0, output SHALL NOT contain "retry: "
fn test_property_7h_zero_retry_field_omitted() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
data := generate_random_string(1, 30)
event := SSEEvent{
data: data
retry: 0 // Zero retry
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Should not contain "retry: " prefix
if content.contains('retry: ') {
println(' Iteration ${i}: Zero retry should not produce "retry: " line')
println(' Got: "${content}"')
return false
}
}
return true
}
// Test 7i: Event separator (empty line) is always present (Requirement 3.9)
// *For any* SSE event, output SHALL end with an additional "\n" to separate events
fn test_property_7i_event_separator_present() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
event := generate_random_sse_event()
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Content should end with double newline (data line + separator)
// The last line before separator should be data:, id:, or retry:
// followed by \n\n (the separator)
if !content.ends_with('\n\n') {
println(' Iteration ${i}: Event should end with double newline (separator)')
println(' Got: "${content}"')
println(' Last chars: ${content.bytes()[content.len - 4..]}')
return false
}
}
return true
}
// Test 7j: Complete SSE event with all fields (Requirements 3.4-3.9)
// *For any* SSE event with all fields set, output SHALL follow correct format and order
fn test_property_7j_complete_event_format() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
event_type := generate_random_string(3, 15)
data := generate_random_string(1, 30)
id := generate_random_string(1, 10)
retry := rand.int_in_range(100, 10000) or { 1000 }
event := SSEEvent{
data: data
event: event_type
id: id
retry: retry
}
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Verify all fields are present
if !content.contains('event: ${event_type}\n') {
println(' Iteration ${i}: Event field missing')
return false
}
if !content.contains('data: ${data}\n') {
println(' Iteration ${i}: Data field missing')
return false
}
if !content.contains('id: ${id}\n') {
println(' Iteration ${i}: ID field missing')
return false
}
if !content.contains('retry: ${retry}\n') {
println(' Iteration ${i}: Retry field missing')
return false
}
// Verify order: event -> data -> id -> retry -> separator
event_pos := content.index('event: ') or { -1 }
data_pos := content.index('data: ') or { -1 }
id_pos := content.index('id: ') or { -1 }
retry_pos := content.index('retry: ') or { -1 }
if event_pos >= data_pos {
println(' Iteration ${i}: Event should come before data')
return false
}
if data_pos >= id_pos {
println(' Iteration ${i}: Data should come before id')
return false
}
if id_pos >= retry_pos {
println(' Iteration ${i}: ID should come before retry')
return false
}
// Verify ends with separator
if !content.ends_with('\n\n') {
println(' Iteration ${i}: Should end with event separator')
return false
}
}
return true
}
// Test 7k: Random SSE events are formatted correctly (Requirements 3.4-3.9)
// *For any* randomly generated SSE event, the formatting SHALL be correct
fn test_property_7k_random_events_formatted_correctly() bool {
for i in 0 .. test_iterations {
mut writer := MockStreamWriter.new()
mut ctx := TestStreamContext.new(writer)
event := generate_random_sse_event()
ctx.write_sse(event) or {
println(' Iteration ${i}: write_sse failed: ${err}')
return false
}
content := writer.extract_sse_content()
// Data field should always be present
lines := event.data.split('\n')
for line in lines {
if !content.contains('data: ${line}\n') {
println(' Iteration ${i}: Data line missing: "data: ${line}"')
return false
}
}
// Event field should be present only if non-empty
if event.event.len > 0 {
if !content.contains('event: ${event.event}\n') {
println(' Iteration ${i}: Event field should be present')
return false
}
} else {
if content.contains('event: ') {
println(' Iteration ${i}: Event field should not be present when empty')
return false
}
}
// ID field should be present only if non-empty
if event.id.len > 0 {
if !content.contains('id: ${event.id}\n') {
println(' Iteration ${i}: ID field should be present')
return false
}
} else {
if content.contains('id: ') {
println(' Iteration ${i}: ID field should not be present when empty')
return false
}
}
// Retry field should be present only if > 0
if event.retry > 0 {
if !content.contains('retry: ${event.retry}\n') {
println(' Iteration ${i}: Retry field should be present')
return false
}
} else {
if content.contains('retry: ') {
println(' Iteration ${i}: Retry field should not be present when zero')
return false
}
}
// Should always end with separator
if !content.ends_with('\n\n') {
println(' Iteration ${i}: Should end with event separator')
return false
}
}
return true
}
fn main() {
println('🚀 开始 stream_sse() 函数属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// Run Property 6 tests
// Feature: sse-streaming-helper, Property 6: streamSSE() 设置正确的 HTTP 头
// Validates: Requirements 3.1, 3.2, 3.3
println('--- Property 6: streamSSE() 设置正确的 HTTP 头 ---')
stats.run_property_test('Property 6a: Content-Type header (text/event-stream)', test_property_6a_content_type_header)
stats.run_property_test('Property 6b: Cache-Control header (no-cache)', test_property_6b_cache_control_header)
stats.run_property_test('Property 6c: Connection header (keep-alive)', test_property_6c_connection_header)
stats.run_property_test('Property 6d: Transfer-Encoding header (chunked)', test_property_6d_transfer_encoding_header)
stats.run_property_test('Property 6e: Headers consistency', test_property_6e_headers_consistency)
stats.run_property_test('Property 6f: All required headers present', test_property_6f_all_required_headers_present)
stats.run_property_test('Property 6g: SSE headers differ from text', test_property_6g_sse_headers_differ_from_text)
// Run Property 7 tests
// Feature: sse-streaming-helper, Property 7: SSE 事件格式化正确性
// Validates: Requirements 3.4, 3.5, 3.6, 3.7, 3.8, 3.9
println('\n--- Property 7: SSE 事件格式化正确性 ---')
stats.run_property_test('Property 7a: Data field formatting', test_property_7a_data_field_formatting)
stats.run_property_test('Property 7b: Multi-line data formatting', test_property_7b_multiline_data_formatting)
stats.run_property_test('Property 7c: Event field before data', test_property_7c_event_field_before_data)
stats.run_property_test('Property 7d: Empty event field omitted', test_property_7d_empty_event_field_omitted)
stats.run_property_test('Property 7e: ID field after data', test_property_7e_id_field_after_data)
stats.run_property_test('Property 7f: Empty ID field omitted', test_property_7f_empty_id_field_omitted)
stats.run_property_test('Property 7g: Retry field when positive', test_property_7g_retry_field_when_positive)
stats.run_property_test('Property 7h: Zero retry field omitted', test_property_7h_zero_retry_field_omitted)
stats.run_property_test('Property 7i: Event separator present', test_property_7i_event_separator_present)
stats.run_property_test('Property 7j: Complete event format', test_property_7j_complete_event_format)
stats.run_property_test('Property 7k: Random events formatted correctly', test_property_7k_random_events_formatted_correctly)
// Print summary
stats.print_summary()
}