diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/GitHubWorkItemAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/GitHubWorkItemAttribute.cs
new file mode 100644
index 0000000000..a8320e61f5
--- /dev/null
+++ b/src/TestFramework/TestFramework/Attributes/TestMethod/GitHubWorkItemAttribute.cs
@@ -0,0 +1,46 @@
+// 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;
+
+///
+/// GitHubWorkItem attribute; used to specify a GitHub issue associated with this test.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+public sealed partial class GitHubWorkItemAttribute : WorkItemAttribute
+{
+ ///
+ /// Initializes a new instance of the class for the GitHub WorkItem Attribute.
+ ///
+ /// The URL to a GitHub issue, pull request, or discussion.
+ public GitHubWorkItemAttribute(string url)
+ : base(ExtractId(url))
+ => Url = url;
+
+ ///
+ /// Gets the URL to the GitHub issue associated.
+ ///
+ public string Url { get; }
+
+ ///
+ /// Extracts the ID from the GitHub issue/pull/discussion URL.
+ ///
+ /// The URL to a GitHub ticket.
+ /// The ticket ID.
+ private static int ExtractId(string url)
+ {
+#if NET7_0_OR_GREATER
+ Match match = GitHubTicketRegex().Match(url);
+#else
+ Match match = Regex.Match(url, @"https:\/\/github\.com\/.+\/.+\/(issues|pull|discussions)\/(\d+)(#.+)?");
+#endif
+ return match.Success && int.TryParse(match.Groups[2].Value, out int issueId)
+ ? issueId
+ : throw new ArgumentException(FrameworkMessages.InvalidGitHubUrl, nameof(url));
+ }
+
+#if NET7_0_OR_GREATER
+ [GeneratedRegex("https:\\/\\/github\\.com\\/.+\\/.+\\/(issues|pull|discussions)\\/(\\d+)(#.+)?")]
+ private static partial Regex GitHubTicketRegex();
+#endif
+}
diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/WorkItemAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/WorkItemAttribute.cs
index 65dc5f16db..68ba3f62fd 100644
--- a/src/TestFramework/TestFramework/Attributes/TestMethod/WorkItemAttribute.cs
+++ b/src/TestFramework/TestFramework/Attributes/TestMethod/WorkItemAttribute.cs
@@ -7,7 +7,7 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// WorkItem attribute; used to specify a work item associated with this test.
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
-public sealed class WorkItemAttribute : Attribute
+public class WorkItemAttribute : Attribute
{
///
/// Initializes a new instance of the class for the WorkItem Attribute.
diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
index cd52b9d0ba..c1e8cb75a1 100644
--- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt
@@ -244,6 +244,9 @@ Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataAttribute.IgnoreMessage.
Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType.AutoDetect = 2 -> Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType
*REMOVED*Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataAttribute.DynamicDataAttribute(string! dynamicDataSourceName, Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType dynamicDataSourceType = Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType.Property) -> void
*REMOVED*Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataAttribute.DynamicDataAttribute(string! dynamicDataSourceName, System.Type! dynamicDataDeclaringType, Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType dynamicDataSourceType = Microsoft.VisualStudio.TestTools.UnitTesting.DynamicDataSourceType.Property) -> void
+Microsoft.VisualStudio.TestTools.UnitTesting.GitHubWorkItemAttribute
+Microsoft.VisualStudio.TestTools.UnitTesting.GitHubWorkItemAttribute.GitHubWorkItemAttribute(string! url) -> void
+Microsoft.VisualStudio.TestTools.UnitTesting.GitHubWorkItemAttribute.Url.get -> string!
Microsoft.VisualStudio.TestTools.UnitTesting.ITestDataSourceIgnoreCapability
Microsoft.VisualStudio.TestTools.UnitTesting.ITestDataSourceIgnoreCapability.IgnoreMessage.get -> string?
Microsoft.VisualStudio.TestTools.UnitTesting.ITestDataSourceIgnoreCapability.IgnoreMessage.set -> void
diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.Designer.cs b/src/TestFramework/TestFramework/Resources/FrameworkMessages.Designer.cs
index 8495430c23..a4fcfb2340 100644
--- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.Designer.cs
+++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.Designer.cs
@@ -386,6 +386,15 @@ internal static string HasCountFailMsg {
}
}
+ ///
+ /// Looks up a localized string similar to Invalid GitHub ticket URL.
+ ///
+ internal static string InvalidGitHubUrl {
+ get {
+ return ResourceManager.GetString("InvalidGitHubUrl", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The property {0} has type {1}; expected type {2}..
///
diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx
index 1d7681f175..2050ca74b8 100644
--- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx
+++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx
@@ -296,4 +296,7 @@ Actual: {2}
Expected collection to contain any item but it is empty. {0}
+
+ Invalid GitHub ticket URL
+
\ No newline at end of file
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf
index 371236303c..10583c04f8 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf
@@ -131,6 +131,11 @@ Skutečnost: {2}
Očekávala se kolekce {1} velikosti. Skutečnost: {2} {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Očekávaný typ:<{1}>. Aktuální typ:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf
index dd35a72d08..a88b227d92 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf
@@ -131,6 +131,11 @@ Tatsächlich: {2}
Es wurde eine Sammlung mit einer Größe {1} erwartet. Tatsächlich: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0}Erwarteter Typ:<{1}>. Tatsächlicher Typ:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf
index 48db8534e2..256bab594e 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf
@@ -131,6 +131,11 @@ Real: {2}
Se esperaba una colección de tamaño {1}. Real: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Tipo esperado:<{1}>. Tipo real:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf
index 7038f2f401..36ead28fe2 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf
@@ -131,6 +131,11 @@ Réel : {2}
Collection de tailles attendue {1}. Réel : {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0}Type attendu :<{1}>. Type réel :<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf
index 8c99d0653a..1e1af217f2 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf
@@ -131,6 +131,11 @@ Effettivo: {2}
Prevista raccolta di dimensioni {1}. Effettivo: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Tipo previsto:<{1}>. Tipo effettivo:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf
index 7b8d082850..f50ec98709 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf
@@ -131,6 +131,11 @@ Actual: {2}
サイズ {1} のコレクションが必要です。実際: {2}。{0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} には型 <{1}> が必要ですが、型 <{2}> が指定されました。
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf
index 00c6b4425f..1d2ed260fd 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf
@@ -131,6 +131,11 @@ Actual: {2}
{1} 크기 컬렉션이 필요합니다. 실제: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} 예상 형식: <{1}>, 실제 형식: <{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf
index 703108990b..9cf05e688f 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf
@@ -131,6 +131,11 @@ Rzeczywiste: {2}
Oczekiwano kolekcji rozmiaru {1}. Wartość rzeczywista: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Oczekiwany typ:<{1}>. Rzeczywisty typ:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf
index 4256672987..5aa1ead121 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf
@@ -131,6 +131,11 @@ Real: {2}
Coleção esperada de tamanho {1}. Real: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Tipo esperado:<{1}>. Tipo real:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf
index ddb2397c00..37d42fb093 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf
@@ -131,6 +131,11 @@ Actual: {2}
Ожидается коллекция размеров {1}. Фактически: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0}Ожидается тип: <{1}>. Фактический тип: <{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf
index 818c8c95ba..5c9334d859 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf
@@ -131,6 +131,11 @@ Gerçekte olan: {2}
Beklenen boyut {1}. Gerçek: {2}. {0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} Beklenen tür:<{1}>. Gerçek tür:<{2}>.
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf
index 8fec1f371e..a54a6e8d7e 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf
@@ -131,6 +131,11 @@ Actual: {2}
大小 {1} 的预期集合。实际: {2}。{0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} 类型应为: <{1}>。类型实为: <{2}>。
diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf
index 2f681dc303..dd37e65d78 100644
--- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf
+++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf
@@ -131,6 +131,11 @@ Actual: {2}
預期的大小集合 {1}。實際: {2}。{0}
+
+ Invalid GitHub ticket URL
+ Invalid GitHub ticket URL
+
+
{0} Expected type:<{1}>. Actual type:<{2}>.
{0} 預期的類型: <{1}>,實際的類型: <{2}>。
diff --git a/test/UnitTests/TestFramework.UnitTests/GitHubWorkItemTests.cs b/test/UnitTests/TestFramework.UnitTests/GitHubWorkItemTests.cs
new file mode 100644
index 0000000000..deb9db355b
--- /dev/null
+++ b/test/UnitTests/TestFramework.UnitTests/GitHubWorkItemTests.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using TestFramework.ForTestingMSTest;
+
+namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests;
+
+public class GitHubWorkItemTests : TestContainer
+{
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_IssueUrl()
+ {
+ string url = "https://github.com/microsoft/testfx/issues/1234";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_IssueUrlWithEndingSlash()
+ {
+ string url = "https://github.com/microsoft/testfx/issues/1234/";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_IssueUrlWithComment()
+ {
+ string url = "https://github.com/microsoft/testfx/issues/1234#issuecomment-2581012838";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_PRUrl()
+ {
+ string url = "https://github.com/microsoft/testfx/pull/1234";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_PRUrlWithEndingSlash()
+ {
+ string url = "https://github.com/microsoft/testfx/pull/1234/";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_PRUrlWithComment()
+ {
+ string url = "https://github.com/microsoft/testfx/pull/1234#discussion_r1932733213";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_DiscussionUrl()
+ {
+ string url = "https://github.com/microsoft/testfx/discussions/1234";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_DiscussionUrlWithEndingSlash()
+ {
+ string url = "https://github.com/microsoft/testfx/discussions/1234/";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+
+ public void GitHubWorkItemAttributeShouldExtractIdFromUrl_DiscussionUrlWithComment()
+ {
+ string url = "https://github.com/microsoft/testfx/discussions/1234#discussioncomment-11865020";
+ GitHubWorkItemAttribute attribute = new(url);
+ Verify(attribute.Url == url);
+ Verify(attribute.Id == 1234);
+ }
+}