Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(skip-execution): add support to display system logs #957

Draft
wants to merge 7 commits into
base: feat/skip-request
Choose a base branch
from
24 changes: 21 additions & 3 deletions lib/sandbox/console.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const dispatchEvent = require('./dispatch-event');

var teleportJS = require('teleport-javascript'),

arrayProtoSlice = Array.prototype.slice,
Expand All @@ -8,6 +10,8 @@ var teleportJS = require('teleport-javascript'),
*/
CONSOLE_EVENT = 'execution.console',

SYSTEM_MESSAGE = 'system_message',

/**
* List of functions that we expect and create for console
*
Expand Down Expand Up @@ -45,7 +49,7 @@ function replacer (key, value) {
return value;
}

function PostmanConsole (emitter, cursor, originalConsole) {
function PostmanConsole (emitter, cursor, originalConsole, execution) {
const dispatch = function (level) { // create a dispatch function that emits events
const args = arrayProtoSlice.call(arguments, 1);

Expand All @@ -54,7 +58,8 @@ function PostmanConsole (emitter, cursor, originalConsole) {
originalConsole[level].apply(originalConsole, args);
}

emitter.dispatch(CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));

dispatchEvent(emitter, execution, CONSOLE_EVENT, cursor, level, teleportJS.stringify(args, replacer));
};

// setup variants of the logger based on log levels
Expand All @@ -63,4 +68,17 @@ function PostmanConsole (emitter, cursor, originalConsole) {
});
}

module.exports = PostmanConsole;
/**
*
* @param {{log: Function}} console - postman console instance
* @param {'skip_request'} type - this is used to identify the message type in postman-app
* @param {...any} args - any other information like request name to be used to generate console message
*/
function dispatchSystemMessage (console, type, ...args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think if this should be part of this module. Maybe a helper?

return console.log(SYSTEM_MESSAGE, type, ...args);
}

module.exports = {
default: PostmanConsole,
dispatchSystemMessage: dispatchSystemMessage
};
7 changes: 5 additions & 2 deletions lib/sandbox/cookie-store.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const dispatchEvent = require('./dispatch-event');

const _ = require('lodash'),

Store = require('@postman/tough-cookie').Store,
Expand All @@ -14,12 +16,13 @@ const _ = require('lodash'),
arrayProtoSlice = Array.prototype.slice;

class PostmanCookieStore extends Store {
constructor (id, emitter, timers) {
constructor (id, emitter, timers, execution) {
super();

this.id = id; // execution identifier
this.emitter = emitter;
this.timers = timers;
this.execution = execution;
}
}

Expand Down Expand Up @@ -77,7 +80,7 @@ STORE_METHODS.forEach(function (method) {
// Refer: https://github.com/postmanlabs/postman-app-support/issues/11064
setTimeout(() => {
// finally, dispatch event over the bridge
this.emitter.dispatch(eventName, eventId, EVENT_STORE_ACTION, method, args);
dispatchEvent(this.emitter, this.execution, eventName, eventId, EVENT_STORE_ACTION, method, args);
});
};
});
Expand Down
18 changes: 18 additions & 0 deletions lib/sandbox/dispatch-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Why Do we need this wrapper?
* Because when user executes pm.request.stopExecution(), we need to stop the execution of the current request.
* But, we don't stop the execution of the script. We just stop sending events to the bridge.
*
* @param {any} bridge - the bridge object
* @param {{ shouldSkipExecution: boolean }} execution - the execution object
* @param {string} event - the event name
* @param {...any} args - the arguments to be passed to the event
*/
module.exports = function dispatchEvent (bridge, execution, event, ...args) {
// if the execution is skipped, do not dispatch the event
if (execution && execution.shouldSkipExecution) {
return;
}

bridge.dispatch(event, ...args);
};
52 changes: 35 additions & 17 deletions lib/sandbox/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const _ = require('lodash'),
sdk = require('postman-collection'),
PostmanEvent = sdk.Event,
Execution = require('./execution'),
PostmanConsole = require('./console'),
{ default: PostmanConsole, dispatchSystemMessage } = require('./console'),
PostmanTimers = require('./timers'),
PostmanAPI = require('./pmapi'),
PostmanCookieStore = require('./cookie-store'),
Expand All @@ -20,7 +20,8 @@ const _ = require('lodash'),
EXECUTION_ASSERTION_EVENT = 'execution.assertion',
EXECUTION_ASSERTION_EVENT_BASE = 'execution.assertion.',

executeContext = require('./execute-context');
executeContext = require('./execute-context'),
dispatchEvent = require('./dispatch-event');

module.exports = function (bridge, glob) {
// @note we use a common scope for all executions. this causes issues when scripts are run inside the sandbox
Expand Down Expand Up @@ -49,7 +50,7 @@ module.exports = function (bridge, glob) {
if (!template) {
chai.use(require('chai-postman')(sdk, _, Ajv));

return bridge.dispatch('initialize');
return dispatchEvent(bridge, null, 'initialize');
}

const _module = { exports: {} },
Expand All @@ -66,7 +67,7 @@ module.exports = function (bridge, glob) {

scope.exec(template, (err) => {
if (err) {
return bridge.dispatch('initialize', err);
return dispatchEvent(bridge, null, 'initialize', err);
}

const { chaiPlugin, initializeExecution: setupExecution } = (_module && _module.exports) || {};
Expand All @@ -79,7 +80,7 @@ module.exports = function (bridge, glob) {
initializeExecution = setupExecution;
}

bridge.dispatch('initialize');
dispatchEvent(bridge, null, 'initialize');
});
});

Expand All @@ -97,7 +98,8 @@ module.exports = function (bridge, glob) {
*/
bridge.on('execute', function (id, event, context, options) {
if (!(id && _.isString(id))) {
return bridge.dispatch('error', new Error('sandbox: execution identifier parameter(s) missing'));
return dispatchEvent(bridge, null, 'error',
new Error('sandbox: execution identifier parameter(s) missing'));
}

!options && (options = {});
Expand Down Expand Up @@ -136,20 +138,21 @@ module.exports = function (bridge, glob) {
// For compatibility, dispatch the single assertion as an array.
!Array.isArray(assertions) && (assertions = [assertions]);

bridge.dispatch(assertionEventName, options.cursor, assertions);
bridge.dispatch(EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
dispatchEvent(bridge, execution, assertionEventName, options.cursor, assertions);
dispatchEvent(bridge, execution, EXECUTION_ASSERTION_EVENT, options.cursor, assertions);
};

let waiting,
timers;
timers,
postmanConsole;

execution.return.async = false;

// create the controlled timers
timers = new PostmanTimers(null, function (err) {
if (err) { // propagate the error out of sandbox
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
dispatchEvent(bridge, execution, errorEventName, options.cursor, err);
dispatchEvent(bridge, execution, EXECUTION_ERROR_EVENT, options.cursor, err);
}
}, function () {
execution.return.async = true;
Expand All @@ -169,16 +172,22 @@ module.exports = function (bridge, glob) {
bridge.off(cookiesEventName);

if (err) { // fire extra execution error event
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
dispatchEvent(bridge, execution, errorEventName, options.cursor, err);
dispatchEvent(bridge, execution, EXECUTION_ERROR_EVENT, options.cursor, err);
}

// @note delete response from the execution object to avoid dispatching
// the large response payload back due to performance reasons.
execution.response && (delete execution.response);

// fire the execution completion event
(dnd !== true) && bridge.dispatch(executionEventName, err || null, execution);

// note: We are sending shouldSkipExecution: false to dispatchEvent function
// because this event should be fired even if shouldSkipExecution is true as this event is
// used to complete the execution in the sandbox. All other events are fired only if
// shouldSkipExecution is false.
(dnd !== true) && dispatchEvent(bridge, { shouldSkipExecution: false },
executionEventName, err || null, execution);
});

// if a timeout is set, we must ensure that all pending timers are cleared and an execution timeout event is
Expand All @@ -203,18 +212,27 @@ module.exports = function (bridge, glob) {
timers.clearEvent(id, err, res);
});

postmanConsole = new PostmanConsole(bridge, options.cursor, options.debug && glob.console, execution);

// send control to the function that executes the context and prepares the scope
executeContext(scope, code, execution,
// if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console
// inside this closure.
(new PostmanConsole(bridge, options.cursor, options.debug && glob.console)),
postmanConsole,
timers,
(
new PostmanAPI(execution, function (request, callback) {
var eventId = timers.setEvent(callback);

bridge.dispatch(executionRequestEventName, options.cursor, id, eventId, request);
}, dispatchAssertions, new PostmanCookieStore(id, bridge, timers), {
dispatchEvent(bridge, execution, executionRequestEventName, options.cursor, id, eventId, request);
}, /* onStopExecution = */ function () {
// Dispatch event to display system message on console informing user that the request
// execution was skipped from script
dispatchSystemMessage(postmanConsole, 'skip_request', _.get(execution, 'legacy._itemName'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dispatch a system type console event? Why not a "execution_stopped" or such event? Every client can then interpret it and use it however they want to.

Also, "system" type log is not supported in Console interface either.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be a part of any existing events like item event with some indication that the execution was stopped from script.

execution.shouldSkipExecution = true;
timers.terminate(null);
},
dispatchAssertions, new PostmanCookieStore(id, bridge, timers, execution), {
disabledAPIs: initializationOptions.disabledAPIs
})
),
Expand Down
9 changes: 9 additions & 0 deletions lib/sandbox/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ class Execution {
this.id = id;
this.target = event.listen || PROPERTY.SCRIPT;
this.legacy = options.legacy || {};

/**
* This property is set to true if user has called pm.request.stopExecution() in the script.
* This is used to stop the execution of the current request.
* We stop sending events to the bridge if this is set to true.
*
* @type {Boolean}
*/
this.shouldSkipExecution = false;
this.cursor = _.isObject(options.cursor) ? options.cursor : {};

this.data = _.get(context, PROPERTY.DATA, {});
Expand Down
24 changes: 22 additions & 2 deletions lib/sandbox/pmapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ const _ = require('lodash'),
*
* @param {Execution} execution -
* @param {Function} onRequest -
* @param {Function} onStopExecution - callback to execute when pm.request.stopExecution() called
* @param {Function} onAssertion -
* @param {Object} cookieStore -
* @param {Object} [options] -
* @param {Array.<String>} [options.disabledAPIs] -
*/
function Postman (execution, onRequest, onAssertion, cookieStore, options = {}) {
function Postman (execution, onRequest, onStopExecution, onAssertion, cookieStore, options = {}) {
// @todo - ensure runtime passes data in a scope format
let iterationData = new VariableScope();

Expand Down Expand Up @@ -149,12 +150,15 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {})
*/
iterationData: iterationData,

/**
* @Interface IRequest
*/
/**
* The request object inside pm is a representation of the request for which this script is being run.
* For a pre-request script, this is the request that is about to be sent and when in a test script,
* this is the representation of the request that was sent.
*
* @type {Request}
* @type {Request|IRequest}
*/
request: execution.request,

Expand Down Expand Up @@ -273,6 +277,22 @@ function Postman (execution, onRequest, onAssertion, cookieStore, options = {})
return chai.expect(this).to;
}
});

/**
* Halts the execution of current request. No line after this will be executed and
* if invoked from a pre-request script, the request will not be sent.
*
* @type {Function} stopExecution
* @example
* if (pm.environment.get("token")) {
* pm.request.stopExecution();
* }
*
* @name IRequest#stopExecution
*/
Object.defineProperty(this.request, 'stopExecution', {
value: onStopExecution
});
}

iterationData = null; // precautionary
Expand Down
14 changes: 14 additions & 0 deletions npm/build-sandbox-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ module.exports = function (exit) {
c.type.typeName.escapedText = `import("postman-collection").${currentType}`;
}
}

// For properties referencing sdk and some more properties, eg. request: Request|IRequest
if (c.type && c.type.types && c.type.types.length) {
c.type.types.forEach((t) => {
if (t.typeName) {
let currentType = t.typeName.escapedText;

if (collectionSDKTypes.includes(currentType)) {
t.typeName.escapedText = `import("postman-collection").${currentType}`;
}
}
});
}

// takes care of functions with parameters referencing CollectionSDK types
else if (c.parameters && c.parameters.length > 0) {
c.parameters.forEach((p) => {
Expand Down