Java tutorial
// Copyright 2016 The Nomulus 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 google.registry.testing; import static com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig.getLocalTaskQueue; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Predicates.in; import static com.google.common.base.Predicates.not; import static com.google.common.collect.Iterables.getFirst; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Multisets.containsOccurrences; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import com.google.appengine.api.taskqueue.dev.QueueStateInfo; import com.google.appengine.api.taskqueue.dev.QueueStateInfo.HeaderWrapper; import com.google.appengine.api.taskqueue.dev.QueueStateInfo.TaskStateInfo; import com.google.common.base.Ascii; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultiset; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.net.HttpHeaders; import com.google.common.net.MediaType; import google.registry.dns.DnsConstants; import google.registry.model.ImmutableObject; import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import javax.annotation.Nonnull; import org.joda.time.Duration; /** Static utility functions for testing task queues. */ public class TaskQueueHelper { /** * Matcher to match against the tasks in the task queue. Fields that aren't set are not compared. */ public static class TaskMatcher implements Predicate<TaskStateInfo> { MatchableTaskInfo expected = new MatchableTaskInfo(); public TaskMatcher taskName(String taskName) { expected.taskName = taskName; return this; } public TaskMatcher url(String url) { expected.url = url; return this; } public TaskMatcher method(String method) { expected.method = method; return this; } public TaskMatcher payload(String payload) { checkState(expected.params.isEmpty(), "Cannot add a payload to a TaskMatcher with params."); expected.payload = payload; return this; } public TaskMatcher tag(String tag) { expected.tag = tag; return this; } public TaskMatcher header(String name, String value) { // Lowercase for case-insensitive comparison. expected.headers.put(Ascii.toLowerCase(name), value); return this; } public TaskMatcher param(String key, String value) { checkState(expected.payload == null, "Cannot add params to a TaskMatcher with a payload."); expected.params.put(key, value); return this; } public TaskMatcher etaDelta(Duration lowerBound, Duration upperBound) { checkState(!lowerBound.isShorterThan(Duration.ZERO), "lowerBound must be non-negative."); checkState(upperBound.isLongerThan(lowerBound), "upperBound must be greater than lowerBound."); expected.etaDeltaLowerBound = lowerBound.getStandardSeconds(); expected.etaDeltaUpperBound = upperBound.getStandardSeconds(); return this; } /** * Returns {@code true} if there are not more occurrences in {@code sub} of each of its entries * than there are in {@code super}. */ private static boolean containsEntries(Multimap<?, ?> superMultimap, Multimap<?, ?> subMultimap) { return containsOccurrences(ImmutableMultiset.copyOf(superMultimap.entries()), ImmutableMultiset.copyOf(subMultimap.entries())); } /** * Returns true if the fields set on the current object match the given task info. This is not * quite the same contract as {@link #equals}, since it will ignore null fields. * * <p>Match fails if any headers or params expected on the TaskMatcher are not found on the * TaskStateInfo. Note that the inverse is not true (i.e. there may be extra headers on the * TaskStateInfo). */ @Override public boolean apply(@Nonnull TaskStateInfo info) { MatchableTaskInfo actual = new MatchableTaskInfo(info); return (expected.taskName == null || Objects.equals(expected.taskName, actual.taskName)) && (expected.url == null || Objects.equals(expected.url, actual.url)) && (expected.method == null || Objects.equals(expected.method, actual.method)) && (expected.payload == null || Objects.equals(expected.payload, actual.payload)) && (expected.tag == null || Objects.equals(expected.tag, actual.tag)) && (expected.etaDeltaLowerBound == null || expected.etaDeltaLowerBound <= actual.etaDelta) && (expected.etaDeltaUpperBound == null || expected.etaDeltaUpperBound >= actual.etaDelta) && containsEntries(actual.params, expected.params) && containsEntries(actual.headers, expected.headers); } @Override public String toString() { return Joiner.on('\n').withKeyValueSeparator(":\n") .join(Maps.transformValues(expected.toMap(), new Function<Object, String>() { @Override public String apply(Object input) { return "\t" + String.valueOf(input).replaceAll("\n", "\n\t"); } })); } } /** Returns the info object for the provided queue name. */ public static QueueStateInfo getQueueInfo(String queueName) { return getLocalTaskQueue().getQueueStateInfo().get(queueName); } /** * Ensures that the tasks in the named queue are exactly those with the expected property * values after being transformed with the provided property getter function. */ public static void assertTasksEnqueuedWithProperty(String queueName, Function<TaskStateInfo, String> propertyGetter, String... expectedTaskProperties) throws Exception { // Ordering is irrelevant but duplicates should be considered independently. assertThat(transform(getQueueInfo(queueName).getTaskInfo(), propertyGetter)) .containsExactly((Object[]) expectedTaskProperties); } /** Ensures that the tasks in the named queue are exactly those with the expected names. */ public static void assertTasksEnqueued(String queueName, String... expectedTaskNames) throws Exception { Function<TaskStateInfo, String> nameGetter = new Function<TaskStateInfo, String>() { @Nonnull @Override public String apply(@Nonnull TaskStateInfo taskStateInfo) { return taskStateInfo.getTaskName(); } }; assertTasksEnqueuedWithProperty(queueName, nameGetter, expectedTaskNames); } /** * Ensures that the only tasks in the named queue are exactly those that match the expected * matchers. */ public static void assertTasksEnqueued(String queueName, TaskMatcher... taskMatchers) throws Exception { assertTasksEnqueued(queueName, Arrays.asList(taskMatchers)); } /** * Ensures that the only tasks in the named queue are exactly those that match the expected * matchers. */ public static void assertTasksEnqueued(String queueName, List<TaskMatcher> taskMatchers) throws Exception { QueueStateInfo qsi = getQueueInfo(queueName); assertThat(qsi.getTaskInfo()).hasSize(taskMatchers.size()); LinkedList<TaskStateInfo> taskInfos = new LinkedList<>(qsi.getTaskInfo()); for (final TaskMatcher taskMatcher : taskMatchers) { try { taskInfos.remove(Iterables.find(taskInfos, taskMatcher)); } catch (NoSuchElementException e) { final Map<String, Object> taskMatcherMap = taskMatcher.expected.toMap(); assert_().fail("Task not found in queue %s:\n\n%s\n\nPotential candidate match diffs:\n\n%s", queueName, taskMatcher, FluentIterable.from(taskInfos).transform(new Function<TaskStateInfo, String>() { @Override public String apply(TaskStateInfo input) { return prettyPrintEntityDeepDiff(taskMatcherMap, Maps.filterKeys( new MatchableTaskInfo(input).toMap(), in(taskMatcherMap.keySet()))); } }).join(Joiner.on('\n'))); } } } /** Empties the task queue. */ public static void clearTaskQueue(String queueName) throws Exception { getLocalTaskQueue().flushQueue(queueName); } /** Asserts at least one task exists in {@code queue}. */ public static void assertAtLeastOneTaskIsEnqueued(String queue) throws Exception { assertThat(getQueueInfo(queue).getCountTasks()).isGreaterThan(0); } /** Ensures that the named queue contains no tasks. */ public static void assertNoTasksEnqueued(String... queueNames) throws Exception { for (String queueName : queueNames) { assertThat(getQueueInfo(queueName).getCountTasks()).isEqualTo(0); } } /** Returns the value for the param on a task info, or empty if it is missing. */ private static String getParamFromTaskInfo(TaskStateInfo taskInfo, String paramName) { return getFirst(UriParameters.parse(taskInfo.getBody()).get(paramName), ""); } /** Ensures that the DNS queue tasks are exactly those for the expected target names. */ public static void assertDnsTasksEnqueued(String... expectedTaskTargetNames) throws Exception { assertTasksEnqueuedWithProperty(DnsConstants.DNS_PULL_QUEUE_NAME, new Function<TaskStateInfo, String>() { @Nonnull @Override public String apply(@Nonnull TaskStateInfo taskStateInfo) { return getParamFromTaskInfo(taskStateInfo, DnsConstants.DNS_TARGET_NAME_PARAM); } }, expectedTaskTargetNames); } /** Ensures that the DNS queue does not contain any tasks. */ public static void assertNoDnsTasksEnqueued() throws Exception { assertNoTasksEnqueued(DnsConstants.DNS_PULL_QUEUE_NAME); } /** An adapter to clean up a {@link TaskStateInfo} for ease of matching. */ private static class MatchableTaskInfo extends ImmutableObject { String taskName; String method; String url; String payload; String tag; Double etaDelta; Long etaDeltaLowerBound; Long etaDeltaUpperBound; Multimap<String, String> headers = ArrayListMultimap.create(); Multimap<String, String> params = ArrayListMultimap.create(); MatchableTaskInfo() { } MatchableTaskInfo(TaskStateInfo info) { URI uri; try { uri = new URI(info.getUrl()); } catch (java.net.URISyntaxException e) { throw new IllegalArgumentException(e); } this.taskName = info.getTaskName(); this.method = info.getMethod(); this.url = uri.getPath(); this.payload = info.getBody(); this.etaDelta = info.getEtaDelta(); if (info.getTagAsBytes() != null) { this.tag = new String(info.getTagAsBytes(), UTF_8); } ImmutableMultimap.Builder<String, String> headerBuilder = new ImmutableMultimap.Builder<>(); for (HeaderWrapper header : info.getHeaders()) { // Lowercase header name for comparison since HTTP // header names are case-insensitive. headerBuilder.put(Ascii.toLowerCase(header.getKey()), header.getValue()); } this.headers = headerBuilder.build(); ImmutableMultimap.Builder<String, String> inputParams = new ImmutableMultimap.Builder<>(); String query = uri.getQuery(); if (query != null) { inputParams.putAll(UriParameters.parse(query)); } if (headers.containsEntry(Ascii.toLowerCase(HttpHeaders.CONTENT_TYPE), MediaType.FORM_DATA.toString())) { inputParams.putAll(UriParameters.parse(info.getBody())); } this.params = inputParams.build(); } public Map<String, Object> toMap() { Map<String, Object> builder = new HashMap<>(); builder.put("taskName", taskName); builder.put("url", url); builder.put("method", method); builder.put("headers", headers.asMap()); builder.put("params", params.asMap()); builder.put("payload", payload); builder.put("tag", tag); builder.put("etaDelta", etaDelta); builder.put("etaDeltaLowerBound", etaDeltaLowerBound); builder.put("etaDeltaUpperBound", etaDeltaUpperBound); return Maps.filterValues(builder, not(in(asList(null, "", Collections.EMPTY_MAP)))); } } }