Skip to content

Commit

Permalink
Test app service in CI (#3580)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
msftbot[bot] authored Dec 11, 2020
2 parents 9e4065f + a8c3249 commit c402640
Show file tree
Hide file tree
Showing 27 changed files with 889 additions and 174 deletions.
103 changes: 103 additions & 0 deletions UITests/UITests.App/App.AppService.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics;
using Microsoft.Toolkit.Mvvm.Messaging;
using UITests.App.Pages;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace UITests.App
{
/// <summary>
/// This file contains part of the app related to the AppService for communication between this test host and the test harness processes.
/// </summary>
public sealed partial class App
{
private AppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;

protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
IBackgroundTaskInstance taskInstance = args.TaskInstance;
AppServiceTriggerDetails appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;
_appServiceDeferral = taskInstance.GetDeferral();
taskInstance.Canceled += OnAppServicesCanceled;
_appServiceConnection = appService.AppServiceConnection;
_appServiceConnection.RequestReceived += OnAppServiceRequestReceived;
_appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
}

private async void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
AppServiceDeferral messageDeferral = args.GetDeferral();
ValueSet message = args.Request.Message;
string cmd = message["Command"] as string;

try
{
// Return the data to the caller.
if (cmd == "Start")
{
var pageName = message["Page"] as string;

Log.Comment("Received request for Page: {0}", pageName);

ValueSet returnMessage = new ValueSet();

// We await the OpenPage method to ensure the navigation has finished.
if (await WeakReferenceMessenger.Default.Send(new RequestPageMessage(pageName)))
{
returnMessage.Add("Status", "OK");
}
else
{
returnMessage.Add("Status", "BAD");
}

await args.Request.SendResponseAsync(returnMessage);
}
}
catch (Exception e)
{
// Your exception handling code here.
Log.Error("Exception processing request: {0}", e.Message);
}
finally
{
// Complete the deferral so that the platform knows that we're done responding to the app service call.
// Note: for error handling: this must be called even if SendResponseAsync() throws an exception.
messageDeferral.Complete();
}
}

private void OnAppServicesCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
_appServiceDeferral.Complete();
}

private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
_appServiceDeferral.Complete();
}

public async void SendLogMessage(string level, string msg)
{
var message = new ValueSet();
message.Add("Command", "Log");
message.Add("Level", level);
message.Add("Message", msg);

await _appServiceConnection.SendMessageAsync(message);

// TODO: do we care if we have a problem here?
}
}
}
11 changes: 4 additions & 7 deletions UITests/UITests.App/App.xaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
<Application
x:Class="UITests.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.App">

</Application>
<Application x:Class="UITests.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.App" />
17 changes: 16 additions & 1 deletion UITests/UITests.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UITests.App.Pages;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
Expand All @@ -11,12 +15,22 @@

namespace UITests.App
{
/// <summary>
/// Application class for hosting UI pages to test.
/// </summary>
public sealed partial class App
{
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
this.UnhandledException += this.App_UnhandledException;
}

private void App_UnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e)
{
// TODO: Also Log to a file?
Log.Error("Unhandled Exception: " + e.Message);
}

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

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

Expand Down
19 changes: 0 additions & 19 deletions UITests/UITests.App/MainPage.xaml

This file was deleted.

19 changes: 0 additions & 19 deletions UITests/UITests.App/MainPage.xaml.cs

This file was deleted.

17 changes: 17 additions & 0 deletions UITests/UITests.App/MainTestHost.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Page x:Class="UITests.App.MainTestHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:testhelpers="using:AppTestAutomationHelpers"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

<Grid>
<testhelpers:TestAutomationHelpersPanel />

<Frame x:Name="navigationFrame"
Navigated="NavigationFrame_Navigated"
NavigationFailed="NavigationFrame_NavigationFailed" />
</Grid>
</Page>
118 changes: 118 additions & 0 deletions UITests/UITests.App/MainTestHost.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Uwp.Extensions;
using UITests.App.Pages;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation;

namespace UITests.App
{
/// <summary>
/// MainPage hosting all other test pages.
/// </summary>
public sealed partial class MainTestHost : IRecipient<RequestPageMessage>
{
private DispatcherQueue _queue;

private Assembly _executingAssembly = Assembly.GetExecutingAssembly();

private TaskCompletionSource<bool> _loadingStateTask;

public MainTestHost()
{
InitializeComponent();

WeakReferenceMessenger.Default.Register<RequestPageMessage>(this);

_queue = DispatcherQueue.GetForCurrentThread();
}

public void Receive(RequestPageMessage message)
{
// Reply with task back to so it can be properly awaited link:App.AppService.xaml.cs#L56
message.Reply(OpenPage(message.PageName));
}

private async Task<bool> OpenPage(string pageName)
{
try
{
Log.Comment("Trying to Load Page: " + pageName);

_loadingStateTask = new TaskCompletionSource<bool>();

// Ensure we're on the UI thread as we'll be called from the AppService now.
_ = _queue.EnqueueAsync(() =>
{
// Navigate without extra animations
navigationFrame.Navigate(FindPageType(pageName), new SuppressNavigationTransitionInfo());
});

// Wait for load to complete
await _loadingStateTask.Task;
}
catch (Exception e)
{
Log.Error("Exception Loading Page {0}: {1} ", pageName, e.Message);
return false;
}

return true;
}

private Type FindPageType(string pageName)
{
try
{
return _executingAssembly.GetType("UITests.App.Pages." + pageName);
}
catch (Exception e)
{
Log.Error("Exception Finding Page {0}: {1} ", pageName, e.Message);
}

return null;
}

private void NavigationFrame_Navigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
Log.Comment("Navigated to Page {0}", e.SourcePageType.FullName);
if (e.Content is Page page)
{
if (page.IsLoaded)
{
Log.Comment("Loaded Page {0}", e.SourcePageType.FullName);
_loadingStateTask.SetResult(true);
}
else
{
page.Loaded += this.Page_Loaded;
}
}
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
var page = sender as Page;

page.Loaded -= Page_Loaded;

Log.Comment("Loaded Page (E) {0}", page.GetType().FullName);
_loadingStateTask.SetResult(true);
}

private void NavigationFrame_NavigationFailed(object sender, Windows.UI.Xaml.Navigation.NavigationFailedEventArgs e)
{
Log.Error("Failed to navigate to page {0}", e.SourcePageType.FullName);
_loadingStateTask.SetResult(false);
}
}
}
5 changes: 5 additions & 0 deletions UITests/UITests.App/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap:Extension Category="windows.appService">
<uap:AppService Name="TestHarnessCommunicationService"/>
</uap:Extension>
</Extensions>
</Application>
</Applications>

Expand Down
18 changes: 18 additions & 0 deletions UITests/UITests.App/RequestPageMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Toolkit.Mvvm.Messaging.Messages;

namespace UITests.App
{
public sealed class RequestPageMessage : AsyncRequestMessage<bool>
{
public RequestPageMessage(string name)
{
PageName = name;
}

public string PageName { get; }
}
}
Loading

0 comments on commit c402640

Please sign in to comment.