Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions tests/ClrLens.Tests.IL/AssemblyReaderInvalidInputTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using ClrLens.PE;

namespace ClrLens.Tests.IL;

public sealed class AssemblyReaderInvalidInputTests
{
[Fact]
public async Task ReadAsync_MissingFileReportsFileNotFound()
{
var result = await new AssemblyReader().ReadAsync(Path.Combine(TestSupport.RepoRoot, "does-not-exist.dll"));

Assert.False(result.IsSuccess);
Assert.Contains(result.Diagnostics, d => d.Code == AssemblyDiagnosticCode.FileNotFound);
}

[Fact]
public async Task ReadAsync_InvalidMetadataReportsInvalidPeOrMissingMetadata()
{
var result = await new AssemblyReader().ReadAsync(TestSupport.InvalidMetadataFixturePath);

Assert.False(result.IsSuccess);
Assert.Contains(result.Diagnostics, d => d.Code is AssemblyDiagnosticCode.InvalidPe or AssemblyDiagnosticCode.MissingMetadata or AssemblyDiagnosticCode.MetadataReadFailure);
}

[Fact]
public async Task ReadAsync_FileTooLargeAndTimeoutAreReported()
{
var reader = new AssemblyReader();

var tooLarge = await reader.ReadAsync(TestSupport.FixtureAssemblyPath, new AssemblyIngestionOptions(MaxFileSizeBytes: 1));
Assert.Contains(tooLarge.Diagnostics, d => d.Code == AssemblyDiagnosticCode.FileTooLarge);

using var cancelled = new CancellationTokenSource();
await cancelled.CancelAsync();
var timeout = await reader.ReadAsync(TestSupport.FixtureAssemblyPath, cancellationToken: cancelled.Token);
Assert.Contains(timeout.Diagnostics, d => d.Code == AssemblyDiagnosticCode.Timeout);
}
}
21 changes: 21 additions & 0 deletions tests/ClrLens.Tests.IL/AssemblyReaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using ClrLens.PE;

namespace ClrLens.Tests.IL;

public sealed class AssemblyReaderTests
{
[Fact]
public async Task ReadAsync_ValidAssemblyProducesModelWithMetadata()
{
var result = await new AssemblyReader().ReadAsync(TestSupport.FixtureAssemblyPath);

Assert.True(result.IsSuccess, string.Join(";", result.Diagnostics.Select(d => d.Message)));
var model = Assert.IsType<AssemblyModel>(result.Model);
Assert.Equal("ClrLens.SampleFixtures", model.Identity.Name);
Assert.Equal(64, model.Identity.Sha256.Length);
Assert.True(model.TypeCount > 0);
Assert.True(model.MethodCount > 0);
Assert.True(model.HasMetadata);
Assert.Equal(model.AssemblyReferences.OrderBy(x => x, StringComparer.Ordinal), model.AssemblyReferences);
}
}
46 changes: 46 additions & 0 deletions tests/ClrLens.Tests.IL/CilDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using ClrLens.IL;

namespace ClrLens.Tests.IL;

