Break circular dependency between core and util (#379)

* Break circular dependency between core and util

Created a new :common project and moved a minimum
number of classes to break the circular dependency
between the two projects. This gets rid of the
gradle lint dependency warnings.

Also separated api classes and testing helpers into
separate source sets in :common so that testing
classes may be restricted to test configurations.
This commit is contained in:
Weimin Yu 2019-11-21 15:36:55 -05:00 committed by GitHub
parent 98414cb7cb
commit 9f0e24132a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 487 additions and 2742 deletions

View file

@ -1,68 +0,0 @@
// Copyright 2017 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 google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.joda.time.DateTimeZone.UTC;
import static org.joda.time.Duration.millis;
import google.registry.util.Clock;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.ThreadSafe;
import org.joda.time.DateTime;
import org.joda.time.ReadableDuration;
import org.joda.time.ReadableInstant;
/** A mock clock for testing purposes that supports telling, setting, and advancing the time. */
@ThreadSafe
public final class FakeClock implements Clock {
private static final long serialVersionUID = 675054721685304599L;
// Clock isn't a thread synchronization primitive, but tests involving
// threads should see a consistent flow.
private final AtomicLong currentTimeMillis = new AtomicLong();
/** Creates a FakeClock that starts at START_OF_TIME. */
public FakeClock() {
this(START_OF_TIME);
}
/** Creates a FakeClock initialized to a specific time. */
public FakeClock(ReadableInstant startTime) {
setTo(startTime);
}
/** Returns the current time. */
@Override
public DateTime nowUtc() {
return new DateTime(currentTimeMillis.get(), UTC);
}
/** Advances clock by one millisecond. */
public void advanceOneMilli() {
advanceBy(millis(1));
}
/** Advances clock by some duration. */
public void advanceBy(ReadableDuration duration) {
currentTimeMillis.addAndGet(duration.getMillis());
}
/** Sets the time to the specified instant. */
public void setTo(ReadableInstant time) {
currentTimeMillis.set(time.getMillis());
}
}

View file

@ -1,51 +0,0 @@
// Copyright 2017 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.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import google.registry.util.Sleeper;
import java.io.Serializable;
import javax.annotation.concurrent.ThreadSafe;
import org.joda.time.ReadableDuration;
/** Sleeper implementation for unit tests that advances {@link FakeClock} rather than sleep. */
@ThreadSafe
public final class FakeSleeper implements Sleeper, Serializable {
private static final long serialVersionUID = -8975804222581077291L;
private final FakeClock clock;
public FakeSleeper(FakeClock clock) {
this.clock = checkNotNull(clock, "clock");
}
@Override
public void sleep(ReadableDuration duration) throws InterruptedException {
checkArgument(duration.getMillis() >= 0);
if (Thread.interrupted()) {
throw new InterruptedException();
}
clock.advanceBy(duration);
}
@Override
public void sleepUninterruptibly(ReadableDuration duration) {
checkArgument(duration.getMillis() >= 0);
clock.advanceBy(duration);
}
}

View file

@ -1,64 +0,0 @@
// Copyright 2017 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 com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.flogger.FluentLogger;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import javax.annotation.concurrent.ThreadSafe;
/** Utility class for getting system information in tests. */
@ThreadSafe
public final class SystemInfo {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final LoadingCache<String, Boolean> hasCommandCache =
CacheBuilder.newBuilder()
.build(
new CacheLoader<String, Boolean>() {
@Override
public Boolean load(String cmd) throws InterruptedException {
try {
Process pid = Runtime.getRuntime().exec(cmd);
pid.getOutputStream().close();
pid.waitFor();
} catch (IOException e) {
logger.atWarning().withCause(e).log("%s command not available", cmd);
return false;
}
return true;
}
});
/**
* Returns {@code true} if system command can be run from path.
*
* <p><b>Warning:</b> The command is actually run! So there could be side-effects. You might
* need to specify a version flag or something. Return code is ignored.
*
* <p>This result is a memoized. If multiple therads try to get the same result at once, the
* heavy lifting will only be performed by the first thread and the rest will wait.
*
* @throws ExecutionException
*/
public static boolean hasCommand(String cmd) throws ExecutionException {
return hasCommandCache.get(cmd);
}
}

View file

@ -0,0 +1,71 @@
// Copyright 2017 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.util;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.testing.NullPointerTester;
import com.google.common.util.concurrent.UncheckedExecutionException;
import google.registry.testing.AppEngineRule;
import java.util.function.Function;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link Concurrent}. */
@RunWith(JUnit4.class)
public class ConcurrentTest {
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.build();
@Test
public void testTransform_emptyList_returnsEmptyList() {
assertThat(Concurrent.transform(ImmutableList.of(), x -> x)).isEmpty();
}
@Test
public void testTransform_addIntegers() {
assertThat(Concurrent.transform(ImmutableList.of(1, 2, 3), input -> input + 1))
.containsExactly(2, 3, 4)
.inOrder();
}
@Test
public void testTransform_throwsException_isSinglyWrappedByUee() {
UncheckedExecutionException e =
assertThrows(
UncheckedExecutionException.class,
() ->
Concurrent.transform(
ImmutableList.of(1, 2, 3),
input -> {
throw new RuntimeException("hello");
}));
assertThat(e).hasCauseThat().isInstanceOf(RuntimeException.class);
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("hello");
}
@Test
public void testNullness() {
NullPointerTester tester = new NullPointerTester().setDefault(Function.class, x -> x);
tester.testAllPublicStaticMethods(Concurrent.class);
}
}

