proto-socket/agent-task/python_impl/PLAN.md
toki fa273bcc26 feat: add Python implementation and cross-test support
- Add Python toki_socket package (base_client, communicator, tcp/ws client/server)
- Add protobuf message definitions for Python
- Add go_python.go cross-test implementation and python_go_client
- Add agent-task/python_impl plan and code review docs
- Update .gitignore for Python cache files with recursive patterns
2026-04-23 16:44:43 +09:00

25 KiB
Raw Blame History

Python 구현체 추가 (toki_socket)

이 파일을 읽는 구현 에이전트에게

각 항목의 체크리스트를 완료하면서 [ ][x]로 표시하라. 중간 검증 명령은 해당 항목 구현 직후 실행하고, 출력을 CODE_REVIEW.md의 검증 결과 섹션에 붙여넣어라. 최종 검증도 마찬가지로 실행 후 출력을 기록하라. 계획과 다르게 구현한 부분은 이유와 함께 CODE_REVIEW.md의 "계획 대비 변경 사항"에 기록하라.


배경

PROTOCOL.md의 Multi-language Implementations 표에 Python이 Planned로 등록되어 있다. Go 구현체(go/)가 레퍼런스이며, PORTING_GUIDE.md에 Python 매핑 표와 주의사항이 명시되어 있다. Python 패키지를 python/ 디렉터리에 생성하고, Go×Python 양방향 크로스 테스트를 추가하여 프로토콜 호환성을 검증한다. TLS/WSS는 이번 범위에서 제외하고 README에 명시한다.


의존 관계 및 구현 순서

[API-1] scaffold → [API-2] Communicator → [API-3] BaseClient
                                         → [API-4] TCP Transport
                                         → [API-5] WebSocket Transport
[API-2~5] 완료 → [API-6] 단위 테스트 → [API-7] 크로스 테스트

[API-1] Python 패키지 scaffold

문제

python/ 디렉터리가 없다. 패키지 매니저, proto binding, 최소 패키지 구조가 필요하다.

해결 방법

  • python/pyproject.toml 생성 (setuptools, Python 3.11+)
  • python/toki_socket/__init__.py 생성
  • dart/lib/src/packets/message_common.protopython/toki_socket/packets/message_common.proto로 복사 (schema만, 언어 option 추가 불필요)
  • protocmessage_common_pb2.py, message_common_pb2.pyi 생성

pyproject.toml 핵심:

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "toki-socket"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "protobuf>=4.25",
    "websockets>=12.0",
]

[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]

디렉터리 구조:

python/
  toki_socket/
    __init__.py
    communicator.py
    base_client.py
    tcp_client.py
    tcp_server.py
    ws_client.py
    ws_server.py
    packets/
      __init__.py
      message_common.proto
      message_common_pb2.py      (generated)
      message_common_pb2.pyi     (generated)
  test/
    __init__.py
    test_communicator.py
    test_tcp.py
  crosstest/
    go_python_client.py
    python_go.py
  pyproject.toml
  README.md

수정 파일 및 체크리스트

  • python/pyproject.toml 생성
  • python/toki_socket/__init__.py 생성 (public export 비워둠)
  • python/toki_socket/packets/__init__.py 생성
  • python/toki_socket/packets/message_common.proto 복사
  • python/toki_socket/packets/message_common_pb2.py 생성 (protoc 실행 또는 직접 작성)
  • python/toki_socket/packets/message_common_pb2.pyi 생성
  • python/README.md 생성 (TLS 미지원 명시 포함)

테스트 작성

skip — scaffold 단계, 코드 로직 없음.

중간 검증

cd python && pip install -e ".[dev]" --quiet && python -c "from toki_socket.packets import message_common_pb2; print(message_common_pb2.TestData.DESCRIPTOR.name)"

예상 출력: TestData


[API-2] Communicator 구현

문제

go/communicator.go에 해당하는 Python 비동기 구현이 없다. Transport Protocol, Communicator 클래스, write 큐, nonce 관리, listener/requestListener 라우팅을 구현해야 한다.

해결 방법

python/toki_socket/communicator.py에 아래 구조로 구현한다.

Transport Protocol (Go의 Transport interface):

from typing import Protocol, runtime_checkable
from toki_socket.packets.message_common_pb2 import PacketBase

