runtime, testing: support Goexit, SkipNow, FailNow

Keep a pending Goexit separate from the active panic state so recovered
deferred panics do not cancel the original Goexit unwind. Goexit should
also ignore -panic=trap, since it is not a panic.

Wire testing.FailNow and SkipNow through runtime.Goexit, and run tests
and benchmarks in goroutines. This lets Goexit terminate the user
function without skipping test bookkeeping. Add Goexit/recover coverage
and enable crypto/ecdh in the Linux stdlib test list.
This commit is contained in:
Jake Bailey
2026-07-03 16:30:32 -07:00
committed by Ron Evans
parent fb1bf2e6bf
commit 16eefc59eb
15 changed files with 201 additions and 23 deletions
+1
View File
@@ -413,6 +413,7 @@ TEST_PACKAGES_LINUX := \
context \ context \
crypto/aes \ crypto/aes \
crypto/des \ crypto/des \
crypto/ecdh \
crypto/hmac \ crypto/hmac \
debug/dwarf \ debug/dwarf \
debug/plan9obj \ debug/plan9obj \
+7 -3
View File
@@ -116,8 +116,8 @@ func (b *builder) createLandingPad() {
func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value { func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value {
// Construct inline assembly equivalents of setjmp. // Construct inline assembly equivalents of setjmp.
// The assembly works as follows: // The assembly works as follows:
// * All registers (both callee-saved and caller saved) are clobbered // * Registers are either clobbered or, on 386, saved for longjmp to
// after the inline assembly returns. // restore if the ABI requires them to survive calls.
// * The assembly stores the address just past the end of the assembly // * The assembly stores the address just past the end of the assembly
// into the jump buffer. // into the jump buffer.
// * The return value (eax, rax, r0, etc) is set to zero in the inline // * The return value (eax, rax, r0, etc) is set to zero in the inline
@@ -130,8 +130,12 @@ func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value {
asmString = ` asmString = `
xorl %eax, %eax xorl %eax, %eax
movl $$1f, 4(%ebx) movl $$1f, 4(%ebx)
movl %ebx, 8(%ebx)
movl %esi, 12(%ebx)
movl %edi, 16(%ebx)
movl %ebp, 20(%ebx)
1:` 1:`
constraints = "={eax},{ebx},~{ebx},~{ecx},~{edx},~{esi},~{edi},~{ebp},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}" constraints = "={eax},{ebx},~{ecx},~{edx},~{xmm0},~{xmm1},~{xmm2},~{xmm3},~{xmm4},~{xmm5},~{xmm6},~{xmm7},~{fpsr},~{fpcr},~{flags},~{dirflag},~{memory}"
// This doesn't include the floating point stack because TinyGo uses // This doesn't include the floating point stack because TinyGo uses
// newer floating point instructions. // newer floating point instructions.
case "x86_64": case "x86_64":
+3 -3
View File
@@ -3,7 +3,7 @@ source_filename = "defer.go"
target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64"
target triple = "thumbv7m-unknown-unknown-eabi" target triple = "thumbv7m-unknown-unknown-eabi"
%runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr } %runtime.deferFrame = type { ptr, ptr, [0 x ptr], ptr, i8, %runtime._interface, ptr, i1 }
%runtime._interface = type { ptr, ptr } %runtime._interface = type { ptr, ptr }
; Function Attrs: nounwind ; Function Attrs: nounwind
@@ -111,9 +111,9 @@ rundefers.end3: ; preds = %rundefers.loophead6
; Function Attrs: nocallback nofree nosync nounwind willreturn ; Function Attrs: nocallback nofree nosync nounwind willreturn
declare ptr @llvm.stacksave.p0() #2 declare ptr @llvm.stacksave.p0() #2
declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(28), ptr, ptr) #1 declare void @runtime.setupDeferFrame(ptr dereferenceable_or_null(32), ptr, ptr) #1
declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(28), ptr) #1 declare void @runtime.destroyDeferFrame(ptr dereferenceable_or_null(32), ptr) #1
; Function Attrs: nounwind ; Function Attrs: nounwind
define internal void @"main.deferSimple$1"(ptr %context) unnamed_addr #0 { define internal void @"main.deferSimple$1"(ptr %context) unnamed_addr #0 {
+32
View File
@@ -473,6 +473,12 @@ func runTestWithConfig(name string, t *testing.T, options compileopts.Options, c
options.Directory = path options.Directory = path
pkgName = "." pkgName = "."
} }
isWebAssembly := strings.HasPrefix(options.Target, "wasi") ||
strings.HasPrefix(options.Target, "wasm") ||
strings.HasPrefix(options.GOARCH, "wasm")
if name == "testing.go" && isWebAssembly {
expectedOutputPath = TESTDATA + "/testing-wasm.txt"
}
config, err := builder.NewConfig(&options) config, err := builder.NewConfig(&options)
if err != nil { if err != nil {
@@ -948,6 +954,32 @@ func checkOutputData(t *testing.T, expectedOutput, actual []byte) {
} }
} }
func TestGoexitCrash(t *testing.T) {
t.Parallel()
options := optionsFromTarget("", sema)
config, err := builder.NewConfig(&options)
if err != nil {
t.Fatal(err)
}
output := &bytes.Buffer{}
_, err = buildAndRun("testdata/goexit.go", config, output, nil, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
cmd.Stdout = nil
cmd.Stderr = nil
data, err := cmd.CombinedOutput()
output.Write(data)
return err
})
if err == nil {
t.Fatal("program unexpectedly exited successfully")
}
const want = "panic: panic after Goexit"
if !strings.Contains(output.String(), want) {
t.Fatalf("output does not contain %q:\n%s", want, output.String())
}
}
func TestTest(t *testing.T) { func TestTest(t *testing.T) {
t.Parallel() t.Parallel()
+1 -1
View File
@@ -5,7 +5,7 @@ const GOARCH = "386"
// The bitness of the CPU (e.g. 8, 32, 64). // The bitness of the CPU (e.g. 8, 32, 64).
const TargetBits = 32 const TargetBits = 32
const deferExtraRegs = 0 const deferExtraRegs = 4
const callInstSize = 5 // "call someFunction" is 5 bytes const callInstSize = 5 // "call someFunction" is 5 bytes
+4
View File
@@ -43,6 +43,10 @@ tinygo_longjmp:
// Note: the code we jump to assumes eax is set to a non-zero value if we // Note: the code we jump to assumes eax is set to a non-zero value if we
// jump from here. // jump from here.
movl 4(%esp), %eax movl 4(%esp), %eax
movl 8(%eax), %ebx
movl 12(%eax), %esi
movl 16(%eax), %edi
movl 20(%eax), %ebp
movl 0(%eax), %esp // jumpSP movl 0(%eax), %esp // jumpSP
movl 4(%eax), %eax // jumpPC (stash in volatile register) movl 4(%eax), %eax // jumpPC (stash in volatile register)
jmpl *%eax jmpl *%eax
+11 -1
View File
@@ -41,6 +41,7 @@ type deferFrame struct {
Panicking panicState // not panicking, panicking, or in Goexit Panicking panicState // not panicking, panicking, or in Goexit
PanicValue interface{} // panic value, might be nil for panic(nil) for example PanicValue interface{} // panic value, might be nil for panic(nil) for example
DeferPtr unsafe.Pointer // head of the stack-allocated defer list DeferPtr unsafe.Pointer // head of the stack-allocated defer list
Goexit bool // whether Goexit is still pending after a recovered deferred panic
} }
type panicState uint8 type panicState uint8
@@ -57,7 +58,7 @@ func _panic(message interface{}) {
} }
func panicOrGoexit(message interface{}, panicking panicState) { func panicOrGoexit(message interface{}, panicking panicState) {
if panicStrategy() == tinygo.PanicStrategyTrap { if panicking != panicGoexit && panicStrategy() == tinygo.PanicStrategyTrap {
trap() trap()
} }
// Note: recover is not supported inside interrupts. // Note: recover is not supported inside interrupts.
@@ -65,6 +66,9 @@ func panicOrGoexit(message interface{}, panicking panicState) {
if supportsRecover() && !interrupt.In() { if supportsRecover() && !interrupt.In() {
frame := (*deferFrame)(task.Current().DeferFrame) frame := (*deferFrame)(task.Current().DeferFrame)
if frame != nil { if frame != nil {
if panicking == panicGoexit {
frame.Goexit = true
}
frame.PanicValue = message frame.PanicValue = message
frame.Panicking = panicking frame.Panicking = panicking
tinygo_longjmp(frame) tinygo_longjmp(frame)
@@ -138,6 +142,7 @@ func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) {
frame.JumpSP = jumpSP frame.JumpSP = jumpSP
frame.Panicking = panicFalse frame.Panicking = panicFalse
frame.DeferPtr = nil frame.DeferPtr = nil
frame.Goexit = false
currentTask.DeferFrame = unsafe.Pointer(frame) currentTask.DeferFrame = unsafe.Pointer(frame)
} }
@@ -154,6 +159,11 @@ func destroyDeferFrame(frame *deferFrame) {
// Re-raise the panic now. // Re-raise the panic now.
panicOrGoexit(frame.PanicValue, frame.Panicking) panicOrGoexit(frame.PanicValue, frame.Panicking)
} }
if frame.Goexit {
// A deferred function panicked during Goexit, and that panic was
// recovered. Continue the original Goexit instead of returning.
panicOrGoexit(nil, panicGoexit)
}
} }
// _recover is the built-in recover() function. It tries to recover a currently // _recover is the built-in recover() function. It tries to recover a currently
+12 -3
View File
@@ -179,7 +179,12 @@ func (b *B) run1() bool {
ctx.maxLen = n + 8 // Add additional slack to avoid too many jumps in size. ctx.maxLen = n + 8 // Add additional slack to avoid too many jumps in size.
} }
} }
b.runN(1) done := make(chan struct{})
go func() {
defer close(done)
b.runN(1)
}()
<-done
return !b.hasSub return !b.hasSub
} }
@@ -195,8 +200,12 @@ func (b *B) run() {
} }
func (b *B) doBench() BenchmarkResult { func (b *B) doBench() BenchmarkResult {
// in upstream, this uses a goroutine done := make(chan struct{})
b.launch() go func() {
defer close(done)
b.launch()
}()
<-done
return b.result return b.result
} }
+15 -11
View File
@@ -214,9 +214,8 @@ func (c *common) Failed() bool {
// current goroutine). // current goroutine).
func (c *common) FailNow() { func (c *common) FailNow() {
c.Fail() c.Fail()
c.finished = true c.finished = true
c.Error("FailNow is incomplete, requires runtime.Goexit()") runtime.Goexit()
} }
// log generates the output. // log generates the output.
@@ -288,7 +287,7 @@ func (c *common) Skipf(format string, args ...any) {
func (c *common) SkipNow() { func (c *common) SkipNow() {
c.skip() c.skip()
c.finished = true c.finished = true
c.Error("SkipNow is incomplete, requires runtime.Goexit()") runtime.Goexit()
} }
func (c *common) skip() { func (c *common) skip() {
@@ -480,19 +479,19 @@ type InternalTest struct {
} }
func tRunner(t *T, fn func(t *T)) { func tRunner(t *T, fn func(t *T)) {
t.start = time.Now()
defer func() { defer func() {
t.duration += time.Since(t.start) // TODO: capture cleanup time, too.
t.runCleanup() t.runCleanup()
t.report() // Report after all subtests have finished.
if t.parent != nil && !t.hasSub {
t.setRan()
}
}() }()
// Run the test. // Run the test.
t.start = time.Now()
fn(t) fn(t)
t.duration += time.Since(t.start) // TODO: capture cleanup time, too. t.finished = true
t.report() // Report after all subtests have finished.
if t.parent != nil && !t.hasSub {
t.setRan()
}
} }
// Run runs f as a subtest of t called name. It waits until the subtest is finished // Run runs f as a subtest of t called name. It waits until the subtest is finished
@@ -524,7 +523,12 @@ func (t *T) Run(name string, f func(t *T)) bool {
fmt.Fprintf(t.output, "=== RUN %s\n", sub.name) fmt.Fprintf(t.output, "=== RUN %s\n", sub.name)
} }
tRunner(&sub, f) done := make(chan struct{})
go func() {
defer close(done)
tRunner(&sub, f)
}()
<-done
return !sub.failed return !sub.failed
} }
+10
View File
@@ -0,0 +1,10 @@
package main
import "runtime"
func main() {
defer func() {
panic("panic after Goexit")
}()
runtime.Goexit()
}
+65
View File
@@ -158,6 +158,71 @@ func runtimeGoexit() {
runtime.Goexit() runtime.Goexit()
}() }()
wg.Wait() wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
runtimeGoexitRecoveredPanic()
println("unreachable after Goexit helper")
}()
wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
runtimeGoexitNestedRecoveredPanic()
println("unreachable after nested Goexit helper")
}()
wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
runtimeGoexitLoopDefers()
println("unreachable after loop Goexit helper")
}()
wg.Wait()
}
func runtimeGoexitRecoveredPanic() {
defer func() {
if r := recover(); r != nil {
printitf("Goexit recovered deferred panic:", r)
}
}()
defer func() {
panic("panic during Goexit")
}()
runtime.Goexit()
}
func runtimeGoexitNestedRecoveredPanic() {
defer func() {
defer func() {
if r := recover(); r != nil {
printitf("Goexit nested recovered deferred panic:", r)
}
}()
panic("nested panic during Goexit")
}()
runtime.Goexit()
}
func runtimeGoexitLoopDefers() {
for i := 0; i < 2; i++ {
defer func() {
println("Goexit loop defer")
}()
}
defer func() {
if r := recover(); r != nil {
printitf("Goexit loop recovered deferred panic:", r)
}
}()
defer func() {
panic("loop panic during Goexit")
}()
runtime.Goexit()
} }
// Test that a repanic inside a deferred function propagates correctly // Test that a repanic inside a deferred function propagates correctly
+5
View File
@@ -33,6 +33,11 @@ indirect recover returned: indirect panic
# runtime.Goexit # runtime.Goexit
Goexit deferred function, recover is nil: true Goexit deferred function, recover is nil: true
Goexit recovered deferred panic: panic during Goexit
Goexit nested recovered deferred panic: nested panic during Goexit
Goexit loop recovered deferred panic: loop panic during Goexit
Goexit loop defer
Goexit loop defer
# repanic # repanic
inner, repanicking inner, repanicking
+21
View File
@@ -0,0 +1,21 @@
--- FAIL: TestBar (0.00s)
log Bar
log g
h
i
--- FAIL: TestBar/Bar2 (0.00s)
log Bar2
a
b
c
failed
after failed
log Bar end
--- FAIL: TestAllLowercase (0.00s)
--- FAIL: TestAllLowercase/BETA (0.00s)
expected lowercase name, got BETA
--- FAIL: TestAllLowercase/BELTA (0.00s)
expected lowercase name, got BELTA
FAIL
exitcode: 1
+12 -1
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"flag" "flag"
"io" "io"
"runtime"
"strings" "strings"
"testing" "testing"
) )
@@ -25,11 +26,21 @@ func TestBar(t *testing.T) {
t.Log("after failed") t.Log("after failed")
}) })
t.Run("Bar3", func(t *testing.T) {}) t.Run("Bar3", func(t *testing.T) {})
if runtime.GOARCH != "wasm" {
t.Run("Bar4", func(t *testing.T) {
t.Fatal("fatal")
t.Log("after fatal")
})
t.Run("Bar5", func(t *testing.T) {
t.SkipNow()
t.Error("after skip")
})
}
t.Log("log Bar end") t.Log("log Bar end")
} }
func TestAllLowercase(t *testing.T) { func TestAllLowercase(t *testing.T) {
names := []string { names := []string{
"alpha", "alpha",
"BETA", "BETA",
"gamma", "gamma",
+2
View File
@@ -11,6 +11,8 @@
c c
failed failed
after failed after failed
--- FAIL: TestBar/Bar4 (0.00s)
fatal
log Bar end log Bar end
--- FAIL: TestAllLowercase (0.00s) --- FAIL: TestAllLowercase (0.00s)
--- FAIL: TestAllLowercase/BETA (0.00s) --- FAIL: TestAllLowercase/BETA (0.00s)