solidity/scripts/isolate_tests.py
2023-09-06 13:24:40 +02:00

176 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
#
# This script reads C++ or RST source files and writes all
# multi-line strings into individual files.
# This can be used to extract the Solidity test cases
# into files for e.g. fuzz testing as
# scripts/isolate_tests.py test/libsolidity/*
import re
import os
import hashlib
from os.path import join, isfile, basename
from argparse import ArgumentParser
from textwrap import indent, dedent
def extract_test_cases(path):
with open(path, encoding="utf8", errors='ignore', mode='r', newline='') as file:
lines = file.read().splitlines()
inside = False
delimiter = ''
tests = []
for l in lines:
if inside:
if l.strip().endswith(')' + delimiter + '";'):
inside = False
else:
tests[-1] += l + '\n'
else:
m = re.search(r'R"([^(]*)\($', l.strip())
if m:
inside = True
delimiter = m.group(1)
tests += ['']
return tests
def extract_solidity_docs_cases(path):
tests = extract_docs_cases(path, [".. code-block:: solidity", '::'])
codeStart = "(// SPDX-License-Identifier:|pragma solidity|contract.*{|library.*{|interface.*{)"
# Filter out tests that are not supposed to be compilable.
return [
test.lstrip("\n")
for test in tests
if re.search(r'^\s{4}' + codeStart, test, re.MULTILINE) is not None
]
def extract_yul_docs_cases(path):
tests = extract_docs_cases(path, [".. code-block:: yul"])
def wrap_in_object(code):
for line in code.splitlines():
line = line.lstrip()
if line.startswith("//"):
continue
if not line.startswith("object") and not line.startswith("{"):
return indent(f"{{\n{code.rstrip()}\n}}\n\n", " ")
break
return code
return [
wrap_in_object(test)
for test in tests
if test.strip() != ""
]
# Extract code examples based on the 'beginMarker' parameter
# up until we reach EOF or a line that is not empty and doesn't start with 4
# spaces.
def extract_docs_cases(path, beginMarkers):
immediatelyAfterMarker = False
insideBlock = False
tests = []
# Collect all snippets of indented blocks
with open(path, mode='r', errors='ignore', encoding='utf8', newline='') as f:
lines = f.read().splitlines()
for line in lines:
if insideBlock:
if immediatelyAfterMarker:
# Skip Sphinx instructions and empty lines between them
if line == '' or line.lstrip().startswith(":"):
continue
if line == '' or line.startswith(" "):
tests[-1] += line + "\n"
immediatelyAfterMarker = False
continue
insideBlock = False
if any(map(line.lower().startswith, beginMarkers)):
insideBlock = True
immediatelyAfterMarker = True
tests += ['']
return tests
def write_cases(f, solidityTests, yulTests):
cleaned_filename = f.replace(".","_").replace("-","_").replace(" ","_").lower()
for language, test in [("sol", t) for t in solidityTests] + [("yul", t) for t in yulTests]:
# When code examples are extracted they are indented by 8 spaces, which violates the style guide,
# so before checking remove 4 spaces from each line.
remainder = dedent(test)
source_code_hash = hashlib.sha256(test.encode("utf-8")).hexdigest()
sol_filename = f'test_{source_code_hash}_{cleaned_filename}.{language}'
with open(sol_filename, mode='w', encoding='utf8', newline='') as fi:
fi.write(remainder)
def extract_and_write(path, language):
assert language in ["solidity", "yul", ""]
yulCases = []
cases = []
if path.lower().endswith('.rst'):
if language in ("solidity", ""):
cases = extract_solidity_docs_cases(path)
if language in ("yul", ""):
yulCases = extract_yul_docs_cases(path)
elif path.endswith('.sol'):
if language in ("solidity", ""):
with open(path, mode='r', encoding='utf8', newline='') as f:
cases = [f.read()]
else:
cases = extract_test_cases(path)
write_cases(basename(path), cases, yulCases)
if __name__ == '__main__':
script_description = (
"Reads Solidity, C++ or RST source files and extracts compilable solidity and yul code blocks from them. "
"Can be used to generate test cases to validate code examples. "
)
parser = ArgumentParser(description=script_description)
parser.add_argument(dest='path', help='Path to file or directory to look for code in.')
parser.add_argument(
'-l', '--language',
dest='language',
choices=["yul", "solidity"],
default="",
action='store',
help="Extract only code blocks in the given language"
)
options = parser.parse_args()
path = options.path
# ignore the test with broken utf-8 encoding and experimental tests
exclude_files = {
"invalid_utf8_sequence.sol",
}
exclude_subdirs = {
"_build",
"compilationTests",
"experimental"
}
if isfile(path):
extract_and_write(path, options.language)
else:
for root, subdirs, files in os.walk(path, topdown=True):
# exclude specific directories from being recursed further
subdirs[:] = [d for d in subdirs if d not in exclude_subdirs]
for f in files:
if basename(f) in exclude_files:
continue
path = join(root, f)
print(path)
extract_and_write(path, options.language)