@usebruno/js 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/bru.js +29 -28
- package/src/bruno-request.js +4 -0
- package/src/bruno-response.js +4 -1
- package/src/grpc/bruno-grpc-request.js +52 -0
- package/src/grpc/bruno-grpc-response.js +43 -0
- package/src/grpc/grpc-message-list.js +74 -0
- package/src/grpc/grpc-message.js +20 -0
- package/src/grpc/grpc-metadata-list.js +214 -0
- package/src/grpc/grpc-metadata.js +21 -0
- package/src/grpc/grpc-script-runtime.js +302 -0
- package/src/index.js +2 -0
- package/src/interpolate-string.js +2 -1
- package/src/runtime/assert-runtime.js +1 -0
- package/src/runtime/script-runtime.js +20 -4
- package/src/runtime/test-runtime.js +15 -8
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +416 -51
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +896 -0
- package/src/sandbox/quickjs/index.js +101 -17
- package/src/sandbox/quickjs/shims/bru.js +54 -54
- package/src/sandbox/quickjs/shims/bruno-grpc.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-request.js +47 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-response.js +40 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-message-list.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-metadata-list.js +31 -0
- package/src/sandbox/quickjs/utils/index.js +42 -23
- package/src/utils/error-formatter.js +13 -9
- package/src/utils/error-formatter.spec.js +97 -2
- package/src/utils/results.js +30 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { marshallToVm } = require('../../utils');
|
|
2
|
+
const addGrpcMetadataListShimToContext = require('./grpc-metadata-list');
|
|
3
|
+
const addGrpcMessageListShimToContext = require('./grpc-message-list');
|
|
4
|
+
|
|
5
|
+
// Keep this in step with BrunoGrpcResponse.
|
|
6
|
+
const addBrunoGrpcResponseShimToContext = (vm, response, grpcObject) => {
|
|
7
|
+
const responseObject = vm.newObject();
|
|
8
|
+
|
|
9
|
+
// Marshalled once, as on the request: the call is over, so no scalar can change mid-hook.
|
|
10
|
+
const scalars = ['statusCode', 'statusText', 'duration'];
|
|
11
|
+
|
|
12
|
+
for (const property of scalars) {
|
|
13
|
+
const value = marshallToVm(response?.[property], vm);
|
|
14
|
+
vm.setProp(responseObject, property, value);
|
|
15
|
+
value.dispose();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// response.metadata / .trailers / .messages — the same lists `bru.grpc.request` gets, read-only here
|
|
19
|
+
const listEvalCode = ['metadata', 'trailers'].map((property) =>
|
|
20
|
+
addGrpcMetadataListShimToContext(vm, response[property], responseObject, property, 'globalThis.bru.grpc.response')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
listEvalCode.push(
|
|
24
|
+
addGrpcMessageListShimToContext(vm, response.messages, responseObject, 'globalThis.bru.grpc.response')
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// response.message — present only in `afterMessageReceive`
|
|
28
|
+
if (response.message) {
|
|
29
|
+
const message = marshallToVm(response.message, vm);
|
|
30
|
+
vm.setProp(responseObject, 'message', message);
|
|
31
|
+
message.dispose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
vm.setProp(grpcObject, 'response', responseObject);
|
|
35
|
+
responseObject.dispose();
|
|
36
|
+
|
|
37
|
+
return { evalCode: listEvalCode };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
module.exports = addBrunoGrpcResponseShimToContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { createPropertyListBridge } = require('../../utils/property-list-bridge');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridges a GrpcMessageList — `bru.grpc.request.messages`, `bru.grpc.response.messages`
|
|
5
|
+
* — onto a VM object. Keep in sync with GrpcMessageList
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} vm - QuickJS VM instance
|
|
8
|
+
* @param {Object} list - The native GrpcMessageList
|
|
9
|
+
* @param {Object} targetObject - VM object handle the list is attached to
|
|
10
|
+
* @param {string} objectPath - Path to `targetObject` in the VM, e.g. `globalThis.bru.grpc.request`
|
|
11
|
+
* @returns {string} Code the caller must eval once `objectPath` resolves
|
|
12
|
+
*/
|
|
13
|
+
const addGrpcMessageListShimToContext = (vm, list, targetObject, objectPath) => {
|
|
14
|
+
const listObject = vm.newObject();
|
|
15
|
+
|
|
16
|
+
const { evalCode } = createPropertyListBridge(vm, list, listObject, {
|
|
17
|
+
globalPath: `${objectPath}.messages`,
|
|
18
|
+
syncReadMethods: ['count'],
|
|
19
|
+
syncReadObjectMethods: ['get', 'all', 'toJSON'],
|
|
20
|
+
withIterators: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vm.setProp(targetObject, 'messages', listObject);
|
|
24
|
+
listObject.dispose();
|
|
25
|
+
|
|
26
|
+
return evalCode;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = addGrpcMessageListShimToContext;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { createPropertyListBridge } = require('../../utils/property-list-bridge');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridges a GrpcMetadataList — `bru.grpc.request.metadata`, `bru.grpc.response.metadata`,
|
|
5
|
+
* `bru.grpc.response.trailers` — onto a VM object. Keep in sync with GrpcMetadataList
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} vm - QuickJS VM instance
|
|
8
|
+
* @param {Object} list - The native GrpcMetadataList
|
|
9
|
+
* @param {Object} targetObject - VM object handle the list is attached to
|
|
10
|
+
* @param {string} property - Property name on `targetObject`
|
|
11
|
+
* @param {string} objectPath - Path to `targetObject` in the VM, e.g. `globalThis.bru.grpc.request`
|
|
12
|
+
* @returns {string} Code the caller must eval once `objectPath` resolves
|
|
13
|
+
*/
|
|
14
|
+
const addGrpcMetadataListShimToContext = (vm, list, targetObject, property, objectPath) => {
|
|
15
|
+
const listObject = vm.newObject();
|
|
16
|
+
|
|
17
|
+
const { evalCode } = createPropertyListBridge(vm, list, listObject, {
|
|
18
|
+
globalPath: `${objectPath}.${property}`,
|
|
19
|
+
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
20
|
+
syncReadObjectMethods: ['one', 'all', 'toJSON'],
|
|
21
|
+
syncWriteMethods: ['upsert', 'add', 'remove', 'clear'],
|
|
22
|
+
withIterators: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vm.setProp(targetObject, property, listObject);
|
|
26
|
+
listObject.dispose();
|
|
27
|
+
|
|
28
|
+
return evalCode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = addGrpcMetadataListShimToContext;
|
|
@@ -8,7 +8,7 @@ const createManagedQuickJsContext = (module) => {
|
|
|
8
8
|
const vm = module.newContext();
|
|
9
9
|
const disposeTracked = trackQuickJsContext(vm);
|
|
10
10
|
const evalCodeRetained = vm.evalCode.bind(vm);
|
|
11
|
-
const waitForPendingDeferreds = trackPendingDeferreds(vm);
|
|
11
|
+
const { waitForPendingDeferreds, disposePendingDeferreds } = trackPendingDeferreds(vm);
|
|
12
12
|
|
|
13
13
|
vm.evalCode = (code, filename = 'eval.js') => {
|
|
14
14
|
const result = evalCodeRetained(code, filename);
|
|
@@ -25,7 +25,7 @@ const createManagedQuickJsContext = (module) => {
|
|
|
25
25
|
return {
|
|
26
26
|
vm,
|
|
27
27
|
waitForPendingDeferreds,
|
|
28
|
-
dispose: () => disposeQuickJsContext(vm, disposeTracked)
|
|
28
|
+
dispose: () => disposeQuickJsContext(vm, disposeTracked, disposePendingDeferreds)
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -38,27 +38,41 @@ const createManagedQuickJsContext = (module) => {
|
|
|
38
38
|
* `QuickJSUseAfterFree`. Each `.settled` resolves once the deferred is
|
|
39
39
|
* resolved/rejected, so awaiting them keeps the context alive long enough.
|
|
40
40
|
*
|
|
41
|
-
* The hook
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* drains in place until none remain.
|
|
41
|
+
* The hook installs at context creation. `waitForPendingDeferreds` drains in
|
|
42
|
+
* place (waiting can chain new deferreds) with no timeout; `disposePendingDeferreds`
|
|
43
|
+
* frees unsettled resolve/reject handles, which left alive abort JS_FreeRuntime.
|
|
45
44
|
*/
|
|
46
|
-
|
|
47
45
|
const trackPendingDeferreds = (vm) => {
|
|
48
|
-
const
|
|
46
|
+
const pendingSettles = [];
|
|
47
|
+
const deferreds = [];
|
|
49
48
|
const originalNewPromise = vm.newPromise.bind(vm);
|
|
50
49
|
vm.newPromise = (...args) => {
|
|
51
50
|
const deferred = originalNewPromise(...args);
|
|
52
|
-
|
|
51
|
+
deferreds.push(deferred);
|
|
52
|
+
pendingSettles.push(deferred.settled.catch(() => { }));
|
|
53
53
|
return deferred;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
while (
|
|
58
|
-
const batch =
|
|
56
|
+
const waitForPendingDeferreds = async () => {
|
|
57
|
+
while (pendingSettles.length) {
|
|
58
|
+
const batch = pendingSettles.splice(0);
|
|
59
59
|
await Promise.all(batch);
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
+
|
|
63
|
+
const disposePendingDeferreds = () => {
|
|
64
|
+
while (deferreds.length) {
|
|
65
|
+
const deferred = deferreds.pop();
|
|
66
|
+
if (deferred.alive) {
|
|
67
|
+
deferred.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
waitForPendingDeferreds,
|
|
74
|
+
disposePendingDeferreds
|
|
75
|
+
};
|
|
62
76
|
};
|
|
63
77
|
|
|
64
78
|
/**
|
|
@@ -98,22 +112,18 @@ const trackQuickJsContext = (vm) => {
|
|
|
98
112
|
};
|
|
99
113
|
|
|
100
114
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
115
|
+
* Drains pending QuickJS jobs, frees leftover deferreds and tracked handles,
|
|
116
|
+
* and disposes the context. Jobs are drained BEFORE the handle flush: a job
|
|
117
|
+
* can call allocating shims or start new async work, and anything it creates
|
|
118
|
+
* must still be freed or JS_FreeRuntime aborts on the leftover GC objects.
|
|
103
119
|
*/
|
|
104
|
-
const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
120
|
+
const disposeQuickJsContext = (vm, disposeTracked, disposePendingDeferreds) => {
|
|
105
121
|
if (!vm?.alive) {
|
|
106
122
|
return;
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Drain the runtime's pending job queue (resolved/rejected promise callbacks)
|
|
114
|
-
// before disposing. Executing a job can schedule more jobs (chained `.then()`s),
|
|
115
|
-
// so we keep going until `hasPendingJob()` reports the queue is empty or a job
|
|
116
|
-
// throws.
|
|
125
|
+
// Executing a job can schedule more jobs (chained `.then()`s), so keep going
|
|
126
|
+
// until `hasPendingJob()` reports the queue is empty or a job throws.
|
|
117
127
|
while (vm.runtime?.hasPendingJob?.()) {
|
|
118
128
|
const result = vm.runtime.executePendingJobs();
|
|
119
129
|
// On error, dispose the error handle and stop draining.
|
|
@@ -122,6 +132,15 @@ const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
|
122
132
|
break;
|
|
123
133
|
}
|
|
124
134
|
}
|
|
135
|
+
|
|
136
|
+
if (typeof disposePendingDeferreds === 'function') {
|
|
137
|
+
disposePendingDeferreds();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof disposeTracked === 'function') {
|
|
141
|
+
disposeTracked();
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
vm.dispose();
|
|
126
145
|
};
|
|
127
146
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const YAML = require('yaml');
|
|
4
|
+
const { SCRIPT_TYPES } = require('@usebruno/common');
|
|
4
5
|
const { NODEVM_SCRIPT_WRAPPER_OFFSET, QUICKJS_SCRIPT_WRAPPER_OFFSET } = require('./sandbox');
|
|
5
6
|
|
|
6
7
|
const posixifyPath = (p) => (p ? p.replace(/\\/g, '/') : p);
|
|
@@ -11,17 +12,15 @@ const ALLOWED_SOURCE_EXTENSIONS = ['.bru', '.yml'];
|
|
|
11
12
|
const isAllowedSourceFile = (filePath) =>
|
|
12
13
|
typeof filePath === 'string' && ALLOWED_SOURCE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
13
14
|
|
|
14
|
-
const SCRIPT_TYPES = Object.freeze({
|
|
15
|
-
PRE_REQUEST: 'pre-request',
|
|
16
|
-
POST_RESPONSE: 'post-response',
|
|
17
|
-
TEST: 'test'
|
|
18
|
-
});
|
|
19
|
-
|
|
20
15
|
// Bruno script types → OpenCollection YAML script types
|
|
21
16
|
const SCRIPT_TYPE_TO_YML = {
|
|
22
17
|
[SCRIPT_TYPES.PRE_REQUEST]: 'before-request',
|
|
23
18
|
[SCRIPT_TYPES.POST_RESPONSE]: 'after-response',
|
|
24
|
-
[SCRIPT_TYPES.TEST]: 'tests'
|
|
19
|
+
[SCRIPT_TYPES.TEST]: 'tests',
|
|
20
|
+
[SCRIPT_TYPES.BEFORE_CALL_START]: 'grpc:before-call-start',
|
|
21
|
+
[SCRIPT_TYPES.BEFORE_MESSAGE_SEND]: 'grpc:before-message-send',
|
|
22
|
+
[SCRIPT_TYPES.AFTER_MESSAGE_RECEIVE]: 'grpc:after-message-receive',
|
|
23
|
+
[SCRIPT_TYPES.AFTER_CALL_END]: 'grpc:after-call-end'
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
const readFile = (filePath, cache = null) => {
|
|
@@ -38,7 +37,11 @@ const readFile = (filePath, cache = null) => {
|
|
|
38
37
|
const BLOCK_PATTERNS = {
|
|
39
38
|
[SCRIPT_TYPES.PRE_REQUEST]: /^script:pre-request\s*\{/,
|
|
40
39
|
[SCRIPT_TYPES.POST_RESPONSE]: /^script:post-response\s*\{/,
|
|
41
|
-
[SCRIPT_TYPES.TEST]: /^tests\s*\{
|
|
40
|
+
[SCRIPT_TYPES.TEST]: /^tests\s*\{/,
|
|
41
|
+
[SCRIPT_TYPES.BEFORE_CALL_START]: /^script:grpc:before-call-start\s*\{/,
|
|
42
|
+
[SCRIPT_TYPES.BEFORE_MESSAGE_SEND]: /^script:grpc:before-message-send\s*\{/,
|
|
43
|
+
[SCRIPT_TYPES.AFTER_MESSAGE_RECEIVE]: /^script:grpc:after-message-receive\s*\{/,
|
|
44
|
+
[SCRIPT_TYPES.AFTER_CALL_END]: /^script:grpc:after-call-end\s*\{/
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
/** Find the 1-indexed line where a script block's content starts in a .bru file */
|
|
@@ -747,5 +750,6 @@ module.exports = {
|
|
|
747
750
|
findYmlScriptBlockStartLine,
|
|
748
751
|
findYmlScriptBlockEndLine,
|
|
749
752
|
adjustStackTrace,
|
|
750
|
-
getErrorTypeName
|
|
753
|
+
getErrorTypeName,
|
|
754
|
+
posixifyPath
|
|
751
755
|
};
|
|
@@ -11,7 +11,8 @@ const {
|
|
|
11
11
|
parseErrorLocation,
|
|
12
12
|
getSourceContextFromContent,
|
|
13
13
|
adjustStackTrace,
|
|
14
|
-
buildStackFromCallSites
|
|
14
|
+
buildStackFromCallSites,
|
|
15
|
+
posixifyPath
|
|
15
16
|
} = require('./error-formatter');
|
|
16
17
|
const fs = require('fs');
|
|
17
18
|
const path = require('path');
|
|
@@ -120,6 +121,64 @@ const COLLECTION_YML = [
|
|
|
120
121
|
' });'
|
|
121
122
|
].join('\n');
|
|
122
123
|
|
|
124
|
+
// gRPC lifecycle hooks:
|
|
125
|
+
// 11: script:grpc:before-call-start { → blockStartLine = 12
|
|
126
|
+
// 16: script:grpc:after-call-end { → blockStartLine = 17
|
|
127
|
+
const GRPC_BRU = `meta {
|
|
128
|
+
name: grpc-test
|
|
129
|
+
type: grpc
|
|
130
|
+
seq: 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
grpc {
|
|
134
|
+
url: grpc://localhost:50051
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
script:grpc:before-call-start {
|
|
138
|
+
const token = bru.getEnvVar('token');
|
|
139
|
+
request.setHeader('authorization', token);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
script:grpc:after-call-end {
|
|
143
|
+
const data = response.data;
|
|
144
|
+
bru.setVar('userId', data.id);
|
|
145
|
+
console.log(data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
script:grpc:before-message-send {
|
|
149
|
+
bru.setVar('outbound', bru.grpc.request.message.data.greeting);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
script:grpc:after-message-receive {
|
|
153
|
+
const message = bru.grpc.response.message;
|
|
154
|
+
bru.setVar('inbound', message.data.reply);
|
|
155
|
+
}`;
|
|
156
|
+
|
|
157
|
+
// gRPC yml fixture: blockStartLine = 8 (before-call-start), 12 (after-call-end),
|
|
158
|
+
// 16 (before-message-send), 19 (after-message-receive)
|
|
159
|
+
const GRPC_YML = [
|
|
160
|
+
'info:',
|
|
161
|
+
' name: grpc-yaml-test',
|
|
162
|
+
' version: "1"',
|
|
163
|
+
'runtime:',
|
|
164
|
+
' scripts:',
|
|
165
|
+
' - type: grpc:before-call-start',
|
|
166
|
+
' code: |-',
|
|
167
|
+
' const token = bru.getEnvVar(\'token\');',
|
|
168
|
+
' request.setHeader(\'authorization\', token);',
|
|
169
|
+
' - type: grpc:after-call-end',
|
|
170
|
+
' code: |-',
|
|
171
|
+
' const data = response.data;',
|
|
172
|
+
' bru.setVar(\'userId\', data.id);',
|
|
173
|
+
' - type: grpc:before-message-send',
|
|
174
|
+
' code: |-',
|
|
175
|
+
' bru.setVar(\'outbound\', bru.grpc.request.message.data.greeting);',
|
|
176
|
+
' - type: grpc:after-message-receive',
|
|
177
|
+
' code: |-',
|
|
178
|
+
' const message = bru.grpc.response.message;',
|
|
179
|
+
' bru.setVar(\'inbound\', message.data.reply);'
|
|
180
|
+
].join('\n');
|
|
181
|
+
|
|
123
182
|
// Wrapper offsets: QuickJS = 9 (script line 1 = VM line 10), NodeVM = 2 (script line 1 = VM line 3)
|
|
124
183
|
|
|
125
184
|
describe('Error Formatter', () => {
|
|
@@ -128,6 +187,8 @@ describe('Error Formatter', () => {
|
|
|
128
187
|
let ymlFilePath;
|
|
129
188
|
let bruWithCommentsPath;
|
|
130
189
|
let collectionYmlPath;
|
|
190
|
+
let grpcBruPath;
|
|
191
|
+
let grpcYmlPath;
|
|
131
192
|
|
|
132
193
|
beforeEach(() => {
|
|
133
194
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));
|
|
@@ -135,10 +196,14 @@ describe('Error Formatter', () => {
|
|
|
135
196
|
ymlFilePath = path.join(testDir, 'test.yml');
|
|
136
197
|
bruWithCommentsPath = path.join(testDir, 'comments.bru');
|
|
137
198
|
collectionYmlPath = path.join(testDir, 'opencollection.yml');
|
|
199
|
+
grpcBruPath = path.join(testDir, 'grpc.bru');
|
|
200
|
+
grpcYmlPath = path.join(testDir, 'grpc.yml');
|
|
138
201
|
fs.writeFileSync(bruFilePath, MULTI_BLOCK_BRU);
|
|
139
202
|
fs.writeFileSync(ymlFilePath, MULTI_BLOCK_YML);
|
|
140
203
|
fs.writeFileSync(bruWithCommentsPath, BRU_WITH_COMMENTS);
|
|
141
204
|
fs.writeFileSync(collectionYmlPath, COLLECTION_YML);
|
|
205
|
+
fs.writeFileSync(grpcBruPath, GRPC_BRU);
|
|
206
|
+
fs.writeFileSync(grpcYmlPath, GRPC_YML);
|
|
142
207
|
});
|
|
143
208
|
|
|
144
209
|
afterEach(() => {
|
|
@@ -152,6 +217,13 @@ describe('Error Formatter', () => {
|
|
|
152
217
|
expect(findScriptBlockStartLine(bruFilePath, 'test')).toBe(25);
|
|
153
218
|
});
|
|
154
219
|
|
|
220
|
+
it('should find gRPC lifecycle hook blocks in .bru files', () => {
|
|
221
|
+
expect(findScriptBlockStartLine(grpcBruPath, 'before-call-start')).toBe(12);
|
|
222
|
+
expect(findScriptBlockStartLine(grpcBruPath, 'after-call-end')).toBe(17);
|
|
223
|
+
expect(findScriptBlockStartLine(grpcBruPath, 'before-message-send')).toBe(23);
|
|
224
|
+
expect(findScriptBlockStartLine(grpcBruPath, 'after-message-receive')).toBe(27);
|
|
225
|
+
});
|
|
226
|
+
|
|
155
227
|
it('should return null for missing block or non-.bru files', () => {
|
|
156
228
|
const noBlockPath = path.join(testDir, 'no-block.bru');
|
|
157
229
|
fs.writeFileSync(noBlockPath, 'meta {\n name: test\n}');
|
|
@@ -167,6 +239,13 @@ describe('Error Formatter', () => {
|
|
|
167
239
|
expect(findScriptBlockEndLine(bruFilePath, 'test')).toBe(30);
|
|
168
240
|
});
|
|
169
241
|
|
|
242
|
+
it('should find last content line for gRPC lifecycle hook blocks', () => {
|
|
243
|
+
expect(findScriptBlockEndLine(grpcBruPath, 'before-call-start')).toBe(13);
|
|
244
|
+
expect(findScriptBlockEndLine(grpcBruPath, 'after-call-end')).toBe(19);
|
|
245
|
+
expect(findScriptBlockEndLine(grpcBruPath, 'before-message-send')).toBe(23);
|
|
246
|
+
expect(findScriptBlockEndLine(grpcBruPath, 'after-message-receive')).toBe(28);
|
|
247
|
+
});
|
|
248
|
+
|
|
170
249
|
it('should return null for empty block', () => {
|
|
171
250
|
const emptyBlockPath = path.join(testDir, 'empty.bru');
|
|
172
251
|
fs.writeFileSync(emptyBlockPath, 'script:pre-request {\n}');
|
|
@@ -197,6 +276,13 @@ describe('Error Formatter', () => {
|
|
|
197
276
|
expect(findYmlScriptBlockStartLine(collectionYmlPath, 'test')).toBe(11);
|
|
198
277
|
});
|
|
199
278
|
|
|
279
|
+
it('should find gRPC lifecycle hook blocks in .yml files', () => {
|
|
280
|
+
expect(findYmlScriptBlockStartLine(grpcYmlPath, 'before-call-start')).toBe(8);
|
|
281
|
+
expect(findYmlScriptBlockStartLine(grpcYmlPath, 'after-call-end')).toBe(12);
|
|
282
|
+
expect(findYmlScriptBlockStartLine(grpcYmlPath, 'before-message-send')).toBe(16);
|
|
283
|
+
expect(findYmlScriptBlockStartLine(grpcYmlPath, 'after-message-receive')).toBe(19);
|
|
284
|
+
});
|
|
285
|
+
|
|
200
286
|
it('should return null for missing block or non-.yml files', () => {
|
|
201
287
|
const noRuntimePath = path.join(testDir, 'no-runtime.yml');
|
|
202
288
|
fs.writeFileSync(noRuntimePath, 'info:\n name: simple\n version: "1"\n');
|
|
@@ -216,6 +302,13 @@ describe('Error Formatter', () => {
|
|
|
216
302
|
expect(findYmlScriptBlockEndLine(collectionYmlPath, 'test')).toBe(13);
|
|
217
303
|
});
|
|
218
304
|
|
|
305
|
+
it('should find last content line for gRPC lifecycle hook blocks', () => {
|
|
306
|
+
expect(findYmlScriptBlockEndLine(grpcYmlPath, 'before-call-start')).toBe(9);
|
|
307
|
+
expect(findYmlScriptBlockEndLine(grpcYmlPath, 'after-call-end')).toBe(13);
|
|
308
|
+
expect(findYmlScriptBlockEndLine(grpcYmlPath, 'before-message-send')).toBe(16);
|
|
309
|
+
expect(findYmlScriptBlockEndLine(grpcYmlPath, 'after-message-receive')).toBe(20);
|
|
310
|
+
});
|
|
311
|
+
|
|
219
312
|
it('should return null for missing block', () => {
|
|
220
313
|
const noRuntimePath = path.join(testDir, 'no-runtime.yml');
|
|
221
314
|
fs.writeFileSync(noRuntimePath, 'info:\n name: simple\n version: "1"\n');
|
|
@@ -804,7 +897,9 @@ get {
|
|
|
804
897
|
const result = formatErrorWithContextV2(error, 'pre-request');
|
|
805
898
|
|
|
806
899
|
expect(result).not.toBeNull();
|
|
807
|
-
|
|
900
|
+
// display paths are posixified, so the Windows path.join backslashes
|
|
901
|
+
// must be normalized before comparing
|
|
902
|
+
expect(result.filePath).toBe(posixifyPath(bruFilePath));
|
|
808
903
|
});
|
|
809
904
|
});
|
|
810
905
|
|
package/src/utils/results.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const TestResults = require('../test-results');
|
|
2
2
|
const Test = require('../test');
|
|
3
3
|
|
|
4
|
-
// Calculate summary statistics for test results
|
|
5
4
|
const getResultsSummary = (results) => {
|
|
6
5
|
const summary = {
|
|
7
6
|
total: results.length,
|
|
@@ -20,12 +19,40 @@ const getResultsSummary = (results) => {
|
|
|
20
19
|
return summary;
|
|
21
20
|
};
|
|
22
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Called once per script phase - pre-request, post-response, and the Tests tab each call
|
|
24
|
+
* this separately, from their own place in runtime/script-runtime.js and
|
|
25
|
+
* runtime/test-runtime.js. Every call creates its own private `pendingTestPromises` array
|
|
26
|
+
* below, so each phase tracks and waits for only its own test() calls
|
|
27
|
+
*/
|
|
23
28
|
const createBruTestResultMethods = (bru, assertionResults, chai) => {
|
|
24
29
|
const __brunoTestResults = new TestResults();
|
|
25
|
-
const
|
|
30
|
+
const baseTest = Test(__brunoTestResults, chai);
|
|
31
|
+
|
|
32
|
+
const pendingTestPromises = [];
|
|
33
|
+
|
|
34
|
+
const test = (description, callback) => {
|
|
35
|
+
const promise = baseTest(description, callback);
|
|
36
|
+
pendingTestPromises.push(promise.catch(() => {}));
|
|
37
|
+
return promise;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Waits for every test() call registered so far to settle - including a test() called
|
|
42
|
+
* from inside another test()'s callback, after this wait has already started. Mirrors
|
|
43
|
+
* QuickJS's own waitForPendingDeferreds(). If a test() callback has its own delay (a
|
|
44
|
+
* setTimeout, a slow request), this simply waits until that delay is over
|
|
45
|
+
*/
|
|
46
|
+
const waitForPendingTests = async () => {
|
|
47
|
+
while (pendingTestPromises.length) {
|
|
48
|
+
const batch = pendingTestPromises.splice(0);
|
|
49
|
+
await Promise.all(batch);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
26
53
|
setupBruTestMethods(bru, __brunoTestResults, assertionResults);
|
|
27
54
|
|
|
28
|
-
return { __brunoTestResults, test };
|
|
55
|
+
return { __brunoTestResults, test, waitForPendingTests };
|
|
29
56
|
};
|
|
30
57
|
|
|
31
58
|
const setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => {
|