|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import re |
| 4 | +import shutil |
| 5 | +import time |
| 6 | +from metactl_utils import metactl_bin, metactl_upsert, metactl_trigger_snapshot |
| 7 | +from utils import run_command, kill_databend_meta, start_meta_node, print_title |
| 8 | + |
| 9 | + |
| 10 | +def test_dump_raft_log_wal(): |
| 11 | + """Test dump-raft-log-wal subcommand from raft directory.""" |
| 12 | + print_title("Test dump-raft-log-wal from raft directory") |
| 13 | + kill_databend_meta() |
| 14 | + shutil.rmtree(".databend", ignore_errors=True) |
| 15 | + start_meta_node(1, False) |
| 16 | + |
| 17 | + grpc_addr = "127.0.0.1:9191" |
| 18 | + admin_addr = "127.0.0.1:28101" |
| 19 | + |
| 20 | + # Insert test data |
| 21 | + test_keys = [ |
| 22 | + ("app/db/host", "localhost"), |
| 23 | + ("app/db/port", "5432"), |
| 24 | + ("app/config/timeout", "30"), |
| 25 | + ] |
| 26 | + |
| 27 | + for key, value in test_keys: |
| 28 | + metactl_upsert(grpc_addr, key, value) |
| 29 | + print("✓ Test data inserted") |
| 30 | + |
| 31 | + # Trigger snapshot to ensure raft log has data |
| 32 | + metactl_trigger_snapshot(admin_addr) |
| 33 | + print("✓ Snapshot triggered") |
| 34 | + |
| 35 | + # Wait for snapshot to complete |
| 36 | + time.sleep(2) |
| 37 | + |
| 38 | + # Stop meta service before accessing raft directory |
| 39 | + kill_databend_meta() |
| 40 | + |
| 41 | + # Test dump-raft-log-wal from raft directory |
| 42 | + result = run_command([metactl_bin, "dump-raft-log-wal", "--raft-dir", "./.databend/meta1"]) |
| 43 | + |
| 44 | + print("Output:") |
| 45 | + print(result) |
| 46 | + |
| 47 | + # Expected output with time field that will be masked |
| 48 | + expected = """RaftLog: |
| 49 | +ChunkId(00_000_000_000_000_000_000) |
| 50 | + R-00000: [000_000_000, 000_000_018) Size(18): RaftLogState(RaftLogState(vote: None, last: None, committed: None, purged: None, user_data: None)) |
| 51 | + R-00001: [000_000_018, 000_000_046) Size(28): RaftLogState(RaftLogState(vote: None, last: None, committed: None, purged: None, user_data: LogStoreMeta{ node_id: Some(1) })) |
| 52 | + R-00002: [000_000_046, 000_000_125) Size(79): Append(log_id: T0-N0.0, payload: membership:{voters:[{1:EmptyNode}], learners:[]}) |
| 53 | + R-00003: [000_000_125, 000_000_175) Size(50): SaveVote(<T1-N1:->) |
| 54 | + R-00004: [000_000_175, 000_000_225) Size(50): SaveVote(<T1-N1:Q>) |
| 55 | + R-00005: [000_000_225, 000_000_277) Size(52): Append(log_id: T1-N1.1, payload: blank) |
| 56 | + R-00006: [000_000_277, 000_000_472) Size(195): Append(log_id: T1-N1.2, payload: normal:time: <TIME> cmd: add_node(no-override):1=id=1 raft=localhost:28103 grpc=127.0.0.1:9191) |
| 57 | + R-00007: [000_000_472, 000_000_518) Size(46): Commit(T1-N1.1) |
| 58 | + R-00008: [000_000_518, 000_000_564) Size(46): Commit(T1-N1.2) |
| 59 | + R-00009: [000_000_564, 000_000_643) Size(79): Append(log_id: T1-N1.3, payload: membership:{voters:[{1:EmptyNode}], learners:[]}) |
| 60 | + R-00010: [000_000_643, 000_000_689) Size(46): Commit(T1-N1.3) |
| 61 | + R-00011: [000_000_689, 000_000_884) Size(195): Append(log_id: T1-N1.4, payload: normal:time: <TIME> cmd: add_node(override):1=id=1 raft=localhost:28103 grpc=127.0.0.1:9191) |
| 62 | + R-00012: [000_000_884, 000_000_930) Size(46): Commit(T1-N1.4) |
| 63 | + R-00013: [000_000_930, 000_001_192) Size(262): Append(log_id: T1-N1.5, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/db/host >= seq(0)] then:[Put(Put key=app/db/host)] else:[Get(Get key=app/db/host)]}) |
| 64 | + R-00014: [000_001_192, 000_001_238) Size(46): Commit(T1-N1.5) |
| 65 | + R-00015: [000_001_238, 000_001_495) Size(257): Append(log_id: T1-N1.6, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/db/port >= seq(0)] then:[Put(Put key=app/db/port)] else:[Get(Get key=app/db/port)]}) |
| 66 | + R-00016: [000_001_495, 000_001_541) Size(46): Commit(T1-N1.6) |
| 67 | + R-00017: [000_001_541, 000_001_817) Size(276): Append(log_id: T1-N1.7, payload: normal:time: <TIME> cmd: txn:TxnRequest{if:[app/config/timeout >= seq(0)] then:[Put(Put key=app/config/timeout)] else:[Get(Get key=app/config/timeout)]}) |
| 68 | + R-00018: [000_001_817, 000_001_863) Size(46): Commit(T1-N1.7)""" |
| 69 | + |
| 70 | + def mask_time(text): |
| 71 | + """Mask time fields in the output for comparison.""" |
| 72 | + # Mask timestamp in format "time: 2025-10-19T15:03:55.193" |
| 73 | + text = re.sub(r'time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+', 'time: <TIME>', text) |
| 74 | + return text |
| 75 | + |
| 76 | + # Normalize both actual and expected output |
| 77 | + actual_lines = result.strip().split("\n") |
| 78 | + expected_lines = expected.strip().split("\n") |
| 79 | + |
| 80 | + # Mask time in actual output |
| 81 | + actual_masked = [mask_time(line) for line in actual_lines] |
| 82 | + |
| 83 | + # Verify line count matches |
| 84 | + assert len(actual_masked) == len(expected_lines), ( |
| 85 | + f"Line count mismatch: got {len(actual_masked)} lines, expected {len(expected_lines)} lines" |
| 86 | + ) |
| 87 | + |
| 88 | + # Compare line by line |
| 89 | + for i, (actual_line, expected_line) in enumerate(zip(actual_masked, expected_lines)): |
| 90 | + assert actual_line == expected_line, ( |
| 91 | + f"Line {i} mismatch:\n" |
| 92 | + f" Actual : {actual_line}\n" |
| 93 | + f" Expected: {expected_line}" |
| 94 | + ) |
| 95 | + |
| 96 | + print(f"✓ All {len(actual_masked)} lines match expected output (time fields masked)") |
| 97 | + |
| 98 | + # Clean up only on success |
| 99 | + shutil.rmtree(".databend", ignore_errors=True) |
| 100 | + |
| 101 | + |
| 102 | +def main(): |
| 103 | + """Main function to run all dump-raft-log-wal tests.""" |
| 104 | + test_dump_raft_log_wal() |
| 105 | + |
| 106 | + |
| 107 | +if __name__ == "__main__": |
| 108 | + main() |
0 commit comments