-
Notifications
You must be signed in to change notification settings - Fork 0
/
test-options
executable file
·419 lines (353 loc) · 15.2 KB
/
test-options
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
#!/usr/bin/python3
# Check that xpopple's code and documentation are consistent.
import os
import re
import subprocess
import sys
testdir = "testdata"
old_versions = ("3.03", "3.04")
# Check we're in the right dir
assert os.path.exists("test-options")
# Download the old source if it's not already there
old_srcdirs = {}
for version in old_versions:
distfile = "xpdf-" + version + ".tar.gz"
tarball = os.path.join(testdir, "archives", distfile)
srcdir = os.path.join(testdir, "src", "xpdf-" + version)
old_srcdirs[version] = srcdir
os.makedirs(os.path.dirname(tarball), exist_ok=True)
if not os.path.exists(tarball):
subprocess.check_call(["wget", "-O", tarball,
"https://dl.xpdfreader.com/old/" + distfile])
os.makedirs(os.path.dirname(srcdir), exist_ok=True)
if not os.path.exists(srcdir):
subprocess.check_call(["tar", "-C", os.path.dirname(srcdir),
"-xf", tarball])
class XPDFTree:
"""Information extracted from an Xpdf/xpopple source tree."""
def __init__(self, dirname):
self.dirname = dirname
# Command-line option arguments
self.code_args = set()
self.code_args_obsolete = set()
self.man_args = set()
self.man_args_obsolete = set()
# X resources
self.code_resources = set()
self.man_resources = set()
# Config file options
self.code_config = set()
self.code_config_obsolete = set()
self.man_config = set()
self.man_config_obsolete = set()
# Remote commands
self.code_commands = set()
self.man_commands = set()
# Key bindings (only, since these are documented individually)
self.code_keys = set()
self.man_keys = set()
# Default bindings
self.code_bindings = set()
self.man_bindings = set()
self.read_xpdf_code()
self.read_app_code()
self.read_params_code()
self.read_viewer_code()
self.read_xpdf_man()
self.read_xpdfrc_man()
# code_keys are just the keys that have something bound to them
for binding in self.code_bindings:
key = binding.split()[1]
if not key.startswith("mouse"):
self.code_keys.add(key)
# Each of these read_ methods is a little state machine that identifies and
# searches the relevant sections of a source file or man page. It's not a
# proper parser but it'll do the job!
def read_xpdf_code(self):
with open(os.path.join(self.dirname, "xpdf", "xpdf.cc")) as f:
state = "before"
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'argDesc\[\] =', line):
state = "tab"
elif state == "tab":
m = re.search(r'^ *{ *"([^"]*)", *([^ ,]*)', line)
if m:
if m.group(2).startswith("argObsolete"):
self.code_args_obsolete.add(m.group(1))
else:
self.code_args.add(m.group(1))
if re.search(r'^}', line):
break
assert self.code_args != set()
# Old versions didn't handle obsolete args
if self.dirname == ".":
assert self.code_args_obsolete != set()
def read_app_code(self):
args = set()
with open(os.path.join(self.dirname, "xpdf", "XPDFApp.cc")) as f:
state = "before"
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'xOpts\[\] =', line):
state = "tab"
if re.search(r'xResources\[\] =', line):
state = "resources"
elif state == "tab":
m = re.search(r'^ *{ *"([^"]*)", *"[.*]*([^"]*)",', line)
if m:
args.add(m.group(1))
self.code_resources.add(m.group(2))
if re.search(r'^}', line):
state = "before"
elif state == "resources":
m = re.search(r'^ *{ *"([^"]*)",', line)
if m:
self.code_resources.add(m.group(1))
if re.search(r'^}', line):
state = "before"
assert args != set()
self.code_args.update(args)
assert self.code_resources != set()
def read_params_code(self):
fn = os.path.join(self.dirname, "xpdf", "XPDFParams.cc")
if not os.path.exists(fn):
fn = os.path.join(self.dirname, "xpdf", "GlobalParams.cc")
with open(fn) as f:
state = "before"
lines = ""
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'::createDefaultKeyBindings', line):
state = "bindings"
if re.search(r'::parseLine', line):
state = "config"
elif state == "bindings":
# Bindings are broken across multiple lines.
if re.search(r'^ *//', line):
pass
elif re.search(r'^}', line):
state = "before"
else:
lines += " " + line
if re.search(r';$', lines):
m = re.search(r'(?:new KeyBinding|emplace_back)\((.*")\)', lines)
if m:
binding = convert_binding(m.group(1))
self.code_bindings.add(binding)
lines = ""
elif state == "config":
for s in re.findall(r'(?:cmd->cmp\(|cmd == )"([^"]*)"', line):
self.code_config.add(s)
if re.search(r'error.*Unknown config file command', line):
state = "obsolete"
elif state == "obsolete":
for s in re.findall(r'(?:cmd->cmp\(|cmd == )"([^"]*)"', line):
self.code_config_obsolete.add(s)
if re.search(r'^\}', line):
state = "before"
assert self.code_config != set()
assert self.code_config_obsolete != set()
def read_viewer_code(self):
with open(os.path.join(self.dirname, "xpdf", "XPDFViewer.cc")) as f:
state = "before"
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'::cmdTab\[\] =', line):
state = "tab"
elif state == "tab":
m = re.search(r'^ *{ *"([^"]*)",', line)
if m:
self.code_commands.add(m.group(1))
if re.search(r'^}', line):
break
assert self.code_commands != set()
def read_xpdf_man(self):
with open(os.path.join(self.dirname, "doc", "xpdf.1")) as f:
state = "before"
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'^\.SH OPTIONS', line):
state = "args"
if re.search(r'^\.SS Key bindings', line):
state = "keys"
if re.search(r'^\.SH COMMANDS', line):
state = "commands"
elif state == "args":
m = re.search(r'^(?:\.BI?|\.RB \() \\(-[^ ]*)', line)
if m:
self.man_args.add(m.group(1).replace('\\', ''))
m = re.search(r'config file: " ([^ ]*)', line)
if m:
self.man_config.add(m.group(1))
m = re.search(r'(?:X resource: "|^\.B) [Xx]pdf[.*]([^ ]*)', line)
# toolTipEnable is handled by Motif.
if m and m.group(1) != "toolTipEnable":
self.man_resources.add(m.group(1))
if re.search(r'^\.SH OBSOLETE OPTIONS', line):
state = "obsolete"
elif re.search(r'^\.SH', line):
state = "before"
elif state == "obsolete":
m = re.search(r'^(?:\.BI?|\.RB \() \\(-[^ ]*)', line)
if m:
self.man_args_obsolete.add(m.group(1).replace('\\', ''))
if re.search(r'^\.SH', line):
state = "before"
elif state == "keys":
m = re.search(r'^\.BR? (.*)$', line)
if m:
for key in m.group(1).split(' " or " '):
# Convert the key name to bind command form
if key.startswith("control-"):
key = "ctrl-" + key[8:]
i = key.find("<")
if i != -1:
key = key[:i] + key[i + 1:-1]
key = key.lower()
if key == "pagedown":
key = "pgdn"
if key == "pageup":
key = "pgup"
if key == "arrows":
for key in ("up", "down", "left", "right"):
self.man_keys.add(key)
else:
self.man_keys.add(key)
if len(key) == 1:
# Letters should be accepted in
# both lower and upper case
self.man_keys.add(key.upper())
if re.search(r'^\.SH', line):
state = "before"
elif state == "commands":
m = re.search(r'^\.BI? ([^ (]*)', line)
if m:
self.man_commands.add(m.group(1))
if re.search(r'^\.SS Default Bindings', line):
state = "bindings"
elif state == "bindings":
m = re.search(r'^ *(bind .*)$', line)
if m:
binding = m.group(1)
binding = binding.replace("\\&", "")
binding = re.sub(r' +', r' ', binding)
self.man_bindings.add(binding)
if re.search(r'Previous versions of xpdf', line):
state = "before"
assert self.man_args != set()
assert self.man_resources != set()
assert self.man_commands != set()
assert self.man_config != set()
assert self.man_keys != set()
assert self.man_bindings != set()
def read_xpdfrc_man(self):
config = set()
with open(os.path.join(self.dirname, "doc", "xpdfrc.5")) as f:
state = "before"
for line in f.readlines():
line = line.rstrip()
if state == "before":
if re.search(r'^The following sections list all', line):
state = "config"
elif state == "config":
m = re.search(r'^\.B[IR]? ([^ ]*)', line)
if m and m.group(1) != "xpdf":
config.add(m.group(1))
if re.search(r'^\.SH OBSOLETE CONFIG', line):
state = "obsolete"
if re.search(r'^\.SH EXAMPLES', line):
break
elif state == "obsolete":
m = re.search(r'^\.B[IR]? ([^ ]*)', line)
if m:
self.man_config_obsolete.add(m.group(1))
if re.search(r'^\.SH EXAMPLES', line):
break
assert config != set()
self.man_config.update(config)
# Old versions didn't list obsolete options
if self.dirname == ".":
assert self.man_config_obsolete != set()
def convert_binding(arg_string):
"""Convert KeyBinding constructor args to a bind command."""
args = [arg.strip() for arg in arg_string.split(",")]
def lower_first(s):
return s[:1].lower() + s[1:]
def unquote(s, quote):
assert s[0] == quote and s[-1] == quote
return s[1:-1]
cmd = ["bind"]
if args[0].startswith("xpdfKeyCode"):
key = lower_first(args[0][11:])
if key in ("pgUp", "pgDn"):
key = key.lower()
else:
key = unquote(args[0], "'")
if key == " ":
key = "space"
assert args[1].startswith("xpdfKeyMod")
mod = lower_first(args[1][10:])
if mod != "none":
key = mod + "-" + key
cmd.append(key)
assert args[2].startswith("xpdfKeyContext")
cmd.append(lower_first(args[2][14:]))
for arg in args[3:]:
cmd.append(unquote(arg, '"'))
return " ".join(cmd)
rc = 0
def warn(*s):
"""Complain about something and ensure we exit non-zero."""
print(*s, file=sys.stderr)
global rc
rc = 1
# The lists of documentable things we've gathered above
item_types = {
"args": "Command-line arg",
"args_obsolete": "Obsolete command-line arg",
"resources": "X resource",
"config": "Config file option",
"config_obsolete": "Obsolete config file option",
"commands": "Command",
"keys": "Key",
"bindings": "Default binding",
}
# Check current documentation is consistent with code
current = XPDFTree(".")
for item_type, desc in sorted(item_types.items()):
from_code = getattr(current, "code_" + item_type)
from_man = getattr(current, "man_" + item_type)
for item in sorted(from_code):
if not item in from_man:
warn(desc + " in code but not man:", item)
for item in sorted(from_man):
if not item in from_code:
warn(desc + " in man but not code:", item)
# We don't care about bindings being to the same commands as older versions;
# checking keys will tell us if any have been removed.
del item_types["bindings"]
# Check code is consistent with older versions of the code
all_items = {}
for version, srcdir in old_srcdirs.items():
old = XPDFTree(srcdir)
for item_type, desc in item_types.items():
from_old = all_items.setdefault(item_type, set())
from_old.update(getattr(old, "code_" + item_type))
for item_type, desc in item_types.items():
if item_type.endswith("_obsolete"):
continue
from_code = getattr(current, "code_" + item_type)
from_obsolete = getattr(current, "code_" + item_type + "_obsolete", set())
from_old = all_items[item_type]
for item in sorted(from_old):
# Each item must either be implemented, or be documented as obsolete
# (and we checked the documentation matched the code above)
if not (item in from_code or item in from_obsolete):
warn(desc + " in old code but not current code:", item)
sys.exit(rc)