From 004d50cb8c31f4e53434fdd907cb643709f3f4f4 Mon Sep 17 00:00:00 2001
From: David Malcolm <dmalcolm@redhat.com>
Date: Tue, 23 May 2017 12:58:19 -0400
Subject: [PATCH 25/28] Language Server Protocol: work-in-progess on testsuite

This file adds the beginnings of a testsuite for the LSP
implementation.

The test cases are implemented in Python; they aren't currently
wired up to DejaGnu (I've been invoking them directly).

There's an automated test case, and a PyGTK UI.

Both require the language server to be manually started
(see the comments).

gcc/testsuite/ChangeLog:
	* gcc.dg/lsp/lsp.py: New file.
	* gcc.dg/lsp/test.c: New test file.
	* gcc.dg/lsp/test.py: New test case.
	* gcc.dg/lsp/toy-ide.py: New file.
---
 gcc/testsuite/gcc.dg/lsp/lsp.py     | 125 ++++++++++++++++++++++++++++++++++++
 gcc/testsuite/gcc.dg/lsp/test.c     |  12 ++++
 gcc/testsuite/gcc.dg/lsp/test.py    |  24 +++++++
 gcc/testsuite/gcc.dg/lsp/toy-ide.py | 107 ++++++++++++++++++++++++++++++
 4 files changed, 268 insertions(+)
 create mode 100644 gcc/testsuite/gcc.dg/lsp/lsp.py
 create mode 100644 gcc/testsuite/gcc.dg/lsp/test.c
 create mode 100644 gcc/testsuite/gcc.dg/lsp/test.py
 create mode 100644 gcc/testsuite/gcc.dg/lsp/toy-ide.py

diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.py b/gcc/testsuite/gcc.dg/lsp/lsp.py
new file mode 100644
index 0000000..56468da
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/lsp.py
@@ -0,0 +1,125 @@
+# A python module for implementing LSP clients
+
+import json
+
+import jsonrpc # pip install json-rpc
+import requests
+
+# Various types to model the LSP interface
+
+class Position:
+    def __init__(self, line, character):
+        self.line = line
+        self.character = character
+
+    def __repr__(self):
+        return 'Position(%r, %r)' % (self.line, self.character)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {'line': self.line, 'character': self.character}
+
+    @staticmethod
+    def from_json(js):
+        return Position(js['line'], js['character'])
+
+class Range:
+    def __init__(self, start, end):
+        self.start = start
+        self.end = end
+
+    def __repr__(self):
+        return 'Range(%r, %r)' % (self.start, self.end)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    @staticmethod
+    def from_json(js):
+        return Range(Position.from_json(js['start']),
+                     Position.from_json(js['end']))
+
+class Location:
+    def __init__(self, uri, range):
+        self.uri = uri
+        self.range = range
+
+    def __repr__(self):
+        return 'Location(%r, %r)' % (self.uri, self.range)
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    @staticmethod
+    def from_json(js):
+        return Location(js['uri'], Range.from_json(js['range']))
+
+    def dump(self, msg):
+        print('%s:%i:%i: %s' % (self.uri, self.range.start.line,
+                                self.range.start.character, msg))
+        # FIXME: underline
+        # linecache uses 1-based line numbers, whereas LSP uses
+        # 0-based line numbers
+        import linecache
+        line = linecache.getline(self.uri, self.range.start.line + 1)
+        print('line: %r' % line)
+
+class TextDocumentIdentifier:
+    def __init__(self, uri):
+        self.uri = uri
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {'uri': self.uri}
+
+class TextDocumentPositionParams:
+    def __init__(self, textDocument, position):
+        self.textDocument = textDocument
+        self.position = position
+
+    def __eq__(self, other):
+        return self.__dict__ == other.__dict__
+
+    def to_json(self):
+        return {"textDocument" : self.textDocument.to_json(),
+                "position" : self.position.to_json()}
+
+# A wrapper for making LSP calls against a server
+
+class Proxy:
+    def __init__(self, url):
+        self.url = url
+        self.next_id = 0
+
+    def make_request(self, method, params):
+        json_req = {"method": method,
+                    "params": params,
+                    "jsonrpc": "2.0",
+                    "id": self.next_id}
+        self.next_id += 1
+        return json_req
+
+    def post_request(self, method, params):
+        payload = self.make_request(method, params)
+        headers = {'content-type': 'application/json'}
+        response = requests.post(self.url, data=json.dumps(payload),
+                                 headers=headers)
+        print('response: %r' % response)
+        print('response.json(): %r' % response.json())
+        return response.json()
+
+    def goto_definition(self, textDocument, position):
+        params = TextDocumentPositionParams(textDocument, position)
+        json_resp = self.post_request('textDocument/definition',
+                                      params.to_json())
+        print(json_resp)
+        # Expect either a Location or a list of Location
+        if isinstance(json_resp, list):
+            return [Location.from_json(item) for item in json_resp]
+        else:
+            return Location.from_json(json_resp)
+
diff --git a/gcc/testsuite/gcc.dg/lsp/test.c b/gcc/testsuite/gcc.dg/lsp/test.c
new file mode 100644
index 0000000..bb80bd0
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.c
@@ -0,0 +1,12 @@
+/* */
+
+struct foo
+{
+  int color;
+  int shape;
+};
+
+int test (struct foo *ptr)
+{
+  ptr->
+}
diff --git a/gcc/testsuite/gcc.dg/lsp/test.py b/gcc/testsuite/gcc.dg/lsp/test.py
new file mode 100644
index 0000000..b380091
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/test.py
@@ -0,0 +1,24 @@
+from lsp import Proxy, TextDocumentIdentifier, Position, Range, Location
+
+def main():
+    # This assumes that we're running this:
+    # (hardcoding the particular source file for now):
+    # ./xgcc -B. -c ../../src/gcc/testsuite/gcc.dg/lsp/test.c \
+    #    -flsp=4000 -fblt -wrapper gdb,--args
+
+    url = "http://localhost:4000/jsonrpc"
+    proxy = Proxy(url)
+    # Ask for the location of a usage of "struct foo" (0-based lines)
+    result = proxy.goto_definition(TextDocumentIdentifier('test.c'),
+                                   Position(8, 16))
+
+    # We expect to get back the location of where "struct foo" is defined
+    print(result)
+    # FIXME: filename/uri (currently a particular relative location)
+    assert result == [Location(u'../../src/gcc/testsuite/gcc.dg/lsp/test.c',
+                               Range(Position(2, 0), Position(6, 1)))]
+    for loc in result:
+        loc.dump("definition")
+
+if __name__ == "__main__":
+    main()
diff --git a/gcc/testsuite/gcc.dg/lsp/toy-ide.py b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
new file mode 100644
index 0000000..e3c0cce
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/lsp/toy-ide.py
@@ -0,0 +1,107 @@
+# A toy IDE implemented in PyGTK
+
+import pygtk
+pygtk.require('2.0')
+import gtk
+import gtksourceview2
+#help(gtksourceview2)
+
+import lsp
+
+class ToyIde:
+    def delete_event(self, widget, event, data=None):
+        return False
+
+    def destroy(self, widget, data=None):
+        gtk.main_quit()
+
+    def quit_cb(self, b):
+        gtk.main_quit()
+
+    def get_cursor_position(self):
+        """Get the position of the cursor within the buffer
+        as an lsp.Position"""
+        mark = self.buf.get_insert()
+        print(mark)
+        iter = self.buf.get_iter_at_mark(mark)
+        print(iter)
+
+        print('line: %r' % iter.get_line()) # 0-based line
+        print('line_offset: %r' % iter.get_line_offset()) # 0-based offse
+
+        return lsp.Position(iter.get_line(), iter.get_line_offset())
+
+    def goto_definition_cb(self, b):
+        print "goto_definition_cb"
+        locs = self.lsp.goto_definition(lsp.TextDocumentIdentifier('test.c'),
+                                        self.get_cursor_position())
+        print(locs)
+        if len(locs) == 1:
+            loc = locs[0]
+            # Both lsp.Range and gtk.TextBuffer.select_range are inclusive
+            # on the start, exclusive on the end-point
+            self.buf.select_range(
+                self.buf.get_iter_at_line_offset(loc.range.start.line,
+                                                 loc.range.start.character),
+                self.buf.get_iter_at_line_offset(loc.range.end.line,
+                                                 loc.range.end.character))
+
+    def __init__(self, path):
+        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
+        self.window.connect("delete_event", self.delete_event)
+        self.window.connect("destroy", self.destroy)
+        self.window.set_size_request(640, 480)
+        vbox = gtk.VBox()
+        self.window.add(vbox)
+
+        uimanager = gtk.UIManager()
+        accelgroup = uimanager.get_accel_group()
+        self.window.add_accel_group(accelgroup)
+
+        actiongroup = gtk.ActionGroup('UIManagerExample')
+        self.actiongroup = actiongroup
+        actiongroup.add_actions([('Quit', gtk.STOCK_QUIT, '_Quit', None,
+                                  'Quit', self.quit_cb),
+                                 ('File', None, '_File'),
+                                 ('Test', None, '_Test'),
+                                 ('GotoDefinition', None, 'Goto _Definition',
+                                  None, 'Goto the definition of this thing',
+                                  self.goto_definition_cb)
+                             ])
+        actiongroup.get_action('Quit').set_property('short-label', '_Quit')
+        uimanager.insert_action_group(actiongroup, 0)
+
+        merge_id = uimanager.add_ui_from_string("""
+ <ui>
+    <menubar name="MenuBar">
+      <menu action="File">
+        <menuitem action="Quit"/>
+      </menu>
+      <menu action="Test">
+        <menuitem action="GotoDefinition"/>
+      </menu>
+    </menubar>
+  </ui>
+""")
+        menubar = uimanager.get_widget('/MenuBar')
+        vbox.pack_start(menubar, False)
+
+        self.buf = gtksourceview2.Buffer()
+
+        with open(path) as f:
+            text = f.read()
+        self.buf.set_text(text)
+
+        self.sv = gtksourceview2.View(buffer=self.buf)
+        self.sv.set_show_line_numbers(True)
+
+        vbox.add(self.sv)
+        self.window.show_all()
+
+        self.lsp = lsp.Proxy('http://localhost:4000/jsonrpc')
+
+    def main(self):
+        gtk.main()
+
+ide = ToyIde("gcc/testsuite/gcc.dg/lsp/test.c")
+ide.main()
-- 
1.8.5.3

