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 to promises. writeGltf, writeBinaryGltf, writeFile -- callbacks to Promises #135

Merged
merged 22 commits into from
Jul 26, 2016

Conversation

JoshuaStorm
Copy link

@JoshuaStorm JoshuaStorm commented Jul 13, 2016

Not ready for review.

I am currently switching some files to be Promises instead of using callbacks, but I've hit a few snags which have resulted in me doing some anti-patterns that I wanted to ask a few questions about to resolve.

I'll be adding inline comments so it is easier to see what I am talking about.

expect(gltf.buffers.CesiumTexturedBoxTest.uri).toEqual('CesiumTexturedBoxTest.bin');
readFile(outputBufferPath)
.then(function(outputData) {
expect(bufferEqual(outputData, bufferData)).toBe(true);
Copy link
Author

Choose a reason for hiding this comment

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

I am fairly certain I shouldn't have to be nesting .then like this, but I am unsure how I could write this in order to have outputData accessible by the second .then function

I have attempted the code below, but that results in outputData being undefined.

writeGltf(gltf, options)
            .then(function() {
                expect(gltf.buffers.CesiumTexturedBoxTest.extras).not.toBeDefined();
                expect(gltf.buffers.CesiumTexturedBoxTest.uri).toEqual('CesiumTexturedBoxTest.bin');
            })
            .then(function() {
                readFile(outputBufferPath);
            })
            .then(function(outputData) {
                expect(bufferEqual(outputData, bufferData)).toBe(true);
                done();
            });

Copy link
Contributor

Choose a reason for hiding this comment

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

@JoshuaStorm
You need to return a promise to do chaining like that.

writeGltf(gltf, options)
    .then(function() {
        expect(gltf.buffers.CesiumTexturedBoxTest.extras).not.toBeDefined();
        expect(gltf.buffers.CesiumTexturedBoxTest.uri).toEqual('CesiumTexturedBoxTest.bin');
        return readFile(outputBufferPath);
    })
    .then(function(outputData) {
        expect(bufferEqual(outputData, bufferData)).toBe(true);
        done();
    });

Copy link
Author

Choose a reason for hiding this comment

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

@lasalvavida
I appreciate the quick responses to these questions. That makes a lot of sense but I seemed to have just been gleaning over this fact while reading the documentation.

})
.catch(function(err) {
done();
throw err;
Copy link
Author

Choose a reason for hiding this comment

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

The three above tests are failing

The spy is never being triggered and I am not exactly sure why. I do a similar spy setup for writeGltfSpec and writeBinaryGltfSpec that work fine.

@lasalvavida or @lilleyse, if you have a minute could you briefly look over these tests and see if there is anything I'm obviously missing

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's because you remove the args to the fake callback, so args[0] is undefined.

Copy link
Author

Choose a reason for hiding this comment

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

I am fairly certain spy.calls.first() accesses the args passed to the function spied on.

See writeGltfSpec for example

Copy link
Author

Choose a reason for hiding this comment

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

Doing a few console.log traces, it seems that for some reason the the processFileToDisk and processJSONToDisk are resolving before the outputJson in writeGltf

I'm guessing there is something wrong with my promises

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm guessing there is something wrong with my promises

That is true. Hang tight for a little bit; I'm almost done fixing this

@lasalvavida
Copy link
Contributor

@JoshuaStorm This branch should be clean again

@lasalvavida
Copy link
Contributor

I'm seeing errors in Travis, but this is clean locally for me

@JoshuaStorm
Copy link
Author

Ignore the most recent commit, it was off-base

I'll look more into this

@JoshuaStorm
Copy link
Author

I have resolved the Travis failure that occurred due to attempting to write to a directory without the directory first being made.

However, this solution is a bit hacky:

I want to use both synchronous and asynchronous fs-extra functions. As such, I cannot just promisifyAll of fs-extra since that results in an error being thrown when a synchronous function is called.

A simple solution would be just to require('fs-extra) and promisify the individual functions I want to use as promises, but this causes an issue where I cannot spy on the individual function (there may be a simple resolution to this that I am unaware of)

For example, if I do

var fsExtra = require('fs-extra');
var outputJson = Promise.promisify(fsExtra.outputJson);

and attempt to do spyOn(fsExtra, 'outputJson'), the spy does not seem to be triggered by calling the promisified outputJson

My current solution is a bit clunky, and inefficient: I use both var fsExtra = require('fs-extra') and var fsPromise = Promise.promisifyAll(require('fs-extra')) so I can use the synchronous and asynchronous functions, while also being able to spyOn(fsPromise, 'outputJson')

@lasalvavida
Copy link
Contributor

@JoshuaStorm do the following:

fsExtra.outputJsonAsync = Promise.promisify(fsExtra.outputJson);
...
spyOn(fsExtra, 'outputJsonAsync')

@JoshuaStorm
Copy link
Author

I knew I had to be forgetting something straight forward like that.
Thanks!

@JoshuaStorm JoshuaStorm changed the title Async to promises. writeGltf, writeBinaryGltf, writeFile -- callbacks -> Promises Async to promises. writeGltf, writeBinaryGltf, writeFile -- callbacks to Promises Jul 14, 2016
@JoshuaStorm
Copy link
Author

This should be good for review now!

// Create the output directory if specified
if (createDirectory) {
outputPath = path.join(path.dirname(outputPath), 'output', path.basename(outputPath));
fsExtra.ensureDirSync(path.dirname(outputPath));
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need this because outputJson will create the dir if it doesn't exist.

Copy link
Author

Choose a reason for hiding this comment

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

I had to do this due to writeSource using fs.writeFile (not fsExtras), but a better solution is probably just to use fsExtras in writeSource, so I'll go ahead and do that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah ok, that solution seems good.

var vertexShaderPath = './specs/data/boxTexturedUnoptimized/CesiumTexturedBoxTest0VS_Binary.glsl';

describe('getBinaryGltf', function() {

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove line.

@lasalvavida
Copy link
Contributor

lasalvavida commented Jul 15, 2016

Async tests should use

expect(promise, done).toResolve();

https://github.com/AnalyticalGraphicsInc/agi-cesium-people/tree/node-coding-guide/NodeCodingGuide#testing-differences-from-cesium

This is more robust and will keep failing tests from falling through promises.

@JoshuaStorm
Copy link
Author

@lasalvavida Good catch, I forgot about that.

Just to make sure I do these properly:

This

        writeGltf(gltf, options)
            .then(function() {
                expect(gltf.shaders.CesiumTexturedBoxTest0FS.extras).not.toBeDefined();
                expect(gltf.shaders.CesiumTexturedBoxTest0FS.uri).toEqual('CesiumTexturedBoxTest0FS.glsl');
                return fsReadFile(outputFragmentShaderPath);
            })
            .then(function(outputData) {
                expect(bufferEqual(outputData, fragmentShaderData)).toBe(true);
                done();
            });

should become

        var promise = writeGltf(gltf, options)
            .then(function() {
                expect(gltf.shaders.CesiumTexturedBoxTest0FS.extras).not.toBeDefined();
                expect(gltf.shaders.CesiumTexturedBoxTest0FS.uri).toEqual('CesiumTexturedBoxTest0FS.glsl');
                return fsReadFile(outputFragmentShaderPath);
            })
            .then(function(outputData) {
                expect(bufferEqual(outputData, fragmentShaderData)).toBe(true);
                done();
            });
        expect(promise, done).toResolve();

Right?

Thanks!

@lasalvavida
Copy link
Contributor

Make sure you don't call done like in your example.

I would also write it a little more compactly, like this:

expect(writeGltf(gltf, options)
    .then(function() {
        expect(gltf.shaders.CesiumTexturedBoxTest0FS.extras).not.toBeDefined();
        expect(gltf.shaders.CesiumTexturedBoxTest0FS.uri).toEqual('CesiumTexturedBoxTest0FS.glsl');
        return fsReadFile(outputFragmentShaderPath);
    })
    .then(function(outputData) {
        expect(bufferEqual(outputData, fragmentShaderData)).toBe(true);
    }), done).toResolve();

var AttributeCompression = Cesium.AttributeCompression;
var Cartesian2 = Cesium.Cartesian2;
var defined = Cesium.defined;
Copy link
Contributor

@lasalvavida lasalvavida Jul 15, 2016

Choose a reason for hiding this comment

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

defined should be at the bottom of this section.

@lasalvavida
Copy link
Contributor

I think bin/gltf-pipeline.js may need updating since processFileToDisk is a promise now.

@lasalvavida
Copy link
Contributor

If node already waits for all promises to terminate, it may be as simple as putting a comment that says that so there's no confusion.

var fs = require('fs');
var path = require('path');
var Promise = require('bluebird');
var readFile = Promise.promisify(fs.readFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this fsReadFile for consistency with the node guide.

@lasalvavida
Copy link
Contributor

Those are my last few comments; this looks awesome @JoshuaStorm!

@lasalvavida
Copy link
Contributor

@lilleyse This does change the public API; do we want to merge this now or wait until after the first alpha release?

@lilleyse
Copy link
Contributor

lilleyse commented Jul 18, 2016

@lilleyse This does change the public API; do we want to merge this now or wait until after the first alpha release?

I was thinking about this too. It's probably best to wait.

I have another question though. Should our public API return promises at all, or should those functions take callbacks? Internally the callback would just wrap around the promise. It seems like callbacks are still the standard for most other node libraries. @mramato do you have thoughts on this?

@mramato
Copy link
Contributor

mramato commented Jul 18, 2016

Our public API should absolutely return promises. There are two ways to go here:

  1. Return promises and not provide callbacks at all. (The ideal solution)
  2. Support both, which is what I've seen other libraries do. Basically, any public function that normally returns a promise would take an optional callback function as it's final argument. Passing in the callback will cause it to act like a traditional callback function and not return a promise. We could easily write a single promiseToCallback utility function that handles this for us and we would have to call it wherever we return a promise from the public API.

While 2 wouldn't be a ton of code, it does add some additional overhead and clutter the API a bit. We would also have to add an extra test for every public API function to cover it. My gut says to go with 1 until enough people ask for 2. It would be a non-breaking change to add later.

@pjcozzi What do you think?

@pjcozzi
Copy link
Contributor

pjcozzi commented Jul 18, 2016

My gut says to go with 1 until enough people ask for 2

Big +1

@lilleyse lilleyse mentioned this pull request Jul 20, 2016
@lilleyse
Copy link
Contributor

Thanks @lasalvavida and @JoshuaStorm

@lilleyse lilleyse merged commit 41c90ae into master Jul 26, 2016
@lilleyse lilleyse deleted the async-to-promises branch July 26, 2016 01:58
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.

None yet

5 participants