editor_utils.py 18.8 KB
Newer Older
1
2
#############################################################################
##
3
## Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## Contact: http://www.qt-project.org/legal
##
## This file is part of Qt Creator.
##
## Commercial License Usage
## Licensees holding valid commercial Qt licenses may use this file in
## accordance with the commercial license agreement provided with the
## Software or, alternatively, in accordance with the terms contained in
## a written agreement between you and Digia.  For licensing terms and
## conditions see http://qt.digia.com/licensing.  For further information
## use the contact form at http://qt.digia.com/contact-us.
##
## GNU Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 2.1 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL included in the
## packaging of this file.  Please review the following information to
## ensure the GNU Lesser General Public License version 2.1 requirements
## will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
##
## In addition, as a special exception, Digia gives you certain additional
## rights.  These rights are described in the Digia Qt LGPL Exception
## version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
##
#############################################################################

30
31
32
33
34
35
36
import re;

# places the cursor inside the given editor into the given line
# (leading and trailing whitespaces are ignored!)
# and goes to the end of the line
# line can be a regex - but if so, remember to set isRegex to True
# the function returns True if this went fine, False on error
37
def placeCursorToLine(editor, line, isRegex=False):
38
39
40
41
42
43
    def getEditor():
        return waitForObject(editor)

    isDarwin = platform.system() == 'Darwin'
    if not isinstance(editor, (str, unicode)):
        editor = objectMap.realName(editor)
44
    oldPosition = 0
45
46
47
48
    if isDarwin:
        type(getEditor(), "<Home>")
    else:
        type(getEditor(), "<Ctrl+Home>")
49
50
51
52
    found = False
    if isRegex:
        regex = re.compile(line)
    while not found:
53
        currentLine = str(lineUnderCursor(getEditor())).strip()
54
55
        found = isRegex and regex.match(currentLine) or not isRegex and currentLine == line
        if not found:
56
57
            type(getEditor(), "<Down>")
            newPosition = getEditor().textCursor().position()
58
59
60
            if oldPosition == newPosition:
                break
            oldPosition = newPosition
61
62
63
    if not found:
        test.fatal("Couldn't find line matching\n\n%s\n\nLeaving test..." % line)
        return False
64
65
66
67
    if isDarwin:
        type(getEditor(), "<Ctrl+Right>")
    else:
        type(getEditor(), "<End>")
68
69
    return True

70
71
72
73
74
75
76
77
# this function returns True if a QMenu is
# popped up above the given editor
# param editor is the editor where the menu should appear
# param menuInList is a list containing one item. This item will be assigned the menu if there is one.
#                  THIS IS A HACK to get a pass-by-reference
def menuVisibleAtEditor(editor, menuInList):
    menuInList[0] = None
    try:
78
79
80
81
82
83
84
        # Hack for Squish 5.0.1 handling menus of Qt5.2 on Mac (avoids crash) - remove asap
        if platform.system() == 'Darwin':
            for obj in object.topLevelObjects():
                if className(obj) == "QMenu" and obj.visible and widgetContainsPoint(editor, obj.mapToGlobal(QPoint(0, 0))):
                    menuInList[0] = obj
                    return True
            return False
85
        menu = waitForObject("{type='QMenu' unnamed='1' visible='1'}", 500)
86
87
        if platform.system() == 'Darwin':
            menu.activateWindow()
88
89
90
91
92
93
94
95
96
97
98
99
        success = menu.visible and widgetContainsPoint(editor, menu.mapToGlobal(QPoint(0, 0)))
        if success:
            menuInList[0] = menu
        return success
    except:
        return False

# this function checks whether the given global point (QPoint)
# is contained in the given widget
def widgetContainsPoint(widget, point):
    return QRect(widget.mapToGlobal(QPoint(0, 0)), widget.size).contains(point)

100
101
102
103
104
# this function simply opens the context menu inside the given editor
# at the same position where the text cursor is located at
def openContextMenuOnTextCursorPosition(editor):
    rect = editor.cursorRect(editor.textCursor())
    openContextMenu(editor, rect.x+rect.width/2, rect.y+rect.height/2, 0)
105
106
107
    menuInList = [None]
    waitFor("menuVisibleAtEditor(editor, menuInList)", 5000)
    return menuInList[0]
108

109
110
111
112
113
114
# this function marks/selects the text inside the given editor from current cursor position
# param direction is one of "Left", "Right", "Up", "Down", but "End" and combinations work as well
# param typeCount defines how often the cursor will be moved in the given direction (while marking)
def markText(editor, direction, typeCount=1):
    for i in range(typeCount):
        type(editor, "<Shift+%s>" % direction)
115

116
117
# works for all standard editors
def replaceEditorContent(editor, newcontent):
118
    type(editor, "<Ctrl+a>")
119
120
    type(editor, "<Delete>")
    type(editor, newcontent)
121
122
123
124
125
126
127

def typeLines(editor, lines):
    if isinstance(lines, (str, unicode)):
        lines = [lines]
    if isinstance(lines, (list, tuple)):
        for line in lines:
            type(editor, line)
Christian Stenger's avatar
Christian Stenger committed
128
            type(editor, "<Return>")
129
130
    else:
        test.warning("Illegal parameter passed to typeLines()")
Christian Stenger's avatar
Christian Stenger committed
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

# function to verify hoverings on e.g. code inside of the given editor
# param editor the editor object
# param lines a list/tuple of regex that indicates which lines should be verified
# param additionalKeyPresses an array holding the additional typings to do (special chars for cursor movement)
#       to get to the location (inside line) where to trigger the hovering (must be the same for all lines)
# param expectedTypes list/tuple holding the type of the (tool)tips that should occur (for each line)
# param expectedValues list/tuple of dict or list/tuple of strings regarding the types that have been used
#       if it's a dict it indicates a property value pair, if it's a string it is type specific (e.g. color value for ColorTip)
# param alternativeValues same as expectedValues, but here you can submit alternatives - this is for example
#       necessary if you do not add the correct documentation (from where the tip gets its content)
def verifyHoveringOnEditor(editor, lines, additionalKeyPresses, expectedTypes, expectedValues, alternativeValues=None):
    counter = 0
    for line in lines:
        expectedVals = expectedValues[counter]
        expectedType = expectedTypes[counter]
        altVal = None
        if isinstance(alternativeValues, (list, tuple)):
            altVal = alternativeValues[counter]
        counter += 1
        placeCursorToLine(editor, line, True)
        for ty in additionalKeyPresses:
            type(editor, ty)
        rect = editor.cursorRect(editor.textCursor())
        sendEvent("QMouseEvent", editor, QEvent.MouseMove, rect.x+rect.width/2, rect.y+rect.height/2, Qt.NoButton, 0)
        try:
            tip = waitForObject("{type='%s' visible='1'}" % expectedType)
        except:
            tip = None
        if tip == None:
            test.warning("Could not get %s for line containing pattern '%s'" % (expectedType,line))
        else:
            if expectedType == "ColorTip":
164
                __handleColorTips__(tip, expectedVals, altVal)
Christian Stenger's avatar
Christian Stenger committed
165
166
167
168
            elif expectedType == "TextTip":
                __handleTextTips__(tip, expectedVals, altVal)
            elif expectedType == "WidgetTip":
                test.warning("Sorry - WidgetTip checks aren't implemented yet.")
169
170
            sendEvent("QMouseEvent", editor, QEvent.MouseMove, 0, -50, Qt.NoButton, 0)
            waitFor("isNull(tip)", 10000)
Christian Stenger's avatar
Christian Stenger committed
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

