66import dev .dochia .cli .core .io .ServiceCaller ;
77import dev .dochia .cli .core .model .HttpResponse ;
88import dev .dochia .cli .core .model .TestCase ;
9+ import dev .dochia .cli .core .report .TestCaseExporter ;
910import dev .dochia .cli .core .report .TestCaseListener ;
10- import dev .dochia .cli .core .util .CommonUtils ;
1111import dev .dochia .cli .core .util .JsonUtils ;
1212import dev .dochia .cli .core .util .KeyValuePair ;
1313import dev .dochia .cli .core .util .VersionProvider ;
2020
2121import java .io .IOException ;
2222import java .nio .file .Files ;
23+ import java .nio .file .Path ;
2324import java .nio .file .Paths ;
24- import java .util .*;
25+ import java .util .ArrayList ;
26+ import java .util .Arrays ;
27+ import java .util .Collections ;
28+ import java .util .HashMap ;
29+ import java .util .List ;
30+ import java .util .Map ;
31+ import java .util .Optional ;
2532
2633/**
2734 * This will replay a given list of tests solely based on the information received in the test case file(s).
4148 footer = {" Replay Test 1 from the default reporting folder:" ,
4249 " dochia replay Test1" ,
4350 "" , " Replay Test 1 from the default reporting folder and write the new output in another folder" ,
44- " dochia replay Test1 --output path/to/new/folder" },
51+ " dochia replay Test1 --output path/to/new/folder" ,
52+ "" , " Retry all failed (error) tests from the default dochia-report folder:" ,
53+ " dochia replay --errors" ,
54+ "" , " Retry all failed tests including warnings:" ,
55+ " dochia replay --errors --warnings" },
4556 versionProvider = VersionProvider .class )
4657@ Unremovable
4758public class ReplayCommand implements Runnable {
@@ -61,7 +72,7 @@ public class ReplayCommand implements Runnable {
6172 @ CommandLine .Mixin
6273 HelpFullOption helpFullOption ;
6374
64- @ CommandLine .Option (names = {"-v" },
75+ @ CommandLine .Option (names = {"-v" , "--verbose" },
6576 description = "Prints verbose information about the execution" )
6677 private boolean verbose ;
6778
@@ -77,6 +88,18 @@ public class ReplayCommand implements Runnable {
7788 description = "If supplied, it will create TestXXX.json files within the given folder with the updated responses received when replaying the tests" )
7889 private String outputReportFolder ;
7990
91+ @ CommandLine .Option (names = {"--errors" },
92+ description = "Retry all tests with error results from the dochia-summary-report.json in the report folder" )
93+ private boolean errors ;
94+
95+ @ CommandLine .Option (names = {"--warnings" },
96+ description = "Retry all tests with warning results from the dochia-summary-report.json in the report folder" )
97+ private boolean warnings ;
98+
99+ @ CommandLine .Option (names = {"--report-folder" , "-r" },
100+ description = "The folder containing the dochia-summary-report.json file when using --errors/--warnings. Default: @|bold,underline dochia-report|@" )
101+ private String reportFolder = "dochia-report" ;
102+
80103
81104 /**
82105 * Constructs a new instance of the {@code ReplayCommand} class.
@@ -91,13 +114,102 @@ public ReplayCommand(ServiceCaller serviceCaller, TestCaseListener testCaseListe
91114 }
92115
93116 private List <String > parseTestCases () {
94- return Arrays .stream (tests )
95- .map (testCase -> testCase .trim ().strip ())
96- .map (testCase -> testCase .endsWith (".json" ) ? testCase : "dochia-report/" + testCase + ".json" )
97- .toList ();
117+ List <String > testCaseFiles = new ArrayList <>();
118+
119+ // Add tests from retry options (--errors, --warnings)
120+ if (errors || warnings ) {
121+ testCaseFiles .addAll (loadTestIdsFromSummaryReport ());
122+ }
123+
124+ // Add explicitly provided test cases
125+ if (tests != null && tests .length > 0 ) {
126+ testCaseFiles .addAll (Arrays .stream (tests )
127+ .map (testCase -> testCase .trim ().strip ())
128+ .map (testCase -> testCase .endsWith (".json" ) ? testCase : reportFolder + "/" + testCase + ".json" )
129+ .toList ());
130+ }
131+
132+ return testCaseFiles ;
133+ }
134+
135+ private List <String > loadTestIdsFromSummaryReport () {
136+ Path summaryPath = Paths .get (reportFolder , TestCaseExporter .REPORT_JS );
137+ if (!Files .exists (summaryPath )) {
138+ logger .error ("Summary report not found at: {}" , summaryPath );
139+ return Collections .emptyList ();
140+ }
141+
142+ try {
143+ String content = Files .readString (summaryPath );
144+ SummaryReport report = JsonUtils .GSON .fromJson (content , SummaryReport .class );
145+
146+ if (report == null || report .testCases == null ) {
147+ logger .error ("Invalid summary report format" );
148+ return Collections .emptyList ();
149+ }
150+
151+ List <String > failedIds = new ArrayList <>();
152+ for (TestCaseSummaryEntry entry : report .testCases ) {
153+ if (shouldRetryTest (entry )) {
154+ String testId = entry .id .replace (" " , "" );
155+ failedIds .add (reportFolder + "/" + testId + ".json" );
156+ }
157+ }
158+
159+ if (failedIds .isEmpty ()) {
160+ logger .info ("No failed tests found to retry" );
161+ } else {
162+ logger .info ("Found {} failed test(s) to retry" , failedIds .size ());
163+ }
164+
165+ return failedIds ;
166+ } catch (IOException e ) {
167+ logger .error ("Failed to read summary report: {}" , e .getMessage ());
168+ logger .debug ("Stacktrace:" , e );
169+ return Collections .emptyList ();
170+ } catch (Exception e ) {
171+ logger .error ("Failed to parse summary report: {}" , e .getMessage ());
172+ logger .debug ("Stacktrace:" , e );
173+ return Collections .emptyList ();
174+ }
175+ }
176+
177+ private boolean shouldRetryTest (TestCaseSummaryEntry entry ) {
178+ if (entry .result == null ) {
179+ return false ;
180+ }
181+ boolean isError = errors && "error" .equalsIgnoreCase (entry .result );
182+ boolean isWarning = warnings && "warn" .equalsIgnoreCase (entry .result );
183+ return isError || isWarning ;
184+ }
185+
186+ /**
187+ * Internal class for deserializing the summary report.
188+ */
189+ static class SummaryReport {
190+ List <TestCaseSummaryEntry > testCases ;
191+ }
192+
193+ /**
194+ * Internal class for deserializing individual test case entries from the summary.
195+ */
196+ static class TestCaseSummaryEntry {
197+ String id ;
198+ String result ;
199+ }
200+
201+ /**
202+ * Tracks replay statistics for summary display.
203+ */
204+ static class ReplayStats {
205+ int initialErrors ;
206+ int initialWarnings ;
207+ int unchanged ;
208+ int improved ;
209+ int regressed ;
98210 }
99211
100- private void executeTestCase (String testCaseFileName ) throws IOException {
212+ private void executeTestCase (String testCaseFileName , ReplayStats stats ) throws IOException {
101213 TestCase testCase = this .loadTestCaseFile (testCaseFileName );
102214 logger .start ("Calling service endpoint: {}" , testCase .getRequest ().getUrl ());
103215 this .loadHeadersIfSupplied (testCase );
@@ -114,20 +226,46 @@ private void executeTestCase(String testCaseFileName) throws IOException {
114226 .build ();
115227 }
116228
117- logger .complete ("Response body: \n {}" , response .getBody ());
229+ if (verbose ) {
230+ logger .complete ("Response body: \n {}" , response .getBody ());
231+ }
118232 this .writeTestJsonsIfSupplied (testCase , response );
119233 this .showResponseCodesDifferences (testCase , response );
234+ this .updateStats (testCase , response , stats );
235+ }
236+
237+ private void updateStats (TestCase testCase , HttpResponse response , ReplayStats stats ) {
238+ if (stats == null ) {
239+ return ;
240+ }
241+ int oldCode = testCase .getResponse ().getResponseCode ();
242+ int newCode = response .getResponseCode ();
243+
244+ boolean wasError = oldCode >= 500 || oldCode == 0 ;
245+ boolean isNowError = newCode >= 500 || newCode == 0 ;
246+ boolean isNowClientError = newCode >= 400 && newCode < 500 ;
247+ boolean isNowSuccess = newCode >= 200 && newCode < 300 ;
248+
249+ if (oldCode == newCode ) {
250+ stats .unchanged ++;
251+ } else if (isNowSuccess || (wasError && isNowClientError )) {
252+ stats .improved ++;
253+ } else if (isNowError ) {
254+ stats .regressed ++;
255+ }
120256 }
121257
122258 void showResponseCodesDifferences (TestCase testCase , HttpResponse response ) {
123259 logger .noFormat ("" );
124260 logger .star ("Old response code: {}" , testCase .getResponse ().getResponseCode ());
125261 logger .star ("New response code: {}" , response .getResponseCode ());
126-
127- logger .noFormat ("" );
128- logger .star ("Old response body: {}" , testCase .getResponse ().getJsonBody ());
129- logger .star ("New response body: {}" , response .getJsonBody ());
130262 logger .noFormat ("" );
263+
264+ if (verbose ) {
265+ logger .star ("Old response body: {}" , testCase .getResponse ().getJsonBody ());
266+ logger .star ("New response body: {}" , response .getJsonBody ());
267+ logger .noFormat ("" );
268+ }
131269 }
132270
133271 void writeTestJsonsIfSupplied (TestCase testCase , HttpResponse response ) {
@@ -142,19 +280,17 @@ void writeTestJsonsIfSupplied(TestCase testCase, HttpResponse response) {
142280 private void loadHeadersIfSupplied (TestCase testCase ) {
143281 List <KeyValuePair <String , Object >> headersFromFile = new java .util .ArrayList <>(Optional .ofNullable (testCase .getRequest ().getHeaders ()).orElse (Collections .emptyList ()));
144282
145- //remove old headers
146283 headersFromFile .removeIf (header -> headersMap .containsKey (header .getKey ()));
147-
148- //add new headers
149284 headersFromFile .addAll (headersMap .entrySet ().stream ().map (entry -> new KeyValuePair <>(entry .getKey (), entry .getValue ())).toList ());
150285
151- //see if any header is dynamic and it needs a parser
152286 headersFromFile .forEach (header -> header .setValue (DSLParser .parseAndGetResult (header .getValue ().toString (), authArgs .getAuthScriptAsMap ())));
153287 }
154288
155289 private TestCase loadTestCaseFile (String testCaseFileName ) throws IOException {
156290 String testCaseFile = Files .readString (Paths .get (testCaseFileName ));
157- logger .config ("Loaded content: \n " + testCaseFile );
291+ if (verbose ) {
292+ logger .config ("Loaded content: \n " + testCaseFile );
293+ }
158294 TestCase testCase = JsonUtils .GSON .fromJson (testCaseFile , TestCase .class );
159295 testCase .updateServer (server );
160296 return testCase ;
@@ -177,21 +313,70 @@ private void initReportingPath() {
177313
178314 @ Override
179315 public void run () {
180- if (verbose ) {
181- CommonUtils .setDochiaLogLevel ("ALL" );
182- logger .fav ("Setting dochia log level to ALL!" );
316+ List <String > testCases = this .parseTestCases ();
317+ if (testCases .isEmpty ()) {
318+ logger .warning ("No tests to replay. Provide test names as arguments or use --errors/--warnings" );
319+ return ;
183320 }
321+
184322 this .initReportingPath ();
185- for (String testCaseFileName : this .parseTestCases ()) {
323+ ReplayStats stats = (errors || warnings ) ? createInitialStats () : null ;
324+
325+ for (String testCaseFileName : testCases ) {
186326 try {
327+ logger .noFormat ("" );
187328 logger .start ("Executing {}" , testCaseFileName );
188- this .executeTestCase (testCaseFileName );
329+ this .executeTestCase (testCaseFileName , stats );
189330 logger .complete ("Finish executing {}" , testCaseFileName );
190331 } catch (IOException e ) {
191332 logger .debug ("Exception while replaying test!" , e );
192333 logger .error ("Something went wrong while replaying {}. If the test name ends with .json it is searched as a full path. " +
193- "If it doesn't have an extension it will be searched in dochia-report/ folder. Error message: {}" , testCaseFileName , e .toString ());
334+ "If it doesn't have an extension it will be searched in the {} folder. Error message: {}" , testCaseFileName , reportFolder , e .toString ());
335+ }
336+ }
337+
338+ if (stats != null ) {
339+ printSummary (stats , testCases .size ());
340+ }
341+ }
342+
343+ private ReplayStats createInitialStats () {
344+ ReplayStats stats = new ReplayStats ();
345+ Path summaryPath = Paths .get (reportFolder , TestCaseExporter .REPORT_JS );
346+ try {
347+ String content = Files .readString (summaryPath );
348+ SummaryReport report = JsonUtils .GSON .fromJson (content , SummaryReport .class );
349+ if (report != null && report .testCases != null ) {
350+ for (TestCaseSummaryEntry entry : report .testCases ) {
351+ if ("error" .equalsIgnoreCase (entry .result )) {
352+ stats .initialErrors ++;
353+ } else if ("warn" .equalsIgnoreCase (entry .result )) {
354+ stats .initialWarnings ++;
355+ }
356+ }
194357 }
358+ } catch (Exception e ) {
359+ logger .debug ("Could not read initial stats: {}" , e .getMessage ());
360+ }
361+ return stats ;
362+ }
363+
364+ private void printSummary (ReplayStats stats , int totalReplayed ) {
365+ logger .noFormat ("" );
366+ logger .noFormat ("─" .repeat (60 ));
367+ logger .info ("Replay Summary" );
368+ logger .noFormat ("─" .repeat (60 ));
369+ logger .star ("Total tests replayed: {}" , totalReplayed );
370+ logger .star ("Initial errors in report: {}" , stats .initialErrors );
371+ logger .star ("Initial warnings in report: {}" , stats .initialWarnings );
372+ logger .noFormat ("" );
373+ logger .star ("Unchanged (same response code): {}" , stats .unchanged );
374+ logger .complete ("Improved (better response): {}" , stats .improved );
375+ if (stats .regressed > 0 ) {
376+ logger .error ("Regressed (worse response): {}" , stats .regressed );
377+ } else {
378+ logger .star ("Regressed (worse response): {}" , stats .regressed );
195379 }
380+ logger .noFormat ("─" .repeat (60 ));
196381 }
197382}
0 commit comments