diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 49e482d..996cb10 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -1,8 +1,8 @@ [package] org = "ballerina" -name = "tool_protoc" +name = "tool.grpc" version = "@toml.version@" authors = ["Ballerina"] keywords = ["grpc", "protoc tool"] license = ["Apache-2.0"] -distribution = "2201.12.3" +distribution = "2201.13.0" diff --git a/gradle.properties b/gradle.properties index 68d893f..bd85fbb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,20 +2,22 @@ org.gradle.caching=true group=io.ballerina version=1.0.0-SNAPSHOT -#dependency versions -ballerinaLangVersion=2201.12.0 -checkstylePluginVersion=10.12.1 +# Java Dependencies +ballerinaLangVersion=2201.13.0-20250924-081800-3dae8c03 commonsLang3Version=3.8.1 slf4jVersion=1.7.30 protoGoogleCommonsVersion=1.17.0 protobufJavaVersion=3.23.4 picocliVersion=4.0.1 -githubSpotbugsVersion=6.0.18 -githubJohnrengelmanShadowVersion=8.1.1 -underCouchDownloadVersion=5.4.0 -researchgateReleaseVersion=2.8.0 testngVersion=7.6.1 + +# Gradle Plugin Versions jacocoVersion=0.8.10 +checkstylePluginVersion=10.12.1 +spotbugsPluginVersion=6.0.18 +shadowJarPluginVersion=8.1.1 +downloadPluginVersion=5.4.0 +releasePluginVersion=2.8.0 ballerinaGradlePluginVersion=2.3.0 stdlibIoVersion=1.8.0 diff --git a/protoc-cli/build.gradle b/protoc-cli/build.gradle index 16847a8..796af83 100644 --- a/protoc-cli/build.gradle +++ b/protoc-cli/build.gradle @@ -110,6 +110,7 @@ checkstyle { } checkstyleMain.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") +checkstyleTest.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") def excludePattern = '**/module-info.java' tasks.withType(Checkstyle) { diff --git a/protoc-cli/src/main/java/io/ballerina/protoc/cli/GrpcCmd.java b/protoc-cli/src/main/java/io/ballerina/protoc/cli/GrpcCmd.java index 464b044..6a35fae 100644 --- a/protoc-cli/src/main/java/io/ballerina/protoc/cli/GrpcCmd.java +++ b/protoc-cli/src/main/java/io/ballerina/protoc/cli/GrpcCmd.java @@ -70,18 +70,47 @@ public class GrpcCmd implements BLauncherCmd { private static final Logger LOG = LoggerFactory.getLogger(GrpcCmd.class); + private static final int EXIT_CODE_0 = 0; + private static final int EXIT_CODE_2 = 2; + private static final ExitHandler DEFAULT_EXIT_HANDLER = code -> Runtime.getRuntime().exit(code); private PrintStream outStream; private static final String PROTO_EXTENSION = "proto"; private CommandLine parentCmdParser; + private final ExitHandler exitHandler; + + /** + * Functional interface for handling exit behavior. + * Public to allow test access from other packages. + */ + @FunctionalInterface + public interface ExitHandler { + void exit(int code); + } public GrpcCmd() { - this.outStream = System.out; + this(System.out, DEFAULT_EXIT_HANDLER); } public GrpcCmd(PrintStream outStream) { + this(outStream, DEFAULT_EXIT_HANDLER); + } + + /** + * Constructor for testing with custom exit handler. + * This is public to allow tests in other packages to use it. + * + * @param outStream output stream + * @param exitHandler custom exit handler (for testing) + */ + public GrpcCmd(PrintStream outStream, ExitHandler exitHandler) { this.outStream = outStream; + this.exitHandler = exitHandler; + } + + private void exit(int code) { + exitHandler.exit(code); } @CommandLine.Option(names = {"-h", "--help"}, hidden = true, usageHelp = true) @@ -134,9 +163,16 @@ private static void exportResource(String resourceName, ClassLoader classLoader) @Override public void execute() { - // Show help text - if (helpFlag || protoPath == null || protoPath.trim().isEmpty()) { + // Show help text with exit code 0 if help flag is present + if (helpFlag) { + printLongDesc(new StringBuilder()); + exit(EXIT_CODE_0); + return; + } + // Show help text with exit code 2 if no input provided + if (protoPath == null || protoPath.trim().isEmpty()) { printLongDesc(new StringBuilder()); + exit(EXIT_CODE_2); return; } diff --git a/protoc-cli/src/test/java/io/ballerina/protoc/cli/ExitCodeCaptor.java b/protoc-cli/src/test/java/io/ballerina/protoc/cli/ExitCodeCaptor.java new file mode 100644 index 0000000..84af8c2 --- /dev/null +++ b/protoc-cli/src/test/java/io/ballerina/protoc/cli/ExitCodeCaptor.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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 io.ballerina.protoc.cli; + +import io.ballerina.protoc.cli.GrpcCmd.ExitHandler; + +/** + * Test helper for capturing exit codes in tests. + */ +public class ExitCodeCaptor implements ExitHandler { + private int exitCode = -1; + private boolean exitCalled = false; + + @Override + public void exit(int code) { + this.exitCode = code; + this.exitCalled = true; + } + + public int getExitCode() { + if (!exitCalled) { + throw new IllegalStateException("exit() was not called"); + } + return exitCode; + } + + public boolean wasExitCalled() { + return exitCalled; + } +} diff --git a/protoc-cli/src/test/java/io/ballerina/protoc/cli/GrpcCmdTest.java b/protoc-cli/src/test/java/io/ballerina/protoc/cli/GrpcCmdTest.java new file mode 100644 index 0000000..e1e0376 --- /dev/null +++ b/protoc-cli/src/test/java/io/ballerina/protoc/cli/GrpcCmdTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. 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 io.ballerina.protoc.cli; + +import org.testng.Assert; +import org.testng.annotations.Test; +import picocli.CommandLine; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * This class is used to test the functionality of the gRPC command exit codes. + */ +public class GrpcCmdTest { + + @Test(description = "Test grpc command execution without arguments - should return exit code 2") + public void testExecuteWithoutArguments() { + String[] args = {}; + ExitCodeCaptor exitCaptor = new ExitCodeCaptor(); + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outContent); + GrpcCmd grpcCmd = new GrpcCmd(printStream, exitCaptor); + new CommandLine(grpcCmd).parseArgs(args); + grpcCmd.execute(); + Assert.assertEquals(exitCaptor.getExitCode(), 2, + "grpc command without arguments should exit with code 2"); + } + + @Test(description = "Test grpc command execution with help flag - should return exit code 0") + public void testExecuteWithHelpFlag() { + String[] args = {"-h"}; + ExitCodeCaptor exitCaptor = new ExitCodeCaptor(); + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outContent); + GrpcCmd grpcCmd = new GrpcCmd(printStream, exitCaptor); + new CommandLine(grpcCmd).parseArgs(args); + grpcCmd.execute(); + Assert.assertEquals(exitCaptor.getExitCode(), 0, + "grpc command with -h flag should exit with code 0"); + } + + @Test(description = "Test grpc command execution with invalid flag - should throw exception during parsing") + public void testExecuteWithInvalidFlag() { + String[] args = {"--invalidFlag"}; + ExitCodeCaptor exitCaptor = new ExitCodeCaptor(); + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outContent); + GrpcCmd grpcCmd = new GrpcCmd(printStream, exitCaptor); + try { + new CommandLine(grpcCmd).parseArgs(args); + Assert.fail("Expected picocli to throw exception for invalid flag"); + } catch (CommandLine.UnmatchedArgumentException e) { + // Expected: picocli rejects invalid flags + Assert.assertTrue(e.getMessage().contains("Unknown option")); + } + } +} diff --git a/protoc-cli/src/test/resources/testng.xml b/protoc-cli/src/test/resources/testng.xml new file mode 100644 index 0000000..0a58df9 --- /dev/null +++ b/protoc-cli/src/test/resources/testng.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/protoc-tool/Ballerina.toml b/protoc-tool/Ballerina.toml index 86467dc..e4ff08d 100644 --- a/protoc-tool/Ballerina.toml +++ b/protoc-tool/Ballerina.toml @@ -1,8 +1,8 @@ [package] org = "ballerina" -name = "tool_protoc" +name = "tool.grpc" version = "1.0.0" authors = ["Ballerina"] keywords = ["grpc", "protoc tool"] license = ["Apache-2.0"] -distribution = "2201.12.3" +distribution = "2201.13.0" diff --git a/protoc-tool/Dependencies.toml b/protoc-tool/Dependencies.toml index eb693a8..5dbc9b3 100644 --- a/protoc-tool/Dependencies.toml +++ b/protoc-tool/Dependencies.toml @@ -5,13 +5,13 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.12.0" +distribution-version = "2201.13.0-20250924-081800-3dae8c03" [[package]] org = "ballerina" -name = "tool_protoc" +name = "tool.grpc" version = "1.0.0" modules = [ - {org = "ballerina", packageName = "tool_protoc", moduleName = "tool_protoc"} + {org = "ballerina", packageName = "tool.grpc", moduleName = "tool.grpc"} ] diff --git a/protoc-tool/build.gradle b/protoc-tool/build.gradle index 10654c5..f1a06d1 100644 --- a/protoc-tool/build.gradle +++ b/protoc-tool/build.gradle @@ -25,7 +25,7 @@ plugins { description = 'Ballerina - Protoc Tool' -def packageName = "tool_protoc" +def packageName = "tool.grpc" def packageOrg = "ballerina" def tomlVersion = stripBallerinaExtensionVersion("${project.version}") def ballerinaTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/Ballerina.toml") @@ -94,6 +94,10 @@ publishing { } } +clean { + delete file("$projectDir/target") +} + updateTomlFiles.dependsOn copyStdlibs build.dependsOn updateTomlFiles build.dependsOn "generatePomFileForMavenPublication" diff --git a/settings.gradle b/settings.gradle index 44f5f07..5f96cf8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,10 +9,10 @@ pluginManagement { plugins { - id "com.github.spotbugs" version "${githubSpotbugsVersion}" - id "com.github.johnrengelman.shadow" version "${githubJohnrengelmanShadowVersion}" - id "de.undercouch.download" version "${underCouchDownloadVersion}" - id "net.researchgate.release" version "${researchgateReleaseVersion}" + id "com.github.spotbugs" version "${spotbugsPluginVersion}" + id "com.github.johnrengelman.shadow" version "${shadowJarPluginVersion}" + id "de.undercouch.download" version "${downloadPluginVersion}" + id "net.researchgate.release" version "${releasePluginVersion}" id "io.ballerina.plugin" version "${ballerinaGradlePluginVersion}" } diff --git a/tooling-tests/build.gradle b/tooling-tests/build.gradle index f2bc8be..6d904a9 100644 --- a/tooling-tests/build.gradle +++ b/tooling-tests/build.gradle @@ -150,8 +150,11 @@ test { dependsOn(copyStdlibs) systemProperty "ballerina.home", ballerinaDist systemProperty "ballerina.offline.flag", "true" + binaryResultsDirectory = file("$buildDir/test-results/binary") useTestNG() { suites 'src/test/resources/testng.xml' + outputDirectory = file("$buildDir/test-output") + useDefaultListeners = true } testLogging.showStandardStreams = true testLogging { @@ -168,6 +171,10 @@ test { finalizedBy jacocoTestReport } +clean { + delete file("$buildDir/generated-sources") +} + jacoco { toolVersion = "${jacocoVersion}" } diff --git a/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingCommonTest.java b/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingCommonTest.java index 9a6047b..1857f54 100644 --- a/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingCommonTest.java +++ b/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingCommonTest.java @@ -18,6 +18,7 @@ package io.ballerina.protoc.tools; +import io.ballerina.protoc.cli.GrpcCmd; import org.testng.Assert; import org.testng.annotations.Test; @@ -57,11 +58,7 @@ public void testHelloWorldWithDependency() { public void testCommandWithoutArguments() { // Capture output using ByteArrayOutputStream with explicit encoding java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); - java.io.PrintStream printStream = new java.io.PrintStream(outputStream, true, - java.nio.charset.StandardCharsets.UTF_8); - - // Create a GrpcCmd instance with the captured print stream - io.ballerina.protoc.cli.GrpcCmd grpcCmd = new io.ballerina.protoc.cli.GrpcCmd(printStream); + GrpcCmd grpcCmd = ToolingTestUtils.createGrpcCmd(outputStream); try { grpcCmd.execute(); @@ -78,9 +75,7 @@ public void testCommandWithoutArguments() { @Test public void testCommandWithHelpFlag() { java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream(); - java.io.PrintStream printStream = new java.io.PrintStream(outputStream, true, - java.nio.charset.StandardCharsets.UTF_8); - io.ballerina.protoc.cli.GrpcCmd grpcCmd = new io.ballerina.protoc.cli.GrpcCmd(printStream); + GrpcCmd grpcCmd = ToolingTestUtils.createGrpcCmd(outputStream); try { java.lang.reflect.Field helpFlagField = grpcCmd.getClass().getDeclaredField("helpFlag"); diff --git a/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingTestUtils.java b/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingTestUtils.java index f955bdb..2fd451b 100644 --- a/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingTestUtils.java +++ b/tooling-tests/src/test/java/io/ballerina/protoc/tools/ToolingTestUtils.java @@ -29,8 +29,9 @@ import io.ballerina.tools.text.TextDocuments; import org.testng.Assert; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; +import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -172,22 +173,16 @@ public static void assertGeneratedSourcesWithNestedDirectories(String subDir, St if (outputDir.contains("tool_test_packaging")) { protocOutputDirPath = Paths.get(GENERATED_SOURCES_DIRECTORY); } else { - protocOutputDirPath = Paths.get(GENERATED_SOURCES_DIRECTORY, outputDir); + protocOutputDirPath = outputDirPath; } - try { - Class grpcCmdClass = Class.forName("io.ballerina.protoc.cli.GrpcCmd"); - GrpcCmd grpcCmd = (GrpcCmd) grpcCmdClass.getDeclaredConstructor().newInstance(); - grpcCmd.setProtoPath(RESOURCE_DIRECTORY + FILE_SEPARATOR + PROTO_FILE_DIRECTORY + subDir); - grpcCmd.setBalOutPath(protocOutputDirPath.toAbsolutePath().toString()); - if (importDir != null) { - grpcCmd.setImportPath(Paths.get(RESOURCE_DIRECTORY.toString(), PROTO_FILE_DIRECTORY, importDir) - .toAbsolutePath().toString()); - } - grpcCmd.execute(); - } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | - NoSuchMethodException | InvocationTargetException e) { - throw new RuntimeException(e); + GrpcCmd grpcCmd = createGrpcCmd(); + grpcCmd.setProtoPath(RESOURCE_DIRECTORY + FILE_SEPARATOR + PROTO_FILE_DIRECTORY + subDir); + grpcCmd.setBalOutPath(protocOutputDirPath.toAbsolutePath().toString()); + if (importDir != null) { + grpcCmd.setImportPath(Paths.get(RESOURCE_DIRECTORY.toString(), PROTO_FILE_DIRECTORY, importDir) + .toAbsolutePath().toString()); } + grpcCmd.execute(); Path destTomlFile = outputDirPath.resolve(BALLERINA_TOML_FILE); copyBallerinaToml(destTomlFile); Assert.assertFalse(hasSemanticDiagnostics(outputDirPath, false)); @@ -204,25 +199,46 @@ public static void assertGeneratedDataTypeSourcesNegative(String subDir, String } public static void generateSourceCode(Path sProtoFilePath, Path sOutputDirPath, String mode, Path sImportDirPath) { - Class grpcCmdClass; - try { - grpcCmdClass = Class.forName("io.ballerina.protoc.cli.GrpcCmd"); - GrpcCmd grpcCmd = (GrpcCmd) grpcCmdClass.getDeclaredConstructor().newInstance(); - grpcCmd.setProtoPath(sProtoFilePath.toAbsolutePath().toString()); - if (!sOutputDirPath.toString().isBlank()) { - grpcCmd.setBalOutPath(sOutputDirPath.toAbsolutePath().toString()); - } - if (mode != null) { - grpcCmd.setMode(mode); - } - if (sImportDirPath != null) { - grpcCmd.setImportPath(sImportDirPath.toAbsolutePath().toString()); - } - grpcCmd.execute(); - } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | - NoSuchMethodException | InvocationTargetException e) { - throw new RuntimeException(e); + GrpcCmd grpcCmd = createGrpcCmd(); + grpcCmd.setProtoPath(sProtoFilePath.toAbsolutePath().toString()); + if (!sOutputDirPath.toString().isBlank()) { + grpcCmd.setBalOutPath(sOutputDirPath.toAbsolutePath().toString()); + } + if (mode != null) { + grpcCmd.setMode(mode); } + if (sImportDirPath != null) { + grpcCmd.setImportPath(sImportDirPath.toAbsolutePath().toString()); + } + grpcCmd.execute(); + } + + /** + * Creates a GrpcCmd instance with a no-op exit handler to prevent System.exit() from killing the test process. + * + * @param outputStream the output stream to capture command output + * @return a GrpcCmd instance configured for testing + */ + public static GrpcCmd createGrpcCmd(ByteArrayOutputStream outputStream) { + PrintStream printStream = new PrintStream(outputStream, true, java.nio.charset.StandardCharsets.UTF_8); + // Use a no-op exit handler to prevent System.exit() from killing the test process + GrpcCmd.ExitHandler noOpExitHandler = code -> { + // Do nothing - just capture the exit intent without actually exiting + }; + return new GrpcCmd(printStream, noOpExitHandler); + } + + /** + * Creates a GrpcCmd instance with a no-op exit handler using System.out. + * + * @return a GrpcCmd instance configured for testing + */ + public static GrpcCmd createGrpcCmd() { + // Use a no-op exit handler to prevent System.exit() from killing the test process + GrpcCmd.ExitHandler noOpExitHandler = code -> { + // Do nothing - just capture the exit intent without actually exiting + }; + return new GrpcCmd(System.out, noOpExitHandler); } public static boolean hasSemanticDiagnostics(Path projectPath, boolean isSingleFile) {