public sealed class CilDecoderTests
{
[Fact]
public void Decode_FixtureMethodPreservesBranchesAndMetadataTokens()
{
var decoded = TestSupport.DecodeFixtureMethod("ExceptionFlow");

Assert.NotEmpty(decoded.Instructions);
Assert.Contains(decoded.Instructions, i => i.BranchTargets.Count > 0);
Assert.Contains(decoded.Instructions, i => i.Operand is int);
}

[Fact]
public void DecodeInstructions_SwitchOperandPreservesAllTargets()
{
var decodeInstructions = typeof(CilDecoder)
.GetMethod("DecodeInstructions", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new MissingMethodException("CilDecoder.DecodeInstructions");
var diagnostics = new List<CilDiagnostic>();

var il = new byte[]
{
0x45,
0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00,
0x00,
0x2A,
0x00,
0x2A
};

var instructions = (List<CilInstruction>)decodeInstructions.Invoke(null, [il, diagnostics])!;
var first = Assert.Single(instructions, i => i.OpCode.Name == "switch");

Assert.Equal(2, first.BranchTargets.Count);
Assert.Equal(13, first.BranchTargets[0]);
Assert.Equal(17, first.BranchTargets[1]);
Assert.Empty(diagnostics);
}
}
21 changes: 21 additions & 0 deletions tests/ClrLens.Tests.IL/CilExceptionRegionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Reflection.Metadata;

namespace ClrLens.Tests.IL;

public sealed class CilExceptionRegionTests
{
[Fact]
public void Decode_PreservesExceptionRegions()
{
var decoded = TestSupport.DecodeFixtureMethod("ExceptionFlow");

Assert.NotEmpty(decoded.ExceptionRegions);
Assert.Contains(decoded.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Catch);
Assert.Contains(decoded.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Finally);
Assert.All(decoded.ExceptionRegions, region =>
{
Assert.True(region.TryLength > 0);
Assert.True(region.HandlerLength > 0);
});
}
}
58 changes: 58 additions & 0 deletions tests/ClrLens.Tests.IL/CilStackValidationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Reflection;
using System.Reflection.Emit;
using ClrLens.IL;

namespace ClrLens.Tests.IL;

public sealed class CilStackValidationTests
{
[Fact]
public void ValidateBranchTargets_ReportsInvalidBranchTarget()
{
var method = typeof(CilDecoder).GetMethod("ValidateBranchTargets", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new MissingMethodException("CilDecoder.ValidateBranchTargets");

var instructions = new[]
{
new CilInstruction(0, OpCodes.Br_S, 99, 2, [99]),
new CilInstruction(2, OpCodes.Ret, null, 1, [])
};
var diagnostics = new List<CilDiagnostic>();

method.Invoke(null, [instructions, instructions.Select(i => i.Offset).ToHashSet(), diagnostics]);

var diagnostic = Assert.Single(diagnostics);
Assert.Equal(CilDiagnosticCode.InvalidBranchTarget, diagnostic.Code);
Assert.Contains("Branch target", diagnostic.Message, StringComparison.Ordinal);
}

[Fact]
public void ValidateStack_ReportsUnderflowHeightMismatchAndVariableBehavior()
{
var method = typeof(CilDecoder).GetMethod("ValidateStack", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new MissingMethodException("CilDecoder.ValidateStack");

var mismatchInstructions = new[]
{
new CilInstruction(0, OpCodes.Ldc_I4_0, null, 1, []),
new CilInstruction(1, OpCodes.Brtrue_S, 5, 2, [5]),
new CilInstruction(3, OpCodes.Ldc_I4_1, null, 1, []),
new CilInstruction(4, OpCodes.Br_S, 5, 2, [5]),
new CilInstruction(5, OpCodes.Pop, null, 1, []),
new CilInstruction(6, OpCodes.Ret, null, 1, [])
};
var variableInstructions = new[]
{
new CilInstruction(0, OpCodes.Call, 0x06000001, 5, []),
new CilInstruction(5, OpCodes.Ret, null, 1, [])
};
var diagnostics = new List<CilDiagnostic>();

method.Invoke(null, [mismatchInstructions, diagnostics]);
method.Invoke(null, [variableInstructions, diagnostics]);

Assert.Contains(diagnostics, d => d.Code == CilDiagnosticCode.StackUnderflow);
Assert.Contains(diagnostics, d => d.Code == CilDiagnosticCode.StackHeightMismatch);
Assert.Contains(diagnostics, d => d.Code == CilDiagnosticCode.VariableStackBehavior);
}
}
16 changes: 16 additions & 0 deletions tests/ClrLens.Tests.IL/ClrLens.Tests.IL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@
<OutputType>Exe</OutputType>
<AssemblyName>ClrLens.Tests.IL</AssemblyName>
<RootNamespace>ClrLens.Tests.IL</RootNamespace>
<IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);CA1707;CA1305;CA1861;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../../src/ClrLens.PE/ClrLens.PE.csproj" />
<ProjectReference Include="../../src/ClrLens.IL/ClrLens.IL.csproj" />
Expand Down
81 changes: 81 additions & 0 deletions tests/ClrLens.Tests.IL/FixtureManifestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text.Json;
using ClrLens.Analysis;
using ClrLens.IR;
using ClrLens.PE;
using ClrLens.SampleFixtures;

namespace ClrLens.Tests.IL;

public sealed class FixtureManifestTests
{
[Fact]
public void Manifest_ExistsAndMatchesFixtureShape()
{
Assert.True(File.Exists(TestSupport.ManifestPath));

using var document = JsonDocument.Parse(File.ReadAllText(TestSupport.ManifestPath));
var assemblyNode = document.RootElement.GetProperty("assemblies").EnumerateArray().First(e => e.GetProperty("project").GetString() == "ClrLens.SampleFixtures");

Assert.Equal("0.1", document.RootElement.GetProperty("schemaVersion").GetString());
Assert.Equal(64, assemblyNode.GetProperty("expectedHash").GetString()!.Length);

using var stream = File.OpenRead(TestSupport.FixtureAssemblyPath);
using var peReader = new PEReader(stream);
var metadata = peReader.GetMetadataReader();
var typeNames = metadata.TypeDefinitions
.Select(h => metadata.GetTypeDefinition(h))
.Select(t => $"{metadata.GetString(t.Namespace)}.{metadata.GetString(t.Name)}")
.ToHashSet(StringComparer.Ordinal);
var methodNames = metadata.MethodDefinitions
.Select(h => metadata.GetMethodDefinition(h))
.Select(m => metadata.GetString(m.Name))
.ToHashSet(StringComparer.Ordinal);

foreach (var requiredType in assemblyNode.GetProperty("mustContainTypes").EnumerateArray().Select(e => e.GetString()!))
{
Assert.Contains(requiredType, typeNames);
}

foreach (var requiredMethod in assemblyNode.GetProperty("mustContainMethods").EnumerateArray().Select(e => e.GetString()!))
{
Assert.Contains(requiredMethod, methodNames);
}

Assert.True(File.Exists(TestSupport.InvalidMetadataFixturePath));
}

[Fact]
public async Task FixtureHash_IsDeterministic_AndStaticInspectionDoesNotExecuteFixtureMethods()
{
Assert.Equal(0, RetentionPatterns.CacheCount);

var bytes = await File.ReadAllBytesAsync(TestSupport.FixtureAssemblyPath);
var hashA = Convert.ToHexString(SHA256.HashData(bytes));
var hashB = Convert.ToHexString(SHA256.HashData(bytes));

Assert.Equal(hashA, hashB);

var ingestion = await new AssemblyReader().ReadAsync(TestSupport.FixtureAssemblyPath);
Assert.True(ingestion.IsSuccess);

using var stream = File.OpenRead(TestSupport.FixtureAssemblyPath);
using var peReader = new PEReader(stream);
var metadata = peReader.GetMetadataReader();
var bodies = TestSupport.DecodeAllMethods(peReader, metadata);

var irBuilder = new CilToIrBuilder();
var cfgBuilder = new ControlFlowGraphBuilder();
foreach (var body in bodies.Values)
{
_ = irBuilder.Build(body);
_ = cfgBuilder.Build(body);
}

_ = CallGraphBuilder.Build(metadata, bodies);

Assert.Equal(0, RetentionPatterns.CacheCount);
}
}
1 change: 1 addition & 0 deletions tests/ClrLens.Tests.IL/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
12 changes: 12 additions & 0 deletions tests/ClrLens.Tests.IL/HarnessIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace ClrLens.Tests.IL;

public sealed class HarnessIntegrationTests
{
[Fact]
public async Task RunAsync_Passes()
{
var result = await FixtureHarness.RunAsync();

Assert.Contains("PASS", result, StringComparison.Ordinal);
}
}
Loading