Skip to content

Commit 3a4eabe

Browse files
authored
Run Categories Separately (#29362)
* Run Categories Separately * - fix windows * - cleanup * - wait for last event to fire before finishing * - keep retry for windows and android
1 parent 13e0853 commit 3a4eabe

File tree

26 files changed

+364
-99
lines changed

26 files changed

+364
-99
lines changed

eng/devices/catalyst.cake

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,32 +85,26 @@ void ExecuteBuild(string project, string binDir, string config, string rid, stri
8585
});
8686
}
8787

88+
8889
void ExecuteTests(string project, string device, string resultsDir, string config, string tfm, string rid, string toolPath)
8990
{
9091
CleanResults(resultsDir);
9192

9293
var testApp = GetTestApplications(project, device, config, tfm, rid).FirstOrDefault();
93-
9494
Information($"Testing App: {testApp}");
95-
var settings = new DotNetToolSettings
96-
{
97-
DiagnosticOutput = true,
98-
ToolPath = toolPath,
99-
ArgumentCustomization = args => args.Append($"run xharness apple test --app=\"{testApp}\" --targets=\"{device}\" --output-directory=\"{resultsDir}\" --verbosity=\"Debug\" ")
100-
};
10195

102-
bool testsFailed = true;
103-
try
104-
{
105-
DotNetTool("tool", settings);
106-
testsFailed = false;
107-
}
108-
finally
96+
RunMacAndiOSTests(project, device, resultsDir, config, tfm, rid, toolPath, projectPath, (category) =>
10997
{
110-
HandleTestResults(resultsDir, testsFailed, true);
111-
}
112-
113-
Information("Testing completed.");
98+
return new DotNetToolSettings
99+
{
100+
DiagnosticOutput = true,
101+
ToolPath = toolPath,
102+
ArgumentCustomization = args =>
103+
args.Append($"run xharness apple test --app=\"{testApp}\" --targets=\"{device}\" --output-directory=\"{resultsDir}\" " +
104+
" --verbosity=\"Debug\" " +
105+
$"--set-env=\"TestFilter={category}\" ")
106+
};
107+
});
114108
}
115109

116110
void ExecuteUITests(string project, string app, string device, string resultsDir, string binDir, string config, string tfm, string rid, string toolPath)

eng/devices/devices-shared.cake

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,72 @@ void CleanResults(string resultsDir)
133133
}
134134
}
135135

136-
void HandleTestResults(string resultsDir, bool testsFailed, bool needsNameFix)
136+
List<string> GetTestCategoriesToRunSeparately(string projectPath)
137+
{
138+
139+
if (!string.IsNullOrEmpty(testFilter))
140+
{
141+
return new List<string> { testFilter };
142+
}
143+
144+
if (!projectPath.EndsWith("Controls.DeviceTests.csproj") && !projectPath.EndsWith("Core.DeviceTests.csproj"))
145+
{
146+
return new List<string>
147+
{
148+
""
149+
};
150+
}
151+
152+
var file = Context.GetCallerInfo().SourceFilePath;
153+
var directoryPath = file.GetDirectory().FullPath;
154+
Information($"Directory: {directoryPath}");
155+
Information(directoryPath);
156+
157+
// Search for files that match the pattern
158+
List<FilePath> dllFilePath = null;
159+
160+
if (projectPath.EndsWith("Controls.DeviceTests.csproj"))
161+
dllFilePath = GetFiles($"{directoryPath}/../../**/Microsoft.Maui.Controls.DeviceTests.dll").ToList();
162+
163+
if (projectPath.EndsWith("Core.DeviceTests.csproj"))
164+
dllFilePath = GetFiles($"{directoryPath}/../../**/Microsoft.Maui.Core.DeviceTests.dll").ToList();
165+
166+
System.Reflection.Assembly loadedAssembly = null;
167+
168+
foreach (var filePath in dllFilePath)
169+
{
170+
try
171+
{
172+
loadedAssembly = System.Reflection.Assembly.LoadFrom(filePath.FullPath);
173+
Information($"Loaded assembly from {filePath}: {loadedAssembly.FullName}");
174+
break; // Exit the loop if the assembly is loaded successfully
175+
}
176+
catch (Exception ex)
177+
{
178+
Warning($"Failed to load assembly from {filePath}: {ex.Message}");
179+
}
180+
}
181+
182+
if (loadedAssembly == null)
183+
{
184+
throw new Exception("No test assembly found.");
185+
}
186+
var testCategoryType = loadedAssembly.GetType("Microsoft.Maui.DeviceTests.TestCategory");
187+
188+
var values = new List<string>();
189+
190+
foreach (var field in testCategoryType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
191+
{
192+
if (field.FieldType == typeof(string))
193+
{
194+
values.Add($"Category={(string)field.GetValue(null)}");
195+
}
196+
}
197+
198+
return values.ToList();
199+
}
200+
201+
void HandleTestResults(string resultsDir, bool testsFailed, bool needsNameFix, string suffix = null)
137202
{
138203
Information($"Handling test results: {resultsDir}");
139204

@@ -145,10 +210,24 @@ void HandleTestResults(string resultsDir, bool testsFailed, bool needsNameFix)
145210
{
146211
throw new Exception("No test results found.");
147212
}
213+
148214
if (FileExists(resultsFile))
149215
{
150216
Information($"Test results found on {resultsDir}");
151-
CopyFile(resultsFile, resultsFile.GetDirectory().CombineWithFilePath("TestResults.xml"));
217+
MoveFile(resultsFile, resultsFile.GetDirectory().CombineWithFilePath($"TestResults{suffix}.xml"));
218+
var logFiles = GetFiles($"{resultsDir}/*.log");
219+
220+
foreach (var logFile in logFiles)
221+
{
222+
if (logFile.GetFilename().ToString().StartsWith("TestResults"))
223+
{
224+
// These are log files that have already been renamed
225+
continue;
226+
}
227+
228+
Information($"Log file found: {logFile.GetFilename().ToString()}");
229+
MoveFile(logFile, resultsFile.GetDirectory().CombineWithFilePath($"TestResults{suffix}-{logFile.GetFilename()}"));
230+
}
152231
}
153232
}
154233

@@ -158,12 +237,21 @@ void HandleTestResults(string resultsDir, bool testsFailed, bool needsNameFix)
158237
EnsureDirectoryExists(failurePath);
159238
// The tasks will retry the tests and overwrite the failed results each retry
160239
// we want to retain the failed results for diagnostic purposes
161-
CopyFiles($"{resultsDir}/*.*", failurePath);
240+
241+
var searchQuery = "*.*";
242+
243+
if (!string.IsNullOrWhiteSpace(suffix))
244+
{
245+
searchQuery = $"*{suffix}*.*";
246+
}
247+
248+
// Only copy files from this suffix set of failures
249+
CopyFiles($"{resultsDir}/{searchQuery}", failurePath);
162250

163251
// We don't want these to upload
164-
MoveFile($"{failurePath}/TestResults.xml", $"{failurePath}/Results.xml");
252+
MoveFile($"{failurePath}/TestResults{suffix}.xml", $"{failurePath}/Results{suffix}.xml");
165253
}
166-
FailRunOnOnlyInconclusiveTests($"{resultsDir}/TestResults.xml");
254+
FailRunOnOnlyInconclusiveTests($"{resultsDir}/TestResults{suffix}.xml");
167255
}
168256

169257
DirectoryPath DetermineBinlogDirectory(string projectPath, string binlogArg)
@@ -185,6 +273,67 @@ DirectoryPath DetermineBinlogDirectory(string projectPath, string binlogArg)
185273
}
186274
}
187275

276+
void RunMacAndiOSTests(
277+
string project, string device, string resultsDir, string config, string tfm, string rid, string toolPath, string projectPath,
278+
Func<string, DotNetToolSettings> getSettings)
279+
{
280+
Exception exception = null;
281+
foreach (var category in GetTestCategoriesToRunSeparately(projectPath))
282+
{
283+
bool testsFailed = true;
284+
Information($"Running tests for category: {category}");
285+
var settings = getSettings(category);
286+
var suffix = category.Split('=').Skip(1).FirstOrDefault();
287+
288+
try
289+
{
290+
for(int i = 0; i < 2; i++)
291+
{
292+
Information($"Running test attempt {i}");
293+
try
294+
{
295+
DotNetTool("tool", settings);
296+
testsFailed = false;
297+
break;
298+
}
299+
catch (Exception ex)
300+
{
301+
Information($"Test attempt {i} failed: {ex.Message}");
302+
if (i == 1)
303+
{
304+
throw;
305+
}
306+
else
307+
{
308+
// delete any log files created so it's fresh for the rerun
309+
HandleTestResults(resultsDir, false, true, "-" + suffix);
310+
var logFiles = GetFiles($"{resultsDir}/*-{suffix}*");
311+
312+
foreach (var logFile in logFiles)
313+
{
314+
DeleteFile(logFile);
315+
}
316+
}
317+
}
318+
}
319+
}
320+
catch (Exception ex)
321+
{
322+
exception = ex;
323+
}
324+
finally
325+
{
326+
HandleTestResults(resultsDir, testsFailed, true, "-" + suffix);
327+
}
328+
}
329+
330+
Information("Testing completed.");
331+
if (exception is not null)
332+
{
333+
throw exception;
334+
}
335+
}
336+
188337
void LogSetupInfo(string toolPath)
189338
{
190339
Information($"DOTNET_TOOL_PATH: {toolPath}");

eng/devices/ios.cake

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -173,45 +173,38 @@ void ExecuteTests(string project, string device, string resultsDir, string confi
173173

174174
Information($"Testing App: {testApp}");
175175

176-
var settings = new DotNetToolSettings
176+
177+
178+
RunMacAndiOSTests(project, device, resultsDir, config, tfm, rid, toolPath, projectPath, (category) =>
177179
{
178-
ToolPath = toolPath,
179-
DiagnosticOutput = true,
180-
ArgumentCustomization = args =>
180+
return new DotNetToolSettings
181181
{
182-
args.Append("run xharness apple test " +
183-
$"--app=\"{testApp}\" " +
184-
$"--targets=\"{device}\" " +
185-
$"--output-directory=\"{resultsDir}\" " +
186-
$"--timeout=01:15:00 " +
187-
$"--launch-timeout=00:06:00 " +
188-
xcode_args +
189-
$"--verbosity=\"Debug\" ");
190-
191-
if (device.Contains("device"))
182+
ToolPath = toolPath,
183+
DiagnosticOutput = true,
184+
ArgumentCustomization = args =>
192185
{
193-
if (string.IsNullOrEmpty(DEVICE_UDID))
186+
args.Append("run xharness apple test " +
187+
$"--app=\"{testApp}\" " +
188+
$"--targets=\"{device}\" " +
189+
$"--output-directory=\"{resultsDir}\" " +
190+
$"--timeout=01:15:00 " +
191+
$"--launch-timeout=00:06:00 " +
192+
xcode_args +
193+
$"--verbosity=\"Debug\" " +
194+
$"--set-env=\"TestFilter={category}\" ");
195+
196+
if (device.Contains("device"))
194197
{
195-
throw new Exception("No device was found to install the app on. See the Setup method for more details.");
198+
if (string.IsNullOrEmpty(DEVICE_UDID))
199+
{
200+
throw new Exception("No device was found to install the app on. See the Setup method for more details.");
201+
}
202+
args.Append($"--device=\"{DEVICE_UDID}\" ");
196203
}
197-
args.Append($"--device=\"{DEVICE_UDID}\" ");
204+
return args;
198205
}
199-
return args;
200-
}
201-
};
202-
203-
bool testsFailed = true;
204-
try
205-
{
206-
DotNetTool("tool", settings);
207-
testsFailed = false;
208-
}
209-
finally
210-
{
211-
HandleTestResults(resultsDir, testsFailed, true);
212-
}
213-
214-
Information("Testing completed.");
206+
};
207+
});
215208
}
216209

217210
void ExecutePrepareUITests(string project, string app, string device, string resultsDir, string binDir, string config, string tfm, string rid, string ver, string toolPath)

