Thu, 26 Aug 2021 19:36:44 +0300
Merge commit
colours.py | file | annotate | diff | comparison | revisions | |
ldcheck.py | file | annotate | diff | comparison | revisions |
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,1 @@ +8c2ca8d368d4c17c3d46f0ee3c5db562cf578bb5 1.0
--- a/LICENSE Thu Aug 26 19:16:25 2021 +0300 +++ b/LICENSE Thu Aug 26 19:36:44 2021 +0300 @@ -1,4 +1,4 @@ -Copyright (c) 2019, Teemu Piippo +Copyright (c) 2019 - 2020, Teemu Piippo All rights reserved. Redistribution and use in source and binary forms, with or without
--- a/colours.py Thu Aug 26 19:16:25 2021 +0300 +++ b/colours.py Thu Aug 26 19:36:44 2021 +0300 @@ -19,26 +19,6 @@ @property def is_direct_colour(self): return self.index >= 0x2000000 - @property - def is_ldconfig_colour(self): - return self.index in ldconfig_colour_data - @property - def name(self): - if self.is_ldconfig_colour: - return ldconfig_colour_data[self.index]['name'] - else: - return str(self) - @property - def face_colour(self): - if self.is_ldconfig_colour: - return ldconfig_colour_data[self.index]['value'] - elif self.is_direct_colour: - return '#%06X' % (self.index & 0xffffff) - else: - return '#000000' - @property - def is_valid(self): - return self.is_ldconfig_colour or self.is_direct_colour def __eq__(self, other): return self.index == other.index def __lt__(self, other): @@ -95,12 +75,9 @@ colour = parse_ldconfig_ldr_line(line) yield (colour['code'], colour) -# LDConfig lookup table -ldconfig_colour_data = {} - def load_colours(ldconfig_ldr): ''' Loads colours. Expects a file pointer to LDConfig.ldr as the parameter. + Returns a lookup table ''' - global ldconfig_colour_data - ldconfig_colour_data = dict(parse_ldconfig_ldr(ldconfig_ldr)) + return dict(parse_ldconfig_ldr(ldconfig_ldr))
--- a/filecache.py Thu Aug 26 19:16:25 2021 +0300 +++ b/filecache.py Thu Aug 26 19:36:44 2021 +0300 @@ -40,19 +40,12 @@ self.problem = None self.has_studs = None # Whether or not it contains studs self.vertices = set() - def __init__(self, ldraw_directories): + def __init__(self, context): ''' Initializes a new subfile cache ''' self.cache = dict() - if ldraw_directories and isinstance(ldraw_directories[0], str): - self.ldraw_directories = [ - Path(os.path.expanduser(directory)) - for directory in ldraw_directories - ] - else: - from copy import copy - self.ldraw_directories = copy(ldraw_directories) + self.context = context self.reference_stack = [] def flatness_of(self, filename): ''' @@ -69,7 +62,7 @@ def find_file(self, filename): return find_ldraw_file( filename = filename, - libraries = self.ldraw_directories, + libraries = self.context.libraries, ) def prepare_file(self, filename): ''' @@ -90,10 +83,7 @@ try: path = self.find_file(filename) with path.open('rb') as file: - model = parse.read_ldraw( - file, - ldraw_directories = self.ldraw_directories, - ) + model = parse.read_ldraw(file, context = self.context) except (FileNotFoundError, IOError, PermissionError) as error: subfile.valid = False subfile.problem = str(error) @@ -129,6 +119,3 @@ for subfile_reference in model.subfile_references ) self.reference_stack.pop() - -if __name__ == '__main__': - cache = SubfileCache(ldraw_directories = ["~/ldraw"])
--- a/geometry.py Thu Aug 26 19:16:25 2021 +0300 +++ b/geometry.py Thu Aug 26 19:36:44 2021 +0300 @@ -195,20 +195,16 @@ def perimeter_lines(self): for v1, v2 in pairs(self.vertices): yield LineSegment(v1, v2) - @property - def angles(self): - from math import acos, isclose + def angle_cosines(self): + from math import isclose for v1, v2, v3 in pairs(self.vertices, count = 3): vec1 = (position_vector(v3) - position_vector(v2)).normalized() vec2 = (position_vector(v1) - position_vector(v2)).normalized() if not isclose(vec1.length(), 0) and not isclose(vec2.length(), 0): cosine = dot_product(vec1, vec2) / vec1.length() / vec2.length() - yield acos(cosine) - @property - def smallest_angle(self): - return min( - angle_magnitude_key(angle) - for angle in self.angles) + yield cosine + else: + yield 1 # cos(0) @property def hairline_ratio(self): lengths = [line.length for line in self.perimeter_lines]
--- a/header.py Thu Aug 26 19:16:25 2021 +0300 +++ b/header.py Thu Aug 26 19:36:44 2021 +0300 @@ -3,6 +3,10 @@ import datetime class Header: + ''' + Result type of header processing, this contains all the header + information. + ''' def __init__(self): self.description = None self.name = None @@ -17,18 +21,27 @@ self.keywords = '' self.cmdline = None self.history = [] - self.first_occurrence = dict() + from collections import defaultdict + self.occurrences = defaultdict(list) @property def valid(self): return True @property def effective_filetype(self): + ''' + What's the effective file type? The "Unofficial_" prefix is + left out. + ''' if self.filetype.startswith('Unofficial_'): return self.filetype.rsplit('Unofficial_')[1] else: return self.filetype @property def effective_category(self): + ''' + Returns the category of the part. Leading punctuation marks + are ignored. + ''' if self.category: return self.category else: @@ -39,6 +52,11 @@ return category class BadHeader: + ''' + If header processing fails this object is returned as the resulting + header instead. It contains the details of where the header could not + be understood and why. + ''' def __init__(self, index, reason): self.index = index self.reason = reason @@ -57,9 +75,14 @@ and entry.text == "BFC INVERTNEXT" def is_suitable_header_object(entry): + ''' + Is the given object something that we can consider to be + part of the header? + ''' if is_invertnext(entry): - # BFC INVERTNEXT is not a header command anymore. + # It's BFC INVERTNEXT, that's not a header command. return False + # Check if it's one of the functional linetypes return not any( isinstance(entry, linetype) for linetype in [ @@ -74,6 +97,9 @@ ) class HeaderError(Exception): + ''' + An error raised during header parsing + ''' def __init__(self, index, reason): self.index, self.reason = index, reason def __repr__(self): @@ -86,6 +112,9 @@ return reason class HistoryEntry: + ''' + Represents a single !HISTORY entry + ''' def __init__(self, date, user, text): self.date, self.user, self.text = date, user, text def __repr__(self): @@ -111,9 +140,14 @@ self.skip_to_next() result.name = self.parse_pattern(r'^Name: (.+)$', 'name')[0] self.skip_to_next() - result.author, result.username = self.parse_pattern(r'^Author: (?:([^ \[]*[^\[]+) )?(?:\[([^\]]+)\])?', 'author') + # Parse author line + result.author, result.username = self.parse_pattern(r'^Author: (?:([^\[]+))?(?:\[([^\]]+)\])?', 'author') + if isinstance(result.author, str): + # clean leading spaces + result.author = str.strip(result.author) if not result.author and not result.username: self.parse_error('author line does not contain a name nor username') + # use more patterns to parse the rest of the header for header_entry in self.get_more_header_stuff(): if self.try_to_match( r'^!LDRAW_ORG ' \ @@ -179,24 +213,33 @@ self.parse_error('LDRAW_ORG line is missing') return { 'header': result, - 'end-index': self.cursor + 1, + 'end-index': self.cursor + 1, # record where the header ended } def parse_error(self, message): raise HeaderError(index = self.cursor, reason = message) def get_more_header_stuff(self): + ''' + Iterates through the header and yields metacommand entries + one after the other. + ''' self.cursor += 1 new_cursor = self.cursor while new_cursor < len(self.model_body): entry = self.model_body[new_cursor] if not is_suitable_header_object(entry): + # looks like the header ended break if isinstance(entry, linetypes.MetaCommand): self.cursor = new_cursor yield entry new_cursor += 1 def skip_to_next(self, *, spaces_expected = 0): + ''' + Skip to the next header line. + ''' while True: if self.cursor + 1 >= len(self.model_body): + # wound up past the end of model self.parse_error('file does not have a proper header') self.cursor += 1 entry = self.model_body[self.cursor] @@ -205,21 +248,32 @@ if isinstance(entry, linetypes.MetaCommand): return def try_to_match(self, pattern, patterntype): + ''' + Tries to parse the specified pattern and to store the groups in + self.groups. Returns whether or not this succeeded. + ''' try: self.groups = self.parse_pattern(pattern, patterntype) return True except: return False def current(self): + ''' + Returns the text of the header line we're currently processing. + ''' entry = self.model_body[self.cursor] assert isinstance(entry, linetypes.MetaCommand) return entry.text def parse_pattern(self, pattern, description): + ''' + Matches the current header line against the specified pattern. + If not, raises an exception. See try_to_match for a softer wrapper + that does not raise exceptions. + ''' match = re.search(pattern, self.current()) if match: self.order.append(description) - if description not in self.result.first_occurrence: - self.result.first_occurrence[description] = self.cursor + list.append(self.result.occurrences[description], self.cursor) return match.groups() else: self.parse_error(str.format("couldn't parse {}", description))
--- a/ldcheck.py Thu Aug 26 19:16:25 2021 +0300 +++ b/ldcheck.py Thu Aug 26 19:36:44 2021 +0300 @@ -1,14 +1,29 @@ #!/usr/bin/env python3 +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py import sys if sys.version_info < (3, 4): +======= +import argparse +from sys import version_info +if version_info < (3, 4): +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py raise RuntimeError('Python 3.4 or newer required') +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py from colours import load_colours +======= + +appname = 'ldcheck' +version = (1, 0, 9999) +version_string = str.join('.', map(str, version)) + +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py from geometry import * from pathlib import Path import linetypes import header import parse +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py def check_library_paths(library_paths): for library_path in library_paths: if not library_path.exists(): @@ -37,7 +52,104 @@ for ldconfig_ldr_path in ldconfig_ldr_paths: with ldconfig_ldr_path.open() as ldconfig_ldr: load_colours(ldconfig_ldr) +======= +from os.path import realpath +script_directory = Path(realpath(__file__)).parent +def config_dirs(): + import appdirs + appauthor = 'Teemu Piippo' + dirs = appdirs.AppDirs(appname, appauthor) + return { + 'user': Path(dirs.user_config_dir), + 'system': Path(dirs.site_config_dir), + } + +def ldraw_dirs_from_config(): + libraries = [] + dirs = config_dirs() + for dirpath in [dirs['system'], dirs['user']]: + filename = dirpath / 'ldcheck.cfg' + from configobj import ConfigObj + config = ConfigObj(str(filename), encoding = 'UTF8') + if 'libraries' in config: + libraries = expand_paths(config['libraries']) + return libraries + +def expand_paths(paths): + return [ + Path(library).expanduser() + for library in paths + ] + +class LDrawContext: + ''' + Contains context-dependant LDraw information, like library directory + paths and the colour table. + ''' + def __init__(self, libraries = None): + self._libraries = libraries + if not self._libraries: + self._libraries = ldraw_dirs_from_config() + self.ldconfig_colour_data = self.load_ldconfig_ldr() + self.check_library_paths() + @property + def libraries(self): + return self._libraries + @property + def colours(self): + return self.ldconfig_colour_data + def ldconfig_ldr_discovery(self): + for library_path in self.libraries: + yield from [ + library_path / path + for path in ['LDConfig.ldr', 'ldconfig.ldr'] + if (library_path / path).is_file() + ] + def load_ldconfig_ldr(self): + from colours import load_colours + for ldconfig_ldr_path in self.ldconfig_ldr_discovery(): + with open(ldconfig_ldr_path) as ldconfig_ldr: + return load_colours(ldconfig_ldr) + def check_library_paths(self): + from sys import stderr + problems = False + have_paths = False + for library_path in self.libraries: + have_paths = True + if not library_path.exists(): + problems = True + print(str.format( + 'Library path {} does not exist', + library_path, + ), file = stderr) + elif not library_path.exists(): + problems = True + print(str.format( + 'Library path {} is not a directory', + library_path, + ), file = stderr) + if not have_paths: + raise RuntimeError('no LDraw library paths specified') + def is_ldconfig_colour(self, colour): + return colour.index in self.colours + def colour_name(self, colour): + if self.is_ldconfig_colour(colour): + return self.colours[self.index]['name'] + else: + return str(colour) + def colour_face(self, colour): + if self.is_ldconfig_colour(colour): + return self.colours[self.index]['value'] + elif colour.is_direct_colour: + return '#%06X' % (self.index & 0xffffff) + else: + return '#000000' + def is_valid_colour(self, colour): + return self.is_ldconfig_colour(colour) or colour.is_direct_colour +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py + +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py def parse_commandline_arguments(): import os rcpath = Path(os.path.expanduser('~/.config/ldcheckrc')) @@ -60,6 +172,51 @@ message = warning_type.placeholder_message(), )) sys.exit(0) +======= +class ListProblemsAction(argparse.Action): + def __init__(self, option_strings, dest, nargs = None, **kwargs): + super().__init__(option_strings, dest, nargs = 0, **kwargs) + def __call__(self, *args, **kwargs): + import testsuite + from sys import exit + from re import sub + test_suite = testsuite.load_tests() + for warning_type in testsuite.all_problem_types(test_suite): + print(str.format('{name}: {severity}: "{message}"', + name = warning_type.name, + severity = warning_type.severity, + message = warning_type.placeholder_message(), + )) + exit(0) + +def format_report(report, model, test_suite, *, use_colors = True): + from testsuite import problem_text + messages = [] + for problem in report['problems']: + text_colour = '' + if use_colors: + if problem.severity == 'hold': + text_colour = colorama.Fore.LIGHTRED_EX + elif problem.severity == 'warning': + text_colour = colorama.Fore.LIGHTBLUE_EX + ldraw_code = model.body[problem.body_index].textual_representation() + message = str.format( + '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' + '{colour_reset}\n\t{ldraw_code}', + text_colour = text_colour, + model_name = model.name, + line_number = problem.line_number, + problem_type = problem.severity, + message = problem_text(problem, test_suite), + colour_reset = use_colors and colorama.Fore.RESET or '', + ldraw_code = ldraw_code, + ) + messages.append(message) + return '\n'.join(messages) + +if __name__ == '__main__': + from sys import argv, stderr, exit +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py parser = argparse.ArgumentParser() parser.add_argument('filename') parser.add_argument('--list', @@ -79,6 +236,7 @@ action = 'store_true', help = 'finds a subfile by name and prints out information about it' ) +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py parser.add_argument('-l', '--library', action = 'append') arglist = rcargs + sys.argv[1:] return parser.parse_args(arglist) @@ -97,12 +255,49 @@ libraries = [Path(os.path.expanduser(library)) for library in args.library] check_library_paths(libraries) load_ldconfig(libraries) +======= + parser.add_argument('--color', + action = 'store_true', + help = 'use colors' + ) + parser.add_argument('-d', '--ldraw-dir', + nargs = '+', + help = 'specify LDraw directory path(s)', + ) + parser.add_argument('-v', '--version', + action = 'version', + version = str.format('{appname} {version}', + appname = appname, + version = version_string, + ), + ) + args = parser.parse_args() + libraries = ldraw_dirs_from_config() + if args.ldraw_dir: + libraries = expand_paths(args.ldraw_dir) + try: + context = LDrawContext(libraries) + except RuntimeError as error: + print('error:', str(error), file = stderr) + exit(1) + if args.color: + try: + import colorama + colorama.init() + except ImportError: + print('Use of --color requires the colorama module, disabling colours', file = stderr) + args.color = False +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py if args.subfile: # Subfile debug mode: searches for the specified subfile from the LDraw # libraries, opens it as if it was referenced by something and prints # out all information that is calculated from this subfile. import filecache +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py cache = filecache.SubfileCache(ldraw_directories = libraries) +======= + cache = filecache.SubfileCache(context = context) +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py subfile = cache.prepare_file(args.filename) if not subfile.valid: print(subfile.problem) @@ -111,6 +306,7 @@ print('Description:', repr(subfile.description)) print('Contains studs:', repr(subfile.has_studs)) else: +<<<<<<< /home/teemu/dev/ldcheck/ldcheck.py with open(args.filename, 'rb') as file: from os.path import basename model = parse.read_ldraw( @@ -154,3 +350,40 @@ import sys print('error:', str(e), file = sys.stderr) sys.exit(1) +======= + try: + with open(args.filename, 'rb') as file: + from os.path import basename + model = parse.read_ldraw( + file, + name = basename(args.filename), + context = context) + if args.dump: + print('header: ' + type(model.header).__name__) + for key in sorted(dir(model.header)): + if not key.startswith('__'): + print('\t' + key + ': ' + repr(getattr(model.header, key))) + for i, entry in enumerate(model.body): + if model.header.valid and i == model.header_size: + print('--------- End of header') + print(entry) + elif args.rebuild: + for entry in model.body: + print(entry.textual_representation(), end = '\r\n') + else: + from testsuite import load_tests, check_model + test_suite = load_tests() + report = check_model(model, test_suite) + print(format_report( + report, + model, + test_suite, + use_colors = args.color + )) + except FileNotFoundError: + print(str.format( + 'no such file: {filename!r}', + filename = args.filename + ), file = stderr) + exit(1) +>>>>>>> /tmp/ldcheck~other.ou_xbg_k.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/library-standards.ini Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,134 @@ +[scaling restrictions] +axle.dat = y-scaling only +axleend.dat = y-scaling only +axl?hol?.dat = y-scaling only +axlesphe.dat = no scaling +bushloc?.dat = no scaling +steerend.dat = no scaling +connect.dat = no scaling +connect?.dat = no scaling +confric?.dat = no scaling +connhol?.dat = no scaling +beamhole.dat = no scaling +peghole.dat = y-scaling only +peghole?.dat = y-scaling only +npeghol?.dat = y-scaling only +tooth*.dat = no scaling +daxle.dat = y-scaling only +daxlehole.dat = y-scaling only +daxlehub.dat = y-scaling only +dtoothc.dat = no scaling +stug?*.dat = no scaling +arm?.dat = no scaling +clh?*.dat = no scaling +4-4crh?.dat = no scaling +clip?.dat = no scaling +clip??.dat = no scaling +finger1.dat = no scaling +h[12].dat = no scaling +plug34.dat = no scaling +wpin*.dat = no scaling +bump5000.dat = no scaling +slotm.dat = no scaling +handle*.dat = no scaling +rail12v.dat = no scaling +primotop.dat = no scaling +primobot.dat = no scaling +zstud.dat = no scaling +# no scaling on studs +stud*.dat = no scaling +# allow y-scaling only on stud4 for historical reasons +stud4.dat = y-scaling only +# allow -1 y-scaling only on some stud primitives for historical reasons +stud3.dat = stud3-like scaling +stud4s.dat = stud3-like scaling +stud4s2.dat = stud3-like scaling +stud16.dat = stud3-like scaling +stud21a.dat = stud3-like scaling +stud22a.dat = stud3-like scaling + +[categories] +Animal = +Antenna = +Arch = +Arm = +Bar = +Baseplate = +Belville = +Boat = +Bracket = +Brick = +Car = +Clikits = +Cockpit = +Cone = +Constraction = +Constraction Accessory = +Container = +Conveyor = +Crane = +Cylinder = +Dish = +Door = +Electric = +Exhaust = +Fence = +Figure = +Figure Accessory = +Flag = +Forklift = +Freestyle = +Garage = +Glass = +Grab = +Hinge = +Homemaker = +Hose = +Ladder = +Lever = +Magnet = +Minifig = +Minifig Accessory = +Minifig Footwear = +Minifig Headwear = +Minifig Hipwear = +Minifig Neckwear = +Monorail = +Obsolete = +Panel = +Plane = +Plant = +Plate = +Platform = +Propellor = +Rack = +Roadsign = +Rock = +Scala = +Screw = +Sheet Cardboard = +Sheet Fabric = +Sheet Plastic = +Slope = +Sphere = +Staircase = +Sticker = +Support = +Tail = +Tap = +Technic = +Tile = +Tipper = +Tractor = +Trailer = +Train = +Turntable = +Tyre = +Vehicle = +Wedge = +Wheel = +Winch = +Window = +Windscreen = +Wing = +Znap =
--- a/librarystandards.py Thu Aug 26 19:16:25 2021 +0300 +++ b/librarystandards.py Thu Aug 26 19:36:44 2021 +0300 @@ -2,7 +2,7 @@ from pathlib import Path from os.path import dirname -ini_path = Path(dirname(__file__)) / 'tests' / 'library-standards.ini' +ini_path = Path(dirname(__file__)) / 'library-standards.ini' library_standards = ConfigParser() with ini_path.open() as file:
--- a/parse.py Thu Aug 26 19:16:25 2021 +0300 +++ b/parse.py Thu Aug 26 19:36:44 2021 +0300 @@ -9,13 +9,13 @@ class Model: def __init__( - self, header, body, *, ldraw_directories, \ + self, header, body, *, context, \ header_size = 0, line_ending_errors = None ): self.header = header self.body = body self.header_size = header_size - self.ldraw_directories = ldraw_directories + self.context = context # contains stuff like library paths self.line_ending_errors = line_ending_errors def filter_by_type(self, type): yield from [ @@ -39,7 +39,15 @@ def has_header(self): return self.header and not isinstance(self.header, header.BadHeader) def find_first_header_object(self, object_type): - return self.body[self.header.first_occurrence[object_type]] + return self.find_header_object(object_type, 0) + def find_header_object(self, object_type, n): + try: + return self.body[self.header.occurrences[object_type][n]] + except IndexError: + raise KeyError(str.format( + '{type} not found in header', + type = object_type + )) def model_vertices( model, @@ -50,7 +58,7 @@ transformation_matrix = complete_matrix(Matrix3x3(), Vertex(0, 0, 0)) if file_cache is None: import filecache - file_cache = filecache.SubfileCache(model.ldraw_directories) + file_cache = filecache.SubfileCache(ldraw_directories = model.context.libraries) for element in model.body: if isinstance(element, linetypes.BasePolygon): for point in element.geometry.vertices: @@ -65,7 +73,7 @@ point @= matrix_4x4 yield point @ transformation_matrix -def read_ldraw(file, *, ldraw_directories, name = ''): +def read_ldraw(file, *, context, name = ''): line_ending_errors = { 'count': 0, 'first-at': None, @@ -92,7 +100,7 @@ header = header_object, body = model_body, header_size = end, - ldraw_directories = ldraw_directories, + context = context, line_ending_errors = line_ending_errors, ) model.name = name
--- a/templates/webfront.html Thu Aug 26 19:16:25 2021 +0300 +++ b/templates/webfront.html Thu Aug 26 19:36:44 2021 +0300 @@ -7,7 +7,7 @@ </head> <body> <div class="top-form"> - <h1>Check your part here</h1> + <h1>{{appname}} {{version}} - Check your part here</h1> <form method="post" enctype="multipart/form-data"> <input type="file" name="file"/> <input type="submit" /> @@ -15,6 +15,10 @@ </div> <div class="report-container"> +{% if is_debug %} +<p>Note: this is an unreleased version, everything may not work as intended.</p> +{% endif %} + {% if report %} <h1>{{name}}</h1> {% if report['problems'] %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/headertest.py Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,151 @@ +from testsuite import problem_type, report_problem +import linetypes + +@problem_type('bad-header', + severity = 'hold', + message = lambda reason: str.format('bad header: {}', reason), +) +@problem_type('no-license-set', + severity = 'hold', + message = 'no license set', +) +@problem_type('non-ca-license', + severity = 'hold', + message = 'no new non-CA-submits are accepted', +) +def bad_header(model): + import header + ca_license = 'Redistributable under CCAL version 2.0 : see CAreadme.txt' + if isinstance(model.header, header.BadHeader): + yield report_problem( + 'bad-header', + bad_object = model.body[model.header.index], + reason = model.header.reason, + ) + elif not model.header.license: + yield report_problem( + 'no-license-set', + bad_object = model.body[0], + ) + elif model.header.license != ca_license: + yield report_problem( + 'non-ca-license', + bad_object = model.find_first_header_object('license'), + ) + +@problem_type('bfc-nocertify', + severity = 'hold', + message = 'all new parts must be BFC certified', +) +def nocertify_test(model): + import header + if model.header.valid and model.header.bfc == 'NOCERTIFY': + yield report_problem( + 'bfc-nocertify', + bad_object = model.find_first_header_object('bfc'), + ) + +@problem_type('physical-colour-part', + severity = 'hold', + message = 'no new physical colour parts are accepted', +) +def physical_colours_test(model): + if model.header.valid and 'Physical_Colour' in model.header.qualifiers: + yield report_problem( + 'physical-colour-part', + bad_object = model.find_first_header_object('part type'), + ) + +@problem_type('unofficial-part', + severity = 'hold', + message = 'new parts must be unofficial', +) +def unofficiality_test(model): + if model.header.valid and not model.header.filetype.startswith('Unofficial_'): + yield report_problem( + 'unofficial-part', + bad_object = model.find_first_header_object('part type') + ) + +@problem_type('primitive-non-ccw', + severity = 'hold', + message = 'primitives must have CCW winding', +) +@problem_type('no-bfc-line', + severity = 'hold', + message = 'BFC declaration is missing', +) +def header_bfc_test(model): + if model.header.valid and not model.header.bfc: + yield report_problem( + 'no-bfc-line', + bad_object = model.body[0], + ) + elif model.header.valid \ + and model.header.filetype.endswith('Primitive') \ + and model.header.bfc != 'CERTIFY CCW': + yield report_problem( + 'primitive-non-ccw', + bad_object = model.find_first_header_object('bfc'), + ) + +@problem_type('keywords-for-nonparts', + severity = 'warning', + message = lambda type: str.format( + 'keywords are not allowed for {type} files', + type = type, + ), +) +def keywords_tests(model): + if model.header.valid: + if model.header.keywords \ + and model.header.effective_filetype != 'Part': + yield report_problem( + 'keywords-for-nonparts', + bad_object = model.find_first_header_object('keywords'), + type = model.header.effective_filetype, + ) + +@problem_type('bad-colour-24-nonline', + severity = 'hold', + message = 'colour 24 used on non-lines', +) +@problem_type('bad-colour-24-line', + severity = 'hold', + message = 'line with colour other than 24', +) +def colour_24_test(model): + for element in model.body: + if hasattr(element, 'colour'): + is_line = isinstance(element, linetypes.LineSegment) + if not is_line and element.colour.index == 24: + yield report_problem('bad-colour-24-nonline', bad_object = element) + if is_line and element.colour.index != 24: + yield report_problem('bad-colour-24-line', bad_object = element) + +@problem_type('moved-to-with-extension', + severity = 'hold', + message = 'moved-to files must not contain the ' + '".dat"-extension in the description', +) +def moved_to_with_extension_test(model): + if model.header.valid \ + and model.header.description.startswith('~Moved to') \ + and '.dat' in model.header.description: + yield report_problem( + 'moved-to-with-extension', + bad_object = model.body[0], + ) + +manifest = { + 'tests': [ + bad_header, + nocertify_test, + physical_colours_test, + unofficiality_test, + header_bfc_test, + keywords_tests, + colour_24_test, + moved_to_with_extension_test, + ], +}
--- a/tests/library-standards.ini Thu Aug 26 19:16:25 2021 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,134 +0,0 @@ -[scaling restrictions] -axle.dat = y-scaling only -axleend.dat = y-scaling only -axl?hol?.dat = y-scaling only -axlesphe.dat = no scaling -bushloc?.dat = no scaling -steerend.dat = no scaling -connect.dat = no scaling -connect?.dat = no scaling -confric?.dat = no scaling -connhol?.dat = no scaling -beamhole.dat = no scaling -peghole.dat = y-scaling only -peghole?.dat = y-scaling only -npeghol?.dat = y-scaling only -tooth*.dat = no scaling -daxle.dat = y-scaling only -daxlehole.dat = y-scaling only -daxlehub.dat = y-scaling only -dtoothc.dat = no scaling -stug?*.dat = no scaling -arm?.dat = no scaling -clh?*.dat = no scaling -4-4crh?.dat = no scaling -clip?.dat = no scaling -clip??.dat = no scaling -finger1.dat = no scaling -h[12].dat = no scaling -plug34.dat = no scaling -wpin*.dat = no scaling -bump5000.dat = no scaling -slotm.dat = no scaling -handle*.dat = no scaling -rail12v.dat = no scaling -primotop.dat = no scaling -primobot.dat = no scaling -zstud.dat = no scaling -# no scaling on studs -stud*.dat = no scaling -# allow y-scaling only on stud4 for historical reasons -stud4.dat = y-scaling only -# allow -1 y-scaling only on some stud primitives for historical reasons -stud3.dat = stud3-like scaling -stud4s.dat = stud3-like scaling -stud4s2.dat = stud3-like scaling -stud16.dat = stud3-like scaling -stud21a.dat = stud3-like scaling -stud22a.dat = stud3-like scaling - -[categories] -Animal = -Antenna = -Arch = -Arm = -Bar = -Baseplate = -Belville = -Boat = -Bracket = -Brick = -Car = -Clikits = -Cockpit = -Cone = -Constraction = -Constraction Accessory = -Container = -Conveyor = -Crane = -Cylinder = -Dish = -Door = -Electric = -Exhaust = -Fence = -Figure = -Figure Accessory = -Flag = -Forklift = -Freestyle = -Garage = -Glass = -Grab = -Hinge = -Homemaker = -Hose = -Ladder = -Lever = -Magnet = -Minifig = -Minifig Accessory = -Minifig Footwear = -Minifig Headwear = -Minifig Hipwear = -Minifig Neckwear = -Monorail = -Obsolete = -Panel = -Plane = -Plant = -Plate = -Platform = -Propellor = -Rack = -Roadsign = -Rock = -Scala = -Screw = -Sheet Cardboard = -Sheet Fabric = -Sheet Plastic = -Slope = -Sphere = -Staircase = -Sticker = -Support = -Tail = -Tap = -Technic = -Tile = -Tipper = -Tractor = -Trailer = -Train = -Turntable = -Tyre = -Vehicle = -Wedge = -Wheel = -Winch = -Window = -Windscreen = -Wing = -Znap =
--- a/tests/misc.py Thu Aug 26 19:16:25 2021 +0300 +++ b/tests/misc.py Thu Aug 26 19:36:44 2021 +0300 @@ -14,7 +14,7 @@ from collections import defaultdict bad_colours = defaultdict(lambda: {'count': 0, 'first-occurrence': None}) for element in model.body: - if hasattr(element, 'colour') and not element.colour.is_valid: + if hasattr(element, 'colour') and not model.context.is_valid_colour(element.colour): bad_colours[element.colour.index]['count'] += 1 if not bad_colours[element.colour.index]['first-occurrence']: bad_colours[element.colour.index]['first-occurrence'] = element @@ -42,167 +42,6 @@ if isinstance(element, linetypes.Error) ) -@problem_type('bad-header', - severity = 'hold', - message = lambda reason: str.format('bad header: {}', reason), -) -@problem_type('no-license-set', - severity = 'hold', - message = 'no license set', -) -@problem_type('non-ca-license', - severity = 'hold', - message = 'no new non-CA-submits are accepted', -) -def bad_header(model): - import header - ca_license = 'Redistributable under CCAL version 2.0 : see CAreadme.txt' - if isinstance(model.header, header.BadHeader): - yield report_problem( - 'bad-header', - bad_object = model.body[model.header.index], - reason = model.header.reason, - ) - elif not model.header.license: - yield report_problem( - 'no-license-set', - bad_object = model.body[0], - ) - elif model.header.license != ca_license: - yield report_problem( - 'non-ca-license', - bad_object = model.find_first_header_object('license'), - ) - -@problem_type('bfc-nocertify', - severity = 'hold', - message = 'all new parts must be BFC certified', -) -def nocertify_test(model): - import header - if model.header.valid and model.header.bfc == 'NOCERTIFY': - yield report_problem( - 'bfc-nocertify', - bad_object = model.find_first_header_object('bfc'), - ) - -@problem_type('physical-colour-part', - severity = 'hold', - message = 'no new physical colour parts are accepted', -) -def physical_colours_test(model): - if model.header.valid and 'Physical_Colour' in model.header.qualifiers: - yield report_problem( - 'physical-colour-part', - bad_object = model.find_first_header_object('part type'), - ) - -@problem_type('unofficial-part', - severity = 'hold', - message = 'new parts must be unofficial', -) -def unofficiality_test(model): - if model.header.valid and not model.header.filetype.startswith('Unofficial_'): - yield report_problem( - 'unofficial-part', - bad_object = model.find_first_header_object('part type') - ) - -@problem_type('primitive-ccw', - severity = 'hold', - message = 'primitives must have CCW winding', -) -@problem_type('no-bfc-line', - severity = 'hold', - message = 'BFC declaration is missing', -) -def header_bfc_test(model): - if model.header.valid and not model.header.bfc: - yield report_problem( - 'no-bfc-line', - bad_object = model.body[0], - ) - elif model.header.valid \ - and model.header.filetype.endswith('Primitive') \ - and model.header.bfc != 'CERTIFY CCW': - yield report_problem( - 'primitive-bfc-ccw', - bad_object = model.find_first_header_object('bfc'), - ) - -@problem_type('keywords-for-nonparts', - severity = 'warning', - message = lambda type: str.format( - 'keywords are not allowed for {type} files', - type = type, - ), -) -def keywords_tests(model): - if model.header.valid: - if model.header.keywords \ - and model.header.effective_filetype != 'Part': - yield report_problem( - 'keywords-for-nonparts', - bad_object = model.find_first_header_object('keywords'), - type = model.header.effective_filetype, - ) - -@problem_type('bad-colour-24-nonline', - severity = 'hold', - message = 'colour 24 used on non-lines', -) -@problem_type('bad-colour-24-line', - severity = 'hold', - message = 'line with colour other than 24', -) -def colour_24_test(model): - for element in model.body: - if hasattr(element, 'colour'): - is_line = isinstance(element, linetypes.LineSegment) - if not is_line and element.colour.index == 24: - yield report_problem('bad-colour-24-nonline', bad_object = element) - if is_line and element.colour.index != 24: - yield report_problem('bad-colour-24-line', bad_object = element) - -@problem_type('moved-to-with-extension', - severity = 'hold', - message = 'moved-to files must not contain the ' - '".dat"-extension in the description', -) -def moved_to_with_extension_test(model): - if model.header.valid \ - and model.header.description.startswith('~Moved to') \ - and '.dat' in model.header.description: - yield report_problem( - 'moved-to-with-extension', - bad_object = model.body[0], - ) - -@problem_type('bfc-invertnext-not-on-subfile', - severity = 'hold', - message = '"BFC INVERTNEXT" not followed by a type-1 line', -) -def bfc_invertnext_not_on_subfile_test(model): - def get_invertnexts(model): - yield from [ - (index, element) - for index, element in enumerate(model.body) - if isinstance(element, linetypes.MetaCommand) \ - and element.text == 'BFC INVERTNEXT' - ] - def has_subfile_after_invertnext(index): - index_subfile = index + 1 # subfile reference should be on the next line - if index_subfile >= len(model.body): - return False # past the end... - else: - element = model.body[index_subfile] - return isinstance(element, linetypes.SubfileReference) - for index, invertnext in get_invertnexts(model): - if not has_subfile_after_invertnext(index): - yield report_problem('bfc-invertnext-not-on-subfile', - bad_object = model.body[index], - ) - @problem_type('unknown-metacommand', severity = 'hold', message = lambda command_text: str.format( @@ -246,18 +85,35 @@ count = model.line_ending_errors['count'], ) +@problem_type('bfc-invertnext-not-on-subfile', + severity = 'hold', + message = '"BFC INVERTNEXT" not followed by a type-1 line', +) +def bfc_invertnext_not_on_subfile_test(model): + def get_invertnexts(model): + yield from [ + (index, element) + for index, element in enumerate(model.body) + if isinstance(element, linetypes.MetaCommand) \ + and element.text == 'BFC INVERTNEXT' + ] + def has_subfile_after_invertnext(index): + index_subfile = index + 1 # subfile reference should be on the next line + if index_subfile >= len(model.body): + return False # past the end... + else: + element = model.body[index_subfile] + return isinstance(element, linetypes.SubfileReference) + for index, invertnext in get_invertnexts(model): + if not has_subfile_after_invertnext(index): + yield report_problem('bfc-invertnext-not-on-subfile', + bad_object = model.body[index], + ) + manifest = { 'tests': [ colours_test, syntax_errors, - bad_header, - nocertify_test, - physical_colours_test, - unofficiality_test, - header_bfc_test, - keywords_tests, - colour_24_test, - moved_to_with_extension_test, bfc_invertnext_not_on_subfile_test, metacommands_test, line_endings_test,
--- a/tests/quadrilaterals.py Thu Aug 26 19:16:25 2021 +0300 +++ b/tests/quadrilaterals.py Thu Aug 26 19:36:44 2021 +0300 @@ -25,14 +25,14 @@ @problem_type('skew-major', severity = 'hold', message = lambda skew_angle: - str.format('skew quadrilateral (plane angle {})', + str.format('skew (non-coplanar) quadrilateral (plane angle {})', degree_rep(skew_angle), ), ) @problem_type('skew-minor', severity = 'warning', message = lambda skew_angle: - str.format('slightly skew quadrilateral (plane angle {})', + str.format('slightly skew (non-coplanar) quadrilateral (plane angle {})', degree_rep(skew_angle), ), ) @@ -60,7 +60,7 @@ @problem_type('self-intersecting', severity = 'hold', - message = 'self-intersecting quadrilateral', + message = 'bowtie (self-intersecting) quadrilateral', ) def bowtie_test(model): for quadrilateral in model.quadrilaterals: @@ -77,31 +77,34 @@ ) break -@problem_type('collinear', severity = 'hold', message = 'collinear polygon') +@problem_type('collinear', + severity = 'hold', + message = lambda min_angle, max_angle: str.format( + 'collinear polygon (smallest interior angle {min_angle}, largest {max_angle})', + min_angle = degree_rep(min_angle), + max_angle = degree_rep(max_angle), + ), +) def collinear_test(model): + from math import radians, cos, acos + # according to the LDraw spec, all interior angles must be + # between 0.025 degrees and 179.9 degrees. Note that between 0 and pi, + # cos(x) is a downwards curve, so the minimum cosine is for the maximum + # angle. + min_cosine = cos(radians(179.9)) + max_cosine = cos(radians(0.025)) for element in model.body: if hasattr(element, 'geometry') and len(element.geometry.vertices) >= 3: - for a, b, c in pairs(element.geometry.vertices, count = 3): - if cross_product(b - a, c - a).length() < 1e-5: - yield report_problem('collinear', bad_object = element) - break - -@problem_type('hairline-polygon', - severity = 'warning', - message = lambda smallest_angle: str.format( - 'hairline polygon (smallest angle {})', - degree_rep(smallest_angle), - ), -) -def hairline_test(model): - for element in model.body: - if hasattr(element, 'geometry') and len(element.geometry.vertices) >= 3: - smallest_angle = element.geometry.smallest_angle - if smallest_angle < radians(0.5): + cosines = list(element.geometry.angle_cosines()) + if any( + not(min_cosine < cosine < max_cosine) + for cosine in cosines + ): yield report_problem( - 'hairline-polygon', + 'collinear', bad_object = element, - smallest_angle = smallest_angle, + min_angle = min(map(acos, cosines)), + max_angle = max(map(acos, cosines)), ) manifest = { @@ -110,6 +113,5 @@ concave_test, bowtie_test, collinear_test, - hairline_test, ], }
--- a/tests/subfiles.py Thu Aug 26 19:16:25 2021 +0300 +++ b/tests/subfiles.py Thu Aug 26 19:36:44 2021 +0300 @@ -16,7 +16,7 @@ yield from ( report_problem('zero-determinant', bad_object = subfile_reference) for subfile_reference in model.subfile_references - if abs(subfile_reference.matrix.determinant() - 0) < 1e-15 + if abs(subfile_reference.matrix.determinant() - 0) < 1e-5 ) def scaling_description(scaling, axes = 'xyz'): @@ -115,7 +115,7 @@ ), ) @problem_type('bad-subfile', - severity = 'hold', + severity = 'warning', message = lambda path, problem_text: str.format('cannot process subfile "{path}": {problem_text}', **locals(), @@ -162,7 +162,7 @@ Checks whether flat subfiles are scaled in the flat direction. ''' import filecache - cache = filecache.SubfileCache(model.ldraw_directories) + cache = filecache.SubfileCache(context = model.context) if model.header.valid: cache.reference_stack.append(model.header.name) failed_subfiles = set()
--- a/testsuite.py Thu Aug 26 19:16:25 2021 +0300 +++ b/testsuite.py Thu Aug 26 19:36:44 2021 +0300 @@ -129,48 +129,6 @@ message = message(**problem.args) return message -def format_report_html(report, model, test_suite): - messages = [] - for problem in report['problems']: - ldraw_code = model.body[problem.body_index].textual_representation() - message = str.format( - '<li class="{problem_type}">{model_name}:{line_number}:' - '{problem_type}: {message}<br />{ldraw_code}</li>', - model_name = model.name, - line_number = problem.line_number, - problem_type = problem.severity, - message = problem_text(problem, test_suite), - ldraw_code = ldraw_code, - ) - messages.append(message) - return '\n'.join(messages) - -def format_report(report, model, test_suite): - import colorama - colorama.init() - messages = [] - for problem in report['problems']: - if problem.severity == 'hold': - text_colour = colorama.Fore.LIGHTRED_EX - elif problem.severity == 'warning': - text_colour = colorama.Fore.LIGHTBLUE_EX - else: - text_colour = '' - ldraw_code = model.body[problem.body_index].textual_representation() - message = str.format( - '{text_colour}{model_name}:{line_number}: {problem_type}: {message}' - '{colour_reset}\n\t{ldraw_code}', - text_colour = text_colour, - model_name = model.name, - line_number = problem.line_number, - problem_type = problem.severity, - message = problem_text(problem, test_suite), - colour_reset = colorama.Fore.RESET, - ldraw_code = ldraw_code, - ) - messages.append(message) - return '\n'.join(messages) - def iterate_problems(test_suite): for test_function in test_suite['tests']: yield from test_function.ldcheck_problem_types.values() @@ -180,7 +138,12 @@ iterate_problems(test_suite), key = lambda problem_type: problem_type.name ) - + +def all_problem_type_names(test_suite): + return set( + problem_type.name + for problem_type in iterate_problems(test_suite) + ) if __name__ == '__main__': from pprint import pprint
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittest.py Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +from ldcheck import appname, version, version_string +from ldcheck import script_directory +from pathlib import Path +from parse import read_ldraw +from testsuite import load_tests, check_model, problem_text, all_problem_type_names + +def unit_test_discovery(): + ''' Yields unit test paths ''' + import os + unit_test_dir = Path(script_directory) / 'unittests' + for dirpath, dirnames, filenames in os.walk(unit_test_dir): + yield from sorted( + Path(dirpath) / filename + for filename in filenames + if filename.endswith('.test') + ) + +def parse_expectation(text): + problem_type, line_no_str = str.split(text, ':') + return problem_type, int(line_no_str) + +def load_unit_test(unit_test_path, *, name, context): + with open(unit_test_path, 'rb') as device: + import re + problem_types = set() + expecting = None + while True: + pos = device.tell() + line = bytes.decode(device.readline()) + if not line: + raise ValueError('unit test ended unexpectedly') + match = re.match('^0 Testing: (.+)', line) + if match: + set.update(problem_types, match.group(1).split()) + elif str.strip(line) == '0 Expecting: none': + expecting = set() + else: + match = re.match('^0 Expecting: (.+)', line) + if match: + if not expecting: + expecting = set() + set.update(expecting, map(parse_expectation, match.group(1).split())) + else: + device.seek(pos) + break + if not problem_types or expecting is None: + raise ValueError(str.format( + 'Unit test {name} does not have a proper manifest', + name = name, + )) + return { + 'problem_types': problem_types, + 'expecting': expecting, + 'model': read_ldraw( + device, + name = name, + context = context + ), + } + +def parse_problem(problem): + return problem.problem_class.name, problem.line_number + +def run_unit_test(unit_test_path, *, context, test_suite): + from os.path import basename + unit_test = load_unit_test( + unit_test_path, + name = basename(unit_test_path), + context = context, + ) + bad_problems = set.difference( + unit_test['problem_types'], + all_problem_type_names(test_suite) + ) + if bad_problems: + raise ValueError(str.format( + 'unknown problem type: {names}', + names = ' '.join(sorted(bad_problems)) + )) + problem_types = unit_test['problem_types'] + report = check_model(unit_test['model'], test_suite) + expected_problems = unit_test['expecting'] + problems = set( + filter( + lambda problem: problem[0] in problem_types, + map( + parse_problem, + report['problems'] + ) + ) + ) + return { + 'passed': problems == expected_problems, + 'unexpected': set.difference(problems, expected_problems), + 'missing': set.difference(expected_problems, problems), + 'problem_types': problem_types, + } + +def format_problem_tuple(problem_tuple): + return problem_tuple[0] + ':' + str(problem_tuple[1]) + +def run_unit_test_suite(): + from argparse import ArgumentParser + parser = ArgumentParser() + parser.add_argument('-d', '--debug', action = 'store_true') + args = parser.parse_args() + from ldcheck import LDrawContext + context = LDrawContext() + test_suite = load_tests() + num_tested = 0 + num_passed = 0 + all_problem_types = all_problem_type_names(test_suite) + problem_types_tested = set() + print('Running unit test suite.') + for unit_test_path in unit_test_discovery(): + try: + unit_test_report = run_unit_test( + unit_test_path, + context = context, + test_suite = test_suite + ) + except Exception as error: + if args.debug: + raise + else: + print(str.format( + 'Error running {test_name}: {error}', + test_name = unit_test_path.name, + error = str(error), + )) + else: + print(str.format( + '{name}: {verdict}', + name = unit_test_path.name, + verdict = ('FAILED', 'PASSED')[unit_test_report['passed']], + )) + num_tested += 1 + num_passed += int(unit_test_report['passed']) + set.update(problem_types_tested, unit_test_report['problem_types']) + if not unit_test_report['passed']: + def format_problem_list(key): + return str.join( + ' ', + map(format_problem_tuple, unit_test_report[key]) + ) + print('\tunexpected:', format_problem_list('unexpected')) + print('\tmissing:', format_problem_list('missing')) + print(str.format( + '{num_tested} tests run, {num_passed} tests passed.', + num_tested = num_tested, + num_passed = num_passed, + )) + untested_problem_types = set.difference(all_problem_types, problem_types_tested) + if untested_problem_types: + print('The following problem types were not tested:') + for problem_type in sorted(untested_problem_types): + print('\t' + problem_type) +if __name__ == '__main__': + run_unit_test_suite()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/alias-bad.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,11 @@ +0 Testing: alias-bad-colour alias-not-prefixed-with-equals alias-with-polygon +0 Expecting: alias-bad-colour:8 alias-not-prefixed-with-equals:1 alias-with-polygon:9 +0 Test part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part Alias +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt + +0 // Alias of 2345 +1 4 0 0 0 1 0 0 0 1 0 0 0 1 2345.dat +3 16 0 0 0 1 0 0 1 0 1
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/alias-ok.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,10 @@ +0 Testing: alias-bad-colour alias-not-prefixed-with-equals alias-with-polygon +0 Expecting: none +0 =Test part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part Alias +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt + +0 // Alias of 2345 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 2345.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/bad-line-endings-dos.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,3 @@ +0 Testing: bad-line-endings +0 Expecting: none +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/bad-line-endings-unix.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,3 @@ +0 Testing: bad-line-endings +0 Expecting: bad-line-endings:1 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/bowtie.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,3 @@ +0 Testing: self-intersecting +0 Expecting: self-intersecting:1 +4 16 0 0 0 2 2 0 0 2 0 2 0 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/category-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,11 @@ +0 Testing: bad-header bad-category bad-category-in-description +0 Expecting: bad-category:9 +0 Test part +0 Name: 3002.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt + +0 BFC CERTIFY CCW + +0 !CATEGORY Brix
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/category-3.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,9 @@ +0 Testing: bad-header bad-category bad-category-in-description +0 Expecting: bad-category-in-description:1 +0 Minifig Accessory Test +0 Name: 3002.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt + +0 BFC CERTIFY CCW
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/category.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,11 @@ +0 Testing: bad-header bad-category bad-category-in-description +0 Expecting: none +0 Test part +0 Name: 3002.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt + +0 BFC CERTIFY CCW + +0 !CATEGORY Brick
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/collinearity.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,4 @@ +0 Testing: collinear +0 Expecting: collinear:1 collinear:2 +3 16 0 0 0 0 100 0 0.01745 100 0 +3 16 0 0 0 0.0436 100 0 0.0436 -100 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/colors.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,5 @@ +0 Testing: bad-colour bad-colour-24-line bad-colour-24-nonline +0 Expecting: bad-colour-24-nonline:1 bad-colour-24-line:2 bad-colour:3 +4 24 0 0 0 1 0 0 1 1 0 0 1 0 +2 4 0 0 0 1 0 0 +4 123 0 0 0 1 0 0 1 1 0 0 1 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/concave.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,3 @@ +0 Testing: concave +0 Expecting: concave:1 +4 16 0 0 0 -1 2 0 0 1 0 1 2 0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/determinant.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: zero-determinant +0 Expecting: zero-determinant:2 zero-determinant:3 +0 Expecting: zero-determinant:4 zero-determinant:5 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat +1 16 0 0 0 0 0 0 0 1 0 0 0 1 box.dat +1 16 0 0 0 1 0 0 0 1 0 0 0 0 box.dat +1 16 0 0 0 1 0 0 0 0 0 0 0 1 box.dat +1 16 0 0 0 0 0 0 0 0 0 0 0 0 box.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/illegal-scaling.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,5 @@ +0 Testing: illegal-scaling unnecessary-scaling +0 Expecting: illegal-scaling:2 unnecessary-scaling:3 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 stud.dat +1 16 0 0 0 1 0 0 0 10 0 0 0 1 stud.dat +1 16 0 0 0 1 0 0 0 5 0 0 0 1 4-4disc.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/invertnext.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,9 @@ +0 Testing: bfc-invertnext-not-on-subfile unnecessary-invertnext +0 Expecting: unnecessary-invertnext:4 bfc-invertnext-not-on-subfile:6 +0 BFC INVERTNEXT +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat +0 BFC INVERTNEXT +1 16 0 0 0 1 0 0 0 1 0 0 0 1 4-4disc.dat +3 16 1 0 0 3 0 0 1 0 -3 +0 BFC INVERTNEXT +3 16 6 0 0 9 0 0 6 0 -3
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/keywords-for-nonparts-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: keywords-for-nonparts +0 Expecting: none +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/keywords-for-nonparts.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: keywords-for-nonparts +0 Expecting: keywords-for-nonparts:6 +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 !KEYWORDS a, b, c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/license-ca.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: no-license-set non-ca-license +0 Expecting: none +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/license-nonca.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: no-license-set non-ca-license +0 Expecting: non-ca-license:5 +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Not redistributable
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/license-none.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,6 @@ +0 Testing: no-license-set non-ca-license +0 Expecting: no-license-set:1 +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/mirrored-studs.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,6 @@ +0 Testing: mirrored-studs mirrored-studs-indirect +0 Expecting: mirrored-studs:2 mirrored-studs-indirect:4 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 stud.dat +1 16 0 0 0 1 0 0 0 1 0 0 0 -1 stud.dat +1 16 0 0 0 1 0 0 0 1 0 0 0 1 stug-3x3.dat +1 16 0 0 0 1 0 0 0 1 0 0 0 -1 stug-3x3.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/moved-extension-1.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: moved-to-with-extension +0 Expecting: none +0 ~Moved to 1234 +0 Name: 1.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/moved-extension-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: moved-to-with-extension +0 Expecting: moved-to-with-extension:1 +0 ~Moved to 1234.dat +0 Name: 1.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/moved.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,4 @@ +0 Testing: moved-file-used +0 Expecting: moved-file-used:1 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 1-16ri19.dat +1 16 0 0 1 1 0 0 0 1 0 0 0 1 1-19ring19.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/official-part.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,6 @@ +0 Testing: unofficial-part +0 Expecting: unofficial-part:4 +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Part UPDATE 2000-01
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/part-bfc-nocertify-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: bfc-nocertify +0 Expecting: none +0 Example part +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 BFC CERTIFY CCW
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/part-bfc-nocertify.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: bfc-nocertify +0 Expecting: bfc-nocertify:6 +0 Example part +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 BFC NOCERTIFY
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/physical-color-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: physical-colour-part +0 Expecting: none +0 Example part +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/physical-color.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: physical-colour-part +0 Expecting: physical-colour-part:4 +0 Example part +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part Physical_Colour +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/primitive-ccw.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: primitive-non-ccw +0 Expecting: none +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 BFC CERTIFY CCW
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/primitive-cw.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: primitive-non-ccw +0 Expecting: primitive-non-ccw:6 +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 BFC CERTIFY CW
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/primitive-nobfc.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: no-bfc-line +0 Expecting: no-bfc-line:1 +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/primitive-nocertify.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: primitive-non-ccw +0 Expecting: primitive-non-ccw:6 +0 Example primitive +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt +0 BFC NOCERTIFY
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/skew.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,8 @@ +0 Testing: skew-major skew-minor +0 Expecting: skew-major:1 skew-major:3 skew-minor:5 +4 16 25 0 -5 25 0 -15 35 0 -15 35 5 -5 +4 16 25 0 -5 25 0 -15 35 0 -15 35 0 -5 +4 16 40 0 -5 40 0 -15 50 5 -15 50 0 -5 +4 16 40 0 -5 40 0 -15 50 0 -15 50 0 -5 +4 16 0 0 0 100 0 0 100 0 100 0 5 100 +4 16 0 0 0 100 0 0 100 0 100 0 0 100
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/syntax.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,4 @@ +0 Testing: syntax-error +0 Expecting: syntax-error:2 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat +Blah
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/unknown-metacommand.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: unknown-metacommand +0 Expecting: unknown-metacommand:5 +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part +0 !WEIRD
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/unknown-subfile.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,4 @@ +0 Testing: bad-subfile +0 Expecting: bad-subfile:2 +1 16 0 0 0 1 0 0 0 1 0 0 0 1 box.dat +1 16 0 0 0 1 0 0 0 1 0 0 0 1 companioncube.dat
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/unofficial-part.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,6 @@ +0 Testing: unofficial-part +0 Expecting: none +0 Example part +0 Name: 1234.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Part
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/whitespace-1.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: bad-whitespace +0 Expecting: none +0 Example part with regular spaces +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/unittests/whitespace-2.test Thu Aug 26 19:36:44 2021 +0300 @@ -0,0 +1,7 @@ +0 Testing: bad-whitespace +0 Expecting: bad-whitespace:1 +0 Example part with weird spaces +0 Name: example.dat +0 Author: Me +0 !LDRAW_ORG Unofficial_Primitive +0 !LICENSE Redistributable under CCAL version 2.0 : see CAreadme.txt
--- a/webfront.py Thu Aug 26 19:16:25 2021 +0300 +++ b/webfront.py Thu Aug 26 19:36:44 2021 +0300 @@ -1,29 +1,50 @@ #!/usr/bin/env python3 from flask import Flask, render_template, redirect, request -from ldcheck import load_config, load_colours, find_ldconfig_ldr_paths +from ldcheck import appname, version, version_string from parse import read_ldraw from testsuite import load_tests, check_model, problem_text, all_problem_types -app = Flask('LDCheck') +app = Flask(appname) + +from ldcheck import LDrawContext +try: + context = LDrawContext() +except RuntimeError as error: + from sys import stderr, exit + print('error:', str(error), file = stderr) + exit(1) + +def format_report_html(report, model, test_suite): + messages = [] + for problem in report['problems']: + ldraw_code = model.body[problem.body_index].textual_representation() + message = str.format( + '<li class="{problem_type}">{model_name}:{line_number}:' + '{problem_type}: {message}<br />{ldraw_code}</li>', + model_name = model.name, + line_number = problem.line_number, + problem_type = problem.severity, + message = problem_text(problem, test_suite), + ldraw_code = ldraw_code, + ) + messages.append(message) + return '\n'.join(messages) @app.route('/', methods = ['GET', 'POST']) def web_front(): test_suite = load_tests() + common_args = { + 'appname': appname, + 'version': version_string, + 'is_debug': max(version) == 9999, + } if request.method == 'POST': # check if the post request has the file part if 'file' not in request.files or not request.files['file'].filename: return redirect(request.url) file = request.files['file'] filename = file.filename - config = load_config('ldcheck.cfg') - for ldconfig_ldr_path in find_ldconfig_ldr_paths(config): - with ldconfig_ldr_path.open() as ldconfig_ldr: - load_colours(ldconfig_ldr) - model = read_ldraw( - file.stream, - name = filename, - ldraw_directories = config['libraries'], - ) + model = read_ldraw(file.stream, name = filename, context = context) report = check_model(model, test_suite) # Amend human-readable messages into the report @@ -34,14 +55,16 @@ return render_template('webfront.html', report = report, name = filename, - problem_types = all_problem_types(test_suite) + problem_types = all_problem_types(test_suite), + **common_args, ) else: test_suite = load_tests() return render_template('webfront.html', report = None, name = None, - problem_types = all_problem_types(test_suite) + problem_types = all_problem_types(test_suite), + **common_args, ) @app.route('/static/<path:path>')