Skip to content
Source
Sign inSign up
Open History to see the last commit

azureDevOpsDiff.test.ts

243 lines7.6 KB

1import { unquoteGitPatchPath } from "@t3tools/shared/gitPatchPath";
2import { describe, expect, it } from "vite-plus/test";
3​
4import {
5 azureDevOpsFilePatch,
6 byteLength,
7 MAX_FILE_DIFF_EDITS,
8 parseAzureDevOpsDiffCursor,
9} from "./azureDevOpsDiff.ts";
10import type { AzureDevOpsChangeEntry } from "./azureDevOpsPullRequestJson.ts";
11​
12function change(overrides: Partial<AzureDevOpsChangeEntry> = {}): AzureDevOpsChangeEntry {
13 return {
14 path: "README.md",
15 oldPath: "README.md",
16 changeKind: "change",
17 objectId: "8f80",
18 originalObjectId: "0ca4",
19 ...overrides,
20 };
22​
23function texts(oldContents: string, newContents: string, binary = false) {
24 return { oldContents, newContents, binary };
26​
27describe("azureDevOpsFilePatch", () => {
28 it("writes a changed file as the unified patch every diff viewer already reads", () => {
29 const patch = azureDevOpsFilePatch({
30 change: change(),
31 texts: texts("one\ntwo\nthree\n", "one\ntwo again\nthree\n"),
32 });
33​
34 expect(patch.truncated).toBe(false);
35 expect(patch.section).toBe(
36 [
37 "diff --git a/README.md b/README.md",
38 "--- a/README.md",
39 "+++ b/README.md",
40 "@@ -1,3 +1,3 @@",
41 " one",
42 "-two",
43 "+two again",
44 " three",
45 "",
46 ].join("\n"),
47 );
48 });
49​
50 it("names the side a deleted file no longer has as /dev/null", () => {
51 const patch = azureDevOpsFilePatch({
52 change: change({ path: "OLD.md", oldPath: "OLD.md", changeKind: "deleted" }),
53 texts: texts("gone\n", ""),
54 });
55​
56 expect(patch.section).toContain("deleted file mode 100644");
57 expect(patch.section).toContain("--- a/OLD.md");
58 expect(patch.section).toContain("+++ /dev/null");
59 expect(patch.section).toContain("@@ -1 +0,0 @@");
60 expect(patch.section).toContain("-gone");
61 });
62​
63 it("keeps the carriage returns of a file with Windows line endings", () => {
64 const patch = azureDevOpsFilePatch({
65 change: change(),
66 texts: texts("one\r\ntwo\r\n", "one\r\ntwo again\r\n"),
67 });
68​
69 expect(patch.section).toContain("-two\r");
70 expect(patch.section).toContain("+two again\r");
71 });
72​
73 it("keeps a file that only moved, which has no hunks to give", () => {
74 const patch = azureDevOpsFilePatch({
75 change: change({ path: "docs/new.md", oldPath: "docs/old.md", changeKind: "rename-pure" }),
76 texts: texts("same\n", "same\n"),
77 });
78​
79 expect(patch.truncated).toBe(false);
80 expect(patch.section).toBe(
81 [
82 "diff --git a/docs/old.md b/docs/new.md",
83 "similarity index 100%",
84 "rename from docs/old.md",
85 "rename to docs/new.md",
86 "--- a/docs/old.md",
87 "+++ b/docs/new.md",
88 "",
89 ].join("\n"),
90 );
91 });
92​
93 it("reports a binary file as changed rather than spelling it out", () => {
94 const patch = azureDevOpsFilePatch({
95 change: change({ path: "logo.png", oldPath: "logo.png" }),
96 texts: texts("PNG\u0000old", "PNG\u0000new"),
97 });
98​
99 expect(patch.truncated).toBe(true);
100 expect(patch.section).toContain("Binary files a/logo.png and b/logo.png differ");
101 });
102​
103 it("takes the host's word that a file is binary, whatever its bytes look like", () => {
104 const patch = azureDevOpsFilePatch({
105 change: change({ path: "logo.png", oldPath: "logo.png" }),
106 texts: texts("b2xk", "bmV3", true),
107 });
108​
109 expect(patch.truncated).toBe(true);
110 expect(patch.section).toContain("Binary files a/logo.png and b/logo.png differ");
111 });
112​
113 it("counts an overlong file in bytes rather than in characters", () => {
114 const patch = azureDevOpsFilePatch({
115 change: change({ path: "notes.md", oldPath: "notes.md" }),
116 texts: texts("\u4e00".repeat(200_000), "\u4e8c".repeat(200_000)),
117 });
118​
119 expect(patch.truncated).toBe(true);
120 expect(patch.section).toBe(
121 ["diff --git a/notes.md b/notes.md", "--- a/notes.md", "+++ b/notes.md", ""].join("\n"),
122 );
123 });
124​
125 it("lists a file too far apart to diff without its hunks", () => {
126 const lines = (prefix: string) =>
127 Array.from({ length: MAX_FILE_DIFF_EDITS }, (_, line) => `${prefix} ${line}`).join("\n");
128 const patch = azureDevOpsFilePatch({
129 change: change({ path: "generated.ts", oldPath: "generated.ts" }),
130 texts: texts(`${lines("old")}\n`, `${lines("new")}\n`),
131 });
132​
133 expect(patch.truncated).toBe(true);
134 expect(patch.abandoned).toBe(true);
135 expect(patch.section).toBe(
136 [
137 "diff --git a/generated.ts b/generated.ts",
138 "--- a/generated.ts",
139 "+++ b/generated.ts",
140 "",
141 ].join("\n"),
142 );
143 });
144​
145 it("keeps a wholly new file too heavy to write out listed without its hunks", () => {
146 const contents = `${Array.from({ length: 200_000 }, () => "x").join("\n")}\n`;
147 const patch = azureDevOpsFilePatch({
148 change: change({ path: "bundle.min.js", oldPath: "bundle.min.js", changeKind: "new" }),
149 texts: texts("", contents),
150 });
151​
152 expect(byteLength(contents)).toBeLessThan(512 * 1024);
153 expect(patch.truncated).toBe(true);
154 expect(patch.abandoned).toBe(false);
155 expect(patch.edits).toBe(200_000);
156 expect(patch.section).toBe(
157 [
158 "diff --git a/bundle.min.js b/bundle.min.js",
159 "new file mode 100644",
160 "--- /dev/null",
161 "+++ b/bundle.min.js",
162 "",
163 ].join("\n"),
164 );
165 });
166​
167 it("gives an empty new file no hunk to read", () => {
168 const patch = azureDevOpsFilePatch({
169 change: change({ path: "EMPTY.md", oldPath: "EMPTY.md", changeKind: "new" }),
170 texts: texts("", ""),
171 });
172​
173 expect(patch.edits).toBe(0);
174 expect(patch.section).not.toContain("@@");
175 });
176​
177 it("marks a wholly new file whose last line has no newline after it", () => {
178 const patch = azureDevOpsFilePatch({
179 change: change({ path: "NOTES.md", oldPath: "NOTES.md", changeKind: "new" }),
180 texts: texts("", "one\ntwo"),
181 });
182​
183 expect(patch.section).toBe(
184 [
185 "diff --git a/NOTES.md b/NOTES.md",
186 "new file mode 100644",
187 "--- /dev/null",
188 "+++ b/NOTES.md",
189 "@@ -0,0 +1,2 @@",
190 "+one",
191 "+two",
192 "\\ No newline at end of file",
193 "",
194 ].join("\n"),
195 );
196 });
197});
198​
199describe("a file Azure names something a patch header cannot carry plainly", () => {
200 it("quotes the names a rename states, which carry no side letter", () => {
201 const patch = azureDevOpsFilePatch({
202 change: change({
203 path: "docs/new\tname.md",
204 oldPath: "docs/old\tname.md",
205 changeKind: "rename-pure",
206 }),
207 texts: texts("same\n", "same\n"),
208 });
209​
210 expect(patch.section).toBe(
211 [
212 'diff --git "a/docs/old\\tname.md" "b/docs/new\\tname.md"',
213 "similarity index 100%",
214 'rename from "docs/old\\tname.md"',
215 'rename to "docs/new\\tname.md"',
216 '--- "a/docs/old\\tname.md"',
217 '+++ "b/docs/new\\tname.md"',
218 "",
219 ].join("\n"),
220 );
221 });
222​
223 it("hands a reader of the header back the name Azure gave", () => {
224 const path = 'every\t\n"kind"\\of.md';
225 const patch = azureDevOpsFilePatch({
226 change: change({ path, oldPath: path }),
227 texts: texts("one\n", "two\n"),
228 });
229 const [header, oldLine, newLine] = patch.section.split("\n");
230​
231 expect(header).not.toContain("\t");
232 expect(unquoteGitPatchPath(oldLine?.slice(4) ?? "")).toBe(`a/${path}`);
233 expect(unquoteGitPatchPath(newLine?.slice(4) ?? "")).toBe(`b/${path}`);
234 });
235});
236​
237describe("a diff cursor", () => {
238 it("reads anything it did not write as no position at all", () => {
239 for (const raw of [undefined, null, "", "abc", "1", "0:4", "1:-2", "1:2:3"]) {
240 expect(parseAzureDevOpsDiffCursor(raw)).toBeNull();
241 }
242 });
243});