norn-cli 1.6.0 → 1.6.2
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/AGENTS.md +9 -1
- package/CHANGELOG.md +23 -0
- package/dist/cli.js +246 -80
- package/package.json +1 -1
- package/out/assertionRunner.js +0 -537
- package/out/chatParticipant.js +0 -722
- package/out/cli/colors.js +0 -129
- package/out/cli/formatters/assertion.js +0 -75
- package/out/cli/formatters/index.js +0 -23
- package/out/cli/formatters/response.js +0 -106
- package/out/cli/formatters/summary.js +0 -187
- package/out/cli/redaction.js +0 -237
- package/out/cli/reporters/html.js +0 -634
- package/out/cli/reporters/index.js +0 -22
- package/out/cli/reporters/junit.js +0 -211
- package/out/cli.js +0 -989
- package/out/codeLensProvider.js +0 -248
- package/out/compareContentProvider.js +0 -85
- package/out/completionProvider.js +0 -2404
- package/out/contractDecorationProvider.js +0 -243
- package/out/coverageCalculator.js +0 -837
- package/out/coveragePanel.js +0 -545
- package/out/diagnosticProvider.js +0 -1113
- package/out/environmentProvider.js +0 -442
- package/out/extension.js +0 -1114
- package/out/httpClient.js +0 -269
- package/out/jsonFileReader.js +0 -320
- package/out/nornPrompt.js +0 -580
- package/out/nornapiParser.js +0 -326
- package/out/parser.js +0 -725
- package/out/responsePanel.js +0 -4674
- package/out/schemaGenerator.js +0 -393
- package/out/scriptRunner.js +0 -419
- package/out/sequenceRunner.js +0 -3046
- package/out/swaggerBodyIntellisenseCache.js +0 -147
- package/out/swaggerParser.js +0 -419
- package/out/test/coverageCalculator.test.js +0 -100
- package/out/test/extension.test.js +0 -48
- package/out/testProvider.js +0 -658
- package/out/validationCache.js +0 -245
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Coverage Calculator for Swagger/OpenAPI specs
|
|
4
|
-
*
|
|
5
|
-
* Calculates what percentage of swagger-defined endpoints and their
|
|
6
|
-
* response codes are covered by test sequences.
|
|
7
|
-
*/
|
|
8
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
-
if (k2 === undefined) k2 = k;
|
|
10
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
-
}
|
|
14
|
-
Object.defineProperty(o, k2, desc);
|
|
15
|
-
}) : (function(o, m, k, k2) {
|
|
16
|
-
if (k2 === undefined) k2 = k;
|
|
17
|
-
o[k2] = m[k];
|
|
18
|
-
}));
|
|
19
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
-
}) : function(o, v) {
|
|
22
|
-
o["default"] = v;
|
|
23
|
-
});
|
|
24
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
-
var ownKeys = function(o) {
|
|
26
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
-
var ar = [];
|
|
28
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
-
return ar;
|
|
30
|
-
};
|
|
31
|
-
return ownKeys(o);
|
|
32
|
-
};
|
|
33
|
-
return function (mod) {
|
|
34
|
-
if (mod && mod.__esModule) return mod;
|
|
35
|
-
var result = {};
|
|
36
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
-
__setModuleDefault(result, mod);
|
|
38
|
-
return result;
|
|
39
|
-
};
|
|
40
|
-
})();
|
|
41
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.calculateWorkspaceCoverage = calculateWorkspaceCoverage;
|
|
43
|
-
exports.getCoverage = getCoverage;
|
|
44
|
-
exports.getCoverageForNornapiFile = getCoverageForNornapiFile;
|
|
45
|
-
exports.refreshCoverage = refreshCoverage;
|
|
46
|
-
exports.refreshCoverageForNornapiFile = refreshCoverageForNornapiFile;
|
|
47
|
-
exports.recalculateCoverageAfterExecution = recalculateCoverageAfterExecution;
|
|
48
|
-
exports.clearCoverageCache = clearCoverageCache;
|
|
49
|
-
exports.onCoverageUpdate = onCoverageUpdate;
|
|
50
|
-
const vscode = __importStar(require("vscode"));
|
|
51
|
-
const fs = __importStar(require("fs"));
|
|
52
|
-
const path = __importStar(require("path"));
|
|
53
|
-
const swaggerParser_1 = require("./swaggerParser");
|
|
54
|
-
const nornapiParser_1 = require("./nornapiParser");
|
|
55
|
-
/**
|
|
56
|
-
* Extract the path portion from a full URL and normalize it for swagger matching.
|
|
57
|
-
* e.g., "https://petstore.swagger.io/v2/store/inventory" -> "/store/inventory"
|
|
58
|
-
*
|
|
59
|
-
* We need to strip the base URL (which typically includes version like /v2) to match swagger paths.
|
|
60
|
-
*/
|
|
61
|
-
function extractSwaggerPath(fullUrl, swaggerBaseUrl) {
|
|
62
|
-
const normalizeWithBasePath = (rawPath) => {
|
|
63
|
-
let normalizedPath = rawPath || '/';
|
|
64
|
-
try {
|
|
65
|
-
normalizedPath = decodeURIComponent(normalizedPath);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// Keep original path if decoding fails
|
|
69
|
-
}
|
|
70
|
-
if (!normalizedPath.startsWith('/')) {
|
|
71
|
-
normalizedPath = `/${normalizedPath}`;
|
|
72
|
-
}
|
|
73
|
-
if (swaggerBaseUrl) {
|
|
74
|
-
try {
|
|
75
|
-
const baseUrl = new URL(swaggerBaseUrl);
|
|
76
|
-
const basePath = decodeURIComponent(baseUrl.pathname);
|
|
77
|
-
if (basePath && basePath !== '/' && normalizedPath.startsWith(basePath)) {
|
|
78
|
-
normalizedPath = normalizedPath.substring(basePath.length);
|
|
79
|
-
if (!normalizedPath.startsWith('/')) {
|
|
80
|
-
normalizedPath = '/' + normalizedPath;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// Ignore base URL parsing errors
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return normalizedPath || '/';
|
|
89
|
-
};
|
|
90
|
-
const normalizeTemplatePath = (rawPath) => {
|
|
91
|
-
let candidate = rawPath.trim();
|
|
92
|
-
if (!candidate.startsWith('/')) {
|
|
93
|
-
candidate = `/${candidate}`;
|
|
94
|
-
}
|
|
95
|
-
// Remove one or more leading template segments such as
|
|
96
|
-
// /{{baseUrl}}/{{version}}/pet/{petId} -> /pet/{petId}
|
|
97
|
-
candidate = candidate.replace(/^(?:\/\{\{[^}]+\}\})+/, '');
|
|
98
|
-
if (!candidate.startsWith('/')) {
|
|
99
|
-
candidate = `/${candidate}`;
|
|
100
|
-
}
|
|
101
|
-
return normalizeWithBasePath(candidate);
|
|
102
|
-
};
|
|
103
|
-
try {
|
|
104
|
-
const trimmed = fullUrl.trim();
|
|
105
|
-
// Handle template variables such as {{baseUrl}}/pet/{petId}
|
|
106
|
-
const variablePrefixMatch = trimmed.match(/^\{\{[^}]+\}\}(.*)$/);
|
|
107
|
-
if (variablePrefixMatch) {
|
|
108
|
-
const remainder = variablePrefixMatch[1] || '/';
|
|
109
|
-
return normalizeTemplatePath(remainder);
|
|
110
|
-
}
|
|
111
|
-
// Handle path-form template segments such as /{{version}}/pet/{petId}
|
|
112
|
-
if (/^\/(?:\{\{[^}]+\}\}\/)+/.test(trimmed) || /^\/\{\{[^}]+\}\}$/.test(trimmed)) {
|
|
113
|
-
return normalizeTemplatePath(trimmed);
|
|
114
|
-
}
|
|
115
|
-
// If it's already just a path (starts with /), return as-is
|
|
116
|
-
if (trimmed.startsWith('/')) {
|
|
117
|
-
return normalizeWithBasePath(trimmed);
|
|
118
|
-
}
|
|
119
|
-
const url = new URL(trimmed);
|
|
120
|
-
return normalizeWithBasePath(url.pathname);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
// If URL parsing fails, try to extract path manually
|
|
124
|
-
const match = fullUrl.match(/https?:\/\/[^\/]+(\/.*)/);
|
|
125
|
-
if (match) {
|
|
126
|
-
return normalizeWithBasePath(match[1]);
|
|
127
|
-
}
|
|
128
|
-
return normalizeWithBasePath(fullUrl);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Extract swagger URL from .nornapi file content
|
|
133
|
-
*/
|
|
134
|
-
function extractSwaggerUrl(content) {
|
|
135
|
-
const match = content.match(/^swagger\s+["']?(https?:\/\/[^\s"']+)["']?\s*$/m);
|
|
136
|
-
return match?.[1];
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Parse assert status lines from sequence content.
|
|
140
|
-
* Returns array of status codes/patterns (e.g., ["200", "2xx", "404"])
|
|
141
|
-
*/
|
|
142
|
-
function extractAssertedStatusCodes(sequenceContent) {
|
|
143
|
-
const codes = [];
|
|
144
|
-
const lines = sequenceContent.split('\n');
|
|
145
|
-
for (const line of lines) {
|
|
146
|
-
const code = extractStatusCodeFromAssert(line);
|
|
147
|
-
if (code) {
|
|
148
|
-
codes.push(code);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return codes;
|
|
152
|
-
}
|
|
153
|
-
function extractStatusCodeFromAssert(line) {
|
|
154
|
-
const trimmed = line.trim();
|
|
155
|
-
// Match various assert status patterns:
|
|
156
|
-
// - assert status == 200
|
|
157
|
-
// - assert $1.status == 200
|
|
158
|
-
// - assert response.status == 200
|
|
159
|
-
// - assert inventory.status == 200
|
|
160
|
-
// - assert variableName.status >= 400
|
|
161
|
-
const statusMatch = trimmed.match(/^assert\s+(?:(?:\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)\.)?status\s*(?:==|>=|<=|>|<|!=)?\s*(\d{3}|[1-5]xx)$/i);
|
|
162
|
-
if (statusMatch) {
|
|
163
|
-
return statusMatch[1].toLowerCase();
|
|
164
|
-
}
|
|
165
|
-
// Also match simpler patterns like: assert status 200
|
|
166
|
-
const simpleMatch = trimmed.match(/^assert\s+status\s+(\d{3}|[1-5]xx)$/i);
|
|
167
|
-
if (simpleMatch) {
|
|
168
|
-
return simpleMatch[1].toLowerCase();
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
function isCoverageScanBoundary(line) {
|
|
173
|
-
return /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(line) ||
|
|
174
|
-
/^end\s+sequence$/i.test(line) ||
|
|
175
|
-
/^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+/i.test(line) ||
|
|
176
|
-
/^if\s+/i.test(line) ||
|
|
177
|
-
/^foreach\s+/i.test(line);
|
|
178
|
-
}
|
|
179
|
-
function collectStatusAssertionsFollowingLine(lines, startIndex) {
|
|
180
|
-
const assertedCodes = [];
|
|
181
|
-
for (let j = startIndex + 1; j < lines.length; j++) {
|
|
182
|
-
const nextLine = lines[j].trim();
|
|
183
|
-
// Stop at next request, sequence end, or other control flow
|
|
184
|
-
if (isCoverageScanBoundary(nextLine)) {
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
const code = extractStatusCodeFromAssert(nextLine);
|
|
188
|
-
if (code) {
|
|
189
|
-
assertedCodes.push(code);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return assertedCodes;
|
|
193
|
-
}
|
|
194
|
-
function extractReturnedVariable(sequenceContent) {
|
|
195
|
-
let returnedVariable;
|
|
196
|
-
for (const line of sequenceContent.split('\n')) {
|
|
197
|
-
const trimmed = line.trim();
|
|
198
|
-
const returnMatch = trimmed.match(/^return\s+([a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
|
199
|
-
if (returnMatch) {
|
|
200
|
-
returnedVariable = returnMatch[1];
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return returnedVariable;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Check if a wildcard pattern matches a status code
|
|
207
|
-
* e.g., "2xx" matches "200", "201", "204"
|
|
208
|
-
*/
|
|
209
|
-
function wildcardMatches(pattern, code) {
|
|
210
|
-
const lowerPattern = pattern.toLowerCase();
|
|
211
|
-
// Exact match
|
|
212
|
-
if (lowerPattern === code) {
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
// Wildcard match (2xx, 4xx, 5xx)
|
|
216
|
-
if (/^[1-5]xx$/.test(lowerPattern)) {
|
|
217
|
-
const patternPrefix = lowerPattern[0];
|
|
218
|
-
return code.startsWith(patternPrefix);
|
|
219
|
-
}
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Check if a concrete URL path matches a swagger endpoint path template
|
|
224
|
-
* e.g., "/pet/1" matches "/pet/{petId}"
|
|
225
|
-
* "/v2/pet/123" matches "/pet/{petId}" (with base path /v2)
|
|
226
|
-
*/
|
|
227
|
-
function pathMatchesSwaggerEndpoint(urlPath, swaggerPath) {
|
|
228
|
-
// Normalize paths - remove leading/trailing slashes for comparison
|
|
229
|
-
const normalizedUrl = urlPath.replace(/^\/+|\/+$/g, '');
|
|
230
|
-
const normalizedSwagger = swaggerPath.replace(/^\/+|\/+$/g, '');
|
|
231
|
-
const urlParts = normalizedUrl.split('/');
|
|
232
|
-
const swaggerParts = normalizedSwagger.split('/');
|
|
233
|
-
// Try matching from different starting positions to handle base paths
|
|
234
|
-
// e.g., URL /v2/pet/1 should match swagger /pet/{petId} if base is /v2
|
|
235
|
-
for (let offset = 0; offset <= urlParts.length - swaggerParts.length; offset++) {
|
|
236
|
-
let matches = true;
|
|
237
|
-
for (let i = 0; i < swaggerParts.length; i++) {
|
|
238
|
-
const urlPart = urlParts[offset + i];
|
|
239
|
-
const swaggerPart = swaggerParts[i];
|
|
240
|
-
// Path parameter in swagger (e.g., {petId}) matches any value
|
|
241
|
-
if (swaggerPart.startsWith('{') && swaggerPart.endsWith('}')) {
|
|
242
|
-
// Path parameter matches any non-empty value
|
|
243
|
-
if (!urlPart || urlPart.length === 0) {
|
|
244
|
-
matches = false;
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (urlPart !== swaggerPart) {
|
|
249
|
-
// Static segments must match exactly
|
|
250
|
-
matches = false;
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (matches) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Extract API calls from sequence content.
|
|
262
|
-
* Returns array of { endpointName, assertedCodes, method?, url? }
|
|
263
|
-
*
|
|
264
|
-
* @param sequenceContent - The content of the sequence to analyze
|
|
265
|
-
* @param allSequences - Map of all sequences by name (for following run SequenceName calls)
|
|
266
|
-
* @param allNamedRequests - Map of named request blocks by name (for following run NamedRequest calls)
|
|
267
|
-
* @param visited - Set of already visited sequence names (to prevent infinite recursion)
|
|
268
|
-
*/
|
|
269
|
-
function extractApiCalls(sequenceContent, allSequences, allNamedRequests, visited) {
|
|
270
|
-
const calls = [];
|
|
271
|
-
const lines = sequenceContent.split('\n');
|
|
272
|
-
const visitedSet = visited || new Set();
|
|
273
|
-
for (let i = 0; i < lines.length; i++) {
|
|
274
|
-
const line = lines[i].trim();
|
|
275
|
-
// Check for "run Name" or "var x = run Name" calls
|
|
276
|
-
const runMatch = line.match(/^(?:var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*)?run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(.*?\))?$/i);
|
|
277
|
-
if (runMatch) {
|
|
278
|
-
const assignedVar = runMatch[1];
|
|
279
|
-
const targetName = runMatch[2];
|
|
280
|
-
const assertedCodesAfterRun = collectStatusAssertionsFollowingLine(lines, i);
|
|
281
|
-
// Avoid infinite recursion
|
|
282
|
-
if (!visitedSet.has(targetName)) {
|
|
283
|
-
visitedSet.add(targetName);
|
|
284
|
-
// First check if it's a sequence
|
|
285
|
-
if (allSequences) {
|
|
286
|
-
const subSequenceContent = allSequences.get(targetName);
|
|
287
|
-
if (subSequenceContent) {
|
|
288
|
-
// Recursively extract API calls from the sub-sequence
|
|
289
|
-
const subCalls = extractApiCalls(subSequenceContent, allSequences, allNamedRequests, visitedSet);
|
|
290
|
-
// If caller asserts status after "run Sequence", apply those assertions
|
|
291
|
-
// to the response returned by that sequence (fallback: last API call).
|
|
292
|
-
if (assertedCodesAfterRun.length > 0 && subCalls.length > 0) {
|
|
293
|
-
const returnedVar = extractReturnedVariable(subSequenceContent);
|
|
294
|
-
const returnedCall = returnedVar
|
|
295
|
-
? [...subCalls].reverse().find(call => call.assignedVar === returnedVar) || subCalls[subCalls.length - 1]
|
|
296
|
-
: subCalls[subCalls.length - 1];
|
|
297
|
-
returnedCall.assertedCodes.push(...assertedCodesAfterRun);
|
|
298
|
-
}
|
|
299
|
-
calls.push(...subCalls);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Then check if it's a named request block
|
|
304
|
-
if (allNamedRequests) {
|
|
305
|
-
const namedRequest = allNamedRequests.get(targetName);
|
|
306
|
-
if (namedRequest) {
|
|
307
|
-
// Add this as a URL-based API call (will be matched by URL later)
|
|
308
|
-
calls.push({
|
|
309
|
-
endpointName: `__url__${namedRequest.method}__${namedRequest.url}`,
|
|
310
|
-
assertedCodes: assertedCodesAfterRun,
|
|
311
|
-
method: namedRequest.method,
|
|
312
|
-
url: namedRequest.url,
|
|
313
|
-
assignedVar
|
|
314
|
-
});
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
// Match API request by name with optional variable assignment:
|
|
322
|
-
// - GET EndpointName
|
|
323
|
-
// - GET EndpointName(args)
|
|
324
|
-
// - GET EndpointName(args) HeaderGroup
|
|
325
|
-
// - var result = GET EndpointName
|
|
326
|
-
// - var inventory = GET GetInventory
|
|
327
|
-
// - var order = GET GetOrderById(1)
|
|
328
|
-
const apiCallMatch = line.match(/^(?:var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*\(.*?\))?(?:\s+[a-zA-Z_][a-zA-Z0-9_]*)?$/i);
|
|
329
|
-
if (apiCallMatch) {
|
|
330
|
-
const assignedVar = apiCallMatch[1];
|
|
331
|
-
const endpointName = apiCallMatch[3];
|
|
332
|
-
const assertedCodes = collectStatusAssertionsFollowingLine(lines, i);
|
|
333
|
-
calls.push({ endpointName, assertedCodes, assignedVar });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return calls;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Extract all test sequences from a .norn file content.
|
|
340
|
-
* Only "test sequence" blocks count towards coverage, not regular "sequence" blocks.
|
|
341
|
-
*/
|
|
342
|
-
function extractTestSequences(content) {
|
|
343
|
-
const sequences = [];
|
|
344
|
-
const lines = content.split('\n');
|
|
345
|
-
let currentSequence = null;
|
|
346
|
-
for (let i = 0; i < lines.length; i++) {
|
|
347
|
-
const line = lines[i].trim();
|
|
348
|
-
// Match only "test sequence Name" - regular sequences don't count towards coverage
|
|
349
|
-
const sequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
|
|
350
|
-
if (sequenceMatch) {
|
|
351
|
-
currentSequence = { name: sequenceMatch[1], startLine: i };
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
// Match sequence end
|
|
355
|
-
if (/^end\s+sequence$/i.test(line) && currentSequence) {
|
|
356
|
-
const sequenceContent = lines.slice(currentSequence.startLine, i + 1).join('\n');
|
|
357
|
-
sequences.push({ name: currentSequence.name, content: sequenceContent });
|
|
358
|
-
currentSequence = null;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
return sequences;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Extract ALL sequences from a .norn file content (both test and regular).
|
|
365
|
-
* Used to build a map for following "run SequenceName" calls.
|
|
366
|
-
*/
|
|
367
|
-
function extractAllSequences(content) {
|
|
368
|
-
const sequences = new Map();
|
|
369
|
-
const lines = content.split('\n');
|
|
370
|
-
let currentSequence = null;
|
|
371
|
-
for (let i = 0; i < lines.length; i++) {
|
|
372
|
-
const line = lines[i].trim();
|
|
373
|
-
// Match both "test sequence Name" and "sequence Name"
|
|
374
|
-
const testSequenceMatch = line.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
|
|
375
|
-
const sequenceMatch = line.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?$/);
|
|
376
|
-
if (testSequenceMatch || sequenceMatch) {
|
|
377
|
-
const name = testSequenceMatch ? testSequenceMatch[1] : sequenceMatch[1];
|
|
378
|
-
currentSequence = { name, startLine: i };
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
// Match sequence end
|
|
382
|
-
if (/^end\s+sequence$/i.test(line) && currentSequence) {
|
|
383
|
-
const sequenceContent = lines.slice(currentSequence.startLine + 1, i).join('\n');
|
|
384
|
-
sequences.set(currentSequence.name, sequenceContent);
|
|
385
|
-
currentSequence = null;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return sequences;
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Extract named request blocks from a .norn file content.
|
|
392
|
-
* Named requests are blocks like [RequestName] followed by HTTP method + URL.
|
|
393
|
-
*/
|
|
394
|
-
function extractNamedRequests(content) {
|
|
395
|
-
const requests = new Map();
|
|
396
|
-
const lines = content.split('\n');
|
|
397
|
-
let currentRequest = null;
|
|
398
|
-
for (let i = 0; i < lines.length; i++) {
|
|
399
|
-
const line = lines[i].trim();
|
|
400
|
-
// Match named request start: [RequestName]
|
|
401
|
-
const nameMatch = line.match(/^\[([a-zA-Z_][a-zA-Z0-9_-]*)\]$/);
|
|
402
|
-
if (nameMatch) {
|
|
403
|
-
// Save previous request if exists
|
|
404
|
-
if (currentRequest) {
|
|
405
|
-
const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
|
|
406
|
-
const parsed = parseRequestContent(requestContent);
|
|
407
|
-
if (parsed) {
|
|
408
|
-
requests.set(currentRequest.name, { ...parsed, content: requestContent });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
currentRequest = { name: nameMatch[1], startLine: i };
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
// Check for sequence start or end - terminates named request block
|
|
415
|
-
if (/^(?:test\s+)?sequence\s+/i.test(line) || /^end\s+sequence$/i.test(line)) {
|
|
416
|
-
if (currentRequest) {
|
|
417
|
-
const requestContent = lines.slice(currentRequest.startLine + 1, i).join('\n').trim();
|
|
418
|
-
const parsed = parseRequestContent(requestContent);
|
|
419
|
-
if (parsed) {
|
|
420
|
-
requests.set(currentRequest.name, { ...parsed, content: requestContent });
|
|
421
|
-
}
|
|
422
|
-
currentRequest = null;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
// Don't forget the last request
|
|
427
|
-
if (currentRequest) {
|
|
428
|
-
const requestContent = lines.slice(currentRequest.startLine + 1).join('\n').trim();
|
|
429
|
-
const parsed = parseRequestContent(requestContent);
|
|
430
|
-
if (parsed) {
|
|
431
|
-
requests.set(currentRequest.name, { ...parsed, content: requestContent });
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return requests;
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Parse the content of a named request block to extract method and URL.
|
|
438
|
-
*/
|
|
439
|
-
function parseRequestContent(content) {
|
|
440
|
-
const lines = content.split('\n');
|
|
441
|
-
for (const line of lines) {
|
|
442
|
-
const trimmed = line.trim();
|
|
443
|
-
// Match: GET "url" or GET url or POST "url" etc.
|
|
444
|
-
const match = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+["']?([^"'\s]+)["']?/i);
|
|
445
|
-
if (match) {
|
|
446
|
-
return { method: match[1].toUpperCase(), url: match[2] };
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
function isPathAtOrBelowRoot(filePath, rootPath) {
|
|
452
|
-
const relative = path.relative(rootPath, filePath);
|
|
453
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
454
|
-
}
|
|
455
|
-
function buildScopedCoverage(nornFiles) {
|
|
456
|
-
const byName = new Map();
|
|
457
|
-
const byUrl = new Map();
|
|
458
|
-
const allSequencesMap = new Map();
|
|
459
|
-
const allNamedRequestsMap = new Map();
|
|
460
|
-
for (const file of nornFiles) {
|
|
461
|
-
try {
|
|
462
|
-
const content = fs.readFileSync(file.fsPath, 'utf-8');
|
|
463
|
-
const fileSequences = extractAllSequences(content);
|
|
464
|
-
for (const [name, seqContent] of fileSequences) {
|
|
465
|
-
allSequencesMap.set(name, seqContent);
|
|
466
|
-
}
|
|
467
|
-
const fileNamedRequests = extractNamedRequests(content);
|
|
468
|
-
for (const [name, reqInfo] of fileNamedRequests) {
|
|
469
|
-
allNamedRequestsMap.set(name, reqInfo);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
console.error(`Error reading .norn file ${file.fsPath}:`, error);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
for (const file of nornFiles) {
|
|
477
|
-
try {
|
|
478
|
-
const content = fs.readFileSync(file.fsPath, 'utf-8');
|
|
479
|
-
const testSequences = extractTestSequences(content);
|
|
480
|
-
for (const sequence of testSequences) {
|
|
481
|
-
const apiCalls = extractApiCalls(sequence.content, allSequencesMap, allNamedRequestsMap);
|
|
482
|
-
for (const call of apiCalls) {
|
|
483
|
-
if (call.url && call.method) {
|
|
484
|
-
const key = `${call.method} ${call.url}`;
|
|
485
|
-
if (!byUrl.has(key)) {
|
|
486
|
-
byUrl.set(key, { method: call.method, url: call.url, assertedCodes: new Set() });
|
|
487
|
-
}
|
|
488
|
-
for (const code of call.assertedCodes) {
|
|
489
|
-
byUrl.get(key).assertedCodes.add(code);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
if (!byName.has(call.endpointName)) {
|
|
494
|
-
byName.set(call.endpointName, new Set());
|
|
495
|
-
}
|
|
496
|
-
for (const code of call.assertedCodes) {
|
|
497
|
-
byName.get(call.endpointName).add(code);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
console.error(`Error reading .norn file ${file.fsPath}:`, error);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return { byName, byUrl };
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Calculate coverage for the entire workspace
|
|
511
|
-
* @param useCachedSpecs If true, only fetch swagger specs for new URLs (used after execution)
|
|
512
|
-
*/
|
|
513
|
-
async function calculateWorkspaceCoverage(useCachedSpecs = false, scopedNornapiFilePath) {
|
|
514
|
-
const workspaceFolders = vscode.workspace.workspaceFolders;
|
|
515
|
-
if (!workspaceFolders) {
|
|
516
|
-
return {
|
|
517
|
-
total: 0,
|
|
518
|
-
covered: 0,
|
|
519
|
-
percentage: 0,
|
|
520
|
-
specs: [],
|
|
521
|
-
hasSwagger: false
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
// Step 1: Find all .nornapi files and extract swagger URLs + endpoint mappings
|
|
525
|
-
const allNornapiFiles = await vscode.workspace.findFiles('**/*.nornapi', '**/node_modules/**');
|
|
526
|
-
const normalizedScopedPath = scopedNornapiFilePath ? path.resolve(scopedNornapiFilePath) : undefined;
|
|
527
|
-
const nornapiFiles = normalizedScopedPath
|
|
528
|
-
? allNornapiFiles.filter(file => path.resolve(file.fsPath) === normalizedScopedPath)
|
|
529
|
-
: allNornapiFiles;
|
|
530
|
-
const swaggerUrls = new Map(); // swagger URL -> list of nornapi files
|
|
531
|
-
// Store endpoint info temporarily - we'll create mappings after fetching swagger specs
|
|
532
|
-
const pendingEndpoints = [];
|
|
533
|
-
for (const file of nornapiFiles) {
|
|
534
|
-
try {
|
|
535
|
-
const content = fs.readFileSync(file.fsPath, 'utf-8');
|
|
536
|
-
const swaggerUrl = extractSwaggerUrl(content);
|
|
537
|
-
if (swaggerUrl) {
|
|
538
|
-
// Track this nornapi file for this swagger URL
|
|
539
|
-
if (!swaggerUrls.has(swaggerUrl)) {
|
|
540
|
-
swaggerUrls.set(swaggerUrl, []);
|
|
541
|
-
}
|
|
542
|
-
swaggerUrls.get(swaggerUrl).push({ url: swaggerUrl, filePath: file.fsPath });
|
|
543
|
-
// Parse endpoints from this nornapi file
|
|
544
|
-
const nornApiDef = (0, nornapiParser_1.parseNornApiFile)(content);
|
|
545
|
-
for (const endpoint of nornApiDef.endpoints) {
|
|
546
|
-
// Store for later processing after we have swagger specs
|
|
547
|
-
pendingEndpoints.push({
|
|
548
|
-
endpoint: { name: endpoint.name, method: endpoint.method, path: endpoint.path },
|
|
549
|
-
swaggerUrl,
|
|
550
|
-
filePath: file.fsPath
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
catch (error) {
|
|
556
|
-
console.error(`Error reading .nornapi file ${file.fsPath}:`, error);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
if (swaggerUrls.size === 0) {
|
|
560
|
-
return {
|
|
561
|
-
total: 0,
|
|
562
|
-
covered: 0,
|
|
563
|
-
percentage: 0,
|
|
564
|
-
specs: [],
|
|
565
|
-
hasSwagger: false
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
// Detect new swagger URLs (not seen before)
|
|
569
|
-
const currentUrls = new Set(swaggerUrls.keys());
|
|
570
|
-
const newUrls = new Set();
|
|
571
|
-
for (const url of currentUrls) {
|
|
572
|
-
if (!knownSwaggerUrls.has(url)) {
|
|
573
|
-
newUrls.add(url);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
// Update known URLs
|
|
577
|
-
knownSwaggerUrls = currentUrls;
|
|
578
|
-
// Step 2: Find all .norn files and extract API calls with assert status
|
|
579
|
-
const nornFiles = await vscode.workspace.findFiles('**/*.norn', '**/node_modules/**');
|
|
580
|
-
const scopedCoverageByRoot = new Map();
|
|
581
|
-
const uniqueNornapiRoots = Array.from(new Set(nornapiFiles.map(file => path.dirname(file.fsPath))));
|
|
582
|
-
for (const root of uniqueNornapiRoots) {
|
|
583
|
-
const scopedFiles = nornFiles.filter(file => isPathAtOrBelowRoot(file.fsPath, root));
|
|
584
|
-
scopedCoverageByRoot.set(root, buildScopedCoverage(scopedFiles));
|
|
585
|
-
}
|
|
586
|
-
// Step 3: For each swagger spec, calculate coverage
|
|
587
|
-
// First fetch all specs so we have baseUrl for path normalization
|
|
588
|
-
const swaggerSpecs = new Map();
|
|
589
|
-
for (const [swaggerUrl] of swaggerUrls) {
|
|
590
|
-
try {
|
|
591
|
-
const swaggerSpec = await (0, swaggerParser_1.getCachedSwaggerSpec)(swaggerUrl);
|
|
592
|
-
swaggerSpecs.set(swaggerUrl, swaggerSpec);
|
|
593
|
-
}
|
|
594
|
-
catch (error) {
|
|
595
|
-
console.error(`Error fetching swagger spec ${swaggerUrl}:`, error);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
// Now create endpoint mappings with proper path normalization
|
|
599
|
-
const endpointMappings = [];
|
|
600
|
-
for (const pending of pendingEndpoints) {
|
|
601
|
-
const spec = swaggerSpecs.get(pending.swaggerUrl);
|
|
602
|
-
if (spec) {
|
|
603
|
-
// Extract the swagger-style path from the full URL
|
|
604
|
-
const swaggerPath = extractSwaggerPath(pending.endpoint.path, spec.baseUrl);
|
|
605
|
-
const swaggerKey = `${pending.endpoint.method} ${swaggerPath}`;
|
|
606
|
-
endpointMappings.push({
|
|
607
|
-
name: pending.endpoint.name,
|
|
608
|
-
swaggerKey,
|
|
609
|
-
swaggerUrl: pending.swaggerUrl,
|
|
610
|
-
filePath: pending.filePath
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const specs = [];
|
|
615
|
-
for (const [swaggerUrl, swaggerSpec] of swaggerSpecs) {
|
|
616
|
-
const coverageResult = calculateCoverageForSpec(swaggerUrl, swaggerSpec, endpointMappings.filter(m => m.swaggerUrl === swaggerUrl), scopedCoverageByRoot);
|
|
617
|
-
specs.push(coverageResult);
|
|
618
|
-
}
|
|
619
|
-
// Step 4: Aggregate results
|
|
620
|
-
let totalCodes = 0;
|
|
621
|
-
let coveredCodes = 0;
|
|
622
|
-
for (const spec of specs) {
|
|
623
|
-
totalCodes += spec.total;
|
|
624
|
-
coveredCodes += spec.covered;
|
|
625
|
-
}
|
|
626
|
-
return {
|
|
627
|
-
total: totalCodes,
|
|
628
|
-
covered: coveredCodes,
|
|
629
|
-
percentage: totalCodes > 0 ? Math.round((coveredCodes / totalCodes) * 100) : 0,
|
|
630
|
-
specs,
|
|
631
|
-
hasSwagger: true
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Calculate coverage for a single swagger spec
|
|
636
|
-
*/
|
|
637
|
-
function calculateCoverageForSpec(swaggerUrl, spec, endpointMappings, scopedCoverageByRoot) {
|
|
638
|
-
const endpoints = [];
|
|
639
|
-
let total = 0;
|
|
640
|
-
let covered = 0;
|
|
641
|
-
// Build a lookup from swagger key to mapped .nornapi endpoints (with source file context)
|
|
642
|
-
const keyToMappings = new Map();
|
|
643
|
-
for (const mapping of endpointMappings) {
|
|
644
|
-
if (!keyToMappings.has(mapping.swaggerKey)) {
|
|
645
|
-
keyToMappings.set(mapping.swaggerKey, []);
|
|
646
|
-
}
|
|
647
|
-
keyToMappings.get(mapping.swaggerKey).push(mapping);
|
|
648
|
-
}
|
|
649
|
-
// Process each endpoint in the swagger spec
|
|
650
|
-
for (const section of spec.sections) {
|
|
651
|
-
for (const endpoint of section.endpoints) {
|
|
652
|
-
const swaggerKey = `${endpoint.method} ${endpoint.path}`;
|
|
653
|
-
const mappingsForEndpoint = keyToMappings.get(swaggerKey) || [];
|
|
654
|
-
// Get all asserted codes for any name that maps to this endpoint
|
|
655
|
-
const allAssertedCodes = new Set();
|
|
656
|
-
const coveredByNames = {}; // code -> names that cover it
|
|
657
|
-
const endpointRootPaths = new Set();
|
|
658
|
-
for (const mapping of mappingsForEndpoint) {
|
|
659
|
-
const rootPath = path.dirname(mapping.filePath);
|
|
660
|
-
endpointRootPaths.add(rootPath);
|
|
661
|
-
const scopedCoverage = scopedCoverageByRoot.get(rootPath);
|
|
662
|
-
const codes = scopedCoverage?.byName.get(mapping.name);
|
|
663
|
-
if (codes) {
|
|
664
|
-
for (const code of codes) {
|
|
665
|
-
allAssertedCodes.add(code);
|
|
666
|
-
// Track which names cover which codes
|
|
667
|
-
for (const responseCode of endpoint.responseCodes) {
|
|
668
|
-
if (wildcardMatches(code, responseCode)) {
|
|
669
|
-
if (!coveredByNames[responseCode]) {
|
|
670
|
-
coveredByNames[responseCode] = [];
|
|
671
|
-
}
|
|
672
|
-
if (!coveredByNames[responseCode].includes(mapping.name)) {
|
|
673
|
-
coveredByNames[responseCode].push(mapping.name);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
// Also check URL-based coverage (from named request blocks)
|
|
681
|
-
// Match URLs to swagger endpoints by comparing the path portion
|
|
682
|
-
for (const rootPath of endpointRootPaths) {
|
|
683
|
-
const scopedCoverage = scopedCoverageByRoot.get(rootPath);
|
|
684
|
-
if (!scopedCoverage) {
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
for (const [, urlCoverage] of scopedCoverage.byUrl) {
|
|
688
|
-
// Check if method matches
|
|
689
|
-
if (urlCoverage.method.toUpperCase() !== endpoint.method.toUpperCase()) {
|
|
690
|
-
continue;
|
|
691
|
-
}
|
|
692
|
-
// Try to match URL to swagger endpoint path
|
|
693
|
-
// Extract the path from the full URL
|
|
694
|
-
try {
|
|
695
|
-
const url = new URL(urlCoverage.url);
|
|
696
|
-
const urlPath = url.pathname;
|
|
697
|
-
// Check if URL path matches the swagger endpoint path
|
|
698
|
-
// Handle path parameters like /pet/{petId} matching /pet/1
|
|
699
|
-
if (pathMatchesSwaggerEndpoint(urlPath, endpoint.path)) {
|
|
700
|
-
for (const code of urlCoverage.assertedCodes) {
|
|
701
|
-
allAssertedCodes.add(code);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
catch {
|
|
706
|
-
// Invalid URL, skip
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
// Calculate coverage for each response code
|
|
711
|
-
const responseCodes = [];
|
|
712
|
-
let endpointCovered = 0;
|
|
713
|
-
for (const code of endpoint.responseCodes) {
|
|
714
|
-
let isCovered = false;
|
|
715
|
-
const coveredBy = coveredByNames[code] || [];
|
|
716
|
-
// Check if any asserted code matches this response code
|
|
717
|
-
for (const assertedCode of allAssertedCodes) {
|
|
718
|
-
if (wildcardMatches(assertedCode, code)) {
|
|
719
|
-
isCovered = true;
|
|
720
|
-
break;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
responseCodes.push({
|
|
724
|
-
code,
|
|
725
|
-
covered: isCovered,
|
|
726
|
-
coveredBy
|
|
727
|
-
});
|
|
728
|
-
total++;
|
|
729
|
-
if (isCovered) {
|
|
730
|
-
covered++;
|
|
731
|
-
endpointCovered++;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
endpoints.push({
|
|
735
|
-
method: endpoint.method,
|
|
736
|
-
path: endpoint.path,
|
|
737
|
-
tag: endpoint.tag,
|
|
738
|
-
summary: endpoint.summary,
|
|
739
|
-
responseCodes,
|
|
740
|
-
totalCodes: endpoint.responseCodes.length,
|
|
741
|
-
coveredCodes: endpointCovered
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
// Sort endpoints by tag, then by path
|
|
746
|
-
endpoints.sort((a, b) => {
|
|
747
|
-
if (a.tag !== b.tag) {
|
|
748
|
-
return a.tag.localeCompare(b.tag);
|
|
749
|
-
}
|
|
750
|
-
return a.path.localeCompare(b.path);
|
|
751
|
-
});
|
|
752
|
-
return {
|
|
753
|
-
total,
|
|
754
|
-
covered,
|
|
755
|
-
percentage: total > 0 ? Math.round((covered / total) * 100) : 0,
|
|
756
|
-
endpoints,
|
|
757
|
-
swaggerUrl,
|
|
758
|
-
swaggerTitle: spec.title,
|
|
759
|
-
lastUpdated: new Date()
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
// Cached coverage result for status bar updates
|
|
763
|
-
let cachedCoverage = null;
|
|
764
|
-
let coverageUpdateListeners = [];
|
|
765
|
-
// Track known swagger URLs to detect new ones
|
|
766
|
-
let knownSwaggerUrls = new Set();
|
|
767
|
-
/**
|
|
768
|
-
* Get cached coverage or calculate if not available
|
|
769
|
-
*/
|
|
770
|
-
async function getCoverage() {
|
|
771
|
-
if (!cachedCoverage) {
|
|
772
|
-
cachedCoverage = await calculateWorkspaceCoverage();
|
|
773
|
-
notifyCoverageListeners();
|
|
774
|
-
}
|
|
775
|
-
return cachedCoverage;
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Calculate coverage scoped to a specific .nornapi file.
|
|
779
|
-
* Includes tests from that file's directory and descendants only.
|
|
780
|
-
*/
|
|
781
|
-
async function getCoverageForNornapiFile(nornapiFilePath) {
|
|
782
|
-
return calculateWorkspaceCoverage(false, nornapiFilePath);
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Force recalculate coverage - re-fetches swagger specs
|
|
786
|
-
*/
|
|
787
|
-
async function refreshCoverage() {
|
|
788
|
-
cachedCoverage = await calculateWorkspaceCoverage();
|
|
789
|
-
notifyCoverageListeners();
|
|
790
|
-
return cachedCoverage;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Refresh coverage scoped to a specific .nornapi file.
|
|
794
|
-
*/
|
|
795
|
-
async function refreshCoverageForNornapiFile(nornapiFilePath) {
|
|
796
|
-
return calculateWorkspaceCoverage(false, nornapiFilePath);
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* Recalculate coverage after execution - only re-scans .norn files,
|
|
800
|
-
* uses cached swagger specs unless new URLs are detected
|
|
801
|
-
*/
|
|
802
|
-
async function recalculateCoverageAfterExecution() {
|
|
803
|
-
cachedCoverage = await calculateWorkspaceCoverage(true);
|
|
804
|
-
notifyCoverageListeners();
|
|
805
|
-
return cachedCoverage;
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Clear cached coverage
|
|
809
|
-
*/
|
|
810
|
-
function clearCoverageCache() {
|
|
811
|
-
cachedCoverage = null;
|
|
812
|
-
}
|
|
813
|
-
/**
|
|
814
|
-
* Register a listener for coverage updates
|
|
815
|
-
*/
|
|
816
|
-
function onCoverageUpdate(listener) {
|
|
817
|
-
coverageUpdateListeners.push(listener);
|
|
818
|
-
return {
|
|
819
|
-
dispose: () => {
|
|
820
|
-
const index = coverageUpdateListeners.indexOf(listener);
|
|
821
|
-
if (index !== -1) {
|
|
822
|
-
coverageUpdateListeners.splice(index, 1);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
};
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Notify all listeners of coverage update
|
|
829
|
-
*/
|
|
830
|
-
function notifyCoverageListeners() {
|
|
831
|
-
if (cachedCoverage) {
|
|
832
|
-
for (const listener of coverageUpdateListeners) {
|
|
833
|
-
listener(cachedCoverage);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
//# sourceMappingURL=coverageCalculator.js.map
|