diff --git a/.gitignore b/.gitignore index 811b518..050b750 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Temporarily exclude these files from aide to make the token size manageable +#account/ +#order-processor/ + ### How to update # This is copied from OpenHFT/.gitignore # update the original and run OpenHFT/update_gitignore.sh diff --git a/README.adoc b/README.adoc index 0754899..1775817 100644 --- a/README.adoc +++ b/README.adoc @@ -1,12 +1,57 @@ -= Chronicle-Queue-Demo -Peter Lawrey -:imagesdir: images += Chronicle Queue Demo - README +:toc: +:toclevels: 3 -This is a tutorial demonstrating the usage of Chronicle Queue with simple demo programs. +This repository demonstrates the usage of Chronicle Queue (and related Chronicle libraries) through multiple examples, including an order processor, event routing, and more. Below is a quick start and pointers to further documentation. + +== Quick Start + +.Basic Steps +---- +git clone https://github.com/OpenHFT/Chronicle-Queue-Demo.git +cd Chronicle-Queue-Demo +mvn clean install +---- + +To run a simple example, like the hello-world module: + +---- +cd hello-world +mvn install exec:java@RecordInputToConsoleMain +---- + +== Documentation + +The project’s documentation is consolidated in five AsciiDoc files: + +1. xref:architecture.adoc[Architecture] +2. xref:usage-and-tests.adoc[Usage & Tests] +3. xref:reference.adoc[Reference, Style & Glossary] +4. xref:account/README.adoc[Account Management System (AMS)] +5. xref:order-processor/README.adoc[Order Processor] + +Refer to them for details on architecture, usage instructions, testing approaches, style guides, and advanced references. + +== Repository Overview + +The modules in this repository include: + +* **account**: An Account Management System (AMS) example using Chronicle Queue event-driven logic. +* **benchmarks**: Scripts and classes demonstrating throughput/latency benchmarks (e.g., LatencyDistributionMain, ThroughputMain). +* **event-routing**: Showcases how messages can be routed via Chronicle Queues with interfaces like `ViaIn`, `ViaOut`. +* **hello-world**: A simple introduction to an event-driven microservice using Chronicle Queue (input, exclamation addition, output). +* **md-pipeline**: A Market Data pipeline example with aggregator, strategy, and an exchange simulator. +* **message-history-demo**: Demonstrates Chronicle’s `MessageHistory` in bridging or event processing scenarios. +* **messages-with-text**: An example of writing/reading textual content with minimal garbage creation. +* **order-processor**: A submodule that acts as a simple OMS (Order Management System), referencing FIX 4.2 concepts. + +For deeper details on each, see xref:architecture.adoc[Architecture]. == Order Processor -image::Two-hop-latency.PNG[] +This is a tutorial demonstrating the usage of Chronicle Queue with simple demo programs. + +image::images/Two-hop-latency.png[] You can find the source code for the order processor example https://github.com/OpenHFT/Chronicle-Queue-Demo/tree/master/order-processor[here]. @@ -18,13 +63,13 @@ This allows you to install Linux packages that aren't already on Windows. When this asks you which packages you want to install, search for and add `git`. This is under `Development` and you need to click `skip` so it says to `install`. -image::gitpack.png[] +image::images/gitpack.png[] Open IntelliJ or your favorite https://en.wikipedia.org/wiki/Integrated_development_environment[Integrated Development Environment (IDE)]. If you haven't installed an IDE, we recommend https://www.jetbrains.com/idea/download/#section=windows[IntelliJ], which we'll use for this tutorial. In IntelliJ, select `Get from VCS` to clone the Chronicle Queue Sample code. -image::homegit.png[] +image::images/homegit.png[] Then, copy the following URL into the `Git Repository URL` field and remember the `Parent Directory`. Click `Clone` to get the code. @@ -33,11 +78,11 @@ Then, copy the following URL into the `Git Repository URL` field and remember th https://github.com/OpenHFT/Chronicle-Queue-Demo.git ---- -image::Clone.PNG[] +image::images/Clone.png[] If you close the project, you can reopen it by going to `File -> Open`. You'll find the repository in the directory where you saved it. -image::directory.PNG[] +image::images/directory.png[] Now you're ready to run the example programs! You can start with https://github.com/OpenHFT/Chronicle-Queue-Demo/tree/master/simple-input[Simple Input]. diff --git a/account.ad b/account.ad new file mode 100644 index 0000000..0ebbea3 --- /dev/null +++ b/account.ad @@ -0,0 +1,63 @@ += Project Summary + +== Overview + +The project is an Account Management System (AMS) leveraging the Chronicle libraries for high-performance, low-latency event-driven operations. It emphasizes documentation-driven development with AI integration for streamlined engineering. The AMS manages account creation, transfers, and state snapshots through a comprehensive suite of YAML-based tests, detailed functional requirements, and a modular architecture. + +== Key Features + +* **Low-Latency and High-Throughput**: Designed for operations with microsecond-level latencies and over 1M messages per second throughput. +* **Event-Driven Design**: Utilizes Chronicle Queue for durable message persistence and asynchronous operations. +* **Rich Documentation**: Implements AsciiDoc for capturing requirements, workflows, and style guides to align development with AI tools. +* **Testing Framework**: YAML-driven scenarios test various operations, including edge cases and invalid inputs, ensuring robust functionality. + +== Functional Components + +=== Documentation +* **AIDE Glossary**: Defines project-specific terminology, such as AIDE (Artificial Intelligence Development Environment) and tokens/line ratios. +* **Style Guide**: Enforces British English conventions and standardizes coding/documentation practices. +* **Workflow**: Outlines iterative development—document, test, code, review, and repeat—keeping code and requirements synchronized. + +=== Code +* **Core Classes**: +* `AccountManagerImpl`: Orchestrates incoming events and delegates logic to `AccountService`. +* `AccountService`: Validates accounts, manages balances, and processes transfers. +* DTOs (e.g., `CreateAccount`, `Transfer`): Represent operations with fluent setter methods for chaining. +* **Utilities**: +* `LogsAccountManagerOut`: A mock implementation for testing event outputs. +* `ErrorListener`: Handles JVM-level errors gracefully. + +=== Testing +* **Parameterized Tests**: YAML scenarios validate the AMS against predefined inputs and outputs. +* **Coverage**: +* Account creation, transfers, checkpoints. +* Edge cases, including invalid currencies, missing fields, and insufficient funds. +* Performance benchmarks using JLBH for latency testing. + +=== Benchmark Results +Demonstrated latencies: + +* Shared Memory: ~1.5 µs +* TCP: ~20 µs +* End-to-End Variance: Minimal under high load conditions. + +== Directories and Files + +All directories here are under `account` directory + +=== Code +* `src/main/java`: Implementation of AMS. +* `AccountManagerImpl.java`: Main orchestration logic. +* `AccountService.java`: Domain operations. + +=== Tests +* `src/test/java`: Unit and integration tests. +* YAML files: Input (`in.yaml`), expected output (`out.yaml`), and setup (`_setup.yaml`) configurations. + +=== Utilities +* `list_files_asciidoc.sh`: Script to generate directory content summaries. +* `benchmark-results.txt`: Performance insights. + +== Summary + +The AMS is a modular, high-performance system underpinned by comprehensive documentation, rigorous testing, and cutting-edge Chronicle technologies. This structure ensures maintainability, scalability, and alignment with evolving requirements. diff --git a/account/README.adoc b/account/README.adoc index 12dae6f..a72e0d1 100644 --- a/account/README.adoc +++ b/account/README.adoc @@ -1,68 +1,111 @@ = Low-Latency Account Management System Over TCP and Shared Memory -Peter Lawrey +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.3 +:toc: left +:toclevels: 3 -This example shows a simple high performance microservice accessible via TCP or shared memory storing each message in and out persisted in a Chronicle Queue. -There are a number of configurations with a comparison of the performance. -It is designed to produce minimal garbage to minimise jitter. +An event-driven account management system utilising Chronicle libraries for high-performance and low-latency operations. This project demonstrates a reference architecture for creating accounts, performing transfers, and capturing checkpoints of system state, supported by comprehensive testing and benchmarking tools. -This Account Management System is designed to handle basic account operations such as account creation and fund transfers. -It's built on Chronicle Wire and uses Fluent Interface pattern, taking advantage of YAML files for test input data. +This project showcases a low-latency, high-performance account management microservice accessible via TCP or shared memory. +Each inbound and outbound message is persisted in a Chronicle Queue, ensuring durability and traceability. +The system is designed to produce minimal garbage and reduce latency jitter, making it suitable for high-frequency financial operations. -This system leverages the `channels` package in Chronicle Wire, a high-speed messaging framework that excels in handling high volume and latency-critical data. +Built on Chronicle Queue and leveraging YAML-based input data, this service uses a fluent interface pattern to handle various account operations, such as account creation, fund transfers, and state checkpoints. +It also includes benchmarking and testing utilities for performance evaluation. -== Overview +== Key Features -The Account Management System serves as an efficient banking system that facilitates basic account operations, including account creation and fund transfers. -It is architected for low latency, efficient memory utilization, and performance under high transaction volume scenarios. +* **Low Latency**: Designed for microsecond-level response times, especially when using shared memory transport. +* **High Throughput**: Capable of handling high transaction volumes with minimal garbage production. +* **Fault Tolerance & Durability**: All messages persist in a Chronicle Queue, aiding recovery and auditability. +* **Flexible Communication**: Supports TCP and shared memory transport configurations. +* **Modular Design**: Separates API, DTOs, and implementations for easier maintenance and extension. -The core functionality is built around three main components: +== System Overview -. *Account Creation*: This feature allows for the establishment of new bank accounts with relevant details, such as account holder's name, account ID, currency type, and the initial balance. -. *Fund Transfer*: The system is capable of processing funds transfer between different accounts securely and swiftly. -Transfer details include sender account, target account, transfer amount, currency type, and a reference note. -. *Checkpointing*: The checkpoint functionality provides a snapshot of the current state of all accounts in the system at a specific point in time. +=== Core Operations -=== Network Communication +. **Account Creation**: +Create new accounts with specified details (name, account ID, currency, initial balance, and optional overdraft). -The system communicates over TCP to ensure reliable data transmission, or shared memory where possible to minimise latencies. -TCP is chosen for its built-in error checking and correction, ensuring that all account management data reaches the correct destination in the correct order and without errors. -All shared memory messaging is persisted to memory mapped files as it is written. +. **Fund Transfers**: +Transfer funds between accounts, validating currencies, balances, and overdrafts. +Produces events indicating success or failure. + +. **Checkpointing**: +Emit a snapshot of all accounts' current state at a given time for auditing or recovery. + +=== Architecture and Communication + +The service can operate in various configurations: + +* **TCP Client to TCP Service**: Offers the most flexibility for distributed deployments, with typical latencies under 20 µs. +* **TCP Client to Shared Memory Service**: Achieves better performance, typically ~10 µs latency. +* **Shared Memory Client and Service**: This method achieves the lowest latency, around 1.5 µs for typical operations. + +Chronicle Services' `channels` package and Chronicle Queue facilitate fast, low-overhead message passing. +State changes are persistently recorded for replay or state restoration. + +== Architecture and Key Components + +=== Overview +The system is composed of the following layers: + +* **Input/Output Layer**: Handles external requests (e.g., through Chronicle Channels) and emits responses/events. +* **Service Layer**: Contains domain services (e.g., `AccountService`) that enforce business rules like account creation, validation, currency checks, and balance updates. +* **DTOs (Data Transfer Objects)**: Represent business events such as `CreateAccount`, `Transfer`, `OnTransfer`, etc. DTOs are validated before processing to ensure data integrity. +* **Event-Driven Model**: Uses Chronicle’s method readers/writers and queues to handle requests asynchronously and at low latency. +* **Benchmarking and Testing Tools**: Includes JLBH for microsecond-level latency testing and YAML-driven scenario tests. + +=== Key Classes +* `AccountManagerImpl`: Orchestrates incoming events, delegating domain logic to `AccountService` and communicating results back via `AccountManagerOut`. +* `AccountService`: Encapsulates the domain logic for account management (e.g., validating accounts, performing transfers). +* `DTO Classes (CreateAccount, Transfer, OnTransfer, etc.)`: Define the structure and validation rules for events. +* `BenchmarkMain Classes`: Run performance tests and measure end-to-end latencies under various throughput settings. + +A typical flow: +1. A request (e.g., `createAccount`) arrives via a Chronicle channel. +2. `AccountManagerImpl` receives the request, delegates validation and business logic to `AccountService`. +3. `AccountService` returns success or a failure reason. +4. `AccountManagerImpl` emits corresponding events (e.g., `onCreateAccount` or `createAccountFailed`). === Testability and Benchmarking -The system also comes with a comprehensive set of tests and a benchmarking suite, allowing for easy performance measurement and regression testing. -The `AccountManagerBenchmarkMain` class acts as a benchmarking and testing entry point, demonstrating the functionality of the system and showing the execution time for the operations. +A suite of tests and benchmarks (e.g., `AccountManagerBenchmarkMain`) allows easy verification of correctness and performance. +You can measure latency, throughput, and other key metrics to ensure the service meets performance goals. -=== Using a Production Server +== Making It Production-Ready with Chronicle Services -In production, you want a server that has +To further enhance this solution for production environments, consider using https://chronicle.software/services/[Chronicle Services]. +Chronicle Services provides advanced features to ensure high availability, resilience, and smooth operations, including: -- A recent, fast processor with enough cores. -More cores isn't always better esp if it's more than you need. -- A Fast enterprise grade storage device e.g. NVMe. -This can reduce jitter due to writes by a factor fo 10 or better. -- Fast memory. -As event processing systems stream a large amount of data, yet retain relatively little, the memory size doesn't need to be large if your IO subsystem is fast enough. -- As we advocate producing little garbage and storing most data off heap, your heap size might be relatively small. e.g. 2 GB -- If you have a large eden space of 24 GB, you can produce 1 GB/hour of garbage and only minor collect once per day. -Possibly in a maintenance window if you have one. -However, for modest volumes an Eden size of 1 GB might be enough. +* **Failover Support**: If the primary fails, the system automatically switches to a standby service instance, minimizing downtime. +* **Process Restarting**: Automatically restart services after unexpected terminations or during scheduled maintenance windows. +* **Live Upgrades**: Deploy updates without halting the entire system, allowing for rolling upgrades with minimal latency impact. +* **Idempotent Collections**: Simplify restartable event processing by ensuring operations can be safely replayed without side effects. +* **Acknowledged Replication**: Achieve high availability and real-time distribution across hosts, ensuring data consistency and durability. +* **Encryption**: Protect sensitive transaction data with on-disk encryption. +* **Monitoring & Management**: Gain insights into system performance and health, enabling proactive troubleshooting and optimization. -=== Commercial Extensions to Productionise Microservices +Integrating Chronicle Services allows your account management microservice to meet strict service-level agreements (SLAs), maintain continuous uptime, and adapt to evolving business and technical requirements. -There is a commercial extension https://chronicle.software/services/ that supports +image::img/Chronicle-Services-Diagram.png[] -- Control over restarting of services -- Idempotent collections to simplify restarting event processing. -- Acknowledged Replication for High Availability and Realtime Distribution restartable across hosts. -- Encryption of messages stored on disk. -- Monitoring and Management support +== Recommended Production Environment -image:img/Chronicle-Services-Diagram.png[] +Consider running the service on a server with: + +* **Modern, Fast CPU**: Ensure sufficient but not excessive CPU cores. +* **High-Performance Storage (NVMe)**: Reduces jitter from IO operations. +* **Fast Memory**: Memory-intensive operations benefit from faster RAM. +* **Small Heap Size**: A modest heap (e.g., 2 GB) is sufficient since most data is off-heap. +* **Tuned Eden Space**: A large Eden space can reduce GC frequency. +For modest volumes, ~1 GB Eden size may suffice. == Network Layouts -The client and the service can be laid out in a variety of ways without changing the code. +The client and the service can be laid out in various ways without changing the code. .Ballpark latencies running on a production quality server |=== @@ -72,7 +115,7 @@ The client and the service can be laid out in a variety of ways without changing | Shared Memory | Shared Memory | 1.5 µs | 3 µs |=== -=== TCP Client, TCP Service +=== Example: TCP Client, TCP Service This approach offers the most distributed option. The typical latencies are under 20 µs with the 99%ile latency not much higher. @@ -80,58 +123,85 @@ The typical latencies are under 20 µs with the 99%ile latency not much hig [source,mermaid] .... sequenceDiagram -autonumber -Client->>Gateway: transfer -Note over Client,Gateway: via TCP, persisted -Gateway->>+Service: transfer -Note right of Gateway: via TCP -Note right of Service: processes event -Service->>-Gateway: onTransfer -Note over Service,Gateway: via TCP, persisted -Gateway->>Client: onTransfer -Note left of Gateway: via TCP + autonumber + + participant Client as Client Application + participant Gateway as Gateway
(TCP <-> Shared Memory) + participant Service as Service
(Account Manager) + + Client->>Gateway: transfer (via TCP) + Note over Client,Gateway: The request is persisted
to a Chronicle Queue + + Gateway->>+Service: transfer (via TCP) + Note right of Gateway: Gateway acts as a bridge
and persists the message + Note right of Service: Service processes the event
(update balances, etc.) + + Service->>-Gateway: onTransfer (via TCP) + Note over Service,Gateway: Response is persisted again
for audit and recovery + + Gateway->>Client: onTransfer (via TCP) + Note left of Gateway: Client receives
the result of the operation .... This can be benchmarked all-in-one with the command line properties `-Durl=tcp://localhost:1248 -DserviceUrl=tcp://:1248` running `AccountManagerBenchmarkMain` -=== TCP Client, Shared Memory Service +=== Example: TCP Client, Shared Memory Service -This approach offers the most distributed option. The typical latencies are around 10 µs with the 99%ile latency not much higher. +This approach offers the most distributed option. +The typical latencies are around 10 µs with the 99%ile latency not much higher. [source,mermaid] .... sequenceDiagram -autonumber -Client->>Gateway: transfer -Note over Client,Gateway: via TCP, persisted -Gateway->>+Service: transfer -Note right of Gateway: via Shared Memory -Note right of Service: processes event -Service->>-Gateway: onTransfer -Note over Service,Gateway: via Shared Memory, persisted -Gateway->>Client: onTransfer -Note left of Gateway: via TCP + autonumber + + participant Client as Client Application + participant Gateway as Gateway
(TCP <-> Shared Memory) + participant Service as Service
(Account Manager) + + Client->>Gateway: transfer (via TCP) + Note over Client,Gateway: The request is persisted
to a Chronicle Queue on the Gateway. + + Gateway->>+Service: transfer (via Shared Memory) + Note right of Gateway: Gateway provides low-latency
shared memory messaging + Note right of Service: Service processes the event
(e.g., debit & credit accounts) + + Service->>-Gateway: onTransfer (via Shared Memory) + Note over Service,Gateway: Response is persisted again
for audit and recovery + + Gateway->>Client: onTransfer (via TCP) + Note left of Gateway: Client receives the result
of the transfer operation .... This can be benchmarked all-in-one with the command line properties `-Durl=tcp://:1248` running `AccountManagerBenchmarkMain` -=== Shared Memory Client and Service +=== Example: Shared Memory Client and Service -This approach offers the most distributed option. The typical latencies are under 2 µs with the 99%ile latency about double this. +This approach offers the most distributed option. +The typical latencies are under 2 µs with the 99%ile latency about double this. [source,mermaid] .... sequenceDiagram -autonumber -Client->>queue: transfer -Note over Client,queue: via Shared Memory, persisted -queue->>+Service: transfer -Note right of queue: via Shared Memory -Note right of Service: processes event -Service->>-queue: onTransfer -Note over Service,queue: via Shared Memory, persisted -queue->>Client: onTransfer -Note left of queue: via Shared Memory + autonumber + + participant Client as Client Application + participant Queue as Queue
(Shared Memory Channel) + participant Service as Service
(Account Manager) + + Client->>Queue: transfer (via Shared Memory) + Note over Client,Queue: The transfer request is immediately
persisted in a Chronicle Queue
for audit and recovery. + + Queue->>+Service: transfer (via Shared Memory) + Note right of Queue: The Service reads the request
directly from the Queue. + Note right of Service: The Service processes the event
(e.g., adjust balances). + + Service->>-Queue: onTransfer (via Shared Memory) + Note over Service,Queue: The response event is
also persisted in the Queue. + + Queue->>Client: onTransfer (via Shared Memory) + Note left of Queue: The Client reads the response,
completing the round-trip
with minimal latency. + .... This can be benchmarked all-in-one with the default command line properties running `AccountManagerBenchmarkMain` @@ -140,19 +210,18 @@ This can be benchmarked all-in-one with the default command line properties runn We lay out our packages in the following manner -- `api` package for the input and output interfaces. -The input of one microservice might be the output of another microservice. -- `dto` package for POJOs (Plain Old Java Objects) that hold the data associated with each event. -- `impl` package for the service implementation and the high level classes it uses -- `util` package for separating low level helper methods and classes. +* `api` – Input and output interfaces defining the service contract. +* `dto` – Data Transfer Objects (POJOs) representing commands and events. +* `impl` – The core implementation of the account management logic. +* `util` – Utility classes for low-level operations and helpers. -For demos, we might include `main` classes, however for a production system, we use a framework Chronicle Services https://chronicle.software/services/ to handle manageability, monitoring, restart and fail over. +For demos, we might include `main` classes; however, for a production system, we use a framework https://chronicle.software/services/[Chronicle Services] to handle manageability, monitoring, restart and failover. == How to Run . Compile the source files using your preferred Java compiler. -. `AccountManagerServiceMain` runs the end service responsible for holding state and generating results of transactions - +. `AccountManagerServiceMain` runs the end service responsible for holding the state and generating results of transactions +. . `AccountManagerGatewayMain` acts as a gateway listening for TCP connections and writing to/reading from the shared memory queue the microservices uses . `AccountManagerClientMain` injects a few simple messages and waits for resulting events from the AccountManagerImpl @@ -199,13 +268,11 @@ transfer: { } ---- -== Note - -This is a basic implementation and does not handle many edge cases. -It also lacks a user-friendly interface, and the input is provided directly through YAML files. -It's intended as a demonstration of a system built on Chronicle Wire, and may not be suitable for production use without further modifications and improvements. +== Limitations and Future Work -== Contribute +This demo does not handle all edge cases and lacks a user-friendly interface. +Future enhancements may include: -We would love your contributions! -Please submit a pull request with any improvements or bug fixes you have made. +* Improved error handling +* Better user interfaces or REST/HTTP endpoints +* More robust fault tolerance and recovery strategies, possibly leveraging Chronicle Services further diff --git a/account/pom.xml b/account/pom.xml index 51b3eb6..ccffd7d 100644 --- a/account/pom.xml +++ b/account/pom.xml @@ -54,5 +54,41 @@ 4.11.0 test + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + test + + + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M9 + + false + + + + diff --git a/account/prompts/improve-code.adoc b/account/prompts/improve-code.adoc new file mode 100644 index 0000000..e79548d --- /dev/null +++ b/account/prompts/improve-code.adoc @@ -0,0 +1,72 @@ += Developer Prompt: Improving Classes for AMS + +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.0 +:doctype: book +:toc: left +:toclevels: 3 + +You are given a set of requirements and an existing implementation of an Account Management Service (AMS). +The AMS processes account-related commands (events) such as account creation, fund transfers, and checkpoint requests, and produces corresponding success/failure events. +The code currently meets the basic functional requirements but can be improved for clarity, maintainability, and robustness. + +Your task is to improve selected classes in the codebase. +Consider the following options and guidelines: + +== Guidelines + +1. **Adhere to the Provided Requirements**: +The code must continue to fulfill the requirements specified in the `Account Management Service Requirements` document. +Any changes should not break the contract defined there. + +2. **Validation and Error Handling**: +Assume that the framework validates that DTOs (Data Transfer Objects) before use. +Add a comment showing where validation would otherwise occur. +If a command is missing critical fields or contains invalid values, handle it gracefully by producing failure events rather than exceptions visible to callers. + +3. **Time Management**: +All events should use `SystemTimeProvider.CLOCK.currentTimeNanos()` to set their `sendingTime` fields, ensuring nanosecond-precision wall clock timestamps. + +4. **Logging and Comments**: +Add meaningful comments where appropriate to explain the rationale behind certain decisions, especially if the code deviates from typical patterns. +Consider using `Jvm.debug()`, `Jvm.warn()`, and `Jvm.error()` for logging. +Comments should clarify non-obvious logic, error handling decisions, or performance trade-offs. +Do not add comments for trivial logic. + +5. **Fluent Interfaces and Deep Copies**: +Preserve the fluent interface style for DTO setters to allow method chaining. +When storing new accounts, ensure that `CreateAccount` objects are deep copied before saving them to the internal map, as per the requirements. + +6. **Checkpoints and State Serialization**: +During checkpoint processing, ensure that all currently known accounts are emitted as `onCreateAccount` events. +Consider how to handle any edge cases (e.g., empty account lists). + +7. **Readability and Maintainability**: +Consider extracting common logic (e.g., target checks, currency checks, funds checks) into separate helper methods to reduce code repetition. +Make sure your class-level and method-level documentation provides a clear picture of what the code does, why, and how it aligns with the requirements. + +== Options to Consider + +* Add Javadoc to all classes and their public methods, describing the class’s role, its main responsibilities, and linking it back to the requirements. +* Introduce private helper methods to streamline complex validation or repetitive tasks. +* Use descriptive variable and method names to enhance clarity. +* Check that all failure events include a meaningful `reason` field that matches the requirements. +* Consider adding `@Override` annotations, if missing, to clarify implemented methods from interfaces. +* Add informative comments that explain why certain validations or steps are necessary, rather than just stating what the code does. +* Ensure that the codebase is consistent in its style and adheres to the project’s coding standards. +* Consider how to handle edge cases and exceptional conditions, ensuring that the code behaves predictably and correctly in all scenarios. + +== Deliverables + +Improve the existing codebase by addressing the guidelines and options provided. +Submit the updated classes with the changes you have made, along with a brief summary of the modifications you implemented and why you chose to make them. + +== Objective + +By following the above guidelines and considering the options, improve the existing codebase to be more robust, understandable, and aligned with the specified requirements. +The resulting classes should present a clean, well-documented, and maintainable code structure that clearly communicates their purpose and logic. + +== Code To Improve + +Find the code to improve below: diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc new file mode 100644 index 0000000..26ac9ba --- /dev/null +++ b/account/prompts/improve-test-data.adoc @@ -0,0 +1,159 @@ += Developer Prompt: Improving Test Cases for AMS + +Your task is to enhance an existing test configuration that utilizes YAML files for initializing system state (`_setup.yaml`), specifying input commands (`in.yaml`), and verifying expected outcomes (`out.yaml`). +The goal is to produce maintainable, clear, and requirements-aligned test cases. + +== Overview + +The testing approach involves the following files: + +* `_setup.yaml` - Initialises the system state before the test scenario begins (e.g., creating initial accounts). +* `in.yaml` - Defines the input commands (events) that the system under test will process. +* `out.yaml` - Specifies the expected events produced by the system in response to the inputs, along with helpful comments that link the outputs back to the corresponding input events. + +Below is an illustrative structure: + +.Setup (`_setup.yaml`) +---- +--- +# Create account for Alice (account #101013) starting with 15 EUR. +# Rationale: This sets up a baseline account state for subsequent operations. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +... +---- + +.Input (`in.yaml`) +---- +--- +# Transfer 10 EUR from Alice (101013) to Bob (101025). +# Scenario: This should succeed if Bob’s account is in EUR and both accounts are valid. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: Dog food +} +... +--- +# This operation requests a checkpoint. +# Checkpoints are typically used to dump or save the state of the system at a certain point in time. +# In this case, it will dump all the accounts. +checkPoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00, +} +... +---- + +[source,mermaid] +---- +sequenceDiagram + participant SetupFile as _setup.yaml + participant InputFile as in.yaml + participant TestRunner as YamlTester + participant System as System Under Test
(AMS) + participant ExpectedOutput as out.yaml + + SetupFile->>TestRunner: Load initial state instructions + Note over TestRunner: The Test Runner reads _setup.yaml
and applies initial configurations
(e.g., create accounts) + + TestRunner->>System: Initialise system state
from _setup.yaml + Note over System: System now has initial state
(e.g., Alice’s account with 15 EUR) + + InputFile->>TestRunner: Provide input
commands/events + Note over TestRunner: The Test Runner reads in.yaml
which includes operations
like transfers, checkpoints + + TestRunner->>System: Replay input events
from in.yaml + Note over System: System processes each command
and produces corresponding output events
(e.g., onTransfer, createAccountFailed) + + System->>TestRunner: Return
produced events + Note over TestRunner: The Test Runner captures all events
generated by the system
in response to input + + ExpectedOutput->>TestRunner: Provide expected events (out.yaml) + Note over TestRunner: The Test Runner checks the produced events
against the expected output
defined in out.yaml + + TestRunner->>TestRunner: Compare actual vs expected events + alt All Events Match + TestRunner->>System: Test PASSED + else Some Events Differ + TestRunner->>System: Test FAILED + end +---- + +== Guidelines + +1. **Clarity and Context**: +Add descriptive comments to `_setup.yaml` and `in.yaml` to explain each operation’s intent. +In `out.yaml`, reference the input event that caused the output. +This makes it easier for other developers to understand the test scenarios at a glance. + +2. **Time Management**: +Document that real-time tests should use `SystemTimeProvider.CLOCK.currentTimeNanos()` for `sendingTime`. +Though test files may use fixed timestamps, emphasize in comments that production environments rely on `SystemTimeProvider` for consistent, nanosecond-precision timestamps. + +3. **Validation Checks**: +Introduce failure scenarios: +* A `createAccount` command with an invalid balance (e.g., negative balance) to produce `createAccountFailed`. +* A `transfer` from a non-existent account or with insufficient funds to produce `transferFailed`. + + In `in.yaml`, comment these scenarios and in `out.yaml`, show the expected failure outputs, including a `reason` field that aligns with the system’s requirements. + +4. **Reusability and Maintenance**: +If your setup becomes complex, consider YAML anchors, aliases, or splitting large scenarios into multiple files. +Add comments linking tricky scenarios to relevant sections of the requirements document, ensuring future maintainers understand the rationale behind each test. + +5. **Coverage**: +Include scenarios that cover: +* Multiple successful account creations and transfers. +* At least one invalid `createAccount` scenario. +* At least one invalid `transfer` scenario. +* A `checkPoint` command to verify the sequence of `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events. + +6. **Naming and Organization**: +Use meaningful and specific operation descriptions. +Instead of generic comments, specify the exact accounts, currencies, and reasons. +Label scenarios (e.g., "Scenario: Insufficient Funds Transfer") to quickly identify their purpose. + +== Sections for Setup and Input Data + +- **Setup Section (`_setup.yaml`)**: +Place all initial state operations here. +Add comments that justify these initial states and their relevance to the upcoming tests. + +---- +# Example (in `_setup.yaml`): +# Creating initial accounts to ensure subsequent transfers have valid source and destination accounts. +createAccount: { ... } +... + +---- + +- **Input Section (`in.yaml`)**: +Define the sequence of commands tested. +Include both normal and edge cases, clearly tagging scenarios for quick reference. + +---- +# Example (in `in.yaml`): +# Scenario: Attempt to transfer from a non-existent account to test transferFailed event. +transfer: { ... } +... + +---- + +== Deliverables + +Enhance the existing `_setup.yaml` and `in.yaml` files according to the above guidelines. +Once updated, provide a brief summary of the changes made and the reasons behind them, focusing on improved clarity, test coverage, and alignment with requirements. diff --git a/account/prompts/requirements.adoc b/account/prompts/requirements.adoc new file mode 100644 index 0000000..495d36f --- /dev/null +++ b/account/prompts/requirements.adoc @@ -0,0 +1,183 @@ += Account Management Service Requirements + +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.1 +:doctype: book +:toc: left +:toclevels: 3 + +== Introduction + +This document specifies the functional requirements for an Account Management Service (AMS) that processes account-related commands and generates corresponding events. +The AMS is designed to be driven by incoming commands (events) such as account creation, fund transfers, and checkpoint requests. +It produces events to indicate success or failure and to provide state snapshots. + +== Terminology + +*Account*: A financial store of value characterized by an account number, owner name, currency, balance, and an optional overdraft limit. + +*Command*: An inbound request to the system (e.g., create an account, transfer funds, request checkpoint). +Commands are modelled as Events in this system. + +*Event*: An outbound notification from the system that indicates a command's state change, success, or failure. + +*Checkpoint*: A request to serialize or snapshot the current state of all accounts for audit or recovery purposes. + +== Functional Requirements + +=== Account Creation + +==== Inputs + +1. A `CreateAccount` command containing: + +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: Holds when the command was sent as a wall clock timestamp with nanosecond resolution. +* `name`: The account holder's name. +* `account`: The numeric identifier for the account (long). +* `currency`: The currency in which the account operates. +* `balance`: The initial balance of the account. +* `overdraft`: The overdraft limit allowed for the account. + +The `sendingTime` can be set using `SystemTimeProvider.CLOCK.currentTimeNanos()`. + +==== Processing + +Upon receiving a `CreateAccount` command: + +1. Validate that `target` matches this instance's configured identifier. +2. Validate that `balance` ≥ 0. +3. Validate that the `account` number does not already exist in the system. +4. If validation fails, output a `createAccountFailed` event, including the reason. +5. If validation succeeds: + +* Store the account details in a local data structure (e.g., a `Map`). +* Emit an `onCreateAccount` event with the stored `CreateAccount` details. + +==== Outputs + +* `onCreateAccount` event on success: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `CreateAccount` command +* `sendingTime`: The system's current time +* Embedded `createAccount` field containing the original request data. + +* `createAccountFailed` event on failure: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `CreateAccount` command +* `sendingTime`: The system's current time +* `reason`: A textual description of the failure (e.g., "invalid balance", "account already exists"). + +=== Transfer Funds + +==== Inputs + +1. A `Transfer` command containing: + +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: Holds when the command was sent as a wall clock timestamp with nanosecond resolution. +* `from`: The source account number. +* `to`: The destination account number. +* `currency`: The currency of the transfer. +* `amount`: The amount to transfer. +* `reference`: A reference field for the reason for the transfer or details. + +==== Processing + +Upon receiving a `Transfer` command: + +1. Validate that `target` matches this instance's identifier. +2. Verify that the `from` account exists and its currency matches the `Transfer.currency`. +3. Verify that the `to` account exists and its currency matches the `Transfer.currency`. +4. Check that the `from` account has sufficient funds (`balance + overdraft ≥ amount`). +5. If validation fails, emit a `transferFailed` event with an appropriate `reason`. +6. If valid, update both accounts: +* Deduct the `amount` from the `from` account's balance. +* Add `amount` to the `to` account's balance. +7. Emit an `onTransfer` event indicating success. + +==== Outputs + +* `onTransfer` event on success: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `Transfer` command +* `sendingTime`: The system's current time +* Embedded `transfer` field containing the original `Transfer` command data. + +* `transferFailed` event on failure: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `Transfer` command +* `sendingTime`: The system's current time +* Embedded `transfer` field containing the original request +* `reason`: A textual description of the failure (e.g., "from account doesn't exist", "insufficient funds"). + +=== Checkpoint + +==== Inputs + +1. A `CheckPoint` command containing: +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: The timestamp of when the command was sent. + +==== Processing + +Upon receiving a `CheckPoint` command: + +1. Validate that `target` matches this instance's identifier. +2. Emit a `startCheckpoint` event. +3. For every account currently held in the system: +* Emit an `onCreateAccount` event representing its current state. +4. Emit an `endCheckpoint` event. + +==== Outputs + +* `startCheckpoint` event: +* `sender`: The original `sender` of the `CheckPoint` +* `target`: The system ID (e.g., `vault`) +* `sendingTime`: The system's current time + +* A series of `onCreateAccount` events for each known account, reflecting their current state at the time of checkpoint. + +* `endCheckpoint` event: +* `sender`: The original `sender` of the `CheckPoint` +* `target`: The system ID (e.g., `vault`) +* `sendingTime`: The system's current time + +== Non-Functional Requirements + +1. **Performance**: The system should handle account lookups and updates in O(1) average time via efficient data structures (e.g., HashMap or LinkedHashMap). + +2. **Concurrency**: The system may assume single-threaded inputs. + +3. **Error Handling**: All invalid or unexpected command conditions result in failure events rather than exceptions visible to callers. + +4. **Time Management**: `sendingTime` should be based on a reliable system clock. + +== Validation and Testing + +To verify these requirements: + +1. Send a `createAccount` command with valid parameters and ensure `onCreateAccount` is emitted. +2. Send a `createAccount` command with invalid parameters (e.g., negative balance or duplicate account number) and confirm that `createAccountFailed` is emitted. +3. Perform a valid `transfer` and ensure `onTransfer` is emitted with the updated balances. +4. Attempt invalid transfers and ensure `transferFailed` events are emitted. +5. Issue a `checkPoint` command and validate that `startCheckpoint`, multiple `onCreateAccount` events (one per account), and `endCheckpoint` are produced in order. + +== Traceability + +Each requirement above directly corresponds to a portion of the Java implementation in `AccountManagerImpl.java`: + +* Account creation logic: `createAccount(CreateAccount createAccount)` +* Transfer logic: `transfer(Transfer transfer)` +* Checkpoint logic: `checkPoint(CheckPoint checkPoint)` + +Events and conditions are explicitly handled in private utility methods (e.g., `sendCreateAccountFailed`, `sendOnCreateAccount`, `sendTransferFailed`, `sendOnTransfer`). + +== Conclusion + +This document provides a high-level specification of the required functionalities and expected behaviours of the Account Management Service. +Implementing these requirements should align with the Java code structure and produce consistent events for all supported operations. diff --git a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java index b8ce7f7..c856fc0 100644 --- a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java +++ b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java @@ -25,37 +25,6 @@ import static net.openhft.chronicle.core.time.SystemTimeProvider.CLOCK; -/* --Xmx64m -Dthroughput=100000 -DrunTime=30 -Dbuffered=false -Durl=tcp://localhost:1248 -DaccountForCoordinatedOmission=false --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 10.99 10.99 11.02 11.02 10.99 0.19 -90.0: 18.02 17.89 15.70 11.22 11.12 28.86 -99.0: 20.96 19.94 15.86 15.86 15.79 14.89 -99.7: 34.11 21.02 16.11 15.92 15.89 17.73 -99.9: 42.18 22.62 16.67 16.34 16.18 21.00 -99.97: 2021.38 26.08 18.21 17.44 17.25 25.45 -99.99: 5480.45 60.48 44.48 22.82 47.68 52.39 -99.997: 6938.62 332.29 426.50 80.26 451.07 75.49 -worst: 7593.98 728.06 820.22 303.62 838.66 54.02 - -Windows 11 laptop, i7-1360P, Java 11 --Dthroughput=20000 -Durl=internal:// --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 1.60 1.70 1.60 1.60 1.60 3.84 -90.0: 2.10 2.10 2.10 2.10 2.00 3.16 -99.0: 23.39 22.62 22.18 20.70 17.12 17.65 -99.7: 155.90 168.19 191.74 177.92 170.24 8.54 -99.9: 857.09 723.97 824.32 816.13 764.93 8.46 - --Dthroughput=20000 --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 24.93 24.67 24.93 24.80 24.93 0.69 -90.0: 37.95 35.26 38.34 35.52 35.14 5.72 -99.0: 1198.08 250.62 1243.14 469.50 477.70 72.53 - */ @SuppressWarnings("deprecation") public class AccountManagerBenchmarkMain { public static final int THROUGHPUT = Integer.getInteger("throughput", OS.isLinux() ? 100_000 : 10_000); @@ -84,7 +53,7 @@ public static void main(String[] args) throws InterruptedException, MalformedURL // This ExecutorService is used for running the client/gateway/service in the current process ExecutorService es = Executors.newCachedThreadPool(new AffinityThreadFactory("test")); - // Initialize a new instance of AccountManagerServiceMain. This is the main service for managing accounts. + // Initialise a new instance of AccountManagerServiceMain. This is the main service for managing accounts. AccountManagerServiceMain service = null; // Check if the host part of the URL is empty. If it is, that means we are running the service locally. diff --git a/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java b/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java index c55f18a..69cb476 100644 --- a/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java +++ b/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java @@ -14,6 +14,7 @@ import run.chronicle.account.util.LogsAccountManagerOut; import java.util.concurrent.atomic.AtomicBoolean; + /** * This class acts as the main entry point for the AccountManagerClient. * It creates a client which connects to a Chronicle server and performs various actions. diff --git a/account/src/main/java/run/chronicle/account/benchmark-results.txt b/account/src/main/java/run/chronicle/account/benchmark-results.txt new file mode 100644 index 0000000..45fb9d1 --- /dev/null +++ b/account/src/main/java/run/chronicle/account/benchmark-results.txt @@ -0,0 +1,29 @@ +-Xmx64m -Dthroughput=100000 -DrunTime=30 -Dbuffered=false -Durl=tcp://localhost:1248 -DaccountForCoordinatedOmission=false +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 10.99 10.99 11.02 11.02 10.99 0.19 +90.0: 18.02 17.89 15.70 11.22 11.12 28.86 +99.0: 20.96 19.94 15.86 15.86 15.79 14.89 +99.7: 34.11 21.02 16.11 15.92 15.89 17.73 +99.9: 42.18 22.62 16.67 16.34 16.18 21.00 +99.97: 2021.38 26.08 18.21 17.44 17.25 25.45 +99.99: 5480.45 60.48 44.48 22.82 47.68 52.39 +99.997: 6938.62 332.29 426.50 80.26 451.07 75.49 +worst: 7593.98 728.06 820.22 303.62 838.66 54.02 + +Windows 11 laptop, i7-1360P, Java 11 +-Dthroughput=20000 -Durl=internal:// +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 1.60 1.70 1.60 1.60 1.60 3.84 +90.0: 2.10 2.10 2.10 2.10 2.00 3.16 +99.0: 23.39 22.62 22.18 20.70 17.12 17.65 +99.7: 155.90 168.19 191.74 177.92 170.24 8.54 +99.9: 857.09 723.97 824.32 816.13 764.93 8.46 + +-Dthroughput=20000 +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 24.93 24.67 24.93 24.80 24.93 0.69 +90.0: 37.95 35.26 38.34 35.52 35.14 5.72 +99.0: 1198.08 250.62 1243.14 469.50 477.70 72.53 diff --git a/account/src/main/java/run/chronicle/account/domain/AccountService.java b/account/src/main/java/run/chronicle/account/domain/AccountService.java new file mode 100644 index 0000000..5f42c0e --- /dev/null +++ b/account/src/main/java/run/chronicle/account/domain/AccountService.java @@ -0,0 +1,94 @@ +package run.chronicle.account.domain; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import run.chronicle.account.dto.CreateAccount; +import run.chronicle.account.dto.Transfer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Encapsulates all domain logic related to account creation, validation, and funds transfers. + * This service is responsible for: + * - Managing the in-memory store of accounts. + * - Validating currency, balances, and overdraft limits. + * - Updating account balances upon successful transfers. + */ +public class AccountService { + + // In-memory account store + private final Map accountsMap = new LinkedHashMap<>(); + + /** + * Attempts to create a new account. Throws an InvalidMarshallableException if invalid, + * or returns a reason string if creation should fail due to domain rules. + */ + public String tryCreateAccount(CreateAccount createAccount, long expectedTargetId) throws InvalidMarshallableException { + // Validate DTO properties + createAccount.validate(); + + if (createAccount.target() != expectedTargetId) { + return "target mismatch"; + } + + if (!(createAccount.balance() >= 0)) { + return "invalid balance"; + } + + Long accountNumber = createAccount.account(); + if (accountsMap.containsKey(accountNumber)) { + return "account already exists"; + } + + // If valid, store the account + accountsMap.put(accountNumber, createAccount.deepCopy()); + return null; // Indicates success + } + + /** + * Attempts to perform a transfer. Returns a reason string if transfer fails domain checks, + * or null if the transfer is successful. + */ + public String tryTransfer(Transfer transfer, long expectedTargetId) throws InvalidMarshallableException { + // Validate DTO properties + transfer.validate(); + + if (transfer.target() != expectedTargetId) { + return "target mismatch"; + } + + CreateAccount fromAccount = accountsMap.get(transfer.from()); + if (fromAccount == null) { + return "from account doesn't exist"; + } + if (fromAccount.currency() != transfer.currency()) { + return "from account currency doesn't match"; + } + + double amount = transfer.amount(); + if (fromAccount.balance() + fromAccount.overdraft() < amount) { + return "insufficient funds"; + } + + CreateAccount toAccount = accountsMap.get(transfer.to()); + if (toAccount == null) { + return "to account doesn't exist"; + } + if (toAccount.currency() != transfer.currency()) { + return "to account currency doesn't match"; + } + + // Perform the transfer + fromAccount.balance(fromAccount.balance() - amount); + toAccount.balance(toAccount.balance() + amount); + + return null; // Indicates success + } + + /** + * Provides access to all accounts for checkpoint operations. + */ + public Map getAllAccounts() { + return accountsMap; + } +} diff --git a/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java b/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java index f9ed76f..8a819ba 100644 --- a/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java +++ b/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java @@ -30,20 +30,25 @@ * It provides common properties like sender, target, and sending time, * along with fluent setter methods for easy chaining. * + *

Concrete subclasses must ensure that a valid sender, target, and sendingTime + * are provided before use. Validation throws an {@link InvalidMarshallableException} + * if any required field is not set.

+ * * @param the type of the event extending {@code AbstractEvent} */ @SuppressWarnings("unchecked") public abstract class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { + @ShortText - private long sender; // sender represented in ShortText + private long sender; @ShortText - private long target; // target represented in ShortText + private long target; @NanoTime - private long sendingTime; // sendingTime represented as a unique timestamp in nanoseconds + private long sendingTime; /** * Retrieves the sender identifier. @@ -86,7 +91,7 @@ public E target(long target) { } /** - * Retrieves the sending time. + * Retrieves the sending wall clock time since epoch. * * @return the sending time in nanoseconds */ @@ -95,7 +100,7 @@ public long sendingTime() { } /** - * Sets the sending time and returns the updated object. + * Sets the sending wall clock time since epoch and returns the updated object. * * @param sendingTime the sending time to set * @return the updated object @@ -106,17 +111,18 @@ public E sendingTime(long sendingTime) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required fields (sender, target, and sendingTime) have been set. + * If any field is unset (0 indicates an unset value), it throws an {@link InvalidMarshallableException}. * * @throws InvalidMarshallableException If any of these properties is not set */ @Override public void validate() throws InvalidMarshallableException { if (sender == 0) - throw new InvalidMarshallableException("sender must be set"); // ensure sender is set + throw new InvalidMarshallableException("sender must be set"); if (target == 0) - throw new InvalidMarshallableException("target must be set"); // ensure target is set + throw new InvalidMarshallableException("target must be set"); if (sendingTime == 0) - throw new InvalidMarshallableException("sendingTime must be set"); // ensure sendingTime is set + throw new InvalidMarshallableException("sendingTime must be set"); } } diff --git a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java index 0c547ba..f8b4309 100644 --- a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java +++ b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java @@ -19,8 +19,22 @@ /** - * The {@code CheckPoint} class represents a request to dump all the current state of the system. - * It extends {@link AbstractEvent} to include common event properties. + * The {@code CheckPoint} class represents a request to produce a snapshot + * (or "dump") of the entire current system state at a given moment in time. + * This includes all accounts and their balances, ensuring that the state + * can be recorded for audit, recovery, or analysis. + *

+ * A valid {@code CheckPoint} event must have all these fields set; validation + * is performed automatically when the event is processed. + * + *

Usage example: + *

{@code
+ * CheckPoint cp = new CheckPoint()
+ *     .sender(gw2Id)
+ *     .target(vaultId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos());
+ * }
*/ public class CheckPoint extends AbstractEvent { + // The CheckPoint event leverages the common fields and validation logic provided by AbstractEvent. } diff --git a/account/src/main/java/run/chronicle/account/dto/CreateAccount.java b/account/src/main/java/run/chronicle/account/dto/CreateAccount.java index d21b604..3ba80f2 100644 --- a/account/src/main/java/run/chronicle/account/dto/CreateAccount.java +++ b/account/src/main/java/run/chronicle/account/dto/CreateAccount.java @@ -22,18 +22,36 @@ import net.openhft.chronicle.wire.converter.ShortText; /** - * Represents the event of creating a new account. - * This class extends {@link AbstractEvent} and adds properties specific to account creation, - * such as the account holder's name, account number, currency, balance, and overdraft limit. - * Setters are designed using the fluent interface pattern for method chaining. + * Represents an event for creating a new account. This event includes all necessary details + * about the account to be created, such as: + *
    + *
  • name: The account holder's name.
  • + *
  • account: A unique identifier for this account.
  • + *
  • currency: The currency code for the account (stored as an integer code).
  • + *
  • balance: The initial balance of the account. Must be ≥ 0.
  • + *
  • overdraft: The overdraft limit for the account. Must be ≥ 0.
  • + *
+ * + *

This class uses a fluent interface style for setter methods, allowing for chaining: + *

{@code
+ * CreateAccount event = new CreateAccount()
+ *     .sender(gatewayId)
+ *     .target(serviceId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .name("Alice")
+ *     .account(101013L)
+ *     .currency(EUR_CODE)
+ *     .balance(1000.0)
+ *     .overdraft(100.0);
+ * }
*/ public class CreateAccount extends AbstractEvent { - private String name; // Name associated with the account - private long account; // Account identifier + private String name; + private long account; @ShortText - private int currency; // Currency for the account - private double balance; // Initial Balance of the account - private double overdraft; // Overdraft limit of the account + private int currency; + private double balance; + private double overdraft; /** * Retrieves the account identifier. @@ -76,7 +94,8 @@ public CreateAccount name(String name) { } /** - * Retrieves the currency code. + * Returns the currency code of the account. + * This is typically an integer code mapping to a currency (e.g., EUR, USD). * * @return the currency code */ @@ -85,10 +104,11 @@ public int currency() { } /** - * Sets the currency code. + * Sets the currency code and returns this instance. + * It is expected that the caller uses predefined integer codes for currencies. * * @param currency the currency code to set (e.g., "EUR", "USD") - * @return this object for method chaining + * @return this instance for method chaining */ public CreateAccount currency(int currency) { this.currency = currency; @@ -105,7 +125,8 @@ public double balance() { } /** - * Sets the initial balance of the account. + * Sets the initial balance of the account and returns this instance. + * The balance must be ≥ 0. * * @param balance the balance to set * @return this object for method chaining @@ -125,7 +146,8 @@ public double overdraft() { } /** - * Sets the overdraft limit of the account. + * Sets the overdraft limit of the account and returns this instance. + * The overdraft limit must be ≥ 0. * * @param overdraft the overdraft limit to set * @return this object for method chaining @@ -136,7 +158,8 @@ public CreateAccount overdraft(double overdraft) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required properties (sender, target, sendingTime, name, account, currency, balance, overdraft) + * have been set correctly. * * @throws InvalidMarshallableException if validation fails */ diff --git a/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java b/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java index 155454f..6955036 100644 --- a/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java +++ b/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java @@ -21,15 +21,27 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * This class, CreateAccountFailed, is an extension of AbstractEvent used to represent a situation - * where an attempt to create an account has failed. This class adds two properties to the event: - * a reference to the original CreateAccount object that failed, and a reason string describing why - * the account creation failed. As with other classes in this system, it uses a fluent style of - * setters, and includes a validate method to ensure all necessary properties have been set. + * Represents an event indicating that an attempt to create an account has failed. + * This event includes: + *
    + *
  • createAccount: The original {@link CreateAccount} request that failed.
  • + *
  • reason: A descriptive message explaining why the creation failed.
  • + *
+ * + *

Example usage:

+ *
{@code
+ * CreateAccountFailed caf = new CreateAccountFailed()
+ *     .sender(vaultId)
+ *     .target(gwId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .createAccount(originalRequest)
+ *     .reason("account already exists");
+ * }
*/ public class CreateAccountFailed extends AbstractEvent { - private CreateAccount createAccount; // Reference to the CreateAccount instance that failed - private String reason; // The reason for the failure + + private CreateAccount createAccount; + private String reason; /** * @return the CreateAccount instance that failed @@ -71,6 +83,8 @@ public CreateAccountFailed reason(String reason) { /** * Validates that all necessary properties have been set and are valid. + * Ensures that the original {@link CreateAccount} request and the reason are present + * and valid. If validation fails, an {@link InvalidMarshallableException} is thrown. * * @throws InvalidMarshallableException if validation fails */ diff --git a/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java b/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java index 867d63c..2cb683a 100644 --- a/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java +++ b/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java @@ -21,14 +21,21 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * The class OnCreateAccount is an extension of the AbstractEvent class, - * and it represents an event that occurs when a CreateAccount action has successfully occurred. - * The class contains a reference to the CreateAccount instance that initiated the event. - * This class follows the convention of using a fluent style for its setters, - * and it also includes a validate method to make sure that the createAccount field has been properly set. + * Represents an event indicating that an account was successfully created. + * This event references the original {@link CreateAccount} request that + * led to the successful creation. + * + *

Example usage: + *

{@code
+ * OnCreateAccount event = new OnCreateAccount()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .createAccount(originalRequest);
+ * }
*/ public class OnCreateAccount extends AbstractEvent { - private CreateAccount createAccount; // The CreateAccount instance that triggered this event + private CreateAccount createAccount; /** * Retrieves the {@link CreateAccount} instance that triggered this event. @@ -51,18 +58,22 @@ public OnCreateAccount createAccount(CreateAccount createAccount) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required properties are set and valid. This includes: + *
    + *
  • All fields from the superclass (sender, target, sendingTime)
  • + *
  • A non-null {@link CreateAccount} instance
  • + *
  • Validation of the {@code CreateAccount} instance itself
  • + *
* - * @throws InvalidMarshallableException if validation fails + * @throws InvalidMarshallableException if validation fails for this event */ @Override public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (createAccount == null) { - throw new InvalidMarshallableException("CreateAccount must be set"); - } else { - createAccount.validate(); // Validate the CreateAccount instance + throw new InvalidMarshallableException("Invalid OnCreateAccount: 'createAccount' must not be null."); } + createAccount.validate(); // Validate the associated CreateAccount object } } diff --git a/account/src/main/java/run/chronicle/account/dto/OnTransfer.java b/account/src/main/java/run/chronicle/account/dto/OnTransfer.java index 5536111..d23a9c2 100644 --- a/account/src/main/java/run/chronicle/account/dto/OnTransfer.java +++ b/account/src/main/java/run/chronicle/account/dto/OnTransfer.java @@ -21,15 +21,21 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * Represents an event that occurs when a {@code Transfer} action takes place. - * This class extends {@link AbstractEvent} and encapsulates a reference to - * the {@link Transfer} instance that initiated this event. - *

- * The class follows the Fluent Interface pattern for setter methods, - * allowing for method chaining. + * Represents an event indicating that a funds transfer has taken place. + * This class extends {@link AbstractEvent}, adding a reference to the + * {@link Transfer} instance that initiated the event. + * + *

The class follows a fluent interface pattern for setter methods: + *

{@code
+ * OnTransfer event = new OnTransfer()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .transfer(transferRequest);
+ * }
*/ public class OnTransfer extends AbstractEvent { - private Transfer transfer; // The Transfer instance that triggered this event + private Transfer transfer; /** * Retrieves the {@link Transfer} instance that triggered this event. @@ -52,7 +58,9 @@ public OnTransfer transfer(Transfer transfer) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required properties have been set and are valid. This includes + * the fields inherited from {@link AbstractEvent} (sender, target, sendingTime) and + * the {@code Transfer} instance itself. * * @throws InvalidMarshallableException If any of these properties is not set */ @@ -61,17 +69,16 @@ public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (transfer == null) { - throw new InvalidMarshallableException("Transfer must be set"); - } else { - transfer.validate(); // Validate the Transfer instance + throw new InvalidMarshallableException("Invalid OnTransfer event: 'transfer' must not be null."); } + transfer.validate(); } /** - * Overridden to specify the message format. In this case, it uses a lower level binary format, - * not a self-describing message. + * Indicates that this event does not use a self-describing message format and instead + * relies on a more compact binary representation. * - * @return {@code false} as it does not use a self-describing message + * @return {@code false} as this event does not use a self-describing message format */ @Override public boolean usesSelfDescribingMessage() { diff --git a/account/src/main/java/run/chronicle/account/dto/Transfer.java b/account/src/main/java/run/chronicle/account/dto/Transfer.java index abfd525..bf9f3e3 100644 --- a/account/src/main/java/run/chronicle/account/dto/Transfer.java +++ b/account/src/main/java/run/chronicle/account/dto/Transfer.java @@ -21,17 +21,29 @@ import net.openhft.chronicle.bytes.Bytes; import net.openhft.chronicle.core.io.InvalidMarshallableException; import net.openhft.chronicle.wire.converter.ShortText; + /** - * The Transfer class extends AbstractEvent and represents a transfer event in the banking system. - * It encapsulates all necessary details of a transfer, including the account numbers of both sender and receiver, the currency, the amount, and a reference to the transaction details. Like the previous classes, - * it also provides a fluent interface for setters and includes a validate method to ensure all required fields are set. + * Represents a funds transfer operation between two accounts. + * + *

Example usage:

+ *
{@code
+ * Transfer tx = new Transfer()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .from(101013L)
+ *     .to(101025L)
+ *     .currency(EUR_CODE)
+ *     .amount(10.0)
+ *     .reference(Bytes.from("Payment for services"));
+ * }
*/ public class Transfer extends AbstractEvent { - private long from, to; // The account numbers for the transfer + private long from, to; @ShortText - private int currency; // The currency of the transfer, represented in ShortText format - private double amount; // The amount to be transferred - private Bytes reference = Bytes.allocateElasticOnHeap(); // Reference to the transaction details + private int currency; + private double amount; + private final Bytes reference = Bytes.allocateElasticOnHeap(); /** * Retrieves the sender's account number. @@ -83,7 +95,8 @@ public int currency() { } /** - * Sets the currency code of the transfer. + * Sets the currency code of the transfer and returns this instance. + * The currency code should typically map to a known currency. * * @param currency the currency code to set (e.g., "EUR", "USD") * @return this object for method chaining @@ -114,7 +127,8 @@ public Transfer amount(double amount) { } /** - * Retrieves the reference to the transaction details. + * Returns the reference data associated with this transfer. + * This could be a note, a reference number, or any additional context. * * @return the transaction reference */ @@ -123,7 +137,8 @@ public Bytes reference() { } /** - * Sets the reference to the transaction details. + * Sets the reference details for this transfer and returns this instance. + * The provided {@code Bytes} data is appended after clearing the existing reference. * * @param reference the reference to the transaction details * @return the updated object @@ -134,9 +149,17 @@ public Transfer reference(Bytes reference) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required fields have been set and are valid. + * This includes: + *
    + *
  • from: must be nonzero
  • + *
  • to: must be nonzero
  • + *
  • currency: must be nonzero
  • + *
  • amount: must be positive
  • + *
  • reference: must be non-null and non-empty
  • + *
* - * @throws InvalidMarshallableException If any of these properties is not set + * @throws InvalidMarshallableException if any validation check fails */ @Override public void validate() throws InvalidMarshallableException { @@ -154,10 +177,9 @@ public void validate() throws InvalidMarshallableException { } /** - * Overridden to specify the message format. In this case, it uses a lower level binary format, - * not a self-describing message. + * Specifies that this event uses a lower-level binary format rather than a self-describing message. * - * @return false as it does not use a self-describing message. + * @return {@code false}, indicating a non-self-describing message format */ @Override public boolean usesSelfDescribingMessage() { diff --git a/account/src/main/java/run/chronicle/account/dto/TransferFailed.java b/account/src/main/java/run/chronicle/account/dto/TransferFailed.java index 57c46d2..39fec4b 100644 --- a/account/src/main/java/run/chronicle/account/dto/TransferFailed.java +++ b/account/src/main/java/run/chronicle/account/dto/TransferFailed.java @@ -23,11 +23,19 @@ /** * The TransferFailed class is a type of AbstractEvent that represents a failed transfer operation in the system. * It contains the Transfer object that failed and the reason for the failure. - * Like other classes, it also follows the Fluent Interface pattern for setters, allowing chaining of method calls. + *

Example usage:

+ *
{@code
+ * TransferFailed event = new TransferFailed()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .transfer(originalTransfer)
+ *     .reason("Insufficient funds");
+ * }
*/ public class TransferFailed extends AbstractEvent { - private Transfer transfer; // The original transfer that failed - private String reason; // The reason for the failure + private Transfer transfer; + private String reason; /** * Retrieves the original {@link Transfer} that failed. @@ -70,21 +78,26 @@ public TransferFailed reason(String reason) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required fields have been set and are valid. This includes: + *
    + *
  • All fields from {@link AbstractEvent} (sender, target, sendingTime)
  • + *
  • A non-null {@link Transfer} instance, which is itself validated
  • + *
  • A non-null reason string
  • + *
* - * @throws InvalidMarshallableException if validation fails + * @throws InvalidMarshallableException if any required field is missing or invalid */ @Override public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (transfer == null) { - throw new InvalidMarshallableException("Transfer must be set"); - } else { - transfer.validate(); // Validate the Transfer instance + throw new InvalidMarshallableException("Invalid TransferFailed: 'transfer' must not be null."); } + transfer.validate(); // Validate the Transfer object - if (reason == null) - throw new InvalidMarshallableException("reason must be set"); // Ensure 'reason' is set + if (reason == null) { + throw new InvalidMarshallableException("Invalid TransferFailed: 'reason' must not be null."); + } } } diff --git a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java index 4bead7a..d233661 100644 --- a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java +++ b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java @@ -22,176 +22,77 @@ import net.openhft.chronicle.wire.SelfDescribingMarshallable; import run.chronicle.account.api.AccountManagerIn; import run.chronicle.account.api.AccountManagerOut; +import run.chronicle.account.domain.AccountService; import run.chronicle.account.dto.*; -import java.util.LinkedHashMap; -import java.util.Map; - import static net.openhft.chronicle.core.time.SystemTimeProvider.CLOCK; /** - * AccountManagerImpl is the implementation of the AccountManagerIn interface. - * It handles account creation, money transfers and checkpoints in the account management system. + * This class now primarily orchestrates the handling of events, delegating all + * domain logic (account validation, fund transfers, currency checks) to the + * AccountService. */ -public class AccountManagerImpl - extends SelfDescribingMarshallable - implements AccountManagerIn { - private transient final AccountManagerOut out; - // use a primitive long map - private final Map accountsMap = new LinkedHashMap<>(); - // DTOs for events out +public class AccountManagerImpl extends SelfDescribingMarshallable implements AccountManagerIn { + private final AccountManagerOut out; + private final AccountService accountService; + + // Reusable event objects private final OnCreateAccount onCreateAccount = new OnCreateAccount(); private final CreateAccountFailed createAccountFailed = new CreateAccountFailed(); private final OnTransfer onTransfer = new OnTransfer(); private final TransferFailed transferFailed = new TransferFailed(); + private long id; - /** - * The constructor for the AccountManagerImpl class. - * - * @param out An instance of AccountManagerOut, which handles output events. - */ public AccountManagerImpl(AccountManagerOut out) { + this(out, new AccountService()); + } + + public AccountManagerImpl(AccountManagerOut out, AccountService accountService) { this.out = out; + this.accountService = accountService; } - /** - * Sets the id of the AccountManagerImpl instance. - * - * @param id A long representing the id to be set. - * @return This AccountManagerImpl instance. - */ public AccountManagerImpl id(long id) { this.id = id; return this; } - /** - * Handles account creation. - * - * @param createAccount An instance of CreateAccount containing details of the account to be created. - * @throws InvalidMarshallableException If there's an error during the process. - */ @Override public void createAccount(CreateAccount createAccount) throws InvalidMarshallableException { - // Verify if the account creation request is intended for this instance by checking the target of the request against the id of this instance - // If they don't match, a failure message is sent with the reason "target mismatch" and the method returns - if (createAccount.target() != id) { - sendCreateAccountFailed(createAccount, "target mismatch"); - return; - } - - // Verify if the initial balance for the account is greater than or equal to 0 - // If it isn't, a failure message is sent with the reason "invalid balance" and the method returns - if (!(createAccount.balance() >= 0)) { - sendCreateAccountFailed(createAccount, "invalid balance"); - return; - } - - // Get the account number for the new account - Long account = createAccount.account(); - - // Check if the account already exists in the accounts map - // If it does, a failure message is sent with the reason "account already exists" and the method returns - if (accountsMap.containsKey(account)) { - sendCreateAccountFailed(createAccount, "account already exists"); - return; + String failureReason = accountService.tryCreateAccount(createAccount, id); + if (failureReason == null) { + sendOnCreateAccount(createAccount); + } else { + sendCreateAccountFailed(createAccount, failureReason); } - - // If all checks pass, create a copy of the CreateAccount object and add it to the accounts map - // This is to ensure we retain a version of the data, even if the original CreateAccount object changes later - accountsMap.put(account, createAccount.deepCopy()); - - // Send a confirmation message indicating the account was successfully created - sendOnCreateAccount(createAccount); } - - /** - * Handles transfers between accounts. - * - * @param transfer An instance of Transfer containing details of the transfer to be performed. - */ @Override public void transfer(Transfer transfer) { - // Verify if the transfer is intended for this instance by checking the target of the transfer against the id of this instance - // If it doesn't match, a failure message is sent with the reason "target mismatch" and the method returns - if (transfer.target() != id) { - sendTransferFailed(transfer, "target mismatch"); - return; - } - - // Get the account from which funds are to be transferred - // If it doesn't exist, a failure message is sent with the reason "from account doesn't exist" and the method returns - CreateAccount fromAccount = accountsMap.get(transfer.from()); - if (fromAccount == null) { - sendTransferFailed(transfer, "from account doesn't exist"); - return; + try { + String failureReason = accountService.tryTransfer(transfer, id); + if (failureReason == null) { + sendOnTransfer(transfer); + } else { + sendTransferFailed(transfer, failureReason); + } + } catch (InvalidMarshallableException e) { + sendTransferFailed(transfer, e.getMessage()); } - - // Check if the currency of the transfer matches the currency of the "from" account - // If they don't match, a failure message is sent with the reason "from account currency doesn't match" and the method returns - if (fromAccount.currency() != transfer.currency()) { - sendTransferFailed(transfer, "from account currency doesn't match"); - return; - } - - double amount = transfer.amount(); - - // Check if the balance of the "from" account is sufficient to perform the transfer - // If it isn't, a failure message is sent with the reason "insufficient funds" and the method returns - if (fromAccount.balance() + fromAccount.overdraft() < amount) { - sendTransferFailed(transfer, "insufficient funds"); - return; - } - - // Get the account to which funds are to be transferred - // If it doesn't exist, a failure message is sent with the reason "to account doesn't exist" and the method returns - CreateAccount toAccount = accountsMap.get(transfer.to()); - if (toAccount == null) { - sendTransferFailed(transfer, "to account doesn't exist"); - return; - } - - // Check if the currency of the transfer matches the currency of the "to" account - // If they don't match, a failure message is sent with the reason "to account currency doesn't match" and the method returns - if (toAccount.currency() != transfer.currency()) { - sendTransferFailed(transfer, "to account currency doesn't match"); - return; - } - - // Perform the transfer: deduct the amount from the "from" account and add it to the "to" account - fromAccount.balance(fromAccount.balance() - amount); - toAccount.balance(toAccount.balance() + amount); - - // Send a confirmation message indicating the transfer was successful - sendOnTransfer(transfer); } - /** - * Handles checkpointing in the account management system. - * - * @param checkPoint An instance of CheckPoint which provides details of the checkpoint. - */ @Override public void checkPoint(CheckPoint checkPoint) { - // Check if the checkpoint target matches the ID of this instance. If it does not match, ignore this checkpoint. - if (checkPoint.target() != id) - return; // ignored + if (checkPoint.target() != id) { + // Ignoring checkpoint as target does not match + return; + } - // Start the checkpoint process. This is typically when we serialize and store the state of the system - // to an output stream (out). CheckPoint instance contains details about the checkpoint. out.startCheckpoint(checkPoint); - - // Iterate over all accounts in the accountsMap. For each account, send a "create account" event - // This essentially stores the current state of each account to the output stream. This is useful in case - // we need to restore the state of the system from this checkpoint in the future. - for (CreateAccount createAccount : accountsMap.values()) { - sendOnCreateAccount(createAccount); + for (CreateAccount ca : accountService.getAllAccounts().values()) { + sendOnCreateAccount(ca); } - - // End the checkpoint process. This is typically when we complete the serialization of the system state - // and finalize the checkpoint data in the output stream. out.endCheckpoint(checkPoint); } diff --git a/account/src/main/java/run/chronicle/account/util/ErrorListener.java b/account/src/main/java/run/chronicle/account/util/ErrorListener.java index 9672cd8..cbd3ae9 100644 --- a/account/src/main/java/run/chronicle/account/util/ErrorListener.java +++ b/account/src/main/java/run/chronicle/account/util/ErrorListener.java @@ -18,16 +18,40 @@ package run.chronicle.account.util; /** - * This interface provides a method to handle JVM errors. - * Implementations of this interface will define how these errors are handled. + * A functional interface for handling critical JVM-level errors in the system. + * Implementations can define custom error-handling strategies such as logging, + * triggering alerts, or performing cleanup and shutdown procedures. + *

+ * Using a replicated queue, this can be done on another machine to avoid + * the impact of GC or IO the monitoring system might have. + * + *

Example usage: + *

+ * public class LoggingErrorListener implements ErrorListener {
+ *     public void jvmError(String msg) {
+ *         // Log the error to monitoring system, a file, or console
+ *         logger.error(msg);
+ *     }
+ * }
+ * + *

By providing different {@code ErrorListener} implementations, the system + * can adapt to various environments (production vs. development) or integrate + * with different error-handling frameworks. */ +@FunctionalInterface public interface ErrorListener { /** - * This method handles JVM errors. - * It takes a String message which provides details about the error. + * Handles a critical JVM-level error. + *

+ * Implementations may choose to: + *

    + *
  • Log the error message to files or monitoring systems.
  • + *
  • Send alerts to administrators.
  • + *
  • Trigger a controlled shutdown or cleanup process.
  • + *
* - * @param msg a String providing details about the JVM error. + * @param msg a human-readable message providing details about the encountered JVM error. */ void jvmError(String msg); } diff --git a/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java b/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java index dda222d..726b204 100644 --- a/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java +++ b/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java @@ -1,3 +1,20 @@ +/* + * Copyright 2016-2022 chronicle.software + * + * https://chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package run.chronicle.account.util; import net.openhft.chronicle.core.Jvm; @@ -5,39 +22,80 @@ import run.chronicle.account.dto.*; /** - * Mock interface implementation of AccountManagerOut that logs everything + * A mock implementation of {@link AccountManagerOut} that logs all received events. + *

+ * This class is useful for testing, demonstration, or diagnostic purposes. It prints + * events to the console (via {@link Jvm} logging) without performing any real actions. + * This allows developers to trace the flow of events through the system and verify + * that commands and results are being produced as expected. */ public class LogsAccountManagerOut implements AccountManagerOut { + + /** + * Logs the start of a checkpoint operation. + * + * @param checkPoint The checkpoint command indicating that a state snapshot should begin. + */ @Override public void startCheckpoint(CheckPoint checkPoint) { Jvm.debug().on(getClass(), "startCheckpoint " + checkPoint); } + /** + * Logs the end of a checkpoint operation. + * + * @param checkPoint The checkpoint command indicating that the state snapshot should conclude. + */ @Override public void endCheckpoint(CheckPoint checkPoint) { Jvm.debug().on(getClass(), "endCheckpoint " + checkPoint); } + /** + * Logs a successful account creation event. + * + * @param onCreateAccount The event confirming that an account has been created. + */ @Override public void onCreateAccount(OnCreateAccount onCreateAccount) { Jvm.debug().on(getClass(), "onCreateAccount " + onCreateAccount); } + /** + * Logs a failed account creation attempt. + * + * @param createAccountFailed The event detailing why an account creation failed. + */ @Override public void createAccountFailed(CreateAccountFailed createAccountFailed) { Jvm.warn().on(getClass(), "createAccountFailed " + createAccountFailed); } + /** + * Logs a successful funds transfer event. + * + * @param onTransfer The event confirming that a transfer has completed successfully. + */ @Override public void onTransfer(OnTransfer onTransfer) { Jvm.debug().on(getClass(), "onTransfer " + onTransfer); } + /** + * Logs a failed transfer attempt. + * + * @param transferFailed The event detailing why a funds transfer failed. + */ @Override public void transferFailed(TransferFailed transferFailed) { Jvm.warn().on(getClass(), "transferFailed " + transferFailed); } + /** + * Logs a critical JVM-level error event. + * + * @param msg A descriptive message about the JVM error encountered. + */ @Override public void jvmError(String msg) { Jvm.error().on(getClass(), "jvmError " + msg); diff --git a/account/src/test/java/run/chronicle/account/AccountsTest.java b/account/src/test/java/run/chronicle/account/AccountsTest.java index d2d69ec..2c08ce3 100644 --- a/account/src/test/java/run/chronicle/account/AccountsTest.java +++ b/account/src/test/java/run/chronicle/account/AccountsTest.java @@ -36,19 +36,32 @@ import static org.junit.Assert.assertEquals; /** - * This class AccountsTest is a test class that uses - * JUnit's Parameterized runner to run multiple tests with different parameters. - * The test parameters are set up in the parameters method - * and are used to create an instance of YamlTester for each test. - * Each test runs through the runTester method which sets the system clock to a specific time, - * runs the test, and checks the output. - * After each test, the system clock is reset to its default state in the tearDown method. + * The {@code AccountsTest} class uses JUnit's Parameterized runner to execute a suite + * of YAML-driven tests for the Account Management Service (AMS). Each test scenario is + * defined by YAML files representing inputs and expected outputs. The tests verify that + * the {@link AccountManagerImpl} responds correctly to various commands, including edge cases + * introduced via "agitators" that manipulate or remove fields to test the system's resilience. + * + *

Key Features: + *

    + *
  • Parameterized Testing: Multiple scenarios defined in YAML are run using + * the same code, providing comprehensive coverage of both normal and abnormal conditions.
  • + *
  • Agitators for Robustness: Deliberate modifications (e.g., missing fields, + * invalid amounts) are introduced to ensure that the system handles validation failures gracefully.
  • + *
  • Time Management: The system clock is controlled via a {@link SetTimeProvider} + * to produce deterministic timestamps, making tests reproducible and consistent.
  • + *
  • Integration with Jinjava: Templates containing placeholders (e.g., {{...}}) + * can be rendered at runtime, enabling dynamic test inputs or scenario generation.
  • + *
*/ -// This class is used to run tests for the Account system. @SuppressWarnings("deprecation") @RunWith(Parameterized.class) public class AccountsTest { - // Defines the paths to the tests to run. + + /** + * Paths to the YAML test directories. Each directory contains sets of input/output YAML files + * and may represent different categories of tests (e.g., simple scenarios, mixed scenarios, generated tests). + */ static final String paths = "" + "account/simple," + "account/simple-gen," + @@ -58,51 +71,95 @@ public class AccountsTest { "account/gpt-gen," + "account/gpt-jinja," + "account/bard-gen," + - "account/bard-jinja"; + "account/bard-jinja," + + "account/o1-pro"; + + /** + * The identifier ("vault") used as the system ID (target) in tests. This matches the requirements + * that commands and responses must reference a known target identifier. + */ static final long VAULT = ShortText.INSTANCE.parse("vault"); - // The name of the test, and the tester that will run the test. + /** + * The test name and YamlTester instance for each parameterized test run. + *
    + *
  • {@code name} is the scenario name (often derived from the directory name).
  • + *
  • {@code tester} is the utility that loads YAML input, runs the {@link AccountManagerImpl}, + * and compares the actual output against the expected output specified in the YAML files.
  • + *
+ */ final String name; final net.openhft.chronicle.wire.utils.YamlTester tester; - // Constructor that sets the name and tester. + /** + * Constructs a single test parameter instance with a given scenario name and YamlTester. + * + * @param name A descriptive name for the test scenario. + * @param tester The YamlTester that will execute and verify the test scenario. + */ public AccountsTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { this.name = name; this.tester = tester; } - // Defines the parameters for the parameterized test runner. + /** + * Provides a list of test parameters for the Parameterized runner. + *

+ * Uses {@link net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder} to: + *

    + *
  • Create an {@link AccountManagerImpl} instance for each scenario.
  • + *
  • Introduce "agitators" that simulate invalid or missing data, ensuring robustness and proper validation handling.
  • + *
  • Optionally render templates if the YAML files contain Jinjava placeholders.
  • + *
+ * + * @return A list of arrays, each containing a scenario name and a YamlTester instance. + */ @Parameterized.Parameters(name = "{0}") public static List parameters() { // Returns a list of test parameters to run the tests with. // Each test will be run with an instance of AccountManagerImpl, // and will be subjected to various agitations to ensure robustness. return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new AccountManagerImpl(out).id(VAULT), AccountManagerOut.class, paths) + // Add agitators to test the system's ability to handle missing or invalid fields. .agitators( net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("currency: , amount: NaN, amount: -1, balance: NaN, balance: -1, target: no-vault".split(", *")), net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("name, account, balance, sender, target, sendingTime, from, to, currency, amount, reference".split(", *"))) + // Use a custom exception handler to ensure JVM errors are logged as events. .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) .exceptionHandlerFunctionAndLog(true) - .inputFunction(s -> s.contains("{{")||s.contains("{#") ? new Jinjava().render(s, Collections.emptyMap()) : s) + // Render any templates found in the test YAML using Jinjava. + .inputFunction(s -> s.contains("{{") || s.contains("{#") ? new Jinjava().render(s, Collections.emptyMap()) : s) .get(); } - // After each test, this method resets the system time provider. + /** + * Reset the system time provider after each test to avoid affecting subsequent tests. + * Ensures that tests are isolated and no global state "leaks" between them. + */ @After public void tearDown() { SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; } - // This is the actual test method, which uses the provided tester - // to run the test and then compares the expected output to the actual output. + /** + * Executes the test scenario using the provided YamlTester. Before running the test, sets a known, + * deterministic time using {@link SetTimeProvider} to ensure reproducible timestamps. + * + *

The {@code tester.expected()} and {@code tester.actual()} methods are used to compare the + * expected output with the actual output produced by the AccountManagerImpl. + * + *

Assertions ensure that the scenario behaves as defined in the YAML specification. + */ @Test public void runTester() { - // Sets the system clock to a specific time for the purpose of testing. - SystemTimeProvider.CLOCK = new SetTimeProvider("2023-01-21T11:00:00") - .autoIncrement(1, TimeUnit.SECONDS); - // Asserts that the expected output matches the actual output. + // Set the system clock to a fixed starting time (2023-01-21T11:00:00) and increment by 1 second each event. + // This gives consistent timestamps for the events, aligning with the requirements. + SystemTimeProvider.CLOCK = new SetTimeProvider("2023-01-21T11:00:00").autoIncrement(1, TimeUnit.SECONDS); + + // Validate that the actual output matches the expected output defined in the scenario's YAML files. + // This ensures the AccountManagerImpl logic aligns with the system requirements and handles all specified conditions. assertEquals(tester.expected(), tester.actual()); } } diff --git a/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java b/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java new file mode 100644 index 0000000..37d54d4 --- /dev/null +++ b/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java @@ -0,0 +1,192 @@ +package run.chronicle.account.domain; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import run.chronicle.account.dto.CreateAccount; +import run.chronicle.account.dto.Transfer; +import net.openhft.chronicle.wire.converter.ShortText; +import net.openhft.chronicle.wire.converter.NanoTime; + +import static net.openhft.chronicle.bytes.Bytes.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AccountServiceTest { + private static final long VAULT_ID = ShortText.INSTANCE.parse("vault"); + private static final int EUR = (int) ShortText.INSTANCE.parse("EUR"); + private static final int USD = (int) ShortText.INSTANCE.parse("USD"); + + private AccountService accountService; + + @BeforeEach + void setUp() { + accountService = new AccountService(); + } + + @Test + void shouldCreateAccountSuccessfully() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + String failureReason = accountService.tryCreateAccount(ca, VAULT_ID); + + assertThat(failureReason).isNull(); + assertThat(accountService.getAllAccounts()).containsKey(101013L); + } + + @Test + void shouldFailToCreateAccountIfTargetMismatch() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + String failureReason = accountService.tryCreateAccount(ca, ShortText.INSTANCE.parse("other")); + + assertThat(failureReason).isEqualTo("target mismatch"); + assertThat(accountService.getAllAccounts()).doesNotContainKey(101013L); + } + + @Test + void shouldFailToCreateAccountIfAlreadyExists() throws InvalidMarshallableException { + CreateAccount ca1 = validCreateAccount("charlie", 101040, EUR, 50, 0); + CreateAccount ca2 = validCreateAccount("charlie2", 101040, EUR, 60, 0); + + accountService.tryCreateAccount(ca1, VAULT_ID); + String failureReason = accountService.tryCreateAccount(ca2, VAULT_ID); + + assertThat(failureReason).isEqualTo("account already exists"); + assertThat(accountService.getAllAccounts()).hasSize(1); + } + + @Test + void shouldTransferFundsSuccessfully() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 30, "Payment"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isNull(); + assertThat(accountService.getAllAccounts().get(101013L).balance()).isEqualTo(70.0); + assertThat(accountService.getAllAccounts().get(101025L).balance()).isEqualTo(80.0); + } + + @Test + void shouldFailTransferIfTargetMismatch() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, ShortText.INSTANCE.parse("other")); + + assertThat(failureReason).isEqualTo("target mismatch"); + } + + @Test + void shouldFailTransferIfFromAccountDoesNotExist() throws InvalidMarshallableException { + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(999999, 101025, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("from account doesn't exist"); + } + + @Test + void shouldFailTransferIfToAccountDoesNotExist() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + accountService.tryCreateAccount(from, VAULT_ID); + + Transfer transfer = validTransfer(101013, 999999, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("to account doesn't exist"); + } + + @Test + void shouldFailTransferIfCurrencyMismatch() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, USD, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 10, "Mismatched Currency"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("to account currency doesn't match"); + } + + @Test + void shouldFailTransferIfInsufficientFunds() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 10, 0); + CreateAccount to = validCreateAccount("charlie", 101040, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101040, EUR, 100, "Too Big"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("insufficient funds"); + } + + @Test + void shouldThrowExceptionForInvalidCreateAccountDto() { + CreateAccount invalidDto = new CreateAccount() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name("invalid") + .account(0) // invalid account + .currency(EUR) + .balance(10); + + assertThatThrownBy(() -> accountService.tryCreateAccount(invalidDto, VAULT_ID)) + .isInstanceOf(InvalidMarshallableException.class); + } + + @Test + void shouldThrowExceptionForInvalidTransferDto() { + Transfer invalidTransfer = new Transfer() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .from(0) // invalid from + .to(101025) + .currency(EUR) + .amount(10) + .reference(from("Test")); + + assertThatThrownBy(() -> accountService.tryTransfer(invalidTransfer, VAULT_ID)) + .isInstanceOf(InvalidMarshallableException.class); + } + + // Helper methods to create valid DTOs + private CreateAccount validCreateAccount(String name, long accountNum, int currency, double balance, double overdraft) { + return new CreateAccount() + .sender(ShortText.INSTANCE.parse("gw1")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name(name) + .account(accountNum) + .currency(currency) + .balance(balance) + .overdraft(overdraft); + } + + private Transfer validTransfer(long from, long to, int currency, double amount, String reference) { + return new Transfer() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(from) + .to(to) + .currency(currency) + .amount(amount) + .reference(from(reference)); + } +} diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java index 312644d..d2d29b0 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java @@ -9,10 +9,27 @@ import static org.junit.Assert.fail; import static run.chronicle.account.dto.CreateAccountTest.getCreateAccount; +/** + * Unit tests for {@link CreateAccountFailed} events. + *

+ * These tests verify that a {@code CreateAccountFailed} object can be deserialized + * correctly from a YAML representation and that appropriate validation errors + * are thrown when required fields are missing. + *

+ * Validations: + * - Ensure all mandatory fields (sender, target, sendingTime, createAccount, reason) are present. + * - Confirm that absence of createAccount or reason fields results in an InvalidMarshallableException. + */ public class CreateAccountFailedTest { + + /** + * Tests that a fully populated {@link CreateAccountFailed} event can be deserialized + * from a YAML string. Verifies that all fields match expected values. + */ @Test public void testFromString() { - CreateAccountFailed asf = Marshallable.fromString("" + + // YAML representation of a valid CreateAccountFailed event. + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + @@ -24,31 +41,55 @@ public void testFromString() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0," + - " overdraft: 3.0" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + " },\n" + " reason: reasons\n" + - "}"); - assertEquals("sender", ShortText.INSTANCE.asString(asf.sender())); - assertEquals("target", ShortText.INSTANCE.asString(asf.target())); - assertEquals("reasons", asf.reason()); - assertEquals(getCreateAccount(), asf.createAccount()); + "}"; + + CreateAccountFailed event = Marshallable.fromString(yaml); + + // Verify fields are set as expected + assertEquals("Sender should match the provided ShortText 'sender'.", + "sender", ShortText.INSTANCE.asString(event.sender())); + assertEquals("Target should match the provided ShortText 'target'.", + "target", ShortText.INSTANCE.asString(event.target())); + assertEquals("Reason should match the provided reason string.", + "reasons", event.reason()); + assertEquals("The embedded createAccount object should match the expected CreateAccount.", + getCreateAccount(), event.createAccount()); } + /** + * Tests that attempting to deserialize a CreateAccountFailed event with no createAccount field + * results in an InvalidMarshallableException. + *

+ * The createAccount field is mandatory, so if it's missing, the validation should fail. + */ @Test(expected = InvalidMarshallableException.class) public void missingCreateAccount() { - CreateAccountFailed asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + " sendingTime: 2001/02/03T04:05:06.007008009,\n" + - "}"); - fail(asf.toString()); + "}"; + CreateAccountFailed event = Marshallable.fromString(yaml); + + // If we reach this line, the test failed to throw the expected exception. + fail("Expected InvalidMarshallableException due to missing createAccount field. Event: " + event); } + /** + * Tests that attempting to deserialize a CreateAccountFailed event with no reason field + * results in an InvalidMarshallableException. + *

+ * The reason field is mandatory, so if it's missing, the validation should fail. + */ @Test(expected = InvalidMarshallableException.class) public void missingReason() { - CreateAccountFailed asf = Marshallable.fromString("" + + // YAML missing 'reason' field + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + @@ -60,11 +101,13 @@ public void missingReason() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0" + - " },\n" + - "}"); - fail(asf.toString()); - } + " balance: 1.0\n" + + " }\n" + + "}"; + CreateAccountFailed event = Marshallable.fromString(yaml); -} \ No newline at end of file + // If we reach this line, the test failed to throw the expected exception. + fail("Expected InvalidMarshallableException due to missing reason field. Event: " + event); + } +} diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java index d370ee3..b597628 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java @@ -6,7 +6,22 @@ import static org.junit.Assert.assertEquals; +/** + * Unit tests for the {@link CreateAccount} class. + *

+ * These tests verify that a {@code CreateAccount} object is constructed correctly, + * serialized as expected via {@code toString()}, and that its fields are properly set. + * The test uses known, fixed values for fields such as sender, target, and sendingTime + * to produce deterministic outputs. + */ public class CreateAccountTest { + + /** + * Helper method to create a fully-initialised {@link CreateAccount} object + * for testing. Uses consistent, known values for all fields. + * + * @return a {@code CreateAccount} instance populated with test data + */ static CreateAccount getCreateAccount() { return new CreateAccount() .sender(ShortText.INSTANCE.parse("sender")) @@ -19,23 +34,50 @@ static CreateAccount getCreateAccount() { .name("name"); } + /** + * Tests the {@code toString()} representation of a {@link CreateAccount} object. + * Also verifies selected fields (e.g., sendingTime, name) to ensure they are set correctly. + */ @Test public void testToString() { CreateAccount createAccount = getCreateAccount(); - assertEquals("" + - "!run.chronicle.account.dto.CreateAccount {\n" + - " sender: sender,\n" + - " target: target,\n" + - " sendingTime: 2001-02-03T04:05:06.007008009,\n" + - " name: name,\n" + - " account: 2,\n" + - " currency: CURR,\n" + - " balance: 1.0,\n" + - " overdraft: 3.0\n" + - "}\n", - createAccount.toString()); - assertEquals(NanoTime.INSTANCE.parse("2001-02-03T04:05:06.007008009"), - createAccount.sendingTime()); - assertEquals("name", createAccount.name()); + + // Check the string representation line-by-line to ensure it matches the expected format. + String expected = "" + + "!run.chronicle.account.dto.CreateAccount {\n" + + " sender: sender,\n" + + " target: target,\n" + + " sendingTime: 2001-02-03T04:05:06.007008009,\n" + + " name: name,\n" + + " account: 2,\n" + + " currency: CURR,\n" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + + "}\n"; + assertEquals("The toString() output of CreateAccount should match the expected YAML-like format.", + expected, createAccount.toString()); + + // Verify the parsed sendingTime matches what was set. + long expectedTime = NanoTime.INSTANCE.parse("2001-02-03T04:05:06.007008009"); + assertEquals("The sendingTime field should match the expected nanosecond timestamp.", + expectedTime, createAccount.sendingTime()); + + // Verify the name field is correctly set. + assertEquals("The name field should match the initialised value.", + "name", createAccount.name()); + + // Additional checks to ensure all fields are as expected. + assertEquals("The sender field should match the initialised value.", + "sender", ShortText.INSTANCE.asString(createAccount.sender())); + assertEquals("The target field should match the initialised value.", + "target", ShortText.INSTANCE.asString(createAccount.target())); + assertEquals("The account number should match the initialised value.", + 2L, createAccount.account()); + assertEquals("The currency field should match the initialised value.", + "CURR", ShortText.INSTANCE.asString(createAccount.currency())); + assertEquals("The balance should match the initialised value.", + 1.0, createAccount.balance(), 0.0); + assertEquals("The overdraft should match the initialised value.", + 3.0, createAccount.overdraft(), 0.0); } } diff --git a/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java b/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java index a005e8e..774686f 100755 --- a/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java +++ b/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java @@ -9,10 +9,24 @@ import static org.junit.Assert.assertEquals; import static run.chronicle.account.dto.CreateAccountTest.getCreateAccount; +/** + * Unit tests for {@link OnCreateAccount} event deserialization and validation. + *

+ * These tests verify that a {@code OnCreateAccount} object can be correctly + * deserialized from its YAML representation. Additionally, they confirm that + * mandatory fields are present and that an {@link InvalidMarshallableException} + * is thrown if any required fields (e.g., createAccount) are missing. + */ public class OnCreateAccountTest { + + /** + * Tests that a fully formed {@link OnCreateAccount} event is correctly + * deserialized from a YAML string. Verifies that mandatory fields are set + * and the embedded {@link CreateAccount} object matches expectations. + */ @Test public void testFromString() { - OnCreateAccount asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.OnCreateAccount {\n" + " sender: sender,\n" + " target: target,\n" + @@ -24,24 +38,48 @@ public void testFromString() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0," + - " overdraft: 3.0" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + " }\n" + - "}"); - assertEquals("sender", ShortText.INSTANCE.asString(asf.sender())); - assertEquals("target", ShortText.INSTANCE.asString(asf.target())); - assertEquals(getCreateAccount(), asf.createAccount()); + "}"; + + OnCreateAccount event = Marshallable.fromString(yaml); + + // Check sender field + assertEquals("The sender field should match the 'sender' ShortText value.", + "sender", ShortText.INSTANCE.asString(event.sender())); + + // Check target field + assertEquals("The target field should match the 'target' ShortText value.", + "target", ShortText.INSTANCE.asString(event.target())); + + // Check sendingTime field + long expectedTime = net.openhft.chronicle.wire.converter.NanoTime.INSTANCE.parse("2001/02/03T04:05:06.007008009"); + assertEquals("The sendingTime field should match the provided timestamp.", + expectedTime, event.sendingTime()); + + // Check the embedded CreateAccount object + assertEquals("The embedded createAccount object should match the expected reference instance.", + getCreateAccount(), event.createAccount()); } + /** + * Tests that deserializing a {@link OnCreateAccount} event without the mandatory + * createAccount field results in an {@link InvalidMarshallableException}. This + * confirms that validation logic is working as intended. + */ @Test(expected = InvalidMarshallableException.class) public void missingCreateAccount() { - OnCreateAccount asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.OnCreateAccount {\n" + " sender: sender,\n" + " target: target,\n" + " sendingTime: 2001/02/03T04:05:06.007008009\n" + - "}"); - Assert.fail(asf.toString()); - } + "}"; -} \ No newline at end of file + OnCreateAccount event = Marshallable.fromString(yaml); + + // If we reach here, no exception was thrown, which means the test failed. + Assert.fail("Expected InvalidMarshallableException due to missing createAccount field, but got: " + event); + } +} diff --git a/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java b/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java index 3a0160b..cf4a3ee 100755 --- a/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java +++ b/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java @@ -9,8 +9,22 @@ import static org.junit.Assert.*; import static run.chronicle.account.dto.TransferTest.getTransfer; +/** + * Unit tests for the {@link OnTransfer} DTO. + *

+ * These tests verify that: + *

    + *
  • {@code OnTransfer} objects can be correctly serialized to and deserialized from their YAML representation.
  • + *
  • Mandatory fields (like the embedded {@link Transfer}) are present and validated.
  • + *
  • The {@link OnTransfer#usesSelfDescribingMessage()} method behaves as expected.
  • + *
+ */ public class OnTransferTest { + /** + * A YAML representation of a fully populated {@link OnTransfer} instance, + * including a nested {@link Transfer} object and all required fields. + */ public static final String EXPECTED = "" + "!run.chronicle.account.dto.OnTransfer {\n" + " sender: target,\n" + @@ -28,32 +42,67 @@ public class OnTransferTest { " }\n" + "}\n"; + /** + * Tests that the {@link OnTransfer#toString()} method produces the expected YAML representation. + * Also serves as a regression check to ensure the serialized format remains stable. + */ @Test public void testToString() { - assertEquals(EXPECTED, - new OnTransfer() - .target(ShortText.INSTANCE.parse("sender")) - .sender(ShortText.INSTANCE.parse("target")) - .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) - .transfer(getTransfer()) - .toString()); + OnTransfer event = new OnTransfer() + .target(ShortText.INSTANCE.parse("sender")) + .sender(ShortText.INSTANCE.parse("target")) + .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) + .transfer(getTransfer()); + + assertEquals("The toString() output should match the expected YAML.", EXPECTED, event.toString()); } + /** + * Tests that an {@link OnTransfer} object can be deserialized from a YAML string, + * and that the resulting object matches the expected state. + */ @Test public void testFromString() { - OnTransfer tok = Marshallable.fromString(EXPECTED); - assertFalse(tok.usesSelfDescribingMessage()); - assertEquals(TransferTest.getTransfer(), tok.transfer()); + OnTransfer event = Marshallable.fromString(EXPECTED); + + // Verify that the event does not use a self-describing message format. + assertFalse("usesSelfDescribingMessage() should return false for OnTransfer events.", + event.usesSelfDescribingMessage()); + + // Verify that the transfer object matches the expected reference Transfer. + assertEquals("The embedded Transfer object should match the expected instance.", + getTransfer(), event.transfer()); + + // Check sender and target fields + String senderStr = ShortText.INSTANCE.asString(event.sender()); + String targetStr = ShortText.INSTANCE.asString(event.target()); + assertEquals("The sender field should match 'target' as defined in the YAML.", + "target", senderStr); + assertEquals("The target field should match 'sender' as defined in the YAML.", + "sender", targetStr); + + // Check sendingTime field + long expectedTime = NanoTime.INSTANCE.parse("2001-02-03T04:05:06.777888999"); + assertEquals("The sendingTime field should match the given timestamp.", + expectedTime, event.sendingTime()); } + /** + * Tests that attempting to deserialize an {@link OnTransfer} event without the mandatory + * transfer field results in an {@link InvalidMarshallableException}. This ensures that + * validation logic is properly enforced. + */ @Test(expected = InvalidMarshallableException.class) public void missingTransfer() { - OnTransfer tok = Marshallable.fromString("" + + String yamlWithoutTransfer = "" + "!run.chronicle.account.dto.OnTransfer {\n" + " sender: target,\n" + " target: sender,\n" + " sendingTime: 2001-02-03T04:05:06.777888999,\n" + - "}\n"); - fail(tok.toString()); + "}\n"; + OnTransfer event = Marshallable.fromString(yamlWithoutTransfer); + + // If no exception is thrown, the test fails. + fail("Expected InvalidMarshallableException due to missing 'transfer' field, but got: " + event); } } diff --git a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java index 88c6d29..0c9ffa2 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java @@ -6,13 +6,24 @@ import net.openhft.chronicle.wire.converter.NanoTime; import org.junit.Test; -import java.io.InputStream; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +/** + * Unit tests for the {@link TransferFailed} DTO. + *

+ * These tests verify: + *

    + *
  • Serialization: Ensuring {@link TransferFailed#toString()} produces the expected YAML representation.
  • + *
  • Deserialization: Confirming that a {@link TransferFailed} object can be reconstructed from YAML.
  • + *
  • Validation: Ensuring that missing required fields result in {@link InvalidMarshallableException}.
  • + *
+ */ public class TransferFailedTest { + /** + * A fully populated {@link TransferFailed} YAML representation used in these tests. + */ public static final String EXPECTED = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + @@ -31,54 +42,72 @@ public class TransferFailedTest { " reason: reasons\n" + "}\n"; + /** + * Tests that calling {@link TransferFailed#toString()} on a fully populated object + * matches the expected YAML string. + */ @Test public void testToString() { - assertEquals(EXPECTED, - new TransferFailed() - .target(ShortText.INSTANCE.parse("sender")) - .sender(ShortText.INSTANCE.parse("target")) - .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) - .reason("reasons") - .transfer(TransferTest.getTransfer()) - .toString()); + TransferFailed tf = new TransferFailed() + .target(ShortText.INSTANCE.parse("sender")) + .sender(ShortText.INSTANCE.parse("target")) + .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) + .reason("reasons") + .transfer(TransferTest.getTransfer()); + + assertEquals("The toString() output of TransferFailed should match the expected YAML.", + EXPECTED, tf.toString()); } + /** + * Tests that a {@link TransferFailed} object can be deserialized from the EXPECTED YAML, + * and that all fields match the original expected values. + */ @Test public void testFromString() { TransferFailed tf = Marshallable.fromString(EXPECTED); - assertEquals(TransferTest.getTransfer(), tf.transfer()); - assertEquals("reasons", tf.reason()); + + // Verify that the transfer object matches the expected instance. + assertEquals("The embedded Transfer object should match the expected reference.", + TransferTest.getTransfer(), tf.transfer()); + + // Verify that the reason field matches the expected value. + assertEquals("The reason field should match the 'reasons' string.", + "reasons", tf.reason()); } /** - * This test verifies the scenario where a required field in TransferFailed object is missing. - * It is expected that when the {@link Marshallable#fromString(CharSequence)} method is used to parse a String - * that represents a TransferFailed object missing a required field, an InvalidMarshallableException will be thrown. - * - * @throws InvalidMarshallableException if a required field in the marshalled String is missing. + * Tests that deserializing a {@link TransferFailed} event without the required 'transfer' field + * throws an {@link InvalidMarshallableException}. + *

+ * Here, the 'transfer' field is missing. According to the DTO's requirements, + * this should cause validation to fail. */ @Test(expected = InvalidMarshallableException.class) public void missingFieldInTransferFailed() { - OnTransfer tok = Marshallable.fromString("" + + String yamlMissingTransfer = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + " target: sender,\n" + - " sendingTime: 2001-02-03T04:05:06.777888999," + + " sendingTime: 2001-02-03T04:05:06.777888999,\n" + " reason: None\n" + - "}\n"); + "}\n"; + + TransferFailed tf = Marshallable.fromString(yamlMissingTransfer); - fail(tok.toString()); + // If no exception is thrown, fail the test. + fail("Expected InvalidMarshallableException due to missing 'transfer' field, but got: " + tf); } /** - * Tests that an exception is thrown when the `reason` field is missing from a `TransferFailed` DTO. - * - * @throws InvalidMarshallableException if the `TransferFailed` DTO is not valid + * Tests that deserializing a {@link TransferFailed} event without the required 'reason' field + * throws an {@link InvalidMarshallableException}. + *

+ * Here, the 'reason' field is missing. This should cause validation to fail. */ @Test(expected = InvalidMarshallableException.class) public void missingReasonFieldInTransferFailed() { - // Create a `TransferFailed` DTO without the `reason` field. - OnTransfer tok = Marshallable.fromString("" + + String yamlMissingReason = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + " target: sender,\n" + @@ -92,10 +121,12 @@ public void missingReasonFieldInTransferFailed() { " currency: CURR,\n" + " amount: 1.0,\n" + " reference: reference\n" + - " },\n" + - "}\n"); + " }\n" + + "}\n"; + + TransferFailed tf = Marshallable.fromString(yamlMissingReason); - // Fail if the `TransferFailed` DTO is valid. - fail(tok.toString()); + // If no exception is thrown, fail the test. + fail("Expected InvalidMarshallableException due to missing 'reason' field, but got: " + tf); } } diff --git a/account/src/test/java/run/chronicle/account/dto/TransferTest.java b/account/src/test/java/run/chronicle/account/dto/TransferTest.java index c548d90..bdb72a2 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferTest.java @@ -9,12 +9,22 @@ import static org.junit.Assert.assertFalse; /** - * This class TransferTest is a test class that tests the Transfer class. + * Unit tests for the {@link Transfer} DTO. + *

+ * These tests ensure that: + *

    + *
  • A {@link Transfer} object is correctly populated and all fields match expected values.
  • + *
  • The {@code toString()} method produces a stable, expected YAML-like representation.
  • + *
  • The {@link Transfer#usesSelfDescribingMessage()} method returns the correct value.
  • + *
*/ public class TransferTest { + /** - * This method creates a Transfer object with the following values: - * @return a Transfer object + * Creates a {@link Transfer} instance with predetermined, known values for all fields. + * This method acts as a reference object for testing and comparison. + * + * @return a fully populated {@link Transfer} instance for testing */ static Transfer getTransfer() { return new Transfer() @@ -29,24 +39,45 @@ static Transfer getTransfer() { } /** - * This test checks that the toString method of the Transfer class + * Tests that the {@code toString()} method of {@link Transfer} produces the expected output, + * and verifies the {@link Transfer#usesSelfDescribingMessage()} property. + *

+ * This ensures the serialization format remains consistent and that the Transfer DTO + * does not mistakenly switch to a self-describing message format. */ @Test public void testToString() { Transfer transfer = getTransfer(); - assertEquals("" + - "!run.chronicle.account.dto.Transfer {\n" + - " sender: sender,\n" + - " target: target,\n" + - " sendingTime: 2001-02-03T04:05:06.007008009,\n" + - " from: 12345,\n" + - " to: 67890,\n" + - " currency: CURR,\n" + - " amount: 1.0,\n" + - " reference: reference\n" + - "}\n", - transfer.toString()); - assertFalse(transfer.usesSelfDescribingMessage()); + String expectedToString = "" + + "!run.chronicle.account.dto.Transfer {\n" + + " sender: sender,\n" + + " target: target,\n" + + " sendingTime: 2001-02-03T04:05:06.007008009,\n" + + " from: 12345,\n" + + " to: 67890,\n" + + " currency: CURR,\n" + + " amount: 1.0,\n" + + " reference: reference\n" + + "}\n"; + + // Check that the string representation matches the expected format. + assertEquals("The toString() output should match the expected YAML representation.", + expectedToString, transfer.toString()); + + // Verify that usesSelfDescribingMessage() returns false as expected. + assertFalse("usesSelfDescribingMessage() should return false for Transfer events.", + transfer.usesSelfDescribingMessage()); + + // Additional field verifications to ensure the object is correctly set up. + assertEquals("sender", ShortText.INSTANCE.asString(transfer.sender())); + assertEquals("target", ShortText.INSTANCE.asString(transfer.target())); + assertEquals(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.007008009"), transfer.sendingTime()); + assertEquals("Expected account 'from' to match initialised value.", 12345, transfer.from()); + assertEquals("Expected account 'to' to match initialised value.", 67890, transfer.to()); + assertEquals("Expected currency to be 'CURR' after parsing.", "CURR", ShortText.INSTANCE.asString(transfer.currency())); + assertEquals("Expected amount to be 1.0.", 1.0, transfer.amount(), 0.0); + assertEquals("Expected reference field to match 'reference' bytes.", + "reference", transfer.reference().toString()); } } diff --git a/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java b/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java new file mode 100644 index 0000000..bbadd62 --- /dev/null +++ b/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java @@ -0,0 +1,220 @@ +package run.chronicle.account.impl; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import run.chronicle.account.api.AccountManagerOut; +import run.chronicle.account.domain.AccountService; +import run.chronicle.account.dto.*; +import net.openhft.chronicle.wire.converter.ShortText; +import net.openhft.chronicle.wire.converter.NanoTime; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class AccountManagerImplTest { + + private static final long VAULT_ID = ShortText.INSTANCE.parse("vault"); + private static final long SENDER_ID = ShortText.INSTANCE.parse("gw1"); + private static final int EUR = (int) ShortText.INSTANCE.parse("EUR"); + private static final int USD = (int) ShortText.INSTANCE.parse("USD"); + + private AccountManagerOut out; + private AccountService accountService; + private AccountManagerImpl manager; + + @BeforeEach + void setUp() { + out = mock(AccountManagerOut.class); + accountService = mock(AccountService.class); + manager = new AccountManagerImpl(out, accountService).id(VAULT_ID); + } + + @Test + void createAccount_Success() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + + when(accountService.tryCreateAccount(ca, VAULT_ID)).thenReturn(null); + + manager.createAccount(ca); + + // Verify onCreateAccount event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(OnCreateAccount.class); + verify(out).onCreateAccount(captor.capture()); + OnCreateAccount event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(SENDER_ID); + } + + @Test + void createAccount_Failure() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("bob", 101025, EUR, -1, 0); + + when(accountService.tryCreateAccount(ca, VAULT_ID)).thenReturn("invalid balance"); + + manager.createAccount(ca); + + // Verify createAccountFailed event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateAccountFailed.class); + verify(out).createAccountFailed(captor.capture()); + CreateAccountFailed event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + assertThat(event.reason()).isEqualTo("invalid balance"); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(SENDER_ID); + } + + @Test + void createAccount_InvalidDtoThrows() throws InvalidMarshallableException { + CreateAccount invalidDto = new CreateAccount() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name("invalid") + .account(0) // invalid account + .currency(EUR) + .balance(10); + + // Simulate that AccountService will throw InvalidMarshallableException + doThrow(new InvalidMarshallableException("sender must be set")) + .when(accountService).tryCreateAccount(invalidDto, VAULT_ID); + + try { + manager.createAccount(invalidDto); + } catch (InvalidMarshallableException e) { + // expected + } + + // Since an exception was thrown, no events should be emitted + verifyNoInteractions(out); + } + + @Test + void transfer_Success() throws InvalidMarshallableException { + Transfer transfer = validTransfer(101013, 101025, EUR, 50, "Payment"); + + when(accountService.tryTransfer(transfer, VAULT_ID)).thenReturn(null); + + manager.transfer(transfer); + + // Verify onTransfer event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(OnTransfer.class); + verify(out).onTransfer(captor.capture()); + OnTransfer event = captor.getValue(); + assertThat(event.transfer()).isEqualTo(transfer); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(transfer.sender()); + } + + @Test + void transfer_Failure() throws InvalidMarshallableException { + Transfer transfer = validTransfer(101013, 101025, USD, 1000, "HugePayment"); + + when(accountService.tryTransfer(transfer, VAULT_ID)).thenReturn("insufficient funds"); + + manager.transfer(transfer); + + // Verify transferFailed event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(TransferFailed.class); + verify(out).transferFailed(captor.capture()); + TransferFailed event = captor.getValue(); + assertThat(event.reason()).isEqualTo("insufficient funds"); + assertThat(event.transfer()).isEqualTo(transfer); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(transfer.sender()); + } + + @Test + void transfer_InvalidDtoThrows() throws InvalidMarshallableException { + Transfer invalidTransfer = new Transfer() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(0) // invalid from + .to(101025) + .currency(EUR) + .amount(10) + .reference(net.openhft.chronicle.bytes.Bytes.from("Test")); + + doThrow(new InvalidMarshallableException("sender must be set")) + .when(accountService).tryTransfer(invalidTransfer, VAULT_ID); + + manager.transfer(invalidTransfer); + + // Verify transferFailed event due to exception in service + ArgumentCaptor captor = ArgumentCaptor.forClass(TransferFailed.class); + verify(out).transferFailed(captor.capture()); + TransferFailed event = captor.getValue(); + assertThat(event.reason()).contains("sender must be set"); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(invalidTransfer.sender()); // which is 0 + } + + @Test + void checkPoint_SameTarget() { + CheckPoint cp = new CheckPoint() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T11:00:00")); + + Map accounts = new LinkedHashMap<>(); + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 50, 0); + accounts.put(101013L, ca); + + when(accountService.getAllAccounts()).thenReturn(accounts); + + manager.checkPoint(cp); + + // Verify startCheckpoint, onCreateAccount for each account, and endCheckpoint + verify(out).startCheckpoint(cp); + ArgumentCaptor captor = ArgumentCaptor.forClass(OnCreateAccount.class); + verify(out).onCreateAccount(captor.capture()); + OnCreateAccount event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + verify(out).endCheckpoint(cp); + } + + @Test + void checkPoint_DifferentTargetIgnored() { + CheckPoint cp = new CheckPoint() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(ShortText.INSTANCE.parse("other")) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T11:00:00")); + + manager.checkPoint(cp); + + // Different target means no interaction + verifyNoInteractions(out); + verifyNoInteractions(accountService); + } + + // Helper methods + private CreateAccount validCreateAccount(String name, long accountNum, int currency, double balance, double overdraft) { + return new CreateAccount() + .sender(SENDER_ID) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name(name) + .account(accountNum) + .currency(currency) + .balance(balance) + .overdraft(overdraft); + } + + private Transfer validTransfer(long from, long to, int currency, double amount, String reference) { + return new Transfer() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(from) + .to(to) + .currency(currency) + .amount(amount) + .reference(net.openhft.chronicle.bytes.Bytes.from(reference)); + } +} diff --git a/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java b/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java index 22cc19b..107d0a3 100755 --- a/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java +++ b/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java @@ -11,27 +11,47 @@ import static org.junit.Assert.*; +/** + * Unit tests for {@link LogsAccountManagerOut}, verifying that all events logged by this mock implementation + * produce the expected logging output. This ensures that logging behavior remains stable and predictable. + */ public class LogsAccountManagerOutTest { + + /** + * Resets the Jvm exception handlers after each test to prevent side effects between tests. + */ @After public void reset() { Jvm.resetExceptionHandlers(); } + + /** + * Tests that the {@link LogsAccountManagerOut} correctly logs various events to Jvm's exception handlers. + * This includes verifying that all output events are captured with the expected log level, message, and format. + */ @Test public void expectOutput() { + // Start recording exceptions thrown/logged by Jvm for verification. Map recorded = Jvm.recordExceptions(true); + + // Instantiate a LogsAccountManagerOut and emit various events. LogsAccountManagerOut out = new LogsAccountManagerOut(); - out.createAccountFailed(new CreateAccountFailed()); - out.endCheckpoint(new CheckPoint()); - out.jvmError("jvm error"); - out.onCreateAccount(new OnCreateAccount()); - out.onTransfer(new OnTransfer()); - out.startCheckpoint(new CheckPoint()); - out.transferFailed(new TransferFailed()); - String collect = recorded.keySet().stream() + out.createAccountFailed(new CreateAccountFailed()); // Should produce a WARN log + out.endCheckpoint(new CheckPoint()); // Should produce a DEBUG log + out.jvmError("jvm error"); // Should produce an ERROR log + out.onCreateAccount(new OnCreateAccount()); // Should produce a DEBUG log + out.onTransfer(new OnTransfer()); // Should produce a DEBUG log + out.startCheckpoint(new CheckPoint()); // Should produce a DEBUG log + out.transferFailed(new TransferFailed()); // Should produce a WARN log + + // Filter and collect logs only from the same package as this test. + String collectedLog = recorded.keySet().stream() .filter(e -> e.clazz().getPackage().equals(LogsAccountManagerOutTest.class.getPackage())) .map(Object::toString) .collect(Collectors.joining("\n")); - assertEquals("" + + + // Expected log output. Each event should match these lines exactly. + String expected = "" + "ExceptionKey{level=WARN, clazz=class run.chronicle.account.util.LogsAccountManagerOut, message='createAccountFailed !run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: \"\",\n" + " target: \"\",\n" + @@ -74,8 +94,10 @@ public void expectOutput() { " transfer: !!null \"\",\n" + " reason: !!null \"\"\n" + "}\n" + - "', throwable=}", collect); + "', throwable=}"; + // Assert that the collected log matches the expected output exactly. + assertEquals("The recorded exception logs should match the expected logging output.", + expected, collectedLog); } - -} \ No newline at end of file +} diff --git a/account/src/test/resources/account/bard-gen/_setup.yaml b/account/src/test/resources/account/bard-gen/_setup.yaml index b9c892c..f7772e8 100755 --- a/account/src/test/resources/account/bard-gen/_setup.yaml +++ b/account/src/test/resources/account/bard-gen/_setup.yaml @@ -62,4 +62,4 @@ transfer: { amount: 10, reference: Cat food } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/bard-gen/in.yaml b/account/src/test/resources/account/bard-gen/in.yaml index b6e7d22..3c0d878 100755 --- a/account/src/test/resources/account/bard-gen/in.yaml +++ b/account/src/test/resources/account/bard-gen/in.yaml @@ -1,4 +1,4 @@ -{# Generated by Bard #} +{ # Generated by Bard #} --- # This operation creates an account for Charlie with account number 101037. createAccount: { @@ -48,4 +48,4 @@ checkPoint: { target: vault, sendingTime: 2023-01-20T11:00:00, } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/copilot/in.yaml b/account/src/test/resources/account/copilot/in.yaml index 20ee38e..21905bf 100755 --- a/account/src/test/resources/account/copilot/in.yaml +++ b/account/src/test/resources/account/copilot/in.yaml @@ -1,31 +1,166 @@ -{# Generated by copilot #} +{ # Generated by copilot #} --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation creates an account for Charlie with account number 101045. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5000 +} +--- +# This operation creates an account for Dania with account number 101055. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500 +} +--- +# This operation represents a transfer of 500 EUR from Charlie to Dania. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500, + reference: Grocery Shopping } -... --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation attempts to make a transfer from a non-existent account to Charlie. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500, + reference: Unexpected Income } -... --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1000, + reference: Rent } -... +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +--- +# This operation creates an account for Bob with account number 101025. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1000 +} +--- +# This operation represents a transfer of 10 EUR from Alice to Bob. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: Dog food +} +--- +# This operation attempts to make a transfer from a non-existent account to Bob. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10, + reference: Junk food +} +--- +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10, + reference: Cat food +} +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 +} +--- +# This operation creates an account for Bob with account number 101025. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 +} +--- +# This operation creates an account for Charlie with account number 101040. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 +} +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 +} +--- diff --git a/account/src/test/resources/account/copilot/out-missing-field-balance.yaml b/account/src/test/resources/account/copilot/out-missing-field-balance.yaml new file mode 100644 index 0000000..29af327 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-balance.yaml @@ -0,0 +1,271 @@ +# This operation creates an account for Charlie with account number 101045. +# missing balance: 5000 +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +# missing balance: 3500 +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15 +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +# missing balance: 1000 +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15.0, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +# missing balance: 1E3, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +# missing balance: 60.0, +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15.0, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-name.yaml b/account/src/test/resources/account/copilot/out-missing-field-name.yaml new file mode 100644 index 0000000..4c2e452 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-name.yaml @@ -0,0 +1,146 @@ +# This operation creates an account for Charlie with account number 101045. +# missing name: charlie, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Dania with account number 101055. +# missing name: Dania, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:08, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Bob with account number 101025. +# missing name: bob, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:12, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Bob with account number 101025. +# missing name: bob, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Charlie with account number 101040. +# missing name: charlie, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-reference.yaml b/account/src/test/resources/account/copilot/out-missing-field-reference.yaml new file mode 100644 index 0000000..8579011 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-reference.yaml @@ -0,0 +1,173 @@ +# This operation creates an account for Charlie with account number 101045. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +# missing reference: Grocery Shopping +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +# missing reference: Unexpected Income +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +# missing reference: Rent +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:10, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +# missing reference: Dog food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to make a transfer from a non-existent account to Bob. +# missing reference: Junk food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +# missing reference: Cat food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:11, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:14, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml b/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml new file mode 100644 index 0000000..ff0af02 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml @@ -0,0 +1,56 @@ +# This operation creates an account for Charlie with account number 101045. +# missing sendingTime: 2023-01-20T10:05:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Dania with account number 101055. +# missing sendingTime: 2023-01-20T10:05:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +# missing sendingTime: 2023-01-20T10:06:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +# missing sendingTime: 2023-01-20T10:07:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +# missing sendingTime: 2023-01-20T10:08:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Bob with account number 101025. +# missing sendingTime: 2023-01-20T10:00:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +# missing sendingTime: 2023-01-20T10:01:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to make a transfer from a non-existent account to Bob. +# missing sendingTime: 2023-01-20T10:02:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +# missing sendingTime: 2023-01-20T10:03:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Bob with account number 101025. +# missing sendingTime: 2023-01-20T10:00:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Charlie with account number 101040. +# missing sendingTime: 2023-01-20T10:00:02, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... diff --git a/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml b/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml new file mode 100644 index 0000000..6463e0a --- /dev/null +++ b/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml @@ -0,0 +1,529 @@ +# This operation creates an account for Charlie with account number 101045. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +jvmError: "java.lang.UnsupportedOperationException: TEXT ---..." +... +# This operation creates an account for Charlie with account number 101045. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:21, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Dania with account number 101055. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:22, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:23, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:24, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:25, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:26, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:27, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:28, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:29, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:30, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:31, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:32, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:33, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:34, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml b/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml new file mode 100644 index 0000000..28bb342 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml @@ -0,0 +1,146 @@ +# This operation creates an account for Charlie with account number 101045. +# override balance: 5000 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Dania with account number 101055. +# override balance: 3500 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:08, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1000 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:12, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1E3 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Charlie with account number 101040. +# override balance: 60.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... diff --git a/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml b/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml new file mode 100644 index 0000000..373a840 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml @@ -0,0 +1,274 @@ +# This operation creates an account for Charlie with account number 101045. +# override balance: 5000 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Dania with account number 101055. +# override balance: 3500 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1000 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1E3 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Charlie with account number 101040. +# override balance: 60.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... diff --git a/account/src/test/resources/account/copilot/out.yaml b/account/src/test/resources/account/copilot/out.yaml index 6ce6bec..06dcd08 100755 --- a/account/src/test/resources/account/copilot/out.yaml +++ b/account/src/test/resources/account/copilot/out.yaml @@ -1,13 +1,5 @@ -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation creates an account for Charlie with account number 101045. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 -} -... onCreateAccount: { sender: vault, target: gw1, @@ -15,15 +7,17 @@ onCreateAccount: { createAccount: { sender: gw1, target: vault, - sendingTime: 2023-01-20T10:00:00, - name: alice, - account: 101013, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, currency: EUR, - balance: 5.0, + balance: 5E3, overdraft: 0.0 } } ... +# This operation creates an account for Dania with account number 101055. +--- onCreateAccount: { sender: vault, target: gw1, @@ -31,35 +25,77 @@ onCreateAccount: { createAccount: { sender: gw1, target: vault, - sendingTime: 2023-01-20T10:00:01, - name: bob, - account: 101025, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, currency: EUR, - balance: 1010.0, + balance: 3500.0, overdraft: 0.0 } } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist } ... -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist } ... -onCreateAccount: { +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:09, + sendingTime: 2023-01-21T11:00:12, createAccount: { sender: gw1, target: vault, @@ -67,15 +103,18 @@ onCreateAccount: { name: alice, account: 101013, currency: EUR, - balance: 5.0, + balance: 15.0, overdraft: 0.0 - } + }, + reason: account already exists } ... -onCreateAccount: { +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:10, + sendingTime: 2023-01-21T11:00:13, createAccount: { sender: gw1, target: vault, @@ -83,31 +122,75 @@ onCreateAccount: { name: bob, account: 101025, currency: EUR, - balance: 1010.0, + balance: 1E3, overdraft: 0.0 - } + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist } ... -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds } ... -onCreateAccount: { +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:11, + sendingTime: 2023-01-21T11:00:17, createAccount: { sender: gw1, target: vault, @@ -115,30 +198,65 @@ onCreateAccount: { name: alice, account: 101013, currency: EUR, - balance: 5.0, + balance: 15.0, overdraft: 0.0 - } + }, + reason: account already exists } ... -onCreateAccount: { +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:12, + sendingTime: 2023-01-21T11:00:18, createAccount: { sender: gw1, target: vault, sendingTime: 2023-01-20T10:00:01, name: bob, account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, currency: EUR, - balance: 1010.0, + balance: 60.0, overdraft: 0.0 } } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists } ... diff --git a/account/src/test/resources/account/gpt-gen/_setup.yaml b/account/src/test/resources/account/gpt-gen/_setup.yaml index 80d1564..768142c 100755 --- a/account/src/test/resources/account/gpt-gen/_setup.yaml +++ b/account/src/test/resources/account/gpt-gen/_setup.yaml @@ -1 +1 @@ -# This is GPT-4 generated \ No newline at end of file +# This is GPT-4 generated diff --git a/account/src/test/resources/account/gpt-gen/in.yaml b/account/src/test/resources/account/gpt-gen/in.yaml index 5e67bd1..2bd5644 100755 --- a/account/src/test/resources/account/gpt-gen/in.yaml +++ b/account/src/test/resources/account/gpt-gen/in.yaml @@ -1,4 +1,4 @@ -{# Generated by GPT-4 #} +{ # Generated by GPT-4 #} --- # This operation creates an account for Charlie with account number 101045. # The 'sender' is 'gw1' and the 'target' is 'vault'. diff --git a/account/src/test/resources/account/gpt-jinja/in.yaml b/account/src/test/resources/account/gpt-jinja/in.yaml index b7b1819..5467bde 100644 --- a/account/src/test/resources/account/gpt-jinja/in.yaml +++ b/account/src/test/resources/account/gpt-jinja/in.yaml @@ -16,15 +16,15 @@ createAccount: { --- # Starting from account number 10, we initiate 10 transfers to the next account {% for i in range(10, 20-1) %} {# -1 because the last account won't have a next account to transfer to #} -transfer: { - sender: gw2, - target: vault, - sendingTime: "2023-01-21T11:{{ i }}:00", - from: {{ i }}, - to: {{ i|add(1) }}, - currency: EUR, - amount: 100, - reference: Transfer {{ i }} to {{ i|add(1) }} -} + transfer: { + sender: gw2, + target: vault, + sendingTime: "2023-01-21T11:{{ i }}:00", + from: {{ i }}, + to: {{ i|add(1) }}, + currency: EUR, + amount: 100, + reference: Transfer {{ i }} to {{ i|add(1) }} + } {% endfor %} ... \ No newline at end of file diff --git a/account/src/test/resources/account/mixed/_setup.yaml b/account/src/test/resources/account/mixed/_setup.yaml index 05e8b53..cb5b7ef 100644 --- a/account/src/test/resources/account/mixed/_setup.yaml +++ b/account/src/test/resources/account/mixed/_setup.yaml @@ -21,4 +21,4 @@ createAccount: { currency: USD, balance: 1000 } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/mixed/in.yaml b/account/src/test/resources/account/mixed/in.yaml index cbdd2e7..34ead7b 100644 --- a/account/src/test/resources/account/mixed/in.yaml +++ b/account/src/test/resources/account/mixed/in.yaml @@ -23,4 +23,4 @@ transfer: { amount: 10, reference: Dog food, } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/o1-pro/README.adoc b/account/src/test/resources/account/o1-pro/README.adoc new file mode 100644 index 0000000..9060e4a --- /dev/null +++ b/account/src/test/resources/account/o1-pro/README.adoc @@ -0,0 +1,72 @@ += Test Scenarios for Account Management Service + +This directory contains three YAML files that define and verify specific test scenarios for an Account Management Service (AMS). The goal is to ensure that the AMS behaves as expected when processing various account creation, transfer, and checkpoint commands. + +== Files Overview + +* **_setup.yaml**: Establishes the initial system state before the tests begin. +* **in.yaml**: Specifies the input commands (events) to be replayed against the AMS. +* **out.yaml**: Details the expected output events corresponding to each input scenario. + +== _setup.yaml + +The `_setup.yaml` file configures a baseline state for testing: + +* **Alice (101013, 15 EUR)**: Provides a known starting point for EUR-to-EUR transfers. +* **Bob (101025, 1000 USD)**: Tests scenarios involving currency mismatch, ensuring that transfers fail when currencies differ. +* **Charlie (101040, 50 EUR)**: Allows testing successful EUR-to-EUR transfers and verifying proper balance updates. + +All initial accounts and balances are established here, ensuring that subsequent operations in `in.yaml` have a known and stable starting point. + +== in.yaml + +The `in.yaml` file contains a series of input scenarios that exercise both successful and failing operations: + +1. **Valid EUR-to-EUR Transfer (Alice -> Charlie)**: +Tests a successful transfer, confirming that balances are updated and `onTransfer` events are generated as expected. + +2. **Currency Mismatch Transfer (Alice -> Bob)**: +Attempts a EUR-to-USD transfer and expects a `transferFailed` event due to currency mismatch. + +3. **Invalid Create Account (Derek with -20 EUR)**: +Attempts to create an account with a negative balance and expects a `createAccountFailed` event. + +4. **Non-Existent Account Transfer**: +Tries to transfer funds from an account that doesn't exist, expecting a `transferFailed` event with a "from account doesn't exist" reason. + +5. **Insufficient Funds Transfer (Alice -> Charlie)**: +After a successful transfer, Alice has reduced funds. A large transfer request should fail with `transferFailed` due to insufficient funds. + +6. **Checkpoint Request**: +Requests a checkpoint, verifying `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events to confirm correct state serialization. + +Each scenario is accompanied by comments explaining the rationale, expected outcomes, and relevant details for easy reference. + +== out.yaml + +The `out.yaml` file defines the expected output events for each scenario in `in.yaml`: + +* **onTransfer** events for successful transfers. +* **transferFailed** and **createAccountFailed** events for invalid operations. +* **jvmError** entries if invalid input triggers a validation exception. +* **startCheckpoint**, `onCreateAccount` (for each known account), and `endCheckpoint` events for the checkpoint scenario. + +Comments within `out.yaml` map each output event back to the corresponding input scenario, making it clear which events should be produced in response to each command. + +== General Guidelines + +1. **Clarity and Context**: +The comments in `_setup.yaml`, `in.yaml`, and `out.yaml` provide context and reasoning for each scenario, making the tests more understandable and maintainable. + +2. **Time Management**: +Although fixed timestamps are used here, in production and performance tests, consider using `SystemTimeProvider.CLOCK.currentTimeNanos()` for nanosecond-precision wall clock times. + +3. **Validation Checks**: +Scenarios include invalid operations to ensure `createAccountFailed` and `transferFailed` events are emitted correctly when requirements are not met. + +4. **Coverage and Maintenance**: +The test set covers successful and failed account creations, valid and invalid transfers, currency mismatches, insufficient funds scenarios, and checkpointing. This broad coverage ensures confidence in the AMS's correctness and resilience. + +== Conclusion + +These YAML-based test inputs and expected outputs form a comprehensive testing framework for the Account Management Service. By examining the scenarios in `_setup.yaml`, `in.yaml`, and `out.yaml`, developers and testers can quickly understand the expected behavior of the system and ensure that the AMS meets all specified requirements. diff --git a/account/src/test/resources/account/o1-pro/_setup.yaml b/account/src/test/resources/account/o1-pro/_setup.yaml new file mode 100644 index 0000000..1f28d38 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/_setup.yaml @@ -0,0 +1,38 @@ +--- +# Create an account for Alice (101013) with 15 EUR initial balance. +# Rationale: Alice is used in transfer scenarios that require a known starting balance. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +... +--- +# Create an account for Bob (101025) with 1000 USD initial balance. +# Rationale: Bob's account is in a different currency (USD) to test currency mismatch failures. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1000 +} + +--- +# Create an account for Charlie (101040) with 50 EUR initial balance. +# Rationale: Charlie has an EUR account to test successful EUR-to-EUR transfers. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50 +} diff --git a/account/src/test/resources/account/o1-pro/in.yaml b/account/src/test/resources/account/o1-pro/in.yaml new file mode 100644 index 0000000..ad56ff7 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/in.yaml @@ -0,0 +1,84 @@ +--- +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10, + reference: Groceries Payment +} +... +--- +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: "Dog food" +} + +--- +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +createAccount: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + name: "derek", + account: 101050, + currency: EUR, + balance: -20 +} + +--- +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5, + reference: "Mystery Sender" +} + +--- +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100, + reference: "Big Purchase" +} + +--- +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +checkPoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml new file mode 100644 index 0000000..adb9fe4 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml @@ -0,0 +1,94 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing from: 999999, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:03, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:04, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml new file mode 100644 index 0000000..3cbfbde --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml @@ -0,0 +1,154 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# missing name: "derek", +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml new file mode 100644 index 0000000..7941f8e --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml @@ -0,0 +1,94 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing reference: Groceries Payment +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing reference: "Dog food" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing reference: "Mystery Sender" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing reference: "Big Purchase" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:03, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:04, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml new file mode 100644 index 0000000..985a152 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml @@ -0,0 +1,37 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing sendingTime: 2023-01-20T10:01:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing sendingTime: 2023-01-20T10:02:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# missing sendingTime: 2023-01-20T10:03:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing sendingTime: 2023-01-20T10:04:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing sendingTime: 2023-01-20T10:05:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +# missing sendingTime: 2023-01-20T11:00:00 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... diff --git a/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml b/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml new file mode 100644 index 0000000..c3a34b3 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml @@ -0,0 +1,154 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# override balance: -20 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml b/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml new file mode 100644 index 0000000..1975955 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml @@ -0,0 +1,170 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# override balance: -20 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + name: derek, + account: 101050, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:10, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out.yaml b/account/src/test/resources/account/o1-pro/out.yaml new file mode 100644 index 0000000..21d6cbe --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out.yaml @@ -0,0 +1,153 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/simple/_setup.yaml b/account/src/test/resources/account/simple/_setup.yaml index 96e7e38..f5506b6 100644 --- a/account/src/test/resources/account/simple/_setup.yaml +++ b/account/src/test/resources/account/simple/_setup.yaml @@ -1 +1 @@ -# not setup required \ No newline at end of file +# no setup required diff --git a/account/src/test/resources/account/waterfall/_setup.yaml b/account/src/test/resources/account/waterfall/_setup.yaml index cd2c271..f135ce4 100644 --- a/account/src/test/resources/account/waterfall/_setup.yaml +++ b/account/src/test/resources/account/waterfall/_setup.yaml @@ -33,4 +33,4 @@ createAccount: { currency: EUR, balance: 10 } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/waterfall/in.yaml b/account/src/test/resources/account/waterfall/in.yaml index bf73f37..1ddbdc3 100644 --- a/account/src/test/resources/account/waterfall/in.yaml +++ b/account/src/test/resources/account/waterfall/in.yaml @@ -23,4 +23,4 @@ transfer: { amount: 20, reference: Coffee } -... \ No newline at end of file +... diff --git a/architecture.adoc b/architecture.adoc new file mode 100644 index 0000000..26a565c --- /dev/null +++ b/architecture.adoc @@ -0,0 +1,119 @@ += Architecture +:toc: +:toclevels: 3 + +This document describes the overall system architecture, focusing on the event-driven design powered by Chronicle Queue, how each module connects, and the core concurrency and data-flow patterns. + +== Design Principles + +1. **Event-Driven** + Each service or module produces or consumes events via Chronicle Queue. We avoid tight coupling or synchronous calls, letting queues mediate communication. + +2. **Low Latency & High Throughput** + Chronicle Queue’s in-memory, append-only design gives us sub-microsecond latencies and can handle millions of events per second on commodity hardware. + +3. **Replay & Durability** + Every message is persisted to a queue file. We can replay historical data for debugging or auditing, or even reconstruct an entire system state by replaying logs from the start. + +4. **Modularity** + Each submodule (e.g., `event-routing`, `md-pipeline`, `order-processor`) addresses a distinct domain scenario or pattern while adhering to a consistent queue-based approach. + +== Core Architecture Overview + +At a high level: + +``` + [Producer/Simulator] --> [Chronicle Queue] --> [Consumer/Processor] --> [Potentially Another Queue] --> ... +``` + +- **Producer**: Writes method calls or messages into a queue. Could be an external service, command-line input, or an exchange simulator. +- **Chronicle Queue**: A durable, zero-GC logging of all events (append-only). Another process or thread can read from it with minimal blocking. +- **Consumer**: Subscribes via a `MethodReader`, processes the data (e.g., aggregates, transforms), and may write responses or derived events into another queue. + +Because each queue is independent, you can run each module in a separate process—enabling easy scaling or horizontal distribution. + +== Concurrency Model + +1. **Single-Threaded Readers** +Many demos use a single-threaded approach: a single method reader loop that blocks on the queue for new events. This eliminates complex synchronization and yields consistent micro-latencies. + +2. **Multi-Process** +For pipeline-like flows (`md-pipeline` or `event-routing`), each stage is typically a separate process (or separate main class) reading from one queue and writing to the next. This separation ensures that no single process becomes a bottleneck. + +3. **MethodWriter/MethodReader** +Chronicle’s code generation approach: +- **MethodWriter** automatically serializes method calls (e.g., `in.mdi(...)`) into queue entries. +- **MethodReader** decodes those entries, calling the actual consumer logic in real time. +This allows your code to look like normal method invocations while behind the scenes it’s writing or reading from a queue. + +4. **Message History & Replay** +Some modules (`message-history-demo`) add a small overhead for each message: a `MessageHistory` that tracks where/when it was produced, how many hops it took. If a consumer fails, we can replay the queue from a safe checkpoint. + +== Module-by-Module Breakdown + +=== Hello World +A minimal example: +- Reads lines from console (`SaysInput`). +- Possibly modifies them (e.g., adds an exclamation). +- Outputs to the console (`SaysOutput`). + +The entire flow can happen in a single process or across multiple if you store input lines in a queue for another process to read. +xref:usage-and-tests.adoc#hello-world[See usage instructions →] + +=== Event Routing +A scenario where messages (like `Value` objects) get routed or filtered in different ways: +- **ViaThreeFive**: Demonstrates branching logic (divisible by 3, 5). +- **SifterImpl**: Checks if a value is even or divisible by 3, sending them to different outputs. + +By abstracting reading/writing behind interfaces (`ViaIn`, `ViaOut`, `ValueMessage`), you decouple the logic from the underlying queue, making it easy to swap in new routing conditions. +xref:usage-and-tests.adoc#event-routing[See usage instructions →] + +=== Market Data Pipeline (md-pipeline) +A multi-process pipeline common in trading or real-time data systems: +1. **ExchangeSimulator** (Generate) – Publishes simulated market increments to `agg-in` queue. +2. **Aggregator** (AggregatorImpl) – Reads increments, aggregates them into a snapshot, outputs to `agg-out`. +3. **Strategy** (StrategyImpl) – Reads aggregated snapshots from `agg-out`, decides on trades, writes them to `strat-out`. +4. **OMS** (OMSImpl) – Optionally reads from `strat-out` to process new orders. + +This chain can run in multiple terminals or containers, each tailing one queue and writing to the next. +xref:usage-and-tests.adoc#md-pipeline[See usage instructions →] + +=== Order Processor (OMS) +Implements a small Order Management System referencing some FIX 4.2 tags: +- Processes inbound `NewOrderSingle`, `CancelOrderRequest` events. +- Publishes `ExecutionReport` or `OrderCancelReject`. + +Because the logic is single-threaded, we can handle thousands of orders per second with minimal overhead. For usage and benchmark commands, see xref:usage-and-tests.adoc#order-processor[Order Processor usage →] + +=== Benchmarks +Various classes in `benchmarks/` measure raw throughput, latency distribution, or stress tests. +- **ThroughputMain** blasts tens of millions of messages into a queue and reads them back, measuring speed. +- **LatencyDistributionMain** measures per-message latency. + +For how to run these tests, see xref:usage-and-tests.adoc#benchmarks[Benchmarking →]. + +== Advanced Chronicle Features + +1. **Shared Memory & Replication** +By default, Chronicle queues store data in memory-mapped files. This is nearly as fast as raw memory access. For advanced setups, Chronicle can replicate data across processes or even machines in near real-time. + +2. **Chronicle Map** +Some submodules (like account-based demos) may store large key-value data in an off-heap map (Chronicle Map). This is not extensively shown in the main modules but can be integrated in a similar pattern. + +3. **Custom Converters & Interceptors** +Chronicle supports user-defined converters (e.g., Base85 encoding for IDs), as well as interceptors to log or transform data during reads/writes. + +4. **Message History** +The `message-history-demo` shows how each queue hop can track timing and source IDs, enabling advanced debugging or performance audits. + +== Deployment & Scaling Notes + +- **Single Host**: You can run multiple modules on the same machine, each pointing to different queue directories, achieving near-zero-latency hops. +- **Distributed**: Place each module on a separate host or container. Latencies may increase with network overhead, but you still preserve asynchronous queue-based flows. +- **Auto-Restart**: Because the queue is durable, if a consumer process is restarted, it can resume from the last confirmed index, ensuring no data loss or duplication. + +== Conclusion + +This architecture leverages Chronicle’s strengths—durability, minimal GC, sub-microsecond latencies—while illustrating an event-driven approach across multiple domain demos (Hello World, Event Routing, MD Pipeline, Order Processor, etc.). The result is a flexible yet high-performance platform for real-time event processing, easily extended or replicated into production-ready systems. + +For instructions on **running** these modules or **testing** them with YAML scenarios, see xref:usage-and-tests.adoc[Usage & Tests]. For **style guidelines** and domain references (e.g., FIX 4.2), see xref:reference.adoc[Reference]. diff --git a/event-routing/src/main/adoc/event-routing-requirements.adoc b/event-routing/src/main/adoc/event-routing-requirements.adoc new file mode 100644 index 0000000..78201f2 --- /dev/null +++ b/event-routing/src/main/adoc/event-routing-requirements.adoc @@ -0,0 +1,110 @@ += Event Routing Submodule – Functional Requirements +:toc: +:toclevels: 3 +:sectnums: + +The *event-routing* submodule is a small system that demonstrates how to **route** (or direct) messages from an input source to one or more outputs, based on simple rules like “divisible by 3” or “even number.” By using Chronicle Queue, these messages are passed around in an **asynchronous, event-driven** way. + +== 1. Purpose and Goals + +. **Showcase how messages can be routed** in a low-latency, asynchronous style using Chronicle Queue. +. **Separate routing logic from the rest of the system** by defining easy-to-use interfaces (like `ViaIn`, `ViaOut`, and `ValueMessage`). +. **Provide simple examples** (e.g., messages that get sent to a "three" channel if divisible by 3) to illustrate the concept. + +=== 1.1 Who Is This For? +* **New developers** who want a straightforward example of event-driven routing. +* **Anyone** interested in seeing how Chronicle Queue can be used to build small, modular routing flows. + +== 2. Key Concepts + +* **Value**: A simple data object (DTO) containing a numeric field (e.g., `val`). +* **ValueMessage**: An interface that defines one method: `value(Value value)`. This method is where you handle incoming `Value` objects. +* **ViaIn** and **ViaOut**: Interfaces that let you specify how to **name** a route (`via("someName")`) and choose an **input** (`in()`) or **output** (`out()`). + +NOTE: +Using `via("...")` explicitly sets the “route name” or “channel name” for messages. Any subsequent logic (e.g., filtering by divisibility) can branch messages to one or more output channels based on that route. Think of `via` as the address you stamp on each message so the system knows where to send it. + +== 3. Functional Requirements + +=== 3.1 Basic Message Routing + +. **FR-1** +The system **must** accept messages with at least a numeric field called `val` (inside a `Value` object). + +. **FR-2** +Each incoming message **must** go through a **rule-based check** (like “Is `val % 3 == 0`?”) to decide what output channel(s) it belongs to. + +. **FR-3** +If a message meets **multiple** conditions (for example, divisible by both 3 and 5), it should be sent to all relevant channels (e.g., "three" and "five"). + +=== 3.2 Interfaces and Interaction + +. **FR-4** +The submodule **must** provide a `ValueMessage` interface so developers can call `value(Value v)` when they have a new message. + +. **FR-5** +A `ViaIn` interface **must** let developers specify a route name (`via(String name)`) and then call `in()` to start reading or receiving messages on that route. +Using `via("nameOfRoute")` indicates *how* (or *where*) messages should be tagged for subsequent filtering or distribution. + +. **FR-6** +A `ViaOut` interface **must** let developers specify a route name (`via(String name)`) and then call `out()` to direct messages to that output route. +Similarly, `via("someOutput")` ensures the system appends or publishes messages to a queue (or channel) labeled `"someOutput"`. + +**Why?** +Using these `via(...)` methods keeps routing flexible. You can easily swap out how you decide routes or what queue to write to without changing the rest of the system. + +=== 3.3 Chronicle Queue Integration + +. **FR-7** +The submodule **should** use Chronicle Queue’s `MethodWriter` to turn your interface calls (like `value(...)`) into actual queue entries automatically. + +. **FR-8** +Another part of the system (e.g., a consumer) **must** use a `MethodReader` to listen for these entries and invoke your routing logic, which might write to a second queue or log the result somewhere. + +This ensures **asynchronous** communication: one part writes events, another reads them whenever it’s ready. + +=== 3.4 Error Handling + +. **FR-9** +If a message doesn’t meet basic requirements (like `val` is missing), or the routing fails, the submodule **should** at least log an error and skip the message. + +. **FR-10** +For a message that doesn’t match any rule, the code can still forward it to a default output or do nothing, as determined by the developer’s preference. + +== 4. Testing + +=== 4.1 YAML Tests + +. **FR-11** +The submodule **must** support simple YAML tests in which `in.yaml` lists the messages (like `{ val: 6 }`), and `out.yaml` shows where each message ends up. + +. **FR-12** +If `val=6`, for example, the expected `out.yaml` should indicate it was routed to a `three` channel. Tests compare actual vs. expected results. + +=== 4.2 Unit & Integration Tests + +. **FR-13** +Developers **should** create unit tests for any custom routing logic (like Sifter or Divisible). + +. **FR-14** +If multiple routes or outputs can be triggered at once (e.g., `15` goes to "three" and "five"), test that the system handles multi-route messages without collisions or missed routes. + +== 5. Examples + +=== 5.1 Divisible by Three or Five +If `val = 15`, the routing logic might send it to: +- `via("three")` +- `via("five")` + +=== 5.2 Even-Odd Filtering +A `SifterImpl` could check if `val` is even, then direct it to an “evens” channel. Odd values could remain unmodified or be sent to another path. + +== 6. Non-Functional Considerations + +* **Performance**: Typically microseconds-level, but you’re free to optimize or benchmark if your scenarios require it. +* **Simplicity**: This module is intentionally straightforward—one or two conditions to illustrate the concept. +* **Extensibility**: You can easily add more rules or multiple routing layers without changing core interfaces. + +== 7. Summary + +The event-routing submodule aims to **demonstrate how messages can flow** through **simple, event-driven rules** using Chronicle Queue. With `via(String name)` acting as the explicit route for each message, it’s easy to define or change routing behavior. Simply implement or extend `ValueMessage` and `ViaIn`/`ViaOut` rules, then create matching tests (YAML or JUnit) to confirm your routing works as intended. diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java b/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java index c34dc1b..3351920 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java @@ -6,7 +6,10 @@ import run.chronicle.routing.inout.api.ViaOut; /** - * Routes {@link Value} messages from multiple sources to multiple destinations based on specific criteria. + * A simple router that checks if a Value's 'val' is divisible by 3 and/or 5. + * - If val % 3 == 0 -> route to "three" + * - If val % 5 == 0 -> route to "five" + * If negative or zero doesn't match, we either skip or log an error. * *

