Skip to content

Commit 3e0160a

Browse files
authored
Add coverage to sync http api. (Azure#29531)
* wip. * test HttpClient.sendSync * tests * one try. * pr feedback.
1 parent 53aa670 commit 3e0160a

File tree

4 files changed

+342
-44
lines changed

4 files changed

+342
-44
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.core.test;
5+
6+
import com.azure.core.test.annotation.SyncAsyncTest;
7+
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
8+
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
9+
import org.junit.jupiter.api.extension.Extension;
10+
import org.junit.jupiter.api.extension.ExtensionContext;
11+
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
12+
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
13+
import reactor.core.publisher.Mono;
14+
15+
import java.util.Collections;
16+
import java.util.List;
17+
import java.util.concurrent.Callable;
18+
import java.util.stream.Stream;
19+
20+
/**
21+
* An test template extension that helps to branch out a single test into sync and async invocation.
22+
*/
23+
public final class SyncAsyncExtension implements TestTemplateInvocationContextProvider {
24+
25+
private static final ThreadLocal<Boolean> IS_SYNC_THREAD_LOCAL = new ThreadLocal<>();
26+
private static final ThreadLocal<Boolean> WAS_EXTENSION_USED_THREAD_LOCAL = new ThreadLocal<>();
27+
28+
/**
29+
* Executes sync or async branch depending on the context.
30+
* @param sync sync callable.
31+
* @param async async callable. It should block at some point to return result.
32+
* @param <T> type of result of either sync or async invocation.
33+
* @return result of either sync or async invocation.
34+
* @throws IllegalStateException if extension doesn't work as expected.
35+
* @throws RuntimeException a runtime exception wrapping error from callable.
36+
*/
37+
public static <T> T execute(Callable<T> sync, Callable<Mono<T>> async) {
38+
Boolean isSync = IS_SYNC_THREAD_LOCAL.get();
39+
WAS_EXTENSION_USED_THREAD_LOCAL.set(true);
40+
if (isSync == null) {
41+
throw new IllegalStateException("The IS_SYNC_THREAD_LOCAL is undefined. Make sure you're using"
42+
+ "@SyncAsyncTest with SyncAsyncExtension.execute()");
43+
} else {
44+
try {
45+
if (isSync) {
46+
return sync.call();
47+
} else {
48+
return async.call().block();
49+
}
50+
} catch (RuntimeException e) {
51+
throw e;
52+
} catch (Exception e) {
53+
throw new RuntimeException(e);
54+
}
55+
}
56+
}
57+
58+
/**
59+
* Executes sync or async branch depending on the context.
60+
* @param sync sync callable.
61+
* @param async async callable. It should block at some point to return result.
62+
* @throws IllegalStateException if extension doesn't work as expected.
63+
* @throws RuntimeException a runtime exception wrapping error from callable.
64+
*/
65+
public static void execute(Runnable sync, Callable<Mono<Void>> async) {
66+
Boolean isSync = IS_SYNC_THREAD_LOCAL.get();
67+
WAS_EXTENSION_USED_THREAD_LOCAL.set(true);
68+
if (isSync == null) {
69+
throw new IllegalStateException("The IS_SYNC_THREAD_LOCAL is undefined. Make sure you're using"
70+
+ "@SyncAsyncTest with SyncAsyncExtension.execute()");
71+
} else {
72+
try {
73+
if (isSync) {
74+
sync.run();
75+
} else {
76+
async.call().block();
77+
}
78+
} catch (RuntimeException e) {
79+
throw e;
80+
} catch (Exception e) {
81+
throw new RuntimeException(e);
82+
}
83+
}
84+
}
85+
86+
@Override
87+
public boolean supportsTestTemplate(ExtensionContext extensionContext) {
88+
return extensionContext.getTestMethod()
89+
.map(method -> method.getAnnotation(SyncAsyncTest.class) != null)
90+
.orElse(false);
91+
}
92+
93+
@Override
94+
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
95+
ExtensionContext extensionContext) {
96+
return Stream.of(
97+
new SyncAsyncTestTemplateInvocationContext(true),
98+
new SyncAsyncTestTemplateInvocationContext(false)
99+
);
100+
}
101+
102+
private static final class SyncAsyncTestTemplateInvocationContext implements TestTemplateInvocationContext {
103+
private final boolean isSync;
104+
105+
private SyncAsyncTestTemplateInvocationContext(boolean isSync) {
106+
this.isSync = isSync;
107+
}
108+
109+
@Override
110+
public String getDisplayName(int invocationIndex) {
111+
return isSync ? "sync" : "async";
112+
}
113+
114+
@Override
115+
public List<Extension> getAdditionalExtensions() {
116+
return Collections.singletonList(new SyncAsyncTestInterceptor(isSync));
117+
}
118+
}
119+
120+
private static final class SyncAsyncTestInterceptor
121+
implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
122+
private final boolean isSync;
123+
124+
private SyncAsyncTestInterceptor(boolean isSync) {
125+
this.isSync = isSync;
126+
}
127+
128+
@Override
129+
public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
130+
if (IS_SYNC_THREAD_LOCAL.get() != null) {
131+
throw new IllegalStateException("The IS_SYNC_THREAD_LOCAL shouldn't be set at this point");
132+
}
133+
if (WAS_EXTENSION_USED_THREAD_LOCAL.get() != null) {
134+
throw new IllegalStateException("The WAS_EXTENSION_USED_THREAD_LOCAL shouldn't be set at this point");
135+
}
136+
IS_SYNC_THREAD_LOCAL.set(isSync);
137+
WAS_EXTENSION_USED_THREAD_LOCAL.set(false);
138+
}
139+
140+
@Override
141+
public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
142+
IS_SYNC_THREAD_LOCAL.remove();
143+
if (!WAS_EXTENSION_USED_THREAD_LOCAL.get()) {
144+
throw new IllegalStateException(
145+
"You should use SyncAsyncExtension.execute() in test annotated with @SyncAsyncTest");
146+
}
147+
WAS_EXTENSION_USED_THREAD_LOCAL.remove();
148+
}
149+
}
150+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.core.test.annotation;
5+
6+
import com.azure.core.test.SyncAsyncExtension;
7+
import org.junit.jupiter.api.TestTemplate;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
10+
import java.lang.annotation.ElementType;
11+
import java.lang.annotation.Retention;
12+
import java.lang.annotation.RetentionPolicy;
13+
import java.lang.annotation.Target;
14+
import java.util.concurrent.Callable;
15+
16+
/**
17+
* Indicates that test should be branched out into sync and async branch resulting in two separate test cases.
18+
* The {@link com.azure.core.test.SyncAsyncExtension#execute(Callable, Callable)} should be used in the test
19+
* to branch out.
20+
*/
21+
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
22+
@Retention(RetentionPolicy.RUNTIME)
23+
@TestTemplate
24+
@ExtendWith(SyncAsyncExtension.class)
25+
public @interface SyncAsyncTest {
26+
}

0 commit comments

Comments
 (0)