# 测试

## 本地测试

安装项目依赖后运行完整单元测试：

```bash
python3 -m unittest discover -s tests -v
```

## Complete marker 与版本路径 E2E

[`integration_test/e2e_complete_marker_regression.py`](integration_test/e2e_complete_marker_regression.py)
选择相邻两个 UTC 日期中各自最早的 backup，从两份 metadata 的交集初始化全部
Pack-eligible cluster，并执行三轮完整 workflow，覆盖 complete marker、跨 backup 版本
清理，以及 timeout 后的自然重试或空 selection。

### 风险与前置检查

该脚本会主动修改目标系统：

- 从两份 backup metadata 的公共 `ENABLED` cluster 生成初始化 SQL，删除并重建
  `active_cluster_ids` 表中的数据；
- 第一轮后只推进 `active_cluster_ids.updated_at`，保留已经写入的 watermark，以模拟两个
  backup cutoff 之间发生的数据更新；
- 刷新 workflow 使用的 MySQL 和 Lark Secret；
- 生成并应用 GPG ConfigMap、workflow ConfigMap 和 CronJob；
- 创建 Controller、Pack、Export 和 Watermark Job；
- 读取选定 backup metadata 和 GPG 公钥，并读写测试使用的 export prefix；
- 由本机 Python 进程通过显式 MySQL defaults file 连接测试数据库；
- 完成后解除 CronJob 的 suspend 状态。

执行前完成以下检查：

1. 确认数据库和 `active_cluster_ids` 已作为专用测试目标，并接受脚本结束后保留两份 backup
   中公共的 `ENABLED` serverless cluster；完成行的 watermark 等于第二份 backup cutoff，
   因而不再 eligible，timeout 或 marker 未验证行继续 eligible。
2. 检查脚本开头的 context、namespace、ServiceAccount、Secret、AWS profile、bucket、
   backup prefix 和 image 配置。底层 generator 当前要求 namespace 为 `tidb-serverless`。
3. 确认 `BACKUP_PREFIX` 所指 UTC 日期及其前一天都有 backup、两天各自最早的 backup
   至少有两个公共 `ENABLED` serverless cluster，并允许本机 `AWS_PROFILE_NAME` 读取两份
   `<DFS_PREFIX>/backup/<backup-name>` object。
4. 确认目标 output prefix 中允许为这些 cluster 创建 marker 和版本目录。
5. 选择尚未使用的 `RUN_TOKEN`；状态目录必须不存在，对应 Kubernetes Job 名也不能冲突。
6. 使用由待测 revision 构建的 Controller/Watermark image。
7. 确认本机安装 PyMySQL、能够直连测试 MySQL，并已导出 `LARK_BOT_WEBHOOK`。脚本只把
   `MYSQL_DEFAULTS_FILE` 路径交给 PyMySQL 和顶层生成器，不会打印 defaults file 内容。

异常退出时，数据库可能停留在当前测试轮次的状态，CronJob 也可能保持 suspend。恢复或
重跑前先核对数据库、CronJob 和状态目录中的资源清单。

### 执行

`RUN_TOKEN` 接受 4–9 位小写字母或数字。示例：

```bash
export LARK_BOT_WEBHOOK='<test-webhook>'
CONTROLLER_IMAGE='<image-built-from-current-revision>' \
NODE_GROUP='dm-worker' \
PACK_SHARDS='1' \
RUN_TOKEN='<unique-token>' \
./integration_test/e2e_complete_marker_regression.py
```

可按需显式设置以下输入：

- `K8S_CONTEXT`、`K8S_NAMESPACE`、`CRONJOB_NAME`；
- `NODE_GROUP`，默认 `dm-worker`，同时用于 Controller 和各 workflow stage；
- `MYSQL_DEFAULTS_FILE`、`AWS_PROFILE_NAME`；
- `BACKUP_PREFIX`，格式为 `YYYYMMDD`，默认为当前 UTC 日期；脚本把它作为 B2 日期，把前一
  UTC 日期作为 B1 日期，选择两天中各自名称最早的 backup，并固定两个完整 backup name；
- `MIN_TARGET_CLUSTERS`，默认为 `2`；
- `PACK_SHARDS`（默认 `1`）、`EXPORT_PARALLEL`、`WATERMARK_PARALLEL`、
  `DUMPLING_THREADS`；
- `DUMPLING_TIMEOUT_SECONDS`（默认 `300`）、`CONTROLLER_WAIT_SECONDS`；
- `CONTROLLER_IMAGE`（必填，必须由待测 revision 构建）、`RUN_TOKEN`、`STATE_DIR`。

回归生成的 Pack 和 Export Pod 各请求 `3` CPU 与 `14G` 内存，为 dm-worker 节点上的
Kubernetes 系统组件保留资源空间。

每个 Dumpling process 都有超时上限，但脚本不会挑选特定的大 cluster 来强制触发它。
若当前 backup 中的数据库自然触发 timeout，回归要求 Watermark 把相关 cluster 计为 skipped
并保持 eligible；每轮都验证 `mysql_updated_clusters + skipped_clusters == input_clusters`，
防止任何输入既未更新也未被明确计数。

### 覆盖范围

1. B1 全量导出：先 flush WAL，再下载相邻日期中各自最早的 B1、B2 metadata，过滤掉非
   `ENABLED` keyspace 后取 cluster 交集，初始化至少两个 Pack-eligible cluster；从实际
   Export 日志收集 B1 identity，并核对成功 marker、skipped 集合和数据库 eligibility。
2. B2 全量导出：保留 B1 写入的 watermark，只把相同 cluster 集合的 `updated_at` 设置在
   `(B1, B2]`，模拟真实数据更新；从实际 B2 Pack/Export 结果收集 identity，验证 marker
   指向 B2，并确认两轮共同成功的 identity 已清理 B1 版本、保留 B2 版本。
3. 继续使用 B2 且不再修改数据库：若第二轮没有 skipped cluster，确认 workflow no-op 且全部 marker
   snapshot 不变；否则只重试第二轮的 skipped 集合，再次核对 updated/skipped 完整计数，
   并确认不属于该集合的 marker 保持不变。

每个非空轮次都断言 `selected_clusters`、`packed_clusters` 等于本轮输入 cluster 数，
`packed_meta_objects` 等于动态收集的 identity 数。`unverified_complete_marker_clusters`、
`newer_complete_marker_clusters` 和 `newer_mysql_watermark_clusters` 共同构成 skipped 数；
它与实际 marker 分类以及数据库中仍 eligible 的集合必须一致。

### 结果与清理

状态目录 `integration_test/tmp/complete-marker-${RUN_TOKEN}/` 保存：

- 固定的 B1/B2 backup metadata、两份 metadata 的 cluster 交集、初始化 SQL、cluster 与
  identity 清单；
- Controller 日志与最终 JSON；
- Export 日志与 marker snapshot；
- 测试资源清单。

回归入口在内存中构造 workflow 配置，通过 stdin 调用顶层
`generate_packed_backup_cronjob.py` 进程，并把该进程 stdout 中的 manifest 交给
`kubectl apply`；测试代码不 import 生成器内部实现，也不保存 workflow 配置或 manifest。

Controller、Pack、Export 和 Watermark Job 都设置 3600 秒的
`ttlSecondsAfterFinished`，由 Kubernetes 在完成后回收 Job 及其 Pod；Pack Pod 使用的
generic ephemeral PVC 随 Pod 生命周期回收。创建每个 Controller Job 前，脚本先确认对应
label scope 为空。手工创建的回归 Controller Pod 标记
`cluster-autoscaler.kubernetes.io/safe-to-evict=false`；否则节点缩容会终止唯一的
orchestrator，而 `backoffLimit=0` 会让该轮直接进入 Failed。

三轮成功或普通异常退出都会进入同一条清理路径。对于已经终止的 Controller，脚本只按
精确的 `cse.tidbcloud.com/workflow-name` selector 主动删除本轮临时 Secret/ConfigMap；
删除前逐项校验 kind、名称和 `cse.tidbcloud.com/resource-role`。若 Controller 尚未终止，
其输入资源会保留，避免破坏仍在运行的 workflow。任一校验不匹配都会拒绝删除并令测试
失败。

权限错误会触发立即停止并保留现场，避免在权限边界不明确时继续读取或删除资源。

CronJob、部署用 GPG/workflow ConfigMap、MySQL/Lark Secret、S3 cluster-list object、CSV
版本与 complete marker 会保留。清理前的 JSON inventory 和其余测试证据保存在状态目录；
若访问失败或安全校验失败，脚本会报告未完成清理，而不会放宽 selector。
