Skip to content

Commit 8b30683

Browse files
committed
Merge branch 'master' of ssh://github.com/ExpediaGroup/bull into develop
2 parents fee9016 + 4bf6529 commit 8b30683

10 files changed

Lines changed: 350 additions & 27 deletions

File tree

.github/workflows/github-default-actions.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ jobs:
5757
retry_on: error
5858
command: |
5959
mvn verify jacoco:report-aggregate -P compatibility-mode
60+
- name: "Upload coverage baseline"
61+
if: github.ref == 'refs/heads/master'
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: jacoco-coverage-baseline
65+
path: bull-report/target/site/jacoco-aggregate/jacoco.xml
66+
retention-days: 90
6067
- name: "Submit coverage to Coveralls"
6168
if: ${{ env.COVERALLS_IO_TOKEN != '' }}
6269
env:
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: "PR Coverage Report"
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
coverage:
14+
name: "Test Coverage Report"
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: "Checkout Repository"
18+
uses: actions/checkout@v6
19+
with:
20+
ref: ${{ github.event.pull_request.head.sha }}
21+
22+
- name: "Cache Maven repository"
23+
uses: actions/cache@v5.0.4
24+
with:
25+
path: ~/.m2
26+
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
27+
restore-keys: ${{ runner.os }}-m2
28+
29+
- name: "JDK set-up"
30+
uses: actions/setup-java@v5
31+
with:
32+
java-version: '17'
33+
distribution: 'temurin'
34+
35+
- name: "Build and Test"
36+
uses: nick-invision/retry@v4
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
with:
40+
max_attempts: 3
41+
timeout_minutes: 15
42+
retry_on: error
43+
command: |
44+
mvn verify jacoco:report-aggregate -B -P compatibility-mode -DskipPerformanceTests=true \
45+
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
46+
47+
- name: "Download baseline coverage"
48+
uses: dawidd6/action-download-artifact@v6
49+
continue-on-error: true
50+
with:
51+
workflow: github-default-actions.yml
52+
branch: master
53+
name: jacoco-coverage-baseline
54+
path: baseline/
55+
56+
- name: "Generate coverage table"
57+
run: |
58+
python3 << 'EOF'
59+
import json, os, xml.etree.ElementTree as ET
60+
61+
def parse(xml_path):
62+
root = ET.parse(xml_path).getroot()
63+
result = {}
64+
for c in root.findall('counter'):
65+
covered = int(c.get('covered', 0))
66+
missed = int(c.get('missed', 0))
67+
total = covered + missed
68+
result[c.get('type')] = {
69+
'covered': covered,
70+
'total': total,
71+
'pct': round(covered / total * 100, 2) if total > 0 else 0.0
72+
}
73+
return result
74+
75+
def fmt(p): return f'{p:.2f}%'
76+
def frac(c, t): return f'{c}/{t}'
77+
def sign(d): return f'{d:+.2f}%'
78+
def emoji(d):
79+
if d > 0.01: return '💚'
80+
if d < -0.01: return '🔴'
81+
return '💙'
82+
83+
build_xml = 'bull-report/target/site/jacoco-aggregate/jacoco.xml'
84+
baseline_xml = 'baseline/jacoco.xml'
85+
86+
build = parse(build_xml)
87+
baseline = parse(baseline_xml) if os.path.exists(baseline_xml) else build
88+
89+
metrics = [
90+
('LINE', 'Lines'),
91+
('BRANCH', 'Branches'),
92+
('INSTRUCTION', 'Instructions'),
93+
('COMPLEXITY', 'Complexity'),
94+
('METHOD', 'Methods'),
95+
('CLASS', 'Classes'),
96+
]
97+
98+
rows = []
99+
norm_base = norm_build = 0
100+
for key, label in metrics:
101+
b = baseline.get(key, {})
102+
c = build.get(key, {})
103+
bp = b.get('pct', 0.0)
104+
cp = c.get('pct', 0.0)
105+
d = cp - bp
106+
norm_base += bp
107+
norm_build += cp
108+
rows.append(
109+
f'| {label} | {fmt(bp)} | {frac(b.get("covered",0), b.get("total",0))} '
110+
f'| {fmt(cp)} | {frac(c.get("covered",0), c.get("total",0))} '
111+
f'| {sign(d)} | {emoji(d)} |'
112+
)
113+
114+
count = len(metrics)
115+
nb = norm_base / count
116+
nc = norm_build / count
117+
nd = nc - nb
118+
rows.append(f'| **Normalised Score** | {fmt(nb)} | | {fmt(nc)} | | {sign(nd)} | {emoji(nd)} |')
119+
120+
header = (
121+
'| Coverage | Baseline | | Build | | Diff | |\n'
122+
'|---|---|---|---|---|---|---|'
123+
)
124+
table = header + '\n' + '\n'.join(rows)
125+
report = f'## Test Coverage Report\n\n{table}\n'
126+
127+
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
128+
f.write(report)
129+
with open('coverage_comment.md', 'w') as f:
130+
f.write(report)
131+
EOF
132+
133+
- name: "Post coverage comment"
134+
uses: marocchino/sticky-pull-request-comment@v2
135+
with:
136+
path: coverage_comment.md
137+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CHANGELOG-JDK11.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
### [2.1.5-jdk11] TBD
6+
* Fixes an issue that was preventing the transformation of Kotlin classes with default parameter values
7+
58
### [2.1.4-jdk11] 2024.01.03
69
* Bumps to the latest version available all the project dependencies
710

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
### [3.0.1] TBD
6+
* Fixes an issue that was preventing the transformation of Kotlin classes with default parameter values
7+
58
### [3.0.0] 2025.05.21
69
* Increase the jdk version to 17
710

