Skip to content

Commit

Permalink
Synchronous API for test
Browse files Browse the repository at this point in the history
Introduce "TestRequestSender" to provide send operation API for test.
The change also includes exception handling(convert checked exception
to unchecked).

This will be an ongoing effort to replace/cleanup existing tests.
As a first example, `TestUserService` has been updated to use new API.

Change-Id: I11f237eb34c57c7d5f12461d99329ceabf9c3ccc
  • Loading branch information
ttddyy committed Aug 12, 2016
1 parent c20d3d9 commit 7a262f0
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2014-2016 VMware, Inc. 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.vmware.xenon.common.test;

/**
* Utility class for handling java exceptions for test.
*/
public class ExceptionTestUtils {
private ExceptionTestUtils() {
}

/**
* Throw any {@link Throwable} as unchecked exception.
* This does not wrap the {@link Throwable}, rather uses java trick to throw it as is.
* TODO: more documentation
*/
public static RuntimeException throwAsUnchecked(Throwable throwable) {
throwsUnchecked(throwable);

// allowing this method to be used with return statement. this line will never be executed.
return null;
}

@SuppressWarnings("unchecked")
private static <T extends Exception> void throwsUnchecked(Throwable throwable) throws T {
throw (T) throwable;
}

public static void executeSafely(ExecutableBlock block) {
try {
block.execute();
} catch (Throwable throwable) {
throwsUnchecked(throwable);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2014-2016 VMware, Inc. 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.vmware.xenon.common.test;

/**
* Represent code block that may throw {@link Throwable}
*/
@FunctionalInterface
public interface ExecutableBlock {
void execute() throws Throwable;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,81 +13,141 @@

package com.vmware.xenon.common.test;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import com.vmware.xenon.common.Operation.CompletionHandler;
import com.vmware.xenon.common.Utils;

/**
* Test context used for synchronous tests. Provides an isolated version of the
* {@code VerificationHost} testStart and testWait methods and allows nesting
*/
public class TestContext {

private CountDownLatch latch;
private long expiration;

private Duration interval = Duration.ofSeconds(1);

private Duration duration;

private volatile Throwable error;

private boolean started;

/**
* Consider using {@link #TestContext(int, Duration)}
* This method exists for backward compatibility, and may be deprecated/deleted in future.
*/
public static TestContext create(int count, long expIntervalMicros) {
TestContext ctx = new TestContext();
ctx.latch = new CountDownLatch(count);
ctx.expiration = Utils.getNowMicrosUtc();
ctx.expiration += expIntervalMicros;
return ctx;
return new TestContext(count, Duration.of(expIntervalMicros, ChronoUnit.MICROS));
}

public void completeIteration() {
public TestContext(int count, Duration duration) {
this.latch = new CountDownLatch(count);
this.duration = duration;
}

public void complete() {
this.started = true;
this.latch.countDown();
}

public void failIteration(Throwable e) {
public void fail(Throwable e) {
this.started = true;
this.error = e;
this.latch.countDown();
}

public void await() throws Throwable {
if (this.latch == null) {
throw new IllegalStateException("This context is already used");
public void setCount(int count) {
if (this.started) {
throw new RuntimeException(String.format(
"%s has already started. count=%d", getClass().getSimpleName(), this.latch.getCount()));
}
this.latch = new CountDownLatch(count);
}

// keep polling latch every second, allows for easier debugging
while (Utils.getNowMicrosUtc() < this.expiration) {
if (this.latch.await(1, TimeUnit.SECONDS)) {
break;
}
public void setTimeout(Duration duration) {
if (this.started) {
throw new RuntimeException(String.format(
"%s has already started. count=%d", getClass().getSimpleName(), this.latch.getCount()));
}
this.duration = duration;
}

if (this.expiration < Utils.getNowMicrosUtc()) {
throw new TimeoutException();
}
public void setCheckInterval(Duration interval) {
this.interval = interval;
}

// prevent this latch from being reused
this.latch = null;
public void await() {

if (this.error != null) {
throw this.error;
}
ExceptionTestUtils.executeSafely(() -> {

return;
if (this.latch == null) {
throw new IllegalStateException("This context is already used");
}

LocalDateTime expireAt = LocalDateTime.now().plus(this.duration);
// keep polling latch every interval
while (expireAt.isAfter(LocalDateTime.now())) {
if (this.latch.await(this.interval.toNanos(), TimeUnit.NANOSECONDS)) {
break;
}
}

LocalDateTime now = LocalDateTime.now();
if (expireAt.isBefore(now)) {
Duration difference = Duration.between(expireAt, now);

throw new TimeoutException(String.format(
"%s has expired. [duration=%s, count=%d]", getClass().getSimpleName(), difference,
this.latch.getCount()));
}

// prevent this latch from being reused
this.latch = null;

if (this.error != null) {
throw this.error;
}
});
}

/**
* Consider using {@link #complete()}.
* This method exists for backward compatibility, and may be deprecated/deleted in future.
*/
public void completeIteration() {
complete();
}

/**
* Consider using {@link #fail(Throwable)}.
* This method exists for backward compatibility, and may be deprecated/deleted in future.
*/
public void failIteration(Throwable e) {
fail(e);
}

public CompletionHandler getCompletion() {
return (o, e) -> {
if (e != null) {
this.failIteration(e);
this.fail(e);
} else {
this.completeIteration();
this.complete();
}
};
}

public CompletionHandler getExpectedFailureCompletion() {
return (o, e) -> {
if (e != null) {
this.completeIteration();
this.complete();
} else {
this.failIteration(new IllegalStateException("got success, expected failure"));
this.fail(new IllegalStateException("got success, expected failure"));
}
};
}
Expand Down
Loading

0 comments on commit 7a262f0

Please sign in to comment.