eng/pipelines/common/device-tests-steps.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ steps:
145145
displayName: Execute Test Run
146146
workingDirectory: ${{ parameters.checkoutDirectory }}
147147
condition: and(succeeded(), ne('${{ parameters.buildType }}', 'buildOnly'))
148-
retryCountOnTaskFailure: 1
148+
${{ if or(eq(parameters.buildType, 'windows'), eq(parameters.platform, 'android')) }}:
149+
retryCountOnTaskFailure: 1
149150

150151

151152
##################################################

src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ protected Task CreateHandlerAndAddToWindow<THandler>(IElement view, Action<THand
140140
protected Task CreateHandlerAndAddToWindow<THandler>(IElement view, Func<THandler, Task> action, IMauiContext mauiContext = null, TimeSpan? timeOut = null)
141141
where THandler : class, IElementHandler
142142
{
143+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
143144
mauiContext ??= MauiContext;
144145

145146
if (System.Diagnostics.Debugger.IsAttached)
@@ -275,7 +276,11 @@ void OnBatchCommitted(object sender, Controls.Internals.EventArg<VisualElement>
275276
finally
276277
{
277278
_takeOverMainContentSempahore.Release();
278-
TestRunnerLogger.LogDebug($"Finished Running Test");
279+
stopwatch.Stop();
280+
TestRunnerLogger.LogDebug($"Finished Running Test: {stopwatch.Elapsed}");
281+
282+
if (stopwatch.ElapsedMilliseconds > 15000)
283+
TestRunnerLogger.LogError($"Test took longer than 15 seconds to complete.");
279284
}
280285
});
281286
}

src/Controls/tests/DeviceTests/Elements/Button/ButtonTests.iOS.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ UIButton GetPlatformButton(ButtonHandler buttonHandler) =>
1717

1818
Task<string> GetPlatformText(ButtonHandler buttonHandler)
1919
{
20-
return InvokeOnMainThreadAsync(() => GetPlatformButton(buttonHandler).CurrentTitle);
20+
return InvokeOnMainThreadAsync(() => GetPlatformButton(buttonHandler).CurrentTitle!);
2121
}
2222

2323
UILineBreakMode GetPlatformLineBreakMode(ButtonHandler buttonHandler) =>

src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,6 @@ await InvokeOnMainThreadAsync(async () =>
326326
}
327327

328328
[Category(TestCategory.Editor)]
329-
[Category(TestCategory.TextInput)]
330329
[Collection(RunInNewWindowCollection)]
331330
public class EditorTextInputTests : TextInputTests<EditorHandler, Editor>
332331
{
@@ -340,4 +339,4 @@ protected override Task<string> GetPlatformText(EditorHandler handler) =>
340339
EditorTests.GetPlatformText(handler);
341340
}
342341
}
343-
}
342+
}

src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ await InvokeOnMainThreadAsync(async () =>
239239
}
240240

241241
[Category(TestCategory.Entry)]
242-
[Category(TestCategory.TextInput)]
243242
[Collection(RunInNewWindowCollection)]
244243
public class EntryTextInputTests : TextInputTests<EntryHandler, Entry>
245244
{

src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Task<bool> GetPlatformIsVisible(EntryHandler entryHandler)
6666
}
6767

6868
[Collection(ControlsHandlerTestBase.RunInNewWindowCollection)]
69+
[Category(TestCategory.Entry)]
6970
public class ScrollTests : ControlsHandlerTestBase
7071
{
7172
[Fact]
@@ -126,6 +127,7 @@ await contentViewHandler.PlatformView.AttachAndRun(async () =>
126127
}
127128

128129
[Collection(ControlsHandlerTestBase.RunInNewWindowCollection)]
130+
[Category(TestCategory.Entry)]
129131
public class NextKeyboardTests : ControlsHandlerTestBase
130132
{
131133
void SetupNextBuilder()

src/Controls/tests/DeviceTests/Elements/Page/PageTests.Android.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace Microsoft.Maui.DeviceTests
1111
{
12+
[Category(TestCategory.Page)]
1213
public partial class PageTests : ControlsHandlerTestBase
1314
{
1415
//src/Compatibility/Core/tests/Android/EmbeddingTests.cs

0 commit comments

Comments
 (0)