Skip to content

Commit ec5da32

Browse files
authored
test: test for console output in Turtle tests (#21794)
Signed-off-by: Michael Heinrichs <netopyr@users.noreply.github.com>
1 parent 799fceb commit ec5da32

File tree

9 files changed

+232
-19
lines changed

9 files changed

+232
-19
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
package org.hiero.otter.fixtures.turtle.logging;
3+
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.PrintStream;
8+
import java.nio.charset.StandardCharsets;
9+
import java.time.Duration;
10+
import java.util.regex.Matcher;
11+
import java.util.regex.Pattern;
12+
import org.apache.logging.log4j.LogManager;
13+
import org.apache.logging.log4j.core.LoggerContext;
14+
import org.hiero.otter.fixtures.Network;
15+
import org.hiero.otter.fixtures.TestEnvironment;
16+
import org.hiero.otter.fixtures.TimeManager;
17+
import org.hiero.otter.fixtures.turtle.TurtleTestEnvironment;
18+
import org.junit.jupiter.api.Test;
19+
20+
class TurtleConsoleOutputTest {
21+
22+
/**
23+
* Pattern for parsing log messages.
24+
* Matches: [thread] [optional marker] LEVEL logger - message
25+
* Group 1: log level (INFO, WARN, ERROR, etc.)
26+
* Group 2: logger name (e.g., org.hiero.otter.fixtures.internal.AbstractNetwork)
27+
*/
28+
private static final Pattern LOG_PATTERN = Pattern.compile("\\[.*?]\\s+(?:\\[.*?])?\\s*(\\w+)\\s+(\\S+)\\s+-");
29+
30+
/**
31+
* Checks if a line matches the log message pattern.
32+
*/
33+
private static boolean isLogMessage(final String line) {
34+
return LOG_PATTERN.matcher(line).find();
35+
}
36+
37+
/**
38+
* Extracts the log level from a log message line.
39+
* Returns empty string if the line doesn't match the expected pattern.
40+
*/
41+
private static String extractLogLevel(final String line) {
42+
final Matcher matcher = LOG_PATTERN.matcher(line);
43+
return matcher.find() ? matcher.group(1) : "";
44+
}
45+
46+
/**
47+
* Extracts the logger name from a log message line.
48+
* Returns empty string if the line doesn't match the expected pattern.
49+
*/
50+
private static String extractLoggerName(final String line) {
51+
final Matcher matcher = LOG_PATTERN.matcher(line);
52+
return matcher.find() ? matcher.group(2) : "";
53+
}
54+
55+
@Test
56+
void testBasicConsoleOutput() {
57+
// Capture console output
58+
final ByteArrayOutputStream consoleCapture = new ByteArrayOutputStream();
59+
final PrintStream originalOut = System.out;
60+
final PrintStream captureOut = new PrintStream(consoleCapture, true, StandardCharsets.UTF_8);
61+
62+
try {
63+
// Redirect System.out BEFORE creating the test environment
64+
System.setOut(captureOut);
65+
66+
// Force Log4j to reconfigure so ConsoleAppender picks up the new System.out
67+
final LoggerContext context = (LoggerContext) LogManager.getContext(false);
68+
context.reconfigure();
69+
70+
final TestEnvironment env = new TurtleTestEnvironment();
71+
try {
72+
final Network network = env.network();
73+
final TimeManager timeManager = env.timeManager();
74+
75+
// Start a 4-node network
76+
network.addNodes(4);
77+
network.start();
78+
79+
System.out.println("Hello Otter!");
80+
LogManager.getLogger().info("Hello Hiero!");
81+
LogManager.getLogger("com.acme.ExternalOtterTest").info("Hello World!");
82+
83+
// Wait 5 seconds
84+
timeManager.waitFor(Duration.ofSeconds(5L));
85+
86+
} finally {
87+
env.destroy();
88+
}
89+
90+
// Restore System.out before examining captured output
91+
System.setOut(originalOut);
92+
93+
// Get the captured console output
94+
final String consoleOutput = consoleCapture.toString(StandardCharsets.UTF_8);
95+
96+
// Verify that the console output contains expected log messages
97+
assertThat(consoleOutput)
98+
.as("Console output should contain 'Random seed:' entry")
99+
.contains("Random seed:");
100+
assertThat(consoleOutput)
101+
.as("Console output should contain 'Starting network...' message")
102+
.contains("Starting network...");
103+
assertThat(consoleOutput)
104+
.as("Console output should contain 'Network started.' message")
105+
.contains("Network started.");
106+
assertThat(consoleOutput)
107+
.as("Console output should contain 'Waiting for PT5S' message")
108+
.contains("Waiting for PT5S");
109+
assertThat(consoleOutput)
110+
.as("Console output should contain 'Destroying network...' message")
111+
.contains("Destroying network...");
112+
assertThat(consoleOutput)
113+
.as("Console output should contain 'Hello Otter!' message")
114+
.contains("Hello Otter!");
115+
assertThat(consoleOutput)
116+
.as("Console output should contain 'Hello Hiero!' message")
117+
.contains("Hello Hiero!");
118+
assertThat(consoleOutput)
119+
.as("Console output should contain 'Hello World!' message")
120+
.contains("Hello World!");
121+
122+
// Parse each line and verify log messages follow the expected pattern
123+
final String[] lines = consoleOutput.split("\n");
124+
for (final String line : lines) {
125+
if (line.trim().isEmpty()) {
126+
continue; // Skip empty lines
127+
}
128+
129+
// Check if this line is a log message (matches the log pattern)
130+
// Pattern: yyyy-MM-dd HH:mm:ss.SSS [thread] [optional marker] LEVEL logger - message
131+
// Example: 2025-10-22 16:29:13.345 [Test worker] INFO org.hiero.otter.fixtures... - ...
132+
if (isLogMessage(line)) {
133+
// Extract log level and logger name
134+
final String logLevel = extractLogLevel(line);
135+
final String loggerName = extractLoggerName(line);
136+
137+
// Verify log level is INFO or more critical (not DEBUG or TRACE)
138+
assertThat(logLevel)
139+
.as("Log message should have INFO or higher level: %s", line)
140+
.isIn("INFO", "WARN", "ERROR", "FATAL");
141+
}
142+
}
143+
144+
} finally {
145+
// Ensure System.out is always restored even if an exception occurs
146+
System.setOut(originalOut);
147+
}
148+
}
149+
}

platform-sdk/consensus-otter-tests/src/test/java/org/hiero/otter/fixtures/turtle/logging/TurtleHashstreamLogTest.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
import com.swirlds.logging.legacy.LogMarker;
1212
import edu.umd.cs.findbugs.annotations.NonNull;
13+
import java.io.IOException;
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
1516
import java.time.Duration;
1617
import java.util.List;
18+
import org.apache.logging.log4j.LogManager;
1719
import org.hiero.otter.fixtures.Network;
1820
import org.hiero.otter.fixtures.Node;
1921
import org.hiero.otter.fixtures.OtterAssertions;
@@ -54,7 +56,7 @@ final class TurtleHashstreamLogTest {
5456
*/
5557
@ParameterizedTest
5658
@ValueSource(ints = {1, 4})
57-
void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
59+
void testBasicHashstreamLogFunctionality(final int numNodes) throws IOException {
5860
final TestEnvironment env = new TurtleTestEnvironment();
5961
try {
6062
final Network network = env.network();
@@ -64,6 +66,11 @@ void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
6466

6567
network.start();
6668

69+
// Generate log messages in the test. These should not appear in the log.
70+
System.out.println("Hello Otter!");
71+
LogManager.getLogger().info("Hello Hiero!");
72+
LogManager.getLogger("com.acme.ExternalOtterTest").info("Hello World!");
73+
6774
// Let the nodes run for a bit to generate log messages
6875
timeManager.waitFor(Duration.ofSeconds(5L));
6976

@@ -112,6 +119,19 @@ void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
112119
assertThat(logContent)
113120
.as("Log should NOT contain TRACE level messages")
114121
.doesNotContainPattern("\\bTRACE\\b");
122+
123+
// Test Message Verification
124+
125+
// Verify that our test log messages do NOT appear in the log
126+
assertThat(logContent)
127+
.as("Log should NOT contain test log message 'Hello Otter!'")
128+
.doesNotContain("Hello Otter!");
129+
assertThat(logContent)
130+
.as("Log should NOT contain test log message 'Hello Hiero!'")
131+
.doesNotContain("Hello Hiero!");
132+
assertThat(logContent)
133+
.as("Log should NOT contain test log message 'Hello World!'")
134+
.doesNotContain("Hello World!");
115135
}
116136
} finally {
117137
env.destroy();

platform-sdk/consensus-otter-tests/src/test/java/org/hiero/otter/fixtures/turtle/logging/TurtleInMemoryLogTest.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.time.Duration;
1313
import java.util.List;
1414
import org.apache.logging.log4j.Level;
15+
import org.apache.logging.log4j.LogManager;
1516
import org.hiero.otter.fixtures.Network;
1617
import org.hiero.otter.fixtures.Node;
1718
import org.hiero.otter.fixtures.OtterAssertions;
@@ -59,7 +60,7 @@ final class TurtleInMemoryLogTest {
5960
*/
6061
@ParameterizedTest
6162
@ValueSource(ints = {1, 4})
62-
void testNodesLogAllMarkers(final int numNodes) throws Exception {
63+
void testBasicInMemoryLogging(final int numNodes) {
6364
final TestEnvironment env = new TurtleTestEnvironment();
6465
try {
6566
final Network network = env.network();
@@ -68,6 +69,11 @@ void testNodesLogAllMarkers(final int numNodes) throws Exception {
6869
final List<Node> nodes = network.addNodes(numNodes);
6970
network.start();
7071

72+
// Generate log messages in the test. These should not appear in the log.
73+
System.out.println("Hello Otter!");
74+
LogManager.getLogger().info("Hello Hiero!");
75+
LogManager.getLogger("com.acme.ExternalOtterTest").info("Hello World!");
76+
7177
// Let the nodes run for a bit to generate log messages
7278
timeManager.waitFor(Duration.ofSeconds(5L));
7379

@@ -119,6 +125,19 @@ void testNodesLogAllMarkers(final int numNodes) throws Exception {
119125
assertThat(hasTraceLogs)
120126
.as("Node %d in-memory log should NOT contain TRACE level messages", nodeId)
121127
.isFalse();
128+
129+
// Test Message Verification
130+
131+
// Verify that our test log messages do NOT appear in the log
132+
OtterAssertions.assertThat(logResult)
133+
.as("Log should NOT contain test log message 'Hello Otter!'")
134+
.hasNoMessageContaining("Hello Otter!");
135+
OtterAssertions.assertThat(logResult)
136+
.as("Log should NOT contain test log message 'Hello Hiero!'")
137+
.hasNoMessageContaining("Hello Hiero!");
138+
OtterAssertions.assertThat(logResult)
139+
.as("Log should NOT contain test log message 'Hello World!'")
140+
.hasNoMessageContaining("Hello World!");
122141
}
123142
} finally {
124143
env.destroy();
@@ -132,7 +151,7 @@ void testNodesLogAllMarkers(final int numNodes) throws Exception {
132151
* only contain logs with that node's ID, ensuring logs are not mixed between nodes.
133152
*/
134153
@Test
135-
void testPerNodeLogTracking() throws Exception {
154+
void testPerNodeLogTracking() {
136155
final TestEnvironment env = new TurtleTestEnvironment();
137156
try {
138157
final Network network = env.network();
@@ -193,14 +212,14 @@ void testPerNodeLogTracking() throws Exception {
193212
* </ul>
194213
*/
195214
@Test
196-
void testNetworkLogResults() throws Exception {
215+
void testNetworkLogResults() {
197216
final TestEnvironment env = new TurtleTestEnvironment();
198217
try {
199218
final Network network = env.network();
200219
final TimeManager timeManager = env.timeManager();
201220

202221
// Spin up 4 nodes
203-
final List<Node> nodes = network.addNodes(4);
222+
network.addNodes(4);
204223
network.start();
205224

206225
// Let the nodes run for a bit to generate log messages
@@ -240,7 +259,7 @@ void testNetworkLogResults() throws Exception {
240259
* </ul>
241260
*/
242261
@Test
243-
void testLogsAddedContinuously() throws Exception {
262+
void testLogsAddedContinuously() {
244263
final TestEnvironment env = new TurtleTestEnvironment();
245264
try {
246265
final Network network = env.network();

platform-sdk/consensus-otter-tests/src/test/java/org/hiero/otter/fixtures/turtle/logging/TurtleSwirdsLogTest.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
import com.swirlds.logging.legacy.LogMarker;
1212
import edu.umd.cs.findbugs.annotations.NonNull;
13+
import java.io.IOException;
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
1516
import java.time.Duration;
1617
import java.util.List;
18+
import org.apache.logging.log4j.LogManager;
1719
import org.hiero.otter.fixtures.Network;
1820
import org.hiero.otter.fixtures.Node;
1921
import org.hiero.otter.fixtures.OtterAssertions;
@@ -61,7 +63,7 @@ final class TurtleSwirdsLogTest {
6163
*/
6264
@ParameterizedTest
6365
@ValueSource(ints = {1, 4})
64-
void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
66+
void testBasicSwirldsLogFunctionality(final int numNodes) throws IOException {
6567
final TestEnvironment env = new TurtleTestEnvironment();
6668
try {
6769
final Network network = env.network();
@@ -70,6 +72,11 @@ void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
7072
final List<Node> nodes = network.addNodes(numNodes);
7173
network.start();
7274

75+
// Generate log messages in the test. These should not appear in the log.
76+
System.out.println("Hello Otter!");
77+
LogManager.getLogger().info("Hello Hiero!");
78+
LogManager.getLogger("com.acme.ExternalOtterTest").info("Hello World!");
79+
7380
// Let the nodes run for a bit to generate log messages
7481
timeManager.waitFor(Duration.ofSeconds(5L));
7582

@@ -118,6 +125,19 @@ void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
118125
assertThat(logContent)
119126
.as("Log should NOT contain TRACE level messages")
120127
.doesNotContainPattern("\\bTRACE\\b");
128+
129+
// Test Message Verification
130+
131+
// Verify that our test log messages do NOT appear in the log
132+
assertThat(logContent)
133+
.as("Log should NOT contain test log message 'Hello Otter!'")
134+
.doesNotContain("Hello Otter!");
135+
assertThat(logContent)
136+
.as("Log should NOT contain test log message 'Hello Hiero!'")
137+
.doesNotContain("Hello Hiero!");
138+
assertThat(logContent)
139+
.as("Log should NOT contain test log message 'Hello World!'")
140+
.doesNotContain("Hello World!");
121141
}
122142
} finally {
123143
env.destroy();
@@ -131,7 +151,7 @@ void testNodesLogAllAllowedMarkers(final int numNodes) throws Exception {
131151
* then checking that only that node's log contains the restart messages while other nodes' logs don't.
132152
*/
133153
@Test
134-
void testPerNodeLogRouting() throws Exception {
154+
void testPerNodeLogRouting() throws IOException {
135155
final TestEnvironment env = new TurtleTestEnvironment();
136156
try {
137157
final Network network = env.network();
@@ -157,10 +177,10 @@ void testPerNodeLogRouting() throws Exception {
157177
final long nodeId2 = node2.selfId().id();
158178
final long nodeId3 = node3.selfId().id();
159179

160-
final Path log0 = Path.of("build/turtle/node-" + nodeId0 + "/output/swirlds.log");
161-
final Path log1 = Path.of("build/turtle/node-" + nodeId1 + "/output/swirlds.log");
162-
final Path log2 = Path.of("build/turtle/node-" + nodeId2 + "/output/swirlds.log");
163-
final Path log3 = Path.of("build/turtle/node-" + nodeId3 + "/output/swirlds.log");
180+
final Path log0 = Path.of(String.format(LOG_DIR, nodeId0), LOG_FILENAME);
181+
final Path log1 = Path.of(String.format(LOG_DIR, nodeId1), LOG_FILENAME);
182+
final Path log2 = Path.of(String.format(LOG_DIR, nodeId2), LOG_FILENAME);
183+
final Path log3 = Path.of(String.format(LOG_DIR, nodeId3), LOG_FILENAME);
164184

165185
// Wait for initial log files to be created
166186
awaitFile(log0, Duration.ofSeconds(5L));

platform-sdk/consensus-otter-tests/src/testFixtures/java/org/hiero/otter/fixtures/app/services/consistency/ConsistencyServiceRoundHistory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,9 @@ public void onRoundComplete() {
230230
*/
231231
public void close() {
232232
try {
233-
writer.close();
233+
if (writer != null) {
234+
writer.close();
235+
}
234236
} catch (final IOException e) {
235237
throw new UncheckedIOException("Failed to close writer for transaction handling history", e);
236238
}

platform-sdk/consensus-otter-tests/src/testFixtures/java/org/hiero/otter/fixtures/logging/internal/LogConfigHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public final class LogConfigHelper {
6565

6666
/** Default pattern for text-based appenders. */
6767
public static final String DEFAULT_PATTERN =
68-
"%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] }%notEmpty{[%marker] }%-5level %logger{36} - %msg %n";
68+
"%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %notEmpty{[%marker] }%-5level %logger{36} - %msg %n";
6969

7070
private static final ObjectMapper objectMapper = new ObjectMapper();
7171

@@ -162,7 +162,7 @@ public static ComponentBuilder<?> createAllowedMarkerFilters(
162162
* @return a composite {@link ComponentBuilder} that suppresses unwanted markers
163163
*/
164164
@NonNull
165-
public static ComponentBuilder<?> creatIgnoreMarkerFilters(
165+
public static ComponentBuilder<?> createIgnoreMarkerFilters(
166166
@NonNull final ConfigurationBuilder<BuiltConfiguration> builder) {
167167
final ComponentBuilder<?> ignoredMarkerFilters = builder.newComponent("Filters");
168168
for (final LogMarker marker : IGNORED_CONSOLE_MARKERS) {

0 commit comments

Comments
 (0)