Skip to content

Commit fd9c56b

Browse files
authored
Stabilize most commonly failing tests (Azure#36956)
Stabilize most commonly failing tests
1 parent 611ea33 commit fd9c56b

File tree

10 files changed

+205
-115
lines changed

10 files changed

+205
-115
lines changed

sdk/attestation/azure-security-attestation/src/test/java/com/azure/security/attestation/models/AttestationTokenTests.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.security.cert.X509Certificate;
1515
import java.time.OffsetDateTime;
1616
import java.time.ZoneOffset;
17+
import java.time.format.DateTimeFormatter;
18+
import java.time.temporal.ChronoUnit;
1719
import java.util.LinkedHashMap;
1820

1921
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -24,6 +26,7 @@
2426
import static org.junit.jupiter.api.Assertions.assertNull;
2527
import static org.junit.jupiter.api.Assertions.assertThrows;
2628
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
import static org.junit.jupiter.api.Assertions.fail;
2730

2831
/**
2932
* Test for Attestation Signing Certificates APIs.
@@ -269,8 +272,7 @@ void verifyAttestationTokenIssuer() {
269272

270273
@Test
271274
void verifyAttestationTokenExpireTimeout() {
272-
OffsetDateTime timeNow = OffsetDateTime.now();
273-
timeNow = timeNow.minusNanos(timeNow.getNano());
275+
final OffsetDateTime timeNow = OffsetDateTime.now().truncatedTo(ChronoUnit.MICROS);
274276

275277
TestObject testObjectExpired30SecondsAgo = new TestObject()
276278
.setAlg("Test Algorithm")
@@ -290,14 +292,36 @@ void verifyAttestationTokenExpireTimeout() {
290292
((AttestationTokenImpl) newToken).validate(null,
291293
new AttestationTokenValidationOptions()));
292294
// Both the current time and the expiration time should be in the exception message.
293-
assertTrue(ex.getMessage().contains("expiration"));
295+
String exceptionMessage = ex.getMessage();
296+
assertTrue(exceptionMessage.contains("expiration"), () ->
297+
"Expected exception message to contain 'expiration' but it didn't. Actual exception message: "
298+
+ exceptionMessage);
294299
// Because the TestObject round-trips times through Epoch times, they are in UTC time.
295300
// Adjust the target time to be in UTC rather than the current time zone, since we're checking to ensure
296301
// that the time is reflected in the exception message.
297302
OffsetDateTime expTime = timeNow.minusSeconds(30).withOffsetSameInstant(ZoneOffset.UTC);
298303

299-
assertTrue(ex.getMessage().contains(String.format("%tc", timeNow)));
300-
assertTrue(ex.getMessage().contains(String.format("%tc", expTime)));
304+
// Format of the exception message is "Current time: <current time> Expiration time: <expiration time>"
305+
// Since the test could take a while and the current time is based on the time when the exception is thrown
306+
// this can cause it to be different than 'timeNow' when the test started.
307+
// To make sure this test isn't flaky capture the datetime string in the exception message, turn it into an
308+
// OffsetDateTime and compare it to 'timeNow' allowing for some skew.
309+
// Date format is 'Wed Sep 27 12:48:15 -04:00 2023'
310+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss XXX yyyy");
311+
312+
int currentTimeIndex = exceptionMessage.indexOf("Current time: ");
313+
int expirationTimeIndex = exceptionMessage.indexOf("Expiration time: ");
314+
String currentTimeInExceptionString = exceptionMessage.substring(currentTimeIndex + 14, expirationTimeIndex - 1);
315+
OffsetDateTime currentTimeInException = OffsetDateTime.parse(currentTimeInExceptionString, formatter);
316+
long skew = timeNow.until(currentTimeInException, ChronoUnit.SECONDS);
317+
if (skew > 5 || skew < 0) {
318+
fail(String.format("Expected exception message to contain 'Current Time' within 5 seconds, but not before, "
319+
+ "of %tc but it was greater. Actual exception message: %s", timeNow, exceptionMessage));
320+
}
321+
322+
assertTrue(exceptionMessage.contains(String.format("%tc", expTime)), () -> String.format(
323+
"Expected exception message to contain '%tc' but it didn't. Actual exception message: %s", expTime,
324+
exceptionMessage));
301325
}
302326

303327
@Test

sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/NettyAsyncHttpClient.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import io.netty.buffer.ByteBuf;
2727
import io.netty.buffer.Unpooled;
2828
import io.netty.channel.EventLoopGroup;
29+
import io.netty.handler.codec.http.HttpHeaders;
2930
import io.netty.handler.codec.http.HttpMethod;
31+
import io.netty.handler.proxy.HttpProxyHandler;
3032
import io.netty.handler.proxy.ProxyConnectException;
3133
import io.netty.handler.stream.ChunkedNioFile;
3234
import io.netty.handler.stream.ChunkedStream;
@@ -40,6 +42,8 @@
4042
import reactor.netty.NettyPipeline;
4143
import reactor.netty.http.client.HttpClientRequest;
4244
import reactor.netty.http.client.HttpClientResponse;
45+
import reactor.util.function.Tuple2;
46+
import reactor.util.function.Tuples;
4347

4448
import javax.net.ssl.SSLException;
4549
import java.io.IOException;
@@ -120,7 +124,8 @@ public Mono<HttpResponse> send(HttpRequest request, Context context) {
120124
private Mono<HttpResponse> attemptAsync(HttpRequest request, boolean eagerlyReadResponse,
121125
boolean ignoreResponseBody, boolean headersEagerlyConverted, Long responseTimeout,
122126
ProgressReporter progressReporter, boolean proxyRetry) {
123-
Flux<HttpResponse> nettyRequest = nettyClient.request(toReactorNettyHttpMethod(request.getHttpMethod()))
127+
Flux<Tuple2<HttpResponse, HttpHeaders>> nettyRequest = nettyClient
128+
.request(toReactorNettyHttpMethod(request.getHttpMethod()))
124129
.uri(request.getUrl().toString())
125130
.send(bodySendDelegate(request))
126131
.responseConnection(responseDelegate(request, disableBufferCopy, eagerlyReadResponse, ignoreResponseBody,
@@ -132,11 +137,18 @@ private Mono<HttpResponse> attemptAsync(HttpRequest request, boolean eagerlyRead
132137
}
133138

134139
return nettyRequest.single()
135-
.flatMap(response -> {
140+
.flatMap(responseAndHeaders -> {
141+
HttpResponse response = responseAndHeaders.getT1();
136142
if (addProxyHandler && response.getStatusCode() == 407) {
137-
return proxyRetry
138-
? Mono.error(new ProxyConnectException("Connection to proxy failed."))
139-
: Mono.error(new ProxyConnectException("First attempt to connect to proxy failed."));
143+
if (proxyRetry) {
144+
// Exhausted retry attempt return an error.
145+
return Mono.error(new HttpProxyHandler.HttpProxyConnectException(
146+
"Failed to connect to proxy. Status: 407", responseAndHeaders.getT2()));
147+
} else {
148+
// Retry the request.
149+
return attemptAsync(request, eagerlyReadResponse, ignoreResponseBody, headersEagerlyConverted,
150+
responseTimeout, progressReporter, true);
151+
}
140152
} else {
141153
return Mono.just(response);
142154
}
@@ -285,7 +297,7 @@ private static NettyOutbound sendInputStream(NettyOutbound reactorNettyOutbound,
285297
* HttpHeaders.
286298
* @return a delegate upon invocation setup Rest response object
287299
*/
288-
private static BiFunction<HttpClientResponse, Connection, Mono<HttpResponse>> responseDelegate(
300+
private static BiFunction<HttpClientResponse, Connection, Mono<Tuple2<HttpResponse, HttpHeaders>>> responseDelegate(
289301
HttpRequest restRequest, boolean disableBufferCopy, boolean eagerlyReadResponse, boolean ignoreResponseBody,
290302
boolean headersEagerlyConverted) {
291303
return (reactorNettyResponse, reactorNettyConnection) -> {
@@ -314,11 +326,11 @@ private static BiFunction<HttpClientResponse, Connection, Mono<HttpResponse>> re
314326
return reactorNettyConnection.inbound().receive().aggregate().asByteArray()
315327
.doFinally(ignored -> closeConnection(reactorNettyConnection))
316328
.switchIfEmpty(Mono.just(EMPTY_BYTES))
317-
.map(bytes -> new NettyAsyncHttpBufferedResponse(reactorNettyResponse, restRequest, bytes,
318-
headersEagerlyConverted));
329+
.map(bytes -> Tuples.of(new NettyAsyncHttpBufferedResponse(reactorNettyResponse, restRequest, bytes,
330+
headersEagerlyConverted), reactorNettyResponse.responseHeaders()));
319331
} else {
320-
return Mono.just(new NettyAsyncHttpResponse(reactorNettyResponse, reactorNettyConnection, restRequest,
321-
disableBufferCopy, headersEagerlyConverted));
332+
return Mono.just(Tuples.of(new NettyAsyncHttpResponse(reactorNettyResponse, reactorNettyConnection,
333+
restRequest, disableBufferCopy, headersEagerlyConverted), reactorNettyResponse.responseHeaders()));
322334
}
323335
};
324336
}

sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/HttpProxyHandler.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import java.util.HashMap;
4646
import java.util.List;
4747
import java.util.Map;
48-
import java.util.concurrent.atomic.AtomicBoolean;
4948
import java.util.concurrent.atomic.AtomicReference;
5049
import java.util.function.Supplier;
5150

@@ -94,7 +93,6 @@ public final class HttpProxyHandler extends ProxyHandler {
9493
private final AuthorizationChallengeHandler challengeHandler;
9594
private final AtomicReference<ChallengeHolder> proxyChallengeHolderReference;
9695
private final HttpClientCodec codec;
97-
private final AtomicBoolean hasHandledChallenge = new AtomicBoolean();
9896

9997
private String authScheme = null;
10098
private HttpResponseStatus status;
@@ -190,7 +188,6 @@ private String createAuthorizationHeader() {
190188
* created from the same client.
191189
*/
192190
if (proxyChallengeHolder != null) {
193-
hasHandledChallenge.set(true);
194191
// Attempt to apply digest challenges, these are preferred over basic authorization.
195192
List<Map<String, String>> digestChallenges = proxyChallengeHolder.getDigestChallenges();
196193
if (!CoreUtils.isNullOrEmpty(digestChallenges)) {
@@ -238,19 +235,9 @@ protected boolean handleResponse(ChannelHandlerContext ctx, Object o) throws Pro
238235
}
239236

240237
boolean responseComplete = o instanceof LastHttpContent;
241-
if (responseComplete) {
242-
if (status == null) {
243-
throw new io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException(
244-
"Never received response for CONNECT request.", innerHeaders);
245-
} else if (status.code() != 200) {
246-
// Return the error response on the first attempt as the proxy handler doesn't apply credentials on the
247-
// first attempt.
248-
if (hasHandledChallenge.get()) {
249-
// Later attempts throw an exception.
250-
throw new io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException(
251-
"Failed to connect to proxy. Status: " + status, innerHeaders);
252-
}
253-
}
238+
if (responseComplete && status == null) {
239+
throw new io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException(
240+
"Never received response for CONNECT request.", innerHeaders);
254241
}
255242

256243
return responseComplete;

sdk/core/azure-core/src/main/java/com/azure/core/http/ProxyOptions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.regex.PatternSyntaxException;
2323

2424
/**
25-
* This represents proxy configuration to be used in http clients..
25+
* This represents proxy configuration to be used in http clients.
2626
*/
2727
public class ProxyOptions {
2828
private static final ClientLogger LOGGER = new ClientLogger(ProxyOptions.class);
@@ -136,7 +136,7 @@ public InetSocketAddress getAddress() {
136136
}
137137

138138
/**
139-
* Gets the type of the prxoy.
139+
* Gets the type of the proxy.
140140
*
141141
* @return the type of the proxy.
142142
*/

sdk/core/azure-core/src/main/java/com/azure/core/http/policy/HttpLoggingPolicy.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -246,16 +246,14 @@ private void logBody(HttpRequest request, int contentLength, LoggingEventBuilder
246246
} else {
247247
// Add non-mutating operators to the data stream.
248248
AccessibleByteArrayOutputStream stream = new AccessibleByteArrayOutputStream(contentLength);
249-
request.setBody(
250-
content.toFluxByteBuffer()
251-
.doOnNext(byteBuffer -> {
252-
try {
253-
ImplUtils.writeByteBufferToStream(byteBuffer.duplicate(), stream);
254-
} catch (IOException ex) {
255-
throw LOGGER.logExceptionAsError(new UncheckedIOException(ex));
256-
}
257-
})
258-
.doFinally(ignored -> logBody(logBuilder, logger, contentType, stream.toString(StandardCharsets.UTF_8))));
249+
request.setBody(Flux.using(() -> content, con -> con.toFluxByteBuffer()
250+
.doOnNext(byteBuffer -> {
251+
try {
252+
ImplUtils.writeByteBufferToStream(byteBuffer.duplicate(), stream);
253+
} catch (IOException ex) {
254+
throw LOGGER.logExceptionAsError(new UncheckedIOException(ex));
255+
}
256+
}), ignored -> logBody(logBuilder, logger, contentType, stream.toString(StandardCharsets.UTF_8))));
259257
}
260258
}
261259

@@ -548,15 +546,14 @@ public HttpHeaders getHeaders() {
548546
public Flux<ByteBuffer> getBody() {
549547
AccessibleByteArrayOutputStream stream = new AccessibleByteArrayOutputStream(contentLength);
550548

551-
return actualResponse.getBody()
549+
return Flux.using(() -> actualResponse, response -> response.getBody()
552550
.doOnNext(byteBuffer -> {
553551
try {
554552
ImplUtils.writeByteBufferToStream(byteBuffer.duplicate(), stream);
555553
} catch (IOException ex) {
556554
throw LOGGER.logExceptionAsError(new UncheckedIOException(ex));
557555
}
558-
})
559-
.doFinally(ignored -> doLog(stream.toString(StandardCharsets.UTF_8)));
556+
}), ignored -> doLog(stream.toString(StandardCharsets.UTF_8)));
560557
}
561558

562559
@Override

sdk/core/azure-core/src/main/java/com/azure/core/implementation/logging/DefaultLogger.java

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@
1212

1313
import java.io.PrintWriter;
1414
import java.io.StringWriter;
15+
import java.nio.charset.StandardCharsets;
1516
import java.nio.file.InvalidPathException;
1617
import java.time.LocalDateTime;
17-
import java.time.format.DateTimeFormatter;
18+
import java.time.temporal.ChronoField;
1819

1920
/**
2021
* This class is an internal implementation of slf4j logger.
2122
*/
2223
public final class DefaultLogger extends MarkerIgnoringBase {
2324
private static final long serialVersionUID = -144261058636441630L;
24-
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
2525

2626
// The template for the log message:
2727
// YYYY-MM-DD HH:MM:ss.SSS [thread] [level] classpath - message
@@ -49,7 +49,7 @@ public final class DefaultLogger extends MarkerIgnoringBase {
4949
* @param clazz Class creating the logger.
5050
*/
5151
public DefaultLogger(Class<?> clazz) {
52-
this(clazz.getName());
52+
this(clazz.getCanonicalName(), false);
5353
}
5454

5555
/**
@@ -59,14 +59,20 @@ public DefaultLogger(Class<?> clazz) {
5959
* name passes in.
6060
*/
6161
public DefaultLogger(String className) {
62-
String classPath;
62+
this(getClassPathFromClassName(className), false);
63+
}
64+
65+
private static String getClassPathFromClassName(String className) {
6366
try {
64-
classPath = Class.forName(className).getCanonicalName();
67+
return Class.forName(className).getCanonicalName();
6568
} catch (ClassNotFoundException | InvalidPathException e) {
6669
// Swallow ClassNotFoundException as the passed class name may not correlate to an actual class.
6770
// Swallow InvalidPathException as the className may contain characters that aren't legal file characters.
68-
classPath = className;
71+
return className;
6972
}
73+
}
74+
75+
private DefaultLogger(String classPath, boolean ignored) {
7076
this.classPath = classPath;
7177
int configuredLogLevel = fromEnvironment().getLogLevel();
7278

@@ -75,7 +81,6 @@ public DefaultLogger(String className) {
7581
isInfoEnabled = LogLevel.INFORMATIONAL.getLogLevel() >= configuredLogLevel;
7682
isWarnEnabled = LogLevel.WARNING.getLogLevel() >= configuredLogLevel;
7783
isErrorEnabled = LogLevel.ERROR.getLogLevel() >= configuredLogLevel;
78-
7984
}
8085

8186
private static LogLevel fromEnvironment() {
@@ -377,9 +382,57 @@ private void log(String levelName, String message, Throwable t) {
377382
*
378383
* @return The current time in {@code DATE_FORMAT}
379384
*/
380-
private String getFormattedDate() {
385+
private static String getFormattedDate() {
381386
LocalDateTime now = LocalDateTime.now();
382-
return DATE_FORMAT.format(now);
387+
388+
// yyyy-MM-dd HH:mm:ss.SSS
389+
// 23 characters that will be ASCII
390+
byte[] bytes = new byte[23];
391+
392+
// yyyy-
393+
int year = now.getYear();
394+
int round = year / 1000;
395+
bytes[0] = (byte) ('0' + round);
396+
year = year - (1000 * round);
397+
round = year / 100;
398+
bytes[1] = (byte) ('0' + round);
399+
year = year - (100 * round);
400+
round = year / 10;
401+
bytes[2] = (byte) ('0' + round);
402+
bytes[3] = (byte) ('0' + (year - (10 * round)));
403+
bytes[4] = '-';
404+
405+
// MM-
406+
zeroPad(now.getDayOfMonth(), bytes, 5);
407+
bytes[7] = '-';
408+
409+
// dd
410+
zeroPad(now.getDayOfMonth(), bytes, 8);
411+
bytes[10] = ' ';
412+
413+
// HH:
414+
zeroPad(now.getHour(), bytes, 11);
415+
bytes[13] = ':';
416+
417+
// mm:
418+
zeroPad(now.getMinute(), bytes, 14);
419+
bytes[16] = ':';
420+
421+
// ss.
422+
zeroPad(now.getSecond(), bytes, 17);
423+
bytes[19] = '.';
424+
425+
// SSS
426+
int millis = now.get(ChronoField.MILLI_OF_SECOND);
427+
round = millis / 100;
428+
bytes[20] = (byte) ('0' + round);
429+
millis = millis - (100 * round);
430+
round = millis / 10;
431+
bytes[21] = (byte) ('0' + round);
432+
bytes[22] = (byte) ('0' + (millis - (10 * round)));
433+
434+
// Use UTF-8 as it's more performant than ASCII in Java 8
435+
return new String(bytes, StandardCharsets.UTF_8);
383436
}
384437

385438
/**
@@ -398,4 +451,15 @@ void writeWithThrowable(StringBuilder stringBuilder, Throwable t) {
398451
}
399452
System.out.print(stringBuilder.toString());
400453
}
454+
455+
private static void zeroPad(int value, byte[] bytes, int index) {
456+
if (value < 10) {
457+
bytes[index++] = '0';
458+
bytes[index] = (byte) ('0' + value);
459+
} else {
460+
int high = value / 10;
461+
bytes[index++] = (byte) ('0' + high);
462+
bytes[index] = (byte) ('0' + (value - (10 * high)));
463+
}
464+
}
401465
}

0 commit comments

Comments
 (0)