Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;
#if NETFRAMEWORK
// SuspendCodeCoverage (used below to pause dynamic code coverage while deployment copies files) is a VSTest
// object-model type. Its neutralization is deferred to a later platform-services decoupling step; the rest of
// the deployment input is already platform-agnostic.
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
#endif
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.AppContainer;
#endif
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
#if NETFRAMEWORK
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
#endif

namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;

Expand Down Expand Up @@ -79,7 +76,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source)
{
#if NETFRAMEWORK
// This loads the dll in a different app domain. We can optimize this to load in the current domain since this code could be run in a new app domain anyway.
bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName);
bool? utfReference = DoesSourceReferenceAssembly(source, assemblyName);

// If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery.
return !utfReference.HasValue || utfReference.Value;
Expand All @@ -94,6 +91,73 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source)
#endif
}

#if NETFRAMEWORK
/// <summary>
/// Checks whether the source assembly directly references the given assembly.
/// Only the assembly simple name and public key token are matched; version is ignored.
/// Returns <see langword="null"/> if the reference could not be determined.
/// </summary>
/// <param name="source"> The path to the source assembly to inspect. </param>
/// <param name="referenceAssembly"> The assembly to look for in the source's references. </param>
/// <returns> <see langword="true"/> if referenced, <see langword="false"/> if not, <see langword="null"/> if undeterminable. </returns>
private static bool? DoesSourceReferenceAssembly(string source, AssemblyName referenceAssembly)
{
if (string.IsNullOrEmpty(source) || referenceAssembly is null)
{
return null;
}

try
{
string? referenceAssemblyName = referenceAssembly.Name;
byte[] referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken();

// ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not
// execute any code from the loaded assembly.
var assembly = Assembly.ReflectionOnlyLoadFrom(source);

foreach (AssemblyName referencedAssembly in assembly.GetReferencedAssemblies())
{
// Match without version: only the simple name and public key token.
if (!string.Equals(referencedAssembly.Name, referenceAssemblyName, StringComparison.OrdinalIgnoreCase))
{
continue;
}

if (ArePublicKeyTokensEqual(referencedAssembly.GetPublicKeyToken(), referenceAssemblyPublicKeyToken))
{
return true;
}
}

return false;
}
catch
{
// Return null if we are not able to check.
return null;
}
}

private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArePublicKeyTokensEqual declares non-nullable byte[] parameters, but both callers pass AssemblyName.GetPublicKeyToken() which returns byte[]? (null for unsigned assemblies). When either token is null, left.Length on line 144 throws NullReferenceException, caught by the outer try/catch, which returns null → conservative path → discovery proceeds. Functionally safe, but the correctness depends on exception routing rather than explicit logic.

Suggested fix (nullable-aware):

private static bool ArePublicKeyTokensEqual(byte[]? left, byte[]? right)
{
    if (left is null && right is null) return true;
    if (left is null || right is null) return false;
    if (left.Length != right.Length) return false;
    for (int i = 0; i < left.Length; ++i)
    {
        if (left[i] != right[i]) return false;
    }
    return true;
}

This makes "both sides unsigned → match by name" explicit and eliminates the implicit NullReferenceException. Also update referenceAssemblyPublicKeyToken (line 113) to byte[]? to satisfy the nullable annotation.

Not a regression: the original vstest CheckAssemblyReference had the same gap — byte[] publicKeyToken1 = referencedAssembly.GetPublicKeyToken() without null-guard. This is a clean-up opportunity only.

{
if (left.Length != right.Length)
{
return false;
}

for (int i = 0; i < left.Length; ++i)
{
if (left[i] != right[i])
{
return false;
}
}

return true;
}
#endif

/// <summary>
/// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package (appx), return the file (dll/exe) that contains tests from it.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NETFRAMEWORK
namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities;

/// <summary>
/// Suspends dynamic code-coverage instrumentation of the modules that are loaded while this object is alive
/// (between construction and disposal) by setting the well-known collector environment variable. The previous
/// value of the environment variable is restored on dispose.
/// </summary>
internal sealed class SuspendCodeCoverage : IDisposable
{
private const string SuspendCodeCoverageEnvVarName = "__VANGUARD_SUSPEND_INSTRUMENT__";
private const string SuspendCodeCoverageEnvVarTrueValue = "TRUE";

private readonly string? _previousEnvironmentValue;

private bool _isDisposed;

public SuspendCodeCoverage()
{
_previousEnvironmentValue = Environment.GetEnvironmentVariable(SuspendCodeCoverageEnvVarName, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, SuspendCodeCoverageEnvVarTrueValue, EnvironmentVariableTarget.Process);
}

public void Dispose()
{
if (_isDisposed)
{
return;
}

Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, _previousEnvironmentValue, EnvironmentVariableTarget.Process);
_isDisposed = true;
}
}
#endif
Loading