22.1 KB644 lines
Blame
1/**
2 * Portions Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8/*
9
10Copyright (c) 2020 Jun Wu
11
12Permission is hereby granted, free of charge, to any person obtaining a copy
13of this software and associated documentation files (the "Software"), to deal
14in the Software without restriction, including without limitation the rights
15to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16copies of the Software, and to permit persons to whom the Software is
17furnished to do so, subject to the following conditions:
18
19The above copyright notice and this permission notice shall be included in all
20copies or substantial portions of the Software.
21
22THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28SOFTWARE.
29
30*/
31
32// @lint-ignore-every SPELL
33
34import type {LineIdx, Rev} from '../linelog';
35
36import {describe, expect, it} from '@jest/globals';
37import * as Immutable from 'immutable';
38import {splitLines} from 'shared/diff';
39import {LineLog, executeCache} from '../linelog';
40
41describe('LineLog', () => {
42 it('can be empty', () => {
43 const log = new LineLog();
44 expect(log.maxRev).toBe(0);
45 expect(log.checkOut(0)).toBe('');
46 });
47
48 it('supports a single edit', () => {
49 let log = new LineLog();
50 log = log.recordText('c\nd\ne');
51 expect(log.maxRev).toBe(1);
52 expect(log.checkOut(1)).toBe('c\nd\ne');
53
54 expect(log.checkOutLines(1)).toMatchObject([
55 {data: 'c\n', rev: 1},
56 {data: 'd\n', rev: 1},
57 {data: 'e', rev: 1},
58 {data: '', rev: 0},
59 ]);
60 });
61
62 it('supports modifying rev 0', () => {
63 let log = new LineLog();
64 log = log.recordText('c\n', 0);
65 expect(log.maxRev).toBe(0);
66 expect(log.checkOut(0)).toBe('c\n');
67 expect(log.checkOutLines(0)[0]).toMatchObject({rev: 0});
68 log = log.recordText('c\nd', 1);
69 expect(log.checkOutLines(1)[1]).toMatchObject({rev: 1});
70 expect(log.checkOut(0)).toBe('c\n');
71 expect(log.checkOutLines(0)[0]).toMatchObject({rev: 0});
72 });
73
74 it('supports multiple edits', () => {
75 let log = new LineLog();
76 log = log.recordText('c\nd\ne\n');
77 log = log.recordText('d\ne\nf\n');
78 expect(log.maxRev).toBe(2);
79 expect(log.checkOut(2)).toBe('d\ne\nf\n');
80 expect(log.checkOutLines(2)).toMatchObject([
81 {data: 'd\n', rev: 1, deleted: false},
82 {data: 'e\n', rev: 1, deleted: false},
83 {data: 'f\n', rev: 2, deleted: false},
84 {data: '', rev: 0, deleted: false},
85 ]);
86 });
87
88 it('supports checkout', () => {
89 let log = new LineLog();
90 log = log.recordText('c\nd\ne\n');
91 log = log.recordText('d\ne\nf\n');
92 expect(log.checkOut(1)).toBe('c\nd\ne\n');
93 expect(log.checkOutLines(1)[0].deleted).toBe(false);
94 expect(log.checkOut(0)).toBe('');
95 expect(log.checkOutLines(0)).toMatchObject([{data: ''}]);
96 expect(log.checkOut(2)).toBe('d\ne\nf\n');
97 expect(log.checkOutLines(2)[2].deleted).toBe(false);
98 });
99
100 it('supports checkout range', () => {
101 const log = new LineLog()
102 .recordText('c\nd\ne\n') // rev 1
103 .recordText('d\ne\nf\n') // rev 2
104 .recordText('e\ng\nf\n'); // rev 3
105
106 expect(log.checkOutLines(2, 1)).toMatchObject([
107 {data: 'c\n', rev: 1, deleted: true}, // 'c' not in rev 2
108 {data: 'd\n', rev: 1, deleted: false},
109 {data: 'e\n', rev: 1, deleted: false},
110 {data: 'f\n', rev: 2, deleted: false},
111 {data: '', rev: 0, deleted: false}, // END
112 ]);
113
114 expect(log.checkOutLines(3, 0)).toMatchObject([
115 {data: 'c\n', rev: 1, deleted: true}, // 'c' not in rev 3
116 {data: 'd\n', rev: 1, deleted: true}, // 'd' not in rev 3
117 {data: 'e\n', rev: 1, deleted: false}, // 'e' in rev 3
118 {data: 'g\n', rev: 3, deleted: false},
119 {data: 'f\n', rev: 2, deleted: false},
120 {data: '', rev: 0, deleted: false},
121 ]);
122
123 // should not reuse cache
124 expect(log.checkOut(3)).toBe('e\ng\nf\n');
125
126 expect(log.checkOutLines(3, 2)).toMatchObject([
127 {data: 'd\n', rev: 1, deleted: true},
128 {data: 'e\n', rev: 1, deleted: false},
129 {data: 'g\n', rev: 3, deleted: false},
130 {data: 'f\n', rev: 2, deleted: false},
131 {data: ''},
132 ]);
133 });
134
135 it('bumps rev when recording the same content', () => {
136 let log = new LineLog();
137 log = log.recordText('a\n');
138 expect(log.maxRev).toBe(1);
139 log = log.recordText('a\n');
140 expect(log.maxRev).toBe(2);
141 log = log.recordText('a\n');
142 expect(log.maxRev).toBe(3);
143 });
144
145 it('avoids checkout/execute calls for common edits', () => {
146 const log = new LineLog().recordText('a\nb\nc\nd\ne\n', 1);
147
148 // Modifies 3 chunks. This does not introduce new cache
149 // miss, because:
150 // - checkout (calls execute) used by recordText can
151 // reuse cache populated by the previous recordText.
152 // This contributes a cache hit.
153 // - checkout used by editChunk is skipped, because
154 // recordText passes in `aLinesCache`. This does not
155 // change cache miss or hit.
156 const stats = (executeCache.stats = {miss: 0, hit: 0});
157 log.recordText('A\nb\nC\nd\nE\n', 3);
158 expect(stats).toMatchObject({miss: 0, hit: 1});
159 });
160
161 it('works with immutable.is', () => {
162 const log1 = new LineLog().recordText('a').recordText('b');
163 const log2 = new LineLog({code: log1.code, maxRev: log1.maxRev});
164 const log3 = new LineLog().recordText('a').recordText('b');
165
166 expect(Object.is(log1, log2)).toBeFalsy();
167 expect(Immutable.is(log1, log2)).toBeTruthy();
168 expect(Immutable.is(log1, log3)).toBeTruthy();
169 });
170
171 it('provides human readable instructions', () => {
172 const log = logFromTextList(['a\n', 'b\n']);
173 // The instructions are internal details. For example, an
174 // optimization pass might remove unconditional jumps.
175 // Shall the output change, just update the test here.
176 expect(log.code.describeHumanReadableInstructions()).toEqual([
177 '0: J 1',
178 '1: JL 1 3',
179 '2: J 4',
180 '3: END',
181 '4: JL 2 6',
182 '5: LINE 2 "b"',
183 '6: JGE 2 3',
184 '7: LINE 1 "a"',
185 '8: J 3',
186 ]);
187 });
188
189 describe('provides human readable insertion and deletion stacks', () => {
190 const show = (texts: string[]) =>
191 logFromTextList(texts).code.describeHumanReadableInsDelStacks();
192
193 it('interleaved insertion and deletion trees', () => {
194 // First 3 revs are from https://sapling-scm.com/docs/internals/linelog
195 expect(show(['a\nb\nc\n', 'a\nb\n1\n2\nc\n', 'a\n2\nc\n', 'c\n', ''])).toEqual([
196 '+----Insert (rev 1) ',
197 '| Delete (rev 4) ----+',
198 '| Line: a |',
199 '| Delete (rev 3) ---+|',
200 '| Line: b ||',
201 '|+---Insert (rev 2) ||',
202 '|| Line: 1 ||',
203 '|| ---+|',
204 '|| Line: 2 |',
205 '|+--- |',
206 '| ----+',
207 '| Delete (rev 5) ----+',
208 '| Line: c |',
209 '| ----+',
210 '+---- ',
211 ]);
212 });
213
214 it('insertions at the beginning and end are not nested', () => {
215 expect(show(['b\n', 'a\nb\n', 'a\nb\nc\n'])).toEqual([
216 '+---Insert (rev 2) ',
217 '| Line: a ',
218 '+--- ',
219 '+---Insert (rev 1) ',
220 '| Line: b ',
221 '+--- ',
222 '+---Insert (rev 3) ',
223 '| Line: c ',
224 '+--- ',
225 ]);
226 });
227
228 it('insertion between old new revs is not nested', () => {
229 expect(show(['a\n', 'a\nc\n', 'a\nb\nc\n'])).toEqual([
230 '+---Insert (rev 1) ',
231 '| Line: a ',
232 '+--- ',
233 '+---Insert (rev 3) ',
234 '| Line: b ',
235 '+--- ',
236 '+---Insert (rev 2) ',
237 '| Line: c ',
238 '+--- ',
239 ]);
240 });
241
242 it('insertion between new old revs is not nested', () => {
243 expect(show(['c\n', 'a\nc\n', 'a\nb\nc\n'])).toEqual([
244 '+---Insert (rev 2) ',
245 '| Line: a ',
246 '+--- ',
247 '+---Insert (rev 3) ',
248 '| Line: b ',
249 '+--- ',
250 '+---Insert (rev 1) ',
251 '| Line: c ',
252 '+--- ',
253 ]);
254 });
255 });
256
257 describe('supports editing previous revisions', () => {
258 it('edits stack bottom', () => {
259 const textList = ['a\n', 'a\nb\n', 'z\na\nb\n'];
260 let log = logFromTextList(textList);
261
262 log = log.recordText('1\n2\n', 1); // replace rev 1 from "a" to "1 2"
263 expect(log.checkOut(1)).toBe('1\n2\n');
264 expect(log.checkOut(2)).toBe('1\n2\nb\n');
265 expect(log.checkOut(3)).toBe('z\n1\n2\nb\n');
266
267 log = log.recordText('', 1); // replace rev 1 to ""
268 expect(log.checkOut(1)).toBe('');
269 expect(log.checkOut(2)).toBe('b\n');
270 expect(log.checkOut(3)).toBe('z\nb\n');
271 });
272
273 it('edits stack middle', () => {
274 const textList = ['c\nd\ne\n', 'b\nc\nd\n', 'a\nb\nc\nz\n'];
275 let log = logFromTextList(textList);
276
277 log = log.recordText('b\nd\n', 2); // remove "c" from "b c d" in rev 2
278 expect(log.checkOut(1)).toBe('c\nd\ne\n'); // rev 1 is unchanged, despite "c" comes from rev 1
279 expect(log.checkOut(2)).toBe('b\nd\n');
280 expect(log.checkOut(3)).toBe('a\nb\nz\n'); // "c" in rev 3 is also removed
281
282 log = logFromTextList(textList);
283 log = log.recordText('b\nc\ny\ny\n', 2); // change "d" to "y y" from rev 2.
284 expect(log.checkOut(3)).toBe('a\nb\nc\nz\n'); // rev 3 is unchanged, since "d" was deleted
285
286 log = logFromTextList(textList);
287 log = log.recordText('k\n', 2); // replace rev 2 with "k", this is a tricky case
288 expect(log.checkOut(3)).toBe('a\nk\n'); // "a k" is the current implementation, "a k z" might be better
289 });
290 });
291
292 it('calculates dependencies using linelog instructions', () => {
293 const deps = (textList: string[]): (number | number[])[][] => {
294 const insertEOL = (text: string): string =>
295 text
296 .split('')
297 .map(c => `${c}\n`)
298 .join('');
299 const log = logFromTextList(textList.map(insertEOL));
300 return flattenDepMap(log.calculateDepMap());
301 };
302
303 expect(deps([])).toEqual([]);
304
305 // Insertions.
306 expect(deps(['a'])).toEqual([[1, [0]]]);
307 expect(deps(['a', 'b'])).toEqual([
308 [1, [0]],
309 [2, [1]],
310 ]);
311 expect(deps(['a', 'ab'])).toEqual([
312 [1, [0]],
313 [2, [0]],
314 ]);
315 expect(deps(['b', 'ab'])).toEqual([
316 [1, [0]],
317 [2, [0]],
318 ]);
319 expect(deps(['ad', 'abd', 'abcd'])).toEqual([
320 [1, [0]],
321 [2, [1]],
322 [3, [1]],
323 ]);
324 expect(deps(['ad', 'acd', 'abcd'])).toEqual([
325 [1, [0]],
326 [2, [1]],
327 [3, [1]],
328 ]);
329
330 // Deletions.
331 expect(deps(['abcd', 'abd', 'ad', 'a'])).toEqual([
332 [1, [0]],
333 [2, [1]],
334 [3, [1]],
335 [4, [1]],
336 ]);
337 expect(deps(['abcd', 'acd', 'ad', 'd'])).toEqual([
338 [1, [0]],
339 [2, [1]],
340 [3, [1]],
341 [4, [1]],
342 ]);
343
344 // Multi-rev insertion, then delete.
345 expect(deps(['abc', 'abcdef', '']).at(-1)).toEqual([3, [1, 2]]);
346 expect(deps(['abc', 'abcdef', 'af']).at(-1)).toEqual([3, [1, 2]]);
347 expect(deps(['abc', 'abcdef', 'cd']).at(-1)).toEqual([3, [1, 2]]);
348
349 // Another test case.
350 const textList = ['abc', 'abcd', 'zabcd', 'zad', 'ad', 'adef', 'ade', 'ad1e', 'xyz'];
351 expect(deps(textList)).toStrictEqual([
352 [1, [0]],
353 [2, [0]],
354 [3, [0]],
355 // deletes "bc" added by rev 1
356 [4, [1]],
357 // deletes "z" added by rev 3
358 [5, [3]],
359 // appends after "d" added by rev 2, considered as independent
360 [6, [0]],
361 // deletes "f" added by rev 6
362 [7, [6]],
363 // inserts "1" between "d" (rev 2) and "e" (rev 6), still considered as independent
364 [8, [0]],
365 // replaces all: "a" (rev 1), "d" (rev 2), "1" (rev 8), "e" (rev 6)
366 // rev 4 is also a dependent for nested deletions.
367 [9, [1, 2, 4, 6, 8]],
368 ]);
369 });
370
371 it('produces flatten lines', () => {
372 const textList = ['a\nb\nc\n', 'b\nc\nd\ne\n', 'a\nc\nd\nf\n'];
373 const log = logFromTextList(textList);
374 const lines = log.flatten();
375 expect(lines.toJS()).toEqual(
376 [
377 ['a', [1]],
378 ['a', [3]],
379 ['b', [1, 2]],
380 ['c', [1, 2, 3]],
381 ['d', [2, 3]],
382 ['f', [3]],
383 ['e', [2]],
384 ].map(([line, revs]) => ({revs, data: `${line}\n`})),
385 );
386 // Verify the flatten lines against definition - if "revs" contains the rev,
387 // then the line is included in "rev".
388 for (let rev = 1; rev <= textList.length; rev++) {
389 const text = lines
390 .filter(line => line.revs.has(rev))
391 .map(line => line.data)
392 .join('');
393 expect(text).toBe(textList[rev - 1]);
394 }
395 });
396
397 it('supports truncation', () => {
398 const textList = ['a\nb\nc\n', 'b\nc\nd\n', 'b\nd\ne\n', 'f\n'];
399 const log = logFromTextList(textList);
400 // Note: rev 0 is empty.
401 for (let truncateRev = 0; truncateRev < textList.length; truncateRev++) {
402 const truncatedLog = log.truncate(truncateRev);
403 expect(truncatedLog.maxRev).toBe(Math.max(0, truncateRev - 1));
404 for (let rev = 0; rev < textList.length; rev++) {
405 const text = truncatedLog.checkOut(rev);
406 if (rev < truncateRev) {
407 expect(text).toBe(rev < 1 ? '' : textList[rev - 1]);
408 } else {
409 // By contract, rev >= truncateRev are removed, so their content should be the same as truncateRev - 1.
410 expect(text).toBe(truncateRev <= 1 ? '' : log.checkOut(truncateRev - 1));
411 }
412 }
413 // We can still append content to the truncatedLog.
414 const text = 'a\nc\ne\n';
415 const modifiedLog = truncatedLog.recordText(text);
416 expect(modifiedLog.checkOut(modifiedLog.maxRev)).toBe(text);
417 for (let rev = 0; rev < truncateRev; rev++) {
418 expect(modifiedLog.checkOut(rev)).toBe(log.checkOut(rev));
419 }
420 }
421 });
422
423 // Ported from test-linelog-edits.py (D3709431)
424 // Compare LineLog.editChunk against List<string>.splice edits.
425 it('stress tests against random edits', () => {
426 const maxDeltaA = 10; // max(a2 - a1)
427 const maxDeltaB = 10; // max(b2 - b1)
428 const maxB1 = 0xffffff;
429
430 function randInt(min: number, max: number): number {
431 return Math.floor(Math.random() * (max - min + 1) + min);
432 }
433
434 function randBool(): boolean {
435 return Math.random() < 0.5;
436 }
437
438 function* generateCases(
439 endRev = 1000,
440 ): Generator<[Immutable.List<string>, Rev, LineIdx, LineIdx, LineIdx, LineIdx, string[]]> {
441 // Maintain `lines` as an alternative to LineLog
442 let lines: Immutable.List<string> = Immutable.List();
443 for (let rev = 0; rev <= endRev; ++rev) {
444 const n = lines.size;
445 const a1 = randInt(0, n);
446 const a2 = randInt(a1, Math.min(n, a1 + maxDeltaA));
447 const b1 = randInt(0, maxB1);
448 const b2 = randInt(b1, b1 + maxDeltaB);
449 const bLines: string[] = [];
450 for (let bIdx = b1; bIdx < b2; bIdx++) {
451 const line = randBool() ? '\n' : `${rev}:${bIdx}\n`;
452 bLines.push(line);
453 }
454 lines = lines.splice(a1, a2 - a1, ...bLines);
455 yield [lines, rev, a1, a2, b1, b2, bLines];
456 }
457 }
458
459 const cases = [...generateCases()];
460 let log = new LineLog();
461
462 // The use of aLines cache prevents cache miss.
463 // It can reduce editChunk time for 100 revs from 240ms to 8ms.
464 const aLines = [...log.checkOutLines(0)];
465 executeCache.stats = {miss: 0};
466 cases.forEach(([_lines, rev, a1, a2, _b1, _b2, bLines]) => {
467 log = log.editChunk(log.maxRev, a1, a2, rev, bLines, aLines);
468 });
469 expect(executeCache.stats).toMatchObject({miss: 0});
470
471 // Check that every rev can be checked out fine.
472 cases.forEach(([lines, rev, _a1, _a2, _b1, _b2, _bLines]) => {
473 expect(log.checkOut(rev)).toBe(lines.join(''));
474 });
475 });
476
477 describe('supports remapping revisions', () => {
478 it('updates maxRev up', () => {
479 const log = logFromTextList(['a', 'b']).remapRevs(new Map([[1, 10]]));
480 expect(log.maxRev).toBe(10);
481 });
482
483 it('updates maxRev down', () => {
484 const log = new LineLog().recordText('a\n', 10).remapRevs(new Map([[10, 5]]));
485 expect(log.maxRev).toBe(5);
486 });
487
488 it('invalidates previous checkout', () => {
489 let log = logFromTextList(['b\n', 'b\nc\n', 'a\nb\nc\n']);
490 expect(log.checkOut(2)).toBe('b\nc\n');
491 log = log.remapRevs(
492 new Map([
493 [2, 3],
494 [3, 2],
495 ]),
496 );
497 expect(log.checkOut(2)).not.toBe('b\nc\n');
498 });
499
500 describe('can reorder changes', () => {
501 const defaultSwap: [Rev, Rev] = [2, 3];
502
503 /** Follow the swap. For example, mapSwap(2, [2, 3]) is 3. */
504 const mapSwap = (rev: Rev, swap: [Rev, Rev] = defaultSwap) =>
505 rev === swap[0] ? swap[1] : rev === swap[1] ? swap[0] : rev;
506
507 /**
508 * Swap revs from revs to newRevs. All "line"s are added, by different revs.
509 * For example, when lines = ['a\n', 'b\n', 'c\n'], lineAddedOrder = [1, 3, 2],
510 * it means 'a' was added by rev 1, 'b' by rev 3, 'c' by rev 2, so the 3 revs
511 * are ['a\n', 'a\nc\n', 'a\nb\n'].
512 *
513 * By default, this test takes 3 revs and swap rev 2 and 3.
514 */
515 function testReorderInsertions(
516 lines: string[],
517 lineAddedOrder: Rev[],
518 opts?: {
519 swap?: [Rev, Rev] /* 2 revs to swap, default: [2, 3] */;
520 join?: string /* join character, default: '' */;
521 expectedDepMapOverride?: [Rev, Rev[]][] /* override the expected depMap */;
522 expectedTextOverride?: string[] /* override the expected texts after swapping */;
523 },
524 ) {
525 expect(lines.length).toBe(lineAddedOrder.length);
526 const revs = Array.from({length: lines.length}, (_, i) => i + 1); /* ex. [1, 2, 3] */
527 const swap = opts?.swap ?? defaultSwap;
528 const optJoin = opts?.join ?? '';
529 const getTexts = (revOrder: Rev[], join = optJoin): string[] => {
530 const revSet = new Set<Rev>();
531 return revOrder.map(rev => {
532 revSet.add(rev);
533 return lines.filter((_l, i) => revSet.has(lineAddedOrder[i])).join(join);
534 });
535 };
536 const texts = getTexts(revs);
537 const log = logFromTextList(texts);
538
539 // Nothing depends on each other.
540 expect(flattenDepMap(log.calculateDepMap())).toEqual(
541 opts?.expectedDepMapOverride ?? revs.map(r => [r, [0]]),
542 );
543
544 // Reorder.
545 const reorderedLog = log.remapRevs(new Map([swap, [swap[1], swap[0]]]));
546
547 // Check reorder result.
548 // Remove "join" for easier comparison.
549 const removeJoin = (text: string) =>
550 splitLines(text)
551 .filter(t => t !== optJoin)
552 .join('');
553 const reorderedRevs = revs.map(r => mapSwap(r, swap));
554 const expectedReorderedText = opts?.expectedTextOverride ?? getTexts(reorderedRevs, '');
555 revs.forEach((rev, i) => {
556 expect(removeJoin(reorderedLog.checkOut(rev))).toBe(expectedReorderedText[i]);
557 });
558
559 return reorderedLog;
560 }
561
562 const threeRevPermutations = [
563 [1, 2, 3],
564 [1, 3, 2],
565 [2, 1, 3],
566 [2, 3, 1],
567 [3, 1, 2],
568 [3, 2, 1],
569 ];
570
571 /** Make the test a bit more interesting with multi-line input. */
572 const charToFunction = (c: string): string => `function ${c} () {\n return '${c}';\n}\n`;
573
574 const abcTexts = ['a\n', 'b\n', 'c\n'];
575 const functionTexts = ['a', 'b', 'c'].map(charToFunction);
576
577 threeRevPermutations.forEach(revOrder => {
578 it(`insert 'a','b','c' in ${revOrder} order`, () => {
579 const log = testReorderInsertions(abcTexts, revOrder);
580 expect(log.checkOutLines(3)).toMatchObject([
581 {data: 'a\n', rev: mapSwap(revOrder[0])},
582 {data: 'b\n', rev: mapSwap(revOrder[1])},
583 {data: 'c\n', rev: mapSwap(revOrder[2])},
584 {data: '', rev: 0},
585 ]);
586 });
587
588 it(`insert 3 functions in ${revOrder} order`, () => {
589 testReorderInsertions(functionTexts, revOrder);
590 });
591
592 it(`insert 3 functions with new line join, in ${revOrder} order`, () => {
593 testReorderInsertions(abcTexts, revOrder, {join: '\n'});
594 });
595 });
596 });
597
598 it('takes a mapping function', () => {
599 const log = logFromTextList(['a\n', 'a\nb\n']).remapRevs(r =>
600 r === 2 ? 1 : r === 1 ? 2 : r,
601 );
602 expect(log.checkOut(1)).toBe('b\n');
603 expect(log.checkOut(2)).toBe('a\nb\n');
604 });
605
606 it('can merge changes', () => {
607 const log = logFromTextList(['b\n', 'b\nc\n', 'a\nb\nc\n']).remapRevs(new Map([[2, 1]]));
608 expect(log.checkOut(1)).toBe('b\nc\n');
609 expect(log.checkOut(2)).toBe('b\nc\n');
610 expect(log.checkOut(3)).toBe('a\nb\nc\n');
611 });
612
613 it('can insert changes', () => {
614 const log = logFromTextList(['b\n', 'b\nc\n'])
615 .remapRevs(new Map([[2, 3]]))
616 .recordText('a\nb\n', 2);
617 expect(log.checkOut(3)).toBe('a\nb\nc\n');
618 });
619
620 it('does not check dependencies or conflicts', () => {
621 // rev 2: +b between a and c. rev 2 depends on rev 1.
622 const log = logFromTextList(['a\nc\n', 'a\nb\nc\n']).remapRevs(
623 new Map([
624 [1, 2],
625 [2, 1],
626 ]),
627 );
628 // rev 1 is now empty, not 'b'.
629 expect(log.checkOut(1)).toBe('');
630 expect(log.checkOut(2)).toBe('a\nb\nc\n');
631 });
632 });
633});
634
635function logFromTextList(textList: string[]): LineLog {
636 let log = new LineLog();
637 textList.forEach(text => (log = log.recordText(text)));
638 return log;
639}
640
641function flattenDepMap(depMap: Map<Rev, Set<Rev>>): [Rev, Rev[]][] {
642 return [...depMap.entries()].map(([rev, set]) => [rev, [...set].sort()] as [Rev, Rev[]]).sort();
643}
644