Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FreePort resource to create ServerSocket #733

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@
url: /docs/simple-arguments-aggregator/
- title: "Vintage @Test"
url: /docs/vintage-test/
- title: "Free port"
url: /docs/free-port/
37 changes: 37 additions & 0 deletions docs/free-port.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
:page-title: Free Port
:page-description: Extends JUnit Jupiter with `@FreePort` to get a free port.
:xp-demo-dir: ../src/demo/java
:demo: {xp-demo-dir}/org/junitpioneer/jupiter/resource/FreePortDemo.java

== Introduction

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.

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.

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

If you just need any random port open for a single test, you can do:

[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]
----

WARNING: The extension makes no guarantees that the port will still be available by the time you use it!
38 changes: 38 additions & 0 deletions src/demo/java/org/junitpioneer/jupiter/resource/FreePortDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2016-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.resource;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.ServerSocket;

import org.junit.jupiter.api.Test;

public class FreePortDemo {

// tag::basic_free_port_example[]
@Test
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[]

}
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
exports org.junitpioneer.jupiter.cartesian;
exports org.junitpioneer.jupiter.params;
exports org.junitpioneer.jupiter.json;
exports org.junitpioneer.jupiter.resource;
exports org.junitpioneer.jupiter.converter;

opens org.junitpioneer.vintage to org.junit.platform.commons;
Expand Down
94 changes: 94 additions & 0 deletions src/main/java/org/junitpioneer/jupiter/resource/FreePort.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

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;

/**
* Resource factory for creating a {@link ServerSocket}.
* This can be done via the {@link Shared} or {@link New} annotations.
*
* <p>For more details and examples, see
* <a href="https://junit-pioneer.org/docs/free-port/" target="_top">the documentation on the FreePort extension</a>.</p>
*
* <p>This extension is based on the JUnit Pioneer abstract extension for resources.</p>
* <p>For more information about that, see
* <a href="https://junit-pioneer.org/docs/resources/" target="_top">the documentation on the resource extension.</a>.</p>
*
* @see Shared
* @see New
*/
public final class FreePort implements ResourceFactory<ServerSocket> {
Michael1993 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Resource factories should not be instantiated directly, only
* by using {@code @New} or {@code @Shared}.
*/
public FreePort() {
// recreate default constructor to prevent compiler warning
}

@Override
public Resource<ServerSocket> create(List<String> arguments) throws Exception {
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)));
}
}
}

/**
* Wrapper/resource class for creating a {@link ServerSocket} on a specific port.
* If no port number is specified then the port number is automatically allocated,
* typically from an ephemeral port range.
*
* <p>ServerSocket instances get closed automatically by the resource extension.</p>
*
* @see ServerSocket#ServerSocket(int)
*/
private static final class FreePortResource implements Resource<ServerSocket> {

private final ServerSocket serverSocket;

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

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

@Override
public ServerSocket get() {
return serverSocket;
}

@Override
public void close() throws IOException {
this.serverSocket.close();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,19 @@ 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 +88,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 +161,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 +304,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 @@ -43,6 +43,10 @@
*/
public final class TemporaryDirectory implements ResourceFactory<Path> {

public TemporaryDirectory() {
// recreate default constructor to prevent compiler warning
}

@Override
public Resource<Path> create(List<String> arguments) throws Exception {
if (arguments.size() >= 2) {
Expand Down
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
3 changes: 2 additions & 1 deletion src/test/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
exports org.junitpioneer.jupiter.params;
exports org.junitpioneer.jupiter.json;
exports org.junitpioneer.jupiter.converter;
exports org.junitpioneer.jupiter.resource;

opens org.junitpioneer.vintage to org.junit.platform.commons;
opens org.junitpioneer.jupiter to org.junit.platform.commons, nl.jqno.equalsverifier;
opens org.junitpioneer.jupiter.cartesian to org.junit.platform.commons;
opens org.junitpioneer.jupiter.issue to org.junit.platform.commons;
opens org.junitpioneer.jupiter.params to org.junit.platform.commons;
opens org.junitpioneer.jupiter.resource to org.junit.platform.commons;
opens org.junitpioneer.jupiter.resource to nl.jqno.equalsverifier, org.junit.platform.commons;
opens org.junitpioneer.jupiter.json to org.junit.platform.commons, com.fasterxml.jackson.databind;
opens org.junitpioneer.jupiter.converter to org.junit.platform.commons;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/

package org.junitpioneer.jupiter.resource;

import static org.junitpioneer.testkit.PioneerTestKit.executeTestClass;
import static org.junitpioneer.testkit.assertion.PioneerAssert.assertThat;

import java.net.ServerSocket;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junitpioneer.testkit.ExecutionResults;

@DisplayName("Free port extension")
public class FreePortExtensionTests {

@Test
@DisplayName("resolve FreePort parameter successfully")
void testFreePortParameterResolution() {
ExecutionResults results = executeTestClass(FreePortTestCaseTests.class);
assertThat(results).hasNumberOfSucceededTests(2);
}

static class FreePortTestCaseTests {

@Test
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);
}

}

}