File
Blob: test/packed-object-candidates.worker.test.ts
| 1 | import { describe, expect, it } from "vitest"; |
| 2 | import { env } from "cloudflare:workers"; |
| 3 | import { buildCopyPrefixDelta, buildPack } from "./util/git-pack"; |
| 4 | import { uniqueRepoId } from "./util/test-helpers"; |
| 5 | import { |
| 6 | makeActiveCatalogRow, |
| 7 | makeLimiter, |
| 8 | packIndexerLog as log, |
| 9 | } from "./util/pack-indexer.helpers"; |
| 10 | |
| 11 | import { computeOid } from "@/worker/git/core/objects"; |
| 12 | import { |
| 13 | collectPackedObjectCandidates, |
| 14 | findFirstPackedObjectCandidate, |
| 15 | findOidRunInIdx, |
| 16 | loadIdxView, |
| 17 | materializePackedObjectCandidate, |
| 18 | type IndexedPackSource, |
| 19 | type PackedMaterializerCyclePolicy, |
| 20 | type PackedObjectCandidate, |
| 21 | } from "@/worker/git/object-store"; |
| 22 | import type { PackedObjectResult, PackCatalogRow } from "@/worker/git/object-store/types"; |
| 23 | import { scanPack, resolveDeltasAndWriteIdx } from "@/worker/git/pack/indexer"; |
| 24 | |
| 25 | type IndexedTestPack = { |
| 26 | packKey: string; |
| 27 | packBytes: number; |
| 28 | objectCount: number; |
| 29 | idxBytes: number; |
| 30 | source: IndexedPackSource; |
| 31 | }; |
| 32 | |
| 33 | async function putIndexedPack(args: { |
| 34 | packKey: string; |
| 35 | packBytes: Uint8Array; |
| 36 | activeCatalog?: PackCatalogRow[]; |
| 37 | }): Promise<IndexedTestPack> { |
| 38 | await env.REPO_BUCKET.put(args.packKey, args.packBytes); |
| 39 | const packSize = args.packBytes.byteLength; |
| 40 | const scanResult = await scanPack({ |
| 41 | env, |
| 42 | packKey: args.packKey, |
| 43 | packSize, |
| 44 | limiter: makeLimiter(), |
| 45 | countSubrequest: () => {}, |
| 46 | log, |
| 47 | }); |
| 48 | const resolveResult = await resolveDeltasAndWriteIdx({ |
| 49 | env, |
| 50 | packKey: args.packKey, |
| 51 | packSize, |
| 52 | limiter: makeLimiter(), |
| 53 | countSubrequest: () => {}, |
| 54 | log, |
| 55 | scanResult, |
| 56 | activeCatalog: args.activeCatalog, |
| 57 | repoId: "test", |
| 58 | }); |
| 59 | |
| 60 | const idx = await loadIdxView(env, args.packKey, undefined, packSize); |
| 61 | if (!idx) throw new Error(`test failed to load idx for ${args.packKey}`); |
| 62 | |
| 63 | return { |
| 64 | packKey: args.packKey, |
| 65 | packBytes: packSize, |
| 66 | objectCount: scanResult.objectCount, |
| 67 | idxBytes: resolveResult.idxBytes, |
| 68 | source: { |
| 69 | packKey: args.packKey, |
| 70 | packBytes: packSize, |
| 71 | idx, |
| 72 | }, |
| 73 | }; |
| 74 | } |
| 75 | |
| 76 | function catalogRow(pack: IndexedTestPack): PackCatalogRow { |
| 77 | return makeActiveCatalogRow({ |
| 78 | packKey: pack.packKey, |
| 79 | packBytes: pack.packBytes, |
| 80 | objectCount: pack.objectCount, |
| 81 | idxBytes: pack.idxBytes, |
| 82 | }); |
| 83 | } |
| 84 | |
| 85 | async function materializeWithFirstHitRefs(args: { |
| 86 | candidate: PackedObjectCandidate; |
| 87 | sources: IndexedPackSource[]; |
| 88 | cyclePolicy: PackedMaterializerCyclePolicy; |
| 89 | visited: Set<string>; |
| 90 | }): Promise<PackedObjectResult | undefined> { |
| 91 | return await materializePackedObjectCandidate({ |
| 92 | env, |
| 93 | candidate: args.candidate, |
| 94 | limiter: makeLimiter(), |
| 95 | countSubrequest: () => {}, |
| 96 | log, |
| 97 | cyclePolicy: args.cyclePolicy, |
| 98 | resolveRefBase: async (baseOid, nextVisited) => { |
| 99 | const baseCandidate = findFirstPackedObjectCandidate(args.sources, baseOid); |
| 100 | if (!baseCandidate) return undefined; |
| 101 | return await materializeWithFirstHitRefs({ |
| 102 | ...args, |
| 103 | candidate: baseCandidate, |
| 104 | visited: nextVisited, |
| 105 | }); |
| 106 | }, |
| 107 | visited: args.visited, |
| 108 | }); |
| 109 | } |
| 110 | |
| 111 | describe("packed object candidate helpers", () => { |
| 112 | it("enumerates the full duplicate OID run in an idx", async () => { |
| 113 | const prefix = uniqueRepoId("candidate-duplicate-run"); |
| 114 | const duplicatePayload = new TextEncoder().encode("duplicate blob\n"); |
| 115 | const duplicateOid = await computeOid("blob", duplicatePayload); |
| 116 | const otherPayload = new TextEncoder().encode("other blob\n"); |
| 117 | const packBytes = await buildPack([ |
| 118 | { type: "blob", payload: otherPayload }, |
| 119 | { type: "blob", payload: duplicatePayload }, |
| 120 | { type: "blob", payload: duplicatePayload }, |
| 121 | { type: "blob", payload: duplicatePayload }, |
| 122 | ]); |
| 123 | const pack = await putIndexedPack({ |
| 124 | packKey: `test/${prefix}.pack`, |
| 125 | packBytes, |
| 126 | }); |
| 127 | |
| 128 | const run = findOidRunInIdx(pack.source.idx, duplicateOid); |
| 129 | if (!run) throw new Error("test failed to find duplicate OID run"); |
| 130 | expect(run.endIndex - run.startIndex + 1).toBe(3); |
| 131 | |
| 132 | const candidates = collectPackedObjectCandidates([pack.source], duplicateOid); |
| 133 | expect(candidates).toHaveLength(3); |
| 134 | expect(new Set(candidates.map((candidate) => candidate.objectIndex)).size).toBe(3); |
| 135 | expect(candidates.every((candidate) => candidate.oid === duplicateOid)).toBe(true); |
| 136 | }); |
| 137 | |
| 138 | it("keeps first-candidate lookup in snapshot order", async () => { |
| 139 | const prefix = uniqueRepoId("candidate-first-hit"); |
| 140 | const payload = new TextEncoder().encode("shared object\n"); |
| 141 | const oid = await computeOid("blob", payload); |
| 142 | const older = await putIndexedPack({ |
| 143 | packKey: `test/${prefix}-older.pack`, |
| 144 | packBytes: await buildPack([{ type: "blob", payload }]), |
| 145 | }); |
| 146 | const newer = await putIndexedPack({ |
| 147 | packKey: `test/${prefix}-newer.pack`, |
| 148 | packBytes: await buildPack([{ type: "blob", payload }]), |
| 149 | }); |
| 150 | |
| 151 | const first = findFirstPackedObjectCandidate([newer.source, older.source], oid); |
| 152 | expect(first?.source.packKey).toBe(newer.packKey); |
| 153 | expect(first?.packSlot).toBe(0); |
| 154 | |
| 155 | const reversed = findFirstPackedObjectCandidate([older.source, newer.source], oid); |
| 156 | expect(reversed?.source.packKey).toBe(older.packKey); |
| 157 | expect(reversed?.packSlot).toBe(0); |
| 158 | }); |
| 159 | |
| 160 | it("returns a miss for cyclic candidates so callers can try an older duplicate", async () => { |
| 161 | const prefix = uniqueRepoId("candidate-cycle-miss"); |
| 162 | const payload = new TextEncoder().encode("cycle base\n"); |
| 163 | const oid = await computeOid("blob", payload); |
| 164 | const older = await putIndexedPack({ |
| 165 | packKey: `test/${prefix}-older.pack`, |
| 166 | packBytes: await buildPack([{ type: "blob", payload }]), |
| 167 | }); |
| 168 | const newer = await putIndexedPack({ |
| 169 | packKey: `test/${prefix}-newer.pack`, |
| 170 | packBytes: await buildPack([ |
| 171 | { |
| 172 | type: "ref-delta", |
| 173 | baseOid: oid, |
| 174 | delta: buildCopyPrefixDelta(payload, payload.length), |
| 175 | }, |
| 176 | ]), |
| 177 | activeCatalog: [catalogRow(older)], |
| 178 | }); |
| 179 | |
| 180 | const sources = [newer.source, older.source]; |
| 181 | const candidates = collectPackedObjectCandidates(sources, oid); |
| 182 | let resolved: PackedObjectResult | undefined; |
| 183 | for (const candidate of candidates) { |
| 184 | resolved = await materializeWithFirstHitRefs({ |
| 185 | candidate, |
| 186 | sources, |
| 187 | cyclePolicy: "miss", |
| 188 | visited: new Set<string>(), |
| 189 | }); |
| 190 | if (resolved) break; |
| 191 | } |
| 192 | |
| 193 | expect(resolved?.packKey).toBe(older.packKey); |
| 194 | expect(resolved?.payload).toEqual(payload); |
| 195 | }); |
| 196 | |
| 197 | it("throws on recursion cycles when object-store policy is requested", async () => { |
| 198 | const prefix = uniqueRepoId("candidate-cycle-throw"); |
| 199 | const payload = new TextEncoder().encode("throwing cycle base\n"); |
| 200 | const oid = await computeOid("blob", payload); |
| 201 | const older = await putIndexedPack({ |
| 202 | packKey: `test/${prefix}-older.pack`, |
| 203 | packBytes: await buildPack([{ type: "blob", payload }]), |
| 204 | }); |
| 205 | const newer = await putIndexedPack({ |
| 206 | packKey: `test/${prefix}-newer.pack`, |
| 207 | packBytes: await buildPack([ |
| 208 | { |
| 209 | type: "ref-delta", |
| 210 | baseOid: oid, |
| 211 | delta: buildCopyPrefixDelta(payload, payload.length), |
| 212 | }, |
| 213 | ]), |
| 214 | activeCatalog: [catalogRow(older)], |
| 215 | }); |
| 216 | |
| 217 | const sources = [newer.source, older.source]; |
| 218 | const first = findFirstPackedObjectCandidate(sources, oid); |
| 219 | if (!first) throw new Error("test failed to find cyclic candidate"); |
| 220 | |
| 221 | await expect( |
| 222 | materializeWithFirstHitRefs({ |
| 223 | candidate: first, |
| 224 | sources, |
| 225 | cyclePolicy: "throw", |
| 226 | visited: new Set<string>(), |
| 227 | }) |
| 228 | ).rejects.toThrow("pack object recursion cycle"); |
| 229 | }); |
| 230 | }); |