forked from nodejs/node
/
tap_checker.js
155 lines (128 loc) · 4.14 KB
/
tap_checker.js
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
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeFind,
NumberParseInt,
} = primordials;
const {
codes: { ERR_TAP_VALIDATION_ERROR },
} = require('internal/errors');
const { TokenKind } = require('internal/test_runner/tap_lexer');
// TODO(@manekinekko): add more validation rules based on the TAP14 spec.
// See https://testanything.org/tap-version-14-specification.html
class TAPValidationStrategy {
validate(ast) {
this.#validateVersion(ast);
this.#validatePlan(ast);
this.#validateTestPoints(ast);
return true;
}
#validateVersion(ast) {
const entry = ArrayPrototypeFind(
ast,
(node) => node.kind === TokenKind.TAP_VERSION
);
if (!entry) {
throw new ERR_TAP_VALIDATION_ERROR('missing TAP version');
}
const { version } = entry.node;
// TAP14 specification is compatible with observed behavior of existing TAP13 consumers and producers
if (version !== '14' && version !== '13') {
throw new ERR_TAP_VALIDATION_ERROR('TAP version should be 13 or 14');
}
}
#validatePlan(ast) {
const entry = ArrayPrototypeFind(
ast,
(node) => node.kind === TokenKind.TAP_PLAN
);
if (!entry) {
throw new ERR_TAP_VALIDATION_ERROR('missing TAP plan');
}
const plan = entry.node;
if (!plan.start) {
throw new ERR_TAP_VALIDATION_ERROR('missing plan start');
}
if (!plan.end) {
throw new ERR_TAP_VALIDATION_ERROR('missing plan end');
}
const planStart = NumberParseInt(plan.start, 10);
const planEnd = NumberParseInt(plan.end, 10);
if (planEnd !== 0 && planStart > planEnd) {
throw new ERR_TAP_VALIDATION_ERROR(
`plan start ${planStart} is greater than plan end ${planEnd}`
);
}
}
// TODO(@manekinekko): since we are dealing with a flat AST, we need to
// validate test points grouped by their "nesting" level. This is because a set of
// Test points belongs to a TAP document. Each new subtest block creates a new TAP document.
// https://testanything.org/tap-version-14-specification.html#subtests
#validateTestPoints(ast) {
const bailoutEntry = ArrayPrototypeFind(
ast,
(node) => node.kind === TokenKind.TAP_BAIL_OUT
);
const planEntry = ArrayPrototypeFind(
ast,
(node) => node.kind === TokenKind.TAP_PLAN
);
const testPointEntries = ArrayPrototypeFilter(
ast,
(node) => node.kind === TokenKind.TAP_TEST_POINT
);
const plan = planEntry.node;
const planStart = NumberParseInt(plan.start, 10);
const planEnd = NumberParseInt(plan.end, 10);
if (planEnd === 0 && testPointEntries.length > 0) {
throw new ERR_TAP_VALIDATION_ERROR(
`found ${testPointEntries.length} Test Point${
testPointEntries.length > 1 ? 's' : ''
} but plan is ${planStart}..0`
);
}
if (planEnd > 0) {
if (testPointEntries.length === 0) {
throw new ERR_TAP_VALIDATION_ERROR('missing Test Points');
}
if (!bailoutEntry && testPointEntries.length !== planEnd) {
throw new ERR_TAP_VALIDATION_ERROR(
`test Points count ${testPointEntries.length} does not match plan count ${planEnd}`
);
}
for (let i = 0; i < testPointEntries.length; i++) {
const test = testPointEntries[i].node;
const testId = NumberParseInt(test.id, 10);
if (testId < planStart || testId > planEnd) {
throw new ERR_TAP_VALIDATION_ERROR(
`test ${testId} is out of plan range ${planStart}..${planEnd}`
);
}
}
}
}
}
// TAP14 and TAP13 are compatible with each other
class TAP13ValidationStrategy extends TAPValidationStrategy {}
class TAP14ValidationStrategy extends TAPValidationStrategy {}
class TapChecker {
static TAP13 = '13';
static TAP14 = '14';
constructor({ specs }) {
switch (specs) {
case TapChecker.TAP13:
this.strategy = new TAP13ValidationStrategy();
break;
default:
this.strategy = new TAP14ValidationStrategy();
}
}
check(ast) {
return this.strategy.validate(ast);
}
}
module.exports = {
TapChecker,
TAP14ValidationStrategy,
TAP13ValidationStrategy,
};