QProcess i 'nieprzewidywalne' wyjście (UnitTest)

QProcess i 'nieprzewidywalne' wyjście (UnitTest)
MI
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 8 lat
  • Postów:110
0

Witam,
mam problem z dojściem, dlaczego na wyjściu QProcessu otrzymuje takie a nie inne wyniki.. Pokazując to na przykładzie unittestu, który ma zwrócić wiadomość o niemożliwości utworzenia katalogu, gdyż takowy już istnieje..

Kopiuj
void MyCmdLine::test_mkdir_with_error()
{
    QString mydir = QDir::currentPath() + "/hello";

    QDir().mkdir(mydir);

    systemshell = new SystemShell();
    systemshell->runProgram("/bin/sh");

    QString cmd = "mkdir hello";
    systemshell->writeAtProgram(cmd.toLatin1());
    systemshell->closeChannel();

    QSignalSpy spy(systemshell,SIGNAL(outputSignal(QByteArray)));
    spy.wait();
    qDebug() << "number of messages: " << spy.count() << " data: " << spy.at(0).count();
    for (int i = 0; i<spy.count(); i++)
        for (int j =0; j<spy.at(i).count(); j++)
            data.append(qvariant_cast<QByteArray>(spy.at(0).at(0)));

    systemshell->closeProcess();

    QCOMPARE(QString(data), QString("mkdir: "));

    data.clear();
    systemshell->deleteLater();

    QDir().rmdir(mydir);
}

Ten test 'zazwyczaj' wychodzi poprawie. Czemu zazwyczaj - bo czasami na wyjściu uzyskiwane są inne dane ..

Przykładowo:

  • Start testing of SystemShellTest *********
    Config: Using QtTest library 5.3.2, Qt 5.3.2
    PASS : SystemShellTest::initTestCase()
    QDEBUG : SystemShellTest::testExecuteCommandWithError() "mkdir: "
    QDEBUG : SystemShellTest::testExecuteCommandWithError() number of messages: 1 data: 1
    QDEBUG : SystemShellTest::testExecuteCommandWithError() "nie można utworzyć katalogu „hello”"
    QDEBUG : SystemShellTest::testExecuteCommandWithError() ": Plik istnieje"
    QDEBUG : SystemShellTest::testExecuteCommandWithError() "
    "
    PASS : SystemShellTest::testExecuteCommandWithError()
    PASS : SystemShellTest::cleanupTestCase()
    Totals: 3 passed, 0 failed, 0 skipped
    * Finished testing of SystemShellTest *********

Innym razem:

  • Start testing of SystemShellTest *********
    Config: Using QtTest library 5.3.2, Qt 5.3.2
    PASS : SystemShellTest::initTestCase()
    QDEBUG : SystemShellTest::testExecuteCommandWithError() "mkdir: " <-- samo mkdir
    QDEBUG : SystemShellTest::testExecuteCommandWithError() number of messages: 1 data: 1
    PASS : SystemShellTest::testExecuteCommandWithError()
    PASS : SystemShellTest::testExecuteCommandWithError()
    PASS : SystemShellTest::cleanupTestCase()
    Totals: 3 passed, 0 failed, 0 skipped
    * Finished testing of SystemShellTest *********

Jeszcze innym razem:

  • Start testing of SystemShellTest *********
    Config: Using QtTest library 5.3.2, Qt 5.3.2
    PASS : SystemShellTest::initTestCase()
    ..
    Actual (QString(data)) : "mkdir: nie mo\u017Cna utworzy\u0107 katalogu \u201Ehello\u201D: Plik istnieje\n"
    ..
    PASS : SystemShellTest::cleanupTestCase()
    Totals: 2 passed, 1 failed, 0 skipped
    * Finished testing of SystemShellTest *********

Ogólnie, dlatego test 'zazwyczaj' wypada poprawnie ponieważ porównywaniu podlega string "mkdir: ", który jest oczekiwany w pierwszej kolejności. Domyślam się, że informacje z QProcessu mogą dochdzić 'różnie', bo to pewnie osobny wątek (tak?), ale wtedy jak mogę poprawnie wykonać ten unittest?

ps. Dorzucam jeszcze fragmenty klasy, w której jest logika QProcessu.. (m.in. gdzie wyrzucane są na konsole dane z QProcesu..).

Kopiuj
 
bool SystemShell::runProgram(QString program, QStringList parameters)
{
    m_Shell->start(program, parameters);
    return m_Shell->waitForStarted();
}
void SystemShell::showOutput()
{
    QByteArray output = m_Shell->readAllStandardOutput();
    //m_Shell->close();
    //#ifdef DEBUG
        qDebug() << output;
    //#endif
    emit outputSignal(output);
}

void SystemShell::closeChannel()
{
   m_Shell->closeWriteChannel();
   //m_Shell->waitForFinished();
}

void SystemShell::closeProcess()
{
    m_Shell->close();
}
MarekR22
Moderator C/C++
  • Rejestracja:około 17 lat
  • Ostatnio:2 minuty
2

Problemy jakie widzę:

  1. Najwyraźniej dziedziczysz po QProcess - IMO nie powinieneś tego robić. Twoja pklasa powinna mieć QProcess nie być tą klasą
  2. efekt jest taki, że twój test sprawdza QProcess a nie twoją kalsę
  3. Twój kod wygląda tak jakbyś oczekiwał, że dostanie kilka sygnałów SIGNAL(outputSignal(QByteArray)) tymczawsem używasz spy.wait();, który czeka na JEDEN sygnał lub timoout. Skutek jest taki, że spy.count() zawsze zwróci ci wartość 1 (chyba, że coś nie będzie działać to 0). Na dodatek masz tu klasyczny błąd z indeksami: data.append(qvariant_cast<QByteArray>(spy.at(0).at(0))) (gdzie i)!
  4. po co logujesz spy.at(0).count()? Zawsze będzie 1 bo do takiego sygnału się podłączyłeś.
  5. czyli te pętle for nic nie robią
  6. nie widać jak skonfigurowałeś QProcess! Jaką stosujesz wartość dla setProcessChannelMode - przypuszczalnie łączysz strumienie?

No i teraz najważniejsze.
Musisz zdać sobie sprawę z tego, że w tym wypadku masz do czynienia z dwoma strumieniami danych. Strumień standardowego wyjścia i strumień błędów.
Zasadniczo oba strumienie są niezależne! A to jak się połączą w niektórych przypadkach może być dość losowe.
Przypuszczalnie właśnie na tym polega problem.
Zależnie jak oba strumienie się połączą to masz błąd, albo nie.

Jak można to naprawić?
Strumienie

  • czytać oba strumienie niezależnie - doczytaj dokumentację
  • albo połączyć je już na poziomie systemu operacyjnego (dodać do argumentów procesu odpowiednie przekierowanie strumienia błędów).
  • czekać na zakończenie całego procesu zanim odczytasz dane

Złe użycie Signal Spy:

  • twoja klasa powinna śledzić wyjście i emitować sygnał, gdy pojawi się oczekiwana wartość. Sygnał ten podłączasz do event loop. Coś takiego:
Kopiuj
QString mydir = QDir::currentPath() + "/hello";
 
    QDir().mkdir(mydir);
 
    systemshell = new SystemShell();
    systemshell->runProgram("/bin/sh");
 
    QString cmd = "mkdir hello";
    systemshell->writeAtProgram(cmd.toLatin1());
    systemshell->closeChannel();
 
    QSignalSpy spy(systemshell,SIGNAL(outputSignal(QByteArray)));
    QEventLoop evLoop;
    connect(systemshell, SIGNAL(endDetected()), &evLoop, SLOT(quit()));
//  albo: 
//  connect(systemshell, SIGNAL(finished(int,QProcess::ExitStatus)), &evLoop, SLOT(quit()));
    QTimer::singleShot(5000, &evLoop, SLOT(quit())); // test musi się kiedyś kończyć!
    evLoop.exec();

    // teraz to ma sens:
    qDebug() << "number of messages: " << spy.count();
    for (int i = 0; i<spy.count(); i++)
        for (int j =0; j<spy.at(i).count(); j++)
            data.append(qvariant_cast<QByteArray>(spy.at(i).at(0)));
 
    systemshell->closeProcess();
 
    QCOMPARE(QString(data), QString("mkdir: "));
 
    data.clear();
    systemshell->deleteLater();
 
    QDir().rmdir(mydir);

Ten kod jest na szybko poprawiany by pokazać koncept, pewnie ma więcej błędów, które zapominałem poprawić lub przegapiłem.


Jeśli chcesz pomocy, NIE pisz na priva, ale zadaj dobre pytanie na forum.
edytowany 4x, ostatnio: MarekR22
MI
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 8 lat
  • Postów:110
0

@MarekR22 - Po wprowadzeniu zmian, jakie zaproponowałeś wszystkie komunikaty 'dochodzą' i oczekiwane wyjście jest poprawne za każdym razem.. Dzięki :) Rozumiem wskazówki, które podałeś, ale jak teraz dobrze używać QProcessu do wykonywania komend w terminalu, aby czekać na całe wyjście z QProcessu ? Wystarczy poczekać do momentu wyemitowania sygnału finished?

Natomiast odnośnie rozdzielenia obydwu strumieni, należy zastosować:

setProcessChannelMode(QProcess::SeparateChannels);

?

EDIT:

OK, znalazłem chyba potwierdzenie ( https://forum.qt.io/topic/7165/waitforfinished-fires-early/8 ), że QEventLoop można stosować wszędzie tam, gdzie chcemy mieć 'responsywny' główny program..

edytowany 1x, ostatnio: mikajlo

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.