diff --git a/DistIL.sln b/DistIL.sln
index 86cb745..5244d34 100644
--- a/DistIL.sln
+++ b/DistIL.sln
@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeTests", "tests\Prac
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{3CE5B206-AEC2-4A18-9ECF-8DAF17CB893F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistIL.Testing", "src\DistIL.Testing\DistIL.Testing.csproj", "{D3E25BCB-E6EC-436A-8BA1-269BFB248764}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -47,6 +49,10 @@ Global
{3CE5B206-AEC2-4A18-9ECF-8DAF17CB893F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CE5B206-AEC2-4A18-9ECF-8DAF17CB893F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CE5B206-AEC2-4A18-9ECF-8DAF17CB893F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3E25BCB-E6EC-436A-8BA1-269BFB248764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3E25BCB-E6EC-436A-8BA1-269BFB248764}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3E25BCB-E6EC-436A-8BA1-269BFB248764}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3E25BCB-E6EC-436A-8BA1-269BFB248764}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/DistIL.Testing/DistIL.Testing.csproj b/src/DistIL.Testing/DistIL.Testing.csproj
new file mode 100644
index 0000000..6a94274
--- /dev/null
+++ b/src/DistIL.Testing/DistIL.Testing.csproj
@@ -0,0 +1,28 @@
+
+
+
+ true
+ DistIL.Testing
+ Testing suite for DistIL
+
+ true
+
+ CS1591;CS1573
+
+ true
+ snupkg
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DistIL.Testing/ModuleAssertions.cs b/src/DistIL.Testing/ModuleAssertions.cs
new file mode 100644
index 0000000..1fe4391
--- /dev/null
+++ b/src/DistIL.Testing/ModuleAssertions.cs
@@ -0,0 +1,46 @@
+using DistIL.AsmIO;
+
+using Shouldly;
+
+namespace DistIL.Testing;
+
+[ShouldlyMethods]
+public static class ModuleAssertions
+{
+ public static void ShouldHaveName(this ModuleDef module, string name)
+ {
+ module.AsmName.Name.ShouldBe(name);
+ }
+
+ public static void ShouldHaveVersion(this ModuleDef module, Version version)
+ {
+ module.AsmName.Version.ShouldBe(version);
+ }
+
+ public static void ShouldHaveCustomAttribute(this ModuleDef module, string fullname, bool forAssembly = true)
+ {
+ module.GetCustomAttribs(forAssembly).Any(ca => $"{ca.Type.Namespace}.{ca.Type.Name}" == fullname).ShouldBeTrue();
+ }
+
+ public static void ShouldHaveCustomAttribute(this ModuleDef module, bool forAssembly = true)
+ where TAttribute : Attribute
+ {
+ module.ShouldHaveCustomAttribute(typeof(TAttribute)!.FullName!, forAssembly);
+ }
+
+ public static void ShouldContainType(this ModuleDef module, string ns, string name, bool includeExports = true)
+ {
+ module.FindType(ns, name, includeExports, throwIfNotFound: true).ShouldNotBeNull();
+ }
+
+ public static void ShouldNotContainType(this ModuleDef module, string ns, string name, bool includeExports = true)
+ {
+ module.FindType(ns, name, includeExports, throwIfNotFound: false).ShouldBeNull();
+ }
+
+ public static void ShouldTargetRuntimeVersion(this ModuleDef module, string version)
+ {
+ module.GetCustomAttribs(true).FirstOrDefault(a => a.Type.Namespace == "System.Runtime.Versioning" && a.Type.Name == "TargetFrameworkAttribute")?
+ .Args[0].ShouldBe(".NETCoreApp,Version=v" + version);
+ }
+}
diff --git a/src/DistIL.Testing/ModuleEntityAssertions.cs b/src/DistIL.Testing/ModuleEntityAssertions.cs
new file mode 100644
index 0000000..6c80a40
--- /dev/null
+++ b/src/DistIL.Testing/ModuleEntityAssertions.cs
@@ -0,0 +1,20 @@
+using DistIL.AsmIO;
+
+using Shouldly;
+
+namespace DistIL.Testing;
+
+[ShouldlyMethods]
+public static class ModuleEntityAssertions
+{
+ public static void ShouldHaveCustomAttribute(this ModuleEntity entity, string fullname)
+ {
+ entity.GetCustomAttribs(true).Any(ca => $"{ca.Type.Namespace}.{ca.Type.Name}" == fullname).ShouldBeTrue();
+ }
+
+ public static void ShouldHaveCustomAttribute(this ModuleEntity entity)
+ where TAttribute : Attribute
+ {
+ entity.ShouldHaveCustomAttribute(typeof(TAttribute)!.FullName!);
+ }
+}
\ No newline at end of file
diff --git a/src/DistIL.Testing/TypeAssertions.cs b/src/DistIL.Testing/TypeAssertions.cs
new file mode 100644
index 0000000..f3d5e0b
--- /dev/null
+++ b/src/DistIL.Testing/TypeAssertions.cs
@@ -0,0 +1,120 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+using DistIL.AsmIO;
+
+using Shouldly;
+
+namespace DistIL.Testing;
+
+[ShouldlyMethods]
+public static class TypeAssertions
+{
+ public static void ShouldHaveName(this TypeDef type, string name)
+ {
+ type.Name.ShouldBe(name);
+ }
+
+ public static void ShouldHaveNamespace(this TypeDef type, string ns)
+ {
+ type.Namespace.ShouldBe(ns);
+ }
+
+ public static void ShouldHaveBaseType(this TypeDef type, string ns, string name)
+ {
+ type.BaseType.ShouldNotBeNull();
+ type.BaseType!.Namespace.ShouldBe(ns);
+ type.BaseType!.Name.ShouldBe(name);
+ }
+
+ public static void ShouldBeEnum(this TypeDef type)
+ {
+ type.IsEnum.ShouldBeTrue();
+ }
+
+ public static void ShouldBeEnumWithFlags(this TypeDef type)
+ {
+ type.IsEnum.ShouldBeTrue();
+ type.HasCustomAttrib(typeof(FlagsAttribute));
+ }
+
+ public static void ShouldBeClass(this TypeDef type)
+ {
+ type.IsClass.ShouldBeTrue();
+ }
+
+ public static void ShouldBeInterface(this TypeDef type)
+ {
+ type.IsInterface.ShouldBeTrue();
+ }
+
+ public static void ShouldBeValueType(this TypeDef type)
+ {
+ type.IsValueType.ShouldBeTrue();
+ }
+
+ public static void ShouldBeAbstract(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.Abstract).ShouldBeTrue();
+ }
+
+ public static void ShouldBeSealed(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.Sealed).ShouldBeTrue();
+ }
+
+ public static void ShouldBePublic(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.Public).ShouldBeTrue();
+ }
+
+ public static void ShouldBePrivate(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.NotPublic).ShouldBeTrue();
+ }
+
+ public static void ShouldBeInternal(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.NestedAssembly).ShouldBeTrue();
+ }
+
+ public static void ShouldBeProtected(this TypeDef type)
+ {
+ type.Attribs.HasFlag(TypeAttributes.NestedFamily).ShouldBeTrue();
+ }
+
+ public static void ShouldImplement(this TypeDef type, string ns, string name)
+ {
+ type.Interfaces.ShouldContain(i => i.Namespace == ns && i.Name == name);
+ }
+
+ public static void ShouldImplement(this TypeDef type)
+ {
+ type.Interfaces.ShouldContain(i => i.Namespace == typeof(T).Namespace && i.Name == typeof(T).Name);
+ }
+
+ public static void ShouldHaveField(this TypeDef type, string name, string fieldType)
+ {
+ type.Fields.ShouldContain(f => f.Name == name && f.Type.Namespace == fieldType);
+ }
+
+ public static void ShouldHaveProperty(this TypeDef type, string name)
+ {
+ type.Properties.ShouldContain(p => p.Name == name);
+ }
+
+ public static void ShouldHaveMethod(this TypeDef type, string name)
+ {
+ type.Methods.ShouldContain(m => m.Name == name);
+ }
+
+ public static void ShouldHaveMethod(this TypeDef type, string name, string returnType)
+ {
+ type.Methods.ShouldContain(m => m.Name == name && m.ReturnType.Namespace == returnType);
+ }
+
+ public static void ShouldHaveEvent(this TypeDef type, string name)
+ {
+ type.Events.ShouldContain(e => e.Name == name);
+ }
+}