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
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

#if NET
/// <summary>
Comment thread
Evangelink marked this conversation as resolved.
/// This attribute is used to ignore a test class or a test method based on the current process architecture.
/// </summary>
/// <remarks>
/// This attribute isn't inherited. Applying it to a base class will not cause derived classes to be ignored.
/// It can be combined with <see cref="OSConditionAttribute"/> (or any other condition attribute) to gate a test
/// on both the operating system and the process architecture, because condition attributes that expose a different
/// <see cref="ConditionBaseAttribute.GroupName"/> are combined with a logical AND.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class ArchitectureConditionAttribute : ConditionBaseAttribute
{
private readonly TestArchitectures _architectures;

/// <summary>
/// Initializes a new instance of the <see cref="ArchitectureConditionAttribute"/> class.
/// </summary>
/// <param name="mode">Decides whether the architectures will be included or excluded.</param>
/// <param name="architectures">The process architectures that this test includes/excludes.</param>
public ArchitectureConditionAttribute(ConditionMode mode, TestArchitectures architectures)
: base(mode)
{
_architectures = architectures;
IgnoreMessage = mode == ConditionMode.Include
? $"Test is only supported on {architectures}"
: $"Test is not supported on {architectures}";
}

/// <summary>
/// Initializes a new instance of the <see cref="ArchitectureConditionAttribute"/> class.
/// </summary>
/// <param name="architectures">The process architectures that this test supports.</param>
public ArchitectureConditionAttribute(TestArchitectures architectures)
: this(ConditionMode.Include, architectures)
{
}

/// <summary>
/// Gets a value indicating whether the test method or test class should be ignored.
/// </summary>
public override bool IsConditionMet
{
get
{
TestArchitectures current = MapArchitecture(RuntimeInformation.ProcessArchitecture);
return (_architectures & current) != 0;
}
}

/// <summary>
/// Gets the group name for this attribute.
/// </summary>
public override string GroupName => "ArchitectureCondition";

/// <summary>
/// Maps a <see cref="Architecture"/> value to the matching <see cref="TestArchitectures"/> flag.
/// </summary>
/// <param name="architecture">The current process <see cref="Architecture"/>.</param>
/// <returns>The matching <see cref="TestArchitectures"/> flag.</returns>
private static TestArchitectures MapArchitecture(Architecture architecture)
=> architecture switch
{
Architecture.X86 => TestArchitectures.X86,
Architecture.X64 => TestArchitectures.X64,
Architecture.Arm => TestArchitectures.Arm,
Architecture.Arm64 => TestArchitectures.Arm64,
Architecture.Wasm => TestArchitectures.Wasm,
Architecture.S390x => TestArchitectures.S390x,
Architecture.LoongArch64 => TestArchitectures.LoongArch64,
Architecture.Armv6 => TestArchitectures.Armv6,
Architecture.Ppc64le => TestArchitectures.Ppc64le,
#if NET9_0_OR_GREATER
// Architecture.RiscV64 was added in .NET 9 and isn't part of the net8.0 ref assembly, so both the
// enum member and the mapping are compiled only when targeting net9.0+. When this assembly runs on an
// older runtime the value can't be reported; any other unknown value falls through to the throw below.
Architecture.RiscV64 => TestArchitectures.RiscV64,
#endif
_ => throw new ArgumentOutOfRangeException(nameof(architecture), architecture, null),
};
Comment thread
Evangelink marked this conversation as resolved.
Comment on lines +66 to +85
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

#if NET
/// <summary>
/// An enum that is used with <see cref="ArchitectureConditionAttribute"/> to control which process architectures a test method or test class supports or doesn't support.
/// </summary>
[Flags]
Comment thread
Evangelink marked this conversation as resolved.
public enum TestArchitectures
{
Comment thread
Evangelink marked this conversation as resolved.
/// <summary>
/// Represents the x86 architecture.
/// </summary>
X86 = 1 << 0,

/// <summary>
/// Represents the x64 architecture.
/// </summary>
X64 = 1 << 1,

/// <summary>
/// Represents the ARM architecture.
/// </summary>
Arm = 1 << 2,

/// <summary>
/// Represents the ARM64 architecture.
/// </summary>
Arm64 = 1 << 3,

/// <summary>
/// Represents the WebAssembly platform.
/// </summary>
Wasm = 1 << 4,

/// <summary>
/// Represents the S390x architecture.
/// </summary>
S390x = 1 << 5,

/// <summary>
/// Represents the LoongArch64 architecture.
/// </summary>
LoongArch64 = 1 << 6,

/// <summary>
/// Represents the 32-bit ARMv6 architecture.
/// </summary>
Armv6 = 1 << 7,

/// <summary>
/// Represents the 64-bit PowerPC little-endian architecture.
/// </summary>
Ppc64le = 1 << 8,

#if NET9_0_OR_GREATER
/// <summary>
/// Represents the 64-bit RISC-V architecture.
/// </summary>
RiscV64 = 1 << 9,
#endif
}
#endif
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
#nullable enable
Microsoft.VisualStudio.TestTools.UnitTesting.ArchitectureConditionAttribute
Microsoft.VisualStudio.TestTools.UnitTesting.ArchitectureConditionAttribute.ArchitectureConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures architectures) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.ArchitectureConditionAttribute.ArchitectureConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures architectures) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<TItem>.AssertCountInterpolatedStringHandler(int literalLength, int formattedCount, int count, System.Memory<TItem> collection, out bool shouldAppend) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<TItem>.AssertCountInterpolatedStringHandler(int literalLength, int formattedCount, int count, System.ReadOnlyMemory<TItem> collection, out bool shouldAppend) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<TItem>.AssertCountInterpolatedStringHandler(int literalLength, int formattedCount, int count, System.ReadOnlySpan<TItem> collection, out bool shouldAppend) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<TItem>.AssertCountInterpolatedStringHandler(int literalLength, int formattedCount, int count, System.Span<TItem> collection, out bool shouldAppend) -> void
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.Arm = 4 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.Arm64 = 8 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.Armv6 = 128 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.LoongArch64 = 64 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.Ppc64le = 256 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.S390x = 32 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.Wasm = 16 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.X64 = 2 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.X86 = 1 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
override Microsoft.VisualStudio.TestTools.UnitTesting.ArchitectureConditionAttribute.GroupName.get -> string!
override Microsoft.VisualStudio.TestTools.UnitTesting.ArchitectureConditionAttribute.IsConditionMet.get -> bool
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.HasCount<T>(int expected, System.Memory<T> collection, ref Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<T> message, string! collectionExpression = "") -> void
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.HasCount<T>(int expected, System.Memory<T> collection, string? message = "", string! collectionExpression = "") -> void
static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.HasCount<T>(int expected, System.ReadOnlyMemory<T> collection, ref Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AssertCountInterpolatedStringHandler<T> message, string! collectionExpression = "") -> void
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#nullable enable
Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures.RiscV64 = 512 -> Microsoft.VisualStudio.TestTools.UnitTesting.TestArchitectures
3 changes: 3 additions & 0 deletions src/TestFramework/TestFramework/TestFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

<AdditionalFiles Include="PublicAPI\net\PublicAPI.Shipped.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netcoreapp3.1'))" />
<AdditionalFiles Include="PublicAPI\net\PublicAPI.Unshipped.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netcoreapp3.1'))" />

<AdditionalFiles Include="PublicAPI\net9.0\PublicAPI.Shipped.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))" />
<AdditionalFiles Include="PublicAPI\net9.0\PublicAPI.Unshipped.txt" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

#if NET
using AwesomeAssertions;
Comment thread
Evangelink marked this conversation as resolved.

using TestFramework.ForTestingMSTest;

namespace UnitTestFramework.Tests;

/// <summary>
/// Tests for class ArchitectureConditionAttribute.
/// </summary>
public class ArchitectureConditionAttributeTests : TestContainer
{
private const TestArchitectures AllArchitectures =
TestArchitectures.X86 | TestArchitectures.X64 | TestArchitectures.Arm | TestArchitectures.Arm64 | TestArchitectures.Wasm
| TestArchitectures.S390x | TestArchitectures.LoongArch64 | TestArchitectures.Armv6 | TestArchitectures.Ppc64le
#if NET9_0_OR_GREATER
| TestArchitectures.RiscV64
#endif
;

public void Constructor_SetsCorrectMode()
{
// Act
var includeAttribute = new ArchitectureConditionAttribute(ConditionMode.Include, TestArchitectures.X64);
var excludeAttribute = new ArchitectureConditionAttribute(ConditionMode.Exclude, TestArchitectures.X64);

// Assert
includeAttribute.Mode.Should().Be(ConditionMode.Include);
excludeAttribute.Mode.Should().Be(ConditionMode.Exclude);
}

public void Constructor_SingleArgument_DefaultsToIncludeMode()
{
// Act
var attribute = new ArchitectureConditionAttribute(TestArchitectures.X64);

// Assert
attribute.Mode.Should().Be(ConditionMode.Include);
}

public void GroupName_ReturnsCorrectValue()
{
// Arrange
var attribute = new ArchitectureConditionAttribute(TestArchitectures.X64);

// Act & Assert
attribute.GroupName.Should().Be("ArchitectureCondition");
}

// A different GroupName is what makes MSTest combine the two conditions with a logical AND,
// so an architecture requirement can be composed with an OS requirement.
public void GroupName_DiffersFromOSCondition()
=> new ArchitectureConditionAttribute(TestArchitectures.X64).GroupName
.Should().NotBe(new OSConditionAttribute(OperatingSystems.Windows).GroupName);

public void IgnoreMessage_IncludeMode_ReturnsCorrectMessage()
{
// Arrange
var attribute = new ArchitectureConditionAttribute(ConditionMode.Include, TestArchitectures.X64);

// Act & Assert
attribute.IgnoreMessage.Should().Be($"Test is only supported on {TestArchitectures.X64}");
}

public void IgnoreMessage_ExcludeMode_ReturnsCorrectMessage()
{
// Arrange
var attribute = new ArchitectureConditionAttribute(ConditionMode.Exclude, TestArchitectures.X64);

// Act & Assert
attribute.IgnoreMessage.Should().Be($"Test is not supported on {TestArchitectures.X64}");
}

public void IsConditionMet_WhenAllArchitecturesIncluded_ReturnsTrue()
{
// The current process architecture is necessarily one of the known TestArchitectures.
var attribute = new ArchitectureConditionAttribute(AllArchitectures);

attribute.IsConditionMet.Should().BeTrue();
}

public void IsConditionMet_WhenNoArchitecturesIncluded_ReturnsFalse()
{
var attribute = new ArchitectureConditionAttribute(default);

attribute.IsConditionMet.Should().BeFalse();
}

public void IsConditionMet_WhenCurrentArchitectureMatches_ReturnsTrue()
{
var attribute = new ArchitectureConditionAttribute(GetCurrentArchitecture());

attribute.IsConditionMet.Should().BeTrue();
}

public void IsConditionMet_WhenCurrentArchitectureExcludedFromSet_ReturnsFalse()
{
var attribute = new ArchitectureConditionAttribute(AllArchitectures & ~GetCurrentArchitecture());

attribute.IsConditionMet.Should().BeFalse();
}

// Derive the expected flag from the *name* of the current architecture rather than re-implementing the
// integer→flag mapping, so this test doesn't silently mirror (and therefore can't validate) the production logic.
// Enum.GetName returns null (rather than a numeric string) for a value missing from this TFM's Architecture enum,
// so an unknown architecture fails fast instead of Enum.TryParse accepting a numeric string as a bogus flag.
private static TestArchitectures GetCurrentArchitecture()
=> Enum.GetName(typeof(Architecture), RuntimeInformation.ProcessArchitecture) is { } name
&& Enum.TryParse(name, out TestArchitectures architecture)
? architecture
: throw new InvalidOperationException($"Unknown process architecture: {RuntimeInformation.ProcessArchitecture}");
}
#endif
Loading