// Copyright 2017 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.lib.collect.nestedset; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Objects; import com.google.common.collect.HashMultiset; import com.google.common.collect.Multiset; import com.google.devtools.build.lib.actions.CommandLineItem; import com.google.devtools.build.lib.actions.CommandLineItem.CapturingMapFn; import com.google.devtools.build.lib.actions.CommandLineItem.MapFn; import com.google.devtools.build.lib.testutil.MoreAsserts; import com.google.devtools.build.lib.util.Fingerprint; import java.util.function.Consumer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link NestedSetFingerprintCache}. */ @RunWith(JUnit4.class) public class NestedSetFingerprintCacheTest { private class TestNestedSetFingerprintCache extends NestedSetFingerprintCache { private Multiset fingerprinted = HashMultiset.create(); @Override void addToFingerprint(MapFn mapFn, Fingerprint fingerprint, T object) { super.addToFingerprint(mapFn, fingerprint, object); fingerprinted.add(object); } } private TestNestedSetFingerprintCache cache; @Before public void setup() { cache = new TestNestedSetFingerprintCache(); } @Test public void testBasic() { NestedSet nestedSet = NestedSetBuilder.stableOrder().add("a").add("b").build(); // This test does reimplement the inner algorithm of the cache, but serves // as a simple check that the basic operations do something sensible Fingerprint fingerprint = new Fingerprint(); fingerprint.addInt(nestedSet.getOrder().ordinal()); Fingerprint subFingerprint = new Fingerprint(); subFingerprint.addString("a"); subFingerprint.addString("b"); fingerprint.addBytes(subFingerprint.digestAndReset()); String controlDigest = fingerprint.hexDigestAndReset(); Fingerprint nestedSetFingerprint = new Fingerprint(); cache.addNestedSetToFingerprint(nestedSetFingerprint, nestedSet); String nestedSetDigest = nestedSetFingerprint.hexDigestAndReset(); assertThat(controlDigest).isEqualTo(nestedSetDigest); } @Test public void testOnlyFingerprintedOncePerString() { // Leaving leaf nodes with a single item will defeat this check // The nested set builder will effectively inline single-item objects into their parent, // meaning they will get hashed multiple times. NestedSet a = NestedSetBuilder.stableOrder().add("a0").add("a1").build(); NestedSet b = NestedSetBuilder.stableOrder().add("b0").add("b1").build(); NestedSet c = NestedSetBuilder.stableOrder().add("c").addTransitive(a).addTransitive(b).build(); NestedSet d = NestedSetBuilder.stableOrder().add("d").addTransitive(a).addTransitive(b).build(); NestedSet e = NestedSetBuilder.stableOrder().add("e").addTransitive(c).addTransitive(d).build(); cache.addNestedSetToFingerprint(new Fingerprint(), e); assertThat(cache.fingerprinted.elementSet()) .containsExactly("a0", "a1", "b0", "b1", "c", "d", "e"); for (Multiset.Entry entry : cache.fingerprinted.entrySet()) { assertThat(entry.getCount()).isEqualTo(1); } } @Test public void testMapFn() { // Make sure that the map function assigns completely different key spaces NestedSet a = NestedSetBuilder.stableOrder().add("a0").add("a1").build(); Fingerprint defaultMapFnFingerprint = new Fingerprint(); cache.addNestedSetToFingerprint(defaultMapFnFingerprint, a); Fingerprint explicitDefaultMapFnFingerprint = new Fingerprint(); cache.addNestedSetToFingerprint( CommandLineItem.MapFn.DEFAULT, explicitDefaultMapFnFingerprint, a); Fingerprint mappedFingerprint = new Fingerprint(); cache.addNestedSetToFingerprint((s, args) -> args.accept(s + "_mapped"), mappedFingerprint, a); String defaultMapFnDigest = defaultMapFnFingerprint.hexDigestAndReset(); String explicitDefaultMapFnDigest = explicitDefaultMapFnFingerprint.hexDigestAndReset(); String mappedDigest = mappedFingerprint.hexDigestAndReset(); assertThat(defaultMapFnDigest).isEqualTo(explicitDefaultMapFnDigest); assertThat(mappedDigest).isNotEqualTo(defaultMapFnDigest); assertThat(cache.fingerprinted.elementSet()).containsExactly("a0", "a1"); for (Multiset.Entry entry : cache.fingerprinted.entrySet()) { assertThat(entry.getCount()).isEqualTo(2); } } @Test public void testMultipleInstancesOfMapFnThrows() { NestedSet nestedSet = NestedSetBuilder.stableOrder().add("a0").add("a1").build(); // Make sure a normal method reference doesn't get blacklisted. for (int i = 0; i < 2; ++i) { cache.addNestedSetToFingerprint( NestedSetFingerprintCacheTest::simpleExpand, new Fingerprint(), nestedSet); } // Try again to make sure Java synthesizes a new class for a second method reference. for (int i = 0; i < 2; ++i) { cache.addNestedSetToFingerprint( NestedSetFingerprintCacheTest::simpleExpand2, new Fingerprint(), nestedSet); } // Make sure a non-capturing lambda doesn't get blacklisted for (int i = 0; i < 2; ++i) { cache.addNestedSetToFingerprint( (s, args) -> args.accept(s + "_mapped"), new Fingerprint(), nestedSet); } // Make sure a CapturingMapFn doesn't get blacklisted for (int i = 0; i < 2; ++i) { cache.addNestedSetToFingerprint( (CapturingMapFn) (s, args) -> args.accept(s + 1), new Fingerprint(), nestedSet); } // Make sure a ParametrizedMapFn doesn't get blacklisted until it exceeds its instance count cache.addNestedSetToFingerprint(new IntParametrizedMapFn(1), new Fingerprint(), nestedSet); cache.addNestedSetToFingerprint(new IntParametrizedMapFn(2), new Fingerprint(), nestedSet); MoreAsserts.assertThrows( IllegalArgumentException.class, () -> cache.addNestedSetToFingerprint( new IntParametrizedMapFn(3), new Fingerprint(), nestedSet)); // Make sure a capturing method reference gets blacklisted MoreAsserts.assertThrows( IllegalArgumentException.class, () -> { for (int i = 0; i < 2; ++i) { StringJoiner str = new StringJoiner("hello"); cache.addNestedSetToFingerprint(str::expand, new Fingerprint(), nestedSet); } }); // Do make sure that a capturing lambda gets blacklisted MoreAsserts.assertThrows( IllegalArgumentException.class, () -> { for (int i = 0; i < 2; ++i) { final int capturedVariable = i; cache.addNestedSetToFingerprint( (s, args) -> args.accept(s + capturedVariable), new Fingerprint(), nestedSet); } }); } private static class IntParametrizedMapFn extends CommandLineItem.ParametrizedMapFn { private final int i; private IntParametrizedMapFn(int i) { this.i = i; } @Override public void expandToCommandLine(String object, Consumer args) { args.accept(object + i); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } IntParametrizedMapFn that = (IntParametrizedMapFn) o; return i == that.i; } @Override public int maxInstancesAllowed() { return 2; } @Override public int hashCode() { return Objects.hashCode(i); } } private static class StringJoiner { private final String str; private StringJoiner(String str) { this.str = str; } private void expand(String other, Consumer args) { args.accept(str + other); } } private static void simpleExpand(String o, Consumer args) { args.accept(o + "_mapped"); } private static void simpleExpand2(String o, Consumer args) { args.accept(o + "_mapped2"); } }