@runtime_checkable
class Transport(Protocol):
    async def write_packet(self, base: PacketBase) -> None: ...
    async def close(self) -> None: ...

ParserMap: dict[str, Callable[[bytes], Message]]

Communicator 핵심:

class Communicator:
    def __init__(self) -> None:
        self._nonce: int = 0
        self._is_alive: bool = False
        self._parser_map: dict[str, Callable] = {}
        self._handlers: dict[str, list[Callable]] = {}
        self._req_handlers: dict[str, Callable] = {}
        self._pending_requests: dict[int, asyncio.Future] = {}
        self._write_queue: asyncio.Queue = asyncio.Queue(maxsize=64)
        self._closed: asyncio.Event = asyncio.Event()
        self._transport: Transport | None = None
        self._write_loop_task: asyncio.Task | None = None
        self._write_error_handler: Callable[[Exception], None] | None = None

    def initialize(self, transport: Transport, parser_map: dict) -> None:
        ...  # 하트비트 파서 추가, write_loop Task 시작

    def is_alive(self) -> bool: ...
    def _next_nonce(self) -> int: ...
    def _shutdown(self) -> None: ...
    async def close(self) -> None: ...
    async def _write_loop(self) -> None: ...
    async def queue_packet(self, base: PacketBase) -> None: ...
    async def send(self, message: Message) -> None: ...
    async def send_request(self, req: Message, res_type: type[T], timeout: float = 30.0) -> T: ...
    def add_listener(self, type_name: str, fn: Callable) -> None: ...
    def remove_listeners(self, type_name: str) -> None: ...
    def add_request_listener(self, type_name: str, fn: Callable) -> None: ...
    def on_received_data(self, type_name: str, data: bytes, nonce: int, response_nonce: int) -> None: ...
    def _handle_response(self, type_name: str, data: bytes, response_nonce: int) -> None: ...
    def _parse(self, type_name: str, data: bytes) -> Message: ...

def type_name_of(message_class) -> str:
    return message_class.DESCRIPTOR.name

write_loop 패턴 — asyncio 단일 스레드이므로 sentinel 방식 사용:

_STOP = object()

async def _write_loop(self) -> None:
    while True:
        item = await self._write_queue.get()
        if item is _STOP:
            return
        base, fut = item
        try:
            await self._transport.write_packet(base)
            if not fut.done():
                fut.set_result(None)
        except Exception as e:
            if not fut.done():
                fut.set_exception(e)
            if self._write_error_handler:
                self._write_error_handler(e)

_shutdown()에서 _write_queue.put_nowait(_STOP) 호출.

send_requestasyncio.get_event_loop().create_future()로 pending 등록, asyncio.wait_for(fut, timeout)으로 대기.

addListener / addRequestListener 상호 배타 — 같은 typeName 중복 시 ValueError raise.

HeartBeat 자동 처리initialize() 내에서 HeartBeat 파서를 _parser_map에 추가; BaseClient에서 listener 등록.

on_received_data — asyncio 단일 스레드이므로 요청 핸들러는 asyncio.create_task()로 실행 (Go의 go reqHandler(...)와 동일).

수정 파일 및 체크리스트

  • python/toki_socket/communicator.py 생성
    • Transport Protocol 정의
    • type_name_of(cls) 함수 — cls.DESCRIPTOR.name 반환
    • Communicator.initialize() — parser_map + HeartBeat 파서 등록, write_loop Task 시작
    • Communicator._write_loop() — sentinel 패턴
    • Communicator.queue_packet() — closed 체크, Future 등록, put, await
    • Communicator.send() — marshal → queue_packet
    • Communicator.send_request() — pending 등록, marshal, queue, asyncio.wait_for
    • Communicator.add_listener() / add_request_listener() — 상호 배타 검사
    • Communicator.on_received_data() — response_nonce > 0이면 handleResponse, 아니면 listener/reqHandler
    • Communicator._shutdown() — is_alive=False, closed.set(), write_queue에 STOP 전달

테스트 작성

python/test/test_communicator.py에 작성:

  • test_add_listener_exclusivity — 같은 typeName에 add_listener 후 add_request_listener 시 ValueError 확인
  • test_send_and_receive — FakeTransport (write_packet 캡처)로 send() 후 패킷 내용 검증
  • test_send_request_response — FakeTransport로 send_request 후 수동으로 on_received_data 호출하여 Future resolve 검증
  • test_shutdown — close() 후 is_alive() == False 검증

중간 검증

cd python && python -m pytest test/test_communicator.py -v

예상: 4개 테스트 PASS


[API-3] BaseClient 구현

문제

go/base_client.go에 해당하는 heartbeat, disconnect listener, connCloseOnce 패턴이 없다.

해결 방법

python/toki_socket/base_client.py에 구현.

Go sync.Onceasyncio.Lock + bool 플래그:

self._close_called: bool = False
self._close_lock: asyncio.Lock = asyncio.Lock()
class BaseClient:
    def __init__(self, interval_sec: int, wait_sec: int, do_close: Callable[[], Awaitable[None]]) -> None:
        self.communicator: Communicator = Communicator()
        self._interval_sec = interval_sec
        self._wait_sec = wait_sec
        self._do_close = do_close
        self._close_called = False
        self._close_lock: asyncio.Lock  # initialize()에서 생성
        self._disconnect_listeners: list[Callable] = []
        self._hb_task: asyncio.Task | None = None
        self._waiting_hb_response: bool = False

    def _init_base(self) -> None:
        self._close_lock = asyncio.Lock()

    async def close(self) -> None:
        async with self._close_lock:
            if self._close_called:
                return
            self._close_called = True
        self.communicator._shutdown()
        self._stop_heartbeat()
        if self._do_close:
            await self._do_close()
        await self._notify_disconnected()

    def add_disconnect_listener(self, fn: Callable) -> None: ...
    def remove_disconnect_listeners(self) -> None: ...
    async def _notify_disconnected(self) -> None: ...
    async def _on_disconnected(self) -> None:
        await self.close()
    async def _send_heartbeat(self) -> None: ...  # Go sendHeartBeat() 패턴
    async def _on_heartbeat(self) -> None: ...    # Go onHeartBeat() 패턴
    def _stop_heartbeat(self) -> None: ...

Heartbeat 타이머: asyncio.get_event_loop().call_later() 또는 asyncio.create_task(asyncio.sleep(n))으로 구현. Go의 HeartbeatTimer에 해당하는 _HbTimer 헬퍼 클래스를 동일 파일에 작성.

수정 파일 및 체크리스트

  • python/toki_socket/base_client.py 생성
    • _HbTimerasyncio.create_task(sleep+callback), cancel() 메서드
    • BaseClient._init_base() — close_lock 초기화 (asyncio.Lock은 루프 바인딩 필요)
    • BaseClient.close() — connCloseOnce 패턴, shutdown → stop_heartbeat → do_close → notify
    • BaseClient._send_heartbeat() — interval > 0이면 타이머 설정
    • BaseClient._on_heartbeat() — waiting 상태이면 False로 리셋, 아니면 HeartBeat 송신
    • BaseClient._notify_disconnected() — listener 복사 후 asyncio.gather로 호출

테스트 작성

단위 테스트 skip — Transport 없이 단독 테스트가 어렵고, TCP 테스트에서 통합 검증.

중간 검증

cd python && python -c "from toki_socket.base_client import BaseClient; print('OK')"

예상: OK


[API-4] TCP Transport 구현

문제

Go의 TcpClient / TcpServer에 해당하는 asyncio TCP 구현이 없다.

해결 방법

python/toki_socket/tcp_client.py, python/toki_socket/tcp_server.py 구현.

framing (PROTOCOL.md 기준):

  • 송신: 4바이트 big-endian 길이 + PacketBase bytes
  • 수신: 4바이트 읽기 → 길이 → 나머지 읽기
  • 최대 패킷 크기: 64 MiB (Go와 동일)
