diff --git a/Microsoft.PowerApps.TestAutomation.Api/Microsoft.PowerApps.TestAutomation.Api.csproj b/Microsoft.PowerApps.TestAutomation.Api/Microsoft.PowerApps.TestAutomation.Api.csproj
index 0acd6061..76d7bfe2 100644
--- a/Microsoft.PowerApps.TestAutomation.Api/Microsoft.PowerApps.TestAutomation.Api.csproj
+++ b/Microsoft.PowerApps.TestAutomation.Api/Microsoft.PowerApps.TestAutomation.Api.csproj
@@ -31,8 +31,8 @@
4
-
- ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
diff --git a/Microsoft.PowerApps.TestAutomation.Api/Pages/TestAutomation.cs b/Microsoft.PowerApps.TestAutomation.Api/Pages/TestAutomation.cs
index 35d750c1..33f5c919 100644
--- a/Microsoft.PowerApps.TestAutomation.Api/Pages/TestAutomation.cs
+++ b/Microsoft.PowerApps.TestAutomation.Api/Pages/TestAutomation.cs
@@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Text;
namespace Microsoft.PowerApps.TestAutomation.Api
{
@@ -150,6 +151,50 @@ internal void InitiateTest(IWebDriver driver, Uri uri)
}
}
+ ///
+ /// Normalizes text that originates from the app under test before it is written to
+ /// standard output.
+ ///
+ ///
+ /// Test suite and test case names, descriptions and failure messages are supplied by the
+ /// app being tested rather than by the pipeline that runs it. Build agents interpret the
+ /// console stream line by line, so these values are flattened to a single line before they
+ /// are logged and cannot affect how surrounding output is parsed.
+ ///
+ /// The value to normalize.
+ /// A single-line representation of .
+ public static string SanitizeForLog(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return string.Empty;
+ }
+
+ var sanitized = new StringBuilder(value.Length);
+ char previous = '\0';
+
+ foreach (char character in value)
+ {
+ // char.IsControl already covers CR, LF and NEL (U+0085). The Unicode line and
+ // paragraph separators sit outside that range, so they are named explicitly.
+ char current = char.IsControl(character) || character == '\u2028' || character == '\u2029'
+ ? ' '
+ : character;
+
+ // Keep consecutive '#' characters apart so they cannot form a marker, for runs
+ // of any length.
+ if (current == '#' && previous == '#')
+ {
+ sanitized.Append(' ');
+ }
+
+ sanitized.Append(current);
+ previous = current;
+ }
+
+ return sanitized.ToString();
+ }
+
public Tuple ReportResultsToDevOps(JObject jObject, int testRunNumber)
{
var testExecutionMode = (int)jObject.GetValue("ExecutionMode");
@@ -183,17 +228,17 @@ public Tuple ReportResultsToDevOps(JObject jObject, int testRunNumber)
// Output results to Console
Console.WriteLine("\t" +
- $"TestSuite Name: {testCaseResults.TestSuiteName} with ID {testCaseResults.TestSuiteId}");
+ $"TestSuite Name: {SanitizeForLog(testCaseResults.TestSuiteName)} with ID {SanitizeForLog(testCaseResults.TestSuiteId)}");
Console.WriteLine("\t" +
- $"TestSuite Description: {testCaseResults.TestSuiteDescription}");
+ $"TestSuite Description: {SanitizeForLog(testCaseResults.TestSuiteDescription)}");
Console.WriteLine("\t" +
- $"TestCase Name: {testCaseResults.TestCaseName} with ID {testCaseResults.TestCaseId}");
+ $"TestCase Name: {SanitizeForLog(testCaseResults.TestCaseName)} with ID {SanitizeForLog(testCaseResults.TestCaseId)}");
Console.WriteLine("\t" +
- $"TestCase Description: {testCaseResults.TestCaseDescription}");
+ $"TestCase Description: {SanitizeForLog(testCaseResults.TestCaseDescription)}");
Console.WriteLine("\t" +
$"Test Case Result: {testCaseResult}");
Console.WriteLine("\t" +
- $"Test Case Failure Message: {testCaseResults.TestFailureMessage}");
+ $"Test Case Failure Message: {SanitizeForLog(testCaseResults.TestFailureMessage)}");
Console.WriteLine("\t" +
$"Test Case execution time: {testCaseElapsedTime}");
@@ -213,15 +258,15 @@ public Tuple ReportResultsToDevOps(JObject jObject, int testRunNumber)
// Output results to Console
Console.WriteLine("\t" +
- $"TestSuite Name: {testSuiteResults.TestSuiteName} with ID {testSuiteResults.TestSuiteId}");
+ $"TestSuite Name: {SanitizeForLog(testSuiteResults.TestSuiteName)} with ID {SanitizeForLog(testSuiteResults.TestSuiteId)}");
Console.WriteLine("\t" +
- $"TestSuite Description: {testSuiteResults.TestSuiteDescription}");
+ $"TestSuite Description: {SanitizeForLog(testSuiteResults.TestSuiteDescription)}");
Console.WriteLine("\t" +
$"Total Tests: {testSuiteCount}");
Console.WriteLine("\t" +
$"Tests Passed: {testSuiteResults.TestsPassed}");
Console.WriteLine("\t" +
- $"Tests Failed: {testSuiteResults.TestsPassed}");
+ $"Tests Failed: {testSuiteResults.TestsFailed}");
Console.WriteLine("\t" +
$"TestSuite execution time: {testSuiteElapsedTime}");
diff --git a/Microsoft.PowerApps.TestAutomation.Browser/Microsoft.PowerApps.TestAutomation.Browser.csproj b/Microsoft.PowerApps.TestAutomation.Browser/Microsoft.PowerApps.TestAutomation.Browser.csproj
index 7377a311..30a52850 100644
--- a/Microsoft.PowerApps.TestAutomation.Browser/Microsoft.PowerApps.TestAutomation.Browser.csproj
+++ b/Microsoft.PowerApps.TestAutomation.Browser/Microsoft.PowerApps.TestAutomation.Browser.csproj
@@ -39,8 +39,8 @@
-
- ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
..\packages\DotNetSeleniumExtras.WaitHelpers.3.11.0\lib\net45\SeleniumExtras.WaitHelpers.dll
diff --git a/Microsoft.PowerApps.TestAutomation.Tests/Microsoft.PowerApps.TestAutomation.Tests.csproj b/Microsoft.PowerApps.TestAutomation.Tests/Microsoft.PowerApps.TestAutomation.Tests.csproj
index d4d32dc5..c3833506 100644
--- a/Microsoft.PowerApps.TestAutomation.Tests/Microsoft.PowerApps.TestAutomation.Tests.csproj
+++ b/Microsoft.PowerApps.TestAutomation.Tests/Microsoft.PowerApps.TestAutomation.Tests.csproj
@@ -38,8 +38,8 @@
True
-
- ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
+
+ ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll
@@ -65,6 +65,7 @@
+
diff --git a/Microsoft.PowerApps.TestAutomation.Tests/TestAutomation/SanitizeForLogTests.cs b/Microsoft.PowerApps.TestAutomation.Tests/TestAutomation/SanitizeForLogTests.cs
new file mode 100644
index 00000000..791a2902
--- /dev/null
+++ b/Microsoft.PowerApps.TestAutomation.Tests/TestAutomation/SanitizeForLogTests.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using ApiTestAutomation = Microsoft.PowerApps.TestAutomation.Api.TestAutomation;
+
+namespace Microsoft.PowerApps.TestAutomation.Tests
+{
+ ///
+ /// Unit tests for the console output helper. These do not drive a browser, and they
+ /// deliberately carry no test category so that the PowerAppsTestAutomation category filter
+ /// used by the UI test run does not select them.
+ ///
+ [TestClass]
+ public class SanitizeForLogTests
+ {
+ [TestMethod]
+ public void SanitizeForLog_ReturnsEmptyForNull()
+ {
+ Assert.AreEqual(string.Empty, ApiTestAutomation.SanitizeForLog(null));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_ReturnsEmptyForEmpty()
+ {
+ Assert.AreEqual(string.Empty, ApiTestAutomation.SanitizeForLog(string.Empty));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_LeavesOrdinaryTextUnchanged()
+ {
+ const string value = "Contoso Suite 1 - validates the order form (v2.1)";
+
+ Assert.AreEqual(value, ApiTestAutomation.SanitizeForLog(value));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_FlattensLineFeed()
+ {
+ Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\nafter"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_FlattensCarriageReturn()
+ {
+ Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\rafter"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_FlattensCarriageReturnLineFeed()
+ {
+ Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\r\nafter"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_FlattensUnicodeLineSeparators()
+ {
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u0085b"));
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u2028b"));
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u2029b"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_ReplacesOtherControlCharacters()
+ {
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\tb"));
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\0b"));
+ Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u001bb"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_SeparatesAdjacentMarkerCharacters()
+ {
+ Assert.AreEqual("# #", ApiTestAutomation.SanitizeForLog("##"));
+ Assert.AreEqual("# # #", ApiTestAutomation.SanitizeForLog("###"));
+ Assert.AreEqual("# # # #", ApiTestAutomation.SanitizeForLog("####"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_KeepsSingleMarkerCharacter()
+ {
+ Assert.AreEqual("issue #42", ApiTestAutomation.SanitizeForLog("issue #42"));
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_ResultNeverContainsAdjacentMarkerCharacters()
+ {
+ string[] values =
+ {
+ "##", "###", "####", "a##b", "#\r#", "#\n#", "##vso[task.setvariable]",
+ };
+
+ foreach (string value in values)
+ {
+ string result = ApiTestAutomation.SanitizeForLog(value);
+
+ Assert.IsFalse(
+ result.Contains("##"),
+ "Result for '" + value + "' still contains adjacent marker characters: " + result);
+ }
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_ResultIsAlwaysASingleLine()
+ {
+ string[] values =
+ {
+ "one\ntwo",
+ "one\r\ntwo",
+ "one\rtwo",
+ "one\u0085two",
+ "one\u2028two",
+ "one\u2029two",
+ "trailing\n",
+ "\nleading",
+ "many\n\n\nbreaks",
+ };
+
+ foreach (string value in values)
+ {
+ string result = ApiTestAutomation.SanitizeForLog(value);
+
+ Assert.AreEqual(
+ 1,
+ result.Split(new[] { '\r', '\n', '\u0085', '\u2028', '\u2029' }).Length,
+ "Result for '" + value.Replace("\r", "\\r").Replace("\n", "\\n") + "' spans more than one line.");
+ }
+ }
+
+ [TestMethod]
+ public void SanitizeForLog_PreservesLength()
+ {
+ // Line terminators are replaced rather than removed, so no characters are lost.
+ const string value = "a\nb\rc\td";
+
+ Assert.AreEqual(value.Length, ApiTestAutomation.SanitizeForLog(value).Length);
+ }
+ }
+}