diff --git a/docs/complexity-antipatterns-and-refactoring-strategies.md b/docs/complexity-antipatterns-and-refactoring-strategies.md index 6f1b7c79..0a9a4771 100644 --- a/docs/complexity-antipatterns-and-refactoring-strategies.md +++ b/docs/complexity-antipatterns-and-refactoring-strategies.md @@ -6,11 +6,11 @@ Software development is an inherently complex endeavor. As systems evolve and features are added, the intricacy of the codebase tends to increase, often leading to challenges in maintenance, scalability, and developer productivity. "Any fool can write code that a computer can understand. Good programmers write -code that humans can understand".1 This adage underscores a fundamental truth: +code that humans can understand". This adage underscores a fundamental truth: the long-term viability of a software project hinges significantly on its comprehensibility and modifiability. Unchecked complexity can transform a once-manageable system into a "full-blown algorithmic monster," torpedoing -performance and maintainability.2 +performance and maintainability. This report aims to equip code implementers and maintainers with a deeper understanding of two critical complexity metrics—Cyclomatic Complexity and @@ -35,26 +35,26 @@ codebase. Cyclomatic Complexity (CC), developed by Thomas J. McCabe, Sr. in 1976, is a quantitative measure of the number of linearly independent paths through a -program's source code.3 It essentially quantifies the structural complexity of a -program by counting decision points that can affect the execution flow.4 This +program's source code. It essentially quantifies the structural complexity of a +program by counting decision points that can affect the execution flow. This metric is computed using the control-flow graph of the program, where nodes represent indivisible groups of commands and directed edges connect nodes if one -command can immediately follow another.3 +command can immediately follow another. The formula for Cyclomatic Complexity is often given as M=E−N+2P, where E is the number of edges, N is the number of nodes, and P is the number of connected -components (typically 1 for a single program or method).3 A simpler formulation +components (typically 1 for a single program or method). A simpler formulation for a single subroutine is M=number of decision points+1, where decision points include constructs like -`if` statements and conditional loops.3 +`if` statements and conditional loops. Thresholds and Implications: High Cyclomatic Complexity indicates a more intricate control flow, which -directly impacts testability and maintainability.1 More paths mean more test -cases are required for comprehensive coverage.4 McCabe proposed the following -risk categorization based on CC scores 3: +directly impacts testability and maintainability. More paths mean more test +cases are required for comprehensive coverage. McCabe proposed the following +risk categorization based on CC scores: - 1-10: Simple procedure, little risk. @@ -65,23 +65,23 @@ risk categorization based on CC scores 3: - 50: Untestable code, very high risk. SonarQube suggests similar thresholds, with scores above 20 generally - indicating a need for refactoring.1 While CC is valuable for assessing how + indicating a need for refactoring. While CC is valuable for assessing how difficult code will be to test, it doesn't always align with how difficult it - is for a human to understand.5 + is for a human to understand. ### B. Cognitive Complexity: Measuring Understandability Cognitive Complexity, a metric notably championed by SonarSource, addresses a different facet of code complexity: how difficult a piece of code is to -intuitively read and understand by a human.1 Unlike Cyclomatic Complexity, which +intuitively read and understand by a human. Unlike Cyclomatic Complexity, which is rooted in mathematical graph theory, Cognitive Complexity aims to more accurately reflect the mental effort required to comprehend the control flow of -a unit of code.7 It acknowledges that developers spend more time reading and -understanding code than writing it.8 +a unit of code. It acknowledges that developers spend more time reading and +understanding code than writing it. Core Principles of Calculation: -Cognitive Complexity is incremented based on three main rules 8: +Cognitive Complexity is incremented based on three main rules. 1. **Breaks in Linear Flow:** Each time the code breaks the normal linear reading flow (e.g., loops, conditionals like `if`/`else`/`switch`, @@ -97,23 +97,23 @@ Cognitive Complexity is incremented based on three main rules 8: penalties as the raw statements they encapsulate. Method calls are generally "free" in terms of cognitive complexity, as a well-chosen name summarizes the underlying logic, allowing readers to grasp the high-level view before diving - into details. Recursive calls, however, do increment the score.8 + into details. Recursive calls, however, do increment the score. For instance, a `switch` statement with multiple cases might have a high Cyclomatic Complexity because each case represents a distinct path for testing. However, if the structure is straightforward and easy to follow, its Cognitive -Complexity might be relatively low.5 Conversely, deeply nested conditional -logic, even with fewer paths, can significantly increase Cognitive Complexity -due to the mental effort required to track the conditions and context.6 +Complexity might be relatively low. Conversely, deeply nested conditional logic, +even with fewer paths, can significantly increase Cognitive Complexity due to +the mental effort required to track the conditions and context. Thresholds and Implications: Code with high Cognitive Complexity is harder to read, understand, test, and -modify.8 SonarQube, for example, raises issues when a function's Cognitive +modify. SonarQube, for example, raises issues when a function's Cognitive Complexity exceeds a certain threshold, signaling that the code should likely be -refactored into smaller, more manageable pieces.8 The primary impact of high +refactored into smaller, more manageable pieces. The primary impact of high Cognitive Complexity is a slowdown in development and an increase in maintenance -costs.8 +costs. ### Table 1: Cyclomatic vs. Cognitive Complexity @@ -144,20 +144,20 @@ crucial metric for sustainable software development. ## III. The "Bumpy Road" Antipattern The "Bumpy Road" is a code smell that visually and structurally represents -functions or methods laden with excessive and poorly organized complexity.6 +functions or methods laden with excessive and poorly organized complexity. Coined by Adam Tornhill, this antipattern describes code where the indentation level repeatedly increases and decreases, forming a "lumpy" or "bumpy" visual -pattern when looking at the code's shape.6 +pattern when looking at the code's shape. ### A. Definition and Characteristics A method exhibiting the Bumpy Road antipattern typically contains multiple -sections, each characterized by deep nesting of conditional logic or loops.6 -Each "bump" in the road—a segment of deeply indented code—often signifies a -distinct responsibility or a separate logical chunk that has not been properly -encapsulated.6 +sections, each characterized by deep nesting of conditional logic or loops. Each +"bump" in the road—a segment of deeply indented code—often signifies a distinct +responsibility or a separate logical chunk that has not been properly +encapsulated. -Key characteristics include 6: +Key characteristics include: - **Multiple Chunks of Nested Logic:** The function isn't just deeply nested in one place, but has several such areas. @@ -176,9 +176,9 @@ Key characteristics include 6: - **Feature Entanglement:** In imperative languages, this structure increases the risk of feature entanglement, where different logical concerns become intertwined, leading to complex state management and a higher likelihood of - defects.6 + defects. -The severity of a Bumpy Road can be assessed by 6: +The severity of a Bumpy Road can be assessed by: - The depth of nesting within each bump (deeper is worse). @@ -191,15 +191,15 @@ The severity of a Bumpy Road can be assessed by 6: Fundamentally, a Bumpy Road signifies a function that is trying to do too many things, violating the Single Responsibility Principle. It acts as an obstacle to comprehension, forcing developers to slow down and pay meticulous attention, -much like a physical bumpy road slows down driving.6 +much like a physical bumpy road slows down driving. ### B. How It Forms and Its Impact The Bumpy Road antipattern, like many software antipatterns, often emerges from development practices that prioritize short-term speed over long-term structural -integrity.2 Rushed development cycles, lack of clear design, or cutting corners +integrity. Rushed development cycles, lack of clear design, or cutting corners on maintenance can lead to the gradual accumulation of conditional logic within -a single function.2 As new requirements or edge cases are handled, developers +a single function. As new requirements or edge cases are handled, developers might add more `if` statements or loops to an existing method rather than stepping back to @@ -209,25 +209,25 @@ The impact of this antipattern is significant: - **Reduced Readability and Understandability:** The convoluted structure makes it extremely difficult for developers to follow the logic and understand the - method's overall purpose.6 + method's overall purpose. - **Increased Maintenance Costs:** Modifying or debugging such code is time-consuming and error-prone. A change in one "bump" can have unintended consequences in another, especially if state is shared or manipulated across - these logical chunks.2 + these logical chunks. - **Higher Defect Rates:** The heavy tax on working memory and the risk of - feature entanglement contribute to a higher likelihood of introducing bugs.6 + feature entanglement contribute to a higher likelihood of introducing bugs. - **Impeded Evolvability:** Adding new features or adapting to changing requirements becomes a daunting task, as the existing complex structure - resists modification.6 + resists modification. - **Decreased Developer Productivity and Morale:** Continuously working with - such code can be frustrating and demotivating.2 + such code can be frustrating and demotivating. The Bumpy Road is a strong predictor of code that is expensive to maintain and -risky to evolve.6 It's a clear signal that the code is not well-aligned with how +risky to evolve. It's a clear signal that the code is not well-aligned with how human brains process information, making it a prime candidate for refactoring. ## IV. Navigating and Rectifying the Bumpy Road @@ -242,28 +242,28 @@ Preventing the Bumpy Road begins with a commitment to sound software engineering principles from the outset. 1. **Adherence to Single Responsibility Principle (SRP):** Ensure that each - function or method has one clear, well-defined responsibility.8 If a function + function or method has one clear, well-defined responsibility. If a function starts handling multiple distinct logical blocks, it's a sign that it needs to be decomposed. 2. **Incremental Refactoring:** Don't wait for complexity to accumulate. Refactor code regularly as part of the development process, not as a - separate, deferred task.10 "Make the change easy, and then make the easy - change".12 + separate, deferred task. "Make the change easy, and then make the easy + change". 3. **Early Abstraction:** When a new piece of logic is being added, consider if it represents a distinct concept that warrants its own function or class. - Well-named abstractions improve clarity.8 + Well-named abstractions improve clarity. 4. **Code Reviews Focused on Structure:** Code reviews should not only check for correctness but also for structural integrity and complexity. Reviewers should look for emerging "bumps" or excessive nesting. 5. **Using Complexity Metrics:** Regularly monitor Cognitive Complexity scores - using tools like SonarQube.1 Set thresholds and address violations promptly. + using tools like SonarQube. Set thresholds and address violations promptly. 6. **Return Early / Guard Clauses:** To avoid deep nesting for validation or - pre-condition checks, process exceptional cases first and return early.8 This + pre-condition checks, process exceptional cases first and return early. This flattens the main logic path. For example, instead of: ```cpp @@ -304,22 +304,22 @@ Complexity for the main execution path. ### B. Rectifying Existing Bumpy Road Code Once a Bumpy Road is identified, the primary remediation strategy is the -**Extract Method** refactoring.6 +**Extract Method** refactoring. 1. **Identify Logical Chunks:** Each "bump" or deeply nested section often corresponds to a specific sub-task or responsibility within the larger - method.6 + method. 2. **Extract to New Methods/Functions:** Encapsulate each identified chunk into - its own well-named method or function.8 The name of the new method should + its own well-named method or function. The name of the new method should clearly describe its purpose. - This breaks down the large, complex function into smaller, more manageable, - and understandable pieces.8 + and understandable pieces. - Even if the overall Cognitive Complexity of the program doesn't change significantly, the complexity is spread out, making individual functions - easier to grasp.8 + easier to grasp. 3. **Parameterize Extracted Methods:** Pass necessary data to the new methods as parameters. Avoid relying on shared mutable state within the original class @@ -327,13 +327,13 @@ Once a Bumpy Road is identified, the primary remediation strategy is the 4. **Iterative Refinement:** Refactoring complex code is often an iterative process. After initial extractions, further opportunities for simplification - or abstraction may become apparent.10 Sometimes, extracting methods reveals + or abstraction may become apparent. Sometimes, extracting methods reveals that a more significant restructuring, perhaps involving new classes or design patterns (like the Command pattern for different actions within the - bumps), is warranted.10 + bumps), is warranted. Tools like CodeScene can automatically identify Bumpy Roads and even suggest or -perform auto-refactoring for certain languages.6 +perform auto-refactoring for certain languages. ### C. Red Flags Portending the Bumpy Road @@ -341,33 +341,33 @@ Being vigilant for early warning signs can help prevent a minor complexity issue from escalating into a full-blown Bumpy Road. 1. **Increasing Cognitive Complexity Scores:** A rising Cognitive Complexity - score for a method in static analysis tools is a direct indicator.8 + score for a method in static analysis tools is a direct indicator. 2. **Deeply Nested Logic:** Even a single area of deep nesting (more than 2-3 levels) should be a concern. If multiple such areas appear in the same - function, it's a strong red flag.6 + function, it's a strong red flag. 3. **Functions Doing "Too Much":** If describing what a function does requires using the word "and" multiple times (e.g., "it validates the input, AND processes the data, AND then updates the UI, AND logs the result"), it's - likely violating SRP and on its way to becoming bumpy.8 + likely violating SRP and on its way to becoming bumpy. 4. **Frequent Modifications to the Same Function for Different Reasons:** If a function needs to be changed for various unrelated feature enhancements or bug fixes, it indicates it has too many responsibilities (related to the "Shotgun Surgery" code smell, which can be a consequence or co-occur with - Bumpy Roads).2 + Bumpy Roads). 5. **Difficulty in Unit Testing:** If a method becomes hard to unit test due to numerous conditions and paths that need to be set up and verified, it often correlates with high complexity that could manifest as a Bumpy Road. 6. **Code "Smells" like Long Method:** A Bumpy Road is often, though not always, - a Long Method.15 The length itself isn't the core problem, but it provides - more space for bumps to accumulate. + a Long Method. The length itself isn't the core problem, but it provides more + space for bumps to accumulate. 7. **Declining Code Health Metrics:** Tools like CodeScene provide "Code Health" - metrics which can degrade if Bumpy Roads are introduced.6 + metrics which can degrade if Bumpy Roads are introduced. By proactively addressing these red flags through disciplined refactoring, teams can maintain a smoother, more navigable codebase. @@ -385,21 +385,21 @@ building truly maintainable systems. Separation of Concerns is a design principle that advocates for dividing a computer program into distinct sections, where each section addresses a separate -concern.17 A "concern" is a set of information that affects the code of a -computer program. Modularity is achieved by encapsulating information within a -section of code that has a well-defined interface.17 +concern. A "concern" is a set of information that affects the code of a computer +program. Modularity is achieved by encapsulating information within a section of +code that has a well-defined interface. The Bumpy Road antipattern is a direct violation of SoC. Each "bump" in the code often represents a distinct concern or responsibility that has been improperly -co-located within a single method.6 For example, a single method might handle +co-located within a single method. For example, a single method might handle input validation, business logic processing for different cases, data transformation, and error handling for each case, all intermingled. Refactoring a Bumpy Road by extracting methods inherently applies SoC, as each extracted -method ideally handles a single, well-defined concern.10 This leads to increased +method ideally handles a single, well-defined concern. This leads to increased freedom for simplification, maintenance, module upgrade, reuse, and independent -development.17 While SoC might introduce more interfaces and potentially more -code to execute, the benefits in clarity and maintainability often outweigh -these costs, especially as systems grow.17 +development. While SoC might introduce more interfaces and potentially more code +to execute, the benefits in clarity and maintainability often outweigh these +costs, especially as systems grow. Consider a function that processes different types of user commands. A Bumpy Road approach might have a large `if-else if-else` structure, with each block @@ -413,12 +413,12 @@ system easier to understand, test, and extend with new commands. 2\. Command Query Responsibility Segregation (CQRS) CQRS is an architectural pattern that segregates operations that modify state -(Commands) from operations that read state (Queries).18 Commands are task-based +(Commands) from operations that read state (Queries). Commands are task-based and should represent specific business intentions (e.g., -`BookHotelRoomCommand` rather than `SetReservationStatusCommand`).18 Queries, on +`BookHotelRoomCommand` rather than `SetReservationStatusCommand`). Queries, on the other hand, never alter data and return Data Transfer Objects (DTOs) -optimized for display needs.18 +optimized for display needs. While CQRS operates at a higher architectural level than a single Bumpy Road method, the principles are related. Complex methods often arise when read and @@ -429,8 +429,8 @@ together. operations in terms of distinct commands and queries from the start. This naturally leads to smaller, more focused methods or handlers for each command and query, reducing the likelihood of a single method accumulating many - "bumps" of unrelated logic.18 For instance, a method that both fetches data - for a complex report and then allows modifications based on that report could + "bumps" of unrelated logic. For instance, a method that both fetches data for + a complex report and then allows modifications based on that report could become very complex. CQRS would split this into a query to fetch the data and separate commands for any modifications. @@ -438,39 +438,39 @@ together. characteristics because it handles multiple types of updates or decisions leading to state changes, CQRS principles can guide its refactoring. The different "bumps" that correspond to different update logics could be - refactored into separate command handlers.20 This aligns with the Single + refactored into separate command handlers. This aligns with the Single Responsibility Principle, as each command handler focuses on a single way of - modifying state.20 + modifying state. - **God Objects and CQRS:** The "God Object" or "God Class" antipattern, where a - single class hoards too much logic and responsibility 2, often leads to - methods within that class becoming Bumpy Roads. CQRS can help decompose God - Objects by separating their command-handling responsibilities from their - query-handling responsibilities, potentially leading to smaller, more focused - classes (e.g., one class for command processing, another for query processing, - or even finer-grained handlers).21 This separation simplifies each part, - making them easier to manage and reducing the cognitive load associated with - the original monolithic structure. + single class hoards too much logic and responsibility, often leads to methods + within that class becoming Bumpy Roads. CQRS can help decompose God Objects by + separating their command-handling responsibilities from their query-handling + responsibilities, potentially leading to smaller, more focused classes (e.g., + one class for command processing, another for query processing, or even + finer-grained handlers). This separation simplifies each part, making them + easier to manage and reducing the cognitive load associated with the original + monolithic structure. CQRS promotes a clear separation that can prevent the kind of tangled logic that forms Bumpy Roads. By isolating write operations (commands) from read operations (queries), and by encouraging task-based commands, the system naturally tends -towards smaller, more cohesive units of behavior, thus reducing overall -cognitive complexity within individual components.18 The separation allows for +towards smaller, more cohesive units of behaviour, thus reducing overall +cognitive complexity within individual components. The separation allows for independent optimization and scaling of read and write sides, but more importantly for this discussion, it enforces a structural discipline that -discourages methods from accumulating diverse responsibilities.18 +discourages methods from accumulating diverse responsibilities. ### B. Avoiding Spaghetti Code Turning into Ravioli Code When refactoring complex, tangled code (often called "Spaghetti Code" 2), a common approach is to break it down into smaller pieces, such as functions or classes. However, if this is done without careful consideration for cohesion and -appropriate levels of abstraction, it can lead to "Ravioli Code".24 Ravioli Code +appropriate levels of abstraction, it can lead to "Ravioli Code". Ravioli Code is characterized by a multitude of small, often overly granular classes or functions, where understanding the overall program flow requires navigating through numerous tiny, disconnected pieces, making it as hard to follow as the -original spaghetti.24 +original spaghetti. **Strategies to Avoid Ravioli Code:** @@ -478,59 +478,59 @@ original spaghetti.24 ensure that the extracted code is functionally cohesive. Elements within a module (function or class) should be closely related and work together to achieve a single, well-defined purpose. Don't break down code arbitrarily - based on length alone; base it on behavior and meaningful abstractions.10 + based on length alone; base it on behaviour and meaningful abstractions. 2. **Balance Abstraction Levels:** Abstraction is about hiding unnecessary - details and exposing essential features.27 + details and exposing essential features. - **Under-abstraction** (common in Spaghetti Code) leads to duplication and - tight coupling.29 + tight coupling. - **Over-abstraction** (risk in creating Ravioli Code) can make code harder to understand due to excessive layering and indirection, where simple - operations are forced into complex object structures.24 + operations are forced into complex object structures. - The key is to find the "right" level of abstraction that simplifies the problem domain without introducing unnecessary complexity. Create abstractions when painful duplication emerges or when a clear conceptual boundary can be established, not just for the sake of having more - classes/objects.29 Start with simple, straightforward code and introduce - abstractions only when genuinely needed.30 + classes/objects. Start with simple, straightforward code and introduce + abstractions only when genuinely needed. 3. **Meaningful Naming:** Clear and descriptive names for classes, methods, and variables are crucial, especially when dealing with many small components. Good names help convey the purpose and relationships between different parts - of the code.10 + of the code. 4. **Consider the "Why," Not Just the "How":** When refactoring, understand the underlying responsibilities and collaborations. Simply breaking code into smaller pieces without a clear architectural vision can lead to Ravioli. Design patterns, when applied appropriately, can provide a "system metaphor" or structure that makes the "ravioli" manageable by revealing symmetries and - common sense in the design.25 + common sense in the design. 5. **Iterative Refactoring and Review:** Refactoring is not always a one-shot process. Continuously review the abstractions. Are they helping or hindering - understanding? Are there too many trivial classes that could be - consolidated?.10 Pair programming can also help maintain a balanced - perspective during refactoring.25 + understanding? Are there too many trivial classes that could be consolidated? + Pair programming can also help maintain a balanced perspective during + refactoring. 6. **The "You Aren't Gonna Need It" (YAGNI) Principle:** This principle helps avoid unnecessary abstractions and features, which can contribute to Ravioli - code if abstractions are created for anticipated but not actual needs.25 + code if abstractions are created for anticipated but not actual needs. 7. **Focus on System Flow:** While individual components in Ravioli code might be simple, the difficulty lies in tracing the overall execution flow. Ensure that the interactions and dependencies between components are clear and easy to follow. Sometimes, a slightly larger, more cohesive component is preferable to many tiny ones if it improves the clarity of the overall system - behavior. + behaviour. The goal is not to have the fewest classes or methods, but to have a structure where each component is easy to understand in isolation, and the interactions between components are also clear and manageable. It's about finding a "recursive Ravioli" structure, where at each level of containment, one deals -with a manageable number (e.g., 7 +/- 2) of components.25 +with a manageable number (e.g., 7 +/- 2) of components. ### C. Clean Refactoring Approaches to Reduce Cognitive Complexity @@ -562,27 +562,27 @@ and method structure. Structural pattern matching, available in languages like Python (since 3.10 with match-case) and C#, offers a declarative and expressive way to handle complex conditional logic, often replacing verbose if-elif-else chains or switch -statements.31 +statements. It works by allowing code to match against the *structure* of data—such as its type, shape, or specific values within sequences (lists, tuples) or mappings (dictionaries)—and simultaneously destructure this data, binding parts of it to -variables.32 This approach can significantly reduce cognitive load. The clarity +variables. This approach can significantly reduce cognitive load. The clarity comes from the direct mapping of data shapes to code blocks, making it easier to -understand the conditions under which a piece of code executes.31 For instance, +understand the conditions under which a piece of code executes. For instance, instead of multiple `isinstance` checks followed by key lookups and value comparisons in a nested `if` structure to parse a JSON object, a single `case` statement with a mapping pattern can define the expected structure and extract the necessary values -concisely.32 This shifts the focus from an imperative sequence of checks to a +concisely. This shifts the focus from an imperative sequence of checks to a declarative description of data shapes, which is often more intuitive. The destructuring capability is particularly powerful, as it eliminates the manual code otherwise needed to extract values after a condition has been met, reducing -boilerplate and the number of mental steps a developer must follow.32 +boilerplate and the number of mental steps a developer must follow. Consider processing different event types from a UI framework, where events are -represented as dictionaries.39 +represented as dictionaries. - *Imperative (Python-like pseudocode):* @@ -618,44 +618,44 @@ The pattern matching version is more readable and directly expresses the expected structure of each event type, reducing the cognitive effort to understand the conditions and data extraction. Key features like guards (`if` conditions on `case` statements) allow for additional non-structural checks, -further enhancing its power.32 +further enhancing its power. 2\. Embracing Declarative Programming Declarative programming focuses on describing what result is desired, rather than detailing how to achieve it step-by-step, as is typical in imperative -programming.34 This paradigm shift can significantly reduce cognitive complexity +programming. This paradigm shift can significantly reduce cognitive complexity by abstracting away low-level control flow and state management. When developers write declarative code, they operate at a higher level of -abstraction, allowing them to reason about the program's intent more directly.34 +abstraction, allowing them to reason about the program's intent more directly. This often leads to more concise, readable, and maintainable code because the "noise" of explicit iteration, temporary variables, and manual state updates is -minimized.34 Many declarative approaches also inherently favor immutability and +minimized. Many declarative approaches also inherently favor immutability and reduce side effects, which are common culprits for bugs and increased cognitive -load in imperative code.35 +load in imperative code. Examples include using SQL for database queries (specifying the desired dataset, -not the retrieval algorithm) 34, or employing functional programming constructs +not the retrieval algorithm), or employing functional programming constructs like `map`, `filter`, and `reduce` on collections instead of writing explicit loops. Refactoring imperative code to a declarative style can start small, perhaps by converting a loop that filters and transforms a list into a chain of `filter` -and `map` operations.35 The broader adoption of declarative approaches in areas +and `map` operations. The broader adoption of declarative approaches in areas like UI development (e.g., React) and data querying signifies an industry trend towards managing complexity by raising abstraction levels. However, the effectiveness of declarative programming relies on well-designed underlying abstractions; a poorly designed declarative layer might not successfully hide -complexity or could introduce its own.41 +complexity or could introduce its own. 3\. Employing Dispatcher and Command Patterns -For managing complex conditional logic that selects different behaviors (often +For managing complex conditional logic that selects different behaviours (often found in Bumpy Roads or large switch statements), the Command and Dispatcher patterns offer a structured and extensible alternative. -The **Command pattern** encapsulates a request or an action as an object.36 Each +The **Command pattern** encapsulates a request or an action as an object. Each command object implements a common interface (e.g., with an `execute()` method). This decouples the object that invokes the command from the @@ -663,17 +663,17 @@ object that knows how to perform it. Instead of a large conditional checking a type and then executing logic, different command objects can be instantiated based on the type, and then their `execute()` method is called. This promotes SRP, as each command class handles a single action, making the system easier to -test and extend.37 +test and extend. The **Dispatcher pattern** often works in conjunction with the Command pattern. A dispatcher is a central component that receives requests (which could be command objects or simple identifiers) and routes them to the appropriate -handler.37 For instance, a +handler. For instance, a `switch` statement where each `case` calls a different method can be refactored by creating an interface for handlers, a concrete handler class for each original `case`, and a dispatcher (perhaps a map from case identifiers to -handler instances) that looks up and invokes the correct handler.38 This +handler instances) that looks up and invokes the correct handler. This transforms the control flow from a monolithic conditional block into a more manageable registration and lookup mechanism. The cognitive load is reduced because developers can focus on individual, self-contained handlers and trust @@ -736,17 +736,17 @@ adding new handler classes and registering them with the dispatcher, often without modifying existing dispatcher code (aligning with the Open/Closed Principle). However, it's important to ensure that the dispatch mechanism itself remains clear and that the proliferation of small classes doesn't lead to -Ravioli Code, where the overall system flow becomes obscured.24 Clear naming -conventions and logical organization are vital.42 +Ravioli Code, where the overall system flow becomes obscured. Clear naming +conventions and logical organization are vital. -The **State pattern** is a related behavioral pattern useful when an object's -behavior changes depending on its internal state.45 Instead of using large +The **State pattern** is a related behavioural pattern useful when an object's +behaviour changes depending on its internal state. Instead of using large conditionals based on state variables, each state is encapsulated in its own -object. The context object delegates behavior to its current state object. +object. The context object delegates behaviour to its current state object. Transitions involve changing the context's state object. This is particularly effective for refactoring state machines implemented with complex -`if/else` or `switch` statements.45 +`if/else` or `switch` statements. By thoughtfully applying these refactoring strategies, developers can significantly reduce cognitive complexity, making codebases more understandable, @@ -756,7 +756,7 @@ maintainable, and adaptable to future changes. Managing software complexity, particularly the cognitive load it imposes on developers, is not a one-time task but a continuous discipline crucial for the -long-term health and success of any software project.12 This report has explored +long-term health and success of any software project. This report has explored Cyclomatic and Cognitive Complexity as vital metrics for quantifying different aspects of this challenge, with Cognitive Complexity offering a more nuanced view of human understandability. The Bumpy Road antipattern serves as a clear @@ -771,7 +771,7 @@ entanglement of responsibilities that leads to high complexity in individual components. Ultimately, writing clean code—code that is easy to read, understand, and -modify—is paramount.2 This is achieved not merely through aesthetic choices but +modify—is paramount. This is achieved not merely through aesthetic choices but through deliberate design and refactoring efforts. Techniques such as ensuring balanced abstraction, leveraging structural pattern matching for clearer conditional logic, embracing declarative programming paradigms, and employing @@ -785,6 +785,6 @@ A proactive and disciplined approach, where these principles and techniques are integrated into daily development practices, is essential. This includes regular code reviews, monitoring complexity metrics, and fostering a team culture that values code quality and continuous improvement. The oft-quoted wisdom, "Good -programmers write code that humans can understand" 1, remains the guiding +programmers write code that humans can understand", remains the guiding principle. By striving for this ideal, development teams can build systems that are not only powerful and efficient but also a pleasure to evolve and maintain. diff --git a/docs/generic-message-fragmentation-and-re-assembly-design.md b/docs/generic-message-fragmentation-and-re-assembly-design.md index d4aee2ea..a51dcc70 100644 --- a/docs/generic-message-fragmentation-and-re-assembly-design.md +++ b/docs/generic-message-fragmentation-and-re-assembly-design.md @@ -87,7 +87,7 @@ struct PartialMessage { The use of `dashmap::DashMap` allows for lock-free reads and sharded writes, providing efficient and concurrent access to the re-assembly buffers without -blocking the entire connection task. 1 +blocking the entire connection task. ## 4. Public API: The `FragmentStrategy` Trait diff --git a/docs/mocking-network-outages-in-rust.md b/docs/mocking-network-outages-in-rust.md index 998af047..43a47056 100644 --- a/docs/mocking-network-outages-in-rust.md +++ b/docs/mocking-network-outages-in-rust.md @@ -150,7 +150,7 @@ With this change, `client_handler` no longer assumes a real network `TcpStream`; we can pass in any in-memory or mock stream for testing. **Importantly**, the production code doesn’t lose functionality – we still create actual TCP listeners/streams, but we hand off to the generic handler. This refactor -maintains the same behavior while enabling injection of test streams. +maintains the same behaviour while enabling injection of test streams. *Example – generic handler signature:* @@ -227,7 +227,7 @@ but on generic `reader`/`writer`. This refactoring sets the stage for injecting With the transport abstracted, we can create **dummy streams** to simulate various network outage scenarios. Tokio’s testing utilities include `tokio_test::io::Builder`, which allows building an object that implements -`AsyncRead` and `AsyncWrite` with predetermined behavior. We can script a +`AsyncRead` and `AsyncWrite` with predetermined behaviour. We can script a sequence of reads/writes and even inject errors. For example, the Tokio documentation demonstrates using `Builder` to simulate a @@ -570,7 +570,8 @@ explicit mocking might be useful. The `mockall` crate can generate mocks for our abstractions. For example, if we had defined a trait `trait Transport: AsyncRead + AsyncWrite + Unpin {}` (or a trait with specific async methods for read/write), we could use `mockall` to create a -`MockTransport` and program its behavior (return errors on certain calls, etc.). +`MockTransport` and program its behaviour (return errors on certain calls, +etc.). However, mocking `AsyncRead/Write` directly can be complex. An easier target for mocking might be higher-level components: @@ -608,7 +609,7 @@ mocking might be higher-level components: don’t invoke the real DB or commands at all – the mock could simply return a simple “OK” response transaction when called. Then we only simulate the network failing on sending that response. Such a mock ensures our test is - laser-focused on networking behavior. + laser-focused on networking behaviour. In summary, **use** `mockall` **when stubbing out parts of the system that are not the primary target of the test**. For testing network outages in `mxd`, the @@ -654,7 +655,7 @@ demonstrated how to simulate timeouts, abrupt disconnects, and I/O errors for both reads and writes. With parameterized tests and careful use of mocks, the server’s resilience under adverse network conditions can be validated thoroughly. This not only prevents regressions but also documents the intended -behavior (for example, that a timeout should result in a specific error code to +behaviour (for example, that a timeout should result in a specific error code to the client, or that an EOF is treated as a graceful shutdown). **In conclusion**, testing for network outages in async Rust requires a mix of diff --git a/docs/multi-packet-and-streaming-responses-design.md b/docs/multi-packet-and-streaming-responses-design.md index 09d20c87..c9d09461 100644 --- a/docs/multi-packet-and-streaming-responses-design.md +++ b/docs/multi-packet-and-streaming-responses-design.md @@ -14,7 +14,7 @@ response feature. The core philosophy is to enable this complex functionality through a simple, declarative, and ergonomic API. By embracing modern asynchronous Rust patterns, we will avoid the complexities of imperative, sink-based APIs and provide a unified handler model that is both powerful for -streaming and simple for single-frame replies. 1 +streaming and simple for single-frame replies. This feature is a key component of the "Road to Wireframe 1.0," working in concert with asynchronous push messaging and fragmentation to create a fully @@ -227,12 +227,12 @@ If the stream yields an `Err(WireframeError)`, the connection actor will: The design is inherently cancellation-safe. The `select!` macro in the connection actor will drop the `FrameStream` future if another branch (e.g., a shutdown signal) completes first. Because `StreamExt::next()` is -cancellation-safe, no frames will be lost; the stream will simply be dropped. 12 +cancellation-safe, no frames will be lost; the stream will simply be dropped. Similarly, if a handler panics or returns early, the `Stream` object it created is simply dropped. The connection actor will see the stream end as if it had completed normally, ensuring no resources are leaked and the connection does not -hang. 17 +hang. ## 7. Synergy with Other 1.0 Features diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index 1d8559ec..fd515843 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -154,14 +154,14 @@ network protocols, offering insights into effective abstractions. Although designed for RPC, its approach of defining service schemas directly in Rust code (using the `#[tarpc::service]` attribute to generate service traits and client/server boilerplate) is an interesting parallel to - "wireframe's" goal of reducing boilerplate for message handlers.20 Features - like pluggable transports and serde serialization further highlight its modern + "wireframe's" goal of reducing boilerplate for message handlers. Features like + pluggable transports and serde serialization further highlight its modern design. A clear pattern emerges from these libraries: the use of derive macros and trait-based designs is a prevalent and effective strategy in Rust for -simplifying protocol handling and reducing boilerplate code. Both `bin-proto` 14 -and `protocol` 16 leverage custom derives to generate (de)serialization logic +simplifying protocol handling and reducing boilerplate code. Both `bin-proto` +and `protocol` leverage custom derives to generate (de)serialization logic directly from struct and enum definitions. This is a proven pattern for enhancing developer ergonomics and reducing the likelihood of manual implementation errors. "wireframe" should strongly consider adopting a similar @@ -1348,7 +1348,7 @@ simplicity; "wireframe" aims for similar illustrative power with its examples. A primary motivation for "wireframe" is to reduce the inherent source code complexity often encountered when developing systems that communicate over -custom binary protocols. The inaccessibility of the `leynos/mxd` repository 7 +custom binary protocols. The inaccessibility of the `leynos/mxd` repository prevents a direct before-and-after comparison, but we can identify common sources of complexity in such projects and articulate how "wireframe's" design choices aim to mitigate them. diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index 5a3417b0..f02b1d98 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -6,7 +6,7 @@ built-in testing framework provides a solid foundation, managing test dependencies and creating parameterized tests can become verbose. The `rstest` crate (`github.com/la10736/rstest`) emerges as a powerful solution, offering a sophisticated fixture-based and parameterized testing framework that -significantly simplifies these tasks through the use of procedural macros.1 This +significantly simplifies these tasks through the use of procedural macros. This document provides a comprehensive exploration of `rstest`, from fundamental concepts to advanced techniques, enabling Rust developers to write cleaner, more expressive, and robust tests. @@ -25,8 +25,8 @@ Managing this setup and teardown logic within each test function can lead to considerable boilerplate code and repetition, making tests harder to read and maintain. -Fixtures address this by encapsulating these dependencies and their setup -logic.1 For instance, if multiple tests require a logged-in user object or a +Fixtures address this by encapsulating these dependencies and their setup logic. +For instance, if multiple tests require a logged-in user object or a pre-populated database, instead of creating these in every test, a fixture can provide them. This approach allows developers to focus on the specific logic being tested rather than the auxiliary utilities. @@ -43,13 +43,13 @@ become shorter, more focused, and thus more readable and maintainable. `rstest` is a Rust crate specifically designed to simplify and enhance testing by leveraging the concept of fixtures and providing powerful parameterization -capabilities.1 It is available on `crates.io` and its source code is hosted at -`github.com/la10736/rstest` 3, distinguishing it from other software projects -that may share the same name but operate in different ecosystems (e.g., a -JavaScript/TypeScript framework mentioned in 5). +capabilities. It is available on `crates.io` and its source code is hosted at +`github.com/la10736/rstest`, distinguishing it from other software projects that +may share the same name but operate in different ecosystems (e.g., a +JavaScript/TypeScript framework mentioned). The `rstest` crate utilizes Rust's procedural macros, such as `#[rstest]` and -`#[fixture]`, to achieve its declarative and expressive syntax.2 These macros +`#[fixture]`, to achieve its declarative and expressive syntax. These macros allow developers to define fixtures and inject them into test functions simply by listing them as arguments. This compile-time mechanism analyzes test function signatures and fixture definitions to wire up dependencies automatically. @@ -60,9 +60,9 @@ level. Developers declare the dependencies their tests need, and the macros handle the resolution and injection. While this significantly improves the developer experience for writing tests, the underlying macro expansion involves compile-time code generation. This complexity, though hidden, can have -implications for build times, particularly in large test suites.7 Furthermore, +implications for build times, particularly in large test suites. Furthermore, understanding the macro expansion can sometimes be necessary for debugging -complex test scenarios or unexpected behavior.8 +complex test scenarios or unexpected behaviour. ### C. Core Benefits: Readability, Reusability, Reduced Boilerplate @@ -70,10 +70,10 @@ The primary advantages of using `rstest` revolve around enhancing test code quality and developer productivity: - **Readability:** By injecting dependencies as function arguments, `rstest` - makes the requirements of a test explicit and easy to understand.9 The test + makes the requirements of a test explicit and easy to understand. The test function's signature clearly documents what it needs to run. This allows developers to "focus on the important stuff in your tests" by abstracting away - the setup details.1 + the setup details. - **Reusability:** Fixtures defined with `rstest` are reusable components. A single fixture, such as one setting up a database connection or creating a complex data structure, can be used across multiple tests, eliminating @@ -114,10 +114,10 @@ rstest = "0.18" # Or the latest version available on crates.io It is advisable to check `crates.io` for the latest stable version of `rstest` (and `rstest_macros` if required separately by the version of `rstest` being -used).1 Using `dev-dependencies` is a standard practice in Rust for testing +used). Using `dev-dependencies` is a standard practice in Rust for testing libraries. This convention prevents testing utilities from being included in production binaries, which helps keep them small and reduces compile times for -non-test builds.11 +non-test builds. ### B. Your First Fixture: Defining with `#[fixture]` @@ -138,13 +138,13 @@ pub fn answer_to_life() -> u32 { ``` In this example, `answer_to_life` is a public function marked with `#[fixture]`. -It takes no arguments and returns a `u32` value of 42.9 The `#[fixture]` macro +It takes no arguments and returns a `u32` value of 42. The `#[fixture]` macro effectively registers this function with the `rstest` system, transforming it into a component that `rstest` can discover and utilize. The return type of the fixture function (here, `u32`) defines the type of the data that will be injected into tests requesting this fixture. Fixtures can return any valid Rust -type, from simple primitives to complex structs or trait objects.1 Fixtures can -also depend on other fixtures, allowing for compositional setup.12 +type, from simple primitives to complex structs or trait objects. Fixtures can +also depend on other fixtures, allowing for compositional setup. ### C. Injecting Fixtures into Tests with `#[rstest]` @@ -170,9 +170,9 @@ fn test_with_fixture(answer_to_life: u32) { ``` In `test_with_fixture`, the argument `answer_to_life: u32` signals to `rstest` -that the `answer_to_life` fixture should be injected.1 `rstest` resolves this by +that the `answer_to_life` fixture should be injected. `rstest` resolves this by name: it looks for a fixture function named `answer_to_life`, calls it, and -passes its return value as the argument to the test function.13 +passes its return value as the argument to the test function. The argument name in the test function serves as the primary key for fixture resolution. This convention makes usage intuitive but necessitates careful @@ -180,7 +180,7 @@ naming of fixtures to avoid ambiguity, especially if multiple fixtures with the same name exist in different modules but are brought into the same scope. `rstest` generally follows Rust's standard name resolution rules, meaning an identically named fixture can be used in different contexts depending on -visibility and `use` declarations.1 +visibility and `use` declarations. ## III. Mastering Fixture Injection and Basic Usage @@ -191,7 +191,7 @@ leveraging `rstest` effectively. The flexibility of `rstest` fixtures allows them to provide a wide array of data types and perform various setup tasks. Fixtures are not limited by the kind of -data they can return; any valid Rust type is permissible.1 This enables fixtures +data they can return; any valid Rust type is permissible. This enables fixtures to encapsulate diverse setup logic, providing ready-to-use dependencies for tests. @@ -281,23 +281,23 @@ Here are a few examples illustrating different kinds of fixtures: ``` - This example, adapted from concepts in 1 and 1, demonstrates a fixture - providing a mutable `Repository` implementation. +This example demonstrates a fixture providing a mutable `Repository` +implementation. -### B. Understanding Fixture Scope and Lifetime (Default Behavior) +### B. Understanding fixture scope and lifetime (default behaviour) By default, `rstest` calls a fixture function anew for each test that uses it. This means if five different tests inject the same fixture, the fixture function will be executed five times, and each test will receive a fresh, independent -instance of the fixture's result. This behavior is crucial for test isolation. +instance of the fixture's result. This behaviour is crucial for test isolation. The `rstest` macro effectively desugars a test like `fn the_test(injected: i32)` into something conceptually similar to `#[test] fn the_test() { let injected = injected_fixture_func(); /*... */ }` -within the test body, implying a new call each time.13 +within the test body, implying a new call each time. Test isolation prevents the state from one test from inadvertently affecting another. If fixtures were shared by default, a mutation to a fixture's state in -one test could lead to unpredictable behavior or failures in subsequent tests +one test could lead to unpredictable behaviour or failures in subsequent tests that use the same fixture. Such dependencies would make tests order-dependent and significantly harder to debug. By providing a fresh instance for each test (unless explicitly specified otherwise using `#[once]`), `rstest` upholds this @@ -319,7 +319,7 @@ defines a specific scenario with a distinct set of input arguments for the test function. Arguments within the test function that are intended to receive these values must also be annotated with `#[case]`. -A classic example is testing the Fibonacci sequence 1: +A classic example is testing the Fibonacci sequence: ```rust use rstest::rstest; @@ -349,16 +349,16 @@ independent test. If one case fails, the others are still executed and reported individually by the test runner. These generated tests are often named by appending `::case_N` to the original test function name (e.g., `test_fibonacci::case_1`, `test_fibonacci::case_2`, etc.), which aids in -identifying specific failing cases.8 This individual reporting mechanism -provides clearer feedback than a loop within a single test, where the first -failure might obscure subsequent ones. +identifying specific failing cases. This individual reporting mechanism provides +clearer feedback than a loop within a single test, where the first failure might +obscure subsequent ones. ### B. Combinatorial Testing with `#[values]`: Generating Test Matrices The `#[values(...)]` attribute is used on test function arguments to generate tests for every possible combination of the provided values (the Cartesian product). This is particularly useful for testing interactions between different -parameters or ensuring comprehensive coverage across various input states.1 +parameters or ensuring comprehensive coverage across various input states. Consider testing a state machine's transition logic based on current state and an incoming event: @@ -399,7 +399,7 @@ fn test_state_transitions( In this scenario, `rstest` will generate 3×3=9 individual test cases, covering all combinations of `initial_state` and `event` specified in the `#[values]` -attributes.1 +attributes. It is important to be mindful that the number of generated tests can grow very rapidly with `#[values]`. If a test function has three arguments, each with ten @@ -416,7 +416,7 @@ Fixtures can be seamlessly combined with parameterized arguments (`#[case]` or testing different aspects of a component (varied by parameters) within a consistent environment or context (provided by fixtures). The "Complete Example" in the `rstest` documentation hints at this synergy, stating that all features -can be used together, mixing fixture variables, fixed cases, and value lists.9 +can be used together, mixing fixture variables, fixed cases, and value lists. For example, a test might use a fixture to obtain a database connection and then use `#[case]` arguments to test operations with different user IDs: @@ -453,7 +453,7 @@ Fixtures can depend on other fixtures. This is achieved by simply listing one fixture as an argument to another fixture function. `rstest` will resolve this dependency graph, ensuring that prerequisite fixtures are evaluated first. This allows for the construction of complex setup logic from smaller, modular, and -reusable fixture components.4 +reusable fixture components. ```rust use rstest::*; @@ -494,7 +494,7 @@ promoting modularity and maintainability in test setups. For fixtures that are expensive to create or represent read-only shared data, `rstest` provides the `#[once]` attribute. A fixture marked `#[once]` is initialized only a single time, and all tests using it will receive a static -reference to this shared instance.9 +reference to this shared instance. ```rust use rstest::*; @@ -521,7 +521,7 @@ fn test_once_2(expensive_setup: &'static AtomicUsize) { } ``` -When using `#[once]`, there are critical caveats 12: +When using `#[once]`, there are critical caveats: 1. **Resource Lifetime:** The value returned by an `#[once]` fixture is effectively promoted to a `static` lifetime and is **never dropped**. This @@ -534,7 +534,7 @@ When using `#[once]`, there are critical caveats 12: and cannot be generic functions (neither with generic type parameters nor using `impl Trait` in arguments or return types). -The "never dropped" behavior arises because `rstest` typically creates a +The "never dropped" behaviour arises because `rstest` typically creates a `static` variable to hold the result of the `#[once]` fixture. `static` variables in Rust live for the entire duration of the program, and their `Drop` implementations are not usually called at program exit. This is a crucial @@ -544,7 +544,7 @@ consideration for resource management. Sometimes a fixture's function name might be long and descriptive, but a shorter or different name is preferred for the argument in a test or another fixture. -The `#[from(original_fixture_name)]` attribute on an argument allows renaming.12 +The `#[from(original_fixture_name)]` attribute on an argument allows renaming. This is particularly useful when destructuring the result of a fixture. ```rust @@ -570,7 +570,7 @@ The `#[from]` attribute decouples the fixture's actual function name from the variable name used within the consuming function. As shown, if a fixture returns a tuple or struct and the test only cares about some parts or wants to use more idiomatic names for destructured elements, `#[from]` is essential to link the -argument pattern to the correct source fixture.12 +argument pattern to the correct source fixture. ### D. Partial Fixture Injection & Default Arguments @@ -579,10 +579,10 @@ fixtures using `#[default(...)]` for fixture arguments and `#[with(...)]` to override these defaults on a per-test basis. - `#[default(...)]`: Used within a fixture function's signature to provide - default values for its own arguments.4 + default values for its own arguments. - `#[with(...)]`: Used on a test function's fixture argument (or a fixture argument within another fixture) to supply specific values to the parameters - of the invoked fixture, overriding any defaults.4 + of the invoked fixture, overriding any defaults. ```rust use rstest::*; @@ -619,7 +619,7 @@ fn test_admin_user(#[with("AdminUser", 42, "Admin")] user_fixture: User) { } // Example of overriding only specific arguments (syntax may vary based on rstest version for named overrides) -// The provided snippets (e.g., [12] `#[with(3)] second: i32`) suggest positional overrides. +// The provided snippets (e.g., `#[with(3)] second: i32`) suggest positional overrides. // For named overrides, one might need to define intermediate fixtures or check specific rstest version capabilities. // Assuming positional override for the first argument (name): #[rstest] @@ -641,7 +641,7 @@ different fixtures. For convenience, if a type implements the `std::str::FromStr` trait, `rstest` can often automatically convert string literals provided in `#[case]` or -`#[values]` attributes directly into an instance of that type.1 +`#[values]` attributes directly into an instance of that type. An example is converting string literals to `std::net::SocketAddr`: @@ -677,7 +677,7 @@ with common async runtimes and offering syntactic sugar for managing futures. ### A. Defining Asynchronous Fixtures (`async fn`) Creating an asynchronous fixture is straightforward: simply define the fixture -function as an `async fn`.12 `rstest` will recognize it as an async fixture and +function as an `async fn`. `rstest` will recognize it as an async fixture and handle its execution accordingly when used in an async test. ```rust @@ -695,17 +695,17 @@ async fn async_data_fetcher() -> String { ``` The example above uses `async_std::task::sleep`, aligning with `rstest`'s -default async runtime support, but the fixture logic can be any async code.4 +default async runtime support, but the fixture logic can be any async code. ### B. Writing Asynchronous Tests (`async fn` with `#[rstest]`) Test functions themselves can also be `async fn`. `rstest` will manage the execution of these async tests. By default, `rstest` often uses -`#[async_std::test]` to annotate the generated async test functions.9 However, -it is designed to be largely runtime-agnostic and can be integrated with other +`#[async_std::test]` to annotate the generated async test functions. However, it +is designed to be largely runtime-agnostic and can be integrated with other popular async runtimes like Tokio or Actix. This is typically done by adding the runtime's specific test attribute (e.g., `#[tokio::test]` or -`#[actix_rt::test]`) alongside `#[rstest]`.4 +`#[actix_rt::test]`) alongside `#[rstest]`. ```rust use rstest::*; @@ -726,14 +726,14 @@ async fn my_async_test(async_fixture_value: u32) { } ``` -The order of procedural macro attributes can sometimes matter.15 While `rstest` +The order of procedural macro attributes can sometimes matter. While `rstest` documentation and examples show flexibility (e.g., `#[rstest]` then -`#[tokio::test]` 4, or vice versa), users should ensure their chosen async +`#[tokio::test]`, or vice versa), users should ensure their chosen async runtime's test macro is correctly placed to provide the necessary execution context for the async test body and any async fixtures. `rstest` itself does not bundle a runtime; it integrates with existing ones. The "Inject Test Attribute" -feature mentioned in `rstest` documentation 10 may offer more explicit control -over which test runner attribute is applied. +feature mentioned in `rstest` documentation may offer more explicit control over +which test runner attribute is applied. ### C. Managing Futures: `#[future]` and `#[awt]` Attributes @@ -744,7 +744,7 @@ To improve the ergonomics of working with async fixtures and values in tests, its type is `impl Future`. The `#[future]` attribute on such an argument allows developers to refer to it with type `T` directly in the test signature, removing the `impl Future` boilerplate. However, the value still - needs to be `.await`ed explicitly within the test body or by using `#[awt]`.4 + needs to be `.await`ed explicitly within the test body or by using `#[awt]`. - `#[awt]` (or `#[future(awt)]`): This attribute, when applied to the entire test function (`#[awt]`) or a specific `#[future]` argument (`#[future(awt)]`), tells `rstest` to automatically insert `.await` calls for @@ -782,12 +782,12 @@ async fn test_with_future_awt_arg( // Need to explicitly await base_value_async if it's not covered by a function-level #[awt] // However, if base_value_async is a simple fixture (not a case) and the test is async, // rstest might await it automatically when #[awt] is not used. - // The precise behavior of auto-awaiting non-case futures without #[awt] should be verified. + // The precise behaviour of auto-awaiting non-case futures without #[awt] should be verified. // For clarity, using #[awt] or explicit.await is recommended. // Assuming base_value_async needs explicit await here if no function-level #[awt]: // assert_eq!(base_value_async.await / divisor_async, 6); // If base_value_async fixture is also awaited by rstest implicitly: - assert_eq!(base_value_async / divisor_async, 6); // [4] example suggests this works + assert_eq!(base_value_async / divisor_async, 6); // Example suggests this works } ``` @@ -799,8 +799,8 @@ away some of the explicit `async`/`.await` mechanics. Long-running or stalled asynchronous operations can cause tests to hang indefinitely. `rstest` provides a `#[timeout(...)]` attribute to set a maximum -execution time for async tests.10 This feature typically relies on the -`async-timeout` feature of `rstest`, which is enabled by default.1 +execution time for async tests. This feature typically relies on the +`async-timeout` feature of `rstest`, which is enabled by default. ```rust use rstest::*; @@ -829,8 +829,8 @@ async fn test_operation_exceeds_timeout() { A default timeout for all `rstest` async tests can also be set using the `RSTEST_TIMEOUT` environment variable (value in seconds), evaluated at test -compile time.10 This built-in timeout support is a practical feature for -ensuring test suite stability. +compile time. This built-in timeout support is a practical feature for ensuring +test suite stability. ## VII. Working with External Resources and Test Data @@ -843,8 +843,8 @@ resources and test data. Managing temporary files and directories is a common requirement for tests that involve file I/O. While `rstest` itself doesn't directly provide temporary file utilities, its fixture system integrates seamlessly with crates like `tempfile` -or `test-temp-dir`.16 A fixture can create a temporary file or directory, -provide its path or handle to the test, and ensure cleanup (often via RAII). +or `test-temp-dir`. A fixture can create a temporary file or directory, provide +its path or handle to the test, and ensure cleanup (often via RAII). Here's an illustrative example using the `tempfile` crate: @@ -907,8 +907,8 @@ reliable. `rstest` fixtures are an ideal place to encapsulate the setup and configuration of mock objects. Crates like `mockall` can be used to create mocks, or they can be hand-rolled. The fixture would then provide the configured mock instance to the test. General testing advice also strongly recommends -mocking external dependencies.17 The `rstest` documentation itself shows -examples with fakes or mocks like `empty_repository` and `string_processor`.1 +mocking external dependencies. The `rstest` documentation itself shows examples +with fakes or mocks like `empty_repository` and `string_processor`. A conceptual example using a hypothetical mocking library: @@ -983,7 +983,7 @@ verbose, involving defining expectations, return values, and call counts) from the actual test function. Tests then simply request the configured mock as an argument. If different tests require the mock to behave differently, multiple specialized mock fixtures can be created, or fixture arguments combined with -`#[with(...)]` can be used to dynamically configure the mock's behavior within +`#[with(...)]` can be used to dynamically configure the mock's behaviour within the fixture itself. This makes tests that depend on external services more readable and maintainable. @@ -994,9 +994,9 @@ the `#[files("glob_pattern")]` attribute. This attribute can be used on a test function argument to inject file paths that match a given glob pattern. The argument type is typically `PathBuf`. It can also inject file contents directly as `&str` or `&[u8]` by specifying a mode, e.g., -`#[files("glob_pattern", mode = "str")]`.13 Additional attributes like +`#[files("glob_pattern", mode = "str")]`. Additional attributes like `#[base_dir = "..."]` can specify a base directory for the glob, and -`#[exclude("regex")]` can filter out paths matching a regular expression.10 +`#[exclude("regex")]` can filter out paths matching a regular expression. ```rust use rstest::*; @@ -1041,7 +1041,7 @@ support these goals. While `rstest`'s `#[case]` attribute is excellent for parameterization, repeating the same set of `#[case]` attributes across multiple test functions can lead to duplication. The `rstest_reuse` crate addresses this by allowing the -definition of reusable test templates.9 +definition of reusable test templates. `rstest_reuse` introduces two main attributes: @@ -1054,7 +1054,7 @@ definition of reusable test templates.9 // Add to Cargo.toml: rstest_reuse = "0.7" (or latest) // In your test module or lib.rs/main.rs for crate-wide visibility if needed: // #[cfg(test)] -// use rstest_reuse; // Important for template macro expansion [18] +// use rstest_reuse; // Important for template macro expansion use rstest::rstest; use rstest_reuse::{self, template, apply}; // Or use rstest_reuse::*; @@ -1063,7 +1063,7 @@ use rstest_reuse::{self, template, apply}; // Or use rstest_reuse::*; #[template] #[rstest] #[case(2, 2)] -#[case(4 / 2, 2)] // Cases can use expressions [13] +#[case(4 / 2, 2)] // Cases can use expressions #[case(6, 3 * 2)] fn common_math_cases(#[case] a: i32, #[case] b: i32) {} @@ -1075,7 +1075,7 @@ fn test_addition_is_commutative(#[case] a: i32, #[case] b: i32) { // Apply the template to another test function, possibly with additional cases #[apply(common_math_cases)] -#[case(10, 5 + 5)] // Composition: add more cases [18] +#[case(10, 5 + 5)] // Composition: add more cases fn test_multiplication_by_one(#[case] a: i32, #[case] b: i32) { // This test might not use 'b', but the template provides it. assert_eq!(a * 1, a); @@ -1085,11 +1085,11 @@ fn test_multiplication_by_one(#[case] a: i32, #[case] b: i32) { `rstest_reuse` works by having `#[template]` define a macro. When `#[apply(template_name)]` is used, this macro is called and expands to the set -of attributes (like `#[case]`) onto the target function.18 This meta-programming +of attributes (like `#[case]`) onto the target function. This meta-programming technique effectively avoids direct code duplication of parameter sets, promoting DRY principles in test case definitions. `rstest_reuse` also supports composing templates with additional `#[case]` or `#[values]` attributes when -applying them.18 +applying them. ### B. Best Practices for Organizing Fixtures and Tests @@ -1099,7 +1099,7 @@ for maintainability and scalability. - **Placement:** - For fixtures used within a single module, they can be defined within that - module's `tests` submodule (annotated with `#[cfg(test)]`).11 + module's `tests` submodule (annotated with `#[cfg(test)]`). - For fixtures intended to be shared across multiple integration test files (in the `tests/` directory), consider creating a common module within the `tests/` directory (e.g., `tests/common/fixtures.rs`) and re-exporting @@ -1109,24 +1109,23 @@ for maintainability and scalability. integration tests. - **Naming Conventions:** Use clear, descriptive names for fixtures that indicate what they provide or set up. Test function names should clearly state - what behavior they are verifying. + what behaviour they are verifying. - **Fixture Responsibility:** Aim for fixtures with a single, well-defined responsibility. Complex setups can be achieved by composing smaller, focused - fixtures.12 + fixtures. - **Scope Management (**`#[once]` **vs. Regular):** Make conscious decisions about fixture lifetimes. Use `#[once]` sparingly, only for genuinely expensive, read-only, and safely static resources, being mindful of its "never - dropped" nature.12 Prefer regular (per-test) fixtures for test isolation and + dropped" nature. Prefer regular (per-test) fixtures for test isolation and proper resource management. - **Modularity:** Group related fixtures and tests into modules. This improves navigation and understanding of the test suite. -- **Readability:** Utilize features like `#[from]` for renaming 12 and - `#[default]` / `#[with]` for configurable fixtures to enhance the clarity of - both fixture definitions and their usage in tests. + - **Readability:** Utilize features like `#[from]` for renaming and + `#[default]` / `#[with]` for configurable fixtures to enhance the clarity of + both fixture definitions and their usage in tests. General testing advice, such as keeping tests small and focused and mocking -external dependencies 17, also applies and is well-supported by `rstest`'s -design. +external dependencies, also applies and is well-supported by `rstest`'s design. ## IX. `rstest` in Context: Comparison and Considerations @@ -1194,19 +1193,19 @@ mind: - **Macro Expansion Impact:** Procedural macros, by their nature, involve code generation at compile time. This can sometimes lead to longer compilation - times for test suites, especially large ones.7 Debugging macro-related issues + times for test suites, especially large ones. Debugging macro-related issues can also be less straightforward if the developer is unfamiliar with how the - macros expand.8 + macros expand. - **Debugging Parameterized Tests:** `rstest` generates individual test functions for parameterized cases, often named like - `test_function_name::case_N`.8 Understanding this naming convention is helpful + `test_function_name::case_N`. Understanding this naming convention is helpful for identifying and running specific failing cases with `cargo test test_function_name::case_N`. Some IDEs or debuggers might require specific configurations or might not fully support stepping through the macro-generated code as seamlessly as handwritten code, though support is improving. - **Static Nature of Test Cases:** Test cases (e.g., from `#[case]` or - `#[files]`) are defined and discovered at compile time.7 This means the + `#[files]`) are defined and discovered at compile time. This means the structure of the tests is validated by the Rust compiler, which can catch structural errors (like type mismatches in `#[case]` arguments or references to non-existent fixtures) earlier than runtime test discovery mechanisms. This @@ -1218,7 +1217,7 @@ mind: (`std`) being available, as test runners and many common testing utilities depend on `std`. Therefore, it is typically not suitable for testing `#![no_std]` libraries in a truly `no_std` test environment where the test - harness itself cannot link `std`.20 + harness itself cannot link `std`. - **Learning Curve:** While designed for simplicity in basic use cases, the full range of attributes and advanced features (e.g., fixture composition, partial injection, async management attributes) has a learning curve. @@ -1231,7 +1230,7 @@ specific needs like logging and conditional test execution. ### A. `rstest-log`: Logging in `rstest` Tests For developers who rely on logging frameworks like `log` or `tracing` for -debugging tests, the `rstest-log` crate can simplify integration.21 Test runners +debugging tests, the `rstest-log` crate can simplify integration. Test runners often capture standard output and error streams, and logging frameworks require proper initialization. `rstest-log` likely provides attributes or wrappers to ensure that logging is correctly set up before each `rstest`-generated test case @@ -1265,13 +1264,13 @@ are logged under specific conditions. The `test-with` crate allows for conditional execution of tests based on various runtime conditions, such as the presence of environment variables, the existence -of specific files or folders, or the availability of network services.22 It can -be used with `rstest`. For example, an `rstest` test could be further annotated +of specific files or folders, or the availability of network services. It can be +used with `rstest`. For example, an `rstest` test could be further annotated with `test-with` attributes to ensure it only runs if a particular database configuration file exists or if a dependent web service is reachable. The order of macros is important: `rstest` should typically generate the test cases first, and then `test-with` can apply its conditional execution logic to these -generated tests.22 This allows `rstest` to focus on test structure and data +generated tests. This allows `rstest` to focus on test structure and data provision, while `test-with` provides an orthogonal layer of control over test execution conditions. @@ -1311,12 +1310,12 @@ requirements. For further exploration and the most up-to-date information, the following resources are recommended: -- **Official** `rstest` **Documentation:** 1 -- `rstest` **GitHub Repository:** 3 -- `rstest_reuse` **Crate:** 18 +- **Official** `rstest` **Documentation:** +- `rstest` **GitHub Repository:** +- `rstest_reuse` **Crate:** - **Rust Community Forums:** Platforms like the Rust Users Forum (users.rust-lang.org) and Reddit (e.g., r/rust) may contain discussions and - community experiences with `rstest`.19 + community experiences with `rstest`. The following table provides a quick reference to some of the key attributes provided by `rstest`: diff --git a/docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md b/docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md index d7a44cef..1748dce2 100644 --- a/docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md +++ b/docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md @@ -64,11 +64,11 @@ pub enum Response { ``` This design is powered by the `async-stream` crate, which allows developers to -write imperative-looking logic that generates a declarative `Stream` object. 4 +write imperative-looking logic that generates a declarative `Stream` object. This provides the best of both worlds: the intuitive feel of a `for` loop for generating frames, without the API complexity of a separate -`Sink` type. 9 +`Sink` type. Rust @@ -101,7 +101,7 @@ The core of this actor is a `tokio::select!` loop that multiplexes frames from multiple sources onto the outbound socket. To ensure that time-sensitive control messages (like heartbeats or session notifications) are not delayed by large data transfers, this loop will be explicitly prioritized using -`select!(biased;)`. 17 +`select!(biased;)`. The polling order will be: @@ -184,7 +184,7 @@ quality assurance. A robust network service must be able to shut down cleanly. `wireframe` will adopt a canonical, proactive shutdown pattern using -`tokio_util::sync::CancellationToken` and `tokio_util::task::TaskTracker`. 24 +`tokio_util::sync::CancellationToken` and `tokio_util::task::TaskTracker`. - A single `CancellationToken` will be created at server startup. @@ -212,14 +212,14 @@ Rust's ownership model and `Drop` trait are the foundation of resource safety. `SessionRegistry` for push handles) will use `Arc` and `Weak` pointers. This prevents the registry from artificially keeping connection resources alive after a client disconnects, eliminating a common source of memory leaks - in long-running servers. 39 + in long-running servers. - **DoS Protection:** The framework will provide built-in, configurable protections against resource exhaustion attacks: - **Rate Limiting:** An asynchronous, token-bucket-based rate limiter will be available on a per-connection basis to throttle high-frequency message - pushes. 46 + pushes. - **Memory Caps:** The fragmentation layer will enforce a strict `max_message_size` to prevent a single client from consuming excessive @@ -252,23 +252,23 @@ committed to an API that is intuitive, flexible, and idiomatic. - **Fluent Builder API:** All configuration will be done through a fluent builder pattern (`WireframeApp::new().with_feature_x().with_config_y()`), - which is readable and self-documenting. 63 + which is readable and self-documenting. - **Trait-Based Extensibility:** Instead of a collection of disparate callback closures, protocol-specific logic will be encapsulated within a single, - cohesive `WireframeProtocol` trait. 68 This promotes better organization, + cohesive `WireframeProtocol` trait. This promotes better organization, reusability, and makes the framework easier to extend. - **Idiomatic Asynchronous APIs:** The library will consistently favor - declarative, stream-based APIs over imperative, sink-based ones. 74 This - aligns with the broader async Rust ecosystem and leads to code that is easier - to compose and reason about. 79 + declarative, stream-based APIs over imperative, sink-based ones. This aligns + with the broader async Rust ecosystem and leads to code that is easier to + compose and reason about. ### C. Pervasive Observability A production system is a black box without good instrumentation. `wireframe` 1.0 will treat observability as a first-class feature, integrating the `tracing` -crate throughout its core. 83 +crate throughout its core. - **Structured Logging and Tracing:** The entire lifecycle of a connection, request, and response will be wrapped in `tracing::span!`s. 83 This provides @@ -303,12 +303,12 @@ traditional unit and integration tests. - **Stateful Property Testing:** For validating complex, stateful protocol conversations (like fragmentation and re-assembly), `proptest` will be used. - 98 This technique generates thousands of random-but-valid sequences of - operations to uncover edge cases that manual tests would miss. + This technique generates thousands of random-but-valid sequences of operations + to uncover edge cases that manual tests would miss. - **Concurrency Verification:** The `loom` crate will be used for permutation testing of concurrency hotspots, such as the connection actor's `select!` - loop. 103 + loop. `loom` deterministically explores all possible interleavings of concurrent operations, providing a strong guarantee against data races and deadlocks.