Skip to content

Commit

Permalink
[external commands]
Browse files Browse the repository at this point in the history
- support the console display while external program is actively executing via `nexial.external.console` (default is `false`).

Signed-off-by: automike <mikeliucc@users.noreply.github.com>
  • Loading branch information
mikeliucc committed May 31, 2019
1 parent c26ce18 commit caf727b
Show file tree
Hide file tree
Showing 18 changed files with 352 additions and 26 deletions.
69 changes: 56 additions & 13 deletions src/main/java/org/nexial/commons/proc/ProcessInvoker.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@
package org.nexial.commons.proc;

import java.io.*;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.nexial.commons.utils.FileUtil;
import org.nexial.core.utils.ConsoleUtils;

import static org.nexial.core.NexialConst.DEF_FILE_ENCODING;

/**
* A fairly straight-forward of invoking another process from current JVM process. This class assumes 3 arguments,
Expand All @@ -45,27 +50,44 @@
public class ProcessInvoker {
public static final String WORKING_DIRECTORY = "[working.directory]";
public static final String PROC_REDIRECT_OUT = "[proc.redirect]";
public static final String PROC_CONSOLE_OUT = "[proc.console]";
public static final String PROC_CONSOLE_ID = "[proc.consoleId]";

private static final String NL = System.getProperty("line.separator");

@SuppressWarnings("PMD.DoNotUseThreads")
/** internally class to trap stdout/stderr output. */
private static class StreamGobbler extends Thread {
private static final int DEFAULT_SIZE = 32 * 1024;
private static final String NL = System.getProperty("line.separator");

private String output;
private BufferedReader reader;
private InputStreamReader streamReader;
private StringWriter stringWriter;
private boolean enableConsole;
private String consoleId = "";

StreamGobbler(InputStream is) {
streamReader = new InputStreamReader(is);
reader = new BufferedReader(streamReader);
stringWriter = new StringWriter(DEFAULT_SIZE);
}

public StreamGobbler setEnableConsole(boolean enableConsole) {
this.enableConsole = enableConsole;
return this;
}

public StreamGobbler setConsoleId(String consoleId) {
this.consoleId = consoleId;
return this;
}

public void run() {
try {
String line;
while ((line = reader.readLine()) != null) {
if (enableConsole) { ConsoleUtils.log(consoleId, line); }
stringWriter.write(line);
stringWriter.write(NL);
}
Expand Down Expand Up @@ -108,16 +130,31 @@ public static ProcessOutcome invoke(String command, List<String> params, Map<Str
processArg.add(0, command);

// create processbuilder and mod. environment, if need be
ProcessBuilder pb = new ProcessBuilder(processArg);
String[] envStrings = prepareEnv(pb, env, outcome);
ProcessBuilder pb = new ProcessBuilder(processArg).redirectErrorStream(true);

boolean enableConsole = false;
String consoleId = null;
File out = null;
if (env != null) {
if (env.containsKey(PROC_REDIRECT_OUT)) { out = new File(env.remove(PROC_REDIRECT_OUT)); }
enableConsole = BooleanUtils.toBoolean(env.remove(PROC_CONSOLE_OUT));
consoleId = env.remove(PROC_CONSOLE_ID);
}

prepareEnv(pb, env, outcome);

// here we go...
// jdk5-specific...
Process process = pb.start();

StreamGobbler stderr = new StreamGobbler(process.getErrorStream());
StreamGobbler stderr = new StreamGobbler(process.getErrorStream())
.setEnableConsole(enableConsole)
.setConsoleId(consoleId);
stderr.start();
StreamGobbler stdout = new StreamGobbler(process.getInputStream());

StreamGobbler stdout = new StreamGobbler(process.getInputStream())
.setEnableConsole(enableConsole)
.setConsoleId(consoleId);
stdout.start();

int exitValue = 0;
Expand All @@ -132,6 +169,18 @@ public static ProcessOutcome invoke(String command, List<String> params, Map<Str
outcome.setStdout(stdout.getOutput());
outcome.setExitStatus(exitValue);

if (out != null) {
FileUtils.writeStringToFile(out, outcome.getStdout(), DEF_FILE_ENCODING);
if (StringUtils.isNotBlank(outcome.getStderr())) {
FileUtils.writeStringToFile(out,
NL + NL +
"ERROR:" + NL +
outcome.getStderr(),
DEF_FILE_ENCODING,
true);
}
}

// be a good java citizen
stderr = null;
stdout = null;
Expand All @@ -151,13 +200,7 @@ public static void invokeNoWait(String command, List<String> params, Map<String,
processArg.add(0, command);

// create processbuilder and mod. environment, if need be
ProcessBuilder pb;
if (env != null && env.containsKey(PROC_REDIRECT_OUT)) {
File out = new File(env.remove(PROC_REDIRECT_OUT));
pb = new ProcessBuilder(processArg).redirectErrorStream(true).redirectOutput(Redirect.appendTo(out));
} else {
pb = new ProcessBuilder(processArg);
}
ProcessBuilder pb = new ProcessBuilder(processArg).inheritIO();

prepareEnv(pb, env, outcome);

Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/nexial/core/NexialConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public final class NexialConst {
// plugin:external
// store the file name of the output resulted from a `external.runProgram` command
public static final String OPT_RUN_PROGRAM_OUTPUT = registerSystemVariable(NAMESPACE + "external.output");
public static final String OPT_RUN_PROGRAM_CONSOLE = registerSystemVariable(NAMESPACE + "external.console", false);

// plugin:rdbms
public static final String DAO_PREFIX = NAMESPACE + "dao.";
Expand Down
4 changes: 3 additions & 1 deletion src/main/kotlin/org/nexial/core/CommandConst.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ object CommandConst {

// common commands
const val CMD_VERBOSE = "base.verbose(text)"
const val CMD_RUN_PROGRAM = "external.runProgram(programPathAndParams)"
const val CMD_RUN_PROGRAM_NO_WAIT = "external.runProgramNoWait(programPathAndParams)"

@JvmStatic
val MERGE_OUTPUTS: MutableList<String> = Arrays.asList(".", CMD_VERBOSE)
val MERGE_OUTPUTS: MutableList<String> = Arrays.asList(".", CMD_VERBOSE, CMD_RUN_PROGRAM, CMD_RUN_PROGRAM_NO_WAIT)

const val CMD_MACRO = "base.macro(file,sheet,name)"
const val CMD_REPEAT_UNTIL = "base.repeatUntil(steps,maxWaitMs)"
Expand Down
47 changes: 35 additions & 12 deletions src/main/kotlin/org/nexial/core/plugins/external/ExternalCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,20 @@

package org.nexial.core.plugins.external

import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.junit.runner.JUnitCore
import org.nexial.commons.proc.ProcessInvoker
import org.nexial.commons.proc.ProcessInvoker.PROC_REDIRECT_OUT
import org.nexial.commons.proc.ProcessInvoker.*
import org.nexial.commons.proc.RuntimeUtils
import org.nexial.commons.utils.TextUtils
import org.nexial.core.NexialConst.DEF_CHARSET
import org.nexial.core.NexialConst.OPT_RUN_PROGRAM_OUTPUT
import org.nexial.core.NexialConst.*
import org.nexial.core.SystemVariables.getDefaultBool
import org.nexial.core.model.StepResult
import org.nexial.core.plugins.base.BaseCommand
import org.nexial.core.utils.CheckUtils.requires
import org.nexial.core.utils.CheckUtils.requiresNotBlank
import org.nexial.core.variable.Syspath
import java.io.File
import java.io.File.separator
import java.io.IOException
import java.lang.System.lineSeparator
Expand Down Expand Up @@ -93,13 +91,24 @@ class ExternalCommand : BaseCommand() {
requires(StringUtils.isNotBlank(programPathAndParams), "empty/null programPathAndParams")

try {
val output = exec(programPathAndParams)
// val output = exec(programPathAndParams)

val programAndParams = RuntimeUtils.formatCommandLine(programPathAndParams)
if (programAndParams.isEmpty()) {
throw IllegalArgumentException("Unable to parse programPathAndParams: $programAndParams")
}

//attach link to results
val outputFileName = "runProgram_${context.currentTestStep.row[0].reference}.log"
val currentRow = context.currentTestStep.row[0].reference
val outputFileName = "runProgram_$currentRow.log"
context.setData(OPT_RUN_PROGRAM_OUTPUT, outputFileName)
val fileName = Syspath().out("fullpath") + separator + outputFileName
FileUtils.write(File(fileName), output, DEF_CHARSET, false)

val env = prepEnv(fileName, currentRow)

ProcessInvoker.invoke(programAndParams[0], programAndParams.filterIndexed { index, _ -> index > 0 }, env)

//attach link to results
addLinkRef("Follow the link to view the output", "output", fileName)

return StepResult.success()
Expand All @@ -117,14 +126,14 @@ class ExternalCommand : BaseCommand() {
throw IllegalArgumentException("Unable to parse programPathAndParams: $programAndParams")
}

val outputFileName = "runProgramNoWait_${context.currentTestStep.row[0].reference}.log"
val currentRow = context.currentTestStep.row[0].reference
val outputFileName = "runProgramNoWait_$currentRow.log"
context.setData(OPT_RUN_PROGRAM_OUTPUT, outputFileName)
val fileName = Syspath().out("fullpath") + separator + outputFileName

val env = mutableMapOf<String, String>()
env[PROC_REDIRECT_OUT] = fileName
val env = prepEnv(fileName, currentRow)

ProcessInvoker.invokeNoWait(programAndParams[0], programAndParams.asList().drop(0), env)
ProcessInvoker.invokeNoWait(programAndParams[0], programAndParams.filterIndexed { index, _ -> index > 0 }, env)

//attach link to results
addLinkRef("Follow the link to view the output", "output", fileName)
Expand All @@ -135,6 +144,20 @@ class ExternalCommand : BaseCommand() {
}
}

private fun prepEnv(outputFile: String, currentRow: String): MutableMap<String, String> {
val env = mutableMapOf<String, String>()

env[PROC_REDIRECT_OUT] = outputFile

val consoleOut = context.getBooleanData(OPT_RUN_PROGRAM_CONSOLE, getDefaultBool(OPT_RUN_PROGRAM_CONSOLE))
if (consoleOut) {
env[PROC_CONSOLE_OUT] = "true"
env[PROC_CONSOLE_ID] = "${context.runId}][$currentRow"
}

return env
}

companion object {
@Throws(IOException::class)
fun exec(programPathAndParams: String): String {
Expand Down
Empty file.
11 changes: 11 additions & 0 deletions src/test/resources/showcase/artifact/data/dummy.data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
query
math
science
physics
computer
programming
physed
spanish
basketball
guitar
music
Loading

0 comments on commit caf727b

Please sign in to comment.