test: 引入代码分析器工具

This commit is contained in:
Fu Diwei 2022-02-21 18:27:07 +08:00
parent ab96ebba31
commit 022ef5bc71
36 changed files with 232 additions and 1380 deletions

View File

@ -24,13 +24,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C95AF531-CF44-44AA-AC90-F4DF9F941674}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{4C48D9D5-1D7F-4616-A05D-256555C310FC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TestTools", "test\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj", "{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj", "{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj", "{574A567A-6D2C-49F6-9A98-0133CA9B007D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests.csproj", "{574A567A-6D2C-49F6-9A98-0133CA9B007D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj", "{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}"
EndProject
@ -78,14 +74,14 @@ Global
{AAE2E9BC-4D0B-4495-8825-DF8C405DB4A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAE2E9BC-4D0B-4495-8825-DF8C405DB4A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAE2E9BC-4D0B-4495-8825-DF8C405DB4A0}.Release|Any CPU.Build.0 = Release|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Release|Any CPU.Build.0 = Release|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.Build.0 = Release|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -114,10 +110,6 @@ Global
{7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7667F0D3-B41D-43C2-B69D-A68FE230EBF7}.Release|Any CPU.Build.0 = Release|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{574A567A-6D2C-49F6-9A98-0133CA9B007D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -129,9 +121,8 @@ Global
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{AAE2E9BC-4D0B-4495-8825-DF8C405DB4A0} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
{4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3} = {4C48D9D5-1D7F-4616-A05D-256555C310FC}
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{574A567A-6D2C-49F6-9A98-0133CA9B007D} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
{561E0BFB-7817-41FE-BAF2-D78817679AC1} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
@ -139,7 +130,6 @@ Global
{D1B321C9-3004-4645-A78D-A85C152062FA} = {35C901ED-C234-4A91-9561-AD89B3BB788D}
{65E51735-73CE-4E9B-AA65-4BF5E4C8A705} = {35C901ED-C234-4A91-9561-AD89B3BB788D}
{7667F0D3-B41D-43C2-B69D-A68FE230EBF7} = {35C901ED-C234-4A91-9561-AD89B3BB788D}
{574A567A-6D2C-49F6-9A98-0133CA9B007D} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39}

6
test/NuGet.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" value="https://www.myget.org/F/skit/api/v3/index.json" />
</packageSources>
</configuration>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -22,7 +22,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -32,7 +33,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Ads\SKIT.FlurlHttpClient.Wechat.Ads.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.Ads",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest,
AllowNotFoundEventTypes = true,
AllowNotFoundEventSamples = true
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,57 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Ads");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void TestApiModelsNaming()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void TestApiModelsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void TestApiExtensionsNaming()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void TestCodeStyle()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -23,8 +23,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests
WechatAgencyApiKey = config.GetProperty("AgencyApiKey").GetString()!;
WechatAccessToken = config.GetProperty("AccessToken").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -37,7 +37,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests
public static readonly string WechatAgencyApiKey;
public static readonly string WechatAccessToken;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -5,6 +5,6 @@
"AgencyApiKey": "请在此填写用于测试的微信广告 AgencyApiKey",
"AccessToken": "请在此填写用于测试的微信广告 AccessToken"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Ads\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Ads\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\\"
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -24,7 +24,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -34,7 +35,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Api\SKIT.FlurlHttpClient.Wechat.Api.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.Api",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,69 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Api");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void TestApiModelsNaming()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void TestApiModelsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 事件定义")]
public void TestApiEventsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "EventSamples");
CodeStyleUtil.VerifyApiEventsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void TestApiExtensionsNaming()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void TestCodeStyle()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -23,8 +23,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
WechatAccessToken = config.GetProperty("AccessToken").GetString()!;
WechatOpenId = config.GetProperty("OpenId").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -37,7 +37,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
public static readonly string WechatAccessToken;
public static readonly string WechatOpenId;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -5,6 +5,6 @@
"AccessToken": "请在此填写用于测试的微信 AccessToken",
"OpenId": "请在此填写用于测试的微信用户唯一标识"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Api\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Api\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\\"
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -23,7 +23,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -33,7 +34,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.OpenAI\SKIT.FlurlHttpClient.Wechat.OpenAI.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.OpenAI",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest,
AllowNotFoundEventTypes = true,
AllowNotFoundEventSamples = true
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,69 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.OpenAI");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void TestApiModelsNaming()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void TestApiModelsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 事件定义")]
public void TestApiEventsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "EventSamples");
CodeStyleUtil.VerifyApiEventsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void TestApiExtensionsNaming()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void TestCodeStyle()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -25,8 +25,8 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
WechatEncodingAESKey = config.GetProperty("EncodingAESKey").GetString()!;
WechatAccessToken = config.GetProperty("AccessToken").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -41,7 +41,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests
public static readonly string WechatEncodingAESKey;
public static readonly string WechatAccessToken;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -7,6 +7,6 @@
"EncodingAESKey": "请在此填写用于测试的微信智能对话 EncodingAESKey",
"AccessToken": "请在此填写用于测试的微信智能对话 AccessToken"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.OpenAI\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.OpenAI\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.OpenAI.UnitTests\\"
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -25,7 +25,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -35,7 +36,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.TenpayV2\SKIT.FlurlHttpClient.Wechat.TenpayV2.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.TenpayV2",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest,
AllowNotFoundEventTypes = true,
AllowNotFoundEventSamples = true
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,69 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV2");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void TestApiModelsNaming()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void TestApiModelsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 事件定义")]
public void ApiEventsDefinitionTest()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "EventSamples");
CodeStyleUtil.VerifyApiEventsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void TestApiExtensionsNaming()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void TestCodeStyle()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -24,8 +24,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests
WechatMerchantCertificateBytes = config.GetProperty("MerchantCertificateBase64String").GetBytesFromBase64();
WechatOpenId = config.GetProperty("OpenId").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -39,7 +39,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests
public static readonly byte[] WechatMerchantCertificateBytes;
public static readonly string WechatOpenId;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -6,6 +6,6 @@
"AppId": "请在此填写用于测试的微信 AppId",
"OpenId": "请在此填写用于测试的微信用户唯一标识"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV2\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV2\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests\\"
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -23,7 +23,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -33,7 +34,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.TenpayV3",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest,
AllowNotFoundEventTypes = true,
AllowNotFoundEventSamples = true
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,69 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV3");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void ApiModelsNamingTest()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void ApiModelsDefinitionTest()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 事件定义")]
public void ApiEventsDefinitionTest()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "EventSamples");
CodeStyleUtil.VerifyApiEventsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void ApiExtensionsNamingTest()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void CodeStyleTest()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -24,8 +24,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
WechatAppId = config.GetProperty("AppId").GetString()!;
WechatOpenId = config.GetProperty("OpenId").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -40,7 +40,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
public static readonly string WechatAppId;
public static readonly string WechatOpenId;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -7,6 +7,6 @@
"AppId": "请在此填写用于测试的微信 AppId",
"OpenId": "请在此填写用于测试的微信用户唯一标识"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV3\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.TenpayV3\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\\"
}

