| // 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(); | |
| } | |
| } | |
| } |