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

Implement process isolation support for tests #853

Open
horenmar opened this issue Mar 11, 2017 · 15 comments
Open

Implement process isolation support for tests #853

horenmar opened this issue Mar 11, 2017 · 15 comments

Comments

@horenmar
Copy link
Member

horenmar commented Mar 11, 2017

Major features that could be implemented afterwards:

  1. Catch should not go crazy if the test case uses fork or similar system call
  2. Catch should allow running tests in isolated processes
  3. Catch should allow testing for abort -- there should be a macro along the lines of REQUIRE_ABORT(expr), that launches separate process to check expr and succeeds if the launched process aborts
  4. Catch should allow limited time for test suite execution
@pavelponomarev
Copy link

Really, really needed feature!

@Leandros
Copy link

Leandros commented Mar 2, 2018

👍

@ranty-fugue
Copy link

@philsquared Is this still under consideration? Would really like to see this feature! :-)

@Cloudef
Copy link

Cloudef commented Mar 22, 2020

#pragma once

#include <functional>
#include <iostream>
#include <sstream>
#include <thread>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

struct catchpp_stdstream { int fd[3], target; std::stringstream ss; };

static inline bool
catchpp_fork_and_run(std::function<void(void)> fun) {
    struct catchpp_stdstream stream[] = {
        { { -1, -1, -1 }, STDOUT_FILENO, std::stringstream() },
        { { -1, -1, -1 }, STDERR_FILENO, std::stringstream() },
    };

    for (size_t i = 0; i < sizeof(stream) / sizeof(stream[0]); ++i) {
        pipe(stream[i].fd);
        fcntl(stream[i].fd[0], F_SETFL, O_NONBLOCK);
        fcntl(stream[i].fd[1], F_SETFL, O_NONBLOCK);
    }

    pid_t pid;
    if ((pid = fork()) == 0) {
        for (size_t i = 0; i < sizeof(stream) / sizeof(stream[0]); ++i) {
            dup2(stream[i].fd[1], stream[i].target);
            close(stream[i].fd[1]);
            close(stream[i].fd[0]);
        }
        fun();
        _exit(0);
    }

    int code;
    waitpid(pid, &code, 0);

    char buf[1024];
    for (size_t i = 0; i < sizeof(stream) / sizeof(stream[0]); ++i) {
        close(stream[i].fd[1]);
        for (ssize_t r = 0; (r = read(stream[i].fd[0], buf, sizeof(buf))) > 0;) stream[i].ss.write(buf, r);
        close(stream[i].fd[0]);
    }

    std::cout << stream[0].ss.str();
    std::cerr << stream[1].ss.str();
    return WIFEXITED(code);
}

#define REQUIRE_ABORT(x) REQUIRE_FALSE(catchpp_fork_and_run(x))

static inline void
catchpp_catch_stdstreams(std::function<void(void)> fun, std::function<void(const std::stringstream &sout, const std::stringstream &serr)> then) {
    struct catchpp_stdstream stream[] = {
        { { -1, -1, -1 }, STDOUT_FILENO, std::stringstream() },
        { { -1, -1, -1 }, STDERR_FILENO, std::stringstream() },
    };

    for (size_t i = 0; i < sizeof(stream) / sizeof(stream[0]); ++i) {
        pipe(stream[i].fd);
        fcntl(stream[i].fd[0], F_SETFL, O_NONBLOCK);
        fcntl(stream[i].fd[1], F_SETFL, O_NONBLOCK);
        stream[i].fd[2] = dup(stream[i].target);
        dup2(stream[i].fd[1], stream[i].target);
    }

    fun();

    char buf[1024];
    for (size_t i = 0; i < sizeof(stream) / sizeof(stream[0]); ++i) {
        fsync(stream[i].target);
        for (ssize_t r = 0; (r = read(stream[i].fd[0], buf, sizeof(buf))) > 0;) stream[i].ss.write(buf, r);
        close(stream[i].fd[0]);
        close(stream[i].fd[1]);
        dup2(stream[i].fd[2], stream[i].target);
        close(stream[i].fd[2]);
    }

    std::cout << stream[0].ss.str();
    std::cerr << stream[1].ss.str();
    then(stream[0].ss, stream[1].ss);
}

#define CATCH_STDSTREAM(x, y) catchpp_catch_stdstreams(x, y)

Something I've used. Fork is not really safe for various things though. Isolating tests to own processes would be the right way.

@robert-andrzejuk
Copy link
Contributor

microsoft/GSL#831

@johnmcfarlane
Copy link

johnmcfarlane commented May 17, 2020

I think I've mentioned this in person but ASSERT_DEATH is the only thing keeping using GTest both at work and at home. It's vital to testing contract tests.

@rfbird
Copy link

rfbird commented Oct 6, 2020

I think I've mentioned this in person but ASSERT_DEATH is the only think keeping using GTest both at work and at home. It's vital to testing contract tests.

+1, we might have to swap over to gtest..

@escherstair
Copy link

+1 from my side too

@tsondergaard
Copy link
Contributor

tsondergaard commented Mar 13, 2021

Here is an idea for a design:

I imagine that we extend the existing runner Catch::Session::run() with an undocumented command-line switch --managed where you can pass two file descriptors used for communication. Then we create an external runner called catch2-runner that you run e.g like this: catch2-runner test-executable . The catch2-runner will then create two pipes and launch test-executable with test-executable --managed fd1 fd2. It will then be able to send instructions to the test-executable via one of the fds and receive responses on the other. I imagine we make a simple protocol for communication - perhaps json based. I imagine the protocol will provide catch2-runner with commands like these:

  • Request list all available tests with tags
  • Run test

The test-executable running Catch::session::run() in managed mode will be able to respond with

  • List of all available tests with tags
  • Test competion with metadata - duration and test result

catch2-runner will execute the first command to get a list of all tests and after that it will issue Run test commands every time it receives a response back about a test completing.

This would give us the following benefits

  • A crashing test will still give a full (junit) test report xml - only the failing test(s) will be flagged as crashing instead of what happens now which is leave an empty or missing xml report
  • We can reliably capture stderr and stdout (I’d like them to be joined) and include that in the junit xml report for much improved usability in CI. stderr+stdout can be thrown away for all tests that don’t fail to avoid generating huge xml files
  • We have a path for running multiple instances of test-executable in parallel. For large suites of tests this would be a significant win. All it would take is for catch2-runner to be able to launch multiple copies of test-executable and run different subset of tests in each.
  • Ability to timeout individual tests.

Doing this would require some platform specific code and/or some dependencies, ie for creating pipes, starting and managing processes and perhaps a JSON library for the wire protocol.

@Raekye
Copy link

Raekye commented Apr 1, 2021

I have some code that calls std::terminate() if a requested/required allocation would be too large (I have exceptions disabled so throwing an exception is not an option). Would love this feature!

@Zerophase
Copy link

Any word on when this is being implemented? I have an assert in my code to make sure passed values match as an optimization to avoid a strlen call. Notifies others of proper usage, and gets optimized out on release builds. An exception is not exactly ideal here.

@bcaddy
Copy link

bcaddy commented Jul 23, 2021

Any word on this? The lack of death tests is forcing me to switch to google test and I'd rather not.

@Bktero
Copy link

Bktero commented Nov 17, 2021

Same same! This feature would be highly appreciated :)

1 similar comment
@tlifschitz
Copy link

Same same! This feature would be highly appreciated :)

@MiaoDX
Copy link

MiaoDX commented Dec 6, 2022

I had one rather "happy" night by looking at gtest and catch2 (I want one proper lib for clang-tidy integration):

Now, gtest thinks clang-tidy is giving false positive: google/googletest#2442

and catch2 currently has no EXPECT_DEATH

Both seems will keep as is for a while 0.0 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests