Run Blazor E2E tests on SauceLabs (#18456)

* Run Blazor E2E tests on SauceLabs

* Added azure pipeline

* update yml

* Update meta

* More changes
This commit is contained in:
Ajay Bhargav B 2020-01-27 17:54:18 -08:00 committed by GitHub
parent e24f73e14b
commit 24be2992de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 797 additions and 19 deletions

View File

@ -0,0 +1,59 @@
# Uses Scheduled Triggers, which aren't supported in YAML yet.
# https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=vsts&tabs=yaml#scheduled
# Daily Tests for Blazor
# These use Sauce Labs resources, hence they run daily rather than per-commit.
# We just need one Windows machine because all it does is trigger SauceLabs.
variables:
SAUCE_CONNECT_DOWNLOAD_ON_INSTALL: true
E2ETESTS_SauceTest: true
E2ETESTS_Sauce__TunnelIdentifier: 'blazor-e2e-sc-proxy-tunnel'
E2ETESTS_Sauce__HostName: 'sauce.local'
jobs:
- template: jobs/default-build.yml
parameters:
buildDirectory: src/Components
isTestingJob: true
agentOs: Windows
jobName: BlazorDailyTests
jobDisplayName: "Blazor Daily Tests"
afterBuild:
# macOS/Safari
- script: 'dotnet test --filter "StandaloneAppTest"'
workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - macOS/Safari'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - macOS/Safari'
E2ETESTS_Sauce__PlatformName: 'macOS 10.14'
E2ETESTS_Sauce__BrowserName: 'Safari'
# Need to explicitly set version here because some older versions don't support timeouts in Safari.
E2ETESTS_Sauce__SeleniumVersion: '3.4.0'
# Android/Chrome
- script: 'dotnet test --filter "StandaloneAppTest"'
workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - Android/Chrome'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - Android/Chrome'
E2ETESTS_Sauce__PlatformName: 'Android'
E2ETESTS_Sauce__PlatformVersion: '10.0'
E2ETESTS_Sauce__BrowserName: 'Chrome'
E2ETESTS_Sauce__DeviceName: 'Android GoogleAPI Emulator'
E2ETESTS_Sauce__DeviceOrientation: 'portrait'
E2ETESTS_Sauce__AppiumVersion: '1.9.1'
artifacts:
- name: Windows_Logs
path: ../../artifacts/log/
publishOnError: true

View File

@ -2,7 +2,8 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width">
<!-- Forcing the device width here so that our automated tests work consistently on mobile browsers. -->
<meta name="viewport" content="width=1024">
<title>Blazor standalone</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />

View File

@ -6,3 +6,4 @@ using Xunit;
[assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")]
[assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))]
[assembly: AssemblyFixture(typeof(SauceConnectServer))]

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
@ -34,9 +35,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
var assembly = ApplicationAssembly ?? BuildWebHostMethod.Method.DeclaringType.Assembly;
var sampleSitePath = FindSampleOrTestSitePath(assembly.FullName);
var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}
return BuildWebHostMethod(new[]
{
"--urls", "http://127.0.0.1:0",
"--urls", $"http://{host}:0",
"--contentroot", sampleSitePath,
"--environment", Environment.ToString(),
}.Concat(AdditionalArguments).ToArray());

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
@ -24,9 +25,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
ContentRoot = FindSampleOrTestSitePath(
typeof(TProgram).Assembly.FullName);
var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}
var args = new List<string>
{
"--urls", "http://127.0.0.1:0",
"--urls", $"http://{host}:0",
"--contentroot", ContentRoot,
"--pathbase", PathBase,
"--applicationpath", typeof(TProgram).Assembly.Location,

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using Microsoft.AspNetCore.E2ETesting;
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
{
@ -22,7 +23,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
public ServerFixture()
{
_rootUriInitializer = new Lazy<Uri>(() =>
new Uri(StartAndGetRootUri()));
{
var uri = new Uri(StartAndGetRootUri());
if (E2ETestOptions.Instance.SauceTest)
{
uri = new UriBuilder(uri.Scheme, E2ETestOptions.Instance.Sauce.HostName, uri.Port).Uri;
}
return uri;
});
}
public abstract void Dispose();

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
@ -26,13 +27,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
var sampleSitePath = FindSampleOrTestSitePath(SampleSiteName);
var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
{
host = E2ETestOptions.Instance.Sauce.HostName;
}
return new HostBuilder()
.ConfigureWebHost(webHostBuilder => webHostBuilder
.UseKestrel()
.UseContentRoot(sampleSitePath)
.UseWebRoot(string.Empty)
.UseStartup<StaticSiteStartup>()
.UseUrls("http://127.0.0.1:0"))
.UseUrls($"http://{host}:0"))
.Build();
}

