Skip to content

Commit a9dfd5b

Browse files
committed
test: add dual DB engine (LevelDB + RocksDB) test coverage
- Extract shared test logic into DbDataSourceImplTest, BaseMethodTest, TestConstants - Enable DB engine override via system property so tests run against both engines - Refactor 20+ test classes to support dual-engine testing - Fix flaky tests: FreezeTest, FreezeV2Test, VoteTest, ManagerTest, ShieldedReceiveTest - Update build.gradle and CI workflow accordingly
1 parent 7e5bbbd commit a9dfd5b

36 files changed

+1098
-1441
lines changed

.github/workflows/pr-check.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ jobs:
178178
- name: Build
179179
run: ./gradlew clean build --no-daemon
180180

181+
- name: Test with RocksDB engine
182+
if: matrix.arch == 'x86_64'
183+
run: ./gradlew :framework:testWithRocksDb --no-daemon
184+
181185
docker-build-rockylinux:
182186
name: Build rockylinux (JDK 8 / x86_64)
183187
if: github.event.action != 'edited' && !failure()

common/src/main/java/org/tron/core/config/args/Storage.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.apache.commons.lang3.StringUtils;
3030
import org.iq80.leveldb.CompressionType;
3131
import org.iq80.leveldb.Options;
32-
import org.tron.common.arch.Arch;
3332
import org.tron.common.cache.CacheStrategies;
3433
import org.tron.common.cache.CacheType;
3534
import org.tron.common.utils.DbOptionalsUtils;
@@ -90,7 +89,6 @@ public class Storage {
9089
* Default values of directory
9190
*/
9291
private static final String DEFAULT_DB_ENGINE = "LEVELDB";
93-
private static final String ROCKS_DB_ENGINE = "ROCKSDB";
9492
private static final boolean DEFAULT_DB_SYNC = false;
9593
private static final boolean DEFAULT_EVENT_SUBSCRIBE_CONTRACT_PARSE = true;
9694
private static final String DEFAULT_DB_DIRECTORY = "database";
@@ -175,10 +173,6 @@ public class Storage {
175173
private final Map<String, Sha256Hash> dbRoots = Maps.newConcurrentMap();
176174

177175
public static String getDbEngineFromConfig(final Config config) {
178-
if (Arch.isArm64()) {
179-
logger.warn("Arm64 architecture detected, using RocksDB as db engine, ignore config.");
180-
return ROCKS_DB_ENGINE;
181-
}
182176
return config.hasPath(DB_ENGINE_CONFIG_KEY)
183177
? config.getString(DB_ENGINE_CONFIG_KEY) : DEFAULT_DB_ENGINE;
184178
}

framework/build.gradle

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,38 @@ run {
106106
}
107107
}
108108

109-
test {
110-
retry {
109+
def configureTestTask = { Task t ->
110+
t.retry {
111111
maxRetries = 5
112112
maxFailures = 20
113113
}
114-
testLogging {
114+
t.testLogging {
115115
exceptionFormat = 'full'
116116
}
117+
if (isWindows()) {
118+
t.exclude '**/ShieldedTransferActuatorTest.class'
119+
t.exclude '**/BackupDbUtilTest.class'
120+
t.exclude '**/ManagerTest.class'
121+
t.exclude 'org/tron/core/zksnark/**'
122+
t.exclude 'org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.class'
123+
t.exclude 'org/tron/core/ShieldedTRC20BuilderTest.class'
124+
t.exclude 'org/tron/common/runtime/vm/WithdrawRewardTest.class'
125+
}
126+
t.maxHeapSize = "1024m"
127+
t.doFirst {
128+
t.forkEvery = 100
129+
t.jvmArgs "-XX:MetaspaceSize=128m", "-XX:MaxMetaspaceSize=256m", "-XX:+UseG1GC"
130+
}
131+
}
132+
133+
test {
134+
configureTestTask(it)
117135
jacoco {
118136
destinationFile = file("$buildDir/jacoco/jacocoTest.exec")
119137
classDumpDir = file("$buildDir/jacoco/classpathdumps")
120138
}
121139
if (rootProject.archInfo.isArm64) {
140+
systemProperty 'storage.db.engine', 'ROCKSDB'
122141
exclude { element ->
123142
element.file.name.toLowerCase().contains('leveldb')
124143
}
@@ -129,21 +148,14 @@ test {
129148
excludeTestsMatching '*.*LevelDb*'
130149
}
131150
}
132-
if (isWindows()) {
133-
exclude '**/ShieldedTransferActuatorTest.class'
134-
exclude '**/BackupDbUtilTest.class'
135-
exclude '**/ManagerTest.class'
136-
exclude 'org/tron/core/zksnark/**'
137-
exclude 'org/tron/common/runtime/vm/PrecompiledContractsVerifyProofTest.class'
138-
exclude 'org/tron/core/ShieldedTRC20BuilderTest.class'
139-
exclude 'org/tron/common/runtime/vm/WithdrawRewardTest.class'
140-
}
141-
maxHeapSize = "1024m"
142-
doFirst {
143-
// Restart the JVM after every 100 tests to avoid memory leaks and ensure test isolation
144-
forkEvery = 100
145-
jvmArgs "-XX:MetaspaceSize=128m","-XX:MaxMetaspaceSize=256m", "-XX:+UseG1GC"
146-
}
151+
}
152+
153+
task testWithRocksDb(type: Test) {
154+
description = 'Run tests with RocksDB engine'
155+
group = 'verification'
156+
configureTestTask(it)
157+
systemProperty 'storage.db.engine', 'ROCKSDB'
158+
exclude '**/LevelDbDataSourceImplTest.class'
147159
}
148160

149161
jacocoTestReport {

framework/src/main/java/org/tron/common/logsfilter/nativequeue/NativeMessageQueue.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ public boolean start(int bindPort, int sendQueueLength) {
5151
public void stop() {
5252
if (Objects.nonNull(publisher)) {
5353
publisher.close();
54+
publisher = null;
5455
}
5556

5657
if (Objects.nonNull(context)) {
5758
context.close();
59+
context = null;
60+
}
61+
synchronized (NativeMessageQueue.class) {
62+
instance = null;
5863
}
5964
}
6065

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ public static void setParam(final String[] args, final String confFileName) {
134134
initLocalWitnesses(config, cmd);
135135
}
136136

137+
/**
138+
* Validate final configuration after all sources (defaults, config, CLI) are applied.
139+
*/
140+
public static void validateConfig() {
141+
if (Arch.isArm64() && !"ROCKSDB".equals(PARAMETER.storage.getDbEngine())) {
142+
throw new TronError(
143+
"ARM64 architecture only supports RocksDB. Current engine: "
144+
+ PARAMETER.storage.getDbEngine(),
145+
TronError.ErrCode.PARAMETER_INIT);
146+
}
147+
}
148+
137149
/**
138150
* Apply parameters from config file.
139151
*/

framework/src/main/java/org/tron/core/db/backup/NeedBeanCondition.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ public class NeedBeanCondition implements Condition {
99

1010
@Override
1111
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
12-
return ("ROCKSDB".equals(Args.getInstance().getStorage().getDbEngine().toUpperCase()))
12+
if (Args.getInstance() == null || Args.getInstance().getStorage() == null
13+
|| Args.getInstance().getStorage().getDbEngine() == null
14+
|| Args.getInstance().getDbBackupConfig() == null) {
15+
return false;
16+
}
17+
return "ROCKSDB".equalsIgnoreCase(Args.getInstance().getStorage().getDbEngine())
1318
&& Args.getInstance().getDbBackupConfig().isEnable() && !Args.getInstance().isWitness();
1419
}
1520
}

framework/src/main/java/org/tron/program/FullNode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public static void main(String[] args) {
2424
ExitManager.initExceptionHandler();
2525
checkJdkVersion();
2626
Args.setParam(args, "config.conf");
27+
Args.validateConfig();
2728
CommonParameter parameter = Args.getInstance();
2829

2930
LogService.load(parameter.getLogbackPath());
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package org.tron.common;
2+
3+
import java.io.IOException;
4+
import java.util.Arrays;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.junit.After;
7+
import org.junit.Before;
8+
import org.junit.Rule;
9+
import org.junit.rules.TemporaryFolder;
10+
import org.tron.common.application.Application;
11+
import org.tron.common.application.ApplicationFactory;
12+
import org.tron.common.application.TronApplicationContext;
13+
import org.tron.core.ChainBaseManager;
14+
import org.tron.core.config.DefaultConfig;
15+
import org.tron.core.config.args.Args;
16+
import org.tron.core.db.Manager;
17+
18+
/**
19+
* Base class for tests that need a fresh Spring context per test method.
20+
*
21+
* Each @Test method gets its own TronApplicationContext, created in @Before
22+
* and destroyed in @After, ensuring full isolation between tests.
23+
*
24+
* Subclasses can customize behavior by overriding hook methods:
25+
* extraArgs() — additional CLI args (e.g. "--debug")
26+
* configFile() — config file (default: config-test.conf)
27+
* beforeContext() — runs after Args.setParam, before Spring context creation
28+
* afterInit() — runs after Spring context is ready (e.g. get extra beans)
29+
* beforeDestroy() — runs before context shutdown (e.g. close resources)
30+
*
31+
* Use this when:
32+
* - methods modify database state that would affect other methods
33+
* - methods need different CommonParameter settings (e.g. setP2pDisable)
34+
* - methods need beforeContext() to configure state before Spring starts
35+
*
36+
* If methods are read-only and don't interfere with each other, use BaseTest instead.
37+
* Tests that don't need Spring (e.g. pure unit tests) should NOT extend either base class.
38+
*/
39+
@Slf4j
40+
public abstract class BaseMethodTest {
41+
42+
@Rule
43+
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
44+
45+
protected TronApplicationContext context;
46+
protected Application appT;
47+
protected Manager dbManager;
48+
protected ChainBaseManager chainBaseManager;
49+
50+
protected String[] extraArgs() {
51+
return new String[0];
52+
}
53+
54+
protected String configFile() {
55+
return TestConstants.TEST_CONF;
56+
}
57+
58+
@Before
59+
public final void initContext() throws IOException {
60+
String[] baseArgs = new String[]{
61+
"--output-directory", temporaryFolder.newFolder().toString()};
62+
String[] allArgs = mergeArgs(baseArgs, extraArgs());
63+
Args.setParam(allArgs, configFile());
64+
beforeContext();
65+
context = new TronApplicationContext(DefaultConfig.class);
66+
appT = ApplicationFactory.create(context);
67+
dbManager = context.getBean(Manager.class);
68+
chainBaseManager = context.getBean(ChainBaseManager.class);
69+
afterInit();
70+
}
71+
72+
protected void beforeContext() {
73+
}
74+
75+
protected void afterInit() {
76+
}
77+
78+
@After
79+
public final void destroyContext() {
80+
beforeDestroy();
81+
if (appT != null) {
82+
appT.shutdown();
83+
}
84+
if (context != null) {
85+
context.close();
86+
}
87+
Args.clearParam();
88+
}
89+
90+
protected void beforeDestroy() {
91+
}
92+
93+
private static String[] mergeArgs(String[] base, String[] extra) {
94+
String[] result = Arrays.copyOf(base, base.length + extra.length);
95+
System.arraycopy(extra, 0, result, base.length, extra.length);
96+
return result;
97+
}
98+
}

framework/src/test/java/org/tron/common/BaseTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,31 @@
3030
import org.tron.core.store.AccountStore;
3131
import org.tron.protos.Protocol;
3232

33+
/**
34+
* Base class for tests that need a Spring context (Manager, ChainBaseManager, etc.).
35+
* The context is created once per test class.
36+
*
37+
* Args.setParam() is called in a static block, the context is shared by all @Test
38+
* methods in the class, and destroyed in @AfterClass. This is faster but means
39+
* test methods within the same class are not isolated from each other.
40+
*
41+
* Use this when:
42+
* - test methods are read-only or don't interfere with each other
43+
* - no need to change CommonParameter/Args between methods
44+
*
45+
* Use BaseMethodTest instead when:
46+
* - methods modify database state that would affect other methods
47+
* - methods need different CommonParameter settings (e.g. setP2pDisable)
48+
* - methods need beforeContext() to configure state before Spring starts
49+
*
50+
* Tests that don't need Spring (e.g. pure unit tests like ArgsTest,
51+
* LevelDbDataSourceImplTest) should NOT extend either base class.
52+
*
53+
* Subclasses must call Args.setParam() in a static initializer block:
54+
* static {
55+
* Args.setParam(new String[]{"--output-directory", dbPath()}, TEST_CONF);
56+
* }
57+
*/
3358
@Slf4j
3459
@RunWith(SpringJUnit4ClassRunner.class)
3560
@ContextConfiguration(classes = {DefaultConfig.class})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,39 @@
11
package org.tron.common;
22

3+
import static org.junit.Assume.assumeFalse;
4+
5+
import org.tron.common.arch.Arch;
6+
7+
/**
8+
* Centralized test environment constants and utilities.
9+
*
10+
* <h3>DB engine override for dual-engine testing</h3>
11+
* Gradle tasks ({@code test} on ARM64, {@code testWithRocksDb}) set the JVM system property
12+
* {@code -Dstorage.db.engine=ROCKSDB}. Because test config files are loaded from the classpath
13+
* via {@code ConfigFactory.load(fileName)}, Typesafe Config automatically merges system
14+
* properties with higher priority than the config file values. This means the config's
15+
* {@code storage.db.engine = "LEVELDB"} is overridden transparently, without any code changes
16+
* in individual tests.
17+
*
18+
* <p><b>IMPORTANT:</b> Config files MUST be classpath resources (in {@code src/test/resources/}),
19+
* NOT standalone files in the working directory. If a config file exists on disk,
20+
* {@code Configuration.getByFileName} falls back to {@code ConfigFactory.parseFile()},
21+
* which does NOT merge system properties, and the engine override will silently fail.
22+
*/
323
public class TestConstants {
424

525
public static final String TEST_CONF = "config-test.conf";
626
public static final String NET_CONF = "config.conf";
27+
public static final String MAINNET_CONF = "config-test-mainnet.conf";
28+
public static final String DBBACKUP_CONF = "config-test-dbbackup.conf";
29+
public static final String LOCAL_CONF = "config-localtest.conf";
30+
public static final String STORAGE_CONF = "config-test-storagetest.conf";
31+
public static final String INDEX_CONF = "config-test-index.conf";
32+
33+
/**
34+
* Skips the current test on ARM64 where LevelDB JNI is unavailable.
35+
*/
36+
public static void assumeLevelDbAvailable() {
37+
assumeFalse("LevelDB JNI unavailable on ARM64", Arch.isArm64());
38+
}
739
}

0 commit comments

Comments
 (0)