Browse Source

C++ cleanup and refactor

cpp17-init
chodak166 4 months ago
parent
commit
95a5a2ee04
  1. 254
      cpp17/.clang-tidy
  2. 142
      cpp17/README.md
  3. 55
      cpp17/TODO.md
  4. 45
      cpp17/doc/add-item-sequence.md
  5. 82
      cpp17/doc/architecture-overview.md
  6. 25
      cpp17/doc/request-sequence.md
  7. 4
      cpp17/docker/.dockerignore
  8. 3
      cpp17/docker/Dockerfile
  9. 4
      cpp17/docker/docker-compose.yml
  10. 2
      cpp17/lib/CMakeLists.txt
  11. 7
      cpp17/lib/include/autostore/AutoStore.h
  12. 13
      cpp17/lib/src/AutoStore.cpp
  13. 229
      cpp17/lib/src/application/services/TaskScheduler.cpp
  14. 51
      cpp17/lib/src/application/services/TaskScheduler.h
  15. 138
      cpp17/lib/src/infrastructure/services/TaskScheduler.cpp
  16. 168
      cpp17/tests/unit/TaskScheduler.test.cpp
  17. 3
      cpp17/vcpkg.json

254
cpp17/.clang-tidy

@ -0,0 +1,254 @@
---
Checks: >-
-*,
bugprone-assert-side-effect,
bugprone-bad-signal-to-kill-thread,
bugprone-bool-pointer-implicit-conversion,
bugprone-branch-clone,
bugprone-copy-constructor-init,
bugprone-dangling-handle,
bugprone-dynamic-static-initializers,
bugprone-exception-escape,
bugprone-forward-declaration-namespace,
bugprone-forwarding-reference-overload,
bugprone-inaccurate-erase,
bugprone-incorrect-roundings,
bugprone-infinite-loop,
bugprone-integer-division,
bugprone-macro-parentheses,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-misplaced-widening-cast,
bugprone-move-forwarding-reference,
bugprone-multiple-statement-macro,
bugprone-not-null-terminated-result,
bugprone-parent-virtual-call,
bugprone-posix-return,
bugprone-signed-char-misuse,
bugprone-sizeof-container,
bugprone-sizeof-expression,
bugprone-spuriously-wake-up-functions,
bugprone-string-constructor,
bugprone-string-integer-assignment,
bugprone-string-literal-with-embedded-nul,
bugprone-suspicious-enum-usage,
bugprone-suspicious-include,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-suspicious-string-compare,
bugprone-swapped-arguments,
bugprone-terminating-continue,
bugprone-throw-keyword-missing,
bugprone-too-small-loop-variable,
bugprone-undefined-memory-manipulation,
bugprone-undelegated-constructor,
bugprone-unhandled-self-assignment,
bugprone-unhandled-self-assignment,
bugprone-unused-raii,
bugprone-use-after-move,
cert-dcl21-cpp,
cert-dcl50-cpp,
cert-dcl58-cpp,
cert-env33-c,
cert-err34-c,
cert-err52-cpp,
cert-err58-cpp,
cert-err60-cpp,
cert-flp30-c,
cert-mem57-cpp,
cert-msc50-cpp,
cert-msc51-cpp,
cert-oop57-cpp,
cert-oop58-cpp,
clang-analyzer-core.CallAndMessage,
clang-analyzer-core.DivideZero,
clang-analyzer-core.*,
clang-analyzer-cplusplus.*,
clang-analyzer-deadcode.*,
clang-analyzer-nullability.*,
clang-analyzer-optin.*,
clang-analyzer-valist.*,
clang-analyzer-security.*,
cppcoreguidelines-avoid-goto,
cppcoreguidelines-avoid-non-const-global-variables,
cppcoreguidelines-init-variables,
cppcoreguidelines-interfaces-global-init,
cppcoreguidelines-macro-usage,
cppcoreguidelines-narrowing-conversions,
cppcoreguidelines-no-malloc,
cppcoreguidelines-owning-memory,
cppcoreguidelines-pro-bounds-array-to-pointer-decay,
cppcoreguidelines-pro-bounds-constant-array-index,
cppcoreguidelines-pro-bounds-pointer-arithmetic,
cppcoreguidelines-pro-type-const-cast,
cppcoreguidelines-pro-type-cstyle-cast,
cppcoreguidelines-pro-type-member-init,
cppcoreguidelines-pro-type-reinterpret-cast,
cppcoreguidelines-pro-type-static-cast-downcast,
cppcoreguidelines-pro-type-union-access,
cppcoreguidelines-pro-type-vararg,
cppcoreguidelines-slicing,
cppcoreguidelines-special-member-functions,
google-build-namespaces,
google-default-arguments,
google-explicit-constructor,
google-build-using-namespace,
google-global-names-in-headers,
google-readability-casting,
google-runtime-int,
google-runtime-operator,
hicpp-exception-baseclass,
hicpp-multiway-paths-covered,
hicpp-no-assembler,
hicpp-signed-bitwise,
llvm-namespace-comment,
misc-definitions-in-headers,
misc-misplaced-const,
misc-new-delete-overloads,
misc-no-recursion,
misc-non-copyable-objects,
misc-non-private-member-variables-in-classes,
misc-redundant-expression,
misc-static-assert,
misc-throw-by-value-catch-by-reference,
misc-unconventional-assign-operator,
misc-uniqueptr-reset-release,
misc-unused-parameters,
misc-unused-using-decls,
misc-unused-alias-decls,
modernize-avoid-bind,
modernize-avoid-c-arrays,
modernize-concat-nested-namespaces,
modernize-deprecated-headers,
modernize-deprecated-ios-base-aliases,
modernize-loop-convert,
modernize-make-shared,
modernize-make-unique,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-auto-ptr,
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-auto,
modernize-use-bool-literals,
modernize-use-default-member-init,
modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
modernize-use-nodiscard,
modernize-use-noexcept,
modernize-use-nullptr,
modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
modernize-use-using,
performance-faster-string-find,
performance-for-range-copy,
performance-implicit-conversion-in-loop,
performance-inefficient-algorithm,
performance-inefficient-string-concatenation,
performance-inefficient-vector-operation,
performance-move-const-arg,
performance-move-constructor-init,
performance-no-automatic-move,
performance-noexcept-move-constructor,
performance-trivially-destructible,
performance-type-promotion-in-math-fn,
performance-unnecessary-copy-initialization,
performance-unnecessary-value-param,
readability-avoid-const-params-in-decls,
readability-braces-around-statements,
readability-const-return-type,
readability-container-size-empty,
readability-delete-null-pointer,
readability-deleted-default,
readability-else-after-return,
readability-function-size,
readability-identifier-naming,
readability-implicit-bool-conversion,
readability-inconsistent-declaration-parameter-name,
readability-isolate-declaration,
readability-magic-numbers,
readability-make-member-function-const,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-named-parameter,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-redundant-function-ptr-dereference,
readability-redundant-member-init,
readability-redundant-preprocessor,
readability-redundant-smartptr-get,
readability-redundant-string-cstr,
readability-redundant-string-init,
readability-simplify-boolean-expr,
readability-simplify-subscript-expr,
readability-static-accessed-through-instance,
readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-uniqueptr-delete-release,
readability-uppercase-literal-suffix,
readability-use-anyofallof
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
AnalyzeTemporaryDtors: false
FormatStyle: none
CheckOptions:
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: 1
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: readability-function-size.LineThreshold
value: 50
- key: readability-function-size.StatementThreshold
value: 800
- key: readability-function-size.BranchThreshold
value: 10
- key: readability-function-size.ParameterThreshold
value: 6
- key: readability-function-size.NestingThreshold
value: 15
- key: readability-function-size.VariableThreshold
value: 10
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.ClassMemberCase
value: camelBack
- key: readability-identifier-naming.ClassMethodCase
value: camelBack
- key: readability-identifier-naming.MethodCase
value: camelBack
- key: readability-identifier-naming.ConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.LocalConstantCase
value: camelBack
- key: readability-identifier-naming.NamespaceCase
value: lower_case
- key: readability-identifier-naming.ParameterCase
value: camelBack
- key: readability-identifier-naming.EnumCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: camelBack
- key: misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: 1
...

