| #!/usr/bin/env python3 |
| |
| import itertools |
| import os |
| |
| import jinja2 |
| import yaml |
| |
| HERE = os.path.abspath(os.path.dirname(__file__)) |
| PROJECT_ROOT = os.path.join(HERE, '..', '..', '..') |
| |
| def find_templates(starting_directory): |
| for directory, subdirectories, file_names in os.walk(starting_directory): |
| for file_name in file_names: |
| if file_name.startswith('.'): |
| continue |
| yield file_name, os.path.join(directory, file_name) |
| |
| def test_name(directory, template_name, subtest_flags): |
| ''' |
| Create a test name based on a template and the WPT file name flags [1] |
| required for a given subtest. This name is used to determine how subtests |
| may be grouped together. In order to promote grouping, the combination uses |
| a few aspects of how file name flags are interpreted: |
| |
| - repeated flags have no effect, so duplicates are removed |
| - flag sequence does not matter, so flags are consistently sorted |
| |
| directory | template_name | subtest_flags | result |
| ----------|------------------|-----------------|------- |
| cors | image.html | [] | cors/image.html |
| cors | image.https.html | [] | cors/image.https.html |
| cors | image.html | [https] | cors/image.https.html |
| cors | image.https.html | [https] | cors/image.https.html |
| cors | image.https.html | [https] | cors/image.https.html |
| cors | image.sub.html | [https] | cors/image.https.sub.html |
| cors | image.https.html | [sub] | cors/image.https.sub.html |
| |
| [1] docs/writing-tests/file-names.md |
| ''' |
| template_name_parts = template_name.split('.') |
| flags = set(subtest_flags) | set(template_name_parts[1:-1]) |
| test_name_parts = ( |
| [template_name_parts[0]] + |
| sorted(flags) + |
| [template_name_parts[-1]] |
| ) |
| return os.path.join(directory, '.'.join(test_name_parts)) |
| |
| def merge(a, b): |
| if type(a) != type(b): |
| raise Exception('Cannot merge disparate types') |
| if type(a) == list: |
| return a + b |
| if type(a) == dict: |
| merged = {} |
| |
| for key in a: |
| if key in b: |
| merged[key] = merge(a[key], b[key]) |
| else: |
| merged[key] = a[key] |
| |
| for key in b: |
| if not key in a: |
| merged[key] = b[key] |
| |
| return merged |
| |
| raise Exception('Cannot merge {} type'.format(type(a).__name__)) |
| |
| def product(a, b): |
| ''' |
| Given two lists of objects, compute their Cartesian product by merging the |
| elements together. For example, |
| |
| product( |
| [{'a': 1}, {'b': 2}], |
| [{'c': 3}, {'d': 4}, {'e': 5}] |
| ) |
| |
| returns the following list: |
| |
| [ |
| {'a': 1, 'c': 3}, |
| {'a': 1, 'd': 4}, |
| {'a': 1, 'e': 5}, |
| {'b': 2, 'c': 3}, |
| {'b': 2, 'd': 4}, |
| {'b': 2, 'e': 5} |
| ] |
| ''' |
| result = [] |
| |
| for a_object in a: |
| for b_object in b: |
| result.append(merge(a_object, b_object)) |
| |
| return result |
| |
| def make_provenance(project_root, cases, template): |
| return '\n'.join([ |
| 'This test was procedurally generated. Please do not modify it directly.', |
| 'Sources:', |
| '- {}'.format(os.path.relpath(cases, project_root)), |
| '- {}'.format(os.path.relpath(template, project_root)) |
| ]) |
| |
| def collection_filter(obj, title): |
| if not obj: |
| return 'no {}'.format(title) |
| |
| members = [] |
| for name, value in obj.items(): |
| if value == '': |
| members.append(name) |
| else: |
| members.append('{}={}'.format(name, value)) |
| |
| return '{}: {}'.format(title, ', '.join(members)) |
| |
| def pad_filter(value, side, padding): |
| if not value: |
| return '' |
| if side == 'start': |
| return padding + value |
| |
| return value + padding |
| |
| def main(config_file): |
| with open(config_file, 'r') as handle: |
| config = yaml.safe_load(handle.read()) |
| |
| templates_directory = os.path.normpath( |
| os.path.join(os.path.dirname(config_file), config['templates']) |
| ) |
| |
| environment = jinja2.Environment( |
| variable_start_string='[%', |
| variable_end_string='%]' |
| ) |
| environment.filters['collection'] = collection_filter |
| environment.filters['pad'] = pad_filter |
| templates = {} |
| subtests = {} |
| |
| for template_name, path in find_templates(templates_directory): |
| subtests[template_name] = [] |
| with open(path, 'r') as handle: |
| templates[template_name] = environment.from_string(handle.read()) |
| |
| for case in config['cases']: |
| unused_templates = set(templates) - set(case['template_axes']) |
| |
| # This warning is intended to help authors avoid mistakenly omitting |
| # templates. It can be silenced by extending the`template_axes` |
| # dictionary with an empty list for templates which are intentionally |
| # unused. |
| if unused_templates: |
| print( |
| 'Warning: case does not reference the following templates:' |
| ) |
| print('\n'.join('- {}'.format(name) for name in unused_templates)) |
| |
| common_axis = product( |
| case['common_axis'], [case.get('all_subtests', {})] |
| ) |
| |
| for template_name, template_axis in case['template_axes'].items(): |
| subtests[template_name].extend(product(common_axis, template_axis)) |
| |
| for template_name, template in templates.items(): |
| provenance = make_provenance( |
| PROJECT_ROOT, |
| config_file, |
| os.path.join(templates_directory, template_name) |
| ) |
| get_filename = lambda subtest: test_name( |
| config['output_directory'], |
| template_name, |
| subtest['filename_flags'] |
| ) |
| subtests_by_filename = itertools.groupby( |
| sorted(subtests[template_name], key=get_filename), |
| key=get_filename |
| ) |
| for filename, some_subtests in subtests_by_filename: |
| with open(filename, 'w') as handle: |
| handle.write(templates[template_name].render( |
| subtests=list(some_subtests), |
| provenance=provenance |
| ) + '\n') |
| |
| if __name__ == '__main__': |
| main('fetch-metadata.conf.yml') |