347 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'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);
 | 
						|
    }
 | 
						|
};
 |