# helper function that handles verification of TextTip hoverings
# param textTip the TextTip object
# param expectedVals a dict holding property value pairs that must match
def __handleTextTips__(textTip, expectedVals, alternativeVals):
    props = object.properties(textTip)
    expFail = altFail = False
    eResult = verifyProperties(props, expectedVals)
    for val in eResult.itervalues():
        if not val:
            expFail = True
            break
    if expFail and alternativeVals != None:
        aResult = verifyProperties(props, alternativeVals)
    else:
        altFail = True
        aResult = None
    if not expFail:
        test.passes("TextTip verified")
    else:
        for key,val in eResult.iteritems():
            if val == False:
                if aResult and aResult.get(key):
                    test.passes("Property '%s' does not match expected, but alternative value" % key)
                else:
                    aVal = None
                    if alternativeVals:
                        aVal = alternativeVals.get(key, None)
                    if aVal:
                        test.fail("Property '%s' does not match - expected '%s' or '%s', got '%s'" % (key, expectedVals.get(key), aVal, props.get(key)))
                    else:
                        test.fail("Property '%s' does not match - expected '%s', got '%s" % (key, expectedVals.get(key), props.get(key)))
            else:
                test.fail("Property '%s' could not be found inside properties" % key)

# helper function that handles verification of ColorTip hoverings
# param colTip the ColorTip object
# param expectedColor a single string holding the color the ColorTip should have
# Attention: because of being a non-standard Qt object it's not possible to
# verify colors which are (semi-)transparent!
211
212
213
214
215
216
def __handleColorTips__(colTip, expectedColor, alternativeColor):
    def uint(value):
        if value < 0:
            return 0xffffffff + value + 1
        return value

Christian Stenger's avatar
Christian Stenger committed
217
218
    cmp = QColor()
    cmp.setNamedColor(expectedColor)
219
220
221
222
    if alternativeColor:
        alt = QColor()
        alt.setNamedColor(alternativeColor)
    if cmp.alpha() != 255 or alternativeColor and alt.alpha() != 255:
Christian Stenger's avatar
Christian Stenger committed
223
224
225
226
227
228
        test.warning("Cannot handle transparent colors - cancelling this verification")
        return
    dPM = QPixmap.grabWidget(colTip, 1, 1, colTip.width-2, colTip.height-2)
    img = dPM.toImage()
    rgb = img.pixel(1, 1)
    rgb = QColor(rgb)
229
    if rgb.rgba() == cmp.rgba() or alternativeColor and rgb.rgba() == alt.rgba():
Christian Stenger's avatar
Christian Stenger committed
230
231
        test.passes("ColorTip verified")
    else:
232
233
234
235
236
        altColorText = ""
        if alternativeColor:
            altColorText = " or '%X'" % uint(alt.rgb())
        test.fail("ColorTip does not match - expected color '%X'%s got '%X'"
                  % (uint(cmp.rgb()), altColorText, uint(rgb.rgb())))
Christian Stenger's avatar
Christian Stenger committed
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256

# function that checks whether all expected properties (including their values)
# match the given properties
# param properties a dict holding the properties to check
# param expectedProps a dict holding the key value pairs that must be found inside properties
# this function returns a dict holding the keys of the expectedProps - the value of each key
# is a boolean that indicates whether this key could have been found inside properties and
# the values matched or None if the key could not be found
def verifyProperties(properties, expectedProps):
    if not isinstance(properties, dict) or not isinstance(expectedProps, dict):
        test.warning("Wrong usage - both parameter must be of type dict")
        return {}
    result = {}
    for key,val in expectedProps.iteritems():
        foundVal = properties.get(key, None)
        if foundVal != None:
            result[key] = val == foundVal
        else:
            result[key] = None
    return result
257

258
def getEditorForFileSuffix(curFile, treeViewSyntax=False):
259
260
261
262
    cppEditorSuffixes = ["cpp", "cc", "CC", "h", "H", "cp", "cxx", "C", "c++", "inl", "moc", "qdoc",
                         "tcc", "tpp", "t++", "c", "cu", "m", "mm", "hh", "hxx", "h++", "hpp", "hp"]
    qmlEditorSuffixes = ["qml", "qmlproject", "js", "qs", "qtt"]
    proEditorSuffixes = ["pro", "pri", "prf"]
263
    glslEditorSuffixes= ["frag", "vert", "fsh", "vsh", "glsl", "shader", "gsh"]
