tests/subfiles.py

Sat, 01 Jun 2019 10:47:27 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Sat, 01 Jun 2019 10:47:27 +0300
changeset 55
388df1fa18a2
parent 54
0c686d10eb49
child 62
f0a6bf48b05e
permissions
-rw-r--r--

fixed handling of invalid subfiles

from testsuite import warning, error
import testsuite
from geometry import *
from os.path import dirname
from pathlib import Path
from configparser import ConfigParser
import math
ini_path = Path(dirname(__file__)) / 'library-standards.ini'
library_standards = ConfigParser()

with ini_path.open() as file:
    library_standards.read_file(file)

def determinant_test(model):
    '''
        Checks all subfile references for matrices with rows or columns all
        zero.
    '''
    yield from (
        error(subfile_reference, 'zero-determinant')
        for subfile_reference in model.subfile_references
        if abs(subfile_reference.matrix.determinant() - 0) < 1e-15
    )

def scaling_description(scaling, axes = 'xyz'):
    '''
        Returns a pretty description of a scaling vector. The axes parameter
        controls what axes are printed and can be used to filter away
        uninteresting values.
    '''
    return ', '.join(
        str.format('{} = {}', letter, getattr(scaling, letter))
        for letter in axes
    )

def check_scaling(scaling, axes):
    ''' Returns whether all given axes on the given scaling vector are 1. '''
    return all(
        abs(getattr(scaling, axis) - 1) < 1e-5
        for axis in axes
    )

# Restriction to checking function mapping.
restriction_tests = {
    'no scaling': lambda scaling: check_scaling(scaling, 'xyz'),
    'y-scaling only': lambda scaling: check_scaling(scaling, 'xz'),
    'stud3-like scaling': lambda scaling: all([
        check_scaling(scaling, 'xz'),
        # check if y-scaling is 1 or -1
        abs(abs(scaling.y) - 1) < 1e-5,
    ]),
}

def scaling_legality_test(model):
    '''
        Checks the part against primitive references with bad scaling. Some
        primitives (e.g. pegs) are not allowed to be scaled in the
        X or Z directions. Some (e.g. most studs) are not allowed to be scaled
        in the Y direction either.
    '''
    from fnmatch import fnmatch
    scaling_restrictions = library_standards['scaling restrictions']
    for subfile_reference in model.subfile_references:
        primitive = subfile_reference.subfile_path.lower()
        scaling = subfile_reference.matrix.scaling_vector()
        # Find all restrictions that apply to this subfile reference.
        restrictions = {
            restriction
            for pattern, restriction in scaling_restrictions.items()
            if fnmatch(primitive, pattern)
        }
        # Check restrictions against the subfile. If any restrictions were
        # found then the scaling vector must pass at least one of them.
        if restrictions and not any(
            restriction_tests[restriction](scaling)
            for restriction in restrictions
        ):
            interesting_axes = ''.join(
                axis
                for axis in 'xyz'
                if abs(getattr(scaling, axis) - 1) > 1e-5
            )
            yield warning(subfile_reference, 'illegal-scaling',
                primitive = primitive,
                axes = interesting_axes,
                scaling = scaling)

def dependent_subfile_tests(model):
    '''
        Tests subfile references for such qualities that are dependent on the
        actual contents of the subfiles. Checks whether moved-to files are used.
        Checks whether flat subfiles are scaled in the flat direction.
    '''
    import filecache
    cache = filecache.SubfileCache(model.ldraw_directories)
    failed_subfiles = set()
    for subfile_reference in model.subfile_references:
        path = subfile_reference.subfile_path.lower()
        if path in failed_subfiles:
            # Already proven to be a bad apple, don't complain twice
            pass
        else:
            try:
                subfile = cache.prepare_file(path)
            except filecache.CyclicalReferenceError as e:
                failed_subfiles.add(path)
                yield error(subfile_reference, 'cyclical-reference',
                    chain = str(e),
                )
            if not subfile.valid:
                yield error(subfile_reference, 'bad-subfile',
                    path = path,
                    problem_text = subfile.problem,
                )
                failed_subfiles.add(path)
            else:
                import re
                match = re.search(r'^\~Moved(?: to (\w+))?$', subfile.description)
                if match:
                    yield error(subfile_reference, 'moved-file-used',
                        moved_file = path,
                        new_file = match.group(1))
                scaling_vector = subfile_reference.matrix.scaling_vector()
                scaled_dimensions = {
                    dimension
                    for dimension in subfile.flatness
                    if not math.isclose(
                        getattr(scaling_vector, dimension),
                        1,
                        abs_tol = 1.0e-05
                    )
                }
                scaled_flat_dimensions = subfile.flatness & scaled_dimensions
                if scaled_flat_dimensions:
                    yield testsuite.notice(subfile_reference, 'unnecessary-scaling',
                        scaled_flat_dimensions = scaled_flat_dimensions,
                        scaling_vector = scaling_vector,
                    )

def dimensions_description(dimensions):
    sorted_dims = sorted(dimensions)
    if len(sorted_dims) == 1:
        return sorted_dims[0] + ' dimension'
    else:
        return str.format('{} and {} dimensions',
            ', '.join(sorted_dims[:-1]),
            sorted_dims[-1],
        )

manifest = {
    'tests': {
        'determinant': determinant_test,
        'scaling-legality': scaling_legality_test,
        'dependent-subfiles': dependent_subfile_tests,
    },
    'messages': {
        'zero-determinant': 'matrix determinant is zero '
            '(row or column all zero)',
        'illegal-scaling': lambda primitive, scaling, axes:
            str.format('scaling of unscalable primitive {} ({})',
                primitive,
                scaling_description(scaling, axes),
            ),
        'cyclical-reference': lambda chain:
            str.format('cyclical subfile dependency: {chain}',
                **locals(),
            ),
        'bad-subfile': lambda path, problem_text:
            str.format('cannot process subfile "{path}": {problem_text}',
                **locals(),
            ),
        'moved-file-used': lambda moved_file, new_file:
            str.format('subfile "{moved_file}" has been moved to "{new_file}"',
                **locals(),
            ),
        'unnecessary-scaling': lambda scaled_flat_dimensions, scaling_vector:
            str.format(
                'subfile unnecessarily scaled in the {dims} ({scaling})',
                dims = dimensions_description(scaled_flat_dimensions),
                scaling = scaling_description(scaling_vector),
            )
    },
}

mercurial