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
27 changes: 27 additions & 0 deletions src/AssemblyCrawler.Tests/AssemblyCrawler.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AssemblyCrawler\AssemblyCrawler.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
67 changes: 67 additions & 0 deletions src/AssemblyCrawler.Tests/AssemblyExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using AssemblyCrawler;
using System.IO;

namespace AssemblyCrawler.Tests;

public class AssemblyExtensionTests : IDisposable
{
private readonly string _tempDir;

public AssemblyExtensionTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "AssemblyExtensionTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
}

// IsManagedAssembly uses FName (lowercased), so copy the DLL with a lowercase filename
private FileInfo GetManagedDllFileInfo()
{
string location = typeof(AssemblyExtensionTests).Assembly.Location;
string lowercaseName = Path.GetFileName(location).ToLowerInvariant();
string dest = Path.Combine(_tempDir, lowercaseName);
File.Copy(location, dest, overwrite: true);
return new FileInfo(dest);
}

[Fact]
public void IsManagedAssembly_ManagedDll_ReturnsTrue()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.True(assemblyInfo.IsManagedAssembly());
}

[Fact]
public void IsManagedAssembly_NonManagedFile_ReturnsFalse()
{
var tempFile = Path.Combine(_tempDir, "notmanaged.dll");
File.WriteAllBytes(tempFile, new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 });
var fileInfo = new FileInfo(tempFile);
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.False(assemblyInfo.IsManagedAssembly());
}

[Fact]
public void SortByHashCode_GroupsIdenticalAssembliesTogether()
{
var fileInfo = GetManagedDllFileInfo();
var a1 = new AssemblyInfo(fileInfo);
var a2 = new AssemblyInfo(fileInfo);
var a3 = new AssemblyInfo(fileInfo);

var list = new List<AssemblyInfo> { a1, a2, a3 };
var sorted = list.SortByHashCode();

// All three should have the same hash code and be in the same group
Assert.Single(sorted);
Assert.Equal(3, sorted.Values.First().Count);
}
}
89 changes: 89 additions & 0 deletions src/AssemblyCrawler.Tests/AssemblyInfoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using AssemblyCrawler;
using System.IO;
using System.Reflection;

namespace AssemblyCrawler.Tests;

public class AssemblyInfoTests : IDisposable
{
private readonly string _tempDir;

public AssemblyInfoTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "AssemblyInfoTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
}

// IsManagedAssembly uses FName (lowercased), so copy the DLL with a lowercase filename
private FileInfo GetManagedDllFileInfo()
{
string location = typeof(AssemblyInfoTests).Assembly.Location;
string lowercaseName = Path.GetFileName(location).ToLowerInvariant();
string dest = Path.Combine(_tempDir, lowercaseName);
File.Copy(location, dest, overwrite: true);
return new FileInfo(dest);
}

[Fact]
public void Constructor_ManagedDll_PopulatesFName()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.Equal(fileInfo.Name.ToLowerInvariant(), assemblyInfo.FName.Value);
}

[Fact]
public void Constructor_ManagedDll_IsManaged_ReturnsTrue()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.True(assemblyInfo.IsManaged.Value);
}

[Fact]
public void Constructor_ManagedDll_AName_IsNotEmpty()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.NotEmpty(assemblyInfo.AName.Value);
}

[Fact]
public void Constructor_ManagedDll_AssemblyVersion_IsNotNull()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.NotNull(assemblyInfo.AssemblyVersion.Value);
}

[Fact]
public void Constructor_NonManagedBinary_IsManaged_ReturnsFalse()
{
var tempFile = Path.Combine(_tempDir, "notadll.dll");
File.WriteAllBytes(tempFile, new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 });
var fileInfo = new FileInfo(tempFile);
var assemblyInfo = new AssemblyInfo(fileInfo);

Assert.False(assemblyInfo.IsManaged.Value);
}

[Fact]
public void GetHashCode_SameAssembly_ReturnsConsistentValue()
{
var fileInfo = GetManagedDllFileInfo();
var assemblyInfo1 = new AssemblyInfo(fileInfo);
var assemblyInfo2 = new AssemblyInfo(fileInfo);

Assert.Equal(assemblyInfo1.GetHashCode(), assemblyInfo2.GetHashCode());
}
}
102 changes: 102 additions & 0 deletions src/AssemblyCrawler.Tests/CrawlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using AssemblyCrawler;
using System.IO;

namespace AssemblyCrawler.Tests;

