Skip to content

Commit

Permalink
proxyless: move the 'experimentalProxyless' option from factory funct…
Browse files Browse the repository at this point in the history
…ion to the runner. (#7565)
  • Loading branch information
miherlosev authored Mar 23, 2023
1 parent a335bed commit 90318fd
Show file tree
Hide file tree
Showing 40 changed files with 410 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
"source-map-support": "^0.5.16",
"strip-bom": "^2.0.0",
"testcafe-browser-tools": "2.0.23",
"testcafe-hammerhead": "29.0.0",
"testcafe-hammerhead": "30.0.0",
"testcafe-legacy-api": "5.1.6",
"testcafe-reporter-dashboard": "^0.2.10",
"testcafe-reporter-json": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import loadAssets from '../../load-assets';
import loadAssets from '../../../load-assets';
import {
respond404,
respond500,
respondWithJSON,
redirect,
preventCaching,
} from '../../utils/http';
} from '../../../utils/http';

import RemotesQueue from './remotes-queue';
import RemotesQueue from '../remotes-queue';
import { Proxy, acceptCrossOrigin } from 'testcafe-hammerhead';
import { Dictionary } from '../../configuration/interfaces';
import BrowserConnection from './index';
import { Dictionary } from '../../../configuration/interfaces';
import BrowserConnection from '../index';
import { IncomingMessage, ServerResponse } from 'http';
import SERVICE_ROUTES from './service-routes';
import EMPTY_PAGE_MARKUP from '../../proxyless/empty-page-markup';
import PROXYLESS_ERROR_ROUTE from '../../proxyless/error-route';
import { initSelector } from '../../test-run/commands/validations/initializers';
import TestRun from '../../test-run';
import SERVICE_ROUTES from '../service-routes';
import EMPTY_PAGE_MARKUP from '../../../proxyless/empty-page-markup';
import PROXYLESS_ERROR_ROUTE from '../../../proxyless/error-route';
import { initSelector } from '../../../test-run/commands/validations/initializers';
import TestRun from '../../../test-run';
import { TestCafeStartOptions } from '../../../configuration/testcafe-configuration';
import BrowserConnectionGatewayStatus from './status';
import { GeneralError } from '../../../errors/runtime';
import { RUNTIME_ERRORS } from '../../../errors/types';
import { EventEmitter } from 'events';

export interface BrowserConnectionGatewayOptions {
retryTestPages: boolean;
Expand All @@ -28,20 +33,22 @@ const DEFAULT_BROWSER_CONNECTION_GATEWAY_OPTIONS = {
proxyless: false,
};

export default class BrowserConnectionGateway {
export default class BrowserConnectionGateway extends EventEmitter {
private _connections: Dictionary<BrowserConnection> = {};
private _remotesQueue: RemotesQueue;
public readonly connectUrl: string;
public connectUrl: string;
private readonly _options: BrowserConnectionGatewayOptions;
public readonly proxy: Proxy;
private _status: BrowserConnectionGatewayStatus;

public constructor (proxy: Proxy, options: BrowserConnectionGatewayOptions) {
super();

this._remotesQueue = new RemotesQueue();
this.connectUrl = proxy.resolveRelativeServiceUrl(SERVICE_ROUTES.connect);
this.connectUrl = '';
this._options = this._calculateResultOptions(options);
this.proxy = proxy;

this._registerRoutes(proxy);
this._status = BrowserConnectionGatewayStatus.uninitialized;
}

private _calculateResultOptions (options: BrowserConnectionGatewayOptions): BrowserConnectionGatewayOptions {
Expand Down Expand Up @@ -335,8 +342,32 @@ export default class BrowserConnectionGateway {
return this._options.proxyless;
}

public get status (): BrowserConnectionGatewayStatus {
return this._status;
}

public get retryTestPages (): boolean {
return this._options.retryTestPages;
}

public initialize (options: TestCafeStartOptions): void {
if (this._status === BrowserConnectionGatewayStatus.initialized)
throw new GeneralError(RUNTIME_ERRORS.proxyInitializedMoreThanOnce);

this.proxy.start(options);

this._registerRoutes(this.proxy);

this.connectUrl = this.proxy.resolveRelativeServiceUrl(SERVICE_ROUTES.connect);
this._status = BrowserConnectionGatewayStatus.initialized;

this.emit('initialized');
}

public switchToProxyless (): void {
this._options.proxyless = true;

this.proxy.switchToProxyless();
}
}

6 changes: 6 additions & 0 deletions src/browser/connection/gateway/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum BrowserConnectionGatewayStatus {
uninitialized,
initialized,
}

export default BrowserConnectionGatewayStatus;
10 changes: 6 additions & 4 deletions src/browser/connection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,13 @@ export default class BrowserConnection extends EventEmitter {
this.pendingTestRunInfo = null;
this._options = this._calculateResultOptions(options);

this._buildCommunicationUrls(gateway.proxy);
this._setEventHandlers();

BrowserConnectionTracker.add(this);

this.previousActiveWindowId = null;

this.browserConnectionGateway.startServingConnection(this);

// NOTE: Give a caller time to assign event listeners
process.nextTick(() => this._runBrowser());
}

private _calculateResultOptions (options: Partial<BrowserConnectionOptions>): BrowserConnectionOptions {
Expand Down Expand Up @@ -660,4 +656,10 @@ export default class BrowserConnection extends EventEmitter {
this.status === BrowserConnectionStatus.opened ||
this.status === BrowserConnectionStatus.closing;
}

public initialize (): void {
this._buildCommunicationUrls(this.browserConnectionGateway.proxy);

process.nextTick(() => this._runBrowser());
}
}
67 changes: 37 additions & 30 deletions src/configuration/testcafe-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { GeneralError } from '../errors/runtime';
import { RUNTIME_ERRORS } from '../errors/types';
import { LOCALHOST_NAMES } from '../utils/localhost-names';
import { BrowserConnectionGatewayOptions } from '../browser/connection/gateway';
import { getValidHostname } from './utils';

const BASE_CONFIGURATION_FILENAME = '.testcaferc';
const CONFIGURATION_FILENAMES = CONFIGURATION_EXTENSIONS.map(ext => `${BASE_CONFIGURATION_FILENAME}${ext}`);
Expand Down Expand Up @@ -71,7 +72,10 @@ const OPTION_INIT_FLAG_NAMES = [
OPTION_NAMES.disableCrossDomain,
];

interface TestCafeAdditionalStartOptions {
export interface TestCafeStartOptions {
hostname: string;
port1: number;
port2: number;
retryTestPages: boolean;
ssl: object;
developmentMode: boolean;
Expand All @@ -81,15 +85,10 @@ interface TestCafeAdditionalStartOptions {
disableCrossDomain: boolean;
}

interface TestCafeStartOptions {
hostname?: string;
port1?: number;
port2?: number;
options: TestCafeAdditionalStartOptions;
}

type BrowserInfoSource = BrowserInfo | BrowserConnection;

type CalculateHostnameFn = (hostname: string) => Promise<string>;

export default class TestCafeConfiguration extends Configuration {
protected readonly _isExplicitConfig: boolean;

Expand Down Expand Up @@ -159,29 +158,18 @@ export default class TestCafeConfiguration extends Configuration {
}

public get startOptions (): TestCafeStartOptions {
const proxyless = this.getOption(OPTION_NAMES.experimentalProxyless) as boolean;
let hostname = this.getOption(OPTION_NAMES.hostname) as string;

if (!hostname && proxyless)
hostname = LOCALHOST_NAMES.LOCALHOST;

const result: TestCafeStartOptions = {
hostname,
port1: this.getOption(OPTION_NAMES.port1),
port2: this.getOption(OPTION_NAMES.port2),

options: {
ssl: this.getOption(OPTION_NAMES.ssl),
developmentMode: this.getOption(OPTION_NAMES.developmentMode),
retryTestPages: this.getOption(OPTION_NAMES.retryTestPages),
cache: this.getOption(OPTION_NAMES.cache),
disableHttp2: this.getOption(OPTION_NAMES.disableHttp2),
disableCrossDomain: this.getOption(OPTION_NAMES.disableCrossDomain),
proxyless,
},
return {
hostname: this.getOption(OPTION_NAMES.hostname),
port1: this.getOption(OPTION_NAMES.port1),
port2: this.getOption(OPTION_NAMES.port2),
ssl: this.getOption(OPTION_NAMES.ssl),
developmentMode: this.getOption(OPTION_NAMES.developmentMode),
retryTestPages: this.getOption(OPTION_NAMES.retryTestPages),
cache: this.getOption(OPTION_NAMES.cache),
disableHttp2: this.getOption(OPTION_NAMES.disableHttp2),
disableCrossDomain: this.getOption(OPTION_NAMES.disableCrossDomain),
proxyless: this.getOption(OPTION_NAMES.experimentalProxyless),
};

return result;
}

public get browserConnectionGatewayOptions (): BrowserConnectionGatewayOptions {
Expand Down Expand Up @@ -338,4 +326,23 @@ export default class TestCafeConfiguration extends Configuration {
public static get FILENAMES (): string[] {
return CONFIGURATION_FILENAMES;
}

public async ensureHostname (calculateFn: CalculateHostnameFn = getValidHostname): Promise<void> {
let hostname = this.getOption<string>(OPTION_NAMES.hostname);

hostname = await calculateFn(hostname);

this.mergeOptions({ hostname });
}

public async calculateHostname ({ proxyless } = { proxyless: false }): Promise<void> {
await this.ensureHostname(async hostname => {
if (proxyless)
hostname = hostname || LOCALHOST_NAMES.LOCALHOST;
else
hostname = await getValidHostname(hostname);

return hostname;
});
}
}
29 changes: 29 additions & 0 deletions src/configuration/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { GeneralError } from '../errors/runtime';
import { RUNTIME_ERRORS } from '../errors/types';
import endpointUtils from 'endpoint-utils';

export async function getValidHostname (hostname: string): Promise<string> {
if (hostname) {
const valid = await endpointUtils.isMyHostname(hostname);

if (!valid)
throw new GeneralError(RUNTIME_ERRORS.invalidHostname, hostname);
}
else
hostname = endpointUtils.getIPAddress();

return hostname;
}

export async function getValidPort (port: number): Promise<number> {
if (port) {
const isFree = await endpointUtils.isFreePort(port);

if (!isFree)
throw new GeneralError(RUNTIME_ERRORS.portIsNotFree, port);
}
else
port = await endpointUtils.getFreePort();

return port;
}
2 changes: 2 additions & 0 deletions src/errors/runtime/templates.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/errors/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,6 @@ export const RUNTIME_ERRORS = {
invalidCustomActionsOptionType: 'E1079',
invalidCustomActionType: 'E1080',
cannotImportESMInCommonsJS: 'E1081',
proxyInitializedMoreThanOnce: 'E1082',
setProxylessForUnsupportedBrowsers: 'E1083',
};
2 changes: 1 addition & 1 deletion src/live/test-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class LiveModeRunner extends Runner {
delete this._running;
});

this.opts = Object.assign({}, this.opts, options);
this._options = Object.assign({}, this._options, options);

this._setConfigurationOptions()
.then(() => this._setBootstrapperOptions())
Expand Down
47 changes: 42 additions & 5 deletions src/runner/bootstrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { RUNTIME_ERRORS } from '../errors/types';
import TestedApp from './tested-app';
import parseFileList from '../utils/parse-file-list';
import loadClientScripts from '../custom-client-scripts/load';
import { getConcatenatedValuesString } from '../utils/string';
import { getConcatenatedValuesString, getPluralSuffix } from '../utils/string';
import { ReporterSource } from '../reporter/interfaces';
import ClientScript from '../custom-client-scripts/client-script';
import ClientScriptInit from '../custom-client-scripts/client-script-init';
Expand All @@ -36,8 +36,9 @@ import { assertType, is } from '../errors/runtime/type-assertions';
import { generateUniqueId } from 'testcafe-hammerhead';
import assertRequestHookType from '../api/request-hooks/assert-type';
import userVariables from '../api/user-variables';
import Configuration from '../configuration/configuration-base';
import OPTION_NAMES from '../configuration/option-names';
import TestCafeConfiguration from '../configuration/testcafe-configuration';
import BrowserConnectionGatewayStatus from '../browser/connection/gateway/status';

const DEBUG_SCOPE = 'testcafe:bootstrapper';

Expand Down Expand Up @@ -97,7 +98,7 @@ export default class Bootstrapper {
public compilerOptions?: CompilerOptions;
public browserInitTimeout?: number;
public hooks?: GlobalHooks;
public configuration: Configuration;
public configuration: TestCafeConfiguration;

private readonly compilerService?: CompilerService;
private readonly debugLogger: debug.Debugger;
Expand Down Expand Up @@ -158,10 +159,14 @@ export default class Bootstrapper {
const options = {
disableMultipleWindows: this.disableMultipleWindows,
developmentMode: this.configuration.getOption(OPTION_NAMES.developmentMode) as boolean,
proxyless: this.configuration.getOption(OPTION_NAMES.experimentalProxyless) as boolean,
proxyless: this.proxyless,
};

return new BrowserConnection(this.browserConnectionGateway, { ...browser }, false, options, this.messageBus);
const connection = new BrowserConnection(this.browserConnectionGateway, { ...browser }, false, options, this.messageBus);

connection.initialize();

return connection;
}));
}

Expand All @@ -173,14 +178,46 @@ export default class Bootstrapper {
};
}

private async _setupProxy (): Promise<void> {
if (this.browserConnectionGateway.status === BrowserConnectionGatewayStatus.initialized)
return;

await this.configuration.calculateHostname({ proxyless: this.proxyless });

this.browserConnectionGateway.initialize(this.configuration.startOptions);

if (this.proxyless)
this.browserConnectionGateway.switchToProxyless();
}

private assertUnsupportedBrowsersForProxylessMode (automatedBrowserConnections: BrowserConnection[][]): void {
if (!this.proxyless)
return;

const unsupportedBrowsers = flatten(automatedBrowserConnections)
.filter(connection => {
return !connection.provider.supportProxyless();
})
.map(connection => connection.browserInfo.providerName);

if (unsupportedBrowsers.length)
throw new GeneralError(RUNTIME_ERRORS.setProxylessForUnsupportedBrowsers, getConcatenatedValuesString(unsupportedBrowsers), getPluralSuffix(unsupportedBrowsers));
}

private async _getBrowserConnections (browserInfo: BrowserInfoSource[]): Promise<BrowserSet> {
const { automated, remotes } = Bootstrapper._splitBrowserInfo(browserInfo);

if (remotes && remotes.length % this.concurrency)
throw new GeneralError(RUNTIME_ERRORS.cannotDivideRemotesCountByConcurrency);

this.proxyless = this.configuration.getOption(OPTION_NAMES.experimentalProxyless);

await this._setupProxy();

let browserConnections = this._createAutomatedConnections(automated);

this.assertUnsupportedBrowsersForProxylessMode(browserConnections);

remotes.forEach(remoteConnection => {
remoteConnection.messageBus = this.messageBus;

Expand Down
Loading

0 comments on commit 90318fd

Please sign in to comment.