mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
176 lines
5.7 KiB
Python
Executable File
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)
|