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: adds partialDeepStrictEqual #54630

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

puskin94
Copy link
Contributor

@puskin94 puskin94 commented Aug 29, 2024

Fixes: #50399

Took heavy inspiration from #53415 , trying to push it to the finish line 🚀

On top of it, I took the liberty of:

  1. refactor the code a bit
  2. wrote more documentation
  3. added more tests and restructured old ones
  4. implement previously reported comments / suggestions

Co-Authored-By: Cristian Barlutiu

@nodejs-github-bot nodejs-github-bot added assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run. labels Aug 29, 2024
@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch from 466c2f0 to c0a58e2 Compare August 29, 2024 12:28
Copy link

codecov bot commented Aug 29, 2024

Codecov Report

Attention: Patch coverage is 94.37500% with 9 lines in your changes missing coverage. Please review.

Project coverage is 88.41%. Comparing base (4f88179) to head (c47e009).
Report is 362 commits behind head on main.

Files with missing lines Patch % Lines
lib/assert.js 94.33% 8 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #54630      +/-   ##
==========================================
+ Coverage   88.23%   88.41%   +0.17%     
==========================================
  Files         652      654       +2     
  Lines      183920   187906    +3986     
  Branches    35863    36165     +302     
==========================================
+ Hits       162286   166132    +3846     
- Misses      14913    15011      +98     
- Partials     6721     6763      +42     
Files with missing lines Coverage Δ
lib/internal/test_runner/test.js 96.98% <100.00%> (+0.04%) ⬆️
lib/assert.js 98.96% <94.33%> (-0.91%) ⬇️

... and 282 files with indirect coverage changes

@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch from c0a58e2 to 04e92f0 Compare August 29, 2024 14:20
doc/api/assert.md Outdated Show resolved Hide resolved
@RedYetiDev RedYetiDev added semver-minor PRs that contain new features and should be released in the next minor version. notable-change PRs with changes that should be highlighted in changelogs. labels Aug 29, 2024
Copy link
Contributor

The notable-change PRs with changes that should be highlighted in changelogs. label has been added by @RedYetiDev.

Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the Other Notable Changes section.

@RedYetiDev
Copy link
Member

This PR adds new functionality, hence semver-minor

This PR's new functionality (IMO) is notable-change

@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch from 04e92f0 to 0ee83d4 Compare August 29, 2024 16:04
doc/api/assert.md Outdated Show resolved Hide resolved
// AssertionError
```

## `assert.includes(actual, expected[, message])`
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 less convinced that this one is useful. Why not just simply use assert(actual.includes(expected))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jasnell I just followed the most "approved" comment in the original PR and implemented it :)

#50399 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

I agree with @jasnell and don't think this should be implemented. Let us concentrate on the partial inclusion. If anyone would ever ask for something else, we can still implement more.

lib/assert.js Outdated Show resolved Hide resolved
lib/assert.js Show resolved Hide resolved
lib/assert.js Show resolved Hide resolved
@puskin94
Copy link
Contributor Author

is there any way we can push this forward? ar far as I know there is a little bit of people waiting for this and no clear consensus on the naming convention :)

@jasnell
Copy link
Member

jasnell commented Sep 10, 2024

I still have concerns about whether we need everything in this PR but would very much like others to weigh in. @nodejs/test_runner @nodejs/assert ... anyone have thoughts?

@ljharb
Copy link
Member

ljharb commented Sep 10, 2024

re naming, I definitely think the term "match" should be reserved for regexes.

if the only difference between these new methods and deepEqual etc is that they allow extra properties, then perhaps it would make more sense as an option to the existing methods rather than entirely new ones?

@puskin94
Copy link
Contributor Author

@ljharb the discussion where the "option" was discarded in favor of the new API started from this comment: #50399 (comment)

@puskin94
Copy link
Contributor Author

other method names we could consider:

assert.subsetEqual() assert.containsSubset() assert.partialEqual()

@ljharb
Copy link
Member

ljharb commented Sep 10, 2024

To clarify, the current proposal takes an "actual" and "expected", and it checks that every property on expected is deepEqual to the same property on actual? Are nested objects on expected checked using full or partial equality?

What about if I want to assert that a property is not present? is there a way to represent "never"?

@puskin94
Copy link
Contributor Author

good questions!

yes, this test will pass:

      {
        description: 'compares two deeply nested objects with partial equality',
        actual: { a: {nested: {property: true, some: 'other'}} },
        expected: { a: {nested: {property: true}} },
      },

and no, there is no way to check "not inclusion" , just if an object is a subset of another

@ljharb
Copy link
Member

ljharb commented Sep 10, 2024

assert.subsetDeepEqual? (also shouldn't the default be strict, and the extra one be "Loose"?)

@puskin94
Copy link
Contributor Author

that would deviate from the already present implementations, like deepStrictEqual and deepEqual

Copy link
Member

@MoLow MoLow left a comment

Choose a reason for hiding this comment

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

implementation LGTM, I agree with the concerns on naming, lets change the name so this can land?
+1 for partialDeepEqual

lib/assert.js Outdated Show resolved Hide resolved
lib/assert.js Outdated Show resolved Hide resolved
lib/assert.js Outdated Show resolved Hide resolved
test/parallel/test-assert-objects.js Show resolved Hide resolved
@RedYetiDev
Copy link
Member

+1 for partialDeepEqual

Agreed. Also, should this have a notPartialDeepEqual inverse?

@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch 2 times, most recently from ab7e68e to 3d52d4b Compare September 11, 2024 06:34
Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

I just look at this now and I could not find any comments about why we want to have two methods includes() and partialDeepEqual(). I think it would be nicer to simplify that and just have partialDeepEqual() as it should IMO just do the same what includes() currently does.

Could someone please point me to some discussion around that?

@RedYetiDev
Copy link
Member

Also, includes() does not work on buffers, should it?

// AssertionError
```

## `assert.includes(actual, expected[, message])`
Copy link
Member

Choose a reason for hiding this comment

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

WDYT about includes and includesStrict?

includesStrict([obj, otherObj], obj) -> True
includesStrict([{}], {}) -> False

But

includes([obj, otherObj], obj) -> True
includes([{}], {}) -> True

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

The name does not tell anything about how the comparison works. The original one was about strict equality as in three equal signs. This is about reference equality and I don't think we should do this.

@@ -2548,6 +2548,232 @@ assert.throws(throwingFirst, /Second$/);
Due to the confusing error-prone notation, avoid a string as the second
argument.

## `assert.partialDeepEqual(actual, expected[, message])`
Copy link
Member

Choose a reason for hiding this comment

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

The existence of the strict method is something that is very confusing and I strongly recommend we do not add any "non"-strict version. I could not find anyone asking for this either.

That should never be used. If we ever want to have a different comparison, it should be done by e.g., assert.any(Number) or similar.

Copy link
Contributor

Choose a reason for hiding this comment

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

I give you a whole discussion around wanting this fastify/fastify#5628

Copy link
Member

Choose a reason for hiding this comment

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

@jsumners I am absolutely +1 on adding some kind of partial deep equal comparison as done here. This PR just adds three different APIs while I believe we should only ship the partialDeepStrictEqual one. I am not a super fan of the name but I know there was a lot of back and forth and it is tricky to find the right name.

@puskin94 what about doing the following as next steps:

  1. Remove everything besides the current partialDeepStrictEqual() implementation.
  2. Have a small poll for the name.

That way we could ship the main requested feature and later on decide if we want to add anything else on top.

I think this is great work and we should definitely ship that functionality!

@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch from aaa2b60 to 237cf53 Compare October 13, 2024 12:40
@puskin94 puskin94 changed the title assert: adds partialDeepEqual, partialDeepStrictEqual, includes assert: adds partialDeepStrictEqual Oct 13, 2024
@puskin94
Copy link
Contributor Author

Small poll about the name of the partialDeepStrictEqual method, which allows to perform subset comparisons as:

assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });

👍 partialDeepStrictEqual
😄 subsetDeepStrictEqual
🎉 matchStrictObject
❤️ deepStrictMatch
🚀 ??? (please write the new proposed name)

You can use the emojis above to react on this comment to vote your favorite!

@boukeversteegh
Copy link

boukeversteegh commented Oct 14, 2024

Small poll about the name of the partialDeepStrictEqual method, which allows to perform subset comparisons as:

assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });

👍 partialDeepStrictEqual
😄 subsetDeepStrictEqual
🎉 matchStrictObject
❤️ deepStrictMatch
🚀 ??? (please write the new proposed name)

You can use the emojis above to react on this comment to vote your favorite!

To determine the best name, I'm thinking of the structure of the method name by looking at two main components:

  • The Operator: What is the primary action or assertion being performed? Examples include "equals" or "match".
  • Modifiers: What options modify the evaluation? Examples include "deep" or "strict".

It's conventional to place the operator at the end of the method name (e.g., deepEqual). The key question is whether this method is a modification of the existing "Equal" operator or if it represents a new operator altogether.

If it's a modification of "Equal," the method name would logically be constructed as:

  • [Modifiers] + Partial + Equal

Regarding the modifiers:

  • Depth: Since the assertion inherently recurses through nested objects, "deep" might be redundant.
  • Strictness: "Strict" is relevant because it specifies how the property values are compared (using strict equality).

With this in mind, a possible name could be strictPartialEqual, aligning with existing naming conventions like strictEqual.

However, given that this method operates differently from a standard equality check—specifically, it checks for a subset rather than full equality—it might be better to consider a new operator. The goal is to find an operator that implies recursive comparison and ideally is a single word.

Possible operators and considerations:

  • Match: Could be confused with regex matching.
  • MatchObject: More explicit and reduces confusion.
  • Subset: Meaning commonly understood, although set-subsets and recursive property subsets are quite different.
  • Is Subset: Since verbs are typically omitted in assertion method names, not the best choice.
  • Subset Equal: "Equal" implies a different operation so not the best choice either.
  • Includes: Might not clearly convey recursive comparison, as it's commonly associated with arrays or strings.
  • Contains: Not commonly used in JavaScript for this purpose.
  • Has: Lacks clarity about the depth and nature of the assertion

Final suggestions:

  • strictMatchObject -- Match Object implies recursion, so "deep' is not needed
  • deepStrictMatch -- As suggested, using deep to clarify recursive nature and distinguish it from regex
  • deepStrictSubset -- Adding deep to allow for non recursive subsets like for lists
  • deepStrictIncludes -- Adding deep to clarify the recursive nature of the assertion
  • strictPartialEqual -- Only if the assertion is considered a variation of equal, which it isn't, strictly speaking

@danielbayley
Copy link
Contributor

danielbayley commented Oct 14, 2024

Small poll about the name of the partialDeepStrictEqual method, which allows to perform subset comparisons

@puskin94 See also my reasoning for deepMatch/deepStrictMatch in previous #50399 (comment):

assert.matchObject/matchObjectStrict

This way, if we also wanted a corresponding method to match array subsets, it would have to be called assert.matchArray or something…

Given that we currently have deepEqual for comparing objects and arrays for exact equality, match for matching substrings, and considering existing parlance… I think we should go with the name deepMatch, to assert that A (object or array) contains subset B. We could/should also have an assert.includes method that mirrors the behaviour of the native JS .includes (string or array).

assert.contains

‘contains’ makes sense, but the naming should match existing JavaScript parlance, which, for reasons, went with .includes instead. Aiming for consistency here…

To clarify, something like:

import assert from "node:assert/strict"

assert.deepMatch({ a: 1, b: 2, c: 3 }, { b: 2 }) // pass

assert.deepMatch([1, 2, 3], [2]) // pass

Then, for assert.includes, something like:

import assert from "node:assert/strict"

assert.includes ??= (a, b) => assert.equal(a.includes(b), true)

assert.includes("abc", "b")   // pass

assert.includes([1, 2, 3], 2) // pass

@arthurfiorette @targos @ljharb @MoLow @reconbot @synapse @simoneb What do you guys think?

@jsumners
Copy link
Contributor

jsumners commented Oct 14, 2024

I understand that partialDeepStrictEqual is an attempt to disclose that the comparisons actually evaluated are done so strictly, while the overall comparison is a loose one. But I'd just call it assert.match with clear documentation about the operation of it.

@BridgeAR
Copy link
Member

BridgeAR commented Oct 14, 2024

Using strict in the name is IMO not needed. The original usage was already not ideal and it is still confusing people.
I think just dropping that overall would be best.

@puskin94
Copy link
Contributor Author

Using strict in the name is IMO not needed. The original usage was already not ideal and it is still confusing people. I think just dropping that overall would be best.

I agree with you, I would remove it too, but wouldn't it be more confusing to remove the strict part here and keeping it in the other methods? If you do that, I would assume this is loose

@aduh95
Copy link
Contributor

aduh95 commented Oct 14, 2024

If you don't want strict in the name, you can import it from node:assert/strict. Removing it from the exported name in node:assert would be confusing IMO.

@puskin94
Copy link
Contributor Author

How do we want to move with this one?

@pmarchini
Copy link
Member

Small poll about the name of the partialDeepStrictEqual method, which allows to perform subset comparisons as:

assert.partialDeepStrictEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 });

👍 partialDeepStrictEqual 😄 subsetDeepStrictEqual 🎉 matchStrictObject ❤️ deepStrictMatch 🚀 ??? (please write the new proposed name)

You can use the emojis above to react on this comment to vote your favorite!

I've seen this PR open for quite a while. Before this PR, I noticed a very similar discussion in another PR, from which this one originated (#53415).

I think that this feature would add significant value to the assertions set, so I strongly believe we should unblock this by deciding on a name.
Considering that this discussion has already been going on for quite some time, I think it’s time to ask for a TSC vote.

@nodejs/tsc

@aduh95 aduh95 added the tsc-agenda Issues and PRs to discuss during the meetings of the TSC. label Nov 5, 2024
@aduh95
Copy link
Contributor

aduh95 commented Nov 5, 2024

@pmarchini are you happy with the proposal in #54630 (comment), or is there a more up-to-date list if it does go to a TSC vote?

