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

io.github.microcks.service.TestService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The Microcks Authors.
 *
 * 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 io.github.microcks.service;

import io.github.microcks.domain.*;
import io.github.microcks.event.TestCompletionEvent;
import io.github.microcks.repository.EventMessageRepository;
import io.github.microcks.repository.RequestRepository;
import io.github.microcks.repository.ResponseRepository;
import io.github.microcks.repository.TestResultRepository;
import io.github.microcks.util.IdBuilder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Bean defining service operations around Test domain objects.
 * @author laurent
 */
@org.springframework.stereotype.Service
public class TestService {

   /** A simple logger for diagnostic messages. */
   private static final Logger log = LoggerFactory.getLogger(TestService.class);

   private final RequestRepository requestRepository;
   private final ResponseRepository responseRepository;
   private final EventMessageRepository eventMessageRepository;
   private final TestResultRepository testResultRepository;
   private final TestRunnerService testRunnerService;
   private final ApplicationContext applicationContext;

   /**
    * Build a new TestService with the required dependencies.
    * @param requestRepository      The repository to manage persistent requests
    * @param responseRepository     The repository to manage persistent responses
    * @param eventMessageRepository The repository to manage persistent eventMessages
    * @param testResultRepository   The repository to manage persistent testResults
    * @param testRunnerService      The service for running tests
    * @param applicationContext     The Spring application context
    */
   public TestService(RequestRepository requestRepository, ResponseRepository responseRepository,
         EventMessageRepository eventMessageRepository, TestResultRepository testResultRepository,
         TestRunnerService testRunnerService, ApplicationContext applicationContext) {
      this.requestRepository = requestRepository;
      this.responseRepository = responseRepository;
      this.eventMessageRepository = eventMessageRepository;
      this.testResultRepository = testResultRepository;
      this.testRunnerService = testRunnerService;
      this.applicationContext = applicationContext;
   }

   /**
    * Launch tests for a Service on dedicated endpoint URI.
    * @param service       Service to launch tests for
    * @param testEndpoint  Endpoint URI for running the tests
    * @param runnerType    The type of runner fo tests
    * @param testOptionals Additional / optionals elements for test
    * @return An initialized TestResults (mostly empty for now since tests run asynchronously)
    */
   public TestResult launchTests(Service service, String testEndpoint, TestRunnerType runnerType,
         TestOptionals testOptionals) {
      TestResult testResult = new TestResult();
      testResult.setTestDate(new Date());
      testResult.setTestedEndpoint(testEndpoint);
      testResult.setServiceId(service.getId());
      testResult.setRunnerType(runnerType);
      testResult.setTimeout(testOptionals.getTimeout());
      testResult.setSecretRef(testOptionals.getSecretRef());
      testResult.setOperationsHeaders(testOptionals.getOperationsHeaders());

      // Initialize the TestCaseResults containers before saving it.
      initializeTestCaseResults(testResult, service, testOptionals);
      testResultRepository.save(testResult);

      // Launch test asynchronously before returning result.
      log.debug("Calling launchTestsInternal() marked as Async");
      testRunnerService.launchTestsInternal(testResult, service, runnerType, testOptionals.getOAuth2Context());
      log.debug("Async launchTestsInternal() as now finished");
      return testResult;
   }

   /**
    * Endpoint for reporting test case results
    * @param testResultId  Unique identifier of test results we report results for
    * @param operationName Name of operation to report a result for
    * @param testReturns   List of test returns to add to this test case.
    * @return A completed TestCaseResult object
    */
   public TestCaseResult reportTestCaseResult(String testResultId, String operationName, List testReturns) {
      log.info("Reporting a TestCaseResult for testResult {} on operation '{}'", testResultId, operationName);
      TestResult testResult = testResultRepository.findById(testResultId).orElse(null);
      TestCaseResult updatedTestCaseResult = null;

      // This part can be done safely with no race condition because we only
      // record new requests/responses corresponding to testReturns.
      // So just find the correct testCase to build a suitable id and then createTestReturns.

      for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) {
         // Ensure we have a testCaseResult matching operation name.
         if (testCaseResult.getOperationName().equals(operationName)) {
            // If results, we need to create requests/responses pairs and associate them to testCase.
            if (testReturns != null && !testReturns.isEmpty()) {
               String testCaseId = IdBuilder.buildTestCaseId(testResult, operationName);
               createTestReturns(testReturns, testCaseId);
            }
            break;
         }
      }

      // There may be a race condition while updating testResult at each testReturn report.
      // So be prepared to catch a org.springframework.dao.OptimisticLockingFailureException and retry
      // saving a bunch of time. Hopefully, we'll succeed. It does not matter if it takes time because
      // everything runs asynchronously.
      int times = 0;
      boolean saved = false;

      while (!saved && times < 5) {

         for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) {
            // Ensure we have a testCaseResult matching operation name.
            if (testCaseResult.getOperationName().equals(operationName)) {
               updatedTestCaseResult = testCaseResult;
               // If results we now update the success flag and elapsed time of testCase?
               if (testReturns == null || testReturns.isEmpty()) {
                  log.info("testReturns are null or empty, setting elapsedTime to -1 and success to false for {}",
                        operationName);
                  testCaseResult.setElapsedTime(-1);
                  testCaseResult.setSuccess(false);
               } else {
                  updateTestCaseResultWithReturns(testCaseResult, testReturns,
                        TestRunnerType.ASYNC_API_SCHEMA != testResult.getRunnerType(),
                        TestRunnerType.ASYNC_API_SCHEMA == testResult.getRunnerType());
               }
               break;
            }
         }

         // Finally, update success, progress indicators and total time before saving and returning.
         try {
            updateTestResult(testResult);
            saved = true;
            log.debug("testResult {} has been updated !", testResult.getId());
            // If test is completed, publish a completion event.
            if (!testResult.isInProgress()) {
               publishTestCompletionEvent(testResult);
            }
         } catch (org.springframework.dao.OptimisticLockingFailureException olfe) {
            // Update counter and refresh domain object.
            log.warn("Caught an OptimisticLockingFailureException, trying refreshing for {} times", times);
            saved = false;
            waitSomeRandomMS(5, 50);
            testResult = testResultRepository.findById(testResult.getId()).orElse(null);
            times++;
         }
      }
      return updatedTestCaseResult;
   }

   /** */
   private void initializeTestCaseResults(TestResult testResult, Service service, TestOptionals testOptionals) {
      for (Operation operation : service.getOperations()) {
         // Pick operation if no filter or present in filtered operations.
         if (testOptionals.getFilteredOperations() == null || testOptionals.getFilteredOperations().isEmpty()
               || testOptionals.getFilteredOperations().contains(operation.getName())) {
            TestCaseResult testCaseResult = new TestCaseResult();
            testCaseResult.setOperationName(operation.getName());
            testResult.getTestCaseResults().add(testCaseResult);
         }
      }
   }

   /** */
   private void createTestReturns(List testReturns, String testCaseId) {
      List responses = new ArrayList<>();
      List actualRequests = new ArrayList<>();
      List eventMessages = new ArrayList<>();

      for (TestReturn testReturn : testReturns) {
         if (testReturn.isRequestResponseTest()) {
            // Extract, complete and store response and request.
            testReturn.getResponse().setTestCaseId(testCaseId);
            testReturn.getRequest().setTestCaseId(testCaseId);
            responses.add(testReturn.getResponse());
            actualRequests.add(testReturn.getRequest());
         } else if (testReturn.isEventTest()) {
            // Complete and store event messages for tracking testCaseId.
            testReturn.getEventMessage().setTestCaseId(testCaseId);
            eventMessages.add(testReturn.getEventMessage());
         }
      }

      if (!responses.isEmpty() && !actualRequests.isEmpty()) {
         // Save the responses into repository to get their ids.
         log.debug("Saving {} responses with testCaseId {}", responses.size(), testCaseId);
         responseRepository.saveAll(responses);

         // Associate responses to requests before saving requests.
         for (int i = 0; i < actualRequests.size(); i++) {
            actualRequests.get(i).setResponseId(responses.get(i).getId());
         }
         log.debug("Saving {} requests with testCaseId {}", responses.size(), testCaseId);
         requestRepository.saveAll(actualRequests);
      }

      if (!eventMessages.isEmpty()) {
         // Save the eventMessages into repository.
         log.debug("Saving {} eventMessages with testCaseId {}", eventMessages.size(), testCaseId);
         eventMessageRepository.saveAll(eventMessages);
      }
   }

   /**
    *
    */
   private void updateTestCaseResultWithReturns(TestCaseResult testCaseResult, List testReturns,
         boolean sumElapsedTimes, boolean findMaxElapsedTime) {

      // Prepare a bunch of flag we're going to complete.
      boolean successFlag = true;
      long caseElapsedTime = 0;

      for (TestReturn testReturn : testReturns) {
         // Deal with elapsed time and success flag.
         if (sumElapsedTimes) {
            caseElapsedTime += testReturn.getElapsedTime();
         } else if (findMaxElapsedTime) {
            if (testReturn.getElapsedTime() > caseElapsedTime) {
               caseElapsedTime = testReturn.getElapsedTime();
            }
         }
         TestStepResult testStepResult = testReturn.buildTestStepResult();
         if (!testStepResult.isSuccess()) {
            successFlag = false;
         }

         // Add testStepResult to testCase.
         testCaseResult.getTestStepResults().add(testStepResult);
      }

      // Update and save the completed TestCaseResult.
      // We cannot consider as success if we have no TestStepResults associated...
      if (!testCaseResult.getTestStepResults().isEmpty()) {
         testCaseResult.setSuccess(successFlag);
      }
      testCaseResult.setElapsedTime(caseElapsedTime);
      log.debug("testCaseResult for {} have been updated with {} elapsedTime and success flag to {}",
            testCaseResult.getOperationName(), testCaseResult.getElapsedTime(), testCaseResult.isSuccess());
   }

   /**
    *
    */
   private void updateTestResult(TestResult testResult) {
      // Update success, progress indicators and total time before saving and returning.
      boolean globalSuccessFlag = true;
      boolean globalProgressFlag = false;
      long totalElapsedTime = 0;
      for (TestCaseResult testCaseResult : testResult.getTestCaseResults()) {
         totalElapsedTime += testCaseResult.getElapsedTime();
         if (!testCaseResult.isSuccess()) {
            globalSuccessFlag = false;
         }
         // -1 is default elapsed time for testcase so it means that still in
         // progress because not updated yet.
         if (testCaseResult.getElapsedTime() == -1) {
            log.debug("testCaseResult.elapsedTime is -1, set globalProgressFlag to true");
            globalProgressFlag = true;
         }
      }

      // Update aggregated flags before saving whole testResult.
      testResult.setSuccess(globalSuccessFlag);
      testResult.setInProgress(globalProgressFlag);
      testResult.setElapsedTime(totalElapsedTime);

      log.debug("Trying to update testResult {} with {} elapsedTime and success flag to {}", testResult.getId(),
            testResult.getElapsedTime(), testResult.isSuccess());
      testResultRepository.save(testResult);
   }

   private void waitSomeRandomMS(int min, int max) {
      Object semaphore = new Object();
      long timeout = ThreadLocalRandom.current().nextInt(min, max + 1);
      synchronized (semaphore) {
         try {
            semaphore.wait(timeout);
         } catch (Exception e) {
            log.debug("waitSomeRandomMS semaphore was interrupted");
         }
      }
   }

   /** Publish a TestCompletionEvent towards asynchronous consumers. */
   private void publishTestCompletionEvent(TestResult testResult) {
      TestCompletionEvent event = new TestCompletionEvent(this, testResult);
      applicationContext.publishEvent(event);
      log.debug("Test completion event has been published");
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy