|
| 1 | +# Architecture Tests |
| 2 | + |
| 3 | +This project uses [ArchUnit](https://www.archunit.org/) to enforce architectural constraints and detect design smells. |
| 4 | + |
| 5 | +## Current Architecture Tests |
| 6 | + |
| 7 | +### ✅ Passing Tests |
| 8 | + |
| 9 | +1. **`packages_should_be_free_of_cycles()`** |
| 10 | + - Verifies no cyclic dependencies between top-level packages |
| 11 | + - Pattern: `edu.luc.etl.cs313.android.simplestopwatch.(*)` |
| 12 | + |
| 13 | +2. **`subpackages_should_be_free_of_cycles()`** |
| 14 | + - Verifies no cyclic dependencies at sub-package level |
| 15 | + - Pattern: `edu.luc.etl.cs313.android.simplestopwatch.(*).(*)` |
| 16 | + |
| 17 | +### ❌ Failing Tests (Demonstrating Architecture Violations) |
| 18 | + |
| 19 | +3. **`model_should_only_depend_on_java_common_or_model()`** |
| 20 | + - **Purpose**: Ensures model layer is self-contained |
| 21 | + - **Allowed Dependencies**: `java.*`, `javax.*`, `..common.*`, `..model.*` |
| 22 | + - **Current Violations** (4): |
| 23 | + - `LapRunningState.getId()` → `R$string.LAP_RUNNING` |
| 24 | + - `LapStoppedState.getId()` → `R$string.LAP_STOPPED` |
| 25 | + - `RunningState.getId()` → `R$string.RUNNING` |
| 26 | + - `StoppedState.getId()` → `R$string.STOPPED` |
| 27 | + |
| 28 | +4. **`model_should_not_depend_on_R_class()`** |
| 29 | + - **Purpose**: Explicitly forbids dependencies on Android resource class `R` |
| 30 | + - **Rationale**: Model classes should not depend on UI-specific resources |
| 31 | + - **Current Violations** (4): Same as test #3 above |
| 32 | + |
| 33 | +## Technical Notes |
| 34 | + |
| 35 | +### Why R.java Uses `Integer.valueOf()` |
| 36 | + |
| 37 | +The `R` class uses `Integer.valueOf()` instead of literal integer constants: |
| 38 | + |
| 39 | +```java |
| 40 | +public static final int STOPPED = Integer.valueOf(0); // Not: = 0; |
| 41 | +``` |
| 42 | + |
| 43 | +**Reason**: Java's compiler performs **compile-time constant inlining** for `static final` primitive fields with literal values. This means: |
| 44 | + |
| 45 | +- With `int STOPPED = 0;` → Compiler replaces `R.string.STOPPED` with `0` in bytecode |
| 46 | +- With `int STOPPED = Integer.valueOf(0);` → Bytecode contains `getstatic R$string.STOPPED` |
| 47 | + |
| 48 | +ArchUnit operates on **compiled bytecode**, not source code. If constants were inlined, ArchUnit couldn't detect the dependency because it wouldn't exist in the bytecode—only in the source. |
| 49 | + |
| 50 | +By using `Integer.valueOf()`, we ensure the dependency exists at runtime, making it detectable by ArchUnit. |
| 51 | + |
| 52 | +### Bytecode Comparison |
| 53 | + |
| 54 | +**With literal constant** (inlined): |
| 55 | +``` |
| 56 | +public int getId(); |
| 57 | + Code: |
| 58 | + 0: iconst_0 // Push constant 0 |
| 59 | + 1: ireturn |
| 60 | +``` |
| 61 | + |
| 62 | +**With Integer.valueOf()** (not inlined): |
| 63 | +``` |
| 64 | +public int getId(); |
| 65 | + Code: |
| 66 | + 0: getstatic #37 // Field R$string.STOPPED:I |
| 67 | + 3: ireturn |
| 68 | +``` |
| 69 | + |
| 70 | +## Running the Tests |
| 71 | + |
| 72 | +```bash |
| 73 | +# Run all architecture tests |
| 74 | +./gradlew test --tests "*.PackageDependencyTest" |
| 75 | + |
| 76 | +# Run specific test |
| 77 | +./gradlew test --tests "*.PackageDependencyTest.model_should_not_depend_on_R_class" |
| 78 | + |
| 79 | +# Run with detailed violation output |
| 80 | +./gradlew test --tests "*.PackageDependencyTest" --info |
| 81 | +``` |
| 82 | + |
| 83 | +## How to Fix the Violations |
| 84 | + |
| 85 | +To eliminate the architectural violations, the `R` class constants should be moved to the `common` package or the state classes should use a different mechanism for state identification that doesn't depend on UI resources. |
| 86 | + |
| 87 | +For example: |
| 88 | +1. Move constants to `Constants.java` in the `common` package |
| 89 | +2. Have states return their own type/class as identification |
| 90 | +3. Use an enum for state identification |
0 commit comments