View File

@ -1,690 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
namespace SKIT.FlurlHttpClient.Wechat
{
public static class CodeStyleUtil
{
private static readonly Regex CODESTYLE_RULE_REGEX_NO_JSONABLE_PROPERTY_IN_REQUEST = new Regex(@"\/\* \@codestyle-disable[a-zA-Z0-9,\-\s]*no-jsonable-property-in-request-get[a-zA-Z0-9,\-\s]*\*\/", RegexOptions.Compiled);
private static readonly Regex CODESTYLE_RULE_REGEX_NO_INSTANTIATED_PROPERTY_IN_RESPONSE = new Regex(@"\/\* \@codestyle-disable[a-zA-Z0-9,\-\s]*no-instantiated-property-in-response[a-zA-Z0-9,\-\s]*\*\/", RegexOptions.Compiled);
private static bool TryJsonize(Type type, string json, out Exception exception)
{
exception = null;
var newtonsoftJsonSettings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
newtonsoftJsonSettings.CheckAdditionalContent = true;
newtonsoftJsonSettings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
var newtonsoftJsonSerializer = new FlurlNewtonsoftJsonSerializer(newtonsoftJsonSettings);
var systemTextJsonOptions = FlurlSystemTextJsonSerializer.GetDefaultSerializerOptions();
systemTextJsonOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
var systemTextJsonSerializer = new FlurlSystemTextJsonSerializer(systemTextJsonOptions);
try
{
newtonsoftJsonSerializer.Deserialize(json, type);
systemTextJsonSerializer.Deserialize(json, type);
}
catch (Exception ex)
{
if (ex is Newtonsoft.Json.JsonException)
exception = new Exception($"[模型] 通过 Newtonsoft.Json 反序列化 `{type.Name}` 失败。", ex);
else if (ex is System.Text.Json.JsonException)
exception = new Exception($"[模型] 通过 System.Text.Json 反序列化 `{type.Name}` 失败。", ex);
else
exception = new Exception($"[模型] JSON 反序列化 `{type.Name}` 遇到问题。", ex);
return false;
}
try
{
object instance = Activator.CreateInstance(type);
TestReflectionUtil.InitializeProperties(instance);
newtonsoftJsonSerializer.Serialize(instance, type);
systemTextJsonSerializer.Serialize(instance, type);
}
catch (Exception ex)
{
if (ex is Newtonsoft.Json.JsonException)
exception = new Exception($"[模型] 通过 Newtonsoft.Json 序列化 `{type.Name}` 失败。", ex);
else if (ex is System.Text.Json.JsonException)
exception = new Exception($"[模型] 通过 System.Text.Json 序列化 `{type.Name}` 失败。", ex);
else
exception = new Exception($"[模型] JSON 序列化 `{type.Name}` 遇到问题。", ex);
return false;
}
try
{
PropertyInfo[] lstPropInfo = TestReflectionUtil.GetAllProperties(type);
foreach (PropertyInfo propInfo in lstPropInfo)
{
var newtonsoftJsonAttribute = propInfo.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>(inherit: true);
var systemTextJsonAttribute = propInfo.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>(inherit: true);
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
{
exception = new Exception($"[模型] 类型 `{type.Name}` 的可 JSON 序列化字段 `{propInfo.Name}` 声明不一致:`{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`。");
return false;
}
}
}
catch (Exception ex)
{
exception = new Exception($"[模型] JSON 分析 `{type.Name}` 遇到问题。", ex);
return false;
}
return exception == null;
}
private static bool TryXmlize(Type type, string xml, out Exception exception)
{
exception = null;
try
{
using var reader = new StringReader(xml);
var xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml"));
xmlSerializer.Deserialize(reader);
}
catch (Exception ex)
{
exception = new Exception($"[模型] XML 反序列化 `{type.Name}` 遇到问题。", ex);
return false;
}
try
{
var xmlDoc = new System.Xml.XmlDocument();
xmlDoc.LoadXml(xml);
Func<System.Xml.XmlNode, Exception> fn = default;
fn = new Func<System.Xml.XmlNode, Exception>((xmlNode) =>
{
if (xmlNode.NodeType == System.Xml.XmlNodeType.Element)
{
PropertyInfo[] lstPropInfo = TestReflectionUtil.GetAllProperties(type);
var xmlElementAttributes = lstPropInfo.SelectMany(e => e.GetCustomAttributes<XmlElementAttribute>(inherit: true)).ToArray();
var xmlArrayAttributes = lstPropInfo.SelectMany(e => e.GetCustomAttributes<XmlArrayAttribute>(inherit: true)).ToArray();
var xmlArrayItemAttributes = lstPropInfo.SelectMany(e => e.GetCustomAttributes<XmlArrayItemAttribute>(inherit: true)).ToArray();
if (!xmlElementAttributes.Any(e => e.ElementName == xmlNode.Name) &&
!xmlArrayAttributes.Any(e => e.ElementName == xmlNode.Name) &&
!xmlArrayItemAttributes.Any(e => e.ElementName == xmlNode.Name))
{
return new Exception($"[模型] 类型 `{type.Name}` 的可 XML 序列化字段缺失项:`{xmlNode.Name}`。");
}
}
for (int i = 0; i < xmlNode.ChildNodes.Count; i++)
{
Exception ex = fn.Invoke(xmlNode.ChildNodes[i]);
if (ex != null)
return ex;
}
return null;
});
for (int i = 0; i < xmlDoc.DocumentElement.ChildNodes.Count; i++)
{
exception = fn.Invoke(xmlDoc.DocumentElement.ChildNodes[i]);
if (exception != null)
return false;
}
}
catch (Exception ex)
{
exception = new Exception($"[模型] XML 分析 `{type.Name}` 遇到问题。", ex);
return false;
}
return exception == null;
}
public static bool VerifyApiModelsNaming(Assembly assembly, out Exception exception)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
var lstError = new List<Exception>();
Parallel.ForEach(lstModelType, (modelType) =>
{
string name = modelType.Name.Split('`')[0];
if (!name.EndsWith("Request") && !name.EndsWith("Response"))
{
lstError.Add(new Exception($"[模型] `{name}` 类名结尾应为 \"Request\" 或 \"Response\"。"));
}
if (name.EndsWith("Request"))
{
if (!typeof(ICommonRequest).IsAssignableFrom(modelType))
{
lstError.Add(new Exception($"[模型] `{name}` 是请求模型,需实现自 `{nameof(ICommonRequest)}`。"));
}
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Request".Length)}Response"))
{
lstError.Add(new Exception($"[模型] `{name}` 是请求模型,但不存在对应的响应模型。"));
}
}
if (name.EndsWith("Response"))
{
if (!typeof(ICommonResponse).IsAssignableFrom(modelType))
{
lstError.Add(new Exception($"[模型] `{name}` 是响应模型,需实现自 `{nameof(ICommonResponse)}`。"));
}
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Response".Length)}Request"))
{
lstError.Add(new Exception($"[模型] `{name}` 是响应模型,但不存在对应的请求模型。"));
}
}
});
if (lstError.Any())
{
exception = new AggregateException(lstError);
return false;
}
exception = null;
return true;
}
public static bool VerifyApiModelsDefinition(Assembly assembly, string workdir, out Exception exception)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
var lstError = new List<Exception>();
var lstFilePath = TestIOUtil.GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.OrdinalIgnoreCase))
.ToList();
if (!lstFilePath.Any())
{
lstError.Add(new Exception($"[模型] \"{workdir}\" 下不存在示例文件,请检查配置的扫描路径是否正确。"));
}
Parallel.ForEach(lstFilePath, (filePath) =>
{
string name = Path.GetFileNameWithoutExtension(filePath).Split('.')[0];
Type type = assembly.GetTypes().FirstOrDefault(e => e.FullName.StartsWith($"{assembly.GetName().Name}.Models.") && e.Name == name);
if (type == null)
{
lstError.Add(new Exception($"[模型] 扫描到示例文件 \"{filePath}\",但类型 `{name}` 不存在。"));
return;
}
if (string.Equals(Path.GetExtension(filePath), ".json", StringComparison.OrdinalIgnoreCase))
{
string json = File.ReadAllText(filePath);
if (!TryJsonize(type, json, out Exception ex))
{
lstError.Add(ex);
}
}
});
if (lstError.Any())
{
exception = new AggregateException(lstError);
return false;
}
exception = null;
return true;
}
public static bool VerifyApiEventsDefinition(Assembly assembly, string workdir, out Exception exception)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
var lstError = new List<Exception>();
var lstFilePath = TestIOUtil.GetAllFiles(workdir)
.Where(e =>
string.Equals(Path.GetExtension(e), ".json", StringComparison.OrdinalIgnoreCase) ||
string.Equals(Path.GetExtension(e), ".xml", StringComparison.OrdinalIgnoreCase)
)
.ToArray();
if (!lstFilePath.Any())
{
lstError.Add(new Exception($"[模型] \"{workdir}\" 下不存在示例文件,请检查配置的扫描路径是否正确。"));
}
Parallel.ForEach(lstFilePath, (filePath) =>
{
string name = Path.GetFileNameWithoutExtension(filePath).Split('.')[0];
Type type = assembly.GetTypes().SingleOrDefault(e => e.FullName.StartsWith($"{assembly.GetName().Name}.Events.") && e.Name == name);
if (type == null)
{
lstError.Add(new Exception($"[模型] 扫描到示例文件 \"{filePath}\",但类型 `{name}` 不存在。"));
return;
}
if (string.Equals(Path.GetExtension(filePath), ".json", StringComparison.OrdinalIgnoreCase))
{
string json = File.ReadAllText(filePath);
if (!TryJsonize(type, json, out Exception ex))
{
lstError.Add(ex);
}
}
else if (string.Equals(Path.GetExtension(filePath), ".xml", StringComparison.OrdinalIgnoreCase))
{
string xml = File.ReadAllText(filePath);
if (!TryXmlize(type, xml, out Exception ex))
{
lstError.Add(ex);
}
}
});
if (lstError.Any())
{
exception = new AggregateException(lstError);
return false;
}
exception = null;
return true;
}
public static bool VerifyApiExtensionsNaming(Assembly assembly, out Exception exception)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
var lstExtType = TestReflectionUtil.GetAllApiExtensionsTypes(assembly);
var lstError = new List<Exception>();
Parallel.ForEach(lstExtType, (extensionsType) =>
{
MethodInfo[] lstMethod = extensionsType.GetMethods()
.Where(e =>
e.IsPublic && e.IsStatic &&
typeof(ICommonClient).IsAssignableFrom(e.GetParameters().FirstOrDefault().ParameterType)
)
.ToArray();
foreach (MethodInfo methodInfo in lstMethod)
{
ParameterInfo[] lstParamInfo = methodInfo.GetParameters();
if (lstParamInfo.Length != 3)
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法需有且仅有 3 个入参。"));
continue;
}
if (!typeof(ICommonRequest).IsAssignableFrom(lstParamInfo[1].ParameterType))
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法第 1 个入参未实现 `{nameof(ICommonRequest)}`。"));
continue;
}
// 方法名与第二个参数、返回值均有相同命名
string func = methodInfo.Name;
string para = lstParamInfo[1].ParameterType.Name;
string retv = methodInfo.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
if (para == null || !para.EndsWith("Request"))
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法第 1 个入参类名未以 `Request` 结尾。"));
continue;
}
else if (retv == null || !retv.EndsWith("Response"))
{
if (!methodInfo.ReturnType.GenericTypeArguments.First().IsGenericType)
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法返回值类名未以 `Response` 结尾。"));
}
continue;
}
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法与请求模型不同名。"));
continue;
}
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
{
lstError.Add(new Exception($"[接口] `{extensionsType.Name}.{methodInfo.Name}` 方法与响应模型不同名。"));
continue;
}
}
});
if (lstError.Any())
{
exception = new AggregateException(lstError);
return false;
}
exception = null;
return true;
}
public static bool VerifySourceCodeStyle(string workdir, out Exception exception)
{
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
var lstError = new List<Exception>();
var lstExtensionsCodeFile = TestIOUtil.GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".cs", StringComparison.OrdinalIgnoreCase))
.Where(e => Path.GetDirectoryName(e).StartsWith(Path.Combine(workdir, "Extensions")))
.Where(e => Path.GetFileNameWithoutExtension(e).Contains("ClientExecute"))
.Where(e => Path.GetFileNameWithoutExtension(e).EndsWith("Extensions"))
.ToArray();
var lstModelsCodeFile = TestIOUtil.GetAllFiles(workdir)
.Where(e => string.Equals(Path.GetExtension(e), ".cs", StringComparison.OrdinalIgnoreCase))
.Where(e => Path.GetDirectoryName(e).StartsWith(Path.Combine(workdir, "Models")))
.Where(e => Path.GetFileNameWithoutExtension(e).EndsWith("Request") || Path.GetFileNameWithoutExtension(e).EndsWith("Response"))
.ToArray();
if (!lstExtensionsCodeFile.Any() || !lstModelsCodeFile.Any())
{
lstError.Add(new Exception($"[风格] \"{workdir}\" 下不存在源代码文件,请检查配置的扫描路径是否正确。"));
}
Parallel.ForEach(lstExtensionsCodeFile, (extCodeFilePath) =>
{
string extCodeFileName = Path.GetFileName(extCodeFilePath);
string[] segments = File.ReadAllText(extCodeFilePath)
.Split(new string[] { "<summary>" }, StringSplitOptions.RemoveEmptyEntries)
.Where(e => e.Contains("Async") && !e.Contains("public static class"))
.ToArray();
for (int i = 0; i < segments.Length; i++)
{
bool TryCheckExtensionSourceCode(string sourceCode, out (string Name, string Method, string Url)? request)
{
request = null;
// 匹配 <para> ... </para> 结构
var regexPara = new Regex("<para(([\\s\\S])*?)</para>").Match(sourceCode);
if (!regexPara.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释不齐全,未能匹配到 \"<para> ... </para>\"。"));
return false;
}
// 匹配 [...] ... 结构
var regexApi = new Regex("\\[(\\S*)\\]\\s*(\\S*)").Match(regexPara.Groups[1].Value);
if (!regexApi.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释不齐全,未能匹配到 \"异步调用 ... 接口\"。"));
return false;
}
// 比对请求谓词
string expectedMethod = regexApi.Groups[1].Value.Trim();
string expectedUrl = regexApi.Groups[2].Value.Split('?')[0].Trim();
string actualMethod = sourceCode.Contains(".CreateRequest(request, new HttpMethod(\"") ?
sourceCode.Split(new string[] { ".CreateRequest(request, new HttpMethod(\"" }, StringSplitOptions.None)[1].Split('\"')[0] :
sourceCode.Contains(".CreateRequest(request, HttpMethod.") ?
sourceCode.Split(new string[] { ".CreateRequest(request, HttpMethod." }, StringSplitOptions.None)[1].Split(',')[0].Split(')')[0] :
string.Empty;
if (!string.Equals(expectedMethod, actualMethod, StringComparison.OrdinalIgnoreCase))
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口谓词不一致。"));
return false;
}
// 比对请求路由
string actualUrl = sourceCode
.Split(new string[] { "CreateRequest(request," }, StringSplitOptions.RemoveEmptyEntries)[1]
.Substring(sourceCode.Split(new string[] { "CreateRequest(request," }, StringSplitOptions.RemoveEmptyEntries)[1].Split(',')[0].Length + 1)
.Split('\n')[0]
.Trim()
.TrimEnd(')', ';')
.Trim();
string[] expectedUrlSegments = expectedUrl.Split(new string[] { "/" }, StringSplitOptions.RemoveEmptyEntries);
string[] actualUrlSegments = actualUrl.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(e => e.Trim()).ToArray();
if (expectedUrlSegments.Length != actualUrlSegments.Length)
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:节长不等(实际 {actualUrlSegments.Length},期望 {expectedUrlSegments.Length})。"));
return false;
}
else
{
for (int urlSegmentIndex = 0; urlSegmentIndex < expectedUrlSegments.Length; urlSegmentIndex++)
{
string expectedUrlSegment = expectedUrlSegments[urlSegmentIndex].Trim('/');
string actualUrlSegment = actualUrlSegments[urlSegmentIndex].Trim('/');
if (expectedUrlSegment.Contains("{"))
{
if (actualUrlSegment.StartsWith("\""))
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:第 {urlSegmentIndex} 节值不同。"));
break;
}
}
else
{
actualUrlSegment = actualUrlSegment.Replace("\"", string.Empty).Trim('/');
if (!string.Equals(expectedUrlSegment, actualUrlSegment))
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段文档注释有误,`[{expectedMethod}] {expectedUrl}` 与实际接口路由不一致:第 {urlSegmentIndex} 节值不同。"));
break;
}
}
}
}
// 比对 .SendRequestAsync() 方法
if (!"GET".Equals(actualMethod, StringComparison.OrdinalIgnoreCase))
{
if (sourceCode.Contains("flurlReq, cancellationToken"))
{
lstError.Add(new Exception($"[风格] 源代码 \"{extCodeFileName}\" 下第 {i + 1} 段代码有误,`[{expectedMethod}] {expectedUrl}` 为非简单请求但不包含请求正文。"));
return false;
}
}
string apiName = new Regex("Execute([a-zA-Z0-9]*)Async").Match(sourceCode).Groups[1].Value;
request = (apiName, expectedMethod, expectedUrl);
return true;
}
bool TryCheckRequestModelSourceCode(string filePath, string expectedRequestMethod, string expectedRequestUrl)
{
if (!File.Exists(filePath))
{
lstError.Add(new Exception($"[风格] 源代码 \"{filePath}\" 不存在。"));
return false;
}
string reqCodeFileName = Path.GetFileName(filePath);
string reqCodeSourceCode = File.ReadAllText(filePath);
// 匹配 <para> ... </para> 结构
var regexPara = new Regex("<para(([\\s\\S])*?)</para>").Match(reqCodeSourceCode);
if (!regexPara.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下文档注释不齐全,未能匹配到 \"<para> ... </para>\"。"));
return false;
}
// 匹配 [...] ... 结构
var regexApi = new Regex("\\[(\\S*)\\]\\s*(\\S*)").Match(regexPara.Groups[1].Value);
if (!regexApi.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下文档注释不齐全,未能匹配到 \"异步调用 ... 接口\"。"));
return false;
}
// 比对请求谓词
string actualMethod = regexApi.Groups[1].Value.Trim();
if (!string.Equals(expectedRequestMethod, actualMethod, StringComparison.OrdinalIgnoreCase))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下文档注释有误,与实际接口谓词不一致。"));
return false;
}
// 对比请求路由
string actualUrl = regexApi.Groups[2].Value.Split('?')[0].Trim();
if (!string.Equals(expectedRequestUrl, actualUrl, StringComparison.OrdinalIgnoreCase))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下文档注释有误,与实际接口路由不一致。"));
return false;
}
// 检验是否包含 `default!` 的赋值
if (new Regex("=\\s*default!").IsMatch(reqCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下代码有误,请求模型不应包含 `= default!` 赋值。"));
return false;
}
// 检验是否包含数组类型字段
if (new Regex("public\\s*[a-zA-Z0-9.]*\\[\\]\\s*[a-zA-Z0-9]*\\s*{\\s*get;\\s*set;\\s*}").IsMatch(reqCodeSourceCode.Replace("byte[]", string.Empty).Replace("Byte[]", string.Empty)))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下代码有误,请求模型不应包含数组类型字段。"));
return false;
}
// 校验是否包含可空字段的初始化
if (new Regex("\\? [a-zA-Z0-9]* \\{ get; set; \\} =").IsMatch(reqCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下代码有误,可空字段不应初始化。"));
return false;
}
// 如果是 GET 请求,检查是否包含 JSON 序列化字段
if ("GET".Equals(expectedRequestMethod, StringComparison.OrdinalIgnoreCase))
{
if (!(CODESTYLE_RULE_REGEX_NO_JSONABLE_PROPERTY_IN_REQUEST.IsMatch(reqCodeSourceCode)))
{
if (new Regex("\\[Newtonsoft.Json.JsonProperty\\(\"[a-zA-Z0-9_]*\"\\)\\]").IsMatch(reqCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下代码有误,请求模型为简单请求、不应包含可 JSON 序列化字段。"));
return false;
}
if (new Regex("\\[System.Text.Json.Serialization.JsonPropertyName\\(\"[a-zA-Z0-9_]*\"\\)\\]").IsMatch(reqCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{reqCodeFileName}\" 下代码有误,请求模型为简单请求、不应包含可 JSON 序列化字段。"));
return false;
}
}
}
return true;
}
bool TryCheckResponseModelSourceCode(string filePath, string expectedRequestMethod, string expectedRequestUrl)
{
if (!File.Exists(filePath))
{
lstError.Add(new Exception($"[风格] 源代码 \"{filePath}\" 不存在。"));
return false;
}
string resCodeFileName = Path.GetFileName(filePath);
string resCodeSourceCode = File.ReadAllText(filePath);
// 匹配 <para> ... </para> 结构
var regexPara = new Regex("<para(([\\s\\S])*?)</para>").Match(resCodeSourceCode);
if (!regexPara.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下文档注释不齐全,未能匹配到 \"<para> ... </para>\"。"));
return false;
}
// 匹配 [...] ... 结构
var regexApi = new Regex("\\[(\\S*)\\]\\s*(\\S*)").Match(regexPara.Groups[1].Value);
if (!regexApi.Success)
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下文档注释不齐全,未能匹配到 \"异步调用 ... 接口\"。"));
return false;
}
// 比对请求谓词
string actualMethod = regexApi.Groups[1].Value.Trim();
if (!string.Equals(expectedRequestMethod, actualMethod, StringComparison.OrdinalIgnoreCase))
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下文档注释有误,与实际接口谓词不一致。"));
return false;
}
// 对比请求路由
string actualUrl = regexApi.Groups[2].Value.Split('?')[0].Trim();
if (!string.Equals(expectedRequestUrl, actualUrl, StringComparison.OrdinalIgnoreCase))
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下文档注释有误,与实际接口路由不一致。"));
return false;
}
// 检验是否包含 `string.Empty` 的赋值
if (new Regex("=\\s*string.Empty").IsMatch(resCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下代码有误,响应模型不应包含 `= string.Empty` 赋值。"));
return false;
}
// 检验是否包含 `new class()` 的赋值
if (new Regex("=\\s*new\\s[a-zA-Z0-9.]*\\(\\)").IsMatch(resCodeSourceCode))
{
if (!(CODESTYLE_RULE_REGEX_NO_INSTANTIATED_PROPERTY_IN_RESPONSE.IsMatch(resCodeSourceCode)))
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下代码有误,响应模型不应包含 `= new class()` 赋值。"));
return false;
}
}
// 检验是否包含列表类型字段
if (new Regex("public\\s*IList<[a-zA-Z0-9.]*>\\s*[a-zA-Z0-9]*\\s*{\\s*get;\\s*set;\\s*}").IsMatch(resCodeSourceCode))
{
lstError.Add(new Exception($"[风格] 源代码 \"{resCodeFileName}\" 下代码有误,响应模型不应包含列表类型字段。"));
return false;
}
return true;
}
if (!TryCheckExtensionSourceCode(segments[i], out var request))
{
continue;
}
else
{
string reqModelFileName = lstModelsCodeFile.FirstOrDefault(e => string.Equals(Path.GetFileNameWithoutExtension(e), $"{request.Value.Name}Request"));
string resModelFileName = lstModelsCodeFile.FirstOrDefault(e => string.Equals(Path.GetFileNameWithoutExtension(e), $"{request.Value.Name}Response"));
bool isValidReq = TryCheckRequestModelSourceCode(reqModelFileName, request.Value.Method, request.Value.Url);
bool isValidRes = TryCheckResponseModelSourceCode(resModelFileName, request.Value.Method, request.Value.Url);
if (!isValidReq || !isValidRes)
{
continue;
}
}
}
});
if (lstError.Any())
{
exception = new AggregateException(lstError);
return false;
}
exception = null;
return true;
}
}
}