bull-common/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
<artifactId>joda-time</artifactId>
5858
<scope>test</scope>
5959
</dependency>
60+
<dependency>
61+
<groupId>org.jetbrains.kotlin</groupId>
62+
<artifactId>kotlin-stdlib</artifactId>
63+
<scope>test</scope>
64+
</dependency>
6065
</dependencies>
6166
<build>
6267
<plugins>

bull-common/src/main/java/com/expediagroup/transformer/utils/ClassUtils.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (C) 2019-2023 Expedia, Inc.
2+
* Copyright (C) 2019-2026 Expedia, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -89,6 +89,11 @@ public final class ClassUtils {
8989
*/
9090
private static final String CLAZZ_CANNOT_BE_NULL = "clazz cannot be null!";
9191

92+
/**
93+
* Fully qualified class name of the Kotlin synthetic default constructor marker.
94+
*/
95+
private static final String KOTLIN_DEFAULT_CONSTRUCTOR_MARKER = "kotlin.jvm.internal.DefaultConstructorMarker";
96+
9297
/**
9398
* CacheManager class {@link CacheManager}.
9499
*/
@@ -585,13 +590,30 @@ public <K> Constructor<K> getAllArgsConstructor(final Class<K> clazz) {
585590
final String cacheKey = "AllArgsConstructor-" + clazz.getName();
586591
return CACHE_MANAGER.getFromCache(cacheKey, Constructor.class).orElseGet(() -> {
587592
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
588-
final var constructor = max(asList(declaredConstructors), comparing(Constructor::getParameterCount));
593+
var candidates = stream(declaredConstructors)
594+
.filter(c -> !isKotlinSyntheticConstructor(c))
595+
.collect(toList());
596+
if (candidates.isEmpty()) {
597+
candidates = asList(declaredConstructors);
598+
}
599+
final var constructor = max(candidates, comparing(Constructor::getParameterCount));
589600
constructor.setAccessible(true);
590601
CACHE_MANAGER.cacheObject(cacheKey, constructor);
591602
return constructor;
592603
});
593604
}
594605

606+
/**
607+
* Checks if a constructor is a Kotlin synthetic constructor generated for default parameter values.
608+
* Such constructors have a parameter of type {@code kotlin.jvm.internal.DefaultConstructorMarker}.
609+
* @param constructor the constructor to check
610+
* @return true if the constructor is a Kotlin synthetic default constructor
611+
*/
612+
private boolean isKotlinSyntheticConstructor(final Constructor<?> constructor) {
613+
return stream(constructor.getParameterTypes())
614+
.anyMatch(paramType -> KOTLIN_DEFAULT_CONSTRUCTOR_MARKER.equals(paramType.getName()));
615+
}
616+
595617
/**
596618
* Gets all the constructor parameters.
597619
* @param constructor the constructor.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (C) 2019-2026 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.expediagroup.beans.sample.immutable;
17+
18+
import kotlin.jvm.internal.DefaultConstructorMarker;
19+
20+
/**
21+
* Sample immutable object that mimics a Kotlin data class with default parameter values.
22+
* Kotlin compiler generates a synthetic constructor with additional {@code int} (bitmask)
23+
* and {@link DefaultConstructorMarker} parameters for classes with default values.
24+
*/
25+
public class ImmutableToFooWithKotlinDefaultConstructor {
26+
private final String name;
27+
private final int id;
28+
29+
/**
30+
* Primary constructor (equivalent to the Kotlin data class constructor).
31+
*/
32+
public ImmutableToFooWithKotlinDefaultConstructor(final String name, final int id) {
33+
this.name = name;
34+
this.id = id;
35+
}
36+
37+
/**
38+
* Synthetic constructor generated by the Kotlin compiler for default parameter values.
39+
* This constructor should be ignored when selecting the all-args constructor.
40+
*/
41+
@SuppressWarnings("unused")
42+
ImmutableToFooWithKotlinDefaultConstructor(final String name, final int id, final int defaultMask, final DefaultConstructorMarker marker) {
43+
this(name, id);
44+
}
45+
46+
public String getName() {
47+
return name;
48+
}
49+
50+
public int getId() {
51+
return id;
52+
}
53+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (C) 2019-2026 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.expediagroup.beans.sample.immutable;
17+
18+
import kotlin.jvm.internal.DefaultConstructorMarker;
19+
20+
/**
21+
* Sample class with only a synthetic Kotlin-like constructor (no primary constructor).
22+
* Used to verify the fallback behavior in {@code getAllArgsConstructor} when all constructors
23+
* are synthetic and the candidates list is empty after filtering.
24+
*/
25+
public class ImmutableToFooWithOnlySyntheticConstructor {
26+
private final String name;
27+
private final int id;
28+
29+
@SuppressWarnings("unused")
30+
ImmutableToFooWithOnlySyntheticConstructor(final String name, final int id, final int defaultMask, final DefaultConstructorMarker marker) {
31+
this.name = name;
32+
this.id = id;
33+
}
34+
35+
public String getName() {
36+
return name;
37+
}
38+
39+
public int getId() {
40+
return id;
41+
}
42+
}

bull-common/src/test/java/com/expediagroup/transformer/utils/ClassUtilsTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (C) 2019-2023 Expedia, Inc.
2+
* Copyright (C) 2019-2026 Expedia, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,6 +60,8 @@
6060
import com.expediagroup.beans.sample.immutable.ImmutableToFoo;
6161
import com.expediagroup.beans.sample.immutable.ImmutableToFooCustomAnnotation;
6262
import com.expediagroup.beans.sample.immutable.ImmutableToFooSubClass;
63+
import com.expediagroup.beans.sample.immutable.ImmutableToFooWithKotlinDefaultConstructor;
64+
import com.expediagroup.beans.sample.immutable.ImmutableToFooWithOnlySyntheticConstructor;
6365
import com.expediagroup.beans.sample.mixed.MixedToFoo;
6466
import com.expediagroup.beans.sample.mixed.MixedToFooMissingConstructor;
6567
import com.expediagroup.beans.sample.mixed.MixedToFooStaticField;
@@ -105,6 +107,8 @@ public class ClassUtilsTest {
105107
private static final long[] PRIMITIVE_LONG_ARRAY = {};
106108
private static final Integer[] PRIMITIVE_INTEGER_ARRAY = {};
107109
private static final FromFoo[] NOT_PRIMITIVE_ARRAY = {};
110+
private static final int KOTLIN_PRIMARY_CONSTRUCTOR_PARAMS = 2;
111+
private static final int KOTLIN_SYNTHETIC_CONSTRUCTOR_PARAMS = 4;
108112
private static final int FROM_FOO_ADV_FIELD_EXPECTED_GETTER_METHODS = 11;
109113
private static final int FROM_FOO_SIMPLE_EXPECTED_GETTER_METHODS = 3;
110114
private static final int FROM_FOO_SUB_CLASS_EXPECTED_GETTER_METHODS = 9;
@@ -501,6 +505,38 @@ public void testGetAllArgsConstructorWorksAsExpected() {
501505
assertThat(actual).isNotNull();
502506
}
503507

508+
/**
509+
* Tests that the method {@code getAllArgsConstructor} skips the Kotlin synthetic constructor
510+
* with {@code DefaultConstructorMarker} and returns the real all-args constructor.
511+
*/
512+
@Test
513+
public void testGetAllArgsConstructorSkipsKotlinSyntheticConstructor() {
514+
// GIVEN
515+
516+
// WHEN
517+
Constructor<?> actual = underTest.getAllArgsConstructor(ImmutableToFooWithKotlinDefaultConstructor.class);
518+
519+
// THEN
520+
assertThat(actual).isNotNull();
521+
assertThat(actual.getParameterCount()).isEqualTo(KOTLIN_PRIMARY_CONSTRUCTOR_PARAMS);
522+
}
523+
524+
/**
525+
* Tests that the method {@code getAllArgsConstructor} falls back to all constructors
526+
* when every constructor is synthetic, returning the one with the most parameters.
527+
*/
528+
@Test
529+
public void testGetAllArgsConstructorFallsBackWhenAllConstructorsAreSynthetic() {
530+
// GIVEN
531+
532+
// WHEN
533+
Constructor<?> actual = underTest.getAllArgsConstructor(ImmutableToFooWithOnlySyntheticConstructor.class);
534+
535+
// THEN
536+
assertThat(actual).isNotNull();
537+
assertThat(actual.getParameterCount()).isEqualTo(KOTLIN_SYNTHETIC_CONSTRUCTOR_PARAMS);
538+
}
539+
504540
/**
505541
* Tests that the method {@code getNoArgsConstructor} works as expected.
506542
*/

0 commit comments

Comments
 (0)