Skip to content

Commit 457cea1

Browse files
committed
Add google gemini unit test
1 parent a731889 commit 457cea1

10 files changed

Lines changed: 2452 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System.Text;
2+
3+
namespace Chats.BE.Tests.ChatServices;
4+
5+
/// <summary>
6+
/// 用于解析从Fiddler导出的HTTP会话dump文件
7+
/// </summary>
8+
public class FiddlerHttpDumpParser
9+
{
10+
public record HttpDump(
11+
HttpRequest Request,
12+
HttpResponse Response
13+
);
14+
15+
public record HttpRequest(
16+
string Method,
17+
string Url,
18+
string HttpVersion,
19+
Dictionary<string, string> Headers,
20+
string Body
21+
);
22+
23+
public record HttpResponse(
24+
int StatusCode,
25+
string StatusText,
26+
string HttpVersion,
27+
Dictionary<string, string> Headers,
28+
List<string> Chunks
29+
)
30+
{
31+
/// <summary>
32+
/// 原始响应体(包含chunked编码)
33+
/// </summary>
34+
public string RawBody => string.Join("\n", Chunks);
35+
36+
/// <summary>
37+
/// 去除chunked编码后的响应体
38+
/// </summary>
39+
public string DechunkedBody => string.Join("\n", Chunks.Where(line => !IsChunkSizeLine(line))).Trim();
40+
41+
private static bool IsChunkSizeLine(string line)
42+
{
43+
var trimmed = line.Trim();
44+
if (string.IsNullOrEmpty(trimmed)) return false;
45+
46+
// chunked编码的长度行是16进制数字
47+
return trimmed.Length <= 8 && trimmed.All(c =>
48+
(c >= '0' && c <= '9') ||
49+
(c >= 'a' && c <= 'f') ||
50+
(c >= 'A' && c <= 'F'));
51+
}
52+
};
53+
54+
/// <summary>
55+
/// 解析Fiddler导出的dump文件
56+
/// </summary>
57+
public static HttpDump Parse(string content)
58+
{
59+
var lines = content.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
60+
61+
// 找到请求和响应的分界线
62+
int responseStartIndex = FindResponseStartIndex(lines);
63+
64+
var request = ParseRequest(lines[..responseStartIndex]);
65+
var response = ParseResponse(lines[responseStartIndex..]);
66+
67+
return new HttpDump(request, response);
68+
}
69+
70+
/// <summary>
71+
/// 从文件解析
72+
/// </summary>
73+
public static HttpDump ParseFile(string filePath)
74+
{
75+
var content = File.ReadAllText(filePath, Encoding.UTF8);
76+
return Parse(content);
77+
}
78+
79+
private static int FindResponseStartIndex(string[] lines)
80+
{
81+
for (int i = 0; i < lines.Length; i++)
82+
{
83+
if (lines[i].StartsWith("HTTP/"))
84+
{
85+
return i;
86+
}
87+
}
88+
throw new InvalidOperationException("找不到HTTP响应的起始位置");
89+
}
90+
91+
private static HttpRequest ParseRequest(string[] lines)
92+
{
93+
// 解析请求行: POST https://... HTTP/1.1
94+
var requestLine = lines[0].Split(' ', 3);
95+
var method = requestLine[0];
96+
var url = requestLine[1];
97+
var httpVersion = requestLine[2];
98+
99+
// 解析请求头
100+
var headers = new Dictionary<string, string>();
101+
int bodyStartIndex = 1;
102+
103+
for (int i = 1; i < lines.Length; i++)
104+
{
105+
if (string.IsNullOrWhiteSpace(lines[i]))
106+
{
107+
bodyStartIndex = i + 1;
108+
break;
109+
}
110+
111+
var colonIndex = lines[i].IndexOf(':');
112+
if (colonIndex > 0)
113+
{
114+
var headerName = lines[i][..colonIndex];
115+
var headerValue = lines[i][(colonIndex + 1)..].TrimStart();
116+
headers[headerName] = headerValue;
117+
}
118+
}
119+
120+
// 解析请求体
121+
string body = string.Empty;
122+
if (bodyStartIndex < lines.Length)
123+
{
124+
body = string.Join("", lines[bodyStartIndex..]);
125+
}
126+
127+
return new HttpRequest(method, url, httpVersion, headers, body);
128+
}
129+
130+
private static HttpResponse ParseResponse(string[] lines)
131+
{
132+
// 解析状态行: HTTP/1.1 200 OK
133+
var statusLine = lines[0].Split(' ', 3);
134+
var httpVersion = statusLine[0];
135+
var statusCode = int.Parse(statusLine[1]);
136+
var statusText = statusLine[2];
137+
138+
// 解析响应头
139+
var headers = new Dictionary<string, string>();
140+
int bodyStartIndex = 1;
141+
142+
for (int i = 1; i < lines.Length; i++)
143+
{
144+
if (string.IsNullOrWhiteSpace(lines[i]))
145+
{
146+
bodyStartIndex = i + 1;
147+
break;
148+
}
149+
150+
var colonIndex = lines[i].IndexOf(':');
151+
if (colonIndex > 0)
152+
{
153+
var headerName = lines[i][..colonIndex];
154+
var headerValue = lines[i][(colonIndex + 1)..].TrimStart();
155+
headers[headerName] = headerValue;
156+
}
157+
}
158+
159+
// 解析响应体为Chunks列表
160+
var chunks = lines[bodyStartIndex..].ToList();
161+
162+
return new HttpResponse(statusCode, statusText, httpVersion, headers, chunks);
163+
}
164+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
namespace Chats.BE.Tests.ChatServices;
2+
3+
public class FiddlerHttpDumpParserTests
4+
{
5+
private const string TestDataPath = "ChatServices/GoogleAI";
6+
7+
[Fact]
8+
public void CanParseCodeExecuteDump()
9+
{
10+
// Arrange
11+
var filePath = Path.Combine(TestDataPath, "CodeExecute.txt");
12+
13+
// Act
14+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
15+
16+
// Assert - Request
17+
Assert.Equal("POST", dump.Request.Method);
18+
Assert.Contains("gemini-2.5-flash:streamGenerateContent", dump.Request.Url);
19+
Assert.Equal("HTTP/1.1", dump.Request.HttpVersion);
20+
Assert.NotEmpty(dump.Request.Headers);
21+
Assert.Contains("x-goog-api-key", dump.Request.Headers.Keys);
22+
Assert.NotEmpty(dump.Request.Body);
23+
Assert.Contains("\"model\"", dump.Request.Body);
24+
25+
// Assert - Response
26+
Assert.Equal(200, dump.Response.StatusCode);
27+
Assert.Equal("OK", dump.Response.StatusText);
28+
Assert.NotEmpty(dump.Response.Headers);
29+
Assert.True(dump.Response.Headers.ContainsKey("Transfer-Encoding"));
30+
Assert.NotEmpty(dump.Response.RawBody);
31+
Assert.NotEmpty(dump.Response.DechunkedBody);
32+
33+
// Dechunked body应该不包含chunk大小行
34+
Assert.DoesNotContain("2da", dump.Response.DechunkedBody.Split('\n')[0]);
35+
Assert.Contains("candidates", dump.Response.DechunkedBody);
36+
}
37+
38+
[Fact]
39+
public void CanParseToolCallDump()
40+
{
41+
// Arrange
42+
var filePath = Path.Combine(TestDataPath, "ToolCall.txt");
43+
44+
// Act
45+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
46+
47+
// Assert - Request
48+
Assert.Equal("POST", dump.Request.Method);
49+
Assert.Contains("run_code", dump.Request.Body);
50+
51+
// Assert - Response
52+
Assert.Equal(200, dump.Response.StatusCode);
53+
Assert.Contains("functionCall", dump.Response.DechunkedBody);
54+
}
55+
56+
[Fact]
57+
public void CanParseWebSearchDump()
58+
{
59+
// Arrange
60+
var filePath = Path.Combine(TestDataPath, "WebSearch.txt");
61+
62+
// Act
63+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
64+
65+
// Assert - Request
66+
Assert.Equal("POST", dump.Request.Method);
67+
Assert.Contains("googleSearch", dump.Request.Body);
68+
69+
// Assert - Response
70+
Assert.Equal(200, dump.Response.StatusCode);
71+
Assert.Contains("groundingMetadata", dump.Response.DechunkedBody);
72+
Assert.Contains("webSearchQueries", dump.Response.DechunkedBody);
73+
}
74+
75+
[Fact]
76+
public void CanParseImageGenerateDump()
77+
{
78+
// Arrange
79+
var filePath = Path.Combine(TestDataPath, "ImageGenerate.txt");
80+
81+
// Act
82+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
83+
84+
// Assert - Request
85+
Assert.Contains("image-generation", dump.Request.Url);
86+
Assert.Contains("IMAGE", dump.Request.Body);
87+
88+
// Assert - Response
89+
Assert.Equal(200, dump.Response.StatusCode);
90+
Assert.Contains("inlineData", dump.Response.DechunkedBody);
91+
}
92+
93+
[Fact]
94+
public void CanExtractRequestHeaders()
95+
{
96+
// Arrange
97+
var filePath = Path.Combine(TestDataPath, "CodeExecute.txt");
98+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
99+
100+
// Assert
101+
Assert.True(dump.Request.Headers.TryGetValue("Content-Type", out var contentType));
102+
Assert.Equal("application/json", contentType);
103+
104+
Assert.True(dump.Request.Headers.TryGetValue("Host", out var host));
105+
Assert.Equal("generativelanguage.googleapis.com", host);
106+
}
107+
108+
[Fact]
109+
public void CanExtractResponseHeaders()
110+
{
111+
// Arrange
112+
var filePath = Path.Combine(TestDataPath, "CodeExecute.txt");
113+
var dump = FiddlerHttpDumpParser.ParseFile(filePath);
114+
115+
// Assert
116+
Assert.True(dump.Response.Headers.TryGetValue("Content-Type", out var contentType));
117+
Assert.Contains("application/json", contentType);
118+
119+
Assert.True(dump.Response.Headers.TryGetValue("Transfer-Encoding", out var encoding));
120+
Assert.Equal("chunked", encoding);
121+
}
122+
}

0 commit comments

Comments
 (0)