Skip to content
This repository was archived by the owner on Sep 3, 2026. It is now read-only.

Commit 3a861ed

Browse files
tehcrashxorCopilot
andauthored
Sanitize test result metadata before console output (#36)
Test suite and test case metadata returned by the app under test is written directly to standard output. Values are now normalized to a single line before they are logged, so result text cannot affect the structure of the build log. Also included: - Correct the Newtonsoft.Json assembly reference. packages.config was moved to 13.0.1 but the project files still pointed at 12.0.3, so the reference did not resolve and the solution did not build. - Correct the suite summary, which reported the passed count on the "Tests Failed" line. - Add unit tests for the new helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3f3ff3f commit 3a861ed

5 files changed

Lines changed: 200 additions & 14 deletions

File tree

‎Microsoft.PowerApps.TestAutomation.Api/Microsoft.PowerApps.TestAutomation.Api.csproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
<WarningLevel>4</WarningLevel>
3232
</PropertyGroup>
3333
<ItemGroup>
34-
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
35-
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
34+
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
35+
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
3636
</Reference>
3737
<Reference Include="System" />
3838
<Reference Include="System.Core" />

‎Microsoft.PowerApps.TestAutomation.Api/Pages/TestAutomation.cs‎

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Generic;
99
using System.Diagnostics;
1010
using System.IO;
11+
using System.Text;
1112

1213
namespace Microsoft.PowerApps.TestAutomation.Api
1314
{
@@ -150,6 +151,50 @@ internal void InitiateTest(IWebDriver driver, Uri uri)
150151
}
151152
}
152153

154+
/// <summary>
155+
/// Normalizes text that originates from the app under test before it is written to
156+
/// standard output.
157+
/// </summary>
158+
/// <remarks>
159+
/// Test suite and test case names, descriptions and failure messages are supplied by the
160+
/// app being tested rather than by the pipeline that runs it. Build agents interpret the
161+
/// console stream line by line, so these values are flattened to a single line before they
162+
/// are logged and cannot affect how surrounding output is parsed.
163+
/// </remarks>
164+
/// <param name="value">The value to normalize.</param>
165+
/// <returns>A single-line representation of <paramref name="value"/>.</returns>
166+
public static string SanitizeForLog(string value)
167+
{
168+
if (string.IsNullOrEmpty(value))
169+
{
170+
return string.Empty;
171+
}
172+
173+
var sanitized = new StringBuilder(value.Length);
174+
char previous = '\0';
175+
176+
foreach (char character in value)
177+
{
178+
// char.IsControl already covers CR, LF and NEL (U+0085). The Unicode line and
179+
// paragraph separators sit outside that range, so they are named explicitly.
180+
char current = char.IsControl(character) || character == '\u2028' || character == '\u2029'
181+
? ' '
182+
: character;
183+
184+
// Keep consecutive '#' characters apart so they cannot form a marker, for runs
185+
// of any length.
186+
if (current == '#' && previous == '#')
187+
{
188+
sanitized.Append(' ');
189+
}
190+
191+
sanitized.Append(current);
192+
previous = current;
193+
}
194+
195+
return sanitized.ToString();
196+
}
197+
153198
public Tuple<int, int> ReportResultsToDevOps(JObject jObject, int testRunNumber)
154199
{
155200
var testExecutionMode = (int)jObject.GetValue("ExecutionMode");
@@ -183,17 +228,17 @@ public Tuple<int, int> ReportResultsToDevOps(JObject jObject, int testRunNumber)
183228

184229
// Output results to Console
185230
Console.WriteLine("\t" +
186-
$"TestSuite Name: {testCaseResults.TestSuiteName} with ID {testCaseResults.TestSuiteId}");
231+
$"TestSuite Name: {SanitizeForLog(testCaseResults.TestSuiteName)} with ID {SanitizeForLog(testCaseResults.TestSuiteId)}");
187232
Console.WriteLine("\t" +
188-
$"TestSuite Description: {testCaseResults.TestSuiteDescription}");
233+
$"TestSuite Description: {SanitizeForLog(testCaseResults.TestSuiteDescription)}");
189234
Console.WriteLine("\t" +
190-
$"TestCase Name: {testCaseResults.TestCaseName} with ID {testCaseResults.TestCaseId}");
235+
$"TestCase Name: {SanitizeForLog(testCaseResults.TestCaseName)} with ID {SanitizeForLog(testCaseResults.TestCaseId)}");
191236
Console.WriteLine("\t" +
192-
$"TestCase Description: {testCaseResults.TestCaseDescription}");
237+
$"TestCase Description: {SanitizeForLog(testCaseResults.TestCaseDescription)}");
193238
Console.WriteLine("\t" +
194239
$"Test Case Result: {testCaseResult}");
195240
Console.WriteLine("\t" +
196-
$"Test Case Failure Message: {testCaseResults.TestFailureMessage}");
241+
$"Test Case Failure Message: {SanitizeForLog(testCaseResults.TestFailureMessage)}");
197242
Console.WriteLine("\t" +
198243
$"Test Case execution time: {testCaseElapsedTime}");
199244

