diff --git a/.azure/pipelines/signalr-daily-tests.yml b/.azure/pipelines/signalr-daily-tests.yml
new file mode 100644
index 0000000000..1b13105f63
--- /dev/null
+++ b/.azure/pipelines/signalr-daily-tests.yml
@@ -0,0 +1,17 @@
+# 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 ASP.NET Core SignalR
+# These use Sauce Labs resources, hence they run daily rather than per-commit.
+
+# The only Daily Tests we have run in Sauce Labs and only need to run on one machine (because they just trigger SauceLabs)
+# Hence we use the 'default-build.yml' template because it represents a single phase
+jobs:
+- template: jobs/default-build.yml
+ parameters:
+ buildDirectory: src/SignalR
+ buildArgs: "/p:DailyTests=true /p:SauceUser='$(asplab-sauce-labs-username)' /p:SauceKey='$(asplab-sauce-labs-access-key)'"
+ agentOs: Windows
+ jobName: SignalRDailyTests
+ jobDisplayName: "SignalR Daily Tests"
+
diff --git a/src/SignalR/build/repo.targets b/src/SignalR/build/repo.targets
index ce7a1193e2..a307f480d4 100644
--- a/src/SignalR/build/repo.targets
+++ b/src/SignalR/build/repo.targets
@@ -44,8 +44,28 @@
-
-
+
+
+
+
+
+
+
+
+
+
+ sauce.local
+ <_TestSauceArgs>--verbose --no-color --configuration $(Configuration) --sauce-user "$(SauceUser)" --sauce-key "$(SauceKey)"
+ <_TestSauceArgs Condition="'$(BrowserTestHostName)' != ''">$(_TestSauceArgs) --use-hostname "$(BrowserTestHostName)"
+
+
+
diff --git a/src/SignalR/build/splat-browser-logs.ps1 b/src/SignalR/build/splat-browser-logs.ps1
new file mode 100644
index 0000000000..85266f57b4
--- /dev/null
+++ b/src/SignalR/build/splat-browser-logs.ps1
@@ -0,0 +1,44 @@
+# Takes an input browser log file and splits it into separate files for each browser
+param(
+ [Parameter(Mandatory = $true, Position = 0)][string]$InputFile,
+ [Parameter(Mandatory = $false)][string]$OutputDirectory
+)
+
+if (!$OutputDirectory) {
+ $OutputDirectory = Split-Path -Parent $InputFile
+}
+
+$browserParser = [regex]"(?[a-zA-Z]*) (?[^ ]*) \((?[^\)]*)\)";
+Write-Host "Processing log file...";
+$browsers = @{}
+Get-Content $InputFile | ForEach-Object {
+ $openSquare = $_.IndexOf("[");
+ $closeSquare = $_.IndexOf("]");
+ if (($openSquare -ge 0) -and ($closeSquare -ge 0)) {
+ $browser = $_.Substring($openSquare + 1, $closeSquare - 1);
+ $message = $_.Substring($closeSquare + 1).Trim();
+
+ # Parse the browser
+ $m = $browserParser.Match($browser)
+ if ($m.Success) {
+ $name = $m.Groups["name"].Value;
+ $version = $m.Groups["version"].Value;
+ $os = $m.Groups["os"].Value;
+
+ # Generate a new file name
+ $fileName = "$($name)_$($version.Replace(".", "_")).log"
+ $lines = $browsers[$fileName]
+ if (!$lines) {
+ $lines = @();
+ }
+
+ $browsers[$fileName] = $lines + $message
+ }
+ }
+}
+
+$browsers.Keys | ForEach-Object {
+ Write-Host "Writing to $_ ..."
+ $destination = Join-Path $OutputDirectory $_
+ [IO.File]::WriteAllText($destination, [string]::Join([Environment]::NewLine, $browsers[$_]))
+}
\ No newline at end of file
diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/karma.base.conf.js b/src/SignalR/clients/ts/FunctionalTests/scripts/karma.base.conf.js
index e1a8953f8c..d71f326f1f 100644
--- a/src/SignalR/clients/ts/FunctionalTests/scripts/karma.base.conf.js
+++ b/src/SignalR/clients/ts/FunctionalTests/scripts/karma.base.conf.js
@@ -31,6 +31,7 @@ try {
// Log browser messages to a file, not the terminal.
browserConsoleLogOptions: {
level: "debug",
+ format: "[%b] %T: %m",
terminal: false
},
diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/karma.sauce.conf.js b/src/SignalR/clients/ts/FunctionalTests/scripts/karma.sauce.conf.js
index 46d78a64d6..5ebebba045 100644
--- a/src/SignalR/clients/ts/FunctionalTests/scripts/karma.sauce.conf.js
+++ b/src/SignalR/clients/ts/FunctionalTests/scripts/karma.sauce.conf.js
@@ -16,7 +16,7 @@ try {
base: "SauceLabs",
browserName: "safari",
version: "latest",
- platform: "OS X 10.13",
+ platform: "macOS 10.13",
},
// Google Chrome Latest, any OS.
@@ -46,8 +46,25 @@ try {
};
// Mobile Browsers
- // TODO: Fill this in.
- var mobileBrowsers = {};
+ // These are a bit too slow and cause Karma to time out trying to "capture" the browser.
+ var mobileBrowsers = {
+ // // Latest iOS
+ // sl_ios_safari: {
+ // base: "SauceLabs",
+ // browserName: "Safari",
+ // deviceName: "iPhone XS Simulator",
+ // platformName: "iOS",
+ // platformVersion: "12.0",
+ // },
+ // // Latest Android Chrome
+ // sl_android_chrome: {
+ // base: "SauceLabs",
+ // browserName: "Chrome",
+ // platformName: "Android",
+ // platformVersion: "6.0",
+ // deviceName: "Android Emulator"
+ // }
+ };
var customLaunchers = {
...evergreenBrowsers,
@@ -64,7 +81,11 @@ try {
connectOptions: {
// Required to enable WebSockets through the Sauce Connect proxy.
noSslBumpDomains: ["all"]
- }
+ },
+ build: process.env.BUILD_BUILDNUMBER,
+ tags: ["aspnet-SignalR", "daily-tests"],
+ username: process.env.SAUCE_USERNAME,
+ accessKey: process.env.SAUCE_ACCESS_KEY
},
});
} catch (e) {
diff --git a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
index 5ae55bad6c..5e11511d9a 100644
--- a/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/scripts/run-tests.ts
@@ -15,13 +15,21 @@ const debug = _debug("signalr-functional-tests:run");
const ARTIFACTS_DIR = path.resolve(__dirname, "..", "..", "..", "..", "artifacts");
const LOGS_DIR = path.resolve(ARTIFACTS_DIR, "logs");
+const HOSTSFILE_PATH = process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\drivers\\etc\\hosts` : null;
+
// 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),
};
+if (!_fs.existsSync(LOGS_DIR)) {
+ _fs.mkdirSync(LOGS_DIR);
+}
+
process.on("unhandledRejection", (reason) => {
console.error(`Unhandled promise rejection: ${reason}`);
process.exit(1);
@@ -96,6 +104,11 @@ let spec: string;
let sauce = false;
let allBrowsers = false;
let noColor = false;
+let skipNode = false;
+let sauceUser = null;
+let sauceKey = null;
+let publicIp = false;
+let hostname = null;
for (let i = 2; i < process.argv.length; i += 1) {
switch (process.argv[i]) {
@@ -119,6 +132,10 @@ for (let i = 2; i < process.argv.length; i += 1) {
sauce = true;
console.log("Running on SauceLabs.");
break;
+ case "--skip-node":
+ skipNode = true;
+ console.log("Running on SauceLabs.");
+ break;
case "-a":
case "--all-browsers":
allBrowsers = true;
@@ -126,9 +143,29 @@ for (let i = 2; i < process.argv.length; i += 1) {
case "--no-color":
noColor = true;
break;
+ case "--sauce-user":
+ i += 1;
+ sauceUser = process.argv[i];
+ break;
+ case "--sauce-key":
+ i += 1;
+ sauceKey = process.argv[i];
+ break;
+ case "--use-hostname":
+ i += 1;
+ hostname = process.argv[i];
+ break;
}
}
+if (sauceUser && !process.env.SAUCE_USERNAME) {
+ process.env.SAUCE_USERNAME = sauceUser;
+}
+
+if (sauceKey && !process.env.SAUCE_ACCESS_KEY) {
+ process.env.SAUCE_ACCESS_KEY = sauceKey;
+}
+
const configFile = sauce ?
path.resolve(__dirname, "karma.sauce.conf.js") :
path.resolve(__dirname, "karma.local.conf.js");
@@ -168,12 +205,17 @@ function runKarma(karmaConfig) {
}
function runJest(httpsUrl: string, httpUrl: string) {
+ if (skipNode) {
+ console.log("Skipping NodeJS tests because '--skip-node' was specified.");
+ return 0;
+ }
+
const jestPath = path.resolve(__dirname, "..", "..", "common", "node_modules", "jest", "bin", "jest.js");
const configPath = path.resolve(__dirname, "..", "func.jest.config.js");
console.log("Starting Node tests using Jest.");
return new Promise((resolve, reject) => {
- const logStream = fs.createWriteStream(path.resolve(__dirname, "..", "..", "..", "..", "artifacts", "logs", "node.functionaltests.log"));
+ const logStream = fs.createWriteStream(path.resolve(LOGS_DIR, "node.functionaltests.log"));
// Use NODE_TLS_REJECT_UNAUTHORIZED to allow our test cert to be used by the Node tests (NEVER use this environment variable outside of testing)
const p = exec(`"${process.execPath}" "${jestPath}" --config "${configPath}"`, { env: { SERVER_URL: `${httpsUrl};${httpUrl}`, NODE_TLS_REJECT_UNAUTHORIZED: 0 }, timeout: 200000, maxBuffer: 10 * 1024 * 1024 },
(error: any, stdout, stderr) => {
@@ -199,7 +241,7 @@ function runJest(httpsUrl: string, httpUrl: string) {
if (sauce) {
// SauceLabs can only proxy certain ports for Edge and Safari.
// https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+FAQS
- desiredServerUrl = "http://127.0.0.1:9000";
+ desiredServerUrl = "http://127.0.0.1:9000;https://127.0.0.1:9001";
}
const dotnet = spawn("dotnet", [serverPath], {
@@ -216,7 +258,7 @@ function runJest(httpsUrl: string, httpUrl: string) {
}
}
- const logStream = fs.createWriteStream(path.resolve(__dirname, "..", "..", "..", "..", "artifacts", "logs", "ts.functionaltests.dotnet.log"));
+ const logStream = fs.createWriteStream(path.resolve(LOGS_DIR, "ts.functionaltests.dotnet.log"));
dotnet.stdout.pipe(logStream);
process.on("SIGINT", cleanup);
@@ -224,11 +266,27 @@ function runJest(httpsUrl: string, httpUrl: string) {
debug("Waiting for Functional Test Server to start");
const matches = await waitForMatches("dotnet", dotnet, /Now listening on: (https?:\/\/[^\/]+:[\d]+)/, 2);
- const httpsUrl = matches[1];
- const httpUrl = matches[3];
- debug(`Functional Test Server has started at ${httpsUrl} and ${httpUrl}`);
- debug(`Using SignalR Server: ${httpsUrl} and ${httpUrl}`);
+ // The order of HTTP and HTTPS isn't guaranteed
+ let httpsUrl;
+ let httpUrl;
+ if (matches[1].indexOf("https://") == 0) {
+ httpsUrl = matches[1];
+ } else if (matches[3].indexOf("https://") == 0) {
+ httpsUrl = matches[3];
+ }
+ if (matches[1].indexOf("http://") == 0) {
+ httpUrl = matches[1];
+ } else if (matches[3].indexOf("http://") == 0) {
+ httpUrl = matches[3];
+ }
+
+ if (!httpUrl || !httpsUrl) {
+ console.error("Unable to identify URLs");
+ process.exit(1);
+ }
+
+ debug(`Functional Test Server has started at ${httpsUrl} and ${httpUrl}`);
// Start karma server
const conf = {
@@ -243,14 +301,48 @@ function runJest(httpsUrl: string, httpUrl: string) {
if (!await fs.exists(LOGS_DIR)) {
await fs.mkdir(LOGS_DIR);
}
- conf.browserConsoleLogOptions.path = path.resolve(LOGS_DIR, `browserlogs.console.${new Date().toISOString().replace(/:|\./g, "-")}`);
+ conf.browserConsoleLogOptions.path = path.resolve(LOGS_DIR, `browserlogs.console.log`);
if (noColor) {
conf.colors = false;
}
+ if (hostname) {
+ if (process.platform !== "win32") {
+ throw new Error("Can't use '--use-hostname' on non-Windows platform.");
+ }
+
+ // Register a custom hostname in the hosts file (requires Admin, but AzDO agents run as Admin)
+ // Used to work around issues in Sauce Labs
+ debug(`Updating Hosts file (${HOSTSFILE_PATH}) to register host name '${hostname}'`);
+ await fs.appendFile(HOSTSFILE_PATH, `${EOL}127.0.0.1 ${hostname}${EOL}`);
+
+ conf.hostname = hostname;
+
+ // Rewrite the URL. Try with the host name and the IP address just to make sure
+ httpUrl = httpUrl.replace(/localhost/g, hostname);
+ httpsUrl = httpsUrl.replace(/localhost/g, hostname);
+ httpUrl = httpUrl.replace(/\d+\.\d+\.\d+\.\d+/g, hostname);
+ httpsUrl = httpsUrl.replace(/\d+\.\d+\.\d+\.\d+/g, hostname);
+ }
+
+ conf.client.args = [];
+
+ if (sauce) {
+ // Configure Sauce Connect logging
+ conf.sauceLabs.connectOptions.logfile = path.resolve(LOGS_DIR, "sauceConnect.log");
+
+ // Don't use https, Safari and Edge don't trust the cert.
+ httpsUrl = "";
+
+ conf.client.args = [...conf.client.args, '--sauce'];
+ }
+
+ debug(`Using SignalR Servers: ${httpsUrl} (https) and ${httpUrl} (http)`);
+
// Pass server URL to tests
- conf.client.args = ["--server", `${httpsUrl};${httpUrl}`];
+ conf.client.args = [...conf.client.args, "--server", `${httpsUrl};${httpUrl}`];
+ debug(`Passing client args: ${conf.client.args.join(" ")}`);
const jestExit = await runJest(httpsUrl, httpUrl);
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
index 37b9b532fb..86bfe113fd 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/Common.ts
@@ -4,40 +4,55 @@
import { HttpTransportType, IHubProtocol, JsonHubProtocol } from "@aspnet/signalr";
import { MessagePackHubProtocol } from "@aspnet/signalr-protocol-msgpack";
-export let ENDPOINT_BASE_URL: string;
-export let ENDPOINT_BASE_HTTPS_URL: string;
+// On slower CI machines, these tests sometimes take longer than 5s
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000;
+
+export let ENDPOINT_BASE_URL: string = "";
+export let ENDPOINT_BASE_HTTPS_URL: string = "";
if (typeof window !== "undefined" && (window as any).__karma__) {
const args = (window as any).__karma__.config.args as string[];
let httpsServer = "";
let httpServer = "";
+ let sauce = false;
for (let i = 0; i < args.length; i += 1) {
switch (args[i]) {
case "--server":
i += 1;
const urls = args[i].split(";");
- httpsServer = urls[0];
httpServer = urls[1];
- console.log(httpServer);
+ httpsServer = urls[0];
+ break;
+ case "--sauce":
+ sauce = true;
break;
}
}
+ // Increase test timeout in sauce because of the proxy
+ if (sauce) {
+ // Double the timeout.
+ jasmine.DEFAULT_TIMEOUT_INTERVAL *= 2;
+ }
+
// Running in Karma? Need to use an absolute URL
ENDPOINT_BASE_URL = httpServer;
ENDPOINT_BASE_HTTPS_URL = httpsServer;
- console.log(`Using SignalR Server: ${ENDPOINT_BASE_URL}`);
} else if (typeof document !== "undefined") {
ENDPOINT_BASE_URL = `${document.location.protocol}//${document.location.host}`;
} else if (process && process.env && process.env.SERVER_URL) {
const urls = process.env.SERVER_URL.split(";");
- ENDPOINT_BASE_HTTPS_URL = urls[0];
ENDPOINT_BASE_URL = urls[1];
+ ENDPOINT_BASE_HTTPS_URL = urls[0];
} else {
throw new Error("The server could not be found.");
}
+console.log(`Using SignalR HTTP Server: '${ENDPOINT_BASE_URL}'`);
+console.log(`Using SignalR HTTPS Server: '${ENDPOINT_BASE_HTTPS_URL}'`);
+console.log(`Jasmine DEFAULT_TIMEOUT_INTERVAL: ${jasmine.DEFAULT_TIMEOUT_INTERVAL}`);
+
export const ECHOENDPOINT_URL = ENDPOINT_BASE_URL + "/echo";
export function getHttpTransportTypes(): HttpTransportType[] {
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
index b7665cb306..1fda5080cd 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/HubConnectionTests.ts
@@ -12,7 +12,8 @@ import "./LogBannerReporter";
import { TestLogger } from "./TestLogger";
const TESTHUBENDPOINT_URL = ENDPOINT_BASE_URL + "/testhub";
-const TESTHUBENDPOINT_HTTPS_URL = ENDPOINT_BASE_HTTPS_URL + "/testhub";
+const TESTHUBENDPOINT_HTTPS_URL = ENDPOINT_BASE_HTTPS_URL ? (ENDPOINT_BASE_HTTPS_URL + "/testhub") : undefined;
+
const TESTHUB_NOWEBSOCKETS_ENDPOINT_URL = ENDPOINT_BASE_URL + "/testhub-nowebsockets";
// On slower CI machines, these tests sometimes take longer than 5s
@@ -22,6 +23,17 @@ const commonOptions: IHttpConnectionOptions = {
logMessageContent: true,
};
+// Run test in Node or Chrome, but not on macOS
+const shouldRunHttpsTests =
+ // Need to have an HTTPS URL
+ !!TESTHUBENDPOINT_HTTPS_URL &&
+
+ // Run on Node, unless macOS
+ (process && process.platform !== "darwin") &&
+
+ // Only run under Chrome browser
+ (typeof navigator === "undefined" || navigator.userAgent.search("Chrome") !== -1);
+
function getConnectionBuilder(transportType?: HttpTransportType, url?: string, options?: IHttpConnectionOptions): HubConnectionBuilder {
let actualOptions: IHttpConnectionOptions = options || {};
if (transportType) {
@@ -63,8 +75,7 @@ describe("hubConnection", () => {
});
});
- // Run test in Node or Chrome, but not on macOS
- if ((process && process.platform !== "darwin") && (typeof navigator === "undefined" || navigator.userAgent.search("Chrome") !== -1)) {
+ if (shouldRunHttpsTests) {
it("using https, can invoke server method and receive result", (done) => {
const message = "你好,世界!";
diff --git a/src/SignalR/clients/ts/FunctionalTests/ts/LogBannerReporter.ts b/src/SignalR/clients/ts/FunctionalTests/ts/LogBannerReporter.ts
index 98d28da93a..387d52151e 100644
--- a/src/SignalR/clients/ts/FunctionalTests/ts/LogBannerReporter.ts
+++ b/src/SignalR/clients/ts/FunctionalTests/ts/LogBannerReporter.ts
@@ -1,4 +1,6 @@
export class LogBannerReporter implements jasmine.CustomReporter {
+ private lastTestStarted?: Date;
+
public jasmineStarted(): void {
console.log("*** JASMINE SUITE STARTED ***");
}
@@ -8,11 +10,16 @@ export class LogBannerReporter implements jasmine.CustomReporter {
}
public specStarted(result: jasmine.CustomReporterResult): void {
- console.log(`*** SPEC STARTED: ${result.fullName} ***`);
+ const timestamp = new Date();
+ this.lastTestStarted = timestamp;
+ console.log(`*** SPEC STARTED: ${result.fullName} [${timestamp.toISOString()}] ***`);
}
public specDone(result: jasmine.CustomReporterResult): void {
- console.log(`*** SPEC DONE: ${result.fullName} ***`);
+ const timestamp = new Date();
+
+ const duration = this.lastTestStarted ? `${timestamp.getTime() - this.lastTestStarted.getTime()}ms` : "<>";
+ console.log(`*** SPEC DONE: ${result.fullName} [${timestamp.toISOString()}; Duration: ${duration}] ***`);
}
}
diff --git a/src/SignalR/clients/ts/common/package-lock.json b/src/SignalR/clients/ts/common/package-lock.json
index 7ad4e6a391..8b3564efc7 100644
--- a/src/SignalR/clients/ts/common/package-lock.json
+++ b/src/SignalR/clients/ts/common/package-lock.json
@@ -2601,8 +2601,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -3017,8 +3016,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3074,7 +3072,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3118,14 +3115,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
diff --git a/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json b/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json
index 68493e002a..565f657642 100644
--- a/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json
+++ b/src/SignalR/clients/ts/signalr-protocol-msgpack/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr-protocol-msgpack",
- "version": "3.0.0-alpha1-t000",
+ "version": "3.0.0-dev",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/src/SignalR/clients/ts/signalr/package-lock.json b/src/SignalR/clients/ts/signalr/package-lock.json
index 9bedb9db53..a7d4e4a695 100644
--- a/src/SignalR/clients/ts/signalr/package-lock.json
+++ b/src/SignalR/clients/ts/signalr/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@aspnet/signalr",
- "version": "3.0.0-alpha1-t000",
+ "version": "3.0.0-dev",
"lockfileVersion": 1,
"requires": true,
"dependencies": {