Skip to content
Draft
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
1 change: 1 addition & 0 deletions score/mw/com/test/common_test_resources/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ cc_library(
srcs = ["general_resources.cpp"],
hdrs = ["general_resources.h"],
features = COMPILER_WARNING_FEATURES,
visibility = ["//score/mw/com/test:__subpackages__"],
deps = [
":check_point_control",
":child_process_guard",
Expand Down
50 changes: 50 additions & 0 deletions score/mw/com/test/inotify_stress_test/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# *******************************************************************************
# Copyright (c) 2026 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************

load("@rules_cc//cc:defs.bzl", "cc_binary")
load("@score_baselibs//score/language/safecpp:toolchain_features.bzl", "COMPILER_WARNING_FEATURES")
load("//score/mw/com/test:pkg_application.bzl", "pkg_application")

cc_binary(
name = "inotify_stress_test",
srcs = [
"controller.cpp",
"controller.h",
"inotify_stress_test.cpp",
"inotify_stress_test_internal.h",
"worker.cpp",
"worker.h",
],
features = COMPILER_WARNING_FEATURES + [
"aborts_upon_exception",
],
visibility = ["//score/mw/com/test/inotify_stress_test:__subpackages__"],
deps = [
"//score/mw/com/test/common_test_resources:check_point_control",
"//score/mw/com/test/common_test_resources:general_resources",
"@boost.program_options",
"@score_baselibs//score/filesystem",
"@score_baselibs//score/language/futurecpp",
"@score_baselibs//score/mw/log",
"@score_baselibs//score/os/utils/inotify:inotify_instance_impl",
],
)

pkg_application(
name = "inotify-stress-test-pkg",
app_name = "InotifyStressTestApp",
bin = [":inotify_stress_test"],
visibility = [
"//score/mw/com/test/inotify_stress_test:__subpackages__",
],
)
63 changes: 63 additions & 0 deletions score/mw/com/test/inotify_stress_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# inotify Stress Test

Stress test that exercises the `os::InotifyInstanceImpl` inotify wrapper under heavy
concurrent load from multiple processes.

## What it does

A controller process spawns `M` worker processes. Each worker owns a dedicated
sub-directory and file under a shared base folder and is optionally assigned a unique
UID/GID. The controller and workers synchronise through `CheckPointControl` objects held
in shared memory, so every cycle is driven in lock-step.

On each of the `N` cycles a worker:

1. Creates its process directory (skipped when it already exists with the correct
access rights).
2. Creates its test file (skipped when it already exists with the correct access rights).
3. Adds an inotify watch on the shared base folder
(`kInCreate | kInDelete`).
4. Removes the watch immediately.
5. Reports the cycle as reached via `CheckPointReached`.

The controller signals each worker to start a cycle (`ProceedToNextCheckpoint`), waits for
every worker to report success or error (`WaitAndVerifyCheckPoint`), removes the per-process
directories so the next cycle re-exercises the creation path, and after all cycles sends
`FinishActions` so workers exit cleanly. Any worker failure fails the whole test.

## Arguments

| Argument | Default | Description |
| ----------------- | ------- | ------------------------------------------------------------------ |
| `--num-processes` | 5 | Number of worker processes to spawn. |
| `--cycles` | 100 | Number of stress cycles before terminating. |
| `--base-uid` | 0 | Worker `N` calls `setuid(base-uid + N)`; `0` skips `setuid`. |
| `--base-gid` | 0 | Worker `N` calls `setgid(base-gid + N)`; `0` skips `setgid`. |

When `--base-uid` / `--base-gid` are non-zero the setuid/setgid must succeed, so the binary
has to run as root (e.g. inside the Docker integration-test container). Local non-root runs
should leave these at their default `0`.

## Layout

| File | Responsibility |
| -------------------------------- | ----------------------------------------------------------- |
| `inotify_stress_test.cpp` | Entry point: argument parsing, setup, fork orchestration. |
| `worker.{h,cpp}` | Per-worker cycle loop and UID/GID credential switching. |
| `controller.{h,cpp}` | Controller loop: signals workers, verifies checkpoints. |
| `inotify_stress_test_internal.h` | Shared constants and path helpers. |
| `integration_test/` | pytest wrapper that runs the binary in the test container. |

## Running

Build and run the binary directly:

```sh
bazel run //score/mw/com/test/inotify_stress_test:inotify_stress_test -- --num-processes 10 --cycles 100
```

Run the integration test (10 processes, 300 cycles, distinct UIDs/GIDs from 2000):

```sh
bazel test //score/mw/com/test/inotify_stress_test/integration_test:test_inotify_stress_test
```
91 changes: 91 additions & 0 deletions score/mw/com/test/inotify_stress_test/controller.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/

#include "score/mw/com/test/inotify_stress_test/controller.h"

#include "score/filesystem/details/standard_filesystem.h"
#include "score/mw/com/test/common_test_resources/check_point_control.h"
#include "score/mw/com/test/inotify_stress_test/inotify_stress_test_internal.h"

#include <score/stop_token.hpp>
#include <unistd.h>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>

namespace score::mw::com::test
{

bool RunController(std::vector<CheckPointControl*>& checkpoint_controls,
const std::size_t num_processes,
const std::size_t cycles,
const score::cpp::stop_token& stop_token)
{
const auto pid{getpid()};
bool test_passed{true};

for (std::size_t i{0U}; i < cycles; ++i)
{
// Signal all workers to begin this cycle
for (std::size_t j{0U}; j < checkpoint_controls.size(); ++j)
{
std::cerr << pid << ": Signalling worker " << j << " to start (cycle " << i << ")" << std::endl;
checkpoint_controls[j]->ProceedToNextCheckpoint();
}

// Wait for every worker to report checkpoint or error — collect all before deciding to abort
for (std::size_t j{0U}; j < checkpoint_controls.size(); ++j)
{
const std::string tag{"[cycle=" + std::to_string(i) + " worker=" + std::to_string(j) + "] "};
const int result =
WaitAndVerifyCheckPoint(tag,
*checkpoint_controls[j],
kCycleDoneCheckpoint,
stop_token,
std::chrono::duration_cast<std::chrono::milliseconds>(kCheckpointWaitDuration));
if (result != EXIT_SUCCESS)
{
std::cerr << pid << ": Worker " << j << " failed at cycle " << i << std::endl;
test_passed = false;
}
}

// Remove the shared test directory so the next cycle exercises the creation path again
static_cast<void>(num_processes);
const score::filesystem::StandardFilesystem fs;
const std::string test_dir{TestDir()};
const auto remove = fs.RemoveAll(test_dir);
if (!remove.has_value())
{
// Non-fatal: directory may not have been created (all workers errored before mkdir)
std::cerr << pid << ": Remove " << test_dir << " info: " << remove.error() << std::endl;
}

if (!test_passed)
{
break;
}
}

// Unblock all workers regardless of outcome so they exit cleanly
for (auto* const cp : checkpoint_controls)
{
cp->FinishActions();
}

return test_passed;
}

} // namespace score::mw::com::test
44 changes: 44 additions & 0 deletions score/mw/com/test/inotify_stress_test/controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/

#ifndef SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H
#define SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H

#include "score/mw/com/test/common_test_resources/check_point_control.h"

#include <score/stop_token.hpp>

#include <cstddef>
#include <vector>

namespace score::mw::com::test
{

/// \brief Runs the controller loop for \p cycles iterations.
///
/// Each iteration:
/// 1. Signals every worker via ProceedToNextCheckpoint().
/// 2. Waits for every worker to report CheckPointReached or ErrorOccurred.
/// 3. Removes per-worker directories so the next cycle exercises the creation path.
///
/// Sends FinishActions() to all workers before returning regardless of outcome.
///
/// \return true if all cycles completed without any worker error; false otherwise.
bool RunController(std::vector<CheckPointControl*>& checkpoint_controls,
std::size_t num_processes,
std::size_t cycles,
const score::cpp::stop_token& stop_token);

} // namespace score::mw::com::test

#endif // SCORE_MW_COM_TEST_INOTIFY_STRESS_TEST_CONTROLLER_H
Loading
Loading