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

Render array moves with arrows #333

Merged
merged 9 commits into from
Sep 26, 2016
Merged

Render array moves with arrows #333

merged 9 commits into from
Sep 26, 2016

Conversation

papandreou
Copy link
Member

@papandreou papandreou commented Sep 11, 2016

Affects the array-like diff and the <array-like> to [exhaustively] satisfy <array-like> assertion.

Here's what it'll look like in unexpected-sinon: https://github.com/unexpectedjs/unexpected-sinon/compare/feature/arrayMoves

And unexpected-dom: papandreou/unexpected-dom@11b7199 (only one of the existing tests seemed to contain a move)

┌─>
│ 3,
└── 2, // should be moved
4 // should be undefined
Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, it shouldn't say "should be undefined" here.

@sunesimonsen
Copy link
Member

This looks pretty insane 😀 I'm still concerned about performance, complexity and verbosity. But we can probably put in stop gaps so this will only run in cases where it is manageable.

Would it be possible to use unicode arrows to avoid using 3 lines to represent one connection.

@sunesimonsen
Copy link
Member

@papandreou
Copy link
Member Author

Hmm, those unicode arrows don't seem to compose very well:

↱
∣
↳
═⇒
⇑
‖
⇓
⋯⇢

@sunesimonsen
Copy link
Member

You are right, you can draw perfect boxes but the arrows does not match 😢

@papandreou
Copy link
Member Author

Fixed those "should be undefined" bugs.

I'm still concerned about performance, complexity and verbosity.

Yeah, me too :). I tried to address the performance and complexity concerns by consolidating the arrow handling in a single style that's used by both <array-like> to satisfy <array-like> and the array-like diff, and by not using the merge style when the are no arrows or just one arrow to render.

As for the verbosity, it seems like the existing heuristic for falling back to an index-by-index comparison is still doing a good job here. We want to additionally limit the number of moves to render using arrows. In my opinion, none of our existing tests seem to exhibit any bad behavior, though.

@alexjeffburke
Copy link
Member

I agree to some extent with @sunesimonsen about verbosity etc, but I am kind of compelled at the sight of this output with the -sinon plugin. Visually it's probably most useful when things are naturally strongly ordered, and when talking about function calls we definitely think of a series of them as having an order. That might explain why I'm less sure about the -dom output with this.

Anyway, I'd be tempted to have a way of turning this on/off per diff and it might be worth considering the default.

@papandreou
Copy link
Member Author

Visually it's probably most useful when things are naturally strongly ordered

Not sure I understand? When comparing arrays, the order is always significant per the semantics?

Anyway, I'd be tempted to have a way of turning this on/off per diff and it might be worth considering the default.

You mean in the code (when subclassing the array-like type) or as a user-facing option?

@alexjeffburke
Copy link
Member

@papandreou Ah sorry I didn't immediately twig that DOM example was a NodeList. I think I was considering the case where something was declared as an array but the order might matter less, though having said that I can't think of a good example that exists now. Perhaps if we did add support for "these mocks but in any order" to -mitm or equivalent we'd always want a diff without arrows?

So yes, I probably meant a within the code opt in/out as appropriate.

@papandreou
Copy link
Member Author

Perhaps if we did add support for "these mocks but in any order" to -mitm or equivalent we'd always want a diff without arrows?

Agreed. In that case the matching semantics are also different, though -- that's why I made http://unexpected.js.org/unexpected-set

By the way, I think the arrows will be really nice if we make that change to unexpected-mitm that we discussed where we would initially pick the first request that matches the spec, but if the requests came in the wrong order, we would error out after the test has completed.

@sunesimonsen
Copy link
Member

I have a feeling that it would be wise to back out of using this for the following cases:

  • too many crossing arrows
  • arrows becomes too long
  • more than 3 levels

@papandreou
Copy link
Member Author

I know this is in the nitpick department, but I would prefer:

expected [ 1, 3, 2, 4 ] to be sorted

[
   1,
┌─>
│  3,
└─ 2, // should be moved
   4
]

Sure thing. I left those spaces there to honor the indent setting of the type, but I agree that it looks better without it when the arrows are there.

@sunesimonsen
Copy link
Member

@papandreou sorry I deleted my comment as I realised that cross arrows would be problematic with my suggestion. But We can always tweak that.

@sunesimonsen
Copy link
Member

sunesimonsen commented Sep 12, 2016

@papandreou you can still honour the indent, you can just use it as available space.

@sunesimonsen
Copy link
Member

But sorry, we should not bikesheet about looks right now :-)

@papandreou
Copy link
Member Author

papandreou commented Sep 12, 2016

I realised that cross arrows would be problematic with my suggestion. But We can always tweak that.

The current implementation is allocating as much horizontal space as it needs (except that it doesn't try to do any "packing", so getting rid of the indentation doesn't cause any trouble in itself. I removed it in 6a97050

@papandreou
Copy link
Member Author

I have a feeling that it would be wise to back out of using this for the following cases:

  • too many crossing arrows
  • arrows becomes too long
  • more than 3 levels

Agreed, seems like no upper limit is currently enforced:

manyarrows

@sunesimonsen
Copy link
Member

hehe 🎉

@papandreou
Copy link
Member Author

Rebased on top of the array-changes and array-changes-async fixes (both this branch and the branches in those repos).

@papandreou
Copy link
Member Author

Andreas Lind @papandreou 14:21
And we still need to work out how many arrows are "too many" so we can bail out of rendering them.

Sune Simonsen @sunesimonsen 14:21
@papandreou I think it is more about the number of layers they contribute
3 or 4 would be a good measure in my opinion.

@papandreou papandreou changed the title WIP: Render array moves with arrows Render array moves with arrows Sep 15, 2016
@papandreou
Copy link
Member Author

I think this is in a good state now. It requires unexpectedjs/array-changes#5 and bruderstein/array-changes-async#5 to be merged and new major versions of array-changes and array-changes-async to be released.

@sunesimonsen
Copy link
Member

@papandreou that is good news. I'll take a look at the code soon.

@papandreou
Copy link
Member Author

Thanks -- I'm curious if you have a better idea of how to implement the merge style, and whether we want something like it in magicpen.

@sunesimonsen
Copy link
Member

@papandreou merge is overlaying the output of one pen on top of another - right?

If yes that should probably be in magicpen as it is very generic and useful.

At some point I was thinking about these kind of things - centering the output of a pen on top of another and other layout stuff. But with a method like this in magicpen, you could probably make the rest as a plugin.

@papandreou
Copy link
Member Author

merge is overlaying the output of one pen on top of another - right?

Yeah, that seemed much easier than rendering all the arrows in one go.

@sunesimonsen
Copy link
Member

Yes makes a lot of sense.

@papandreou
Copy link
Member Author

@bruderstein Are you OK with releasing new versions of array-changes(-async) that would potentially break unexpected-htmllike? Should we explore options to move it (and/or the adaptors) to utilize the modules via the array-like type, or would you rather reimplement the arrow rendering?

@papandreou
Copy link
Member Author

Possible future improvements that have come up:

  • @sunesimonsen noticed that the move arrows always point upwards. This seems to be an artifact of arraydiff's algorithm, and it sometimes leads to more arrows than are really necessary. Fixing this would probably be quite a bit of work.
  • Swapped items could be rendered as one arrow -- probably blocked on the same thing.
  • Multiple moved items (eg. {type: 'move', howMany: 3, ...} from arraydiff) could possible be rendered with one arrow-ish thing, but is currently turned into one per item.

@sunesimonsen
Copy link
Member

Very interesting idea of creating output that follows the array diff more closely.

@papandreou
Copy link
Member Author

papandreou commented Sep 17, 2016

It's certainly doable. I'm just not sure that it'd be obvious that

expected [ 3, 1, 2 ] to equal [ 1, 2, 3 ]

┌─▷
   3,
├── 1, // should be moved
└── 2 // should be moved

is equivalent to

┌───▷
 ┌─▷
    3,
└─├── 1, // should be moved
  └── 2 // should be moved

rather than:

┌───▷
 ┌─▷
    3,
 └── 1, // should be moved
└──── 2 // should be moved

@sunesimonsen
Copy link
Member

I think you are right. The single arrows are easier to understand.

@@ -1044,59 +1044,68 @@ module.exports = function (expect) {
if (subjectType.indent) {
output.indentLines();
}
changes.forEach(function (diffItem, index) {
var packing = utils.packArrows(changes); // NOTE: Will have side effects in changes if the packing results in too many arrow lanes
output.arrowsAlongsideChangeOutputs(packing, changes.map(function (diffItem, index) {
Copy link
Member

Choose a reason for hiding this comment

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

It really has nothing to do with the PR, but I think we have crossed the line where we should split up the assertions in separates files (Not in this PR - but after). After we split the assertions into separate files, I think something like this would be much more readable if it was split into some more helper methods.

this.append(valueDiff.amend(delimiterOutput));
var type = diffItem.type;
if (type === 'moveTarget') {
return output.clone();
Copy link
Member

Choose a reason for hiding this comment

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

It feels like there should be a magicpen construct to map over an array of items and produce output for each item. I know you are just cloning the output, but it feels a bit unsafe that you might append to the outside output by accident. This is just thoughts on how we might refactor later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed!

for (i = 1 ; i < options.width ; i += 1) {
this.text(i === options.width - 1 && options.direction === 'down' ? '▷' : '─', styles);
}
});
Copy link
Member

Choose a reason for hiding this comment

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

Very nice 👍

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 probably make a magicpen-arrow plugin.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we can make it a bit more generic, and if unexpected-htmllike/-react cannot reach this feature through the array-like type, I agree.

Copy link
Member

Choose a reason for hiding this comment

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

But it might also mean implementing a lot of features that we wont use.

}
} while (contentLeft);
}
});
Copy link
Member

Choose a reason for hiding this comment

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

As we talked about, this style should move to magicpen core when it has proven it's stability.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

this.block(function () {
this.merge(arrows);
});
}
Copy link
Member

