| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. |
| 2 | |
| 3 | namespace WixInternal.TestSupport |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.Linq; |
| 8 | using System.Text; |
| 9 | using System.Xml.Linq; |
| 10 | using WixInternal.TestSupport.XunitExtensions; |
| 11 | using Xunit; |
| 12 | using Xunit.Sdk; |
| 13 | |
| 14 | public class WixAssert : Assert |
| 15 | { |
| 16 | public static void CompareLineByLine(string[] expectedLines, string[] actualLines) |
| 17 | { |
| 18 | var lineNumber = 0; |
| 19 | |
| 20 | for (; lineNumber < expectedLines.Length && lineNumber < actualLines.Length; ++lineNumber) |
| 21 | { |
| 22 | WixAssert.StringEqual($"{lineNumber}: {expectedLines[lineNumber]}", $"{lineNumber}: {actualLines[lineNumber]}"); |
| 23 | } |
| 24 | |
| 25 | var additionalExpectedLines = expectedLines.Length > lineNumber ? String.Join(Environment.NewLine, expectedLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {actualLines.Length - lineNumber} lines"; |
| 26 | var additionalActualLines = actualLines.Length > lineNumber ? String.Join(Environment.NewLine, actualLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {expectedLines.Length - lineNumber} lines"; |
| 27 | |
| 28 | Assert.Equal<object>(additionalExpectedLines, additionalActualLines, StringObjectEqualityComparer.InvariantCulture); |
| 29 | } |
| 30 | |
| 31 | public static void CompareXml(XContainer xExpected, XContainer xActual) |
| 32 | { |
| 33 | var expecteds = ComparableElements(xExpected); |
| 34 | var actuals = ComparableElements(xActual); |
| 35 | |
| 36 | CompareLineByLine(expecteds.OrderBy(s => s).ToArray(), actuals.OrderBy(s => s).ToArray()); |
| 37 | } |
| 38 | |
| 39 | public static void CompareXml(string expectedPath, string actualPath) |
| 40 | { |
| 41 | var expectedDoc = XDocument.Load(expectedPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); |
| 42 | var actualDoc = XDocument.Load(actualPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); |
| 43 | |
| 44 | CompareXml(expectedDoc, actualDoc); |
| 45 | } |
| 46 | |
| 47 | private static IEnumerable<string> ComparableElements(XContainer container) |
| 48 | { |
| 49 | return container.Descendants().Select(x => $"{x.Name.LocalName}:{String.Join(",", x.Attributes().OrderBy(a => a.Name.LocalName).Select(a => $"{a.Name.LocalName}={ComparableAttribute(a)}"))}"); |
| 50 | } |
| 51 | |
| 52 | private static string ComparableAttribute(XAttribute attribute) |
| 53 | { |
| 54 | switch (attribute.Name.LocalName) |
| 55 | { |
| 56 | case "SourceFile": |
| 57 | return "<SourceFile>"; |
| 58 | default: |
| 59 | return attribute.Value; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /// <summary> |
| 64 | /// Dynamically skips the test. |
| 65 | /// Requires that the test was marked with a fact attribute derived from <see cref="WixInternal.TestSupport.XunitExtensions.SkippableFactAttribute" /> |
| 66 | /// or <see cref="WixInternal.TestSupport.XunitExtensions.SkippableTheoryAttribute" /> |
| 67 | /// </summary> |
| 68 | public static void Skip(string message) |
| 69 | { |
| 70 | throw new SkipTestException(message); |
| 71 | } |
| 72 | |
| 73 | public static void SpecificReturnCode(int hrExpected, int hr, string format, params object[] formatArgs) |
| 74 | { |
| 75 | if (hrExpected != hr) |
| 76 | { |
| 77 | throw new SpecificReturnCodeException(hrExpected, hr, String.Format(format, formatArgs)); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | public static void Succeeded(int hr, string format, params object[] formatArgs) |
| 82 | { |
| 83 | if (0 > hr) |
| 84 | { |
| 85 | throw new SucceededException(hr, String.Format(format, formatArgs)); |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | public static void StringCollectionEmpty(IList<string> collection) |
| 90 | { |
| 91 | if (collection.Count > 0) |
| 92 | { |
| 93 | Assert.Fail($"The collection was expected to be empty, but instead was [{Environment.NewLine}\"{String.Join($"\", {Environment.NewLine}\"", collection)}\"{Environment.NewLine}]"); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | public static void StringEqual(string expected, string actual, bool ignoreCase = false) |
| 98 | { |
| 99 | WixStringEqualException.ThrowIfNotEqual(expected, actual, ignoreCase); |
| 100 | } |
| 101 | |
| 102 | public static void NotStringEqual(string expected, string actual, bool ignoreCase = false) |
| 103 | { |
| 104 | var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture; |
| 105 | Assert.NotEqual<object>(expected, actual, comparer); |
| 106 | } |
| 107 | |
| 108 | // There appears to have been a bug in VC++, which might or might not have been partially |
| 109 | // or completely corrected. It was unable to disambiguate a call to: |
| 110 | // Xunit::Assert::Throws(System::Type^, System::Action^) |
| 111 | // from a call to: |
| 112 | // Xunit::Assert::Throws(System::Type^, System::Func<System::Object^>^) |
| 113 | // that implicitly ignores its return value. |
| 114 | // |
| 115 | // The ambiguity may have been reported by some versions of the compiler and not by others. |
| 116 | // Some versions of the compiler may not have emitted any code in this situation, making it |
| 117 | // appear that the test has passed when, in fact, it hasn't been run. |
| 118 | // |
| 119 | // This situation is not an issue for C#. |
| 120 | // |
| 121 | // The following method is used to isolate DUtilTests in order to overcome the above problem. |
| 122 | |
| 123 | /// <summary> |
| 124 | /// This shim allows C++/CLR code to call the Xunit method with the same signature |
| 125 | /// without getting an ambiguous overload error. If the specified test code |
| 126 | /// fails to generate an exception of the exact specified type, an assertion |
| 127 | /// exception is thrown. Otherwise, execution flow proceeds as normal. |
| 128 | /// </summary> |
| 129 | /// <typeparam name="T">The type name of the expected exception.</typeparam> |
| 130 | /// <param name="testCode">An Action delegate to run the test code.</param> |
| 131 | public static new void Throws<T>(System.Action testCode) |
| 132 | where T : System.Exception |
| 133 | { |
| 134 | Xunit.Assert.Throws<T>(testCode); |
| 135 | } |
| 136 | |
| 137 | // This shim has been tested, but is not currently used anywhere. It was provided |
| 138 | // at the same time as the preceding shim because it involved the same overload |
| 139 | // resolution conflict. |
| 140 | |
| 141 | /// <summary> |
| 142 | /// This shim allows C++/CLR code to call the Xunit method with the same signature |
| 143 | /// without getting an ambiguous overload error. If the specified test code |
| 144 | /// fails to generate an exception of the exact specified type, an assertion |
| 145 | /// exception is thrown. Otherwise, execution flow proceeds as normal. |
| 146 | /// </summary> |
| 147 | /// <param name="exceptionType">The type object associated with exceptions of the expected type.</param> |
| 148 | /// <param name="testCode">An Action delegate to run the test code.</param> |
| 149 | /// <returns>An exception of a type other than the type specified, is such an exception is thrown.</returns> |
| 150 | public static new System.Exception Throws(System.Type exceptionType, System.Action testCode) |
| 151 | { |
| 152 | return Xunit.Assert.Throws(exceptionType, testCode); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | internal class StringObjectEqualityComparer : IEqualityComparer<object> |
| 157 | { |
| 158 | public static readonly StringObjectEqualityComparer InvariantCultureIgnoreCase = new StringObjectEqualityComparer(true); |
| 159 | public static readonly StringObjectEqualityComparer InvariantCulture = new StringObjectEqualityComparer(false); |
| 160 | |
| 161 | private readonly StringComparer stringComparer; |
| 162 | |
| 163 | public StringObjectEqualityComparer(bool ignoreCase) |
| 164 | { |
| 165 | this.stringComparer = ignoreCase ? StringComparer.InvariantCultureIgnoreCase : StringComparer.InvariantCulture; |
| 166 | } |
| 167 | |
| 168 | public new bool Equals(object x, object y) |
| 169 | { |
| 170 | return this.stringComparer.Equals((string)x, (string)y); |
| 171 | } |
| 172 | |
| 173 | public int GetHashCode(object obj) |
| 174 | { |
| 175 | return this.stringComparer.GetHashCode((string)obj); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | public class WixStringEqualException : XunitException |
| 180 | { |
| 181 | public WixStringEqualException(string userMessage) : base(userMessage) { } |
| 182 | |
| 183 | public static void ThrowIfNotEqual(string expected, string actual, bool ignoreCase) |
| 184 | { |
| 185 | var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture; |
| 186 | if (comparer.Equals(expected, actual)) |
| 187 | { |
| 188 | return; |
| 189 | } |
| 190 | |
| 191 | var sbMessage = new StringBuilder(); |
| 192 | |
| 193 | try |
| 194 | { |
| 195 | Assert.Equal(expected, actual, ignoreCase); |
| 196 | } |
| 197 | catch (XunitException xe) |
| 198 | { |
| 199 | // If either string is not completely in the message, then make sure it gets in there. |
| 200 | if (!xe.Message.Contains(expected) || !xe.Message.Contains(actual)) |
| 201 | { |
| 202 | sbMessage.AppendLine(xe.Message); |
| 203 | sbMessage.AppendLine(); |
| 204 | sbMessage.AppendFormat("Expected: {0}", expected); |
| 205 | sbMessage.AppendLine(); |
| 206 | sbMessage.AppendFormat("Actual: {0}", actual); |
| 207 | } |
| 208 | else |
| 209 | { |
| 210 | throw; |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | throw new WixStringEqualException(sbMessage.ToString()); |
| 215 | } |
| 216 | } |
| 217 | } |