Repository: paularmstrong/build-tracker
Pull request #209 (2020-10-20 19:40:50)

feat(comparator cli): allow comparisons even if size keys do not match
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

Switching from Node 10 to Node 12 produces an issue that suddenly `brotli` will be included in available `sizeKeys` for builds. This causes various things to throw errors because they're hardcoded to not allow a change in the `sizeKeys`

# Solution

This manual throwing seems unnecessary. By removing these checks, we can allow comparisons with the given keys, the result will just cause the comparison to show `0`s for the build missing that size key.

Also updated CI to node 12

# TODO

- [x] 🤓 Add & update tests (always try to _increase_ test coverage)
- [x] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [x] 📖 Update relevant documentation


github-actions[bot]: <!-- sentinel:build-tracker-action -->
⚠️: *Group "Web App"* failed the gzip budget size limit of 150 KiB by 3.55 KiB

<details><summary>View table</summary>

|         |    e00fac7 |    91137e2 |                   Δ1 |
| :------ | ---------: | ---------: | -------------------: |
| All     | 477.24 KiB | 476.81 KiB |    -0.42 KiB (-0.1%) |
| Web App | 153.69 KiB | 153.55 KiB | ⚠️ -0.14 KiB (-0.1%) |

</details>

View on [<img src="https://buildtracker.dev/img/favicon.png" alt="" width="20" height="20" /> Build Tracker](https://build-tracker-demo.herokuapp.com/builds/e00fac7b1e14a7b3a58e826770d4962e68c16a4b/91137e232fd0244466040f779ee4ae0ffa51f063?comparedRevisions=e00fac7b1e14a7b3a58e826770d4962e68c16a4b&comparedRevisions=91137e232fd0244466040f779ee4ae0ffa51f063)
PranaytheSingh: Nice!

Patch:
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index aa9cf41..fef91d0 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -25,7 +25,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
@@ -48,7 +48,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
@@ -73,7 +73,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
@@ -96,7 +96,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index fbd99c4..9137d29 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -13,7 +13,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
@@ -41,7 +41,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
@@ -68,7 +68,7 @@ jobs:
       - uses: actions/checkout@master
       - uses: actions/setup-node@master
         with:
-          node-version: 10.x
+          node-version: 12.x
       - run: npm install -g yarn
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
diff --git a/plugins/with-mariadb/src/setup.ts b/plugins/with-mariadb/src/setup.ts
index 03b5aec..7851e8c 100644
--- a/plugins/with-mariadb/src/setup.ts
+++ b/plugins/with-mariadb/src/setup.ts
@@ -4,13 +4,13 @@
 import { Pool } from 'mariadb';
 
 export default function setup(pool: Pool): () => Promise<boolean> {
-  const setup = async (): Promise<boolean> => {
+  return async function setup(): Promise<boolean> {
     const client = await pool.getConnection();
     try {
       await client.query(`
 CREATE TABLE IF NOT EXISTS builds(
   revision VARCHAR(64) PRIMARY KEY NOT NULL,
-  branch VARCHAR(64) NOT NULL,
+  branch VARCHAR(256) NOT NULL,
   parentRevision VARCHAR(64),
   timestamp INT NOT NULL,
   meta VARCHAR(1024),
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS builds(
 )`);
       await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)');
       await client.query('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
+      await client.query('ALTER TABLE builds MODIFY branch VARCHAR(256)');
     } catch (err) {
       client.release();
       throw err;
@@ -26,7 +27,4 @@ CREATE TABLE IF NOT EXISTS builds(
     client.release();
     return Promise.resolve(true);
   };
-  return setup;
 }
-
-module.exports = setup;
diff --git a/plugins/with-mysql/src/setup.ts b/plugins/with-mysql/src/setup.ts
index a675e68..6b0b313 100644
--- a/plugins/with-mysql/src/setup.ts
+++ b/plugins/with-mysql/src/setup.ts
@@ -4,18 +4,20 @@
 import { Pool } from 'mysql';
 
 export default function setup(pool: Pool): () => Promise<boolean> {
-  const setup = (): Promise<boolean> => {
+  return async function setup(): Promise<boolean> {
     return new Promise((resolve) => {
       pool.getConnection((err, client) => {
         if (err) {
-          client.release();
+          if (client) {
+            client.release();
+          }
           throw err;
         }
         client.query(
           `
 CREATE TABLE IF NOT EXISTS builds(
   revision VARCHAR(64) PRIMARY KEY NOT NULL,
-  branch VARCHAR(64) NOT NULL,
+  branch VARCHAR(256) NOT NULL,
   parentRevision VARCHAR(64),
   timestamp INT NOT NULL,
   meta VARCHAR(1024),
@@ -37,8 +39,14 @@ CREATE TABLE IF NOT EXISTS builds(
                   client.release();
                   throw err;
                 }
-                client.release();
-                resolve(true);
+                client.query('ALTER TABLE builds MODIFY branch VARCHAR(256)', (err) => {
+                  if (err) {
+                    client.release();
+                    throw err;
+                  }
+                  client.release();
+                  resolve(true);
+                });
               });
             });
           }
@@ -46,8 +54,4 @@ CREATE TABLE IF NOT EXISTS builds(
       });
     });
   };
-
-  return setup;
 }
-
-module.exports = setup;
diff --git a/plugins/with-postgres/src/setup.ts b/plugins/with-postgres/src/setup.ts
index c691419..7e0fe77 100644
--- a/plugins/with-postgres/src/setup.ts
+++ b/plugins/with-postgres/src/setup.ts
@@ -4,12 +4,12 @@
 import { Pool } from 'pg';
 
 export default function setup(pool: Pool): () => Promise<boolean> {
-  const setup = async (): Promise<boolean> => {
+  return async function setup(): Promise<boolean> {
     const client = await pool.connect();
     try {
       await client.query(`CREATE TABLE IF NOT EXISTS builds(
   revision char(64) PRIMARY KEY NOT NULL,
-  branch char(64) NOT NULL,
+  branch char(256) NOT NULL,
   parentRevision char(64),
   timestamp int NOT NULL,
   meta jsonb NOT NULL,
@@ -17,6 +17,7 @@ export default function setup(pool: Pool): () => Promise<boolean> {
 )`);
       await client.query('CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)');
       await client.query('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
+      await client.query('ALTER TABLE builds ALTER COLUMN branch TYPE VARCHAR(256)');
     } catch (err) {
       client.release();
       throw err;
@@ -24,7 +25,4 @@ export default function setup(pool: Pool): () => Promise<boolean> {
     client.release();
     return Promise.resolve(true);
   };
-  return setup;
 }
-
-module.exports = setup;
diff --git a/src/comparator/src/BuildDelta.ts b/src/comparator/src/BuildDelta.ts
index e2c0a53..891a99f 100644
--- a/src/comparator/src/BuildDelta.ts
+++ b/src/comparator/src/BuildDelta.ts
@@ -50,12 +50,6 @@ export default class BuildDelta<M extends BuildMeta = BuildMeta> {
       this._prevBuild.artifactSizes.forEach((key) => {
         this._sizeKeys.add(key);
       });
-      if (
-        this._sizeKeys.size !== this._baseBuild.artifactSizes.length ||
-        this._sizeKeys.size !== this._prevBuild.artifactSizes.length
-      ) {
-        throw new Error('Size keys do not match between builds');
-      }
     }
 
     return Array.from(this._sizeKeys);
diff --git a/src/comparator/src/index.ts b/src/comparator/src/index.ts
index 3da2973..060d8a2 100644
--- a/src/comparator/src/index.ts
+++ b/src/comparator/src/index.ts
@@ -205,7 +205,7 @@ export default class BuildComparator {
         if (buildA.getMetaValue('parentRevision') === buildB.getMetaValue('revision')) {
           return 1;
         }
-        if (buildB.getMetaValue('revision') === buildA.getMetaValue('parentRevision')) {
+        if (buildA.getMetaValue('revision') === buildB.getMetaValue('parentRevision')) {
           return -1;
         }
         return 0;
@@ -286,10 +286,6 @@ export default class BuildComparator {
           });
         });
       });
-
-      if (allSizeKeys.size !== this._sizeKeys.length || !this._sizeKeys.every((key) => allSizeKeys.has(key))) {
-        throw new Error('builds provided do not have same size keys for artifacts');
-      }
     }
     return this._sizeKeys;
   }
diff --git a/src/server/src/commands/seed.ts b/src/server/src/commands/seed.ts
index fa0964a..db7471d 100644
--- a/src/server/src/commands/seed.ts
+++ b/src/server/src/commands/seed.ts
@@ -31,10 +31,12 @@ export const handler = async (args: Args): Promise<void> => {
 
   await config.setup();
 
-  glob.sync(`${path.join(fixturePath, 'builds')}/*.json`).forEach(async (build) => {
+  const builds = glob.sync(`${path.join(fixturePath, 'builds-medium')}/*.json`);
+
+  for (const build of builds) {
     const buildData = require(build);
     await config.queries.build.insert(new Build(buildData.meta, buildData.artifacts));
-  });
+  }
 
   return Promise.resolve();
 };


Test patch:
diff --git a/plugins/with-mariadb/src/__tests__/setup.test.ts b/plugins/with-mariadb/src/__tests__/setup.test.ts
index bbb1678..db39295 100644
--- a/plugins/with-mariadb/src/__tests__/setup.test.ts
+++ b/plugins/with-mariadb/src/__tests__/setup.test.ts
@@ -35,6 +35,11 @@ describe('withMariadb setup', () => {
     expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
   });
 
+  test('alters the branch column to length 256', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith('ALTER TABLE builds MODIFY branch VARCHAR(256)');
+  });
+
   test('releases the client on complete', async () => {
     await expect(setupFn()).resolves.toBe(true);
     expect(release).toHaveBeenCalled();
diff --git a/plugins/with-mysql/src/__tests__/setup.test.ts b/plugins/with-mysql/src/__tests__/setup.test.ts
index 60586a0..2e3ed2d 100644
--- a/plugins/with-mysql/src/__tests__/setup.test.ts
+++ b/plugins/with-mysql/src/__tests__/setup.test.ts
@@ -43,6 +43,11 @@ describe('withMysql setup', () => {
     expect(query).toHaveBeenCalledWith('ALTER TABLE builds ADD INDEX (timestamp)', expect.any(Function));
   });
 
+  test('alters the branch column to length 256', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith('ALTER TABLE builds MODIFY branch VARCHAR(256)', expect.any(Function));
+  });
+
   test('releases the client on complete', async () => {
     await expect(setupFn()).resolves.toBe(true);
     expect(release).toHaveBeenCalled();
diff --git a/plugins/with-postgres/src/__tests__/setup.test.ts b/plugins/with-postgres/src/__tests__/setup.test.ts
index c5fb2ff..efcfb47 100644
--- a/plugins/with-postgres/src/__tests__/setup.test.ts
+++ b/plugins/with-postgres/src/__tests__/setup.test.ts
@@ -19,39 +19,37 @@ describe('withPostgres', () => {
     setupFn = setup(new Pool());
   });
 
-  test('creates the table if not exists', () => {
-    return setupFn().then((result) => {
-      expect(result).toBe(true);
-      expect(query).toHaveBeenCalledWith(expect.stringMatching('CREATE TABLE IF NOT EXISTS builds'));
-    });
+  test('creates the table if not exists', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith(expect.stringMatching('CREATE TABLE IF NOT EXISTS builds'));
   });
 
-  test('creates a multi-index', () => {
-    return setupFn().then(() => {
-      expect(query).toHaveBeenCalledWith(
-        'CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)'
-      );
-    });
+  test('creates a multi-index', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith(
+      'CREATE INDEX IF NOT EXISTS parent ON builds (revision, parentRevision, branch)'
+    );
   });
 
-  test('creates an index on timestamp', () => {
-    return setupFn().then(() => {
-      expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
-    });
+  test('creates an index on timestamp', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith('CREATE INDEX IF NOT EXISTS timestamp ON builds (timestamp)');
   });
 
-  test('releases the client on complete', () => {
-    return setupFn().then(() => {
-      expect(release).toHaveBeenCalled();
-    });
+  test('alters the branch column to length 256', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(query).toHaveBeenCalledWith('ALTER TABLE builds ALTER COLUMN branch TYPE VARCHAR(256)');
   });
 
-  test('releases the client on error', () => {
+  test('releases the client on complete', async () => {
+    await expect(setupFn()).resolves.toBe(true);
+    expect(release).toHaveBeenCalled();
+  });
+
+  test('releases the client on error', async () => {
     const error = new Error('tacos');
     query.mockReturnValueOnce(Promise.reject(error));
-    return setupFn().catch((err) => {
-      expect(release).toHaveBeenCalled();
-      expect(err).toBe(error);
-    });
+    await expect(setupFn()).rejects.toEqual(error);
+    expect(release).toHaveBeenCalled();
   });
 });
diff --git a/src/cli/src/commands/__tests__/create-build.test.ts b/src/cli/src/commands/__tests__/create-build.test.ts
index df4d719..8d18215 100644
--- a/src/cli/src/commands/__tests__/create-build.test.ts
+++ b/src/cli/src/commands/__tests__/create-build.test.ts
@@ -47,7 +47,7 @@ describe('create-build', () => {
 
     test('writes the artifact stats to stdout', async () => {
       await expect(Command.handler({ config, out: true, 'skip-dirty-check': true })).resolves.toBeUndefined();
-      expect(writeSpy).toHaveBeenCalledWith(expect.stringMatching('"parentRevision": "7654321",'));
+      expect(writeSpy).toHaveBeenCalledWith(expect.stringMatching('"parentRevision": "'));
     });
 
     test('converts JSON string-encoded metadata', async () => {
diff --git a/src/comparator/src/__tests__/BuildDelta.test.ts b/src/comparator/src/__tests__/BuildDelta.test.ts
index 2db31f7..455431d 100644
--- a/src/comparator/src/__tests__/BuildDelta.test.ts
+++ b/src/comparator/src/__tests__/BuildDelta.test.ts
@@ -57,22 +57,6 @@ describe('BuildDelta', () => {
       const bd = new BuildDelta(buildA, buildB);
       expect(bd.artifactSizes).toEqual(['stat', 'gzip']);
     });
-
-    test('throws an error if builds do not have same artifact size keys', () => {
-      const bd = new BuildDelta(
-        buildA,
-        new Build(
-          {
-            branch: 'master',
-            revision: { value: '123', url: 'https://build-tracker.local' },
-            parentRevision: 'abc',
-            timestamp: Date.now(),
-          },
-          [{ name: 'tacos', hash: 'abc', sizes: {} }]
-        )
-      );
-      expect(() => bd.artifactSizes).toThrow();
-    });
   });
 
   describe('artifactNames', () => {
diff --git a/src/comparator/src/__tests__/index.test.ts b/src/comparator/src/__tests__/index.test.ts
index 95f536f..d5b77ce 100644
--- a/src/comparator/src/__tests__/index.test.ts
+++ b/src/comparator/src/__tests__/index.test.ts
@@ -65,8 +65,15 @@ describe('BuildComparator', () => {
 
   describe('builds', () => {
     test('sorts by timestamp, then parentRevision', () => {
+      const build2 = new Build(
+        { branch: 'master', revision: '8901234abcdef', parentRevision: 'abcdef1', timestamp: 8901234 },
+        [
+          { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 43 } },
+          { name: 'churros~burritos~tacos', hash: 'def', sizes: { stat: 469, gzip: 120 } },
+        ]
+      );
       const build3 = new Build(
-        { branch: 'master', revision: 'abcdef', parentRevision: build1.getMetaValue('revision'), timestamp: 1234566 },
+        { branch: 'master', revision: 'abcdef1', parentRevision: build1.getMetaValue('revision'), timestamp: 1234566 },
         [
           { name: 'burritos', hash: 'abc', sizes: { stat: 456, gzip: 90 } },
           { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 45 } },
@@ -76,6 +83,16 @@ describe('BuildComparator', () => {
       expect(comparator.builds[0]).toBe(build1);
       expect(comparator.builds[1]).toBe(build3);
       expect(comparator.builds[2]).toBe(build2);
+
+      const comparatorRev = new BuildComparator({ builds: [build1, build2, build3] });
+      expect(comparatorRev.builds[0]).toBe(build1);
+      expect(comparatorRev.builds[1]).toBe(build3);
+      expect(comparatorRev.builds[2]).toBe(build2);
+
+      const comparatorRev2 = new BuildComparator({ builds: [build3, build1, build2] });
+      expect(comparatorRev2.builds[0]).toBe(build1);
+      expect(comparatorRev2.builds[1]).toBe(build3);
+      expect(comparatorRev2.builds[2]).toBe(build2);
     });
   });
 
@@ -134,14 +151,6 @@ describe('BuildComparator', () => {
       expect(comparator.sizeKeys).toEqual(['gzip', 'stat']);
     });
 
-    test('throws an error if some builds have size keys that others do not', () => {
-      expect(() => {
-        new BuildComparator({
-          builds: [new Build(build1.meta, [{ name: 'tacos', hash: 'abc', sizes: { tacos: 123 } }]), build2],
-        });
-      }).toThrowErrorMatchingInlineSnapshot(`"builds provided do not have same size keys for artifacts"`);
-    });
-
     test('returns an empty array if there are no builds', () => {
       const comparator = new BuildComparator({ builds: [] });
       expect(comparator.sizeKeys).toEqual([]);
@@ -372,10 +381,23 @@ describe('BuildComparator', () => {
         | All                      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
         | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
         | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+        | tacos                    | 0.04 KiB | 0.04 KiB |      -0 KiB (-4.4%) |"
       `);
     });
 
+    test('Allows comparing even when sizeKeys are not present', () => {
+      const comparator = new BuildComparator({
+        builds: [new Build(build1.meta, [{ name: 'tacos', hash: 'abc', sizes: { tacos: 123 } }]), build2],
+      });
+      expect(comparator.toMarkdown({ sizeKey: 'tacos' })).toMatchInlineSnapshot(`
+"|                          |  1234567 | 8901234 |           Δ1 |
+| :----------------------- | -------: | ------: | -----------: |
+| All                      | 0.12 KiB |   0 KiB | 0 KiB (0.0%) |
+| tacos                    | 0.12 KiB |   0 KiB | 0 KiB (0.0%) |
+| churros\\\\~burritos\\\\~tacos |    0 KiB |   0 KiB | 0 KiB (0.0%) |"
+`);
+    });
+
     test('handles 0 artifacts', () => {
       const comparator = new BuildComparator({ builds: [build1, build1b] });
       expect(comparator.toMarkdown({ artifactFilter: () => false })).toMatchInlineSnapshot(`
@@ -419,10 +441,10 @@ describe('BuildComparator', () => {
     test('does not include filtered artifacts', () => {
       const comparator = new BuildComparator({ builds: [build1, build2], artifactFilters });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|       |  1234567 |  8901234 |            Δ1 |
-        | :---- | -------: | -------: | ------------: |
-        | All   | 0.04 KiB | 0.04 KiB | 0 KiB (-4.4%) |
-        | tacos | 0.04 KiB | 0.04 KiB | 0 KiB (-4.4%) |"
+        "|       |  1234567 |  8901234 |             Δ1 |
+        | :---- | -------: | -------: | -------------: |
+        | All   | 0.04 KiB | 0.04 KiB | -0 KiB (-4.4%) |
+        | tacos | 0.04 KiB | 0.04 KiB | -0 KiB (-4.4%) |"
       `);
     });
 
@@ -463,7 +485,7 @@ describe('BuildComparator', () => {
         | All                      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
         | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
         | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos                    | 0.04 KiB | 0.04 KiB |    🚨 0 KiB (-4.4%) |"
+        | tacos                    | 0.04 KiB | 0.04 KiB |   🚨 -0 KiB (-4.4%) |"
       `);
     });
 
@@ -481,7 +503,7 @@ describe('BuildComparator', () => {
         | All                      | 0.13 KiB | 0.16 KiB | 🚨 0.03 KiB (20.7%) |
         | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
         | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+        | tacos                    | 0.04 KiB | 0.04 KiB |      -0 KiB (-4.4%) |"
       `);
     });
 
@@ -496,7 +518,7 @@ describe('BuildComparator', () => {
 | All                      | 0.13 KiB | 0.16 KiB | ⚠️ 0.03 KiB (20.7%) |
 | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
 | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-| tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+| tacos                    | 0.04 KiB | 0.04 KiB |      -0 KiB (-4.4%) |"
 `);
     });
 
@@ -537,7 +559,7 @@ describe('BuildComparator', () => {
 | error                    | 0.04 KiB | 0.16 KiB | 🚨 0.12 KiB (262.2%) |
 | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |    0.12 KiB (100.0%) |
 | burritos                 | 0.09 KiB |    0 KiB |  -0.09 KiB (-100.0%) |
-| tacos                    | 0.04 KiB | 0.04 KiB |        0 KiB (-4.4%) |"
+| tacos                    | 0.04 KiB | 0.04 KiB |       -0 KiB (-4.4%) |"
 `);
     });
   });
@@ -546,12 +568,12 @@ describe('BuildComparator', () => {
     test('gets a CSV formatted table', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
       expect(comparator.toCsv()).toMatchInlineSnapshot(`
-        ",1234567,8901234,Δ1
-        All,0.13 KiB,0.16 KiB,0.03 KiB (20.7%)
-        churros~burritos~tacos,0 KiB,0.12 KiB,0.12 KiB (100.0%)
-        burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)
-        tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
-      `);
+",1234567,8901234,Δ1
+All,0.13 KiB,0.16 KiB,0.03 KiB (20.7%)
+churros~burritos~tacos,0 KiB,0.12 KiB,0.12 KiB (100.0%)
+burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)
+tacos,0.04 KiB,0.04 KiB,-0 KiB (-4.4%)"
+`);
     });
 
     test('can specify a different size key', () => {
@@ -587,8 +609,8 @@ burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)"
       const comparator = new BuildComparator({ builds: [build1, build2], artifactFilters });
       expect(comparator.toCsv()).toMatchInlineSnapshot(`
 ",1234567,8901234,Δ1
-All,0.04 KiB,0.04 KiB,0 KiB (-4.4%)
-tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
+All,0.04 KiB,0.04 KiB,-0 KiB (-4.4%)
+tacos,0.04 KiB,0.04 KiB,-0 KiB (-4.4%)"
 `);
     });