diff --git a/AGENTS.md b/AGENTS.md index 12c8e845f..fa7765a43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,6 +97,30 @@ The runner: - Has a 300s timeout per test - Reports pass/fail counts in format: `passed/total` - Saves results to `test_results_YYYYMMDD_HHMMSS.txt` +- Sets required environment variables automatically (see below) + +#### Running Tests Directly (without perl_test_runner.pl) + +If you run tests directly with `./jperl`, you may need to set these environment variables: + +```bash +# For tests that use unimplemented features (re/pat.t, op/pack.t, etc.) +# Without this, unimplemented features cause fatal errors +export JPERL_UNIMPLEMENTED=warn + +# For memory-intensive tests (re/pat.t, op/repeat.t, op/list.t) +# Increases JVM stack size to prevent StackOverflowError +export JPERL_OPTS="-Xss256m" + +# Skip tests with 300KB+ strings that crash the JVM +export PERL_SKIP_BIG_MEM_TESTS=1 + +# Example: running re/pat.t directly +cd perl5_t/t +JPERL_UNIMPLEMENTED=warn JPERL_OPTS="-Xss256m" PERL_SKIP_BIG_MEM_TESTS=1 ../../jperl re/pat.t +``` + +The perl_test_runner.pl sets these automatically based on the test file being run. ### Git Workflow diff --git a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java index 6e934abab..2235d8a9c 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java @@ -1,5 +1,7 @@ package org.perlonjava.runtime.io; +import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache; @@ -91,6 +93,9 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException { String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); + // Start the process process = processBuilder.start(); @@ -348,4 +353,29 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(); // undef } } + + /** + * Copies the Perl %ENV hash to the ProcessBuilder environment. + * This ensures that changes to %ENV in Perl are reflected in child processes. + * + * @param processBuilder The ProcessBuilder to update + */ + private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) { + try { + RuntimeHash envHash = GlobalVariable.getGlobalHash("main::ENV"); + java.util.Map pbEnv = processBuilder.environment(); + + // Clear the inherited environment and replace with Perl's %ENV + pbEnv.clear(); + + for (java.util.Map.Entry entry : envHash.elements.entrySet()) { + String value = entry.getValue().toString(); + if (value != null) { + pbEnv.put(entry.getKey(), value); + } + } + } catch (Exception e) { + // If we can't access %ENV, just use inherited environment (default behavior) + } + } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java index 0da23ce4c..4b734b746 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java @@ -1,5 +1,7 @@ package org.perlonjava.runtime.io; +import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache; @@ -150,6 +152,9 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException { String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); + // Start the process process = processBuilder.start(); @@ -384,4 +389,29 @@ public RuntimeScalar syswrite(String data) { return new RuntimeScalar(); // undef } } + + /** + * Copies the Perl %ENV hash to the ProcessBuilder environment. + * This ensures that changes to %ENV in Perl are reflected in child processes. + * + * @param processBuilder The ProcessBuilder to update + */ + private void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) { + try { + RuntimeHash envHash = GlobalVariable.getGlobalHash("main::ENV"); + java.util.Map pbEnv = processBuilder.environment(); + + // Clear the inherited environment and replace with Perl's %ENV + pbEnv.clear(); + + for (java.util.Map.Entry entry : envHash.elements.entrySet()) { + String value = entry.getValue().toString(); + if (value != null) { + pbEnv.put(entry.getKey(), value); + } + } + } catch (Exception e) { + // If we can't access %ENV, just use inherited environment (default behavior) + } + } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index ae7831f1e..9cd8b0661 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -2211,6 +2211,27 @@ public static RuntimeIO openFileHandleDup(String fileName, String mode) { break; default: // Try to look up as a global filehandle + // First, try the current package if no :: qualifier is present + if (!fileName.contains("::")) { + // Use RuntimeCode.getCurrentPackage() which uses caller() to determine + // the current package - this works for both JVM-compiled and interpreter code + String currentPkg = RuntimeCode.getCurrentPackage(); + // Remove trailing "::" for consistent naming + if (currentPkg.endsWith("::")) { + currentPkg = currentPkg.substring(0, currentPkg.length() - 2); + } + if (currentPkg != null && !currentPkg.isEmpty() && !currentPkg.equals("main")) { + String currentPkgName = currentPkg + "::" + fileName; + RuntimeGlob currentGlob = GlobalVariable.getGlobalIO(currentPkgName); + if (currentGlob != null) { + sourceHandle = currentGlob.getRuntimeIO(); + if (sourceHandle != null && sourceHandle.ioHandle != null) { + break; // Found it in current package + } + } + } + } + // Fall back to main:: or fully qualified name String normalizedName = fileName.contains("::") ? fileName : "main::" + fileName; RuntimeGlob glob = GlobalVariable.getGlobalIO(normalizedName); if (glob != null) { diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index af8681ed7..4e00eac7c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -557,9 +557,19 @@ private static RuntimeScalar completeForkOpen(List flattenedArgs, boolea try { flushAllHandles(); - // Build the command + // Build the command - mirror the logic from exec() for consistency List command; - if (!hasHandle && flattenedArgs.size() == 1) { + if (hasHandle && flattenedArgs.size() >= 2) { + // Indirect object syntax: exec { $program } @args + // flattenedArgs[0] is the program from the indirect object + // flattenedArgs[1:] are the arguments from @args + // In Perl, @args[0] becomes argv[0] (process name), @args[1:] are actual arguments + // Java's ProcessBuilder can't set argv[0] separately, so we skip it + String program = flattenedArgs.get(0); + command = new ArrayList<>(); + command.add(program); + command.addAll(flattenedArgs.subList(2, flattenedArgs.size())); + } else if (!hasHandle && flattenedArgs.size() == 1) { String cmdStr = flattenedArgs.getFirst(); if (SHELL_METACHARACTERS.matcher(cmdStr).find()) { // Use shell for metacharacters diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java index 9d74294d3..55b480acf 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -81,6 +81,15 @@ public static RuntimeList canonpath(RuntimeArray args, int ctx) { String quotedSeparator = Matcher.quoteReplacement(File.separator); String canonPath = path.replaceAll("[/\\\\]+", quotedSeparator) .replaceAll(Pattern.quote(File.separator) + "\\." + Pattern.quote(File.separator), quotedSeparator); + + // Remove leading ./ unless the path is exactly "./" + // This matches Perl's File::Spec::Unix behavior + if (!canonPath.equals("." + File.separator)) { + while (canonPath.startsWith("." + File.separator)) { + canonPath = canonPath.substring(2); + } + } + return new RuntimeScalar(canonPath).getList(); } diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java index 09bea7374..737b8179c 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java @@ -55,6 +55,7 @@ public class RegexPreprocessor { static int captureGroupCount; static boolean deferredUnicodePropertyEncountered; static boolean inlinePFlagEncountered; + static boolean branchResetEncountered; static void markDeferredUnicodePropertyEncountered() { deferredUnicodePropertyEncountered = true; @@ -68,6 +69,10 @@ static boolean hadInlinePFlag() { return inlinePFlagEncountered; } + static boolean hadBranchReset() { + return branchResetEncountered; + } + /** * Preprocesses a given regex string to make it compatible with Java's regex engine. * This involves handling various constructs and escape sequences that Java does not @@ -82,6 +87,7 @@ static String preProcessRegex(String s, RegexFlags regexFlags) { captureGroupCount = 0; deferredUnicodePropertyEncountered = false; inlinePFlagEncountered = false; + branchResetEncountered = false; // First, escape invalid quantifier braces (Perl compatibility) // DISABLED: Causes test regressions - needs more work @@ -1153,6 +1159,9 @@ private static int handleNamedCapture(int c, String s, int offset, int length, S * @return New offset after processing the branch reset group */ private static int handleBranchReset(String s, int offset, int length, StringBuilder sb, RegexFlags regexFlags) { + // Mark that this pattern uses branch reset + branchResetEncountered = true; + // Save the starting group count int startGroupCount = captureGroupCount; diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 28b8d8891..03f6d0760 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -81,6 +81,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { private boolean matched = false; private boolean hasCodeBlockCaptures = false; // True if regex has (?{...}) code blocks private boolean deferredUserDefinedUnicodeProperties = false; + private boolean hasBranchReset = false; // True if pattern uses (?|...) branch reset public RuntimeRegex() { this.regexFlags = null; @@ -145,6 +146,7 @@ public static RuntimeRegex compile(String patternString, String modifiers) { // These need to be resolved later, once the corresponding Perl subs are defined. regex.deferredUserDefinedUnicodeProperties = RegexPreprocessor.hadDeferredUnicodePropertyEncountered(); regex.hasPreservesMatch = regex.regexFlags.preservesMatch() || RegexPreprocessor.hadInlinePFlag(); + regex.hasBranchReset = RegexPreprocessor.hadBranchReset(); regex.patternString = patternString; @@ -620,7 +622,17 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc if (ctx == RuntimeContextType.LIST) { for (int i = 1; i <= captureCount; i++) { String matchedStr = matcher.group(i); - if (matchedStr != null) { + if (regex.hasBranchReset) { + // For branch reset patterns (?|...), skip null groups + // because Java creates separate groups for each alternative + // but Perl reuses group numbers across alternatives + if (matchedStr != null) { + matchedGroups.add(new RuntimeScalar(matchedStr)); + } + } else { + // Include undef for groups that didn't participate in the match + // This is important for patterns like m{^(.*/)?(.*)}s where + // the optional group returns undef when it doesn't match matchedGroups.add(new RuntimeScalar(matchedStr)); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 33ef879a0..0d7e1e533 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -421,6 +421,11 @@ public RuntimeArray getGlobArray() { } public RuntimeGlob setIO(RuntimeScalar io) { + // Check if the current IO is the selected handle - if so, update it + RuntimeIO oldIO = null; + if (this.IO.value instanceof RuntimeIO) { + oldIO = (RuntimeIO) this.IO.value; + } // If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely // Otherwise use set() to modify in place, preserving sharing with detached copies if (this.IO.type == RuntimeScalarType.TIED_SCALAR) { @@ -432,6 +437,10 @@ public RuntimeGlob setIO(RuntimeScalar io) { // If the IO scalar contains a RuntimeIO, set its glob name if (io.value instanceof RuntimeIO runtimeIO) { runtimeIO.globName = this.globName; + // Update selectedHandle if the old IO was the selected handle + if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = runtimeIO; + } } return this; } @@ -439,6 +448,11 @@ public RuntimeGlob setIO(RuntimeScalar io) { public RuntimeGlob setIO(RuntimeIO io) { // Set the glob name in the RuntimeIO for proper stringification io.globName = this.globName; + // Check if the current IO is the selected handle - if so, update it + RuntimeIO oldIO = null; + if (this.IO.value instanceof RuntimeIO) { + oldIO = (RuntimeIO) this.IO.value; + } // If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely // Otherwise modify in place, preserving sharing with detached copies if (this.IO.type == RuntimeScalarType.TIED_SCALAR) { @@ -447,6 +461,12 @@ public RuntimeGlob setIO(RuntimeIO io) { this.IO.type = RuntimeScalarType.GLOB; // RuntimeIO is stored as GLOB type this.IO.value = io; } + // Update selectedHandle if the old IO was the selected handle + // This ensures that when STDOUT is redirected, print without explicit + // filehandle uses the new handle + if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { + RuntimeIO.selectedHandle = io; + } return this; } diff --git a/src/main/perl/lib/Test/Harness.pm b/src/main/perl/lib/Test/Harness.pm index e197fbb22..a76552c44 100644 --- a/src/main/perl/lib/Test/Harness.pm +++ b/src/main/perl/lib/Test/Harness.pm @@ -249,6 +249,10 @@ sub _new_harness { sub _filtered_inc { my @inc = grep { !ref } @INC; #28567 + # PerlOnJava: Filter out jar: paths - these are internal markers for + # modules bundled in the JAR and don't exist as filesystem directories + @inc = grep { !/^jar:/ } @inc; + if (IS_VMS) { # VMS has a 255-byte limit on the length of %ENV entries, so @@ -279,6 +283,15 @@ sub _filtered_inc { shift @default_inc while @default_inc and $seen{ $default_inc[0] }; } + # Convert relative paths to absolute paths so they work in child processes + # that may run from a different directory + require Cwd; + @new_inc = map { + # Skip if already absolute or doesn't exist + ($_ =~ m{^/} || $_ =~ m{^[A-Za-z]:}) ? $_ : + (-e $_) ? Cwd::abs_path($_) // $_ : $_ + } @new_inc; + return @new_inc; } @@ -297,6 +310,8 @@ sub _filtered_inc { # Avoid using -l for the benefit of Perl 6 chomp( @inc = `"$perl" -e "print join qq[\\n], \@INC, q[]"` ); + # PerlOnJava: Filter out jar: paths from default @INC + @inc = grep { !/^jar:/ } @inc; return @inc; } } diff --git a/src/main/perl/lib/blib.pm b/src/main/perl/lib/blib.pm index f8fd500d5..f3cd1977e 100644 --- a/src/main/perl/lib/blib.pm +++ b/src/main/perl/lib/blib.pm @@ -82,6 +82,15 @@ sub import if (-d $blib && -d $blib_arch && -d $blib_lib) { unshift(@INC,$blib_arch,$blib_lib); + # PerlOnJava: Also set PERL5LIB so child processes can find modules. + # This is needed because PerlOnJava can't use fork() to share address space. + my $sep = $^O eq 'MSWin32' ? ';' : ':'; + my $new_perl5lib = join($sep, $blib_arch, $blib_lib); + if (exists $ENV{PERL5LIB} && defined $ENV{PERL5LIB} && $ENV{PERL5LIB} ne '') { + $ENV{PERL5LIB} = $new_perl5lib . $sep . $ENV{PERL5LIB}; + } else { + $ENV{PERL5LIB} = $new_perl5lib; + } warn "Using $blib\n" if $Verbose; return; }