proto-socket/go/crosstest/go_dart.go
toki 21eed0e2ab fullname 테스트 지원: 모든 언어 간 crosstest에 FullName 필드 추가
- Dart, Go, Kotlin, Python, TypeScript 전 언어 간 crosstest에 FullName 테스트 추가
- crosstest 파일 40개 이상에 FullName 필드 지원 코드 추가
- 테스트 매트릭스 스크립트 및 마일스톤 문서 업데이트
2026-06-16 21:56:48 +09:00

560 lines
16 KiB
Go

//go:build ignore
package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sync"
"time"
"google.golang.org/protobuf/proto"
"nhooyr.io/websocket"
toki "git.toki-labs.com/toki/proto-socket/go"
"git.toki-labs.com/toki/proto-socket/go/packets"
)
const (
host = "127.0.0.1"
goDartTCPPort = 29190
goDartWSPort = 29192
goDartTLSPort = 29194
goDartWSSPort = 29196
wsPath = "/"
processTimeout = 20 * time.Second
serverObservationWindow = 200 * time.Millisecond
)
func parserMap() toki.ParserMap {
return toki.ParserMap{
toki.TypeNameOf(&packets.TestData{}): func(b []byte) (proto.Message, error) {
m := &packets.TestData{}
return m, proto.Unmarshal(b, m)
},
}
}
func main() {
fmt.Printf("INFO typeName go=%s\n", toki.TypeNameOf(&packets.TestData{}))
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "FAIL crosstest error=%v\n", err)
os.Exit(1)
}
fmt.Println("PASS all go-server/dart-client crosstests passed")
}
func run() error {
selectedTransport := os.Getenv("PROTO_SOCKET_CROSSTEST_TRANSPORT")
if selectedTransport == "" || selectedTransport == "tcp" {
if err := runTCPSendPush(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTCPRequests(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTCPLegacySimpleReceive(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
}
if selectedTransport == "" || selectedTransport == "ws" {
if err := runWSSendPush(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runWSRequests(); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
}
dartDir, err := dartPackageDir()
if err != nil {
return fmt.Errorf("cannot find dart package dir: %w", err)
}
certFile := filepath.Join(dartDir, "test", "certs", "server.crt")
keyFile := filepath.Join(dartDir, "test", "certs", "server.key")
serverTLS, err := loadServerTLS(certFile, keyFile)
if err != nil {
return fmt.Errorf("load TLS certs: %w", err)
}
if selectedTransport == "" || selectedTransport == "tls" {
if err := runTLSTCPSendPush(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
if err := runTLSTCPRequests(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
}
if selectedTransport == "" || selectedTransport == "wss" {
if err := runWSSendPushSecure(serverTLS, certFile); err != nil {
return err
}
time.Sleep(150 * time.Millisecond)
return runWSSRequestsSecure(serverTLS, certFile)
}
return nil
}
func loadServerTLS(certFile, keyFile string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
}
func runTCPSendPush() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServer(host, goDartTCPPort, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runDartClient("tcp", goDartTCPPort, "send-push", map[string]bool{"1": true, "2": true}); err != nil {
return err
}
// The subprocess only proves the client observed PASS lines. This short
// wait also confirms the server-side listener recorded the expected data.
select {
case ok := <-received:
if !ok {
return errors.New("TCP send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TCP send-push server did not receive expected data")
}
return nil
}
func runTCPRequests() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewTcpServer(host, goDartTCPPort, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runDartClient("tcp", goDartTCPPort, "requests", map[string]bool{"3": true, "4": true})
}
func runWSSendPush() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewWsServer(host, goDartWSPort, wsPath, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runDartClient("ws", goDartWSPort, "send-push", map[string]bool{"1": true, "2": true}); err != nil {
return err
}
// The subprocess only proves the client observed PASS lines. This short
// wait also confirms the server-side listener recorded the expected data.
select {
case ok := <-received:
if !ok {
return errors.New("WS send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("WS send-push server did not receive expected data")
}
return nil
}
func runWSRequests() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewWsServer(host, goDartWSPort, wsPath, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runDartClient("ws", goDartWSPort, "requests", map[string]bool{"3": true, "4": true})
}
func runTLSTCPSendPush(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServerTLS(host, goDartTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runDartClient("tls", goDartTLSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("TLS TCP send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TLS TCP send-push server did not receive expected data")
}
return nil
}
func runTLSTCPRequests(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewTcpServerTLS(host, goDartTLSPort, serverTLS, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runDartClient("tls", goDartTLSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runWSSendPushSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewWsServerTLS(host, goDartWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
fmt.Printf("SERVER_RECEIVED index=%d message=%s\n", data.GetIndex(), data.GetMessage())
valid := data.GetIndex() == 101 && data.GetMessage() == "fire from dart client"
select {
case received <- valid:
default:
}
if valid {
_ = client.Send(&packets.TestData{Index: 200, Message: "push from go server"})
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runDartClient("wss", goDartWSSPort, "send-push", map[string]bool{"1": true, "2": true}, certFile); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("WSS send-push server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("WSS send-push server did not receive expected data")
}
return nil
}
func runTCPLegacySimpleReceive() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
received := make(chan bool, 1)
server := toki.NewTcpServer(host, goDartTCPPort, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.TcpClient) {
toki.AddListenerTyped[*packets.TestData](&client.Communicator, func(data *packets.TestData) {
select {
case received <- data.GetIndex() == 101:
default:
}
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
if err := runDartClient("tcp", goDartTCPPort, "legacy-receive", map[string]bool{}); err != nil {
return err
}
select {
case ok := <-received:
if !ok {
return errors.New("TCP legacy simple-name server received unexpected data")
}
case <-time.After(serverObservationWindow):
return errors.New("TCP legacy simple-name server did not receive packet")
}
fmt.Println("PASS scenario=tcp-legacy-simple-receive detail=server-received-legacy-alias")
return nil
}
func runWSSRequestsSecure(serverTLS *tls.Config, certFile string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := toki.NewWsServerTLS(host, goDartWSSPort, wsPath, serverTLS, func(conn *websocket.Conn) *toki.WsClient {
return toki.NewWsClient(conn, 0, 0, parserMap())
})
server.OnClientConnected = func(client *toki.WsClient) {
toki.AddRequestListenerTyped[*packets.TestData, *packets.TestData](&client.Communicator, func(req *packets.TestData) (*packets.TestData, error) {
return &packets.TestData{
Index: req.GetIndex() * 2,
Message: "echo: " + req.GetMessage(),
}, nil
})
}
if err := server.Start(ctx); err != nil {
return err
}
defer server.Stop()
return runDartClient("wss", goDartWSSPort, "requests", map[string]bool{"3": true, "4": true}, certFile)
}
func runDartClient(mode string, port int, phase string, expected map[string]bool, certFile ...string) error {
ctx, cancel := context.WithTimeout(context.Background(), processTimeout)
defer cancel()
dartDir, err := dartPackageDir()
if err != nil {
return err
}
args := []string{
"run", "crosstest/go_dart_client.dart",
"--mode=" + mode,
fmt.Sprintf("--port=%d", port),
"--phase=" + phase,
}
if len(certFile) > 0 && certFile[0] != "" {
args = append(args, "--cert="+certFile[0])
}
cmd := exec.CommandContext(ctx, "dart", args...)
cmd.Dir = dartDir
cmd.WaitDelay = 5 * time.Second
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
var mu sync.Mutex
var resultLines []string
var wg sync.WaitGroup
wg.Add(2)
go scanLines(&wg, stdoutPipe, os.Stdout, &mu, &resultLines)
go scanLines(&wg, stderrPipe, os.Stderr, nil, nil)
waitErr := cmd.Wait()
done := make(chan struct{})
go func() { wg.Wait(); close(done) }()
select {
case <-done:
case <-time.After(5 * time.Second):
return fmt.Errorf("dart client %s/%s pipe scan timed out", mode, phase)
}
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("dart client %s/%s timed out", mode, phase)
}
if err := validateResultLines("dart-client "+mode+"/"+phase, waitErr, resultLines, expected); err != nil {
return err
}
return nil
}
func dartPackageDir() (string, error) {
candidates := make([]string, 0, 3)
_, filename, _, ok := runtime.Caller(0)
if ok {
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename)))
candidates = append(candidates, filepath.Join(repoRoot, "dart"))
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, findDartPackageCandidates(wd)...)
}
if executable, err := os.Executable(); err == nil {
candidates = append(candidates, findDartPackageCandidates(filepath.Dir(executable))...)
}
for _, candidate := range candidates {
if isDartPackageDir(candidate) {
return candidate, nil
}
}
return "", fmt.Errorf("cannot resolve dart package directory from candidates %v", candidates)
}
func findDartPackageCandidates(start string) []string {
candidates := make([]string, 0)
for dir := start; ; dir = filepath.Dir(dir) {
candidates = append(candidates, filepath.Join(dir, "dart"))
if filepath.Base(dir) == "dart" {
candidates = append(candidates, dir)
}
parent := filepath.Dir(dir)
if parent == dir {
return candidates
}
}
}
func isDartPackageDir(dir string) bool {
info, err := os.Stat(filepath.Join(dir, "pubspec.yaml"))
return err == nil && !info.IsDir()
}
func scanLines(wg *sync.WaitGroup, reader io.Reader, writer *os.File, mu *sync.Mutex, resultLines *[]string) {
defer wg.Done()
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
fmt.Fprintln(writer, line)
if mu != nil && (startsWithResult(line)) {
mu.Lock()
*resultLines = append(*resultLines, line)
mu.Unlock()
}
}
}
func validateResultLines(label string, waitErr error, lines []string, expected map[string]bool) error {
failed := make([]string, 0)
passed := make(map[string]bool)
re := regexp.MustCompile(`scenario=([^ ]+)`)
for _, line := range lines {
if len(line) >= 5 && line[:5] == "FAIL " {
failed = append(failed, line)
continue
}
if len(line) >= 5 && line[:5] == "PASS " {
match := re.FindStringSubmatch(line)
if len(match) == 2 {
passed[match[1]] = true
}
}
}
missing := make([]string, 0)
for scenario := range expected {
if !passed[scenario] {
missing = append(missing, scenario)
}
}
if waitErr != nil || len(failed) > 0 || len(missing) > 0 {
return fmt.Errorf("%s failed waitErr=%v failed=%v missing=%v", label, waitErr, failed, missing)
}
return nil
}
func startsWithResult(line string) bool {
return (len(line) >= 5 && line[:5] == "PASS ") || (len(line) >= 5 && line[:5] == "FAIL ")
}