From ad180be21200a37ca06b94c1687ec5ea0eb555cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 15 May 2026 16:07:32 +0200 Subject: [PATCH 1/4] Apply structured assertion messages (RFC 012) to Assert.HasCount / IsEmpty / IsNotEmpty Migrates Assert.HasCount, Assert.IsEmpty, and Assert.IsNotEmpty (and their interpolated-string-handler overloads) to the structured assertion message format defined in RFC 012. The HasCount/IsEmpty evidence block surfaces 'expected count:' and 'actual count:' so the size mismatch is visible at a glance; IsNotEmpty omits the expected line because the only actionable count is the actual zero. The interpolated-string-handler now passes the user-built message directly to the structured report and lets FormatCallSiteExpression reconstruct the call-site (e.g. Assert.HasCount(1, Array.Empty())) so the literal expected count remains visible even on the interpolated path. Updates the affected message-format tests in AssertTests.Items.cs to assert against the new layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestFramework/Assertions/Assert.Count.cs | 59 ++++++++++-------- .../Resources/FrameworkMessages.resx | 9 +++ .../Resources/xlf/FrameworkMessages.cs.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.de.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.es.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.fr.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.it.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.ja.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.ko.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.pl.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.ru.xlf | 15 +++++ .../Resources/xlf/FrameworkMessages.tr.xlf | 15 +++++ .../xlf/FrameworkMessages.zh-Hans.xlf | 15 +++++ .../xlf/FrameworkMessages.zh-Hant.xlf | 15 +++++ .../Assertions/AssertTests.Items.cs | 61 +++++++++++++++++-- 16 files changed, 294 insertions(+), 30 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 4ad00d8d02..038dfc3da0 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -47,8 +47,7 @@ internal void ComputeAssertion(string assertionName, string collectionExpression { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "collection", collectionExpression) + " "); - ReportAssertCountFailed(assertionName, _expectedCount, _actualCount, _builder.ToString()); + ReportAssertCountFailed(assertionName, _expectedCount, _actualCount, _builder.ToString(), collectionExpression); } } @@ -115,8 +114,7 @@ internal void ComputeAssertion(string collectionExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "collection", collectionExpression) + " "); - ReportAssertIsNotEmptyFailed(_builder.ToString()); + ReportAssertIsNotEmptyFailed(_builder.ToString(), collectionExpression); } } @@ -202,8 +200,7 @@ public static void IsNotEmpty(IEnumerable collection, string? message = "" return; } - string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ReportAssertIsNotEmptyFailed(userMessage); + ReportAssertIsNotEmptyFailed(message, collectionExpression); } /// @@ -222,8 +219,7 @@ public static void IsNotEmpty(IEnumerable collection, string? message = "", [Cal return; } - string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ReportAssertIsNotEmptyFailed(userMessage); + ReportAssertIsNotEmptyFailed(message, collectionExpression); } #endregion // IsNotEmpty @@ -326,32 +322,47 @@ private static void HasCount(string assertionName, int expected, IEnumerable< return; } - string userMessage = BuildUserMessageForCollectionExpression(message, collectionExpression); - ReportAssertCountFailed(assertionName, expected, actualCount, userMessage); + ReportAssertCountFailed(assertionName, expected, actualCount, message, collectionExpression); } private static void HasCount(string assertionName, int expected, IEnumerable collection, string? message, string collectionExpression) => HasCount(assertionName, expected, collection.Cast(), message, collectionExpression); [DoesNotReturn] - private static void ReportAssertCountFailed(string assertionName, int expectedCount, int actualCount, string userMessage) + private static void ReportAssertCountFailed(string assertionName, int expectedCount, int actualCount, string? userMessage, string collectionExpression) { - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.HasCountFailMsg, - userMessage, - expectedCount, - actualCount); - ReportAssertFailed($"Assert.{assertionName}", finalMessage); + string summary = assertionName == "IsEmpty" + ? FrameworkMessages.IsEmptyFailedSummary + : FrameworkMessages.HasCountFailedSummary; + + string expectedText = expectedCount.ToString(CultureInfo.CurrentCulture); + string actualText = actualCount.ToString(CultureInfo.CurrentCulture); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected count:", expectedText) + .AddLine("actual count:", actualText); + + StructuredAssertionMessage structured = new(summary); + structured.WithUserMessage(userMessage); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedText, actualText); + structured.WithCallSiteExpression(assertionName == "IsEmpty" + ? FormatCallSiteExpression($"Assert.{assertionName}", collectionExpression, "") + : FormatCallSiteExpression($"Assert.{assertionName}", expectedText, collectionExpression, "", "")); + + ReportAssertFailed(structured); } [DoesNotReturn] - private static void ReportAssertIsNotEmptyFailed(string userMessage) + private static void ReportAssertIsNotEmptyFailed(string? userMessage, string collectionExpression) { - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.IsNotEmptyFailMsg, - userMessage); - ReportAssertFailed("Assert.IsNotEmpty", finalMessage); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("actual count:", "0"); + + StructuredAssertionMessage structured = new(FrameworkMessages.IsNotEmptyFailedSummary); + structured.WithUserMessage(userMessage); + structured.WithEvidence(evidence); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotEmpty", collectionExpression, "")); + + ReportAssertFailed(structured); } } diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 691f3edab7..c75a1ff6b1 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -420,4 +420,13 @@ Actual: {2} Expected value to not be null. + + Expected collection to contain a specific number of elements. + + + Expected collection to be empty. + + + Expected collection to not be empty. + diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 4ad5deac23..b442c81cc8 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -262,6 +262,11 @@ Skutečnost: {2} Očekávala se kolekce {1} velikosti. Skutečnost: {2} {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. Vlastnost TestContext.{0} souvisí s aktuálním testem a není k dispozici během sestavení nebo používání testovacích přípravků tříd. @@ -272,6 +277,11 @@ Skutečnost: {2} Neplatná adresa URL lístku GitHubu + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Skutečnost: {2} Očekávalo se, že kolekce bude obsahovat libovolnou položku, ale je prázdná. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Špatný typ:<{1}>. Aktuální typ:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index f61ee46c58..c4d9dcc987 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -262,6 +262,11 @@ Tatsächlich: {2} Es wurde eine Sammlung mit einer Größe {1} erwartet. Tatsächlich: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. Die Eigenschaft „TestContext.{0}“ im Zusammenhang mit dem aktuellen Test steht während Assembly- oder Klassenfixierungen nicht zur Verfügung. @@ -272,6 +277,11 @@ Tatsächlich: {2} Ungültige GitHub-Ticket-URL. + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Tatsächlich: {2} Es wurde erwartet, dass die Sammlung ein beliebiges Element enthält, aber leer ist. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Falscher Typ:<{1}>. Tatsächlicher Typ:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 94a5797fc0..3975021539 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -262,6 +262,11 @@ Real: {2} Se esperaba una colección de tamaño {1}. Real: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. La propiedad "TestContext.{0}" está relacionado con la prueba actual y no está disponible durante los accesorios de ensamblado o clase. @@ -272,6 +277,11 @@ Real: {2} Dirección URL de vale de GitHub no válida + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Real: {2} Se esperaba que la colección contenga cualquier elemento, pero está vacía. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Tipo incorrecto:<{1}>. Tipo actual:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 00d78302f3..8eb73ed3f6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -262,6 +262,11 @@ Réel : {2} Collection de tailles attendue {1}. Réel : {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. La propriété « TestContext.{0} », liée au test en cours, n’est pas disponible pendant les fixtures d’assembly ou de classe. @@ -272,6 +277,11 @@ Réel : {2} URL de ticket GitHub non valide + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Réel : {2} La collection doit contenir n’importe quel élément, mais elle est vide. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Type incorrect : <{1}>, Type réel : <{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 80b0b1b715..d3ef3d8d30 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -262,6 +262,11 @@ Effettivo: {2} Prevista raccolta di dimensioni {1}. Effettivo: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. La proprietà 'TestContext.{0}' relativa al test corrente non è disponibile durante le fixture di assembly o classe. @@ -272,6 +277,11 @@ Effettivo: {2} L'URL del ticket GitHub non è valido + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Effettivo: {2} È previsto che la raccolta contenga qualsiasi elemento, ma è vuota. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Tipo errato:<{1}>. Tipo effettivo:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index ee5344f86b..16d7d5456e 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -262,6 +262,11 @@ Actual: {2} サイズ {1} のコレクションが必要です。実際: {2}。{0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. プロパティ 'TestContext.{0}' は現在のテストに関連しており、アセンブリまたはクラス フィクスチャの間は使用できません。 @@ -272,6 +277,11 @@ Actual: {2} GitHub チケット URL が無効です + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Actual: {2} コレクションには項目が含まれている必要がありますが、空です。{0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} 正しくない型は <{1}> であり、実際の型は <{2}> です。{0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index e1d64ff123..896c34531f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -262,6 +262,11 @@ Actual: {2} {1} 크기 컬렉션이 필요합니다. 실제: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. 현재 테스트와 관련된 'TestContext.{0}' 속성은 어셈블리나 클래스 픽스처 실행 중에는 사용할 수 없습니다. @@ -272,6 +277,11 @@ Actual: {2} 잘못된 GitHub 티켓 URL + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Actual: {2} 항목을 포함할 컬렉션이 필요한데 비어 있습니다. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} 잘못된 형식: <{1}>, 실제 형식: <{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index b09f5eb893..28584660a6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -262,6 +262,11 @@ Rzeczywiste: {2} Oczekiwano kolekcji rozmiaru {1}. Wartość rzeczywista: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. Właściwość „TestContext.{0}” jest powiązana z bieżącym testem i nie jest dostępna podczas montażu lub konfiguracji klasy. @@ -272,6 +277,11 @@ Rzeczywiste: {2} Nieprawidłowy adres URL biletu usługi GitHub + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Rzeczywiste: {2} Oczekiwano, że kolekcja będzie zawierać dowolny element, ale jest pusta. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Zły typ:<{1}>. Rzeczywisty typ:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 3fdabfe37d..9e419bacf6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -262,6 +262,11 @@ Real: {2} Coleção esperada de tamanho {1}. Real: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. A propriedade "TestContext.{0}" está relacionada ao teste atual não está disponível durante os acessórios de assembly ou classe. @@ -272,6 +277,11 @@ Real: {2} URL de tíquete do GitHub inválida + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Real: {2} A coleção esperada conter qualquer item, mas ela está vazia. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Tipo errado:<{1}>. Tipo real:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 8262985ee7..77b838ba52 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -262,6 +262,11 @@ Actual: {2} Ожидается коллекция размеров {1}. Фактически: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. Свойство "TestContext.{0}" связано с текущим тестом и недоступно во время выполнения средств тестирования сборок или классов. @@ -272,6 +277,11 @@ Actual: {2} Недопустимый URL-адрес билета GitHub + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Actual: {2} Ожидается, что коллекция будет содержать любой элемент, но она пуста. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Неверный тип: <{1}>. Фактический тип: <{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 8420e144a3..dd6653dfcc 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -262,6 +262,11 @@ Gerçekte olan: {2} Beklenen boyut {1}. Gerçek: {2}. {0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. 'TestContext.{0}' özelliği, derleme veya sınıf sabitlemeleri sırasında mevcut testle ilişkili değildir. @@ -272,6 +277,11 @@ Gerçekte olan: {2} Geçersiz GitHub anahtar URL'si + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Gerçekte olan: {2} Koleksiyonun herhangi bir öğe içermesi bekleniyordu ancak boş. {0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} Yanlış Tür:<{1}>. Gerçek tür:<{2}>. {0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 0b7558f104..c5664c0cb7 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -262,6 +262,11 @@ Actual: {2} 大小 {1} 的预期集合。实际: {2}。{0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. 属性‘TestContext.{0}’与当前测试相关,在程序集或类固定例程期间不可用。 @@ -272,6 +277,11 @@ Actual: {2} GitHub 票证 URL 无效 + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Actual: {2} 集合应包含任何项,但它为空。{0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} 错误类型为: <{1}>,实际类型为: <{2}>。{0} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index cc658d8057..90d528b024 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -262,6 +262,11 @@ Actual: {2} 預期的大小集合 {1}。實際: {2}。{0} + + Expected collection to contain a specific number of elements. + Expected collection to contain a specific number of elements. + + The property 'TestContext.{0}' is related to current test is not available during assembly or class fixtures. 屬性 'TestContext.{0}' 與目前測試相關,無法在組件或類別測試夾具期間使用。 @@ -272,6 +277,11 @@ Actual: {2} 無效的 GitHub 票證 URL + + Expected collection to be empty. + Expected collection to be empty. + + Expected condition to be false. Expected condition to be false. @@ -302,6 +312,11 @@ Actual: {2} 預期集合包含任何專案,但卻是空的。{0} + + Expected collection to not be empty. + Expected collection to not be empty. + + Wrong Type:<{1}>. Actual type:<{2}>. {0} 錯誤的類型: <{1}>。實際的類型: <{2}>。{0} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs index 98a5160f29..a20a0d5be9 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs @@ -25,7 +25,15 @@ public void Count_WhenCountIsNotSame_ShouldFail() var collection = new List { 1 }; Action action = () => Assert.HasCount(3, collection); action.Should().Throw() - .WithMessage("Assert.HasCount failed. Expected collection of size 3. Actual: 1. 'collection' expression: 'collection'."); + .WithMessage( + """ + Assertion failed. Expected collection to contain a specific number of elements. + + expected count: 3 + actual count: 1 + + Assert.HasCount(3, collection) + """); } public async Task Count_InterpolatedString_WhenCountIsNotSame_ShouldFail() @@ -34,7 +42,16 @@ public async Task Count_InterpolatedString_WhenCountIsNotSame_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.HasCount(1, Array.Empty(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.HasCount failed. Expected collection of size 1. Actual: 0. 'collection' expression: 'Array.Empty()'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage( + $$""" + Assertion failed. Expected collection to contain a specific number of elements. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + expected count: 1 + actual count: 0 + + Assert.HasCount(1, Array.Empty()) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -53,7 +70,15 @@ public void NotAny_WhenNotEmpty_ShouldFail() var collection = new List { 1 }; Action action = () => Assert.IsEmpty(collection); action.Should().Throw() - .WithMessage("Assert.IsEmpty failed. Expected collection of size 0. Actual: 1. 'collection' expression: 'collection'."); + .WithMessage( + """ + Assertion failed. Expected collection to be empty. + + expected count: 0 + actual count: 1 + + Assert.IsEmpty(collection) + """); } public async Task NotAny_InterpolatedString_WhenNotEmpty_ShouldFail() @@ -63,7 +88,16 @@ public async Task NotAny_InterpolatedString_WhenNotEmpty_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsEmpty(collection, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsEmpty failed. Expected collection of size 0. Actual: 1. 'collection' expression: 'collection'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage( + $$""" + Assertion failed. Expected collection to be empty. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + expected count: 0 + actual count: 1 + + Assert.IsEmpty(collection) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -194,7 +228,14 @@ public void Any_WhenNoItem_ShouldFail() { Action action = () => Assert.IsNotEmpty(Array.Empty()); action.Should().Throw() - .WithMessage("Assert.IsNotEmpty failed. Expected collection to contain any item but it is empty. 'collection' expression: 'Array.Empty()'."); + .WithMessage( + """ + Assertion failed. Expected collection to not be empty. + + actual count: 0 + + Assert.IsNotEmpty(Array.Empty()) + """); } public async Task Any_InterpolatedString_WhenNoItem_ShouldFail() @@ -203,7 +244,15 @@ public async Task Any_InterpolatedString_WhenNoItem_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsNotEmpty(Array.Empty(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsNotEmpty failed. Expected collection to contain any item but it is empty. 'collection' expression: 'Array.Empty()'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage( + $$""" + Assertion failed. Expected collection to not be empty. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {{string.Format(null, "{0:tt}", dateTime)}}, {{string.Format(null, "{0,5:tt}", dateTime)}} + + actual count: 0 + + Assert.IsNotEmpty(Array.Empty()) + """); o.WasToStringCalled.Should().BeTrue(); } } From 843346d7c6329f015fd645b774be418bd9f8082b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 15 May 2026 18:24:03 +0200 Subject: [PATCH 2/4] Fix Assert.Count call-site formatting Use invariant formatting for HasCount call-site arguments while keeping evidence culture-aware, and replace magic assertion-name strings with nameof usage. Add a regression test covering culture-specific negative-sign formatting for the displayed call site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestFramework/Assertions/Assert.Count.cs | 26 +++++++++------- .../Assertions/AssertTests.Items.cs | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 038dfc3da0..e92a6daf1c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -239,7 +239,7 @@ public static void IsNotEmpty(IEnumerable collection, string? message = "", [Cal #pragma warning disable IDE0060 // Remove unused parameter public static void HasCount(int expected, IEnumerable collection, [InterpolatedStringHandlerArgument(nameof(expected), nameof(collection))] ref AssertCountInterpolatedStringHandler message, [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter - => message.ComputeAssertion("HasCount", collectionExpression); + => message.ComputeAssertion(nameof(HasCount), collectionExpression); /// /// Tests whether the collection has the expected count/length. @@ -253,7 +253,7 @@ public static void HasCount(int expected, IEnumerable collection, [Interpo /// Users shouldn't pass a value for this parameter. /// public static void HasCount(int expected, IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") - => HasCount("HasCount", expected, collection, message, collectionExpression); + => HasCount(nameof(HasCount), expected, collection, message, collectionExpression); /// /// Tests whether the collection has the expected count/length. @@ -266,7 +266,7 @@ public static void HasCount(int expected, IEnumerable collection, string? /// Users shouldn't pass a value for this parameter. /// public static void HasCount(int expected, IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") - => HasCount("HasCount", expected, collection, message, collectionExpression); + => HasCount(nameof(HasCount), expected, collection, message, collectionExpression); #endregion // HasCount @@ -285,7 +285,7 @@ public static void HasCount(int expected, IEnumerable collection, string? messag #pragma warning disable IDE0060 // Remove unused parameter public static void IsEmpty(IEnumerable collection, [InterpolatedStringHandlerArgument(nameof(collection))] ref AssertCountInterpolatedStringHandler message, [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") #pragma warning restore IDE0060 // Remove unused parameter - => message.ComputeAssertion("IsEmpty", collectionExpression); + => message.ComputeAssertion(nameof(IsEmpty), collectionExpression); /// /// Tests that the collection is empty. @@ -298,7 +298,7 @@ public static void IsEmpty(IEnumerable collection, [InterpolatedStringHand /// Users shouldn't pass a value for this parameter. /// public static void IsEmpty(IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") - => HasCount("IsEmpty", 0, collection, message, collectionExpression); + => HasCount(nameof(IsEmpty), 0, collection, message, collectionExpression); /// /// Tests that the collection is empty. @@ -310,7 +310,7 @@ public static void IsEmpty(IEnumerable collection, string? message = "", [ /// Users shouldn't pass a value for this parameter. /// public static void IsEmpty(IEnumerable collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") - => HasCount("IsEmpty", 0, collection, message, collectionExpression); + => HasCount(nameof(IsEmpty), 0, collection, message, collectionExpression); #endregion // IsEmpty @@ -331,23 +331,25 @@ private static void HasCount(string assertionName, int expected, IEnumerable col [DoesNotReturn] private static void ReportAssertCountFailed(string assertionName, int expectedCount, int actualCount, string? userMessage, string collectionExpression) { - string summary = assertionName == "IsEmpty" + bool isEmptyAssertion = string.Equals(assertionName, nameof(IsEmpty), StringComparison.Ordinal); + string summary = isEmptyAssertion ? FrameworkMessages.IsEmptyFailedSummary : FrameworkMessages.HasCountFailedSummary; - string expectedText = expectedCount.ToString(CultureInfo.CurrentCulture); + string expectedEvidenceText = expectedCount.ToString(CultureInfo.CurrentCulture); + string expectedCallSiteText = expectedCount.ToString(CultureInfo.InvariantCulture); string actualText = actualCount.ToString(CultureInfo.CurrentCulture); EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("expected count:", expectedText) + .AddLine("expected count:", expectedEvidenceText) .AddLine("actual count:", actualText); StructuredAssertionMessage structured = new(summary); structured.WithUserMessage(userMessage); structured.WithEvidence(evidence); - structured.WithExpectedAndActual(expectedText, actualText); - structured.WithCallSiteExpression(assertionName == "IsEmpty" + structured.WithExpectedAndActual(expectedEvidenceText, actualText); + structured.WithCallSiteExpression(isEmptyAssertion ? FormatCallSiteExpression($"Assert.{assertionName}", collectionExpression, "") - : FormatCallSiteExpression($"Assert.{assertionName}", expectedText, collectionExpression, "", "")); + : FormatCallSiteExpression($"Assert.{assertionName}", expectedCallSiteText, collectionExpression, "", "")); ReportAssertFailed(structured); } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs index a20a0d5be9..4dd180314a 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Globalization; + using AwesomeAssertions; namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; @@ -55,6 +57,35 @@ Assertion failed. Expected collection to contain a specific number of elements. o.WasToStringCalled.Should().BeTrue(); } + public void Count_WhenCurrentCultureUsesCustomNegativeSign_ShouldUseInvariantCallSiteValue() + { + CultureInfo originalCulture = CultureInfo.CurrentCulture; + CultureInfo customCulture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + customCulture.NumberFormat.NegativeSign = "−"; + + try + { + CultureInfo.CurrentCulture = customCulture; + var collection = new List(); + + Action action = () => Assert.HasCount(-1, collection); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain a specific number of elements. + + expected count: −1 + actual count: 0 + + Assert.HasCount(-1, collection) + """); + } + finally + { + CultureInfo.CurrentCulture = originalCulture; + } + } + public void NotAny_WhenEmpty_ShouldPass() => Assert.IsEmpty(Array.Empty()); From 201784a7895c9619f35a3b02929ca31978534483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 16 May 2026 13:48:17 +0200 Subject: [PATCH 3/4] Apply suggestion from @Evangelink --- .../TestFramework.UnitTests/Assertions/AssertTests.Items.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs index 1a1f846433..d3b717e3b7 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.Items.cs @@ -60,7 +60,7 @@ Assertion failed. Expected collection to contain a specific number of elements. public void Count_WhenCurrentCultureUsesCustomNegativeSign_ShouldUseInvariantCallSiteValue() { CultureInfo originalCulture = CultureInfo.CurrentCulture; - CultureInfo customCulture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + var customCulture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); customCulture.NumberFormat.NegativeSign = "−"; try From ef50f919047245e05514b7722a54723c79ae66d3 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 16:31:23 +0200 Subject: [PATCH 4/4] Address review: culture-aware actual count and WithExpectedAndActual for IsNotEmpty Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/TestFramework/TestFramework/Assertions/Assert.Count.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs index 2d0120c4e4..470f8d6383 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.Count.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.Count.cs @@ -382,12 +382,14 @@ private static void ReportAssertCountFailed(string assertionName, int expectedCo [DoesNotReturn] private static void ReportAssertIsNotEmptyFailed(string? userMessage, string collectionExpression) { + string actualText = 0.ToString(CultureInfo.CurrentCulture); EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("actual count:", "0"); + .AddLine("actual count:", actualText); StructuredAssertionMessage structured = new(FrameworkMessages.IsNotEmptyFailedSummary); structured.WithUserMessage(userMessage); structured.WithEvidence(evidence); + structured.WithExpectedAndActual("> 0", actualText); structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotEmpty", collectionExpression, "")); ReportAssertFailed(structured);