Repository: paularmstrong/build-tracker
Pull request #95 (2019-09-08 01:04:51)

Add summary output option to comparator
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

Big tables of data are not always preferred from the comparator output, especially when you have a bunch of budgets set. The tables could provide too much noise and not get enough attention over time.

# Solution

Add a `toSummary()` method to the `Comparator` that outputs an array of strings for each failing budget — or a success message.

# TODO

- [ ] 🤓 Add & update tests (always try to _increase_ test coverage)
- [ ] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [ ] 📖 Update relevant documentation



Patch:
diff --git a/src/comparator/src/index.ts b/src/comparator/src/index.ts
index 4074b51..b40b678 100644
--- a/src/comparator/src/index.ts
+++ b/src/comparator/src/index.ts
@@ -5,7 +5,7 @@ import Build from '@build-tracker/build';
 import BuildDelta from './BuildDelta';
 import markdownTable from 'markdown-table';
 import { ArtifactBudgets, ArtifactFilters, Budget, BudgetLevel, BudgetResult, Group } from '@build-tracker/types';
-import { formatBytes, formatSha } from '@build-tracker/formatting';
+import { formatBudgetResult, formatBytes, formatSha } from '@build-tracker/formatting';
 
 export interface ArtifactSizes {
   [key: string]: number;
@@ -400,4 +400,36 @@ export default class BuildComparator {
 
     return [header, ...groups, ...rows].map(row => `${row.join(',')}`).join(`\r\n`);
   }
+
+  public toSummary(useEmoji: boolean = true): Array<string> {
+    const groupResults = this.matrixGroups.reduce((memo, row): Array<string> => {
+      row.forEach(cell => {
+        if (cell.type !== CellType.TOTAL_DELTA) {
+          return;
+        }
+        cell.failingBudgets.forEach(budget => {
+          memo.push(formatBudgetResult(budget, `Group "${row[0].text}"`, useEmoji));
+        });
+      });
+      return memo;
+    }, []);
+
+    const artifactResults = this.matrixArtifacts.reduce((memo, row): Array<string> => {
+      row.forEach(cell => {
+        if (cell.type !== CellType.DELTA) {
+          return;
+        }
+        cell.failingBudgets.forEach(budget => {
+          memo.push(formatBudgetResult(budget, row[0].text, useEmoji));
+        });
+      });
+      return memo;
+    }, []);
+
+    const output = [...groupResults, ...artifactResults].filter(Boolean);
+    if (output.length === 0) {
+      return [`${useEmoji ? '✅' : 'Success:'} No failing budgets`];
+    }
+    return output;
+  }
 }
diff --git a/src/formatting/src/index.ts b/src/formatting/src/index.ts
index 3086748..d240815 100644
--- a/src/formatting/src/index.ts
+++ b/src/formatting/src/index.ts
@@ -37,14 +37,19 @@ const levelToString = {
   [BudgetLevel.ERROR]: 'Error'
 };
 
