function innerAssertImplementation(var1, var2, path) {
    if (var1 === var2) {
      return;
    }

    var typeOfVar1 = _trueTypeOf(var1);
    var typeOfVar2 = _trueTypeOf(var2);

    if (typeOfVar1 === typeOfVar2) {
      // For two objects of the same type, if one is a prototype of another, use
      // the custom equality function for the more generic of the two
      // prototypes, if available.
      if (var1 && typeof var1 === 'object') {
        try {
          const o1 = /** @type {!Object} */ (var1);
          const o2 = /** @type {!Object} */ (var2);
          const comparator =
              goog.testing.asserts.getMostSpecificCustomEquality(o1, o2);
          if (comparator) {
            const result = goog.testing.asserts.applyCustomEqualityFunction(
                comparator, o1, o2, path);
            if (result != null) {
              if (path) {
                failures.push(path + ': ' + result);
              } else {
                rootFailure = result;
              }
            }
            return;
          }
        } catch (e) {
          // Catch and log errors from custom comparators but fall back onto
          // ordinary comparisons. Such errors can occur, e.g. with proxies or
          // when the prototypes of a polyfill are not traversable.
          //
          // If you see a failure due to this line, please do not use
          // findDifferences or assertObjectEquals on these argument types.
          goog.global.console.error('Error in custom comparator: ' + e);
        }
      }

      const isArrayBuffer = typeOfVar1 === 'ArrayBuffer';
      if (isArrayBuffer) {
        // Since ArrayBuffer instances can't themselves be iterated through,
        // compare 1-byte-per-element views of them.
        var1 = new Uint8Array(/** @type {!ArrayBuffer} */ (var1));
        var2 = new Uint8Array(/** @type {!ArrayBuffer} */ (var2));
      }
      const isArray =
          isArrayBuffer || goog.testing.asserts.ARRAY_TYPES[typeOfVar1];
      var errorMessage = equalityPredicate(typeOfVar1, var1, var2);
      if (errorMessage !=
          goog.testing.asserts.EQUALITY_PREDICATE_CANT_PROCESS) {
        if (errorMessage !=
            goog.testing.asserts.EQUALITY_PREDICATE_VARS_ARE_EQUAL) {
          if (path) {
            failures.push(path + ': ' + errorMessage);
          } else {
            rootFailure = errorMessage;
          }
        }
      } else if (isArray && var1.length != var2.length) {
        failures.push(
            (path ? path + ': ' : '') + 'Expected ' + var1.length +
            '-element array ' +
            'but got a ' + var2.length + '-element array');
      } else if (typeOfVar1 == 'String') {
        // If the comparer cannot process strings (eg, roughlyEquals).
        if (var1 != var2) {
          const error = goog.testing.asserts.getDefaultErrorMsg_(var1, var2);
          if (path) {
            failures.push(path + ': ' + error);
          } else {
            rootFailure = error;
          }
        }
      } else {
        var childPath = path + (isArray ? '[%s]' : (path ? '.%s' : '%s'));
        // These type checks do not use _trueTypeOf because that does not work
        // for polyfilled Map/Set. Note that these checks may potentially fail
        // if var1 comes from a different window.
        if ((typeof Map != 'undefined' && var1 instanceof Map) ||
            (typeof Set != 'undefined' && var1 instanceof Set)) {
          var1.forEach(function(value, key) {
            'use strict';
            if (var2.has(key)) {
              // For a map, the values must be compared, but with Set, checking
              // that the second set contains the first set's "keys" is
              // sufficient.
              if (var2.get) {
                innerAssertWithCycleCheck(
                    // NOTE: replace will call functions, so stringify eagerly.
                    value, var2.get(key), childPath.replace('%s', String(key)));
              }
            } else {
              failures.push(
                  key + ' not present in actual ' + (path || typeOfVar2));
            }
          });

          var2.forEach(function(value, key) {
            'use strict';
            if (!var1.has(key)) {
              failures.push(
                  key + ' not present in expected ' + (path || typeOfVar1));
            }
          });
        } else if (!var1['__iterator__']) {
          // if an object has an __iterator__ property, we have no way of
          // actually inspecting its raw properties, and JS 1.7 doesn't
          // overload [] to make it possible for someone to generically
          // use what the iterator returns to compare the object-managed
          // properties. This gets us into deep poo with things like
          // goog.structs.Map, at least on systems that support iteration.
          for (var prop in var1) {
            if (isArray && goog.testing.asserts.isArrayIndexProp_(prop)) {
              // Skip array indices for now. We'll handle them later.
              continue;
            }

            if (prop in var2) {
              innerAssertWithCycleCheck(
                  var1[prop], var2[prop], childPath.replace('%s', prop));
            } else {
              failures.push(
                  'property ' + prop + ' not present in actual ' +
                  (path || typeOfVar2));
            }
          }
          // make sure there aren't properties in var2 that are missing
          // from var1. if there are, then by definition they don't
          // match.
          for (var prop in var2) {
            if (isArray && goog.testing.asserts.isArrayIndexProp_(prop)) {
              // Skip array indices for now. We'll handle them later.
              continue;
            }

            if (!(prop in var1)) {
              failures.push(
                  'property ' + prop + ' not present in expected ' +
                  (path || typeOfVar1));
            }
          }

          // Handle array indices by iterating from 0 to arr.length.
          //
          // Although all browsers allow holes in arrays, browsers
          // are inconsistent in what they consider a hole. For example,
          // "[0,undefined,2]" has a hole on IE but not on Firefox.
          //
          // Because our style guide bans for...in iteration over arrays,
          // we assume that most users don't care about holes in arrays,
          // and that it is ok to say that a hole is equivalent to a slot
          // populated with 'undefined'.
          if (isArray) {
            for (prop = 0; prop < var1.length; prop++) {
              innerAssertWithCycleCheck(
                  var1[prop], var2[prop],
                  childPath.replace('%s', String(prop)));
            }
          }
        } else {
          // special-case for closure objects that have iterators
          if (typeof var1.equals === 'function') {
            // use the object's own equals function, assuming it accepts an
            // object and returns a boolean
            if (!var1.equals(var2)) {
              failures.push(
                  'equals() returned false for ' + (path || typeOfVar1));
            }
          } else if (var1.map_) {
            // assume goog.structs.Map or goog.structs.Set, where comparing
            // their private map_ field is sufficient
            innerAssertWithCycleCheck(
                var1.map_, var2.map_, childPath.replace('%s', 'map_'));
          } else {
            // else die, so user knows we can't do anything
            failures.push(
                'unable to check ' + (path || typeOfVar1) +
                ' for equality: it has an iterator we do not ' +
                'know how to handle. please add an equals method');
          }
        }
      }
    } else if (path) {
      failures.push(
          path + ': ' + goog.testing.asserts.getDefaultErrorMsg_(var1, var2));
    } else {
      rootFailure = goog.testing.asserts.getDefaultErrorMsg_(var1, var2);
    }
  }
@param {*} var1 An item in the expected object.
@param {*} var2 The corresponding item in the actual object.
@param {string} path Their path in the objects.
@suppress {missingProperties} The map_ property is unknown to the compiler
    unless goog.structs.Map is loaded.