2828import static com .github .tomakehurst .wiremock .client .WireMock .urlEqualTo ;
2929import static com .github .tomakehurst .wiremock .client .WireMock .urlPathEqualTo ;
3030import static com .github .tomakehurst .wiremock .core .WireMockConfiguration .wireMockConfig ;
31+ import static com .github .tomakehurst .wiremock .stubbing .Scenario .STARTED ;
3132import static org .junit .jupiter .api .Assertions .assertDoesNotThrow ;
3233import static org .junit .jupiter .api .Assertions .assertEquals ;
3334import static org .junit .jupiter .api .Assertions .assertFalse ;
3839import java .time .Duration ;
3940import java .util .UUID ;
4041
42+ import org .junit .jupiter .api .AfterEach ;
43+ import org .junit .jupiter .api .BeforeEach ;
4144import org .junit .jupiter .api .Test ;
4245import org .junit .jupiter .api .extension .RegisterExtension ;
46+ import org .slf4j .LoggerFactory ;
47+
48+ import ch .qos .logback .classic .Level ;
49+ import ch .qos .logback .classic .Logger ;
50+ import ch .qos .logback .classic .spi .ILoggingEvent ;
51+ import ch .qos .logback .core .read .ListAppender ;
4352
4453import com .github .tomakehurst .wiremock .junit5 .WireMockExtension ;
4554import com .github .tomakehurst .wiremock .matching .RequestPatternBuilder ;
@@ -59,6 +68,15 @@ class ParasoftCoverageApiClientTest
5968 private static final String BAGGAGE_HEADER = "test-operator-id=automation-user+parallel-id" ;
6069 private static final String FAILURE_MESSAGE = "expected 4 but was 5" ;
6170
71+ private static final String MISSING_BAGGAGE_WARNING =
72+ "This version of CTP does not support parallel tests within a single coverage session." ;
73+ private static final String PUBLISH_STATUS_MESSAGE =
74+ "Publishing coverage and test results to DTP..." ;
75+ private static final String PUBLISH_SUCCESS_MESSAGE =
76+ "Successfully published coverage and results to DTP." ;
77+ private static final String PUBLISH_FAILURE_MESSAGE =
78+ "Failed to publish coverage and results to DTP." ;
79+
6280 private static final String SESSION_START_PATH = "/api/v3/environments/42/agents/session/start" ;
6381 private static final String TEST_START_PATH = "/api/v3/environments/42/agents/test/start" ;
6482 private static final String TEST_STOP_PATH = "/api/v3/environments/42/agents/test/stop" ;
@@ -82,11 +100,45 @@ class ParasoftCoverageApiClientTest
82100 }
83101 """ ;
84102
103+ private static final String TEST_STATUS_WITHOUT_BAGGAGE_RESPONSE = """
104+ {
105+ "test": "com.example.CalculatorTest",
106+ "testCase": "addsNumbers",
107+ "session": "coverage-session-123"
108+ }
109+ """ ;
110+
85111 @ RegisterExtension
86112 static final WireMockExtension WIREMOCK = WireMockExtension .newInstance ()
87113 .options (wireMockConfig ().dynamicPort ())
88114 .build ();
89115
116+ private final Logger clientLogger =
117+ (Logger ) LoggerFactory .getLogger (ParasoftCoverageApiClient .class );
118+ private final ListAppender <ILoggingEvent > logAppender = new ListAppender <>();
119+ private Level originalLogLevel ;
120+ private boolean originalAdditive ;
121+
122+ @ BeforeEach
123+ void attachLogAppender ()
124+ {
125+ originalLogLevel = clientLogger .getLevel ();
126+ originalAdditive = clientLogger .isAdditive ();
127+ clientLogger .setLevel (Level .DEBUG );
128+ clientLogger .setAdditive (false );
129+ logAppender .start ();
130+ clientLogger .addAppender (logAppender );
131+ }
132+
133+ @ AfterEach
134+ void detachLogAppender ()
135+ {
136+ clientLogger .detachAppender (logAppender );
137+ logAppender .stop ();
138+ clientLogger .setLevel (originalLogLevel );
139+ clientLogger .setAdditive (originalAdditive );
140+ }
141+
90142 @ Test
91143 void executesCoverageLifecycleWithBearerAuthenticationAndParallelId ()
92144 {
@@ -207,6 +259,55 @@ void omitsParallelIdWhenParallelExecutionIsDisabled()
207259 """ )));
208260 }
209261
262+ @ Test
263+ void logsDebugAndWarningWhenParallelStartTestResponseDoesNotIncludeBaggage ()
264+ {
265+ WIREMOCK .stubFor (post (urlEqualTo (TEST_START_PATH ))
266+ .willReturn (okJson (TEST_STATUS_WITHOUT_BAGGAGE_RESPONSE )));
267+
268+ ParasoftCoverageApiClient client = createClient (
269+ true ,
270+ null ,
271+ null ,
272+ BEARER_TOKEN );
273+
274+ CoverageTestContext testContext = client .startTest (TEST_ID , TEST_CASE_ID );
275+
276+ assertNotNull (testContext );
277+ assertNotNull (testContext .getParallelId ());
278+ assertNull (testContext .getBaggageHeader ());
279+ assertEquals (1 , countLogEvents (
280+ Level .DEBUG ,
281+ "CTP startTest response did not include the baggage property: test="
282+ + TEST_ID
283+ + ", testCase="
284+ + TEST_CASE_ID
285+ + ", parallelId="
286+ + testContext .getParallelId ()
287+ + ", responsePresent=true" ));
288+ assertEquals (1 , countLogEvents (Level .WARN , MISSING_BAGGAGE_WARNING ));
289+ }
290+
291+ @ Test
292+ void doesNotLogMissingBaggageWarningWhenParallelExecutionIsDisabled ()
293+ {
294+ WIREMOCK .stubFor (post (urlEqualTo (TEST_START_PATH ))
295+ .willReturn (okJson (TEST_STATUS_WITHOUT_BAGGAGE_RESPONSE )));
296+
297+ ParasoftCoverageApiClient client = createClient (
298+ false ,
299+ null ,
300+ null ,
301+ BEARER_TOKEN );
302+
303+ CoverageTestContext testContext = client .startTest (TEST_ID , TEST_CASE_ID );
304+
305+ assertNotNull (testContext );
306+ assertNull (testContext .getParallelId ());
307+ assertNull (testContext .getBaggageHeader ());
308+ assertEquals (0 , countLogEvents (Level .WARN , MISSING_BAGGAGE_WARNING ));
309+ }
310+
210311 @ Test
211312 void usesBasicAuthenticationWhenBearerTokenIsNotConfigured ()
212313 {
@@ -692,6 +793,7 @@ void returnsTestContextWithoutBaggageWhenStartTestRequestFails()
692793 assertFalse (testContext .getParallelId ().isBlank ());
693794 assertDoesNotThrow (() -> UUID .fromString (testContext .getParallelId ()));
694795 assertNull (testContext .getBaggageHeader ());
796+ assertEquals (0 , countLogEvents (Level .WARN , MISSING_BAGGAGE_WARNING ));
695797
696798 WIREMOCK .verify (1 , postRequestedFor (urlEqualTo (TEST_START_PATH ))
697799 .withHeader ("Authorization" , equalTo ("Bearer " + BEARER_TOKEN ))
@@ -885,6 +987,114 @@ void publishesCoverageAsynchronouslyAndPollsUntilPublished()
885987 WIREMOCK .verify (1 , pollRequest );
886988 }
887989
990+ @ Test
991+ void logsPublishStartAndCompletionOnceWithoutRepeatingIntermediateInfoMessages ()
992+ {
993+ WIREMOCK .stubFor (post (urlPathEqualTo (COVERAGE_PATH ))
994+ .willReturn (okJson ("""
995+ {
996+ "status": "PUBLISHING"
997+ }
998+ """ )));
999+
1000+ String scenarioName = "publish status logging" ;
1001+ String secondPollState = "second poll" ;
1002+ String publishedState = "published" ;
1003+
1004+ WIREMOCK .stubFor (get (urlPathEqualTo (COVERAGE_PATH ))
1005+ .inScenario (scenarioName )
1006+ .whenScenarioStateIs (STARTED )
1007+ .willReturn (okJson ("""
1008+ {
1009+ "status": "PUBLISHING",
1010+ "message": "Publishing coverage and test results to DTP..."
1011+ }
1012+ """ ))
1013+ .willSetStateTo (secondPollState ));
1014+
1015+ WIREMOCK .stubFor (get (urlPathEqualTo (COVERAGE_PATH ))
1016+ .inScenario (scenarioName )
1017+ .whenScenarioStateIs (secondPollState )
1018+ .willReturn (okJson ("""
1019+ {
1020+ "status": "PUBLISHING",
1021+ "message": "Publishing coverage and test results to DTP..."
1022+ }
1023+ """ ))
1024+ .willSetStateTo (publishedState ));
1025+
1026+ WIREMOCK .stubFor (get (urlPathEqualTo (COVERAGE_PATH ))
1027+ .inScenario (scenarioName )
1028+ .whenScenarioStateIs (publishedState )
1029+ .willReturn (okJson ("""
1030+ {
1031+ "status": "PUBLISHED",
1032+ "message": "Successfully published coverage and results to DTP.",
1033+ "passed": 1,
1034+ "failed": 0,
1035+ "incomplete": 0
1036+ }
1037+ """ )));
1038+
1039+ ParasoftCoverageApiClient client = createClient (
1040+ false ,
1041+ null ,
1042+ null ,
1043+ BEARER_TOKEN );
1044+
1045+ client .publishResults (
1046+ SESSION_ID ,
1047+ "Unit Test Configuration" ,
1048+ USER_ID ,
1049+ "JUnit" );
1050+
1051+ assertEquals (1 , countLogEvents (Level .INFO , PUBLISH_STATUS_MESSAGE ));
1052+ assertEquals (2 , countLogEvents (Level .DEBUG , PUBLISH_STATUS_MESSAGE ));
1053+ assertEquals (1 , countLogEvents (Level .INFO , PUBLISH_SUCCESS_MESSAGE ));
1054+ }
1055+
1056+ @ Test
1057+ void logsPublishFailureOnce ()
1058+ {
1059+ WIREMOCK .stubFor (post (urlPathEqualTo (COVERAGE_PATH ))
1060+ .willReturn (okJson ("""
1061+ {
1062+ "status": "PUBLISHING"
1063+ }
1064+ """ )));
1065+
1066+ WIREMOCK .stubFor (get (urlPathEqualTo (COVERAGE_PATH ))
1067+ .willReturn (okJson ("""
1068+ {
1069+ "status": "ERROR",
1070+ "message": "Failed to publish coverage and results to DTP."
1071+ }
1072+ """ )));
1073+
1074+ ParasoftCoverageApiClient client = createClient (
1075+ false ,
1076+ null ,
1077+ null ,
1078+ BEARER_TOKEN );
1079+
1080+ client .publishResults (
1081+ SESSION_ID ,
1082+ "Unit Test Configuration" ,
1083+ USER_ID ,
1084+ "JUnit" );
1085+
1086+ assertEquals (1 , countLogEvents (Level .INFO , PUBLISH_STATUS_MESSAGE ));
1087+ assertEquals (1 , countLogEvents (Level .ERROR , PUBLISH_FAILURE_MESSAGE ));
1088+ }
1089+
1090+ private long countLogEvents (Level level , String message )
1091+ {
1092+ return logAppender .list .stream ()
1093+ .filter (event -> event .getLevel () == level )
1094+ .filter (event -> message .equals (event .getFormattedMessage ()))
1095+ .count ();
1096+ }
1097+
8881098 private static ParasoftCoverageApiClient createClient (
8891099 boolean parallelIdEnabled ,
8901100 String username ,
0 commit comments