isolate_tests: Extract code blocks from documentation using code block header

This commit is contained in:
Marenz 2021-07-01 15:19:38 +02:00
parent f62b80530b
commit 5291ca2dd4
6 changed files with 59 additions and 60 deletions

View File

@ -324,7 +324,7 @@ from the documentation or the other tests:
# extract from tests: # extract from tests:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp path/to/solidity/scripts/isolate_tests.py path/to/solidity/test/libsolidity/SolidityEndToEndTest.cpp
# extract from documentation: # extract from documentation:
path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs docs path/to/solidity/scripts/isolate_tests.py path/to/solidity/docs
The AFL documentation states that the corpus (the initial input files) should not be The AFL documentation states that the corpus (the initial input files) should not be
too large. The files themselves should not be larger than 1 kB and there should be too large. The files themselves should not be larger than 1 kB and there should be

View File

@ -136,7 +136,7 @@ SOLTMPDIR=$(mktemp -d)
( (
set -e set -e
cd "$SOLTMPDIR" cd "$SOLTMPDIR"
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/ docs "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/
getAllAvailableVersions getAllAvailableVersions

View File

@ -10,7 +10,8 @@ import sys
import re import re
import os import os
import hashlib import hashlib
from os.path import join, isfile, split from os.path import join, isfile, split, basename
from argparse import ArgumentParser
def extract_test_cases(path): def extract_test_cases(path):
with open(path, encoding="utf8", errors='ignore', mode='r', newline='') as file: with open(path, encoding="utf8", errors='ignore', mode='r', newline='') as file:
@ -35,54 +36,50 @@ def extract_test_cases(path):
return tests return tests
# Contract sources are indented by 4 spaces. # Extract code examples based on a start marker
# Look for `pragma solidity`, `contract`, `library` or `interface` # up until we reach EOF or a line that is not empty and doesn't start with 4
# and abort a line not indented properly. # spaces.
def extract_docs_cases(path): def extract_docs_cases(path):
beginMarkers = ['.. code-block:: solidity', '::']
immediatelyAfterMarker = False
insideBlock = False insideBlock = False
insideBlockParameters = False
pastBlockParameters = False
extractedLines = []
tests = [] tests = []
# Collect all snippets of indented blocks # Collect all snippets of indented blocks
with open(path, mode='r', errors='ignore', encoding='utf8', newline='') as f: with open(path, mode='r', errors='ignore', encoding='utf8', newline='') as f:
lines = f.read().splitlines() lines = f.read().splitlines()
for l in lines:
if l != '':
if not insideBlock and l.startswith(' '):
# start new test
extractedLines += ['']
insideBlockParameters = False
pastBlockParameters = False
insideBlock = l.startswith(' ')
if insideBlock:
if not pastBlockParameters:
# NOTE: For simplicity this allows blank lines between block parameters even
# though Sphinx does not. This does not matter since the first non-empty line in
# a Solidity file cannot start with a colon anyway.
if not l.strip().startswith(':') and (l != '' or not insideBlockParameters):
insideBlockParameters = False
pastBlockParameters = True
else:
insideBlockParameters = True
if not insideBlockParameters: for line in lines:
extractedLines[-1] += l + '\n' 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
else:
insideBlock = False
elif any(map(line.lower().startswith, beginMarkers)):
insideBlock = True
immediatelyAfterMarker = True
tests += ['']
codeStart = "(// SPDX-License-Identifier:|pragma solidity|contract.*{|library.*{|interface.*{)" codeStart = "(// SPDX-License-Identifier:|pragma solidity|contract.*{|library.*{|interface.*{)"
# Filter all tests that do not contain Solidity or are indented incorrectly. for test in tests:
for lines in extractedLines: if re.search(r'^\s{0,3}' + codeStart, test, re.MULTILINE):
if re.search(r'^\s{0,3}' + codeStart, lines, re.MULTILINE):
print("Indentation error in " + path + ":") print("Indentation error in " + path + ":")
print(lines) print(test)
exit(1) exit(1)
if re.search(r'^\s{4}' + codeStart, lines, re.MULTILINE):
tests.append(lines)
return tests # 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 write_cases(f, tests): def write_cases(f, tests):
cleaned_filename = f.replace(".","_").replace("-","_").replace(" ","_").lower() cleaned_filename = f.replace(".","_").replace("-","_").replace(" ","_").lower()
@ -94,30 +91,30 @@ def write_cases(f, tests):
with open(sol_filename, mode='w', encoding='utf8', newline='') as fi: with open(sol_filename, mode='w', encoding='utf8', newline='') as fi:
fi.write(remainder) fi.write(remainder)
def extract_and_write(f, path): def extract_and_write(path):
if docs: if path.lower().endswith('.rst'):
cases = extract_docs_cases(path) cases = extract_docs_cases(path)
elif path.endswith('.sol'):
with open(path, mode='r', encoding='utf8', newline='') as f:
cases = [f.read()]
else: else:
if f.endswith('.sol'): cases = extract_test_cases(path)
with open(path, mode='r', encoding='utf8', newline='') as _f:
cases = [_f.read()] write_cases(basename(path), cases)
else:
cases = extract_test_cases(path)
write_cases(f, cases)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) == 1: script_description = (
print("Usage: " + sys.argv[0] + " path-to-file-or-folder-to-extract-code-from [docs]") "Reads Solidity, C++ or RST source files and extracts compilable solidity and yul code blocks from them. "
exit(1) "Can be used to generate test cases to validade code examples. "
)
path = sys.argv[1] parser = ArgumentParser(description=script_description)
docs = False parser.add_argument(dest='path', help='Path to file or directory to look for code in.')
if len(sys.argv) > 2 and sys.argv[2] == 'docs': options = parser.parse_args()
docs = True path = options.path
if isfile(path): if isfile(path):
_, tail = split(path) extract_and_write(path)
extract_and_write(tail, path)
else: else:
for root, subdirs, files in os.walk(path): for root, subdirs, files in os.walk(path):
if '_build' in subdirs: if '_build' in subdirs:
@ -125,8 +122,7 @@ if __name__ == '__main__':
if 'compilationTests' in subdirs: if 'compilationTests' in subdirs:
subdirs.remove('compilationTests') subdirs.remove('compilationTests')
for f in files: for f in files:
_, tail = split(f) if basename(f) == "invalid_utf8_sequence.sol":
if tail == "invalid_utf8_sequence.sol":
continue # ignore the test with broken utf-8 encoding continue # ignore the test with broken utf-8 encoding
path = join(root, f) path = join(root, f)
extract_and_write(f, path) extract_and_write(path)

View File

@ -1,4 +1,7 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
#
# Not actively tested or maintained. Exists in case we want to rebuild an
# ancient release.
import sys import sys
import re import re

View File

@ -361,7 +361,7 @@ SOLTMPDIR=$(mktemp -d)
( (
set -e set -e
cd "$SOLTMPDIR" cd "$SOLTMPDIR"
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/ docs "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/
developmentVersion=$("$REPO_ROOT/scripts/get_version.sh") developmentVersion=$("$REPO_ROOT/scripts/get_version.sh")
for f in *.sol for f in *.sol
@ -510,7 +510,7 @@ SOLTMPDIR=$(mktemp -d)
set -e set -e
cd "$SOLTMPDIR" cd "$SOLTMPDIR"
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/test/ "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/test/
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/ docs "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/
echo ./*.sol | xargs -P 4 -n 50 "${SOLIDITY_BUILD_DIR}/test/tools/solfuzzer" --quiet --input-files echo ./*.sol | xargs -P 4 -n 50 "${SOLIDITY_BUILD_DIR}/test/tools/solfuzzer" --quiet --input-files
echo ./*.sol | xargs -P 4 -n 50 "${SOLIDITY_BUILD_DIR}/test/tools/solfuzzer" --without-optimizer --quiet --input-files echo ./*.sol | xargs -P 4 -n 50 "${SOLIDITY_BUILD_DIR}/test/tools/solfuzzer" --without-optimizer --quiet --input-files

View File

@ -22,7 +22,7 @@ SOLTMPDIR=$(mktemp -d)
( (
set -e set -e
cd "$SOLTMPDIR" cd "$SOLTMPDIR"
"$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/ docs "$REPO_ROOT"/scripts/isolate_tests.py "$REPO_ROOT"/docs/
if npm -v >/dev/null 2>&1; then if npm -v >/dev/null 2>&1; then
if npm list -g | grep solhint >/dev/null 2>&1; then if npm list -g | grep solhint >/dev/null 2>&1; then