Choose a reason for hiding this comment

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

If would be better if this was just:

this.block(function () {
  this.merge(arrows);
});

Magicpen should be smart enough to avoid the extra block.

Copy link
Member

Choose a reason for hiding this comment

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

and merge should be smart enough to handle one pen.

Copy link
Member Author

Choose a reason for hiding this comment

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

The way merge is implemented, it takes an array of pens and produces a fresh pen with the merged output. It does handle the single pen case, but I'd like to avoid creating a copy here. My initial attempt at implementing the merging was to merge one or more other pens into the current pen, but that turned out to be harder to implement and not as understandable.

Copy link
Member

Choose a reason for hiding this comment

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

My initial attempt at implementing the merging was to merge one or more other pens into the current pen, but that turned out to be harder to implement and not as understandable.

Ohh I though it worked that way. That is probably how I want it to work in magicpen.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I agree that would be the most useful API-wise.

}
});
}
}));
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 feels a lot like Déjà vu, it would be nice of the two method shared more common parts. But let's look at if it is possible to refactored it afterwards.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, a long time ago we made the decision to keep the array/object diffs separate from the generation of the 'to satisfy' diffs. Since then there has been a lot of fluctuation back and forth, but I think they're still sufficiently different that it could pose a challenge to reconsolidate them.

If we decide to go for #330 they would align closer.

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 not saying that we should reconcile them, just that we should look for common parts that we can extract :-)

"array-changes": "1.3.1",
"array-changes-async": "2.2.1",
"array-changes": "unexpectedjs/array-changes#e2385e5b3130e7c715dee6d77489c78565833bcb",
"array-changes-async": "bruderstein/array-changes-async#8c965f125be6f45604ef764a7750b4c23eafd9f9",
Copy link
Member

Choose a reason for hiding this comment

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

We of cause needs these released before this can be merged. But I guess we are waiting on an answer from @bruderstein - right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'd like him to sign off on it so I don't create any stumbling blocks for unexpected-htmllike and unexpected-react.

for (i = 1 ; i < options.width ; i += 1) {
this.text(i === options.width - 1 && options.direction === 'down' ? '▷' : '─', styles);
}
});
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 probably make a magicpen-arrow plugin.

Copy link
Member

@sunesimonsen sunesimonsen left a comment

Choose a reason for hiding this comment

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

This seems to be ready to merge after the dependencies has been released. But we should really take unexpected for a refactoring dance afterwards.

I'm still a bit curious about the performance effects, can we get any kind of measurements, comparing two arrays with 500 items. We should probably skip the diff for arrays with more then 50 items - I don't really know what the limit should be.

Are we completely sure the code does the right thing? Should we create a unexpected-check test that compares random arrays, when it fails checks that the output contains the same number of moves, removes and inserts as the array-changes?

@@ -0,0 +1,24 @@
/*global expect*/
describe('overlay', function () {
Copy link
Member

Choose a reason for hiding this comment

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

This should be merge style.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@papandreou
Copy link
Member Author

Thanks for reviewing!

I'm still a bit curious about the performance effects, can we get any kind of measurements, comparing two arrays with 500 items. We should probably skip the diff for arrays with more then 50 items - I don't really know what the limit should be.

I tried writing a quick script:

var a1 = new Array(500);
var a2 = new Array(500);
for (var i = 0 ; i < 500 ; i += 1) {
    a2[i] = i;
    if (i * 2 < 500) {
        a1[i * 2] = i;
    } else {
        a1[(i - 250) * 2 + 1] = i;
    }
}
var expect = require('./');

for (var i = 0 ; i < 10 ; i += 1) {
    try {
        expect(a1, 'to equal', a2);
    } catch (e) {}
}

expect(a1, 'to equal', a2);

To my surprise it actually runs faster with these changes than on master, 4.3 seconds vs. 5.1 seconds. The <array-like> to [exhaustively] satisfy <array-like> assertion already falls back to index-by-index if either the subject or value has more than 10 items, so I'm not really worried about that.

Are we completely sure the code does the right thing? Should we create a unexpected-check test that compares random arrays, when it fails checks that the output contains the same number of moves, removes and inserts as the array-changes?

I'm reasonably sure it does the right thing, but it's an interesting idea to exercise it like that.

@sunesimonsen
Copy link
Member

Creating an unexpected-check test might be a bit more challenging than I initially imagined as you need to take the fallback to indexed diffing into account. But you could make a test that asserts that the either don't contain an arrow or that the diff is consistent with the array-changes. But I'm completely okay with merging this without such a test.

@sunesimonsen
Copy link
Member

The interval packer already has randomized tests so that part should be good: https://github.com/One-com/greedy-interval-packer/blob/master/test/randomized.spec.js

@papandreou
Copy link
Member Author

Fixed up and rebased. Merging if CI is green.

@papandreou papandreou merged commit 75b0c16 into master Sep 26, 2016
@papandreou papandreou deleted the feature/arrayMoveArrows branch September 26, 2016 07:18
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.

3 participants