coroutine: add native behavior tests. (#18817)

This commit is contained in:
Samuel Williams authored and GitHub committed 2026-09-14 19:27:52 +12:00
1 parent f30fdb01f3
commit e96cea4e03
15 files changed
+477 -4
Notes: git 2026-09-14 07:28:21 +00:00
Merged-By: ioquatix <samuel@codeotaku.com>

No files matched your search

+1
View File
@@ -59,6 +59,7 @@ lcov*.info
/*-fake.rb
/*.dll
/*.exe
/coroutine-test
/*.ilk
/*.res
/*.pc
+41 -2
View File
@@ -94,6 +94,13 @@ MAKE_ENC = -f $(ENC_MK) V="$(V)" UNICODE_HDR_DIR="$(UNICODE_HDR_DIR)" \
PRISM_BUILD_DIR = prism
COROUTINE_TEST = coroutine-test$(EXEEXT)
COROUTINE_TEST_OBJS = coroutine-main.$(OBJEXT) \
coroutine-stack.$(OBJEXT) \
coroutine-test_initialize_destroy.$(OBJEXT) \
coroutine-test_transfer_repeat.$(OBJEXT) \
coroutine-test_transfer_return.$(OBJEXT)
LIBPRISM_OBJS = \
prism/arena.$(OBJEXT) \
prism/buffer.$(OBJEXT) \
@@ -691,6 +698,7 @@ noarch_config_h = tmp/include/noarch/ruby/config.h
clean: clean-ext clean-enc clean-golf clean-docs clean-extout clean-modular-gc clean-local clean-platform clean-spec
clean-local:: clean-runnable
$(Q)$(RM) $(ALLOBJS) $(LIBRUBY_A) $(LIBRUBY_SO) $(LIBRUBY) $(LIBRUBY_ALIASES)
$(Q)$(RM) $(COROUTINE_TEST) $(COROUTINE_TEST_OBJS)
$(Q)$(RM) $(PROGRAM) $(WPROGRAM) miniruby$(EXEEXT) dmyext.$(OBJEXT) dmyenc.$(OBJEXT) $(ARCHFILE) .*.time
$(Q)$(RM) y.tab.c y.output encdb.h transdb.h config.log rbconfig.rb $(ruby_pc) $(COROUTINE_H:/Context.h=/.time)
$(Q)$(RM) probes.h probes.$(OBJEXT) probes.stamp ruby-glommed.$(OBJEXT) ruby.imp ChangeLog $(STATIC_RUBY)$(EXEEXT)
@@ -931,8 +939,15 @@ yes-test-tool: prog PHONY
$(ACTIONS_ENDGROUP)
no-test-tool: PHONY
test-coroutine: $(TEST_RUNNABLE)-test-coroutine
yes-test-coroutine: $(COROUTINE_TEST) PHONY
$(ACTIONS_GROUP)
$(Q)$(exec) $(COROUTINE_TEST_RUN)
$(ACTIONS_ENDGROUP)
no-test-coroutine: PHONY
test-sample: test-basic # backward compatibility for mswin-build
test-short: btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic
test-short: test-coroutine $(DOT_WAIT) btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic
test: test-short
# Separate to skip updating encs and exts by `make -o test-precheck`
@@ -1043,7 +1058,7 @@ $(ENC_MK): $(srcdir)/enc/make_encmake.rb $(srcdir)/enc/Makefile.in $(srcdir)/enc
.PHONY: distclean-srcs distclean-srcs-local distclean-srcs-ext
.PHONY: realclean realclean-ext realclean-local realclean-enc realclean-golf realclean-extout
.PHONY: realclean-srcs realclean-srcs-local realclean-srcs-ext
.PHONY: exam check test test-short test-all btest btest-ruby test-basic test-knownbug
.PHONY: exam check test test-short test-all test-coroutine btest btest-ruby test-basic test-knownbug
.PHONY: run runruby parse benchmark gdb gdb-ruby
.PHONY: update-mspec update-rubyspec test-rubyspec test-spec
.PHONY: touch-unicode-files
@@ -1137,6 +1152,30 @@ $(COROUTINE_H:/Context.h=/.time):
$(Q) $(MAKEDIRS) $(@D)
@$(NULLCMD) > $@
$(COROUTINE_TEST): $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ)
$(ECHO) linking $@
$(Q) $(COROUTINE_TEST_LINK)
coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c
$(ECHO) compiling $(srcdir)/coroutine/test/main.c
$(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/main.c
coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c
$(ECHO) compiling $(srcdir)/coroutine/test/stack.c
$(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/stack.c
coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c
$(ECHO) compiling $(srcdir)/coroutine/test/test_initialize_destroy.c
$(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_initialize_destroy.c
coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c
$(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_repeat.c
$(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_repeat.c
coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c
$(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_return.c
$(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_return.c
###
# dependencies for generated C sources.
+31
View File
@@ -0,0 +1,31 @@
#include "test_initialize_destroy.h"
#include "test_transfer_repeat.h"
#include "test_transfer_return.h"
#include <stdlib.h>
static int
run_tests(void)
{
int result = EXIT_SUCCESS;
if (test_initialize_destroy() != EXIT_SUCCESS) result = EXIT_FAILURE;
if (test_transfer_repeat() != EXIT_SUCCESS) result = EXIT_FAILURE;
if (test_transfer_return() != EXIT_SUCCESS) result = EXIT_FAILURE;
return result;
}
int
main(void)
{
return run_tests();
}
#if defined(_WIN32)
int
wmain(void)
{
return run_tests();
}
#endif
+49
View File
@@ -0,0 +1,49 @@
#include "ruby/internal/config.h"
#include "stack.h"
#include <stdlib.h>
#if defined(_WIN32)
# include <windows.h>
#elif defined(HAVE_SYS_MMAN_H)
# include <sys/mman.h>
#endif
#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
# define MAP_ANONYMOUS MAP_ANON
#endif
int
coroutine_stack_allocate(struct coroutine_stack *stack, size_t size)
{
#if defined(_WIN32)
stack->base = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS)
stack->base = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (stack->base == MAP_FAILED) stack->base = NULL;
#else
stack->base = malloc(size);
#endif
stack->size = stack->base == NULL ? 0 : size;
return stack->base == NULL ? -1 : 0;
}
void
coroutine_stack_free(struct coroutine_stack *stack)
{
if (stack->base == NULL) return;
#if defined(_WIN32)
VirtualFree(stack->base, 0, MEM_RELEASE);
#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS)
munmap(stack->base, stack->size);
#else
free(stack->base);
#endif
stack->base = NULL;
stack->size = 0;
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <stddef.h>
struct coroutine_stack {
void *base;
size_t size;
};
int coroutine_stack_allocate(struct coroutine_stack *stack, size_t size);
void coroutine_stack_free(struct coroutine_stack *stack);
+41
View File
@@ -0,0 +1,41 @@
#include "ruby/internal/config.h"
#include COROUTINE_H
#include "stack.h"
#include "test_initialize_destroy.h"
#include <stdio.h>
#include <stdlib.h>
#define STACK_SIZE (1024 * 1024)
static COROUTINE
never_started(struct coroutine_context *from, struct coroutine_context *self)
{
(void)from;
(void)self;
abort();
}
int
test_initialize_destroy(void)
{
struct coroutine_stack stack = {0};
struct coroutine_context main_context;
struct coroutine_context context;
if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) {
fprintf(stderr, "failed to allocate coroutine stack\n");
return EXIT_FAILURE;
}
coroutine_initialize_main(&main_context);
coroutine_initialize(&context, never_started, stack.base, stack.size);
coroutine_destroy(&context);
coroutine_destroy(&main_context);
coroutine_stack_free(&stack);
return EXIT_SUCCESS;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
int test_initialize_destroy(void);
+141
View File
@@ -0,0 +1,141 @@
#include "ruby/internal/config.h"
#include COROUTINE_H
#include "stack.h"
#include "test_transfer_repeat.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define STACK_SIZE (1024 * 1024)
#define TRANSFER_COUNT 1000
static struct coroutine_context main_context;
static struct coroutine_context worker_context;
static unsigned int yielded_iteration;
static uint32_t yielded_state;
static int completed;
static void
check_context(const char *message, struct coroutine_context *actual,
struct coroutine_context *expected)
{
if (actual != expected) {
fprintf(stderr, "%s: expected %p, got %p\n",
message, (void *)expected, (void *)actual);
abort();
}
}
static void
context_start(struct coroutine_context *from, struct coroutine_context *self)
{
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_finish_switch_fiber(self->fake_stack,
(const void **)&from->stack_base,
&from->stack_size);
#else
(void)from;
(void)self;
#endif
}
static struct coroutine_context *
transfer(struct coroutine_context *current, struct coroutine_context *target)
{
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_start_switch_fiber(&current->fake_stack,
target->stack_base, target->stack_size);
#endif
#if defined(COROUTINE_SANITIZE_THREAD)
__tsan_switch_to_fiber(target->tsan_fiber, 0);
#endif
struct coroutine_context *from = coroutine_transfer(current, target);
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL);
#endif
return from;
}
static COROUTINE
worker_entry(struct coroutine_context *from, struct coroutine_context *self)
{
context_start(from, self);
check_context("worker context was entered by", from, &main_context);
check_context("worker context received self", self, &worker_context);
uint32_t state = UINT32_C(0x12345678);
for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) {
state = state * UINT32_C(1664525) + UINT32_C(1013904223);
yielded_iteration = iteration;
yielded_state = state;
from = transfer(self, &main_context);
check_context("worker context was resumed by", from, &main_context);
if (yielded_iteration != iteration || yielded_state != state) {
fprintf(stderr, "worker local state was not preserved\n");
abort();
}
}
completed = 1;
transfer(self, &main_context);
abort();
}
int
test_transfer_repeat(void)
{
struct coroutine_stack stack = {0};
int result = EXIT_FAILURE;
if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) {
fprintf(stderr, "failed to allocate coroutine stack\n");
return EXIT_FAILURE;
}
yielded_iteration = 0;
yielded_state = 0;
completed = 0;
coroutine_initialize_main(&main_context);
coroutine_initialize(&worker_context, worker_entry, stack.base, stack.size);
uint32_t expected_state = UINT32_C(0x12345678);
for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) {
expected_state = expected_state * UINT32_C(1664525) + UINT32_C(1013904223);
struct coroutine_context *from = transfer(&main_context, &worker_context);
if (from != &worker_context || yielded_iteration != iteration ||
yielded_state != expected_state || completed) {
fprintf(stderr, "repeated coroutine transfer failed at iteration %u\n",
iteration);
goto finish;
}
}
if (transfer(&main_context, &worker_context) != &worker_context || !completed) {
fprintf(stderr, "worker context did not complete repeated transfers\n");
goto finish;
}
result = EXIT_SUCCESS;
finish:
coroutine_destroy(&worker_context);
coroutine_destroy(&main_context);
coroutine_stack_free(&stack);
return result;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
int test_transfer_repeat(void);
+142
View File
@@ -0,0 +1,142 @@
#include "ruby/internal/config.h"
#include COROUTINE_H
#include "stack.h"
#include "test_transfer_return.h"
#include <stdio.h>
#include <stdlib.h>
#define STACK_SIZE (1024 * 1024)
static struct coroutine_context main_context;
static struct coroutine_context first_context;
static struct coroutine_context second_context;
static void
check_context(const char *message, struct coroutine_context *actual,
struct coroutine_context *expected)
{
if (actual != expected) {
fprintf(stderr, "%s: expected %p, got %p\n",
message, (void *)expected, (void *)actual);
abort();
}
}
static void
context_start(struct coroutine_context *from, struct coroutine_context *self)
{
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_finish_switch_fiber(self->fake_stack,
(const void **)&from->stack_base,
&from->stack_size);
#else
(void)from;
(void)self;
#endif
}
static struct coroutine_context *
transfer(struct coroutine_context *current, struct coroutine_context *target)
{
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_start_switch_fiber(&current->fake_stack,
target->stack_base, target->stack_size);
#endif
#if defined(COROUTINE_SANITIZE_THREAD)
__tsan_switch_to_fiber(target->tsan_fiber, 0);
#endif
struct coroutine_context *from = coroutine_transfer(current, target);
#if defined(COROUTINE_SANITIZE_ADDRESS)
__sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL);
#endif
return from;
}
static COROUTINE
second_entry(struct coroutine_context *from, struct coroutine_context *self)
{
context_start(from, self);
check_context("second context was entered by", from, &first_context);
check_context("second context received self", self, &second_context);
from = transfer(self, &main_context);
check_context("second context was resumed by", from, &main_context);
transfer(self, &first_context);
abort();
}
static COROUTINE
first_entry(struct coroutine_context *from, struct coroutine_context *self)
{
context_start(from, self);
check_context("first context was entered by", from, &main_context);
check_context("first context received self", self, &first_context);
from = transfer(self, &second_context);
check_context("first context was resumed by", from, &second_context);
transfer(self, &main_context);
abort();
}
int
test_transfer_return(void)
{
struct coroutine_stack first_stack = {0};
struct coroutine_stack second_stack = {0};
int result = EXIT_FAILURE;
if (coroutine_stack_allocate(&first_stack, STACK_SIZE) != 0 ||
coroutine_stack_allocate(&second_stack, STACK_SIZE) != 0) {
fprintf(stderr, "failed to allocate coroutine stacks\n");
goto finish;
}
coroutine_initialize_main(&main_context);
coroutine_initialize(&first_context, first_entry,
first_stack.base, first_stack.size);
coroutine_initialize(&second_context, second_entry,
second_stack.base, second_stack.size);
result = EXIT_SUCCESS;
/* The original target is first_context, but second_context resumes us. */
struct coroutine_context *from = transfer(&main_context, &first_context);
if (from != &second_context) {
fprintf(stderr,
"coroutine_transfer returned %p, expected actual resumer %p\n",
(void *)from, (void *)&second_context);
result = EXIT_FAILURE;
}
/* Resume second_context, which resumes first_context, which resumes us. */
from = transfer(&main_context, &second_context);
if (from != &first_context) {
fprintf(stderr,
"coroutine_transfer returned %p, expected actual resumer %p\n",
(void *)from, (void *)&first_context);
result = EXIT_FAILURE;
}
coroutine_destroy(&first_context);
coroutine_destroy(&second_context);
coroutine_destroy(&main_context);
finish:
coroutine_stack_free(&first_stack);
coroutine_stack_free(&second_stack);
return result;
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
int test_transfer_return(void);
+5
View File
@@ -55,6 +55,11 @@ compile.$(OBJEXT): {$(VPATH)}compile.c
complex.$(OBJEXT): {$(VPATH)}complex.c
concurrent_set.$(OBJEXT): {$(VPATH)}concurrent_set.c
cont.$(OBJEXT): {$(VPATH)}cont.c
coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c
coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c
coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c
coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c
coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c
debug.$(OBJEXT): {$(VPATH)}debug.c
debug_counter.$(OBJEXT): {$(VPATH)}debug_counter.c
dir.$(OBJEXT): {$(VPATH)}dir.c
+2
View File
@@ -185,6 +185,8 @@ BOOTSTRAPRUBY_FAKE = $(yes_baseruby:yes=$(arch)-fake.rb)
COROUTINE_H = @X_COROUTINE_H@
COROUTINE_OBJ = $(COROUTINE_H:.h=.$(OBJEXT))
COROUTINE_SRC = @X_COROUTINE_SRC@
COROUTINE_TEST_LINK = $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST)
COROUTINE_TEST_RUN = ./$(COROUTINE_TEST)
#### End of system configuration section. ####
+2 -2
View File
@@ -161,8 +161,8 @@ struct rb_native_thread {
bool retiring;
// A terminating coroutine records its context here before its final
// transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s
// return value: its meaning differs between the amd64 asm and ucontext.)
// transfer; this nt's loop reclaims it. coroutine_transfer() cannot be
// used because a terminating coroutine never resumes to return a value.
struct coroutine_context *dead_co;
};
+2
View File
@@ -344,6 +344,8 @@ COROUTINE_SRC = $(COROUTINE_OBJ:.obj=.asm)
!error copy coroutine has been replaced with pthread implementation at 42130a64f02294dc8025af3a51bda518c67ab33d
!endif
COROUTINE_H = $(COROUTINE_OBJ:.obj=.h)
COROUTINE_TEST_LINK = $(CC) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST) -link $(LDFLAGS) $(XLDFLAGS)
COROUTINE_TEST_RUN = .\$(COROUTINE_TEST)
ARFLAGS = -machine:$(MACHINE) -out:
LD = $(CC)