@@ -213,15 +258,15 @@ public Tuple<int, int> ReportResultsToDevOps(JObject jObject, int testRunNumber)
213258

214259
// Output results to Console
215260
Console.WriteLine("\t" +
216-
$"TestSuite Name: {testSuiteResults.TestSuiteName} with ID {testSuiteResults.TestSuiteId}");
261+
$"TestSuite Name: {SanitizeForLog(testSuiteResults.TestSuiteName)} with ID {SanitizeForLog(testSuiteResults.TestSuiteId)}");
217262
Console.WriteLine("\t" +
218-
$"TestSuite Description: {testSuiteResults.TestSuiteDescription}");
263+
$"TestSuite Description: {SanitizeForLog(testSuiteResults.TestSuiteDescription)}");
219264
Console.WriteLine("\t" +
220265
$"Total Tests: {testSuiteCount}");
221266
Console.WriteLine("\t" +
222267
$"Tests Passed: {testSuiteResults.TestsPassed}");
223268
Console.WriteLine("\t" +
224-
$"Tests Failed: {testSuiteResults.TestsPassed}");
269+
$"Tests Failed: {testSuiteResults.TestsFailed}");
225270
Console.WriteLine("\t" +
226271
$"TestSuite execution time: {testSuiteElapsedTime}");
227272

‎Microsoft.PowerApps.TestAutomation.Browser/Microsoft.PowerApps.TestAutomation.Browser.csproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
<AssemblyOriginatorKeyFile />
4040
</PropertyGroup>
4141
<ItemGroup>
42-
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
43-
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
42+
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
43+
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
4444
</Reference>
4545
<Reference Include="SeleniumExtras.WaitHelpers, Version=3.11.0.0, Culture=neutral, processorArchitecture=MSIL">
4646
<HintPath>..\packages\DotNetSeleniumExtras.WaitHelpers.3.11.0\lib\net45\SeleniumExtras.WaitHelpers.dll</HintPath>

