123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- 'use strict';
- const DeepEqual = require('@hapi/hoek/lib/deepEqual');
- const Pinpoint = require('@sideway/pinpoint');
- const Errors = require('./errors');
- const internals = {
- codes: {
- error: 1,
- pass: 2,
- full: 3
- },
- labels: {
- 0: 'never used',
- 1: 'always error',
- 2: 'always pass'
- }
- };
- exports.setup = function (root) {
- const trace = function () {
- root._tracer = root._tracer || new internals.Tracer();
- return root._tracer;
- };
- root.trace = trace;
- root[Symbol.for('@hapi/lab/coverage/initialize')] = trace;
- root.untrace = () => {
- root._tracer = null;
- };
- };
- exports.location = function (schema) {
- return schema.$_setFlag('_tracerLocation', Pinpoint.location(2)); // base.tracer(), caller
- };
- internals.Tracer = class {
- constructor() {
- this.name = 'Joi';
- this._schemas = new Map();
- }
- _register(schema) {
- const existing = this._schemas.get(schema);
- if (existing) {
- return existing.store;
- }
- const store = new internals.Store(schema);
- const { filename, line } = schema._flags._tracerLocation || Pinpoint.location(5); // internals.tracer(), internals.entry(), exports.entry(), validate(), caller
- this._schemas.set(schema, { filename, line, store });
- return store;
- }
- _combine(merged, sources) {
- for (const { store } of this._schemas.values()) {
- store._combine(merged, sources);
- }
- }
- report(file) {
- const coverage = [];
- // Process each registered schema
- for (const { filename, line, store } of this._schemas.values()) {
- if (file &&
- file !== filename) {
- continue;
- }
- // Process sub schemas of the registered root
- const missing = [];
- const skipped = [];
- for (const [schema, log] of store._sources.entries()) {
- // Check if sub schema parent skipped
- if (internals.sub(log.paths, skipped)) {
- continue;
- }
- // Check if sub schema reached
- if (!log.entry) {
- missing.push({
- status: 'never reached',
- paths: [...log.paths]
- });
- skipped.push(...log.paths);
- continue;
- }
- // Check values
- for (const type of ['valid', 'invalid']) {
- const set = schema[`_${type}s`];
- if (!set) {
- continue;
- }
- const values = new Set(set._values);
- const refs = new Set(set._refs);
- for (const { value, ref } of log[type]) {
- values.delete(value);
- refs.delete(ref);
- }
- if (values.size ||
- refs.size) {
- missing.push({
- status: [...values, ...[...refs].map((ref) => ref.display)],
- rule: `${type}s`
- });
- }
- }
- // Check rules status
- const rules = schema._rules.map((rule) => rule.name);
- for (const type of ['default', 'failover']) {
- if (schema._flags[type] !== undefined) {
- rules.push(type);
- }
- }
- for (const name of rules) {
- const status = internals.labels[log.rule[name] || 0];
- if (status) {
- const report = { rule: name, status };
- if (log.paths.size) {
- report.paths = [...log.paths];
- }
- missing.push(report);
- }
- }
- }
- if (missing.length) {
- coverage.push({
- filename,
- line,
- missing,
- severity: 'error',
- message: `Schema missing tests for ${missing.map(internals.message).join(', ')}`
- });
- }
- }
- return coverage.length ? coverage : null;
- }
- };
- internals.Store = class {
- constructor(schema) {
- this.active = true;
- this._sources = new Map(); // schema -> { paths, entry, rule, valid, invalid }
- this._combos = new Map(); // merged -> [sources]
- this._scan(schema);
- }
- debug(state, source, name, result) {
- state.mainstay.debug && state.mainstay.debug.push({ type: source, name, result, path: state.path });
- }
- entry(schema, state) {
- internals.debug(state, { type: 'entry' });
- this._record(schema, (log) => {
- log.entry = true;
- });
- }
- filter(schema, state, source, value) {
- internals.debug(state, { type: source, ...value });
- this._record(schema, (log) => {
- log[source].add(value);
- });
- }
- log(schema, state, source, name, result) {
- internals.debug(state, { type: source, name, result: result === 'full' ? 'pass' : result });
- this._record(schema, (log) => {
- log[source][name] = log[source][name] || 0;
- log[source][name] |= internals.codes[result];
- });
- }
- resolve(state, ref, to) {
- if (!state.mainstay.debug) {
- return;
- }
- const log = { type: 'resolve', ref: ref.display, to, path: state.path };
- state.mainstay.debug.push(log);
- }
- value(state, by, from, to, name) {
- if (!state.mainstay.debug ||
- DeepEqual(from, to)) {
- return;
- }
- const log = { type: 'value', by, from, to, path: state.path };
- if (name) {
- log.name = name;
- }
- state.mainstay.debug.push(log);
- }
- _record(schema, each) {
- const log = this._sources.get(schema);
- if (log) {
- each(log);
- return;
- }
- const sources = this._combos.get(schema);
- for (const source of sources) {
- this._record(source, each);
- }
- }
- _scan(schema, _path) {
- const path = _path || [];
- let log = this._sources.get(schema);
- if (!log) {
- log = {
- paths: new Set(),
- entry: false,
- rule: {},
- valid: new Set(),
- invalid: new Set()
- };
- this._sources.set(schema, log);
- }
- if (path.length) {
- log.paths.add(path);
- }
- const each = (sub, source) => {
- const subId = internals.id(sub, source);
- this._scan(sub, path.concat(subId));
- };
- schema.$_modify({ each, ref: false });
- }
- _combine(merged, sources) {
- this._combos.set(merged, sources);
- }
- };
- internals.message = function (item) {
- const path = item.paths ? Errors.path(item.paths[0]) + (item.rule ? ':' : '') : '';
- return `${path}${item.rule || ''} (${item.status})`;
- };
- internals.id = function (schema, { source, name, path, key }) {
- if (schema._flags.id) {
- return schema._flags.id;
- }
- if (key) {
- return key;
- }
- name = `@${name}`;
- if (source === 'terms') {
- return [name, path[Math.min(path.length - 1, 1)]];
- }
- return name;
- };
- internals.sub = function (paths, skipped) {
- for (const path of paths) {
- for (const skip of skipped) {
- if (DeepEqual(path.slice(0, skip.length), skip)) {
- return true;
- }
- }
- }
- return false;
- };
- internals.debug = function (state, event) {
- if (state.mainstay.debug) {
- event.path = state.debug ? [...state.path, state.debug] : state.path;
- state.mainstay.debug.push(event);
- }
- };
|