addons/isl/src/stackEdit/__tests__/absorb.test.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {Map as ImMap} from 'immutable';
b69ab319import type {AbsorbEdit, AbsorbEditId} from '../absorb';
b69ab3110import type {FileRev} from '../fileStackState';
b69ab3111
b69ab3112import {splitLines} from 'shared/diff';
b69ab3113import {
b69ab3114 analyseFileStack,
b69ab3115 applyFileStackEditsWithAbsorbId,
b69ab3116 calculateAbsorbEditsForFileStack,
b69ab3117 embedAbsorbId,
b69ab3118 extractRevAbsorbId,
b69ab3119 revWithAbsorb,
b69ab3120} from '../absorb';
b69ab3121import {FileStackState} from '../fileStackState';
b69ab3122
b69ab3123// See also [test-fb-ext-absorb-filefixupstate.py](https://github.com/facebook/sapling/blob/eb3d35d/eden/scm/tests/test-fb-ext-absorb-filefixupstate.py#L75)
b69ab3124describe('analyseFileStack', () => {
b69ab3125 it('edits an empty file', () => {
b69ab3126 // Public: empty.
b69ab3127 const stack = createStack(['']);
b69ab3128 // No Selectedion - cannot edit the public (rev 0) content.
b69ab3129 expect(analyseFile(stack, 'a')).toMatchInlineSnapshot(`"0:0=>'a': Rev 0+ Selected null"`);
b69ab3130 });
b69ab3131
b69ab3132 it('edits 2 lines by one insertion', () => {
b69ab3133 // Public: empty. Rev 1: "1\n1\n".
b69ab3134 const stack = createStack(['', '1↵1↵']);
b69ab3135 // Delete the chunk.
b69ab3136 expect(analyseFile(stack, '')).toMatchInlineSnapshot(`"0:2=>'': Rev 1+ Selected 1"`);
b69ab3137 // Replace to 1 line.
b69ab3138 expect(analyseFile(stack, '2')).toMatchInlineSnapshot(`"0:2=>'2': Rev 1+ Selected 1"`);
b69ab3139 // Replace to 2 lines.
b69ab3140 expect(analyseFile(stack, '22')).toMatchInlineSnapshot(`"0:2=>'22': Rev 1+ Selected 1"`);
b69ab3141 // Replace to 3 lines.
b69ab3142 expect(analyseFile(stack, '222')).toMatchInlineSnapshot(`"0:2=>'222': Rev 1+ Selected 1"`);
b69ab3143 });
b69ab3144
b69ab3145 it('edits 3 lines by 3 insertions', () => {
b69ab3146 // Public: empty. Rev 1: "1". Rev 2: "1", "2". Rev 3: "1", "2", "3".
b69ab3147 const stack = createStack(['', '1↵', '1↵2↵', '1↵2↵3↵']);
b69ab3148 // No change.
b69ab3149 expect(analyseFile(stack, '1↵2↵3↵')).toMatchInlineSnapshot(`""`);
b69ab3150 // Replave the last line.
b69ab3151 expect(analyseFile(stack, '1↵2↵c↵')).toMatchInlineSnapshot(`"2:3=>'c': Rev 3+ Selected 3"`);
b69ab3152 // Replave the 2nd line.
b69ab3153 expect(analyseFile(stack, '1↵b↵3↵')).toMatchInlineSnapshot(`"1:2=>'b': Rev 2+ Selected 2"`);
b69ab3154 // Replace the last 2 lines.
b69ab3155 expect(analyseFile(stack, '1↵b↵c↵')).toMatchInlineSnapshot(`
b69ab3156 "1:2=>'b': Rev 2+ Selected 2
b69ab3157 2:3=>'c': Rev 3+ Selected 3"
b69ab3158 `);
b69ab3159 // Replace the first line.
b69ab3160 expect(analyseFile(stack, 'a↵2↵3↵')).toMatchInlineSnapshot(`"0:1=>'a': Rev 1+ Selected 1"`);
b69ab3161 // Replace the first and the last lines.
b69ab3162 expect(analyseFile(stack, 'a↵2↵c↵')).toMatchInlineSnapshot(`
b69ab3163 "0:1=>'a': Rev 1+ Selected 1
b69ab3164 2:3=>'c': Rev 3+ Selected 3"
b69ab3165 `);
b69ab3166 // Replace the first 2 lines.
b69ab3167 expect(analyseFile(stack, 'a↵b↵3↵')).toMatchInlineSnapshot(`
b69ab3168 "0:1=>'a': Rev 1+ Selected 1
b69ab3169 1:2=>'b': Rev 2+ Selected 2"
b69ab3170 `);
b69ab3171 // Replace all 3 lines.
b69ab3172 expect(analyseFile(stack, 'a↵b↵c↵')).toMatchInlineSnapshot(`
b69ab3173 "0:1=>'a': Rev 1+ Selected 1
b69ab3174 1:2=>'b': Rev 2+ Selected 2
b69ab3175 2:3=>'c': Rev 3+ Selected 3"
b69ab3176 `);
b69ab3177 // Non 1:1 line mapping.
b69ab3178 expect(analyseFile(stack, 'a↵b↵c↵d↵')).toMatchInlineSnapshot(
b69ab3179 `"0:3=>'abcd': Rev 3+ Selected null"`,
b69ab3180 );
b69ab3181 expect(analyseFile(stack, 'a↵b↵')).toMatchInlineSnapshot(`"0:3=>'ab': Rev 3+ Selected null"`);
b69ab3182 // Deletion.
b69ab3183 expect(analyseFile(stack, '')).toMatchInlineSnapshot(`
b69ab3184 "0:1=>'': Rev 1+ Selected 1
b69ab3185 1:2=>'': Rev 2+ Selected 2
b69ab3186 2:3=>'': Rev 3+ Selected 3"
b69ab3187 `);
b69ab3188 expect(analyseFile(stack, '1↵')).toMatchInlineSnapshot(`
b69ab3189 "1:2=>'': Rev 2+ Selected 2
b69ab3190 2:3=>'': Rev 3+ Selected 3"
b69ab3191 `);
b69ab3192 expect(analyseFile(stack, '2↵')).toMatchInlineSnapshot(`
b69ab3193 "0:1=>'': Rev 1+ Selected 1
b69ab3194 2:3=>'': Rev 3+ Selected 3"
b69ab3195 `);
b69ab3196 expect(analyseFile(stack, '3↵')).toMatchInlineSnapshot(`
b69ab3197 "0:1=>'': Rev 1+ Selected 1
b69ab3198 1:2=>'': Rev 2+ Selected 2"
b69ab3199 `);
b69ab31100 expect(analyseFile(stack, '1↵3↵')).toMatchInlineSnapshot(`"1:2=>'': Rev 2+ Selected 2"`);
b69ab31101 // Replace the 2nd line with multiple lines.
b69ab31102 expect(analyseFile(stack, '1↵b↵b↵3↵')).toMatchInlineSnapshot(`"1:2=>'bb': Rev 2+ Selected 2"`);
b69ab31103 // "Confusing" replaces.
b69ab31104 expect(analyseFile(stack, '1↵b↵b↵b↵')).toMatchInlineSnapshot(
b69ab31105 `"1:3=>'bbb': Rev 3+ Selected null"`,
b69ab31106 );
b69ab31107 expect(analyseFile(stack, 'b↵b↵b↵3↵')).toMatchInlineSnapshot(
b69ab31108 `"0:2=>'bbb': Rev 2+ Selected null"`,
b69ab31109 );
b69ab31110 expect(analyseFile(stack, '1↵b↵')).toMatchInlineSnapshot(`"1:3=>'b': Rev 3+ Selected null"`);
b69ab31111 expect(analyseFile(stack, 'b↵3↵')).toMatchInlineSnapshot(`"0:2=>'b': Rev 2+ Selected null"`);
b69ab31112 // Insertion at the beginning and the end.
b69ab31113 expect(analyseFile(stack, '1↵2↵3↵c↵')).toMatchInlineSnapshot(`"3:3=>'c': Rev 3+ Selected 3"`);
b69ab31114 expect(analyseFile(stack, 'a↵1↵2↵3↵')).toMatchInlineSnapshot(`"0:0=>'a': Rev 1+ Selected 1"`);
b69ab31115 // "Confusing" insertions.
b69ab31116 expect(analyseFile(stack, '1↵a↵2↵3↵')).toMatchInlineSnapshot(
b69ab31117 `"1:1=>'a': Rev 2+ Selected null"`,
b69ab31118 );
b69ab31119 expect(analyseFile(stack, '1↵2↵b↵3↵')).toMatchInlineSnapshot(
b69ab31120 `"2:2=>'b': Rev 3+ Selected null"`,
b69ab31121 );
b69ab31122 });
b69ab31123
b69ab31124 it('does not edit the public commit', () => {
b69ab31125 const stack = createStack(['1↵3↵5↵7↵', '0↵1↵2↵5↵6↵7↵8↵']);
b69ab31126 // Nothing changed.
b69ab31127 expect(analyseFile(stack, '0↵1↵2↵5↵6↵7↵8↵')).toMatchInlineSnapshot(`""`);
b69ab31128 // No Selectedion. "1" (from public) is changed to "a".
b69ab31129 expect(analyseFile(stack, '0↵a↵2↵5↵6↵7↵8↵')).toMatchInlineSnapshot(
b69ab31130 `"1:2=>'a': Rev 0+ Selected null"`,
b69ab31131 );
b69ab31132 // Whole block changed. NOTE: This is different from the Python behavior.
b69ab31133 expect(analyseFile(stack, 'a↵b↵c↵d↵e↵f↵g↵')).toMatchInlineSnapshot(
b69ab31134 `"0:7=>'abcdefg': Rev 1+ Selected 1"`,
b69ab31135 );
b69ab31136 expect(analyseFile(stack, 'a↵b↵c↵d↵e↵f↵')).toMatchInlineSnapshot(
b69ab31137 `"0:7=>'abcdef': Rev 1+ Selected 1"`,
b69ab31138 );
b69ab31139 expect(analyseFile(stack, '')).toMatchInlineSnapshot(`"0:7=>'': Rev 1+ Selected 1"`);
b69ab31140 // Insert 2 lines.
b69ab31141 expect(analyseFile(stack, '0↵1↵2↵3↵4↵5↵6↵7↵8↵9↵')).toMatchInlineSnapshot(`
b69ab31142 "3:3=>'34': Rev 1+ Selected 1
b69ab31143 7:7=>'9': Rev 1+ Selected 1"
b69ab31144 `);
b69ab31145 });
b69ab31146
b69ab31147 describe('applyFileStackEdits', () => {
b69ab31148 it('edits 3 lines by 3 insertions', () => {
b69ab31149 // Replace ['1','2','3'] to ['a','b','c'], 1->a, 2->b, 3->c.
b69ab31150 const fullStack = createStack(['', '1↵', '1↵2↵', '1↵2↵3↵', 'a↵b↵c↵']);
b69ab31151 const edits = calculateAbsorbEditsForFileStack(fullStack)[1];
b69ab31152 const stack = fullStack.truncate((fullStack.revLength - 1) as FileRev);
b69ab31153 expect(applyEdits(stack, edits.values())).toMatchInlineSnapshot(`" a↵ a↵b↵ a↵b↵c↵"`);
b69ab31154 // Tweak the `selectedRev` so the 1->a, 2->b changes happen at the last rev.
b69ab31155 const edits2 = edits.map(c => c.set('selectedRev', 3 as FileRev));
b69ab31156 expect(applyEdits(stack, edits2.values())).toMatchInlineSnapshot(`" 1↵ 1↵2↵ a↵b↵c↵"`);
b69ab31157 // Drop the "2->b" change by setting selectedRev to `null`.
b69ab31158 const edits3 = edits.map(c => (c.oldStart === 1 ? c.set('selectedRev', null) : c));
b69ab31159 expect(applyEdits(stack, edits3.values())).toMatchInlineSnapshot(`" a↵ a↵2↵ a↵2↵c↵ a↵b↵c↵"`);
b69ab31160 });
b69ab31161
b69ab31162 it('edits do not need to be 1:1 line mapping', () => {
b69ab31163 // Replace ['111','2','333'] to ['aaaa','2','cc']. 111->aaaa. 333->cc.
b69ab31164 const fullStack = createStack(['', '2↵', '1↵1↵1↵2↵3↵3↵3↵', 'a↵a↵a↵a↵2↵c↵c↵']);
b69ab31165 const edits = calculateAbsorbEditsForFileStack(fullStack)[1];
b69ab31166 const stack = fullStack.truncate((fullStack.revLength - 1) as FileRev);
b69ab31167 expect(applyEdits(stack, edits.values())).toMatchInlineSnapshot(`" 2↵ a↵a↵a↵a↵2↵c↵c↵"`);
b69ab31168 // Drop the "1->aaa" change by setting selectedRev to `null`.
b69ab31169 const edits2 = edits.map(c => (c.oldStart === 0 ? c.set('selectedRev', null) : c));
b69ab31170 expect(applyEdits(stack, edits2.values())).toMatchInlineSnapshot(
b69ab31171 `" 2↵ 1↵1↵1↵2↵c↵c↵ a↵a↵a↵a↵2↵c↵c↵"`,
b69ab31172 );
b69ab31173 });
b69ab31174 });
b69ab31175
b69ab31176 describe('absorbId', () => {
b69ab31177 it('can be embedded into rev, and extracted out', () => {
b69ab31178 const plainRev = 567;
b69ab31179 const absorbEditId = 890;
b69ab31180 const rev = embedAbsorbId(plainRev as FileRev, absorbEditId);
b69ab31181 expect(extractRevAbsorbId(rev)).toEqual([plainRev, absorbEditId]);
b69ab31182 });
b69ab31183 });
b69ab31184
b69ab31185 describe('calculateAbsorbEditsForFileStack', () => {
b69ab31186 it('analyses a stack', () => {
b69ab31187 const stack = createStack([
b69ab31188 'p↵u↵b↵',
b69ab31189 'p↵u↵b↵1↵2↵3↵4↵',
b69ab31190 'p↵u↵b↵2↵3↵4↵5↵6↵',
b69ab31191 'p↵U↵b↵x↵3↵4↵6↵y↵',
b69ab31192 ]);
b69ab31193 const [analysedStack, absorbMap] = calculateAbsorbEditsForFileStack(stack);
b69ab31194 expect(describeAbsorbIdChunkMap(absorbMap)).toMatchInlineSnapshot(`
b69ab31195 [
b69ab31196 "0: -u↵ +U↵ Introduced=0",
b69ab31197 "1: -2↵ +x↵ Selected=1 Introduced=1",
b69ab31198 "2: -5↵ Selected=2 Introduced=2",
b69ab31199 "3: +y↵ Selected=2 Introduced=2",
b69ab31200 ]
b69ab31201 `);
b69ab31202 const show = (rev: number) => compactText(analysedStack.getRev(rev as FileRev));
b69ab31203 // Rev 1 original.
b69ab31204 expect(show(1)).toMatchInlineSnapshot(`"p↵u↵b↵1↵2↵3↵4↵"`);
b69ab31205 // Rev 1.99 is Rev 1 with the absorb "-2 -x" chunk applied.
b69ab31206 expect(show(1.99)).toMatchInlineSnapshot(`"p↵u↵b↵1↵x↵3↵4↵"`);
b69ab31207 // Rev 2 original.
b69ab31208 expect(show(2)).toMatchInlineSnapshot(`"p↵u↵b↵x↵3↵4↵5↵6↵"`);
b69ab31209 // Rev 2.99 is Rev 2 with the absorb "-5 +y" applied.
b69ab31210 const rev299 = revWithAbsorb(2 as FileRev);
b69ab31211 expect(show(rev299)).toMatchInlineSnapshot(`"p↵u↵b↵x↵3↵4↵6↵y↵"`);
b69ab31212 // Rev 3 "wdir()" is dropped - no changes from 2.99.
b69ab31213 expect(show(3)).toMatchInlineSnapshot(`"p↵u↵b↵x↵3↵4↵6↵y↵"`);
b69ab31214 // Rev 3.99 includes changes left in "wdir()": "pub" -> "pUb".
b69ab31215 // This edit changes the "public" portion so it wasn't absorbed by default.
b69ab31216 expect(show(3.99)).toMatchInlineSnapshot(`"p↵U↵b↵x↵3↵4↵6↵y↵"`);
b69ab31217 expect(analysedStack.convertToLineLog().code.describeHumanReadableInstructions())
b69ab31218 .toMatchInlineSnapshot(`
b69ab31219 [
b69ab31220 "0: J 1",
b69ab31221 "1: JL 0 5",
b69ab31222 "2: LINE 0 "p"",
b69ab31223 "3: J 30",
b69ab31224 "4: LINE 0 "b"",
b69ab31225 "5: J 6",
b69ab31226 "6: JL 1 11",
b69ab31227 "7: J 16",
b69ab31228 "8: J 25",
b69ab31229 "9: LINE 1 "3"",
b69ab31230 "10: LINE 1 "4"",
b69ab31231 "11: J 12",
b69ab31232 "12: JL 2 15",
b69ab31233 "13: J 22",
b69ab31234 "14: LINE 2 "6"",
b69ab31235 "15: J 19",
b69ab31236 "16: JGE 2 8",
b69ab31237 "17: LINE 1 "1"",
b69ab31238 "18: J 8",
b69ab31239 "19: J 21",
b69ab31240 "20: J 21",
b69ab31241 "21: J 35",
b69ab31242 "22: J 23",
b69ab31243 "23: J 38",
b69ab31244 "24: J 14",
b69ab31245 "25: J 27",
b69ab31246 "26: J 27",
b69ab31247 "27: J 28",
b69ab31248 "28: J 41",
b69ab31249 "29: J 9",
b69ab31250 "30: J 32",
b69ab31251 "31: J 32",
b69ab31252 "32: J 33",
b69ab31253 "33: J 46",
b69ab31254 "34: J 4",
b69ab31255 "35: JL 2.0000038146972656 37",
b69ab31256 "36: LINE 2.0000038146972656 "y"",
b69ab31257 "37: END",
b69ab31258 "38: JGE 2.000002861022949 24",
b69ab31259 "39: LINE 2 "5"",
b69ab31260 "40: J 24",
b69ab31261 "41: JL 1.0000019073486328 43",
b69ab31262 "42: LINE 1.0000019073486328 "x"",
b69ab31263 "43: JGE 1.0000019073486328 29",
b69ab31264 "44: LINE 1 "2"",
b69ab31265 "45: J 29",
b69ab31266 "46: JL 3.0000009536743164 48",
b69ab31267 "47: LINE 3.0000009536743164 "U"",
b69ab31268 "48: JGE 3.0000009536743164 34",
b69ab31269 "49: LINE 0 "u"",
b69ab31270 "50: J 34",
b69ab31271 ]
b69ab31272 `);
b69ab31273 });
b69ab31274
b69ab31275 it('1:1 line mapping edit can include immutable lines', () => {
b69ab31276 const stack = createStack(['p↵', 'p↵1↵', 'p↵1↵2↵', 'P↵X↵Y↵']);
b69ab31277 const [, absorbMap] = calculateAbsorbEditsForFileStack(stack);
b69ab31278 // Absorbed edits: "1 => X"; "2 => Y" (selected is set)
b69ab31279 // "p => P" is left in the working copy, since "p" is considered immutable.
b69ab31280 expect(describeAbsorbIdChunkMap(absorbMap)).toMatchInlineSnapshot(`
b69ab31281 [
b69ab31282 "0: -p↵ +P↵ Introduced=0",
b69ab31283 "1: -1↵ +X↵ Selected=1 Introduced=1",
b69ab31284 "2: -2↵ +Y↵ Selected=2 Introduced=2",
b69ab31285 ]
b69ab31286 `);
b69ab31287 });
b69ab31288 });
b69ab31289
b69ab31290 function createStack(texts: string[]): FileStackState {
b69ab31291 return new FileStackState(texts.map(t => injectNewLines(t)));
b69ab31292 }
b69ab31293
b69ab31294 function analyseFile(stack: FileStackState, newText: string): string {
b69ab31295 const text = injectNewLines(newText);
b69ab31296 const oldLines = splitLines(stack.getRev((stack.revLength - 1) as FileRev));
b69ab31297 const newLines = splitLines(text);
b69ab31298 const chunks = analyseFileStack(stack, text);
b69ab31299 return chunks
b69ab31300 .map(c => {
b69ab31301 // Check the old and new line numbers and content match.
b69ab31302 expect(oldLines.slice(c.oldStart, c.oldEnd)).toEqual(c.oldLines.toArray());
b69ab31303 expect(newLines.slice(c.newStart, c.newEnd)).toEqual(c.newLines.toArray());
b69ab31304 return `${c.oldStart}:${c.oldEnd}=>'${c.newLines
b69ab31305 .map(l => l.replace('\n', ''))
b69ab31306 .join('')}': Rev ${c.introductionRev}+ Selected ${c.selectedRev}`;
b69ab31307 })
b69ab31308 .join('\n');
b69ab31309 }
b69ab31310
b69ab31311 function applyEdits(stack: FileStackState, edits: Iterable<AbsorbEdit>): string {
b69ab31312 const editedStack = applyFileStackEditsWithAbsorbId(stack, edits);
b69ab31313 return compactTexts(editedStack.revs().map(rev => editedStack.getRev(revWithAbsorb(rev))));
b69ab31314 }
b69ab31315
b69ab31316 /** Replace "↵" with "\n" */
b69ab31317 function injectNewLines(text: string): string {
b69ab31318 return text.replaceAll('↵', '\n');
b69ab31319 }
b69ab31320
b69ab31321 function compactText(text: string): string {
b69ab31322 return text.replaceAll('\n', '↵');
b69ab31323 }
b69ab31324});
b69ab31325
b69ab31326/** Turn ["a\n", "a\nb\n"] to "a↵ ab↵". */
b69ab31327function compactTexts(texts: Iterable<string>): string {
b69ab31328 return [...texts].map(t => t.replaceAll('\n', '↵')).join(' ');
b69ab31329}
b69ab31330
b69ab31331export function describeAbsorbIdChunkMap(map: ImMap<AbsorbEditId, AbsorbEdit>): string[] {
b69ab31332 const result: string[] = [];
b69ab31333 map.forEach((chunk, id) => {
b69ab31334 const words: string[] = [`${id}:`];
b69ab31335 if (!chunk.oldLines.isEmpty()) {
b69ab31336 words.push(`-${compactTexts(chunk.oldLines)}`);
b69ab31337 }
b69ab31338 if (!chunk.newLines.isEmpty()) {
b69ab31339 words.push(`+${compactTexts(chunk.newLines)}`);
b69ab31340 }
b69ab31341 if (chunk.selectedRev != null) {
b69ab31342 words.push(`Selected=${chunk.selectedRev}`);
b69ab31343 }
b69ab31344 words.push(`Introduced=${chunk.introductionRev}`);
b69ab31345 result.push(words.join(' '));
b69ab31346 });
b69ab31347 return result;
b69ab31348}