-
Notifications
You must be signed in to change notification settings - Fork 79
/
verifier.py
222 lines (183 loc) · 6.91 KB
/
verifier.py
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""This script checks a bunch of informal rules for PreTeXt/Runestone.
It recurses from the current directory looking for ".ptx" files and
parses each passing the parsed XML representation to a bunch of tests.
If all tests pass, the script exits with status 0, and 1 otherwise.
"""
import xml.dom.minidom
import os
from abc import abstractmethod
import sys
import argparse
class CppsXmlTest:
"""generic test"""
@classmethod
@abstractmethod
def test_file(cls, fname, doc):
"""Overridden by subclasses, should return true if "fname" passes the test"""
@classmethod
def teardown(cls):
"""May be overriden: called after main tests are run"""
return True
@classmethod
def setup(cls):
"""may be overridden: called before any tests are run"""
return True
class TabNodeIsNotAThing(CppsXmlTest):
"""TabNode is something the automatic conversion tools emitted.
PreTeXt doesn't actually have the concept of a TabNode per se,
so everywhere this tag occurs we have to do some manual work.
"""
@classmethod
def test_file(cls, fname, doc):
n = len(doc.getElementsByTagName("TabNode"))
if n > 0:
print(f"{fname} has {n} TabNode tags")
return n == 0
class TagsNeedsCaption(CppsXmlTest):
"""The tags below should have a <caption> elements as a child"""
captioned_things = ('figure', 'listing')
@classmethod
def test_file(cls, fname, doc):
ret = True
for captioned_thing in cls.captioned_things:
for fig in doc.getElementsByTagName(captioned_thing):
found = False
for child in fig.childNodes:
if child.nodeType != xml.dom.Node.ELEMENT_NODE:
continue
if child.nodeName == "caption":
found = True
if not found:
ret = False
print(f'{fname}: {captioned_thing} is missing caption')
class TagsNeedsTitle(CppsXmlTest):
"""The tags below should have a <title> elements as a child"""
captioned_things = ('table', 'exploration', 'task')
@classmethod
def test_file(cls, fname, doc):
ret = True
for captioned_thing in cls.captioned_things:
for fig in doc.getElementsByTagName(captioned_thing):
found = False
for child in fig.childNodes:
if child.nodeType != xml.dom.Node.ELEMENT_NODE:
continue
if child.nodeName == "title":
found = True
if not found:
ret = False
print(f'{fname}: {captioned_thing} is missing title')
class ImageNeedsDescription(CppsXmlTest):
"""<image> should have a <description> as a child"""
captioned_things = ('image',)
@classmethod
def test_file(cls, fname, doc):
ret = True
for fig in doc.getElementsByTagName('image'):
src = fig.getAttribute("source")
found = False
for child in fig.childNodes:
if child.nodeType != xml.dom.Node.ELEMENT_NODE:
continue
if child.nodeName == "description":
found = True
if not found:
ret = False
print(f'{fname}: image is missing description: {src}')
return ret
class LabelsShouldBeUnique(CppsXmlTest):
"""label attributes should be globally unique"""
labels = {}
@classmethod
def test_file(cls, fname, doc):
for child in doc.childNodes:
if child.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
continue
cls.check_node(fname, doc, child)
@classmethod
def check_node(cls, fname, doc, element):
"""recursively check all nodes in this document for labels"""
label = element.getAttribute("label")
if len(label) > 0:
if label not in cls.labels:
cls.labels[label] = []
cls.labels[label].append(fname)
for child in element.childNodes:
if child.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
continue
cls.check_node(fname, doc, child)
@classmethod
def setup(cls):
cls.labels = {}
return True
@classmethod
def teardown(cls):
ok = True
for k, v in cls.labels.items():
if len(v) > 1:
ok = False
print(f"label {k} is used {len(v)} times")
for fname in v:
print(f" in {fname}")
return ok
class TagsNeedLabels(CppsXmlTest):
"""The tags below are supposed to have labels so that Runestone knows
where to store the user data."""
labeled_items = ('exercise', 'task')
@classmethod
def test_file(cls, fname, doc):
ret = True
for labeled_thing in cls.labeled_items:
for task in doc.getElementsByTagName(labeled_thing):
label = task.getAttribute("label")
if len(label) == 0:
print(f'{fname}: {labeled_thing} does not have label')
ret = False
return ret
ALL_TESTS = CppsXmlTest.__subclasses__()
def handle_file(fname, tests):
"""Run all the tests on the given filename"""
dom = xml.dom.minidom.parse(fname)
r = []
for t in tests:
r.append(t.test_file(fname, dom))
return all(r)
def main():
"""run through all of the ptx files running tests"""
parser = argparse.ArgumentParser(prog="pretext verifier",
description="PreTeXt XML verifier")
parser.add_argument('-D', '--disable', action='append',
metavar='test',
help='disable a test (can be repeated)', default=[])
parser.add_argument('-L', '--listtests', action='store_true',
help="list available tests and exit")
parser.add_argument('dirs', nargs='*',
help="directories to search for ptx files")
args = parser.parse_args()
if not args.dirs:
args.dirs = ["."]
if args.listtests:
_ = [print(t.__name__) for t in ALL_TESTS]
sys.exit(0)
failed = False
for x in args.disable:
if x not in [t.__name__ for t in ALL_TESTS]:
failed = True
print(f"No such test: {x}")
if failed:
sys.exit(1)
tests = [t for t in ALL_TESTS if t.__name__ not in args.disable]
res = []
for t in tests:
res.append(t.setup())
for dir in args.dirs:
for root, _, files in os.walk(dir):
for fname in files:
if not fname.endswith('.ptx'):
continue
res.append(handle_file(os.path.join(root, fname), tests))
for t in tests:
res.append(t.teardown())
sys.exit(0) if all(res) else sys.exit(1)
if __name__ == "__main__":
main()