From 45db18242f5534d5bc445b70415fd60bc72cf36f Mon Sep 17 00:00:00 2001 From: Pepe Rivera Date: Mon, 20 May 2019 16:27:01 -0700 Subject: [PATCH] Add a robust way to start/stop the WinAppDriverService * Add robust way of starting and stopping service --- build/pipelines/templates/run-ui-tests.yaml | 16 +- src/CalculatorUITestFramework/WinAppDriver.cs | 24 ++- .../WindowsDriverLocalService.cs | 178 ++++++++++++++++++ .../WindowsDriverServiceBuilder.cs | 93 +++++++++ 4 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 src/CalculatorUITestFramework/WindowsDriverLocalService.cs create mode 100644 src/CalculatorUITestFramework/WindowsDriverServiceBuilder.cs diff --git a/build/pipelines/templates/run-ui-tests.yaml b/build/pipelines/templates/run-ui-tests.yaml index 070293db..664a0111 100644 --- a/build/pipelines/templates/run-ui-tests.yaml +++ b/build/pipelines/templates/run-ui-tests.yaml @@ -15,6 +15,10 @@ jobs: steps: - checkout: none + - powershell: Set-DisplayResolution -Width 1920 -Height 1080 -Force + displayName: Set resolution to 1920x1080 + continueOnError: true + - task: DownloadBuildArtifacts@0 displayName: Download AppxBundle and CalculatorUITests inputs: @@ -35,11 +39,6 @@ jobs: filePath: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/Calculator/AppPackages/Calculator_$(Build.BuildNumber)_Test/Add-AppDevPackage.ps1 arguments: -Force - - task: WinAppDriver.winappdriver-pipelines-task.winappdriver-pipelines-task.Windows Application Driver@0 - displayName: 'WinAppDriver - Start' - inputs: - AgentResolution: 1080p - - task: VSTest@2 displayName: Run CalculatorUITests inputs: @@ -47,9 +46,4 @@ jobs: vsTestVersion: 16.0 runSettingsFile: $(Build.ArtifactStagingDirectory)/drop/Release/${{ parameters.platform }}/publish/CalculatorUITests.runsettings platform: ${{ parameters.platform }} - configuration: Release - - - task: WinAppDriver.winappdriver-pipelines-task.winappdriver-pipelines-task.Windows Application Driver@0 - displayName: 'WinAppDriver - Stop' - inputs: - OperationType: Stop \ No newline at end of file + configuration: Release \ No newline at end of file diff --git a/src/CalculatorUITestFramework/WinAppDriver.cs b/src/CalculatorUITestFramework/WinAppDriver.cs index ce2dddb2..01e49aa6 100644 --- a/src/CalculatorUITestFramework/WinAppDriver.cs +++ b/src/CalculatorUITestFramework/WinAppDriver.cs @@ -4,14 +4,14 @@ using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Text; namespace CalculatorUITestFramework { public sealed class WinAppDriver { - // Note: append /wd/hub to the URL if you're directing the test at Appium - private const string windowsApplicationDriverUrl = "http://127.0.0.1:4723"; + private WindowsDriverLocalService windowsDriverService = null; private const string calculatorAppId = "Microsoft.WindowsCalculator.Dev_8wekyb3d8bbwe!App"; private static WinAppDriver instance = null; public static WinAppDriver Instance @@ -36,6 +36,18 @@ namespace CalculatorUITestFramework public void SetupCalculatorSession(TestContext context) { + windowsDriverService = new WindowsDriverServiceBuilder().Build(); + + windowsDriverService.OutputDataReceived += new DataReceivedEventHandler((sender, e) => + { + if (!String.IsNullOrEmpty(e.Data)) + { + Console.WriteLine(e.Data); + } + }); + + windowsDriverService.Start(); + // Launch Calculator application if it is not yet launched if (CalculatorSession == null) { @@ -44,7 +56,7 @@ namespace CalculatorUITestFramework var options = new AppiumOptions(); options.AddAdditionalCapability("app", calculatorAppId); options.AddAdditionalCapability("deviceName", "WindowsPC"); - CalculatorSession = new WindowsDriver(new Uri(windowsApplicationDriverUrl), options); + CalculatorSession = new WindowsDriver(windowsDriverService.ServiceUrl, options); CalculatorSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); Assert.IsNotNull(CalculatorSession); } @@ -58,6 +70,12 @@ namespace CalculatorUITestFramework CalculatorSession.Quit(); CalculatorSession = null; } + + if (windowsDriverService != null) + { + windowsDriverService.Dispose(); + windowsDriverService = null; + } } diff --git a/src/CalculatorUITestFramework/WindowsDriverLocalService.cs b/src/CalculatorUITestFramework/WindowsDriverLocalService.cs new file mode 100644 index 00000000..8fec7a9a --- /dev/null +++ b/src/CalculatorUITestFramework/WindowsDriverLocalService.cs @@ -0,0 +1,178 @@ +using OpenQA.Selenium.Appium.Service; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; + +namespace CalculatorUITestFramework +{ + public class WindowsDriverLocalService : IDisposable + { + private FileInfo FileName; + private string Arguments; + private IPAddress IP; + private int Port; + private TimeSpan InitializationTimeout; + private Process Service; + + public event DataReceivedEventHandler OutputDataReceived; + + internal WindowsDriverLocalService( + FileInfo fileName, + string arguments, + IPAddress ip, + int port, + TimeSpan initializationTimeout) + { + FileName = fileName; + Arguments = arguments; + IP = ip; + Port = port; + InitializationTimeout = initializationTimeout; + } + + [MethodImpl(MethodImplOptions.Synchronized)] + public void Start() + { + if (IsRunning) + { + return; + } + + Service = new Process(); + Service.StartInfo.FileName = FileName.FullName; + Service.StartInfo.Arguments = Arguments; + Service.StartInfo.UseShellExecute = false; + Service.StartInfo.CreateNoWindow = true; + + Service.StartInfo.RedirectStandardOutput = true; + Service.OutputDataReceived += (sender, e) => OutputDataReceived?.Invoke(this, e); + + bool isLaunched = false; + string msgTxt = + $"The local WinAppDriver server has not been started: {FileName.FullName} Arguments: {Arguments}. " + + "\n"; + + try + { + Service.Start(); + + Service.BeginOutputReadLine(); + } + catch (Exception e) + { + DestroyProcess(); + throw new Exception(msgTxt, e); + } + + isLaunched = Ping(); + if (!isLaunched) + { + DestroyProcess(); + throw new Exception( + msgTxt + + $"Time {InitializationTimeout.TotalMilliseconds} ms for the service starting has been expired!"); + } + } + + public bool IsRunning + { + get + { + if (Service == null) + { + return false; + } + + try + { + var pid = Service.Id; + } + catch (Exception) + { + return false; + } + + return Ping(); + } + } + + public void Dispose() + { + DestroyProcess(); + GC.SuppressFinalize(this); + } + + public Uri ServiceUrl + { + // Note: append /wd/hub to the URL if you're directing the test at Appium + get { return new Uri($"http://{IP.ToString()}:{Convert.ToString(Port)}"); } + } + + private void DestroyProcess() + { + if (Service == null) + { + return; + } + + try + { + Service.Kill(); + } + catch (Exception) + { + } + finally + { + Service.Close(); + } + } + + private bool Ping() + { + bool pinged = false; + + Uri status; + + Uri service = ServiceUrl; + if (service.IsLoopback) + { + status = new Uri("http://localhost:" + Convert.ToString(Port) + "/status"); + } + else + { + status = new Uri(service.ToString() + "/status"); + } + + DateTime endTime = DateTime.Now.Add(this.InitializationTimeout); + while (!pinged & DateTime.Now < endTime) + { + HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(status); + HttpWebResponse response = null; + try + { + using (response = (HttpWebResponse)request.GetResponse()) + { + pinged = true; + } + } + catch (Exception) + { + pinged = false; + } + finally + { + if (response != null) + { + response.Close(); + } + } + } + return pinged; + } + } +} diff --git a/src/CalculatorUITestFramework/WindowsDriverServiceBuilder.cs b/src/CalculatorUITestFramework/WindowsDriverServiceBuilder.cs new file mode 100644 index 00000000..1dacdd85 --- /dev/null +++ b/src/CalculatorUITestFramework/WindowsDriverServiceBuilder.cs @@ -0,0 +1,93 @@ +using OpenQA.Selenium; +using OpenQA.Selenium.Appium.Service; +using OpenQA.Selenium.Appium.Service.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace CalculatorUITestFramework +{ + public class WindowsDriverServiceBuilder + { + private string IpAddress = "127.0.0.1"; + private int Port = 4723; + private TimeSpan StartUpTimeout = new TimeSpan(0, 2, 0); + private FileInfo FileInfo; + + public WindowsDriverLocalService Build() + { + if (FileInfo == null) + { + FileInfo = new FileInfo(@"c:\Program Files (x86)\Windows Application Driver\winappdriver.exe"); + } + return new WindowsDriverLocalService(FileInfo, string.Empty, IPAddress.Parse(this.IpAddress), this.Port, StartUpTimeout); + } + + public WindowsDriverServiceBuilder WithFileInfo(FileInfo fileInfo) + { + if (fileInfo == null) + { + throw new ArgumentNullException("FileInfo should not be NULL"); + } + FileInfo = fileInfo; + return this; + } + + public WindowsDriverServiceBuilder WithStartUpTimeOut(TimeSpan startUpTimeout) + { + if (startUpTimeout == null) + { + throw new ArgumentNullException("A startup timeout should not be NULL"); + } + StartUpTimeout = startUpTimeout; + return this; + } + + public WindowsDriverServiceBuilder WithIPAddress(string ipAddress) + { + IpAddress = ipAddress; + return this; + } + + public WindowsDriverServiceBuilder UsingPort(int port) + { + if (port < 0) + { + throw new ArgumentException("The port parameter should not be negative"); + } + + if (port == 0) + { + return UsingAnyFreePort(); + } + + Port = port; + return this; + } + + public WindowsDriverServiceBuilder UsingAnyFreePort() + { + Socket sock = null; + + try + { + sock = new Socket(AddressFamily.InterNetwork, + SocketType.Stream, ProtocolType.Tcp); + sock.Bind(new IPEndPoint(IPAddress.Any, 0)); + Port = ((IPEndPoint)sock.LocalEndPoint).Port; + return this; + } + finally + { + if (sock != null) + { + sock.Dispose(); + } + } + } + } +}