diff --git a/cpp17/.clang-tidy b/cpp17/.clang-tidy new file mode 100644 index 0000000..6ab9da2 --- /dev/null +++ b/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 +... diff --git a/cpp17/README.md b/cpp17/README.md index d36b05e..ad0c933 100644 --- a/cpp17/README.md +++ b/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 - -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. - -#### 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 +```bash +cd docker +docker compose build +docker compose up +``` -| Layer | Responsibility | Internal Dependencies | External Dependencies | -|-------------------|--------------------------------------------------------------- |----------------------|-----------------------| -| **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.| +Note: do not use this for development. See `.devcontainer` directory for development setup. ---- +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 -AutoStore/ -├── App -│ ├── Main -│ ├── AppConfig -│ └── ... -├── Extern -│ ├── -│ └── <...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/ -``` +OUT_DIR=./out +DEBUG_DIR=$OUT_DIR/build/debug -## 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 `/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 `/README.md` file with setup and running instructions. - -## API Endpoints -See `openapi.yaml` file for suggested API (test it with Tavern, Postman etc.). -Here's a summary of example API endpoints: +# Testing -| Endpoint | Method | Description | -|-------------------------|--------|--------------------------------------| -| `/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 | +Unit tests are added to ctest and executed on docker build. Execute `ctest .` in build dir to run it. -Suggested base URL is `http://localhost:8080/api/v1/`. \ No newline at end of file +Use top-level testing/tavern scripts to run functional tests. diff --git a/cpp17/TODO.md b/cpp17/TODO.md deleted file mode 100644 index c0dc4d8..0000000 --- a/cpp17/TODO.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/cpp17/doc/add-item-sequence.md b/cpp17/doc/add-item-sequence.md index 90070bd..187bfea 100644 --- a/cpp17/doc/add-item-sequence.md +++ b/cpp17/doc/add-item-sequence.md @@ -2,32 +2,57 @@ ```mermaid sequenceDiagram + participant Client as HTTP Client + participant Middleware as HttpJwtMiddleware + participant Server as HttpServer participant Controller as StoreController - participant UseCase as AddItem Use Case + participant AuthService as IAuthService + participant UseCase as AddItem participant Clock as ITimeProvider - participant Policy as ExpirationPolicy - participant OrderService as OrderingService - participant HttpClient as HttpClient + participant Policy as ItemExpirationPolicy + participant OrderService as IOrderService + participant HttpClient as HttpOrderService 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) UseCase->>Clock: now() - Clock-->>UseCase: DateTime + Clock-->>UseCase: current time - UseCase->>Policy: IsExpired(item, currentTime) + UseCase->>Policy: isExpired(item, currentTime) Policy-->>UseCase: boolean alt Item is expired UseCase->>OrderService: orderItem(item) OrderService->>HttpClient: POST to order URL HttpClient-->>OrderService: Response - OrderService-->>UseCase: OrderResult + OrderService-->>UseCase: void end UseCase->>Repo: save(item) - Repo->>Repo: Persist to storage - Repo-->>UseCase: Saved Item ID + Repo->>Repo: Persist to file storage + 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 ``` diff --git a/cpp17/doc/architecture-overview.md b/cpp17/doc/architecture-overview.md new file mode 100644 index 0000000..71463ec --- /dev/null +++ b/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 \ No newline at end of file diff --git a/cpp17/doc/request-sequence.md b/cpp17/doc/request-sequence.md deleted file mode 100644 index a38295f..0000000 --- a/cpp17/doc/request-sequence.md +++ /dev/null @@ -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 -``` diff --git a/cpp17/docker/.dockerignore b/cpp17/docker/.dockerignore new file mode 100644 index 0000000..3cd18dc --- /dev/null +++ b/cpp17/docker/.dockerignore @@ -0,0 +1,4 @@ +.devcontainer +.out +build +build-* diff --git a/cpp17/docker/Dockerfile b/cpp17/docker/Dockerfile index c70024b..21cc569 100644 --- a/cpp17/docker/Dockerfile +++ b/cpp17/docker/Dockerfile @@ -8,6 +8,7 @@ COPY .. . RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ -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"] diff --git a/cpp17/docker/docker-compose.yml b/cpp17/docker/docker-compose.yml index e6a35ec..c1d34f2 100644 --- a/cpp17/docker/docker-compose.yml +++ b/cpp17/docker/docker-compose.yml @@ -3,6 +3,8 @@ services: app: build: context: .. - dockerfile: Docker/Dockerfile + dockerfile: docker/Dockerfile image: autostore-build-cpp-vcpkg-img container_name: autostore-build-cpp-vcpkg + ports: + - 8080:8080 diff --git a/cpp17/lib/CMakeLists.txt b/cpp17/lib/CMakeLists.txt index 37040fa..2e32299 100644 --- a/cpp17/lib/CMakeLists.txt +++ b/cpp17/lib/CMakeLists.txt @@ -26,7 +26,7 @@ add_library(${TARGET_NAME} STATIC src/infrastructure/helpers/Jsend.cpp src/infrastructure/helpers/JsonItem.cpp src/infrastructure/auth/FileJwtAuthService.cpp - src/infrastructure/services/TaskScheduler.cpp + src/application/services/TaskScheduler.cpp src/infrastructure/adapters/CvBlocker.cpp src/infrastructure/adapters/SystemThreadManager.cpp src/infrastructure/adapters/SystemTimeProvider.cpp diff --git a/cpp17/lib/include/autostore/AutoStore.h b/cpp17/lib/include/autostore/AutoStore.h index c8860e8..b5ab753 100644 --- a/cpp17/lib/include/autostore/AutoStore.h +++ b/cpp17/lib/include/autostore/AutoStore.h @@ -13,11 +13,12 @@ class IItemRepository; class ITimeProvider; class IOrderService; class IAuthService; +class IThreadManager; +class TaskScheduler; } // namespace application namespace infrastructure { class HttpServer; -class TaskScheduler; } // namespace infrastructure namespace webapi { @@ -52,13 +53,15 @@ private: ILoggerPtr log; std::unique_ptr httpServer; - std::unique_ptr taskScheduler; + std::unique_ptr taskScheduler; std::unique_ptr storeController; std::unique_ptr authController; std::unique_ptr itemRepository; std::unique_ptr clock; std::unique_ptr orderService; std::unique_ptr authService; + std::unique_ptr timeProvider; + std::unique_ptr threadManager; }; } // namespace nxl::autostore \ No newline at end of file diff --git a/cpp17/lib/src/AutoStore.cpp b/cpp17/lib/src/AutoStore.cpp index 6860a11..4f68e55 100644 --- a/cpp17/lib/src/AutoStore.cpp +++ b/cpp17/lib/src/AutoStore.cpp @@ -6,7 +6,7 @@ #include "webapi/controllers/StoreController.h" #include "webapi/controllers/AuthController.h" #include "infrastructure/http/HttpServer.h" -#include "infrastructure/services/TaskScheduler.h" +#include "application/services/TaskScheduler.h" #include "application/commands/HandleExpiredItems.h" #include "infrastructure/adapters/SystemTimeProvider.h" #include "infrastructure/adapters/SystemThreadManager.h" @@ -18,6 +18,7 @@ namespace nxl::autostore { using namespace infrastructure; +using namespace application; AutoStore::AutoStore(Config config, ILoggerPtr logger) : config{std::move(config)}, log{std::move(logger)} @@ -50,13 +51,13 @@ bool AutoStore::initialize() authService = std::make_unique(usersDbPath); // Initialize dependencies for task scheduler - auto timeProvider = std::make_shared(); - auto threadManager = std::make_shared(); - auto blocker = std::make_shared(); + timeProvider = std::make_unique(); + threadManager = std::make_unique(); + auto blocker = std::make_unique(); // Initialize task scheduler (for handling expired items) - taskScheduler = std::make_unique(log, timeProvider, - threadManager, blocker); + taskScheduler = std::make_unique( + log, *timeProvider, *threadManager, std::move(blocker)); // Initialize HTTP server httpServer = std::make_unique(log, *authService); diff --git a/cpp17/lib/src/application/services/TaskScheduler.cpp b/cpp17/lib/src/application/services/TaskScheduler.cpp new file mode 100644 index 0000000..45b9005 --- /dev/null +++ b/cpp17/lib/src/application/services/TaskScheduler.cpp @@ -0,0 +1,229 @@ +#include "TaskScheduler.h" +#include + +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>; +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(mode) & static_cast(RunMode::Forever)) + && (static_cast(mode) & static_cast(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(std::chrono::floor(now)); + auto offset = Hours{hour} + Minutes{minute} + Seconds{second}; + return midnight + offset; +} + +bool shouldExecuteOnStart(const TaskScheduler::ScheduledTask& task) +{ + return (static_cast(task.mode) & static_cast(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(task.mode) & static_cast(RunMode::Forever)) { + taskTime += Hours(24); + } else if ((static_cast(task.mode) & static_cast(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(task.mode) & static_cast(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& tasks, + ILogger& logger, const ITimeProvider& timeProvider, + std::atomic& 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(task.mode) & static_cast(RunMode::Once)) + || (static_cast(task.mode) + & static_cast(RunMode::Forever))) { + if (task.nextExecution == TimePoint{}) { + auto taskTime = calculateNextExecutionTime(task, timeProvider, now); + + if ((static_cast(task.mode) & static_cast(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& stopRequested, + TimePoint now, TimePoint nextWakeupTime) +{ + if (!stopRequested && nextWakeupTime > now) { + auto waitDuration = + std::chrono::duration_cast(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 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 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 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 \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/services/TaskScheduler.h b/cpp17/lib/src/application/services/TaskScheduler.h similarity index 50% rename from cpp17/lib/src/infrastructure/services/TaskScheduler.h rename to cpp17/lib/src/application/services/TaskScheduler.h index 17987f0..c56fdf2 100644 --- a/cpp17/lib/src/infrastructure/services/TaskScheduler.h +++ b/cpp17/lib/src/application/services/TaskScheduler.h @@ -12,17 +12,22 @@ #include #include -namespace nxl::autostore::infrastructure { +namespace nxl::autostore::application { class TaskScheduler { public: - /** - * @note Forever and Once are mutually exclusive - */ - enum class RunMode { OnStart = 1, Forever = 2, Once = 4 }; + enum class RunMode { + OnStart = 1, + Forever = 2, + OnStartThenForever = 3, // OnStart | Forever + Once = 4, + OnStartThenOnce = 5 // OnStart | Once + }; using TaskFunction = std::function; + using TimePoint = application::ITimeProvider::Clock::time_point; + using ThreadHandle = application::IThreadManager::ThreadHandle; struct ScheduledTask { @@ -31,19 +36,17 @@ public: int minute; int second; RunMode mode; - bool executed; - application::ITimeProvider::Clock::time_point nextExecution; + bool executed = false; + TimePoint nextExecution{}; ScheduledTask(TaskFunction t, int h, int m, int s, RunMode md) - : function(std::move(t)), hour(h), minute(m), second(s), mode(md), - executed(false) + : function(std::move(t)), hour(h), minute(m), second(s), mode(md) {} }; - TaskScheduler(ILoggerPtr logger, - std::shared_ptr timeProvider, - std::shared_ptr threadManager, - std::shared_ptr blocker); + TaskScheduler(ILoggerPtr logger, ITimeProvider& timeProvider, + IThreadManager& threadManager, + std::unique_ptr blocker); TaskScheduler(const TaskScheduler&) = delete; TaskScheduler& operator=(const TaskScheduler&) = delete; @@ -56,27 +59,27 @@ public: private: ILoggerPtr logger; - std::shared_ptr timeProvider; - std::shared_ptr threadManager; - std::shared_ptr blocker; + ITimeProvider& timeProvider; + IThreadManager& threadManager; + std::unique_ptr blocker; std::vector tasks; std::mutex tasksMutex; - std::atomic running; - std::atomic stopRequested; - std::unique_ptr threadHandle; + std::atomic running{false}; + std::atomic stopRequested{false}; + std::unique_ptr threadHandle; }; constexpr TaskScheduler::RunMode operator|(TaskScheduler::RunMode a, TaskScheduler::RunMode b) { - return static_cast(static_cast(a) - | static_cast(b)); + return static_cast(static_cast(a) + | static_cast(b)); } -constexpr int operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b) +constexpr uint8_t operator&(TaskScheduler::RunMode a, TaskScheduler::RunMode b) { - return static_cast(a) & static_cast(b); + return static_cast(a) & static_cast(b); } -} // namespace nxl::autostore::infrastructure \ No newline at end of file +} // namespace nxl::autostore::application \ No newline at end of file diff --git a/cpp17/lib/src/infrastructure/services/TaskScheduler.cpp b/cpp17/lib/src/infrastructure/services/TaskScheduler.cpp deleted file mode 100644 index 6118b9f..0000000 --- a/cpp17/lib/src/infrastructure/services/TaskScheduler.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "TaskScheduler.h" -#include -#include -#include -#include -#include - -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>; - - auto now = timeProvider.now(); - auto midnight = time_point_cast(floor(now)); - auto offset = hours{hour} + minutes{minute} + seconds{second}; - return midnight + offset; -} - -} // namespace - -TaskScheduler::TaskScheduler( - ILoggerPtr logger, std::shared_ptr timeProvider, - std::shared_ptr threadManager, - std::shared_ptr 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 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 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 \ No newline at end of file diff --git a/cpp17/tests/unit/TaskScheduler.test.cpp b/cpp17/tests/unit/TaskScheduler.test.cpp index 55149a2..3fc3b7c 100644 --- a/cpp17/tests/unit/TaskScheduler.test.cpp +++ b/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/MockTimeProvider.h" #include "mocks/MockThreadManager.h" @@ -12,7 +12,7 @@ using trompeloeil::_; using namespace nxl::autostore; using namespace std::chrono; -using nxl::autostore::infrastructure::TaskScheduler; +using nxl::autostore::application::TaskScheduler; namespace test { @@ -26,9 +26,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") { // Common mock objects that all sections can use auto logger = std::make_shared(); - auto timeProvider = std::make_shared(); - auto threadMgr = std::make_shared(); - auto blocker = std::make_shared(); + auto timeProvider = std::make_unique(); + auto threadMgr = std::make_unique(); + auto blocker = std::make_unique(); SECTION("when start is called then createThread is called") { @@ -37,7 +37,8 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") REQUIRE_CALL(*threadMgr, createThread(_)) .RETURN(std::make_unique()); - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(blocker)); // When scheduler.start(); @@ -45,8 +46,13 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") 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(); + // 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 // and no thread operations should be called @@ -61,15 +67,19 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") bool taskExecuted = false; std::function threadFn; + // Recreate blocker for this test + auto testBlocker = std::make_unique(); + // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .RETURN(std::make_unique()) .LR_SIDE_EFFECT(threadFn = std::move(_1)); 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 = [&]() { taskExecuted = true; @@ -104,6 +114,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); + // Recreate blocker for this test + auto testBlocker = std::make_unique(); + // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) @@ -113,12 +126,13 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // 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 ); - ALLOW_CALL(*blocker, notify()); + ALLOW_CALL(*testBlocker, notify()); - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); auto taskFunction = [&]() { taskExecuted = true; @@ -151,6 +165,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); + // Recreate blocker for this test + auto testBlocker = std::make_unique(); + // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) @@ -160,10 +177,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // Allow blocker calls and simulate time passage - ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); - ALLOW_CALL(*blocker, notify()); + ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); + ALLOW_CALL(*testBlocker, notify()); - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); auto taskFunction = [&]() { executionCount++; @@ -187,8 +205,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") SECTION("when invalid time parameters are provided then exception is thrown") { - // Given - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + // Given - recreate blocker for this test + auto testBlocker = std::make_unique(); + + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); // When & Then - invalid hour REQUIRE_THROWS_AS( @@ -214,13 +235,14 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once), std::invalid_argument); } - // std::invalid_argument); - // } SECTION("when invalid mode combination is used then exception is thrown") { - // Given - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + // Given - recreate blocker for this test + auto testBlocker = std::make_unique(); + + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); // When & Then REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0, @@ -228,6 +250,7 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") | TaskScheduler::RunMode::Once), std::invalid_argument); } + SECTION("when multiple tasks are scheduled then all execute") { // Given @@ -240,6 +263,9 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); + // Recreate blocker for this test + auto testBlocker = std::make_unique(); + // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .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 blocker calls - ALLOW_CALL(*blocker, blockFor(_)); - ALLOW_CALL(*blocker, notify()); + ALLOW_CALL(*testBlocker, blockFor(_)); + ALLOW_CALL(*testBlocker, notify()); - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); auto taskFunction1 = [&]() { task1Executed = true; }; @@ -273,7 +300,6 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") REQUIRE(task1Executed); REQUIRE(task2Executed); } - // } 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, joinable()).RETURN(true); + // Recreate blocker for this test + auto testBlocker = std::make_unique(); + // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) @@ -296,10 +325,11 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // Allow blocker calls and simulate time passage - ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); - ALLOW_CALL(*blocker, notify()); + ALLOW_CALL(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); + ALLOW_CALL(*testBlocker, notify()); - TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); + TaskScheduler scheduler(logger, *timeProvider, *threadMgr, + std::move(testBlocker)); auto taskFunction = [&]() { executionCount++; @@ -322,5 +352,85 @@ TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") // Then 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(); + std::function threadFn; + std::vector 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(); + + // 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); + } } diff --git a/cpp17/vcpkg.json b/cpp17/vcpkg.json index 5c81417..b4f0d6c 100644 --- a/cpp17/vcpkg.json +++ b/cpp17/vcpkg.json @@ -8,5 +8,6 @@ "spdlog", "catch2", "trompeloeil" - ] + ], + "builtin-baseline": "ef7dbf94b9198bc58f45951adcf1f041fcbc5ea0" }