-export function formatBudgetResult(budgetResult: BudgetResult, itemName: string): string {
+const levelToEmoji = {
+  [BudgetLevel.WARN]: '⚠️',
+  [BudgetLevel.ERROR]: '🚫'
+};
+
+export function formatBudgetResult(budgetResult: BudgetResult, itemName: string, useEmoji: boolean = false): string {
   const { actual, expected, level, type } = budgetResult;
   const actualFormatted = type === BudgetType.PERCENT_DELTA ? formatPercent(actual) : formatBytes(actual);
   const expectedFormatted = type === BudgetType.PERCENT_DELTA ? formatPercent(expected) : formatBytes(expected);
   const diffFormatted =
     type === BudgetType.PERCENT_DELTA ? formatPercent(actual - expected) : formatBytes(actual - expected);
 
-  const prefix = `${levelToString[level]}: "${itemName}"`;
+  const prefix = `${(useEmoji ? levelToEmoji : levelToString)[level]}: \`${itemName}\``;
 
   switch (type) {
     case BudgetType.DELTA:


Test patch:
diff --git a/src/comparator/src/__tests__/index.test.ts b/src/comparator/src/__tests__/index.test.ts
index e69b02a..fc1709d 100644
--- a/src/comparator/src/__tests__/index.test.ts
+++ b/src/comparator/src/__tests__/index.test.ts
@@ -536,4 +536,73 @@ describe('BuildComparator', () => {
       `);
     });
   });
+
+  describe('summary', () => {
+    test('returns a list of errors and warnings for artifacts', () => {
+      const comparator = new BuildComparator({
+        builds: [build1, build2],
+        artifactBudgets: {
+          churros: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 100, sizeKey: 'gzip' }],
+          tacos: [
+            { level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' },
+            { level: BudgetLevel.ERROR, type: BudgetType.SIZE, maximum: 30, sizeKey: 'gzip' }
+          ]
+        }
+      });
+      expect(comparator.toSummary().join('\n')).toMatchInlineSnapshot(`
+        "⚠️: \`churros\` failed budget size limit of 0.1 KiB by 0.02 KiB
+        ⚠️: \`tacos\` failed budget size limit of 0 KiB by 0.04 KiB
+        🚫: \`tacos\` failed budget size limit of 0.03 KiB by 0.01 KiB"
+      `);
+    });
+
+    test('returns a list of errors and warnings for groups', () => {
+      const comparator = new BuildComparator({
+        builds: [build1, build2],
+        budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
+        groups: [
+          {
+            artifactNames: ['churros', 'burritos'],
+            budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
+            name: 'warning'
+          },
+          {
+            artifactNames: ['churros', 'tacos'],
+            budgets: [{ level: BudgetLevel.ERROR, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
+            name: 'error'
+          }
+        ]
+      });
+      expect(comparator.toSummary().join('\n')).toMatchInlineSnapshot(`
+        "⚠️: \`Group \\"All\\"\` failed budget size limit of 0 KiB by 0.16 KiB
+        ⚠️: \`Group \\"warning\\"\` failed budget size limit of 0 KiB by 0.12 KiB
+        🚫: \`Group \\"error\\"\` failed budget size limit of 0 KiB by 0.16 KiB"
+      `);
+    });
+
+    test('can render budgets without emoji', () => {
+      const comparator = new BuildComparator({
+        builds: [build1, build2],
+        budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
+        artifactBudgets: {
+          tacos: [{ level: BudgetLevel.ERROR, type: BudgetType.SIZE, maximum: 30, sizeKey: 'gzip' }]
+        }
+      });
+
+      expect(comparator.toSummary(false).join('\n')).toMatchInlineSnapshot(`
+        "Warning: \`Group \\"All\\"\` failed budget size limit of 0 KiB by 0.16 KiB
+        Error: \`tacos\` failed budget size limit of 0.03 KiB by 0.01 KiB"
+      `);
+    });
+
+    test('returns success if no failing budgets', () => {
+      const comparator = new BuildComparator({ builds: [build1, build2] });
+      expect(comparator.toSummary().join('\n')).toMatchInlineSnapshot(`"✅ No failing budgets"`);
+    });
+
+    test('can render success without emoji', () => {
+      const comparator = new BuildComparator({ builds: [build1, build2] });
+      expect(comparator.toSummary(false).join('\n')).toMatchInlineSnapshot(`"Success: No failing budgets"`);
+    });
+  });
 });
diff --git a/src/formatting/src/__tests__/index.test.ts b/src/formatting/src/__tests__/index.test.ts
index 4d56ef5..31d4316 100644
--- a/src/formatting/src/__tests__/index.test.ts
+++ b/src/formatting/src/__tests__/index.test.ts
@@ -46,6 +46,23 @@ describe('formatSha', () => {
 
 describe('formatBudgetResult', () => {
   describe('error', () => {
+    test('can use emoji label', () => {
+      expect(
+        formatBudgetResult(
+          {
+            sizeKey: 'stat',
+            passing: false,
+            expected: 2048,
+            actual: 4096,
+            type: BudgetType.SIZE,
+            level: BudgetLevel.ERROR
+          },
+          'tacos',
+          true
+        )
+      ).toEqual('🚫: `tacos` failed budget size limit of 2 KiB by 2 KiB');
+    });
+
     test('size', () => {
       expect(
         formatBudgetResult(
@@ -59,7 +76,7 @@ describe('formatBudgetResult', () => {
           },
           'tacos'
         )
-      ).toEqual('Error: "tacos" failed budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('Error: `tacos` failed budget size limit of 2 KiB by 2 KiB');
     });
 
     test('delta', () => {
@@ -76,7 +93,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Error: "tacos" failed budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
+        'Error: `tacos` failed budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
       );
     });
 
@@ -94,12 +111,29 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Error: "tacos" failed budget percent change limit. Expected no increase by no more than 10.000%, but increased by 20.000%'
+        'Error: `tacos` failed budget percent change limit. Expected no increase by no more than 10.000%, but increased by 20.000%'
       );
     });
   });
 
   describe('warning', () => {
+    test('can use emoji label', () => {
+      expect(
+        formatBudgetResult(
+          {
+            sizeKey: 'stat',
+            passing: false,
+            expected: 2048,
+            actual: 4096,
+            type: BudgetType.SIZE,
+            level: BudgetLevel.WARN
+          },
+          'tacos',
+          true
+        )
+      ).toEqual('⚠️: `tacos` failed budget size limit of 2 KiB by 2 KiB');
+    });
+
     test('size', () => {
       expect(
         formatBudgetResult(
@@ -113,7 +147,7 @@ describe('formatBudgetResult', () => {
           },
           'tacos'
         )
-      ).toEqual('Warning: "tacos" failed budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('Warning: `tacos` failed budget size limit of 2 KiB by 2 KiB');
     });
 
     test('delta', () => {
@@ -130,7 +164,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Warning: "tacos" failed budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
+        'Warning: `tacos` failed budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
       );
     });
 
@@ -148,7 +182,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Warning: "tacos" failed budget percent change limit. Expected no increase by no more than 10.000%, but increased by 20.000%'
+        'Warning: `tacos` failed budget percent change limit. Expected no increase by no more than 10.000%, but increased by 20.000%'
       );
     });
   });