trace.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. 'use strict';
  2. const DeepEqual = require('@hapi/hoek/lib/deepEqual');
  3. const Pinpoint = require('@sideway/pinpoint');
  4. const Errors = require('./errors');
  5. const internals = {
  6. codes: {
  7. error: 1,
  8. pass: 2,
  9. full: 3
  10. },
  11. labels: {
  12. 0: 'never used',
  13. 1: 'always error',
  14. 2: 'always pass'
  15. }
  16. };
  17. exports.setup = function (root) {
  18. const trace = function () {
  19. root._tracer = root._tracer || new internals.Tracer();
  20. return root._tracer;
  21. };
  22. root.trace = trace;
  23. root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;
  24. root.untrace = () => {
  25. root._tracer = null;
  26. };
  27. };
  28. exports.location = function (schema) {
  29. return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller
  30. };
  31. internals.Tracer = class {
  32. constructor() {
  33. this.name = 'Joi';
  34. this._schemas = new Map();
  35. }
  36. _register(schema) {
  37. const existing = this._schemas.get(schema);
  38. if (existing) {
  39. return existing.store;
  40. }
  41. const store = new internals.Store(schema);
  42. const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
  43. this._schemas.set(schema, { filename, line, store });
  44. return store;
  45. }
  46. _combine(merged, sources) {
  47. for (const { store } of this._schemas.values()) {
  48. store._combine(merged, sources);
  49. }
  50. }
  51. report(file) {
  52. const coverage = [];
  53. // Process each registered schema
  54. for (const { filename, line, store } of this._schemas.values()) {
  55. if (file &&
  56. file !== filename) {
  57. continue;
  58. }
  59. // Process sub schemas of the registered root
  60. const missing = [];
  61. const skipped = [];
  62. for (const [schema, log] of store._sources.entries()) {
  63. // Check if sub schema parent skipped
  64. if (internals.sub(log.paths, skipped)) {
  65. continue;
  66. }
  67. // Check if sub schema reached
  68. if (!log.entry) {
  69. missing.push({
  70. status: 'never reached',
  71. paths: [...log.paths]
  72. });
  73. skipped.push(...log.paths);
  74. continue;
  75. }
  76. // Check values
  77. for (const type of ['valid', 'invalid']) {
  78. const set = schema[`_${type}s`];
  79. if (!set) {
  80. continue;
  81. }
  82. const values = new Set(set._values);
  83. const refs = new Set(set._refs);
  84. for (const { value, ref } of log[type]) {
  85. values.delete(value);
  86. refs.delete(ref);
  87. }
  88. if (values.size ||
  89. refs.size) {
  90. missing.push({
  91. status: [...values, ...[...refs].map((ref) => ref.display)],
  92. rule: `${type}s`
  93. });
  94. }
  95. }
  96. // Check rules status
  97. const rules = schema._rules.map((rule) => rule.name);
  98. for (const type of ['default', 'failover']) {
  99. if (schema._flags[type] !== undefined) {
  100. rules.push(type);
  101. }
  102. }
  103. for (const name of rules) {
  104. const status = internals.labels[log.rule[name] || 0];
  105. if (status) {
  106. const report = { rule: name, status };
  107. if (log.paths.size) {
  108. report.paths = [...log.paths];
  109. }
  110. missing.push(report);
  111. }
  112. }
  113. }
  114. if (missing.length) {
  115. coverage.push({
  116. filename,
  117. line,
  118. missing,
  119. severity: 'error',
  120. message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
  121. });
  122. }
  123. }
  124. return coverage.length ? coverage : null;
  125. }
  126. };
  127. internals.Store = class {
  128. constructor(schema) {
  129. this.active = true;
  130. this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid }
  131. this._combos = new Map(); // merged -> [sources]
  132. this._scan(schema);
  133. }
  134. debug(state, source, name, result) {
  135. state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
  136. }
  137. entry(schema, state) {
  138. internals.debug(state, { type: 'entry' });
  139. this._record(schema, (log) => {
  140. log.entry = true;
  141. });
  142. }
  143. filter(schema, state, source, value) {
  144. internals.debug(state, { type: source, ...value });
  145. this._record(schema, (log) => {
  146. log[source].add(value);
  147. });
  148. }
  149. log(schema, state, source, name, result) {
  150. internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });
  151. this._record(schema, (log) => {
  152. log[source][name] = log[source][name] || 0;
  153. log[source][name] |= internals.codes[result];
  154. });
  155. }
  156. resolve(state, ref, to) {
  157. if (!state.mainstay.debug) {
  158. return;
  159. }
  160. const log = { type: 'resolve', ref: ref.display, to, path: state.path };
  161. state.mainstay.debug.push(log);
  162. }
  163. value(state, by, from, to, name) {
  164. if (!state.mainstay.debug ||
  165. DeepEqual(from, to)) {
  166. return;
  167. }
  168. const log = { type: 'value', by, from, to, path: state.path };
  169. if (name) {
  170. log.name = name;
  171. }
  172. state.mainstay.debug.push(log);
  173. }
  174. _record(schema, each) {
  175. const log = this._sources.get(schema);
  176. if (log) {
  177. each(log);
  178. return;
  179. }
  180. const sources = this._combos.get(schema);
  181. for (const source of sources) {
  182. this._record(source, each);
  183. }
  184. }
  185. _scan(schema, _path) {
  186. const path = _path || [];
  187. let log = this._sources.get(schema);
  188. if (!log) {
  189. log = {
  190. paths: new Set(),
  191. entry: false,
  192. rule: {},
  193. valid: new Set(),
  194. invalid: new Set()
  195. };
  196. this._sources.set(schema, log);
  197. }
  198. if (path.length) {
  199. log.paths.add(path);
  200. }
  201. const each = (sub, source) => {
  202. const subId = internals.id(sub, source);
  203. this._scan(sub, path.concat(subId));
  204. };
  205. schema.$_modify({ each, ref: false });
  206. }
  207. _combine(merged, sources) {
  208. this._combos.set(merged, sources);
  209. }
  210. };
  211. internals.message = function (item) {
  212. const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
  213. return `${path}${item.rule || ''} (${item.status})`;
  214. };
  215. internals.id = function (schema, { source, name, path, key }) {
  216. if (schema._flags.id) {
  217. return schema._flags.id;
  218. }
  219. if (key) {
  220. return key;
  221. }
  222. name = `@${name}`;
  223. if (source === 'terms') {
  224. return [name, path[Math.min(path.length - 1, 1)]];
  225. }
  226. return name;
  227. };
  228. internals.sub = function (paths, skipped) {
  229. for (const path of paths) {
  230. for (const skip of skipped) {
  231. if (DeepEqual(path.slice(0, skip.length), skip)) {
  232. return true;
  233. }
  234. }
  235. }
  236. return false;
  237. };
  238. internals.debug = function (state, event) {
  239. if (state.mainstay.debug) {
  240. event.path = state.debug ? [...state.path, state.debug] : state.path;
  241. state.mainstay.debug.push(event);
  242. }
  243. };