‎Microsoft.PowerApps.TestAutomation.Tests/Microsoft.PowerApps.TestAutomation.Tests.csproj‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
<Private>True</Private>
3939
</Reference>
4040
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
41-
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
42-
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
41+
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
42+
<HintPath>..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
4343
</Reference>
4444
<Reference Include="System" />
4545
<Reference Include="System.Configuration" />
@@ -65,6 +65,7 @@
6565
<ItemGroup>
6666
<Compile Include="Properties\AssemblyInfo.cs" />
6767
<Compile Include="TestAutomation\RunTestAutomation.cs" />
68+
<Compile Include="TestAutomation\SanitizeForLogTests.cs" />
6869
<Compile Include="RunTestSettings.cs" />
6970
</ItemGroup>
7071
<ItemGroup>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using System;
6+
using ApiTestAutomation = Microsoft.PowerApps.TestAutomation.Api.TestAutomation;
7+
8+
namespace Microsoft.PowerApps.TestAutomation.Tests
9+
{
10+
/// <summary>
11+
/// Unit tests for the console output helper. These do not drive a browser, and they
12+
/// deliberately carry no test category so that the PowerAppsTestAutomation category filter
13+
/// used by the UI test run does not select them.
14+
/// </summary>
15+
[TestClass]
16+
public class SanitizeForLogTests
17+
{
18+
[TestMethod]
19+
public void SanitizeForLog_ReturnsEmptyForNull()
20+
{
21+
Assert.AreEqual(string.Empty, ApiTestAutomation.SanitizeForLog(null));
22+
}
23+
24+
[TestMethod]
25+
public void SanitizeForLog_ReturnsEmptyForEmpty()
26+
{
27+
Assert.AreEqual(string.Empty, ApiTestAutomation.SanitizeForLog(string.Empty));
28+
}
29+
30+
[TestMethod]
31+
public void SanitizeForLog_LeavesOrdinaryTextUnchanged()
32+
{
33+
const string value = "Contoso Suite 1 - validates the order form (v2.1)";
34+
35+
Assert.AreEqual(value, ApiTestAutomation.SanitizeForLog(value));
36+
}
37+
38+
[TestMethod]
39+
public void SanitizeForLog_FlattensLineFeed()
40+
{
41+
Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\nafter"));
42+
}
43+
44+
[TestMethod]
45+
public void SanitizeForLog_FlattensCarriageReturn()
46+
{
47+
Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\rafter"));
48+
}
49+
50+
[TestMethod]
51+
public void SanitizeForLog_FlattensCarriageReturnLineFeed()
52+
{
53+
Assert.AreEqual("before after", ApiTestAutomation.SanitizeForLog("before\r\nafter"));
54+
}
55+
56+
[TestMethod]
57+
public void SanitizeForLog_FlattensUnicodeLineSeparators()
58+
{
59+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u0085b"));
60+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u2028b"));
61+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u2029b"));
62+
}
63+
64+
[TestMethod]
65+
public void SanitizeForLog_ReplacesOtherControlCharacters()
66+
{
67+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\tb"));
68+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\0b"));
69+
Assert.AreEqual("a b", ApiTestAutomation.SanitizeForLog("a\u001bb"));
70+
}
71+
72+
[TestMethod]
73+
public void SanitizeForLog_SeparatesAdjacentMarkerCharacters()
74+
{
75+
Assert.AreEqual("# #", ApiTestAutomation.SanitizeForLog("##"));
76+
Assert.AreEqual("# # #", ApiTestAutomation.SanitizeForLog("###"));
77+
Assert.AreEqual("# # # #", ApiTestAutomation.SanitizeForLog("####"));
78+
}
79+
80+
[TestMethod]
81+
public void SanitizeForLog_KeepsSingleMarkerCharacter()
82+
{
83+
Assert.AreEqual("issue #42", ApiTestAutomation.SanitizeForLog("issue #42"));
84+
}
85+
86+
[TestMethod]
87+
public void SanitizeForLog_ResultNeverContainsAdjacentMarkerCharacters()
88+
{
89+
string[] values =
90+
{
91+
"##", "###", "####", "a##b", "#\r#", "#\n#", "##vso[task.setvariable]",
92+
};
93+
94+
foreach (string value in values)
95+
{
96+
string result = ApiTestAutomation.SanitizeForLog(value);
97+
98+
Assert.IsFalse(
99+
result.Contains("##"),
100+
"Result for '" + value + "' still contains adjacent marker characters: " + result);
101+
}
102+
}
103+
104+
[TestMethod]
105+
public void SanitizeForLog_ResultIsAlwaysASingleLine()
106+
{
107+
string[] values =
108+
{
109+
"one\ntwo",
110+
"one\r\ntwo",
111+
"one\rtwo",
112+
"one\u0085two",
113+
"one\u2028two",
114+
"one\u2029two",
115+
"trailing\n",
116+
"\nleading",
117+
"many\n\n\nbreaks",
118+
};
119+
120+
foreach (string value in values)
121+
{
122+
string result = ApiTestAutomation.SanitizeForLog(value);
123+
124+
Assert.AreEqual(
125+
1,
126+
result.Split(new[] { '\r', '\n', '\u0085', '\u2028', '\u2029' }).Length,
127+
"Result for '" + value.Replace("\r", "\\r").Replace("\n", "\\n") + "' spans more than one line.");
128+
}
129+
}
130+
131+
[TestMethod]
132+
public void SanitizeForLog_PreservesLength()
133+
{
134+
// Line terminators are replaced rather than removed, so no characters are lost.
135+
const string value = "a\nb\rc\td";
136+
137+
Assert.AreEqual(value.Length, ApiTestAutomation.SanitizeForLog(value).Length);
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)