1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
|
#!/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')
|