# tcp_client.py
class TcpClient(BaseClient):
    def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
                 interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
        super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
        self._reader = reader
        self._writer = writer
        self._init_base()
        self.communicator.initialize(self, parser_map)
        self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
        self.communicator.add_listener(
            type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
        )
        asyncio.create_task(self._read_loop())
        asyncio.create_task(self._send_heartbeat())

    async def write_packet(self, base: PacketBase) -> None:
        data = base.SerializeToString()
        header = struct.pack(">I", len(data))
        self._writer.write(header + data)
        await self._writer.drain()

    async def _do_close_impl(self) -> None:
        self._writer.close()
        await self._writer.wait_closed()

    async def _read_loop(self) -> None:
        while self.communicator.is_alive():
            try:
                header = await self._reader.readexactly(4)
                length = struct.unpack(">I", header)[0]
                if length == 0:
                    continue
                if length > MAX_PACKET_SIZE:
                    await self._on_disconnected(); return
                data = await self._reader.readexactly(length)
                base = PacketBase()
                base.ParseFromString(data)
                self.communicator.on_received_data(
                    base.typeName, base.data, base.nonce, base.responseNonce
                )
                asyncio.create_task(self._send_heartbeat())
            except (asyncio.IncompleteReadError, ConnectionError, OSError):
                await self._on_disconnected(); return

async def connect_tcp(host: str, port: int, interval_sec: int, wait_sec: int,
                      parser_map: ParserMap) -> TcpClient:
    reader, writer = await asyncio.open_connection(host, port)
    return TcpClient(reader, writer, interval_sec, wait_sec, parser_map)
# tcp_server.py
class TcpServer:
    def __init__(self, host: str, port: int, new_client: Callable) -> None:
        self._host = host
        self._port = port
        self._new_client = new_client
        self._clients: list[TcpClient] = []
        self._server: asyncio.Server | None = None
        self.on_client_connected: Callable[[TcpClient], None] = lambda c: None

    async def start(self) -> None: ...
    async def stop(self) -> None: ...
    def clients(self) -> list[TcpClient]: ...
    async def broadcast(self, message) -> None: ...

수정 파일 및 체크리스트

  • python/toki_socket/tcp_client.py 생성
    • MAX_PACKET_SIZE = 64 << 20
    • TcpClient(BaseClient) — reader/writer, read_loop, write_packet, do_close
    • connect_tcp(host, port, interval_sec, wait_sec, parser_map) async 함수
  • python/toki_socket/tcp_server.py 생성
    • TcpServer — start/stop/clients/broadcast, on_client_connected 콜백
    • _handle_connection() — TcpClient 생성, disconnect_listener로 clients 목록 관리

테스트 작성

python/test/test_tcp.py:

  • test_tcp_send_receive — 루프백 TCP로 양방향 TestData 교환
  • test_tcp_request_response — send_request / add_request_listener 검증
@pytest.mark.asyncio
async def test_tcp_send_receive():
    received = asyncio.Future()
    server = TcpServer("127.0.0.1", 0, lambda r, w: TcpClient(r, w, 0, 0, parser_map()))
    server.on_client_connected = lambda c: c.communicator.add_listener(
        "TestData", lambda m: received.set_result(m)
    )
    await server.start()
    port = server.port  # 동적 포트
    client = await connect_tcp("127.0.0.1", port, 0, 0, parser_map())
    await client.communicator.send(TestData(index=1, message="hello"))
    result = await asyncio.wait_for(received, 2.0)
    assert result.index == 1 and result.message == "hello"
    await client.close()
    await server.stop()

중간 검증

cd python && python -m pytest test/test_tcp.py -v

예상: 2개 테스트 PASS


[API-5] WebSocket Transport 구현

문제

Go의 WsClient / WsServer에 해당하는 asyncio WebSocket 구현이 없다.

해결 방법

websockets 라이브러리 사용. WebSocket은 메시지 경계가 보장되므로 길이 헤더 없이 바이너리 프레임으로 전송.

# ws_client.py
import websockets

class WsClient(BaseClient):
    def __init__(self, ws: websockets.WebSocketClientProtocol | websockets.WebSocketServerProtocol,
                 interval_sec: int, wait_sec: int, parser_map: ParserMap) -> None:
        super().__init__(interval_sec, wait_sec, do_close=self._do_close_impl)
        self._ws = ws
        self._init_base()
        self.communicator.initialize(self, parser_map)
        self.communicator.set_write_error_handler(lambda e: asyncio.create_task(self._on_disconnected()))
        self.communicator.add_listener(
            type_name_of(HeartBeat), lambda m: asyncio.create_task(self._on_heartbeat())
        )
        asyncio.create_task(self._read_loop())
        asyncio.create_task(self._send_heartbeat())

    async def write_packet(self, base: PacketBase) -> None:
        await self._ws.send(base.SerializeToString())

    async def _do_close_impl(self) -> None:
        await self._ws.close()

    async def _read_loop(self) -> None:
        try:
            async for message in self._ws:
                if not isinstance(message, bytes):
                    continue
                base = PacketBase()
                base.ParseFromString(message)
                self.communicator.on_received_data(
                    base.typeName, base.data, base.nonce, base.responseNonce
                )
                asyncio.create_task(self._send_heartbeat())
        except Exception:
            pass
        finally:
            await self._on_disconnected()

async def connect_ws(host: str, port: int, path: str,
                     interval_sec: int, wait_sec: int, parser_map: ParserMap) -> WsClient:
    uri = f"ws://{host}:{port}{path}"
    ws = await websockets.connect(uri)
    return WsClient(ws, interval_sec, wait_sec, parser_map)
# ws_server.py
class WsServer:
    def __init__(self, host: str, port: int, path: str, new_client: Callable) -> None: ...
    async def start(self) -> None: ...
    async def stop(self) -> None: ...
    def clients(self) -> list[WsClient]: ...
    async def broadcast(self, message) -> None: ...

수정 파일 및 체크리스트

  • python/toki_socket/ws_client.py 생성
    • WsClient(BaseClient) — websocket conn, read_loop, write_packet, do_close
    • connect_ws(host, port, path, interval_sec, wait_sec, parser_map) async 함수
  • python/toki_socket/ws_server.py 생성
    • WsServer — start/stop/clients/broadcast, path 기반 핸들러
    • _handler(ws, path) — WsClient 생성, disconnect_listener로 목록 관리

테스트 작성

skip — TCP 테스트로 핵심 로직이 검증되며, WS는 크로스 테스트에서 검증.

중간 검증

cd python && python -c "from toki_socket.ws_client import WsClient; from toki_socket.ws_server import WsServer; print('OK')"

예상: OK


[API-6] 단위 테스트

API-2, API-4 항목에서 이미 기술. 해당 항목 체크리스트 참조.


[API-7] Python×Go 크로스 테스트

문제

Python이 Go 서버와 통신할 수 있는지, Go가 Python 서버와 통신할 수 있는지 검증이 없다.

해결 방법

포트 할당:

  • Go 서버 / Python 클라이언트: TCP 29390, WS 29392
  • Python 서버 / Go 클라이언트: TCP 29490, WS 29492

Python 클라이언트 헬퍼 python/crosstest/go_python_client.py:

  • --mode tcp|ws, --port N, --phase send-push|requests 플래그
  • INFO typeName python=TestData 출력
  • 각 시나리오 결과를 PASS scenario=N detail=... / FAIL scenario=N error=... 출력
  • send-push: 시나리오 1(fire-and-forget), 2(server push 수신)
  • requests: 시나리오 3(단일 요청), 4(동시 5개 요청 nonce 검증)
  • Go crosstest 클라이언트(go/crosstest/dart_go_client/main.go)와 동일한 구조

Go 오케스트레이터 go/crosstest/go_python.go:

  • Go 서버를 시작하고 python go_python_client.py 서브프로세스를 실행
  • 기존 go_dart.go 패턴과 동일 (//go:build ignore, runPythonClient() 헬퍼)
  • Python 패키지 디렉터리를 소스 위치 기반으로 찾는 pythonPackageDir() 함수

Python 오케스트레이터 python/crosstest/python_go.py:

  • Python 서버를 시작하고 go run ./crosstest/python_go_client 서브프로세스를 실행
  • go/crosstest/dart_go_client/main.go 패턴을 참고하여 --mode, --port, --phase 플래그 지원
  • Go 패키지 디렉터리를 소스 위치 기반으로 찾는 헬퍼

Go 클라이언트 헬퍼 go/crosstest/python_go_client/main.go:

  • 기존 dart_go_client/main.go와 동일한 구조
  • Python 서버 포트(29490 TCP, 29492 WS)에 연결
  • --phase send-push: 시나리오 1, 2
  • --phase requests: 시나리오 3, 4
  • push 메시지 검증: index=200 && message=="push from python server"

시나리오별 메시지 규격:

시나리오 클라이언트 송신 서버 검증 / 응답
1 (send) index=101, message="fire from python client" Go서버: index==101, message 검증
2 (push) Go서버 → index=200, message="push from go server"
3 (request) index=21, message="single request from python" Go서버 → index=42, message="echo: single request from python"
4 (concurrent) index=30+i, message="multi request {i} from python" Go서버 → index=(30+i)*2, message="echo: ..."

Python서버일 때는 go클라이언트 메시지 검증:

  • 1: index=101, message="fire from go client"
  • 2: Python서버 → index=200, message="push from python server"

수정 파일 및 체크리스트

  • python/crosstest/__init__.py 생성 (빈 파일)
  • python/crosstest/go_python_client.py 생성
    • argparse로 --mode, --port, --phase 파싱
    • INFO typeName python=TestData 출력
    • dial_with_retry(mode, port) — 3초 내 재시도
    • run_send_push(client) — 시나리오 1, 2
    • run_requests(client) — 시나리오 3, 4 (asyncio.gather로 동시 요청)
    • 비동기 main → asyncio.run(main())
  • go/crosstest/go_python.go 생성
    • //go:build ignore 태그
    • goPythonTCPPort = 29390, goPythonWSPort = 29392
    • runTCPSendPush/TCPRequests/WSSendPush/WSRequests() 4개 함수
    • runPythonClient(mode, port, phase, expected)python go_python_client.py 실행
    • pythonPackageDir() — 소스 위치 기반 탐색
  • python/crosstest/python_go.py 생성
    • pythonGoTCPPort = 29490, pythonGoWSPort = 29492
    • run_tcp_send_push/tcp_requests/ws_send_push/ws_requests() 4개 함수
    • run_go_client(mode, port, phase, expected)go run ./crosstest/python_go_client 실행
    • go_package_dir() — 소스 위치 기반 탐색
    • 비동기 main → asyncio.run(main())
  • go/crosstest/python_go_client/main.go 생성
    • dart_go_client/main.go와 동일 구조
    • Python서버 포트(--port N)에 연결
    • push 메시지: "push from python server"

테스트 작성

크로스 테스트가 곧 테스트. 별도 단위 테스트 skip.

중간 검증

Go 서버 / Python 클라이언트:

cd go && go run ./crosstest/go_python.go

Python 서버 / Go 클라이언트:

cd python && python crosstest/python_go.py

예상: 각각 PASS all ... 출력


수정 파일 요약

파일 항목
python/pyproject.toml API-1
python/toki_socket/__init__.py API-1
python/toki_socket/packets/__init__.py API-1
python/toki_socket/packets/message_common.proto API-1
python/toki_socket/packets/message_common_pb2.py API-1
python/toki_socket/packets/message_common_pb2.pyi API-1
python/README.md API-1
python/toki_socket/communicator.py API-2
python/test/__init__.py API-2
python/test/test_communicator.py API-2
python/toki_socket/base_client.py API-3
python/toki_socket/tcp_client.py API-4
python/toki_socket/tcp_server.py API-4
python/test/test_tcp.py API-4
python/toki_socket/ws_client.py API-5
python/toki_socket/ws_server.py API-5
python/crosstest/__init__.py API-7
python/crosstest/go_python_client.py API-7
python/crosstest/python_go.py API-7
go/crosstest/go_python.go API-7
go/crosstest/python_go_client/main.go API-7

최종 검증

# 1. Python 단위 테스트
cd python && pip install -e ".[dev]" && python -m pytest test/ -v

# 2. Go 서버 / Python 클라이언트 크로스 테스트
cd go && go run ./crosstest/go_python.go

# 3. Python 서버 / Go 클라이언트 크로스 테스트
cd python && python crosstest/python_go.py

# 4. 기존 Go 크로스 테스트 회귀 확인
cd go && go run ./crosstest/go_dart.go
cd go && go run ./crosstest/go_kotlin.go

예상 결과:

  1. pytest: 모든 테스트 PASS
  2. go run ./crosstest/go_python.go: PASS all go-server/python-client crosstests passed
  3. python crosstest/python_go.py: PASS all python-server/go-client crosstests passed
  4. 기존 크로스 테스트: 회귀 없음 (PASS)