Skip to content

Commit c402640

Browse files
author
msftbot[bot]
authored
Test app service in CI (#3580)
Quick prototype to test out an app service on top of #3552. Working locally with MSTest, though some oddities when trying to shore-up timing guards, unknown unhandled exception... Went back to looser code and working well. Not sure if this resolves the TAEF error we're seeing in #3552 yet, but wanted to see if this provided different info. Lots to do here to clean this work up, as I just hacked things together to see if it would work, but looking very promising. This should allow us to optimize the start-up performance of the app more easily as well as have a bi-directional communication pipe between our two processes for logging messages from the host within the harness/CI logs. 🎉 FYI @RosarioPulella
2 parents 9e4065f + a8c3249 commit c402640

27 files changed

+889
-174
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics;
7+
using Microsoft.Toolkit.Mvvm.Messaging;
8+
using UITests.App.Pages;
9+
using Windows.ApplicationModel.Activation;
10+
using Windows.ApplicationModel.AppService;
11+
using Windows.ApplicationModel.Background;
12+
using Windows.Foundation.Collections;
13+
using Windows.UI.Xaml;
14+
using Windows.UI.Xaml.Controls;
15+
16+
namespace UITests.App
17+
{
18+
/// <summary>
19+
/// This file contains part of the app related to the AppService for communication between this test host and the test harness processes.
20+
/// </summary>
21+
public sealed partial class App
22+
{
23+
private AppServiceConnection _appServiceConnection;
24+
private BackgroundTaskDeferral _appServiceDeferral;
25+
26+
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
27+
{
28+
base.OnBackgroundActivated(args);
29+
IBackgroundTaskInstance taskInstance = args.TaskInstance;
30+
AppServiceTriggerDetails appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;
31+
_appServiceDeferral = taskInstance.GetDeferral();
32+
taskInstance.Canceled += OnAppServicesCanceled;
33+
_appServiceConnection = appService.AppServiceConnection;
34+
_appServiceConnection.RequestReceived += OnAppServiceRequestReceived;
35+
_appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
36+
}
37+
38+
private async void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
39+
{
40+
AppServiceDeferral messageDeferral = args.GetDeferral();
41+
ValueSet message = args.Request.Message;
42+
string cmd = message["Command"] as string;
43+
44+
try
45+
{
46+
// Return the data to the caller.
47+
if (cmd == "Start")
48+
{
49+
var pageName = message["Page"] as string;
50+
51+
Log.Comment("Received request for Page: {0}", pageName);
52+
53+
ValueSet returnMessage = new ValueSet();
54+
55+
// We await the OpenPage method to ensure the navigation has finished.
56+
if (await WeakReferenceMessenger.Default.Send(new RequestPageMessage(pageName)))
57+
{
58+
returnMessage.Add("Status", "OK");
59+
}
60+
else
61+
{
62+
returnMessage.Add("Status", "BAD");
63+
}
64+
65+
await args.Request.SendResponseAsync(returnMessage);
66+
}
67+
}
68+
catch (Exception e)
69+
{
70+
// Your exception handling code here.
71+
Log.Error("Exception processing request: {0}", e.Message);
72+
}
73+
finally
74+
{
75+
// Complete the deferral so that the platform knows that we're done responding to the app service call.
76+
// Note: for error handling: this must be called even if SendResponseAsync() throws an exception.
77+
messageDeferral.Complete();
78+
}
79+
}
80+
81+
private void OnAppServicesCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
82+
{
83+
_appServiceDeferral.Complete();
84+
}
85+
86+
private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
87+
{
88+
_appServiceDeferral.Complete();
89+
}
90+
91+
public async void SendLogMessage(string level, string msg)
92+
{
93+
var message = new ValueSet();
94+
message.Add("Command", "Log");
95+
message.Add("Level", level);
96+
message.Add("Message", msg);
97+
98+
await _appServiceConnection.SendMessageAsync(message);
99+
100+
// TODO: do we care if we have a problem here?
101+
}
102+
}
103+
}

UITests/UITests.App/App.xaml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
<Application
2-
x:Class="UITests.App.App"
3-
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4-
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5-
xmlns:local="using:UITests.App">
6-
7-
</Application>
1+
<Application x:Class="UITests.App.App"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:local="using:UITests.App" />

UITests/UITests.App/App.xaml.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Reflection;
9+
using UITests.App.Pages;
610
using Windows.ApplicationModel;
711
using Windows.ApplicationModel.Activation;
812
using Windows.UI.Xaml;
@@ -11,12 +15,22 @@
1115

1216
namespace UITests.App
1317
{
18+
/// <summary>
19+
/// Application class for hosting UI pages to test.
20+
/// </summary>
1421
public sealed partial class App
1522
{
1623
public App()
1724
{
1825
this.InitializeComponent();
1926
this.Suspending += OnSuspending;
27+
this.UnhandledException += this.App_UnhandledException;
28+
}
29+
30+
private void App_UnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
31+
{
32+
// TODO: Also Log to a file?
33+
Log.Error("Unhandled Exception: " + e.Message);
2034
}
2135

2236
/// <summary>
@@ -53,7 +67,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
5367
// When the navigation stack isn't restored navigate to the first page,
5468
// configuring the new page by passing required information as a navigation
5569
// parameter
56-
rootFrame.Navigate(typeof(MainPage), e.Arguments);
70+
rootFrame.Navigate(typeof(MainTestHost), e.Arguments);
5771
}
5872

5973
// Ensure the current window is active
@@ -68,6 +82,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
6882
/// <param name="e">Details about the navigation failure</param>
6983
private void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
7084
{
85+
Log.Error("Failed to load root page: " + e.SourcePageType.FullName);
7186
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
7287
}
7388

UITests/UITests.App/MainPage.xaml

Lines changed: 0 additions & 19 deletions
This file was deleted.

UITests/UITests.App/MainPage.xaml.cs

Lines changed: 0 additions & 19 deletions
This file was deleted.

UITests/UITests.App/MainTestHost.xaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Page x:Class="UITests.App.MainTestHost"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
xmlns:testhelpers="using:AppTestAutomationHelpers"
7+
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
8+
mc:Ignorable="d">
9+
10+
<Grid>
11+
<testhelpers:TestAutomationHelpersPanel />
12+
13+
<Frame x:Name="navigationFrame"
14+
Navigated="NavigationFrame_Navigated"
15+
NavigationFailed="NavigationFrame_NavigationFailed" />
16+
</Grid>
17+
</Page>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Reflection;
7+
using System.Threading.Tasks;
8+
using Microsoft.Toolkit.Mvvm.Messaging;
9+
using Microsoft.Toolkit.Uwp.Extensions;
10+
using UITests.App.Pages;
11+
using Windows.System;
12+
using Windows.UI.Xaml;
13+
using Windows.UI.Xaml.Controls;
14+
using Windows.UI.Xaml.Media.Animation;
15+
16+
namespace UITests.App
17+
{
18+
/// <summary>
19+
/// MainPage hosting all other test pages.
20+
/// </summary>
21+
public sealed partial class MainTestHost : IRecipient<RequestPageMessage>
22+
{
23+
private DispatcherQueue _queue;
24+
25+
private Assembly _executingAssembly = Assembly.GetExecutingAssembly();
26+
27+
private TaskCompletionSource<bool> _loadingStateTask;
28+
29+
public MainTestHost()
30+
{
31+
InitializeComponent();
32+
33+
WeakReferenceMessenger.Default.Register<RequestPageMessage>(this);
34+
35+
_queue = DispatcherQueue.GetForCurrentThread();
36+
}
37+
38+
public void Receive(RequestPageMessage message)
39+
{
40+
// Reply with task back to so it can be properly awaited link:App.AppService.xaml.cs#L56
41+
message.Reply(OpenPage(message.PageName));
42+
}
43+
44+
private async Task<bool> OpenPage(string pageName)
45+
{
46+
try
47+
{
48+
Log.Comment("Trying to Load Page: " + pageName);
49+
50+
_loadingStateTask = new TaskCompletionSource<bool>();
51+
52+
// Ensure we're on the UI thread as we'll be called from the AppService now.
53+
_ = _queue.EnqueueAsync(() =>
54+
{
55+
// Navigate without extra animations
56+
navigationFrame.Navigate(FindPageType(pageName), new SuppressNavigationTransitionInfo());
57+
});
58+
59+
// Wait for load to complete
60+
await _loadingStateTask.Task;
61+
}
62+
catch (Exception e)
63+
{
64+
Log.Error("Exception Loading Page {0}: {1} ", pageName, e.Message);
65+
return false;
66+
}
67+
68+
return true;
69+
}
70+
71+
private Type FindPageType(string pageName)
72+
{
73+
try
74+
{
75+
return _executingAssembly.GetType("UITests.App.Pages." + pageName);
76+
}
77+
catch (Exception e)
78+
{
79+
Log.Error("Exception Finding Page {0}: {1} ", pageName, e.Message);
80+
}
81+
82+
return null;
83+
}
84+
85+
private void NavigationFrame_Navigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
86+
{
87+
Log.Comment("Navigated to Page {0}", e.SourcePageType.FullName);
88+
if (e.Content is Page page)
89+
{
90+
if (page.IsLoaded)
91+
{
92+
Log.Comment("Loaded Page {0}", e.SourcePageType.FullName);
93+
_loadingStateTask.SetResult(true);
94+
}
95+
else
96+
{
97+
page.Loaded += this.Page_Loaded;
98+
}
99+
}
100+
}
101+
102+
private void Page_Loaded(object sender, RoutedEventArgs e)
103+
{
104+
var page = sender as Page;
105+
106+
page.Loaded -= Page_Loaded;
107+
108+
Log.Comment("Loaded Page (E) {0}", page.GetType().FullName);
109+
_loadingStateTask.SetResult(true);
110+
}
111+
112+
private void NavigationFrame_NavigationFailed(object sender, Windows.UI.Xaml.Navigation.NavigationFailedEventArgs e)
113+
{
114+
Log.Error("Failed to navigate to page {0}", e.SourcePageType.FullName);
115+
_loadingStateTask.SetResult(false);
116+
}
117+
}
118+
}

UITests/UITests.App/Package.appxmanifest

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
4141
<uap:SplashScreen Image="Assets\SplashScreen.png" />
4242
</uap:VisualElements>
43+
<Extensions>
44+
<uap:Extension Category="windows.appService">
45+
<uap:AppService Name="TestHarnessCommunicationService"/>
46+
</uap:Extension>
47+
</Extensions>
4348
</Application>
4449
</Applications>
4550

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
6+
7+
namespace UITests.App
8+
{
9+
public sealed class RequestPageMessage : AsyncRequestMessage<bool>
10+
{
11+
public RequestPageMessage(string name)
12+
{
13+
PageName = name;
14+
}
15+
16+
public string PageName { get; }
17+
}
18+
}

0 commit comments

Comments
 (0)