diff --git a/src/main/java/com/endava/cats/command/CatsCommand.java b/src/main/java/com/endava/cats/command/CatsCommand.java index f3fae2c3e..04f80a616 100644 --- a/src/main/java/com/endava/cats/command/CatsCommand.java +++ b/src/main/java/com/endava/cats/command/CatsCommand.java @@ -356,21 +356,10 @@ private void fuzzPath(Map.Entry pathItemEntry, OpenAPI openAPI .collect(Collectors.toSet()); List fuzzersToRun = filterArguments.filterOutFuzzersNotMatchingHttpMethods(allHttpMethodsFromFuzzingData); - int totalToRun = this.computeTotalsToRun(fuzzersToRun, filteredFuzzingData); - testCaseListener.setTotalRunsPerPath(pathItemEntry.getKey(), totalToRun); this.runFuzzers(filteredFuzzingData, fuzzersToRun); this.runFuzzers(filteredFuzzingData, filterArguments.getSecondPhaseFuzzers()); } - private int computeTotalsToRun(List fuzzersToRun, List filteredFuzzingData) { - int total = 0; - for (FuzzingData data : filteredFuzzingData) { - total = total + (int) fuzzersToRun.stream().filter(fuzzer -> !fuzzer.skipForHttpMethods().contains(data.getMethod())).count(); - } - - return total; - } - private void runFuzzers(List fuzzingDataListWithHttpMethodsFiltered, List configuredFuzzers) { /*We only run the fuzzers supplied and exclude those that do not apply for certain HTTP methods*/ @@ -384,7 +373,9 @@ private void runFuzzers(List fuzzingDataListWithHttpMethodsFiltered filteredData.forEach(data -> { logger.start("Starting Fuzzer {}, http method {}, path {}", ansi().fgGreen().a(fuzzer.toString()).reset(), data.getMethod(), data.getPath()); logger.debug("Fuzzing payload: {}", data.getPayload()); - testCaseListener.beforeFuzz(fuzzer.getClass(), data.getContractPath(), data.getMethod().name()); + if (!(fuzzer instanceof FunctionalFuzzer)) { + testCaseListener.beforeFuzz(fuzzer.getClass(), data.getContractPath(), data.getMethod().name()); + } fuzzer.fuzz(data); if (!(fuzzer instanceof FunctionalFuzzer)) { testCaseListener.afterFuzz(data.getContractPath(), data.getMethod().name()); diff --git a/src/main/java/com/endava/cats/command/ListCommand.java b/src/main/java/com/endava/cats/command/ListCommand.java index 57f04bb4b..69ce621df 100644 --- a/src/main/java/com/endava/cats/command/ListCommand.java +++ b/src/main/java/com/endava/cats/command/ListCommand.java @@ -15,10 +15,10 @@ import com.endava.cats.fuzzer.special.mutators.api.Mutator; import com.endava.cats.generator.format.api.OpenAPIFormat; import com.endava.cats.http.HttpMethod; -import com.endava.cats.util.JsonUtils; import com.endava.cats.model.FuzzingData; import com.endava.cats.openapi.OpenApiUtils; import com.endava.cats.util.ConsoleUtils; +import com.endava.cats.util.JsonUtils; import com.endava.cats.util.VersionProvider; import io.github.ludovicianul.prettylogger.PrettyLogger; import io.github.ludovicianul.prettylogger.PrettyLoggerFactory; @@ -196,7 +196,7 @@ void listPath(OpenAPI openAPI, String path) { } else { logger.noFormat(path); for (PathDetailsEntry.OperationDetails operation : pathDetailsEntry.getOperations()) { - logger.noFormat(" "); + logger.noFormat(ConsoleUtils.SEPARATOR); logger.noFormat(" ◼ Operation: " + operation.getOperationId()); logger.noFormat(" ◼ HTTP Method: " + operation.getHttpMethod()); logger.noFormat(" ◼ Response Codes: " + operation.getResponses()); diff --git a/src/main/java/com/endava/cats/fuzzer/special/CustomFuzzerUtil.java b/src/main/java/com/endava/cats/fuzzer/special/CustomFuzzerUtil.java index fff91a76b..901c04863 100644 --- a/src/main/java/com/endava/cats/fuzzer/special/CustomFuzzerUtil.java +++ b/src/main/java/com/endava/cats/fuzzer/special/CustomFuzzerUtil.java @@ -8,7 +8,6 @@ import com.endava.cats.http.ResponseCodeFamilyDynamic; import com.endava.cats.io.ServiceCaller; import com.endava.cats.io.ServiceData; -import com.endava.cats.util.JsonUtils; import com.endava.cats.model.CatsHeader; import com.endava.cats.model.CatsResponse; import com.endava.cats.model.FuzzingData; @@ -16,6 +15,7 @@ import com.endava.cats.strategy.FuzzingStrategy; import com.endava.cats.util.CatsDSLWords; import com.endava.cats.util.CatsUtil; +import com.endava.cats.util.JsonUtils; import com.endava.cats.util.WordUtils; import io.github.ludovicianul.prettylogger.PrettyLogger; import io.github.ludovicianul.prettylogger.PrettyLoggerFactory; @@ -40,7 +40,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import static com.endava.cats.util.JsonUtils.NOT_SET; import static com.endava.cats.util.CatsDSLWords.CATS_BODY_FUZZ; import static com.endava.cats.util.CatsDSLWords.CATS_HEADERS; import static com.endava.cats.util.CatsDSLWords.CHECKS; @@ -50,6 +49,7 @@ import static com.endava.cats.util.CatsDSLWords.OUTPUT; import static com.endava.cats.util.CatsDSLWords.RESERVED_WORDS; import static com.endava.cats.util.CatsDSLWords.VERIFY; +import static com.endava.cats.util.JsonUtils.NOT_SET; /** * Common methods used by the FunctionalFuzzer and SecurityFuzzer. @@ -332,7 +332,7 @@ private boolean isCatsRemove(Map.Entry keyValue) { * @param fuzzer The custom fuzzer used for generating individual test cases. */ public void executeTestCases(FuzzingData data, String key, Object value, CustomFuzzerBase fuzzer) { - testCaseListener.notifySummaryObservers(data.getContractPath(), data.getMethod().name(), 0d); + testCaseListener.notifySummaryObservers(data.getContractPath()); log.debug("Path [{}] for method [{}] has the following custom data [{}]", data.getContractPath(), data.getMethod(), value); boolean isValidOneOf = this.isValidOneOf(data, (Map) value); List missingKeywords = this.getMissingKeywords(fuzzer, (Map) value); diff --git a/src/main/java/com/endava/cats/fuzzer/special/FunctionalFuzzer.java b/src/main/java/com/endava/cats/fuzzer/special/FunctionalFuzzer.java index 0900f2685..92a8ae7f6 100644 --- a/src/main/java/com/endava/cats/fuzzer/special/FunctionalFuzzer.java +++ b/src/main/java/com/endava/cats/fuzzer/special/FunctionalFuzzer.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * Executes functional tests written in Cats DSL. @@ -78,9 +77,6 @@ public void executeCustomFuzzerTests() { logger.debug("Executing {} functional tests.", executions.size()); Collections.sort(executions); - executions.stream().collect(Collectors.groupingBy(customFuzzerExecution -> customFuzzerExecution.getFuzzingData().getContractPath(), Collectors.counting())) - .forEach((s, aLong) -> testCaseListener.setTotalRunsPerPath(s, aLong.intValue())); - for (Map.Entry> entry : filesArguments.getCustomFuzzerDetails().entrySet()) { executions.stream().filter(customFuzzerExecution -> customFuzzerExecution.getFuzzingData().getContractPath().equalsIgnoreCase(entry.getKey())) .forEach(customFuzzerExecution -> { diff --git a/src/main/java/com/endava/cats/fuzzer/special/RandomFuzzer.java b/src/main/java/com/endava/cats/fuzzer/special/RandomFuzzer.java index 60a3724d8..e84348a58 100644 --- a/src/main/java/com/endava/cats/fuzzer/special/RandomFuzzer.java +++ b/src/main/java/com/endava/cats/fuzzer/special/RandomFuzzer.java @@ -11,7 +11,6 @@ import com.endava.cats.fuzzer.special.mutators.api.CustomMutatorConfig; import com.endava.cats.fuzzer.special.mutators.api.CustomMutatorKeywords; import com.endava.cats.fuzzer.special.mutators.api.Mutator; -import com.endava.cats.util.JsonUtils; import com.endava.cats.model.CatsHeader; import com.endava.cats.model.CatsResponse; import com.endava.cats.model.FuzzingData; @@ -19,6 +18,7 @@ import com.endava.cats.report.TestCaseListener; import com.endava.cats.util.CatsUtil; import com.endava.cats.util.ConsoleUtils; +import com.endava.cats.util.JsonUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -95,7 +95,7 @@ public void fuzz(FuzzingData data) { boolean shouldStop = false; Set allCatsFields = data.getAllFieldsByHttpMethod(); - testCaseListener.startUnknownProgress(data); + testCaseListener.updateUnknownProgress(data); while (!shouldStop) { String targetField = CatsUtil.selectRandom(allCatsFields); diff --git a/src/main/java/com/endava/cats/fuzzer/special/TemplateFuzzer.java b/src/main/java/com/endava/cats/fuzzer/special/TemplateFuzzer.java index dbe704d51..bf425528e 100644 --- a/src/main/java/com/endava/cats/fuzzer/special/TemplateFuzzer.java +++ b/src/main/java/com/endava/cats/fuzzer/special/TemplateFuzzer.java @@ -7,15 +7,15 @@ import com.endava.cats.generator.simple.StringGenerator; import com.endava.cats.generator.simple.UnicodeGenerator; import com.endava.cats.io.ServiceCaller; -import com.endava.cats.util.JsonUtils; import com.endava.cats.model.CatsHeader; import com.endava.cats.model.CatsRequest; import com.endava.cats.model.CatsResponse; import com.endava.cats.model.FuzzingData; -import com.endava.cats.util.KeyValuePair; import com.endava.cats.report.TestCaseListener; import com.endava.cats.strategy.FuzzingStrategy; import com.endava.cats.util.ConsoleUtils; +import com.endava.cats.util.JsonUtils; +import com.endava.cats.util.KeyValuePair; import com.jayway.jsonpath.JsonPathException; import io.github.ludovicianul.prettylogger.PrettyLogger; import io.github.ludovicianul.prettylogger.PrettyLoggerFactory; @@ -65,7 +65,7 @@ public TemplateFuzzer(ServiceCaller sc, TestCaseListener lr, UserArguments ua, M @Override public void fuzz(FuzzingData data) { - testCaseListener.startUnknownProgress(data); + testCaseListener.updateUnknownProgress(data); for (String targetField : Optional.ofNullable(data.getTargetFields()).orElse(Collections.emptySet())) { int payloadSize = this.getPayloadSize(data, targetField); diff --git a/src/main/java/com/endava/cats/report/TestCaseListener.java b/src/main/java/com/endava/cats/report/TestCaseListener.java index bb6ce6a97..a630c9a73 100644 --- a/src/main/java/com/endava/cats/report/TestCaseListener.java +++ b/src/main/java/com/endava/cats/report/TestCaseListener.java @@ -61,7 +61,7 @@ @ApplicationScoped @DryRun public class TestCaseListener { - private final Iterator cycle = Iterators.cycle("\\", "|", "/", "-"); + private static final Iterator cycle = Iterators.cycle('\\', '\\', '\\', '|', '|', '|', '/', '/', '/', '-', '-', '-'); private static final String DEFAULT = "*******"; static final String ID = "id"; private static final String FUZZER_KEY = "fuzzerKey"; @@ -91,8 +91,7 @@ public class TestCaseListener { @ConfigProperty(name = "app.timestamp", defaultValue = "1-1-1") String appBuildTime; - private final Map runPerPathListener = new HashMap<>(); - private final Map runTotals = new HashMap<>(); + private final Deque runPerPathListener = new ArrayDeque<>(); /** * Constructs a TestCaseListener with the provided dependencies and configuration. @@ -136,7 +135,7 @@ public void beforeFuzz(Class fuzzer, String path, String method) { String clazz = ConsoleUtils.removeTrimSanitize(fuzzer.getSimpleName()).replaceAll("[a-z]", ""); MDC.put(FUZZER, ConsoleUtils.centerWithAnsiColor(clazz, getKeyDefault().length(), Ansi.Color.MAGENTA)); MDC.put(FUZZER_KEY, ConsoleUtils.removeTrimSanitize(fuzzer.getSimpleName())); - this.notifySummaryObservers(path, method, 1); + this.notifySummaryObservers(path); } @@ -147,8 +146,7 @@ public void beforeFuzz(Class fuzzer, String path, String method) { * @param httpMethod the HTTP method for which fuzzing has been completed */ public void afterFuzz(String path, String httpMethod) { - double chunkSize = 100d / runTotals.getOrDefault(path, 1) + 0.01; - this.notifySummaryObservers(path, httpMethod, chunkSize); + this.notifySummaryObservers(path); MDC.put(FUZZER, this.getKeyDefault()); MDC.put(FUZZER_KEY, this.getKeyDefault()); @@ -203,16 +201,6 @@ private void startTestCase() { testCaseMap.get(testId).setTestId("Test " + testId); } - /** - * Sets the total number of runs to be executed for a specific path. - * - * @param path the path for which the total runs are being set - * @param totalToBeRun the total number of runs to be executed for the specified path - */ - public void setTotalRunsPerPath(String path, Integer totalToBeRun) { - this.runTotals.put(path, totalToBeRun); - } - /** * Adds a scenario to the test case and logs it using the provided logger. * @@ -326,49 +314,38 @@ private void keepExecutionDetails(CatsTestCase testCase) { * Notifies summary observers about the progress of a specific path and HTTP method during the testing session. * If configured to display summaries in the console, this method renders the progress dynamically. * - * @param path the path for which the progress is being reported - * @param method the HTTP method associated with the path - * @param chunkSize the chunk size representing the progress + * @param path the path for which the progress is being reported */ - public void notifySummaryObservers(String path, String method, double chunkSize) { - if (reportingArguments.isSummaryInConsole()) { - double percentage = runPerPathListener.getOrDefault(path, 0d) + chunkSize; - String printPath = path + " " + (percentage >= 100 ? executionStatisticsListener.resultAsStringPerPath(path) : method); + public void notifySummaryObservers(String path) { + if (!reportingArguments.isSummaryInConsole()) { + return; + } + String printPath = path + ConsoleUtils.SEPARATOR + executionStatisticsListener.resultAsStringPerPath(path); - if (runPerPathListener.get(path) != null) { - ConsoleUtils.renderSameRow(printPath, percentage); - } else { - ConsoleUtils.renderNewRow(printPath, percentage); - } - runPerPathListener.merge(path, chunkSize, Double::sum); + if (runPerPathListener.contains(path)) { + ConsoleUtils.renderSameRow(printPath, cycle.next()); + } else { + this.markPreviousPathAsDone(); + runPerPathListener.push(path); + ConsoleUtils.renderNewRow(printPath, cycle.next()); } } - /** - * Use this to start a summary progress when the number of tests to run is not known. - * - * @param data the FuzzingData context - */ - public void startUnknownProgress(FuzzingData data) { - if (!reportingArguments.isSummaryInConsole()) { - return; + private void markPreviousPathAsDone() { + String previousPath = runPerPathListener.peek(); + if (previousPath != null) { + String toRenderPreviousPath = previousPath + ConsoleUtils.SEPARATOR + executionStatisticsListener.resultAsStringPerPath(previousPath); + ConsoleUtils.renderSameRow(toRenderPreviousPath, '✔'); } - this.notifySummaryObservers(data.getContractPath(), data.getMethod().name(), 0d); - ConsoleUtils.renderSameRow(data.getPath() + " " + data.getMethod(), cycle.next()); } /** - * Updates the progress with a new character to signla progress. + * Updates the progress with a new character to signal progress. * * @param data the FuzzingData context */ public void updateUnknownProgress(FuzzingData data) { - if (!reportingArguments.isSummaryInConsole()) { - return; - } - if (this.getCurrentTestCaseNumber() % 20 == 0) { - ConsoleUtils.renderSameRow(data.getPath() + " " + data.getMethod(), cycle.next()); - } + this.notifySummaryObservers(data.getContractPath()); } /** @@ -432,6 +409,7 @@ public void writeHelperFiles() { * Additionally, prints execution details using the associated logger. */ public void endSession() { + markPreviousPathAsDone(); reportingArguments.enableAdditionalLoggingIfSummary(); testCaseExporter.writeSummary(testCaseSummaryDetails, executionStatisticsListener); testCaseExporter.writeHelperFiles(); @@ -571,7 +549,7 @@ void reportError(PrettyLogger logger, String message, Object... params) { */ private void renderProgress(CatsResponse catsResponse) { if (reportingArguments.isPrintProgress()) { - ConsoleUtils.renderSameRow("+ " + catsResponse.getPath()); + ConsoleUtils.renderSameRowAndMoveToNextLine("+ " + catsResponse.getPath()); } } diff --git a/src/main/java/com/endava/cats/util/ConsoleUtils.java b/src/main/java/com/endava/cats/util/ConsoleUtils.java index 54777bb29..f95c5326d 100644 --- a/src/main/java/com/endava/cats/util/ConsoleUtils.java +++ b/src/main/java/com/endava/cats/util/ConsoleUtils.java @@ -14,16 +14,15 @@ * Utility class for console-related operations. */ public abstract class ConsoleUtils { - private static final String QUARKUS_PROXY_SUFFIX = "_Subclass"; private static final String REGEX_TO_REMOVE_FROM_FUZZER_NAMES = "TrimValidate|ValidateTrim|SanitizeValidate|ValidateSanitize|Fuzzer"; private static final PrettyLogger LOGGER = PrettyLoggerFactory.getConsoleLogger(); private static final Pattern ANSI_REMOVE_PATTERN = Pattern.compile("\u001B\\[[;\\d]*m"); + public static final String SEPARATOR = " "; /** - * -- GETTER -- * Get the width of the terminal. *

* Size is cached, so it won't reach to width changes during run. @@ -35,6 +34,11 @@ private ConsoleUtils() { //ntd } + /** + * Initialize the terminal width. + * + * @param spec The command spec. + */ public static void initTerminalWidth(CommandLine.Model.CommandSpec spec) { try { terminalWidth = spec.usageMessage().width(); @@ -100,49 +104,38 @@ public static String getShell() { return Optional.ofNullable(System.getenv("SHELL")).orElse("unknown"); } - /** - * Render a progress row on the same console row. - * - * @param path The path being processed. - * @param percentage The completion percentage. - */ - public static void renderSameRow(String path, double percentage) { - renderRow("\r", path, ((int) Math.min(percentage, 100d)) + "%"); - } - /** * Render a progress row on the same console row. * * @param path The path being processed. - * @param progress A progress character. + * @param progress The progress character to be displayed. */ - public static void renderSameRow(String path, String progress) { + public static void renderSameRow(String path, char progress) { renderRow("\r", path, progress); } /** * Render a progress row on a new console row. * - * @param path The path being processed. - * @param percentage The completion percentage. + * @param path The path being processed. + * @param progress The progress character to be displayed. */ - public static void renderNewRow(String path, double percentage) { - renderRow(System.lineSeparator(), path, ((int) Math.min(percentage, 100d)) + "%"); + public static void renderNewRow(String path, char progress) { + renderRow(System.lineSeparator(), path, progress); } /** * Render a progress row with a specific prefix. * - * @param prefix The prefix for the progress row. - * @param path The path being processed. - * @param rightTextToRender The text to be written on the right hand side of the screen. + * @param prefix The prefix for the progress row. + * @param path The path being processed. */ - public static void renderRow(String prefix, String path, String rightTextToRender) { + public static void renderRow(String prefix, String path, char progressChar) { String withoutAnsi = ANSI_REMOVE_PATTERN.matcher(path).replaceAll(""); - int dots = Math.max(terminalWidth - withoutAnsi.length() - rightTextToRender.length() - 2, 1); - String firstPart = path.substring(0, path.indexOf(" ")); - String secondPart = path.substring(path.indexOf(" ") + 1); - String toPrint = Ansi.ansi().bold().a(prefix + firstPart + " " + ".".repeat(dots) + secondPart + " " + rightTextToRender).reset().toString(); + int dots = Math.max(terminalWidth - withoutAnsi.length() - 2, 1); + String firstPart = path.substring(0, path.indexOf(SEPARATOR)); + String secondPart = path.substring(path.indexOf(SEPARATOR) + 1); + String toPrint = Ansi.ansi().bold().a(prefix + firstPart + " " + ".".repeat(dots) + secondPart + " " + progressChar).reset().toString(); //we just use system.out as the logger adds a new line System.out.print(toPrint); @@ -174,7 +167,7 @@ public static void renderHeader(String header) { * * @param message the message */ - public static void renderSameRow(String message) { + public static void renderSameRowAndMoveToNextLine(String message) { int spacesToAdd = Math.max(getConsoleColumns(message.length()), 0); //we just use system.out as the logger adds a new line diff --git a/src/test/java/com/endava/cats/report/TestCaseListenerTest.java b/src/test/java/com/endava/cats/report/TestCaseListenerTest.java index e5918fe01..e9472194d 100644 --- a/src/test/java/com/endava/cats/report/TestCaseListenerTest.java +++ b/src/test/java/com/endava/cats/report/TestCaseListenerTest.java @@ -810,20 +810,11 @@ void shouldReturnCurrentFuzzer() { @Test void shouldStartUnknownProgress() { - FuzzingData data = FuzzingData.builder().contractPath("/test").method(HttpMethod.POST).path("/test").build(); - Mockito.when(reportingArguments.isSummaryInConsole()).thenReturn(true); - TestCaseListener testCaseListenerSpy = Mockito.spy(testCaseListener); - testCaseListenerSpy.startUnknownProgress(data); - Mockito.verify(testCaseListenerSpy).notifySummaryObservers(Mockito.eq("/test"), Mockito.eq("POST"), Mockito.anyDouble()); - } - - @Test - void shouldUpdateUnknownProgress() { FuzzingData data = FuzzingData.builder().contractPath("/test").method(HttpMethod.POST).path("/test").build(); Mockito.when(reportingArguments.isSummaryInConsole()).thenReturn(true); TestCaseListener testCaseListenerSpy = Mockito.spy(testCaseListener); testCaseListenerSpy.updateUnknownProgress(data); - Mockito.verify(testCaseListenerSpy, Mockito.times(1)).getCurrentTestCaseNumber(); + Mockito.verify(testCaseListenerSpy).notifySummaryObservers(Mockito.eq("/test")); } private void prepareTestCaseListenerSimpleSetup(CatsResponse build, Runnable runnable) {