diff --git a/.gitignore b/.gitignore index 446fe4d..3e87e4b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ jbang.egg-info jbang/__pycache__ __pycache__ .coverage +.venv diff --git a/.justfile b/.justfile index 18a95eb..578e0db 100644 --- a/.justfile +++ b/.justfile @@ -1,5 +1,5 @@ test: - source .venv/bin/activate + .venv/bin/activate #pip3 install -e . uv pip install -e ".[test]" @@ -7,6 +7,6 @@ test: PATH=$(echo $PATH | tr ':' '\n' | grep -v "\.jbang/bin" | tr '\n' ':' | sed 's/:$//') .venv/bin/python -m pytest -o log_cli_level=DEBUG -o log_cli=true release: - source .venv/bin/activate + .venv/bin/activate uv pip install setuptools gh release create `python3 setup.py --version` --generate-notes diff --git a/jbang/__init__.py b/jbang/__init__.py index b08f380..70d5795 100644 --- a/jbang/__init__.py +++ b/jbang/__init__.py @@ -1,6 +1,6 @@ -from .jbang import exec, spawnSync, main, quote +from .jbang import exec, spawnSync, main, quote, popen -__all__ = ['exec', 'spawnSync', 'main', 'quote'] +__all__ = ['exec', 'spawnSync', 'main', 'quote', 'popen'] if __name__ == "__main__": main() \ No newline at end of file diff --git a/jbang/jbang.py b/jbang/jbang.py index c2b7245..6367d32 100644 --- a/jbang/jbang.py +++ b/jbang/jbang.py @@ -199,6 +199,30 @@ def spawnSync(args: Union[str, List[str]]) -> Any: 2 ) +def popen(args: Union[str, List[str]]) -> subprocess.Popen: + """Returns a reference to a subprocess.Popen instance for streaming stdout.""" + log.debug(f"trying to execute popen command: {args}") + + cmdLine = _getCommandLine(args) + + if not cmdLine: + print("Could not locate a way to run jbang. Try installing jbang manually and try again.") + raise Exception( + "Could not locate a way to run jbang. Try installing jbang manually and try again.", + 2 + ) + + return subprocess.Popen( + cmdLine, + shell=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + universal_newlines=True, + ) + def main(): """Command-line entry point for jbang-python.""" log.debug("Starting jbang-python CLI") diff --git a/tests/test_jbang.py b/tests/test_jbang.py index 8629bf1..2bf0592 100644 --- a/tests/test_jbang.py +++ b/tests/test_jbang.py @@ -1,10 +1,48 @@ import sys +from pathlib import Path import pytest import jbang from jbang.jbang import CommandResult +@pytest.fixture +def java_hello(tmp_path: Path): + code = """ + public class HelloWorld { + public static void main(String[] args) { + System.out.print("Hello, world!"); + } + } + """ + + file_path = tmp_path / "HelloWorld.java" + file_path.write_text(code.strip()) + return file_path + +@pytest.fixture +def streamable_java_hello(tmp_path: Path): + code = """ + import java.io.*; + import java.util.Random; + + public class HelloStreamable { + static final BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out), 1 << 20); + public static void main(String[] args) throws Exception { + String message = \"Hello, world!\"; + Random rand = new Random(); + for (int i = 0; i < message.length(); i++){ + out.write(message.charAt(i)); + out.newLine(); + out.flush(); + Thread.sleep(rand.nextInt(3)); + } + } + } + """ + file_path = tmp_path / "HelloStreamable.java" + file_path.write_text(code.strip()) + return file_path def test_version_command(): """Test version command.""" @@ -60,6 +98,23 @@ def test_invalid_java_version(): out = jbang.exec('--java invalid properties@jbangdev java.version') assert 'Invalid version' in out.stderr +def test_popen_single_write(java_hello): + process = jbang.popen(str(java_hello)) + process.wait() + + assert process.returncode == 0 + assert process.stdout.readlines()[0] == "Hello, world!" + +def test_popen_streamable_java_hello(streamable_java_hello): + process = jbang.popen(str(streamable_java_hello)) + rows = [] + message = "Hello, world!" + for i, line in enumerate(process.stdout): + assert line.rstrip("\n") == message[i] + + return_code = process.wait() + assert return_code == 0 + @pytest.mark.skipif(sys.platform == 'win32', reason="Quote tests behave differently on Windows") class TestQuoting: def test_quote_empty_string(self):