Skip to content

Commit 6eb352c

Browse files
committed
test(sdk/csharp)+fix: TCPTransport 并发写帧损坏修复 + 入站/校验器缺口补测
产品修复(测试暴露的真 bug): - WriteFrameAsync 无写锁:header/payload/flush 三段 await 并发交错损坏 帧——多入站 handler 并发回写时 16 并发即复现挂死。加 _writeLock 串行化(对齐 Go MuxConn writeMu) - ReadLoop 超大帧分支只 break 不置 _connected=false——传输层假活 - 删除死代码 HandleInboundRequestAsync(无调用方,DispatchInbound 已内联) 补测(727→752): - TcpTransportInboundTests:无 handler/异常 handler 回错、超大帧断连、 零长帧跳过、无 pending 响应忽略、16 并发全应答 - JsonSchemaValidatorBranchTests:numeric/string/array/object 约束全 分支、$ref 未解析/转义指针、enum/const/integer 形态(87.1%→97.2%) - CoverageBoost4:FunctionHandlerBase 同步入口/RetryConfig 抖动上限/ MainThreadDispatcher 泛型入队/DI 空守卫
1 parent 3a35d76 commit 6eb352c

4 files changed

Lines changed: 581 additions & 19 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2025 Croupier Authors
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using System;
5+
using Croupier.Sdk;
6+
using Croupier.Sdk.Extensions;
7+
using Croupier.Sdk.Configuration;
8+
using Croupier.Sdk.Models;
9+
using Croupier.Sdk.Threading;
10+
using FluentAssertions;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Xunit;
13+
14+
namespace Croupier.Sdk.Tests;
15+
16+
/// <summary>
17+
/// 中小缺口补测:FunctionHandlerBase 同步入口 / RetryConfig 抖动与上限 /
18+
/// MainThreadDispatcher 泛型入队 / DI 扩展空参守卫 / InvokeOptions 回退链。
19+
/// </summary>
20+
public class CoverageBoost4Tests
21+
{
22+
private class EchoHandler : FunctionHandlerBase
23+
{
24+
public override Task<string> HandleAsync(FunctionContext context, string payload)
25+
{
26+
return Task.FromResult("handled:" + payload);
27+
}
28+
}
29+
30+
[Fact]
31+
public void FunctionHandlerBase_SyncHandle_InvokesAsync()
32+
{
33+
var handler = new EchoHandler();
34+
var ctx = new FunctionContext
35+
{
36+
FunctionId = "fn.echo",
37+
CallId = Guid.NewGuid().ToString(),
38+
GameId = "g",
39+
Env = "dev",
40+
};
41+
handler.Handle(ctx, "x").Should().Be("handled:x");
42+
}
43+
44+
[Fact]
45+
public void RetryConfig_JitterAndCaps_Applied()
46+
{
47+
var cfg = new RetryConfig
48+
{
49+
InitialDelayMs = 10,
50+
BackoffMultiplier = 2,
51+
MaxDelayMs = 15, // 触发上限截断
52+
JitterFactor = 0.0,
53+
};
54+
// attempt 5:10*2^5=320 → 截到 15
55+
cfg.DelayMs(5).Should().Be(15);
56+
57+
var jitter = new RetryConfig
58+
{
59+
InitialDelayMs = 100,
60+
BackoffMultiplier = 1,
61+
MaxDelayMs = 0, // 不设上限
62+
JitterFactor = 0.5,
63+
};
64+
var d = jitter.DelayMs(1);
65+
d.Should().BeInRange(50, 150);
66+
}
67+
68+
[Fact]
69+
public void MainThreadDispatcher_EnqueueGeneric_NullIgnored()
70+
{
71+
var dispatcher = MainThreadDispatcher.Instance;
72+
dispatcher.Enqueue<int>(null!, 5); // null action 必须被忽略
73+
}
74+
75+
[Fact]
76+
public void MainThreadDispatcher_EnqueueGeneric_ProcessesData()
77+
{
78+
var dispatcher = MainThreadDispatcher.Instance;
79+
var seen = 0;
80+
dispatcher.Enqueue<int>(v => seen = v, 42);
81+
for (var i = 0; i < 128 && dispatcher.ProcessQueue(1) > 0; i++)
82+
{
83+
}
84+
seen.Should().Be(42);
85+
}
86+
87+
[Fact]
88+
public void ServiceCollectionExtensions_NullGuards_Throw()
89+
{
90+
var act1 = () => CroupierServiceCollectionExtensionsForTest.AddCroupierNullCheck(null!);
91+
act1.Should().Throw<ArgumentNullException>();
92+
93+
var act2 = () => CroupierServiceCollectionExtensionsForTest.AddCroupierWithProviderNullConfigCheck(
94+
new ServiceCollection(), null!);
95+
act2.Should().Throw<ArgumentNullException>();
96+
}
97+
98+
[Fact]
99+
public void AddCroupier_RegistersClient()
100+
{
101+
var services = new ServiceCollection();
102+
services.AddCroupier(cfg =>
103+
{
104+
cfg.AgentAddr = "127.0.0.1:19091";
105+
cfg.GameId = "g1";
106+
cfg.Env = "dev";
107+
});
108+
using var provider = services.BuildServiceProvider();
109+
provider.GetRequiredService<CroupierClient>().Should().NotBeNull();
110+
}
111+
112+
private static class CroupierServiceCollectionExtensionsForTest
113+
{
114+
// 直接调用公开扩展方法并断言空守卫(不绕私有访问器)。
115+
public static void AddCroupierNullCheck(IServiceCollection services)
116+
{
117+
ServiceCollectionExtensionsTestShim.AddCroupier(services, (Action<ClientConfig>?)null);
118+
}
119+
120+
public static void AddCroupierWithProviderNullConfigCheck(
121+
IServiceCollection services, ICroupierConfigProvider provider)
122+
{
123+
ServiceCollectionExtensionsTestShim.AddCroupier(services, provider);
124+
}
125+
}
126+
}
127+
128+
internal static class ServiceCollectionExtensionsTestShim
129+
{
130+
public static void AddCroupier(IServiceCollection services, Action<ClientConfig>? configAction)
131+
=> services.AddCroupier(configAction);
132+
133+
public static void AddCroupier(IServiceCollection services, ICroupierConfigProvider provider)
134+
=> services.AddCroupier(provider);
135+
}
136+
137+
internal static class MainThreadDispatcherTestExtensions
138+
{
139+
public static void ProcessQueueAllForTest(this MainThreadDispatcher dispatcher)
140+
{
141+
// 处理全部已入队回调(队列容量内)。
142+
for (var i = 0; i < 128; i++)
143+
{
144+
if (dispatcher.ProcessQueue(1) == 0)
145+
{
146+
break;
147+
}
148+
}
149+
}
150+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright 2025 Croupier Authors
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using System.Text.Json;
5+
using Croupier.Sdk.Validation;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace Croupier.Sdk.Tests;
10+
11+
/// <summary>
12+
/// JsonSchemaValidator 字符串入口与各约束分支的缺口补测
13+
/// (numeric/string/array/object 约束、$ref 解析失败、错误消息文案)。
14+
/// </summary>
15+
public class JsonSchemaValidatorBranchTests
16+
{
17+
private static JsonElement Parse(string json)
18+
{
19+
using var doc = JsonDocument.Parse(json);
20+
return doc.RootElement.Clone();
21+
}
22+
23+
[Fact]
24+
public void Validate_StringEntry_MissingRequired_ReportsPath()
25+
{
26+
var schema = """{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}""";
27+
var errors = JsonSchemaValidator.Validate("{}", schema);
28+
errors.Should().NotBeEmpty();
29+
errors[0].Should().Contain("name");
30+
}
31+
32+
[Fact]
33+
public void Numeric_Bounds_AllBranches()
34+
{
35+
var schema = """
36+
{
37+
"type": "number",
38+
"minimum": 1,
39+
"maximum": 10,
40+
"exclusiveMinimum": 0.5,
41+
"exclusiveMaximum": 9.5,
42+
"multipleOf": 3
43+
}
44+
""";
45+
JsonSchemaValidator.Validate("0", schema)[0].Should().Contain("less than minimum");
46+
JsonSchemaValidator.Validate("11", schema)[0].Should().Contain("greater than maximum");
47+
JsonSchemaValidator.Validate("0.4", schema).Should().Contain(e => e.Contains("greater than 0.5") || e.Contains("exclusiveMinimum"));
48+
JsonSchemaValidator.Validate("9.5", schema).Should().Contain(e => e.Contains("less than 9.5"));
49+
JsonSchemaValidator.Validate("4", schema).Should().Contain(e => e.Contains("multiple of"));
50+
JsonSchemaValidator.Validate("6", schema).Should().BeEmpty();
51+
}
52+
53+
[Fact]
54+
public void String_Constraints_AllBranches()
55+
{
56+
var schema = """
57+
{ "type": "string", "minLength": 2, "maxLength": 5, "pattern": "^[a-z]+$" }
58+
""";
59+
JsonSchemaValidator.Validate("\"a\"", schema)[0].Should().Contain("minLength");
60+
JsonSchemaValidator.Validate("\"abcdef\"", schema)[0].Should().Contain("maxLength");
61+
JsonSchemaValidator.Validate("\"AB\"", schema).Should().Contain(e => e.Contains("pattern"));
62+
JsonSchemaValidator.Validate("\"abc\"", schema).Should().BeEmpty();
63+
}
64+
65+
[Fact]
66+
public void String_InvalidPattern_IsIgnored()
67+
{
68+
// 非法正则(未闭合括号)——校验器必须忽略而不是抛出。
69+
var schema = """{"type":"string","pattern":"[unclosed"}""";
70+
var act = () => JsonSchemaValidator.Validate("\"anything\"", schema);
71+
act.Should().NotThrow();
72+
}
73+
74+
[Fact]
75+
public void Array_Constraints_AllBranches()
76+
{
77+
var schema = """
78+
{ "type": "array", "minItems": 2, "maxItems": 3, "uniqueItems": true }
79+
""";
80+
JsonSchemaValidator.Validate("[1]", schema)[0].Should().Contain("minItems");
81+
JsonSchemaValidator.Validate("[1,2,3,4]", schema)[0].Should().Contain("maxItems");
82+
JsonSchemaValidator.Validate("[1,1]", schema).Should().Contain(e => e.Contains("not unique"));
83+
JsonSchemaValidator.Validate("[1,2]", schema).Should().BeEmpty();
84+
}
85+
86+
[Fact]
87+
public void Array_ItemsSchema_AppliedToElements()
88+
{
89+
var schema = """{"type":"array","items":{"type":"integer"}}""";
90+
JsonSchemaValidator.Validate("[1, \"x\"]", schema).Should().Contain(e => e.Contains("integer"));
91+
JsonSchemaValidator.Validate("[1, 2]", schema).Should().BeEmpty();
92+
}
93+
94+
[Fact]
95+
public void Object_AdditionalProperty_Branches()
96+
{
97+
var noAdditional = """{"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":false}""";
98+
JsonSchemaValidator.Validate("""{"a":"x","b":1}""", noAdditional)
99+
.Should().Contain(e => e.Contains("not allowed"));
100+
101+
var typedAdditional = """{"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":{"type":"number"}}""";
102+
JsonSchemaValidator.Validate("""{"a":"x","b":"not-a-number"}""", typedAdditional)
103+
.Should().Contain(e => e.Contains("number"));
104+
105+
JsonSchemaValidator.Validate("""{"a":"x","b":7}""", typedAdditional).Should().BeEmpty();
106+
}
107+
108+
[Fact]
109+
public void Ref_UnknownPointer_ReportsError()
110+
{
111+
var schema = """{"$ref":"#/definitions/missing","definitions":{}}""";
112+
var errors = JsonSchemaValidator.Validate("42", schema);
113+
errors.Should().Contain(e => e.Contains("$ref") || e.Contains("missing"));
114+
}
115+
116+
[Fact]
117+
public void Ref_EscapedPointerSegments_Resolve()
118+
{
119+
// ~1 = /,~0 = ~ 的 JSON Pointer 转义。
120+
var schema = """
121+
{
122+
"$ref": "#/definitions/a~1b",
123+
"definitions": { "a/b": { "type": "string" } }
124+
}
125+
""";
126+
JsonSchemaValidator.Validate("\"ok\"", schema).Should().BeEmpty();
127+
JsonSchemaValidator.Validate("42", schema).Should().NotBeEmpty();
128+
}
129+
130+
[Fact]
131+
public void Enum_MismatchReports_NonStringValues()
132+
{
133+
var schema = """{"enum":[1,2,3]}""";
134+
JsonSchemaValidator.Validate("4", schema).Should().Contain(e => e.Contains("enum"));
135+
JsonSchemaValidator.Validate("2", schema).Should().BeEmpty();
136+
}
137+
138+
[Fact]
139+
public void Const_MismatchReports()
140+
{
141+
var schema = """{"const":"v1"}""";
142+
JsonSchemaValidator.Validate("\"v2\"", schema).Should().NotBeEmpty();
143+
JsonSchemaValidator.Validate("\"v1\"", schema).Should().BeEmpty();
144+
}
145+
146+
[Fact]
147+
public void Integer_Type_ChecksIntegralForm()
148+
{
149+
var schema = """{"type":"integer"}""";
150+
JsonSchemaValidator.Validate("1.5", schema).Should().NotBeEmpty();
151+
JsonSchemaValidator.Validate("1e3", schema).Should().NotBeEmpty();
152+
JsonSchemaValidator.Validate("7", schema).Should().BeEmpty();
153+
}
154+
155+
[Fact]
156+
public void TypeNames_InErrorMessages()
157+
{
158+
var schema = """{"type":"string"}""";
159+
foreach (var payload in new[] { "42", "true", "null", "[1]", """{"k":1}""" })
160+
{
161+
var errors = JsonSchemaValidator.Validate(payload, schema);
162+
errors.Should().NotBeEmpty($"payload {payload} is not a string");
163+
}
164+
}
165+
}

0 commit comments

Comments
 (0)