io.github.microcks.minion.async.AsyncAPITestManager Maven / Gradle / Ivy
/*
* Licensed to Laurent Broudoux (the "Author") under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Author licenses this
* file to you 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 io.github.microcks.minion.async;
import io.github.microcks.domain.EventMessage;
import io.github.microcks.domain.TestReturn;
import io.github.microcks.minion.async.client.MicrocksAPIConnector;
import io.github.microcks.minion.async.client.dto.TestCaseReturnDTO;
import io.github.microcks.minion.async.consumer.ConsumedMessage;
import io.github.microcks.minion.async.consumer.KafkaMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.MQTTMessageConsumptionTask;
import io.github.microcks.minion.async.consumer.MessageConsumptionTask;
import io.github.microcks.minion.async.consumer.WebSocketMessageConsumptionTask;
import io.github.microcks.util.SchemaMap;
import io.github.microcks.util.asyncapi.AsyncAPISchemaValidator;
import com.fasterxml.jackson.databind.JsonNode;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Manager that takes care of launching and running an AsyncAPI test from an
* AsyncTestSpecification.
* @author laurent
*/
@ApplicationScoped
public class AsyncAPITestManager {
@Inject
@RestClient
MicrocksAPIConnector microcksAPIConnector;
@Inject
SchemaRegistry schemaRegistry;
/**
* Launch a new test using this specification. This is a fire and forget operation.
* Tests results will be reported later once finished using a MicrocksAPIConnector.
* @param specification The specification of Async test to launch.
*/
public void launchTest(AsyncTestSpecification specification) {
AsyncAPITestThread thread = new AsyncAPITestThread(specification);
thread.start();
}
/**
* Actually run the Async test by instantiating a MessageConsumptionTask,
* gathering its outputs and validating them against an AsyncAPI schema.
*/
class AsyncAPITestThread extends Thread {
/** Get a JBoss logging logger. */
private final Logger logger = Logger.getLogger(getClass());
private final AsyncTestSpecification specification;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private long startTime;
/**
* Build a new test thread from a test specification.
* @param specification The specification for async test to run.
*/
public AsyncAPITestThread(AsyncTestSpecification specification) {
this.specification = specification;
}
@Override
public void run() {
logger.infof("Launching a new AsyncAPITestThread for {%s} on {%s}",
specification.getTestResultId(), specification.getEndpointUrl());
// Start initialising a TestCaseReturn and build the correct MessageConsumptionTasK.
TestCaseReturnDTO testCaseReturn = new TestCaseReturnDTO(specification.getOperationName());
MessageConsumptionTask messageConsumptionTask = buildMessageConsumptionTask(specification);
// Actually start test counter.
startTime = System.currentTimeMillis();
List outputs = null;
if (messageConsumptionTask != null) {
try {
logger.debugf("Starting consuming messages for {%d} ms", specification.getTimeoutMS());
outputs = executorService.invokeAny(Collections.singletonList(messageConsumptionTask),
specification.getTimeoutMS(), TimeUnit.MILLISECONDS);
logger.debugf("Consumption ends and we got {%d} messages to validate", outputs.size());
} catch (InterruptedException e) {
logger.infof("AsyncAPITestThread for {%s} was interrupted", specification.getTestResultId());
} catch (ExecutionException e) {
logger.errorf(e, "AsyncAPITestThread for {%s} raise an ExecutionException", specification.getTestResultId());
} catch (TimeoutException e) {
// Message consumption has timed-out, add an empty test return with failure and message.
logger.infof("AsyncAPITestThread for {%s} was timed-out", specification.getTestResultId());
testCaseReturn.addTestReturn(
new TestReturn(TestReturn.FAILURE_CODE, specification.getTimeoutMS(),
"Timeout: no message received in " + specification.getTimeoutMS() + " ms",
null, null)
);
} catch (Throwable t) {
// We faced a low-level issue... add an empty test return with failure and message.
logger.error("Caught a low-level throwable", t);
testCaseReturn.addTestReturn(
new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime,
"Exception: low-level failure: " + t.getMessage(),
null, null)
);
} finally {
if (messageConsumptionTask != null) {
try {
messageConsumptionTask.close();
} catch (Throwable t) {
// This was best effort, just ignore the exception...
}
}
executorService.shutdown();
}
} else {
logger.errorf("Found no suitable MessageConsumptionTask implementation. {%s} is not a supported endpoint", specification.getEndpointUrl());
testCaseReturn.addTestReturn(
new TestReturn(TestReturn.FAILURE_CODE, System.currentTimeMillis() - startTime,
"Exception: found no suitable MessageConsumptionTask implementation for endpoint",
null, null)
);
}
// Validate all the received outputs if any.
if (outputs != null && !outputs.isEmpty()) {
validateConsumedMessage(testCaseReturn, outputs);
}
// Finally, report the testCase results using Microcks API.
microcksAPIConnector.reportTestCaseResult(specification.getTestResultId(), testCaseReturn);
}
/**
* Validate consumed messages and complete {@code testCaseReturn} with {@code TestReturn} objects.
* @param testCaseReturn The TestCase to complete with schema validation results
* @param outputs The consumed messages from tested endpoint.
*/
private void validateConsumedMessage(TestCaseReturnDTO testCaseReturn, List outputs) {
JsonNode specificationNode = null;
try {
specificationNode = AsyncAPISchemaValidator.getJsonNodeForSchema(specification.getAsyncAPISpec());
} catch (IOException e) {
logger.errorf("Retrieval of AsyncAPI schema for validation fails for {%s}", specification.getTestResultId());
}
// Compute operation JSON pointer to navigate the spec.
String[] operationElements = specification.getOperationName().split(" ");
String operationNamePtr = "/channels/" + operationElements[1].replaceAll("/", "~1");
if ("SUBSCRIBE".equals(operationElements[0])) {
operationNamePtr += "/subscribe";
} else {
operationNamePtr += "/publish";
}
String messagePathPointer = operationNamePtr + "/message";
// Retrieve expected content type from specification and produce a schema registry snapshot.
String expectedContentType = null;
SchemaMap schemaMap = new SchemaMap();
if (specificationNode != null) {
expectedContentType = getExpectedContentType(specificationNode, messagePathPointer);
if (expectedContentType.contains("avro")) {
logger.debug("Expected content type is Avro so extracting service resources into a SchemaMap");
schemaRegistry.updateRegistryForService(specification.getServiceId());
schemaRegistry.getSchemaEntries(specification.getServiceId())
.stream().forEach(schemaEntry -> schemaMap.putSchemaEntry(schemaEntry.getPath(), schemaEntry.getContent()));
}
}
for (int i=0; i errors = null;
if (Constants.AVRO_BINARY_CONTENT_TYPES.contains(expectedContentType)) {
// Use the correct Avro message validation method depending on what has been read.
if (message.getPayloadRecord() != null) {
errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, message.getPayloadRecord(),
messagePathPointer, schemaMap);
} else {
errors = AsyncAPISchemaValidator.validateAvroMessage(specificationNode, message.getPayload(),
messagePathPointer, schemaMap);
}
} else if (expectedContentType.contains("application/json")) {
// Now parse the payloadNode and validate it according the operation message
// found in specificationNode.
JsonNode payloadNode = AsyncAPISchemaValidator.getJsonNode(responseContent);
errors = AsyncAPISchemaValidator.validateJsonMessage(specificationNode, payloadNode, messagePathPointer);
}
if (errors == null || errors.isEmpty()) {
// This is a success.
logger.infof("No errors found while validating message payload! Reporting a success for test {%s}", specification.getTestResultId());
testReturn = new TestReturn(TestReturn.SUCCESS_CODE, elapsedTime, eventMessage);
} else {
// This is a failure. Records all errors using \\n as delimiter.
logger.infof("Validation errors found... Reporting a failure for test {%s}", specification.getTestResultId());
testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime, String.join("\\n", errors), eventMessage);
}
} catch (IOException e) {
logger.error("Exception while parsing the output message", e);
testReturn = new TestReturn(TestReturn.FAILURE_CODE, elapsedTime,
"Message content cannot be parsed as JSON", eventMessage);
}
}
testCaseReturn.addTestReturn(testReturn);
}
}
/** Retrieve the expected content type for an AsyncAPI message. */
private String getExpectedContentType(JsonNode specificationNode, String messagePathPointer) {
// Retrieve default content type, defaulting to application/json.
String defaultContentType = specificationNode.path("defaultContentType").asText("application/json");
// Get message real content type if defined.
String contentType = defaultContentType;
JsonNode messageNode = specificationNode.at(messagePathPointer);
// If it's a $ref, then navigate to it.
if (messageNode.has("$ref")) {
// $ref: '#/components/messages/lightMeasured'
String ref = messageNode.path("$ref").asText();
messageNode = messageNode.at(ref.substring(1));
}
if (messageNode.has("contentType")) {
contentType = messageNode.path("contentType").asText();
}
return contentType;
}
/** Find the appropriate MessageConsumptionTask implementation depending on specification. */
private MessageConsumptionTask buildMessageConsumptionTask(AsyncTestSpecification testSpecification) {
if (KafkaMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
return new KafkaMessageConsumptionTask(testSpecification);
}
if (MQTTMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
return new MQTTMessageConsumptionTask(testSpecification);
}
if (WebSocketMessageConsumptionTask.acceptEndpoint(testSpecification.getEndpointUrl().trim())) {
return new WebSocketMessageConsumptionTask(testSpecification);
}
return null;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy