Skip to content

Commit 164ac0b

Browse files
add test infra & visitor tests
1 parent 1a565f5 commit 164ac0b

13 files changed

+1189
-1
lines changed

.github/workflows/main.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,15 @@ jobs:
4343
--logger "trx;LogFilePrefix=smoke"
4444
--results-directory ${{github.workspace}}/artifacts/test-results
4545
${{ env.version_suffix_args}}
46+
47+
- name: Run codegen visitor tests
48+
run: dotnet test codegen/generator/test/
49+
--configuration Release
50+
--logger "trx;LogFilePrefix=codegen"
51+
--results-directory ${{github.workspace}}/artifacts/test-results
52+
${{ env.version_suffix_args}}
4653

47-
- name: Upload artifact
54+
- name: Upload artifacts
4855
uses: actions/upload-artifact@v4
4956
if: ${{ !cancelled() }}
5057
with:

codegen/generator/OpenAI.Library.Plugin.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ VisualStudioVersion = 17.11.35327.3
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenAI.Library.Plugin", "src\OpenAI.Library.Plugin.csproj", "{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests", "test\OpenAI.Library.Plugin.Tests.csproj", "{8502C759-8CE7-418D-9C5B-49ADECFCD79C}"
9+
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAI.Library.Plugin.Tests.Common", "test\common\OpenAI.Library.Plugin.Tests.Common.csproj", "{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}"
11+
EndProject
812
Global
913
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1014
Debug|Any CPU = Debug|Any CPU
@@ -15,8 +19,19 @@ Global
1519
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
1620
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
1721
{E46178E4-F3F0-4E2F-8D42-A7F021B23E63}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{8502C759-8CE7-418D-9C5B-49ADECFCD79C}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{666F7CD4-4D78-460A-AA9B-A2981EF96AD9}.Release|Any CPU.Build.0 = Release|Any CPU
1830
EndGlobalSection
1931
GlobalSection(SolutionProperties) = preSolution
2032
HideSolutionNode = FALSE
2133
EndGlobalSection
34+
GlobalSection(ExtensibilityGlobals) = postSolution
35+
SolutionGuid = {F0115F71-1DEE-403B-99F9-E1F06D6B5271}
36+
EndGlobalSection
2237
EndGlobal
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net9.0</TargetFrameworks>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\src\OpenAI.Library.Plugin.csproj" />
13+
<ProjectReference Include="common\OpenAI.Library.Plugin.Tests.Common.csproj" />
14+
<PackageReference Include="NUnit" Version="4.4.0" />
15+
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
16+
<PackageReference Include="Moq" Version="[4.18.2]" />
17+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<None Update="TestHelpers\Configuration.json">
22+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23+
</None>
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<Compile Remove="**\TestData\**\*.cs" />
28+
<Compile Remove="common\**" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<None Include="**\TestData\**\*.cs">
33+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
34+
</None>
35+
</ItemGroup>
36+
37+
</Project>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"output-folder": "./outputFolder",
3+
"project-folder": "./projectFolder",
4+
"unknown-bool-property": false,
5+
"package-name": "Samples",
6+
"unknown-string-property": "unknownPropertyValue"
7+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using Microsoft.TypeSpec.Generator;
2+
using Microsoft.TypeSpec.Generator.ClientModel;
3+
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
4+
using Microsoft.TypeSpec.Generator.Input;
5+
using Microsoft.TypeSpec.Generator.Primitives;
6+
using Microsoft.TypeSpec.Generator.Providers;
7+
using Microsoft.TypeSpec.Generator.SourceInput;
8+
using Moq;
9+
using Moq.Protected;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.IO;
13+
using System.Reflection;
14+
15+
namespace OpenAILibraryPlugin.Tests.TestHelpers
16+
{
17+
internal class MockHelpers
18+
{
19+
private static readonly string _configFilePath = Path.Combine(AppContext.BaseDirectory, TestHelpersFolder);
20+
private const string TestHelpersFolder = "TestHelpers";
21+
22+
public static Mock<ScmCodeModelGenerator> LoadMockGenerator(
23+
Func<InputType, TypeProvider, IReadOnlyList<TypeProvider>>? createSerializationsCore = null,
24+
Func<InputType, CSharpType>? createCSharpTypeCore = null,
25+
Func<InputApiKeyAuth>? apiKeyAuth = null,
26+
Func<InputOAuth2Auth>? oauth2Auth = null,
27+
Func<IReadOnlyList<string>>? apiVersions = null,
28+
Func<IReadOnlyList<InputLiteralType>>? inputLiterals = null,
29+
Func<IReadOnlyList<InputEnumType>>? inputEnums = null,
30+
Func<IReadOnlyList<InputModelType>>? inputModels = null,
31+
Func<IReadOnlyList<InputClient>>? clients = null,
32+
Func<InputClient, ClientProvider?>? createClientCore = null,
33+
ClientResponseApi? clientResponseApi = null,
34+
ClientPipelineApi? clientPipelineApi = null,
35+
HttpMessageApi? httpMessageApi = null,
36+
string? configurationJson = null,
37+
string? inputNamespace = null)
38+
{
39+
IReadOnlyList<string> inputNsApiVersions = apiVersions?.Invoke() ?? [];
40+
IReadOnlyList<InputLiteralType> inputNsLiterals = inputLiterals?.Invoke() ?? [];
41+
IReadOnlyList<InputEnumType> inputNsEnums = inputEnums?.Invoke() ?? [];
42+
IReadOnlyList<InputClient> inputNsClients = clients?.Invoke() ?? [];
43+
IReadOnlyList<InputModelType> inputNsModels = inputModels?.Invoke() ?? [];
44+
InputAuth inputNsAuth = new InputAuth(apiKeyAuth?.Invoke(), oauth2Auth?.Invoke());
45+
var mockInputNs = new Mock<InputNamespace>(
46+
inputNamespace ?? "Samples",
47+
inputNsApiVersions,
48+
inputNsLiterals,
49+
inputNsEnums,
50+
inputNsModels,
51+
inputNsClients,
52+
inputNsAuth);
53+
var mockInputLibrary = new Mock<InputLibrary>(_configFilePath);
54+
mockInputLibrary.Setup(p => p.InputNamespace).Returns(mockInputNs.Object);
55+
56+
Mock<ScmTypeFactory>? mockTypeFactory = null;
57+
if (createCSharpTypeCore is not null)
58+
{
59+
mockTypeFactory = new Mock<ScmTypeFactory>() { CallBase = true };
60+
mockTypeFactory.Protected().Setup<CSharpType>("CreateCSharpTypeCore", ItExpr.IsAny<InputType>()).Returns(createCSharpTypeCore);
61+
}
62+
63+
if (createClientCore is not null)
64+
{
65+
mockTypeFactory ??= new Mock<ScmTypeFactory>() { CallBase = true };
66+
mockTypeFactory.Protected().Setup<ClientProvider?>("CreateClientCore", ItExpr.IsAny<InputClient>()).Returns(createClientCore);
67+
}
68+
69+
// initialize the mock singleton instance of the plugin
70+
var codeModelInstance = typeof(CodeModelGenerator).GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic);
71+
// invoke the load method with the config file path
72+
var loadMethod = typeof(Configuration).GetMethod("Load", BindingFlags.Static | BindingFlags.NonPublic);
73+
object?[] parameters = [_configFilePath, configurationJson];
74+
var config = loadMethod?.Invoke(null, parameters);
75+
var mockGeneratorContext = new Mock<GeneratorContext>(config!);
76+
var mockGeneratorInstance = new Mock<ScmCodeModelGenerator>(mockGeneratorContext.Object) { CallBase = true };
77+
codeModelInstance!.SetValue(null, mockGeneratorInstance.Object);
78+
mockGeneratorInstance.SetupGet(p => p.InputLibrary).Returns(mockInputLibrary.Object);
79+
80+
if (mockTypeFactory is not null)
81+
{
82+
mockGeneratorInstance.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object);
83+
}
84+
85+
var sourceInputModel = new Mock<SourceInputModel>(() => new SourceInputModel(null, null)) { CallBase = true };
86+
mockGeneratorInstance.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object);
87+
var configureMethod = typeof(CodeModelGenerator).GetMethod(
88+
"Configure",
89+
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod
90+
);
91+
configureMethod!.Invoke(mockGeneratorInstance.Object, null);
92+
return mockGeneratorInstance;
93+
}
94+
95+
public static void SetCustomCodeView(ModelProvider modelProvider, TypeProvider customCodeTypeProvider)
96+
{
97+
modelProvider.GetType().BaseType!.GetField(
98+
"_customCodeView",
99+
BindingFlags.NonPublic | BindingFlags.Instance)?
100+
.SetValue(modelProvider, new Lazy<TypeProvider>(() => customCodeTypeProvider));
101+
}
102+
}
103+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Microsoft.TypeSpec.Generator.ClientModel;
2+
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
3+
using Microsoft.TypeSpec.Generator.Input;
4+
using Microsoft.TypeSpec.Generator.Providers;
5+
using NUnit.Framework;
6+
using OpenAILibraryPlugin.Tests.Common;
7+
using OpenAILibraryPlugin.Tests.TestHelpers;
8+
using System.Linq;
9+
10+
namespace OpenAILibraryPlugin.Tests.Visitors
11+
{
12+
[Category("Visitor")]
13+
public class OpenAILibraryVisitorTests
14+
{
15+
[SetUp]
16+
public void Setup()
17+
{
18+
MockHelpers.LoadMockGenerator(configurationJson: "{ \"package-name\": \"TestLibrary\" }");
19+
}
20+
21+
// This test validates that the serialization is updated correctly for both dynamic and non-dynamic models.
22+
[TestCase(true)]
23+
[TestCase(false)]
24+
public void TestVisitMethod_JsonModelWriteCore(bool isDynamicModel)
25+
{
26+
var visitor = new TestOpenAILibraryVisitor();
27+
28+
var inputType = InputFactory.Model("TestModel", "Samples", isDynamicModel: isDynamicModel, properties: [
29+
InputFactory.Property("cat", InputPrimitiveType.String),
30+
InputFactory.Property("requiredDog", InputPrimitiveType.String, isRequired: true)
31+
]);
32+
var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputType);
33+
Assert.That(model, Is.Not.Null);
34+
35+
var jsonWriteCoreMethod = model!.SerializationProviders
36+
.OfType<MrwSerializationTypeDefinition>()
37+
.FirstOrDefault()?
38+
.Methods
39+
.OfType<MethodProvider>()
40+
.First(m => m.Signature.Name == "JsonModelWriteCore");
41+
Assert.That(jsonWriteCoreMethod, Is.Not.Null);
42+
43+
// Invoke the visitor
44+
jsonWriteCoreMethod = visitor.InvokeVisitMethod(jsonWriteCoreMethod!);
45+
Assert.That(jsonWriteCoreMethod!.BodyStatements, Is.Not.Null);
46+
47+
var methodBody = jsonWriteCoreMethod!.BodyStatements!.ToDisplayString();
48+
Assert.That(methodBody, Is.EqualTo(Helpers.GetExpectedFromFile(isDynamicModel.ToString())));
49+
}
50+
51+
// This test validates that the serialization for known properties that should have additional conditions
52+
// is updated correctly.
53+
[TestCase(true)]
54+
[TestCase(false)]
55+
public void TestVisitMethod_JsonModelWriteCore_CustomConditions(bool isDynamicModel)
56+
{
57+
var visitor = new TestOpenAILibraryVisitor();
58+
59+
var inputType = InputFactory.Model("ChatCompletionOptions", "Samples", isDynamicModel: isDynamicModel, properties: [
60+
InputFactory.Property("model", InputPrimitiveType.String, isRequired: true),
61+
]);
62+
var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputType);
63+
Assert.That(model, Is.Not.Null);
64+
65+
var jsonWriteCoreMethod = model!.SerializationProviders
66+
.OfType<MrwSerializationTypeDefinition>()
67+
.FirstOrDefault()?
68+
.Methods
69+
.OfType<MethodProvider>()
70+
.First(m => m.Signature.Name == "JsonModelWriteCore");
71+
Assert.That(jsonWriteCoreMethod, Is.Not.Null);
72+
73+
// Invoke the visitor
74+
jsonWriteCoreMethod = visitor.InvokeVisitMethod(jsonWriteCoreMethod!);
75+
Assert.That(jsonWriteCoreMethod!.BodyStatements, Is.Not.Null);
76+
77+
var methodBody = jsonWriteCoreMethod!.BodyStatements!.ToDisplayString();
78+
Assert.That(methodBody, Is.EqualTo(Helpers.GetExpectedFromFile(isDynamicModel.ToString())));
79+
}
80+
81+
private class TestOpenAILibraryVisitor : OpenAILibraryVisitor
82+
{
83+
public MethodProvider? InvokeVisitMethod(MethodProvider method)
84+
{
85+
return base.VisitMethod(method);
86+
}
87+
}
88+
89+
private class TestTypeProvider : TypeProvider
90+
{
91+
protected override string BuildNamespace() => "Samples";
92+
93+
protected override string BuildRelativeFilePath() => $"{Name}.cs";
94+
95+
protected override string BuildName() => "TestModel";
96+
}
97+
}
98+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Samples.TestModel>)this).GetFormatFromOptions(options) : options.Format;
2+
if ((format != "J"))
3+
{
4+
throw new global::System.FormatException($"The model {nameof(global::Samples.TestModel)} does not support writing '{format}' format.");
5+
}
6+
if ((global::Samples.Optional.IsDefined(Cat) && (this._additionalBinaryDataProperties?.ContainsKey("cat") != true)))
7+
{
8+
writer.WritePropertyName("cat"u8);
9+
writer.WriteStringValue(Cat);
10+
}
11+
if ((this._additionalBinaryDataProperties?.ContainsKey("requiredDog") != true))
12+
{
13+
writer.WritePropertyName("requiredDog"u8);
14+
writer.WriteStringValue(RequiredDog);
15+
}
16+
if (((options.Format != "W") && (_additionalBinaryDataProperties != null)))
17+
{
18+
foreach (var item in _additionalBinaryDataProperties)
19+
{
20+
if (global::Samples.ModelSerializationExtensions.IsSentinelValue(item.Value))
21+
{
22+
continue;
23+
}
24+
writer.WritePropertyName(item.Key);
25+
#if NET6_0_OR_GREATER
26+
writer.WriteRawValue(item.Value);
27+
#else
28+
using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value))
29+
{
30+
global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement);
31+
}
32+
#endif
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Samples.TestModel>)this).GetFormatFromOptions(options) : options.Format;
2+
if ((format != "J"))
3+
{
4+
throw new global::System.FormatException($"The model {nameof(global::Samples.TestModel)} does not support writing '{format}' format.");
5+
}
6+
#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
7+
if ((global::Samples.Optional.IsDefined(Cat) && !Patch.Contains("$.cat"u8)))
8+
{
9+
writer.WritePropertyName("cat"u8);
10+
writer.WriteStringValue(Cat);
11+
}
12+
if (!Patch.Contains("$.requiredDog"u8))
13+
{
14+
writer.WritePropertyName("requiredDog"u8);
15+
writer.WriteStringValue(RequiredDog);
16+
}
17+
18+
Patch.WriteTo(writer0);
19+
#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel<global::Samples.ChatCompletionOptions>)this).GetFormatFromOptions(options) : options.Format;
2+
if ((format != "J"))
3+
{
4+
throw new global::System.FormatException($"The model {nameof(global::Samples.ChatCompletionOptions)} does not support writing '{format}' format.");
5+
}
6+
// Plugin customization: apply Optional.Is*Defined() check based on type name dictionary lookup
7+
if ((Optional.IsDefined(Model) && (this._additionalBinaryDataProperties?.ContainsKey("model") != true)))
8+
{
9+
writer.WritePropertyName("model"u8);
10+
writer.WriteStringValue(Model);
11+
}
12+
if (((options.Format != "W") && (_additionalBinaryDataProperties != null)))
13+
{
14+
foreach (var item in _additionalBinaryDataProperties)
15+
{
16+
if (global::Samples.ModelSerializationExtensions.IsSentinelValue(item.Value))
17+
{
18+
continue;
19+
}
20+
writer.WritePropertyName(item.Key);
21+
#if NET6_0_OR_GREATER
22+
writer.WriteRawValue(item.Value);
23+
#else
24+
using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value))
25+
{
26+
global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement);
27+
}
28+
#endif
29+
}
30+
}

0 commit comments

Comments
 (0)