Skip to content

Commit f590752

Browse files
committed
Tests: Refactor and improve stability
1 parent 1ee4548 commit f590752

File tree

6 files changed

+146
-103
lines changed

6 files changed

+146
-103
lines changed

src/tests/itemtests/itemtests.cpp

Lines changed: 108 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
namespace {
1313

1414
Q_DECLARE_LOGGING_CATEGORY(plugin)
15-
Q_LOGGING_CATEGORY(plugin, "copyq.plugin.itemtests")
15+
Q_LOGGING_CATEGORY(plugin, "copyq.keys")
1616

17-
QString objectAddress(QObject *object)
17+
QString widgetAddress(QWidget *widget)
1818
{
19-
if (!object)
19+
if (!widget)
2020
return QStringLiteral("<null>");
2121

2222
QString result;
23-
QObject *current = object;
23+
QWidget *current = widget;
2424
while (current) {
2525
const QString className = current->metaObject()->className();
2626
const QString objectName = current->objectName();
@@ -33,14 +33,16 @@ QString objectAddress(QObject *object)
3333
if (!result.isEmpty())
3434
result.append('<');
3535
result.append(name);
36-
const QString text = current->property("text").toString()
36+
QString text = current->property("text").toString()
3737
.remove('&')
3838
// Remove HTML tags
3939
.remove(QRegularExpression(QStringLiteral("</?[^>]*>")));
40+
if ( text.isEmpty() && current->isWindow() )
41+
text = current->windowTitle();
4042
if ( !text.isEmpty() )
4143
result.append(QStringLiteral("'%1'").arg(text));
4244
}
43-
current = current->parent();
45+
current = current->parentWidget();
4446
}
4547
return result;
4648
}
@@ -75,7 +77,7 @@ QWidget *findWidgetWithProperties(const QString &properties, QWidget *parent)
7577
const QStringList props = name.split('|', Qt::SkipEmptyParts);
7678
for (QWidget *child : parent->findChildren<QWidget*>()) {
7779
if (child->isVisible() && matchesProperties(child, props)) {
78-
qCDebug(plugin) << "Found target:" << objectAddress(child);
80+
qCDebug(plugin) << "Found target:" << widgetAddress(child);
7981
return child;
8082
}
8183
}
@@ -84,10 +86,28 @@ QWidget *findWidgetWithProperties(const QString &properties, QWidget *parent)
8486
});
8587
}
8688

89+
bool checkEventTarget(
90+
QWidget *target, const QString &keys, const QString &widgetName, const char *event)
91+
{
92+
if (target && target->isVisible())
93+
return true;
94+
95+
qCCritical(plugin) << "Failed to send postponed" << event
96+
<< keys << "to" << widgetName << ";"
97+
<< (target ? "Target widget is no longer visible" : "Target widget no longer exists");
98+
return false;
99+
}
100+
87101
} // namespace
88102

89103
class KeyClicker final : public QObject {
90104
public:
105+
enum Status {
106+
Pending,
107+
Success,
108+
Failed
109+
};
110+
91111
KeyClicker(QObject *parent)
92112
: QObject(parent)
93113
{
@@ -106,10 +126,10 @@ class KeyClicker final : public QObject {
106126
if (retry > 0)
107127
sendKeyClicks(expectedWidgetName, keys, delay + 100, retry - 1);
108128
else
109-
keyClicksFailed(expectedWidgetName);
129+
keyClicksFailed();
110130
}
111131

112-
void keyClicksFailed(const QRegularExpression &expectedWidgetName)
132+
void keyClicksFailed()
113133
{
114134
qCCritical(plugin) << "Failed to send key press to target widget";
115135

@@ -124,21 +144,21 @@ class KeyClicker final : public QObject {
124144
qCCritical(plugin) << "App is INACTIVE! State:" << state;
125145

126146
qCCritical(plugin).noquote().nospace()
127-
<< "Expected: /" + expectedWidgetName.pattern() + "/"
128-
<< "\nActual: " + objectAddress(actual)
129-
<< "\nPopup: " + objectAddress(popup)
130-
<< "\nWidget: " + objectAddress(widget)
131-
<< "\nWindow: " + objectAddress(window)
132-
<< "\nModal: " + objectAddress(modal);
133-
134-
m_failed = true;
147+
<< "Expected: /" + m_expectedWidgetName.pattern() + "/"
148+
<< "\nActual: " + widgetAddress(actual)
149+
<< "\nPopup: " + widgetAddress(popup)
150+
<< "\nWidget: " + widgetAddress(widget)
151+
<< "\nWindow: " + widgetAddress(window)
152+
<< "\nModal: " + widgetAddress(modal);
153+
154+
m_status = Failed;
135155
}
136156

137-
void keyClicks(const QRegularExpression &expectedWidgetName, const QString &keys, int delay, int retry)
157+
void keyClicks(const QString &keys, int delay, int retry)
138158
{
139159
auto widget = keyClicksTarget();
140160
if (!widget) {
141-
keyClicksRetry(expectedWidgetName, keys, delay, retry);
161+
keyClicksRetry(m_expectedWidgetName, keys, delay, retry);
142162
return;
143163
}
144164

@@ -147,26 +167,27 @@ class KeyClicker final : public QObject {
147167
if (qApp->applicationState() != Qt::ApplicationActive && m_wnd->isVisible()) {
148168
qCDebug(plugin) << "Re-activating the main window (macOS)";
149169
m_wnd->activateWindow();
170+
m_wnd->raise();
150171
}
151172
#endif
152173

153174
if (qApp->applicationState() != Qt::ApplicationActive) {
154175
qCDebug(plugin) << "Waiting for application to become active";
155-
keyClicksRetry(expectedWidgetName, keys, delay, retry);
176+
keyClicksRetry(m_expectedWidgetName, keys, delay, retry);
156177
return;
157178
}
158179

159-
const QString widgetName = objectAddress(widget);
160-
if ( !expectedWidgetName.pattern().isEmpty()
161-
&& !expectedWidgetName.match(widgetName).hasMatch() )
180+
const QString widgetName = widgetAddress(widget);
181+
if ( !m_expectedWidgetName.pattern().isEmpty()
182+
&& !m_expectedWidgetName.match(widgetName).hasMatch() )
162183
{
163-
keyClicksRetry(expectedWidgetName, keys, delay, retry);
184+
keyClicksRetry(m_expectedWidgetName, keys, delay, retry);
164185
return;
165186
}
166187

167188
// Only verified focused widget.
168189
if ( keys.isEmpty() ) {
169-
m_succeeded = true;
190+
m_status = Success;
170191
return;
171192
}
172193

@@ -175,18 +196,14 @@ class KeyClicker final : public QObject {
175196
if ( qobject_cast<QCheckBox*>(widget) )
176197
QTest::qWait(100);
177198

178-
qCDebug(plugin) << "Sending event" << keys << "to" << widgetName;
199+
qCDebug(plugin) << "Sending" << keys << "to" << widgetName;
179200

180201
static const auto keyClicksPrefix = QLatin1String(":");
181202
static const auto mousePrefix = QLatin1String("mouse|");
182203
static const auto dragPrefix = QLatin1String("isDraggingFrom|");
183204
if ( keys.startsWith(keyClicksPrefix) ) {
184205
const auto text = keys.mid(keyClicksPrefix.size());
185-
186-
QTest::keyClicks(widget, text, Qt::NoModifier, 0);
187-
188-
// Increment key clicks sequence number after typing all the text.
189-
m_succeeded = true;
206+
QTest::keyClicks(widget, text, Qt::NoModifier);
190207
} else if ( keys.startsWith(mousePrefix) ) {
191208
const QString action = keys.section('|', 1, 1);
192209
const QString properties = keys.section('|', 2);
@@ -199,24 +216,18 @@ class KeyClicker final : public QObject {
199216
if ( !validActions.contains(action) ) {
200217
qCCritical(plugin) << "Failed to match mouse action:" << keys;
201218
qCCritical(plugin) << "Valid mouse actions are:" << validActions;
202-
m_failed = true;
219+
m_status = Failed;
203220
return;
204221
}
205222
QPointer<QWidget> source = findWidgetWithProperties(properties, m_wnd);
206223
if (!source) {
207-
m_failed = true;
224+
m_status = Failed;
208225
return;
209226
}
210227
// Don't block while processing the events.
211228
runAfterInterval(delay, [=](){
212-
if (!source) {
213-
qCCritical(plugin) << "Target widget was destroyed";
229+
if (!checkEventTarget(source, keys, widgetName, "mouse"))
214230
return;
215-
}
216-
if (!source->isVisible()) {
217-
qCCritical(plugin) << "Target widget is no longer visible";
218-
return;
219-
}
220231

221232
// Send the event to window instead - it works better.
222233
QWidget *windowWidget = source->window();
@@ -239,81 +250,69 @@ class KeyClicker final : public QObject {
239250
QTest::mouseMove(source, source->rect().bottomRight());
240251
}
241252
});
242-
m_succeeded = true;
243253
} else if ( keys.startsWith(dragPrefix) ) {
244254
const QObject *drag = m_wnd->findChild<QDrag*>();
245255
if (!drag) {
246256
qCCritical(plugin) << "QDrag not started";
247-
m_failed = true;
257+
m_status = Failed;
248258
return;
249259
}
250-
qCDebug(plugin) << "QDrag started with parent:" << objectAddress(drag->parent());
260+
qCDebug(plugin) << "QDrag started with parent:" << widgetAddress(qobject_cast<QWidget*>(drag->parent()));
251261
const QString properties = keys.section('|', 1);
252262
QWidget* source = findWidgetWithProperties(properties, m_wnd);
253263
if (!source) {
254-
m_failed = true;
264+
m_status = Failed;
255265
return;
256266
}
257267
if (drag->parent() != source) {
258268
qCCritical(plugin) << "Unexpected QDrag parent; Expected:" << properties
259-
<< "; Actual:" << objectAddress(drag->parent());
260-
m_failed = true;
269+
<< "; Actual:" << widgetAddress(qobject_cast<QWidget*>(drag->parent()));
270+
m_status = Failed;
261271
return;
262272
}
263-
m_succeeded = true;
264273
} else {
265274
const QKeySequence shortcut(keys, QKeySequence::PortableText);
266-
267275
if ( shortcut.isEmpty() ) {
268276
qCCritical(plugin) << "Failed to parse shortcut" << keys;
269-
m_failed = true;
277+
m_status = Failed;
270278
return;
271279
}
272280

273-
// Increment key clicks sequence number before opening any modal dialogs.
274-
m_succeeded = true;
275-
276281
const auto key = static_cast<uint>(shortcut[0]);
277-
const bool postpone = QApplication::activeModalWidget() != nullptr;
278-
if (postpone) {
279-
qCDebug(plugin) << "Postponing event due to a modal window";
280-
// WORKAROUND: Avoid sending release event to a destroyed widget.
281-
QPointer<QWidget> target(widget);
282-
QTest::keyPress(
283-
widget,
284-
Qt::Key(key & ~Qt::KeyboardModifierMask),
285-
Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask),
286-
1 );
287-
QTimer::singleShot(2, m_wnd, [=]() {
288-
if (!target)
289-
return;
290-
QTest::keyRelease(
291-
target,
292-
Qt::Key(key & ~Qt::KeyboardModifierMask),
293-
Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask) );
294-
});
295-
} else {
282+
const QPointer<QWidget> target = widget;
283+
// Avoid blocking on modal dialogs
284+
runAfterInterval(0, [=](){
285+
if (!target || !target->isVisible()) {
286+
qCCritical(plugin) << "Target no longer valid:" << widgetName;
287+
return;
288+
}
296289
QTest::keyClick(
297-
widget,
290+
target,
298291
Qt::Key(key & ~Qt::KeyboardModifierMask),
299-
Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask) );
300-
}
292+
Qt::KeyboardModifiers(key & Qt::KeyboardModifierMask));
293+
});
301294
}
302295

303-
qCDebug(plugin) << "Event" << keys << "sent to" << widgetName;
296+
m_status = Success;
297+
qCDebug(plugin) << "Sent" << keys << "to" << widgetName;
304298
}
305299

306300
void sendKeyClicks(const QRegularExpression &expectedWidgetName, const QString &keys, int delay, int retry)
307301
{
308-
m_succeeded = false;
309-
m_failed = false;
302+
m_status = Pending;
303+
m_expectedWidgetName = expectedWidgetName;
310304

311305
// Don't stop when modal window is open.
312-
runAfterInterval(delay, [=](){ keyClicks(expectedWidgetName, keys, delay, retry); });
306+
runAfterInterval(delay, [=](){ keyClicks(keys, delay, retry); });
313307
}
314308

315-
bool succeeded() const { return m_succeeded; }
316-
bool failed() const { return m_failed; }
309+
int status(bool forceRetrieve) {
310+
if (m_status == Pending && forceRetrieve) {
311+
keyClicksFailed();
312+
return Failed;
313+
}
314+
return m_status;
315+
}
317316

318317
private:
319318
template <typename Callable>
@@ -352,8 +351,8 @@ class KeyClicker final : public QObject {
352351
}
353352

354353
QWidget *m_wnd = nullptr;
355-
bool m_succeeded = true;
356-
bool m_failed = false;
354+
Status m_status = Success;
355+
QRegularExpression m_expectedWidgetName;
357356
};
358357

359358
void ItemTestsScriptable::keys()
@@ -375,7 +374,17 @@ void ItemTestsScriptable::keys()
375374
QString expectedWidgetName;
376375

377376
const auto focusPrefix = QLatin1String("focus:");
377+
bool interrupted = false;
378378
for (const auto &arg : currentArguments()) {
379+
if (interrupted) {
380+
const auto message =
381+
"Client was interrupted."
382+
" This is allowed to happen only when processing the last key event.";
383+
qCCritical(plugin) << message;
384+
throwError(message);
385+
return;
386+
}
387+
379388
const QString keys = arg.toString();
380389

381390
if (keys.startsWith(focusPrefix)) {
@@ -384,18 +393,30 @@ void ItemTestsScriptable::keys()
384393
} else {
385394
call("sleep", {wait});
386395
call("callPlugin", {"itemtests", "sendKeys", expectedWidgetName, keys, delay});
396+
QTest::qWait(qMax(5, delay));
387397
}
388398

389399
// Make sure all keys are send (shortcuts are postponed because they can be blocked by modal windows).
390-
for (;;) {
391-
if ( call("callPlugin", {"itemtests", "sendKeysSucceeded"}).toBool() ) {
400+
for (int i = 1; ; ++i) {
401+
const QVariant result = call("callPlugin", {"itemtests", "sendKeysStatus", i > 15});
402+
bool ok = false;
403+
const int status = result.toInt(&ok);
404+
if (!ok) {
405+
qCDebug(plugin) << "Client interrupted, got status:" << result;
406+
interrupted = true;
392407
break;
393408
}
394409

395-
if ( call("callPlugin", {"itemtests", "sendKeysFailed"}).toBool() ) {
410+
if (status == KeyClicker::Success)
411+
break;
412+
413+
if (status == KeyClicker::Failed) {
396414
throwError("Failed to send key presses");
397415
return;
398416
}
417+
418+
const int waitMs = 8 * i * i;
419+
QTest::qWait(qMin(1000, waitMs));
399420
}
400421
}
401422
}
@@ -413,7 +434,6 @@ QVariant ItemTestsLoader::scriptCallback(const QVariantList &arguments)
413434
const QString expectedWidgetName = arguments.value(1).toString();
414435
const QString keys = arguments.value(2).toString();
415436
const int delay = arguments.value(3).toInt();
416-
Q_ASSERT( keyClicker()->succeeded() || keyClicker()->failed() );
417437
const QRegularExpression re = QRegularExpression(
418438
QString(expectedWidgetName)
419439
.replace(QLatin1String("<"), QLatin1String(".*<.*"))
@@ -422,11 +442,8 @@ QVariant ItemTestsLoader::scriptCallback(const QVariantList &arguments)
422442
return {};
423443
}
424444

425-
if (cmd == "sendKeysSucceeded")
426-
return keyClicker()->succeeded();
427-
428-
if (cmd == "sendKeysFailed")
429-
return keyClicker()->failed();
445+
if (cmd == "sendKeysStatus")
446+
return keyClicker()->status(arguments.value(1).toBool());
430447

431448
return QStringLiteral("Unexpected command: %1").arg(cmd);
432449
}

0 commit comments

Comments
 (0)