[Ktutorial-commits] SF.net SVN: ktutorial:[356] trunk/ktutorial/ktutorial-editor
Status: Alpha
Brought to you by:
danxuliu
From: <dan...@us...> - 2012-08-04 15:55:07
|
Revision: 356 http://ktutorial.svn.sourceforge.net/ktutorial/?rev=356&view=rev Author: danxuliu Date: 2012-08-04 15:54:59 +0000 (Sat, 04 Aug 2012) Log Message: ----------- Fix bug where the RemoteObjectNameRegister/Widget "missed" the name of some remote objects. Before, the name of the remote objects was registered when the object was added to its parent in the target application. This could lead to the object being registered before its name was set, so it was registered with an empty name. Now, when the object is added to its parent in the target application it is queued for its name to be registered, and after a little delay (500ms), the register of the name takes place. During that delay the register should not be queried. If during the name update in the register something is done in the RemoteObjectNameWidget which has to query the register, the operation is also queued and executed once the name update has finished. Modified Paths: -------------- trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.cpp trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.h trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.cpp trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.h trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameRegisterTest.cpp trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameWidgetTest.cpp trunk/ktutorial/ktutorial-editor/tests/unit/view/WaitForPropertyWidgetTest.cpp Modified: trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.cpp =================================================================== --- trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.cpp 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.cpp 2012-08-04 15:54:59 UTC (rev 356) @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2011 by Daniel Calviño Sánchez * + * Copyright (C) 2011-2012 by Daniel Calviño Sánchez * * dan...@gm... * * * * This program is free software; you can redistribute it and/or modify * @@ -28,7 +28,8 @@ //public: RemoteObjectNameRegister::RemoteObjectNameRegister(QObject* parent /*= 0*/): - QObject(parent) { + QObject(parent), + mIsBeingUpdated(false) { if (TargetApplication::self()->remoteEditorSupport()) { registerRemoteObjects(); } @@ -107,6 +108,10 @@ return findRemoteObject(name, 0); } +bool RemoteObjectNameRegister::isBeingUpdated() const { + return mIsBeingUpdated; +} + //private: void RemoteObjectNameRegister::registerRemoteObject(RemoteObject* remoteObject, @@ -116,13 +121,10 @@ mRemoteObjectForParent.insert(parent, remoteObject); - QString name = remoteObject->name(); - if (!name.isEmpty()) { - emit nameAdded(name); + mRemoteObjectsPendingNameRegister.append( + QPointer<RemoteObject>(remoteObject)); + QTimer::singleShot(500, this, SLOT(deferredRegisterRemoteObjectName())); - mRemoteObjectForName.insert(name, remoteObject); - } - foreach (RemoteObject* child, remoteObject->children()) { registerRemoteObject(child, remoteObject); } @@ -278,9 +280,21 @@ return false; } +void RemoteObjectNameRegister::startNameUpdate() { + mIsBeingUpdated = true; + emit nameUpdateStarted(); +} + +void RemoteObjectNameRegister::finishNameUpdate() { + mIsBeingUpdated = false; + emit nameUpdateFinished(); +} + //private slots: void RemoteObjectNameRegister::registerRemoteObjects() { + startNameUpdate(); + try { registerRemoteObject(TargetApplication::self()-> remoteEditorSupport()->mainWindow(), 0); @@ -288,7 +302,11 @@ kWarning() << "The remote objects could not be registered to provide " << "name completion (" << e.message() << ")."; } - + + if (mRemoteObjectsPendingNameRegister.isEmpty()) { + finishNameUpdate(); + } + try { RemoteEventSpy* remoteEventSpy = TargetApplication::self()->remoteEditorSupport()->enableEventSpy(); @@ -311,6 +329,10 @@ return; } + if (eventType == "ChildAdded") { + startNameUpdate(); + } + try { QList<RemoteObject*> children = remoteObject->children(); @@ -337,4 +359,39 @@ << "name completion could not be updated (" << e.message() << ")."; } + + if (eventType == "ChildAdded" && + mRemoteObjectsPendingNameRegister.isEmpty()) { + finishNameUpdate(); + } } + +void RemoteObjectNameRegister::deferredRegisterRemoteObjectName() { + QPointer<RemoteObject> remoteObject = + mRemoteObjectsPendingNameRegister.takeFirst(); + if (!remoteObject) { + if (mRemoteObjectsPendingNameRegister.isEmpty()) { + finishNameUpdate(); + } + + return; + } + + QString name; + try { + name = remoteObject->name(); + } catch (DBusException e) { + kWarning() << "There was a problem getting the name of the remote" + << "object (" << e.message() << ")."; + } + + if (!name.isEmpty()) { + emit nameAdded(name); + + mRemoteObjectForName.insert(name, remoteObject); + } + + if (mRemoteObjectsPendingNameRegister.isEmpty()) { + finishNameUpdate(); + } +} Modified: trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.h =================================================================== --- trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.h 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameRegister.h 2012-08-04 15:54:59 UTC (rev 356) @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2011 by Daniel Calviño Sánchez * + * Copyright (C) 2011-2012 by Daniel Calviño Sánchez * * dan...@gm... * * * * This program is free software; you can redistribute it and/or modify * @@ -21,6 +21,7 @@ #include <QMultiHash> #include <QObject> +#include <QPointer> #include "../targetapplication/DBusException.h" @@ -51,6 +52,17 @@ * "File information dialog" had a also button called "Ok button", now the * unique name of the first button would be "Configuration dialog/Ok button". In * this situation, "Ok button" would not represent a specific remote object. + * + * Also note that names are not registered when the objects are created in the + * target application, but a slight delay is introduced instead. This prevents + * registering objects too soon, before their name was even set in the target + * application. While the update in the registered names is taking place the + * methods described above should not be used. When a name update is started, + * the signal nameUpdateStarted() is emitted; when the update is finished, the + * signal nameUpdateFinished() is emitted. It can be checked if the register is + * being updated or not using isBeingUpdated(). The "updating" state is only + * enabled and the signals emitted when new objects are created; if an object is + * deleted there is no delay in the update of the names. */ class RemoteObjectNameRegister: public QObject { Q_OBJECT @@ -58,6 +70,12 @@ /** * Creates a new RemoteObjectNameRegister. + * Note that if the target application is already started when the + * RemoteObjectNameRegister is created the RemoteObjects will be registered + * and the nameUpdateStarted() signal will be emitted, even before being + * able to connect to it. Thus, you may want to check whether the register + * is being updated or not after the constructor to call the slot that will + * be connected with nameUpdateStarted() signal. * * @param parent The parent QObject. */ @@ -125,6 +143,13 @@ */ RemoteObject* findRemoteObject(const QString& name) const; + /** + * Returns whether the names are being registered or not. + * + * @return True if the names are being registered, false otherwise. + */ + bool isBeingUpdated() const; + Q_SIGNALS: /** @@ -141,6 +166,21 @@ */ void nameRemoved(const QString& name); + /** + * Emitted when one or more remote objects are created in the target + * application, and thus queued for their names to be registered. + * Note that this signal can be emitted several times before + * nameUpdateFinished() signal is emitted. + */ + void nameUpdateStarted(); + + /** + * Emitted when there are no more remote objects pending to be registered. + * Note that this signal may be emitted just once after several + * nameUpdateStarted() signals are emitted. + */ + void nameUpdateFinished(); + private: /** @@ -154,8 +194,21 @@ QMultiHash<RemoteObject*, RemoteObject*> mRemoteObjectForParent; /** + * Whether the names are being registered or not. + */ + bool mIsBeingUpdated; + + /** + * The list of RemoteObjects queued to register their name. + * A guarded pointer is needed, as the RemoteObjects may be deleted if the + * target application is closed before finishing the update of the names. + */ + QList< QPointer<RemoteObject> > mRemoteObjectsPendingNameRegister; + + /** * Registers the given RemoteObject and all its children. - * The objects are only registered if they have a name. + * The name itself is not registered yet, but queued to be registered after + * a little time. * * @param remoteObject The RemoteObject to register. * @param parent The parent of the RemoteObject to register. @@ -289,10 +342,24 @@ bool isDescendantOf(RemoteObject* remoteObject, RemoteObject* ancestor) const; + /** + * Sets this RemoteObjectNameRegister as being updated and emits the + * nameUpdateStarted() signal. + */ + void startNameUpdate(); + + /** + * Sets this RemoteObjectNameRegister as not being updated and emits the + * nameUpdateFinished() signal. + */ + void finishNameUpdate(); + private Q_SLOTS: /** * Registers all the remote objects. + * The name update is started (and finished if no names are queued to be + * registered). */ void registerRemoteObjects(); @@ -304,6 +371,8 @@ /** * Updates the registered remote objects if they received a ChildAdded or * ChildRemoved event. + * If the ChildAdded event is received, the name update is started (and + * finished if no names are queued to be registered). * * @param remoteObject The RemoteObject that received the event. * @param eventType The type of the event received. @@ -311,6 +380,14 @@ void updateRemoteObjects(RemoteObject* remoteObject, const QString& eventType); + /** + * Registers, if it is still available, the name of the remote object + * pending since the longest time ago. + * If there are no more names pending to be registered, the name update is + * finished. + */ + void deferredRegisterRemoteObjectName(); + }; #endif Modified: trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.cpp =================================================================== --- trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.cpp 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.cpp 2012-08-04 15:54:59 UTC (rev 356) @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2010-2011 by Daniel Calviño Sánchez * + * Copyright (C) 2010-2012 by Daniel Calviño Sánchez * * dan...@gm... * * * * This program is free software; you can redistribute it and/or modify * @@ -24,6 +24,7 @@ #include "RemoteObjectChooser.h" #include "RemoteObjectNameRegister.h" +#include "../targetapplication/RemoteObject.h" #include "../targetapplication/TargetApplication.h" /** @@ -104,10 +105,19 @@ //public: RemoteObjectNameWidget::RemoteObjectNameWidget(QWidget* parent): - QWidget(parent) { + QWidget(parent), + mIsRemoteObjectChosenPending(false), + mIsNameChangedPending(false), + mIsCompletionPending(false), + mIsSubstringCompletionPending(false) { mRemoteObjectNameRegister = new RemoteObjectNameRegister(this); + connect(mRemoteObjectNameRegister, SIGNAL(nameUpdateStarted()), + this, SLOT(startNameUpdate())); + connect(mRemoteObjectNameRegister, SIGNAL(nameUpdateFinished()), + this, SLOT(finishNameUpdate())); + ui = new Ui::RemoteObjectNameWidget(); ui->setupUi(this); @@ -120,6 +130,9 @@ connect(ui->objectNamePushButton, SIGNAL(clicked(bool)), this, SLOT(showRemoteObjectChooser())); + //It will probably be IBeamCursor, but just in case + mObjectNameLineEditCursorShape = ui->objectNameLineEdit->cursor().shape(); + KCompletion* completion = new RemoteObjectNameCompletion(mRemoteObjectNameRegister); completion->setOrder(KCompletion::Sorted); @@ -127,9 +140,10 @@ ui->objectNameLineEdit->setCompletionObject(completion); ui->objectNameLineEdit->setAutoDeleteCompletionObject(true); - foreach (const QString& name, mRemoteObjectNameRegister->names()) { - completion->addItem(name); - } + connect(ui->objectNameLineEdit, SIGNAL(completion(QString)), + this, SLOT(handleCompletion())); + connect(ui->objectNameLineEdit, SIGNAL(substringCompletion(QString)), + this, SLOT(handleSubstringCompletion())); connect(mRemoteObjectNameRegister, SIGNAL(nameAdded(QString)), completion, SLOT(addItem(QString))); @@ -137,6 +151,13 @@ completion, SLOT(removeItem(QString))); connect(TargetApplication::self(), SIGNAL(finished()), completion, SLOT(clear())); + + //If the target application already exists when the register is + //created the nameUpdateStarted is emitted before being able to connect to + //it. + if (mRemoteObjectNameRegister->isBeingUpdated()) { + startNameUpdate(); + } } RemoteObjectNameWidget::~RemoteObjectNameWidget() { @@ -162,6 +183,16 @@ } void RemoteObjectNameWidget::setChosenRemoteObject(RemoteObject* remoteObject) { + if (mRemoteObjectNameRegister->isBeingUpdated()) { + mIsRemoteObjectChosenPending = true; + mPendingRemoteObjectChosen = QPointer<RemoteObject>(remoteObject); + return; + } + + if (!remoteObject) { + return; + } + try { ui->objectNameLineEdit->setText( mRemoteObjectNameRegister->uniqueName(remoteObject)); @@ -175,5 +206,62 @@ } void RemoteObjectNameWidget::handleNameChanged(const QString& name) { + if (mRemoteObjectNameRegister->isBeingUpdated()) { + mIsNameChangedPending = true; + return; + } + emit remoteObjectChosen(mRemoteObjectNameRegister->findRemoteObject(name)); } + +void RemoteObjectNameWidget::handleCompletion() { + if (!mRemoteObjectNameRegister->isBeingUpdated()) { + return; + } + + mIsCompletionPending = true; +} + +void RemoteObjectNameWidget::handleSubstringCompletion() { + if (!mRemoteObjectNameRegister->isBeingUpdated()) { + return; + } + + mIsSubstringCompletionPending = true; +} + +//This method may be called several times before finishNameUpdate() is called, +//so it should not be assumed that the widget will be in a "not updating" state +void RemoteObjectNameWidget::startNameUpdate() { + ui->objectNameLineEdit->setCursor(Qt::BusyCursor); + + ui->objectNameLineEdit->setHandleSignals(false); +} + +void RemoteObjectNameWidget::finishNameUpdate() { + if (mIsRemoteObjectChosenPending) { + mIsRemoteObjectChosenPending = false; + setChosenRemoteObject(mPendingRemoteObjectChosen); + } + + if (mIsNameChangedPending) { + mIsNameChangedPending = false; + handleNameChanged(ui->objectNameLineEdit->text()); + } + + ui->objectNameLineEdit->setHandleSignals(true); + + if (mIsSubstringCompletionPending) { + mIsSubstringCompletionPending = false; + ui->objectNameLineEdit->setCompletedItems( + ui->objectNameLineEdit->completionObject()->substringCompletion( + ui->objectNameLineEdit->text())); + } + + if (mIsCompletionPending) { + mIsCompletionPending = false; + ui->objectNameLineEdit->doCompletion(ui->objectNameLineEdit->text()); + } + + ui->objectNameLineEdit->setCursor(mObjectNameLineEditCursorShape); +} Modified: trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.h =================================================================== --- trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.h 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/src/view/RemoteObjectNameWidget.h 2012-08-04 15:54:59 UTC (rev 356) @@ -1,5 +1,5 @@ /*************************************************************************** - * Copyright (C) 2010-2011 by Daniel Calviño Sánchez * + * Copyright (C) 2010-2012 by Daniel Calviño Sánchez * * dan...@gm... * * * * This program is free software; you can redistribute it and/or modify * @@ -19,6 +19,7 @@ #ifndef REMOTEOBJECTNAMEWIDGET_H #define REMOTEOBJECTNAMEWIDGET_H +#include <QPointer> #include <QWidget> class RemoteObject; @@ -42,6 +43,16 @@ * * When a name is set in the line edit the signal * remoteObjectChosen(RemoteObject*) is emitted. + * + * To provide name completion, name substring completion, setting the name based + * on the RemoteObject chosen or emitting the signal + * remoteObjectChosen(RemoteObject*) based on the current name, the widget uses + * a register with the names of the remote objects. During the updates of this + * register it should not be queried (as the information provided will not be + * accurate), so if any of the aforementioned operations is requested during a + * register update, the operation is queued and executed once the update + * finishes.The cursor shape is changed to busy during register updates to + * reflect this fact. */ class RemoteObjectNameWidget: public QWidget { Q_OBJECT @@ -99,6 +110,43 @@ */ Ui::RemoteObjectNameWidget* ui; + /** + * Whether the remote object was chosen while the register was being updated + * or not. + */ + bool mIsRemoteObjectChosenPending; + + /** + * The last remote object chosen while the register was being updated, if + * any. + * A guarded pointer is needed, as the RemoteObject may be deleted if the + * target application is closed before finishing the update of the names. + */ + QPointer<RemoteObject> mPendingRemoteObjectChosen; + + /** + * Whether the name was set (programatically or by the user) while the + * register was being updated or not. + */ + bool mIsNameChangedPending; + + /** + * Whether the name completion was triggered while the register was being + * updated or not. + */ + bool mIsCompletionPending; + + /** + * Whether the name substring completion was triggered while the register + * was being updated or not. + */ + bool mIsSubstringCompletionPending; + + /** + * The cursor shape used by default by the object name line edit. + */ + Qt::CursorShape mObjectNameLineEditCursorShape; + private Q_SLOTS: /** @@ -108,6 +156,9 @@ /** * Sets the object name based in the chosen remote object. + * If the name register is being updated, nothing will be done, but marking + * setting the chosen remote object as a pending operation. + * If the given remote object is null no name is set. * * @param remoteObject The chosen RemoteObject. */ @@ -116,11 +167,38 @@ /** * Emits remoteObjectChosen(RemoteObject*) with the remote object with the * given name. + * If the name register is being updated, nothing will be done, but marking + * handling the name change as a pending operation. * * @param name The name set. */ void handleNameChanged(const QString& name); + /** + * If the name register is being updated, marks the name completion as a + * pending operation. + */ + void handleCompletion(); + + /** + * If the name register is being updated, marks the name substring + * completion as a pending operation. + */ + void handleSubstringCompletion(); + + /** + * Enters the name register update state. + * Changes the cursor to busy and disables the completion. + */ + void startNameUpdate(); + + /** + * Exists the name register update state. + * Executes all the pending operations, enables again the completion and + * restores the cursor. + */ + void finishNameUpdate(); + }; #endif Modified: trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameRegisterTest.cpp =================================================================== --- trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameRegisterTest.cpp 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameRegisterTest.cpp 2012-08-04 15:54:59 UTC (rev 356) @@ -42,6 +42,7 @@ void testTargetApplicationStartedAfterRegister(); void testTargetApplicationStopped(); + void testTargetApplicationStoppedBeforeFinishingNameUpdate(); void testAddingOrRemovingRemoteObjects(); @@ -68,6 +69,10 @@ bool waitForTargetApplicationToStart(int timeout) const; bool waitForTargetApplicationToStop(int timeout) const; + bool waitForNamesToBeRegistered( + const RemoteObjectNameRegister& remoteObjectNameRegister, + int timeout) const; + void assertNames(const QStringList& names) const; }; @@ -95,6 +100,9 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(remoteObjectNameRegister.isBeingUpdated()); + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + assertNames(remoteObjectNameRegister.names()); } @@ -104,12 +112,26 @@ SIGNAL(nameAdded(QString))); QSignalSpy nameRemovedSpy(&remoteObjectNameRegister, SIGNAL(nameRemoved(QString))); + QSignalSpy nameUpdateStartedSpy(&remoteObjectNameRegister, + SIGNAL(nameUpdateStarted())); + QSignalSpy nameUpdateFinishedSpy(&remoteObjectNameRegister, + SIGNAL(nameUpdateFinished())); TargetApplication::self()->setTargetApplicationFilePath(mPath); TargetApplication::self()->start(); QVERIFY(waitForTargetApplicationToStart(10000)); + QVERIFY(remoteObjectNameRegister.isBeingUpdated()); + + QCOMPARE(nameUpdateStartedSpy.count(), 1); + QCOMPARE(nameUpdateFinishedSpy.count(), 0); + + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + + QCOMPARE(nameUpdateStartedSpy.count(), 1); + QCOMPARE(nameUpdateFinishedSpy.count(), 1); + assertNames(remoteObjectNameRegister.names()); QCOMPARE(nameAddedSpy.count(), 82); QCOMPARE(nameAddedSpy.at(0).at(0).toString(), @@ -139,6 +161,8 @@ QCOMPARE(nameAddedSpy.at(71).at(0).toString(), QString("The object name 803")); QCOMPARE(nameRemovedSpy.count(), 0); + QCOMPARE(nameUpdateStartedSpy.count(), 1); + QCOMPARE(nameUpdateFinishedSpy.count(), 1); } void RemoteObjectNameRegisterTest::testTargetApplicationStopped() { @@ -149,10 +173,16 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + QSignalSpy nameAddedSpy(&remoteObjectNameRegister, SIGNAL(nameAdded(QString))); QSignalSpy nameRemovedSpy(&remoteObjectNameRegister, SIGNAL(nameAdded(QString))); + QSignalSpy nameUpdateStartedSpy(&remoteObjectNameRegister, + SIGNAL(nameUpdateStarted())); + QSignalSpy nameUpdateFinishedSpy(&remoteObjectNameRegister, + SIGNAL(nameUpdateFinished())); TargetApplication::self()->mProcess->kill(); @@ -161,8 +191,42 @@ QVERIFY(remoteObjectNameRegister.names().isEmpty()); QCOMPARE(nameAddedSpy.count(), 0); QCOMPARE(nameRemovedSpy.count(), 0); + QCOMPARE(nameUpdateStartedSpy.count(), 0); + QCOMPARE(nameUpdateFinishedSpy.count(), 0); } +void RemoteObjectNameRegisterTest:: + testTargetApplicationStoppedBeforeFinishingNameUpdate() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameRegister remoteObjectNameRegister; + + QSignalSpy nameUpdateFinishedSpy(&remoteObjectNameRegister, + SIGNAL(nameUpdateFinished())); + + TargetApplication::self()->mProcess->kill(); + + //Process deleteLater events for the RemoteObjects + QApplication::processEvents(QEventLoop::DeferredDeletion); + + //Ensure that there are still names pending to be updated + QVERIFY(remoteObjectNameRegister.isBeingUpdated()); + + QVERIFY(waitForTargetApplicationToStop(10000)); + + //If the pending RemoteObjects are not stored in the + //RemoteObjectNameRegister using a guarded pointer, getting the names to + //register them after the target application was killed would lead to a + //crash + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + + QVERIFY(remoteObjectNameRegister.names().isEmpty()); + QCOMPARE(nameUpdateFinishedSpy.count(), 1); +} + void RemoteObjectNameRegisterTest::testAddingOrRemovingRemoteObjects() { QSKIP("Unfortunately, testing if the signals are emitted when an object is " "added or removed in the target application is too burdensome so the " @@ -177,6 +241,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); @@ -192,6 +258,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); @@ -208,6 +276,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); @@ -225,6 +295,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); @@ -241,6 +313,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + QCOMPARE(remoteObjectNameRegister.findRemoteObject("The object name 108"), (RemoteObject*)0); } @@ -253,6 +327,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -270,6 +346,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -288,6 +366,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -306,6 +386,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -324,6 +406,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -342,6 +426,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = mainWindow->children()[7]->children()[0]; @@ -357,6 +443,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + QStringList uniqueNames = remoteObjectNameRegister.uniqueNames("Duplicated object"); @@ -376,6 +464,8 @@ RemoteObjectNameRegister remoteObjectNameRegister; + QVERIFY(waitForNamesToBeRegistered(remoteObjectNameRegister, 10000)); + QStringList uniqueNames = remoteObjectNameRegister.uniqueNames("The object name 423"); @@ -417,6 +507,39 @@ return waitFor(&isTargetApplicationStopped, timeout); } +template <typename Class> +bool waitFor(Class &object, bool (Class::*condition)() const, int timeout) { + QElapsedTimer timer; + timer.start(); + do { + QTest::qWait(100); + } while (!(object.*condition)() && timer.elapsed() < timeout); + + if (timer.elapsed() >= timeout) { + return false; + } + + return true; +} + +class RegisterNotBeingUpdatedCondition { +public: + const RemoteObjectNameRegister* mRemoteObjectNameRegister; + + bool condition() const { + return !mRemoteObjectNameRegister->isBeingUpdated(); + } +}; + +bool RemoteObjectNameRegisterTest::waitForNamesToBeRegistered( + const RemoteObjectNameRegister& remoteObjectNameRegister, + int timeout) const { + RegisterNotBeingUpdatedCondition helper; + helper.mRemoteObjectNameRegister = &remoteObjectNameRegister; + return waitFor(helper, &RegisterNotBeingUpdatedCondition::condition, + timeout); +} + void RemoteObjectNameRegisterTest::assertNames(const QStringList& names) const { QCOMPARE(names.count(), 82); QVERIFY(names.contains("The object name 42")); Modified: trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameWidgetTest.cpp =================================================================== --- trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameWidgetTest.cpp 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/tests/unit/view/RemoteObjectNameWidgetTest.cpp 2012-08-04 15:54:59 UTC (rev 356) @@ -32,6 +32,7 @@ #include <KLineEdit> #include <KProcess> +#include "RemoteObjectNameRegister.h" #include "../targetapplication/RemoteClassStubs.h" #include "../targetapplication/RemoteEditorSupport.h" #include "../targetapplication/RemoteObject.h" @@ -55,12 +56,17 @@ void testSetName(); void testSetNameWithPath(); void testSetNameWithUnknownRemoteObjectName(); + void testSetNameDuringNameRegisterUpdate(); void testSetChosenRemoteObject(); void testSetChosenRemoteObjectWithNameNotUnique(); + void testSetChosenRemoteObjectDuringNameRegisterUpdate(); + void testSetChosenRemoteObjectDuringNameRegisterUpdateKillingApplication(); void testNameCompletion(); void testDuplicatedNameCompletion(); + void testNameCompletionDuringNameRegisterUpdate(); + void testNameSubcompletionDuringNameRegisterUpdate(); void testTargetApplicationStartedAfterWidget(); void testTargetApplicationStopped(); @@ -84,6 +90,9 @@ bool waitForTargetApplicationToStart(int timeout) const; bool waitForTargetApplicationToStop(int timeout) const; + + bool waitForNamesToBeRegistered(const RemoteObjectNameWidget* widget, + int timeout) const; void assertRemoteObjectSignal(const QSignalSpy& spy, int index, const RemoteObject* remoteObject) const; @@ -121,6 +130,12 @@ QWidget parent; RemoteObjectNameWidget* widget = new RemoteObjectNameWidget(&parent); + QCOMPARE(objectNameLineEdit(widget)->cursor().shape(), Qt::BusyCursor); + + QVERIFY(waitForNamesToBeRegistered(widget, 10000)); + + QCOMPARE(objectNameLineEdit(widget)->cursor().shape(), Qt::IBeamCursor); + QCOMPARE(widget->parentWidget(), &parent); QCOMPARE(widget->name(), QString("")); QStringList items = objectNameLineEdit(widget)->completionObject()->items(); @@ -137,6 +152,8 @@ QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + widget.setName("The object name 423"); RemoteObject* mainWindow = @@ -158,6 +175,8 @@ QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + widget.setName("The object name 7/Duplicated object"); RemoteObject* mainWindow = @@ -179,6 +198,8 @@ QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + widget.setName("The object name 108"); QCOMPARE(widget.name(), QString("The object name 108")); @@ -186,6 +207,36 @@ assertRemoteObjectSignal(remoteObjectChosenSpy, 0, 0); } +void RemoteObjectNameWidgetTest::testSetNameDuringNameRegisterUpdate() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameWidget widget; + QSignalSpy remoteObjectChosenSpy(&widget, + SIGNAL(remoteObjectChosen(RemoteObject*))); + + widget.setName("The object name 423"); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + //Check that the name is set, but the signal was not emitted yet + QCOMPARE(widget.name(), QString("The object name 423")); + QCOMPARE(remoteObjectChosenSpy.count(), 0); + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + RemoteObject* mainWindow = + TargetApplication::self()->remoteEditorSupport()->mainWindow(); + + QCOMPARE(widget.name(), QString("The object name 423")); + QCOMPARE(remoteObjectChosenSpy.count(), 1); + assertRemoteObjectSignal(remoteObjectChosenSpy, 0, + mainWindow->children()[3]); +} + void RemoteObjectNameWidgetTest::testSetChosenRemoteObject() { TargetApplication::self()->setTargetApplicationFilePath(mPath); TargetApplication::self()->start(); @@ -196,6 +247,8 @@ QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); RemoteObject* remoteObject = @@ -218,6 +271,8 @@ QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + RemoteObject* mainWindow = TargetApplication::self()->remoteEditorSupport()->mainWindow(); @@ -245,6 +300,85 @@ assertRemoteObjectSignal(remoteObjectChosenSpy, 2, remoteObject); } +void RemoteObjectNameWidgetTest:: + testSetChosenRemoteObjectDuringNameRegisterUpdate() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameWidget widget; + QSignalSpy remoteObjectChosenSpy(&widget, + SIGNAL(remoteObjectChosen(RemoteObject*))); + + RemoteObject* mainWindow = + TargetApplication::self()->remoteEditorSupport()->mainWindow(); + RemoteObject* remoteObject = + mainWindow->children()[4]->children()[0]->children()[1]; + + widget.setChosenRemoteObject(remoteObject); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + //Check that the name is not set and the signal was not emitted yet + QCOMPARE(widget.name(), QString("")); + QCOMPARE(remoteObjectChosenSpy.count(), 0); + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + QCOMPARE(widget.name(), QString("The object name 501")); + QCOMPARE(remoteObjectChosenSpy.count(), 1); + assertRemoteObjectSignal(remoteObjectChosenSpy, 0, remoteObject); +} + +void RemoteObjectNameWidgetTest:: + testSetChosenRemoteObjectDuringNameRegisterUpdateKillingApplication() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameWidget widget; + widget.setName("An object name"); + + QSignalSpy remoteObjectChosenSpy(&widget, + SIGNAL(remoteObjectChosen(RemoteObject*))); + + RemoteObject* mainWindow = + TargetApplication::self()->remoteEditorSupport()->mainWindow(); + RemoteObject* remoteObject = + mainWindow->children()[4]->children()[0]->children()[1]; + + widget.setChosenRemoteObject(remoteObject); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + TargetApplication::self()->mProcess->kill(); + + QVERIFY(waitForTargetApplicationToStop(10000)); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + //Check that the name is not set and the signal was not emitted yet + QCOMPARE(widget.name(), QString("An object name")); + QCOMPARE(remoteObjectChosenSpy.count(), 0); + + //If the pending RemoteObject is not stored using a guarded pointer, getting + //its unique name after the target application was killed would lead to a + //crash + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + //If there is no RemoteObject to be set, nothing is done. However, the + //remoteObjectChosen signal was queued to be emitted when the name was first + //set (as the names were already being uptated), although it should not be + //emitted again when setting the null RemoteObject. + QCOMPARE(widget.name(), QString("An object name")); + QCOMPARE(remoteObjectChosenSpy.count(), 1); +} + void RemoteObjectNameWidgetTest::testNameCompletion() { TargetApplication::self()->setTargetApplicationFilePath(mPath); TargetApplication::self()->start(); @@ -253,6 +387,8 @@ RemoteObjectNameWidget widget; + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + KCompletion* completion = objectNameLineEdit(&widget)->completionObject(); QCOMPARE(completion->order(), KCompletion::Sorted); @@ -280,6 +416,9 @@ QVERIFY(waitForTargetApplicationToStart(10000)); RemoteObjectNameWidget widget; + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + KLineEdit* lineEdit = objectNameLineEdit(&widget); lineEdit->setText("Duplicated "); @@ -288,6 +427,8 @@ KCompletionBox* completionBox = lineEdit->completionBox(); QStringList completionItems = completionBox->items(); + QVERIFY(completionBox->isVisible()); + QCOMPARE(completionItems.count(), 4); QCOMPARE(completionItems[0], QString("Duplicated grandparent/Duplicated parent/" @@ -300,11 +441,100 @@ QString("The object name 8/Duplicated object")); } +void RemoteObjectNameWidgetTest::testNameCompletionDuringNameRegisterUpdate() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameWidget widget; + + KLineEdit* lineEdit = objectNameLineEdit(&widget); + lineEdit->setText("Duplicated "); + + QTest::keyClick(lineEdit, Qt::Key_O, Qt::NoModifier); + + KCompletionBox* completionBox = lineEdit->completionBox(); + QStringList completionItems = completionBox->items(); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + QVERIFY(!completionBox->isVisible()); + QCOMPARE(completionItems.count(), 0); + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + completionBox = lineEdit->completionBox(); + completionItems = completionBox->items(); + + QVERIFY(completionBox->isVisible()); + + QCOMPARE(completionItems.count(), 4); + QCOMPARE(completionItems[0], + QString("Duplicated grandparent/Duplicated parent/" + "Duplicated object")); + QCOMPARE(completionItems[1], + QString("The object name 50/Duplicated object")); + QCOMPARE(completionItems[2], + QString("The object name 7/Duplicated object")); + QCOMPARE(completionItems[3], + QString("The object name 8/Duplicated object")); +} + +void RemoteObjectNameWidgetTest:: + testNameSubcompletionDuringNameRegisterUpdate() { + TargetApplication::self()->setTargetApplicationFilePath(mPath); + TargetApplication::self()->start(); + + QVERIFY(waitForTargetApplicationToStart(10000)); + + RemoteObjectNameWidget widget; + + KLineEdit* lineEdit = objectNameLineEdit(&widget); + + //Using the configured KShortcut with QTest::keyClick is too cumbersome, + //so the binding is overriden for this test + lineEdit->setKeyBinding(KCompletionBase::SubstringCompletion, + KShortcut("Ctrl+T")); + QTest::keyClick(lineEdit, Qt::Key_T, Qt::ControlModifier); + + KCompletionBox* completionBox = lineEdit->completionBox(); + QStringList completionItems = completionBox->items(); + + //Ensure that the register is still being updated + QVERIFY(widget.mRemoteObjectNameRegister->isBeingUpdated()); + + QVERIFY(!completionBox->isVisible()); + QCOMPARE(completionItems.count(), 0); + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + completionBox = lineEdit->completionBox(); + completionItems = completionBox->items(); + + QVERIFY(completionBox->isVisible()); + + QCOMPARE(completionItems.count(), 82); + QVERIFY(completionItems.contains("The object name 42")); + QVERIFY(completionItems.contains("The object name 420")); + QVERIFY(completionItems.contains("The object name 421")); + QVERIFY(completionItems.contains("The object name 422")); + QVERIFY(completionItems.contains("The object name 423")); + QVERIFY(completionItems.contains("Duplicated grandparent/Duplicated parent/" + "Duplicated object")); + QVERIFY(completionItems.contains("The object name 50/Duplicated object")); + QVERIFY(completionItems.contains("The object name 7/Duplicated object")); + QVERIFY(completionItems.contains("The object name 8/Duplicated object")); +} + void RemoteObjectNameWidgetTest::testTargetApplicationStartedAfterWidget() { RemoteObjectNameWidget widget; QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); + QCOMPARE(objectNameLineEdit(&widget)->cursor().shape(), Qt::IBeamCursor); + QStringList items = objectNameLineEdit(&widget)->completionObject()-> items(); QCOMPARE(items.count(), 0); @@ -320,6 +550,12 @@ QVERIFY(waitForTargetApplicationToStart(10000)); + QCOMPARE(objectNameLineEdit(&widget)->cursor().shape(), Qt::BusyCursor); + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + + QCOMPARE(objectNameLineEdit(&widget)->cursor().shape(), Qt::IBeamCursor); + items = objectNameLineEdit(&widget)->completionObject()->items(); assertCompletionItems(items); @@ -341,6 +577,9 @@ QVERIFY(waitForTargetApplicationToStart(10000)); RemoteObjectNameWidget widget; + + QVERIFY(waitForNamesToBeRegistered(&widget, 10000)); + QSignalSpy remoteObjectChosenSpy(&widget, SIGNAL(remoteObjectChosen(RemoteObject*))); @@ -452,6 +691,38 @@ return waitFor(&isTargetApplicationStopped, timeout); } +template <typename Class> +bool waitFor(Class &object, bool (Class::*condition)() const, int timeout) { + QElapsedTimer timer; + timer.start(); + do { + QTest::qWait(100); + } while (!(object.*condition)() && timer.elapsed() < timeout); + + if (timer.elapsed() >= timeout) { + return false; + } + + return true; +} + +class RegisterNotBeingUpdatedCondition { +public: + const RemoteObjectNameRegister* mRemoteObjectNameRegister; + + bool condition() const { + return !mRemoteObjectNameRegister->isBeingUpdated(); + } +}; + +bool RemoteObjectNameWidgetTest::waitForNamesToBeRegistered( + const RemoteObjectNameWidget* widget, int timeout) const { + RegisterNotBeingUpdatedCondition helper; + helper.mRemoteObjectNameRegister = widget->mRemoteObjectNameRegister; + return waitFor(helper, &RegisterNotBeingUpdatedCondition::condition, + timeout); +} + //RemoteObject* must be declared as a metatype to be used in qvariant_cast Q_DECLARE_METATYPE(RemoteObject*); Modified: trunk/ktutorial/ktutorial-editor/tests/unit/view/WaitForPropertyWidgetTest.cpp =================================================================== --- trunk/ktutorial/ktutorial-editor/tests/unit/view/WaitForPropertyWidgetTest.cpp 2012-07-11 14:29:47 UTC (rev 355) +++ trunk/ktutorial/ktutorial-editor/tests/unit/view/WaitForPropertyWidgetTest.cpp 2012-08-04 15:54:59 UTC (rev 356) @@ -18,13 +18,18 @@ #include <QtTest> +#define protected public +#define private public #include "WaitForPropertyWidget.h" +#undef private +#undef protected #include <KLineEdit> #include "../data/WaitForProperty.h" #ifdef QT_QTDBUS_FOUND +#include "RemoteObjectNameWidget.h" #define protected public #define private public #include "../targetapplication/TargetApplication.h" @@ -56,6 +61,9 @@ bool waitForTargetApplicationToStart(int timeout) const; + bool waitForSignalCount(const QSignalSpy* spy, int count, + int timeout) const; + }; void WaitForPropertyWidgetTest::init() { @@ -112,6 +120,11 @@ QCOMPARE(waitFor.value(), QString("The new value")); } +#ifdef QT_QTDBUS_FOUND +//RemoteObject* must be declared as a metatype to be used in qvariant_cast +Q_DECLARE_METATYPE(RemoteObject*); +#endif + void WaitForPropertyWidgetTest::testPropertyNameCompletion() { #ifdef QT_QTDBUS_FOUND TargetApplication::self()->setTargetApplicationFilePath( @@ -123,8 +136,20 @@ WaitForProperty waitFor; WaitForPropertyWidget widget(&waitFor); + //RemoteObject* must be registered in order to be used with QSignalSpy + qRegisterMetaType<RemoteObject*>("RemoteObject*"); + QSignalSpy remoteObjectChosenSpy(widget.mRemoteObjectNameWidget, + SIGNAL(remoteObjectChosen(RemoteObject*))); + objectNameLineEdit(&widget)->setText("The object name 830"); + //Wait until the RemoteObjectNameWidget emits the remoteObjectChosen signal + //(as it can take some time until the names of the remote objects are + //registered), and ensure that the chosen remote object is not null + QVERIFY(waitForSignalCount(&remoteObjectChosenSpy, 1, 10000)); + QVariant argument = remoteObjectChosenSpy.at(0).at(0); + QVERIFY(qvariant_cast<RemoteObject*>(argument)); + KCompletion* completion = propertyNameLineEdit(&widget)->completionObject(); QStringList items = completion->items(); QCOMPARE(items.count(), 6); @@ -180,6 +205,40 @@ return waitFor(&isTargetApplicationStarted, timeout); } +template <typename Class> +bool waitFor(Class &object, bool (Class::*condition)() const, int timeout) { + QElapsedTimer timer; + timer.start(); + do { + QTest::qWait(100); + } while (!(object.*condition)() && timer.elapsed() < timeout); + + if (timer.elapsed() >= timeout) { + return false; + } + + return true; +} + +class SignalCountCondition { +public: + const QSignalSpy* mSpy; + int mCount; + + bool condition() const { + return mSpy->count() == mCount; + } +}; + +bool WaitForPropertyWidgetTest::waitForSignalCount(const QSignalSpy* spy, + int count, + int timeout) const { + SignalCountCondition helper; + helper.mSpy = spy; + helper.mCount = count; + return waitFor(helper, &SignalCountCondition::condition, timeout); +} + QTEST_MAIN(WaitForPropertyWidgetTest) #include "WaitForPropertyWidgetTest.moc" This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |