2

I'm trying to write a Java test source. How do I handle auto input for a test target that receives input through Scanner class methods in a multi-threaded environment?

class SystemInInterceptor extends InputStream {
    private String input;
    private int position = 0;

    public SystemInInterceptor(String input) {
        this.input = input + "\n";  // Add a newline at the end to simulate Enter key
    }

    @Override
    public int read() {
        if (position >= input.length()) {
            return -1;
        }
        return input.charAt(position++);
    }
}
Thread[] threads = new Thread[5];
        
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        String testInput = "o\n611019\n2\n748943\n8\n \nq\n";
                        InputStream inputStream = new SystemInInterceptor(testInput);
                        System.setIn(inputStream);
                        App.main(null);
                    } catch (SoldOutException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

        }
        for(Thread thread: threads){
            thread.start();
        }
        for(Thread thread: threads){
            thread.join();
        }

When I run the source above, the first thread works fine, but the other threads throw

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" java.util.NoSuchElementException: No line found

It works fine in non-multithreaded situations.

Tsyvarev
  • 60,011
  • 17
  • 110
  • 153
ehbong
  • 21
  • 2
  • Which line, specifically, throws the exception? What is `App.main(null)`? – markspace Aug 27 '23 at 16:13
  • The method `System.setIn` sets InputStream for **all threads**, that is all 5 your threads share the **same** `InputStream` object and share value of `.position` field in it. If you want each thread to have independent value of `.position`, then you could make this field to be *thread local*. See also [that question](https://stackoverflow.com/a/15526391/3440745) as example of "multiplexing" of output stream between threads. – Tsyvarev Aug 27 '23 at 18:44
  • @markspace yes, App.main(null) line. – ehbong Aug 28 '23 at 10:03

1 Answers1

1

In the simple case that I am aware of, a process can only have one input stream, along with one output stream and one error stream. So if you don't have control over what App.main does, or you don't want to modify it, then you could create multiple processes.

Imagine doing the following multiple times:

  1. Opening a CLI instance.
  2. Executing the runnable program you want (the one that calls App.main as far as I understand from your question), but with redirected input stream.

You can do this programmatically with java.lang.Process.

Processes are heavier than threads (in terms of resource consumption), I know, but in case your scenario is simple (eg you need only a handful of processes in parallel) then you can try the following:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public class MultiSystemIn {
    
    private static class ExecuteProcessRunnable implements Runnable {
        
        private final String input;
        private final List<String> command;
        
        public ExecuteProcessRunnable(final List<String> command,
                                      final String input) {
            this.input = Objects.requireNonNull(input);
            if (command == null || command.isEmpty())
                throw new IllegalArgumentException("Empty command.");
            this.command = new ArrayList<>(command);
        }
        
        @Override
        public void run() {
            final ProcessBuilder builder = new ProcessBuilder(command);
            Process process = null;
            try {
                //Start the command:
                process = builder.start();
                
                //process.getOutputStream() is what the process will be reading as its own System.in...
                try (final OutputStreamWriter osw = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)) {
                    osw.write(input);
                }
                
                //process.getErrorStream() is what we can read as the corresponding System.err of the command.get(0) program...
                try (final InputStreamReader isr = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8);
                     final BufferedReader br = new BufferedReader(isr)) {
                    for (String errline = br.readLine(); errline != null; errline = br.readLine())
                        System.out.println("Process " + command + " error stream says: " + errline);
                }
            }
            catch (final IOException ioe) {
                System.err.println("Process " + command + " failed with " + ioe);
            }
            finally {
                if (process != null) {
                    process.destroyForcibly();
                    try {
                        process.waitFor();
                    }
                    catch (final InterruptedException ie) {
                    }
                }
            }
        }
    }
    
    public static void main(final String[] args) {
        for (int i = 0; i < 10; ++i) {
            System.out.println("Starting process " + i + "...");
            final Thread t = new Thread(new ExecuteProcessRunnable(
                    Arrays.asList("java", "App"),
                    "o\n611019\n2\n748943\n8\n \nq\n"
            ));
            t.setDaemon(false);
            t.start();
        }
    }
}

The above code also uses threads, where each one corresponds to managing one of the processes (eg relaying error stream, writing the standard input of the process and waiting for process termination).

By default a process created this way uses a pipe for its standard input stream, but pipes don't have unlimited buffers (you have to read from them at some point in order to be able to write to them again in the case their buffer gets full). This is the reason why I also used threads, because if one did the same without threads (ie just run a loop in main creating and starting the Processes and then writing to their streams) then in the case the input String got too big for some reason then a pipe could block, not letting the other processes start.

You could do this also with redirecting the streams with files instead of writing manually to the stream of each process. There are also some other customizations one can do (just read the documentation of Process and ProcessBuilder).

gthanop
  • 3,035
  • 2
  • 10
  • 27
  • This method also runs the test target as an independent process, so it is not suitable for concurrency testing for the same value as when using multi-thread. But thank you for your sincere reply. – ehbong Aug 28 '23 at 12:56
  • Ah ok! Indeed I wasn't aware you need to share state between the `App.main` calls. One could maybe use IPC for that (in case they used processes), however I now understand that multiprocessing is not what you are trying to do. – gthanop Aug 28 '23 at 13:32