Skip to content

Commit 825cfd6

Browse files
Backfills tests for guest panics (#73)
This backfills tests for guests that panic. Sadly, we don't control the ModuleConfig. Integrators who want to see the data written by TinyGo during a panic need to capture stdout on their own. This is already the case in dapr, which sends stdout to the logger under debug. Signed-off-by: Adrian Cole <[email protected]>
1 parent 0368c00 commit 825cfd6

11 files changed

+199
-17
lines changed

handler/middleware_test.go

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
_ "embed"
66
"reflect"
7-
"strings"
87
"testing"
98

109
"github.com/http-wasm/http-wasm-host-go/api/handler"
@@ -13,16 +12,89 @@ import (
1312

1413
var testCtx = context.Background()
1514

16-
func TestMiddlewareAfterNextErrors(t *testing.T) {
15+
func TestNewMiddleware(t *testing.T) {
1716
tests := []struct {
1817
name string
1918
guest []byte
2019
expectedError string
2120
}{
2221
{
23-
name: "set_header_value request",
24-
guest: test.BinInvalidSetRequestHeaderAfterNext,
25-
expectedError: "can't set request header after next handler",
22+
name: "ok",
23+
guest: test.BinE2EProtocolVersion,
24+
},
25+
{
26+
name: "panic on _start",
27+
guest: test.BinErrorPanicOnStart,
28+
expectedError: `wasm: error instantiating guest: module[1] function[_start] failed: wasm error: unreachable
29+
wasm stack trace:
30+
panic_on_start.main()`,
31+
},
32+
}
33+
34+
for _, tt := range tests {
35+
tc := tt
36+
t.Run(tc.name, func(t *testing.T) {
37+
t.Parallel()
38+
39+
mw, err := NewMiddleware(testCtx, tc.guest, handler.UnimplementedHost{})
40+
requireEqualError(t, err, tc.expectedError)
41+
if mw != nil {
42+
mw.Close(testCtx)
43+
}
44+
})
45+
}
46+
}
47+
48+
func TestMiddlewareHandleRequest_Error(t *testing.T) {
49+
tests := []struct {
50+
name string
51+
guest []byte
52+
expectedError string
53+
}{
54+
{
55+
name: "panic",
56+
guest: test.BinErrorPanicOnHandleRequest,
57+
expectedError: `wasm error: unreachable
58+
wasm stack trace:
59+
panic_on_handle_request.handle_request() i64`,
60+
},
61+
}
62+
63+
for _, tt := range tests {
64+
tc := tt
65+
t.Run(tc.name, func(t *testing.T) {
66+
mw, err := NewMiddleware(testCtx, tc.guest, handler.UnimplementedHost{})
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
defer mw.Close(testCtx)
71+
72+
_, _, err = mw.HandleRequest(testCtx)
73+
requireEqualError(t, err, tc.expectedError)
74+
})
75+
}
76+
}
77+
78+
func TestMiddlewareHandleResponse_Error(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
guest []byte
82+
expectedError string
83+
}{
84+
{
85+
name: "panic",
86+
guest: test.BinErrorPanicOnHandleResponse,
87+
expectedError: `wasm error: unreachable
88+
wasm stack trace:
89+
panic_on_handle_response.handle_response(i32,i32)`,
90+
},
91+
{
92+
name: "set_header_value request",
93+
guest: test.BinErrorSetRequestHeaderAfterNext,
94+
expectedError: `can't set request header after next handler (recovered by wazero)
95+
wasm stack trace:
96+
http_handler.set_header_value(i32,i32,i32,i32,i32)
97+
set_request_header_after_next.handle_response(i32,i32)`,
2698
},
2799
}
28100

@@ -41,18 +113,11 @@ func TestMiddlewareAfterNextErrors(t *testing.T) {
41113

42114
// We do expect an error on the response path
43115
err = mw.HandleResponse(ctx, 0, nil)
44-
requireErrorPrefix(t, err, tc.expectedError)
116+
requireEqualError(t, err, tc.expectedError)
45117
})
46118
}
47119
}
48120

