#!/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')