|
- '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);
- }
- };
|