This is an automated email from the ASF dual-hosted git repository. paulk-asert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 49b577350e893cd20e1f6002cfeaad02864e50bb Author: Paul King <[email protected]> AuthorDate: Sat May 9 08:37:47 2026 +1000 groovysh additional tests --- subprojects/groovy-groovysh/build.gradle | 12 ++ .../groovy/org/apache/groovy/groovysh/Main.groovy | 4 +- .../{TypesTest.groovy => ClassLoaderTest.groovy} | 37 +++--- .../groovysh/commands/ConsoleTestSupport.groovy | 19 ++- .../apache/groovy/groovysh/commands/DelTest.groovy | 56 +++++++++ .../groovysh/commands/GrabCommandTest.groovy | 85 ------------- .../groovy/groovysh/commands/GrabTest.groovy | 53 ++++++++ .../commands/GroovyCommandTestSupport.groovy | 64 ---------- .../groovysh/commands/HelpCommandTest.groovy | 33 +++-- .../groovy/groovysh/commands/InspectTest.groovy | 53 ++++++++ .../groovy/groovysh/commands/MethodsTest.groovy | 9 ++ .../groovy/groovysh/commands/PipeTest.groovy | 61 +++++++++ .../groovy/groovysh/commands/SaveLoadTest.groovy | 76 ++++++++++++ .../groovysh/commands/SlurpCsvFallbackTest.groovy | 79 ++++++++++++ .../groovy/groovysh/commands/SlurpTest.groovy | 74 +++++++++++ .../groovysh/commands/SystemTestSupport.groovy | 72 ++++++++++- .../groovy/groovysh/commands/TypesTest.groovy | 9 ++ .../groovy/groovysh/jline/GroovyEngineTest.groovy | 73 +++++++++++ .../groovysh/jline/GroovyPosixCommandsTest.groovy | 138 +++++++++++++++++++++ .../groovysh/jline/GroovySystemRegistryTest.groovy | 74 +++++++++++ 20 files changed, 896 insertions(+), 185 deletions(-) diff --git a/subprojects/groovy-groovysh/build.gradle b/subprojects/groovy-groovysh/build.gradle index dc23b9eb44..9469ea0a60 100644 --- a/subprojects/groovy-groovysh/build.gradle +++ b/subprojects/groovy-groovysh/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation projects.groovyJson implementation projects.groovyNio testImplementation projects.groovyTest + testImplementation projects.groovyCsv // for /slurp .csv default coverage + testImplementation projects.groovyTestJunit6 // for @ForkedJvm in CSV fallback tests implementation "net.java.dev.jna:jna:${versions.jna}" implementation "org.jline:jansi:${versions.jline}" implementation "org.jline:jline-builtins:${versions.jline}" @@ -51,6 +53,16 @@ plugins.withId('eclipse') { } } +// Forward -Djunit.network from the Gradle invocation to the test JVM so that +// @EnabledIfSystemProperty in network-gated tests (e.g. SlurpCsvFallbackTest) +// can see it. +tasks.named('test') { + def network = System.getProperty('junit.network') + if (network) { + systemProperty 'junit.network', network + } +} + tasks.named('rat') { excludes << '**/jline/GroovyEngine.java' // BSD license as per NOTICE/LICENSE files excludes << '**/jline/ObjectInspector.groovy' // BSD license as per NOTICE/LICENSE files diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy index c4b99fe65f..ac2513bc2d 100644 --- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy +++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy @@ -393,7 +393,7 @@ class Main { } name('groovysh') }.build() - if (terminal.width == 0 || terminal.height == 0) { + if (terminal.columns == 0 || terminal.rows == 0) { terminal.size = new Size(120, 40) // hard-coded terminal size when redirecting } Thread executeThread = Thread.currentThread() @@ -502,7 +502,7 @@ class Main { println render(messages['startup_banner.1']) println render(messages['startup_banner.2']) } - println '-' * (terminal.width - 1) + println '-' * (terminal.columns - 1) // for debugging // def index = 0 // def lines = ['/slurp /Users/paulk/Projects/groovy/subprojects/groovy-json/src/test/resources/groovy9802.json', diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ClassLoaderTest.groovy similarity index 50% copy from subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy copy to subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ClassLoaderTest.groovy index 8080b4f3d2..9bda1f336f 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ClassLoaderTest.groovy @@ -21,27 +21,26 @@ package org.apache.groovy.groovysh.commands import org.junit.jupiter.api.Test /** - * Tests for the {@code /types} command. + * Tests for the {@code /classloader} command. + * + * The command renders the engine's class loader using the {@code COLUMNS} + * option of {@code Printer.println(options, object)}; the support class's + * {@code DummyPrinter} extracts each column as a {@code name=value} entry + * so substring assertions remain straightforward. */ -class TypesTest extends SystemTestSupport { +class ClassLoaderTest extends SystemTestSupport { + @Test - void testImport() { - def names = ['C', 'I', 'T', 'R', 'E', 'A'] - system.execute('/types') + void viewExposesColumnDataForLoadedClassLoader() { + // Define a type so the class loader has at least one entry to render. + engine.execute('class ClassLoaderProbe {}') + system.execute('/classloader') def out = printer.output.join() - names.each{ name -> assert !out.contains(name) } - system.execute('class C {}') - system.execute('interface I {}') - system.execute('trait T {}') - system.execute('record R() {}') - system.execute('enum E {}') - system.execute('@interface A {}') - system.execute('/types') - out = printer.output.join() - names.each{ name -> assert out.contains(name) } - assert engine.types.keySet() == ['C', 'I', 'T', 'R', 'E', 'A'] as Set - system.execute('/types -d C') - system.execute('/types -d R') - assert engine.types.keySet() == ['I', 'T', 'E', 'A'] as Set + // Each column name from the /classloader command appears as a + // `name=...` entry. Don't assert on values' exact shape (lists vary + // by JDK and previous test state); just confirm the columns rendered. + assert out.contains('loadedClasses=') + assert out.contains('definedPackages=') + assert out.contains('classPath=') } } diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ConsoleTestSupport.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ConsoleTestSupport.groovy index 4782c4244b..970ca94987 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ConsoleTestSupport.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/ConsoleTestSupport.groovy @@ -22,6 +22,7 @@ import org.apache.groovy.groovysh.Main import org.apache.groovy.groovysh.jline.GroovyCommands import org.apache.groovy.groovysh.jline.GroovyConsoleEngine import org.apache.groovy.groovysh.jline.GroovyEngine +import org.jline.console.Printer import org.jline.builtins.ClasspathResourceUtil import org.jline.builtins.ConfigurationPath import org.jline.builtins.SyntaxHighlighter @@ -34,6 +35,7 @@ import org.jline.reader.impl.DefaultParser import org.junit.jupiter.api.BeforeEach import java.nio.file.Path +import java.util.function.Supplier /** * Support for testing {@link ConsoleEngine} instances. @@ -44,9 +46,10 @@ abstract class ConsoleTestSupport { private Path root = ClasspathResourceUtil.getResourcePath(rootURL) private Path temp = File.createTempDir().toPath() protected ConfigurationPath configPath = new ConfigurationPath(root, temp) + protected Supplier<Path> workDir = { -> temp } as Supplier<Path> protected DummyPrinter printer = new DummyPrinter(configPath) private highlighter = SyntaxHighlighter.build(root, "DUMMY") - protected CommandRegistry groovy = new GroovyCommands(engine, null, printer, highlighter) + protected CommandRegistry groovy = new GroovyCommands(engine, workDir, printer, highlighter) protected ConsoleEngine console protected CommandRegistry.CommandSession session = new CommandRegistry.CommandSession() protected LineReader reader @@ -70,7 +73,19 @@ abstract class ConsoleTestSupport { @Override void println(Map<String, Object> options, Object object) { - output << object.toString() + // /classloader --view passes the EngineClassLoader as `object` + // with COLUMNS option naming the fields to render. DefaultPrinter + // would format those as a table; in tests we capture each + // column as `name=value` so substring assertions still work. + if (object instanceof GroovyEngine.EngineClassLoader) { + options?[Printer.COLUMNS]?.each { col -> + output << "$col=" + object."$col" + } + } else { + // .toString() (not String.valueOf) preserves Groovy MetaClass + // extensions — Map renders as [k:v] not {k=v}. + output << object.toString() + } } @Override diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DelTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DelTest.groovy new file mode 100644 index 0000000000..70565df6bb --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/DelTest.groovy @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import org.junit.jupiter.api.Test + +/** + * Tests for the {@code /del} command. Uses the full {@link SystemTestSupport} + * stack so each invocation passes through registry parsing, pipe handling, + * and console dispatch — exercising the same path as a real REPL session. + */ +class DelTest extends SystemTestSupport { + + @Test + void testDelVariable() { + console.execute('dummyName', "x = 42") + assert console.hasVariable('x') + system.execute('/del x') + assert !console.hasVariable('x') + } + + @Test + void testDelMultipleVariables() { + console.execute('dummyName', "a = 1; b = 2; c = 3") + ['a', 'b', 'c'].each { assert console.hasVariable(it) } + system.execute('/del a c') + assert !console.hasVariable('a') + assert console.hasVariable('b') + assert !console.hasVariable('c') + } + + @Test + void testDelNonexistentIsHarmless() { + // /del on an unknown variable should be a no-op, not a hard failure + // that aborts the REPL session. + system.execute('/del thisVariableDoesNotExist') + // No exception — pre-existing variables (none) are unaffected. + assert !console.hasVariable('thisVariableDoesNotExist') + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabCommandTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabCommandTest.groovy deleted file mode 100644 index bb441de2e2..0000000000 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabCommandTest.groovy +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.groovy.groovysh.commands - -import groovy.grape.Grape -import groovy.mock.interceptor.StubFor - -import static groovy.test.GroovyAssert.shouldFail - -/** - * Tests for the {@code /grab} command. - */ -class GrabCommandTest /*extends CommandTestSupport*/ { - -// protected GrabCommand command - def grapeStub = new StubFor(Grape.class) - - void setUp() { -/* - Groovysh groovysh = new Groovysh() - PackageHelperImpl packageHelper = new PackageHelperImpl() - packageHelper.metaClass.reset = { } - groovysh.metaClass.packageHelper = packageHelper - command = new GrabCommand(groovysh) - command.metaClass.fail = { String message -> - throw new RuntimeException("fail(${message}) called") - } - def stubber = new StubFor(Grape.class) -*/ - } - - void testWrongNumberOfArguments() { - shouldFail(RuntimeException) { command.execute([]) } - shouldFail(RuntimeException) { command.execute(['alpha', 'beta']) } - } - - void testInvalidDependencyFormat() { - shouldFail(RuntimeException) { command.execute(['net.sf.json-lib']) } - } - - void testGroup_Module() { - grapeStub.demand.grab() { arg1, arg2 -> } - grapeStub.use { - command.execute(['net.sf.json-lib:json-lib']) - } - } - - void testGroupModuleVersion() { - grapeStub.demand.grab() { arg1, arg2 -> } - grapeStub.use { - command.execute(['net.sf.json-lib:json-lib:2.2.3']) - } - } - - void testGroupModuleVersionWildcard() { - grapeStub.demand.grab() { arg1, arg2 -> } - grapeStub.use { - command.execute(['net.sf.json-lib:json-lib:*']) - } - } - - void testGroupModuleVersionClassifier() { - grapeStub.demand.grab() { arg1, arg2 -> } - grapeStub.use { - command.execute(['net.sf.json-lib:json-lib:2.2.3:jdk15']) - } - } - -} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabTest.groovy new file mode 100644 index 0000000000..662b75a890 --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GrabTest.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import groovy.junit6.plugin.ForkedJvm +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfSystemProperty + +/** + * Tests for the {@code /grab} command — Maven-coordinate dependency + * resolution via Grape. The actual artifact-fetching test is forked and + * network-gated; the no-arg test runs always to lock in the documented + * "no args is a no-op" behaviour that {@code GroovyCommands.grab} relies + * on for {@code grab(input)} when no xargs are supplied. + */ +class GrabTest extends SystemTestSupport { + + @Test + void grabWithNoArgsIsNoOp() { + // grab() returns null when input.xargs() is empty; this should + // succeed silently rather than throw. + system.execute('/grab') + } + + @Test + @ForkedJvm + @EnabledIfSystemProperty(named = 'junit.network', matches = 'true') + void grabFetchesArtifactAndMakesItLoadable() { + // commons-lang3 is small, stable, and uses well-known coordinates. + // After the grab, the artifact's classes should resolve through + // the engine's classloader. + system.execute('/grab org.apache.commons:commons-lang3:3.14.0') + def cls = engine.execute("Class.forName('org.apache.commons.lang3.StringUtils')") + assert cls != null + assert cls.name == 'org.apache.commons.lang3.StringUtils' + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GroovyCommandTestSupport.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GroovyCommandTestSupport.groovy deleted file mode 100644 index 63f0b9146a..0000000000 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/GroovyCommandTestSupport.groovy +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.groovy.groovysh.commands - -import org.apache.groovy.groovysh.jline.GroovyCommands -import org.apache.groovy.groovysh.jline.GroovyEngine -import org.jline.console.CommandRegistry -import org.jline.console.Printer - -/** - * Support for testing commands from {@link GroovyCommands}. - */ -abstract class GroovyCommandTestSupport { - protected GroovyEngine engine = new GroovyEngine() { - def getLoader() { - classLoader - } - } - protected List<String> output = [] - protected Printer printer = new DummyPrinter(output) - protected CommandRegistry groovy = new GroovyCommands(engine, null, printer, null) - protected CommandRegistry.CommandSession session = new CommandRegistry.CommandSession() - - static class DummyPrinter implements Printer { - DummyPrinter(List<String> output) { - this.output = output - } - private List<String> output - - @Override - void println(Map<String, Object> options, Object object) { - // a bit ugly to partially replicate the logic from - // DefaultPrinter here, but it isn't easy to mock out - if (object instanceof GroovyEngine.EngineClassLoader) { - options?.columns?.each { col -> - output << "$col=" + object."$col" - } - return - } - output << object.toString() - } - - @Override - boolean refresh() { - false - } - } -} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/HelpCommandTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/HelpCommandTest.groovy index 3c715550a2..12ec379986 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/HelpCommandTest.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/HelpCommandTest.groovy @@ -18,19 +18,30 @@ */ package org.apache.groovy.groovysh.commands +import org.junit.jupiter.api.Test + /** - * Tests for the {@link HelpCommand} class. + * Tests for the {@code /help} command, registered by JLine's + * {@code SystemRegistryImpl} and renamed in {@link SystemTestSupport} to + * match the leading-slash convention production uses. + * + * The help builtin writes through {@code terminal.writer()} rather than + * through the printer, so this test demonstrates the + * {@link SystemTestSupport#terminalOutput()} capture pattern for any + * future test that needs to assert on terminal-side output. */ -class HelpCommandTest /*extends CommandTestSupport */{ - void testList() { -// shell.execute(HelpCommand.COMMAND_NAME) - } - - void testCommandHelp() { -// shell.execute(HelpCommand.COMMAND_NAME + ' exit') - } +class HelpCommandTest extends SystemTestSupport { - void testCommandHelpInvalidCommand() { -// shell.execute(HelpCommand.COMMAND_NAME + ' no-such-command') + @Test + void helpListsKnownCommands() { + system.execute('/help') + def out = terminalOutput() + assert !out.empty + // A handful of stable command names that should appear in the listing. + // Names only — don't assert on layout, alignment, or descriptions, so + // the test stays robust across JLine cosmetic changes and platforms. + assert out.contains('help') + assert out.contains('show') + assert out.contains('exit') } } diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/InspectTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/InspectTest.groovy new file mode 100644 index 0000000000..b5ec346c0a --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/InspectTest.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import org.junit.jupiter.api.Test + +/** + * Tests for the {@code /inspect} command. Exercises GroovyCommands and the + * vendored ObjectInspector — neither of which had any direct test coverage + * before. The command writes via the printer, so assertions go against + * {@code printer.output}. + */ +class InspectTest extends SystemTestSupport { + + @Test + void inspectMethodsListsKnownMembers() { + console.execute('dummy', "data = [a: 1, b: 2]") + system.execute('/inspect --methods $data') + def out = printer.output.join() + // LinkedHashMap exposes these methods; assert on names without + // pinning to formatting/columns/parameter shapes. + assert out.contains('get') + assert out.contains('put') + assert out.contains('size') + } + + @Test + void inspectInfoListsPropertyCategories() { + console.execute('dummy', "data = [a: 1, b: 2]") + system.execute('/inspect --info $data') + def out = printer.output.join() + // ObjectInspector.properties() returns a map with these keys. + assert out.contains('propertyInfo') + assert out.contains('publicFields') + assert out.contains('classProps') + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/MethodsTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/MethodsTest.groovy index 08977f9159..f591b1f2b2 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/MethodsTest.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/MethodsTest.groovy @@ -35,4 +35,13 @@ class MethodsTest extends SystemTestSupport { system.execute('/methods -d twice') assert !engine.methodNames.contains('twice') } + + @Test + void testDeleteNonexistentMethodIsHarmless() { + // /methods -d on an unknown method should not throw; the engine's + // method registry stays stable. + def before = engine.methodNames.toSet() + system.execute('/methods -d noSuchMethodEverDefined') + assert engine.methodNames.toSet() == before + } } diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/PipeTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/PipeTest.groovy new file mode 100644 index 0000000000..1c20185722 --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/PipeTest.groovy @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import org.junit.jupiter.api.Test + +/** + * Tests for the {@code /pipe} command — JLine's user-defined pipe operator + * registry. Operators take the form {@code /pipe OPERATOR PREFIX POSTFIX}; + * uses of {@code OPERATOR} on subsequent lines have the right-hand side + * wrapped between {@code PREFIX} and {@code POSTFIX} before evaluation. + * + * Documented in {@code groovysh.adoc}; this is the first automated coverage + * for the user-defined pipe path. The tests cover the {@code /pipe} command + * surface (define, list, delete, reserved-name rejection) — the actual + * pipeline-rewriting machinery is JLine's responsibility upstream. + */ +class PipeTest extends SystemTestSupport { + + @Test + void definedPipeAppearsInList() { + system.execute("/pipe |? '.findAll{' '}'") + printer.output.clear() + system.execute('/pipe --list') + // pipes is rendered as a Map<String, List<String>>; the key is the + // operator name. Substring-match on the operator (and the prefix) + // tolerates rendering changes between JLine versions. + def out = printer.output.join() + assert out.contains('|?') + assert out.contains('.findAll{') + } + + @Test + void deleteAllRemovesPreviouslyDefinedPipes() { + system.execute("/pipe |? '.findAll{' '}'") + system.execute("/pipe |* '.collect{' '}'") + system.execute('/pipe --delete *') + printer.output.clear() + system.execute('/pipe --list') + def out = printer.output.join() + assert !out.contains('|?') + assert !out.contains('|*') + } + +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SaveLoadTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SaveLoadTest.groovy new file mode 100644 index 0000000000..63d764330a --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SaveLoadTest.groovy @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Files +import java.nio.file.Path + +/** + * Tests for the {@code /save} and {@code /load} commands — round-tripping + * the engine's buffer (variables, methods, type definitions) to a file and + * back. Flagship persistence feature; previously had no automated coverage. + */ +class SaveLoadTest extends SystemTestSupport { + + @TempDir + Path tmp + + @Test + void saveLoadRoundTrip() { + // Establish session state. Note: /save serialises the engine's + // *buffer*, which in default (non-interpreter) mode contains + // imports/types/methods but not bare variable assignments. So we + // round-trip definitions; the variables-via-shared-data path is a + // separate code branch (no-arg /save) not covered here. + engine.execute('import java.awt.Point') + engine.execute('def doubler(n) { n * 2 }') + engine.execute('class Probe {}') + assert engine.methodNames.contains('doubler') + assert engine.types.containsKey('Probe') + assert engine.imports.values().any { it.contains('java.awt.Point') } + + Path file = tmp.resolve('session.groovy') + system.execute("/save ${forwardSlashes(file)}") + + assert Files.exists(file) + def saved = file.text + // Assert on identifiers that must round-trip; don't pin to + // whitespace, ordering, or how the snippets are joined. + assert saved.contains('java.awt.Point') + assert saved.contains('doubler') + assert saved.contains('Probe') + + // Wipe the engine; verify a clean slate. + engine.reset() + assert !engine.methodNames.contains('doubler') + assert !engine.types.containsKey('Probe') + + // Replay the saved buffer. + system.execute("/load ${forwardSlashes(file)}") + + // Loaded state matches the original; methods evaluate. + assert engine.methodNames.contains('doubler') + assert engine.execute('doubler(21)') == 42 + assert engine.types.containsKey('Probe') + assert engine.imports.values().any { it.contains('java.awt.Point') } + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpCsvFallbackTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpCsvFallbackTest.groovy new file mode 100644 index 0000000000..4fb64a5eb3 --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpCsvFallbackTest.groovy @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import groovy.grape.Grape +import groovy.junit6.plugin.ForkedJvm +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfSystemProperty +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Files +import java.nio.file.Path + +import static groovy.test.GroovyAssert.shouldFail + +/** + * Variant tests for the {@code /slurp} CSV path that exercise classpath + * configurations the default test classpath can't represent. Each test runs + * in a freshly forked JVM with {@code groovy-csv} filtered off the + * classpath, so the engine genuinely cannot resolve {@code groovy.csv.CsvSlurper}. + * + * The "happy path" (groovy-csv available, preferred over commons-csv) is + * covered in-process by {@link SlurpTest#slurpCsvProducesListOfMaps}. + */ +class SlurpCsvFallbackTest extends SystemTestSupport { + + @TempDir + Path tmp + + @Test + @ForkedJvm(excludeFromClasspath = ['groovy-csv']) + void slurpCsvErrorsWhenNoCsvLibraryAvailable() { + Path file = Files.writeString(tmp.resolve('whiskey.csv'), "name,region\nLagavulin,Islay\n") + // parseCsv throws IllegalArgumentException when neither + // groovy.csv.CsvSlurper nor org.apache.commons.csv.CSVFormat is on + // the classpath; slurpcmd's outer catch re-throws so the user sees + // a clear error message rather than a silently null result. + def thrown = shouldFail(IllegalArgumentException) { + system.execute("data = /slurp ${forwardSlashes(file)}") + } + assert thrown.message.contains('CSV format requires') + assert thrown.message.contains('groovy.csv.CsvSlurper') + assert thrown.message.contains('org.apache.commons.csv.CSVFormat') + } + + @Test + @ForkedJvm(excludeFromClasspath = ['groovy-csv']) + @EnabledIfSystemProperty(named = 'junit.network', matches = 'true') + void slurpCsvUsesCommonsCsvFallback() { + // groovy-csv is filtered off the classpath; pull commons-csv via + // Grape so parseCsv's second branch is the one that fires. + Grape.grab(group: 'org.apache.commons', module: 'commons-csv', version: '1.14.1', + classLoader: engine.classLoader, transitive: false) + Path file = Files.writeString(tmp.resolve('whiskey.csv'), + "name,region\nLagavulin,Islay\nMacallan,Speyside\n") + system.execute("rows = /slurp ${forwardSlashes(file)}") + def rows = console.getVariable('rows') + assert rows instanceof List + assert rows.size() == 2 + assert rows[0].name == 'Lagavulin' && rows[0].region == 'Islay' + assert rows[1].name == 'Macallan' && rows[1].region == 'Speyside' + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpTest.groovy new file mode 100644 index 0000000000..27f0f89a6a --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SlurpTest.groovy @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.commands + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +import java.nio.file.Files +import java.nio.file.Path + +/** + * Tests for the {@code /slurp} command, registered by JLine's + * {@code ConsoleEngineImpl}. Confirms format detection by file extension + * for the formats most groovysh users reach for, and verifies the result + * is bound to a Groovy variable when assigned with {@code = /slurp ...}. + * + * Uses {@link TempDir} for fixture lifecycle so the support class doesn't + * grow a dedicated tempDir field for every file-touching test. + */ +class SlurpTest extends SystemTestSupport { + + @TempDir + Path tmp + + @Test + void slurpJsonProducesMap() { + Path file = Files.writeString(tmp.resolve('answer.json'), '{"answer":42,"name":"groovysh"}') + system.execute("data = /slurp ${forwardSlashes(file)}") + def value = console.getVariable('data') + assert value != null + assert value.answer == 42 + assert value.name == 'groovysh' + } + + @Test + void slurpPropertiesProducesMap() { + Path file = Files.writeString(tmp.resolve('app.properties'), "name=groovysh\nversion=4.x\n") + system.execute("config = /slurp ${forwardSlashes(file)}") + def value = console.getVariable('config') + assert value != null + assert value.name == 'groovysh' + assert value.version == '4.x' + } + + @Test + void slurpCsvProducesListOfMaps() { + // Requires groovy.csv.CsvSlurper on the classpath; supplied here via + // the testImplementation projects.groovyCsv dependency. + Path file = Files.writeString(tmp.resolve('whiskey.csv'), + "name,region\nLagavulin,Islay\nMacallan,Speyside\n") + system.execute("rows = /slurp ${forwardSlashes(file)}") + def rows = console.getVariable('rows') + assert rows instanceof List + assert rows.size() == 2 + assert rows[0].name == 'Lagavulin' && rows[0].region == 'Islay' + assert rows[1].name == 'Macallan' && rows[1].region == 'Speyside' + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SystemTestSupport.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SystemTestSupport.groovy index e9bd2a8215..f2ba1c8746 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SystemTestSupport.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/SystemTestSupport.groovy @@ -21,27 +21,95 @@ package org.apache.groovy.groovysh.commands import org.apache.groovy.groovysh.jline.GroovySystemRegistry import org.jline.terminal.Terminal import org.jline.terminal.TerminalBuilder - +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +import java.nio.charset.StandardCharsets +import java.nio.file.Path import java.util.function.Supplier /** * Support for testing commands involving {@link GroovySystemRegistry}. + * + * The terminal is built explicitly as a {@code dumb} terminal with empty input + * and a captured byte buffer for output. This keeps tests deterministic across + * platforms — no TTY probing, no native FFM/JNI bindings, no signal handlers + * tied to the JVM's actual stdin/stdout. + * + * <p>Two output-capture paths are available; pick by where the command writes: + * <ul> + * <li>{@code printer.output} — for commands that produce results via + * {@code printer.println(options, object)}. Most {@code GroovyCommands} + * commands ({@code /show}, {@code /prnt}, {@code /inspect}, + * {@code /classloader}, {@code /types}, {@code /methods}, …) take this + * path. The captured strings are the {@code object.toString()} forms; + * Groovy MetaClass dispatch is preserved, so a Map renders as + * {@code [k:v]}. + * <li>{@link #terminalOutput()} — for JLine builtins that write directly + * through {@code terminal.writer()} (e.g. {@code /help}). Returns the + * raw bytes decoded as UTF-8; the dumb terminal also emits a couple of + * capability-probe escapes at startup, so prefer substring matches over + * full-string compares. + * </ul> + * + * See {@code subprojects/groovy-groovysh/AGENTS.md} for the platform-fragility + * rationale and the layered test design. */ abstract class SystemTestSupport extends ConsoleTestSupport { protected GroovySystemRegistry system + protected Terminal terminal + private ByteArrayOutputStream terminalBytes @BeforeEach @Override void setUp() { super.setUp() Supplier workDir = { configPath.getUserConfig('.') } - Terminal terminal = TerminalBuilder.builder().build() + terminalBytes = new ByteArrayOutputStream() + terminal = TerminalBuilder.builder() + .dumb(true) + .streams(new ByteArrayInputStream(new byte[0]), terminalBytes) + .encoding(StandardCharsets.UTF_8) + .name('groovysh-test') + .build() system = new GroovySystemRegistry(reader.parser, terminal, workDir, configPath).tap { setCommandRegistries(console, groovy) + // Match production wiring: SystemRegistryImpl's built-in commands + // are renamed to use the leading-slash convention groovysh exposes + // to users. + renameLocal 'exit', '/exit' + renameLocal 'help', '/help' } } + @AfterEach + void tearDownSystem() { + terminal?.close() + } + + /** + * Returns text written to the terminal so far, decoded as UTF-8. The + * underlying terminal is {@code dumb}, so no ANSI escape sequences are + * produced — the returned string is plain text suitable for substring + * assertions. + */ + protected String terminalOutput() { + terminal?.writer()?.flush() + new String(terminalBytes.toByteArray(), StandardCharsets.UTF_8) + } + + /** + * Returns a forward-slash form of the supplied path, suitable for + * interpolating into a {@code system.execute(...)} line. JLine's + * DefaultParser treats {@code \} as an escape character, so a + * Windows-native path like {@code C:\Users\runner\…\foo.json} would + * have its separators eaten before reaching the command. Java NIO + * accepts forward-slash paths on Windows, so this normalisation + * works on every platform. + */ + protected static String forwardSlashes(Path path) { + path.toString().replace('\\', '/') + } + } diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy index 8080b4f3d2..ceadf79822 100644 --- a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/commands/TypesTest.groovy @@ -44,4 +44,13 @@ class TypesTest extends SystemTestSupport { system.execute('/types -d R') assert engine.types.keySet() == ['I', 'T', 'E', 'A'] as Set } + + @Test + void testDeleteNonexistentTypeIsHarmless() { + // /types -d on an unknown type should not throw or corrupt the + // type registry. + def before = engine.types.keySet().toSet() + system.execute('/types -d NoSuchType') + assert engine.types.keySet().toSet() == before + } } diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyEngineTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyEngineTest.groovy new file mode 100644 index 0000000000..da853d70ed --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyEngineTest.groovy @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.jline + +import org.junit.jupiter.api.Test + +/** + * Direct tests for {@link GroovyEngine}. The engine is the foundation of the + * groovysh stack — it holds the binding, runs scripts, and tracks user-defined + * imports / variables / methods / types. Exercising it directly (no JLine + * registry, console, or terminal) gives the most portable test layer. + */ +class GroovyEngineTest { + + private final GroovyEngine engine = new GroovyEngine() + + @Test + void executeReturnsLastValue() { + assert engine.execute('1 + 1') == 2 + assert engine.execute("'hi' + ' there'") == 'hi there' + } + + @Test + void variablesPersistAcrossExecutes() { + engine.execute('x = 5') + assert engine.hasVariable('x') + assert engine.execute('x * 2') == 10 + } + + @Test + void putAndHasVariable() { + engine.put('answer', 42) + assert engine.hasVariable('answer') + assert engine.execute('answer') == 42 + } + + @Test + void methodDefinitionsTracked() { + engine.execute('def twice(n) { n * 2 }') + assert engine.methodNames.contains('twice') + assert engine.execute('twice(21)') == 42 + } + + @Test + void typesAccumulate() { + engine.execute('class Foo {}') + engine.execute('interface Bar {}') + engine.execute('enum Baz { A, B }') + assert engine.types.keySet().containsAll(['Foo', 'Bar', 'Baz']) + } + + @Test + void importsTracked() { + engine.execute('import java.awt.Point') + assert engine.imports.values().any { it.contains('java.awt.Point') } + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommandsTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommandsTest.groovy new file mode 100644 index 0000000000..ee42b41ddf --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovyPosixCommandsTest.groovy @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.jline + +import org.jline.terminal.Terminal +import org.jline.terminal.TerminalBuilder +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.util.function.Function + +/** + * Direct unit tests for static methods in {@link GroovyPosixCommands} — the + * Apache-licensed fork of JLine's PosixCommands. Bypasses the registry stack + * and constructs a {@link GroovyPosixContext} directly so each test exercises + * exactly one command function. JLine refactors PosixCommands frequently; + * these tests reduce regression risk on bumps. + */ +class GroovyPosixCommandsTest { + + private Terminal terminal + private ByteArrayOutputStream out + private ByteArrayOutputStream err + private Path tempDir + + @BeforeEach + void setUp() { + terminal = TerminalBuilder.builder() + .dumb(true) + .streams(new ByteArrayInputStream(new byte[0]), new ByteArrayOutputStream()) + .encoding(StandardCharsets.UTF_8) + .name('groovysh-test') + .build() + out = new ByteArrayOutputStream() + err = new ByteArrayOutputStream() + tempDir = Files.createTempDirectory('groovysh-test-') + } + + @AfterEach + void tearDown() { + terminal?.close() + // Clean up any files left in the temp dir, then the dir itself. + tempDir?.toFile()?.deleteDir() + } + + private GroovyPosixContext context() { + new GroovyPosixContext( + new ByteArrayInputStream(new byte[0]), + new PrintStream(out, true, StandardCharsets.UTF_8), + new PrintStream(err, true, StandardCharsets.UTF_8), + tempDir, + terminal, + { name -> null } as Function<String, Object>) + } + + private String stdout() { + // .normalize() collapses platform line separators to "\n" so + // line-aware assertions work uniformly — PrintStream.println uses + // System.lineSeparator() which is "\r\n" on Windows. + new String(out.toByteArray(), StandardCharsets.UTF_8).normalize() + } + + @Test + void catReadsFileContents() { + Path file = Files.writeString(tempDir.resolve('hello.txt'), "first line\nsecond line\n") + GroovyPosixCommands.cat(context(), ['/cat', file.toString()] as Object[]) + def output = stdout() + assert output.contains('first line') + assert output.contains('second line') + } + + @Test + void catWithNumberFlagPrependsLineNumbers() { + Path file = Files.writeString(tempDir.resolve('numbered.txt'), "alpha\nbeta\n") + GroovyPosixCommands.cat(context(), ['/cat', '-n', file.toString()] as Object[]) + def output = stdout() + // -n produces lines like " 1\talpha". Don't assert on exact spacing + // (it's right-aligned in 6 columns); check the line numbers and content + // appear together. + assert output =~ /1\s*\talpha/ + assert output =~ /2\s*\tbeta/ + } + + @Test + void grepReturnsOnlyMatchingLines() { + Path file = Files.writeString(tempDir.resolve('fruits.txt'), + "apple\nbanana\ncherry\nblueberry\n") + // --color=never disables ANSI match highlighting so the asserted + // substrings appear contiguously in the output. + GroovyPosixCommands.grep(context(), ['/grep', '--color=never', 'b', file.toString()] as Object[]) + def output = stdout() + assert output.contains('banana') + assert output.contains('blueberry') + assert !output.contains('apple') + assert !output.contains('cherry') + } + + @Test + void sortReorderLinesAlphabetically() { + Path file = Files.writeString(tempDir.resolve('mix.txt'), + "cherry\napple\nbanana\n") + GroovyPosixCommands.sort(context(), ['/sort', file.toString()] as Object[]) + def lines = stdout().split('\n').findAll { it } + assert lines == ['apple', 'banana', 'cherry'] + } + + @Test + void headDefaultsToFirstTenLines() { + def content = (1..15).collect { "line${it}" }.join('\n') + '\n' + Path file = Files.writeString(tempDir.resolve('many.txt'), content) + GroovyPosixCommands.head(context(), ['/head', file.toString()] as Object[]) + def output = stdout() + // First ten lines appear; eleventh and beyond don't. + assert output.contains('line1\n') + assert output.contains('line10') + assert !output.contains('line11') + } +} diff --git a/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovySystemRegistryTest.groovy b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovySystemRegistryTest.groovy new file mode 100644 index 0000000000..deccd9b528 --- /dev/null +++ b/subprojects/groovy-groovysh/src/test/groovy/org/apache/groovy/groovysh/jline/GroovySystemRegistryTest.groovy @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.groovysh.jline + +import org.apache.groovy.groovysh.commands.SystemTestSupport +import org.junit.jupiter.api.Test + +/** + * Tests for the Groovy-specific overrides in {@link GroovySystemRegistry}: + * pipe-operator renames, {@code /!} command-prefix recognition, and the + * {@code execute()} rewriting that strips whitespace around {@code =} when + * the right-hand side is a command. Uses {@link SystemTestSupport} so the + * full registry stack is available for the execute-path assertions. + */ +class GroovySystemRegistryTest extends SystemTestSupport { + + @Test + void pipeOperatorsAreRenamedForGroovy() { + // The Groovy fork rebinds the SystemRegistryImpl pipe operators so they + // don't collide with Groovy operators (||, &&, >>, etc). + def names = system.pipeNames + assert names.contains('|||') // Pipe.OR (was '||') + assert names.contains('|&&') // Pipe.AND (was '&&') + assert names.contains('|>') // Pipe.REDIRECT (was '>') + assert names.contains('|>>') // Pipe.APPEND (was '>>') + } + + @Test + void bangPrefixIsRecognisedAsCommand() { + // /!ls etc must be claimed by isCommandOrScript so the parser routes + // them to the registered shell-out handler instead of evaluating them + // as Groovy expressions. + assert system.isCommandOrScript('/!ls') + assert system.isCommandOrScript('/!cd') + assert !system.isCommandOrScript('groovyExpression') + } + + @Test + void plainGroovyAssignmentPassesThrough() { + // When the right-hand side is not a command (no leading slash), the + // execute() override leaves the line unchanged and Groovy evaluates it. + system.execute('answer = 42') + assert console.hasVariable('answer') + assert console.getVariable('answer') == 42 + } + + @Test + void commandResultAssignmentSurvivesWhitespaceAroundEquals() { + // SystemRegistryImpl assumes no whitespace around `=` in command-result + // assignments. Our execute() rewrite normalises `x = /show` to + // `x=/show` so it parses as one. Without the rewrite, JLine would + // either error or hand the line to Groovy for evaluation, which + // would not bind `result` here. + console.execute('dummy', 'a = 99') + system.execute('result = /show') + assert console.hasVariable('result') + } +}
