UNPKG

8.21 kBJavaScriptView Raw
1'use strict';
2
3const DeepEqual = require('@hapi/hoek/lib/deepEqual');
4const Pinpoint = require('@hapi/pinpoint');
5
6const Errors = require('./errors');
7
8
9const internals = {
10 codes: {
11 error: 1,
12 pass: 2,
13 full: 3
14 },
15 labels: {
16 0: 'never used',
17 1: 'always error',
18 2: 'always pass'
19 }
20};
21
22
23exports.setup = function (root) {
24
25 const trace = function () {
26
27 root._tracer = root._tracer || new internals.Tracer();
28 return root._tracer;
29 };
30
31 root.trace = trace;
32 root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;
33
34 root.untrace = () => {
35
36 root._tracer = null;
37 };
38};
39
40
41exports.location = function (schema) {
42
43 return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller
44};
45
46
47internals.Tracer = class {
48
49 constructor() {
50
51 this.name = 'Joi';
52 this._schemas = new Map();
53 }
54
55 _register(schema) {
56
57 const existing = this._schemas.get(schema);
58 if (existing) {
59 return existing.store;
60 }
61
62 const store = new internals.Store(schema);
63 const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
64 this._schemas.set(schema, { filename, line, store });
65 return store;
66 }
67
68 _combine(merged, sources) {
69
70 for (const { store } of this._schemas.values()) {
71 store._combine(merged, sources);
72 }
73 }
74
75 report(file) {
76
77 const coverage = [];
78
79 // Process each registered schema
80
81 for (const { filename, line, store } of this._schemas.values()) {
82 if (file &&
83 file !== filename) {
84
85 continue;
86 }
87
88 // Process sub schemas of the registered root
89
90 const missing = [];
91 const skipped = [];
92
93 for (const [schema, log] of store._sources.entries()) {
94
95 // Check if sub schema parent skipped
96
97 if (internals.sub(log.paths, skipped)) {
98 continue;
99 }
100
101 // Check if sub schema reached
102
103 if (!log.entry) {
104 missing.push({
105 status: 'never reached',
106 paths: [...log.paths]
107 });
108
109 skipped.push(...log.paths);
110 continue;
111 }
112
113 // Check values
114
115 for (const type of ['valid', 'invalid']) {
116 const set = schema[`_${type}s`];
117 if (!set) {
118 continue;
119 }
120
121 const values = new Set(set._values);
122 const refs = new Set(set._refs);
123 for (const { value, ref } of log[type]) {
124 values.delete(value);
125 refs.delete(ref);
126 }
127
128 if (values.size ||
129 refs.size) {
130
131 missing.push({
132 status: [...values, ...[...refs].map((ref) => ref.display)],
133 rule: `${type}s`
134 });
135 }
136 }
137
138 // Check rules status
139
140 const rules = schema._rules.map((rule) => rule.name);
141 for (const type of ['default', 'failover']) {
142 if (schema._flags[type] !== undefined) {
143 rules.push(type);
144 }
145 }
146
147 for (const name of rules) {
148 const status = internals.labels[log.rule[name] || 0];
149 if (status) {
150 const report = { rule: name, status };
151 if (log.paths.size) {
152 report.paths = [...log.paths];
153 }
154
155 missing.push(report);
156 }
157 }
158 }
159
160 if (missing.length) {
161 coverage.push({
162 filename,
163 line,
164 missing,
165 severity: 'error',
166 message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
167 });
168 }
169 }
170
171 return coverage.length ? coverage : null;
172 }
173};
174
175
176internals.Store = class {
177
178 constructor(schema) {
179
180 this.active = true;
181 this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid }
182 this._combos = new Map(); // merged -> [sources]
183 this._scan(schema);
184 }
185
186 debug(state, source, name, result) {
187
188 state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
189 }
190
191 entry(schema, state) {
192
193 internals.debug(state, { type: 'entry' });
194
195 this._record(schema, (log) => {
196
197 log.entry = true;
198 });
199 }
200
201 filter(schema, state, source, value) {
202
203 internals.debug(state, { type: source, ...value });
204
205 this._record(schema, (log) => {
206
207 log[source].add(value);
208 });
209 }
210
211 log(schema, state, source, name, result) {
212
213 internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });
214
215 this._record(schema, (log) => {
216
217 log[source][name] = log[source][name] || 0;
218 log[source][name] |= internals.codes[result];
219 });
220 }
221
222 resolve(state, ref, to) {
223
224 if (!state.mainstay.debug) {
225 return;
226 }
227
228 const log = { type: 'resolve', ref: ref.display, to, path: state.path };
229 state.mainstay.debug.push(log);
230 }
231
232 value(state, by, from, to, name) {
233
234 if (!state.mainstay.debug ||
235 DeepEqual(from, to)) {
236
237 return;
238 }
239
240 const log = { type: 'value', by, from, to, path: state.path };
241 if (name) {
242 log.name = name;
243 }
244
245 state.mainstay.debug.push(log);
246 }
247
248 _record(schema, each) {
249
250 const log = this._sources.get(schema);
251 if (log) {
252 each(log);
253 return;
254 }
255
256 const sources = this._combos.get(schema);
257 for (const source of sources) {
258 this._record(source, each);
259 }
260 }
261
262 _scan(schema, _path) {
263
264 const path = _path || [];
265
266 let log = this._sources.get(schema);
267 if (!log) {
268 log = {
269 paths: new Set(),
270 entry: false,
271 rule: {},
272 valid: new Set(),
273 invalid: new Set()
274 };
275
276 this._sources.set(schema, log);
277 }
278
279 if (path.length) {
280 log.paths.add(path);
281 }
282
283 const each = (sub, source) => {
284
285 const subId = internals.id(sub, source);
286 this._scan(sub, path.concat(subId));
287 };
288
289 schema.$_modify({ each, ref: false });
290 }
291
292 _combine(merged, sources) {
293
294 this._combos.set(merged, sources);
295 }
296};
297
298
299internals.message = function (item) {
300
301 const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
302 return `${path}${item.rule || ''} (${item.status})`;
303};
304
305
306internals.id = function (schema, { source, name, path, key }) {
307
308 if (schema._flags.id) {
309 return schema._flags.id;
310 }
311
312 if (key) {
313 return key;
314 }
315
316 name = `@${name}`;
317
318 if (source === 'terms') {
319 return [name, path[Math.min(path.length - 1, 1)]];
320 }
321
322 return name;
323};
324
325
326internals.sub = function (paths, skipped) {
327
328 for (const path of paths) {
329 for (const skip of skipped) {
330 if (DeepEqual(path.slice(0, skip.length), skip)) {
331 return true;
332 }
333 }
334 }
335
336 return false;
337};
338
339
340internals.debug = function (state, event) {
341
342 if (state.mainstay.debug) {
343 event.path = state.debug ? [...state.path, state.debug] : state.path;
344 state.mainstay.debug.push(event);
345 }
346};
347
\No newline at end of file