public class CrawlerTests : IDisposable
{
private readonly string _tempDir;

public CrawlerTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), "CrawlerTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_tempDir);
}

public void Dispose()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
}

private void CopyTestDllToDir(string dir, string fileName)
{
string sourceDll = typeof(CrawlerTests).Assembly.Location;
// Use lowercase filename to match FName.Value (which lowercases the name)
File.Copy(sourceDll, Path.Combine(dir, fileName.ToLowerInvariant()), overwrite: true);
}

[Fact]
public void Crawl_ValidDirectory_PopulatesAssemblyListAndCounts()
{
CopyTestDllToDir(_tempDir, "test1.dll");
CopyTestDllToDir(_tempDir, "test2.dll");

var crawler = new Crawler(_tempDir);
crawler.Crawl();

Assert.True(crawler.TotalFileCount >= 2);
Assert.True(crawler.TotalAssemblyCount >= 2);
Assert.True(crawler.AssemblyList.Count >= 2);
}

[Fact]
public void Crawl_CalledTwice_DoesNotReCrawl()
{
CopyTestDllToDir(_tempDir, "test1.dll");

var crawler = new Crawler(_tempDir);
crawler.Crawl();
int countAfterFirstCrawl = crawler.TotalAssemblyCount;

// Copy another DLL to the dir between crawls (shouldn't be picked up)
CopyTestDllToDir(_tempDir, "test2.dll");
crawler.Crawl();

Assert.Equal(countAfterFirstCrawl, crawler.TotalAssemblyCount);
}

[Fact]
public void Sort_BeforeCrawl_DoesNotCrash()
{
var crawler = new Crawler(_tempDir);

// Should print "Must parse directory first." but not throw
var exception = Record.Exception(() => crawler.Sort());
Assert.Null(exception);
}

[Fact]
public void Sort_AfterCrawl_SeparatesManagedAndUnmanaged()
{
CopyTestDllToDir(_tempDir, "managed1.dll");
CopyTestDllToDir(_tempDir, "managed2.dll");

// Create a non-managed binary file
var nonManagedPath = Path.Combine(_tempDir, "native.dll");
File.WriteAllBytes(nonManagedPath, new byte[128]);

var crawler = new Crawler(_tempDir);
crawler.Crawl();
crawler.Sort();

// AllAssemblies contains assemblies with duplicates (>1 copy)
// AllManagedAssemblies is the filtered managed subset
// Both should be valid without exception
Assert.NotNull(crawler.AllAssemblies);
Assert.NotNull(crawler.AllManagedAssemblies);
}

[Fact]
public void CrawlDirectory_IgnoresResourceDlls()
{
CopyTestDllToDir(_tempDir, "regular.dll");
CopyTestDllToDir(_tempDir, "resources.resources.dll");

var crawler = new Crawler(_tempDir);
crawler.Crawl();

// The .resources.dll should be ignored; only regular.dll counted
Assert.Equal(1, crawler.TotalAssemblyCount);
}
}
30 changes: 30 additions & 0 deletions src/AssemblyCrawler.Tests/StringExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using AssemblyCrawler;

namespace AssemblyCrawler.Tests;

public class StringExtensionTests
{
[Fact]
public void GetStableHashCode_SameInput_ReturnsSameValue()
{
var hash1 = "hello".GetStableHashCode();
var hash2 = "hello".GetStableHashCode();
Assert.Equal(hash1, hash2);
}

[Fact]
public void GetStableHashCode_DifferentInputs_ReturnsDifferentValues()
{
var hash1 = "hello".GetStableHashCode();
var hash2 = "world".GetStableHashCode();
Assert.NotEqual(hash1, hash2);
}

[Fact]
public void GetStableHashCode_EmptyString_ReturnsConsistentValue()
{
var hash1 = "".GetStableHashCode();
var hash2 = "".GetStableHashCode();
Assert.Equal(hash1, hash2);
}
}
6 changes: 6 additions & 0 deletions src/AssemblyCrawler.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.2.32630.192
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyCrawler", "AssemblyCrawler\AssemblyCrawler.csproj", "{F4896D2F-EC6F-426D-9321-F874423D7DE5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyCrawler.Tests", "AssemblyCrawler.Tests\AssemblyCrawler.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{F4896D2F-EC6F-426D-9321-F874423D7DE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4896D2F-EC6F-426D-9321-F874423D7DE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4896D2F-EC6F-426D-9321-F874423D7DE5}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading