Skip to content
File

Blob: test/packed-object-candidates.worker.test.ts

typescript231 lines
1import { describe, expect, it } from "vitest";
2import { env } from "cloudflare:workers";
3import { buildCopyPrefixDelta, buildPack } from "./util/git-pack";
4import { uniqueRepoId } from "./util/test-helpers";
5import {
6 makeActiveCatalogRow,
7 makeLimiter,
8 packIndexerLog as log,
9} from "./util/pack-indexer.helpers";
10 
11import { computeOid } from "@/worker/git/core/objects";
12import {
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";
22import type { PackedObjectResult, PackCatalogRow } from "@/worker/git/object-store/types";
23import { scanPack, resolveDeltasAndWriteIdx } from "@/worker/git/pack/indexer";
24 
25type IndexedTestPack = {
26 packKey: string;
27 packBytes: number;
28 objectCount: number;
29 idxBytes: number;
30 source: IndexedPackSource;
31};
32 
33async 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 
76function 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 
85async 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 
111describe("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});