33import org .junit .jupiter .api .AfterAll ;
44import org .junit .jupiter .api .Assertions ;
55import org .junit .jupiter .api .BeforeAll ;
6+ import org .junit .jupiter .api .RepeatedTest ;
67import org .junit .jupiter .api .Test ;
78import org .quiltmc .enigma .TestUtil ;
89import org .quiltmc .enigma .api .Enigma ;
910import org .quiltmc .enigma .api .EnigmaProject ;
1011import org .quiltmc .enigma .api .ProgressListener ;
1112import org .quiltmc .enigma .api .class_provider .ClasspathClassProvider ;
12- import org .quiltmc .enigma .api .translation .mapping .EntryRemapper ;
1313import org .quiltmc .enigma .network .packet .c2s .LoginC2SPacket ;
1414import org .quiltmc .enigma .network .packet .c2s .MessageC2SPacket ;
15+ import org .quiltmc .enigma .network .packet .s2c .KickS2CPacket ;
1516import org .quiltmc .enigma .util .Utils ;
1617
1718import java .io .IOException ;
19+ import java .net .BindException ;
20+ import java .net .Socket ;
1821import java .nio .file .Path ;
22+ import java .util .List ;
1923import java .util .concurrent .CountDownLatch ;
2024import java .util .concurrent .TimeUnit ;
2125
26+ /**
27+ * <b>Warning</b>: Using {@link RepeatedTest @RepeatedTest} on tests that wait on
28+ * {@link DummyClientPacketHandler#disconnectFromServerLatch} eventually causes
29+ * {@link BindException}: {@code Address already in use: connect}.
30+ *
31+ * <p> It seems like all outbound sockets get exhausted and have a timeout before they become available.
32+ *
33+ * <p> On my (supersaiyansubtlety's) PC, the first exception comes after ~20,000 combined repetitions within ~2 minutes,
34+ * after which exceptions are frequently thrown until ~2 minutes have passed without any repetitions.
35+ *
36+ * <p> Each {@link DummyClientPacketHandler#disconnectFromServerLatch}-waiting test succeeded with 1,000 repetitions.
37+ *
38+ * <p> Repetitions are set to {@value #DEFAULT_REPETITIONS} to reasonably test flakiness while avoiding the socket cap.
39+ */
2240public class NetworkTest {
2341 private static final Path JAR = TestUtil .obfJar ("complete" );
2442 private static final String PASSWORD = "foobar" ;
43+ private static final int DEFAULT_REPETITIONS = 100 ;
44+
2545 private static byte [] checksum ;
2646 private static TestEnigmaServer server ;
27- private static EntryRemapper remapper ;
2847
2948 @ BeforeAll
3049 public static void startServer () throws IOException {
3150 Enigma enigma = Enigma .create ();
3251 EnigmaProject project = enigma .openJar (JAR , new ClasspathClassProvider (), ProgressListener .createEmpty ());
3352
3453 checksum = Utils .zipSha1 (JAR );
35- remapper = project .getRemapper ();
36- server = new TestEnigmaServer (checksum , PASSWORD .toCharArray (), remapper , 0 );
54+ server = new TestEnigmaServer (checksum , PASSWORD .toCharArray (), project .getRemapper (), 0 );
3755
3856 server .start ();
3957 }
@@ -50,38 +68,63 @@ private static TestEnigmaClient connectClient(ClientPacketHandler handler) throw
5068 return client ;
5169 }
5270
53- @ Test
71+ private static void awaitNextConnection () throws InterruptedException {
72+ Assertions .assertTrue (server .awaitNextConnection (3 , TimeUnit .SECONDS ), "Client did not connect" );
73+ }
74+
75+ /**
76+ * <b>Never</b> directly {@linkplain EnigmaClient#disconnect() disconnect} clients after tests; <b>always</b>
77+ * either manually {@link EnigmaServer#kick(Socket, String) kick} them (like this method does) or wait for them to
78+ * be disconnected using {@link DummyClientPacketHandler#disconnectFromServerLatch}.
79+ *
80+ * <p> Directly {@linkplain EnigmaClient#disconnect() disconnecting} a client creates a race condition:
81+ * The {@code "disconnect.disconnected"} {@link EnigmaServer#kick(Socket, String) kick} call in
82+ * {@link EnigmaServer}'s client threads sends a {@link KickS2CPacket} that can be received by
83+ * <em>other</em> test's clients.
84+ */
85+ static void kickAfterTest (Socket clientSocket ) {
86+ server .kick (clientSocket , "test complete" );
87+ }
88+
89+ /**
90+ * When making changes to this test, <em>always</em> test with 100,000 repetitions.
91+ */
92+ @ RepeatedTest (DEFAULT_REPETITIONS )
5493 public void testLogin () throws IOException , InterruptedException {
55- var handler = new DummyClientPacketHandler ();
56- var client = connectClient (handler );
94+ final var handler = new DummyClientPacketHandler ();
95+
96+ server .queueConnectionWait ();
97+
98+ final TestEnigmaClient client = connectClient (handler );
5799 handler .client = client ;
58100
59- Assertions .assertFalse (server .getClients ().isEmpty ());
60- Assertions .assertFalse (server .getUnapprovedClients ().isEmpty ());
101+ awaitNextConnection ();
61102
103+ Assertions .assertNotEquals (0 , handler .disconnectFromServerLatch .getCount (), "The client was disconnected by the server" );
104+
105+ final Socket clientSocket = server .getClients ().get (0 );
106+ final CountDownLatch changeConfirmation = server .waitChangeConfirmation (clientSocket , 1 );
62107 client .sendPacket (new LoginC2SPacket (checksum , PASSWORD .toCharArray (), "alice" ));
63- var confirmed = server .waitChangeConfirmation (server .getClients ().get (0 ), 1 )
64- .await (3 , TimeUnit .SECONDS );
108+ final boolean confirmed = changeConfirmation .await (3 , TimeUnit .SECONDS );
65109
66- Assertions .assertNotEquals (0 , handler .disconnectFromServerLatch .getCount (), "The client was disconnected by the server" );
67110 Assertions .assertTrue (confirmed , "Timed out waiting for the change confirmation" );
68- client .disconnect ();
111+
112+ kickAfterTest (clientSocket );
69113 }
70114
71- @ Test
115+ @ RepeatedTest ( DEFAULT_REPETITIONS )
72116 public void testInvalidUsername () throws IOException , InterruptedException {
73117 var handler = new DummyClientPacketHandler ();
74118 var client = connectClient (handler );
75119 handler .client = client ;
76120
77121 client .sendPacket (new LoginC2SPacket (checksum , PASSWORD .toCharArray (), "<span style=\" color: lavender\" >eve</span>" ));
78- var disconnected = handler .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
122+ boolean disconnected = handler .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
79123
80124 Assertions .assertTrue (disconnected , "Timed out waiting for the server to kick the client" );
81- client .disconnect ();
82125 }
83126
84- @ Test
127+ @ RepeatedTest ( DEFAULT_REPETITIONS )
85128 public void testWrongPassword () throws IOException , InterruptedException {
86129 var handler = new DummyClientPacketHandler ();
87130 var client = connectClient (handler );
@@ -91,31 +134,43 @@ public void testWrongPassword() throws IOException, InterruptedException {
91134 var disconnected = handler .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
92135
93136 Assertions .assertTrue (disconnected , "Timed out waiting for the server to kick the client" );
94- client .disconnect ();
95137 }
96138
139+ // FIXME this test is flaky when run from workflows/build.yml
97140 @ Test
98141 public void testTakenUsername () throws IOException , InterruptedException {
99- var packet = new LoginC2SPacket (checksum , PASSWORD .toCharArray (), "alice" );
142+ final var packet = new LoginC2SPacket (checksum , PASSWORD .toCharArray (), "alice" );
100143
101- var handler = new DummyClientPacketHandler ();
102- var client = connectClient (handler );
103- handler .client = client ;
104- client .sendPacket (packet );
144+ final var handler = new DummyClientPacketHandler ();
145+
146+ server .queueConnectionWait ();
105147
106- var handler2 = new DummyClientPacketHandler ();
107- var client2 = connectClient (handler2 );
148+ final TestEnigmaClient client1 = connectClient (handler );
149+ handler .client = client1 ;
150+ client1 .sendPacket (packet );
151+
152+ awaitNextConnection ();
153+
154+ final var handler2 = new DummyClientPacketHandler ();
155+
156+ server .queueConnectionWait ();
157+
158+ final TestEnigmaClient client2 = connectClient (handler2 );
108159 handler2 .client = client2 ;
109160
161+ awaitNextConnection ();
162+
110163 client2 .sendPacket (packet );
111- var disconnected = handler2 .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
164+ final boolean disconnected = handler2 .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
112165
113166 Assertions .assertTrue (disconnected , "Timed out waiting for the server to kick the client" );
114- client .disconnect ();
115- client2 .disconnect ();
167+
168+ for (final Socket clientSocket : List .copyOf (server .getClients ())) {
169+ kickAfterTest (clientSocket );
170+ }
116171 }
117172
118- @ Test
173+ @ RepeatedTest ( DEFAULT_REPETITIONS )
119174 public void testWrongChecksum () throws IOException , InterruptedException {
120175 var handler = new DummyClientPacketHandler ();
121176 var client = connectClient (handler );
@@ -126,9 +181,9 @@ public void testWrongChecksum() throws IOException, InterruptedException {
126181 var disconnected = handler .disconnectFromServerLatch .await (3 , TimeUnit .SECONDS );
127182
128183 Assertions .assertTrue (disconnected , "Timed out waiting for the server to kick the client" );
129- client .disconnect ();
130184 }
131185
186+ // no repeats because successes take at least 3 seconds
132187 @ Test
133188 public void testUnapprovedMessage () throws IOException , InterruptedException {
134189 var handler = new DummyClientPacketHandler ();
0 commit comments