264
    pytEditorSuffixes = ["py", "pyw", "wsgi"]
265
    suffix = __getFileSuffix__(curFile)
266
267
268
    expected = os.path.basename(curFile)
    if treeViewSyntax:
        expected = simpleFileName(curFile)
269
    mainWindow = waitForObject(":Qt Creator_Core::Internal::MainWindow")
270
    if not waitFor("expected in str(mainWindow.windowTitle)", 5000):
271
        test.fatal("Window title (%s) did not switch to expected file (%s)."
272
                   % (str(mainWindow.windowTitle), expected))
273
274
275
276
277
    try:
        if suffix in cppEditorSuffixes:
            editor = waitForObject(":Qt Creator_CppEditor::Internal::CPPEditorWidget")
        elif suffix in qmlEditorSuffixes:
            editor = waitForObject(":Qt Creator_QmlJSEditor::QmlJSTextEditorWidget")
278
        elif suffix in proEditorSuffixes or suffix in glslEditorSuffixes or suffix in pytEditorSuffixes:
279
            editor = waitForObject(":Qt Creator_TextEditor::BaseTextEditorWidget")
280
        else:
281
            test.log("Trying BaseTextEditorWidget (file suffix: %s)" % suffix)
282
            try:
283
                editor = waitForObject(":Qt Creator_TextEditor::BaseTextEditorWidget", 3000)
284
285
286
287
288
289
290
291
292
293
294
            except:
                test.fatal("Unsupported file suffix for file '%s'" % curFile)
                editor = None
    except:
        f = str(waitForObject(":Qt Creator_Core::Internal::MainWindow").windowTitle).split(" ", 1)[0]
        if os.path.basename(curFile) == f:
            test.fatal("Could not find editor although expected file matches.")
        else:
            test.fatal("Expected (%s) and current file (%s) do not match. Failed to get editor"
                       % (os.path.basename(curFile), f))
        editor = None
295
296
297
298
299
300
301
302
303
304
    return editor

# helper that determines the file suffix of the given fileName
# (doesn't matter if fileName contains the path as well)
def __getFileSuffix__(fileName):
    suffix = os.path.basename(fileName).rsplit(".", 1)
    if len(suffix) == 1:
        return None
    else:
        return suffix[1]
305
306
307
308
309
310
311
312

def maskSpecialCharsForSearchResult(filename):
    filename = filename.replace("_", "\\_").replace(".","\\.")
    return filename

def validateSearchResult(expectedCount):
    searchResult = waitForObject(":Qt Creator_SearchResult_Core::Internal::OutputPaneToggleButton")
    ensureChecked(searchResult)
313
    resultTreeView = waitForObject(":Qt Creator_Find::Internal::SearchResultTreeView")
314
315
316
317
318
    counterLabel = waitForObject("{type='QLabel' unnamed='1' visible='1' text?='*matches found.' "
                                 "window=':Qt Creator_Core::Internal::MainWindow'}")
    matches = cast((str(counterLabel.text)).split(" ", 1)[0], "int")
    test.compare(matches, expectedCount, "Verified match count.")
    model = resultTreeView.model()
319
    for index in dumpIndices(model):
320
321
322
        itemText = str(model.data(index).toString())
        doubleClickItem(resultTreeView, maskSpecialCharsForSearchResult(itemText), 5, 5, 0, Qt.LeftButton)
        test.log("%d occurrences in %s" % (model.rowCount(index), itemText))
323
        for chIndex in dumpIndices(model, index):
324
            resultTreeView.scrollTo(chIndex)
325
            text = str(chIndex.data()).rstrip('\r')
326
327
328
            rect = resultTreeView.visualRect(chIndex)
            doubleClick(resultTreeView, rect.x+5, rect.y+5, 0, Qt.LeftButton)
            editor = getEditorForFileSuffix(itemText)
329
330
331
            if not waitFor("lineUnderCursor(editor) == text", 2000):
                test.warning("Jumping to search result '%s' is pretty slow." % text)
                waitFor("lineUnderCursor(editor) == text", 2000)
