Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli(output): Add ability to export results to CSV #4912

Merged
merged 13 commits into from
Apr 9, 2018

Conversation

nourikhalass
Copy link
Contributor

These changes implement the ability to export results to an CSV file.
These changes address the idea proposed in #4325 and #4481.

Each row of the CSV file contains the information of one audit. Currently the following properties are exported:

  • Name of the category the audit belongs to
  • Name of the audit
  • Description of the audit
  • Score type used for the audit
  • Score value

These properties were selected arbitrarily and perhaps there are more / better ones to export. The implementation relies on type information about the result object that is passed to the Printer in lighthouse-cli. This information was lacking so it has been added in order to satisfy the typechecker. However, the added type information should probably be expanded further and used in other places to have better type safety in the complete code base.

nourikhalass and others added 7 commits March 30, 2018 21:59
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
These changes were made because the CSV generator requires type information in order to be 'correct'.
It is very likely that parts of the codebase need to be annotated with these typings to increase type correctness.

Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@nourikhalass
Copy link
Contributor Author

@mvanspaendonck, @anandkanav92, @ernstmul Y'all need to give Google the Good Ol' "I hereby give up my rights"!

@mvanspaendonck
Copy link
Contributor

The cla should be signed by me already, but this is still aall fine with me.

@anandkanav92
Copy link
Contributor

Fine by me 🤷🏼‍♂️

@ernstmul
Copy link
Contributor

ernstmul commented Apr 3, 2018

Fine by me as well!


// Possible TODO: tightly couple headers and row values
// This would make it easier to include new / other row values
const header = ['category', 'name', 'description', 'type', 'score'];
Copy link
Member

@paulirish paulirish Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're going to be changing 'description' to 'title' soon, so we might as well use 'title' in the csv now so we don't break you in a week or so. :)

@@ -35,6 +36,22 @@ describe('Printer', () => {
assert.ok(/<html lang="en"/gim.test(htmlOutput));
});

it('creates CSV for results', () => {
const mode = Printer.OutputMode.csv;
const path = './.test-file.csv';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'./.test-file.csv' => './.results-as-csv.csv'

return Printer
.write(sampleResults, mode, path)
.then(_ => csv(path, headers).catch(err => assert.fail(err)))
.then(_ => fs.unlinkSync(path));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we want this unlinkSync in an after block:

  after(() => {
      fs.unlinkSync(path)
    });

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doing this means you don't need that catch() anymore:

      .then(_ => csv(path, headers));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand that would require creating another describe(...). Is this what you had in mind?

describe('CSV report generation', () => {
    const mode = Printer.OutputMode.csv;
    const path = './.results-as-csv.csv';
    const headers = {
      category: '',
      name: '',
      title: '',
      type: '',
      score: 42,
    };

    before(() => {
      return Printer.write(sampleResults, mode, path);
    });

    it('generates valid CSV output', () => {
      return csv(path, headers);
    });

    after(() => {
      fs.unlinkSync(path);
    });
  });

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah true. i was hoping after could be scoped to that test. whoops.

okay in that case, nevermind. :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this working? assert.fail just re-throws the error and we never enter the unlinkSync block when there's a failure, no?

Maybe I'm just confused :)

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the contribution folks! yay teamwork! 🥇


// Map every audit to its corresponding category
// which can be queried when needed
const map = {};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's give this a more descriptive name auditIdToCategoryNameMap?

* - the score type that is used for the audit
* - the score value of the audit
*
* @param {!LH.Results} results
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we don't need the ! anymore

// The document describes how to deal with escaping commas and quotes etc.
const CRLF = '\r\n';
const separator = ',';
/** @param {!string} value @returns {string} */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same !

package.json Outdated
@@ -92,6 +92,7 @@
"chrome-devtools-frontend": "1.0.422034",
"chrome-launcher": "^0.10.2",
"configstore": "^3.1.1",
"csv-validator": "^0.0.3",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move this to devDependencies

Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
@paulirish
Copy link
Member

wfm. over to patrick.

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a few more small things but we're looking good!

return Printer
.write(sampleResults, mode, path)
.then(_ => csv(path, headers).catch(err => assert.fail(err)))
.then(_ => fs.unlinkSync(path));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this working? assert.fail just re-throws the error and we never enter the unlinkSync block when there's a failure, no?

Maybe I'm just confused :)

@@ -88,6 +88,19 @@ declare global {
artifacts?: Object;
initialUrl: string;
fetchedAt: string;
reportCategories: [ReportCategory];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want ReportCategory[], this is a type for an array of length one containing a ReportCategory :)

export interface ReportCategory {
name: string;
description: string;
audits: [ReportAudit];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

@@ -10,6 +10,7 @@ const Printer = require('../../printer.js');
const assert = require('assert');
const fs = require('fs');
const sampleResults = require('../../../lighthouse-core/test/results/sample_v2.json');
const csv = require('csv-validator');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe const csvValidator so it's obvious what it's up to down in the test function?

};
return Printer
.write(sampleResults, mode, path)
.then(_ => csv(path, headers).catch(err => assert.fail(err)))
Copy link
Member

@brendankenny brendankenny Apr 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since csv(path, headers) resolves with a json form of the CSV file, maybe also assert a row or two of the results, so it's not just checking their types?

// Possible TODO: tightly couple headers and row values
// This would make it easier to include new / other row values
const header = ['category', 'name', 'title', 'type', 'score'];
const table = Object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this could be turned into a single (nested) loop to skip making auditCategories and make this a bit simpler? Correct me if I'm wrong though :) Something like

const table = results.reportCategories.map(category => {
  return category.audits.map(catAudit => {
    const audit = results.audits[catAudit.id];
    return [category.name, audit.name, audit.description, audit.scoreDisplayMode, audit.score]
      .map(value => value.toString())
      .map(escape);
  });
})

then just need to smoosh before outputting

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These arrays are so small it really comes down to style, though.

Copy link
Contributor

@mvanspaendonck mvanspaendonck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patrickhulce Maybe this makes it more clear what is going on and I have verified that this works correctly:

it('creates CSV for results', () => {
  const mode = Printer.OutputMode.csv;
  const path = './.results-as-csv.csv';
  const headers = {
    category: '',
    name: '',
    title: '',
    type: '',
    score: 42,
  };
  return Printer
    .write(sampleResults, mode, path)
    .then(async () => {
      try {
        await csvValidator(path, headers);
      } catch (err) {
        assert.fail('CSV parser error:\n' + err.join('\n'));
      } finally {
        fs.unlinkSync(path);
      }
    });
});

@patrickhulce
Copy link
Collaborator

ah yeah async/await cleans this up a lot! let's do it 👍 :)

nourikhalass and others added 3 commits April 4, 2018 18:23
Co-authored-by: Michiel van Spaendonck <mvanspaendonck@users.noreply.github.com>
Co-authored-by: Kanav Anand <anandkanav92@users.noreply.github.com>
Co-authored-by: Ernst Mulders <ernstmul@users.noreply.github.com>
Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could probably async up the rest of the test too ;) but this LGTM!!

thanks again folks, nice work!! 👏 💯

off to you @brendankenny

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@brendankenny
Copy link
Member

appveyor errors should be fixed by #4933, so good to go. Thanks for the PR!

@brendankenny brendankenny merged commit a031e46 into GoogleChrome:master Apr 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants