|
| 1 | +import { |
| 2 | + analyzeFootprintAccess, |
| 3 | + FootprintAccessReport, |
| 4 | +} from "../footprint-access-analyzer"; |
| 5 | + |
| 6 | +describe("SorobanFootprintAccessAnalyzer", () => { |
| 7 | + describe("analyzeFootprintAccess", () => { |
| 8 | + it("should return empty report for source with no storage accesses", () => { |
| 9 | + const source = ` |
| 10 | + pub fn noop() {} |
| 11 | + `; |
| 12 | + const report = analyzeFootprintAccess(source); |
| 13 | + expect(report.findings).toHaveLength(0); |
| 14 | + expect(report.metrics.totalKeys).toBe(0); |
| 15 | + expect(report.summary).toContain("No footprint access patterns detected"); |
| 16 | + }); |
| 17 | + |
| 18 | + it("should detect hot keys with high access frequency", () => { |
| 19 | + const source = ` |
| 20 | + const BALANCE: Symbol = Symbol::new(&env, "balance"); |
| 21 | +
|
| 22 | + pub fn process_transfers() { |
| 23 | + let a: i128 = env.storage().persistent().get(&BALANCE); |
| 24 | + let b: i128 = env.storage().persistent().get(&BALANCE); |
| 25 | + let c: i128 = env.storage().persistent().get(&BALANCE); |
| 26 | + let d: i128 = env.storage().persistent().get(&BALANCE); |
| 27 | + let e: i128 = env.storage().persistent().get(&BALANCE); |
| 28 | + let f: i128 = env.storage().persistent().get(&BALANCE); |
| 29 | + } |
| 30 | + `; |
| 31 | + const report = analyzeFootprintAccess(source); |
| 32 | + |
| 33 | + const hotKey = report.findings.find((f) => f.pattern === "hot_key"); |
| 34 | + expect(hotKey).toBeDefined(); |
| 35 | + expect(hotKey!.ruleId).toBe("SOROBAN-FPA-01"); |
| 36 | + expect(hotKey!.affectedKeys).toContain("balance"); |
| 37 | + }); |
| 38 | + |
| 39 | + it("should detect cold keys with minimal access", () => { |
| 40 | + const source = ` |
| 41 | + const RARE_KEY: Symbol = Symbol::new(&env, "rare_key"); |
| 42 | +
|
| 43 | + pub fn rarely_used() -> i128 { |
| 44 | + env.storage().persistent().get(&RARE_KEY) |
| 45 | + } |
| 46 | + `; |
| 47 | + const report = analyzeFootprintAccess(source); |
| 48 | + |
| 49 | + const coldKey = report.findings.find((f) => f.pattern === "cold_key"); |
| 50 | + expect(coldKey).toBeDefined(); |
| 51 | + expect(coldKey!.ruleId).toBe("SOROBAN-FPA-02"); |
| 52 | + expect(coldKey!.affectedKeys).toContain("rare_key"); |
| 53 | + }); |
| 54 | + |
| 55 | + it("should detect loop access patterns", () => { |
| 56 | + const source = ` |
| 57 | + const ITEM: Symbol = Symbol::new(&env, "item"); |
| 58 | +
|
| 59 | + pub fn process_loop(items: Vec<i128>) { |
| 60 | + for i in 0..items.len() { |
| 61 | + let val: i128 = env.storage().persistent().get(&ITEM); |
| 62 | + env.storage().persistent().set(&ITEM, &val); |
| 63 | + } |
| 64 | + } |
| 65 | + `; |
| 66 | + const report = analyzeFootprintAccess(source); |
| 67 | + |
| 68 | + const loopAccess = report.findings.find((f) => f.pattern === "loop_access"); |
| 69 | + expect(loopAccess).toBeDefined(); |
| 70 | + expect(loopAccess!.ruleId).toBe("SOROBAN-FPA-03"); |
| 71 | + expect(loopAccess!.severity).toBe("high"); |
| 72 | + }); |
| 73 | + |
| 74 | + it("should detect imbalanced access patterns", () => { |
| 75 | + const source = ` |
| 76 | + const CONFIG: Symbol = Symbol::new(&env, "config"); |
| 77 | +
|
| 78 | + pub fn read_heavy() -> i128 { |
| 79 | + let a: i128 = env.storage().persistent().get(&CONFIG); |
| 80 | + let b: i128 = env.storage().persistent().get(&CONFIG); |
| 81 | + let c: i128 = env.storage().persistent().get(&CONFIG); |
| 82 | + let d: i128 = env.storage().persistent().get(&CONFIG); |
| 83 | + let e: i128 = env.storage().persistent().get(&CONFIG); |
| 84 | + env.storage().persistent().set(&CONFIG, &0); |
| 85 | + } |
| 86 | + `; |
| 87 | + const report = analyzeFootprintAccess(source); |
| 88 | + |
| 89 | + const imbalanced = report.findings.find((f) => f.pattern === "imbalanced_access"); |
| 90 | + expect(imbalanced).toBeDefined(); |
| 91 | + expect(imbalanced!.ruleId).toBe("SOROBAN-FPA-04"); |
| 92 | + }); |
| 93 | + |
| 94 | + it("should detect read-heavy functions", () => { |
| 95 | + const source = ` |
| 96 | + const A: Symbol = Symbol::new(&env, "a"); |
| 97 | + const B: Symbol = Symbol::new(&env, "b"); |
| 98 | + const C: Symbol = Symbol::new(&env, "c"); |
| 99 | +
|
| 100 | + pub fn read_heavy_fn() -> i128 { |
| 101 | + let a: i128 = env.storage().persistent().get(&A); |
| 102 | + let b: i128 = env.storage().persistent().get(&B); |
| 103 | + let c: i128 = env.storage().persistent().get(&C); |
| 104 | + let d: i128 = env.storage().persistent().get(&A); |
| 105 | + let e: i128 = env.storage().persistent().get(&B); |
| 106 | + env.storage().persistent().set(&C, &0); |
| 107 | + } |
| 108 | + `; |
| 109 | + const report = analyzeFootprintAccess(source); |
| 110 | + |
| 111 | + const readHeavy = report.findings.find((f) => f.pattern === "read_heavy"); |
| 112 | + expect(readHeavy).toBeDefined(); |
| 113 | + expect(readHeavy!.ruleId).toBe("SOROBAN-FPA-05"); |
| 114 | + }); |
| 115 | + |
| 116 | + it("should detect write-heavy functions", () => { |
| 117 | + const source = ` |
| 118 | + const X: Symbol = Symbol::new(&env, "x"); |
| 119 | + const Y: Symbol = Symbol::new(&env, "y"); |
| 120 | + const Z: Symbol = Symbol::new(&env, "z"); |
| 121 | +
|
| 122 | + pub fn write_heavy_fn() { |
| 123 | + env.storage().persistent().set(&X, &1); |
| 124 | + env.storage().persistent().set(&Y, &2); |
| 125 | + env.storage().persistent().set(&Z, &3); |
| 126 | + env.storage().persistent().set(&X, &4); |
| 127 | + env.storage().persistent().set(&Y, &5); |
| 128 | + let _: i128 = env.storage().persistent().get(&Z); |
| 129 | + } |
| 130 | + `; |
| 131 | + const report = analyzeFootprintAccess(source); |
| 132 | + |
| 133 | + const writeHeavy = report.findings.find((f) => f.pattern === "write_heavy"); |
| 134 | + expect(writeHeavy).toBeDefined(); |
| 135 | + expect(writeHeavy!.ruleId).toBe("SOROBAN-FPA-06"); |
| 136 | + }); |
| 137 | + |
| 138 | + it("should detect high footprint overlap between functions", () => { |
| 139 | + const source = ` |
| 140 | + const A: Symbol = Symbol::new(&env, "a"); |
| 141 | + const B: Symbol = Symbol::new(&env, "b"); |
| 142 | + const C: Symbol = Symbol::new(&env, "c"); |
| 143 | + const D: Symbol = Symbol::new(&env, "d"); |
| 144 | +
|
| 145 | + pub fn fn_one() -> i128 { |
| 146 | + let a: i128 = env.storage().persistent().get(&A); |
| 147 | + let b: i128 = env.storage().persistent().get(&B); |
| 148 | + let c: i128 = env.storage().persistent().get(&C); |
| 149 | + let d: i128 = env.storage().persistent().get(&D); |
| 150 | + a + b + c + d |
| 151 | + } |
| 152 | +
|
| 153 | + pub fn fn_two() -> i128 { |
| 154 | + let a: i128 = env.storage().persistent().get(&A); |
| 155 | + let b: i128 = env.storage().persistent().get(&B); |
| 156 | + let c: i128 = env.storage().persistent().get(&C); |
| 157 | + let d: i128 = env.storage().persistent().get(&D); |
| 158 | + a * b * c * d |
| 159 | + } |
| 160 | + `; |
| 161 | + const report = analyzeFootprintAccess(source); |
| 162 | + |
| 163 | + const highOverlap = report.findings.find((f) => f.pattern === "high_overlap"); |
| 164 | + expect(highOverlap).toBeDefined(); |
| 165 | + expect(highOverlap!.ruleId).toBe("SOROBAN-FPA-07"); |
| 166 | + }); |
| 167 | + |
| 168 | + it("should build function profiles correctly", () => { |
| 169 | + const source = ` |
| 170 | + const A: Symbol = Symbol::new(&env, "a"); |
| 171 | + const B: Symbol = Symbol::new(&env, "b"); |
| 172 | +
|
| 173 | + pub fn profile_test() -> i128 { |
| 174 | + let a: i128 = env.storage().persistent().get(&A); |
| 175 | + let b: i128 = env.storage().persistent().get(&B); |
| 176 | + env.storage().persistent().set(&A, &0); |
| 177 | + a + b |
| 178 | + } |
| 179 | + `; |
| 180 | + const report = analyzeFootprintAccess(source); |
| 181 | + |
| 182 | + expect(report.functionProfiles.length).toBeGreaterThan(0); |
| 183 | + const profile = report.functionProfiles.find((p) => p.functionName === "profile_test"); |
| 184 | + expect(profile).toBeDefined(); |
| 185 | + expect(profile!.keysAccessed).toContain("a"); |
| 186 | + expect(profile!.keysAccessed).toContain("b"); |
| 187 | + expect(profile!.readCount).toBe(2); |
| 188 | + expect(profile!.writeCount).toBe(1); |
| 189 | + }); |
| 190 | + |
| 191 | + it("should report accurate metrics", () => { |
| 192 | + const source = ` |
| 193 | + const A: Symbol = Symbol::new(&env, "a"); |
| 194 | + const B: Symbol = Symbol::new(&env, "b"); |
| 195 | +
|
| 196 | + pub fn metrics_test() -> i128 { |
| 197 | + let a: i128 = env.storage().persistent().get(&A); |
| 198 | + let b: i128 = env.storage().persistent().get(&B); |
| 199 | + env.storage().persistent().set(&A, &42); |
| 200 | + a + b |
| 201 | + } |
| 202 | + `; |
| 203 | + const report = analyzeFootprintAccess(source); |
| 204 | + |
| 205 | + expect(report.metrics.totalKeys).toBe(2); |
| 206 | + expect(report.metrics.totalReads).toBe(2); |
| 207 | + expect(report.metrics.totalWrites).toBe(1); |
| 208 | + expect(report.metrics.averageAccessDensity).toBeGreaterThan(0); |
| 209 | + }); |
| 210 | + |
| 211 | + it("should track access across multiple storage tiers", () => { |
| 212 | + const source = ` |
| 213 | + const INSTANCE_KEY: Symbol = Symbol::new(&env, "instance_key"); |
| 214 | + const PERSISTENT_KEY: Symbol = Symbol::new(&env, "persistent_key"); |
| 215 | + const TEMP_KEY: Symbol = Symbol::new(&env, "temp_key"); |
| 216 | +
|
| 217 | + pub fn multi_tier() { |
| 218 | + env.storage().instance().get(&INSTANCE_KEY); |
| 219 | + env.storage().persistent().get(&PERSISTENT_KEY); |
| 220 | + env.storage().temporary().get(&TEMP_KEY); |
| 221 | + } |
| 222 | + `; |
| 223 | + const report = analyzeFootprintAccess(source); |
| 224 | + |
| 225 | + expect(report.metrics.totalKeys).toBe(3); |
| 226 | + |
| 227 | + const keys = Object.values(report.keyAccessMap); |
| 228 | + const tiers = keys.map((k) => k.storageTier); |
| 229 | + expect(tiers).toContain("instance"); |
| 230 | + expect(tiers).toContain("persistent"); |
| 231 | + expect(tiers).toContain("temporary"); |
| 232 | + }); |
| 233 | + }); |
| 234 | +}); |
0 commit comments