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

assert.propEqual not guarded against circular structures #411

Closed
amb26 opened this issue Feb 4, 2013 · 5 comments
Closed

assert.propEqual not guarded against circular structures #411

amb26 opened this issue Feb 4, 2013 · 5 comments
Labels

Comments

@amb26
Copy link
Contributor

amb26 commented Feb 4, 2013

As a result of #343 we got a new "propEqual" method which is usefully capable of comparing object trees in a "constructor-blind" manner, using a weaker semantic than QUnit.deepEqual. However, this method is not up to the standard of the rest of the framework - in particular, QUnit.deepEqual itself offers support for

  1. correctly comparing cyclic structures without bombing the stack (Infinite recursion issues with cyclic references and equal() #100 - recently improved by QUnit.equiv will erroneously declare that objects with circular references compare equal to those without #397)
  2. not being confused by primitive arguments such as strings - these are compared properly and highlighted in the difference output.

"propEqual" was implemented using a utility function "objectValues" which performs a clone of the arguments before they are dispatched to QUnit.deepEqual, and it is deficiencies in this algorithm which are responsible for deficiencies in propEqual as compared to deepEqual.

  1. is caused by the cloning operation itself bombing out on cyclic structures, and
  2. is caused by the lack of a check for primitive arguments before copying - strings are copied as if they were arrays. The comments at the head of objectValues are clear about the latter restriction, but this contract limitation is not clear when passed up to propEqual itself.

I enclose a screenshot of the results of comparing two string using propEqual. deepEqual by contrast shows a suitable result.
propEqualStrings

@Krinkle
Copy link
Member

Krinkle commented May 7, 2013

  1. The circular structure should be detected and guarded against, indeed.
  2. Primitive values being compared like objects is by design. Perhaps it should throw a warning instead. But treating them as strings instead of objects would be masking an error by the test author. I'd like to get this removed from deepEqual as well.

@ghost ghost assigned Krinkle May 7, 2013
@jzaefferer
Copy link
Member

@Krinkle want to look into fixing this again?

@Krinkle Krinkle self-assigned this Nov 6, 2014
@BraulioVM
Copy link
Contributor

@Krinkle I started working on a patch for this, I hope you don't mind (I just wanted to contribute fixing a bug and saw that this one was open yet).

I started implementing a fix considering what Krinkle said in his first comment in this issue, but while I was writing tests for the new propEqual method I realised that I was not actually sure about what the method should return in certain cases. For example:

function fn1() { return "fn1"; }
function fn2() { return "fn2"; }
var first = { a : fn1 };
var second = { a: fn2 };


assert.propEqual(first, second, "Should they be equal?");
assert.propEqual(fn1, fn2, "Should they be equal here?");

Using the old propEqual, both would be right. I am not sure we wanted to keep the same behaviour as that may not be what the test author would expect. Should we keep it?

Furthermore, Krinkle said we should throw a warning whenever the test author supplied a primitive object. I have been inspecting the codebase and can't seem to find what the best mechanism would be for throwing such a warning. Any ideas?

Thank you very much

@Krinkle
Copy link
Member

Krinkle commented Jun 20, 2020

For example:

function fn1() { return "fn1"; }
function fn2() { return "fn2"; }
var first = { a : fn1 };
var second = { a: fn2 };
assert.propEqual(first, second, "Should they be equal?");

The propEqual assertion is for comparing strict equality over the own properties of a given object (docs). In this case, I expect QUnit to find them non-equal, because fn1 !== fn2. By identity, they point to different functions.

assert.propEqual(fn1, fn2, "Should they be equal here?");

Using propEqual on a function as the compared value is most likely a mistake since Function objects generally have no meaningful properties to compare. What is a Function object? Well, I don't think it's important to know this detail of the JavaScript language, but if you're interested, I have an example.

In JavaScript, all non-primitives are objects, and that includes functions. A function is definetely special in that it can be "called" using the () parenthesis syntax which then leads to the lexical execution of some previously provided source code. But if we ignore that realy important thing for a minute, then what is underneath that is a plain object, which inherits from Function.prototype. The same way that new Date() creates an instance of the Date class, which is a plain object that inherits from Date.prototype. One could even use new Function(). This is generally considered a bad practice, but functionaly that produces (almost) the same thing as a normal function.

Either way, functions can be used as objects

var a = {};
var b = function() {};

a.some = 'thing';
b.some = 'other thing';

And this is what makes "static class properties" work:

function Foo() {
}
Foo.same = 'same';
Foo.but = 'different';

function Bar() {
}
Bar.the = 'same same';
Bar.but = 'different';

.. and if you pass that to propEqual it will treat it the same as it would any other object:

assert.propEqual(Foo, Bar);
severity: failed
actual: {
  "same": "same",
  "but": "different"
}
expected: {
  "the": "same same",
  "but": "different"
}

Now, coming back to the original example:

For example:

function fn1() { return "fn1"; }
function fn2() { return "fn2"; }
assert.propEqual(fn1, fn2, "Should they be equal here?");

The answer is yes, because fn1 and fn2 are equal in the same way that {} and {} are equal. They are objects with no (own) properties to compare.

@Krinkle
Copy link
Member

Krinkle commented Jun 20, 2020

I'm closing this for now as it was filed about circular structured which I believe QUnit handles adequately now. Please comment or file a new issue with an example if that is not the case.

The example of comparing strings with propEqual is understandably confusing, however I argue that it is not problematic given that if the strings are indeed equal as string, they will be equal as objects as well. So it should not cause any test failures.

When the strings differ, they are compared as an object becuase that is what propEqual is for. The docs could use improvement here as well to avoid this mistake, but it is otherwise expected. I'm not sure why, but I suppose it's possible someone might even prefer this for some reason depending on what the string is for. Although if one really wanted that, I suppose one could simply use .split('') to and send them as real arrays to propEqual.

The result is related to how new String("foo") works in JavaScript, which acts as having own properties [0], [1] etc, much like an array.

I would be open to a breaking change in QUnit 3 to throw an error from propEqual if the "expected" parameter holds a non-object. This is also something eslint-plugin-qunit could help with.

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

Successfully merging a pull request may close this issue.

5 participants