1

I'm working on a kotlin application to send data to a third party application. The vendor has provided a jar file that we will need to run in order to actually transmit the data to them (for more details see: How do I execute a jar file as part of a kotlin application?)

While I have figured out how to execute the jar using java.lang.ProcessBuilder, I am struggling to implement a unit test. I wanted to create a test of my logic that calls the jar file without having to worry about the contents of the actual jar. To do that I created a simple java class, packaged it into a jar, and then put that jar into src/test/resources.

In the unit test, I then load the jar as an input stream via a class loader. That input stream then gets written to a tmp file. When I call my function using that tmp file I get an error like this:

java.lang.Exception: Error: Unable to access jarfile hello15286565382928855962.jar

After some experimenting, I found that I can get around this by passing the absolute file path and the original (non-tmp) file to my function. The problem is, that will only work on my local machine and I need this test to be compatible with a CI pipeline.

So then, how to I load a test resource and get the path and file name without needing a hard coded file path?

My dummy class (HelloTest.jar):

public class Main {
    public static void main(String[] args) throws Exception {
        //TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
        // to see how IntelliJ IDEA suggests fixing it.
        System.out.printf("Hello and welcome!");

        if( args.length > 1 ) {
            throw new Exception("Too many args");
        }

        for (int i = 1; i <= 5; i++) {
            //TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
            // for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
            System.out.println("i = " + i);
        }
    }
}

My kotlin function to run the jar:

internal fun executeJavaJar(workingDir:String, jarFile:String, args:List<String>): Either<Throwable, String>
{
   return runCatching {
       val cmdArgs: MutableList<String> = mutableListOf("java", "-jar", jarFile)
       cmdArgs.addAll(args)
       ProcessBuilder(cmdArgs).directory(File(workingDir))
           .redirectOutput(ProcessBuilder.Redirect.PIPE)
           .redirectError(ProcessBuilder.Redirect.PIPE).start()
    }.fold({
        it->
        val output = IOUtils.toString( it.inputStream, Charsets.UTF_8)
        val error = IOUtils.toString( it.errorStream, Charsets.UTF_8)
       if(error.isNotEmpty())
       {
          throw Exception(error)
       }
       else {
           output.right()
       }
    }, {
        error->
        error.left()
    })
}

My unit test:

@Test
fun `test execute java jar`()
{
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResourceAsStream("HelloTest.jar").use { stream ->                 

                //this works locally but would fail in a CI build
                executeJavaJar("/Users/userA/Documents/workspace/app-event-management/lambda-INT295/src/test/resources",
                    "HelloTest.jar", listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, {
                        output->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}

@Test
fun `test execute java jar with tmp`()
{
    mockContext(300).value().let { context ->
        testInt295DependencyProvider(context).let { dp ->
            javaClass.classLoader.getResourceAsStream("HelloTest.jar").use { stream ->
                assertNotNull(stream)
                val inputFile = createTempFile(prefix = "hello", suffix = ".jar").toFile()
                inputFile.copyInputStreamToFile(stream)

                //this throws an access error
                executeJavaJar(".",
                    inputFile.name, listOf("abc")).fold({
                    fail("test execute java jar failed with error: ${it.message}")
                }, {
                        output->
                    println(output)
                    assertNotNull(output)
                    assertThat(output).isNotEmpty
                })
            }
        }
    }
}
6
  • Why not just use getResource instead of getResourceAsStream? You'd get a URL to the jar file and you can pass that to the java -jar command. Commented May 13 at 16:28
  • createTempFile creates the file in the temp directory, but you're passing "." to executeJavaJar instead. Commented May 13 at 16:38
  • @Sweeper really? You can use a URL in a java -jar command? I don't see that documented at docs.oracle.com/en/java/javase/21/docs/specs/man/… Commented May 13 at 16:43
  • I mean the path component of the URL of course :) Commented May 13 at 16:44
  • But the URL might be an entry inside an enclosing jar file. The path component would be unusable in such a case. Commented May 13 at 16:45

2 Answers 2

2

As k314159 said in the comments, you are running java -jar in the current directory, but you are not passing the whole path of the jar file to java -jar. The temporary jar file is created in a temporary directory, which probably is not the current directory.

You can pass the absolutePath of the file instead:

executeJavaJar(".", inputFile.absolutePath, listOf("abc"))
Sign up to request clarification or add additional context in comments.

Comments

2

Instead of passing inputFile.name, pass inputFile.path. The name is just the name part of the file, which doesn't include the directory. The path is the full path. You need to specify the full path because createTempFile creates the file in the default temporary directory (the exact location is platform-dependent).

However, I would also advise that you stop using classes from java.io (such as File) and instead directly use classes from java.nio (such as Path).

    val inputFile = createTempFile(prefix = "hello", suffix = ".jar")
    Files.copy(stream, inputFile)

    executeJavaJar(".", inputFile.toString(), listOf("abc"))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.