332
333
334
335
336
            test.compare(lineUnderCursor(editor), text)

# this function invokes context menu and command from it
def invokeContextMenuItem(editorArea, command1, command2 = None):
    ctxtMenu = openContextMenuOnTextCursorPosition(editorArea)
337
338
339
340
    if platform.system() == 'Darwin':
        activateItem(ctxtMenu, command1)
    else:
        activateItem(waitForObjectItem(objectMap.realName(ctxtMenu), command1, 2000))
341
    if command2:
342
343
344
345
346
347
348
349
350
        # Hack for Squish 5.0.1 handling menus of Qt5.2 on Mac (avoids crash) - remove asap
        if platform.system() == 'Darwin':
            for obj in object.topLevelObjects():
                if className(obj) == 'QMenu' and obj.visible and not obj == ctxtMenu:
                    activateItem(obj, command2)
                    break
        else:
            activateItem(waitForObjectItem("{title='%s' type='QMenu' visible='1' window=%s}"
                                           % (command1, objectMap.realName(ctxtMenu)), command2, 2000))
351
352
353
354
355
356
357
358
359
360
361
362
363

# this function invokes the "Find Usages" item from context menu
# param editor an editor object
# param line a line in editor (content of the line as a string)
# param typeOperation a key to type
# param n how often repeat the type operation?
def invokeFindUsage(editor, line, typeOperation, n=1):
    if not placeCursorToLine(editor, line, True):
        return False
    for i in range(n):
        type(editor, typeOperation)
    invokeContextMenuItem(editor, "Find Usages")
    return True
364

365
366
def addBranchWildcardToRoot(rootNode):
    pos = rootNode.find(".")
Christian Stenger's avatar
Christian Stenger committed
367
    if pos == -1:
368
369
        return rootNode + " [[]*[]]"
    return rootNode[:pos] + " [[]*[]]" + rootNode[pos:]
370

371
372
def openDocument(treeElement):
    try:
Robert Loehning's avatar
Robert Loehning committed
373
        selectFromCombo(":Qt Creator_Core::Internal::NavComboBox", "Projects")
374
        navigator = waitForObject(":Qt Creator_Utils::NavigationTreeView")
375
376
377
378
379
        try:
            item = waitForObjectItem(navigator, treeElement, 3000)
        except:
            treeElement = addBranchWildcardToRoot(treeElement)
            item = waitForObjectItem(navigator, treeElement)
380
381
        doubleClickItem(navigator, treeElement, 5, 5, 0, Qt.LeftButton)
        mainWindow = waitForObject(":Qt Creator_Core::Internal::MainWindow")
382
383
        expected = str(item.text).split("/")[-1]
        waitFor("expected in str(mainWindow.windowTitle)")
384
385
386
        return True
    except:
        return False
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410

def earlyExit(details="No additional information"):
    test.fail("Something went wrong running this test", details)
    invokeMenuItem("File", "Save All")
    invokeMenuItem("File", "Exit")

def openDocumentPlaceCursor(doc, line, additionalFunction=None):
    cppEditorStr = ":Qt Creator_CppEditor::Internal::CPPEditorWidget"
    if openDocument(doc) and placeCursorToLine(cppEditorStr, line):
        if additionalFunction:
            additionalFunction()
        return str(waitForObject(cppEditorStr).plainText)
    else:
        earlyExit("Open %s or placing cursor to line (%s) failed." % (doc, line))
        return None

# Replaces a line in the editor with another
# param fileSpec a string specifying a file in Projects view
# param oldLine a string holding the line to be replaced
# param newLine a string holding the line to be inserted
def replaceLine(fileSpec, oldLine, newLine):
    if openDocumentPlaceCursor(fileSpec, oldLine) == None:
        return False
    editor = waitForObject(":Qt Creator_CppEditor::Internal::CPPEditorWidget")
411
    for _ in oldLine:
412
413
414
        type(editor, "<Backspace>")
    type(editor, newLine)
    return True