mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
411 lines
16 KiB
Python
Executable File
411 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3.9
|
|
|
|
# DEPENDENCIES:
|
|
# pip install git+https://github.com/christianparpart/pylspclient.git --user
|
|
# WHEN DEVELOPING (editable module):
|
|
# pip install -e /path/to/checkout/pylspclient
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
|
|
from pprint import pprint
|
|
from threading import Condition
|
|
from deepdiff import DeepDiff
|
|
|
|
# Requires the one from https://github.com/christianparpart/pylspclient
|
|
# Use `pip install -e $PATH_TO_LIB_CHECKOUT --user` for local development & testing
|
|
import pylspclient
|
|
|
|
lsp_types = pylspclient.lsp_structs
|
|
|
|
SGR_RESET = '\033[m'
|
|
SGR_TEST_BEGIN = '\033[1;33m'
|
|
SGR_STATUS_OKAY = '\033[1;32m'
|
|
SGR_STATUS_FAIL = '\033[1;31m'
|
|
SGR_INSPECT = '\033[1;35m'
|
|
|
|
TEST_NAME = 'test_definition'
|
|
|
|
def dprint(text: str):
|
|
print(SGR_INSPECT + "-- " + text + ":" + SGR_RESET)
|
|
|
|
def dinspect(text, obj):
|
|
dprint(text)
|
|
if not obj is None:
|
|
pprint(obj)
|
|
|
|
class ReadPipe(threading.Thread):
|
|
"""
|
|
Used to link (solc) process stdio.
|
|
"""
|
|
def __init__(self, pipe):
|
|
threading.Thread.__init__(self)
|
|
self.pipe = pipe
|
|
|
|
def run(self):
|
|
dprint("ReadPipe: starting")
|
|
line = self.pipe.readline().decode('utf-8')
|
|
while line:
|
|
print(line)
|
|
#print("\033[1;42m{}\033[m\n".format(line))
|
|
line = self.pipe.readline().decode('utf-8')
|
|
|
|
SOLIDITY_LANGUAGE_ID = "solidity" # lsp_types.LANGUAGE_IDENTIFIER.C
|
|
|
|
LSP_CLIENT_CAPS = {
|
|
'textDocument': {'codeAction': {'dynamicRegistration': True},
|
|
'codeLens': {'dynamicRegistration': True},
|
|
'colorProvider': {'dynamicRegistration': True},
|
|
'completion': {'completionItem': {'commitCharactersSupport': True,
|
|
'documentationFormat': ['markdown', 'plaintext'],
|
|
'snippetSupport': True},
|
|
'completionItemKind': {'valueSet': [
|
|
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
|
|
]},
|
|
'contextSupport': True,
|
|
'dynamicRegistration': True},
|
|
'definition': {'dynamicRegistration': True},
|
|
'documentHighlight': {'dynamicRegistration': True},
|
|
'documentLink': {'dynamicRegistration': True},
|
|
'documentSymbol': {
|
|
'dynamicRegistration': True,
|
|
'symbolKind': {'valueSet': [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
|
|
]}
|
|
},
|
|
'formatting': {'dynamicRegistration': True},
|
|
'hover': {'contentFormat': ['markdown', 'plaintext'],
|
|
'dynamicRegistration': True},
|
|
'implementation': {'dynamicRegistration': True},
|
|
'onTypeFormatting': {'dynamicRegistration': True},
|
|
'publishDiagnostics': {'relatedInformation': True},
|
|
'rangeFormatting': {'dynamicRegistration': True},
|
|
'references': {'dynamicRegistration': True},
|
|
'rename': {'dynamicRegistration': True},
|
|
'signatureHelp': {'dynamicRegistration': True,
|
|
'signatureInformation': {'documentationFormat': ['markdown', 'plaintext']}},
|
|
'synchronization': {'didSave': True,
|
|
'dynamicRegistration': True,
|
|
'willSave': True,
|
|
'willSaveWaitUntil': True},
|
|
'typeDefinition': {'dynamicRegistration': True}},
|
|
'workspace': {'applyEdit': True,
|
|
'configuration': True,
|
|
'didChangeConfiguration': {'dynamicRegistration': True},
|
|
'didChangeWatchedFiles': {'dynamicRegistration': True},
|
|
'executeCommand': {'dynamicRegistration': True},
|
|
'symbol': {
|
|
'dynamicRegistration': True,
|
|
'symbolKind': {'valueSet': [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 ]}
|
|
},
|
|
'workspaceEdit': {'documentChanges': True},
|
|
'workspaceFolders': True}
|
|
}
|
|
|
|
class SolcInstance:
|
|
"""
|
|
Manages the solc executable instance and provides the handle to communicate with it
|
|
"""
|
|
|
|
process: subprocess.Popen
|
|
endpoint: pylspclient.LspEndpoint
|
|
client: pylspclient.LspClient
|
|
read_pipe: ReadPipe
|
|
diagnostics_cond: Condition
|
|
#published_diagnostics: object
|
|
|
|
def __init__(self, _solc_path: str) -> None:
|
|
self.solc_path = _solc_path
|
|
self.published_diagnostics = []
|
|
self.client = pylspclient.LspClient(None)
|
|
self.diagnostics_cond = Condition()
|
|
|
|
def __enter__(self):
|
|
dprint(f"Starting solc LSP instance: {self.solc_path}")
|
|
self.process = subprocess.Popen(
|
|
[self.solc_path, "--lsp"],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE
|
|
)
|
|
self.read_pipe = ReadPipe(self.process.stderr)
|
|
self.read_pipe.start()
|
|
self.endpoint = pylspclient.LspEndpoint(
|
|
json_rpc_endpoint=pylspclient.JsonRpcEndpoint(
|
|
self.process.stdin,
|
|
self.process.stdout
|
|
),
|
|
notify_callbacks={
|
|
'textDocument/publishDiagnostics': self.on_publish_diagnostics
|
|
}
|
|
)
|
|
self.client = pylspclient.LspClient(self.endpoint)
|
|
return self
|
|
|
|
def __exit__(self, _exception_type, _exception_value, _traceback) -> None:
|
|
dprint("Stopping solc instance.")
|
|
self.client.shutdown()
|
|
self.client.exit()
|
|
self.read_pipe.join()
|
|
|
|
def on_publish_diagnostics(self, _diagnostics) -> None:
|
|
dprint("Receiving published diagnostics:")
|
|
pprint(_diagnostics)
|
|
self.published_diagnostics.append(_diagnostics)
|
|
self.diagnostics_cond.acquire()
|
|
self.diagnostics_cond.notify()
|
|
self.diagnostics_cond.release()
|
|
|
|
class SolcTests:
|
|
def __init__(self, _client: SolcInstance, _project_root_dir: str):
|
|
self.solc = _client
|
|
self.project_root_dir = _project_root_dir
|
|
self.project_root_uri = 'file://' + self.project_root_dir
|
|
self.tests = 0
|
|
dprint("root dir: {self.project_root_dir}")
|
|
|
|
# {{{ helpers
|
|
def get_test_file_path(self, _test_case_name):
|
|
return f"{self.project_root_dir}/{_test_case_name}.sol"
|
|
|
|
def get_test_file_uri(self, _test_case_name):
|
|
return "file://" + self.get_test_file_path(_test_case_name)
|
|
|
|
def get_test_file_contents(self, _test_case_name):
|
|
return open(self.get_test_file_path(_test_case_name,), mode="r", encoding="utf-8").read()
|
|
|
|
def lsp_open_file(self, _test_case_name):
|
|
""" Opens file for given test case. """
|
|
version = 1
|
|
file_uri = self.get_test_file_uri(_test_case_name)
|
|
file_contents = self.get_test_file_contents(_test_case_name)
|
|
self.solc.client.didOpen(lsp_types.TextDocumentItem(
|
|
file_uri, SOLIDITY_LANGUAGE_ID, version, file_contents
|
|
))
|
|
|
|
def lsp_open_file_and_wait_for_diagnostics(self, _test_case_name):
|
|
"""
|
|
Opens file for given test case and waits for diagnostics to be published.
|
|
"""
|
|
self.solc.diagnostics_cond.acquire()
|
|
self.lsp_open_file(_test_case_name)
|
|
self.solc.diagnostics_cond.wait_for(
|
|
predicate=lambda: len(self.solc.published_diagnostics) != 0,
|
|
timeout=2.0
|
|
)
|
|
self.solc.diagnostics_cond.release()
|
|
|
|
def expect(self, _cond: bool, _description: str) -> None:
|
|
self.tests = self.tests + 1
|
|
prefix = f"[{self.tests}] {SGR_TEST_BEGIN}{_description}{SGR_RESET} : "
|
|
if _cond:
|
|
print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET)
|
|
else:
|
|
print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET)
|
|
raise RuntimeError("Expectation failed.")
|
|
|
|
def expect_equal(self, _description: str, _actual, _expected) -> None:
|
|
self.tests = self.tests + 1
|
|
prefix = f"[{self.tests}] {SGR_TEST_BEGIN}{_description}: "
|
|
diff = DeepDiff(_actual, _expected)
|
|
if len(diff) == 0:
|
|
print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET)
|
|
return
|
|
|
|
print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET)
|
|
pprint(diff)
|
|
raise RuntimeError('Expectation failed.')
|
|
|
|
# }}}
|
|
|
|
# {{{ actual tests
|
|
def run(self):
|
|
self.open_files_and_test_publish_diagnostics()
|
|
self.test_definition()
|
|
self.test_documentHighlight()
|
|
# self.test_hover()
|
|
# self.test_implementation()
|
|
# self.test_references()
|
|
# self.test_signatureHelp()
|
|
# self.test_semanticTokensFull()
|
|
|
|
def extract_test_file_name(self, _uri: str):
|
|
"""
|
|
Extracts the project-root URI prefix from the URI.
|
|
"""
|
|
subLength = len(self.project_root_uri)
|
|
return _uri[subLength:]
|
|
|
|
def open_files_and_test_publish_diagnostics(self):
|
|
self.lsp_open_file_and_wait_for_diagnostics(TEST_NAME)
|
|
|
|
# should have received one published_diagnostics notification
|
|
dprint("len: {len(self.solc.published_diagnostics)}")
|
|
self.expect(len(self.solc.published_diagnostics) == 1, "one published_diagnostics message")
|
|
published_diagnostics = self.solc.published_diagnostics[0]
|
|
|
|
self.expect(published_diagnostics['uri'] == self.get_test_file_uri(TEST_NAME),
|
|
'diagnostic: uri')
|
|
|
|
# containing one single diagnostics report
|
|
diagnostics = published_diagnostics['diagnostics']
|
|
self.expect(len(diagnostics) == 1, "one diagnostics")
|
|
diagnostic = diagnostics[0]
|
|
self.expect(diagnostic['code'] == 3805, 'diagnostic: pre-release compiler')
|
|
self.expect_equal(
|
|
'check range',
|
|
diagnostic['range'],
|
|
{'end': {'character': 0, 'line': 0}, 'start': {'character': 0, 'line': 0}}
|
|
)
|
|
|
|
def test_definition(self):
|
|
"""
|
|
Tests goto-definition. The following tokens can be used to jump from:
|
|
"""
|
|
|
|
self.solc.published_diagnostics.clear()
|
|
|
|
# LHS enum variable in assignment: `weather`
|
|
result = self.solc.client.definition(
|
|
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
|
|
lsp_types.Position(23, 9)) # line/col numbers are 0-based
|
|
self.expect(len(result) == 1, "only one definition returned")
|
|
self.expect(result[0].range == lsp_types.Range(lsp_types.Position(19, 16),
|
|
lsp_types.Position(19, 23)), "range check")
|
|
|
|
# Test on return parameter symbol: `result` at 35:9 (begin of identifier)
|
|
result = self.solc.client.definition(
|
|
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
|
|
lsp_types.Position(24, 8))
|
|
|
|
# Test goto-def of a function-parameter.
|
|
result = self.solc.client.definition(
|
|
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
|
|
lsp_types.Position(24, 17))
|
|
|
|
# Test on function parameter symbol
|
|
# Test on enum type symbol in expression
|
|
# Test on enum value symbol in expression
|
|
# Test on import statement to jump to imported file
|
|
|
|
def test_documentHighlight(self):
|
|
"""
|
|
Tests symbolic hover-hints, that is, highlighting all other symbols
|
|
that are semantically the same.
|
|
|
|
- hovering a variable symbol will highlight all occurrences of that variable.
|
|
- hovering a function symbol will hover all other calls to it as well
|
|
as the function definition's function name itself.
|
|
- hovering an enum value will highlight all other uses of that enum value,
|
|
including the name of the definition of that enum value.
|
|
- hovering an enum type will highlight all other uses of that enum type,
|
|
including the name of the definition of that enum type.
|
|
- anything else will reply with an empty set.
|
|
"""
|
|
|
|
# variable
|
|
reply = self.solc.client.documentHighlight(
|
|
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
|
|
lsp_types.Position(23, 9) # line/col numbers are 0-based
|
|
)
|
|
self.expect(len(reply) == 2, "2 highlights")
|
|
self.expect(reply[0].kind == lsp_types.DocumentHighlightKind.Text, "kind")
|
|
self.expect(reply[0].range == lsp_types.Range(lsp_types.Position(19, 16),
|
|
lsp_types.Position(19, 23)), "range check")
|
|
self.expect(reply[1].kind == lsp_types.DocumentHighlightKind.Text, "kind")
|
|
self.expect(reply[1].range == lsp_types.Range(lsp_types.Position(23, 8),
|
|
lsp_types.Position(23, 15)), "range check")
|
|
|
|
# enum type: Weather, line 24, col 19 .. 25
|
|
reply = self.solc.client.documentHighlight(
|
|
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
|
|
lsp_types.Position(23, 18) # line/col numbers are 0-based
|
|
)
|
|
ENUM_TYPE_HIGHLIGHTS = [
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 6, 'from': 5, 'to': 12 },
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 14, 'from': 4, 'to': 11 },
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 14, 'from': 27, 'to': 34 },
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 19, 'from': 8, 'to': 15 },
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 19, 'from': 26, 'to': 33 },
|
|
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 23, 'from': 18, 'to': 25 }
|
|
]
|
|
self.expect(len(reply) == len(ENUM_TYPE_HIGHLIGHTS), f"expect {len(ENUM_TYPE_HIGHLIGHTS)} highlights")
|
|
for i in range(0, len(reply)):
|
|
self.expect(reply[i].kind == ENUM_TYPE_HIGHLIGHTS[i]['kind'], "check kind")
|
|
self.expect(
|
|
reply[i].range == lsp_types.Range(
|
|
lsp_types.Position(
|
|
ENUM_TYPE_HIGHLIGHTS[i]['line'],
|
|
ENUM_TYPE_HIGHLIGHTS[i]['from']
|
|
),
|
|
lsp_types.Position(
|
|
ENUM_TYPE_HIGHLIGHTS[i]['line'],
|
|
ENUM_TYPE_HIGHLIGHTS[i]['to']
|
|
)
|
|
),
|
|
"range check"
|
|
)
|
|
|
|
|
|
def test_references(self):
|
|
# Shows all references of given symbol def
|
|
pass
|
|
|
|
def test_hover(self):
|
|
pass
|
|
|
|
# }}}
|
|
|
|
class SolidityLSPTestSuite: # {{{
|
|
def __init__(self):
|
|
self.project_root_dir = ''
|
|
self.solc_path = ''
|
|
|
|
def main(self):
|
|
solc_path, project_root_dir = self.parse_args_and_prepare()
|
|
|
|
with SolcInstance(solc_path) as solc:
|
|
project_root_uri = 'file://' + project_root_dir
|
|
workspace_folders = [ {'name': 'solidity-lsp', 'uri': project_root_uri} ]
|
|
traceServer = 'off'
|
|
solc.client.initialize(solc.process.pid, None, project_root_uri,
|
|
None, LSP_CLIENT_CAPS, traceServer,
|
|
workspace_folders)
|
|
solc.client.initialized()
|
|
tests = SolcTests(solc, project_root_dir)
|
|
tests.run()
|
|
|
|
def parse_args_and_prepare(self):
|
|
"""
|
|
Parses CLI args and retruns tuple of path to solc executable
|
|
and path to solidity-project root dir.
|
|
"""
|
|
parser = argparse.ArgumentParser(description='Solidity LSP Test suite')
|
|
parser.add_argument(
|
|
'solc_path',
|
|
type=str,
|
|
default="/home/trapni/work/solidity/build/solc/solc",
|
|
help='Path to solc binary to test against',
|
|
nargs="?"
|
|
)
|
|
parser.add_argument(
|
|
'project_root_dir',
|
|
type=str,
|
|
default=f"{os.path.dirname(os.path.realpath(__file__))}/..",
|
|
help='Path to Solidity project\'s root directory (must be fully qualified).',
|
|
nargs="?"
|
|
)
|
|
args = parser.parse_args()
|
|
project_root_dir = os.path.realpath(args.project_root_dir) + '/test/libsolidity/lsp'
|
|
return [args.solc_path, project_root_dir]
|
|
# }}}
|
|
|
|
if __name__ == "__main__":
|
|
suite = SolidityLSPTestSuite()
|
|
suite.main()
|