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

Reporter: handle objects with cycles #130

Merged
merged 1 commit into from
Feb 21, 2021

Conversation

zackthehuman
Copy link
Contributor

Updating TapReporter with the ability to handle objects with circular
references. This is needed for proper stringification of actual and
expected values that contain cycles.

Fixes #104

* it will be represented as "[Circular]".
*/
function createCycleSafeReplacer() {
const seen = new Set();
Copy link

Choose a reason for hiding this comment

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

Suggested change
const seen = new Set();
const seen = new WeakSet();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made this change but then later didn't need it since I switched to using a shallow clone and the cloning is stack-based.


if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
Copy link

Choose a reason for hiding this comment

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

node’s util.inspect tracks specific refs here, so you know what it’s circular of - it might be worth doing that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is indeed a neat feature of inspect. One question is whether this would be confusing seeing it in the actual/expected output. To label the refs we need two parts:

  1. To label the object being referenced as a <ref *n>
  2. To use the correct index when labeling cycles [Circular *n]

Labeling the cycles should be pretty easy, but I'm a little worried about changing the structure of the stringified object too much.

There are some other enhancements to this stringification we need to consider, for example, BigInt also throws when being fed to JSON.stringify. I'd rather follow up on enhancements separately and landing this fix first.

Thoughts?

Copy link

Choose a reason for hiding this comment

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

Right - normal JSON.stringify isn't really an option, I think, at least not without a stringifier function also provided.

@zackthehuman
Copy link
Contributor Author

I need to crunch on this a little bit more. The existing approach can have false positives as long as the same object appears more than once (even when it's not cyclical). I also have to fix the linting errors.

@zackthehuman
Copy link
Contributor Author

Circling back on this: I think there are many improvements we can make to the serialization outside of handling ref cycles. It would be worth having an issue dedicated to those improvements so that we can figure out what "ideal" serialization looks like.

In the short term, it would help me (and others) to land this fix. I've squashed/rebased my changes, so it should be in a reviewable state. @Krinkle if you have any time would you mind taking a look?

@Krinkle Krinkle self-assigned this Feb 20, 2021
clone = object.map(function (element) {
return decycledShallowClone(element, ancestors);
});
ancestors.pop(object);
Copy link
Contributor

Choose a reason for hiding this comment

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

Array.pop() does not take an argument.

* been replaced with "[Circular]".
*/
function decycledShallowClone (object, ancestors = []) {
if (ancestors.includes(object)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use Array#indexOf() !== -1 here for browser compat as I'd rather not ship a polyfill with this library, and given its low-level use in test runners it's not feasible for end-users to ship a polyfil either as they generally can't or wouldn't be expected to load something before this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Should I also avoid Array#map?

Object.keys(object).forEach(function (key) {
clone[key] = decycledShallowClone(object[key], ancestors);
});
ancestors.pop(object);
Copy link
Contributor

Choose a reason for hiding this comment

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

(idem)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL what idem means.

* than once in an acyclical way.
*/
function createDuplicateAcyclical () {
const duplicate = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

To improve code coverage a little bit, and to weed out errors that result in objects rendering as empty without keys, perhaps include at least one key-value pair in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Added keys to each of the objects so we can identify them in the output.

*/
function createCyclical () {
const cyclical = {};
cyclical.cycle = cyclical;
Copy link
Contributor

@Krinkle Krinkle Feb 21, 2021

Choose a reason for hiding this comment

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

In order to excercise the stack a bit bit, perhaps add one layer of indirection here, e.g. cyclical = { sub: {} }; cyclical.sub.cycle = cyclical;.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an additional structure with a subobject cycle.

Updating `TapReporter` with the ability to handle objects with circular
references. This is needed for proper stringification of actual and
expected values that contain cycles.

Fixes js-reporters#104
@Krinkle Krinkle merged commit c38f467 into js-reporters:main Feb 21, 2021
@zackthehuman zackthehuman deleted the fix-circular-serialization branch February 21, 2021 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

TapReporter needs circular ref filtering on JSON.stringify
3 participants