Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QtTest driver v2 #142

Closed
wants to merge 16 commits into from
Closed

QtTest driver v2 #142

wants to merge 16 commits into from

Conversation

konserw
Copy link
Contributor

@konserw konserw commented Mar 30, 2017

QtTest based test driver for cucumber-cpp.
Includes also QtTestSteps for both Calc and CalcQt examples.
It is based of PR #140 for Qt in CI
Please take a look at new implementation using pipes for both POSIX and windows.
It doesn't address both your considerations as this is also mostly copied from SO and it's redirecting entire stdout, but for sure it is better than previous implementation. If you still don't like it I'll prepare temporary-file based version.
Only issue I've found is #141 which is here visible as Qt 5.7+ requires C++11.

@konserw konserw mentioned this pull request Mar 30, 2017
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 2.8.12)
cmake_minimum_required(VERSION 3.1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I think we already needed 3.1 anyway (for both find_dependency() and Threads::Threads).

.travis.yml Outdated

before_install:
- if [[ "${TRAVIS_OS_NAME}" = "osx" ]]; then brew update && brew install ninja; fi
- if [[ "${TRAVIS_OS_NAME}" = "osx" ]]; then brew update && brew install ninja && brew install qt5; fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: doesn't brew support installing multiple packages at once? E.g. brew install ninja qt5 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are probably right - I have no expirience with OSX

CMakeLists.txt Outdated
@@ -13,7 +13,9 @@ set(CUKE_ENABLE_EXAMPLES OFF CACHE BOOL "Enable the examples")
set(GMOCK_SRC_DIR "" CACHE STRING "Google Mock framework sources path (otherwise downloaded)")
set(GMOCK_VER "1.7.0" CACHE STRING "Google Mock framework version to be used")
option(VALGRIND_TESTS "Run tests within Valgrind" OFF)
set(CUKE_DISABLE_QT OFF CACHE BOOL "Disable using Qt framework")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For boolean parameters I recommend using option instead (it's intended to give user options and only supports booleans).

CMakeLists.txt Outdated
execute_process(COMMAND ${QMAKE_EXECUTABLE} -query QT_VERSION OUTPUT_VARIABLE QT_VERSION)
if(QT_VERSION GREATER 5.6.99)
message(STATUS "C++11 is needed from Qt version 5.7.0, building with c++11 enabled")
set(CMAKE_CXX_STANDARD 11)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you just build the Qt specific bits with C++11 instead of everything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting this for everything fixed TCPserver tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are they broken? Because they seem to be running fine on Travis.

AFAIK we're doing a lot of work to be C++98 compatible. Just flipping the switch like this throws away our testing of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see issue #141 - we can discuss further there the ServerTests.

CMakeLists.txt Outdated
#check if c++11 is nedded
find_program(QMAKE_EXECUTABLE NAMES qmake HINTS ${QTDIR} ENV QTDIR PATH_SUFFIXES bin)
execute_process(COMMAND ${QMAKE_EXECUTABLE} -query QT_VERSION OUTPUT_VARIABLE QT_VERSION)
if(QT_VERSION GREATER 5.6.99)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use this instead:

if(NOT QT_VERSION VERSION_LESS 5.7)

That does a proper lexicographic comparison of the version number.

// make stdout & stderr streams unbuffered
// so that we don't need to flush the streams
// before capture and after capture
// (fflush can cause a deadlock if the stream is currently being
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering how this sentence should end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used?

if (m_capturing)
return;

secure_pipe(m_pipe);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do all these secure_XXX variants come from? I've never encountered them before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are private methods in that class

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks, I overlooked those.

#define STD_ERR_FD (fileno(stderr))
#endif

class qtCapture {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of this code is dependent on a template parameter, so it can all be moved out of this header and into a .cpp file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean everything or just the methods definitions ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method definitions and all the #includes to support them.

return m_capturing;
}

static void EndCapture() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this entire class a singleton? It seems to me like the restoring of file descriptors that you're doing here is something that you would want to do in a destructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've converted it to normal class

QtTestObject testObject(this);

qtCapture::Init();
qtCapture::BeginCapture();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming you're capturing stdout/stderr here because Qt tends to be overly verbose during execution? Is it possible to make Qt write to some alternate log sink instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately only to file or stdout and we need capturing output to get results

@@ -3,7 +3,7 @@ function(cuke_add_driver_test TEST_FILE)
message(STATUS "Adding " ${TEST_NAME})
add_executable(${TEST_NAME} ${TEST_FILE}.cpp)
target_link_libraries(${TEST_NAME} cucumber-cpp-nomain ${CUKE_EXTRA_LIBRARIES} ${ARGN})
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
add_test(${TEST_NAME} ${TEST_NAME})
Copy link
Contributor

@muggenhor muggenhor Mar 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why you make this change. And I do believe this is causing your test failures when running with Valgrind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll revert it

@konserw
Copy link
Contributor Author

konserw commented Apr 1, 2017

As for the c++ standard: now change to c++11 is triggered by Qt 5.8 in:

  • mingw x32
  • msvc x64
  • OSX / GMock 1.8.0
    so only 3 build out of 14 - is it acceptable ?

Copy link
Contributor

@muggenhor muggenhor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The qtCapture code looks to be written without a complete understanding of how to use syscalls under Unix. This is a non-trivial subject that's sometimes very tricky to get right.

Additionally some 5 minute searching in Qt's documentation gave me the suggestion that a message handler could possibly be installed instead to capture this output. I suggest to look at that first, before trying to go this route of intercepting things: https://doc.qt.io/qt-4.8/qtglobal.html#qInstallMsgHandler

do {
ret = close(fd);
fd_blocked = (errno == EINTR);
if (fd_blocked)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EINTR indicates that the syscall was interrupted by a syscall and can be retried immediately. There's no point in waiting first. (The same applies to other syscalls where EINTR is returned).

bool fd_blocked = false;
do {
ret = dup(src);
fd_blocked = (errno == EINTR || errno == EBUSY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, because errno's content is undefined when the previous syscall succeeded. I.e. in this case errno is only valid when dup's return value == -1.

fd_blocked = (errno == EINTR);
if (fd_blocked)
QThread::msleep(10);
} while (ret < 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes you to wait indefinitely for errors that aren't checked for explicitly. Usually you want the converse: only retry for a specific set of known error conditions.

#include <QtConcurrent/QtConcurrent>

#ifndef STD_OUT_FD
#define STD_OUT_FD (fileno(stdout))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use this macro to hide where you're getting this file descriptor from. You're disguising it as a constant while it isn't.

m_oldStdOut = secure_dup(STD_OUT_FD);
m_oldStdErr = secure_dup(STD_ERR_FD);
secure_dup2(m_pipe[WRITE], STD_OUT_FD);
secure_dup2(m_pipe[WRITE], STD_ERR_FD);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has one rather fatal flaw: it won't work if whatever you write more than the pipe's buffer can hold to either of these file descriptors. At that point it will block and you're process will hang.

@konserw
Copy link
Contributor Author

konserw commented Apr 5, 2017

Additionally some 5 minute searching in Qt's documentation gave me the suggestion that a message handler could possibly be installed instead to capture this output. I suggest to look at that first, before trying to go this route of intercepting things: https://doc.qt.io/qt-4.8/qtglobal.html#qInstallMsgHandler

Unfortunately all QtTestLib does not rely on normal Qt debug output. they use something like this:

void QAbstractTestLogger::outputString(const char *msg)
{
    QTEST_ASSERT(QTest::stream);

    ::fputs(msg, QTest::stream);
    ::fflush(QTest::stream);
}

Where QtTest::stream if you do not want output to file is:

    if (!out) {
        QTest::stream = stdout;
        return;
}

@konserw
Copy link
Contributor Author

konserw commented Apr 5, 2017

Please reevaluate temporary file based solution as qInstallHandler won't do the trick, and pipe based solution wasn't as good as I believed.

Copy link
Contributor

@muggenhor muggenhor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much better. I've got just a few comments and questions now.

int returnValue = QTest::qExec(&testObject, 0, NULL);
capture.EndCapture();
QTemporaryFile file;
QString fileName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this unused variable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if(!file.open()) {
return InvokeResult::failure("Unable to open temporary file needed for this test");
}
file.close();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this or reopening it later destroy the used name? I also don't think it's necessary to close. As long as you don't read from it the buffer shouldn't be filled so you shouldn't have any problems of it containing stale data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't destroy used name, but it turns out to be unnecessary to close and reopen.


QtTestObject testObject(this);
int returnValue = QTest::qExec(&testObject, QStringList() << "test" << "-o" << file.fileName());
if(returnValue == 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space between if and (

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
file.open();
QTextStream ts(&file);
return InvokeResult::failure(ts.readAll().toLocal8Bit());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're converting this to JSON later on. This should definitely be encoded to UTF-8, not to the local character set. Let cucumber take care of doing that conversion when it wishes to display its output.

@konserw
Copy link
Contributor Author

konserw commented Aug 23, 2017

Closing this as I've created another revision of this PR #165.

@konserw konserw closed this Aug 23, 2017
@konserw konserw deleted the qttest2 branch April 2, 2018 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants