tests/subfiles.py

Sun, 23 Jun 2019 00:38:01 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Sun, 23 Jun 2019 00:38:01 +0300
changeset 73
9664583cd1c9
parent 64
1c0884f5506e
child 79
eb93feb6d3a3
permissions
-rw-r--r--

added alias tests

from testsuite import problem_type, report_problem
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)

@problem_type('zero-determinant',
    severity = 'hold',
    message = 'matrix row or column all zero'
)
def determinant_test(model):
    '''
        Checks all subfile references for matrices with rows or columns all
        zero.
    '''
    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
    )

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.
    '''
    if isinstance(scaling, str):
        return scaling
    else:
        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,
    ]),
}

@problem_type('illegal-scaling',
    severity = 'hold',
    message = lambda primitive, scaling, axes:
        str.format('scaling of unscalable primitive {} ({})',
            primitive,
            scaling_description(scaling, axes),
        ),
)
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 report_problem('illegal-scaling',
                bad_object = subfile_reference,
                primitive = primitive,
                axes = interesting_axes,
                scaling = scaling,
            )

@problem_type('cyclical-reference',
    severity = 'hold',
    message = lambda chain:
        str.format('cyclical subfile dependency: {chain}',
            **locals(),
        ),
)
@problem_type('bad-subfile',
    severity = 'hold',
    message = lambda path, problem_text:
        str.format('cannot process subfile "{path}": {problem_text}',
            **locals(),
        ),
)
@problem_type('moved-file-used',
    severity = 'hold',
    message = lambda moved_file, new_file:
        str.format('subfile "{moved_file}" has been moved to "{new_file}"',
            **locals(),
        ),
)
@problem_type('unnecessary-scaling',
    severity = 'warning',
    message = 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),
        ),
)
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 report_problem(
                    'cyclical-reference',
                    bad_object = subfile_reference,
                    chain = str(e),
                )
            if not subfile.valid:
                yield report_problem(
                    'bad-subfile',
                    bad_object = subfile_reference,
                    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 report_problem(
                        'moved-file-used',
                        bad_object = subfile_reference,
                        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 report_problem(
                        'unnecessary-scaling',
                        bad_object = subfile_reference,
                        scaled_flat_dimensions = scaled_flat_dimensions,
                        scaling_vector = scaling_vector,
                    )

def dimensions_description(dimensions):
    if isinstance(dimensions, str):
        return dimensions
    else:
        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_test,
        scaling_legality_test,
        dependent_subfile_tests,
    ],
}

mercurial