
# Testausstrategia

merql käyttää oraakkelipohjaista testausmallia. Kukin testiskenaario määrittää tunnetut syötteet (kolme tietokantatilaa), deterministisen odotetun tuloksen (oraakkelin) ja vertaa toteutunutta yhdistämistulosta tähän odotukseen. 32 skenaariota kattaa puhtaat yhdistämiset, ristiriidat, identiteetin ratkaisun, tietotyypit, skaalan ja poikkeustapaukset.

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

Tämän sivun komennot suoritetaan merql-tietovaraston kloonissa komennon
`composer install` jälkeen. Kehitysskriptit kohteessa `bin/`, `scenarios/`-korpus ja testit eivät kuulu Composer-pakettiin. Kehitysriippuvuuksiin kuuluu myös PHPUnit 13, joka vaatii version PHP 8.4 tai uudemman, vaikka kirjasto toimii versiolla PHP 8.2.

## Oraakkelipohjainen testaus

Olemassa ei ole vertailutyökalua, joka tekisi kolmitieyhdistämisen tietokantojen välillä. Siksi oraakkeli lasketaan tunnettujen syötteiden perusteella ulkoisen työkalun luomisen sijaan. Jokaisella skenaariolla on deterministinen odotettu tulos, jonka oikeellisuus voidaan varmistaa päättelyn avulla:

1. **Valmistelu** -- luo kolme tietokantatilaa (base, ours, theirs), joiden sisältö tunnetaan.
2. **Oraakkeli** -- laske odotettu yhdistämistulos tunnettujen syötteiden perusteella.
3. **Toteutus** -- suorita merql:n yhdistäminen näille kolmelle tilalle.
4. **Vertailu** -- toteutuneen tuloksen on vastattava täsmälleen odotettua tulosta.

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

## Testien suorittaminen

### Täysi varmennus

Skripti `verify-all` on viimeinen tarkistusvaihe. Se suorittaa staattisen analyysin, koodausstandardien tarkistukset, PHPUnit-testit ja oraakkelin regressiotestikokonaisuuden:

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

Tämän on läpäistävä, ennen kuin mitään työtä voidaan pitää valmiina.

### Oraakkelin regressiotestikokonaisuus

Suorita kaikki 32 skenaariota:

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

Suorita yksittäinen luokka:

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

Luokat ovat kohteen `scenarios/` hakemistonimiä: `clean`, `conflict`,
`identity`, `types`, `scale` ja `edge`. Tuntematon luokka tulostaa
`No scenarios found` ja päättyy onnistuneesti. `--category` voidaan toistaa
usean luokan suorittamiseksi.

Skripti hyväksyy myös argumentin `--jobs`, mutta sivuuttaa sen arvon ja suorittaa skenaariot peräkkäin ([#5](https://github.com/inline0/merql/issues/5)).

Suorita vähäisellä tulosteella (vain läpäisy/hylkäys):

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

### Yksittäinen skenaario

Testaa yksi skenaario koko putken läpi:

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

### Yksittäiset oraakkelivaiheet

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

### Yksikkötestit

```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` suorittaa vain kohteen PHPUnit. Kohteen `oracle` PHPUnit-testikokonaisuus osoittaa kohteeseen `tests/Oracle`, jossa ovat skenaariosuorittimen luokat mutta ei testitapauksia. Siksi 32 skenaariota suoritetaan vain kohteiden `composer test:oracle`, `composer verify` tai `./bin/verify-all` kautta.

MySQL-integraatiotestit ohitetaan, elleivät `MERQL_MYSQL_HOST`, `MERQL_MYSQL_DATABASE` ja `MERQL_MYSQL_USER` (sekä haluttaessa `MERQL_MYSQL_PASSWORD` ja `MERQL_MYSQL_PORT`) osoita testitietokantaan.

### Koodin laatu

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

## Skenaarioluokat

### clean (8 skenaariota)

Puhtaat yhdistämiset ilman ristiriitoja. Kumpikin puoli voi muuttaa tietoja, mutta ei koskaan saman rivin samaa saraketta.

| Skenaario | Testit |
|---|---|
| `insert-only-theirs` | Theirs lisäsi rivejä, ours ei muuttunut. |
| `update-only-theirs` | Theirs päivitti rivejä, ours ei muuttunut. |
| `delete-only-theirs` | Theirs poisti rivejä, ours ei muuttunut. |
| `insert-only-ours` | Ours lisäsi rivejä, theirs ei muuttunut. |
| `mixed-no-overlap` | Molemmat muuttivat eri tauluja tai rivejä. |
| `both-same-change` | Molemmat tekivät samat muutokset (ei ristiriitaa). |
| `column-level-clean` | Molemmat muuttivat saman rivin eri sarakkeita. |
| `multi-table` | Muutoksia useissa tauluissa. |

### conflict (6 skenaariota)

Skenaariot, joiden on tuotettava ristiriitoja.

| Skenaario | Testit |
|---|---|
| `both-update-same-column` | Molemmat muuttivat saman sarakkeen eri arvoiksi. |
| `update-vs-delete` | Toinen päivitti rivin, toinen poisti saman rivin. |
| `delete-vs-update` | Päivityksen ja poiston vastakkainen järjestys. |
| `both-insert-same-pk` | Molemmat lisäsivät rivin samalla pääavaimella. |
| `multiple-conflicts` | Useita ristiriitoja yhdessä yhdistämisessä. |
| `partial-conflict` | Osa sarakkeista on ristiriidassa, muut yhdistyvät ongelmitta. |

### identity (4 skenaariota)

Rivi-identiteetin poikkeustapaukset.

| Skenaario | Testit |
|---|---|
| `auto-increment` | Uusilla riveillä on eri tunnisteet eri haaroissa. |
| `natural-key` | Yhdistäminen yksilöllisten sarakkeiden, ei PK:n, perusteella. |
| `composite-key` | Monisarakkeinen pääavain. |
| `no-key` | Taulu ilman pääavainta (sisältötiivisteeseen perustuva vararatkaisu). |

### types (6 skenaariota)

Tietotyyppien käsittely kaikissa saraketyypeissä.

| Skenaario | Testit |
|---|---|
| `text-columns` | VARCHAR-, TEXT- ja LONGTEXT-arvot. |
| `numeric-columns` | INT-, DECIMAL- ja FLOAT-arvot. |
| `date-columns` | DATE-, DATETIME- ja TIMESTAMP-arvot. |
| `json-columns` | JSON-sarakkeen yhdistäminen. |
| `blob-columns` | Binääridata. |
| `null-handling` | Siirtymät NULL-arvosta arvoon ja arvosta NULL-arvoon. |

### scale (4 skenaariota)

Suorituskyky kuormitettuna.

| Skenaario | Testit |
|---|---|
| `1k-rows` | 1,000 riviä. |
| `10k-rows` | 10,000 riviä. |
| `100k-rows` | 100,000 riviä. |
| `wide-table` | Taulu, jossa on yli 50 saraketta. |

### edge (4 skenaariota)

Poikkeustapaukset ja rajaehdot.

| Skenaario | Testit |
|---|---|
| `empty-changeset` | Ei muutoksia toisella tai kummallakaan puolella. |
| `schema-mismatch` | Sarake lisätty tai poistettu tilannevedosten välillä. |
| `encoding` | UTF-8, emoji ja erikoismerkit. |
| `large-text` | Erittäin suuria TEXT- ja LONGTEXT-arvoja. |

## Skenaarion rakenne

Kukin skenaario on hakemisto kohteessa `scenarios/<category>/<name>/`, ja siinä on `scenario.json`-määritys:

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

Ristiriitaskenaarioissa odotetut ristiriidat määritetään yksityiskohtaisesti:

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

## ScenarioRunner-putki

`ScenarioRunner::run()` orkestroi yhden skenaarion:

1. `OracleCapture::compute()` muodostaa kolme tilannevedosta skenaarion JSON-tiedoista ja suorittaa yhdistämisen.
2. `ScenarioComparator::compare()` tarkistaa yhdistämistuloksen kohteessa `scenario.json` määritettyjä odotusarvoja vasten.
3. Palauttaa läpäisy/hylkäys-tuloksen ja luettelon virheilmoituksista.

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

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

`ScenarioRunner::runAll()` suorittaa kaikki skenaariot järjestyksessä:

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

## Uuden skenaarion lisääminen

1. Luo hakemisto: `scenarios/<category>/<name>/`.
2. Luo `scenario.json` skenaarion metatiedoille ja odotuksille.
3. Määritä kolme tietokantatilaa JSON-määrityksessä: base-data, ours-muutokset ja theirs-muutokset.
4. Suorita skenaario: `./bin/test-scenario <name>`.
5. Varmista koko testikokonaisuus: `./bin/verify-all`.

## Koodin laatustandardit

merql edellyttää seuraavaa:

- **PHPStan taso 10** -- tiukka staattinen analyysi ilman poikkeuksia perustasoon.
- **PSR-12** -- koodausstandardit tarkistetaan työkalulla `phpcs`.
- **Oraakkelin regressiotestit** -- kaikkien 32 skenaarion on läpäistävä.
- **Yksikkötestit** -- kunkin komponentin erilliset testit.

Skripti `verify-all` suorittaa kaikki neljä tarkistusta peräkkäin. Osittaista hyväksyntää ei anneta.

## Jatkuva integraatio

Jatkuva integraatio suorittaa jokaisella `main`-tietovaraston pushilla ja pull requestilla koodausstandardien tarkistukset, PHPStan-, `composer test`- ja integraatiotestit versiota MySQL 8.4 vasten mysqli:n kautta, yhdistämisen perustoimintojen testit sekä `composer audit`. Se ei suorita oraakkelin regressiotestikorpusta ([#6](https://github.com/inline0/merql/issues/6)), joten suorita paikallisesti `composer test:oracle` tai `./bin/verify-all` ennen muutokseen luottamista.
