tests/subfiles.py

Tue, 25 Aug 2020 22:20:15 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Tue, 25 Aug 2020 22:20:15 +0300
changeset 100
62759e5c4554
parent 95
a3536e51f6bc
child 104
1ad664f783d6
permissions
-rw-r--r--

add some basic versioning

from testsuite import problem_type, report_problem
import testsuite
from geometry import *
import math
from librarystandards import library_standards

@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,
            )

def subfile_references_with_invertnext(model):
    import linetypes
    has_invertnext = False
    for element in model.body:
        if isinstance(element, linetypes.MetaCommand) \
        and element.text == 'BFC INVERTNEXT':
            has_invertnext = True
        else:
            if isinstance(element, linetypes.SubfileReference):
                yield element, has_invertnext
            has_invertnext = False

@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}.dat"',
            **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),
        ),
)
@problem_type('unnecessary-invertnext',
    severity = 'warning',
    message = 'flat subfile unnecessarily inverted using BFC INVERTNEXT'
)
@problem_type('mirrored-studs',
    severity = 'warning',
    message = lambda primitive: str.format(
        '"{primitive}" should not be mirrored',
        primitive = primitive,
    )
)
@problem_type('mirrored-studs-indirect',
    severity = 'warning',
    message = lambda primitive: str.format(
        '"{primitive}" should not be mirrored because it contains studs',
        primitive = primitive,
    )
)
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)
    if model.header.valid:
        cache.reference_stack.append(model.header.name)
    failed_subfiles = set()
    for subfile_reference, has_invertnext in subfile_references_with_invertnext(model):
        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:
                subfile = None
                failed_subfiles.add(path)
                yield report_problem(
                    'cyclical-reference',
                    bad_object = subfile_reference,
                    chain = str(e),
                )
            if subfile and not subfile.valid:
                yield report_problem(
                    'bad-subfile',
                    bad_object = subfile_reference,
                    path = path,
                    problem_text = subfile.problem,
                )
                failed_subfiles.add(path)
            elif subfile:
                # Test for use of moved-to files
                import re
                match = re.search(r'^\~Moved(?: to (.+))?$', subfile.description)
                if match:
                    yield report_problem(
                        'moved-file-used',
                        bad_object = subfile_reference,
                        moved_file = path,
                        new_file = match.group(1),
                    )
                # Test for scaling in flat direction
                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,
                    )
                # Test whether a flat subfile is inverted using invertnext
                if has_invertnext and subfile.flatness:
                    yield report_problem(
                        'unnecessary-invertnext',
                        bad_object = subfile_reference,
                    )
                from filecache import is_logo_stud_name
                # Test whether any stud subfile is mirrored.
                if is_logo_stud_name(subfile_reference.subfile_path) \
                and subfile_reference.matrix.is_mirrored():
                    yield report_problem(
                        'mirrored-studs',
                        bad_object = subfile_reference,
                        primitive = subfile_reference.subfile_path,
                    )
                elif subfile.has_studs and subfile_reference.matrix.is_mirrored():
                    yield report_problem(
                        'mirrored-studs-indirect',
                        bad_object = subfile_reference,
                        primitive = subfile_reference.subfile_path,
                    )

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