Skip to content

Commit 321183b

Browse files
author
Armando Aguirre
authored
Merge pull request #110 from armanio123/ReturnGroupErrors
Return group errors
2 parents eae0447 + 1dfdb88 commit 321183b

File tree

3 files changed

+227
-74
lines changed

3 files changed

+227
-74
lines changed

src/main.ts

Lines changed: 204 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import fs = require("fs");
99
import path = require("path");
1010
import mdEscape = require("markdown-escape");
1111
import randomSeed = require("random-seed");
12+
import { getErrorMessageFromStack, getHash, getHashForStack } from "./utils/hashStackTrace";
1213

1314
interface Params {
1415
/**
@@ -84,9 +85,31 @@ export type RepoStatus =
8485
| "Detected no interesting changes"
8586
;
8687

88+
interface TSServerResult {
89+
oldServerFailed: boolean;
90+
oldSpawnResult?: SpawnResult;
91+
newServerFailed: boolean;
92+
newSpawnResult: SpawnResult;
93+
replayScriptPath: string;
94+
installCommands: ip.InstallCommand[];
95+
}
96+
97+
interface Summary {
98+
tsServerResult: TSServerResult;
99+
repo: git.Repo;
100+
oldTsEntrypointPath: string;
101+
rawErrorArtifactPath: string;
102+
replayScript: string;
103+
downloadDir: string;
104+
replayScriptArtifactPath: string;
105+
replayScriptName: string;
106+
commit?: string;
107+
}
108+
87109
interface RepoResult {
88110
readonly status: RepoStatus;
89111
readonly summary?: string;
112+
readonly tsServerResult?: TSServerResult;
90113
readonly replayScriptPath?: string;
91114
readonly rawErrorPath?: string;
92115
}
@@ -203,8 +226,6 @@ async function getTsServerRepoResult(
203226
? (await installPackagesAndGetCommands(repo, downloadDir, repoDir, monorepoPackages, /*cleanOnFailure*/ true, diagnosticOutput))!
204227
: [];
205228

206-
const isUserTestRepo = !repo.url;
207-
208229
const replayScriptName = path.basename(replayScriptArtifactPath);
209230
const replayScriptPath = path.join(downloadDir, replayScriptName);
210231

@@ -302,110 +323,169 @@ async function getTsServerRepoResult(
302323
}
303324
}
304325

305-
const owner = repo.owner ? `${mdEscape(repo.owner)}/` : "";
306-
const url = repo.url ? `(${repo.url})` : "";
326+
const tsServerResult = {
327+
oldServerFailed,
328+
oldSpawnResult,
329+
newServerFailed,
330+
newSpawnResult,
331+
replayScriptPath,
332+
installCommands,
333+
};
307334

308-
let summary = `## [${owner}${mdEscape(repo.name)}]${url}\n`;
335+
if (oldServerFailed && !newServerFailed) {
336+
return { status: "Detected interesting changes", tsServerResult }
337+
}
338+
if (!newServerFailed) {
339+
return { status: "Detected no interesting changes" };
340+
}
309341

310-
if (oldServerFailed) {
311-
const oldServerError = oldSpawnResult?.stdout
312-
? prettyPrintServerHarnessOutput(oldSpawnResult.stdout, /*filter*/ true)
342+
return { status: "Detected interesting changes", tsServerResult, replayScriptPath, rawErrorPath };
343+
}
344+
catch (err) {
345+
reportError(err, `Error running tsserver on ${repo.url ?? repo.name}`);
346+
return { status: "Unknown failure" };
347+
}
348+
finally {
349+
console.log(`Done ${repo.url ?? repo.name}`);
350+
logStepTime(diagnosticOutput, repo, "language service", lsStart);
351+
}
352+
}
353+
354+
function groupErrors(summaries: Summary[]) {
355+
const groupedOldErrors = new Map<string, Summary[]>();
356+
const groupedNewErrors = new Map<string, Summary[]>();
357+
let group: Map<string, Summary[]>;
358+
let error: ServerHarnessOutput | string;
359+
for (const summary of summaries) {
360+
if (summary.tsServerResult.newServerFailed) {
361+
// Group new errors
362+
error = parseServerHarnessOutput(summary.tsServerResult.newSpawnResult!.stdout);
363+
group = groupedNewErrors;
364+
}
365+
else if (summary.tsServerResult.oldServerFailed) {
366+
// Group old errors
367+
const { oldSpawnResult } = summary.tsServerResult;
368+
error = oldSpawnResult?.stdout
369+
? parseServerHarnessOutput(oldSpawnResult.stdout)
313370
: `Timed out after ${executionTimeout} ms`;
314-
summary += `
371+
372+
group = groupedOldErrors;
373+
}
374+
else {
375+
continue;
376+
}
377+
378+
const key = typeof error === "string" ? getHash([error]) : getHashForStack(error.message);
379+
const value = group.get(key) ?? [];
380+
value.push(summary);
381+
group.set(key, value);
382+
}
383+
384+
return { groupedOldErrors, groupedNewErrors }
385+
}
386+
387+
function getErrorMessage(output: string): string {
388+
const error = parseServerHarnessOutput(output);
389+
390+
return typeof error === "string" ? error : getErrorMessageFromStack(error.message);
391+
}
392+
393+
function createOldErrorSummary(summaries: Summary[]): string {
394+
const { oldSpawnResult } = summaries[0].tsServerResult;
395+
396+
const oldServerError = oldSpawnResult?.stdout
397+
? prettyPrintServerHarnessOutput(oldSpawnResult.stdout, /*filter*/ true)
398+
: `Timed out after ${executionTimeout} ms`;
399+
400+
const errorMessage = oldSpawnResult?.stdout ? getErrorMessage(oldSpawnResult.stdout) : oldServerError;
401+
402+
let text = `
315403
<details>
316-
<summary>:warning: Note that ${path.basename(path.dirname(path.dirname(oldTsServerPath)))} had errors :warning:</summary>
404+
<summary>${errorMessage}</summary>
317405
318406
\`\`\`
319407
${oldServerError}
320408
\`\`\`
321409
322-
</details>
323-
410+
<h4>Repos no longer reporting the error</h4>
411+
<ul>
324412
`;
325-
if (!newServerFailed) {
326-
summary += `
327-
:tada: New server no longer has errors :tada:
413+
414+
for (const summary of summaries) {
415+
const owner = summary.repo.owner ? `${mdEscape(summary.repo.owner)}/` : "";
416+
const url = summary.repo.url ?? "";
417+
418+
text += `<li><a href="${url}">${owner + mdEscape(summary.repo.name)}</a></li>\n`
419+
}
420+
421+
text += `
422+
</ul>
423+
</details>
328424
`;
329-
return { status: "Detected interesting changes", summary }
330-
}
331-
}
332-
333-
if (!newServerFailed) {
334-
return { status: "Detected no interesting changes" };
335-
}
336425

337-
summary += `
426+
return text;
427+
}
428+
429+
async function createNewErrorSummaryAsync(summaries: Summary[]): Promise<string> {
430+
let text = `<h2>${getErrorMessage(summaries[0].tsServerResult.newSpawnResult.stdout)}</h2>
338431
339432
\`\`\`
340-
${prettyPrintServerHarnessOutput(newSpawnResult.stdout, /*filter*/ true)}
433+
${prettyPrintServerHarnessOutput(summaries[0].tsServerResult.newSpawnResult.stdout, /*filter*/ true)}
341434
\`\`\`
342-
That is a filtered view of the text. To see the raw error text, go to ${rawErrorArtifactPath}</code> in the <a href="${artifactFolderUrlPlaceholder}">artifact folder</a></li>\n
343-
`;
344435
345-
summary += `
436+
<h4>Affected repos</h4>`;
437+
438+
for (const summary of summaries) {
439+
const owner = summary.repo.owner ? `${mdEscape(summary.repo.owner)}/` : "";
440+
const url = summary.repo.url ?? "";
441+
442+
text += `
346443
<details>
347-
<summary><h3>Last few requests</h3></summary>
444+
<summary><a href="${url}">${owner + mdEscape(summary.repo.name)}</a></summary>
445+
Raw error text: <code>${summary.rawErrorArtifactPath}</code> in the <a href="${artifactFolderUrlPlaceholder}">artifact folder</a>
446+
<h4>Last few requests</h4>
348447
349448
\`\`\`json
350-
${fs.readFileSync(replayScriptPath, { encoding: "utf-8" }).split(/\r?\n/).slice(-5).join("\n")}
449+
${summary.replayScript}
351450
\`\`\`
352451
353-
</details>
354-
355-
`;
356-
357-
// Markdown doesn't seem to support a <details> list item, so this chunk is in HTML
358-
359-
summary += `<details>
360-
<summary><h3>Repro Steps</h3></summary>
452+
<h4>Repro steps</h4>
361453
<ol>
362454
`;
363-
if (isUserTestRepo) {
364-
summary += `<li>Download user test <code>${repo.name}</code></li>\n`;
455+
// No url means is user test repo
456+
if (!summary.repo.url) {
457+
text += `<li>Download user test <code>${summary.repo.name}</code></li>\n`;
365458
}
366459
else {
367-
summary += `<li><code>git clone ${repo.url!} --recurse-submodules</code></li>\n`;
460+
text += `<li><code>git clone ${summary.repo.url} --recurse-submodules</code></li>\n`;
368461

369-
try {
370-
console.log("Extracting commit SHA for repro steps");
371-
const commit = (await execAsync(repoDir, `git rev-parse @`)).trim();
372-
summary += `<li>In dir <code>${repo.name}</code>, run <code>git reset --hard ${commit}</code></li>\n`;
373-
}
374-
catch {
462+
if (summary.commit) {
463+
text += `<li>In dir <code>${summary.repo.name}</code>, run <code>git reset --hard ${summary.commit}</code></li>\n`;
375464
}
376465
}
377466

378-
if (installCommands.length > 1) {
379-
summary += "<li><details><summary>Install packages (exact steps are below, but it might be easier to follow the repo readme)</summary><ol>\n";
467+
if (summary.tsServerResult.installCommands.length > 1) {
468+
text += "<li><details><summary>Install packages (exact steps are below, but it might be easier to follow the repo readme)</summary><ol>\n";
380469
}
381-
for (const command of installCommands) {
382-
summary += ` <li>In dir <code>${path.relative(downloadDir, command.directory)}</code>, run <code>${command.tool} ${command.arguments.join(" ")}</code></li>\n`;
470+
for (const command of summary.tsServerResult.installCommands) {
471+
text += ` <li>In dir <code>${path.relative(summary.downloadDir, command.directory)}</code>, run <code>${command.tool} ${command.arguments.join(" ")}</code></li>\n`;
383472
}
384-
if (installCommands.length > 1) {
385-
summary += "</ol></details>\n";
473+
if (summary.tsServerResult.installCommands.length > 1) {
474+
text += "</ol></details>\n";
386475
}
387476

388477
// The URL of the artifact can be determined via AzDO REST APIs, but not until after the artifact is published
389-
summary += `<li>Back in the initial folder, download <code>${replayScriptArtifactPath}</code> from the <a href="${artifactFolderUrlPlaceholder}">artifact folder</a></li>\n`;
390-
summary += `<li><code>npm install --no-save @typescript/server-replay</code></li>\n`;
391-
summary += `<li><code>npx tsreplay ./${repo.name} ./${replayScriptName} path/to/tsserver.js</code></li>\n`;
392-
summary += `<li><code>npx tsreplay --help</code> to learn about helpful switches for debugging, logging, etc</li>\n`;
478+
text += `<li>Back in the initial folder, download <code>${summary.replayScriptArtifactPath}</code> from the <a href="${artifactFolderUrlPlaceholder}">artifact folder</a></li>\n`;
479+
text += `<li><code>npm install --no-save @typescript/server-replay</code></li>\n`;
480+
text += `<li><code>npx tsreplay ./${summary.repo.name} ./${summary.replayScriptName} path/to/tsserver.js</code></li>\n`;
481+
text += `<li><code>npx tsreplay --help</code> to learn about helpful switches for debugging, logging, etc</li>\n`;
393482

394-
summary += `</ol>
483+
text += `</ol>
395484
</details>
396-
397485
`;
398-
399-
return { status: "Detected interesting changes", summary, replayScriptPath, rawErrorPath };
400-
}
401-
catch (err) {
402-
reportError(err, `Error running tsserver on ${repo.url ?? repo.name}`);
403-
return { status: "Unknown failure" };
404-
}
405-
finally {
406-
console.log(`Done ${repo.url ?? repo.name}`);
407-
logStepTime(diagnosticOutput, repo, "language service", lsStart);
408486
}
487+
488+
return text;
409489
}
410490

411491
// Exported for testing
@@ -658,6 +738,8 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
658738

659739
const isPr = params.testType === "user" && !!params.prNumber
660740

741+
var summaries: Summary[] = [];
742+
661743
let i = 1;
662744
for (const repo of repos) {
663745
console.log(`Starting #${i++} / ${repos.length}: ${repo.url ?? repo.name}`);
@@ -672,16 +754,48 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
672754
: repo.name;
673755
const replayScriptFileName = `${repoPrefix}.${replayScriptFileNameSuffix}`;
674756
const rawErrorFileName = `${repoPrefix}.${rawErrorFileNameSuffix}`;
675-
const { status, summary, replayScriptPath, rawErrorPath } = params.entrypoint === "tsc"
757+
758+
const rawErrorArtifactPath = path.join(params.resultDirName, rawErrorFileName);
759+
const replayScriptArtifactPath = path.join(params.resultDirName, replayScriptFileName);
760+
761+
const { status, summary, tsServerResult, replayScriptPath, rawErrorPath } = params.entrypoint === "tsc"
676762
? await getTscRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, params.buildWithNewWhenOldFails, downloadDir, diagnosticOutput)
677-
: await getTsServerRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, downloadDir, path.join(params.resultDirName, replayScriptFileName), path.join(params.resultDirName, rawErrorFileName), diagnosticOutput, isPr);
763+
: await getTsServerRepoResult(repo, userTestsDir, oldTsEntrypointPath, newTsEntrypointPath, downloadDir, replayScriptArtifactPath, rawErrorArtifactPath, diagnosticOutput, isPr);
678764
console.log(`Repo ${repo.url ?? repo.name} had status "${status}"`);
679765
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
680-
if (summary) {
681766

767+
if (summary) {
682768
const resultFileName = `${repoPrefix}.${resultFileNameSuffix}`;
683769
await fs.promises.writeFile(path.join(resultDirPath, resultFileName), summary, { encoding: "utf-8" });
770+
}
771+
772+
if (tsServerResult) {
773+
const replayScriptPath = path.join(downloadDir, path.basename(replayScriptArtifactPath));
774+
const repoDir = path.join(downloadDir, repo.name);
775+
776+
let commit: string | undefined;
777+
try {
778+
console.log("Extracting commit SHA for repro steps");
779+
commit = (await execAsync(repoDir, `git rev-parse @`)).trim()
780+
}
781+
catch {
782+
//noop
783+
}
684784

785+
summaries.push({
786+
tsServerResult,
787+
repo,
788+
oldTsEntrypointPath,
789+
rawErrorArtifactPath,
790+
replayScript: fs.readFileSync(replayScriptPath, { encoding: "utf-8" }).split(/\r?\n/).slice(-5).join("\n"),
791+
downloadDir,
792+
replayScriptArtifactPath,
793+
replayScriptName: path.basename(replayScriptArtifactPath),
794+
commit
795+
});
796+
}
797+
798+
if (summary || tsServerResult) {
685799
// In practice, there will only be a replay script when the entrypoint is tsserver
686800
// There can be replay steps without a summary, but then they're not interesting
687801
if (replayScriptPath) {
@@ -690,9 +804,7 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
690804
if (rawErrorPath) {
691805
await fs.promises.copyFile(rawErrorPath, path.join(resultDirPath, rawErrorFileName));
692806
}
693-
694807
}
695-
696808
}
697809
finally {
698810
// Throw away the repo so we don't run out of space
@@ -727,6 +839,25 @@ export async function mainAsync(params: GitParams | UserParams): Promise<void> {
727839
}
728840
}
729841

842+
// Group errors and create summary files.
843+
if (summaries.length > 0) {
844+
const { groupedOldErrors, groupedNewErrors } = groupErrors(summaries);
845+
846+
for (let [key, value] of groupedOldErrors) {
847+
const summary = createOldErrorSummary(value);
848+
const resultFileName = `!${key}.${resultFileNameSuffix}`; // Exclamation point makes the file to be put first when ordering.
849+
850+
await fs.promises.writeFile(path.join(resultDirPath, resultFileName), summary, { encoding: "utf-8" });
851+
}
852+
853+
for (let [key, value] of groupedNewErrors) {
854+
const summary = await createNewErrorSummaryAsync(value);
855+
const resultFileName = `${key}.${resultFileNameSuffix}`;
856+
857+
await fs.promises.writeFile(path.join(resultDirPath, resultFileName), summary, { encoding: "utf-8" });
858+
}
859+
}
860+
730861
if (params.tmpfs) {
731862
await execAsync(processCwd, "sudo rm -rf " + downloadDir);
732863
await execAsync(processCwd, "sudo rm -rf " + oldTscDirPath);

0 commit comments

Comments
 (0)