editor_utils.py 17.4 KB
Newer Older
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
27
28
29
#############################################################################
##
## Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
## 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
        menu = waitForObject("{type='QMenu' unnamed='1' visible='1'}", 500)
79
80
        if platform.system() == 'Darwin':
            menu.activateWindow()
81
82
83
84
85
86
87
88
89
90
91
92
        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)

93
94
95
96
# 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())
97
    if platform.system() == 'Darwin':
98
        JIRA.performWorkaroundForBug(8735, JIRA.Bug.CREATOR, editor)
99
    openContextMenu(editor, rect.x+rect.width/2, rect.y+rect.height/2, 0)
100
101
102
    menuInList = [None]
    waitFor("menuVisibleAtEditor(editor, menuInList)", 5000)
    return menuInList[0]
103

104
105
106
107
108
109
# 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)
110

111
112
113
114
115
# works for all standard editors
def replaceEditorContent(editor, newcontent):
    type(editor, "<Ctrl+A>")
    type(editor, "<Delete>")
    type(editor, newcontent)
116
117
118
119
120
121
122

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
123
            type(editor, "<Return>")
124
125
    else:
        test.warning("Illegal parameter passed to typeLines()")
Christian Stenger's avatar
Christian Stenger committed
126
127
128
129
130
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

# 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":
159
                __handleColorTips__(tip, expectedVals, altVal)
Christian Stenger's avatar
Christian Stenger committed
160
161
162
163
            elif expectedType == "TextTip":
                __handleTextTips__(tip, expectedVals, altVal)
            elif expectedType == "WidgetTip":
                test.warning("Sorry - WidgetTip checks aren't implemented yet.")
164
165
            sendEvent("QMouseEvent", editor, QEvent.MouseMove, 0, -50, Qt.NoButton, 0)
            waitFor("isNull(tip)", 10000)
Christian Stenger's avatar
Christian Stenger committed
166
167
168
169
170
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

# 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!
206
207
208
209
210
211
def __handleColorTips__(colTip, expectedColor, alternativeColor):
    def uint(value):
        if value < 0:
            return 0xffffffff + value + 1
        return value

Christian Stenger's avatar
Christian Stenger committed
212
213
    cmp = QColor()
    cmp.setNamedColor(expectedColor)
214
215
216
217
    if alternativeColor:
        alt = QColor()
        alt.setNamedColor(alternativeColor)
    if cmp.alpha() != 255 or alternativeColor and alt.alpha() != 255:
Christian Stenger's avatar
Christian Stenger committed
218
219
220
221
222
223
        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)
224
    if rgb.rgba() == cmp.rgba() or alternativeColor and rgb.rgba() == alt.rgba():
Christian Stenger's avatar
Christian Stenger committed
225
226
        test.passes("ColorTip verified")
    else:
227
228
229
230
231
        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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251

# 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
252
253
254
255
256
257

def getEditorForFileSuffix(curFile):
    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"]
258
    glslEditorSuffixes= ["frag", "vert", "fsh", "vsh", "glsl", "shader", "gsh"]
259
    pytEditorSuffixes = ["py", "pyw", "wsgi"]
260
261
    suffix = __getFileSuffix__(curFile)
    if suffix in cppEditorSuffixes:
262
        editor = waitForObject(":Qt Creator_CppEditor::Internal::CPPEditorWidget")
263
    elif suffix in qmlEditorSuffixes:
264
        editor = waitForObject(":Qt Creator_QmlJSEditor::QmlJSTextEditorWidget")
265
    elif suffix in proEditorSuffixes:
266
        editor = waitForObject(":Qt Creator_ProFileEditorWidget")
267
268
269
    elif suffix in glslEditorSuffixes:
        editor = waitForObject("{type='GLSLEditor::GLSLTextEditorWidget' unnamed='1' "
                               "visible='1' window=':Qt Creator_Core::Internal::MainWindow'}")
270
271
    elif suffix in pytEditorSuffixes:
        editor = waitForObject(":Qt Creator_PythonEditor::EditorWidget")
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
    else:
        test.log("Trying PlainTextEditor (file suffix: %s)" % suffix)
        try:
            editor = waitForObject("{type='TextEditor::PlainTextEditorWidget' unnamed='1' "
                                   "visible='1' window=':Qt Creator_Core::Internal::MainWindow'}", 3000)
        except:
            test.fatal("Unsupported file suffix for file '%s'" % curFile)
            editor = None
    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]
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304

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

def validateSearchResult(expectedCount):
    searchResult = waitForObject(":Qt Creator_SearchResult_Core::Internal::OutputPaneToggleButton")
    ensureChecked(searchResult)
    resultTreeView = waitForObject("{type='Find::Internal::SearchResultTreeView' unnamed='1' "
                                   "visible='1' window=':Qt Creator_Core::Internal::MainWindow'}")
    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()
305
    for index in dumpIndices(model):
306
307
308
        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))
309
        for chIndex in dumpIndices(model, index):
310
            resultTreeView.scrollTo(chIndex)
311
            text = str(chIndex.data()).rstrip('\r')
312
313
314
315
316
317
318
319
320
            rect = resultTreeView.visualRect(chIndex)
            doubleClick(resultTreeView, rect.x+5, rect.y+5, 0, Qt.LeftButton)
            editor = getEditorForFileSuffix(itemText)
            waitFor("lineUnderCursor(editor) == text", 2000)
            test.compare(lineUnderCursor(editor), text)

# this function invokes context menu and command from it
def invokeContextMenuItem(editorArea, command1, command2 = None):
    ctxtMenu = openContextMenuOnTextCursorPosition(editorArea)
321
    activateItem(waitForObjectItem(objectMap.realName(ctxtMenu), command1, 2000))
322
    if command2:
323
324
        activateItem(waitForObjectItem("{title='%s' type='QMenu' visible='1' window=%s}"
                                       % (command1, objectMap.realName(ctxtMenu)), command2, 2000))
325
326
327
328
329
330
331
332
333
334
335
336
337

# 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
338

339
340
def addBranchWildcardToRoot(rootNode):
    pos = rootNode.find(".")
Christian Stenger's avatar
Christian Stenger committed
341
342
    if pos == -1:
        return rootNode + " (*)"
343
344
    return rootNode[:pos] + " (*)" + rootNode[pos:]

345
346
def openDocument(treeElement):
    try:
347
        selectFromCombo(":Qt Creator_Core::Internal::NavComboBox", "Open Documents")
348
        navigator = waitForObject(":Qt Creator_Utils::NavigationTreeView")
349
350
351
352
353
        try:
            item = waitForObjectItem(navigator, treeElement, 3000)
        except:
            treeElement = addBranchWildcardToRoot(treeElement)
            item = waitForObjectItem(navigator, treeElement)
354
355
        doubleClickItem(navigator, treeElement, 5, 5, 0, Qt.LeftButton)
        mainWindow = waitForObject(":Qt Creator_Core::Internal::MainWindow")
356
        waitFor("item.text in str(mainWindow.windowTitle)")
357
358
359
        return True
    except:
        return False
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387

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")
    for i in range(len(oldLine)):
        type(editor, "<Backspace>")
    type(editor, newLine)
    return True