QProcess i 'nieprzewidywalne' wyjście (UnitTest)

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..

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..).

 
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();
}
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:
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.

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..

1 użytkowników online, w tym zalogowanych: 0, gości: 1