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:

Reply via email to