View File

@ -8,7 +8,6 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Verify we start at home, with the home link highlighted
Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Home", item.Text));
item => Assert.Equal("Home", item.Text.Trim()));
// Click on the "counter" link
Browser.FindElement(By.LinkText("Counter")).Click();
@ -60,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Verify we're now on the counter page, with that nav link (only) highlighted
Assert.Equal("Counter", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Counter", item.Text));
item => Assert.Equal("Counter", item.Text.Trim()));
// Verify we can navigate back to home too
Browser.FindElement(By.LinkText("Home")).Click();
Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text);
Assert.Collection(Browser.FindElements(activeNavLinksSelector),
item => Assert.Equal("Home", item.Text));
item => Assert.Equal("Home", item.Text.Trim()));
}
[Fact]

View File

@ -1,4 +1,4 @@
{
"DefaultWaitTimeoutInSeconds": 20,
"ScreenShotsPath": "../../screenshots"
"ScreenShotsPath": "../../screenshots",
}

View File

@ -6,11 +6,18 @@
"private": true,
"scripts": {
"selenium-standalone": "selenium-standalone",
"prepare": "selenium-standalone install"
"prepare": "selenium-standalone install",
"sauce": "ts-node ./scripts/sauce.ts"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"sauce-connect-launcher": "^1.3.1",
"selenium-standalone": "^6.15.4"
},
"devDependencies": {
"@types/node": "^13.1.7",
"ts-node": "^8.6.2",
"typescript": "^3.7.5"
}
}

View File

@ -0,0 +1,82 @@
import { EOL } from "os";
import * as _fs from "fs";
import { promisify } from "util";
// Promisify things from fs we want to use.
const fs = {
createWriteStream: _fs.createWriteStream,
exists: promisify(_fs.exists),
mkdir: promisify(_fs.mkdir),
appendFile: promisify(_fs.appendFile),
readFile: promisify(_fs.readFile),
};
process.on("unhandledRejection", (reason) => {
console.error(`Unhandled promise rejection: ${reason}`);
process.exit(1);
});
let sauceUser = null;
let sauceKey = null;
let tunnelIdentifier = null;
let hostName = null;
for (let i = 0; i < process.argv.length; i += 1) {
switch (process.argv[i]) {
case "--sauce-user":
i += 1;
sauceUser = process.argv[i];
break;
case "--sauce-key":
i += 1;
sauceKey = process.argv[i];
break;
case "--sauce-tunnel":
i += 1;
tunnelIdentifier = process.argv[i];
break;
case "--use-hostname":
i += 1;
hostName = process.argv[i];
break;
}
}
const HOSTSFILE_PATH = process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\drivers\\etc\\hosts` : null;
(async () => {
if (hostName) {
// Register a custom hostname in the hosts file (requires Admin, but AzDO agents run as Admin)
// Used to work around issues in Sauce Labs.
if (process.platform !== "win32") {
throw new Error("Can't use '--use-hostname' on non-Windows platform.");
}
try {
console.log(`Updating Hosts file (${HOSTSFILE_PATH}) to register host name '${hostName}'`);
await fs.appendFile(HOSTSFILE_PATH, `${EOL}127.0.0.1 ${hostName}${EOL}`);
} catch (error) {
console.log(`Unable to update hosts file at ${HOSTSFILE_PATH}. Error: ${error}`);
}
}
// Creates a persistent proxy tunnel using Sauce Connect.
var sauceConnectLauncher = require('sauce-connect-launcher');
sauceConnectLauncher({
username: sauceUser,
accessKey: sauceKey,
tunnelIdentifier: tunnelIdentifier,
}, function (err, sauceConnectProcess) {
if (err) {
console.error(err.message);
return;
}
console.log("Sauce Connect ready");
});
})();

View File

@ -2,6 +2,23 @@
# yarn lockfile v1
"@types/node@^13.1.7":
version "13.1.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b"
integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A==
adm-zip@~0.4.3:
version "0.4.13"
resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a"
integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==
agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
dependencies:
es6-promisify "^5.0.0"
ajv@^6.5.5:
version "6.10.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52"
@ -12,6 +29,11 @@ ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
arg@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064"
integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@ -24,7 +46,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
async@^2.6.2:
async@^2.1.2, async@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@ -46,6 +68,11 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
@ -61,11 +88,24 @@ bl@^2.2.0:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -83,6 +123,11 @@ commander@^2.19.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -106,6 +151,13 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
debug@^3.1.0:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
dependencies:
ms "^2.1.1"
debug@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@ -118,6 +170,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -133,6 +190,18 @@ end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
dependencies:
es6-promise "^4.0.3"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -184,6 +253,11 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@ -191,6 +265,18 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -213,7 +299,23 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
inherits@^2.0.3, inherits@~2.0.3:
https-proxy-agent@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
dependencies:
agent-base "^4.3.0"
debug "^3.1.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -268,11 +370,21 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
lodash@^4.16.6:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.11, lodash@^4.17.14:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
make-error@^1.1.1:
version "1.3.5"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
mime-db@1.40.0:
version "1.40.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
@ -285,6 +397,13 @@ mime-types@^2.1.12, mime-types@~2.1.19:
dependencies:
mime-db "1.40.0"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@ -317,13 +436,18 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
once@^1.4.0:
once@^1.3.0, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@ -417,6 +541,13 @@ request@2.88.0:
tunnel-agent "^0.6.0"
uuid "^3.3.2"
rimraf@^2.5.4:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
version "5.2.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
@ -432,6 +563,17 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sauce-connect-launcher@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.1.tgz#31137f57b0f7176e1c0525b7fb09c6da746647cf"
integrity sha512-vIf9qDol3q2FlYzrKt0dr3kvec6LSjX2WS+/mVnAJIhqh1evSkPKCR2AzcJrnSmx9Xt9PtV0tLY7jYh0wsQi8A==
dependencies:
adm-zip "~0.4.3"
async "^2.1.2"
https-proxy-agent "^3.0.0"
lodash "^4.16.6"
rimraf "^2.5.4"
selenium-standalone@^6.15.4:
version "6.16.0"
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.16.0.tgz#ffcf02665c58ff7a7472427ae819ba79c15967ac"
@ -468,6 +610,19 @@ shebang-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
source-map-support@^0.5.6:
version "0.5.16"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -516,6 +671,17 @@ tough-cookie@~2.4.3:
psl "^1.1.24"
punycode "^1.4.1"
ts-node@^8.6.2:
version "8.6.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35"
integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.6"
yn "3.1.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@ -528,6 +694,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
typescript@^3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@ -578,3 +749,8 @@ yauzl@^2.10.0:
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

View File

@ -72,13 +72,24 @@ namespace Microsoft.AspNetCore.E2ETesting
public Task<(IWebDriver, ILogs)> GetOrCreateBrowserAsync(ITestOutputHelper output, string isolationContext = "")
{
if (!IsHostAutomationSupported())
Func<string, ITestOutputHelper, Task<(IWebDriver, ILogs)>> createBrowserFunc;
if (E2ETestOptions.Instance.SauceTest)
{
output.WriteLine($"{nameof(BrowserFixture)}: Host does not support browser automation.");
return Task.FromResult<(IWebDriver, ILogs)>(default);
createBrowserFunc = CreateSauceBrowserAsync;
}
else
{
if (!IsHostAutomationSupported())
{
output.WriteLine($"{nameof(BrowserFixture)}: Host does not support browser automation.");
return Task.FromResult<(IWebDriver, ILogs)>(default);
}
createBrowserFunc = CreateBrowserAsync;
}
return _browsers.GetOrAdd(isolationContext, CreateBrowserAsync, output);
return _browsers.GetOrAdd(isolationContext, createBrowserFunc, output);
}
public Task InitializeAsync() => Task.CompletedTask;
@ -143,5 +154,106 @@ namespace Microsoft.AspNetCore.E2ETesting
throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive");
}
private async Task<(IWebDriver browser, ILogs log)> CreateSauceBrowserAsync(string context, ITestOutputHelper output)
{
var sauce = E2ETestOptions.Instance.Sauce;
if (sauce == null ||
string.IsNullOrEmpty(sauce.TestName) ||
string.IsNullOrEmpty(sauce.Username) ||
string.IsNullOrEmpty(sauce.AccessKey) ||
string.IsNullOrEmpty(sauce.TunnelIdentifier) ||
string.IsNullOrEmpty(sauce.PlatformName) ||
string.IsNullOrEmpty(sauce.BrowserName))
{
throw new InvalidOperationException("Required SauceLabs environment variables not set.");
}
var name = sauce.TestName;
if (!string.IsNullOrEmpty(context))
{
name = $"{name} - {context}";
}
var capabilities = new DesiredCapabilities();
// Required config
capabilities.SetCapability("username", sauce.Username);
capabilities.SetCapability("accessKey", sauce.AccessKey);
capabilities.SetCapability("tunnelIdentifier", sauce.TunnelIdentifier);
capabilities.SetCapability("name", name);
if (!string.IsNullOrEmpty(sauce.BrowserName))
{
capabilities.SetCapability("browserName", sauce.BrowserName);
}
if (!string.IsNullOrEmpty(sauce.PlatformVersion))
{
capabilities.SetCapability("platformName", sauce.PlatformName);
capabilities.SetCapability("platformVersion", sauce.PlatformVersion);
}
else
{
// In some cases (like macOS), SauceLabs expects us to set "platform" instead of "platformName".
capabilities.SetCapability("platform", sauce.PlatformName);
}
if (!string.IsNullOrEmpty(sauce.BrowserVersion))
{
capabilities.SetCapability("browserVersion", sauce.BrowserVersion);
}
if (!string.IsNullOrEmpty(sauce.DeviceName))
{
capabilities.SetCapability("deviceName", sauce.DeviceName);
}
if (!string.IsNullOrEmpty(sauce.DeviceOrientation))
{
capabilities.SetCapability("deviceOrientation", sauce.DeviceOrientation);
}
if (!string.IsNullOrEmpty(sauce.AppiumVersion))
{
capabilities.SetCapability("appiumVersion", sauce.AppiumVersion);
}
if (!string.IsNullOrEmpty(sauce.SeleniumVersion))
{
capabilities.SetCapability("seleniumVersion", sauce.SeleniumVersion);
}
await SauceConnectServer.StartAsync(output);
var attempt = 0;
const int maxAttempts = 3;
do
{
try
{
// Attempt to create a new browser in SauceLabs.
var driver = new RemoteWebDriver(
new Uri("http://localhost:4445/wd/hub"),
capabilities,
TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60)));
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1);
var logs = new RemoteLogs(driver);
return (driver, logs);
}
catch (Exception ex)
{
output.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}");
}
attempt++;
} while (attempt < maxAttempts);
throw new InvalidOperationException("Couldn't create a SauceLabs remote driver client.");
}
}
}

View File

@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.E2ETesting
}
Configuration = builder
.AddEnvironmentVariables("E2ETESTS")
.AddEnvironmentVariables("E2ETESTS_")
.Build();
var instance = new E2ETestOptions();
@ -56,5 +56,9 @@ namespace Microsoft.AspNetCore.E2ETesting
public string ScreenShotsPath { get; set; }
public double DefaultAfterFailureWaitTimeoutInSeconds { get; set; } = 3;
public bool SauceTest { get; set; }
public SauceOptions Sauce { get; set; }
}
}

View File

@ -5,6 +5,7 @@
<SeleniumScreenShotsFolderPath>$([MSBuild]::NormalizeDirectory('$(ArtifactsTestResultsDir)','$(MSBuildProjectName)'))</SeleniumScreenShotsFolderPath>
<SeleniumProcessTrackingFolder Condition="'$(SeleniumProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\selenium\</SeleniumProcessTrackingFolder>
<SeleniumE2ETestsSupported Condition="'$(SeleniumE2ETestsSupported)' == '' and '$(TargetArchitecture)' != 'arm' and '$(OS)' == 'Windows_NT'">true</SeleniumE2ETestsSupported>
<SauceConnectProcessTrackingFolder Condition="'$(SauceConnectProcessTrackingFolder)' == ''">$([MSBuild]::EnsureTrailingSlash('$(RepoRoot)'))artifacts\tmp\sauceconnect\</SauceConnectProcessTrackingFolder>
<!-- We want to enforce prerequisites when we build from the CI or within Visual Studio -->
<EnforcedE2EBuildEnvironment Condition="'$(ContinuousIntegrationBuild)' == 'true' or '$(BuildingInsideVisualStudio)' == 'true'">true</EnforcedE2EBuildEnvironment>

View File

@ -121,6 +121,14 @@
<_Parameter2>$(SeleniumProcessTrackingFolder)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
<MakeDir Directories="$(SauceConnectProcessTrackingFolder)" />
<ItemGroup>
<AssemblyAttribute
Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>Microsoft.AspNetCore.Testing.SauceConnect.ProcessTracking</_Parameter1>
<_Parameter2>$(SauceConnectProcessTrackingFolder)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>
<Target Name="_EnsureSeleniumScreenShotsFolder" BeforeTargets="Build">

View File

@ -0,0 +1,262 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.Extensions.Internal;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.E2ETesting
{
public class SauceConnectServer : IDisposable
{
private static SemaphoreSlim _semaphore = new SemaphoreSlim(1);
private Process _process;
private string _sentinelPath;
private Process _sentinelProcess;
private static IMessageSink _diagnosticsMessageSink;
// 2h
private static int SauceConnectProcessTimeout = 7200;
public SauceConnectServer(IMessageSink diagnosticsMessageSink)
{
if (Instance != null || _diagnosticsMessageSink != null)
{
throw new InvalidOperationException("Sauce connect singleton already created.");
}
// The assembly level attribute AssemblyFixture takes care of this being being instantiated before tests run
// and disposed after tests are run, gracefully shutting down the server when possible by calling Dispose on
// the singleton.
Instance = this;
_diagnosticsMessageSink = diagnosticsMessageSink;
}
private void Initialize(
Process process,
string sentinelPath,
Process sentinelProcess)
{
_process = process;
_sentinelPath = sentinelPath;
_sentinelProcess = sentinelProcess;
}
internal static SauceConnectServer Instance { get; private set; }
public static async Task StartAsync(ITestOutputHelper output)
{
try
{
await _semaphore.WaitAsync();
if (Instance._process == null)
{
// No process was started, meaning the instance wasn't initialized.
await InitializeInstance(output);
}
}
finally
{
_semaphore.Release();
}
}
private static async Task InitializeInstance(ITestOutputHelper output)
{
var psi = new ProcessStartInfo
{
FileName = "npm",
Arguments = "run sauce --" +
$" --sauce-user {E2ETestOptions.Instance.Sauce.Username}" +
$" --sauce-key {E2ETestOptions.Instance.Sauce.AccessKey}" +
$" --sauce-tunnel {E2ETestOptions.Instance.Sauce.TunnelIdentifier}" +
$" --use-hostname {E2ETestOptions.Instance.Sauce.HostName}",
RedirectStandardOutput = true,
RedirectStandardError = true,
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
psi.FileName = "cmd";
psi.Arguments = $"/c npm {psi.Arguments}";
}
// It's important that we get the folder value before we start the process to prevent
// untracked processes when the tracking folder is not correctly configure.
var trackingFolder = GetProcessTrackingFolder();
if (!Directory.Exists(trackingFolder))
{
throw new InvalidOperationException($"Invalid tracking folder. Set the 'SauceConnectProcessTrackingFolder' MSBuild property to a valid folder.");
}
Process process = null;
Process sentinel = null;
string pidFilePath = null;
try
{
process = Process.Start(psi);
pidFilePath = await WriteTrackingFileAsync(output, trackingFolder, process);
sentinel = StartSentinelProcess(process, pidFilePath, SauceConnectProcessTimeout);
}
catch
{
ProcessCleanup(process, pidFilePath);
ProcessCleanup(sentinel, pidFilePath: null);
throw;
}
// Log output for sauce connect process.
// This is for the case where the server fails to launch.
var logOutput = new BlockingCollection<string>();
process.OutputDataReceived += LogOutput;
process.ErrorDataReceived += LogOutput;
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// The Sauce connect server has to be up for the entirety of the tests and is only shutdown when the application (i.e. the test) exits.
AppDomain.CurrentDomain.ProcessExit += (sender, args) => ProcessCleanup(process, pidFilePath);
// Log
void LogOutput(object sender, DataReceivedEventArgs e)
{
logOutput.TryAdd(e.Data);
// We avoid logging on the output here because it is unreliable. We can only log in the diagnostics sink.
lock (_diagnosticsMessageSink)
{
_diagnosticsMessageSink.OnMessage(new DiagnosticMessage(e.Data));
}
}
var uri = new UriBuilder("http", E2ETestOptions.Instance.Sauce.HostName, 4445).Uri;
var httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(1),
};
var retries = 0;
do
{
await Task.Delay(1000);
try
{
var response = await httpClient.GetAsync(uri);
if (response.StatusCode == HttpStatusCode.OK)
{
output = null;
Instance.Initialize(process, pidFilePath, sentinel);
return;
}
}
catch (OperationCanceledException)
{
}
catch (HttpRequestException)
{
}
retries++;
} while (retries < 30);
// Make output null so that we stop logging to it.
output = null;
logOutput.CompleteAdding();
var exitCodeString = process.HasExited ? process.ExitCode.ToString() : "Process has not yet exited.";
var message = $@"Failed to launch the server.
ExitCode: {exitCodeString}
Captured output lines:
{string.Join(Environment.NewLine, logOutput.GetConsumingEnumerable())}.";
// If we got here, we couldn't launch Sauce connect or get it to respond. So shut it down.
ProcessCleanup(process, pidFilePath);
throw new InvalidOperationException(message);
}
private static Process StartSentinelProcess(Process process, string sentinelFile, int timeout)
{
// This sentinel process will start and will kill any rouge sauce connect server that wasn't torn down via normal means.
var psi = new ProcessStartInfo
{
FileName = "powershell",
Arguments = $"-NoProfile -NonInteractive -Command \"Start-Sleep {timeout}; " +
$"if(Test-Path {sentinelFile}){{ " +
$"Write-Output 'Stopping process {process.Id}'; Stop-Process {process.Id}; }}" +
$"else{{ Write-Output 'Sentinel file {sentinelFile} not found.'}}",
};
return Process.Start(psi);
}
private static void ProcessCleanup(Process process, string pidFilePath)
{
try
{
if (process?.HasExited == false)
{
try
{
process?.KillTree(TimeSpan.FromSeconds(10));
process?.Dispose();
}
catch
{
// Ignore errors here since we can't do anything
}
}
if (pidFilePath != null && File.Exists(pidFilePath))
{
File.Delete(pidFilePath);
}
}
catch
{
// Ignore errors here since we can't do anything
}
}
private static async Task<string> WriteTrackingFileAsync(ITestOutputHelper output, string trackingFolder, Process process)
{
var pidFile = Path.Combine(trackingFolder, $"{process.Id}.{Guid.NewGuid()}.pid");
for (var i = 0; i < 3; i++)
{
try
{
await File.WriteAllTextAsync(pidFile, process.Id.ToString());
return pidFile;
}
catch
{
output.WriteLine($"Can't write file to process tracking folder: {trackingFolder}");
}
}
throw new InvalidOperationException($"Failed to write file for process {process.Id}");
}
private static string GetProcessTrackingFolder() =>
typeof(SauceConnectServer).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.Single(a => a.Key == "Microsoft.AspNetCore.Testing.SauceConnect.ProcessTracking").Value;
public void Dispose()
{
ProcessCleanup(_process, _sentinelPath);
ProcessCleanup(_sentinelProcess, pidFilePath: null);
}
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.E2ETesting
{
public class SauceOptions
{
public string Username { get; set; }
public string AccessKey { get; set; }
public string TunnelIdentifier { get; set; }
public string HostName { get; set; }
public string TestName { get; set; }
public bool IsRealDevice { get; set; }
public string PlatformName { get; set; }
public string PlatformVersion { get; set; }
public string BrowserName { get; set; }
public string BrowserVersion { get; set; }
public string DeviceName { get; set; }
public string DeviceOrientation { get; set; }
public string AppiumVersion { get; set; }
public string SeleniumVersion { get; set; }
}
}