Skip to content

Commit

Permalink
WIP commit - see #808
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael1993 committed Mar 30, 2024
1 parent c3f59cd commit 22535df
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 38 deletions.
26 changes: 19 additions & 7 deletions docs/free-port.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,31 @@

== Introduction

Sometimes we want a free port in our tests so that we can either start a server on it or
to test a failure scenario when there is no service at given port.
Sometimes we want a free socket in our tests so that we can either start a server on it or to test a failure scenario when there is no service at the given port.

It's a JUnit Jupiter extension which provides a free port for above-mentioned use cases.
Also, there is no guarantee that the port will still be available by the time you use it.
FreePort is an implementation of link:/docs/resources.adoc[`ResourceFactory`] that opens a `java.net.ServerSocket`.
You can use this simple utility in one of two ways.

NOTE: You can retry the test in such cases based on `FreePort.isFreeNow`
1. Create a new socket for your test with `@New`.
2. Create a socket that's shared between tests with `@Shared`.

You can read more about how these annotations work in the link:/docs/resources.adoc[Resource Extension documentation].

You don't have to worry about closing the socket.
This is done automatically at the end of their respective scope (e.g.: the test, in case of `@New`).

== Usage

To get the `FreePort` as an argument on a test just add the annotation to your test suite as shown in the following example:
If you just need any random port open for a single test, you can do:

[source,java]
[source,java,indent=0]
----
include::{demo}[tag=basic_free_port_example]
----

If you need a specific port open, you can supply your arguments in the annotation:

[source,java,indent=0]
----
include::{demo}[tag=specific_port_example]
----
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ public class FreePortDemo {

// tag::basic_free_port_example[]
@Test
void testFreePort(@NewPort ServerSocket port) {
void testFreePort(@New(FreePort.class) ServerSocket port) {
assertThat(port).isNotNull();
assertThat(port.isClosed()).isFalse();
}
// end::basic_free_port_example[]

// tag::specific_port_example[]
@Test
void testSpecificPort(@New(value = FreePort.class, arguments = "1234") ServerSocket port) {
assertThat(port).isNotNull();
assertThat(port.isClosed()).isFalse();
assertThat(port.getLocalPort()).isEqualTo(1234);
}
// end::specific_port_example[]

}
22 changes: 21 additions & 1 deletion src/main/java/org/junitpioneer/jupiter/resource/FreePort.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@

package org.junitpioneer.jupiter.resource;

import static java.lang.String.format;

import java.io.IOException;
import java.net.ServerSocket;
import java.util.List;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junitpioneer.internal.PioneerPreconditions;

public final class FreePort implements ResourceFactory<ServerSocket> {

public FreePort() {
Expand All @@ -22,7 +27,18 @@ public FreePort() {

@Override
public Resource<ServerSocket> create(List<String> arguments) throws Exception {
return new FreePortResource();
if (arguments.isEmpty())
return new FreePortResource();
else {
try {
int port = Integer.parseInt(arguments.get(0));
return new FreePortResource(port);
}
catch (NumberFormatException exception) {
throw new ExtensionConfigurationException(
format("Could not parse port number %s for opening a socket", arguments.get(0)));
}
}
}

private static final class FreePortResource implements Resource<ServerSocket> {
Expand All @@ -33,6 +49,10 @@ private static final class FreePortResource implements Resource<ServerSocket> {
this.serverSocket = new ServerSocket(0);
}

FreePortResource(int port) throws IOException {
this.serverSocket = new ServerSocket(port);
}

@Override
public ServerSocket get() {
return serverSocket;
Expand Down
22 changes: 0 additions & 22 deletions src/main/java/org/junitpioneer/jupiter/resource/NewPort.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,18 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
parameterContext.getParameter(), testMethodDescription(extensionContext));
// @formatter:on
throw new ParameterResolutionException(message);
} else if (parameterContext.isAnnotated(Shared.Named.class) && findSharedOnClass(extensionContext).isPresent()) {
throw new ParameterResolutionException(format(
"Parameter [%s] in %s is annotated with @Shared.Named but the resource has not been created. Are you missing a @Shared annotation on your test class?",
parameterContext.getParameter(), testMethodDescription(extensionContext)));
}
return parameterContext.isAnnotated(New.class) || parameterContext.isAnnotated(Shared.class);
}

private Optional<Shared> findSharedOnClass(ExtensionContext extensionContext) {
return AnnotationSupport.findAnnotation(extensionContext.getTestClass(), Shared.class);
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
Expand All @@ -79,9 +87,18 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
return checkType(resource, parameterContext.getParameter().getType());
}

Optional<Shared.Named> namedAnnotation = parameterContext.findAnnotation(Shared.Named.class);
if (namedAnnotation.isPresent()) {
// This is checked earlier in supportsParameter
Shared.Scope scope = findSharedOnClass(extensionContext).orElseThrow().scope();
ExtensionContext.Store store = scopedStore(extensionContext, scope);
Object resource = store.get(resourceKey(namedAnnotation.get().value()));
return checkType(resource, parameterContext.getParameter().getType());
}

// @formatter:off
String message = format(
"Parameter [%s] in %s is not annotated with @New or @Shared",
"Parameter [%s] in %s is not annotated with @New, @Shared or @Named",
parameterContext.getParameter(), testMethodDescription(extensionContext));
// @formatter:on
throw new ParameterResolutionException(message);
Expand Down Expand Up @@ -143,7 +160,7 @@ private Object resolveShared(Shared sharedAnnotation, Parameter[] parameters, Ex
ResourceFactory.class);
Resource<?> resource = scopedStore
.getOrComputeIfAbsent( //
resourceKey(sharedAnnotation), //
resourceKey(sharedAnnotation.name()), //
__ -> newResource(sharedAnnotation, resourceFactory), //
Resource.class);
putNewLockForShared(sharedAnnotation, scopedStore);
Expand Down Expand Up @@ -286,8 +303,8 @@ private String factoryKey(Shared sharedAnnotation) {
return sharedAnnotation.name() + " resource factory";
}

private String resourceKey(Shared sharedAnnotation) {
return sharedAnnotation.name() + " resource";
private String resourceKey(String sharedAnnotationName) {
return sharedAnnotationName + " resource";
}

private String resourceLockKey(Shared sharedAnnotation) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/resource/Shared.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@ enum Scope {

}

@interface Named {

String value();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
* <li>{@link org.junitpioneer.jupiter.resource.Dir}</li>
* </ul>
*
* <p>Check out the following types for details on the "free port/server socket" extension:</p>
*
* <ul>
* <li>{@link org.junitpioneer.jupiter.resource.FreePort}</li>
* </ul>
*
* <p>Check out the following types for details on resources in general:</p>
*
* <ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@ public class FreePortExtensionTests {
@Test
@DisplayName("resolve FreePort parameter successfully")
void testFreePortParameterResolution() {
ExecutionResults results = executeTestClass(FreePortTestCase.class);
ExecutionResults results = executeTestClass(FreePortTestCaseTests.class);
assertThat(results).hasSingleSucceededTest();
}

static class FreePortTestCase {
static class FreePortTestCaseTests {

@Test
void testFreePortParameterResolution(@NewPort ServerSocket port) {
void testFreePortParameterResolution(@New(FreePort.class) ServerSocket port) {
Assertions.assertThat(port).isNotNull();
}

@Test
void testFreePortArgumentResolution(@New(value = FreePort.class, arguments = "1334") ServerSocket port) {
Assertions.assertThat(port).isNotNull();
Assertions.assertThat(port.getLocalPort()).isEqualTo(1334);
}

}

}

0 comments on commit 22535df

Please sign in to comment.