Final suggestions:

  • strictMatchObject -- Match Object implies recursion, so "deep' is not needed
  • deepStrictMatch -- As suggested, using deep to clarify recursive nature and distinguish it from regex
  • deepStrictSubset -- Adding deep to allow for non recursive subsets like for lists
  • deepStrictIncludes -- Adding deep to clarify the recursive nature of the assertion
  • strictPartialEqual -- Only if the assertion is considered a variation of equal, which it isn't, strictly speaking

We should at least add the one implemented in the PR:

  • partialDeepStrictEqual -- Matches deepStrictEqual, with the prefix to clarify the difference

@pmarchini
Copy link
Member

pmarchini commented Nov 5, 2024

Hey @aduh95, absolutely, I also agree with considering partialDeepStrictEqual! 😁
I don’t think there’s a more recent list of proposals than the one you just quoted.
(I just realised I quoted the wrong comment)

IMHO I think we should go with that one, but I’d suggest that everyone involved in this discussion be able to give a "final word" on it by a set deadline 🚀

lib/assert.js Outdated Show resolved Hide resolved
@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch 2 times, most recently from ddac6c3 to d58ffe7 Compare November 6, 2024 11:52
@boukeversteegh
Copy link

I would consider two more candidates for the voting list.

  • All previous suggestions without STRICT in the name, assuming we can solve this with a separate import node strict as mentioned
  • match - simply one single term. Although it's not consistent with the rest of the codebase, I do feel like this is going to be one of the most popular assertions in this library and a simple name invites developers to use it

@BridgeAR
Copy link
Member

BridgeAR commented Nov 6, 2024

@boukeversteegh

I would consider two more candidates for the voting list.
All previous suggestions without STRICT in the name, assuming we can solve this with a separate import node strict as mentioned
match - simply one single term. Although it's not consistent with the rest of the codebase, I do feel like this is going to be one of the most popular assertions in this library and a simple name invites developers to use it

We could add the name to the strict export only. That would keep it consistent with the other names.
I would just do that as a separate contribution to get this through the door.

lib/assert.js Outdated
return false;
}

return ArrayPrototypeEvery(expected, (item) => ArrayPrototypeIncludes(actual, item));
Copy link
Member

@BridgeAR BridgeAR Nov 6, 2024

Choose a reason for hiding this comment

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

This is actually incorrect. It will match identical parts multiple times. It is also going to have a bad performance.

I am not going to block this though, to get the general idea in and to improve afterwards. Can we just mark it experimental for now, due to these issues?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BridgeAR this was an actual issue, addressed it!

const key = keysExpected[i];
assert(
ReflectHas(actual, key),
new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }),
Copy link
Member

Choose a reason for hiding this comment

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

This message won't be ideal. If it's a deeper array, it is completely unclear what this belongs to. It was actually my main issue with my first attempt. To highlight what parts are missing is tricky.

if (comparedObjects.has(actual)) {
return true;
}
comparedObjects.add(actual);
Copy link
Member

Choose a reason for hiding this comment

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

I did not verify that but I believe the circular structure won't always work this way. AFAIC we have to handle both sides similar to the current implementation.

Fixes: nodejs#50399

Co-Authored-By: Cristian Barlutiu <[email protected]>
@puskin94 puskin94 force-pushed the assert-deep-match-and-includes branch from d58ffe7 to c47e009 Compare November 6, 2024 13:31
Copy link
Member

@tniessen tniessen left a comment

Choose a reason for hiding this comment

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

I think the documentation should precisely characterize the behavior of this function, including edge cases. Right now, it only mentions the "main difference", but that doesn't explain differences such as the following:

assert.deepStrictEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }]))
// No error.

assert.partialDeepStrictEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }]))
// Uncaught AssertionError.

On a side note, the commit message should use an imperative verb (e.g., add) instead of adds to comply with our guidelines.

@@ -2548,6 +2548,81 @@ assert.throws(throwingFirst, /Second$/);
Due to the confusing error-prone notation, avoid a string as the second
argument.

## `assert.partialDeepStrictEqual(actual, expected[, message])`
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 probably be marked as experimental.

* `expected` {any}
* `message` {string|Error}

[`assert.partialDeepStrictEqual()`][] Assesses the equivalence between the `actual` and `expected` parameters through a
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
[`assert.partialDeepStrictEqual()`][] Assesses the equivalence between the `actual` and `expected` parameters through a
[`assert.partialDeepStrictEqual()`][] asserts the equivalence between the `actual` and `expected` parameters through a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem. author ready PRs that have at least one approval, no pending requests for changes, and a CI started. needs-ci PRs that need a full CI run. notable-change PRs with changes that should be highlighted in changelogs. semver-minor PRs that contain new features and should be released in the next minor version. tsc-agenda Issues and PRs to discuss during the meetings of the TSC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: assert.matchObject