Skip to content

Commit 3ed60dd

Browse files
authored
Test reading and processing JBang metadata blocks (#20)
We make JythonCli testable with JUnit, by changing readJBangBlock to accept a Reader. Then we give it test strings that express mistakes a programmer might make in the jbang block. We expect corresponding kinds of failure. We add a short entry to the README about using it. Failing tests are disabled until addressed by parser change.
1 parent 1b288e6 commit 3ed60dd

File tree

3 files changed

+153
-21
lines changed

3 files changed

+153
-21
lines changed

JythonCli.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
//DEPS org.tomlj:tomlj:1.1.1
44

55
import java.io.*;
6-
import java.nio.file.*;
76
import java.util.*;
87

98
import org.tomlj.Toml;
@@ -92,7 +91,7 @@ void initEnvironment(String[] args) throws IOException {
9291
}
9392

9493
for (String arg : args) {
95-
if (scriptFilename==null && arg.endsWith(".py")) {
94+
if (scriptFilename == null && arg.endsWith(".py")) {
9695
scriptFilename = arg;
9796
jythonArgs.add(arg);
9897
} else if ("--cli-debug".equals(arg)) {
@@ -104,21 +103,21 @@ void initEnvironment(String[] args) throws IOException {
104103
}
105104

106105
/**
107-
* Read the jbang block from the Jython script specified on the command-line
108-
* containing (optional) and interpret it as TOML data. The runtime options
109-
* that are extracted from the TOML data will override default version
110-
* specifications determined earlier.
106+
* Read a script and parse out a {@code jbang} block if possible,
107+
* later to be interpreted as TOML data. Errors to do with framing
108+
* the block are detected here, while errors in content must wait.
111109
*
110+
* @param script supplying text of the script
112111
* @throws IOException
113112
*/
114-
void readJBangBlock() throws IOException {
113+
void readJBangBlock(Reader script) throws IOException {
115114

116115
// Extract TOML data as a String
117-
List<String> lines = Files.readAllLines(Paths.get(scriptFilename));
116+
LineNumberReader lines = new LineNumberReader(script);
117+
String line;
118118
boolean found = false;
119-
int lineno = 0;
120-
for (String line : lines) {
121-
lineno++;
119+
while ((line = lines.readLine())!=null) {
120+
int lineno = lines.getLineNumber();
122121
if (found && !line.startsWith("# ")) {
123122
found = false;
124123
tomlText = new StringBuilder();
@@ -259,7 +258,7 @@ void printIfDebug(String text) {
259258
* @throws IOException
260259
* @throws InterruptedException
261260
*/
262-
public static void main(String[] args) throws IOException, InterruptedException {
261+
public static void main(String[] args) {
263262
// Create an instance of the class in which to compose argument list
264263
JythonCli jythonCli = new JythonCli();
265264

@@ -268,7 +267,10 @@ public static void main(String[] args) throws IOException, InterruptedException
268267

269268
// Normally we have a script file (but it's optional)
270269
if (jythonCli.scriptFilename != null) {
271-
jythonCli.readJBangBlock();
270+
Reader script = new BufferedReader(
271+
new InputStreamReader(
272+
new FileInputStream(jythonCli.scriptFilename)));
273+
jythonCli.readJBangBlock(script);
272274
jythonCli.interpretJBangBlock();
273275
}
274276

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,19 @@ jbang run jython-cli@jython turtle.py
221221

222222
## Development testing
223223

224+
### Systematic
225+
226+
The JUnit test that will run when a PR is submitted may be used locally:
227+
```
228+
jbang --java 17 TestJythonCli.java execute --disable-ansi-colors --select-class=TestJythonCli
229+
```
230+
This makes a fairly thorough test of the parsing of JBang specification blocks.
231+
It has no coverage of actually running a script.
232+
233+
The test won't currently compile with less than Java 17.
234+
235+
### Ad Hoc
236+
224237
If the `jython_cli.java` program is modified and needs to be tested (before changes
225238
are submitted to the repo), the example scripts can be used as tests and run
226239
locally (using Java 21):

TestJythonCli.java

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
2-
///usr/bin/env jbang "$0" "$@" ; exit $?
1+
/// usr/bin/env jbang "$0" "$@" ; exit $?
32

43
//SOURCES JythonCli.java
54

@@ -8,20 +7,25 @@
87
//DEPS org.junit.platform:junit-platform-console:1.13.3
98

109
import java.io.IOException;
10+
import java.io.StringReader;
1111

1212
import static org.junit.jupiter.api.Assertions.*;
13-
import org.junit.jupiter.api.Test;
1413

14+
import org.junit.jupiter.api.Disabled;
15+
import org.junit.jupiter.api.Test;
1516
import org.junit.platform.console.ConsoleLauncher;
1617

1718
/**
18-
* A class to run tests on aspects of {@link JythonCli} by delegating to the
19-
* JUnit console {@link ConsoleLauncher}.
19+
* A class to run tests on aspects of {@link JythonCli} by delegating to
20+
* the JUnit console {@link ConsoleLauncher}.
2021
*/
2122
public class TestJythonCli {
2223

23-
static final String[] ARGS_DEBUG_FOO = {"--cli-debug", "foo.py", "bar", "baz"};
24-
static final String[] ARGS_FOO = {"--version", "foo.py", "bar.py", "baz"};
24+
static final String[] ARGS_DEBUG_FOO =
25+
{"--cli-debug", "foo.py", "bar", "baz"};
26+
static final String[] ARGS_FOO =
27+
{"--version", "foo.py", "bar.py", "baz"};
28+
static final String[] ARGS_NONE = {"--cli-debug"};
2529

2630
/** The {@code --debug-cli} flag is spotted */
2731
@Test
@@ -40,7 +44,7 @@ void testScriptFound() throws IOException {
4044
assertEquals("foo.py", cli.scriptFilename);
4145
}
4246

43-
/** Argumnents to the Jython command are assembled in order. */
47+
/** Arguments to the Jython command are assembled in order. */
4448
@Test
4549
void testJythonArgs() throws IOException {
4650
JythonCli cli = new JythonCli();
@@ -51,6 +55,119 @@ void testJythonArgs() throws IOException {
5155
assertEquals("baz", cli.jythonArgs.get(3));
5256
}
5357

58+
/** An unterminated {@code jbang} block is an error. */
59+
@Test
60+
@Disabled("readJBangBlock does not throw on an unterminated block")
61+
void testUnterminated() throws IOException {
62+
String script = """
63+
# /// jbang
64+
# requires-jython = "2.7.2"
65+
# requires-java = "17"
66+
import sys
67+
""";
68+
JythonCli cli = new JythonCli();
69+
assertThrows(Exception.class, () -> processScript(cli, script));
70+
}
71+
72+
/**
73+
* An unterminated block may gobble up a {@code jbang} block. This
74+
* is not detectable by {@link JythonCli} as the text of a
75+
* {@code jbang} header could be legitimate content.
76+
*/
77+
@Test
78+
void testGobbledBlock() throws IOException {
79+
JythonCli cli = new JythonCli();
80+
processScript(cli, """
81+
# /// script
82+
# requires-python = ">=3.11"
83+
# /// jbang
84+
# requires-jython = "2.7.2"
85+
# requires-java = "8"
86+
# ///
87+
""");
88+
assertTrue(cli.tomlText.isEmpty(), "Check TOML text is empty");
89+
assertNull(cli.tpr, "Check TOML parse not done");
90+
}
91+
92+
/**
93+
* An unterminated {@code jbang} block should gobble up a following
94+
* block. This ought to be detectable by {@link JythonCli}. It isn't
95+
* actually a fault with the block delimiting: but the gobbled
96+
* block-start is not valid TOML.
97+
*/
98+
@Test
99+
@Disabled("interpretJBangBlock does not throw for invalid TOML")
100+
void testCollision() throws IOException {
101+
String script = """
102+
# /// jbang
103+
# requires-jython = "2.7.2"
104+
# requires-java = "8"
105+
# /// script
106+
# requires-python = ">=3.11"
107+
# ///
108+
""";
109+
JythonCli cli = new JythonCli();
110+
assertThrows(Exception.class, () -> processScript(cli, script));
111+
assertFalse(cli.tomlText.isEmpty(), "Detect TOML text is empty");
112+
assertTrue(cli.tpr.hasErrors(), "Check TOML parse reports errors");
113+
}
114+
115+
/** Two {@code jbang} blocks is an error. */
116+
@Test
117+
@Disabled("readJBangBlock does not throw on a second jbang block")
118+
void testTwoBlocks() throws IOException {
119+
String script = """
120+
# /// jbang
121+
# requires-jython = "2.7.2"
122+
# requires-java = "8"
123+
# ///
124+
125+
# Valid but not for us
126+
# /// script
127+
# requires-python = ">=3.11"
128+
# ///
129+
130+
# /// jbang
131+
# requires-jython = "2.7.3"
132+
# ///
133+
""";
134+
JythonCli cli = new JythonCli();
135+
assertThrows(Exception.class, () -> processScript(cli, script));
136+
}
137+
138+
/** Invalid TOML is an error. */
139+
@Test
140+
@Disabled("interpretJBangBlock does not throw for invalid TOML")
141+
void testInvalidTOML() throws IOException {
142+
String script = """
143+
# /// jbang
144+
# requires-java = "8"
145+
# stuff = {
146+
# nonsense = 42
147+
# Quatsch =::
148+
# }
149+
# ///
150+
print("Hello World!")
151+
""";
152+
JythonCli cli = new JythonCli();
153+
assertThrows(Exception.class, () -> processScript(cli, script));
154+
}
155+
156+
/**
157+
* Take an initialised {@link JythonCli} and have it process (but
158+
* not run) the given {@code String} as if the contents of a file.
159+
*
160+
* @param cli to exercise
161+
* @param script to process as script
162+
* @throws IOException on StringReader errors
163+
*/
164+
void processScript(JythonCli cli, String script)
165+
throws IOException {
166+
cli.initEnvironment(ARGS_NONE);
167+
cli.readJBangBlock(new StringReader(script));
168+
cli.interpretJBangBlock();
169+
}
170+
54171
/**
55172
* Run the JUnit console with arguments from our command line.
56173
*

0 commit comments

Comments
 (0)