
# 测试策略

merql 使用由预言结果驱动的测试模型。每个测试场景都会定义一组已知输入（三种数据库状态）、一个确定的预期结果（预言结果），并将实际合并输出与该预期结果进行比较。32 个场景涵盖无冲突合并、冲突、标识解析、数据类型、规模和边界情况。

```bash
# Run the full verification suite.
./bin/verify-all
```

本页中的命令需要在 merql 仓库检出后运行
`composer install`。`bin/` 中的开发脚本、`scenarios/`
语料库和测试不属于 Composer 软件包。开发依赖包括 PHPUnit 13，该版本需要
PHP 8.4 或更高版本，尽管该库本身可在 PHP 8.2 上运行。

## 由预言结果驱动的测试

目前没有可执行数据库三方合并的参考工具，因此预言结果是根据已知输入计算得出的，而不是由外部工具生成。每个场景都有确定的预期结果，可通过逻辑验证：

1. **设置** -- 创建三个内容已知的数据库状态（base、ours、theirs）。
2. **预言结果** -- 根据已知输入计算预期合并结果。
3. **实际结果** -- 对这三个状态运行 merql 合并。
4. **比较** -- 实际结果必须与预期结果完全一致。

```
Known inputs (base, ours, theirs)
         |                    |
    Deterministic          merql merge
    computation
         |                    |
    Expected result       Actual result
         |                    |
         -----> Compare <-----
                  |
            Pass or Fail
```

## 运行测试

### 完整验证

`verify-all` 脚本是最终检查关卡。它会运行静态分析、编码规范检查、PHPUnit 测试和预言结果回归测试套件：

```bash
./bin/verify-all
```

在任何工作被视为完成之前，此检查都必须通过。

### 预言结果回归测试套件

运行全部 32 个场景：

```bash
./bin/test-regression
```

运行单个类别：

```bash
./bin/test-regression --category clean
./bin/test-regression --category conflict
./bin/test-regression --category identity
```

类别对应 `scenarios/` 下的目录名称：`clean`、`conflict`、
`identity`、`types`、`scale` 和 `edge`。未知类别会显示
`No scenarios found` 并成功退出。可以重复使用 `--category` 来运行
多个类别。

脚本也接受 `--jobs`，但会忽略其值，并按顺序逐个运行场景（[#5](https://github.com/inline0/merql/issues/5)）。

以最少输出运行（仅通过/失败）：

```bash
./bin/test-regression --fast
```

### 单个场景

通过完整流程测试一个场景：

```bash
./bin/test-scenario column-level-clean
```

### 单独运行预言结果步骤

```bash
./bin/oracle column-level-clean    # Compute expected result
./bin/actual column-level-clean    # Run merql, capture output
./bin/compare column-level-clean   # Diff oracle vs actual
```

### 单元测试

```bash
composer test:unit         # Isolated component tests
composer test:integration  # Integration tests (SQLite, plus MySQL when configured)
composer test              # All PHPUnit suites
composer test:oracle       # Oracle regression corpus (bin/test-regression)
composer verify            # analyse + cs + test + test:oracle
```

`composer test` 只运行 PHPUnit。`oracle` PHPUnit 套件指向
`tests/Oracle`，其中包含场景运行器类，但没有测试用例，因此这 32 个
场景只会通过 `composer test:oracle`、`composer verify` 或
`./bin/verify-all` 运行。

除非 `MERQL_MYSQL_HOST`、`MERQL_MYSQL_DATABASE` 和 `MERQL_MYSQL_USER`（以及可选的
`MERQL_MYSQL_PASSWORD` 和 `MERQL_MYSQL_PORT`）均指向测试数据库，否则会跳过 MySQL 集成测试。

### 代码质量

```bash
composer cs            # PSR-12 coding standards check
composer cs:fix        # Auto-fix coding standards
composer analyse       # PHPStan level 10 static analysis
```

## 场景类别

### clean（8 个场景）

无冲突的干净合并。双方都可以更改数据，但绝不会更改同一行的同一列。

| 场景 | 测试内容 |
|---|---|
| `insert-only-theirs` | 对方插入行，我们这边未更改。 |
| `update-only-theirs` | 对方更新行，我们这边未更改。 |
| `delete-only-theirs` | 对方删除行，我们这边未更改。 |
| `insert-only-ours` | 我们这边插入行，对方未更改。 |
| `mixed-no-overlap` | 双方更改了不同的表或行。 |
| `both-same-change` | 双方进行了相同的更改（无冲突）。 |
| `column-level-clean` | 双方更改了同一行的不同列。 |
| `multi-table` | 跨多个表的更改。 |

### conflict（6 个场景）

必须产生冲突的场景。

| 场景 | 测试内容 |
|---|---|
| `both-update-same-column` | 双方将同一列更改为不同的值。 |
| `update-vs-delete` | 一方更新某行，另一方删除同一行。 |
| `delete-vs-update` | 更新与删除的反向情况。 |
| `both-insert-same-pk` | 双方都插入了主键相同的行。 |
| `multiple-conflicts` | 一次合并中出现多个冲突。 |
| `partial-conflict` | 部分列存在冲突，其他列可无冲突合并。 |

### identity（4 个场景）

行标识边界情况。

| 场景 | 测试内容 |
|---|---|
| `auto-increment` | 新行在各分支中具有不同的 ID。 |
| `natural-key` | 使用唯一列而不是 PK 进行匹配。 |
| `composite-key` | 多列主键。 |
| `no-key` | 没有主键的表（回退到内容哈希）。 |

### types（6 个场景）

涵盖所有列类型的数据类型处理。

| 场景 | 测试内容 |
|---|---|
| `text-columns` | VARCHAR、TEXT、LONGTEXT 值。 |
| `numeric-columns` | INT、DECIMAL、FLOAT 值。 |
| `date-columns` | DATE、DATETIME、TIMESTAMP 值。 |
| `json-columns` | JSON 列合并。 |
| `blob-columns` | 二进制数据。 |
| `null-handling` | NULL 到值、值到 NULL 的转换。 |

### scale（4 个场景）

负载下的性能。

| 场景 | 测试内容 |
|---|---|
| `1k-rows` | 1,000 行。 |
| `10k-rows` | 10,000 行。 |
| `100k-rows` | 100,000 行。 |
| `wide-table` | 含 50+ 列的表。 |

### edge（4 个场景）

边界情况和边界条件。

| 场景 | 测试内容 |
|---|---|
| `empty-changeset` | 一侧或双方都没有更改。 |
| `schema-mismatch` | 快照之间添加或删除列。 |
| `encoding` | UTF-8、表情符号、特殊字符。 |
| `large-text` | 超大的 TEXT/LONGTEXT 值。 |

## 场景结构

每个场景都是 `scenarios/<category>/<name>/` 下的一个目录，包含 `scenario.json` 配置：

```json
{
    "name": "column-level-clean",
    "category": "clean",
    "description": "Both sides modify same row but different columns",
    "tables": ["test_posts"],
    "expectations": {
        "conflicts": 0,
        "operations": 3,
        "changeset_match": "exact",
        "merge_match": "exact",
        "sql_match": "semantic"
    }
}
```

对于冲突场景，预期冲突会详细列出：

```json
{
    "expectations": {
        "conflicts": 1,
        "conflict_details": [
            {
                "table": "test_posts",
                "primary_key": {"id": 42},
                "column": "title",
                "ours_value": "Welcome",
                "theirs_value": "Greetings"
            }
        ]
    }
}
```

## ScenarioRunner 流程

`ScenarioRunner::run()` 负责协调单个场景：

1. `OracleCapture::compute()` 根据场景的 JSON 数据构建三个快照并运行合并。
2. `ScenarioComparator::compare()` 根据 `scenario.json` 中的预期值检查合并结果。
3. 返回通过/失败状态以及失败消息列表。

```php
use Merql\Tests\Oracle\ScenarioRunner;

$result = ScenarioRunner::run($scenario);
// ['name' => 'column-level-clean', 'pass' => true, 'failures' => []]
```

`ScenarioRunner::runAll()` 按顺序运行每个场景：

```php
$results = ScenarioRunner::runAll($scenarios);
```

## 添加新场景

1. 创建目录：`scenarios/<category>/<name>/`。
2. 创建 `scenario.json`，填写场景元数据和预期结果。
3. 在 JSON 配置中定义三种数据库状态：base 数据、ours 的更改、theirs 的更改。
4. 运行场景：`./bin/test-scenario <name>`。
5. 验证完整测试套件：`./bin/verify-all`。

## 代码质量标准

merql 执行以下检查：

- **PHPStan 10 级** -- 严格静态分析，不设基线例外。
- **PSR-12** -- 由 `phpcs` 检查编码规范。
- **预言结果回归测试** -- 32 个场景必须全部通过。
- **单元测试** -- 针对各个组件的独立测试。

`verify-all` 脚本会依次运行全部四项检查。不接受只通过部分检查。

## 持续集成

CI 会在每次向 `main` 推送或提交拉取请求时，运行编码规范检查、PHPStan、`composer test`、针对 MySQL 8.4 的 mysqli 集成测试套件、合并原语覆盖率检查，以及 `composer audit`。它不会运行预言结果回归语料库（[#6](https://github.com/inline0/merql/issues/6)），因此在依赖某项更改之前，请在本地运行 `composer test:oracle` 或 `./bin/verify-all`。