View File

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SKIT.FlurlHttpClient.Common" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace SKIT.FlurlHttpClient.Wechat
{
public static class TestIOUtil
{
public static string[] GetAllFiles(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
List<string> results = new List<string>();
string[] dirs = Directory.GetDirectories(path);
string[] files = Directory.GetFiles(path);
results.AddRange(files);
foreach (string dir in dirs)
{
results.AddRange(GetAllFiles(dir));
}
return results.ToArray();
}
}
}

View File

@ -1,179 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace SKIT.FlurlHttpClient.Wechat
{
public static class TestReflectionUtil
{
public static Type[] GetAllApiModelsTypes(Assembly assembly)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
return assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(assembly.GetName().Name + ".Models") &&
e.IsClass &&
!e.IsAbstract &&
!e.IsInterface &&
!e.IsNested
)
.ToArray();
}
public static Type[] GetAllApiExtensionsTypes(Assembly assembly)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
return assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(assembly.GetName().Name) &&
e.Name.StartsWith("Wechat") &&
e.Name.Contains("ClientExecute") &&
e.Name.EndsWith("Extensions")
)
.ToArray();
}
public static Type[] GetAllApiEventsTypes(Assembly assembly)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
return assembly.GetTypes()
.Where(e =>
e.Namespace != null &&
e.Namespace.Equals(assembly.GetName().Name + ".Events") &&
e.IsClass &&
!e.IsAbstract &&
!e.IsInterface &&
!e.IsNested
)
.ToArray();
}
public static PropertyInfo[] GetAllProperties(Type type)
{
if (type == null) throw new ArgumentNullException(nameof(type));
static Type[] GetAllNestedTypes(Type externalType)
{
List<Type> lstType = new List<Type>();
foreach (Type innerType in externalType.GetNestedTypes())
{
if (innerType.IsClass)
{
lstType.Add(innerType);
lstType.AddRange(GetAllNestedTypes(innerType));
}
}
return lstType
.Where(e =>
!e.IsAbstract &&
e.IsNestedPublic
)
.ToArray();
}
var lstProperty = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
GetAllNestedTypes(type)
.ToList()
.ForEach(e =>
{
lstProperty.AddRange(GetAllProperties(e));
});
return lstProperty.Distinct().ToArray();
}
public static object InitializeProperties(object obj)
{
const int MAX_DEPTH = 10; // 防止无限递归
int CUR_DEPTH = 0;
Func<object, object> func = null;
func = new Func<object, object>((obj) =>
{
CUR_DEPTH++;
if (CUR_DEPTH >= MAX_DEPTH)
return obj;
PropertyInfo[] lstPropInfo = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo propInfo in lstPropInfo)
{
if (propInfo.SetMethod == null || !propInfo.SetMethod.IsPublic)
continue;
if (propInfo.PropertyType.IsPrimitive)
{
// noop
}
else if (propInfo.PropertyType.IsArray)
{
if (propInfo.PropertyType.FullName.Contains("[][]"))
{
// noop
}
else
{
Type elType = propInfo.PropertyType.Assembly.GetType(propInfo.PropertyType.FullName.Replace("[]", string.Empty));
object elObj = (elType == typeof(string)) ? string.Empty : Activator.CreateInstance(elType);
elObj = Convert.ChangeType(elObj, elType);
func(elObj);
Array prop = Array.CreateInstance(elType, 1);
prop.SetValue(elObj, 0);
propInfo.SetValue(obj, prop);
}
}
else if (propInfo.PropertyType == typeof(string))
{
propInfo.SetValue(obj, string.Empty);
}
else if (propInfo.PropertyType.Namespace == "System" &&
propInfo.PropertyType.Name.StartsWith("Nullable"))
{
// noop
}
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
(propInfo.PropertyType.Name.StartsWith("IDictionary") || propInfo.PropertyType.Name.StartsWith("Dictionary")))
{
// noop
}
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
(propInfo.PropertyType.Name.StartsWith("IList") || propInfo.PropertyType.Name.StartsWith("List")))
{
Type elElementType = propInfo.PropertyType.GetGenericArguments().Single();
object elElementObj = (elElementType == typeof(string)) ? string.Empty : Activator.CreateInstance(elElementType);
elElementObj = Convert.ChangeType(elElementObj, elElementType);
func(elElementObj);
Type elListType = typeof(List<>).MakeGenericType(new Type[] { elElementType });
object elListObj = Activator.CreateInstance(elListType);
elListType.GetMethod("Add").Invoke(elListObj, new[] { elElementObj });
propInfo.SetValue(obj, elListObj);
}
else
{
object elObj = Activator.CreateInstance(propInfo.PropertyType);
func(elObj);
propInfo.SetValue(obj, elObj);
}
}
return obj;
});
return func(obj);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472; netcoreapp3.1; net6.0</TargetFrameworks>
<TargetFrameworks>net472; net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<NullableReferenceTypes>true</NullableReferenceTypes>
@ -24,7 +24,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="SKIT.FlurlHttpClient.Tools.CodeAnalyzer" Version="0.1.0-alpha.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
@ -34,7 +35,6 @@
<ItemGroup>
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj" />
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using SKIT.FlurlHttpClient.Tools.CodeAnalyzer;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{
public class TestCase_CodeReview
{
[Fact(DisplayName = "测试用例:代码质量分析")]
public void TestCodeAnalyzer()
{
Assert.Null(Record.Exception(() =>
{
CodeAnalyzerOptions options = new CodeAnalyzerOptions()
{
AssemblyName = "SKIT.FlurlHttpClient.Wechat.Work",
WorkDirectoryForSourceCode = TestConfigs.WorkDirectoryForSdk,
WorkDirectoryForTestSample = TestConfigs.WorkDirectoryForTest,
AllowNotFoundEventTypes = true,
AllowNotFoundEventSamples = true
};
CodeAnalyzer analyzer = new CodeAnalyzer(options);
analyzer.Start();
analyzer.Assert();
analyzer.Flush();
}));
}
}
}

View File

@ -1,68 +0,0 @@
using System.IO;
using System.Reflection;
using Xunit;
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
{
public class TestCase_CodeReviewAnalyzer
{
private Assembly SourceAssembly { get; } = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Work");
[Fact(DisplayName = "代码评审:分析 API 模型命名")]
public void TestApiModelsNaming()
{
CodeStyleUtil.VerifyApiModelsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 模型定义")]
public void TestApiModelsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "ModelSamples");
CodeStyleUtil.VerifyApiModelsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 事件定义")]
public void TestApiEventsDefinition()
{
string workdir = Path.Combine(TestConfigs.ProjectTestDirectory, "EventSamples");
CodeStyleUtil.VerifyApiEventsDefinition(SourceAssembly, workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析 API 接口命名")]
public void TestApiExtensionsNaming()
{
CodeStyleUtil.VerifyApiExtensionsNaming(SourceAssembly, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
[Fact(DisplayName = "代码评审:分析代码规范")]
public void TestCodeStyle()
{
string workdir = Path.Combine(TestConfigs.ProjectSourceDirectory);
CodeStyleUtil.VerifySourceCodeStyle(workdir, out var ex);
if (ex != null)
throw ex;
Assert.Null(ex);
}
}
}

View File

@ -22,8 +22,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
WechatAgentId = int.Parse(config.GetProperty("AgentId").GetString())!;
WechatAgentSecret = config.GetProperty("AgentSecret").GetString()!;
ProjectSourceDirectory = jdoc.RootElement.GetProperty("ProjectSourceDirectory").GetString()!;
ProjectTestDirectory = jdoc.RootElement.GetProperty("ProjectTestDirectory").GetString()!;
WorkDirectoryForSdk = jdoc.RootElement.GetProperty("WorkDirectoryForSdk").GetString()!;
WorkDirectoryForTest = jdoc.RootElement.GetProperty("WorkDirectoryForTest").GetString()!;
}
catch (Exception ex)
{
@ -35,7 +35,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
public static readonly int WechatAgentId;
public static readonly string WechatAgentSecret;
public static readonly string ProjectSourceDirectory;
public static readonly string ProjectTestDirectory;
public static readonly string WorkDirectoryForSdk;
public static readonly string WorkDirectoryForTest;
}
}

View File

@ -4,6 +4,6 @@
"AgentId": "请在此填写用于测试的企业微信 AgentId",
"AgentSecret": "请在此填写用于测试的企业微信 AgentSecret"
},
"ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Work\\",
"ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\\"
"WorkDirectoryForSdk": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Work\\",
"WorkDirectoryForTest": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\\"
}