Only bind one TestTty at a time (#873)

This commit is contained in:
Jake Wharton
2025-04-29 23:18:39 -04:00
committed by GitHub
parent 546920a2f7
commit af92d751bd
14 changed files with 80 additions and 59 deletions

View File

@ -7,7 +7,7 @@ import kotlin.test.AfterTest
import kotlin.test.BeforeTest import kotlin.test.BeforeTest
abstract class BaseEventParserTest { abstract class BaseEventParserTest {
internal val testTty = TestTty.create() internal val testTty = TestTty.bind()
private val tty = testTty.tty private val tty = testTty.tty
internal val parser = EventParser(tty) internal val parser = EventParser(tty)

View File

@ -23,7 +23,7 @@ import kotlinx.io.unsafe.UnsafeBufferOperations
fun terminalTest(block: suspend TerminalTester.() -> Unit) { fun terminalTest(block: suspend TerminalTester.() -> Unit) {
runBlocking { runBlocking {
TestTty.create().use { testTty -> TestTty.bind().use { testTty ->
TerminalTester(testTty).block() TerminalTester(testTty).block()
} }
} }

View File

@ -1,8 +1,8 @@
public final class com/jakewharton/mosaic/tty/TestTty : java/lang/AutoCloseable { public final class com/jakewharton/mosaic/tty/TestTty : java/lang/AutoCloseable {
public static final field Companion Lcom/jakewharton/mosaic/tty/TestTty$Companion; public static final field Companion Lcom/jakewharton/mosaic/tty/TestTty$Companion;
public synthetic fun <init> (JLcom/jakewharton/mosaic/tty/Tty;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (JLcom/jakewharton/mosaic/tty/Tty;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public static final fun bind ()Lcom/jakewharton/mosaic/tty/TestTty;
public fun close ()V public fun close ()V
public static final fun create ()Lcom/jakewharton/mosaic/tty/TestTty;
public final fun focusEvent (Z)V public final fun focusEvent (Z)V
public final fun getTty ()Lcom/jakewharton/mosaic/tty/Tty; public final fun getTty ()Lcom/jakewharton/mosaic/tty/Tty;
public final fun interruptRead ()V public final fun interruptRead ()V
@ -14,7 +14,7 @@ public final class com/jakewharton/mosaic/tty/TestTty : java/lang/AutoCloseable
} }
public final class com/jakewharton/mosaic/tty/TestTty$Companion { public final class com/jakewharton/mosaic/tty/TestTty$Companion {
public final fun create ()Lcom/jakewharton/mosaic/tty/TestTty; public final fun bind ()Lcom/jakewharton/mosaic/tty/TestTty;
} }
public final class com/jakewharton/mosaic/tty/Tty : java/lang/AutoCloseable { public final class com/jakewharton/mosaic/tty/Tty : java/lang/AutoCloseable {

View File

@ -24,7 +24,7 @@ final class com.jakewharton.mosaic.tty/TestTty : kotlin/AutoCloseable { // com.j
final fun write(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/TestTty.write|write(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] final fun write(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/TestTty.write|write(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final object Companion { // com.jakewharton.mosaic.tty/TestTty.Companion|null[0] final object Companion { // com.jakewharton.mosaic.tty/TestTty.Companion|null[0]
final fun create(): com.jakewharton.mosaic.tty/TestTty // com.jakewharton.mosaic.tty/TestTty.Companion.create|create(){}[0] final fun bind(): com.jakewharton.mosaic.tty/TestTty // com.jakewharton.mosaic.tty/TestTty.Companion.bind|bind(){}[0]
} }
} }

View File

@ -49,8 +49,9 @@ MosaicTestTtyInitResult testTty_init() {
} }
MosaicTtyInitResult ttyInitResult = tty_initWithFd(childFd); MosaicTtyInitResult ttyInitResult = tty_initWithFd(childFd);
if (unlikely(ttyInitResult.error)) { if (unlikely(!ttyInitResult.tty)) {
result.error = ttyInitResult.error; result.error = ttyInitResult.error;
result.already_bound = ttyInitResult.already_bound;
goto err_child; goto err_child;
} }

View File

@ -14,11 +14,11 @@ typedef struct MosaicTestTtyImpl {
atomic_bool interrupt; atomic_bool interrupt;
} MosaicTestTtyImpl; } MosaicTestTtyImpl;
static atomic_flag globalTestTty = ATOMIC_FLAG_INIT;
MosaicTestTtyInitResult testTty_init() { MosaicTestTtyInitResult testTty_init() {
MosaicTestTtyInitResult result = {}; MosaicTestTtyInitResult result = {};
// TODO Atomic boolean guaranteeing single instance
MosaicTestTtyImpl *testTty = calloc(1, sizeof(MosaicTestTtyImpl)); MosaicTestTtyImpl *testTty = calloc(1, sizeof(MosaicTestTtyImpl));
if (unlikely(testTty == NULL)) { if (unlikely(testTty == NULL)) {
// result.testTty is set to 0 which will trigger OOM. // result.testTty is set to 0 which will trigger OOM.
@ -46,10 +46,9 @@ MosaicTestTtyInitResult testTty_init() {
} }
MosaicTtyInitResult ttyInitResult = tty_initWithHandles(conin, conoutWrite, true); MosaicTtyInitResult ttyInitResult = tty_initWithHandles(conin, conoutWrite, true);
if (unlikely(ttyInitResult.error)) { if (unlikely(!ttyInitResult.tty)) {
CloseHandle(conoutRead);
CloseHandle(conoutWrite);
result.error = ttyInitResult.error; result.error = ttyInitResult.error;
result.already_bound = ttyInitResult.already_bound;
goto err_conout; goto err_conout;
} }
@ -59,6 +58,14 @@ MosaicTestTtyInitResult testTty_init() {
result.testTty = testTty; result.testTty = testTty;
if (unlikely(atomic_flag_test_and_set(&globalTestTty))) {
// We initialized an instance but there already was a global instance.
result.testTty = NULL;
result.error = tty_free(ttyInitResult.tty);
result.already_bound = true;
goto err_conout;
}
ret: ret:
return result; return result;
@ -188,6 +195,7 @@ uint32_t testTty_free(MosaicTestTty *testTty) {
result = GetLastError(); result = GetLastError();
} }
atomic_flag_clear(&globalTestTty);
free(testTty); free(testTty);
return result; return result;
} }

View File

@ -10,6 +10,7 @@ typedef struct MosaicTestTtyImpl MosaicTestTty;
typedef struct MosaicTestTtyInitResult { typedef struct MosaicTestTtyInitResult {
MosaicTestTty *testTty; MosaicTestTty *testTty;
uint32_t error; uint32_t error;
bool already_bound;
} MosaicTestTtyInitResult; } MosaicTestTtyInitResult;
MosaicTestTtyInitResult testTty_init(); MosaicTestTtyInitResult testTty_init();

View File

@ -14,6 +14,8 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
static _Atomic(MosaicTty *) globalTty;
MosaicTtyInitResult tty_initWithFd(int fd) { MosaicTtyInitResult tty_initWithFd(int fd) {
MosaicTtyInitResult result = {}; MosaicTtyInitResult result = {};
@ -35,29 +37,6 @@ MosaicTtyInitResult tty_initWithFd(int fd) {
result.tty = tty; result.tty = tty;
ret:
return result;
err:
free(tty);
goto ret;
}
static _Atomic(MosaicTty *) globalTty;
MosaicTtyInitResult tty_init() {
MosaicTtyInitResult result = {};
int fd = open("/dev/tty", O_RDWR);
if (unlikely(fd == -1)) {
result.error = errno;
result.no_tty = true;
goto ret;
}
result = tty_initWithFd(fd);
MosaicTty *tty = result.tty;
MosaicTty *expected = NULL; MosaicTty *expected = NULL;
if (likely(tty) && !atomic_compare_exchange_strong(&globalTty, &expected, tty)) { if (likely(tty) && !atomic_compare_exchange_strong(&globalTty, &expected, tty)) {
// We initialized an instance but there already was a global instance. // We initialized an instance but there already was a global instance.
@ -68,6 +47,22 @@ MosaicTtyInitResult tty_init() {
ret: ret:
return result; return result;
err:
free(tty);
goto ret;
}
MosaicTtyInitResult tty_init() {
int fd = open("/dev/tty", O_RDWR);
if (likely(fd != -1)) {
return tty_initWithFd(fd);
}
MosaicTtyInitResult result = {};
result.error = errno;
result.no_tty = true;
return result;
} }
void tty_setCallback(MosaicTty *tty, MosaicTtyCallback *callback) { void tty_setCallback(MosaicTty *tty, MosaicTtyCallback *callback) {
@ -173,6 +168,8 @@ void sigwinchHandler(int value UNUSED) {
} else { } else {
// TODO Send errno somewhere? Maybe once we get debug logs working. // TODO Send errno somewhere? Maybe once we get debug logs working.
} }
} else {
// TODO Send warning somewhere? Maybe once we get debug logs working.
} }
} }

View File

@ -2,7 +2,16 @@ package com.jakewharton.mosaic.tty
public expect class TestTty : AutoCloseable { public expect class TestTty : AutoCloseable {
public companion object { public companion object {
public fun create(): TestTty
/**
* Initialize a [TestTty] instance. Only a single [TestTty] instance can be bound at a time,
* and only when a [Tty] is not also bound. Subsequent calls will throw an exception until
* [TestTty.close] is called.
*
* @throws IOException If an error occurred creating the PTY.
* @throws IllegalStateException If another instance is already bound.
*/
public fun bind(): TestTty
} }
public val tty: Tty public val tty: Tty

View File

@ -1,7 +1,10 @@
package com.jakewharton.mosaic.tty package com.jakewharton.mosaic.tty
import assertk.assertFailure
import assertk.assertThat import assertk.assertThat
import assertk.assertions.hasMessage
import assertk.assertions.isEqualTo import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import assertk.assertions.isZero import assertk.assertions.isZero
import kotlin.test.AfterTest import kotlin.test.AfterTest
import kotlin.test.BeforeTest import kotlin.test.BeforeTest
@ -13,7 +16,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
class TestTtyTest { class TestTtyTest {
private val testTty = TestTty.create() private val testTty = TestTty.bind()
private val tty = testTty.tty private val tty = testTty.tty
@BeforeTest fun before() { @BeforeTest fun before() {
@ -26,15 +29,11 @@ class TestTtyTest {
testTty.close() testTty.close()
} }
@Test fun canCreateMultiple() { @Test fun onlyOne() {
if (isWindows()) return // TODO Not currently supported. assertFailure {
TestTty.bind()
TestTty.create().use { testTty2 -> }.isInstanceOf<IllegalStateException>()
testTty.write("hey\n") .hasMessage("TestTty or Tty already bound")
testTty2.write("bye\n")
assertThat(testTty2.tty.read(4)).isEqualTo("bye\n")
assertThat(testTty.tty.read(4)).isEqualTo("hey\n")
}
} }
@Test fun multipleRawModeResetCycles() { @Test fun multipleRawModeResetCycles() {

View File

@ -18,7 +18,7 @@ import kotlinx.coroutines.test.runTest
class TtyTest { class TtyTest {
private val events = ArrayDeque<String>() private val events = ArrayDeque<String>()
private val testTty = TestTty.create() private val testTty = TestTty.bind()
private val tty = testTty.tty private val tty = testTty.tty
@BeforeTest fun before() { @BeforeTest fun before() {

View File

@ -360,14 +360,20 @@ Java_com_jakewharton_mosaic_tty_Jni_testTtyInit(
jclass type UNUSED jclass type UNUSED
) { ) {
MosaicTestTtyInitResult result = testTty_init(); MosaicTestTtyInitResult result = testTty_init();
if (likely(!result.error)) { if (likely(result.testTty)) {
return (jlong) result.testTty; return (jlong) result.testTty;
} }
// This throw can fail, but the only condition that should cause that is OOM which if (result.already_bound) {
// will occur from returning 0 (which is otherwise ignored if the throw succeeds). jclass ise = (*env)->FindClass(env, "java/lang/IllegalStateException");
throwIoe(env, result.error); (*env)->ThrowNew(env, ise, "TestTty or Tty already bound");
return 0; } else if (result.error) {
throwIoe(env, result.error);
} else {
jclass ise = (*env)->FindClass(env, "java/lang/OutOfMemoryException");
(*env)->ThrowNew(env, ise, NULL);
}
return 0; // Unused
} }
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL

View File

@ -10,14 +10,11 @@ public actual class TestTty private constructor(
public actual companion object { public actual companion object {
@JvmStatic @JvmStatic
@Throws(IOException::class) @Throws(IOException::class)
public actual fun create(): TestTty { public actual fun bind(): TestTty {
val testTtyPtr = testTtyInit() val testTtyPtr = testTtyInit()
if (testTtyPtr != 0L) { val ttyPtr = testTtyGetTty(testTtyPtr)
val ttyPtr = testTtyGetTty(testTtyPtr) val tty = Tty(ttyPtr)
val tty = Tty(ttyPtr) return TestTty(testTtyPtr, tty)
return TestTty(testTtyPtr, tty)
}
throw OutOfMemoryError()
} }
} }

View File

@ -10,10 +10,13 @@ public actual class TestTty private constructor(
public actual val tty: Tty, public actual val tty: Tty,
) : AutoCloseable { ) : AutoCloseable {
public actual companion object { public actual companion object {
public actual fun create(): TestTty { public actual fun bind(): TestTty {
val testTtyPtr = testTty_init().useContents { val testTtyPtr = testTty_init().useContents {
testTty?.let { return@useContents it } testTty?.let { return@useContents it }
if (already_bound) {
throw IllegalStateException("TestTty or Tty already bound")
}
if (error != 0U) { if (error != 0U) {
throwIoe(error) throwIoe(error)
} }