This Trac instance is not used for development anymore!

We migrated our development workflow to git and Gitea.
To test the future redirection, replace trac by ariadne in the page URL.

source: ps/trunk/source/tools/xmlvalidator/validate_grammar.py

Last change on this file was 26350, checked in by Stan, 3 years ago

Replace checkrefs.pl by a python script. This makes it easier to run on Windows for non technical persons.

  • Add support for tips
  • Fix other scripts not writing to the correct output (they were writing info messages to stderr)

Based on a patch by: @mammadori and @cyrille

Differential Revision: https://code.wildfiregames.com/D3213

  • Property svn:eol-style set to native
File size: 9.0 KB
RevLine 
[26334]1#!/usr/bin/env python3
2from argparse import ArgumentParser
3from pathlib import Path
4from os.path import sep, join, realpath, exists, basename, dirname
5from json import load, loads
6from re import split, match
[26350]7from logging import getLogger, StreamHandler, INFO, WARNING, Filter, Formatter
[26334]8import lxml.etree
[26350]9import sys
[26334]10
[26350]11class SingleLevelFilter(Filter):
12 def __init__(self, passlevel, reject):
13 self.passlevel = passlevel
14 self.reject = reject
15
16 def filter(self, record):
17 if self.reject:
18 return (record.levelno != self.passlevel)
19 else:
20 return (record.levelno == self.passlevel)
21
[26334]22class VFS_File:
23 def __init__(self, mod_name, vfs_path):
24 self.mod_name = mod_name
25 self.vfs_path = vfs_path
26
27class RelaxNGValidator:
28 def __init__(self, vfs_root, mods=None, verbose=False):
29 self.mods = mods if mods is not None else []
30 self.vfs_root = Path(vfs_root)
31 self.__init_logger
32 self.verbose = verbose
33
34 @property
35 def __init_logger(self):
36 logger = getLogger(__name__)
37 logger.setLevel(INFO)
38 # create a console handler, seems nicer to Windows and for future uses
[26350]39 ch = StreamHandler(sys.stdout)
[26334]40 ch.setLevel(INFO)
41 ch.setFormatter(Formatter('%(levelname)s - %(message)s'))
[26350]42 f1 = SingleLevelFilter(INFO, False)
43 ch.addFilter(f1)
[26334]44 logger.addHandler(ch)
[26350]45 errorch = StreamHandler(sys.stderr)
46 errorch.setLevel(WARNING)
47 errorch.setFormatter(Formatter('%(levelname)s - %(message)s'))
48 logger.addHandler(errorch)
[26334]49 self.logger = logger
50
51 def run (self):
52 self.validate_actors()
53 self.validate_variants()
54 self.validate_guis()
55 self.validate_maps()
56 self.validate_materials()
57 self.validate_particles()
58 self.validate_simulation()
59 self.validate_soundgroups()
60 self.validate_terrains()
61 self.validate_textures()
62
63 def main(self):
64 """ Program entry point, parses command line arguments and launches the validation """
65 # ordered uniq mods (dict maintains ordered keys from python 3.6)
66 self.logger.info(f"Checking {'|'.join(self.mods)}'s integrity.")
67 self.logger.info(f"The following mods will be loaded: {'|'.join(self.mods)}.")
68 self.run()
69
70 def find_files(self, vfs_root, mods, vfs_path, *ext_list):
71 """
72 returns a list of 2-size tuple with:
73 - Path relative to the mod base
74 - full Path
75 """
76 full_exts = ['.' + ext for ext in ext_list]
77
78 def find_recursive(dp, base):
79 """(relative Path, full Path) generator"""
80 if dp.is_dir():
81 if dp.name != '.svn' and dp.name != '.git' and not dp.name.endswith('~'):
82 for fp in dp.iterdir():
83 yield from find_recursive(fp, base)
84 elif dp.suffix in full_exts:
85 relative_file_path = dp.relative_to(base)
86 yield (relative_file_path, dp.resolve())
87 return [(rp, fp) for mod in mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]
88
89 def validate_actors(self):
90 self.logger.info('Validating actors...')
91 files = self.find_files(self.vfs_root, self.mods, 'art/actors/', 'xml')
92 self.validate_files('actors', files, 'art/actors/actor.rng')
93
94 def validate_variants(self):
95 self.logger.info("Validating variants...")
96 files = self.find_files(self.vfs_root, self.mods, 'art/variants/', 'xml')
97 self.validate_files('variant', files, 'art/variants/variant.rng')
98
99 def validate_guis(self):
100 self.logger.info("Validating gui files...")
101 pages = [file for file in self.find_files(self.vfs_root, self.mods, 'gui/', 'xml') if match(r".*[\\\/]page(_[^.\/\\]+)?\.xml$", str(file[0]))]
102 self.validate_files('gui page', pages, 'gui/gui_page.rng')
103 xmls = [file for file in self.find_files(self.vfs_root, self.mods, 'gui/', 'xml') if not match(r".*[\\\/]page(_[^.\/\\]+)?\.xml$", str(file[0]))]
104 self.validate_files('gui xml', xmls, 'gui/gui.rng')
105
106 def validate_maps(self):
107 self.logger.info("Validating maps...")
108 files = self.find_files(self.vfs_root, self.mods, 'maps/scenarios/', 'xml')
109 self.validate_files('map', files, 'maps/scenario.rng')
110 files = self.find_files(self.vfs_root, self.mods, 'maps/skirmishes/', 'xml')
111 self.validate_files('map', files, 'maps/scenario.rng')
112
113 def validate_materials(self):
114 self.logger.info("Validating materials...")
115 files = self.find_files(self.vfs_root, self.mods, 'art/materials/', 'xml')
116 self.validate_files('material', files, 'art/materials/material.rng')
117
118 def validate_particles(self):
119 self.logger.info("Validating particles...")
120 files = self.find_files(self.vfs_root, self.mods, 'art/particles/', 'xml')
121 self.validate_files('particle', files, 'art/particles/particle.rng')
122
123 def validate_simulation(self):
124 self.logger.info("Validating simulation...")
125 file = self.find_files(self.vfs_root, self.mods, 'simulation/data/pathfinder', 'xml')
126 self.validate_files('pathfinder', file, 'simulation/data/pathfinder.rng')
127 file = self.find_files(self.vfs_root, self.mods, 'simulation/data/territorymanager', 'xml')
128 self.validate_files('territory manager', file, 'simulation/data/territorymanager.rng')
129
130 def validate_soundgroups(self):
131 self.logger.info("Validating soundgroups...")
132 files = self.find_files(self.vfs_root, self.mods, 'audio/', 'xml')
133 self.validate_files('sound group', files, 'audio/sound_group.rng')
134
135 def validate_terrains(self):
136 self.logger.info("Validating terrains...")
137 terrains = [file for file in self.find_files(self.vfs_root, self.mods, 'art/terrains/', 'xml') if 'terrains.xml' in str(file[0])]
138 self.validate_files('terrain', terrains, 'art/terrains/terrain.rng')
139 terrains_textures = [file for file in self.find_files(self.vfs_root, self.mods, 'art/terrains/', 'xml') if 'terrains.xml' not in str(file[0])]
140 self.validate_files('terrain texture', terrains_textures, 'art/terrains/terrain_texture.rng')
141
142 def validate_textures(self):
143 self.logger.info("Validating textures...")
144 files = [file for file in self.find_files(self.vfs_root, self.mods, 'art/textures/', 'xml') if 'textures.xml' in str(file[0])]
145 self.validate_files('texture', files, 'art/textures/texture.rng')
146
147 def get_physical_path(self, mod_name, vfs_path):
148 return realpath(join(self.vfs_root, mod_name, vfs_path))
149
150 def get_relaxng_file(self, schemapath):
151 """We look for the highest priority mod relax NG file"""
152 for mod in self.mods:
153 relax_ng_path = self.get_physical_path(mod, schemapath)
154 if exists(relax_ng_path):
155 return relax_ng_path
156
157 return ""
158
159 def validate_files(self, name, files, schemapath):
160 relax_ng_path = self.get_relaxng_file(schemapath)
161 if relax_ng_path == "":
162 self.logger.warning(f"Could not find {schemapath}")
163 return
164
165 data = lxml.etree.parse(relax_ng_path)
166 relaxng = lxml.etree.RelaxNG(data)
167 error_count = 0
168 for file in sorted(files):
169 try:
170 doc = lxml.etree.parse(str(file[1]))
171 relaxng.assertValid(doc)
172 except Exception as e:
173 error_count = error_count + 1
174 self.logger.error(f"{file[1]}: " + str(e))
175
176 if self.verbose:
177 self.logger.info(f"{error_count} {name} validation errors")
178 elif error_count > 0:
179 self.logger.error(f"{error_count} {name} validation errors")
180
181
182def get_mod_dependencies(vfs_root, *mods):
183 modjsondeps = []
184 for mod in mods:
185 mod_json_path = Path(vfs_root) / mod / 'mod.json'
186 if not exists(mod_json_path):
187 continue
188
189 with open(mod_json_path, encoding='utf-8') as f:
190 modjson = load(f)
191 # 0ad's folder isn't named like the mod.
192 modjsondeps.extend(['public' if '0ad' in dep else dep for dep in modjson.get('dependencies', [])])
193 return modjsondeps
194
195if __name__ == '__main__':
196 script_dir = dirname(realpath(__file__))
197 default_root = join(script_dir, '..', '..', '..', 'binaries', 'data', 'mods')
198 ap = ArgumentParser(description="Validates XML files againt their Relax NG schemas")
199 ap.add_argument('-r', '--root', action='store', dest='root', default=default_root)
200 ap.add_argument('-v', '--verbose', action='store_true', default=True,
201 help="Log validation errors.")
202 ap.add_argument('-m', '--mods', metavar="MOD", dest='mods', nargs='+', default=['public'],
203 help="specify which mods to check. Default to public and mod.")
204 args = ap.parse_args()
205 mods = list(dict.fromkeys([*args.mods, *get_mod_dependencies(args.root, *args.mods), 'mod']).keys())
206 relax_ng_validator = RelaxNGValidator(args.root, mods=mods, verbose=args.verbose)
207 relax_ng_validator.main()
Note: See TracBrowser for help on using the repository browser.