-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathPerlScriptExecutionTest.java
More file actions
346 lines (304 loc) · 14.3 KB
/
PerlScriptExecutionTest.java
File metadata and controls
346 lines (304 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package org.perlonjava;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.runtime.io.StandardIO;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeIO;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Test class for executing Perl scripts and verifying their output.
* This class uses JUnit 5 for testing and includes parameterized tests
* to run multiple Perl scripts located in the resources directory.
*/
public class PerlScriptExecutionTest {
static {
// Set default locale to US (uses dot as decimal separator)
// This ensures consistent number formatting across different environments
Locale.setDefault(Locale.US);
}
private PrintStream originalOut; // Stores the original System.out
private ByteArrayOutputStream outputStream; // Captures the output of the Perl script execution
/**
* Provides a stream of Perl script filenames located in the resources directory.
*
* @return a Stream of Perl script filenames.
* @throws IOException if an I/O error occurs while accessing the resources.
*/
static Stream<String> providePerlScripts() throws IOException {
return getPerlScripts(false);
}
/**
* Provides a stream of unit test Perl script filenames (only from unit/ directory).
* These are fast-running tests.
*
* @return a Stream of unit test Perl script filenames.
* @throws IOException if an I/O error occurs while accessing the resources.
*/
static Stream<String> provideUnitTestScripts() throws IOException {
return getPerlScripts(true);
}
/**
* Helper method to get Perl scripts, optionally filtered to unit tests only.
*
* @param unitOnly if true, only return scripts from unit/ directory
* @return a Stream of Perl script filenames.
* @throws IOException if an I/O error occurs while accessing the resources.
*/
private static Stream<String> getPerlScripts(boolean unitOnly) throws IOException {
// Locate the test resources directory by finding a known resource first
URL resourceUrl = PerlScriptExecutionTest.class.getClassLoader().getResource("unit/array.t");
if (resourceUrl == null) {
throw new IOException("Resource directory not found");
}
Path resourcePath;
try {
// Get the parent directory twice to get to src/test/resources root
resourcePath = Paths.get(resourceUrl.toURI()).getParent().getParent();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
// Return a stream of filenames for Perl scripts
Stream<Path> pathStream = Files.walk(resourcePath)
.filter(path -> path.toString().endsWith(".t"));
if (unitOnly) {
// Only include tests from the unit/ directory
pathStream = pathStream.filter(path -> {
Path relative = resourcePath.relativize(path);
String pathStr = relative.toString();
return pathStr.startsWith("unit/") || pathStr.startsWith("unit\\");
});
}
List<String> sortedScripts = pathStream
.map(resourcePath::relativize) // Get the relative path
.map(Path::toString) // Convert to string path
.sorted() // Ensure deterministic order
.collect(Collectors.toList());
String testFilter = System.getenv("JPERL_TEST_FILTER");
if (testFilter != null && !testFilter.isEmpty()) {
sortedScripts = sortedScripts.stream()
.filter(s -> s.contains(testFilter))
.collect(Collectors.toList());
if (sortedScripts.isEmpty()) {
throw new IOException("No tests matched JPERL_TEST_FILTER='" + testFilter + "'");
}
return sortedScripts.stream();
}
// Sharding logic.
//
// We use a simple "heavy-test isolation" scheme for balance:
// - The LAST shard is dedicated to known-heavy tests (HEAVY_TESTS).
// - All other shards round-robin the remaining (light) tests.
//
// Rationale: a single test (currently unit/code_too_large.t at ~16s)
// can dominate one shard's wall time well beyond what round-robin
// can balance. Putting it on its own shard lets the others split
// the rest evenly. To extend, just add filenames to HEAVY_TESTS.
String shardIndexProp = System.getProperty("test.shard.index");
String shardTotalProp = System.getProperty("test.shard.total");
if (shardIndexProp != null && !shardIndexProp.isEmpty() &&
shardTotalProp != null && !shardTotalProp.isEmpty()) {
try {
int shardIndex = Integer.parseInt(shardIndexProp);
int shardTotal = Integer.parseInt(shardTotalProp);
// Both Gradle and Maven now pass 0-indexed shard.index values
// (see build.gradle and pom.xml). Earlier code attempted to
// detect Maven 1-indexed values heuristically, which silently
// collapsed shard 1 onto shard 0 and dropped the last shard
// entirely; do not reintroduce that.
if (shardTotal > 1 && shardIndex >= 0 && shardIndex < shardTotal) {
System.out.println("Running shard " + (shardIndex + 1) + " of " + shardTotal);
// Tests known to be much slower than the rest. Use forward
// slashes; we match against both '/' and '\' separators.
final List<String> HEAVY_TESTS = List.of(
"unit/code_too_large.t"
);
java.util.function.Predicate<String> isHeavy = s -> {
String norm = s.replace('\\', '/');
return HEAVY_TESTS.contains(norm);
};
final int lastShard = shardTotal - 1;
if (shardIndex == lastShard) {
// Dedicated shard: only the heavy tests.
return sortedScripts.stream().filter(isHeavy);
}
// Light shards: round-robin over the remaining (shardTotal - 1) shards.
List<String> lightScripts = sortedScripts.stream()
.filter(isHeavy.negate())
.collect(Collectors.toList());
final int lightShards = shardTotal - 1;
final int finalShardIndex = shardIndex;
return IntStream.range(0, lightScripts.size())
.filter(i -> i % lightShards == finalShardIndex)
.mapToObj(lightScripts::get);
}
} catch (NumberFormatException e) {
// Silently fall through to run all tests
}
}
return sortedScripts.stream();
}
/**
* Sets up the test environment by redirecting System.out to a custom output stream.
* This allows capturing the output of the Perl script execution.
*/
@BeforeEach
void setUp() {
originalOut = System.out;
outputStream = new ByteArrayOutputStream();
// Create a new StandardIO with the capture stream
StandardIO newStdout = new StandardIO(outputStream, true);
// Replace RuntimeIO.stdout with a new instance
RuntimeIO.stdout = new RuntimeIO(newStdout);
// Keep Perl's global *STDOUT/*STDERR in sync with the RuntimeIO static fields.
// Some tests call `binmode STDOUT/STDERR` and expect it to affect the real globals.
GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout);
GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr);
// Also update System.out for any direct Java calls
System.setOut(new PrintStream(outputStream));
}
/**
* Restores the original System.out after each test execution.
*/
@AfterEach
void tearDown() {
// Restore original stdout
RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true));
GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout);
GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr);
System.setOut(originalOut);
}
/**
* Gets the root cause of an exception by traversing the cause chain.
*
* @param throwable the exception to analyze
* @return the root cause of the exception
*/
private Throwable getRootCause(Throwable throwable) {
Throwable rootCause = throwable;
while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
rootCause = rootCause.getCause();
}
return rootCause;
}
/**
* Parameterized test that executes unit test Perl scripts and verifies their output.
* Only runs tests from the unit/ directory (fast tests).
* The test fails if the output contains "not ok" at the beginning of a line,
* which indicates a failed test in TAP (Test Anything Protocol) format.
*
* @param filename the name of the Perl script file to be executed.
*/
@ParameterizedTest(name = "Unit test: {0}")
@MethodSource("provideUnitTestScripts")
@Tag("unit")
void testUnitTests(String filename) {
executeTest(filename);
}
/**
* Parameterized test that executes all Perl scripts and verifies their output.
* Runs all tests including comprehensive module tests (slower).
* The test fails if the output contains "not ok" at the beginning of a line,
* which indicates a failed test in TAP (Test Anything Protocol) format.
*
* @param filename the name of the Perl script file to be executed.
*/
@ParameterizedTest(name = "Full test: {0}")
@MethodSource("providePerlScripts")
@Tag("full")
void testAllTests(String filename) {
executeTest(filename);
}
/**
* Executes a single Perl test file and verifies the output.
*
* @param filename the name of the Perl script file to be executed.
*/
private void executeTest(String filename) {
// Load the Perl script as an InputStream
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(filename);
assertNotNull(inputStream, "Resource file not found: " + filename);
try {
PerlLanguageProvider.resetAll();
// Read the content of the Perl script with UTF-8 encoding
String content = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
// Normalize CRLF to LF, matching what readFileWithEncodingDetection() does
// for source files loaded by ./jperl. On Windows, Git checks out files with
// CRLF line endings, but the Lexer expects LF-only line endings.
if (content.indexOf('\r') >= 0) {
content = content.replace("\r\n", "\n").replace("\r", "\n");
}
CompilerOptions options = new CompilerOptions();
options.code = content; // Set the code to be executed
options.fileName = filename; // Set the filename for reference
// Add the path to the Perl modules
RuntimeArray.push(options.inc, new RuntimeScalar("src/main/perl/lib"));
// Execute the Perl code
PerlLanguageProvider.executePerlCode(options, true);
// Capture and verify the output
String output = outputStream.toString();
int lineNumber = 0;
boolean errorFound = false;
for (String line : output.lines().toList()) {
lineNumber++;
// Check for test failures - works with both Test::More TAP format and simple tests
// Skip TODO tests (they are expected to fail) - TAP format: "not ok ... # TODO ..."
if (line.trim().startsWith("not ok") && !line.contains("# TODO")) {
errorFound = true;
fail("Test failure in " + filename + " at line " + lineNumber + ": " + line);
break;
}
// Check for bail out (TAP format)
if (line.trim().startsWith("Bail out!")) {
fail("Test bailed out in " + filename + " at line " + lineNumber + ": " + line);
break;
}
}
if (!errorFound) {
assertFalse(errorFound, "Output should not contain test failures in " + filename);
}
} catch (Exception e) {
// Get the root cause and print its stack trace
Throwable rootCause = getRootCause(e);
System.err.println("Root cause error in " + filename + ":");
rootCause.printStackTrace(System.err);
String msg = rootCause.getMessage();
if (msg == null || msg.isEmpty()) {
msg = rootCause.toString();
}
fail("Execution of " + filename + " failed: " + msg);
}
}
/**
* Test to verify the availability of a specific resource file.
* Ensures that the 'unit/array.t' file is present in the resources.
*/
@Test
void testResourceAvailability() {
// Check if the 'unit/array.t' resource file is available
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("unit/array.t");
assertNotNull(inputStream, "Resource file 'unit/array.t' should be available");
}
}