Skip to content

Commit 3141337

Browse files
Merge pull request #10 from boostcampwm-2024/refactor/#004_tombstone_처리_추가_1
Refactor/#4 tombstone 처리 추가 1
2 parents 3a3a430 + c43f6df commit 3141337

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+550
-92
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
import { LinkedList, BlockLinkedList, TextLinkedList } from "../src/LinkedList";
2+
import { Node, Char, Block } from "../src/Node";
3+
import { NodeId, BlockId, CharId } from "../src/NodeId";
4+
5+
describe("연결 리스트", () => {
6+
let blockList: BlockLinkedList;
7+
let textList: TextLinkedList;
8+
9+
beforeEach(() => {
10+
blockList = new BlockLinkedList();
11+
textList = new TextLinkedList();
12+
});
13+
14+
describe("기본 동작", () => {
15+
test("빈 리스트로 초기화되어야 함", () => {
16+
expect(blockList.head).toBeNull();
17+
expect(blockList.nodeMap).toEqual({});
18+
});
19+
20+
test("노드 생성 및 조회가 가능해야 함", () => {
21+
const id = new BlockId(1, 1); // clock: 1, client: 1
22+
const node = new Block("테스트 내용", id);
23+
blockList.setNode(id, node);
24+
25+
const retrievedNode = blockList.getNode(id);
26+
expect(retrievedNode).toBeDefined();
27+
expect(retrievedNode?.value).toBe("테스트 내용");
28+
});
29+
30+
test("존재하지 않는 노드 조회시 null을 반환해야 함", () => {
31+
const nonExistentId = new BlockId(999, 1);
32+
expect(blockList.getNode(nonExistentId)).toBeNull();
33+
});
34+
});
35+
36+
describe("삽입 연산", () => {
37+
test("리스트 시작에 노드를 삽입해야 함", () => {
38+
const id = new BlockId(1, 1);
39+
const result = blockList.insertAtIndex(0, "첫 번째 노드", id);
40+
41+
expect(blockList.head).toEqual(id);
42+
expect(result.node.value).toBe("첫 번째 노드");
43+
expect(result.node.prev).toBeNull();
44+
expect(result.node.next).toBeNull();
45+
});
46+
47+
test("기존 노드들 사이에 노드를 삽입해야 함", () => {
48+
// 첫 번째 노드 삽입
49+
const id1 = new BlockId(1, 1);
50+
blockList.insertAtIndex(0, "첫번째", id1);
51+
52+
// 두 번째 노드 삽입
53+
const id2 = new BlockId(2, 1);
54+
blockList.insertAtIndex(1, "두번째", id2);
55+
56+
// 중간에 노드 삽입
57+
const id3 = new BlockId(3, 1);
58+
blockList.insertAtIndex(1, "중간", id3);
59+
const middleNode = blockList.getNode(id3);
60+
expect(middleNode?.prev).toEqual(id1);
61+
expect(middleNode?.next).toEqual(id2);
62+
});
63+
64+
test("ID로 노드를 삽입해야 함", () => {
65+
const id1 = new BlockId(1, 1);
66+
const node1 = new Block("첫번째", id1);
67+
const id2 = new BlockId(2, 1);
68+
const node2 = new Block("두번째", id2);
69+
70+
node2.prev = id1;
71+
blockList.insertById(node1);
72+
blockList.insertById(node2);
73+
74+
expect(blockList.stringify()).toBe("첫번째두번째");
75+
});
76+
});
77+
78+
describe("삭제 및 톰스톤", () => {
79+
test("노드가 삭제됨으로 표시되어야 함 (톰스톤)", () => {
80+
const id = new BlockId(1, 1);
81+
blockList.insertAtIndex(0, "삭제될 내용", id);
82+
83+
blockList.deleteNode(id);
84+
const node = blockList.getNode(id);
85+
86+
expect(node?.deleted).toBe(true);
87+
expect(blockList.stringify()).toBe(""); // 삭제된 노드는 문자열에 나타나지 않아야 함
88+
});
89+
90+
test("연속된 톰스톤을 처리해야 함", () => {
91+
const id1 = new BlockId(1, 1);
92+
const id2 = new BlockId(2, 1);
93+
const id3 = new BlockId(3, 1);
94+
95+
blockList.insertAtIndex(0, "첫번째", id1);
96+
blockList.insertAtIndex(1, "두번째", id2);
97+
blockList.insertAtIndex(2, "세번째", id3);
98+
99+
blockList.deleteNode(id1);
100+
blockList.deleteNode(id2);
101+
102+
expect(blockList.stringify()).toBe("세번째");
103+
expect(blockList.spread().length).toBe(1);
104+
});
105+
106+
test("인덱스로 노드를 찾을 때 톰스톤을 건너뛰어야 함", () => {
107+
const id1 = new BlockId(1, 1);
108+
const id2 = new BlockId(2, 1);
109+
const id3 = new BlockId(3, 1);
110+
111+
blockList.insertAtIndex(0, "첫번째", id1);
112+
blockList.insertAtIndex(1, "두번째", id2);
113+
blockList.insertAtIndex(2, "세번째", id3);
114+
115+
blockList.deleteNode(id2); // 중간 노드를 삭제로 표시
116+
117+
const thirdNode = blockList.findByIndex(1); // 삭제된 노드를 건너뛰어야 함
118+
expect(thirdNode.value).toBe("세번째");
119+
});
120+
});
121+
122+
describe("노드 제거", () => {
123+
test("리스트에서 노드를 완전히 제거해야 함", () => {
124+
const id1 = new BlockId(1, 1);
125+
const id2 = new BlockId(2, 1);
126+
127+
blockList.insertAtIndex(0, "첫번째", id1);
128+
blockList.insertAtIndex(1, "두번째", id2);
129+
130+
blockList.removeNode(id1);
131+
132+
expect(blockList.getNode(id1)).toBeNull();
133+
expect(blockList.head).toEqual(id2);
134+
});
135+
136+
test("헤드 노드 제거를 처리해야 함", () => {
137+
const id = new BlockId(1, 1);
138+
blockList.insertAtIndex(0, "헤드", id);
139+
140+
blockList.removeNode(id);
141+
142+
expect(blockList.head).toBeNull();
143+
expect(blockList.nodeMap).toEqual({});
144+
});
145+
});
146+
147+
describe("리스트 연산", () => {
148+
test("인덱스 범위 내의 노드들을 가져와야 함", () => {
149+
const nodes = ["첫번째", "두번째", "세번째", "네번째"].map((value, index) => {
150+
const id = new BlockId(index + 1, 1);
151+
blockList.insertAtIndex(index, value, id);
152+
return blockList.getNode(id)!;
153+
});
154+
155+
const middle = blockList.getNodesBetween(1, 3);
156+
expect(middle.length).toBe(2);
157+
expect(middle[0].value).toBe("두번째");
158+
expect(middle[1].value).toBe("세번째");
159+
});
160+
161+
test("리스트를 배열로 변환해야 함", () => {
162+
["가", "나", "다"].forEach((value, index) => {
163+
const id = new BlockId(index + 1, 1);
164+
blockList.insertAtIndex(index, value, id);
165+
});
166+
167+
const array = blockList.spread();
168+
expect(array.length).toBe(3);
169+
expect(array.map((node) => node.value).join("")).toBe("가나다");
170+
});
171+
});
172+
173+
describe("직렬화", () => {
174+
test("리스트를 직렬화하고 역직렬화해야 함", () => {
175+
const id1 = new BlockId(1, 1);
176+
const id2 = new BlockId(2, 1);
177+
178+
blockList.insertAtIndex(0, "첫번째", id1);
179+
blockList.insertAtIndex(1, "두번째", id2);
180+
181+
const serialized = blockList.serialize();
182+
const newList = new BlockLinkedList();
183+
newList.deserialize(serialized);
184+
185+
expect(newList.stringify()).toBe(blockList.stringify());
186+
expect(newList.head).toEqual(blockList.head);
187+
});
188+
189+
test("BlockId를 올바르게 직렬화하고 역직렬화해야 함", () => {
190+
const originalId = new BlockId(1, 2);
191+
const serialized = originalId.serialize();
192+
const deserialized = BlockId.deserialize(serialized);
193+
194+
expect(deserialized.clock).toBe(originalId.clock);
195+
expect(deserialized.client).toBe(originalId.client);
196+
expect(deserialized.equals(originalId)).toBe(true);
197+
});
198+
});
199+
200+
describe("순서가 있는 리스트 연산", () => {
201+
test("순서가 있는 리스트의 인덱스를 갱신해야 함", () => {
202+
const id1 = new BlockId(1, 1);
203+
const id2 = new BlockId(2, 1);
204+
const id3 = new BlockId(3, 1);
205+
206+
blockList.insertAtIndex(0, "첫번째", id1);
207+
blockList.insertAtIndex(1, "두번째", id2);
208+
blockList.insertAtIndex(2, "세번째", id3);
209+
210+
const node1 = blockList.getNode(id1)!;
211+
const node2 = blockList.getNode(id2)!;
212+
const node3 = blockList.getNode(id3)!;
213+
214+
node1.type = "ol";
215+
node2.type = "ol";
216+
node3.type = "ol";
217+
218+
expect(node1.next).toBe(id2);
219+
expect(node2.prev).toBe(id1);
220+
expect(node2.next).toBe(id3);
221+
expect(node3.prev).toBe(id2);
222+
223+
blockList.updateAllOrderedListIndices();
224+
225+
expect(node1.listIndex).toBe(1);
226+
expect(node2.listIndex).toBe(2);
227+
expect(node3.listIndex).toBe(3);
228+
});
229+
230+
test("들여쓰기가 다른 중첩된 순서 리스트를 처리해야 함", () => {
231+
const id1 = new BlockId(1, 1);
232+
const id2 = new BlockId(2, 1);
233+
234+
blockList.insertAtIndex(0, "부모", id1);
235+
blockList.insertAtIndex(1, "자식", id2);
236+
237+
const node1 = blockList.getNode(id1)!;
238+
const node2 = blockList.getNode(id2)!;
239+
240+
node1.type = "ol";
241+
node2.type = "ol";
242+
node2.indent = 1;
243+
blockList.updateAllOrderedListIndices();
244+
245+
expect(node1.listIndex).toBe(1);
246+
expect(node2.listIndex).toBe(1); // 중첩된 리스트는 1부터 시작
247+
expect(node1.indent).toBe(0);
248+
expect(node2.indent).toBe(1);
249+
});
250+
});
251+
252+
describe("노드 재정렬", () => {
253+
test("노드 순서를 변경해야 함", () => {
254+
const id1 = new BlockId(1, 1);
255+
const id2 = new BlockId(2, 1);
256+
const id3 = new BlockId(3, 1);
257+
258+
blockList.insertAtIndex(0, "첫번째", id1);
259+
blockList.insertAtIndex(1, "두번째", id2);
260+
blockList.insertAtIndex(2, "세번째", id3);
261+
262+
blockList.reorderNodes({
263+
targetId: id3,
264+
beforeId: id1,
265+
afterId: id2,
266+
});
267+
expect(blockList.head).toEqual(id1);
268+
const firstNode = blockList.getNode(blockList.head!);
269+
expect(firstNode?.value).toBe("첫번째");
270+
271+
blockList.reorderNodes({
272+
targetId: id2,
273+
beforeId: null,
274+
afterId: id1,
275+
});
276+
expect(blockList.head).toEqual(id2);
277+
const firstNode2 = blockList.getNode(blockList.head!);
278+
expect(firstNode2?.value).toBe("두번째");
279+
});
280+
281+
test("순서가 있는 리스트의 재정렬을 처리해야 함", () => {
282+
const id1 = new BlockId(1, 1);
283+
const id2 = new BlockId(2, 1);
284+
285+
const node1 = new Block("첫번째", id1);
286+
const node2 = new Block("두번째", id2);
287+
288+
node1.type = "ol";
289+
node2.type = "ol";
290+
291+
blockList.insertById(node1);
292+
blockList.insertById(node2);
293+
294+
blockList.reorderNodes({
295+
targetId: id2,
296+
beforeId: id1,
297+
afterId: null,
298+
});
299+
300+
blockList.updateAllOrderedListIndices();
301+
302+
const firstNode = blockList.getNode(blockList.head!);
303+
expect(firstNode?.listIndex).toBe(1);
304+
});
305+
});
306+
307+
describe("CRDT 속성", () => {
308+
test("노드 우선순위를 올바르게 결정해야 함", () => {
309+
const id1 = new BlockId(1, 1);
310+
const id2 = new BlockId(1, 2);
311+
const id3 = new BlockId(2, 1);
312+
313+
const node1 = new Block("첫번째", id1);
314+
const node2 = new Block("두번째", id2);
315+
const node3 = new Block("세번째", id3);
316+
317+
// 같은 클록, 다른 클라이언트
318+
expect(node1.precedes(node2)).toBe(true);
319+
320+
// 다른 클록
321+
expect(node1.precedes(node3)).toBe(true);
322+
expect(node3.precedes(node1)).toBe(false);
323+
});
324+
});
325+
326+
describe("TombStone 제거", () => {
327+
test("3개 툼스톤 가비지 컬렉팅 확인", () => {
328+
const id1 = new BlockId(1, 1);
329+
const id2 = new BlockId(2, 1);
330+
const id3 = new BlockId(3, 1);
331+
blockList.insertAtIndex(0, "a", id1);
332+
blockList.insertAtIndex(1, "c", id2);
333+
blockList.insertAtIndex(1, "b", id3);
334+
335+
blockList.deleteNode(id3);
336+
337+
blockList.clearDeletedNode();
338+
339+
const node1 = blockList.getNode(id1);
340+
341+
expect(node1?.next).toBe(id2);
342+
});
343+
344+
test("여러개 Linked list 가비지 컬렉팅", () => {
345+
const ids = [];
346+
for (let i = 0; i < 10; i++) {
347+
const id = new BlockId(i, 1);
348+
ids.push(id);
349+
blockList.insertAtIndex(i, `블록${i}`, id);
350+
}
351+
352+
expect(blockList.spread().length).toBe(10);
353+
354+
for (let i = 1; i < 10; i += 2) {
355+
blockList.deleteNode(ids[i]);
356+
}
357+
358+
blockList.clearDeletedNode();
359+
360+
expect(blockList.spread().length).toBe(5);
361+
const node0 = blockList.getNode(ids[0]);
362+
const node3 = blockList.getNode(ids[2]);
363+
expect(node0?.next).toBe(node3?.id);
364+
});
365+
});
366+
});

@noctaCrdt/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
testPathIgnorePatterns: ["/dist/"],
6+
// roots: ['<rootDir>/__tests__'],
7+
// ...
8+
};

0 commit comments

Comments
 (0)