• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jbaldwin / libcoro / 15281032004

27 May 2025 04:49PM UTC coverage: 87.696%. First build
15281032004

Pull #319

github

web-flow
Merge 9a0fe91d7 into 544f33ba3
Pull Request #319: Add coro::condition_variable

163 of 166 new or added lines in 5 files covered. (98.19%)

1625 of 1853 relevant lines covered (87.7%)

3773691.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.65
/include/coro/condition_variable.hpp
1
#pragma once
2

3
#include "coro/detail/task_self_deleting.hpp"
4
#include "coro/concepts/executor.hpp"
5
#include "coro/task.hpp"
6
#include "coro/event.hpp"
7
#include "coro/mutex.hpp"
8
#include "coro/when_any.hpp"
9

10
#include <atomic>
11
#include <chrono>
12
#include <condition_variable>
13
#include <functional>
14
#include <optional>
15

16
#ifdef LIBCORO_FEATURE_NETWORKING
17
#include <stop_token>
18
#endif
19

20
namespace coro
21
{
22

23
class condition_variable
24
{
25
public:
26
    using predicate_type = std::function<bool()>;
27

28
    enum notify_status_t
29
    {
30
        /// @brief The waiter is ready to be resumed, either the predicate passed or its been requested to stop.
31
        ready,
32
        /// @brief The waiter is not ready to be resumed
33
        not_ready,
34
        /// @brief The waiter is a hook and has timed out and is dead.
35
        awaiter_dead,
36
    };
37

38
    struct awaiter_base
39
    {
40
        awaiter_base(coro::condition_variable& cv, coro::scoped_lock& l);
41
        virtual ~awaiter_base() = default;
63✔
42

43
        awaiter_base(const awaiter_base&) = delete;
44
        awaiter_base(awaiter_base&&) = delete;
45
        auto operator=(const awaiter_base&) -> awaiter_base& = delete;
46
        auto operator=(awaiter_base&&) -> awaiter_base& = delete;
47

48
        /// @brief The next waiting awaiter.
49
        awaiter_base* m_next{nullptr};
50
        /// @brief The condition variable this waiter is waiting on.
51
        coro::condition_variable& m_condition_variable;
52
        /// @brief The lock that the wait() was called with.
53
        coro::scoped_lock& m_lock;
54
        /// @brief The coroutine to resume the waiter.
55
        std::coroutine_handle<> m_awaiting_coroutine{nullptr};
56

57
        /// @brief Each awaiter type defines its own notify behavior.
58
        /// @return The status of if the waiter's notify result.
59
        virtual auto on_notify() -> coro::task<notify_status_t> = 0;
60
    };
61

62
    struct awaiter : public awaiter_base
63
    {
64
        awaiter(coro::condition_variable& cv, coro::scoped_lock& l) noexcept;
65
        ~awaiter() override = default;
17✔
66

67
        awaiter(const awaiter&) = delete;
68
        awaiter(awaiter&&) = delete;
69
        auto operator=(const awaiter&) -> awaiter& = delete;
70
        auto operator=(awaiter&&) -> awaiter& = delete;
71

72
        auto await_ready() const noexcept -> bool;
73
        auto await_suspend(std::coroutine_handle<> awaiting_coroutine) noexcept -> bool;
74
        auto await_resume() noexcept {}
17✔
75

76
        auto on_notify() -> coro::task<notify_status_t> override;
77
    };
78

79
    struct awaiter_with_predicate : public awaiter_base
80
    {
81
        awaiter_with_predicate(
82
            coro::condition_variable& cv,
83
            coro::scoped_lock& l,
84
            predicate_type p
85
        ) noexcept;
86
        ~awaiter_with_predicate() override = default;
9✔
87

88
        awaiter_with_predicate(const awaiter_with_predicate&) = delete;
89
        awaiter_with_predicate(awaiter_with_predicate&&) = delete;
90
        auto operator=(const awaiter_with_predicate&) -> awaiter_with_predicate& = delete;
91
        auto operator=(awaiter_with_predicate&&) -> awaiter_with_predicate& = delete;
92

93
        auto await_ready() const noexcept -> bool;
94
        auto await_suspend(std::coroutine_handle<> awaiting_coroutine) noexcept -> bool;
95
        auto await_resume() noexcept {}
9✔
96

97
        auto on_notify() -> coro::task<notify_status_t> override;
98

99
        /// @brief The wait predicate to execute on notify.
100
        predicate_type m_predicate;
101
    };
102

103
#ifndef EMSCRIPTEN
104

105
    struct awaiter_with_predicate_stop_token : public awaiter_base
106
    {
107
        awaiter_with_predicate_stop_token(
108
            coro::condition_variable& cv,
109
            coro::scoped_lock& l,
110
            predicate_type p,
111
            std::stop_token stop_token
112
        ) noexcept;
113
        ~awaiter_with_predicate_stop_token() override = default;
1✔
114

115
        awaiter_with_predicate_stop_token(const awaiter_with_predicate_stop_token&) = delete;
116
        awaiter_with_predicate_stop_token(awaiter_with_predicate_stop_token&&) = delete;
117
        auto operator=(const awaiter_with_predicate_stop_token&) -> awaiter_with_predicate_stop_token& = delete;
118
        auto operator=(awaiter_with_predicate_stop_token&&) -> awaiter_with_predicate_stop_token& = delete;
119

120
        auto await_ready() noexcept -> bool;
121
        auto await_suspend(std::coroutine_handle<> awaiting_coroutine) noexcept -> bool;
122
        auto await_resume() noexcept -> bool { return m_predicate_result; }
1✔
123

124
        auto on_notify() -> coro::task<notify_status_t> override;
125

126
        /// @brief The wait predicate to execute on notify.
127
        predicate_type m_predicate;
128
        /// @brief The stop token that will guarantee the next notify will wake the awaiter regardless of the predicate.
129
        std::stop_token m_stop_token;
130
        /// @brief The last predicate's call result.
131
        bool m_predicate_result{false};
132
    };
133

134
#endif
135

136
#ifdef LIBCORO_FEATURE_NETWORKING
137

138
    /// @brief This structure encapsulates the data from the controller task.
139
    struct controller_data
140
    {
141
        controller_data(
142
            std::optional<std::cv_status>& status,
143
            bool& predicate_result,
144
            std::optional<predicate_type> predicate,
145
            std::optional<const std::stop_token> stop_token
146
        ) noexcept;
147
        ~controller_data() = default;
18✔
148

149
        controller_data(const controller_data&) = delete;
150
        controller_data(controller_data&&) = delete;
151
        auto operator=(const controller_data&) -> controller_data& = delete;
152
        auto operator=(controller_data&&) -> controller_data& = delete;
153

154
        /// @brief Mutex for notify or timeout mutual exclusion.
155
        coro::mutex m_event_mutex{};
156
        /// @brief Event to notify the no timeout task.
157
        coro::event m_notify_callback{};
158
        /// @brief Flag to notify if the awaiter has completed via no_timeout or timeout.
159
        std::atomic<bool> m_awaiter_completed{false};
160
        /// @brief The status of the wait() call, this is _ONLY_ valid if the m_awaiter_completed flag was obtained, otherwise its a dangling reference.
161
        std::optional<std::cv_status>& m_status;
162
        /// @brief The last result of the predicate call.
163
        bool& m_predicate_result;
164
        /// @brief The predicate, this can be no predicate, default predicate or stop token predicate.
165
        std::optional<predicate_type> m_predicate{std::nullopt};
166
        /// @brief The stop token.
167
        std::optional<const std::stop_token> m_stop_token{std::nullopt};
168
    };
169

170
    /**
171
     * @brief This awaiter is a facade to hook in between the wait_[for|unitl]() caller and the actual awaiter object.
172
     * This allows the hook to either call the no_timeout or timeout results correctly based on which one completes first.
173
     *
174
     * This class mimics the awaiter_base class so it can silently sit in the coro:condition_variable.m_awaiters to look
175
     * like its the actual awaiter, but just proxies to the real awaiter when appropriate.
176
     *
177
     * If the wait_[for|until] does timeout, then this will do nothing except delete itself from the list on a notify_[one|all] call.
178
     */
179
    struct awaiter_with_wait_hook : public awaiter_base
180
    {
181
        awaiter_with_wait_hook(
182
            coro::condition_variable& cv,
183
            coro::scoped_lock& l,
184
            controller_data& data
185
        ) noexcept;
186
        ~awaiter_with_wait_hook() override = default;
18✔
187

188
        auto on_notify() -> coro::task<notify_status_t> override;
189

190
        controller_data& m_data;
191
    };
192

193
    template<concepts::io_executor io_executor_type, typename return_type>
194
    struct awaiter_with_wait : public awaiter_base
195
    {
196
        awaiter_with_wait(
18✔
197
            std::shared_ptr<io_executor_type> executor,
198
            coro::condition_variable& cv,
199
            coro::scoped_lock& l,
200
            const std::chrono::nanoseconds wait_for,
201
            std::optional<predicate_type> predicate = std::nullopt,
202
            std::optional<std::stop_token> stop_token = std::nullopt
203
        ) noexcept
204
            : awaiter_base(cv, l),
205
              m_executor(std::move(executor)),
18✔
206
              m_wait_for(wait_for),
18✔
207
              m_predicate(std::move(predicate)),
18✔
208
              m_stop_token(std::move(stop_token))
54✔
209
        { }
18✔
210
        ~awaiter_with_wait() override = default;
18✔
211

212
        awaiter_with_wait(const awaiter_with_wait&) = delete;
213
        awaiter_with_wait(awaiter_with_wait&&) = delete;
214
        auto operator=(const awaiter_with_wait&) -> awaiter_with_wait& = delete;
215
        auto operator=(awaiter_with_wait&&) -> awaiter_with_wait& = delete;
216

217
        /**
218
         * @brief Task to handle the no_timeout case, however it is resumed even after a timeout since it needs to exit for the controller task.
219
         *
220
         * @param data The controller task's data.
221
         * @return coro::task<void>
222
         */
223
        auto make_on_notify_callback_task(controller_data& data) -> coro::task<void>
18✔
224
        {
225
            co_await data.m_notify_callback;
226

227
            // If this is the condition and not a timeout resume from this task.
228
            if (m_status.value() == std::cv_status::no_timeout)
229
            {
230
                // The condition coroutine is resumed from here instead of the awaiter hook task to
231
                // guarantee the controller is still alive until the caller's coroutine is completed.
232
                m_awaiting_coroutine.resume();
233
            }
234

235
            // This was a timeout, do nothing but exit to let the controller task know it can safely exit now.
236
            co_return;
237
        }
36✔
238

239
        /**
240
         * @brief Task to handle the timeout case, this will always wait the duration of the timeout before exiting.
241
         *
242
         * @param data The controller task data.
243
         * @return coro::task<void>
244
         */
245
        auto make_timeout_task(controller_data& data) -> coro::task<void>
18✔
246
        {
247
            co_await m_executor->schedule_after(m_wait_for);
248
            auto lock = co_await data.m_event_mutex.scoped_lock();
249
            bool expected{false};
250
            if (data.m_awaiter_completed.compare_exchange_strong(expected, true, std::memory_order::release, std::memory_order::relaxed))
251
            {
252
                m_status = {std::cv_status::timeout};
253
                lock.unlock();
254

255
                // This means the timeout has occurred first. Before resuming the wait_[for|until]() caller the lock must be re-acquired.
256
                co_await m_lock.m_mutex->lock();
257
                m_predicate_result = data.m_predicate.has_value() ? data.m_predicate.value()() : true;
258
                m_awaiting_coroutine.resume();
259
                co_return;
260
            }
261

262
            // The no_timeout has occurred first, exit so the controller task can complete.
263
            co_return;
264
        }
36✔
265

266
        /**
267
         * @brief Task to manage the no_timeout and timeout tasks, this holds the state for both tasks since they need to reference the actual
268
         *        wait_[for|until]() awaiter, but need to do so safely while it is still alive. To access the true awaiter the awaiter_completed
269
         *        atomic bool must be acquired, if it is not acquired the calling awaiter is invalid since it has already been resumed with the first
270
         *        event of timeout or no_timeout.
271
         *
272
         * @return coro::detail::task_self_deleting This task is self deleting since it has an indeterminate lifetime.
273
         */
274
        auto make_controller_task() -> coro::detail::task_self_deleting
18✔
275
        {
276
            controller_data data{m_status, m_predicate_result, std::move(m_predicate), std::move(m_stop_token)};
277
            // We enqueue the hook_task since we can make it live until the notify occurs and will properly resume the actual coroutine only once.
278
            awaiter_with_wait_hook hook_task{m_condition_variable, m_lock, data};
279
            m_condition_variable.enqueue_waiter(&hook_task);
280
            m_lock.m_mutex->unlock(); // Unlock the actual lock now that we are setup, not the fake hook task.
281

282
            co_await coro::when_all(make_on_notify_callback_task(data), make_timeout_task(data));
283
            co_return;
284
        }
36✔
285

286
        auto await_ready() noexcept -> bool
18✔
287
        {
288
            // If there is no predicate then we are not ready.
289
            if (!m_predicate.has_value())
18✔
290
            {
291
                return false;
10✔
292
            }
293

294
            m_predicate_result = m_predicate.value()();
8✔
295
            if (m_predicate_result && m_stop_token.has_value() && m_stop_token.value().stop_requested())
8✔
296
            {
NEW
297
                m_status = std::cv_status::no_timeout;
×
298
            }
299
            return m_predicate_result;
8✔
300
        }
301

302
        auto await_suspend(std::coroutine_handle<> awaiting_coroutine) noexcept -> bool
18✔
303
        {
304
            m_awaiting_coroutine = awaiting_coroutine; // This is the real coroutine to resume.
18✔
305

306
            // Make the background controller which proxies between the notify task and the timeout task.
307
            auto controller = make_controller_task();
18✔
308
            controller.resume();
18✔
309
            return true;
36✔
310
        }
18✔
311

312
        auto await_resume() noexcept -> return_type
18✔
313
        {
314
            if constexpr (std::is_same_v<return_type, bool>)
315
            {
316
                return m_predicate_result;
8✔
317
            }
318
            else
319
            {
320
                return m_status.value();
10✔
321
            }
322
        }
323

NEW
324
        auto on_notify() -> coro::task<notify_status_t> override
×
325
        {
NEW
326
            throw std::runtime_error("should never be called");
×
327
        }
328

329
        /// @brief The io_executor used to wait for the timeout.
330
        std::shared_ptr<io_executor_type> m_executor{nullptr};
331
        /// @brief The amount of time to wait for before timing out.
332
        const std::chrono::nanoseconds m_wait_for;
333
        /// @brief If the condition timed out or not.
334
        std::optional<std::cv_status> m_status{std::nullopt};
335
        /// @brief The last m_predicate() call result.
336
        bool m_predicate_result{false};
337
        /// @brief The predicate, this can be no predicate, default predicate or stop token predicate. This value is only valid until the awaiter data object takes ownership.
338
        std::optional<predicate_type> m_predicate{std::nullopt};
339
        /// @brief The stop token. This value is only valid until the awater data object takes onwership.
340
        std::optional<const std::stop_token> m_stop_token{std::nullopt};
341
    };
342

343
#endif
344

345
    condition_variable() = default;
346
    ~condition_variable() = default;
347

348
    condition_variable(const condition_variable&) = delete;
349
    condition_variable(condition_variable&&) = delete;
350
    auto operator=(const condition_variable&) -> condition_variable& = delete;
351
    auto operator=(condition_variable&&) -> condition_variable& = delete;
352

353
    /**
354
     * @brief Notifies a single waiter.
355
     */
356
    auto notify_one() -> coro::task<void>;
357

358
    /**
359
     * @brief Notifies all waiters.
360
     */
361
    auto notify_all() -> coro::task<void>;
362

363
#ifdef LIBCORO_FEATURE_NETWORKING
364
    /**
365
     * @brief Notifies all waiters and resumes them on the given executor. Note that each waiter must be notified synchronously so
366
     *        this is useful if the task is long lived and can be immediately parallelized after the condition is ready. This does not
367
     *        need to be co_await'ed like `notify_all()` since this will execute the notify on the given executor.
368
     *
369
     * @tparam executor_type The type of executor that the waiters will be resumed on.
370
     * @param executor The executor that each waiter will be resumed on.
371
     * @return void
372
     */
373
    template<coro::concepts::io_executor executor_type>
374
    auto notify_all(std::shared_ptr<executor_type> executor) -> void
1✔
375
    {
376
        auto* waiter = dequeue_waiter_all();
1✔
377
        awaiter_base* next;
378

379
        std::vector<coro::task<void>> notify_tasks{};
1✔
380

381
        while (waiter != nullptr)
11✔
382
        {
383
            // Need to grab next before notifying since the notifier will self destruct after completing.
384
            next = waiter->m_next;
10✔
385
            // This will kick off each task in parallel on the scheduler, they will fight over the lock
386
            // but will give the best parallelism scheduling them immediately.
387
            executor->spawn(make_notify_all_executor_individual_task(waiter));
10✔
388
            waiter = next;
10✔
389
        }
390

391
        return;
2✔
392
    }
1✔
393
#endif
394

395
    /**
396
     * @brief Waits until notified.
397
     *
398
     * @param lock A lock that must be locked by the caller.
399
     * @return awaiter
400
     */
401
    [[nodiscard]] auto wait(
402
        coro::scoped_lock& lock
403
    ) -> awaiter;
404

405
    /**
406
     * @brief Waits until notified but only wakes up if the predicate passes.
407
     *
408
     * @param lock A lock that must be locked by the caller.
409
     * @param predicate The predicate to check whether the waiting can be completed.
410
     * @return awaiter_with_predicate
411
     */
412
    [[nodiscard]] auto wait(
413
        coro::scoped_lock& lock,
414
        predicate_type predicate
415
    ) -> awaiter_with_predicate;
416

417
#ifndef EMSCRIPTEN
418
    /**
419
     * @brief Waits until notified and wakes up if a stop is requseted or the predicate passes.
420
     *
421
     * @param lock A lock which must be locked by the caller.
422
     * @param stop_token A stop token to register interruption for.
423
     * @param predicate The predicate to check whether the waiting can be completed.
424
     * @return awaiter_with_predicate_stop_token The final predicate call result.
425
     */
426
    [[nodiscard]] auto wait(
427
        coro::scoped_lock& lock,
428
        std::stop_token stop_token,
429
        predicate_type predicate
430
    ) -> awaiter_with_predicate_stop_token;
431
#endif
432

433
#ifdef LIBCORO_FEATURE_NETWORKING
434

435
    template<concepts::io_executor io_executor_type, class rep_type, class period_type>
436
    [[nodiscard]] auto wait_for(
8✔
437
        std::shared_ptr<io_executor_type> executor,
438
        coro::scoped_lock& lock,
439
        const std::chrono::duration<rep_type, period_type> wait_for
440
    ) -> awaiter_with_wait<io_executor_type, std::cv_status>
441
    {
442
        return awaiter_with_wait<io_executor_type, std::cv_status>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for)};
8✔
443
    }
444

445
    template<concepts::io_executor io_executor_type, class rep_type, class period_type>
446
    [[nodiscard]] auto wait_for(
3✔
447
        std::shared_ptr<io_executor_type> executor,
448
        coro::scoped_lock& lock,
449
        const std::chrono::duration<rep_type, period_type> wait_for,
450
        predicate_type predicate
451
    ) -> awaiter_with_wait<io_executor_type, bool>
452
    {
453
        return awaiter_with_wait<io_executor_type, bool>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for), std::move(predicate)};
3✔
454
    }
455

456
    template<concepts::io_executor io_executor_type, class rep_type, class period_type>
457
    [[nodiscard]] auto wait_for(
1✔
458
        std::shared_ptr<io_executor_type> executor,
459
        coro::scoped_lock& lock,
460
        std::stop_token stop_token,
461
        const std::chrono::duration<rep_type, period_type> wait_for,
462
        predicate_type predicate
463
    ) -> awaiter_with_wait<io_executor_type, bool>
464
    {
465
        return awaiter_with_wait<io_executor_type, bool>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for), std::move(predicate), std::move(stop_token)};
1✔
466
    }
467

468
    template<concepts::io_executor io_executor_type, class clock_type, class duration_type>
469
    auto wait_until(
2✔
470
        std::shared_ptr<io_executor_type> executor,
471
        coro::scoped_lock& lock,
472
        const std::chrono::time_point<clock_type, duration_type> wait_until_time
473
    ) -> awaiter_with_wait<io_executor_type, std::cv_status>
474
    {
475
        auto now = std::chrono::time_point<clock_type, duration_type>::clock::now();
2✔
476
        auto wait_for = (now < wait_until_time) ? (wait_until_time - now) : std::chrono::nanoseconds{1};
2✔
477
        return awaiter_with_wait<io_executor_type, std::cv_status>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for)};
2✔
478
    }
479

480
    template<concepts::io_executor io_executor_type, class clock_type, class duration_type>
481
    auto wait_until(
2✔
482
        std::shared_ptr<io_executor_type> executor,
483
        coro::scoped_lock& lock,
484
        const std::chrono::time_point<clock_type, duration_type> wait_until_time,
485
        predicate_type predicate
486
    ) -> awaiter_with_wait<io_executor_type, bool>
487
    {
488
        auto now = std::chrono::time_point<clock_type, duration_type>::clock::now();
2✔
489
        auto wait_for = (now < wait_until_time) ? (wait_until_time - now) : std::chrono::nanoseconds{1};
2✔
490
        return awaiter_with_wait<io_executor_type, bool>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for), std::move(predicate)};
2✔
491
    }
492

493
    template<concepts::io_executor io_executor_type, class clock_type, class duration_type>
494
    auto wait_until(
2✔
495
        std::shared_ptr<io_executor_type> executor,
496
        coro::scoped_lock& lock,
497
        std::stop_token stop_token,
498
        const std::chrono::time_point<clock_type, duration_type> wait_until_time,
499
        predicate_type predicate
500
    ) -> awaiter_with_wait<io_executor_type, bool>
501
    {
502
        auto now = std::chrono::time_point<clock_type, duration_type>::clock::now();
2✔
503
        auto wait_for = (now < wait_until_time) ? (wait_until_time - now) : std::chrono::nanoseconds{1};
2✔
504
        return awaiter_with_wait<io_executor_type, bool>{std::move(executor), *this, lock, std::chrono::duration_cast<std::chrono::nanoseconds>(wait_for), std::move(predicate), std::move(stop_token)};
2✔
505
    }
506
#endif
507

508
private:
509
    /// @brief The list of waiters.
510
    std::atomic<awaiter_base*> m_awaiters{nullptr};
511

512
#ifdef LIBCORO_FEATURE_NETWORKING
513
    auto make_notify_all_executor_individual_task(awaiter_base* waiter) -> coro::task<void>
10✔
514
    {
515
        switch (co_await waiter->on_notify())
516
        {
517
            case notify_status_t::not_ready:
518
                // Re-enqueue since the predicate isn't ready and return since the notify has been satisfied.
519
                enqueue_waiter(waiter);
520
                break;
521
            case notify_status_t::ready:
522
            case notify_status_t::awaiter_dead:
523
                // Don't re-enqueue any awaiters that are ready or dead.
524
                break;
525
        }
526
    }
20✔
527
#endif
528

529
    /**
530
     * @brief Enqueues a waiter at the head to be awoken by a notify.
531
     *
532
     * @param a The waiter to enqueue.
533
     */
534
    auto enqueue_waiter(awaiter_base* a) -> void;
535

536
    /**
537
     * @brief Dequeues a single waiter to try and awaken from a notify.
538
     *
539
     * @return awaiter_base* The awaiter to try and awaken.
540
     */
541
    auto dequeue_waiter() -> awaiter_base*;
542

543
    /**
544
     * @brief Dequeues the entire list of waiters to try and awaken all of them.
545
     *
546
     * @return awaiter_base* The list of awaiters to try and awaken.
547
     */
548
    auto dequeue_waiter_all() -> awaiter_base*;
549
};
550

551
} // namespace coro
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc