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

Async/await refactoring for files in io folder except directory.js #1697

Merged
merged 3 commits into from
Jan 3, 2018
Merged

Async/await refactoring for files in io folder except directory.js #1697

merged 3 commits into from
Jan 3, 2018

Conversation

hameleonka
Copy link
Contributor

@rpl in this PR I refactored files async/await syntax in io folder excluding directory.js (currently in work by another contributor). Please check it out, waiting for your review.

@asamuzaK
Copy link
Contributor

node.js v6 does not support async/await.
Is there a decision somewhere that linter is going to drop v6 support?

@EnTeQuAk
Copy link
Contributor

@asamuzaK no, we won't drop support for v6 anytime soon, at least nothing like that is planned at this moment. This is a follow-up for #1692 which allows our babel stack to transpile async/await so that we should stay compatible with node6.

@asamuzaK
Copy link
Contributor

Got it, thanks.

@rpl rpl self-requested a review December 19, 2017 13:34
src/io/xpi.js Outdated
@@ -33,7 +33,7 @@ export class Xpi extends IOBase {
});
}

handleEntry(entry, reject) {
handleEntry(entry) {
Copy link
Member

Choose a reason for hiding this comment

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

This method still need to get the reject parameter from its callers and to use it to reject the error at line 46 (the DuplicateZipEntry) instead of throwing it as a synchronously raised exception.

The reason is the following:

  • handleEntry is called from inside an EventEmitter event handler, which is subscribed from inside the Promise returned by the getFiles method (in both the "src/io/xpi.js" and "src/io/crx.js" modules)
  • the behavior that we expect from the promise returned from getFiles is:
    • to be resolved with the list of the file entries once the zipFile and no duplicate entries have been found
    • to be rejected as soon as handleEntry is called with a duplicate entry filename

Given that handleEntry is called from inside an event handler, if it throws an exception instead of calling reject, the raised exception is not going to be translated into a rejection of the Promise returned by getFiles, on the contrary it is going to become an "uncaught exception" (which in nodejs can be caught using process.on("uncaughtException", (err) => { ... });).

We definitely need to preserve the previous behavior, which is that the rejection will be caught by the Linter class in one of the following this.handleError(...) calls:

this.handleError(err, deps._console);

this.handleError(err, deps._console);

Also, based on this analysis, the very weird (and concerning) thing is that the different behavior doesn't seem to trigger any test failures, which basically means that something in the current testing strategy is making the tests to behave differently from what is going to happen in the application once executed for real, which is pretty bad and we need to fix it.

After digging a bit into the code and the test suite, I've been able to identify what is affecting the behavior for the tests related to this scenario (related to the DuplicateZipEntry):

In the test cases we are using the _onEventsSubscribed parameter of getFiles to be able to "fake" the zip file entries when the zipfile listeners have been subscribed, see:

https://github.com/hameleonka/addons-linter/blob/6fd67e426da1130e41feb8c3d5a1c529af71129a/tests/io/test.xpi.js#L215-L219

and

https://github.com/hameleonka/addons-linter/blob/6fd67e426da1130e41feb8c3d5a1c529af71129a/src/io/xpi.js#L84-L90

But _onEventsSubscribed is called synchronously at line 88 of the above snippet, and in the test code linked above the definition of _onEventsSubscribed is calling synchronously the this.handleEntry method, which had the (unintentional) side-effect that we are experiencing: an Error raised by the new version of the handleEntry method is becoming a rejction of the promise returned by getFiles... but only in the test cases that are using _onEventsSubscribed in this way.

That's very unfortunate and we should absolutely prevent it from happening again (so that we can be more confident that the test suite is going to be able to catch similar regressions), my suggestion is to change the way we call the _onEventSubscribed callback, so that it is not called synchronously from inside the Promise, and better match what happens when this.handleEntry is called from inside the zipfile listeners, e.g. by chaging it into:

      if (_onEventsSubscribed) {
        // Run optional callback when we know the event handlers
        // have been inited. Useful for testing.
        if (typeof _onEventsSubscribed === 'function') {
          Promise.resolve().then(() => _onEventsSubscribed(reject));
        }
      }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rpl as you suggested in order to see the difference between throw error and reject I put throw error in the beginning of handleEntry function and saw that with reject this error is handled with handleError function and without reject node process just fails. Thanks for your help!

src/io/xpi.js Outdated
@@ -43,53 +43,51 @@ export class Xpi extends IOBase {
}
if (this.entries.includes(entry.fileName)) {
log.info('Found duplicate file entry: "%s" in package', entry.fileName);
reject(new Error(oneLine`DuplicateZipEntry: Entry
"${entry.fileName}" has already been seen`));
throw new Error(oneLine`DuplicateZipEntry: Entry
Copy link
Member

Choose a reason for hiding this comment

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

besides changing this in a reject(new Error(...)), we should probably also return early just after calling reject(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/io/xpi.js Outdated
.catch(reject);
const zipfile = await this.open();

return new Promise((resolve) => {
Copy link
Member

Choose a reason for hiding this comment

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

(resolve, reject) => {...} here for the reasons described in the previous comment.

Copy link
Member

Choose a reason for hiding this comment

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

(same for "src/io/crx.js")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/io/xpi.js Outdated

return new Promise((resolve) => {
zipfile.on('entry', (entry) => {
this.handleEntry(entry);
Copy link
Member

Choose a reason for hiding this comment

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

this.handleEntry(entry, reject); here as previously described (same for "src/io/crx.js").

Copy link
Member

Choose a reason for hiding this comment

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

I'm also wondering if we should also listen to zipfile.on('error', ...)` and call reject also in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rpl do I need to add zipfile.on('error', ...)` and tests for this in this PR or we should open new issue for this?

src/io/xpi.js Outdated
// Run optional callback when we know the event handlers
// have been inited. Useful for testing.
if (typeof _onEventsSubscribed === 'function') {
_onEventsSubscribed();
Copy link
Member

Choose a reason for hiding this comment

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

Promise.resolve().then(() => _onEventsSubscribed(reject)); for the reasons previously described.

Copy link
Member

Choose a reason for hiding this comment

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

(same for "src/io/crx.js")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/io/xpi.js Outdated
zipfile.openReadStream(this.files[path], (err, readStream) => {
if (err) {
return reject(err);
// eslint-disable-next-line consistent-return
Copy link
Member

Choose a reason for hiding this comment

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

We should be able to remove this eslint-disable-next-line here by changing line 143 into:

if (err) {
  reject(err);
  return;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

@rpl rpl left a comment

Choose a reason for hiding this comment

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

just some minor tweaks

src/io/crx.js Outdated
@@ -76,7 +76,7 @@ export class Crx extends Xpi {
// Run optional callback when we know the event handlers
// have been inited. Useful for testing.
if (typeof _onEventsSubscribed === 'function') {
_onEventsSubscribed();
Promise.resolve().then(() => _onEventsSubscribed(reject));
Copy link
Member

Choose a reason for hiding this comment

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

passing reject as a parameter of _onEventsSubscribed doesn't look necessary and none of the tests are currently expecting any parameter passed to this function, eg.

const onEventsSubscribed = () => {
const entryCallback = this.entryStub.firstCall.args[1];
entryCallback.call(null, installFileEntry);
entryCallback.call(null, dupeInstallFileEntry);
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/io/xpi.js Outdated
@@ -85,7 +86,7 @@ export class Xpi extends IOBase {
// Run optional callback when we know the event handlers
// have been inited. Useful for testing.
if (typeof _onEventsSubscribed === 'function') {
_onEventsSubscribed();
Promise.resolve().then(() => _onEventsSubscribed(reject));
Copy link
Member

Choose a reason for hiding this comment

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

passing reject doesn't seem to be necessary (as from the same comment for crx.js)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

src/io/utils.js Outdated
@@ -14,32 +14,27 @@ export function walkPromise(curPath, { shouldIncludePath = () => true } = {}) {
// so all file paths (the result keys) can
// be relative to the starting point.
const basePath = curPath;
// eslint-disable-next-line consistent-return
Copy link
Member

Choose a reason for hiding this comment

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

If I'm reading this walk function correctly, we should be able to remove this eslint-disable-next-line comment by remove the return result from line 24 and 37 and add a single return result outsitde of the if (...) {...} else if (...) ....

(I'm also wondering if we should raise an error if none of the conditions are met)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rpl I made one return statement after if-else block, but I don't know about raising an error...As I understand we check if it's file and if it's directory... but what else could it be?

Copy link
Member

Choose a reason for hiding this comment

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

When the files are coming directly from the filesystem (instead of coming from the content of a zip file, e.g. for an xpi or crx file), the stat result could also be a block or character device, or a FIFO or a socket (based on the related nodejs docs: https://nodejs.org/api/fs.html#fs_class_fs_stats), which is unexpected but still possible.

Currently we just silently ignore these files, and so I'm wondering if it would be better to produce a warning or an error to make it more visible to the addon developer.

Anyway, I'm also ok to defer this to a follow up (so that we keep the changes related to the async/await refactoring separated from changes of the addons-linter behaviors).

@EnTeQuAk what do you think about this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants