blob: 48438bbc5a9d89e28a41e1ce3c4e11fa4fa97c52 [file]
// Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
import com.android.tools.r8.utils.internal.IterableUtils;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.IsEqual;
/** Matcher for a collection of items that matches a collection of matchers one-to-one. */
public class UnorderedCollectionMatcher<T> extends TypeSafeMatcher<Iterable<? extends T>> {
private final List<Matcher<? super T>> matchers;
private final Function<? super T, String> toStringFunction;
private UnorderedCollectionMatcher(
List<Matcher<? super T>> matchers, Function<? super T, String> toStringFunction) {
this.matchers = matchers;
this.toStringFunction = toStringFunction;
}
public static <T> Matcher<Iterable<? extends T>> matchesOneToOne(
Iterable<? extends Matcher<? super T>> matchers) {
return matchesOneToOne(matchers, null);
}
public static <T> Matcher<Iterable<? extends T>> matchesItemsOneToOne(
Iterable<? super T> matchers) {
return matchesOneToOne(IterableUtils.transform(matchers, IsEqual::equalTo), null);
}
public static <T> Matcher<Iterable<? extends T>> matchesOneToOne(
Iterable<? extends Matcher<? super T>> matchers,
Function<? super T, String> toStringFunction) {
return new UnorderedCollectionMatcher<>(ImmutableList.copyOf(matchers), toStringFunction);
}
@Override
protected boolean matchesSafely(Iterable<? extends T> items) {
return match(items).isFullMatch();
}
@Override
public void describeTo(Description description) {
description.appendText("a collection matching matchers one-to-one: ");
description.appendList("[", ", ", "]", matchers);
}
private void appendItem(Description description, T item) {
if (toStringFunction != null) {
description.appendText(toStringFunction.apply(item));
} else {
description.appendValue(item);
}
}
@Override
protected void describeMismatchSafely(
Iterable<? extends T> items, Description mismatchDescription) {
MatchResult result = match(items);
mismatchDescription.appendText("was not matched one-to-one:");
if (!result.unmatchedItems.isEmpty()) {
mismatchDescription.appendText("\nUnmatched items:");
for (T item : result.unmatchedItems) {
mismatchDescription.appendText("\n - ");
appendItem(mismatchDescription, item);
}
}
if (!result.unmatchedMatchers.isEmpty()) {
mismatchDescription.appendText("\nUnmatched matchers:");
for (Matcher<? super T> matcher : result.unmatchedMatchers) {
mismatchDescription.appendText("\n - ");
mismatchDescription.appendDescriptionOf(matcher);
}
}
if (!result.matchedItems.isEmpty()) {
mismatchDescription.appendText("\nMatched pairs:");
for (int i = 0; i < result.matchedItems.size(); i++) {
mismatchDescription.appendText("\n - ");
appendItem(mismatchDescription, result.matchedItems.get(i));
mismatchDescription.appendText("\n (Matched by:) ");
mismatchDescription.appendDescriptionOf(result.matchedMatchers.get(i));
}
}
}
private MatchResult match(Iterable<? extends T> items) {
List<T> itemList = new ArrayList<>();
items.forEach(itemList::add);
MatchResult result = new MatchResult();
Set<Integer> availableMatchers = new LinkedHashSet<>();
for (int i = 0; i < matchers.size(); i++) {
availableMatchers.add(i);
}
// For each item, find the first available matcher that matches it.
for (T item : itemList) {
Iterator<Integer> matcherIterator = availableMatchers.iterator();
boolean found = false;
while (matcherIterator.hasNext()) {
int m = matcherIterator.next();
if (matchers.get(m).matches(item)) {
matcherIterator.remove();
result.matchedItems.add(item);
result.matchedMatchers.add(matchers.get(m));
found = true;
break;
}
}
if (!found) {
result.unmatchedItems.add(item);
}
}
for (int m : availableMatchers) {
result.unmatchedMatchers.add(matchers.get(m));
}
assert (result.matchedItems.size() + result.unmatchedItems.size() == itemList.size());
assert (result.matchedMatchers.size() + result.unmatchedMatchers.size() == matchers.size());
assert (result.matchedItems.size() == result.matchedMatchers.size());
return result;
}
private class MatchResult {
final List<T> matchedItems = new ArrayList<>();
final List<Matcher<? super T>> matchedMatchers = new ArrayList<>();
final List<T> unmatchedItems = new ArrayList<>();
final List<Matcher<? super T>> unmatchedMatchers = new ArrayList<>();
boolean isFullMatch() {
return unmatchedItems.isEmpty() && unmatchedMatchers.isEmpty();
}
}
}