blob: a6b834882509ab2a09678853fcba7495576ac44a [file] [log] [blame]
#!/usr/bin/env python
from __future__ import print_function
import argparse
import io
import os
import platform
import re
import subprocess
import sys
class TestFailedError(Exception):
pass
def escapeCmdArg(arg):
if '"' in arg or ' ' in arg:
return '"%s"' % arg.replace('"', '\\"')
else:
return arg
def run_command(cmd):
if sys.version_info[0] < 3:
cmd = list(map(lambda s: s.encode('utf-8'), cmd))
print(' '.join([escapeCmdArg(arg) for arg in cmd]))
if sys.version_info[0] < 3 or platform.system() == 'Windows':
return subprocess.check_output(cmd, stderr=subprocess.STDOUT)
else:
return subprocess.check_output(list(map(lambda s: s.encode('utf-8'), cmd)),
stderr=subprocess.STDOUT)
def parseLine(line, line_no, test_case, incremental_edit_args, reparse_args,
current_reparse_start):
pre_edit_line = ""
post_edit_line = ""
# We parse one tag at a time in the line while eating away a prefix of the
# line
while line:
# The regular expression to match the template markers
subst_re = re.compile(r'^(.*?)<<(.*?)<(.*?)\|\|\|(.*?)>>>(.*\n?)')
reparse_re = re.compile(r'^(.*?)<(/?)reparse ?(.*?)>(.*\n?)')
subst_match = subst_re.match(line)
reparse_match = reparse_re.match(line)
if subst_match and reparse_match:
# If both regex match use the one with the shorter prefix
if len(subst_match.group(1)) < len(reparse_match.group(1)):
reparse_match = None
else:
subst_match = None
if subst_match:
prefix = subst_match.group(1)
match_test_case = subst_match.group(2)
pre_edit = subst_match.group(3)
post_edit = subst_match.group(4)
suffix = subst_match.group(5)
if match_test_case == test_case:
# Compute the -incremental-edit argument for swift-syntax-test
column = len(pre_edit_line) + len(prefix) + 1
edit_arg = '%d:%d-%d:%d=%s' % \
(line_no, column, line_no, column + len(pre_edit.encode('utf-8')),
post_edit)
incremental_edit_args.append('-incremental-edit')
incremental_edit_args.append(edit_arg)
pre_edit_line += prefix + pre_edit
post_edit_line += prefix + post_edit
else:
# For different test cases just take the pre-edit text
pre_edit_line += prefix + pre_edit
post_edit_line += prefix + pre_edit
line = suffix
elif reparse_match:
prefix = reparse_match.group(1)
is_closing = len(reparse_match.group(2)) > 0
match_test_case = reparse_match.group(3)
suffix = reparse_match.group(4)
if match_test_case == test_case:
column = len(post_edit_line) + len(prefix) + 1
if is_closing:
if not current_reparse_start:
raise TestFailedError('Closing unopened reparse tag '
'in line %d' % line_no)
reparse_args.append('-reparse-region')
reparse_args.append(
'%d:%d-%d:%d' % (current_reparse_start[0],
current_reparse_start[1],
line_no, column))
current_reparse_start = None
else:
if current_reparse_start:
raise TestFailedError('Opening nested reparse tags '
'for the same test case in line '
'%d' % line_no)
current_reparse_start = [line_no, column]
pre_edit_line += prefix
post_edit_line += prefix
line = suffix
else:
pre_edit_line += line
post_edit_line += line
# Nothing more to do
line = ''
return (pre_edit_line.encode('utf-8'),
post_edit_line.encode('utf-8'),
current_reparse_start)
def prepareForIncrParse(test_file, test_case, pre_edit_file, post_edit_file,
incremental_edit_args, reparse_args):
with io.open(test_file, mode='r', encoding='utf-8',
newline='\n') as test_file_handle, \
io.open(pre_edit_file, mode='w+', encoding='utf-8',
newline='\n') as pre_edit_file_handle, \
io.open(post_edit_file, mode='w+', encoding='utf-8',
newline='\n') as post_edit_file_handle:
current_reparse_start = None
line_no = 1
for line in test_file_handle.readlines():
parseLineRes = parseLine(line, line_no, test_case,
incremental_edit_args,
reparse_args, current_reparse_start)
(pre_edit_line, post_edit_line, current_reparse_start) = \
parseLineRes
pre_edit_file_handle.write(pre_edit_line.decode('utf-8'))
post_edit_file_handle.write(post_edit_line.decode('utf-8'))
line_no += 1
if current_reparse_start:
raise TestFailedError('Unclosed reparse tag for test case %s' %
test_case)
def serializeIncrParseMarkupFile(test_file, test_case, mode,
serialization_mode, serialization_format,
omit_node_ids, output_file, diags_output_file,
temp_dir, swift_syntax_test,
print_visual_reuse_info):
test_file_name = os.path.basename(test_file)
pre_edit_file = temp_dir + '/' + test_file_name + '.' + test_case + \
'.pre.swift'
post_edit_file = temp_dir + '/' + test_file_name + '.' + test_case + \
'.post.swift'
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
# =========================================================================
# First generate the pre-edit and post-edit Swift file and gather the edits
# and expected reparse regions. This is the parser for the special edit
# markup for testing incremental parsing
# =========================================================================
# Gather command line arguments for swift-syntax-test specifiying the
# performed edits in this list
incremental_edit_args = []
reparse_args = []
prepareForIncrParse(test_file, test_case, pre_edit_file, post_edit_file,
incremental_edit_args, reparse_args)
# =========================================================================
# Now generate the requested serialized file
# =========================================================================
# Build the command to serialize the tree depending on the command line
# arguments
try:
command = [
swift_syntax_test,
'-serialize-raw-tree',
'-output-filename', output_file
]
if diags_output_file:
command.extend(['-diags-output-filename', diags_output_file])
if omit_node_ids:
command.extend(['-omit-node-ids'])
if serialization_mode == 'full':
# Nothing to do. This is the default behaviour of swift-syntax-test
pass
elif serialization_mode == 'incremental':
command.extend(['-incremental-serialization'])
else:
raise ValueError('Unknown serialization mode "%s"' %
serialization_mode)
if serialization_format == 'json':
# Nothing to do. This is the default behaviour of swift-syntax-test
pass
elif serialization_format == 'byteTree':
command.extend(['-serialize-byte-tree'])
else:
raise ValueError('Unknown serialization format "%s"' %
serialization_format)
if mode == 'pre-edit':
command.extend(['-input-source-filename', pre_edit_file])
elif mode == 'post-edit':
command.extend(['-input-source-filename', post_edit_file])
elif mode == 'incremental':
# We need to build the syntax tree of the pre-edit file first so
# that we can pass it to swift-syntax-test to perform incremental
# parsing
pre_edit_tree_file = pre_edit_file + '.serialized.json'
run_command([swift_syntax_test] +
['-serialize-raw-tree'] +
['-input-source-filename', pre_edit_file] +
['-output-filename', pre_edit_tree_file])
# Then perform incremental parsing with the old syntax tree on the
# post-edit file
command.extend(['-input-source-filename', post_edit_file])
command.extend(['-old-syntax-tree-filename',
pre_edit_tree_file])
command.extend(['--old-source-filename', pre_edit_file])
command.extend(incremental_edit_args)
command.extend(reparse_args)
if print_visual_reuse_info:
command.extend([
'-print-visual-reuse-info',
'-force-colored-output'
])
else:
raise ValueError('Unknown mode "%s"' % mode)
output = run_command(command)
if print_visual_reuse_info:
print(output)
except subprocess.CalledProcessError as e:
raise TestFailedError(e.output.decode('utf-8'))
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description='Utility for testing incremental syntax parsing',
epilog='''
This utility can parse a special markup to dedicate a pre-edit and a
post-edit version of a file simulateously and generate a serialized version
of the libSyntax tree by parsing either the pre-edit file, the post-edit
file or the edits that are required to retrieve the post-edit file from the
pre-edit file incrementally.
To generate the pre-edit and the post-edit file from the template, it
operates on markers of the form:
<<test_case<pre|||post>>>
These placeholders are replaced by:
- 'pre' if a different test case than 'test_case' is run
- 'pre' for the pre-edit version of 'test_case'
- 'post' for the post-edit version of 'test_case''')
parser.add_argument(
'file', type=argparse.FileType(),
help='The template file to test')
parser.add_argument(
'--test-case', default='',
help='The test case to execute. If no test case is specified all \
unnamed substitutions are applied')
parser.add_argument(
'--mode', choices=['pre-edit', 'incremental', 'post-edit'],
required=True, help='''
The type of parsing to perform:
- pre-edit: Serialize the syntax tree when parsing the pre-edit file \
from scratch
- incremental: Serialize the syntax tree that results from parsing the \
edits between the pre-edit and post-edit file incrementally
- post-edit: Serialize the syntax tree that results from parsing the \
post-edit file from scratch
''')
parser.add_argument(
'--serialization-mode', choices=['full', 'incremental'],
default='full', help='''
Only applicable if `--mode` is `incremental`. Whether to serialize the
entire tree or use the incremental transfer mode. Default is `full`.
''')
parser.add_argument(
'--serialization-format', choices=['json', 'byteTree'],
default='json', help='''
The format in which the syntax tree shall be serialized.
''')
parser.add_argument(
'--omit-node-ids', default=False, action='store_true',
help='Don\'t include the ids of the nodes in the serialized syntax \
tree')
parser.add_argument(
'--output-file', required=True,
help='The file to which the serialized tree shall be written.')
parser.add_argument(
'--temp-dir', required=True,
help='A temporary directory where pre-edit and post-edit files can be \
saved')
parser.add_argument(
'--swift-syntax-test', required=True,
help='The path to swift-syntax-test')
parser.add_argument(
'--print-visual-reuse-info', default=False, action='store_true',
help='Print visual reuse information about the incremental parse \
instead of diffing the syntax trees. This option is intended \
for debug purposes only.')
args = parser.parse_args(sys.argv[1:])
test_file = args.file.name
test_case = args.test_case
mode = args.mode
serialization_mode = args.serialization_mode
serialization_format = args.serialization_format
omit_node_ids = args.omit_node_ids
output_file = args.output_file
temp_dir = args.temp_dir
swift_syntax_test = args.swift_syntax_test
visual_reuse_info = args.print_visual_reuse_info
try:
serializeIncrParseMarkupFile(test_file=test_file,
test_case=test_case,
mode=mode,
serialization_mode=serialization_mode,
serialization_format=serialization_format,
omit_node_ids=omit_node_ids,
output_file=output_file,
diags_output_file=None,
temp_dir=temp_dir,
swift_syntax_test=swift_syntax_test,
print_visual_reuse_info=visual_reuse_info)
except TestFailedError as e:
print(e.message, file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()