49-
func requireErrorPrefix(t *testing.T, err error, want string) {
50-
t.Helper()
51-
if have := err.Error(); !strings.HasPrefix(have, want) {
52-
t.Errorf("unexpected error message prefix, want: %s, have: %s", want, have)
53-
}
54-
}
55-
56121
func TestMiddlewareResponseUsesRequestModule(t *testing.T) {
57122
mw, err := NewMiddleware(testCtx, test.BinE2EHandleResponse, handler.UnimplementedHost{})
58123
if err != nil {
@@ -132,3 +197,13 @@ func requireHandleRequest(t *testing.T, mw Middleware, ctxNext handler.CtxNext,
132197
t.Error("expected handler to not return guest to the pool")
133198
}
134199
}
200+
201+
func requireEqualError(t *testing.T, err error, expectedError string) {
202+
if err != nil {
203+
if want, have := expectedError, err.Error(); want != have {
204+
t.Fatalf("unexpected error: want %v, have %v", want, have)
205+
}
206+
} else if want := expectedError; want != "" {
207+
t.Fatalf("expected error %v", want)
208+
}
209+
}

internal/test/testdata.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,17 @@ var BinE2EHandleResponse []byte
8686
//go:embed testdata/e2e/header_names.wasm
8787
var BinE2EHeaderNames []byte
8888

89-
//go:embed testdata/invalid/set_request_header_after_next.wasm
90-
var BinInvalidSetRequestHeaderAfterNext []byte
89+
//go:embed testdata/error/panic_on_handle_request.wasm
90+
var BinErrorPanicOnHandleRequest []byte
91+
92+
//go:embed testdata/error/panic_on_handle_response.wasm
93+
var BinErrorPanicOnHandleResponse []byte
94+
95+
//go:embed testdata/error/panic_on_start.wasm
96+
var BinErrorPanicOnStart []byte
97+
98+
//go:embed testdata/error/set_request_header_after_next.wasm
99+
var BinErrorSetRequestHeaderAfterNext []byte
91100

92101
// binExample instead of go:embed as files aren't relative to this directory.
93102
func binExample(name string) []byte {
315 Bytes
Binary file not shown.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
;; panic_on_handle_request issues an unreachable instruction after writing
2+
;; an error to stdout. This simulates a panic in TinyGo.
3+
(module $panic_on_handle_request
4+
;; Import the fd_write function from wasi, used in TinyGo for println.
5+
(import "wasi_snapshot_preview1" "fd_write"
6+
(func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))
7+
8+
;; Allocate the minimum amount of memory, 1 page (64KB).
9+
(memory (export "memory") 1 1)
10+
11+
;; Pre-populate memory with the panic message, in iovec format
12+
(data (i32.const 0) "\08") ;; iovs[0].offset
13+
(data (i32.const 4) "\06") ;; iovs[0].length
14+
(data (i32.const 8) "panic!") ;; iovs[0]
15+
16+
;; On handle_request, write "panic!" to stdout and crash.
17+
(func $handle_request (export "handle_request") (result (; ctx_next ;) i64)
18+
;; Write the panic to stdout via its iovec [offset, len].
19+
(call $wasi.fd_write
20+
(i32.const 1) ;; stdout
21+
(i32.const 0) ;; where's the iovec
22+
(i32.const 1) ;; only one iovec
23+
(i32.const 0) ;; overwrite the iovec with the ignored result.
24+
)
25+
drop ;; ignore the errno returned
26+
27+
;; Issue the unreachable instruction instead of returning ctx_next
28+
(unreachable))
29+
30+
(func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32))
31+
)
319 Bytes
Binary file not shown.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
;; panic_on_handle_response issues an unreachable instruction after writing
2+
;; an error to stdout. This simulates a panic in TinyGo.
3+
(module $panic_on_handle_response
4+
;; Import the fd_write function from wasi, used in TinyGo for println.
5+
(import "wasi_snapshot_preview1" "fd_write"
6+
(func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))
7+
8+
;; Allocate the minimum amount of memory, 1 page (64KB).
9+
(memory (export "memory") 1 1)
10+
11+
;; Pre-populate memory with the panic message, in iovec format
12+
(data (i32.const 0) "\08") ;; iovs[0].offset
13+
(data (i32.const 4) "\06") ;; iovs[0].length
14+
(data (i32.const 8) "panic!") ;; iovs[0]
15+
16+
(func $handle_request (export "handle_request") (result (; ctx_next ;) i64)
17+
(return (i64.const 1))) ;; call the next handler
18+
19+
;; On handle_response, write "panic!" to stdout and crash.
20+
(func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32)
21+
;; Write the panic to stdout via its iovec [offset, len].
22+
(call $wasi.fd_write
23+
(i32.const 1) ;; stdout
24+
(i32.const 0) ;; where's the iovec
25+
(i32.const 1) ;; only one iovec
26+
(i32.const 0) ;; overwrite the iovec with the ignored result.
27+
)
28+
drop ;; ignore the errno returned
29+
30+
;; Issue the unreachable instruction instead of returning.
31+
(unreachable))
32+
)
333 Bytes
Binary file not shown.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
;; panic_on_start is a WASI command which issues an unreachable instruction
2+
;; after writing an error to stdout. This simulates a panic in TinyGo.
3+
(module $panic_on_start
4+
;; Import the fd_write function from wasi, used in TinyGo for println.
5+
(import "wasi_snapshot_preview1" "fd_write"
6+
(func $wasi.fd_write (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))
7+
8+
;; Allocate the minimum amount of memory, 1 page (64KB).
9+
(memory (export "memory") 1 1)
10+
11+
;; Pre-populate memory with the panic message, in iovec format
12+
(data (i32.const 0) "\08") ;; iovs[0].offset
13+
(data (i32.const 4) "\06") ;; iovs[0].length
14+
(data (i32.const 8) "panic!") ;; iovs[0]
15+
16+
;; On start, write "panic!" to stdout and crash.
17+
(func $main (export "_start")
18+
;; Write the panic to stdout via its iovec [offset, len].
19+
(call $wasi.fd_write
20+
(i32.const 1) ;; stdout
21+
(i32.const 0) ;; where's the iovec
22+
(i32.const 1) ;; only one iovec
23+
(i32.const 0) ;; overwrite the iovec with the ignored result.
24+
)
25+
drop ;; ignore the errno returned
26+
27+
;; Issue the unreachable instruction instead of exiting normally
28+
(unreachable))
29+
30+
;; Export the required functions for the handler ABI
31+
(func $handle_request (export "handle_request") (result (; ctx_next ;) i64)
32+
(return (i64.const 0))) ;; don't call the next handler
33+
34+
(func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32))
35+
)
401 Bytes
Binary file not shown.

internal/test/testdata/invalid/set_request_header_after_next.wat renamed to internal/test/testdata/error/set_request_header_after_next.wat

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
(global $value_len i32 (i32.const 10))
1616

1717
;; handle_request returns non-zero to proceed to the next handler.
18-
(func (export "handle_request") (result (; ctx_next ;) i64)
18+
(func $handle_request (export "handle_request") (result (; ctx_next ;) i64)
1919
;; call the next handler
2020
(return (i64.const 1)))
2121

2222
;; handle_response tries to set a request header even though it is too late.
23-
(func (export "handle_response") (param $reqCtx i32) (param $is_error i32)
23+
(func $handle_response (export "handle_response") (param $reqCtx i32) (param $is_error i32)
2424
(call $set_header_value
2525
(i32.const 0) ;; header_kind_request
2626
(global.get $name) (global.get $name_len)

0 commit comments

Comments
 (0)