All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest Maven / Gradle / Ivy

/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.instrumentation.testing.junit.http;

import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_HEADERS;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.CAPTURE_PARAMETERS;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.INDEXED_CHILD;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.QUERY_PARAM;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.SUCCESS;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.instrumentation.testing.GlobalTraceUtil;
import io.opentelemetry.instrumentation.testing.InstrumentationTestRunner;
import io.opentelemetry.sdk.testing.assertj.SpanDataAssert;
import io.opentelemetry.sdk.testing.assertj.TraceAssert;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.data.StatusData;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import io.opentelemetry.testing.internal.armeria.client.WebClient;
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest;
import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
import io.opentelemetry.testing.internal.armeria.common.HttpData;
import io.opentelemetry.testing.internal.armeria.common.HttpMethod;
import io.opentelemetry.testing.internal.armeria.common.HttpRequest;
import io.opentelemetry.testing.internal.armeria.common.HttpRequestBuilder;
import io.opentelemetry.testing.internal.armeria.common.MediaType;
import io.opentelemetry.testing.internal.armeria.common.QueryParams;
import io.opentelemetry.testing.internal.armeria.common.RequestHeaders;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class AbstractHttpServerTest {
  private static final Logger logger = LoggerFactory.getLogger(AbstractHttpServerTest.class);

  public static final String TEST_REQUEST_HEADER = "X-Test-Request";
  public static final String TEST_RESPONSE_HEADER = "X-Test-Response";

  public static final String TEST_CLIENT_IP = "1.1.1.1";
  public static final String TEST_USER_AGENT = "test-user-agent";

  private final HttpServerTestOptions options = new HttpServerTestOptions();
  private InstrumentationTestRunner testing;
  private SERVER server;
  public WebClient client;
  public int port;
  public URI address;

  protected abstract SERVER setupServer();

  protected abstract void stopServer(SERVER server);

  protected final InstrumentationTestRunner testing() {
    return testing;
  }

  @BeforeAll
  void setupOptions() {
    options.expectedServerSpanNameMapper = this::expectedServerSpanName;
    options.expectedHttpRoute = this::expectedHttpRoute;

    configure(options);

    if (address == null) {
      address = buildAddress();
    }

    server = setupServer();
    if (server != null) {
      logger.info(
          getClass().getName()
              + " http server started at: http://localhost:"
              + port
              + options.contextPath);
    }
  }

  @AfterAll
  void cleanup() {
    if (server == null) {
      logger.info(getClass().getName() + " can't stop null server");
      return;
    }
    stopServer(server);
    server = null;
    logger.info(getClass().getName() + " http server stopped at: http://localhost:" + port + "/");
  }

  protected URI buildAddress() {
    try {
      return new URI("http://localhost:" + port + options.contextPath + "/");
    } catch (URISyntaxException exception) {
      throw new IllegalStateException(exception);
    }
  }

  protected void configure(HttpServerTestOptions options) {}

  @BeforeEach
  void verifyExtension() {
    if (testing == null) {
      throw new AssertionError(
          "Subclasses of AbstractHttpServerTest must register HttpServerInstrumentationExtension");
    }
  }

  public static  T controller(ServerEndpoint endpoint, Supplier closure) {
    assert Span.current().getSpanContext().isValid() : "Controller should have a parent span.";
    if (endpoint == NOT_FOUND) {
      return closure.get();
    }
    return GlobalTraceUtil.runWithSpan("controller", () -> closure.get());
  }

  String resolveAddress(ServerEndpoint uri) {
    String url = uri.resolvePath(address).toString();
    // Force HTTP/1 via h1c so upgrade requests don't show up as traces
    url = url.replace("http://", "h1c://");
    if (uri.getQuery() != null) {
      url += "?" + uri.getQuery();
    }
    return url;
  }

  private AggregatedHttpRequest request(ServerEndpoint uri, String method) {
    return AggregatedHttpRequest.of(HttpMethod.valueOf(method), resolveAddress(uri));
  }

  @ParameterizedTest
  @ValueSource(ints = {1, 4, 50})
  void successfulGetRequest(int count) {
    String method = "GET";
    AggregatedHttpRequest request = request(SUCCESS, method);
    List responses = new ArrayList<>();
    for (int i = 0; i < count; i++) {
      responses.add(client.execute(request).aggregate().join());
    }

    for (AggregatedHttpResponse response : responses) {
      assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
      assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());
    }

    assertTheTraces(count, null, null, method, SUCCESS, responses.get(0));
  }

  @Test
  void successfulGetRequestWithParent() {
    String method = "GET";
    String traceId = "00000000000000000000000000000123";
    String parentId = "0000000000000456";
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            // intentionally sending mixed-case "tracePARENT" to make sure that TextMapGetters are
            // not case-sensitive
            request(SUCCESS, method).headers().toBuilder()
                .set("tracePARENT", "00-" + traceId + "-" + parentId + "-01")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());

    assertTheTraces(1, traceId, parentId, "GET", SUCCESS, response);
  }

  @Test
  void tracingHeaderIsCaseInsensitive() {
    String method = "GET";
    String traceId = "00000000000000000000000000000123";
    String parentId = "0000000000000456";
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            request(SUCCESS, method).headers().toBuilder()
                .set("TRACEPARENT", "00-" + traceId + "-" + parentId + "-01")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(SUCCESS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(SUCCESS.getBody());

    assertTheTraces(1, traceId, parentId, "GET", SUCCESS, response);
  }

  @ParameterizedTest
  @EnumSource(
      value = ServerEndpoint.class,
      names = {"SUCCESS", "QUERY_PARAM"})
  void requestWithQueryString(ServerEndpoint endpoint) {
    String method = "GET";
    AggregatedHttpRequest request = request(endpoint, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(endpoint.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(endpoint.getBody());

    assertTheTraces(1, null, null, method, endpoint, response);
  }

  @Test
  void requestWithRedirect() {
    assumeTrue(options.testRedirect);

    String method = "GET";
    AggregatedHttpRequest request = request(REDIRECT, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(REDIRECT.getStatus());
    assertThat(response.headers().get("location"))
        .satisfiesAnyOf(
            location -> assertThat(location).isEqualTo(REDIRECT.getBody()),
            location ->
                assertThat(new URI(location).normalize().toString())
                    .isEqualTo(address.resolve(REDIRECT.getBody()).toString()));

    assertTheTraces(1, null, null, method, REDIRECT, response);
  }

  @Test
  void requestWithError() {
    assumeTrue(options.testError);

    String method = "GET";
    AggregatedHttpRequest request = request(ERROR, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(ERROR.getStatus());
    if (options.testErrorBody) {
      assertThat(response.contentUtf8()).isEqualTo(ERROR.getBody());
    }

    assertTheTraces(1, null, null, method, ERROR, response);
  }

  @Test
  void requestWithException() {
    assumeTrue(options.testException);

    // async servlet tests may produce uncaught exceptions
    // awaitility rethrows uncaught exceptions while it is waiting on a condition
    Awaitility.doNotCatchUncaughtExceptionsByDefault();
    try {
      String method = "GET";
      AggregatedHttpRequest request = request(EXCEPTION, method);
      AggregatedHttpResponse response = client.execute(request).aggregate().join();

      assertThat(response.status().code()).isEqualTo(EXCEPTION.getStatus());

      assertTheTraces(1, null, null, method, EXCEPTION, response);
    } finally {
      Awaitility.reset();
    }
  }

  @Test
  void requestForNotFound() {
    assumeTrue(options.testNotFound);

    String method = "GET";
    AggregatedHttpRequest request = request(NOT_FOUND, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(NOT_FOUND.getStatus());

    assertTheTraces(1, null, null, method, NOT_FOUND, response);
  }

  @Test
  void requestWithPathParameter() {
    assumeTrue(options.testPathParam);

    String method = "GET";
    AggregatedHttpRequest request = request(PATH_PARAM, method);
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(PATH_PARAM.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(PATH_PARAM.getBody());

    assertTheTraces(1, null, null, method, PATH_PARAM, response);
  }

  @Test
  void captureHttpHeaders() {
    assumeTrue(options.testCaptureHttpHeaders);

    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            request(CAPTURE_HEADERS, "GET").headers().toBuilder()
                .add(TEST_REQUEST_HEADER, "test")
                .build());
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(CAPTURE_HEADERS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(CAPTURE_HEADERS.getBody());
    assertThat(response.headers().get("X-Test-Response")).isEqualTo("test");

    assertTheTraces(1, null, null, "GET", CAPTURE_HEADERS, response);
  }

  @Test
  void captureRequestParameters() {
    assumeTrue(options.testCaptureRequestParameters);

    QueryParams formBody = QueryParams.builder().add("test-parameter", "test value õäöü").build();
    AggregatedHttpRequest request =
        AggregatedHttpRequest.of(
            RequestHeaders.builder(HttpMethod.POST, resolveAddress(CAPTURE_PARAMETERS))
                .contentType(MediaType.FORM_DATA)
                .build(),
            HttpData.ofUtf8(formBody.toQueryString()));
    AggregatedHttpResponse response = client.execute(request).aggregate().join();

    assertThat(response.status().code()).isEqualTo(CAPTURE_PARAMETERS.getStatus());
    assertThat(response.contentUtf8()).isEqualTo(CAPTURE_PARAMETERS.getBody());

    assertTheTraces(1, null, null, "POST", CAPTURE_PARAMETERS, response);
  }

  /**
   * This test fires a bunch of parallel request to the fixed backend endpoint. That endpoint is
   * supposed to create a new child span in the context of the SERVER span. That child span is
   * expected to have an attribute called "test.request.id". The value of that attribute should be
   * the value of request's parameter called "id".
   *
   * 

This test then asserts that there is the correct number of traces (one per request executed) * and that each trace has exactly three spans and both first and the last spans have * "test.request.id" attribute with equal value. Server span is not going to have that attribute * because it is not under the control of this test. * *

This way we verify that child span created by the server actually corresponds to the client * request. */ @Test void highConcurrency() throws InterruptedException { int count = 100; ServerEndpoint endpoint = INDEXED_CHILD; CountDownLatch latch = new CountDownLatch(count); TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); TextMapSetter setter = HttpRequestBuilder::header; for (int i = 0; i < count; i++) { int index = i; HttpRequestBuilder request = HttpRequest.builder() // Force HTTP/1 via h1c so upgrade requests don't show up as traces .get(endpoint.resolvePath(address).toString().replace("http://", "h1c://")) .queryParam(ServerEndpoint.ID_PARAMETER_NAME, index); testing.runWithSpan( "client " + index, () -> { Span.current().setAttribute(ServerEndpoint.ID_ATTRIBUTE_NAME, index); propagator.inject(Context.current(), request, setter); client .execute(request.build()) .aggregate() .whenComplete((result, throwable) -> latch.countDown()); }); } latch.await(); assertHighConcurrency(count); } protected void assertHighConcurrency(int count) { ServerEndpoint endpoint = INDEXED_CHILD; List> assertions = new ArrayList<>(); for (int i = 0; i < count; i++) { assertions.add( trace -> { SpanData rootSpan = trace.getSpan(0); // Traces can be in arbitrary order, let us find out the request id of the current one int requestId = Integer.parseInt(rootSpan.getName().substring("client ".length())); List> spanAssertions = new ArrayList<>(); spanAssertions.add( span -> span.hasName(rootSpan.getName()) .hasKind(SpanKind.INTERNAL) .hasNoParent() .hasAttributesSatisfying( attrs -> assertThat(attrs) .containsEntry(ServerEndpoint.ID_ATTRIBUTE_NAME, requestId))); spanAssertions.add( span -> assertIndexedServerSpan(span, requestId).hasParent(rootSpan)); if (options.hasHandlerSpan.test(endpoint)) { spanAssertions.add( span -> assertHandlerSpan(span, "GET", endpoint).hasParent(trace.getSpan(1))); } int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> assertIndexedControllerSpan(span, requestId) .hasParent(trace.getSpan(parentIndex))); trace.hasSpansSatisfyingExactly(spanAssertions); }); } testing.waitAndAssertTraces(assertions); } // NOTE: this method does not currently implement asserting all the span types that groovy // HttpServerTest does protected void assertTheTraces( int size, String traceId, String parentId, String method, ServerEndpoint endpoint, AggregatedHttpResponse response) { List> assertions = new ArrayList<>(); for (int i = 0; i < size; i++) { assertions.add( trace -> { List> spanAssertions = new ArrayList<>(); spanAssertions.add( span -> { assertServerSpan(span, method, endpoint); if (parentId != null) { span.hasTraceId(traceId).hasParentSpanId(parentId); } else { span.hasNoParent(); } }); if (options.hasHandlerSpan.test(endpoint)) { spanAssertions.add( span -> { assertHandlerSpan(span, method, endpoint); span.hasParent(trace.getSpan(0)); }); } if (endpoint != NOT_FOUND) { int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> { assertControllerSpan( span, endpoint == EXCEPTION ? options.expectedException : null); span.hasParent(trace.getSpan(parentIndex)); }); } if (options.hasResponseSpan.test(endpoint)) { int parentIndex = spanAssertions.size() - 1; spanAssertions.add( span -> { assertResponseSpan(span, method, endpoint); span.hasParent(trace.getSpan(parentIndex)); }); } if (options.hasErrorPageSpans.test(endpoint)) { spanAssertions.addAll(errorPageSpanAssertions(method, endpoint)); } trace.hasSpansSatisfyingExactly(spanAssertions); }); } testing.waitAndAssertTraces(assertions); } protected SpanDataAssert assertControllerSpan(SpanDataAssert span, Throwable expectedException) { span.hasName("controller").hasKind(SpanKind.INTERNAL); if (expectedException != null) { span.hasStatus(StatusData.error()); span.hasException(expectedException); } return span; } protected SpanDataAssert assertHandlerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertHandlerSpan not implemented in " + getClass().getName()); } protected SpanDataAssert assertResponseSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "assertResponseSpan not implemented in " + getClass().getName()); } protected List> errorPageSpanAssertions( String method, ServerEndpoint endpoint) { throw new UnsupportedOperationException( "errorPageSpanAssertions not implemented in " + getClass().getName()); } protected SpanDataAssert assertServerSpan( SpanDataAssert span, String method, ServerEndpoint endpoint) { Set> httpAttributes = options.httpAttributes.apply(endpoint); String expectedRoute = options.expectedHttpRoute.apply(endpoint); String name = expectedRoute != null ? expectedRoute : options.expectedServerSpanNameMapper.apply(endpoint, method); span.hasName(name).hasKind(SpanKind.SERVER); if (endpoint.status >= 500) { span.hasStatus(StatusData.error()); } if (endpoint == EXCEPTION && options.hasExceptionOnServerSpan.test(endpoint)) { span.hasException(options.expectedException); } span.hasAttributesSatisfying( attrs -> { if (httpAttributes.contains(SemanticAttributes.NET_TRANSPORT)) { assertThat(attrs) .containsEntry( SemanticAttributes.NET_TRANSPORT, SemanticAttributes.NetTransportValues.IP_TCP); } // TODO: Move to test knob rather than always treating as optional if (attrs.get(SemanticAttributes.NET_HOST_NAME) != null) { assertThat(attrs).containsEntry(SemanticAttributes.NET_HOST_NAME, "localhost"); } if (attrs.get(SemanticAttributes.NET_HOST_PORT) != null) { assertThat(attrs).containsEntry(SemanticAttributes.NET_HOST_PORT, port); } if (attrs.get(AttributeKey.longKey("net.sock.peer.port")) != null) { assertThat(attrs) .hasEntrySatisfying( AttributeKey.longKey("net.sock.peer.port"), value -> assertThat(value) .isInstanceOf(Long.class) .isNotEqualTo(Long.valueOf(port))); } if (attrs.get(AttributeKey.stringKey("net.sock.peer.addr")) != null) { assertThat(attrs) .containsEntry( AttributeKey.stringKey("net.sock.peer.addr"), options.sockPeerAddr.apply(endpoint)); } if (attrs.get(AttributeKey.stringKey("net.sock.host.addr")) != null) { assertThat(attrs) .containsEntry(AttributeKey.stringKey("net.sock.host.addr"), "127.0.0.1"); } assertThat(attrs) .hasEntrySatisfying( SemanticAttributes.HTTP_CLIENT_IP, entry -> assertThat(entry) .satisfiesAnyOf( value -> assertThat(value).isNull(), value -> assertThat(value).isEqualTo(TEST_CLIENT_IP))); assertThat(attrs).containsEntry(SemanticAttributes.HTTP_METHOD, method); assertThat(attrs).containsEntry(SemanticAttributes.HTTP_STATUS_CODE, endpoint.status); assertThat(attrs) .hasEntrySatisfying( SemanticAttributes.HTTP_FLAVOR, entry -> assertThat(entry).isIn("1.1", "2.0")); assertThat(attrs).containsEntry(SemanticAttributes.HTTP_USER_AGENT, TEST_USER_AGENT); assertThat(attrs).containsEntry(SemanticAttributes.HTTP_SCHEME, "http"); if (endpoint != INDEXED_CHILD) { assertThat(attrs) .containsEntry( SemanticAttributes.HTTP_TARGET, endpoint.resolvePath(address).getPath() + (endpoint == QUERY_PARAM ? "?" + endpoint.body : "")); } if (attrs.get(SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH) != null) { assertThat(attrs) .hasEntrySatisfying( SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH, entry -> assertThat(entry).isNotNegative()); } if (attrs.get(SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH) != null) { assertThat(attrs) .hasEntrySatisfying( SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH, entry -> assertThat(entry).isNotNegative()); } if (httpAttributes.contains(SemanticAttributes.HTTP_ROUTE) && expectedRoute != null) { assertThat(attrs).containsEntry(SemanticAttributes.HTTP_ROUTE, expectedRoute); } if (endpoint == CAPTURE_HEADERS) { assertThat(attrs) .containsEntry("http.request.header.x_test_request", new String[] {"test"}); assertThat(attrs) .containsEntry("http.response.header.x_test_response", new String[] {"test"}); } if (endpoint == CAPTURE_PARAMETERS) { assertThat(attrs) .containsEntry( "servlet.request.parameter.test_parameter", new String[] {"test value õäöü"}); } }); return span; } protected SpanDataAssert assertIndexedServerSpan(SpanDataAssert span, int requestId) { ServerEndpoint endpoint = INDEXED_CHILD; String method = "GET"; assertServerSpan(span, method, endpoint); span.hasAttributesSatisfying( attrs -> assertThat(attrs) .containsEntry( SemanticAttributes.HTTP_TARGET, endpoint.resolvePath(address).getPath() + "?id=" + requestId)); return span; } protected SpanDataAssert assertIndexedControllerSpan(SpanDataAssert span, int requestId) { span.hasName("controller") .hasKind(SpanKind.INTERNAL) .hasAttributesSatisfying( attrs -> assertThat(attrs).containsEntry(ServerEndpoint.ID_ATTRIBUTE_NAME, requestId)); return span; } public String expectedServerSpanName(ServerEndpoint endpoint, String method) { String route = expectedHttpRoute(endpoint); return route == null ? "HTTP " + method : route; } public String expectedHttpRoute(ServerEndpoint endpoint) { // no need to compute route if we're not expecting it if (!options.httpAttributes.apply(endpoint).contains(SemanticAttributes.HTTP_ROUTE)) { return null; } switch (endpoint) { case NOT_FOUND: return null; case PATH_PARAM: return options.contextPath + "/path/:id/param"; default: return endpoint.resolvePath(address).getPath(); } } final void setTesting(InstrumentationTestRunner testing, WebClient client, int port) { setTesting(testing, client, port, null); } final void setTesting( InstrumentationTestRunner testing, WebClient client, int port, URI address) { this.testing = testing; this.client = client; this.port = port; this.address = address; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy