
# Stratégie de test

merql utilise un modèle de test guidé par un oracle. Chaque scénario de test définit un ensemble connu d'entrées (trois états de base de données), un résultat attendu déterministe (l'oracle), puis compare le résultat réel de la fusion à cette attente. 32 scénarios couvrent les fusions sans conflit, les conflits, la résolution d'identité, les types de données, la charge et les cas limites.

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

Les commandes de cette page s'exécutent depuis une copie du dépôt merql après
`composer install`. Les scripts de développement dans `bin/`, le corpus de
`scenarios/` et les tests ne font pas partie du paquet Composer. Les dépendances
de développement incluent PHPUnit 13, qui nécessite PHP 8.4 ou une version
ultérieure, alors que la bibliothèque elle-même fonctionne avec PHP 8.2.

## Tests guidés par un oracle

Il n'existe pas d'outil de référence pour effectuer une fusion de bases de données à trois voies. L'oracle est donc calculé à partir d'entrées connues, plutôt que généré par un outil externe. Chaque scénario possède un résultat attendu déterministe, vérifiable par raisonnement :

1. **Préparation** -- créer trois états de base de données (base, ours, theirs) avec un contenu connu.
2. **Oracle** -- calculer le résultat de fusion attendu à partir des entrées connues.
3. **Résultat réel** -- lancer la fusion de merql sur les trois états.
4. **Comparaison** -- le résultat réel doit correspondre exactement au résultat attendu.

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

## Exécution des tests

### Vérification complète

Le script `verify-all` est le contrôle final. Il exécute l'analyse statique, la vérification des normes de code, les tests PHPUnit et la suite de régression de l'oracle :

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

Cette vérification doit réussir avant que le travail puisse être considéré comme terminé.

### Suite de régression de l'oracle

Exécutez les 32 scénarios :

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

Exécutez une seule catégorie :

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

Les catégories correspondent aux noms des répertoires dans `scenarios/` : `clean`, `conflict`,
`identity`, `types`, `scale` et `edge`. Une catégorie inconnue affiche
`No scenarios found` et se termine avec succès. Il est possible de répéter `--category` pour exécuter
plusieurs catégories.

Le script accepte également `--jobs`, mais ignore sa valeur et exécute les scénarios
l'un après l'autre ([#5](https://github.com/inline0/merql/issues/5)).

Exécutez avec une sortie minimale (réussite/échec uniquement) :

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

### Scénario unique

Testez un scénario avec le pipeline complet :

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

### Étapes individuelles de l'oracle

```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
```

### Tests unitaires

```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` exécute uniquement PHPUnit. La suite `oracle` PHPUnit pointe vers
`tests/Oracle`, qui contient les classes du lanceur de scénarios, mais aucun cas de test ;
les 32 scénarios s'exécutent donc uniquement via `composer test:oracle`, `composer verify` ou
`./bin/verify-all`.

Les tests d'intégration MySQL sont ignorés, sauf si `MERQL_MYSQL_HOST`,
`MERQL_MYSQL_DATABASE` et `MERQL_MYSQL_USER` (ainsi que, facultativement,
`MERQL_MYSQL_PASSWORD` et `MERQL_MYSQL_PORT`) pointent vers une base de données de test.

### Qualité du code

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

## Catégories de scénarios

### clean (8 scénarios)

Fusions sans conflit. Les deux côtés peuvent modifier des données, mais jamais la même colonne de la même ligne.

| Scénario | Tests |
|---|---|
| `insert-only-theirs` | theirs a inséré des lignes, ours n'a rien modifié. |
| `update-only-theirs` | theirs a mis à jour des lignes, ours n'a rien modifié. |
| `delete-only-theirs` | theirs a supprimé des lignes, ours n'a rien modifié. |
| `insert-only-ours` | ours a inséré des lignes, theirs n'a rien modifié. |
| `mixed-no-overlap` | Les deux ont modifié des tables ou des lignes différentes. |
| `both-same-change` | Les deux ont effectué des modifications identiques (sans conflit). |
| `column-level-clean` | Les deux ont modifié la même ligne, mais des colonnes différentes. |
| `multi-table` | Modifications réparties sur plusieurs tables. |

### conflict (6 scénarios)

Scénarios devant produire des conflits.

| Scénario | Tests |
|---|---|
| `both-update-same-column` | Les deux ont modifié la même colonne avec des valeurs différentes. |
| `update-vs-delete` | L'un a mis à jour la ligne, l'autre l'a supprimée. |
| `delete-vs-update` | Cas inverse de mise à jour contre suppression. |
| `both-insert-same-pk` | Les deux ont inséré une ligne avec la même clé primaire. |
| `multiple-conflicts` | Plusieurs conflits dans une seule fusion. |
| `partial-conflict` | Certaines colonnes sont en conflit, tandis que d'autres sont fusionnées sans conflit. |

### identity (4 scénarios)

Cas limites liés à l'identité des lignes.

| Scénario | Tests |
|---|---|
| `auto-increment` | Les nouvelles lignes ont des identifiants différents selon les branches. |
| `natural-key` | Correspondance par colonnes uniques plutôt que par clé primaire. |
| `composite-key` | Clé primaire composée de plusieurs colonnes. |
| `no-key` | Table sans clé primaire (repli sur le hachage du contenu). |

### types (6 scénarios)

Gestion de tous les types de données de colonne.

| Scénario | Tests |
|---|---|
| `text-columns` | Valeurs VARCHAR, TEXT, LONGTEXT. |
| `numeric-columns` | Valeurs INT, DECIMAL, FLOAT. |
| `date-columns` | Valeurs DATE, DATETIME, TIMESTAMP. |
| `json-columns` | Fusion de colonnes JSON. |
| `blob-columns` | Données binaires. |
| `null-handling` | Transitions de NULL vers une valeur et d'une valeur vers NULL. |

### scale (4 scénarios)

Performances en charge.

| Scénario | Tests |
|---|---|
| `1k-rows` | 1,000 lignes. |
| `10k-rows` | 10,000 lignes. |
| `100k-rows` | 100,000 lignes. |
| `wide-table` | Table de plus de 50 colonnes. |

### edge (4 scénarios)

Cas limites et conditions aux frontières.

| Scénario | Tests |
|---|---|
| `empty-changeset` | Aucun changement d'un côté ou des deux. |
| `schema-mismatch` | Colonne ajoutée ou supprimée entre deux instantanés. |
| `encoding` | UTF-8, emoji, caractères spéciaux. |
| `large-text` | Valeurs TEXT/LONGTEXT très volumineuses. |

## Structure des scénarios

Chaque scénario est un répertoire dans `scenarios/<category>/<name>/`, contenant une configuration `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"
    }
}
```

Pour les scénarios avec conflit, les conflits attendus sont spécifiés en détail :

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

## Pipeline du ScenarioRunner

`ScenarioRunner::run()` orchestre un scénario unique :

1. `OracleCapture::compute()` construit trois instantanés à partir des données JSON du scénario et lance la fusion.
2. `ScenarioComparator::compare()` vérifie le résultat de la fusion par rapport aux valeurs attendues dans `scenario.json`.
3. Renvoie une réussite ou un échec, avec la liste des messages d'échec.

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

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

`ScenarioRunner::runAll()` exécute chaque scénario à la suite :

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

## Ajouter un scénario

1. Créez un répertoire : `scenarios/<category>/<name>/`.
2. Créez `scenario.json` avec les métadonnées et les résultats attendus du scénario.
3. Définissez les trois états de base de données dans la configuration JSON : données de base, modifications de ours et modifications de theirs.
4. Lancez le scénario : `./bin/test-scenario <name>`.
5. Vérifiez la suite complète : `./bin/verify-all`.

## Normes de qualité du code

merql impose :

- **Niveau 10 de PHPStan** -- analyse statique stricte, sans exception de référence de base.
- **PSR-12** -- normes de codage vérifiées par `phpcs`.
- **Régression de l'oracle** -- les 32 scénarios doivent réussir.
- **Tests unitaires** -- tests isolés pour chaque composant.

Le script `verify-all` exécute les quatre vérifications à la suite. Aucune validation partielle n'est acceptée.

## Intégration continue

L'intégration continue exécute les vérifications des normes de codage, PHPStan, `composer test`, la suite d'intégration avec MySQL 8.4 via mysqli, les tests de couverture des opérations de fusion primitives et `composer audit` à chaque envoi et demande d'intégration vers `main`. Elle n'exécute pas le corpus de régression de l'oracle ([#6](https://github.com/inline0/merql/issues/6)) ; exécutez donc `composer test:oracle` ou `./bin/verify-all` localement avant de vous fier à une modification.
