
# 테스트 전략

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에서 실행됩니다.

## 오라클 기반 테스트

3방향 데이터베이스 병합을 수행하는 기존 참조 도구가 없으므로, 외부 도구로 결과를 생성하는 대신 알려진 입력에서 오라클을 계산합니다. 각 시나리오에는 논리적으로 검증할 수 있는 결정론적 예상 결과가 있습니다:

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`을 통해서만 실행됩니다.

MySQL 통합 테스트는 `MERQL_MYSQL_HOST`, `MERQL_MYSQL_DATABASE`, `MERQL_MYSQL_USER`(그리고 선택적으로 `MERQL_MYSQL_PASSWORD` 및 `MERQL_MYSQL_PORT`)가 테스트 데이터베이스를 가리키지 않으면 건너뜁니다.

### 코드 품질

```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` | theirs에서 행을 삽입하고 ours에서는 변경하지 않음. |
| `update-only-theirs` | theirs에서 행을 업데이트하고 ours에서는 변경하지 않음. |
| `delete-only-theirs` | theirs에서 행을 삭제하고 ours에서는 변경하지 않음. |
| `insert-only-ours` | ours에서 행을 삽입하고 theirs에서는 변경하지 않음. |
| `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` | 기본 키 대신 고유 열로 일치시킴. |
| `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`, mysqli를 사용하는 MySQL 8.4 대상 통합 테스트, 병합 기본 연산 커버리지, `composer audit`을 실행합니다. 오라클 회귀 테스트 말뭉치([#6](https://github.com/inline0/merql/issues/6))는 실행하지 않으므로, 변경 사항을 신뢰하기 전에 로컬에서 `composer test:oracle` 또는 `./bin/verify-all`을 실행하세요.