142
cpp17/README.md

@ -1,130 +1,44 @@
# About this Repository # Ovierview
This repository hosts multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations. Read top-level `README.md` for more information on this repository.
Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology. # Authentication
Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison). No external service is used. JWT tokens are created and verified using `jwt-cpp` library.
Default, pre-defined user databse is a simple json file (`app/defaults/users.json`).
--- # Build and Run
### Project Idea: AutoStore ```bash
cd docker
A system to store items with expiration dates. When items expire, new ones are automatically ordered by making a POST request to the configured order URL. docker compose build
docker compose up
#### Business Rules (Domain) ```
1. **Each item has a name and an expiration date.**
2. **Expired items are automatically removed from the store.**
3. **When an item expires, a new item of the same type is automatically ordered.**
4. **Expired items can be added to the store, triggering immediate ordering.**
5. **Every item belongs to a user.**
#### Application Requirements
1. **Users can log in to obtain a JWT.**
2. **Authenticated users manage their personal collection of items via an HTTP API.**
3. **Each item has an associated "order URL".**
4. **When an item expires, the system must notify the "order URL" with an HTTP POST request.**
5. **This call should occur immediately when the item's expiration date is reached, or when an expired item is added.**
6. **Upon startup, the system must verify expiration dates for all items.**
7. **Persistent storage must be used (file, database, etc.).**
**Note:** For simplicity, user CRUD is skipped. Integrate with an OP (OpenID Provider) service like Keycloak, Authentic, or Zitadel, or mock authentication with a simple Docker service. Alternatively, simply authenticate a predefined user and return a JWT on login.
---
## Layer Boundaries
| Layer | Responsibility | Internal Dependencies | External Dependencies | Note: do not use this for development. See `.devcontainer` directory for development setup.
|-------------------|--------------------------------------------------------------- |----------------------|-----------------------|
| **Domain** | Entities, value objects, domain services (pure business logic) | None | None (language only) |
| **Application** | Use cases, orchestration, DTOs, infrastructure interfaces | Domain | None or minimal |
| **Infrastructure**| Implementations (repositories, HTTP, auth), background jobs | Application | Any (framework/lib) |
| **Presentation** | API controllers, DTOs, auth middleware | Application | UI/web/CLI/others |
| **Assembly** | Main app, DI, startup logic, job scheduling | Any layer | DI container, config, framework, etc.|
--- For non-container development, see Dockerfile and replicate the steps. Simple `build-and-test.sh`
script would look like this:
### Possible directory layout (will vary from tech to tech) ```bash
#/bin/bash
```plaintext OUT_DIR=./out
AutoStore/ DEBUG_DIR=$OUT_DIR/build/debug
├── App
│ ├── Main
│ ├── AppConfig
│ └── ...
├── Extern
│ ├── <jwt-lib, http-client, etc.>
│ └── <...downloaded libraries and git submodules>
├── Src
│ ├── Domain/
│ │ ├── Entities/
│ │ │ ├── User
│ │ │ └── Item
│ │ └── Services/
│ │ └── ExpirationPolicy
│ ├── Application/
│ │ ├── UseCases/
│ │ │ ├── RegisterUser
│ │ │ ├── LoginUser
│ │ │ ├── AddItem
│ │ │ ├── GetItem
│ │ │ ├── DeleteItem
│ │ │ └── HandleExpiredItems
│ │ ├── Interfaces/
│ │ │ ├── IUserRepository
│ │ │ ├── IItemRepository
│ │ │ ├── IAuthService
│ │ │ └── ITimeProvider
│ │ ├── Dto/
│ │ └── Services/
│ ├── Infrastructure/
│ │ ├── Repositories/
│ │ │ ├── FileUserRepository
│ │ │ └── FileItemRepository
│ │ ├── Adapters/
│ │ │ ├── JwtAuthAdapter
│ │ │ ├── OrderUrlHttpClient
│ │ │ ├── SystemClockImpl
│ │ │ └── <... some extern lib adapters>
│ │ └── Helpers/
│ │ └── <... DRY helpers>
│ └── WebApi/
│ ├── Controllers/
│ │ ├── StoreController
│ │ └── UserController
│ └── Auth/
│ └── JwtMiddleware
└── Tests
├── Unit/
└── Integration/
```
## Build and Run cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_EXPORT_COMPILE_COMMANDS=TRUE \
-DCMAKE_TOOLCHAIN_FILE=/opt/vcpkg/scripts/buildsystems/vcpkg.cmake \
-S /workspace -B $DEBUG_DIR
Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run: cd "$DEBUG_DIR"
cmake --build . -- -j8
ctest --output-on-failure .
```bash
docker compose up
``` ```
to build and run the application.
Otherwise, please provide a `<impl>/README.md` file with setup and running instructions.
## API Endpoints
See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.). # Testing
Here's a summary of example API endpoints:
| Endpoint | Method | Description | Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it.
|-------------------------|--------|--------------------------------------|
| `/login` | POST | Authenticate user and get JWT token |
| `/items` | GET | Get user's items |
| `/items` | POST | Create new item |
| `/items/{id}` | GET | Get item by ID |
| `/items/{id}` | PUT | Update item details |
| `/items/{id}` | DELETE | Delete item |
Suggested base URL is `http://localhost:8080/api/v1/`. Use top-level testing/tavern scripts to run functional tests.

