Innovenergy_trunk/frontend/node_modules/testcafe/lib/services/compiler/host.js

370 lines
64 KiB
JavaScript
Raw Normal View History

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const url_1 = require("url");
const chrome_remote_interface_1 = __importDefault(require("chrome-remote-interface"));
const child_process_1 = require("child_process");
const endpoint_utils_1 = require("endpoint-utils");
const io_1 = require("./io");
const test_structure_1 = require("../serialization/test-structure");
const prepare_options_1 = __importDefault(require("../serialization/prepare-options"));
const test_run_tracker_1 = __importDefault(require("../../api/test-run-tracker"));
const test_controller_1 = __importDefault(require("../../api/test-controller"));
const proxy_1 = require("../utils/ipc/proxy");
const transport_1 = require("../utils/ipc/transport");
const async_event_emitter_1 = __importDefault(require("../../utils/async-event-emitter"));
const error_list_1 = __importDefault(require("../../errors/error-list"));
const debug_action_1 = __importDefault(require("../../utils/debug-action"));
const observation_1 = require("../../test-run/commands/observation");
const method_should_not_be_called_error_1 = __importDefault(require("../utils/method-should-not-be-called-error"));
const test_run_1 = require("../../errors/test-run");
const handle_errors_1 = require("../../utils/handle-errors");
const node_arguments_filter_1 = require("../../cli/node-arguments-filter");
const SERVICE_PATH = require.resolve('./service-loader');
const INTERNAL_FILES_URL = (0, url_1.pathToFileURL)(path_1.default.join(__dirname, '../../'));
const INSPECT_RE = new RegExp(`^(${node_arguments_filter_1.V8_DEBUG_FLAGS.join('|')})`);
const INSPECT_PORT_RE = new RegExp(`^(${node_arguments_filter_1.V8_DEBUG_FLAGS.join('|')})=(.+:)?(\\d+)$`);
const INITIAL_DEBUGGER_BREAK_ON_START = 'Break on start';
const errorTypeConstructors = new Map([
[test_run_1.UnhandledPromiseRejectionError.name, test_run_1.UnhandledPromiseRejectionError],
[test_run_1.UncaughtExceptionError.name, test_run_1.UncaughtExceptionError],
]);
class CompilerHost extends async_event_emitter_1.default {
constructor({ developmentMode, v8Flags }) {
super();
this.runtime = Promise.resolve(void 0);
this.developmentMode = developmentMode;
this.v8Flags = v8Flags;
this.initialized = false;
}
_setupRoutes(proxy) {
proxy.register([
this.executeCommand,
this.ready,
this.onRequestHookEvent,
this.setMock,
this.setConfigureResponseEventOptions,
this.setHeaderOnConfigureResponseEvent,
this.removeHeaderOnConfigureResponseEvent,
this.executeRequestFilterRulePredicate,
this.executeMockPredicate,
this.getWarningMessages,
this.addRequestEventListeners,
this.removeRequestEventListeners,
this.initializeTestRunData,
this.getAssertionActualValue,
this.executeRoleInitFn,
this.getCtx,
this.getFixtureCtx,
this.setCtx,
this.setFixtureCtx,
this.updateRoleProperty,
this.executeJsExpression,
this.executeAsyncJsExpression,
this.executeAssertionFn,
this.addUnexpectedError,
this.checkWindow,
this.removeTestRunFromState,
this.removeFixtureCtxsFromState,
this.removeUnitsFromState,
], this);
}
_setupDebuggerHandlers() {
if (!this.cdp)
return;
test_run_tracker_1.default.on(debug_action_1.default.resume, async () => {
if (!this.cdp)
return;
const disableDebugMethodName = test_controller_1.default.disableDebugForNonDebugCommands.name;
// NOTE: disable `debugger` for non-debug commands if the `Resume` button is clicked
// the `includeCommandLineAPI` option allows to use the `require` functoion in the expression
// TODO: debugging: refactor to use absolute paths
await this.cdp.Runtime.evaluate({
expression: `require.main.require('../../api/test-controller').${disableDebugMethodName}()`,
includeCommandLineAPI: true,
});
await this.cdp.Debugger.resume({ terminateOnResume: false });
});
test_run_tracker_1.default.on(debug_action_1.default.step, async () => {
if (!this.cdp)
return;
const enableDebugMethodName = test_controller_1.default.enableDebugForNonDebugCommands.name;
// NOTE: enable `debugger` for non-debug commands in the `Next Action` button is clicked
// the `includeCommandLineAPI` option allows to use the `require` functoion in the expression
// TODO: debugging: refactor to use absolute paths
await this.cdp.Runtime.evaluate({
expression: `require.main.require('../../api/test-controller').${enableDebugMethodName}()`,
includeCommandLineAPI: true,
});
await this.cdp.Debugger.resume({ terminateOnResume: false });
});
// NOTE: need to step out from the source code until breakpoint is set in the code of test
// force DebugCommand if breakpoint stopped in the test code
// TODO: debugging: refactor to this.cdp.Debugger.on('paused') after updating to chrome-remote-interface@0.30.0
this.cdp.on('Debugger.paused', (args) => {
const { callFrames } = args;
if (this.cdp) {
if (args.reason === INITIAL_DEBUGGER_BREAK_ON_START)
return this.cdp.Debugger.resume({ terminateOnResume: false });
if (callFrames[0].url.includes(INTERNAL_FILES_URL))
return this.cdp.Debugger.stepOut();
Object.values(test_run_tracker_1.default.activeTestRuns).forEach(testRun => {
if (!testRun.debugging)
testRun.executeCommand(new observation_1.DebugCommand());
});
}
return Promise.resolve();
});
// NOTE: need to hide Status Bar if debugger is resumed
// TODO: debugging: refactor to this.cdp.Debugger.on('resumed') after updating to chrome-remote-interface@0.30.0
this.cdp.on('Debugger.resumed', () => {
Object.values(test_run_tracker_1.default.activeTestRuns).forEach(testRun => {
if (testRun.debugging)
testRun.executeCommand(new observation_1.DisableDebugCommand());
});
});
}
parseDebugPort() {
if (this.v8Flags) {
for (let i = 0; i < this.v8Flags.length; i++) {
const match = this.v8Flags[i].match(INSPECT_PORT_RE);
if (match)
return match[3];
}
}
return null;
}
_getServiceProcessArgs(port) {
let args = [];
if (this.v8Flags)
args = this.v8Flags.filter(flag => !INSPECT_RE.test(flag));
// TODO: debugging: refactor to a separate debug info parsing unit
const inspectBrkFlag = `--inspect-brk=127.0.0.1:${port}`;
args.push(inspectBrkFlag, SERVICE_PATH);
return args;
}
async _init(runtime) {
const resolvedRuntime = await runtime;
if (resolvedRuntime)
return resolvedRuntime;
try {
const port = this.parseDebugPort() || await (0, endpoint_utils_1.getFreePort)();
const args = this._getServiceProcessArgs(port.toString());
const service = (0, child_process_1.spawn)(process.argv0, args, { stdio: [0, 1, 2, 'pipe', 'pipe', 'pipe'] });
// NOTE: need to wait, otherwise the error will be at `await cdp(...)`
// TODO: debugging: refactor to use delay and multiple tries
await new Promise(r => setTimeout(r, 2000));
// @ts-ignore
this.cdp = await (0, chrome_remote_interface_1.default)({ port });
if (!this.cdp)
return void 0;
if (!this.developmentMode)
this._setupDebuggerHandlers();
await this.cdp.Debugger.enable({});
await this.cdp.Runtime.enable();
await this.cdp.Runtime.runIfWaitingForDebugger();
// HACK: Node.js definition are not correct when additional I/O channels are sp
const stdio = service.stdio;
const proxy = new proxy_1.IPCProxy(new transport_1.HostTransport(stdio[io_1.HOST_INPUT_FD], stdio[io_1.HOST_OUTPUT_FD], stdio[io_1.HOST_SYNC_FD]));
this._setupRoutes(proxy);
await this.once('ready');
return { proxy, service };
}
catch (e) {
return void 0;
}
}
async _getRuntime() {
const runtime = await this.runtime;
if (!runtime)
throw new Error('Runtime is not available.');
return runtime;
}
_getTargetTestRun(id) {
return test_run_tracker_1.default.activeTestRuns[id];
}
async init() {
this.runtime = this._init(this.runtime);
await this.runtime;
this.initialized = true;
}
async stop() {
if (!this.initialized)
return;
const { service, proxy } = await this._getRuntime();
service.kill();
proxy.stop();
}
_wrapTestFunction(id, functionName) {
return async (testRun) => {
try {
return await this.runTestFn({ id, functionName, testRunId: testRun.id });
}
catch (err) {
const errList = new error_list_1.default();
errList.addError(err);
throw errList;
}
};
}
_wrapRequestFilterRulePredicate({ testId, hookId, ruleId }) {
return async (requestInfo) => {
return await this.executeRequestFilterRulePredicate({ testId, hookId, ruleId, requestInfo });
};
}
_wrapMockPredicate({ mock, testId, hookId, ruleId }) {
mock.body = async (requestInfo, res) => {
return await this.executeMockPredicate({ testId, hookId, ruleId, requestInfo, res });
};
}
_getErrorTypeConstructor(type) {
return errorTypeConstructors.get(type);
}
async ready() {
this.emit('ready');
}
executeCommandSync() {
throw new method_should_not_be_called_error_1.default();
}
async executeCommand({ command, id, callsite }) {
return this
._getTargetTestRun(id)
.executeCommand(command, callsite);
}
async getTests({ sourceList, compilerOptions, runnableConfigurationId }, baseUrl) {
const { proxy } = await this._getRuntime();
const units = await proxy.call(this.getTests, { sourceList, compilerOptions, runnableConfigurationId }, baseUrl);
return (0, test_structure_1.restore)(units, (...args) => this._wrapTestFunction(...args), (ruleLocator) => this._wrapRequestFilterRulePredicate(ruleLocator));
}
async runTestFn({ id, functionName, testRunId }) {
const { proxy } = await this._getRuntime();
return await proxy.call(this.runTestFn, { id, functionName, testRunId });
}
async cleanUp() {
const { proxy } = await this._getRuntime();
await proxy.call(this.cleanUp);
}
async setUserVariables(userVariables) {
const { proxy } = await this._getRuntime();
await proxy.call(this.setUserVariables, userVariables);
}
async setOptions({ value }) {
const { proxy } = await this._getRuntime();
const preparedOptions = (0, prepare_options_1.default)(value);
await proxy.call(this.setOptions, { value: preparedOptions });
}
async onRequestHookEvent({ name, testId, hookId, eventData }) {
const { proxy } = await this._getRuntime();
await proxy.call(this.onRequestHookEvent, {
name,
testId,
hookId,
eventData,
});
}
async setMock({ testId, hookId, ruleId, responseEventId, mock }) {
if (mock.isPredicate)
this._wrapMockPredicate({ mock, testId, hookId, ruleId });
await this.emit('setMock', [responseEventId, mock]);
}
async setConfigureResponseEventOptions({ eventId, opts }) {
await this.emit('setConfigureResponseEventOptions', [eventId, opts]);
}
async setHeaderOnConfigureResponseEvent({ eventId, headerName, headerValue }) {
await this.emit('setHeaderOnConfigureResponseEvent', [eventId, headerName, headerValue]);
}
async removeHeaderOnConfigureResponseEvent({ eventId, headerName }) {
await this.emit('removeHeaderOnConfigureResponseEvent', [eventId, headerName]);
}
async executeRequestFilterRulePredicate({ testId, hookId, ruleId, requestInfo }) {
const { proxy } = await this._getRuntime();
return await proxy.call(this.executeRequestFilterRulePredicate, { testId, hookId, ruleId, requestInfo });
}
async executeMockPredicate({ testId, hookId, ruleId, requestInfo, res }) {
const { proxy } = await this._getRuntime();
return await proxy.call(this.executeMockPredicate, { testId, hookId, ruleId, requestInfo, res });
}
async getWarningMessages({ testRunId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.getWarningMessages, { testRunId });
}
async addRequestEventListeners({ hookId, hookClassName, rules }) {
await this.emit('addRequestEventListeners', { hookId, hookClassName, rules });
}
async removeRequestEventListeners({ rules }) {
await this.emit('removeRequestEventListeners', { rules });
}
async initializeTestRunData({ testRunId, testId, browser, activeWindowId, messageBus }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.initializeTestRunData, { testRunId, testId, browser, activeWindowId, messageBus });
}
async getAssertionActualValue({ testRunId, commandId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.getAssertionActualValue, { testRunId, commandId: commandId });
}
async executeRoleInitFn({ testRunId, roleId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.executeRoleInitFn, { testRunId, roleId });
}
async getCtx({ testRunId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.getCtx, { testRunId });
}
async getFixtureCtx({ testRunId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.getFixtureCtx, { testRunId });
}
async setCtx({ testRunId, value }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.setCtx, { testRunId, value });
}
async setFixtureCtx({ testRunId, value }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.setFixtureCtx, { testRunId, value });
}
onRoleAppeared() {
throw new method_should_not_be_called_error_1.default();
}
async updateRoleProperty({ roleId, name, value }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.updateRoleProperty, { roleId, name, value });
}
async executeJsExpression({ expression, testRunId, options }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.executeJsExpression, { expression, testRunId, options });
}
async executeAsyncJsExpression({ expression, testRunId, callsite }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.executeAsyncJsExpression, { expression, testRunId, callsite });
}
async executeAssertionFn({ testRunId, commandId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.executeAssertionFn, { testRunId, commandId });
}
async addUnexpectedError({ type, message }) {
const ErrorTypeConstructor = this._getErrorTypeConstructor(type);
(0, handle_errors_1.handleUnexpectedError)(ErrorTypeConstructor, message);
}
async checkWindow({ testRunId, commandId, url, title }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.checkWindow, { testRunId, commandId, url, title });
}
async removeTestRunFromState({ testRunId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.removeTestRunFromState, { testRunId });
}
async removeFixtureCtxsFromState({ fixtureIds }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.removeFixtureCtxsFromState, { fixtureIds });
}
async removeUnitsFromState({ runnableConfigurationId }) {
const { proxy } = await this._getRuntime();
return proxy.call(this.removeUnitsFromState, { runnableConfigurationId });
}
}
exports.default = CompilerHost;
module.exports = exports.default;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaG9zdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zZXJ2aWNlcy9jb21waWxlci9ob3N0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsZ0RBQXdCO0FBQ3hCLDZCQUFvQztBQUNwQyxzRkFBMEM7QUFFMUMsaURBQW9EO0FBQ3BELG1EQUE2QztBQUU3Qyw2QkFJYztBQUVkLG9FQUFrRjtBQUNsRix1RkFBOEQ7QUFDOUQsa0ZBQXVFO0FBQ3ZFLGdGQUF1RDtBQUV2RCw4Q0FBOEM7QUFDOUMsc0RBQXVEO0FBQ3ZELDBGQUFnRTtBQUNoRSx5RUFBd0Q7QUFDeEQsNEVBQW9EO0FBaUJwRCxxRUFBd0Y7QUFDeEYsbUhBQXNGO0FBNkJ0RixvREFBK0Y7QUFDL0YsNkRBQWtFO0FBQ2xFLDJFQUFpRTtBQUdqRSxNQUFNLFlBQVksR0FBUyxPQUFPLENBQUMsT0FBTyxDQUFDLGtCQUFrQixDQUFDLENBQUM7QUFDL0QsTUFBTSxrQkFBa0IsR0FBRyxJQUFBLG1CQUFhLEVBQUMsY0FBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQztBQUV6RSxNQUFNLFVBQVUsR0FBUSxJQUFJLE1BQU0sQ0FBQyxLQUFLLHNDQUFjLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUNyRSxNQUFNLGVBQWUsR0FBRyxJQUFJLE1BQU0sQ0FBQyxLQUFLLHNDQUFjLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0FBbUJuRixNQUFNLCtCQUErQixHQUFHLGdCQUFnQixDQUFDO0FBRXpELE1BQU0scUJBQXFCLEdBQUcsSUFBSSxHQUFHLENBQW1CO0lBQ3BELENBQUMseUNBQThCLENBQUMsSUFBSSxFQUFFLHlDQUE4QixDQUFDO0lBQ3JFLENBQUMsaUNBQXNCLENBQUMsSUFBSSxFQUFFLGlDQUFzQixDQUFDO0NBQ3hELENBQUMsQ0FBQztBQU9ILE1BQXFC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