This is an automated email from the ASF dual-hosted git repository.
zhangliang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new e1c924aa784 Add more test cases on
ConvertYamlConfigurationExecutorTest (#38018)
e1c924aa784 is described below
commit e1c924aa78435b75dacbd0e13c9e2c8d4e48ad6f
Author: Liang Zhang <[email protected]>
AuthorDate: Thu Feb 12 16:17:52 2026 +0800
Add more test cases on ConvertYamlConfigurationExecutorTest (#38018)
* Add more test cases on ConvertYamlConfigurationExecutorTest
* Add more test cases on ConvertYamlConfigurationExecutorTest
---
.codex/skills/gen-ut/SKILL.md | 119 ++++++++++++-
.../yaml/ConvertYamlConfigurationExecutorTest.java | 192 +++++++++++++++++----
.../conf/convert/database-empty-rules.yaml | 31 ++++
.../convert/database-with-custom-pool-props.yaml | 33 ++++
.../convert/database-without-database-name.yaml | 23 +++
.../conf/convert/database-without-password.yaml | 30 ++++
6 files changed, 388 insertions(+), 40 deletions(-)
diff --git a/.codex/skills/gen-ut/SKILL.md b/.codex/skills/gen-ut/SKILL.md
index 1ce065eff1c..8560f375dc4 100644
--- a/.codex/skills/gen-ut/SKILL.md
+++ b/.codex/skills/gen-ut/SKILL.md
@@ -46,7 +46,7 @@ Module resolution order:
## Mandatory Constraints
- Norm levels: `MUST` (required), `SHOULD` (preferred), `MAY` (optional).
-- Definition source principle: mandatory constraints are defined only in this
`R1-R14` section; other sections only provide term/workflow/command
descriptions and must not add, override, or relax `R1-R14`.
+- Definition source principle: mandatory constraints are defined only in this
`R1-R15` section; other sections only provide term/workflow/command
descriptions and must not add, override, or relax `R1-R15`.
- `R1`: `MUST` comply with `AGENTS.md` and `CODE_OF_CONDUCT.md`; rule
interpretation should prioritize corresponding clauses and line-number evidence
in `CODE_OF_CONDUCT.md`.
@@ -79,7 +79,9 @@ Module resolution order:
- `R6`: SPI, Mock, and reflection
- If the class under test can be obtained via SPI, `MUST` instantiate by
default with `TypedSPILoader`/`OrderedSPILoader` (or database-specific
loaders), and `MUST` keep the resolved instance as a test-class-level field
(global variable) by default.
- - SPI metadata accessor methods `TypedSPI#getType`, `OrderedSPI#getOrder`,
and `getTypeClass` `MUST` be treated as no-test-required targets by default,
unless the user explicitly requests tests for them.
+ - SPI metadata accessor methods `TypedSPI#getType`, `OrderedSPI#getOrder`,
and `getTypeClass` are default no-test-required targets.
+ - For these accessors, tests `MUST NOT` be added by default; they are
allowed only when the user explicitly requests tests for them in the current
turn.
+ - If such tests are added without explicit request, they `MUST` be removed
before completion.
- If not instantiated via SPI, `MUST` record the reason before
implementation.
- Test dependencies `SHOULD` use Mockito mocks by default.
- Reflection access `MUST` use `Plugins.getMemberAccessor()`, and field
access only.
@@ -91,6 +93,7 @@ Module resolution order:
- Deletion/merge of coverage-equivalent tests is determined by `R13`.
- `R8`: parameterized optimization (enabled by default)
+ - `MUST` run pre-implementation candidate analysis and output an
`R8-CANDIDATES` record (target public method, candidate count, decision, and
evidence).
- `MUST` report the mergeable method set and merge candidate count.
- Candidates meeting all conditions below are considered "high-fit for
parameterization":
- A. target public method and branch skeleton are consistent;
@@ -102,6 +105,7 @@ Module resolution order:
- For high-fit candidates, a "do not recommend refactor" conclusion is
allowed only when refactoring causes significant readability/diagnosability
regression, and the exception `MUST` include a `Necessity reason tag` with
concrete evidence.
- Parameter construction `SHOULD` prefer `Arguments + @MethodSource`; `MAY`
use clearer options such as `@CsvSource`/`@EnumSource`.
- `MUST` provide either a "recommend refactor" or "do not recommend
refactor" conclusion with reasons for each candidate; when no candidates exist,
`MUST` output "no candidates + decision reason".
+ - If high-fit candidates exist but neither parameterized refactor nor valid
`KEEP` evidence is present, status `MUST NOT` be concluded as `R10-A`.
- `R9`: dead code and coverage blockers
- When dead code blocks progress, `MUST` report class name, file path, exact
line number, and unreachable reason.
@@ -114,8 +118,8 @@ Module resolution order:
- target test command succeeds, and surefire report has `Tests run > 0`
(recommended to also satisfy `Tests run - Skipped > 0`);
- coverage evidence satisfies the target (default class/line/branch 100%,
unless explicitly lowered by the user);
- each class in `<ResolvedTargetClasses>` has explicit aggregated
class-level coverage evidence (CLASS/LINE/BRANCH counters with
covered/missed/ratio) over the `Target-class coverage scope`, and all ratios
satisfy the declared target;
- - Checkstyle, Spotless, and two `R14` scans all pass;
- - `R8` analysis and compliance evidence are complete.
+ - Checkstyle, Spotless, two `R14` scans, and all required `R15` scans all
pass;
+ - `R8` analysis and compliance evidence are complete, including
`R8-CANDIDATES` and candidate-level decisions (refactor or valid `KEEP`
evidence).
- `R10-B` (blocked): under the "production code cannot be changed"
constraint, dead code blocks coverage targets, and evidence satisfies `R9`.
- `R10-C` (blocked): failure occurs outside `R3` scope, and evidence
satisfies `R11`.
- `R10-D` (in-progress): none of `R10-INPUT_BLOCKED/R10-B/R10-C/R10-A` is
satisfied yet.
@@ -150,17 +154,23 @@ Module resolution order:
- `assertEquals(..., true|false|Boolean.TRUE|Boolean.FALSE)`
- `MUST` run one hard-gate scan after implementation and another before
delivery; any hit means incomplete.
+- `R15`: pre-delivery hard gates
+ - `R15-A` (parameterization enforcement): if an `R8` high-fit candidate
exists, the corresponding tests `MUST` be parameterized with
`@ParameterizedTest(name = "{0}")`, unless a valid `KEEP` exception is recorded.
+ - `R15-B` (metadata accessor test ban): unless the user explicitly requests
it in the current turn, tests targeting `getType` / `getOrder` / `getTypeClass`
`MUST NOT` be added.
+ - `R15-C` (scope mutation guard): test-generation tasks `MUST NOT` introduce
new diffs under any `src/main/` path.
+
## Workflow
1. Read `AGENTS.md` and `CODE_OF_CONDUCT.md`, and record hard constraints for
this round (`R1`).
+ - Capture scope baseline once: `git status --porcelain >
/tmp/gen-ut-status-before.txt`.
2. Parse target classes, related test classes, and input-blocked state
(`R10-INPUT_BLOCKED`).
3. Resolve `<ResolvedTestClass>`, `<ResolvedTestFileSet>`,
`<ResolvedTestModules>`, and record `pom.xml` evidence (`R3`).
4. Decide whether `R12` is triggered; if not, output `R4` branch mapping.
-5. Execute `R8` parameterized optimization analysis and apply required
refactoring.
+5. Execute `R8` parameterized optimization analysis, output `R8-CANDIDATES`,
and apply required refactoring.
6. Execute `R9` dead-code checks and record evidence.
7. Complete test implementation or extension according to `R2-R7`.
8. Perform necessity trimming and coverage re-verification according to `R13`.
-9. Run verification commands and handle failures by `R11`; execute two `R14`
scans.
+9. Run verification commands and handle failures by `R11`; execute two `R14`
scans and all required `R15` scans.
10. Decide status by `R10` after verification; if status is `R10-D`, return to
Step 5 and continue.
11. Before final response, run a second `R10` status decision and output
`R10=<state>` with rule-to-evidence mapping.
@@ -293,6 +303,62 @@ PY
'
```
+5.1 `R15-A` high-fit candidate enforcement scan (shape-based):
+```bash
+bash -lc '
+python3 - <ResolvedTestFileSet> <<'"'"'PY'"'"'
+import re
+import sys
+from pathlib import Path
+from collections import defaultdict
+
+IGNORE = {"assertThat", "assertTrue", "assertFalse", "mock", "when", "verify",
"is", "not"}
+
+def extract_block(text, brace_index):
+ depth = 0
+ i = brace_index
+ while i < len(text):
+ if "{" == text[i]:
+ depth += 1
+ elif "}" == text[i]:
+ depth -= 1
+ if 0 == depth:
+ return text[brace_index + 1:i]
+ i += 1
+ return ""
+
+decl =
re.compile(r"(?:@Test|@ParameterizedTest(?:\\([^)]*\\))?)\\s+void\\s+(assert\\w+)\\s*\\([^)]*\\)\\s*\\{",
re.S)
+call = re.compile(r"\\b\\w+\\.(\\w+)\\s*\\(")
+param_targets = defaultdict(set)
+plain_target_count = defaultdict(lambda: defaultdict(int))
+for path in (each for each in sys.argv[1:] if each.endswith(".java")):
+ source = Path(path).read_text(encoding="utf-8")
+ for match in decl.finditer(source):
+ brace_index = source.find("{", match.start())
+ body = extract_block(source, brace_index)
+ methods = [each for each in call.findall(body) if each not in IGNORE]
+ if not methods:
+ continue
+ target = methods[0]
+ header = source[max(0, match.start() - 160):match.start()]
+ if "@ParameterizedTest" in header:
+ param_targets[path].add(target)
+ else:
+ plain_target_count[path][target] += 1
+violations = []
+for path, each_counter in plain_target_count.items():
+ for method_name, count in each_counter.items():
+ if count >= 3 and method_name not in param_targets[path]:
+ violations.append(f"{path}: method={method_name}
nonParameterizedCount={count}")
+if violations:
+ print("[R15-A] high-fit candidate likely exists but no parameterized test
found:")
+ for each in violations:
+ print(each)
+ sys.exit(1)
+PY
+'
+```
+
6. `R14` hard-gate scan:
```bash
bash -lc '
@@ -303,15 +369,54 @@ if rg -n -U --pcre2 "$BOOLEAN_ASSERTION_BAN_REGEX"
<ResolvedTestFileSet>; then
exit 1
fi'
```
-7. Scope validation:
+
+7. `R15-B` metadata accessor test ban scan (skip only when explicitly
requested by user):
+```bash
+bash -lc '
+if rg -n -U
"@Test(?s:.*?)void\\s+assert\\w*(GetType|GetOrder|GetTypeClass)\\b|assertThat\\((?s:.*?)\\.getType\\(\\)|assertThat\\((?s:.*?)\\.getOrder\\(\\)|assertThat\\((?s:.*?)\\.getTypeClass\\(\\)"
<ResolvedTestFileSet>; then
+ echo "[R15-B] metadata accessor test detected without explicit user request"
+ exit 1
+fi'
+```
+
+8. Scope validation:
```bash
git diff --name-only
```
+9. `R15-C` production-path mutation guard (baseline-based):
+```bash
+bash -lc '
+# capture once at task start:
+# git status --porcelain > /tmp/gen-ut-status-before.txt
+git status --porcelain > /tmp/gen-ut-status-after.txt
+python3 - <<'"'"'PY'"'"'
+from pathlib import Path
+
+before_path = Path("/tmp/gen-ut-status-before.txt")
+after_path = Path("/tmp/gen-ut-status-after.txt")
+before = set(before_path.read_text(encoding="utf-8").splitlines()) if
before_path.exists() else set()
+after = set(after_path.read_text(encoding="utf-8").splitlines())
+introduced = sorted(after - before)
+violations = []
+for each in introduced:
+ path = each[3:].strip()
+ if "/src/main/" in path or path.startswith("src/main/"):
+ violations.append(path)
+if violations:
+ print("[R15-C] out-of-scope production path modified:")
+ for each in violations:
+ print(each)
+ raise SystemExit(1)
+PY
+'
+```
+
## Final Output Requirements
- `MUST` include a status line `R10=<state>`.
- `MUST` include aggregated class-level coverage evidence for each class in
`<ResolvedTargetClasses>` over the `Target-class coverage scope`
(CLASS/LINE/BRANCH counters and ratios).
+- `MUST` include `R8-CANDIDATES` output (candidate set, counts, and
per-candidate decision evidence).
- `MUST` include executed commands and exit codes.
- If `R10` is not `R10-A`, `MUST` explicitly mark the task as not completed
and provide blocking reason plus next action.
- `MUST NOT` use completion wording when `R10` is `R10-D`.
diff --git
a/proxy/backend/core/src/test/java/org/apache/shardingsphere/proxy/backend/handler/distsql/ral/queryable/yaml/ConvertYamlConfigurationExecutorTest.java
b/proxy/backend/core/src/test/java/org/apache/shardingsphere/proxy/backend/handler/distsql/ral/queryable/yaml/ConvertYamlConfigurationExecutorTest.java
index 2aa151e1b6d..02b55606a49 100644
---
a/proxy/backend/core/src/test/java/org/apache/shardingsphere/proxy/backend/handler/distsql/ral/queryable/yaml/ConvertYamlConfigurationExecutorTest.java
+++
b/proxy/backend/core/src/test/java/org/apache/shardingsphere/proxy/backend/handler/distsql/ral/queryable/yaml/ConvertYamlConfigurationExecutorTest.java
@@ -20,78 +20,95 @@ package
org.apache.shardingsphere.proxy.backend.handler.distsql.ral.queryable.ya
import com.google.common.base.Splitter;
import lombok.SneakyThrows;
import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
+import
org.apache.shardingsphere.distsql.handler.engine.query.DistSQLQueryExecutor;
import
org.apache.shardingsphere.distsql.statement.type.ral.queryable.convert.ConvertYamlConfigurationStatement;
+import org.apache.shardingsphere.infra.exception.generic.FileIOException;
import
org.apache.shardingsphere.infra.merge.result.impl.local.LocalDataQueryResultRow;
import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
-import org.apache.shardingsphere.mode.manager.ContextManager;
+import org.apache.shardingsphere.infra.util.yaml.YamlEngine;
import org.apache.shardingsphere.parser.rule.SQLParserRule;
import
org.apache.shardingsphere.parser.rule.builder.DefaultSQLParserRuleConfigurationBuilder;
+import
org.apache.shardingsphere.proxy.backend.config.yaml.YamlProxyDataSourceConfiguration;
+import
org.apache.shardingsphere.proxy.backend.config.yaml.YamlProxyDatabaseConfiguration;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.MockedStatic;
+import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
+import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
class ConvertYamlConfigurationExecutorTest {
- private final SQLParserRule sqlParserRule = new SQLParserRule(new
DefaultSQLParserRuleConfigurationBuilder().build());
+ private static final String MISSING_FILE_PATH =
"src/test/resources/conf/convert/not-exist.yaml";
- @Test
- void assertExecuteWithEmptyDatabase() {
- assertExecute("/conf/convert/database-empty.yaml",
"/expected/convert-empty-database.yaml");
- }
-
- @Test
- void assertExecuteWithSharding() {
- assertExecute("/conf/convert/database-sharding.yaml",
"/expected/convert-sharding.yaml");
- }
+ private final SQLParserRule sqlParserRule = new SQLParserRule(new
DefaultSQLParserRuleConfigurationBuilder().build());
- @Test
- void assertExecuteWithShardingAutoTables() {
- assertExecute("/conf/convert/database-sharding-auto-tables.yaml",
"/expected/convert-sharding-auto-tables.yaml");
- }
+ private final ConvertYamlConfigurationExecutor executor =
(ConvertYamlConfigurationExecutor)
TypedSPILoader.getService(DistSQLQueryExecutor.class,
ConvertYamlConfigurationStatement.class);
@Test
- void assertExecuteWithReadWriteSplitting() {
- assertExecute("/conf/convert/database-readwrite-splitting.yaml",
"/expected/convert-readwrite-splitting.yaml");
+ void assertGetColumnNames() {
+ assertThat(executor.getColumnNames(new
ConvertYamlConfigurationStatement("foo")),
is(Collections.singleton("dist_sql")));
}
- @Test
- void assertExecuteWithEncrypt() {
- assertExecute("/conf/convert/database-encrypt.yaml",
"/expected/convert-encrypt.yaml");
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("resourceExpectedCases")
+ void assertExecuteWithExpectedResource(final String caseName, final String
configFilePath, final String expectedFilePath) {
+ Collection<LocalDataQueryResultRow> actual = executor.getRows(
+ new
ConvertYamlConfigurationStatement(Objects.requireNonNull(ConvertYamlConfigurationExecutorTest.class.getResource(configFilePath)).getPath()),
mock());
+ assertRowData(actual, loadExpectedRow(expectedFilePath));
}
- @Test
- void assertExecuteWithShadow() {
- assertExecute("/conf/convert/database-shadow.yaml",
"/expected/convert-shadow.yaml");
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("inlineExpectedCases")
+ void assertExecuteWithInlineExpected(final String caseName, final String
configFilePath, final String expectedRow) {
+ Collection<LocalDataQueryResultRow> actual = executor.getRows(
+ new
ConvertYamlConfigurationStatement(Objects.requireNonNull(ConvertYamlConfigurationExecutorTest.class.getResource(configFilePath)).getPath()),
mock());
+ assertRowData(actual, expectedRow);
}
- @Test
- void assertExecuteWithMix() {
- assertExecute("/conf/convert/database-mix.yaml",
"/expected/convert-mix.yaml");
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("mockedYamlCases")
+ void assertExecuteWithMockedYamlConfiguration(final String caseName, final
YamlProxyDatabaseConfiguration yamlConfig, final String expectedRow) {
+ try (MockedStatic<YamlEngine> mockedYamlEngine =
mockStatic(YamlEngine.class)) {
+ mockedYamlEngine.when(() -> YamlEngine.unmarshal(any(File.class),
eq(YamlProxyDatabaseConfiguration.class))).thenReturn(yamlConfig);
+ Collection<LocalDataQueryResultRow> actual = executor.getRows(new
ConvertYamlConfigurationStatement("mocked-path.yaml"), mock());
+ assertRowData(actual, expectedRow);
+ }
}
- private void assertExecute(final String configFilePath, final String
expectedFilePath) {
- ConvertYamlConfigurationExecutor executor = new
ConvertYamlConfigurationExecutor();
- Collection<LocalDataQueryResultRow> actual = executor.getRows(
- new
ConvertYamlConfigurationStatement(Objects.requireNonNull(ConvertYamlConfigurationExecutorTest.class.getResource(configFilePath)).getPath()),
mock(ContextManager.class));
- assertRowData(actual, expectedFilePath);
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("invalidInputCases")
+ void assertExecuteWithInvalidInput(final String caseName, final String
filePath, final boolean classPathResource,
+ final Class<? extends Throwable>
expectedExceptionType, final String expectedMessage) {
+ Throwable actual = assertThrows(expectedExceptionType,
+ () -> executor.getRows(new
ConvertYamlConfigurationStatement(resolveFilePath(filePath,
classPathResource)), mock()));
+ assertThat(actual.getMessage(), is(expectedMessage));
}
- private void assertRowData(final Collection<LocalDataQueryResultRow> data,
final String expectedFilePath) {
+ private void assertRowData(final Collection<LocalDataQueryResultRow> data,
final String expectedRow) {
assertThat(data.size(), is(1));
LocalDataQueryResultRow actual = data.iterator().next();
- assertThat(actual.getCell(1), is(loadExpectedRow(expectedFilePath)));
+ assertThat(actual.getCell(1), is(expectedRow));
assertParseSQL((String) actual.getCell(1));
}
@@ -106,4 +123,113 @@ class ConvertYamlConfigurationExecutorTest {
return Files.readAllLines(Paths.get(url.toURI())).stream().filter(each
-> !each.startsWith("#")).collect(Collectors.joining(System.lineSeparator()))
+ System.lineSeparator() + System.lineSeparator();
}
+
+ private String resolveFilePath(final String filePath, final boolean
classPathResource) {
+ return classPathResource ?
Objects.requireNonNull(ConvertYamlConfigurationExecutorTest.class.getResource(filePath)).getPath()
: filePath;
+ }
+
+ private static Stream<Arguments> resourceExpectedCases() {
+ return Stream.of(
+ Arguments.of("empty-database",
"/conf/convert/database-empty.yaml", "/expected/convert-empty-database.yaml"),
+ Arguments.of("sharding",
"/conf/convert/database-sharding.yaml", "/expected/convert-sharding.yaml"),
+ Arguments.of("sharding-auto-tables",
"/conf/convert/database-sharding-auto-tables.yaml",
"/expected/convert-sharding-auto-tables.yaml"),
+ Arguments.of("readwrite-splitting",
"/conf/convert/database-readwrite-splitting.yaml",
"/expected/convert-readwrite-splitting.yaml"),
+ Arguments.of("encrypt", "/conf/convert/database-encrypt.yaml",
"/expected/convert-encrypt.yaml"),
+ Arguments.of("shadow", "/conf/convert/database-shadow.yaml",
"/expected/convert-shadow.yaml"),
+ Arguments.of("mix", "/conf/convert/database-mix.yaml",
"/expected/convert-mix.yaml"));
+ }
+
+ private static Stream<Arguments> inlineExpectedCases() {
+ return Stream.of(
+ Arguments.of("without-password",
"/conf/convert/database-without-password.yaml", toExpectedRow(
+ "CREATE DATABASE no_password_db;",
+ "USE no_password_db;",
+ "",
+ "REGISTER STORAGE UNIT ds_0 (",
+
"URL='jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false',",
+ "USER='root',",
+
"PROPERTIES('minPoolSize'='1','connectionTimeoutMilliseconds'='30000','maxLifetimeMilliseconds'='1800000',"
+ +
"'idleTimeoutMilliseconds'='60000','maxPoolSize'='50'));")),
+ Arguments.of("empty-rules",
"/conf/convert/database-empty-rules.yaml", toExpectedRow(
+ "CREATE DATABASE empty_rules_db;",
+ "USE empty_rules_db;",
+ "",
+ "REGISTER STORAGE UNIT ds_0 (",
+
"URL='jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false',",
+ "USER='root',",
+ "PASSWORD='12345678',",
+
"PROPERTIES('minPoolSize'='1','connectionTimeoutMilliseconds'='30000','maxLifetimeMilliseconds'='1800000',"
+ +
"'idleTimeoutMilliseconds'='60000','maxPoolSize'='50')",
+ ");")),
+ Arguments.of("custom-pool-props",
"/conf/convert/database-with-custom-pool-props.yaml", toExpectedRow(
+ "CREATE DATABASE custom_pool_props_db;",
+ "USE custom_pool_props_db;",
+ "",
+ "REGISTER STORAGE UNIT ds_0 (",
+
"URL='jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false',",
+ "USER='root',",
+ "PASSWORD='12345678',",
+
"PROPERTIES('minPoolSize'='1','connectionTimeoutMilliseconds'='30000','maxLifetimeMilliseconds'='1800000',"
+ +
"'idleTimeoutMilliseconds'='60000','maxPoolSize'='50','foo1'='bar_1')",
+ ");")));
+ }
+
+ private static Stream<Arguments> mockedYamlCases() {
+ return Stream.of(
+ Arguments.of("null-data-sources",
createYamlConfigWithNullDataSources(), toExpectedRow(
+ "CREATE DATABASE null_data_sources_db;",
+ "USE null_data_sources_db;")),
+ Arguments.of("null-rules", createYamlConfigWithNullRules(),
toExpectedRow(
+ "CREATE DATABASE null_rules_db;",
+ "USE null_rules_db;",
+ "",
+ "REGISTER STORAGE UNIT ds_0 (",
+
"URL='jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false',",
+ "USER='root',",
+ "PASSWORD='12345678',",
+
"PROPERTIES('minPoolSize'='1','connectionTimeoutMilliseconds'='30000','maxLifetimeMilliseconds'='1800000',"
+ +
"'idleTimeoutMilliseconds'='60000','maxPoolSize'='50')",
+ ");")));
+ }
+
+ private static YamlProxyDatabaseConfiguration
createYamlConfigWithNullDataSources() {
+ YamlProxyDatabaseConfiguration result = new
YamlProxyDatabaseConfiguration();
+ result.setDatabaseName("null_data_sources_db");
+ result.setDataSources(null);
+ result.setRules(Collections.emptyList());
+ return result;
+ }
+
+ private static YamlProxyDatabaseConfiguration
createYamlConfigWithNullRules() {
+ YamlProxyDatabaseConfiguration result = new
YamlProxyDatabaseConfiguration();
+ result.setDatabaseName("null_rules_db");
+ result.setDataSources(Collections.singletonMap("ds_0",
createDataSourceConfig()));
+ result.setRules(null);
+ return result;
+ }
+
+ private static YamlProxyDataSourceConfiguration createDataSourceConfig() {
+ YamlProxyDataSourceConfiguration result = new
YamlProxyDataSourceConfiguration();
+
result.setUrl("jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false");
+ result.setUsername("root");
+ result.setPassword("12345678");
+ result.setConnectionTimeoutMilliseconds(30000L);
+ result.setIdleTimeoutMilliseconds(60000L);
+ result.setMaxLifetimeMilliseconds(1800000L);
+ result.setMaxPoolSize(50);
+ result.setMinPoolSize(1);
+ return result;
+ }
+
+ private static Stream<Arguments> invalidInputCases() {
+ return Stream.of(
+ Arguments.of("missing-database-name",
"/conf/convert/database-without-database-name.yaml", true,
+ NullPointerException.class, "`databaseName` in file
`database-without-database-name.yaml` is required."),
+ Arguments.of("missing-file", MISSING_FILE_PATH, false,
FileIOException.class,
+ String.format("File access failed, file is: %s", new
File(MISSING_FILE_PATH).getAbsolutePath())));
+ }
+
+ private static String toExpectedRow(final String... lines) {
+ return String.join(System.lineSeparator(), lines) +
System.lineSeparator() + System.lineSeparator();
+ }
}
diff --git
a/proxy/backend/core/src/test/resources/conf/convert/database-empty-rules.yaml
b/proxy/backend/core/src/test/resources/conf/convert/database-empty-rules.yaml
new file mode 100644
index 00000000000..b52d3633329
--- /dev/null
+++
b/proxy/backend/core/src/test/resources/conf/convert/database-empty-rules.yaml
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+databaseName: empty_rules_db
+
+dataSources:
+ ds_0:
+ url: jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false
+ username: root
+ password: 12345678
+ connectionTimeoutMilliseconds: 30000
+ idleTimeoutMilliseconds: 60000
+ maxLifetimeMilliseconds: 1800000
+ maxPoolSize: 50
+ minPoolSize: 1
+
+rules: []
diff --git
a/proxy/backend/core/src/test/resources/conf/convert/database-with-custom-pool-props.yaml
b/proxy/backend/core/src/test/resources/conf/convert/database-with-custom-pool-props.yaml
new file mode 100644
index 00000000000..4e017b6bc6e
--- /dev/null
+++
b/proxy/backend/core/src/test/resources/conf/convert/database-with-custom-pool-props.yaml
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+databaseName: custom_pool_props_db
+
+dataSources:
+ ds_0:
+ url: jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false
+ username: root
+ password: 12345678
+ connectionTimeoutMilliseconds: 30000
+ idleTimeoutMilliseconds: 60000
+ maxLifetimeMilliseconds: 1800000
+ maxPoolSize: 50
+ minPoolSize: 1
+ customPoolProps:
+ foo_1: bar_1
+
+rules:
diff --git
a/proxy/backend/core/src/test/resources/conf/convert/database-without-database-name.yaml
b/proxy/backend/core/src/test/resources/conf/convert/database-without-database-name.yaml
new file mode 100644
index 00000000000..24af1bc8f78
--- /dev/null
+++
b/proxy/backend/core/src/test/resources/conf/convert/database-without-database-name.yaml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+dataSources:
+ ds_0:
+ url: jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false
+ username: root
+ password: 12345678
+rules:
diff --git
a/proxy/backend/core/src/test/resources/conf/convert/database-without-password.yaml
b/proxy/backend/core/src/test/resources/conf/convert/database-without-password.yaml
new file mode 100644
index 00000000000..44708ee4309
--- /dev/null
+++
b/proxy/backend/core/src/test/resources/conf/convert/database-without-password.yaml
@@ -0,0 +1,30 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+databaseName: no_password_db
+
+dataSources:
+ ds_0:
+ url: jdbc:mysql://127.0.0.1:3306/demo_convert_ds_0?useSSL=false
+ username: root
+ connectionTimeoutMilliseconds: 30000
+ idleTimeoutMilliseconds: 60000
+ maxLifetimeMilliseconds: 1800000
+ maxPoolSize: 50
+ minPoolSize: 1
+
+rules: