/**************************************************************************** ** ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** 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, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include "../network-settings.h" class tst_QHttpSocketEngine : public QObject { Q_OBJECT public: tst_QHttpSocketEngine(); virtual ~tst_QHttpSocketEngine(); public slots: void init(); void cleanup(); private slots: void construction(); void errorTest_data(); void errorTest(); void simpleConnectToIMAP(); void simpleErrorsAndStates(); void tcpSocketBlockingTest(); void tcpSocketNonBlockingTest(); void downloadBigFile(); // void tcpLoopbackPerformance(); void passwordAuth(); protected slots: void tcpSocketNonBlocking_hostFound(); void tcpSocketNonBlocking_connected(); void tcpSocketNonBlocking_closed(); void tcpSocketNonBlocking_readyRead(); void tcpSocketNonBlocking_bytesWritten(qint64); void exitLoopSlot(); void downloadBigFileSlot(); private: QTcpSocket *tcpSocketNonBlocking_socket; QStringList tcpSocketNonBlocking_data; qint64 tcpSocketNonBlocking_totalWritten; QTcpSocket *tmpSocket; qint64 bytesAvailable; }; class MiniHttpServer: public QTcpServer { Q_OBJECT QTcpSocket *client; QList dataToTransmit; public: QByteArray receivedData; MiniHttpServer(const QList &data) : client(0), dataToTransmit(data) { listen(); connect(this, SIGNAL(newConnection()), this, SLOT(doAccept())); } public slots: void doAccept() { client = nextPendingConnection(); connect(client, SIGNAL(readyRead()), this, SLOT(sendData())); } void sendData() { receivedData += client->readAll(); int idx = client->property("dataTransmitionIdx").toInt(); if (receivedData.contains("\r\n\r\n") || receivedData.contains("\n\n")) { if (idx < dataToTransmit.length()) client->write(dataToTransmit.at(idx++)); if (idx == dataToTransmit.length()) { client->disconnectFromHost(); disconnect(client, 0, this, 0); client = 0; } else { client->setProperty("dataTransmitionIdx", idx); } } } }; tst_QHttpSocketEngine::tst_QHttpSocketEngine() { Q_SET_DEFAULT_IAP } tst_QHttpSocketEngine::~tst_QHttpSocketEngine() { } void tst_QHttpSocketEngine::init() { tmpSocket = 0; bytesAvailable = 0; } void tst_QHttpSocketEngine::cleanup() { } //--------------------------------------------------------------------------- void tst_QHttpSocketEngine::construction() { QHttpSocketEngine socketDevice; QVERIFY(!socketDevice.isValid()); // Initialize device QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol)); QVERIFY(socketDevice.isValid()); QVERIFY(socketDevice.protocol() == QAbstractSocket::IPv4Protocol); QVERIFY(socketDevice.socketType() == QAbstractSocket::TcpSocket); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); // QVERIFY(socketDevice.socketDescriptor() != -1); QVERIFY(socketDevice.localAddress() == QHostAddress()); QVERIFY(socketDevice.localPort() == 0); QVERIFY(socketDevice.peerAddress() == QHostAddress()); QVERIFY(socketDevice.peerPort() == 0); QVERIFY(socketDevice.error() == QAbstractSocket::UnknownSocketError); //QTest::ignoreMessage(QtWarningMsg, "QSocketLayer::bytesAvailable() was called in QAbstractSocket::UnconnectedState"); QVERIFY(socketDevice.bytesAvailable() == 0); //QTest::ignoreMessage(QtWarningMsg, "QSocketLayer::hasPendingDatagrams() was called in QAbstractSocket::UnconnectedState"); QVERIFY(!socketDevice.hasPendingDatagrams()); } //--------------------------------------------------------------------------- void tst_QHttpSocketEngine::errorTest_data() { QTest::addColumn("hostname"); QTest::addColumn("port"); QTest::addColumn("username"); QTest::addColumn("response"); QTest::addColumn("expectedError"); QQueue responses; QTest::newRow("proxy-host-not-found") << "this-host-does-not-exist." << 1080 << QString() << QString() << int(QAbstractSocket::ProxyNotFoundError); QTest::newRow("proxy-connection-refused") << QtNetworkSettings::serverName() << 2 << QString() << QString() << int(QAbstractSocket::ProxyConnectionRefusedError); QTest::newRow("garbage1") << QString() << 0 << QString() << "This is not HTTP\r\n\r\n" << int(QAbstractSocket::ProxyProtocolError); QTest::newRow("garbage2") << QString() << 0 << QString() << "This is not HTTP" << int(QAbstractSocket::ProxyConnectionClosedError); QTest::newRow("garbage3") << QString() << 0 << QString() << "" << int(QAbstractSocket::ProxyConnectionClosedError); QTest::newRow("forbidden") << QString() << 0 << QString() << "HTTP/1.0 403 Forbidden\r\n\r\n" << int(QAbstractSocket::SocketAccessError); QTest::newRow("method-not-allowed") << QString() << 0 << QString() << "HTTP/1.0 405 Method Not Allowed\r\n\r\n" << int(QAbstractSocket::SocketAccessError); QTest::newRow("proxy-authentication-too-short") << QString() << 0 << "foo" << "HTTP/1.0 407 Proxy Authentication Required\r\n\r\n" << int(QAbstractSocket::ProxyProtocolError); QTest::newRow("proxy-authentication-invalid-method") << QString() << 0 << "foo" << "HTTP/1.0 407 Proxy Authentication Required\r\n" "Proxy-Authenticate: Frobnicator\r\n\r\n" << int(QAbstractSocket::ProxyProtocolError); QTest::newRow("proxy-authentication-required") << QString() << 0 << "foo" << "HTTP/1.0 407 Proxy Authentication Required\r\n" "Proxy-Connection: close\r\n" "Proxy-Authenticate: Basic, realm=wonderland\r\n\r\n" << int(QAbstractSocket::ProxyAuthenticationRequiredError); QTest::newRow("proxy-authentication-required2") << QString() << 0 << "foo" << "HTTP/1.0 407 Proxy Authentication Required\r\n" "Proxy-Connection: keep-alive\r\n" "Proxy-Authenticate: Basic, realm=wonderland\r\n\r\n" "\1" "HTTP/1.0 407 Proxy Authentication Required\r\n" "Proxy-Authenticate: Basic, realm=wonderland\r\n\r\n" << int(QAbstractSocket::ProxyAuthenticationRequiredError); QTest::newRow("proxy-authentication-required-noclose") << QString() << 0 << "foo" << "HTTP/1.0 407 Proxy Authentication Required\r\n" "Proxy-Authenticate: Basic\r\n" "\r\n" << int(QAbstractSocket::ProxyAuthenticationRequiredError); QTest::newRow("connection-refused") << QString() << 0 << QString() << "HTTP/1.0 503 Service Unavailable\r\n\r\n" << int(QAbstractSocket::ConnectionRefusedError); QTest::newRow("host-not-found") << QString() << 0 << QString() << "HTTP/1.0 404 Not Found\r\n\r\n" << int(QAbstractSocket::HostNotFoundError); QTest::newRow("weird-http-reply") << QString() << 0 << QString() << "HTTP/1.0 206 Partial Content\r\n\r\n" << int(QAbstractSocket::ProxyProtocolError); } void tst_QHttpSocketEngine::errorTest() { QFETCH(QString, hostname); QFETCH(int, port); QFETCH(QString, username); QFETCH(QString, response); QFETCH(int, expectedError); MiniHttpServer server(response.toLatin1().split('\1')); if (hostname.isEmpty()) { hostname = "127.0.0.1"; port = server.serverPort(); } QTcpSocket socket; socket.setProxy(QNetworkProxy(QNetworkProxy::HttpProxy, hostname, port, username, username)); socket.connectToHost("0.1.2.3", 12345); connect(&socket, SIGNAL(error(QAbstractSocket::SocketError)), &QTestEventLoop::instance(), SLOT(exitLoop())); QTestEventLoop::instance().enterLoop(5); QVERIFY(!QTestEventLoop::instance().timeout()); QCOMPARE(int(socket.error()), expectedError); } //--------------------------------------------------------------------------- void tst_QHttpSocketEngine::simpleConnectToIMAP() { QHttpSocketEngine socketDevice; // Initialize device QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol)); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); socketDevice.setProxy(QNetworkProxy(QNetworkProxy::HttpProxy, QtNetworkSettings::serverName(), 3128)); QVERIFY(!socketDevice.connectToHost(QtNetworkSettings::serverIP(), 143)); QVERIFY(socketDevice.state() == QAbstractSocket::ConnectingState); QVERIFY(socketDevice.waitForWrite()); QVERIFY(socketDevice.state() == QAbstractSocket::ConnectedState); QVERIFY(socketDevice.peerAddress() == QtNetworkSettings::serverIP()); QVERIFY(!socketDevice.localAddress().isNull()); QVERIFY(socketDevice.localPort() > 0); // Wait for the greeting QVERIFY(socketDevice.waitForRead()); // Read the greeting qint64 available = socketDevice.bytesAvailable(); QVERIFY(available > 0); QByteArray array; array.resize(available); QVERIFY(socketDevice.read(array.data(), array.size()) == available); // Check that the greeting is what we expect it to be QCOMPARE(array.constData(), QtNetworkSettings::expectedReplyIMAP().constData()); // Write a logout message QByteArray array2 = "XXXX LOGOUT\r\n"; QVERIFY(socketDevice.write(array2.data(), array2.size()) == array2.size()); // Wait for the response QVERIFY(socketDevice.waitForRead()); available = socketDevice.bytesAvailable(); QVERIFY(available > 0); array.resize(available); QVERIFY(socketDevice.read(array.data(), array.size()) == available); // Check that the greeting is what we expect it to be QCOMPARE(array.constData(), "* BYE LOGOUT received\r\nXXXX OK Completed\r\n"); // Wait for the response QVERIFY(socketDevice.waitForRead()); char c; QCOMPARE(socketDevice.read(&c, sizeof(c)), (qint64) -1); QVERIFY(socketDevice.error() == QAbstractSocket::RemoteHostClosedError); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); } //--------------------------------------------------------------------------- void tst_QHttpSocketEngine::simpleErrorsAndStates() { { QHttpSocketEngine socketDevice; // Initialize device QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol)); socketDevice.setProxy(QNetworkProxy(QNetworkProxy::HttpProxy, QtNetworkSettings::serverName(), 3128)); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); QVERIFY(!socketDevice.connectToHost(QHostAddress(QtNetworkSettings::serverName()), 8088)); QVERIFY(socketDevice.state() == QAbstractSocket::ConnectingState); if (socketDevice.waitForWrite(15000)) { QVERIFY(socketDevice.state() == QAbstractSocket::ConnectedState || socketDevice.state() == QAbstractSocket::UnconnectedState); } else { QVERIFY(socketDevice.error() == QAbstractSocket::SocketTimeoutError); } } } /* //--------------------------------------------------------------------------- void tst_QHttpSocketEngine::tcpLoopbackPerformance() { QTcpServer server; // Bind to any port on all interfaces QVERIFY(server.bind(QHostAddress("0.0.0.0"), 0)); QVERIFY(server.state() == QAbstractSocket::BoundState); quint16 port = server.localPort(); // Listen for incoming connections QVERIFY(server.listen()); QVERIFY(server.state() == QAbstractSocket::ListeningState); // Initialize a Tcp socket QHttpSocketEngine client; QVERIFY(client.initialize(QAbstractSocket::TcpSocket)); client.setProxy(QHostAddress("80.232.37.158"), 1081); // Connect to our server if (!client.connectToHost(QHostAddress("127.0.0.1"), port)) { QVERIFY(client.waitForWrite()); QVERIFY(client.connectToHost(QHostAddress("127.0.0.1"), port)); } // The server accepts the connectio int socketDescriptor = server.accept(); QVERIFY(socketDescriptor > 0); // A socket device is initialized on the server side, passing the // socket descriptor from accept(). It's pre-connected. QSocketLayer serverSocket; QVERIFY(serverSocket.initialize(socketDescriptor)); QVERIFY(serverSocket.state() == QAbstractSocket::ConnectedState); const int messageSize = 1024 * 256; QByteArray message1(messageSize, '@'); QByteArray answer(messageSize, '@'); QTime timer; timer.start(); qlonglong readBytes = 0; while (timer.elapsed() < 5000) { qlonglong written = serverSocket.write(message1.data(), message1.size()); while (written > 0) { client.waitForRead(); if (client.bytesAvailable() > 0) { qlonglong readNow = client.read(answer.data(), answer.size()); written -= readNow; readBytes += readNow; } } } qDebug("\t\t%.1fMB/%.1fs: %.1fMB/s", readBytes / (1024.0 * 1024.0), timer.elapsed() / 1024.0, (readBytes / (timer.elapsed() / 1000.0)) / (1024 * 1024)); } */ void tst_QHttpSocketEngine::tcpSocketBlockingTest() { QHttpSocketEngineHandler http; QTcpSocket socket; // Connect socket.connectToHost(QtNetworkSettings::serverName(), 143); QVERIFY(socket.waitForConnected()); QCOMPARE(socket.state(), QTcpSocket::ConnectedState); // Read greeting QVERIFY(socket.waitForReadyRead(5000)); QString s = socket.readLine(); QCOMPARE(s.toLatin1().constData(), QtNetworkSettings::expectedReplyIMAP().constData()); // Write NOOP QCOMPARE((int) socket.write("1 NOOP\r\n", 8), 8); if (!socket.canReadLine()) QVERIFY(socket.waitForReadyRead(5000)); // Read response s = socket.readLine(); QCOMPARE(s.toLatin1().constData(), "1 OK Completed\r\n"); // Write LOGOUT QCOMPARE((int) socket.write("2 LOGOUT\r\n", 10), 10); if (!socket.canReadLine()) QVERIFY(socket.waitForReadyRead(5000)); // Read two lines of respose s = socket.readLine(); QCOMPARE(s.toLatin1().constData(), "* BYE LOGOUT received\r\n"); if (!socket.canReadLine()) QVERIFY(socket.waitForReadyRead(5000)); s = socket.readLine(); QCOMPARE(s.toLatin1().constData(), "2 OK Completed\r\n"); // Close the socket socket.close(); // Check that it's closed QCOMPARE(socket.state(), QTcpSocket::UnconnectedState); } //---------------------------------------------------------------------------------- void tst_QHttpSocketEngine::tcpSocketNonBlockingTest() { QHttpSocketEngineHandler http; QTcpSocket socket; connect(&socket, SIGNAL(hostFound()), SLOT(tcpSocketNonBlocking_hostFound())); connect(&socket, SIGNAL(connected()), SLOT(tcpSocketNonBlocking_connected())); connect(&socket, SIGNAL(disconnected()), SLOT(tcpSocketNonBlocking_closed())); connect(&socket, SIGNAL(bytesWritten(qint64)), SLOT(tcpSocketNonBlocking_bytesWritten(qint64))); connect(&socket, SIGNAL(readyRead()), SLOT(tcpSocketNonBlocking_readyRead())); tcpSocketNonBlocking_socket = &socket; // Connect socket.connectToHost(QtNetworkSettings::serverName(), 143); QVERIFY(socket.state() == QTcpSocket::HostLookupState || socket.state() == QTcpSocket::ConnectingState); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } if (socket.state() == QTcpSocket::ConnectingState) { QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } } QCOMPARE(socket.state(), QTcpSocket::ConnectedState); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } // Read greeting QVERIFY(!tcpSocketNonBlocking_data.isEmpty()); QCOMPARE(tcpSocketNonBlocking_data.at(0).toLatin1().constData(), QtNetworkSettings::expectedReplyIMAP().constData()); tcpSocketNonBlocking_data.clear(); tcpSocketNonBlocking_totalWritten = 0; // Write NOOP QCOMPARE((int) socket.write("1 NOOP\r\n", 8), 8); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } QVERIFY(tcpSocketNonBlocking_totalWritten == 8); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } // Read response QVERIFY(!tcpSocketNonBlocking_data.isEmpty()); QCOMPARE(tcpSocketNonBlocking_data.at(0).toLatin1().constData(), "1 OK Completed\r\n"); tcpSocketNonBlocking_data.clear(); tcpSocketNonBlocking_totalWritten = 0; // Write LOGOUT QCOMPARE((int) socket.write("2 LOGOUT\r\n", 10), 10); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } QVERIFY(tcpSocketNonBlocking_totalWritten == 10); // Wait for greeting QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) { QFAIL("Timed out"); } // Read two lines of respose QCOMPARE(tcpSocketNonBlocking_data.at(0).toLatin1().constData(), "* BYE LOGOUT received\r\n"); QCOMPARE(tcpSocketNonBlocking_data.at(1).toLatin1().constData(), "2 OK Completed\r\n"); tcpSocketNonBlocking_data.clear(); // Close the socket socket.close(); // Check that it's closed QCOMPARE(socket.state(), QTcpSocket::UnconnectedState); } void tst_QHttpSocketEngine::tcpSocketNonBlocking_hostFound() { QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::tcpSocketNonBlocking_connected() { QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::tcpSocketNonBlocking_readyRead() { while (tcpSocketNonBlocking_socket->canReadLine()) tcpSocketNonBlocking_data.append(tcpSocketNonBlocking_socket->readLine()); QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::tcpSocketNonBlocking_bytesWritten(qint64 written) { tcpSocketNonBlocking_totalWritten += written; QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::tcpSocketNonBlocking_closed() { } //---------------------------------------------------------------------------------- void tst_QHttpSocketEngine::downloadBigFile() { QHttpSocketEngineHandler http; if (tmpSocket) delete tmpSocket; tmpSocket = new QTcpSocket; connect(tmpSocket, SIGNAL(connected()), SLOT(exitLoopSlot())); connect(tmpSocket, SIGNAL(readyRead()), SLOT(downloadBigFileSlot())); tmpSocket->connectToHost(QtNetworkSettings::serverName(), 80); QTestEventLoop::instance().enterLoop(30); if (QTestEventLoop::instance().timeout()) QFAIL("Network operation timed out"); QByteArray hostName = QtNetworkSettings::serverName().toLatin1(); QVERIFY(tmpSocket->state() == QAbstractSocket::ConnectedState); QVERIFY(tmpSocket->write("GET /qtest/mediumfile HTTP/1.0\r\n") > 0); QVERIFY(tmpSocket->write("Host: ") > 0); QVERIFY(tmpSocket->write(hostName.data()) > 0); QVERIFY(tmpSocket->write("\r\n") > 0); QVERIFY(tmpSocket->write("\r\n") > 0); bytesAvailable = 0; QTime stopWatch; stopWatch.start(); #if defined(Q_OS_WINCE) || defined(Q_OS_SYMBIAN) QTestEventLoop::instance().enterLoop(240); #else QTestEventLoop::instance().enterLoop(60); #endif if (QTestEventLoop::instance().timeout()) QFAIL("Network operation timed out"); QVERIFY(bytesAvailable >= 10000000); QVERIFY(tmpSocket->state() == QAbstractSocket::ConnectedState); qDebug("\t\t%.1fMB/%.1fs: %.1fMB/s", bytesAvailable / (1024.0 * 1024.0), stopWatch.elapsed() / 1024.0, (bytesAvailable / (stopWatch.elapsed() / 1000.0)) / (1024 * 1024)); delete tmpSocket; tmpSocket = 0; } void tst_QHttpSocketEngine::exitLoopSlot() { QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::downloadBigFileSlot() { bytesAvailable += tmpSocket->readAll().size(); if (bytesAvailable >= 10000000) QTestEventLoop::instance().exitLoop(); } void tst_QHttpSocketEngine::passwordAuth() { QHttpSocketEngine socketDevice; // Initialize device QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol)); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); socketDevice.setProxy(QNetworkProxy(QNetworkProxy::HttpProxy, QtNetworkSettings::serverName(), 3128, "qsockstest", "password")); QVERIFY(!socketDevice.connectToHost(QtNetworkSettings::serverIP(), 143)); QVERIFY(socketDevice.state() == QAbstractSocket::ConnectingState); QVERIFY(socketDevice.waitForWrite()); QVERIFY(socketDevice.state() == QAbstractSocket::ConnectedState); QVERIFY(socketDevice.peerAddress() == QtNetworkSettings::serverIP()); // Wait for the greeting QVERIFY(socketDevice.waitForRead()); // Read the greeting qint64 available = socketDevice.bytesAvailable(); QVERIFY(available > 0); QByteArray array; array.resize(available); QVERIFY(socketDevice.read(array.data(), array.size()) == available); // Check that the greeting is what we expect it to be QCOMPARE(array.constData(), QtNetworkSettings::expectedReplyIMAP().constData()); // Write a logout message QByteArray array2 = "XXXX LOGOUT\r\n"; QVERIFY(socketDevice.write(array2.data(), array2.size()) == array2.size()); // Wait for the response QVERIFY(socketDevice.waitForRead()); available = socketDevice.bytesAvailable(); QVERIFY(available > 0); array.resize(available); QVERIFY(socketDevice.read(array.data(), array.size()) == available); // Check that the greeting is what we expect it to be QCOMPARE(array.constData(), "* BYE LOGOUT received\r\nXXXX OK Completed\r\n"); // Wait for the response QVERIFY(socketDevice.waitForRead()); char c; QVERIFY(socketDevice.read(&c, sizeof(c)) == -1); QVERIFY(socketDevice.error() == QAbstractSocket::RemoteHostClosedError); QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState); } //---------------------------------------------------------------------------------- QTEST_MAIN(tst_QHttpSocketEngine) #include "tst_qhttpsocketengine.moc"