summaryrefslogtreecommitdiffstats
path: root/test/schemas/src/schema.spec.ts
blob: b8264613b68b1e493311937fac8aa840ea117779 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import * as path from "path";
import Ajv from "ajv";
import fs from "fs";
import { minimatch } from "minimatch";
import yaml from "js-yaml";
import { assert } from "chai";
import stringify from "safe-stable-stringify";
import { integer } from "vscode-languageserver-types";
import { exec } from "child_process";
const spawnSync = require("child_process").spawnSync;

function ansiRegex({ onlyFirst = false } = {}) {
  const pattern = [
    "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
    "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
  ].join("|");

  return new RegExp(pattern, onlyFirst ? undefined : "g");
}

function stripAnsi(data: string) {
  if (typeof data !== "string") {
    throw new TypeError(
      `Expected a \`string\`, got \`${typeof data}\ = ${data}`
    );
  }
  return data.replace(ansiRegex(), "");
}

const ajv = new Ajv({
  strictTypes: false,
  strict: false,
  inlineRefs: true, // https://github.com/ajv-validator/ajv/issues/1581#issuecomment-832211568
  allErrors: true, // https://github.com/ajv-validator/ajv/issues/1581#issuecomment-832211568
});

// load whitelist of all test file subjects schemas can reference
const test_files = getAllFiles("./test");
const negative_test_files = getAllFiles("./negative_test");

// load all schemas
const schema_files = fs
  .readdirSync("f/")
  .filter((el) => path.extname(el) === ".json");
console.log(`Schemas: ${schema_files}`);

describe("schemas under f/", function () {
  schema_files.forEach((schema_file) => {
    if (
      schema_file.startsWith("_") ||
      ["ansible-navigator-config.json", "rulebook.json"].includes(schema_file)
    ) {
      return;
    }
    const schema_json = JSON.parse(fs.readFileSync(`f/${schema_file}`, "utf8"));
    ajv.addSchema(schema_json);
    const validator = ajv.compile(schema_json);
    if (schema_json.examples == undefined) {
      console.error(
        `Schema file ${schema_file} is missing an examples key that we need for documenting file matching patterns.`
      );
      return process.exit(1);
    }
    describe(schema_file, function () {
      getTestFiles(schema_json.examples).forEach(
        ({ file: test_file, expect_fail }) => {
          it(`linting ${test_file} using ${schema_file}`, function () {
            var errors_md = "";
            const result = validator(
              yaml.load(fs.readFileSync(test_file, "utf8"))
            );
            if (validator.errors) {
              errors_md += "# ajv errors\n\n```json\n";
              errors_md += stringify(validator.errors, null, 2);
              errors_md += "\n```\n\n";
            }
            // validate using check-jsonschema (python-jsonschema):
            // const py = exec();
            // Do not use python -m ... calling notation because for some
            // reason, nodejs environment lacks some env variables needed
            // and breaks usage from inside virtualenvs.
            const proc = spawnSync(
              `${process.env.VIRTUAL_ENV}/bin/check-jsonschema -v -o json --schemafile f/${schema_file} ${test_file}`,
              { shell: true, encoding: "utf-8", stdio: "pipe" }
            );
            if (proc.status != 0) {
              // real errors are sent to stderr due to https://github.com/python-jsonschema/check-jsonschema/issues/88
              errors_md += "# check-jsonschema\n\nstdout:\n\n```json\n";
              errors_md += stripAnsi(proc.output[1]);
              errors_md += "```\n";
              if (proc.output[2]) {
                errors_md += "\nstderr:\n\n```\n";
                errors_md += stripAnsi(proc.output[2]);
                errors_md += "```\n";
              }
            }

            // dump errors to markdown file for manual inspection
            const md_filename = `${test_file}.md`;
            if (errors_md) {
              fs.writeFileSync(md_filename, errors_md);
            } else {
              // if no error occurs, we should ensure there is no md file present
              fs.unlink(md_filename, function (err) {
                if (err && err.code != "ENOENT") {
                  console.error(`Failed to remove ${md_filename}.`);
                }
              });
            }
            assert.equal(
              result,
              !expect_fail,
              `${JSON.stringify(validator.errors)}`
            );
          });
        }
      );
      // All /$defs/ that have examples property are assumed to be
      // subschemas, "tasks" being the primary such case, which is also used
      // for validating separated files.
      for (var definition in schema_json["$defs"]) {
        if (schema_json["$defs"][definition].examples) {
          const subschema_uri = `${schema_json["$id"]}#/$defs/${definition}`;
          const subschema_validator = ajv.getSchema(subschema_uri);
          if (!subschema_validator) {
            console.error(`Failed to load subschema ${subschema_uri}`);
            return process.exit(1);
          }
          getTestFiles(schema_json["$defs"][definition].examples).forEach(
            ({ file: test_file, expect_fail }) => {
              it(`linting ${test_file} using ${subschema_uri}`, function () {
                const result = subschema_validator(
                  yaml.load(fs.readFileSync(test_file, "utf8"))
                );
                assert.equal(
                  result,
                  !expect_fail,
                  `${JSON.stringify(validator.errors)}`
                );
              });
            }
          );
        }
      }
    });
  });
});

// find all tests for each schema file
function getTestFiles(
  globs: string[]
): { file: string; expect_fail: boolean }[] {
  const files = Array.from(
    new Set(
      globs
        .map((glob: any) => minimatch.match(test_files, path.join("**", glob)))
        .flat()
    )
  );
  const negative_files = Array.from(
    new Set(
      globs
        .map((glob: any) =>
          minimatch.match(negative_test_files, path.join("**", glob))
        )
        .flat()
    )
  );

  // All fails ending with fail, like `foo.fail.yml` are expected to fail validation
  let result = files.map((f) => ({ file: f, expect_fail: false }));
  result = result.concat(
    negative_files.map((f) => ({ file: f, expect_fail: true }))
  );
  return result;
}

function getAllFiles(dir: string): string[] {
  return fs.readdirSync(dir).reduce((files: string[], file: string) => {
    const name = path.join(dir, file);
    const isDirectory = fs.statSync(name).isDirectory();
    return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name];
  }, []);
}