tools/apiview/parsers/csharp-api-parser/CSharpAPIParserTests/CodeFileTests.cs (339 lines of code) (raw):
using System.Reflection;
using ApiView;
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Linq;
using System.Text.Json.Serialization;
using APIView.Model.V2;
using Microsoft.CodeAnalysis;
using NuGet.ContentModel;
namespace CSharpAPIParserTests
{
public class CodeFileTests
{
static CodeFile templateCodeFile;
static Assembly templateAssembly { get; set; }
static CodeFile storageCodeFile;
static Assembly storageAssembly { get; set; }
static CodeFile coreCodeFile;
static Assembly coreAssembly { get; set; }
public CodeFileTests() { }
static CodeFileTests()
{
templateAssembly = Assembly.Load("Azure.Template");
var dllStream = templateAssembly.GetFile("Azure.Template.dll");
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
templateCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
storageAssembly = Assembly.Load("Azure.Storage.Blobs");
dllStream = storageAssembly.GetFile("Azure.Storage.Blobs.dll");
assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
storageCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
coreAssembly = Assembly.Load("Azure.Core");
dllStream = coreAssembly.GetFile("Azure.Core.dll");
assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
coreCodeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
}
public static IEnumerable<object[]> CodeFiles => new List<object[]>
{
new object[] { templateCodeFile, "Azure.Template" , "1.0.3.0", 9},
new object[] { storageCodeFile , "Azure.Storage.Blobs", "12.21.2.0", 15},
new object[] { coreCodeFile, "Azure.Core", "1.44.1.0", 27},
};
[Theory]
[MemberData(nameof(CodeFiles))]
public void TestPackageMetadata(CodeFile codeFile, string expectedPackageName, string expectedVersion, int expectedNumberOfTopLines)
{
Assert.Equal(expectedPackageName, codeFile.PackageName);
Assert.Equal(expectedVersion, codeFile.PackageVersion);
Assert.Equal("C#", codeFile.Language);
Assert.Equal(expectedNumberOfTopLines, codeFile.ReviewLines.Count);
}
[Fact]
public void TestClassReviewLineWithoutBase()
{
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient").FirstOrDefault();
Assert.NotNull(classLine);
Assert.Equal(4, classLine.Tokens.Count());
Assert.Equal("public class BlobServiceClient {", classLine.ToString().Trim());
}
[Fact]
public void TestClassReviewLineWithBase()
{
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs.Models").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.Models.BlobDownloadInfo").FirstOrDefault();
Assert.NotNull(classLine);
Assert.Equal(6, classLine.Tokens.Count());
Assert.Equal("public class BlobDownloadInfo : IDisposable {", classLine.ToString().Trim());
}
[Fact]
public void TestMultipleKeywords()
{
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs.Models").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.Models.AccessTier").FirstOrDefault();
Assert.NotNull(classLine);
Assert.Equal(10, classLine.Tokens.Count());
Assert.Equal("public readonly struct AccessTier : IEquatable<AccessTier> {", classLine.ToString().Trim());
}
[Fact]
public void TestApiReviewLine()
{
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient").FirstOrDefault();
Assert.NotNull(classLine);
var methodLine = classLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient.BlobServiceClient(System.String)").FirstOrDefault();
Assert.NotNull(methodLine);
Assert.Equal(7, methodLine.Tokens.Count());
Assert.Equal("public BlobServiceClient(string connectionString);", methodLine.ToString().Trim());
}
[Fact]
public void TestApiReviewLineMoreParams()
{
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient").FirstOrDefault();
Assert.NotNull(classLine);
var methodLine = classLine.Children.Where(lines => lines.LineId.Contains("UndeleteBlobContainerAsync")).FirstOrDefault();
Assert.NotNull(methodLine);
Assert.Equal(23, methodLine.Tokens.Count);
Assert.Equal("public virtual Task<Response<BlobContainerClient>> UndeleteBlobContainerAsync(string deletedContainerName, string deletedContainerVersion, CancellationToken cancellationToken = default);", methodLine.ToString().Trim());
}
public static IEnumerable<object[]> PackageCodeFiles => new List<object[]>
{
new object[] { templateCodeFile },
new object[] { storageCodeFile },
new object[] { coreCodeFile }
};
[Theory]
[MemberData(nameof(PackageCodeFiles))]
public void TestAllClassesHaveEndOfContextLine(CodeFile codeFile)
{
// If current line is for class then next line at same level is expected to be a end of context line
var lines = codeFile.ReviewLines;
foreach(var namespaceLine in lines)
{
Assert.NotNull(namespaceLine);
bool expectEndOfContext = false;
var classLines = namespaceLine.Children;
for (int i = 0; i < classLines.Count; i++)
{
if (expectEndOfContext)
{
Assert.True(classLines[i].IsContextEndLine == true);
expectEndOfContext = false;
continue;
}
expectEndOfContext = classLines[i].Tokens.Any(t => (t.RenderClasses.Contains("class") ||
t.RenderClasses.Contains("struct") ||
t.RenderClasses.Contains("interface")) && !classLines[i].Tokens.Any(t => t.Value == "abstract"));
}
}
}
[Fact]
public void TestHiddenAPI()
{
var apiText = "protected static BlobServiceClient CreateClient(Uri serviceUri, BlobClientOptions options, HttpPipelinePolicy authentication, HttpPipeline pipeline);";
var lines = storageCodeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Storage.Blobs").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient").FirstOrDefault();
Assert.NotNull(classLine);
var hiddenApis = classLine.Children.Where(lines => lines.LineId == "Azure.Storage.Blobs.BlobServiceClient.CreateClient(System.Uri, Azure.Storage.Blobs.BlobClientOptions, Azure.Core.Pipeline.HttpPipelinePolicy, Azure.Core.Pipeline.HttpPipeline)").FirstOrDefault();
Assert.NotNull(hiddenApis);
Assert.Equal(18, hiddenApis.Tokens.Count());
Assert.Equal(apiText, hiddenApis.ToString().Trim());
}
[Fact]
public void TestAPIReviewContent()
{
string expected = @"namespace Azure.Template {
public class TemplateClient {
public TemplateClient(string vaultBaseUrl, TokenCredential credential);
public TemplateClient(string vaultBaseUrl, TokenCredential credential, TemplateClientOptions options);
protected TemplateClient();
public virtual HttpPipeline Pipeline { get; }
public virtual Response GetSecret(string secretName, RequestContext context);
public virtual Task<Response> GetSecretAsync(string secretName, RequestContext context);
public virtual Response<SecretBundle> GetSecretValue(string secretName, CancellationToken cancellationToken = default);
public virtual Task<Response<SecretBundle>> GetSecretValueAsync(string secretName, CancellationToken cancellationToken = default);
}
public class TemplateClientOptions : ClientOptions {
public enum ServiceVersion {
V7_0 = 1,
}
public TemplateClientOptions(ServiceVersion version = V7_0);
}
}
namespace Azure.Template.Models {
public class SecretBundle {
public string ContentType { get; }
public string Id { get; }
public string Kid { get; }
public bool? Managed { get; }
public IReadOnlyDictionary<string, string> Tags { get; }
public string Value { get; }
}
}
namespace Microsoft.Extensions.Azure {
public static class TemplateClientBuilderExtensions {
public static IAzureClientBuilder<TemplateClient, TemplateClientOptions> AddTemplateClient<TBuilder>(this TBuilder builder, string vaultBaseUrl) where TBuilder : IAzureClientFactoryBuilderWithCredential;
public static IAzureClientBuilder<TemplateClient, TemplateClientOptions> AddTemplateClient<TBuilder, TConfiguration>(this TBuilder builder, TConfiguration configuration) where TBuilder : IAzureClientFactoryBuilderWithConfiguration<TConfiguration>;
}
}
";
Assert.Equal(expected, templateCodeFile.GetApiText());
}
[Theory]
[MemberData(nameof(PackageCodeFiles))]
public void TestCodeFileJsonSchema(CodeFile codeFile)
{
//Verify JSON file generated for Azure.Template
var isValid = validateSchema(codeFile);
Assert.True(isValid);
}
private bool validateSchema(CodeFile codeFile)
{
var json = JsonSerializer.Serialize(codeFile, new JsonSerializerOptions
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
var schema = JSchema.Parse(TestData.TokenJsonSchema);
var jsonObject = JObject.Parse(json);
IList<string> validationErrors = new List<string>();
bool isValid = jsonObject.IsValid(schema, out validationErrors);
if (isValid)
{
Console.WriteLine("JSON is valid.");
}
else
{
Console.WriteLine("JSON is invalid. Errors:");
foreach (string error in validationErrors)
{
Console.WriteLine(error);
}
}
return isValid;
}
[Fact]
public void TestNavigationNodeHasRenderingClass()
{
var jsonString = JsonSerializer.Serialize(templateCodeFile);
var parsedCodeFile = JsonSerializer.Deserialize<CodeFile>(jsonString);
Assert.NotNull(parsedCodeFile);
Assert.Equal(8, CountNavigationNodes(parsedCodeFile.ReviewLines));
}
private int CountNavigationNodes(List<ReviewLine> lines)
{
int count = 0;
foreach (var line in lines)
{
var navTokens = line.Tokens.Where(x => x.NavigationDisplayName != null);
count += navTokens.Count(x => x.RenderClasses.Any());
count += CountNavigationNodes(line.Children);
}
return count;
}
[Fact]
public void VerifyAttributeHAsRelatedLine()
{
Assert.Equal(11, CountAttributeRelatedToProperty(storageCodeFile.ReviewLines));
}
private int CountAttributeRelatedToProperty(List<ReviewLine> lines)
{
int count = 0;
foreach (var line in lines)
{
if(line.LineId != null && line.LineId.StartsWith("System.FlagsAttribute.") && !string.IsNullOrEmpty(line.RelatedToLine))
{
count++;
}
count += CountAttributeRelatedToProperty(line.Children);
}
return count;
}
[Fact]
public void verifyHiddenApiCount()
{
Assert.Equal(4, CountHiddenApiInBlobDownloadInfo(storageCodeFile.ReviewLines));
}
private int CountHiddenApiInBlobDownloadInfo(List<ReviewLine> lines)
{
int count = 0;
foreach (var line in lines)
{
if (line.LineId != null && line.LineId.StartsWith("Azure.Storage.Blobs.Models.BlobDownloadInfo") && line.IsHidden == true)
{
count++;
}
count += CountHiddenApiInBlobDownloadInfo(line.Children);
}
return count;
}
[Fact]
public void VerifyObsoleteMemberIsHidden()
{
var attestationAssembly = Assembly.Load("Azure.Security.Attestation");
var dllStream = attestationAssembly.GetFile("Azure.Security.Attestation.dll");
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
var codeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
var lines = codeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Security.Attestation").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId == "Azure.Security.Attestation.AttestationResult").FirstOrDefault();
Assert.NotNull(classLine);
var obsoleteMethods = classLine.Children.Where(line => line.ToString().StartsWith("[Obsolete("));
Assert.NotEmpty(obsoleteMethods);
//Make sure member lines are marked as hidden if it has obsolete attribute
foreach (var method in obsoleteMethods)
{
Assert.True(method.IsHidden);
Assert.NotNull(method.RelatedToLine);
var relatedLine = classLine.Children.Where(line => line.LineId == method.RelatedToLine).FirstOrDefault();
Assert.True(relatedLine?.IsHidden);
}
}
[Fact]
public void VerifyTemplateClassLine()
{
var coreExprAssembly = Assembly.Load("Azure.Core.Expressions.DataFactory");
var dllStream = coreExprAssembly.GetFile("Azure.Core.Expressions.DataFactory.dll");
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
var codeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
var lines = codeFile.ReviewLines;
var namespaceLine = lines.Where(lines => lines.LineId == "Azure.Core.Expressions.DataFactory").FirstOrDefault();
Assert.NotNull(namespaceLine);
var classLine = namespaceLine.Children.Where(lines => lines.LineId.StartsWith("Azure.Core.Expressions.DataFactory.DataFactoryElement")).FirstOrDefault();
Assert.NotNull(classLine);
Assert.Equal("public sealed class DataFactoryElement<T> {", classLine.ToString().Trim());
var methodLine = classLine.Children.Where(lines => lines.LineId == "Azure.Core.Expressions.DataFactory.DataFactoryElement<T>.FromKeyVaultSecret(Azure.Core.Expressions.DataFactory.DataFactoryKeyVaultSecret)").FirstOrDefault();
Assert.NotNull(methodLine);
Assert.Equal("public static DataFactoryElement<string?> FromKeyVaultSecret(DataFactoryKeyVaultSecret secret);", methodLine.ToString().Trim());
}
[Fact]
public void VerifySkippedAttributes()
{
var serviceBusAssembly = Assembly.Load("Azure.Messaging.ServiceBus");
var dllStream = serviceBusAssembly.GetFile("Azure.Messaging.ServiceBus.dll");
var assemblySymbol = CompilationFactory.GetCompilation(dllStream, null);
var codeFile = new CSharpAPIParser.TreeToken.CodeFileBuilder().Build(assemblySymbol, true, null);
var line = codeFile.ReviewLines.Where(l => l.LineId == "Microsoft.Extensions.Azure").FirstOrDefault();
Assert.NotNull(line);
var classLine = line.Children?.Where(l => l.LineId == "Microsoft.Extensions.Azure.ServiceBusClientBuilderExtensions").FirstOrDefault();
Assert.NotNull(classLine);
var methodLine = classLine.Children?.Where(l => l.LineId.Contains("Microsoft.Extensions.Azure.ServiceBusClientBuilderExtensions.AddServiceBusClient")).FirstOrDefault();
Assert.NotNull(methodLine);
bool isRequiresUnreferencedCodePresent = classLine.Children?.Any(l => l.Tokens.Any(t => t.Value == "RequiresUnreferencedCodeAttribute")) ?? false;
bool isRequiresDynamicCode = classLine.Children?.Any(l => l.Tokens.Any(t => t.Value == "RequiresDynamicCode")) ?? false;
Assert.False(isRequiresUnreferencedCodePresent);
Assert.False(isRequiresDynamicCode);
}
}
}