Skip to content

Commit 01d003d

Browse files
authored
test: overhaul automated compatibility coverage (#123)
* test: overhaul automated compatibility coverage * fix(ci): restore retry for unit integration tests * chore: fix license headers and update changelog * fix: address coderabbit stability and compatibility findings * fix: address new review findings from bots * fix: simplify customizer SPI and tighten CI retry timeout
1 parent df7aef2 commit 01d003d

File tree

57 files changed

+4531
-84
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4531
-84
lines changed

.github/workflows/build.yml

Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
# This workflow will build a Java project with Maven
17-
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
1816

1917
name: build
2018

@@ -25,40 +23,173 @@ on:
2523
branches: [ main ]
2624

2725
jobs:
28-
build:
26+
compile-matrix:
2927
runs-on: ubuntu-latest
3028
strategy:
3129
matrix:
3230
jdk: [8, 11, 17]
3331
steps:
34-
- uses: actions/checkout@v2
32+
- uses: actions/checkout@v4
3533
- name: Set up JDK
36-
uses: actions/setup-java@v1
34+
uses: actions/setup-java@v4
3735
with:
36+
distribution: temurin
3837
java-version: ${{ matrix.jdk }}
3938
- name: Cache Maven packages
4039
uses: actions/cache@v4
4140
with:
4241
path: ~/.m2/repository
43-
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
42+
key: ${{ runner.os }}-maven-compile-${{ matrix.jdk }}-${{ hashFiles('**/pom.xml') }}
4443
restore-keys: |
44+
${{ runner.os }}-maven-compile-${{ matrix.jdk }}-
4545
${{ runner.os }}-maven-
46-
- name: JDK 8
47-
if: matrix.jdk == '8'
46+
- name: Compile
47+
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true
48+
49+
unit-integration-pr:
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- name: Set up JDK 8
54+
uses: actions/setup-java@v4
55+
with:
56+
distribution: temurin
57+
java-version: 8
58+
- name: Cache Maven packages
59+
uses: actions/cache@v4
60+
with:
61+
path: ~/.m2/repository
62+
key: ${{ runner.os }}-maven-unit-integration-${{ hashFiles('**/pom.xml') }}
63+
restore-keys: |
64+
${{ runner.os }}-maven-unit-integration-
65+
${{ runner.os }}-maven-
66+
- name: Run unit and integration tests
4867
uses: nick-fields/retry@v3
4968
with:
50-
timeout_minutes: 3
69+
timeout_minutes: 4
5170
max_attempts: 3
5271
retry_wait_seconds: 1
53-
command: mvn -B clean package -P travis jacoco:report -Dmaven.gitcommitid.skip=true
54-
- name: JDK 11
55-
if: matrix.jdk == '11'
56-
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true
57-
- name: JDK 17
58-
if: matrix.jdk == '17'
59-
run: mvn -B clean compile -Dmaven.gitcommitid.skip=true
72+
command: mvn -B clean test -P travis jacoco:report -Dmaven.gitcommitid.skip=true
6073
- name: Upload coverage to Codecov
61-
if: matrix.jdk == '8'
62-
uses: codecov/codecov-action@v1
74+
uses: codecov/codecov-action@v4
6375
with:
64-
file: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
76+
files: ${{ github.workspace }}/apollo-*/target/site/jacoco/jacoco.xml
77+
78+
compat-api:
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: actions/checkout@v4
82+
- name: Set up JDK 8
83+
uses: actions/setup-java@v4
84+
with:
85+
distribution: temurin
86+
java-version: 8
87+
- name: Cache Maven packages
88+
uses: actions/cache@v4
89+
with:
90+
path: ~/.m2/repository
91+
key: ${{ runner.os }}-maven-compat-api-${{ hashFiles('**/pom.xml') }}
92+
restore-keys: |
93+
${{ runner.os }}-maven-compat-api-
94+
${{ runner.os }}-maven-
95+
- name: Build local artifacts for api compatibility
96+
run: |
97+
mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \
98+
-DskipTests install -Dmaven.gitcommitid.skip=true
99+
- name: Run api compatibility tests
100+
run: |
101+
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-api-compat-it \
102+
test -Dmaven.gitcommitid.skip=true
103+
104+
compat-spring:
105+
runs-on: ubuntu-latest
106+
strategy:
107+
matrix:
108+
include:
109+
- name: spring-3.1.1-jdk8
110+
java: 8
111+
spring_framework: 3.1.1.RELEASE
112+
java_version_prop: 1.8
113+
- name: spring-6.1-jdk17
114+
java: 17
115+
spring_framework: 6.1.18
116+
java_version_prop: 17
117+
name: compat-spring-${{ matrix.name }}
118+
steps:
119+
- uses: actions/checkout@v4
120+
- name: Set up JDK
121+
uses: actions/setup-java@v4
122+
with:
123+
distribution: temurin
124+
java-version: ${{ matrix.java }}
125+
- name: Cache Maven packages
126+
uses: actions/cache@v4
127+
with:
128+
path: ~/.m2/repository
129+
key: ${{ runner.os }}-maven-compat-spring-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }}
130+
restore-keys: |
131+
${{ runner.os }}-maven-compat-spring-${{ matrix.name }}-
132+
${{ runner.os }}-maven-
133+
- name: Build local artifacts for compat tests
134+
run: |
135+
mvn -B -pl apollo-core,apollo-client,apollo-mockserver -am \
136+
-DskipTests install -Dmaven.gitcommitid.skip=true
137+
- name: Run spring compatibility tests
138+
run: |
139+
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-compat-it \
140+
-Dspring.framework.version=${{ matrix.spring_framework }} \
141+
-Djava.version=${{ matrix.java_version_prop }} \
142+
test -Dmaven.gitcommitid.skip=true
143+
144+
compat-spring-boot:
145+
runs-on: ubuntu-latest
146+
strategy:
147+
matrix:
148+
include:
149+
- name: spring-boot-2.7-jdk8
150+
java: 8
151+
spring_boot: 2.7.18
152+
java_version_prop: 1.8
153+
compat_slf4j: 1.7.36
154+
compat_vintage: 5.7.0
155+
- name: spring-boot-3.3-jdk17
156+
java: 17
157+
spring_boot: 3.3.10
158+
java_version_prop: 17
159+
compat_slf4j: 2.0.17
160+
compat_vintage: 5.10.5
161+
- name: spring-boot-4.0-jdk17
162+
java: 17
163+
spring_boot: 4.0.0
164+
java_version_prop: 17
165+
compat_slf4j: 2.0.17
166+
compat_vintage: 6.0.1
167+
name: compat-spring-boot-${{ matrix.name }}
168+
steps:
169+
- uses: actions/checkout@v4
170+
- name: Set up JDK
171+
uses: actions/setup-java@v4
172+
with:
173+
distribution: temurin
174+
java-version: ${{ matrix.java }}
175+
- name: Cache Maven packages
176+
uses: actions/cache@v4
177+
with:
178+
path: ~/.m2/repository
179+
key: ${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}-${{ hashFiles('**/pom.xml') }}
180+
restore-keys: |
181+
${{ runner.os }}-maven-compat-spring-boot-${{ matrix.name }}-
182+
${{ runner.os }}-maven-
183+
- name: Build local artifacts for spring boot compatibility
184+
run: |
185+
mvn -B -pl apollo-core,apollo-client,apollo-mockserver,apollo-client-config-data -am \
186+
-DskipTests \
187+
install -Dmaven.gitcommitid.skip=true
188+
- name: Run spring boot compatibility tests
189+
run: |
190+
mvn -B -f apollo-compat-tests/pom.xml -pl apollo-spring-boot-compat-it \
191+
-Dspring-boot.version=${{ matrix.spring_boot }} \
192+
-Djava.version=${{ matrix.java_version_prop }} \
193+
-Dcompat.slf4j.version=${{ matrix.compat_slf4j }} \
194+
-Dcompat.junit.vintage.version=${{ matrix.compat_vintage }} \
195+
test -Dmaven.gitcommitid.skip=true

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Apollo Java 2.5.0
1010
* [Feature Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo-java/pull/103)
1111
* [Feature Support retry in open api client.](https://github.com/apolloconfig/apollo-java/pull/105)
1212
* [Support Spring Boot 4.0 bootstrap context package relocation for apollo-client-config-data](https://github.com/apolloconfig/apollo-java/pull/115)
13+
* [Test Overhaul automated compatibility coverage across API/Spring/Spring Boot scenarios](https://github.com/apolloconfig/apollo-java/pull/123)
1314

1415
------------------
1516
All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/5?closed=1)

apollo-client-config-data/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,15 @@
7373
</exclusion>
7474
</exclusions>
7575
</dependency>
76+
<dependency>
77+
<groupId>com.ctrip.framework.apollo</groupId>
78+
<artifactId>apollo-mockserver</artifactId>
79+
<scope>test</scope>
80+
</dependency>
81+
<dependency>
82+
<groupId>io.projectreactor.netty</groupId>
83+
<artifactId>reactor-netty-http</artifactId>
84+
<scope>test</scope>
85+
</dependency>
7686
</dependencies>
7787
</project>

apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientPropertiesFactory.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.ctrip.framework.apollo.config.data.extension.initialize;
1818

1919
import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
20-
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
2120
import org.springframework.boot.context.properties.bind.BindHandler;
2221
import org.springframework.boot.context.properties.bind.Bindable;
2322
import org.springframework.boot.context.properties.bind.Binder;
@@ -35,10 +34,4 @@ public ApolloClientProperties createApolloClientProperties(
3534
return binder.bind(PROPERTIES_PREFIX,
3635
Bindable.of(ApolloClientProperties.class), bindHandler).orElse(null);
3736
}
38-
39-
public OAuth2ClientProperties createOauth2ClientProperties(Binder binder,
40-
BindHandler bindHandler) {
41-
return binder.bind("spring.security.oauth2.client", Bindable.of(OAuth2ClientProperties.class),
42-
bindHandler).orElse(null);
43-
}
4437
}

apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
import com.ctrip.framework.apollo.util.http.HttpClient;
2424
import com.ctrip.framework.foundation.internals.ServiceBootstrap;
2525
import java.util.List;
26+
import java.util.function.Consumer;
2627
import org.apache.commons.logging.Log;
2728
import org.springframework.boot.context.properties.bind.BindHandler;
2829
import org.springframework.boot.context.properties.bind.Binder;
2930
import org.springframework.boot.logging.DeferredLogFactory;
30-
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
3131
import org.springframework.util.CollectionUtils;
3232
import org.springframework.web.reactive.function.client.WebClient;
3333

@@ -55,11 +55,11 @@ public void initialize(ApolloClientProperties apolloClientProperties, Binder bin
5555
.loadAllOrdered(ApolloClientWebClientCustomizerFactory.class);
5656
if (!CollectionUtils.isEmpty(factories)) {
5757
for (ApolloClientWebClientCustomizerFactory factory : factories) {
58-
WebClientCustomizer webClientCustomizer = factory
58+
Consumer<WebClient.Builder> webClientCustomizer = factory
5959
.createWebClientCustomizer(apolloClientProperties, binder, bindHandler, this.log,
6060
this.bootstrapContext);
6161
if (webClientCustomizer != null) {
62-
webClientCustomizer.customize(webClientBuilder);
62+
webClientCustomizer.accept(webClientBuilder);
6363
}
6464
}
6565
}

apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloWebClientHttpClient.java

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222
import com.ctrip.framework.apollo.util.http.HttpRequest;
2323
import com.ctrip.framework.apollo.util.http.HttpResponse;
2424
import com.google.gson.Gson;
25+
import java.lang.reflect.Method;
2526
import java.lang.reflect.Type;
2627
import java.net.URI;
2728
import java.util.Map;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.ConcurrentMap;
2831
import org.springframework.http.HttpStatus;
2932
import org.springframework.util.CollectionUtils;
33+
import org.springframework.web.reactive.function.client.ClientResponse;
3034
import org.springframework.web.reactive.function.client.WebClient;
3135
import reactor.core.publisher.Mono;
3236

@@ -35,6 +39,10 @@
3539
*/
3640
public class ApolloWebClientHttpClient implements HttpClient {
3741

42+
private static final Method CLIENT_RESPONSE_STATUS_CODE_METHOD = resolveClientResponseStatusCodeMethod();
43+
private static final ConcurrentMap<Class<?>, Method> STATUS_CODE_VALUE_METHOD_CACHE =
44+
new ConcurrentHashMap<Class<?>, Method>();
45+
3846
private final WebClient webClient;
3947

4048
private final Gson gson;
@@ -64,15 +72,16 @@ private <T> HttpResponse<T> doGetInternal(HttpRequest httpRequest, Type response
6472
}
6573
}
6674
return requestHeadersSpec.exchangeToMono(clientResponse -> {
67-
if (HttpStatus.OK.equals(clientResponse.statusCode())) {
75+
int statusCode = this.resolveStatusCode(clientResponse);
76+
if (HttpStatus.OK.value() == statusCode) {
6877
return clientResponse.bodyToMono(String.class)
6978
.map(body -> new HttpResponse<T>(HttpStatus.OK.value(),
7079
gson.fromJson(body, responseType)));
7180
}
72-
if (HttpStatus.NOT_MODIFIED.equals(clientResponse.statusCode())) {
81+
if (HttpStatus.NOT_MODIFIED.value() == statusCode) {
7382
return Mono.just(new HttpResponse<T>(HttpStatus.NOT_MODIFIED.value(), null));
7483
}
75-
return Mono.error(new ApolloConfigStatusCodeException(clientResponse.rawStatusCode(),
84+
return Mono.error(new ApolloConfigStatusCodeException(statusCode,
7685
String.format("Get operation failed for %s", httpRequest.getUrl())));
7786
}).block();
7887
}
@@ -82,4 +91,43 @@ public <T> HttpResponse<T> doGet(HttpRequest httpRequest, Type responseType)
8291
throws ApolloConfigException {
8392
return this.doGetInternal(httpRequest, responseType);
8493
}
94+
95+
/**
96+
* Resolve HTTP status code across Spring WebFlux 5/6/7.
97+
*
98+
* <p>ClientResponse#statusCode has different return types across major versions
99+
* (HttpStatus in Spring 5, HttpStatusCode in Spring 6/7). Calling it directly would bind
100+
* to one method descriptor at compile time and could fail on another runtime version.
101+
* Reflection keeps this bridge binary-compatible for Boot 2/3/4 compatibility tests.
102+
*/
103+
private int resolveStatusCode(Object clientResponse) {
104+
try {
105+
Object statusCode = CLIENT_RESPONSE_STATUS_CODE_METHOD.invoke(clientResponse);
106+
if (statusCode == null) {
107+
throw new ApolloConfigException("Failed to resolve response status code: statusCode is null");
108+
}
109+
Method valueMethod = STATUS_CODE_VALUE_METHOD_CACHE.computeIfAbsent(statusCode.getClass(),
110+
ApolloWebClientHttpClient::resolveStatusCodeValueMethod);
111+
Object value = valueMethod.invoke(statusCode);
112+
return ((Number) value).intValue();
113+
} catch (Exception ex) {
114+
throw new ApolloConfigException("Failed to resolve response status code", ex);
115+
}
116+
}
117+
118+
private static Method resolveClientResponseStatusCodeMethod() {
119+
try {
120+
return ClientResponse.class.getMethod("statusCode");
121+
} catch (NoSuchMethodException ex) {
122+
throw new ExceptionInInitializerError(ex);
123+
}
124+
}
125+
126+
private static Method resolveStatusCodeValueMethod(Class<?> statusCodeType) {
127+
try {
128+
return statusCodeType.getMethod("value");
129+
} catch (NoSuchMethodException ex) {
130+
throw new IllegalStateException("Failed to resolve value() method from " + statusCodeType, ex);
131+
}
132+
}
85133
}

apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@
1818

1919
import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties;
2020
import com.ctrip.framework.apollo.core.spi.Ordered;
21+
import java.util.function.Consumer;
2122
import org.apache.commons.logging.Log;
2223
import org.springframework.boot.context.properties.bind.BindHandler;
2324
import org.springframework.boot.context.properties.bind.Binder;
24-
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
2525
import org.springframework.lang.Nullable;
26+
import org.springframework.web.reactive.function.client.WebClient;
2627

2728
/**
2829
* @author vdisk <vdisk@foxmail.com>
2930
*/
3031
public interface ApolloClientWebClientCustomizerFactory extends Ordered {
3132

3233
/**
33-
* create a WebClientCustomizer instance
34+
* create a webclient builder customizer
3435
*
3536
* @param apolloClientProperties apollo client binded properties
3637
* @param binder properties binder
@@ -41,10 +42,10 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered {
4142
* Spring Boot 3.x or
4243
* org.springframework.boot.bootstrap.ConfigurableBootstrapContext
4344
* for Spring Boot 4.x)
44-
* @return WebClientCustomizer instance or null
45+
* @return customizer instance or null
4546
*/
4647
@Nullable
47-
WebClientCustomizer createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
48+
Consumer<WebClient.Builder> createWebClientCustomizer(ApolloClientProperties apolloClientProperties,
4849
Binder binder, BindHandler bindHandler, Log log,
4950
Object bootstrapContext);
5051
}

0 commit comments

Comments
 (0)