Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/boost/capy/ex/thread_pool.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ class thread_pool::executor_type
@throws std::system_error If this call posts and must start
the pool's worker threads, and a thread cannot be started.
`c` is not queued.
Threads that did start keep running, and a later call
starts only the missing ones.
*/
BOOST_CAPY_DECL
std::coroutine_handle<>
Expand Down Expand Up @@ -293,6 +295,8 @@ class thread_pool::executor_type
@throws std::system_error If this call must start the pool's
worker threads, and a thread cannot be started. `c` is not
queued.
Threads that did start keep running, and a later call
starts only the missing ones.
*/
BOOST_CAPY_DECL
void
Expand Down
4 changes: 3 additions & 1 deletion src/ex/thread_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,10 @@ class thread_pool::impl
std::lock_guard<std::mutex> lock(mutex_);
if(started_.load(std::memory_order_relaxed) || stop_)
return;
// Start at the threads already running: if an earlier call
// threw partway, this one adds only the missing ones.
threads_.reserve(num_threads_);
for(std::size_t i = 0; i < num_threads_; ++i)
for(std::size_t i = threads_.size(); i < num_threads_; ++i)
threads_.emplace_back([this, i]{ run(i); });
started_.store(true, std::memory_order_release);
}
Expand Down
5 changes: 5 additions & 0 deletions test/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#

file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp)

# The fault harness shadows libc symbols; it links into its own target.
list(FILTER PFILES EXCLUDE REGEX "/fault/")
list(APPEND PFILES
CMakeLists.txt
Jamfile)
Expand All @@ -32,3 +35,5 @@ boost_capy_test_suite_discover_tests(boost_capy_tests)

# Add the main test target to Boost's test suite
add_dependencies(tests boost_capy_tests)

add_subdirectory(fault)
6 changes: 5 additions & 1 deletion test/unit/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ project
<target-os>darwin,<undefined-sanitizer>norecover:<link>static
;

for local f in [ glob-tree-ex . : *.cpp : file*.cpp ]
# `fault` excludes the whole directory: its shadows replace libc
# functions, so it builds its own executables (see fault/Jamfile).
for local f in [ glob-tree-ex . : *.cpp : file*.cpp fault ]
{
local parts = [ SPLIT_BY_CHARACTERS $(f:S=) : / ] ;
local name = $(parts:J=_) ;
Expand Down Expand Up @@ -57,3 +59,5 @@ for local f in [ glob-tree-ex . : file*.cpp ]
<warnings>off <undefined-sanitizer>norecover:<link>static
;
}

build-project fault ;
38 changes: 38 additions & 0 deletions test/unit/fault/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# Copyright (c) 2026 Michael Vandeberg
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
# Official repository: https://github.com/cppalliance/capy
#

# The shadows replace libc functions for the whole program, so they
# link into their own executable. They are written against glibc;
# other platforms create threads differently and have no coverage here.
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
return()
endif()

file(GLOB FAULT_FILES CONFIGURE_DEPENDS *.cpp *.hpp)
list(APPEND FAULT_FILES CMakeLists.txt Jamfile)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${FAULT_FILES})

add_executable(boost_capy_fault_tests ${FAULT_FILES})
target_link_libraries(boost_capy_fault_tests PRIVATE
Boost::capy_test_suite_main
Boost::capy
${CMAKE_DL_LIBS})

# Interposition only works if the shadows land in the executable's
# dynamic symbol table. A superproject that builds everything with
# hidden visibility would strip them, and the readback then refuses
# to run rather than reporting faults that never fire.
set_target_properties(boost_capy_fault_tests PROPERTIES
C_VISIBILITY_PRESET default
CXX_VISIBILITY_PRESET default
VISIBILITY_INLINES_HIDDEN OFF
ENABLE_EXPORTS ON)

boost_capy_test_suite_discover_tests(boost_capy_fault_tests)
add_dependencies(tests boost_capy_fault_tests)
36 changes: 36 additions & 0 deletions test/unit/fault/Jamfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#
# Copyright (c) 2026 Michael Vandeberg
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
# Official repository: https://github.com/cppalliance/capy
#

import ac ;
import os ;
import testing ;

project
: requirements
$(c20-requires)
<library>/boost/capy//boost_capy
<source>../../../extra/test_suite/test_main.cpp
<source>../../../extra/test_suite/test_suite.cpp
<include>../../../extra/test_suite
<warnings>extra
<warnings-as-errors>on
<target-os>linux:<linkflags>-ldl
# Interposition only works if the shadows land in the executable's
# dynamic symbol table; Boost builds tests with hidden visibility.
# <local-visibility> so the flag stays on these targets instead of
# propagating into a private rebuild of capy.
<local-visibility>global
;

# The shadows are written against glibc, so nothing is built anywhere
# else; the CMake side gates the same way.
if [ os.name ] = LINUX
{
run thread_pool_faults.cpp fault_posix.cpp ;
}
88 changes: 88 additions & 0 deletions test/unit/fault/fault.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/capy
//

#ifndef BOOST_CAPY_TEST_FAULT_HPP
#define BOOST_CAPY_TEST_FAULT_HPP

/* A cut-down copy of Corosio's fault harness (test/unit/fault there):
the same fault_scope interface, with only the entry points Capy's
tests need and one arm per thread.
*/

namespace boost {
namespace capy {
namespace test {
namespace fault {

/** OS entry points the harness can fail.

Names equal the libc symbol.
*/
enum class sys
{
pthread_create
};

/** Fail one OS call on the current thread.

While the scope is alive, the `nth` call to `which` made on this
thread returns its documented failure with error `err`. Calls
before and after it forward to the real function, and other
threads never observe the fault. An `nth` that is never reached
leaves an arm that only counts calls.

@par Preconditions
No other `fault_scope` is alive on this thread; a second one
aborts. The scope is destroyed on the thread that created it.

@par Example
@code
fault_scope f(sys::pthread_create, EAGAIN);
std::thread t([]{}); // throws std::system_error
@endcode
*/
class fault_scope
{
public:
/// Arm `which` to fail with `err` on its `nth` call.
fault_scope(sys which, int err, unsigned nth = 1);

/// Disarm, even if the fault never fired.
~fault_scope();

/// Return true once the armed call has been failed.
bool fired() const noexcept;

/** Return how many calls to the armed symbol this arm has seen.

@return Calls seen since the scope was constructed, including
the one that fired.
*/
unsigned count() const noexcept;

fault_scope(fault_scope const&) = delete;
fault_scope& operator=(fault_scope const&) = delete;
};

/** Return true if a call to `which` reaches the harness.

The harness checks at startup that the dynamic loader binds the
symbol to its replacement. A test asks here rather than asserting
on an arm that can never fire.

@param which The entry point to ask about.
*/
bool hook_is_live(sys which) noexcept;

} // fault
} // test
} // capy
} // boost

#endif
Loading
Loading