MCP Terminal Server
by dillip285
- go
- tests
// Copyright 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"gopkg.in/yaml.v2"
)
type testFile struct {
App string
Tests []test
}
type test struct {
Path string
Post any
Body any
}
const hostPort = "http://localhost:3100"
func TestReflectionAPI(t *testing.T) {
filenames, err := filepath.Glob(filepath.FromSlash("../../tests/specs/reflection_api.yaml"))
if err != nil {
t.Fatal(err)
}
for _, fn := range filenames {
if filepath.Base(fn) == "pnpm-lock.yaml" {
continue
}
data, err := os.ReadFile(fn)
if err != nil {
t.Fatal(err)
}
var file testFile
if err := yaml.Unmarshal(data, &file); err != nil {
t.Fatal(err)
}
t.Run(file.App, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
wait, err := startGenkitApp(ctx, file.App)
if err != nil {
t.Fatal(err)
}
t.Log("started app")
defer func() {
cancel()
err := wait()
t.Logf("wait returned %v", err)
}()
for {
time.Sleep(50 * time.Millisecond)
t.Log("checking...")
if _, err := http.Get(hostPort + "/api/__health"); err == nil {
break
}
}
t.Log("app ready")
for _, test := range file.Tests {
runTest(t, test)
}
})
}
}
func runTest(t *testing.T, test test) {
t.Run(test.Path[1:], func(t *testing.T) {
if t.Name() == "TestReflectionAPI/test_app/api/actions" {
t.Skip("FIXME: skipping because Go and JS schemas are not aligned")
}
url := hostPort + test.Path
var (
res *http.Response
err error
)
if test.Post != nil {
body, err := json.Marshal(yamlToJSON(test.Post))
if err != nil {
t.Fatal(err)
}
res, err = http.Post(url, "application/json", bytes.NewReader(body))
} else {
res, err = http.Get(url)
}
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("got status %s", res.Status)
}
var got any
dec := json.NewDecoder(res.Body)
dec.UseNumber()
if err := dec.Decode(&got); err != nil {
t.Fatal(err)
}
want := yamlToJSON(test.Body)
msgs := compare(got, want)
if len(msgs) > 0 {
t.Logf("%s", prettyJSON(got))
t.Fatal(strings.Join(msgs, "\n"))
}
})
}
func startGenkitApp(ctx context.Context, dir string) (func() error, error) {
// If we invoke `go run`, the actual server is a child of the go command, and not
// easy to kill. So instead we compile the app and run it directly.
tmp := os.TempDir()
cmd := exec.Command("go", "build", "-o", tmp, ".")
cmd.Dir = dir
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("build: %w", err)
}
// `go build .` will use the working directory as the name of the executable.
cmd = exec.CommandContext(ctx, "./"+dir)
cmd.Dir = tmp
cmd.Env = append(os.Environ(), "GENKIT_ENV=dev")
cmd.WaitDelay = time.Second
if err := cmd.Start(); err != nil {
return nil, err
}
return cmd.Wait, nil
}
func compare(got, want any) []string {
var msgs []string
compare1(nil, got, want, func(path []string, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
prefix := strings.Join(path, ".")
if prefix == "" {
prefix = "top level"
}
msgs = append(msgs, fmt.Sprintf("%s: %s", prefix, msg))
})
return msgs
}
func compare1(path []string, got, want any, add func([]string, string, ...any)) {
check := func() {
if got != want {
add(path, "\ngot %v (%[1]T)\nwant %v (%[2]T)", got, want)
}
}
switch w := want.(type) {
case map[string]any:
g, ok := got.(map[string]any)
if !ok {
add(path, "want map, got %T", got)
return
}
for k, wv := range w {
gv, ok := g[k]
if !ok {
add(path, "missing key %q", k)
} else {
compare1(append(path, k), gv, wv, add)
}
}
case int:
if n, ok := got.(json.Number); ok {
g, err := n.Int64()
if err != nil {
add(path, "got number %s, want %d (%[2]T)", n, w)
}
if g != int64(w) {
add(path, "got %d, want %d", g, w)
}
} else {
check()
}
case float64:
if n, ok := got.(json.Number); ok {
g, err := n.Float64()
if err != nil {
add(path, "got number %s, want %f (%[2]T)", n, w)
}
if g != w {
add(path, "got %f, want %f", g, w)
}
} else {
check()
}
case []any:
g, ok := got.([]any)
if !ok {
add(path, "want slice, got %T", got)
return
}
if len(g) != len(w) {
add(path, "got slice length %d, want %d", len(g), len(w))
return
}
for i, ew := range w {
compare1(append(path, strconv.Itoa(i)), g[i], ew, add)
}
default:
check()
}
}
// The yaml package unmarshals using different types than
// the json package. Convert the yaml form to the JSON form.
func yamlToJSON(y any) any {
switch y := y.(type) {
case map[any]any:
j := map[string]any{}
for k, v := range y {
j[fmt.Sprint(k)] = yamlToJSON(v)
}
return j
case []any:
j := make([]any, len(y))
for i, e := range y {
j[i] = yamlToJSON(e)
}
return j
default:
return y
}
}
func prettyJSON(x any) string {
var sb strings.Builder
enc := json.NewEncoder(&sb)
enc.SetIndent("", " ")
if err := enc.Encode(x); err != nil {
panic(err)
}
return sb.String()
}