Skip to content

Commit 0f00f38

Browse files
authored
feat(metactl): add comprehensive test suite for all subcommands (#18431)
- Add `get` and `trigger-snapshot` subcommands to metactl CLI - Implement complete test coverage for all metactl subcommands: status, upsert, get, watch, trigger-snapshot, export-from-grpc, export-from-raft-dir, import, transfer-leader - Extract shared utilities into metactl_utils.py and utils.py - Add unified test runner (test_all_subcommands.py) for CI integration - Add comprehensive documentation (README_SUBCOMMAND_TESTS.md) Tests include precise verification of command outputs, error handling, and end-to-end functionality validation with proper cleanup.
1 parent d143d1e commit 0f00f38

19 files changed

+1481
-149
lines changed

.github/actions/test_metactl/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ runs:
1616
shell: bash
1717
run: |
1818
python -m pip install -r ./tests/metactl/requirements.txt
19+
python ./tests/metactl/test_all_subcommands.py
1920
python ./tests/metactl/test-metactl-restore-new-cluster.py

src/meta/binaries/metactl/main.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ use databend_common_meta_client::MetaGrpcClient;
2828
use databend_common_meta_control::admin::MetaAdminClient;
2929
use databend_common_meta_control::args::BenchArgs;
3030
use databend_common_meta_control::args::ExportArgs;
31+
use databend_common_meta_control::args::GetArgs;
3132
use databend_common_meta_control::args::GlobalArgs;
3233
use databend_common_meta_control::args::ImportArgs;
3334
use databend_common_meta_control::args::ListFeatures;
3435
use databend_common_meta_control::args::SetFeature;
3536
use databend_common_meta_control::args::StatusArgs;
3637
use databend_common_meta_control::args::TransferLeaderArgs;
38+
use databend_common_meta_control::args::TriggerSnapshotArgs;
3739
use databend_common_meta_control::args::UpsertArgs;
3840
use databend_common_meta_control::args::WatchArgs;
3941
use databend_common_meta_control::export_from_disk;
@@ -162,6 +164,13 @@ impl App {
162164
Ok(())
163165
}
164166

167+
async fn trigger_snapshot(&self, args: &TriggerSnapshotArgs) -> anyhow::Result<()> {
168+
let client = MetaAdminClient::new(args.admin_api_address.as_str());
169+
client.trigger_snapshot().await?;
170+
println!("triggered snapshot successfully.");
171+
Ok(())
172+
}
173+
165174
async fn export(&self, args: &ExportArgs) -> anyhow::Result<()> {
166175
match args.raft_dir {
167176
None => {
@@ -208,7 +217,20 @@ impl App {
208217
Ok(())
209218
}
210219

220+
async fn get(&self, args: &GetArgs) -> anyhow::Result<()> {
221+
let addresses = vec![args.grpc_api_address.clone()];
222+
let client = self.new_grpc_client(addresses)?;
223+
224+
let res = client.get_kv(&args.key).await?;
225+
println!("{}", serde_json::to_string(&res)?);
226+
Ok(())
227+
}
228+
211229
fn new_grpc_client(&self, addresses: Vec<String>) -> Result<Arc<ClientHandle>, CreationError> {
230+
eprintln!(
231+
"Using gRPC API address: {}",
232+
serde_json::to_string(&addresses).unwrap()
233+
);
212234
MetaGrpcClient::try_create(
213235
addresses,
214236
"root",
@@ -226,11 +248,13 @@ enum CtlCommand {
226248
Export(ExportArgs),
227249
Import(ImportArgs),
228250
TransferLeader(TransferLeaderArgs),
251+
TriggerSnapshot(TriggerSnapshotArgs),
229252
SetFeature(SetFeature),
230253
ListFeatures(ListFeatures),
231254
BenchClientNumConn(BenchArgs),
232255
Watch(WatchArgs),
233256
Upsert(UpsertArgs),
257+
Get(GetArgs),
234258
}
235259

236260
/// Usage:
@@ -271,6 +295,9 @@ async fn main() -> anyhow::Result<()> {
271295
CtlCommand::TransferLeader(args) => {
272296
app.transfer_leader(args).await?;
273297
}
298+
CtlCommand::TriggerSnapshot(args) => {
299+
app.trigger_snapshot(args).await?;
300+
}
274301
CtlCommand::SetFeature(args) => {
275302
let client = MetaAdminClient::new(args.admin_api_address.as_str());
276303
let res = client.set_feature(&args.feature, args.enable).await?;
@@ -296,6 +323,9 @@ async fn main() -> anyhow::Result<()> {
296323
CtlCommand::Upsert(args) => {
297324
app.upsert(args).await?;
298325
}
326+
CtlCommand::Get(args) => {
327+
app.get(args).await?;
328+
}
299329
},
300330
// for backward compatibility
301331
None => {

src/meta/control/src/admin.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,22 @@ impl MetaAdminClient {
7979
}
8080
}
8181

82+
pub async fn trigger_snapshot(&self) -> anyhow::Result<()> {
83+
let resp = self
84+
.client
85+
.get(format!("{}/v1/ctrl/trigger_snapshot", self.endpoint))
86+
.send()
87+
.await?;
88+
let status = resp.status();
89+
if status.is_success() {
90+
Ok(())
91+
} else {
92+
let data = resp.bytes().await?;
93+
let msg = String::from_utf8_lossy(&data);
94+
Err(anyhow::anyhow!("status code: {}, msg: {}", status, msg))
95+
}
96+
}
97+
8298
pub async fn set_feature(
8399
&self,
84100
feature: &str,

src/meta/control/src/args.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,19 @@ pub struct UpsertArgs {
247247
#[clap(long)]
248248
pub value: String,
249249
}
250+
251+
#[derive(Debug, Clone, Deserialize, Args)]
252+
pub struct GetArgs {
253+
#[clap(long, default_value = "127.0.0.1:9191")]
254+
pub grpc_api_address: String,
255+
256+
/// The key to get
257+
#[clap(long)]
258+
pub key: String,
259+
}
260+
261+
#[derive(Debug, Clone, Deserialize, Args)]
262+
pub struct TriggerSnapshotArgs {
263+
#[clap(long, default_value = "127.0.0.1:28101")]
264+
pub admin_api_address: String,
265+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Metactl Subcommand Tests
2+
3+
Comprehensive tests for all `databend-metactl` subcommands.
4+
5+
## Usage
6+
7+
### Build Prerequisites
8+
```bash
9+
cargo build --bin databend-metactl --bin databend-meta
10+
```
11+
12+
### Run Tests
13+
```bash
14+
# All tests
15+
python tests/metactl/test_all_subcommands.py
16+
17+
# Individual tests
18+
python tests/metactl/subcommands/<name>.py
19+
```
20+
21+
## Test Structure
22+
23+
```
24+
tests/metactl/
25+
├── test_all_subcommands.py # Main test runner
26+
├── subcommands/ # Subcommand test modules
27+
│ ├── __init__.py
28+
│ └── <name>.py # Individual subcommand tests
29+
├── metactl_utils.py # Metactl utilities
30+
├── utils.py # General utilities
31+
├── config/ # Node configurations
32+
└── data files # Test data
33+
```
34+
35+
## Test Execution Order
36+
37+
1. Basic: `status`, `upsert`, `get`
38+
2. Data operations: `watch`, `trigger-snapshot`
39+
3. Export/Import: `export-from-grpc`, `export-from-raft-dir`, `import`
40+
4. Advanced: `transfer-leader`
41+
42+
## CI Integration
43+
44+
```yaml
45+
test-metactl-subcommands:
46+
script:
47+
- cargo build --bin databend-metactl --bin databend-meta
48+
- python tests/metactl/test_all_subcommands.py
49+
```
50+
51+
## Guidelines
52+
53+
- Use global imports: `from utils import`
54+
- Cleanup only on test success
55+
- Check actual results, not just command success
56+
- Follow naming: `subcommands/<name>.py`

tests/metactl/metactl_utils.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import subprocess
5+
import time
6+
from typing import Dict
7+
import requests
8+
from pathlib import Path
9+
from utils import print_step, BUILD_PROFILE, run_command
10+
11+
metactl_bin = f"./target/{BUILD_PROFILE}/databend-metactl"
12+
13+
14+
def metactl_upsert(grpc_addr, key, value):
15+
"""Upsert a key-value pair using the upsert subcommand."""
16+
result = run_command([
17+
metactl_bin, "upsert",
18+
"--grpc-api-address", grpc_addr,
19+
"--key", key,
20+
"--value", value
21+
])
22+
return result
23+
24+
25+
def metactl_trigger_snapshot(admin_addr):
26+
"""Trigger snapshot creation using the trigger-snapshot subcommand."""
27+
result = run_command([
28+
metactl_bin, "trigger-snapshot",
29+
"--admin-api-address", admin_addr
30+
])
31+
return result
32+
33+
34+
def verify_kv(grpc_addr, key, expected_value=None):
35+
"""Verify a key-value pair using the get subcommand."""
36+
time.sleep(0.5) # Brief sleep to ensure data is persisted
37+
38+
result = run_command([
39+
metactl_bin, "get",
40+
"--grpc-api-address", grpc_addr,
41+
"--key", key
42+
], check=False)
43+
44+
print(f"Get result for key '{key}': {result}")
45+
46+
if not result.strip():
47+
assert False, f"Key '{key}' not found or empty result"
48+
49+
try:
50+
data = json.loads(result.strip())
51+
except json.JSONDecodeError as e:
52+
assert False, f"Failed to parse JSON result: {e}, result: {result}"
53+
54+
print(f"Parsed data: {data}")
55+
56+
if data is None:
57+
assert expected_value is None, f"Expected None but got {expected_value}"
58+
else:
59+
# Extract the actual value from the stored data
60+
actual_value = bytes(data["data"]).decode('utf-8')
61+
print(f"Actual value: '{actual_value}', Expected: '{expected_value}'")
62+
63+
if expected_value is not None:
64+
assert actual_value == expected_value, f"Expected '{expected_value}', got '{actual_value}'"
65+
66+
67+
def metactl_export_from_grpc(addr: str) -> str:
68+
"""Export meta data from grpc-address to stdout"""
69+
print_step(f"Start: Export meta data from {addr} to stdout")
70+
71+
cmd = [metactl_bin, "export", "--grpc-api-address", addr]
72+
73+
result = run_command(cmd, check=True)
74+
75+
print_step(f"Done: Exported meta data to stdout")
76+
return result
77+
78+
def metactl_export(meta_dir: str, output_path: str) -> str:
79+
"""Export meta data from raft directory to database file or stdout"""
80+
print_step(f"Start: Export meta data from {meta_dir} to {output_path}")
81+
82+
cmd = [metactl_bin, "export", "--raft-dir", meta_dir]
83+
84+
if output_path:
85+
cmd.append("--db")
86+
cmd.append(output_path)
87+
88+
result = run_command(cmd, check=False)
89+
90+
print_step(f"Done: Exported meta data to {output_path}")
91+
return result
92+
93+
94+
def metactl_import(
95+
meta_dir: str, id: int, db_path: str, initial_cluster: Dict[int, str]
96+
):
97+
"""Import meta data from database file to raft directory"""
98+
cmd = [
99+
metactl_bin,
100+
"import",
101+
"--raft-dir",
102+
meta_dir,
103+
"--id",
104+
str(id),
105+
"--db",
106+
db_path,
107+
]
108+
109+
for id, addr in initial_cluster.items():
110+
cmd.append("--initial-cluster")
111+
cmd.append(f"{id}={addr}")
112+
113+
result = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
114+
result.wait()
115+
print(result.stdout.read().decode())
116+
print(result.stderr.read().decode())
117+
118+
119+
def cluster_status(msg: str) -> str:
120+
"""Get cluster status from meta service endpoint"""
121+
print_step(f"Check /v1/cluster/status {msg} start")
122+
123+
response = requests.get("http://127.0.0.1:28101/v1/cluster/status")
124+
status = response.json()
125+
126+
print_step(f"Check /v1/cluster/status {msg} end")
127+
128+
return status

tests/metactl/subcommands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Subcommand tests package

0 commit comments

Comments
 (0)