Skip to content

Commit

Permalink
Kill the test harness if any test exceeds a timeout. (flutter#16349)
Browse files Browse the repository at this point in the history
Our tests depend on explicit latching to verify assertion are checked. If a test
does not respond for a long time, it has probably encoutered a deadlock. Instead
of waiting for the test runner to detect this, apply a very aggresive timeout on
a per test basis.
  • Loading branch information
chinmaygarde authored Feb 4, 2020
1 parent 677b563 commit 7ca44d3
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 1 deletion.
2 changes: 2 additions & 0 deletions testing/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ source_set("testing") {

sources = [
"run_all_unittests.cc",
"test_timeout_listener.cc",
"test_timeout_listener.h",
]

public_deps = [
Expand Down
8 changes: 7 additions & 1 deletion testing/run_all_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

#include "flutter/fml/build_config.h"
#include "flutter/testing/test_timeout_listener.h"
#include "gtest/gtest.h"

#ifdef OS_IOS
Expand All @@ -18,5 +19,10 @@ int main(int argc, char** argv) {
#endif // OS_IOS

::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
auto timeout_listener = new flutter::testing::TestTimeoutListener();
auto& listeners = ::testing::UnitTest::GetInstance()->listeners();
listeners.Append(timeout_listener);
auto result = RUN_ALL_TESTS();
delete listeners.Release(timeout_listener);
return result;
}
121 changes: 121 additions & 0 deletions testing/test_timeout_listener.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/test_timeout_listener.h"

#include <map>
#include <sstream>

namespace flutter {
namespace testing {

class PendingTests : public std::enable_shared_from_this<PendingTests> {
public:
static std::shared_ptr<PendingTests> Create(
fml::RefPtr<fml::TaskRunner> host_task_runner,
fml::TimeDelta timeout) {
return std::shared_ptr<PendingTests>(
new PendingTests(std::move(host_task_runner), timeout));
}

~PendingTests() = default;

void OnTestBegin(const std::string& test_name, fml::TimePoint test_time) {
FML_CHECK(tests_.find(test_name) == tests_.end())
<< "Attempting to start a test that is already pending.";
tests_[test_name] = test_time;

host_task_runner_->PostDelayedTask(
[weak = weak_from_this()] {
if (auto strong = weak.lock()) {
strong->CheckTimedOutTests();
}
},
timeout_);
}

void OnTestEnd(const std::string& test_name) { tests_.erase(test_name); }

void CheckTimedOutTests() const {
const auto now = fml::TimePoint::Now();

for (const auto& test : tests_) {
auto delay = now - test.second;
FML_CHECK(delay < timeout_)
<< "Test " << test.first << " did not complete in "
<< timeout_.ToSeconds()
<< " seconds and is assumed to be hung. Killing the test harness.";
}
}

private:
using TestData = std::map<std::string, fml::TimePoint>;

fml::RefPtr<fml::TaskRunner> host_task_runner_;
TestData tests_;
const fml::TimeDelta timeout_;

PendingTests(fml::RefPtr<fml::TaskRunner> host_task_runner,
fml::TimeDelta timeout)
: host_task_runner_(std::move(host_task_runner)), timeout_(timeout) {}

FML_DISALLOW_COPY_AND_ASSIGN(PendingTests);
};

template <class T>
auto WeakPtr(std::shared_ptr<T> pointer) {
return std::weak_ptr<T>{pointer};
}

TestTimeoutListener::TestTimeoutListener(fml::TimeDelta timeout)
: timeout_(timeout),
listener_thread_("test_timeout_listener"),
listener_thread_runner_(listener_thread_.GetTaskRunner()),
pending_tests_(PendingTests::Create(listener_thread_runner_, timeout_)) {}

TestTimeoutListener::~TestTimeoutListener() {
listener_thread_runner_->PostTask(
[tests = std::move(pending_tests_)]() mutable { tests.reset(); });
FML_CHECK(pending_tests_ == nullptr);
}

static std::string GetTestNameFromTestInfo(
const ::testing::TestInfo& test_info) {
std::stringstream stream;
stream << test_info.test_suite_name();
stream << ".";
stream << test_info.name();
if (auto type_param = test_info.type_param()) {
stream << "/" << type_param;
}
if (auto value_param = test_info.value_param()) {
stream << "/" << value_param;
}
return stream.str();
}

// |testing::EmptyTestEventListener|
void TestTimeoutListener::OnTestStart(const ::testing::TestInfo& test_info) {
listener_thread_runner_->PostTask([weak_tests = WeakPtr(pending_tests_),
name = GetTestNameFromTestInfo(test_info),
now = fml::TimePoint::Now()]() {
if (auto tests = weak_tests.lock()) {
tests->OnTestBegin(std::move(name), now);
}
});
}

// |testing::EmptyTestEventListener|
void TestTimeoutListener::OnTestEnd(const ::testing::TestInfo& test_info) {
listener_thread_runner_->PostTask(
[weak_tests = WeakPtr(pending_tests_),
name = GetTestNameFromTestInfo(test_info)]() {
if (auto tests = weak_tests.lock()) {
tests->OnTestEnd(std::move(name));
}
});
}

} // namespace testing
} // namespace flutter
45 changes: 45 additions & 0 deletions testing/test_timeout_listener.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_TESTING_TEST_TIMEOUT_LISTENER_H_
#define FLUTTER_TESTING_TEST_TIMEOUT_LISTENER_H_

#include <memory>

#include "flutter/fml/macros.h"
#include "flutter/fml/task_runner.h"
#include "flutter/fml/thread.h"
#include "flutter/testing/testing.h"

namespace flutter {
namespace testing {

class PendingTests;

class TestTimeoutListener : public ::testing::EmptyTestEventListener {
public:
TestTimeoutListener(
fml::TimeDelta timeout = fml::TimeDelta::FromSeconds(30u));

~TestTimeoutListener();

private:
const fml::TimeDelta timeout_;
fml::Thread listener_thread_;
fml::RefPtr<fml::TaskRunner> listener_thread_runner_;
std::shared_ptr<PendingTests> pending_tests_;

// |testing::EmptyTestEventListener|
void OnTestStart(const ::testing::TestInfo& test_info) override;

// |testing::EmptyTestEventListener|
void OnTestEnd(const ::testing::TestInfo& test_info) override;

FML_DISALLOW_COPY_AND_ASSIGN(TestTimeoutListener);
};

} // namespace testing
} // namespace flutter

#endif // FLUTTER_TESTING_TEST_TIMEOUT_LISTENER_H_

0 comments on commit 7ca44d3

Please sign in to comment.