View file

@ -0,0 +1,125 @@
// Copyright 2017 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.util;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.appengine.api.log.LogQuery;
import com.google.appengine.api.log.LogService;
import com.google.appengine.api.log.RequestLogs;
import com.google.apphosting.api.ApiProxy;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.LoggerConfig;
import com.google.common.testing.TestLogHandler;
import google.registry.testing.AppEngineRule;
import java.util.logging.Level;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link RequestStatusCheckerImpl}. */
@RunWith(JUnit4.class)
public final class RequestStatusCheckerImplTest {
private static final TestLogHandler logHandler = new TestLogHandler();
private static final RequestStatusChecker requestStatusChecker = new RequestStatusCheckerImpl();
/**
* Matcher for the expected LogQuery in {@link RequestStatusCheckerImpl#isRunning}.
*
* Because LogQuery doesn't have a .equals function, we have to create an actual matcher to make
* sure we have the right argument in our mocks.
*/
private static LogQuery expectedLogQuery(final String requestLogId) {
return argThat(
object -> {
assertThat(object).isInstanceOf(LogQuery.class);
assertThat(object.getRequestIds()).containsExactly(requestLogId);
assertThat(object.getIncludeAppLogs()).isFalse();
assertThat(object.getIncludeIncomplete()).isTrue();
return true;
});
}
@Rule
public AppEngineRule appEngineRule = AppEngineRule.builder().build();
@Before public void setUp() {
LoggerConfig.getConfig(RequestStatusCheckerImpl.class).addHandler(logHandler);
RequestStatusCheckerImpl.logService = mock(LogService.class);
}
@After public void tearDown() {
LoggerConfig.getConfig(RequestStatusCheckerImpl.class).removeHandler(logHandler);
}
// If a logId is unrecognized, it could be that the log hasn't been uploaded yet - so we assume
// it's a request that has just started running recently.
@Test public void testIsRunning_unrecognized() {
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
.thenReturn(ImmutableList.of());
assertThat(requestStatusChecker.isRunning("12345678")).isTrue();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "Queried an unrecognized requestLogId");
}
@Test public void testIsRunning_notFinished() {
RequestLogs requestLogs = new RequestLogs();
requestLogs.setFinished(false);
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
.thenReturn(ImmutableList.of(requestLogs));
assertThat(requestStatusChecker.isRunning("12345678")).isTrue();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "isFinished: false");
}
@Test public void testIsRunning_finished() {
RequestLogs requestLogs = new RequestLogs();
requestLogs.setFinished(true);
when(RequestStatusCheckerImpl.logService.fetch(expectedLogQuery("12345678")))
.thenReturn(ImmutableList.of(requestLogs));
assertThat(requestStatusChecker.isRunning("12345678")).isFalse();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "isFinished: true");
}
@Test public void testGetLogId_returnsRequestLogId() {
String expectedLogId = ApiProxy.getCurrentEnvironment().getAttributes().get(
"com.google.appengine.runtime.request_log_id").toString();
assertThat(requestStatusChecker.getLogId()).isEqualTo(expectedLogId);
}
@Test public void testGetLogId_createsLog() {
requestStatusChecker.getLogId();
assertAboutLogs()
.that(logHandler)
.hasLogAtLevelWithMessage(Level.INFO, "Current requestLogId: ");
}
}

View file

@ -0,0 +1,145 @@
// Copyright 2018 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.util;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import static google.registry.testing.TaskQueueHelper.getQueueInfo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.common.collect.ImmutableList;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeSleeper;
import org.joda.time.DateTime;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link TaskQueueUtils}. */
@RunWith(JUnit4.class)
public final class TaskQueueUtilsTest {
private static final int MAX_RETRIES = 3;
@Rule
public final AppEngineRule appEngine =
AppEngineRule.builder().withDatastore().withTaskQueue().build();
private int origBatchSize;
private final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
private final FakeSleeper sleeper = new FakeSleeper(clock);
private final TaskQueueUtils taskQueueUtils =
new TaskQueueUtils(new Retrier(sleeper, MAX_RETRIES));
private final Queue queue = mock(Queue.class);
private final TaskOptions task = withUrl("url").taskName("name");
private final TaskHandle handle = new TaskHandle(task, "handle");
@Before
public void before() {
origBatchSize = TaskQueueUtils.BATCH_SIZE;
TaskQueueUtils.BATCH_SIZE = 2;
}
@After
public void after() {
TaskQueueUtils.BATCH_SIZE = origBatchSize;
}
@Test
public void testEnqueue_worksOnFirstTry_doesntSleep() {
when(queue.add(ImmutableList.of(task))).thenReturn(ImmutableList.of(handle));
assertThat(taskQueueUtils.enqueue(queue, task)).isSameInstanceAs(handle);
verify(queue).add(ImmutableList.of(task));
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01TZ"));
}
@Test
public void testEnqueue_twoTransientErrorsThenSuccess_stillWorksAfterSleeping() {
when(queue.add(ImmutableList.of(task)))
.thenThrow(new TransientFailureException(""))
.thenThrow(new TransientFailureException(""))
.thenReturn(ImmutableList.of(handle));
assertThat(taskQueueUtils.enqueue(queue, task)).isSameInstanceAs(handle);
verify(queue, times(3)).add(ImmutableList.of(task));
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01T00:00:00.6Z")); // 200 + 400ms
}
@Test
public void testEnqueue_multiple() {
TaskOptions taskA = withUrl("a").taskName("a");
TaskOptions taskB = withUrl("b").taskName("b");
ImmutableList<TaskHandle> handles =
ImmutableList.of(new TaskHandle(taskA, "a"), new TaskHandle(taskB, "b"));
when(queue.add(ImmutableList.of(taskA, taskB))).thenReturn(handles);
assertThat(taskQueueUtils.enqueue(queue, ImmutableList.of(taskA, taskB)))
.isSameInstanceAs(handles);
assertThat(clock.nowUtc()).isEqualTo(DateTime.parse("2000-01-01TZ"));
}
@Test
public void testEnqueue_maxRetries_givesUp() {
when(queue.add(ImmutableList.of(task)))
.thenThrow(new TransientFailureException("one"))
.thenThrow(new TransientFailureException("two"))
.thenThrow(new TransientFailureException("three"))
.thenThrow(new TransientFailureException("four"));
TransientFailureException thrown =
assertThrows(TransientFailureException.class, () -> taskQueueUtils.enqueue(queue, task));
assertThat(thrown).hasMessageThat().contains("three");
}
@Test
public void testEnqueue_transientErrorThenInterrupt_throwsTransientError() {
when(queue.add(ImmutableList.of(task))).thenThrow(new TransientFailureException(""));
try {
Thread.currentThread().interrupt();
assertThrows(TransientFailureException.class, () -> taskQueueUtils.enqueue(queue, task));
} finally {
Thread.interrupted(); // Clear interrupt state so it doesn't pwn other tests.
}
}
@Test
public void testDeleteTasks_usesMultipleBatches() {
Queue defaultQ = QueueFactory.getQueue("default");
TaskOptions taskOptA = withUrl("/a").taskName("a");
TaskOptions taskOptB = withUrl("/b").taskName("b");
TaskOptions taskOptC = withUrl("/c").taskName("c");
taskQueueUtils.enqueue(defaultQ, ImmutableList.of(taskOptA, taskOptB, taskOptC));
assertThat(getQueueInfo("default").getTaskInfo()).hasSize(3);
taskQueueUtils.deleteTasks(
defaultQ,
ImmutableList.of(
new TaskHandle(taskOptA, "default"),
new TaskHandle(taskOptB, "default"),
new TaskHandle(taskOptC, "default")));
assertThat(getQueueInfo("default").getTaskInfo()).hasSize(0);
}
}

View file

@ -0,0 +1,109 @@
// Copyright 2017 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.util;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.MediaType.CSV_UTF_8;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import static google.registry.util.UrlFetchUtils.setPayloadMultipart;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import google.registry.testing.AppEngineRule;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link UrlFetchUtils}. */
@RunWith(JUnit4.class)
public class UrlFetchUtilsTest {
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.build();
private final Random random = mock(Random.class);
@Before
public void setupRandomZeroes() {
doAnswer(
info -> {
Arrays.fill((byte[]) info.getArguments()[0], (byte) 0);
return null;
})
.when(random)
.nextBytes(any(byte[].class));
}
@Test
public void testSetPayloadMultipart() {
HTTPRequest request = mock(HTTPRequest.class);
setPayloadMultipart(
request,
"lol",
"cat",
CSV_UTF_8,
"The nice people at the store say hello. ヘ(◕。◕ヘ)",
random);
ArgumentCaptor<HTTPHeader> headerCaptor = ArgumentCaptor.forClass(HTTPHeader.class);
verify(request, times(2)).addHeader(headerCaptor.capture());
List<HTTPHeader> addedHeaders = headerCaptor.getAllValues();
assertThat(addedHeaders.get(0).getName()).isEqualTo(CONTENT_TYPE);
assertThat(addedHeaders.get(0).getValue())
.isEqualTo(
"multipart/form-data; "
+ "boundary=\"------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"");
assertThat(addedHeaders.get(1).getName()).isEqualTo(CONTENT_LENGTH);
assertThat(addedHeaders.get(1).getValue()).isEqualTo("294");
String payload = "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"
+ "Content-Disposition: form-data; name=\"lol\"; filename=\"cat\"\r\n"
+ "Content-Type: text/csv; charset=utf-8\r\n"
+ "\r\n"
+ "The nice people at the store say hello. ヘ(◕。◕ヘ)\r\n"
+ "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r\n";
verify(request).setPayload(payload.getBytes(UTF_8));
verifyNoMoreInteractions(request);
}
@Test
public void testSetPayloadMultipart_boundaryInPayload() {
HTTPRequest request = mock(HTTPRequest.class);
String payload = "I screamed------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHH";
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
() -> setPayloadMultipart(request, "lol", "cat", CSV_UTF_8, payload, random));
assertThat(thrown)
.hasMessageThat()
.contains(
"Multipart data contains autogenerated boundary: "
+ "------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
}