55
cpp17/TODO.md

@ -1,55 +0,0 @@
# C++17 AutoStore Implementation Plan
This document outlines the steps to implement the C++17 version of the AutoStore application. Implemented classes should use `nxl::` namespace prefix.
## Phase 1: Project Scaffolding & Build System
- [x] Initialize a CMake project structure.
- [x] Set up the root `CMakeLists.txt` to manage the `app` and `lib` subdirectories.
- [x] Create the `lib` directory for the static library.
- [x] Create the `app` directory for the executable.
- [x] Configure `vcpkg` for dependency management and integrate it with CMake.
- [x] Add a dependency for an HTTP library (e.g., `cpp-httplib`) via `vcpkg`.
- [x] Add a dependency for a testing framework (e.g., `catch2`) via `vcpkg`.
## Phase 2: Library (`lib`) - Dummy Implementation
- [x] Create the directory structure for the library: `lib/src`, `lib/include`.
- [x] Create `lib/CMakeLists.txt` to build a static library.
- [x] In `lib/include/autostore`, define the public interface for the `App` to use.
- [x] Create a dummy `AutoStore` class in `lib/include/autostore/AutoStore.h` and a source file in `lib/src/AutoStore.cpp`.
- [ ] Define initial classes for core domain and application logic inside the library (e.g., `ItemRepository`, `UserService`, etc.) to establish the architecture. These will be mostly private to the library initially and implemented later.
- [ ] Ensure the project compiles and links successfully with the dummy implementations.
## Phase 3: Application (`app`) - Dummy Implementation
- [ ] Create the directory structure for the application: `app/src`.
- [x] Create `app/CMakeLists.txt` to build the executable.
- [ ] Link the `app` against the `lib` static library.
- [ ] Implement the main `App` class in `app/src/App.h` and `app/src/App.cpp`.
- [ ] The `App` class will have a constructor `App(int argc, char** argv)` and an `exec()` method.
- [ ] Implement signal handling (for `SIGINT`, `SIGTERM`) in the `App` class for graceful shutdown.
- [x] In `app/src/Main.cpp`, instantiate and run the `App` class.
- [ ] Ensure the project compiles and links successfully with the dummy implementations.
## Phase 4: Core Logic Implementation
- [ ] Implement the Domain layer in `lib/src/domain`.
- [ ] Implement the Application layer in `lib/src/application`.
- [ ] Implement the Infrastructure layer in `lib/src/infrastructure` (e.g., file-based persistence, HTTP client for ordering).
- [ ] Implement the Presentation layer (HTTP API) using the chosen HTTP library.
- [ ] Implement the startup logic to check for expired items.
- [ ] Implement a background mechanism (e.g., a thread) to periodically check for expired items.
## Phase 5: Testing
- [ ] Set up a `tests` directory.
- [ ] Create `tests/CMakeLists.txt` to build the test runner.
- [ ] Write unit tests for the Domain layer.
- [ ] Write unit tests for the Application layer, using mocks for infrastructure interfaces.
- [ ] Write integration tests for the Infrastructure layer.
## Phase 6: Containerization
- [x] Create a `Dockerfile` to build the C++ application in a container.
- [x] Create a `docker-compose.yml` file to easily build and run the application.

45
cpp17/doc/add-item-sequence.md

@ -2,32 +2,57 @@
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Client as HTTP Client
participant Middleware as HttpJwtMiddleware
participant Server as HttpServer
participant Controller as StoreController participant Controller as StoreController
participant UseCase as AddItem Use Case participant AuthService as IAuthService
participant UseCase as AddItem
participant Clock as ITimeProvider participant Clock as ITimeProvider
participant Policy as ExpirationPolicy participant Policy as ItemExpirationPolicy
participant OrderService as OrderingService participant OrderService as IOrderService
participant HttpClient as HttpClient participant HttpClient as HttpOrderService
participant Repo as IItemRepository participant Repo as IItemRepository
Client->>Server: POST /items with JWT
Server->>Middleware: Validate token
Middleware-->>Server: token valid
Server->>Controller: Forward request
Controller->>AuthService: Extract user ID from token
AuthService-->>Controller: User ID
Controller->>Controller: Parse request body to Item
Controller->>UseCase: execute(item) Controller->>UseCase: execute(item)
UseCase->>Clock: now() UseCase->>Clock: now()
Clock-->>UseCase: DateTime Clock-->>UseCase: current time
UseCase->>Policy: IsExpired(item, currentTime) UseCase->>Policy: isExpired(item, currentTime)
Policy-->>UseCase: boolean Policy-->>UseCase: boolean
alt Item is expired alt Item is expired
UseCase->>OrderService: orderItem(item) UseCase->>OrderService: orderItem(item)
OrderService->>HttpClient: POST to order URL OrderService->>HttpClient: POST to order URL
HttpClient-->>OrderService: Response HttpClient-->>OrderService: Response
OrderService-->>UseCase: OrderResult OrderService-->>UseCase: void
end end
UseCase->>Repo: save(item) UseCase->>Repo: save(item)
Repo->>Repo: Persist to storage Repo->>Repo: Persist to file storage
Repo-->>UseCase: Saved Item ID Repo-->>UseCase: Item ID
UseCase-->>Controller: Item ID
Controller->>Controller: Build success response
Controller-->>Client: 201 Created with Item ID
UseCase-->>Controller: Result (success/error) alt Error occurs
UseCase-->>Controller: Exception
Controller->>Controller: Build error response
Controller-->>Client: 4xx/5xx error
end
alt Authentication fails
Middleware-->>Client: 401 Unauthorized
end
``` ```

82
cpp17/doc/architecture-overview.md

@ -0,0 +1,82 @@
# AutoStore Architecture Overview
## Layer Boundaries
```mermaid
graph TB
subgraph PL[Presentation Layer]
A[StoreController]
B[AuthController]
end
subgraph AL[Application Layer]
E[AddItem Use Case]
F[DeleteItem Use Case]
G[LoginUser Use Case]
H[GetItem Use Case]
I[ListItems Use Case]
J[HandleExpiredItems Use Case]
K[TaskScheduler]
L[IItemRepository]
M[IAuthService]
N[IOrderService]
O[ITimeProvider]
P[IThreadManager]
end
subgraph DL[Domain Layer]
Q[Item]
R[User]
S[ItemExpirationPolicy]
end
subgraph IL[Infrastructure Layer]
C[HttpServer]
D[HttpJwtMiddleware]
T[FileItemRepository]
U[FileJwtAuthService]
V[HttpOrderService]
W[SystemTimeProvider]
X[SystemThreadManager]
Y[CvBlocker]
end
```
## Component Dependencies
```mermaid
graph LR
StoreController --> AddItem
StoreController --> DeleteItem
StoreController --> GetItem
StoreController --> ListItems
StoreController --> IAuthService
AuthController --> LoginUser
AddItem --> IItemRepository
AddItem --> ITimeProvider
AddItem --> IOrderService
AddItem --> ItemExpirationPolicy
DeleteItem --> IItemRepository
LoginUser --> IAuthService
GetItem --> IItemRepository
ListItems --> IItemRepository
HandleExpiredItems --> IItemRepository
HandleExpiredItems --> ITimeProvider
HandleExpiredItems --> IOrderService
HandleExpiredItems --> ItemExpirationPolicy
TaskScheduler --> ITimeProvider
TaskScheduler --> IThreadManager
IItemRepository --> Item
IAuthService --> User
IOrderService --> Item
ItemExpirationPolicy --> Item
ItemExpirationPolicy --> ITimeProvider

25
cpp17/doc/request-sequence.md

@ -1,25 +0,0 @@
# General request sequence with authentication
```mermaid
sequenceDiagram
participant Client as HTTP Client
participant Router as Request Router
participant Auth as JwtMiddleware
participant Controller as Controller
participant UseCase as Use Case
Client->>Router: POST /api/items (with JWT)
Router->>Auth: Forward request
alt Authentication successful
Auth->>Auth: Validate JWT
Auth->>Controller: Forward authenticated request
Controller->>Controller: Parse request body to DTO
Controller->>UseCase: execute()
UseCase-->>Controller: Result (success/error)
Controller->>Controller: Convert result to HTTP response
Controller-->>Client: HTTP Response (2xx)
else Authentication fails
Auth-->>Client: 401 Unauthorized
end
```

4
cpp17/docker/.dockerignore

@ -0,0 +1,4 @@
.devcontainer
.out
build
build-*

3
cpp17/docker/Dockerfile

@ -8,6 +8,7 @@ COPY .. .
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \
-H/workspace -B/workspace/build -G Ninja -H/workspace -B/workspace/build -G Ninja
RUN cmake --build /workspace/build --config Release --target all -j 6 -- RUN cmake --build /workspace/build --config Release --target all -j 8 --
RUN cd /workspace/build && ctest --output-on-failure .
CMD ["/workspace/build/bin/AutoStore"] CMD ["/workspace/build/bin/AutoStore"]

4
cpp17/docker/docker-compose.yml

@ -3,6 +3,8 @@ services:
app: app:
build: build:
context: .. context: ..
dockerfile: Docker/Dockerfile dockerfile: docker/Dockerfile
image: autostore-build-cpp-vcpkg-img image: autostore-build-cpp-vcpkg-img
container_name: autostore-build-cpp-vcpkg container_name: autostore-build-cpp-vcpkg
ports:
- 8080:8080

2
cpp17/lib/CMakeLists.txt

@ -26,7 +26,7 @@ add_library(${TARGET_NAME} STATIC
src/infrastructure/helpers/Jsend.cpp src/infrastructure/helpers/Jsend.cpp
src/infrastructure/helpers/JsonItem.cpp src/infrastructure/helpers/JsonItem.cpp
src/infrastructure/auth/FileJwtAuthService.cpp src/infrastructure/auth/FileJwtAuthService.cpp
src/infrastructure/services/TaskScheduler.cpp src/application/services/TaskScheduler.cpp
src/infrastructure/adapters/CvBlocker.cpp src/infrastructure/adapters/CvBlocker.cpp
src/infrastructure/adapters/SystemThreadManager.cpp src/infrastructure/adapters/SystemThreadManager.cpp
src/infrastructure/adapters/SystemTimeProvider.cpp src/infrastructure/adapters/SystemTimeProvider.cpp

7
cpp17/lib/include/autostore/AutoStore.h

@ -13,11 +13,12 @@ class IItemRepository;
class ITimeProvider; class ITimeProvider;
class IOrderService; class IOrderService;
class IAuthService; class IAuthService;
class IThreadManager;
class TaskScheduler;
} // namespace application } // namespace application
namespace infrastructure { namespace infrastructure {
class HttpServer; class HttpServer;
class TaskScheduler;
} // namespace infrastructure } // namespace infrastructure
namespace webapi { namespace webapi {
@ -52,13 +53,15 @@ private:
ILoggerPtr log; ILoggerPtr log;
std::unique_ptr<infrastructure::HttpServer> httpServer; std::unique_ptr<infrastructure::HttpServer> httpServer;
std::unique_ptr<infrastructure::TaskScheduler> taskScheduler; std::unique_ptr<application::TaskScheduler> taskScheduler;
std::unique_ptr<webapi::StoreController> storeController; std::unique_ptr<webapi::StoreController> storeController;
std::unique_ptr<webapi::AuthController> authController; std::unique_ptr<webapi::AuthController> authController;
std::unique_ptr<application::IItemRepository> itemRepository; std::unique_ptr<application::IItemRepository> itemRepository;
std::unique_ptr<application::ITimeProvider> clock; std::unique_ptr<application::ITimeProvider> clock;
std::unique_ptr<application::IOrderService> orderService; std::unique_ptr<application::IOrderService> orderService;
std::unique_ptr<application::IAuthService> authService; std::unique_ptr<application::IAuthService> authService;
std::unique_ptr<application::ITimeProvider> timeProvider;
std::unique_ptr<application::IThreadManager> threadManager;
}; };
} // namespace nxl::autostore } // namespace nxl::autostore

13
cpp17/lib/src/AutoStore.cpp

@ -6,7 +6,7 @@
#include "webapi/controllers/StoreController.h" #include "webapi/controllers/StoreController.h"
#include "webapi/controllers/AuthController.h" #include "webapi/controllers/AuthController.h"
#include "infrastructure/http/HttpServer.h" #include "infrastructure/http/HttpServer.h"
#include "infrastructure/services/TaskScheduler.h" #include "application/services/TaskScheduler.h"
#include "application/commands/HandleExpiredItems.h" #include "application/commands/HandleExpiredItems.h"
#include "infrastructure/adapters/SystemTimeProvider.h" #include "infrastructure/adapters/SystemTimeProvider.h"
#include "infrastructure/adapters/SystemThreadManager.h" #include "infrastructure/adapters/SystemThreadManager.h"
@ -18,6 +18,7 @@
namespace nxl::autostore { namespace nxl::autostore {
using namespace infrastructure; using namespace infrastructure;
using namespace application;
AutoStore::AutoStore(Config config, ILoggerPtr logger) AutoStore::AutoStore(Config config, ILoggerPtr logger)
: config{std::move(config)}, log{std::move(logger)} : config{std::move(config)}, log{std::move(logger)}
@ -50,13 +51,13 @@ bool AutoStore::initialize()
authService = std::make_unique<FileJwtAuthService>(usersDbPath); authService = std::make_unique<FileJwtAuthService>(usersDbPath);
// Initialize dependencies for task scheduler // Initialize dependencies for task scheduler
auto timeProvider = std::make_shared<SystemTimeProvider>(); timeProvider = std::make_unique<SystemTimeProvider>();
auto threadManager = std::make_shared<SystemThreadManager>(); threadManager = std::make_unique<SystemThreadManager>();
auto blocker = std::make_shared<CvBlocker>(); auto blocker = std::make_unique<CvBlocker>();
// Initialize task scheduler (for handling expired items) // Initialize task scheduler (for handling expired items)
taskScheduler = std::make_unique<TaskScheduler>(log, timeProvider, taskScheduler = std::make_unique<TaskScheduler>(
threadManager, blocker); log, *timeProvider, *threadManager, std::move(blocker));
// Initialize HTTP server // Initialize HTTP server
httpServer = std::make_unique<HttpServer>(log, *authService); httpServer = std::make_unique<HttpServer>(log, *authService);

229
cpp17/lib/src/application/services/TaskScheduler.cpp

@ -0,0 +1,229 @@
#include "TaskScheduler.h"
#include <stdexcept>
namespace nxl::autostore::application {
namespace {
using Clock = std::chrono::system_clock;
using TimePoint = Clock::time_point;
using Duration = Clock::duration;
using Hours = std::chrono::hours;
using Minutes = std::chrono::minutes;
using Seconds = std::chrono::seconds;
using Milliseconds = std::chrono::milliseconds;
using Days = std::chrono::duration<int, std::ratio<86400>>;
using RunMode = TaskScheduler::RunMode;
bool isValidTime(int hour, int minute, int second)
{
return (hour >= 0 && hour <= 23) && (minute >= 0 && minute <= 59)
&& (second >= 0 && second <= 59);
}
bool areModesMutuallyExclusive(RunMode mode)
{
return (static_cast<int>(mode) & static_cast<int>(RunMode::Forever))
&& (static_cast<int>(mode) & static_cast<int>(RunMode::Once));
}
TimePoint todayAt(uint8_t hour, uint8_t minute, uint8_t second,
const ITimeProvider& timeProvider)
{
auto now = timeProvider.now();
auto midnight =
std::chrono::time_point_cast<Duration>(std::chrono::floor<Days>(now));
auto offset = Hours{hour} + Minutes{minute} + Seconds{second};
return midnight + offset;
}
bool shouldExecuteOnStart(const TaskScheduler::ScheduledTask& task)
{
return (static_cast<int>(task.mode) & static_cast<int>(RunMode::OnStart))
&& !task.executed;
}
TimePoint calculateNextExecutionTime(const TaskScheduler::ScheduledTask& task,
const ITimeProvider& timeProvider,
TimePoint now)
{
auto taskTime = todayAt(task.hour, task.minute, task.second, timeProvider);
if (taskTime <= now) {
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
taskTime += Hours(24);
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& task.executed) {
return TimePoint{};
}
}
return taskTime;
}
bool shouldExecuteBasedOnTime(const TaskScheduler::ScheduledTask& task,
TimePoint now)
{
if (task.nextExecution == TimePoint{}) {
return false;
}
return task.nextExecution <= now;
}
void executeTask(TaskScheduler::ScheduledTask& task, ILogger& logger,
const ITimeProvider& timeProvider, TimePoint& nextWakeupTime)
{
try {
task.function();
task.executed = true;
logger.info("Task executed successfully");
if (static_cast<int>(task.mode) & static_cast<int>(RunMode::Forever)) {
auto nextTaskTime =
todayAt(task.hour, task.minute, task.second, timeProvider);
nextTaskTime += Hours(24);
task.nextExecution = nextTaskTime;
if (nextTaskTime < nextWakeupTime) {
nextWakeupTime = nextTaskTime;
}
}
} catch (const std::exception& e) {
logger.error("Task execution failed: %s", e.what());
}
}
void processTasks(std::vector<TaskScheduler::ScheduledTask>& tasks,
ILogger& logger, const ITimeProvider& timeProvider,
std::atomic<bool>& stopRequested, TimePoint now,
bool& hasOnStartTask, TimePoint& nextWakeupTime)
{
for (auto& task : tasks) {
if (stopRequested) {
break;
}
bool executeNow = false;
if (shouldExecuteOnStart(task)) {
executeNow = true;
hasOnStartTask = true;
} else if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
|| (static_cast<int>(task.mode)
& static_cast<int>(RunMode::Forever))) {
if (task.nextExecution == TimePoint{}) {
auto taskTime = calculateNextExecutionTime(task, timeProvider, now);
if ((static_cast<int>(task.mode) & static_cast<int>(RunMode::Once))
&& taskTime == TimePoint{} && !task.executed) {
executeNow = true;
} else if (taskTime != TimePoint{}) {
task.nextExecution = taskTime;
}
}
if (!executeNow && shouldExecuteBasedOnTime(task, now)) {
executeNow = true;
}
if (!executeNow && task.nextExecution < nextWakeupTime) {
nextWakeupTime = task.nextExecution;
}
}
if (executeNow) {
executeTask(task, logger, timeProvider, nextWakeupTime);
}
}
}
void waitForNextTask(IBlocker& blocker, std::atomic<bool>& stopRequested,
TimePoint now, TimePoint nextWakeupTime)
{
if (!stopRequested && nextWakeupTime > now) {
auto waitDuration =
std::chrono::duration_cast<Milliseconds>(nextWakeupTime - now);
auto maxWait = Minutes(1);
if (waitDuration > maxWait) {
waitDuration = maxWait;
}
blocker.blockFor(waitDuration);
}
}
} // namespace
TaskScheduler::TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
IThreadManager& threadManager,
std::unique_ptr<IBlocker> blocker)
: logger{std::move(logger)}, timeProvider{timeProvider},
threadManager{threadManager}, blocker{std::move(blocker)}
{}
void TaskScheduler::schedule(TaskFunction task, int hour, int minute,
int second, RunMode mode)
{
if (!isValidTime(hour, minute, second)) {
throw std::invalid_argument("Invalid time parameters");
}
if (areModesMutuallyExclusive(mode)) {
throw std::invalid_argument(
"Forever and Once modes are mutually exclusive");
}
std::lock_guard<std::mutex> lock(tasksMutex);
tasks.emplace_back(std::move(task), hour, minute, second, mode);
}
void TaskScheduler::start()
{
if (running) {
return;
}
running = true;
stopRequested = false;
threadHandle = threadManager.createThread([this]() {
logger->info("TaskScheduler thread started");
while (!stopRequested) {
auto now = timeProvider.now();
bool shouldExecuteOnStart = false;
auto nextWakeupTime = now + Hours(24);
{
std::lock_guard<std::mutex> lock(tasksMutex);
processTasks(tasks, *logger, timeProvider, stopRequested, now,
shouldExecuteOnStart, nextWakeupTime);
}
if (shouldExecuteOnStart) {
continue;
}
waitForNextTask(*blocker, stopRequested, now, nextWakeupTime);
}
running = false;
logger->info("TaskScheduler thread stopped");
});
}
void TaskScheduler::stop()
{
if (!running) {
return;
}
stopRequested = true;
blocker->notify();
if (threadHandle && threadHandle->joinable()) {
threadHandle->join();
}
}
} // namespace nxl::autostore::application

51
cpp17/lib/src/infrastructure/services/TaskScheduler.h → cpp17/lib/src/application/services/TaskScheduler.h

@ -12,17 +12,22 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
namespace nxl::autostore::infrastructure { namespace nxl::autostore::application {
class TaskScheduler class TaskScheduler
{ {
public: public:
/** enum class RunMode {
* @note Forever and Once are mutually exclusive OnStart = 1,
*/ Forever = 2,
enum class RunMode { OnStart = 1, Forever = 2, Once = 4 }; OnStartThenForever = 3, // OnStart | Forever
Once = 4,
OnStartThenOnce = 5 // OnStart | Once
};
using TaskFunction = std::function<void()>; using TaskFunction = std::function<void()>;
using TimePoint = application::ITimeProvider::Clock::time_point;
using ThreadHandle = application::IThreadManager::ThreadHandle;
struct ScheduledTask struct ScheduledTask
{ {
@ -31,19 +36,17 @@ public:
int minute; int minute;
int second; int second;
RunMode mode; RunMode mode;
bool executed; bool executed = false;
application::ITimeProvider::Clock::time_point nextExecution; TimePoint nextExecution{};
ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md) ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md)
: function(std::move(t)), hour(h), minute(m), second(s), mode(md), : function(std::move(t)), hour(h), minute(m), second(s), mode(md)
executed(false)
{} {}
}; };
TaskScheduler(ILoggerPtr logger, TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider,
std::shared_ptr<application::ITimeProvider> timeProvider, IThreadManager& threadManager,
std::shared_ptr<application::IThreadManager> threadManager, std::unique_ptr<IBlocker> blocker);
std::shared_ptr<application::IBlocker> blocker);
TaskScheduler(const TaskScheduler&) = delete; TaskScheduler(const TaskScheduler&) = delete;
TaskScheduler& operator=(const TaskScheduler&) = delete; TaskScheduler& operator=(const TaskScheduler&) = delete;
@ -56,27 +59,27 @@ public:
private: private:
ILoggerPtr logger; ILoggerPtr logger;
std::shared_ptr<application::ITimeProvider> timeProvider; ITimeProvider& timeProvider;
std::shared_ptr<application::IThreadManager> threadManager; IThreadManager& threadManager;
std::shared_ptr<application::IBlocker> blocker; std::unique_ptr<IBlocker> blocker;
std::vector<ScheduledTask> tasks; std::vector<ScheduledTask> tasks;
std::mutex tasksMutex; std::mutex tasksMutex;
std::atomic<bool> running; std::atomic<bool> running{false};
std::atomic<bool> stopRequested; std::atomic<bool> stopRequested{false};
std::unique_ptr<application::IThreadManager::ThreadHandle> threadHandle; std::unique_ptr<ThreadHandle> threadHandle;
}; };
constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a, constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a,
TaskScheduler::RunMode b) TaskScheduler::RunMode b)
{ {
return static_cast<TaskScheduler::RunMode>(static_cast<int>(a) return static_cast<TaskScheduler::RunMode>(static_cast<uint8_t>(a)
| static_cast<int>(b)); | static_cast<uint8_t>(b));
} }
constexpr int operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b) constexpr uint8_t operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b)
{ {
return static_cast<int>(a) & static_cast<int>(b); return static_cast<uint8_t>(a) & static_cast<uint8_t>(b);
} }
} // namespace nxl::autostore::infrastructure } // namespace nxl::autostore::application

138
cpp17/lib/src/infrastructure/services/TaskScheduler.cpp

@ -1,138 +0,0 @@
#include "TaskScheduler.h"
#include <sstream>
#include <iomanip>
#include <random>
#include <stdexcept>
#include <thread>
namespace nxl::autostore::infrastructure {
namespace {
std::chrono::system_clock::time_point
today(uint8_t hour, uint8_t minute, uint8_t second,
const application::ITimeProvider& timeProvider)
{
using namespace std::chrono;
using days = duration<int, std::ratio<86400>>;
auto now = timeProvider.now();
auto midnight = time_point_cast<system_clock::duration>(floor<days>(now));
auto offset = hours{hour} + minutes{minute} + seconds{second};
return midnight + offset;
}
} // namespace
TaskScheduler::TaskScheduler(
ILoggerPtr logger, std::shared_ptr<application::ITimeProvider> timeProvider,
std::shared_ptr<application::IThreadManager> threadManager,
std::shared_ptr<application::IBlocker> blocker)
: logger{std::move(logger)}, timeProvider{std::move(timeProvider)},
threadManager{std::move(threadManager)}, blocker{std::move(blocker)},
running(false), stopRequested(false)
{}
void TaskScheduler::schedule(TaskFunction task, int hour, int minute,
int second, RunMode mode)
{
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0
|| second > 59) {
throw std::invalid_argument("Invalid time parameters");
}
if ((mode & RunMode::Forever) && (mode & RunMode::Once)) {
throw std::invalid_argument(
"Forever and Once modes are mutually exclusive");
}
std::lock_guard<std::mutex> lock(tasksMutex);
tasks.emplace_back(std::move(task), hour, minute, second, mode);
}
void TaskScheduler::start()
{
if (running) {
return;
}
running = true;
stopRequested = false;
threadHandle = threadManager->createThread([this]() {
logger->info("TaskScheduler thread started");
while (!stopRequested) {
auto now = timeProvider->now();
bool shouldExecuteOnStart = false;
{
std::lock_guard<std::mutex> lock(tasksMutex);
for (auto& task : tasks) {
if (stopRequested)
break;
bool executeNow = false;
if ((task.mode & RunMode::OnStart) && !task.executed) {
executeNow = true;
shouldExecuteOnStart = true;
} else if (task.mode & RunMode::Once
|| task.mode & RunMode::Forever) {
if (!task.executed || (task.mode & RunMode::Forever)) {
auto taskTime =
today(task.hour, task.minute, task.second, *timeProvider);
if (taskTime <= now) {
if (task.mode & RunMode::Forever) {
taskTime += std::chrono::hours(24);
}
executeNow = true;
}
task.nextExecution = taskTime;
}
}
if (executeNow) {
try {
task.function();
task.executed = true;
logger->info("Task executed successfully");
} catch (const std::exception& e) {
logger->error("Task execution failed: %s", e.what());
}
}
}
}
if (shouldExecuteOnStart) {
continue;
}
if (!stopRequested) {
blocker->blockFor(std::chrono::milliseconds(100));
}
}
running = false;
logger->info("TaskScheduler thread stopped");
});
}
void TaskScheduler::stop()
{
if (!running) {
return;
}
stopRequested = true;
blocker->notify();
if (threadHandle && threadHandle->joinable()) {
threadHandle->join();
}
}
} // namespace nxl::autostore::infrastructure

168
cpp17/tests/unit/TaskScheduler.test.cpp

@ -1,4 +1,4 @@
#include "infrastructure/services/TaskScheduler.h" #include "application/services/TaskScheduler.h"
#include "mocks/TestLogger.h" #include "mocks/TestLogger.h"
#include "mocks/MockTimeProvider.h" #include "mocks/MockTimeProvider.h"
#include "mocks/MockThreadManager.h" #include "mocks/MockThreadManager.h"
@ -12,7 +12,7 @@ using trompeloeil::_;
using namespace nxl::autostore; using namespace nxl::autostore;
using namespace std::chrono; using namespace std::chrono;
using nxl::autostore::infrastructure::TaskScheduler; using nxl::autostore::application::TaskScheduler;
namespace test { namespace test {
@ -26,9 +26,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
{ {
// Common mock objects that all sections can use // Common mock objects that all sections can use
auto logger = std::make_shared<test::TestLogger>(); auto logger = std::make_shared<test::TestLogger>();
auto timeProvider = std::make_shared<test::MockTimeProvider>(); auto timeProvider = std::make_unique<test::MockTimeProvider>();
auto threadMgr = std::make_shared<test::MockThreadManager>(); auto threadMgr = std::make_unique<test::MockThreadManager>();
auto blocker = std::make_shared<test::MockBlocker>(); auto blocker = std::make_unique<test::MockBlocker>();
SECTION("when start is called then createThread is called") SECTION("when start is called then createThread is called")
{ {
@ -37,7 +37,8 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>()); .RETURN(std::make_unique<test::MockThreadHandle>());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(blocker));
// When // When
scheduler.start(); scheduler.start();
@ -45,8 +46,13 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
SECTION("when scheduler is created then it is not running") SECTION("when scheduler is created then it is not running")
{ {
// Given - recreate blocker for this test since it was moved in previous
// section
auto testBlocker = std::make_unique<test::MockBlocker>();
// When // When
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// Then - calling stop on a non-running scheduler should not cause issues // Then - calling stop on a non-running scheduler should not cause issues
// and no thread operations should be called // and no thread operations should be called
@ -61,15 +67,19 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
bool taskExecuted = false; bool taskExecuted = false;
std::function<void()> threadFn; std::function<void()> threadFn;
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function // Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.RETURN(std::make_unique<test::MockThreadHandle>()) .RETURN(std::make_unique<test::MockThreadHandle>())
.LR_SIDE_EFFECT(threadFn = std::move(_1)); .LR_SIDE_EFFECT(threadFn = std::move(_1));
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
FORBID_CALL(*blocker, blockFor(_)); FORBID_CALL(*testBlocker, blockFor(_));
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() { auto taskFunction = [&]() {
taskExecuted = true; taskExecuted = true;
@ -104,6 +114,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function // Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle)) .LR_RETURN(std::move(threadHandle))
@ -113,12 +126,13 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls, save delay value // Allow blocker calls, save delay value
ALLOW_CALL(*blocker, blockFor(_)) ALLOW_CALL(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow .LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow
); );
ALLOW_CALL(*blocker, notify()); ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() { auto taskFunction = [&]() {
taskExecuted = true; taskExecuted = true;
@ -151,6 +165,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function // Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle)) .LR_RETURN(std::move(threadHandle))
@ -160,10 +177,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage // Allow blocker calls and simulate time passage
ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*blocker, notify()); ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() { auto taskFunction = [&]() {
executionCount++; executionCount++;
@ -187,8 +205,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
SECTION("when invalid time parameters are provided then exception is thrown") SECTION("when invalid time parameters are provided then exception is thrown")
{ {
// Given // Given - recreate blocker for this test
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// When & Then - invalid hour // When & Then - invalid hour
REQUIRE_THROWS_AS( REQUIRE_THROWS_AS(
@ -214,13 +235,14 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once), scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once),
std::invalid_argument); std::invalid_argument);
} }
// std::invalid_argument);
// }
SECTION("when invalid mode combination is used then exception is thrown") SECTION("when invalid mode combination is used then exception is thrown")
{ {
// Given // Given - recreate blocker for this test
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto testBlocker = std::make_unique<test::MockBlocker>();
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
// When & Then // When & Then
REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0, REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0,
@ -228,6 +250,7 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
| TaskScheduler::RunMode::Once), | TaskScheduler::RunMode::Once),
std::invalid_argument); std::invalid_argument);
} }
SECTION("when multiple tasks are scheduled then all execute") SECTION("when multiple tasks are scheduled then all execute")
{ {
// Given // Given
@ -240,6 +263,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function // Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle)) .LR_RETURN(std::move(threadHandle))
@ -249,10 +275,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW);
// Allow blocker calls // Allow blocker calls
ALLOW_CALL(*blocker, blockFor(_)); ALLOW_CALL(*testBlocker, blockFor(_));
ALLOW_CALL(*blocker, notify()); ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction1 = [&]() { task1Executed = true; }; auto taskFunction1 = [&]() { task1Executed = true; };
@ -273,7 +300,6 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
REQUIRE(task1Executed); REQUIRE(task1Executed);
REQUIRE(task2Executed); REQUIRE(task2Executed);
} }
// }
SECTION("when task is scheduled with Forever mode then it repeats") SECTION("when task is scheduled with Forever mode then it repeats")
{ {
@ -287,6 +313,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function // Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_)) REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle)) .LR_RETURN(std::move(threadHandle))
@ -296,10 +325,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Allow blocker calls and simulate time passage // Allow blocker calls and simulate time passage
ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1);
ALLOW_CALL(*blocker, notify()); ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() { auto taskFunction = [&]() {
executionCount++; executionCount++;
@ -322,5 +352,85 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]")
// Then // Then
REQUIRE(executionCount >= 2); REQUIRE(executionCount >= 2);
} }
// }
SECTION("when task is scheduled with Forever and OnStart mode then it "
"executes on start and at scheduled time only")
{
// Given
auto threadHandle = std::make_unique<test::MockThreadHandle>();
std::function<void()> threadFn;
std::vector<std::chrono::system_clock::time_point> executionTimes;
auto currentTime = test::TIMEPOINT_NOW; // 2020-01-01 12:00:00
// Schedule task for 12:00:05 (5 seconds after current time)
auto scheduledTimeDelta = std::chrono::seconds{5};
auto scheduledTime = currentTime + scheduledTimeDelta;
// Set up thread handle expectations before moving it
ALLOW_CALL(*threadHandle, join());
ALLOW_CALL(*threadHandle, joinable()).RETURN(true);
// Recreate blocker for this test
auto testBlocker = std::make_unique<test::MockBlocker>();
// Expect createThread to be called, save thread function
REQUIRE_CALL(*threadMgr, createThread(_))
.LR_RETURN(std::move(threadHandle))
.LR_SIDE_EFFECT(threadFn = std::move(_1));
// Mock time provider calls - simulate time advancing
ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime);
// Also add a timeout mechanism in case the scheduler doesn't execute as
// expected
auto timeoutTime = test::TIMEPOINT_NOW + std::chrono::minutes(2);
// Allow blocker calls and simulate time passage
ALLOW_CALL(*testBlocker, blockFor(_))
.LR_SIDE_EFFECT(
// Advance time by the blocked amount
currentTime += _1;);
ALLOW_CALL(*testBlocker, notify());
TaskScheduler scheduler(logger, *timeProvider, *threadMgr,
std::move(testBlocker));
auto taskFunction = [&]() {
// Record the current time when this execution happens
executionTimes.push_back(currentTime);
// Stop after 2 executions (the expected behavior)
if (executionTimes.size() >= 2) {
scheduler.stop();
}
};
// When - schedule task with both Forever and OnStart modes
// Set time to 12:00:05 (5 seconds after our test TIMEPOINT_NOW)
scheduler.schedule(taskFunction, 12, 0, 5,
TaskScheduler::RunMode::Forever
| TaskScheduler::RunMode::OnStart);
scheduler.start();
// Execute the thread function to simulate the scheduler thread
threadFn();
// Then - task should have executed exactly twice:
// 1. Once immediately due to OnStart (at TIMEPOINT_NOW)
// 2. Once at the scheduled time (12:00:05)
// But NOT more than that (which would indicate the bug)
// With the bug, executionTimes will have many entries due to infinite loop
// Without the bug, we should have exactly 2 entries
REQUIRE(executionTimes.size() == 2);
// First execution should be at the initial time (OnStart)
REQUIRE(executionTimes[0] == test::TIMEPOINT_NOW);
// Second execution should be at or after the scheduled time
REQUIRE(executionTimes[1] >= scheduledTime);
// Verify that time has advanced appropriately
// Current time should be at or after the scheduled time
REQUIRE(currentTime >= scheduledTime);
}
} }

3
cpp17/vcpkg.json

@ -8,5 +8,6 @@
"spdlog", "spdlog",
"catch2", "catch2",
"trompeloeil" "trompeloeil"
] ],
"builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0"
} }

Loading…
Cancel
Save