> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ネットワーク依存拡張機能のテスト

> OS割り当てポートとフォアグラウンドランチャーを使用して、HTTPサーバーや外部リスナーを起動する拡張機能向けの信頼性の高いMTRテストを記述します。

一部の拡張機能は、テスト中に外部のHTTPサーバーやその他のネットワークリスナーと通信する必要があります。たとえば、アウトバウンドHTTPリクエストを行う拡張機能では、それらを受け取るためのローカルのエコーサーバーが必要です。このページでは、それらのテストが`--parallel=auto`のもとで、また複数のオペレーティングシステムにわたって確実に実行されるように構成する方法を説明します。

ここで説明するパターンは、C++拡張機能とRust拡張機能の両方に等しく適用されます。どちらも`cargo vsql test`を介してテストランナーとしてMTRを使用します。

## ハードコードされたポートが失敗する理由

MTRでは、2つの障害モードによってハードコードされたポートが信頼できないものになります。

**`--parallel`下でのポートの競合。** MTRは各ワーカーに、その`@@port`値の周辺のポート範囲を割り当てます。その範囲外の固定ポート（たとえば`18777`）は、別のワーカーの予約済みブロックやシステムサービスと競合する可能性があり、高い並列度でのみ再現する断続的な`EADDRINUSE`を引き起こします。

**OSレベルの変更。** 新しいランナーイメージやOSバージョンでは、以前は機能していたポートを定期的に予約したり制限したりします。今日はローカルとCIでパスするポートが、明日にはサイレントにバインドに失敗する可能性があります。

どちらの場合も解決策は同じです。ポート`0`にバインドしてOSに利用可能なポートを割り当てさせ、その後、実際のポートをテストに伝えます。

## パターンA: サーバー統合型リスナー

これは、ネットワークリスナーがMySQLプロセス内で実行される場合に使用します。たとえば、HTTPサーバーを埋め込む拡張機能です。

**拡張機能内で**、バインド後にバインドされたポートをステータス変数として公開します。

```cpp theme={null}
// After calling bind() with port 0:
vsql_my_http_port = server.actual_port();  // exposed as vsql_my_ext.http_port
```

**テスト内で**、MTRの`wait_condition.inc`を使用して、ステータス変数がゼロでなくなるまでポーリングし、その後、実際のポートをMTR変数にキャプチャします。`UNINSTALL EXTENSION` / `INSTALL EXTENSION`のサイクルの後は再読み取りします。エフェメラルポートはバインドのたびに変わります。

```sql theme={null}
--disable_query_log
let $wait_timeout= 10;
let $wait_condition=
  SELECT VARIABLE_VALUE > 0
  FROM performance_schema.global_status
  WHERE VARIABLE_NAME = 'vsql_my_ext.http_port';
--source include/wait_condition.inc
--let $my_port = `SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'vsql_my_ext.http_port'`
--enable_query_log

--eval SET @url = CONCAT('http://127.0.0.1:', $my_port, '/endpoint');
SELECT my_extension.fetch(@url);
```

## パターンB: 外部プロセスリスナー

これは、リスナーがスタンドアロンのプロセスである場合に使用します。Pythonヘルパーサーバー、モックAPI、またはMTRが直接制御しない任意の外部プログラムです。

<Note>
  このパターンには`PATH`上に`python3`が必要です。GitHubホステッドランナーにはこれが含まれています。セルフホステッドランナーで使用する前に、利用可能かどうかを確認してください。
</Note>

### フォアグラウンドランチャー

サーバーを`--exec ... &`でバックグラウンド化してポートをポーリングするのではなく、次のことを行うフォアグラウンドランチャースクリプトを使用します。

1. `start_new_session=True`でサブプロセス内にサーバーを起動する
2. サーバーがバインドしてレディネスファイルを書き込むまでブロックする
3. 終了して、MTRの続行を許可する

MTRはフォアグラウンドの`--exec`呼び出しでブロックするため、これによりSQLが実行される前にサーバーの準備が完了していることが保証されます。個別のポーラーやスリープは不要です。

**`launcher.py`** — これを`--write_file`で記述します。

```python theme={null}
import os, subprocess, sys, time

_dir = os.path.dirname(os.path.abspath(__file__))

# Start the server subprocess detached from this process
proc = subprocess.Popen(
    [sys.executable, os.path.join(_dir, 'echo_server.py')],
    start_new_session=True,
    stdout=open(os.path.join(_dir, 'echo_server.log'), 'w'),
    stderr=subprocess.STDOUT,
)

# Write PID for cleanup
with open(os.path.join(_dir, 'echo_server.pid'), 'w') as f:
    f.write(str(proc.pid))

# Block until readiness file appears and is non-empty.
# Check content, not just existence — the file is created before the write
# completes and an empty read would produce a broken MTR include.
port_inc = os.path.join(_dir, 'echo_port.inc')
for _ in range(40):
    try:
        if open(port_inc).read().strip():
            sys.exit(0)
    except OSError:
        pass
    time.sleep(0.25)

# Timed out — print server log for diagnosis
with open(os.path.join(_dir, 'echo_server.log')) as f:
    sys.stderr.write(f.read())
sys.exit(1)
```

**`echo_server.py`** — 実際のサーバーで、サービスを提供する前にポートを書き込みます。

```python theme={null}
import http.server, json, os, signal

signal.alarm(60)  # self-terminate if orphaned by a killed MTR job

class Handler(http.server.BaseHTTPRequestHandler):
    def handle_any(self):
        n = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(n).decode() if n else ''
        self.send_response(200)
        payload = json.dumps({'method': self.command, 'body': body}).encode()
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', len(payload))
        self.end_headers()
        self.wfile.write(payload)
    do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = handle_any
    def log_message(self, *args): pass

_dir = os.path.dirname(os.path.abspath(__file__))
srv = http.server.HTTPServer(('127.0.0.1', 0), Handler)
port = srv.server_address[1]

# Write atomically via rename — the launcher reads content, not just existence,
# so the file must be complete before it becomes visible.
tmp = os.path.join(_dir, 'echo_port.tmp')
inc = os.path.join(_dir, 'echo_port.inc')
with open(tmp, 'w') as f:
    f.write('let $echo_port = %d;\n' % port)
os.rename(tmp, inc)

srv.serve_forever()
```

### テスト内で

ランチャーを起動し（フォアグラウンド — MTRは終了までブロックします）、その後ポートファイルをソースします。

```sql theme={null}
--write_file $MYSQLTEST_VARDIR/tmp/echo_server.py
# ... (echo_server.py contents)
EOF

--write_file $MYSQLTEST_VARDIR/tmp/launcher.py
# ... (launcher.py contents)
EOF

--exec python3 $MYSQLTEST_VARDIR/tmp/launcher.py
--disable_query_log
--source $MYSQLTEST_VARDIR/tmp/echo_port.inc
--eval SET @echo_url = CONCAT('http://127.0.0.1:', $echo_port, '/');
--enable_query_log
```

## ポートを結果ファイルから除外する

結果ファイルは、記述されたとおりのSQLを記録します。URLにエフェメラルポート番号が含まれている場合、結果ファイルには実行ごとに異なる値が含まれることになり、テストはレコード/リプレイのたびに必ず失敗します。

ポートファイルをソースし、MySQLセッション変数の設定を`--disable_query_log`のもとでまとめて行い、`let`代入もエフェメラルポートも出力に現れないようにします。URLが必要な場所ではすべてセッション変数を使用します。

```sql theme={null}
--disable_query_log
--source $MYSQLTEST_VARDIR/tmp/echo_port.inc
--eval SET @echo_url = CONCAT('http://127.0.0.1:', $echo_port, '/');
--enable_query_log

# All queries reference @echo_url — the port never appears in the result file
SELECT JSON_VALUE(CONVERT(my_ext.http_get(@echo_url) USING utf8mb4), '$.status') AS status;
```

## プロセス管理

**ヘルパーの出力をログに記録する — 決して破棄しない。** 上記のランチャーパターンは、サーバーのstderrを`echo_server.log`に書き込み、失敗時にそれを出力します。ヘルパーの出力を破棄すると、CIアーティファクトで不安定なテスト（フレーク）の原因が説明できなくなります。

**名前ではなくPIDでkillする。** Linuxでは、`pkill -f echo_server.py`が`/proc/cmdline`を介してMTRプロセス自体に一致する可能性があります。ランチャーによって書き込まれたPIDファイルを使用します。

```sql theme={null}
--exec sh -c 'kill $(cat $MYSQLTEST_VARDIR/tmp/echo_server.pid) 2>/dev/null || true'
```

**テストが終了する前に、すべての一時ファイルを削除する。** MTRのcheck-testcaseは、`$MYSQLTEST_VARDIR/tmp/`に残されたファイルをダーティ状態としてフラグ付けし、テストを失敗させます。

```sql theme={null}
--remove_file $MYSQLTEST_VARDIR/tmp/echo_server.py
--remove_file $MYSQLTEST_VARDIR/tmp/launcher.py
--remove_file $MYSQLTEST_VARDIR/tmp/echo_server.log
--remove_file $MYSQLTEST_VARDIR/tmp/echo_server.pid
--remove_file $MYSQLTEST_VARDIR/tmp/echo_port.inc
```

パターンBの完全でテスト済みの実装については、`vsql-http/mysql-test/t/vsql_http_requests.test`を参照してください。`--write_file`からクリーンアップまで、テストのライフサイクル全体をカバーしています。

## 関連項目

* [C++テスト](/docs/ja/mysql-8.4/0.0.5/testing) — MTRのセットアップ、スイートの実行、結果の記録、失敗のデバッグ
* [C++での拡張機能の作成](/docs/ja/mysql-8.4/0.0.5/create) — エンドツーエンドのビルド手順とCMakeのセットアップ
* [C++開発](/docs/ja/mysql-8.4/0.0.5/development) — VDFの作成、引数の型、結果の処理