This class implements {@link ViaIn} and {@link ValueMessage} to process incoming {@code Value} messages. * It checks if the name contains the character 'e', and if so, routes the value to different @@ -61,7 +64,10 @@ public ValueMessage in() { */ @Override public void value(Value value) { - if (name.contains("e")) { + if (value.val() < 0) + out.via("error") + .value(value); + else if (name.contains("e")) { if (value.val() % 3 == 0) out.via("three") .value(value); diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java index e86a02f..07b24ee 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java @@ -3,8 +3,9 @@ import net.openhft.chronicle.wire.SelfDescribingMarshallable; /** - * This is a Value class extending SelfDescribingMarshallable. - * It wraps a long value and provides methods for setting and retrieving this value. + * Represents a simple DTO with a numeric value 'val'. + * We can route these messages based on val's properties + * (e.g., divisible by 3, negative, etc.). */ public class Value extends SelfDescribingMarshallable { @@ -13,7 +14,7 @@ public class Value extends SelfDescribingMarshallable { /** * This is a constructor for the Value class. - * It initializes the val instance variable with the provided value. + * It initialises the val instance variable with the provided value. * * @param val A long value to set as the initial value for this instance */ @@ -22,12 +23,17 @@ public Value(long val) { } /** - * This is a getter for the val instance variable. - * It returns the current long value of this Value object. - * - * @return The current long value of this Value object + * Returns the numeric value of this message. */ public long val() { return val; } + + /** + * (Optional) Allows updating the numeric value. + */ + public Value val(long newVal) { + this.val = newVal; + return this; + } } diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java index 42ddc73..3f32a37 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java @@ -1,9 +1,9 @@ package run.chronicle.routing.inout.api; /** - * This is a ValueMessage interface. - * It provides a contract for any class that wants to send a {@link Value} message. - * The purpose of this interface is to encapsulate the process of sending a value in a message. + * Describes the contract for any class that handles + * a 'Value' message. The system will call `value(...)` + * whenever a new message is received or processed. */ public interface ValueMessage { diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java index b32cf07..ded91a1 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java @@ -1,8 +1,11 @@ package run.chronicle.routing.inout.api; /** - * This is a ViaIn interface. - * It provides a generic contract for classes that need to specify a route ("via") and an input source ("in"). + * An interface for specifying a route ("via") and obtaining + * an input source ("in"). + * + * Typically, you'd call via("someName") to define a route, + * then in() to start reading from that route as a ValueMessage. * * @param the type returned by the {@code via} method * @param the type returned by the {@code in} method diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java index 464b04e..01445a6 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java @@ -1,8 +1,11 @@ package run.chronicle.routing.inout.api; /** - * This is a ViaOut interface. - * It provides a generic contract for classes that need to specify a route ("via") and an output destination ("out"). + * An interface for specifying a route ("via") and obtaining + * an output destination ("out"). + * + * Typically, you'd call via("someName") to define a route, + * then out() to get a ValueMessage for writing messages. * * @param the type returned by the {@code via} method * @param the type returned by the {@code out} method diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java index 47aa408..ab413b9 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java @@ -3,10 +3,10 @@ * *

This package includes:

*
    - *
  • {@link Value}: A class that encapsulates a long value, providing methods for retrieval.
  • - *
  • {@link ValueMessage}: An interface defining a contract for sending {@code Value} messages.
  • - *
  • {@link ViaIn}: An interface for specifying a route ("via") and obtaining an input source ("in").
  • - *
  • {@link ViaOut}: An interface for specifying a route ("via") and obtaining an output destination ("out").
  • + *
  • {@link run.chronicle.routing.inout.api.Value}: A class that encapsulates a long value, providing methods for retrieval.
  • + *
  • {@link run.chronicle.routing.inout.api.ValueMessage}: An interface defining a contract for sending {@code Value} messages.
  • + *
  • {@link run.chronicle.routing.inout.api.ViaIn}: An interface for specifying a route ("via") and obtaining an input source ("in").
  • + *
  • {@link run.chronicle.routing.inout.api.ViaOut}: An interface for specifying a route ("via") and obtaining an output destination ("out").
  • *
* *

The goal of this package is to offer a flexible and extensible means of routing messages in various formats and protocols, supporting different input/output strategies.

diff --git a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java index 80203ef..c5c6b85 100755 --- a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java +++ b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java @@ -7,7 +7,12 @@ import run.chronicle.routing.out.api.Triple; /** - * The SifterImpl class is an implementation of the SifterIn interface. + * SifterImpl checks if val is even or divisible by 3 (or both), + * then routes to different destinations. For example: + * - Even -> 'evens()' + * - Multiple of 3 -> 'triples()' + * + * * The SifterImpl class is an implementation of the SifterIn interface. * This class implements the logic for processing a Value object. * The Value object is processed based on whether its val property is an even number or divisible by 3. */ @@ -24,7 +29,7 @@ public class SifterImpl implements SifterIn { /** * Constructor for the SifterImpl class. - * This initializes the SifterOut object used for outputting the processed values. + * This initialises the SifterOut object used for outputting the processed values. * * @param so A SifterOut object to be used for outputting the processed values */ diff --git a/event-routing/src/main/java/run/chronicle/routing/out/package-info.java b/event-routing/src/main/java/run/chronicle/routing/out/package-info.java index bf3d50f..1191197 100755 --- a/event-routing/src/main/java/run/chronicle/routing/out/package-info.java +++ b/event-routing/src/main/java/run/chronicle/routing/out/package-info.java @@ -1,12 +1,12 @@ /** * This package, run.chronicle.routing.out, contains classes related to the output routing functionality. - * + *

* It includes: * * - SifterImpl: A class implementing the SifterIn interface. This class processes a Value object based on * whether its 'val' property is an even number or divisible by 3. The processing logic decides how the Value * object will be routed out. - * + *

* The aim of this package is to provide classes and logic for output operations. This includes taking * the processed input data, and routing it to the appropriate channels for further use. */ diff --git a/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java b/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java old mode 100755 new mode 100644 index cfd68ab..c1d7010 --- a/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java +++ b/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java @@ -11,8 +11,17 @@ public class ViaThreeFiveTest { // The `via` method is a unit test for the `via` method in the ViaThreeFive class. @Test public void via() { - // yt is an instance of YamlTester. The `runTest` method runs a test on the ViaThreeFive class with the input "three-five". - final net.openhft.chronicle.wire.utils.YamlTester yt = net.openhft.chronicle.wire.utils.YamlTester.runTest(ViaThreeFive.class, "three-five"); + doTest("three-five"); + } + + @Test + public void values() { + doTest("three-five-values"); + } + + private static void doTest(String path) { + // yt is an instance of YamlTester. The `runTest` method runs a test on the ViaThreeFive class with the input. + final net.openhft.chronicle.wire.utils.YamlTester yt = net.openhft.chronicle.wire.utils.YamlTester.runTest(ViaThreeFive.class, path); // Asserts that the expected result is equal to the actual result. // The `replace` method replaces any occurrences of "---\n---" in the actual result with "---". diff --git a/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java b/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java index e4c8cab..c06b97d 100755 --- a/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java +++ b/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java @@ -18,7 +18,7 @@ public final class ViaOutTextMethodWriter implements ViaOut out; // Constructor for the ViaOutTextMethodWriter class. - // It initializes the Supplier for output handling and a Closeable resource to be managed. + // It initialises the Supplier for output handling and a Closeable resource to be managed. // An UpdateInterceptor is also accepted as an argument, but currently not used in the class. public ViaOutTextMethodWriter(Supplier out, Closeable closeable, UpdateInterceptor updateInterceptor) { this.out = out; diff --git a/event-routing/src/test/resources/three-five-values/in.yaml b/event-routing/src/test/resources/three-five-values/in.yaml new file mode 100644 index 0000000..c763ef9 --- /dev/null +++ b/event-routing/src/test/resources/three-five-values/in.yaml @@ -0,0 +1,20 @@ +# Scenario: 6 -> expect route to "three" +--- +in: eff +value: { val: 6 } +... +# Scenario: 15 -> expect route to "three" and "five" +--- +in: eff +value: { val: 15 } +... +# Scenario: Negative -1 -> error +--- +in: eff +value: { val: -1 } +... +# Scenario: No match (like val=2) +--- +in: eff +value: { val: 2 } +... diff --git a/event-routing/src/test/resources/three-five-values/out.yaml b/event-routing/src/test/resources/three-five-values/out.yaml new file mode 100644 index 0000000..1186bb3 --- /dev/null +++ b/event-routing/src/test/resources/three-five-values/out.yaml @@ -0,0 +1,33 @@ +# Scenario: 6 -> expect route to "three" +--- +out: "" +value: { + val: 6 +} +... +# Scenario: 15 -> expect route to "three" and "five" +--- +out: "" +value: { + val: 15 +} +... +# Scenario: Negative -1 -> error +--- +via: error +value: { + val: -1 +} +... +out: "" +value: { + val: -1 +} +... +# Scenario: No match (like val=2) +--- +out: "" +value: { + val: 2 +} +... diff --git a/hello-world/README.adoc b/hello-world/README.adoc index 2166b81..9e7cc6e 100644 --- a/hello-world/README.adoc +++ b/hello-world/README.adoc @@ -1,23 +1,33 @@ -= Event Driven Program Demo += Event-Driven Program Demo +:toc: +:toclevels: 3 -This demo steps through event driven program development, testing and writing a simple microservice which operates a simple function on input events. -In this demo event driven systems have been modeled so that, the event driven core systems has input and output gateways connecting to external systems that might not be event driven and the business logic is placed in the core event driven system. -Figure 1 shows a general sketch of the introduced model with a simple business logic that adds exclamation mark to input messages (events) and then sends them to the output gateway. +This module demonstrates event-driven program development using Chronicle Queue. +It provides a series of small microservices that showcase how to read incoming events (strings), process or transform those events (e.g., by appending an exclamation mark), and then output the results. -.An event driven system based on the introduced model, the system adds exclamation mark to input messages and sends them to output -image::../docs/images/hello-world-fig1.png[] +image::../docs/images/hello-world-fig1.png[An event-driven system adds an exclamation mark to input messages and sends them to output, align="center"] -== Implementation +== Overview -In the following sections several event driven systems with the general structured shown in Figure 1 are implemented. -All the implementations use the same input and output gateways. +In this demo, we assume an *event-driven* architecture with: -=== Input and Output Gateways +* An **input gateway** that reads incoming lines from the console (or another source). +* An **output gateway** that writes lines to the console (or logs them). +* A **microservice** in between that applies any required business logic (e.g., adding an exclamation mark). -The input and output gateways used in this demo are implementations of the functional interface link:../hello-world/src/main/java/event/driven/program/api/Says.java[Says]. -The input gateway reads each input line and calls say(input line) method. +The figure above gives a high-level sketch of how these components connect. +This design can be extended to larger, more complex systems with multiple inputs and outputs. + +== 1. Implementation Walkthrough + +All implementations in this module rely on the same input and output gateways. Below, we describe each example in detail. + +=== 1.1 Input and Output Gateways + +The gateways used here are built on the link:../hello-world/src/main/java/event/driven/program/api/Says.java[Says] interface. +- **Input Gateway (`SaysInput`)** reads lines from `System.in` and calls `says.say(line)` for each line. +- **Output Gateway (`SaysOutput`)** receives text via `say(String words)` and prints it to `System.out`. -.Input gateway [source,java] ---- public class SaysInput { @@ -28,10 +38,8 @@ public class SaysInput { } } ---- +^ Input gateway: reads from the console and invokes `say(...)` on each line. -The output gateway prints out the received messages. - -.Output gateway [source,java] ---- public class SaysOutput implements Says { @@ -40,28 +48,25 @@ public class SaysOutput implements Says { } } ---- +^ Output gateway: prints each received message to the console. -In the following section several event driven examples are implemented using the above gateways. - -=== Relaying Input Events to Output +=== 1.2 Relaying Input Events to Output -This system simply relays input events to output thus it can be implemented easily as the output of one gateway is wired to the input of the other. -This is implemented in `RecordInputToConsoleMain` class. +The simplest approach wires the input gateway directly to the output gateway: each incoming line is immediately relayed to the console. This is implemented in `RecordInputToConsoleMain`. -.RecordInputToConsoleMain [source,java] ---- public class RecordInputToConsoleMain { public static void main(String[] args) throws IOException { - // Writes text in each call to say(line) to the console + // Create an output gateway final Says says = new SaysOutput(); - // Takes each line input and calls say(line) each time + // Pass it to the input gateway so that each line is echoed SaysInput.input(says); } } ---- -By running `RecordInputToConsoleMain` (right click on the class and select Run), each line of input will be echoed on the console: +When you run `RecordInputToConsoleMain`, each line of input is echoed back: [source,text] ---- @@ -73,58 +78,65 @@ Three // input event Three // output event ---- -=== Log Input Events in Yaml Format +=== 1.3 Logging Input Events as YAML -`RecordInputAsYamlMain` implements an event driven system so that input messages are logged as Yaml on the console. `Wires.recordAsYaml` creates and returns a proxy of `Says` interface which writes inputs into the specified PrintStream in Yaml format. -//TODO add link to Wires.recordAsYaml when it approved and merged +`RecordInputAsYamlMain` demonstrates how to transform input lines into YAML output. +Chronicle Wire’s `Wires.recordAsYaml` creates a proxy of the `Says` interface that writes method calls in YAML format to a specified `PrintStream`. -.RecordInputAsYamlMain [source,java] ---- public class RecordInputAsYamlMain { public static void main(String[] args) throws IOException { - // obtains a proxy that writes to the PrintStream the method calls and their arguments + // Creates a proxy that logs calls to System.out in YAML format final Says says = Wires.recordAsYaml(Says.class, System.out); - // Takes each line input and calls say(theLine) each time + // Reads lines from the console and logs them SaysInput.input(says); } } ---- -To run `RecordInputAsYamlMain` right click on `RecordInputAsYamlMain` and select "Modify Run Configuration ...". -In the pop-up window in the "program arguments" field write the input file name (ie says.yaml) then click on OK and run the program, the following can be observed on console. +.Run RecordInputAsYamlMain +1. Right-click on `RecordInputAsYamlMain` in your IDE. +2. Modify run configuration if needed. +3. Provide an argument for the input file name (e.g., `says.yaml`), though optional if you only want console logs. +Example console output: [source,text] ---- -One // input event -say: One // output event +One // input +say: One // logged as YAML ... -Two // input event -say: Two // output event +Two // input +say: Two // logged as YAML ... -Three // input event -say: Three // output event +Three // input +say: Three // logged as YAML ... ---- -=== Replaying Logged Outputs from a Yaml File +=== 1.4 Replaying Logged Outputs from a YAML File -For this example the outputs from the above section have been logged into resources/says.yaml file. -The file is read and replayed to `SaysOutput` by Wire.replay method. -//TODO add link to Wires.replay method. +We can replay the recorded YAML lines with `ReplayOutputMain`. This reads a YAML file (e.g., `says.yaml`) and invokes the `SaysOutput` gateway to print each event. -.ReplayOutputMain [source,java] ---- public class ReplayOutputMain { public static void main(String[] args) throws IOException { - // Reads the content of a Yaml file specified in args[0] and feeds it to SaysOutput. - Wires.replay(args[0], new SaysOutput()); + // The first argument is the YAML file path + Wires.replay(args[0], new SaysOutput()); } } ---- -By running `ReplayOutputMain` the following output will be printed out on the console. +If your YAML file (`says.yaml`) has lines like: +[source,text] +---- +say: One +say: Two +say: Three +... +---- +`ReplayOutputMain` prints them out as: [source,text] ---- @@ -133,22 +145,25 @@ Two Three ---- -.DirectWithExclamationMain +=== 1.5 Adding an Exclamation Mark + +`DirectWithExclamationMain` shows how to insert a small piece of logic between reading and printing: +It uses `AddsExclamation` to append `"!"` to each line before passing it to the output gateway. + [source,java] ---- public class DirectWithExclamationMain { public static void main(String[] args) throws IOException { - // Adds exclamation mark to input messages + // Decorate the output with an exclamation SaysInput.input(new AddsExclamation(new SaysOutput())); } } ---- -=== Adding a Microservice +=== 1.6 The AddsExclamation Microservice -Now the intermediate block in Figure 1 is implemented using a microservice that appends an exclamation mark to input messages and passes it to the output gateway. +This microservice is a straightforward implementation of `Says`, wrapping another `Says` object: -.AddsExclamation microservice [source,java] ---- public class AddsExclamation implements Says { @@ -158,29 +173,30 @@ public class AddsExclamation implements Says { this.out = out; } + @Override public void say(String words) { - this.out.say(words + "!"); + out.say(words + "!"); } } ---- - -`DirectWithExclamationMain` integrates the three components (Input/Output gateway and `AddsExclalation`) in Figure 1. By running `DirectWithExclamationMain`, an exclamation mark is appended to each input and then will be printed out on the console. +When combined with the input and output gateways (e.g., in `DirectWithExclamationMain`), each input line is printed with an exclamation mark: [source,text] ---- -One // input event -One! // output event -Two // input event -Two! // output event -Three // input event -Three! // output event +One // input +One! // output +Two // input +Two! // output +Three // input +Three! // output ---- -=== Testing the Microservice +== 2. Testing the Microservice + +The link:https://github.com/OpenHFT/Chronicle-Wire/blob/ea/src/main/java/net/openhft/chronicle/wire/TextMethodTester.java[TextMethodTester] allows you to define input (`in.yaml`) and expected output (`out.yaml`) for your microservice, then compare them automatically. -To test the microservice, link:https://github.com/OpenHFT/Chronicle-Wire/blob/ea/src/main/java/net/openhft/chronicle/wire/TextMethodTester.java[TextMethodTester] is used that can read input (resources/in.yaml) and expected output (resources/out.yaml) files of the microservice and compares it to the actual output. +For example: -.Testing the microservice [source,java] ---- public class AddsExclamationTest { @@ -190,12 +206,15 @@ public class AddsExclamationTest { "says/in.yaml", AddsExclamation::new, Says.class, - "says/out.yaml"); + "says/out.yaml" + ); tester.run(); assertEquals(tester.expected(), tester.actual()); } } ---- +If the test fails, you can see the difference between expected and actual outputs in your IDE’s comparison viewer. + Running the test should log "Process finished with exit code 0" on the console which shows the test has passed. Change the second input in in.yaml to Hello World and run the test again, to see the test fails. @@ -210,9 +229,10 @@ By clicking on the link, you will see the difference b .Difference between expected and actual test result image::../docs/images/hello-world-fig3.png[] -== Running the Demo from Maven -Open a shell and from the example directory, hello-world, run the following. commands. +== 3. Running the Demo from Maven + +Below are common commands for building and running each class: .Building the code [source,sh] @@ -234,3 +254,19 @@ mvn install exec:java@ReplayOutputMain [source,sh] mvn install exec:java@DirectWithExclamationMark +== Summary + +The *hello-world* example illustrates a simple event-driven pipeline: + +- **Read** lines (input gateway). +- **Process** them if needed (e.g., add punctuation). +- **Output** them (console or YAML). + +Experiment with each main class: + +* **RecordInputToConsoleMain**: echoes input. +* **RecordInputAsYamlMain**: logs input as YAML. +* **ReplayOutputMain**: replays a YAML file. +* **DirectWithExclamationMain**: adds an exclamation mark. + +This small example demonstrates how Chronicle Queue–based code can handle real-time data flow in a concise, modular way. Once comfortable with these basics, you can explore more advanced modules and features within the repository. diff --git a/hello-world/src/main/adoc/hello-world-requirements.adoc b/hello-world/src/main/adoc/hello-world-requirements.adoc new file mode 100644 index 0000000..e28df7f --- /dev/null +++ b/hello-world/src/main/adoc/hello-world-requirements.adoc @@ -0,0 +1,124 @@ += Hello World Example – Functional Requirements +:toc: +:toclevels: 3 +:sectnums: + +This document specifies the functional requirements for the *hello-world* module in the Chronicle Queue Demo project. It is designed to help new developers understand the basic goals, structure, and usage of the "Hello World" style program that demonstrates how Chronicle Queue can process simple text input and output. + +== 1. Introduction + +=== 1.1 Purpose +The *hello-world* module illustrates a simple, event-driven workflow using Chronicle Queue: +* Reading lines of text (e.g., user input). +* Processing or transforming that text (e.g., adding punctuation). +* Outputting the transformed text (e.g., printing to the console). + +By exploring this module, beginners can learn: +* How to handle **input** in a Chronicle Queue–based microservice. +* How to **process** or modify the text (e.g., by adding an exclamation point). +* How to **output** data back to a console or store it in a queue for later use. + +=== 1.2 Scope +This document only covers the *hello-world* module’s functionality and basic usage steps. It does not address other modules or advanced Chronicle Queue topics like high-throughput event routing, specialized data models, or advanced performance testing. + +== 2. Functional Overview + +=== 2.1 Key Components +The *hello-world* module comprises several main classes. Each class demonstrates a slightly different workflow, but all follow the same general pattern of reading input, optionally transforming it, and printing or logging the result. + +1. **RecordInputToConsoleMain** + - Reads lines from standard input (the keyboard or a piped file). + - Immediately prints each line back to the console. + +2. **RecordInputAsYamlMain** + - Reads lines from standard input. + - Logs or prints each line in YAML format to standard output (useful for structured logging or replay). + +3. **ReplayOutputMain** + - Reads previously saved YAML lines from a file (or queue). + - “Replays” them by printing to standard output. + - Ideal for verifying or demonstrating how recorded input can be replayed later. + +4. **DirectWithExclamationMain** + - Reads lines from standard input. + - Appends an exclamation mark (`!`) to each line. + - Prints the modified line to the console. + +=== 2.2 Data Flow Overview + +[source] +---- +Input (Console) ---> [Hello-World Microservice] ---> Output (Console/YAML/File) +---- +1. The user (or a script) **enters text** in the console. +2. The microservice **captures** that text and can optionally modify it. +3. The microservice **outputs** the original or modified text back to the console or logs it in YAML. + +== 3. Functional Requirements + +=== 3.1 Basic Operations +. **FR-1**: The system **must** read user input from the console or a text file (one line at a time). +. **FR-2**: The system **must** allow for simple text processing or transformation (e.g., adding punctuation). +. **FR-3**: The system **must** output or log each processed line to either the console or a YAML stream. + +=== 3.2 Input Handling +. **FR-4**: On startup, the program **must** prompt the user to type a line. +. **FR-5**: The program **must** recognize an empty line as an instruction to stop reading further input and terminate. + +=== 3.3 Processing Requirements +. **FR-6**: (In direct “echo” mode) The system **must** reprint the exact line without changes. +. **FR-7**: (In exclamation mode) The system **must** append a “!” to the user’s text before printing. +. **FR-8**: (In YAML mode) The system **must** record or display the user input in valid YAML format, enabling replay or inspection. + +=== 3.4 Output and Logging +. **FR-9**: The system **must** show each processed line on the console or standard output. +. **FR-10**: (Optional) The system **may** write lines to a log file or a Chronicle Queue for further inspection or replay. + +=== 3.5 Replay Functionality +. **FR-11**: The system **may** read lines from a pre-recorded YAML file. +. **FR-12**: For each read line, the system **must** print it to the console, simulating “replaying” past inputs. + +== 4. Non-Functional Requirements + +=== 4.1 User Experience +* The software **should** provide a simple, beginner-friendly environment. +* The console output **should** contain short messages guiding the user (e.g., `"Type something"` or `"Press Enter on a blank line to exit"`). + +=== 4.2 Performance +* This demonstration is for **educational** purposes; it does **not** require high-volume or low-latency optimizations. +* Performance is **not** a primary focus, but typical run times for reading and printing a few lines should be near-instant on standard hardware. + +=== 4.3 Dependencies and Environment +* Requires **Java 8** (or later) and **Maven** to build. +* No special hardware or OS constraints. Windows, Linux, and macOS are all supported. + +== 5. User Interactions + +1. The user starts one of the main classes (e.g., `RecordInputToConsoleMain`). +2. The program displays a message: “Type something” or “Input> ”. +3. The user types a line of text and presses Enter. +4. The program processes (or logs) that text and prints the result. +5. The user may type more lines; each is processed in the same manner. +6. If the user presses Enter on an empty line, the program exits gracefully. + +== 6. Testing and Validation + +=== 6.1 Manual Testing +Because the hello-world module is interactive, testers can: +1. Launch one of the main classes (e.g., `DirectWithExclamationMain`). +2. Type sample lines to see whether exclamation marks are appended. +3. Verify the console output matches expectations. + +=== 6.2 Automated Testing +While primarily a demonstration, you can still create simple unit tests for: +* Console I/O (mocking input streams). +* Validation of exclamation appending logic or YAML formatting logic. + +=== 6.3 Potential Edge Cases +* Empty input lines (should exit). +* Very long lines (should still be echoed or appended). +* Special characters and punctuation. + +== 7. Conclusion + +The *hello-world* example program serves as a gentle introduction to Chronicle Queue’s capabilities. By satisfying the above requirements—reading user input, optionally transforming or logging it, and replaying stored data—this module helps beginners understand the foundational concepts of event-driven applications. Further exploration can include queue-based message passing, advanced transformations, and integration with other modules in the Chronicle Queue Demo project. diff --git a/hello-world/src/main/hello-world-requirements.adoc b/hello-world/src/main/hello-world-requirements.adoc new file mode 100644 index 0000000..e69de29 diff --git a/images/Clone.PNG b/images/Clone.png similarity index 100% rename from images/Clone.PNG rename to images/Clone.png diff --git a/images/Two-hop-latency.PNG b/images/Two-hop-latency.png similarity index 100% rename from images/Two-hop-latency.PNG rename to images/Two-hop-latency.png diff --git a/images/directory.PNG b/images/directory.png similarity index 100% rename from images/directory.PNG rename to images/directory.png diff --git a/md-pipeline/src/main/java/org/trading/AggregatorImpl.java b/md-pipeline/src/main/java/org/trading/AggregatorImpl.java index ef77b59..5bd4875 100644 --- a/md-pipeline/src/main/java/org/trading/AggregatorImpl.java +++ b/md-pipeline/src/main/java/org/trading/AggregatorImpl.java @@ -28,7 +28,7 @@ public class AggregatorImpl implements AggregatorIn { /** * Constructor for the AggregatorImpl class. - * Initializes the AggregatorOut instance for output handling. + * Initialises the AggregatorOut instance for output handling. * * @param out An implementation of the AggregatorOut interface for handling aggregated data */ diff --git a/md-pipeline/src/main/java/org/trading/OMSImpl.java b/md-pipeline/src/main/java/org/trading/OMSImpl.java index ed1e008..797a21b 100644 --- a/md-pipeline/src/main/java/org/trading/OMSImpl.java +++ b/md-pipeline/src/main/java/org/trading/OMSImpl.java @@ -19,7 +19,7 @@ public class OMSImpl implements OMSIn { /** * This is the constructor for the OMSImpl class. - * It initializes the out instance variable with the provided OMSOut object. + * It initialises the out instance variable with the provided OMSOut object. * * @param out An instance of the OMSOut interface that will handle the output of the OMS. */ @@ -29,7 +29,7 @@ public OMSImpl(OMSOut out) { /** * This is the main method for the OMSImpl class. - * It sets up the system and initializes the necessary components for running the OMS. + * It sets up the system and initialises the necessary components for running the OMS. * * @param args Command-line arguments (not used in this implementation) */ diff --git a/md-pipeline/src/main/java/org/trading/StrategyImpl.java b/md-pipeline/src/main/java/org/trading/StrategyImpl.java index c3ceb3e..8cbf696 100644 --- a/md-pipeline/src/main/java/org/trading/StrategyImpl.java +++ b/md-pipeline/src/main/java/org/trading/StrategyImpl.java @@ -21,7 +21,7 @@ public class StrategyImpl implements AggregatorOut { /** * Constructor for the StrategyImpl class. - * It initializes the output interface for OMS and a new order object. + * It initialises the output interface for OMS and a new order object. * * @param out An instance of {@link OMSIn} to be used for communication with the OMS */ diff --git a/order-processor.ad b/order-processor.ad new file mode 100644 index 0000000..9345fc9 --- /dev/null +++ b/order-processor.ad @@ -0,0 +1,88 @@ += Order Processor Submodule Documentation +:toc: +:toclevels: 3 + +== Overview + +The **order-processor** submodule is a demonstration of an Order Management System (OMS) implementation. It integrates Chronicle Queue for high-performance message handling and leverages the FIX protocol (4.2) for financial message semantics. The submodule includes: + +- Functional implementations for creating, modifying, and canceling orders. +- Test coverage using unit tests and YAML-based Behavior-Driven Development (BDD) scenarios. +- Utilities for benchmarking and order viewing. + +== Directory Structure + +All directories here are under `order-processor` directory. + +The `order-processor` submodule consists of: + +- **src/main/java**: Source code for OMS interfaces and implementations. +- **src/main/adoc**: AsciiDoc documentation of requirements and FIX mappings. +- **src/test**: Unit tests and YAML-based test scenarios. + +== Key Components + +=== OMS API +Defines inbound (`OMSIn`) and outbound (`OMSOut`) interfaces: + +- `OMSIn`: Handles incoming order-related events such as `NewOrderSingle` and `CancelOrderRequest`. +- `OMSOut`: Publishes responses like `ExecutionReport` and `OrderCancelReject`. + +=== OMS Implementation +- **OMSImpl**: Implements `OMSIn` and processes messages. + - Generates unique `orderID` using a nanosecond timestamp. + - Outputs responses via `OMSOut`. + - Designed for single-threaded operation. + +=== Data Transfer Objects (DTOs) +Encapsulates order-related data: + +- Examples: `NewOrderSingle`, `CancelOrderRequest`, `ExecutionReport`. +- Validated before processing using `validate()` methods. + +=== Utilities + +- **OrderAdderMain**: Simulates order creation. +- **OrderViewerMain**: Displays order messages from the queue. +- **OMSBenchmarkMain**: Benchmarks message throughput and latency. + +== Documentation + +=== Requirements +AsciiDoc files outline the OMS requirements: + +- `oms-requirements.adoc`: Functional requirements for `OMSImpl`. +- `oms-api-requirements.adoc`: API specifications. +- `test-requirements.adoc`: Guidelines for unit and YAML-based tests. + +=== FIX 4.2 Mappings +- `selected-fix-4.2.adoc`: Maps DTO fields to FIX tags. + +== Testing + +- **Unit Tests**: Cover edge cases (e.g., negative quantities, missing fields). +- **BDD Tests**: YAML files specify inputs and expected outputs: + - `newOrderSingle`: Test for `NewOrderSingle` events. + - `cancelOrderRequest`: Test for cancel requests. + - `cancelAll`: Test for mass cancel scenarios. + +== Performance + +- Benchmarking with **OMSBenchmarkMain**. +- Handles 100,000 messages/second with sub-microsecond latencies. + +== Usage + +1. Clone the repository. +2. Build the project with Maven: + + mvn clean install + +3. Run examples: + - `OrderAdderMain` to create orders. + - `OrderViewerMain` to view queue messages. + - `OMSBenchmarkMain` for performance testing. + +== Conclusion + +The `order-processor` submodule demonstrates a robust, high-performance OMS using Chronicle libraries. It ensures alignment with FIX 4.2 and provides comprehensive testing and benchmarking capabilities. diff --git a/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java b/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java index 0e583a3..99bc82d 100644 --- a/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java +++ b/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java @@ -66,7 +66,7 @@ public static void main(String[] args) throws IOException { // Inform the user to add an order System.out.println("\nHit blank line to add an order, anything else to exit"); - // Initialize a BufferedReader to read user input + // Initialise a BufferedReader to read user input BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int orderCounter = 0; while ("".equals(br.readLine())) { diff --git a/reference.adoc b/reference.adoc new file mode 100644 index 0000000..cd4215a --- /dev/null +++ b/reference.adoc @@ -0,0 +1,98 @@ += Reference +:toc: +:toclevels: 3 + +This document compiles various reference materials relevant to this project, such as coding style guidelines, development workflow steps, terminology, and domain-specific mappings (e.g., FIX 4.2 tags). + +== Style Guide + +This section outlines the coding conventions and documentation standards the team follows. + +=== Language and Wording +* **British English** for code comments, doc text, and commit messages. +* Maintain a concise, professional tone. Avoid slang or overly informal expressions. + +=== Code Formatting +* **Indentation**: 4 spaces, no tabs. +* **Line Length**: Typically 128 characters or fewer if possible. +* **Braces**: +** +- Open brace on the same line (`if (condition) {`). +- Close brace on a new line. +* **Blank Lines**: +- One blank line between method definitions. +- Group related lines logically for readability. + +=== Naming Conventions +* **Classes**: PascalCase (e.g., `OrderService`). +* **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). +* **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). +* **Packages**: all lowercase, possibly with dots (`com.example.myproject`). + +=== Comments and Documentation +* Use Javadoc (`/** ... */`) for public classes/methods, describing purpose, parameters, return values, and exceptions. +* Avoid excessive inline comments; code should largely speak for itself. +* Keep commit messages in present tense, referencing issue IDs where relevant. + +=== Additional Points +* Some modules rely on `.ad` or `.adoc` files for large code or doc summaries, especially if token constraints apply for AI usage. +* Use xref where possible to link sections in AsciiDoc. + +== Workflow + +This project embraces an AI-assisted, documentation-first process known as AIDE (Artificial Intelligence Development Environment). + +=== Iterative Stages +1. **Document Requirements** +- Write or update `.adoc` content reflecting new features or changes. +- This serves as the “single source of truth.” +2. **Write or Update Tests** +- Create or refine unit, integration, or YAML-based scenario tests, mirroring the documented requirements. +3. **Write or Update Code** +- Let AI or standard IDE tools generate stubs, boilerplate, or scaffolding based on the docs and tests. +- Manually refine logic where needed. +4. **Review** +- AI highlights discrepancies between docs/tests/code. Humans validate correctness. +5. **Repeat** +- Each change in requirements triggers this loop again, keeping everything in sync. + +=== Document-First Benefits + +* **Minimal Drift**: Fewer mismatches between code and docs. +* **Faster Onboarding**: Updated documentation clarifies system design. +* **Better Quality**: Frequent iteration on specs → more thorough testing. + +== FIX 4.2 Mapping + +For modules referencing FIX protocol (like the Order Processor), the following table maps relevant fields to standard FIX 4.2 tags. Adjust the table to your actual usage: + +|FIX Tag |Field/DTO Property |Description +|-------:|-------------------|------------------------------------------- +|35=D |`NewOrderSingle` |Message type for new orders +|11 |`clOrdID` |Unique client order ID +|54 |`side` |Side of the trade (`buy`/`sell`) +|38 |`orderQty` |Quantity of the order +|44 |`price` |Price for limit orders +|39 |`ordStatus` |Order status (e.g., `new`, `filled`) +|150 |`execType` |Execution type (partial fill, fill, canceled) +|**...** |**(others as needed)**|Expand based on your system’s data + +== Additional References + +Below are some advanced or miscellaneous notes not covered elsewhere. + +=== Large File Summaries +- Certain `.ad` or `.adoc` summaries exist for very large classes or resources that rarely change. +- This practice keeps token usage low for AI processes, while preserving essential context in a succinct form. + +=== Performance Insights +- Typical latencies can reach ~1.5µs (shared memory) or ~20µs (TCP) under load. +- For more detailed throughput or latency data, see xref:usage-and-tests.adoc#benchmarks[Usage & Tests → Benchmarking]. + +=== AI Prompt Templates +- If your build includes scripts or templated prompts for the AI, store them in version control as well. +- Keep track of prompt changes to maintain consistency across feature expansions. + +== Closing Notes + +Refer back to xref:architecture.adoc[Architecture] for design overviews or xref:usage-and-tests.adoc[Usage & Tests] for instructions on running modules and tests. By maintaining these references, the project stays coherent and approachable to both new and returning contributors. diff --git a/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java b/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java index be23cb9..f344914 100644 --- a/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java +++ b/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java @@ -27,7 +27,7 @@ public static void main(String[] args) throws IOException { GenericRecord user = avro.getGenericRecord(); user.put("name", "Ben"); user.put("favorite_number", 7); - user.put("favorite_color", "red"); + user.put("favorite_colour", "red"); avro.writeToOS(user, dc.wire().bytes().outputStream()); } diff --git a/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java b/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java index 5a6dd0a..03c2cba 100755 --- a/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java +++ b/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java @@ -20,7 +20,7 @@ * *

  • * {@link net.openhft.chronicle.queue.simple.translator.TranslatorMain}: - * The entry point that initializes and configures a {@link SimpleTranslator} to bridge messages + * The entry point that initialises and configures a {@link SimpleTranslator} to bridge messages * between the English and French queues. *
  • *
  • diff --git a/usage-and-tests.adoc b/usage-and-tests.adoc new file mode 100644 index 0000000..478eeb2 --- /dev/null +++ b/usage-and-tests.adoc @@ -0,0 +1,216 @@ += Usage & Tests +:toc: +:toclevels: 3 + +This document explains how to build, run, and test the various modules and demos in this project. It also covers the basic steps for performance benchmarking and troubleshooting common issues. + +[#building-the-project] +== Building the Project + +Use Maven (or your preferred build tool) to compile all modules: + +---- +mvn clean install +---- + +If you want to skip tests (for faster iteration): +---- +mvn clean install -DskipTests +---- + +[#running-examples] +== Running Examples by Module + +Below are instructions for starting each primary module or demo. In most cases, you will run a main class via Maven’s `exec:java` plugin, or by launching it directly from an IDE. + +[#hello-world] +=== Hello World + +The simplest demonstration of reading input, optionally modifying it, and printing results. +Commands (examples, might vary slightly depending on your `pom.xml` executions): + +1. **RecordInputToConsoleMain** + ---- + mvn exec:java@RecordInputToConsoleMain + ---- + - Reads input lines, echoes them back to the console. + +2. **RecordInputAsYamlMain** + ---- + mvn exec:java@RecordInputAsYamlMain + ---- + - Logs each line in YAML format to standard output. + +3. **ReplayOutputMain** + ---- + mvn exec:java@ReplayOutputMain -Dexec.args="someFile.yaml" + ---- + - Replays previously logged YAML lines from a file or queue. + +4. **DirectWithExclamationMain** + ---- + mvn exec:java@DirectWithExclamationMain + ---- + - Inserts an exclamation mark into each line before printing it. + +[#event-routing] +=== Event Routing + +Demonstrates the `ViaIn`/`ViaOut` pattern, or advanced filters like `ViaThreeFive`: + +1. **Set Up Input**: Provide input YAML files (`in.yaml`) describing messages. +2. **Run**: + ---- + mvn exec:java -Dexec.mainClass=run.chronicle.routing.inout.ViaThreeFive + ---- + (Adjust to your actual main classes or different test runners.) + +3. **Check Output**: Compare the resulting queue or console logs with expected YAML (`out.yaml`). + +[#md-pipeline] +=== Market Data Pipeline (md-pipeline) + +A multi-service pipeline: + +1. **ExchangeSimulator** – Writes incremental market data. + ---- + mvn exec:java@generate + ---- +2. **Aggregator** – Consumes data increments, aggregates them, and outputs snapshots. + ---- + mvn exec:java@aggregator + ---- +3. **Strategy** – Reads aggregated data, decides on trades, writes out orders. + ---- + mvn exec:java@strategy + ---- + +Optionally, **OMS** can consume the strategy's orders. +In separate terminals, watch each queue with: + +[source] +---- +mvn exec:java@tailf -Dqueue=agg-in +mvn exec:java@tailf -Dqueue=agg-out +mvn exec:java@tailf -Dqueue=strat-out +---- + +[#order-processor] +=== Order Processor (OMS) + +Implements a basic Order Management System referencing FIX 4.2. Typical usage: + +1. **Add Orders** + ---- + mvn exec:java@OrderAdderMain + ---- + - Generates `NewOrderSingle` events. + +2. **View Orders** + ---- + mvn exec:java@OrderViewerMain + ---- + - Reads the queue to display order or execution events. + +3. **Benchmark OMS** + ---- + mvn exec:java@OMSBenchmarkMain + ---- + - Tests throughput or latency for order submission and responses. + +''' + +[#testing] +== Testing + +This project uses a mix of **unit tests**, **integration tests**, and **YAML-based scenario tests**. Most are in `src/test/java` or `src/test/resources`. + +[#unit-tests] +=== Unit Tests + +- Classes named `XxxTest` (e.g., `AddsExclamationTest`, `SifterImplTest`). +- Typical JUnit or JUnit5 style. +- Run all at once with: +---- +mvn test +---- + +[#yaml-tests] +=== YAML-Based Tests + +- Many modules use BDD-like YAML files (`in.yaml`, `out.yaml`, `_setup.yaml`) to define inputs and expected outputs. +- Tools like `YamlTester` or `TextMethodTester` parse these files and compare actual vs. expected queue events. +- **Example**: `ViaThreeFive2Test` references `three-five/in.yaml` and `three-five/out.yaml`. +- To update or regress outputs: +---- +mvn test -Dregress.tests +---- +This can overwrite `out.yaml` if you’re intentionally changing expected outputs. + +[#integration-tests] +=== Integration Tests + +- Some scenarios span multiple processes (e.g., the market data pipeline). +- Start each submodule or main class in a separate terminal so they pass data via Chronicle Queues. +- Verify logs or queue outputs match expectations (YAML out files or console prints). + +[#coverage] +=== Coverage & Tools + +- (Optional) Use Jacoco or another coverage plugin to measure how thoroughly your tests cover the code. + +''' + +[#benchmarks] +== Benchmarking + +Several classes in `benchmarks/` measure throughput and latency: + +* **LatencyDistributionMain** +- Usage: +---- +mvn exec:java -Dexec.mainClass=chronicle.queue.benchmark.LatencyDistributionMain \ +-Dsize=60 -Dthroughput=100000 +---- +- Parameterize `size`, `throughput`, etc. +- Checks microsecond-level round-trip latencies. + +* **ThroughputMain** +- Writes and reads millions of messages to measure raw throughput. +- Example: +---- +mvn exec:java -Dexec.mainClass=chronicle.queue.benchmark.ThroughputMain \ +-Dtime=10 -Dsize=60 +---- + +Keep an eye on CPU scaling, GC logs, and whether you’re using shared memory or TCP. Results typically show 1+ million msgs/sec or sub-micro latencies, depending on hardware. + +''' + +[#troubleshooting] +== Troubleshooting + +- **No Output?** +Double-check your queue path. Maybe specify `-Dpath=queue` or ensure each module points to the correct folder. + +- **File Locks or Collisions** +On Windows, ensure no leftover processes hold open file handles. + +- **YAML Tester Mismatch** +If actual vs. expected outputs differ unexpectedly, see if your scenario changed. Use `-Dregress.tests` only if you deliberately want to update your baseline. + +- **Excessive GC** +Chronicle aims for minimal allocations, so check if you introduced object churn in custom code. Potentially switch to direct Bytes usage or off-heap. + +''' + +[#summary] +== Summary + +You now have clear instructions on: + +1. **How to build and run** each module or submodule example (hello-world, event-routing, md-pipeline, order-processor). +2. **How to test** using either standard JUnit or advanced YAML-based scenario tests. +3. **How to benchmark** with throughput and latency tools provided in `benchmarks/`. + +For deeper architectural background, see xref:architecture.adoc[Architecture]. For style guides and advanced references, see xref:reference.adoc[Reference]. \ No newline at end of file