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