diff --git a/pom.xml b/pom.xml index 46d5994..edfe57a 100644 --- a/pom.xml +++ b/pom.xml @@ -67,18 +67,6 @@ org.jenkins-ci.plugins.workflow workflow-step-api - - org.parboiled - parboiled-java - 1.3.1 - - - - org.ow2.asm - * - - - com.jayway.jsonpath json-path diff --git a/src/main/java/org/jenkinsci/plugins/tokenmacro/Parser.java b/src/main/java/org/jenkinsci/plugins/tokenmacro/Parser.java index 57bd2ff..4902deb 100644 --- a/src/main/java/org/jenkinsci/plugins/tokenmacro/Parser.java +++ b/src/main/java/org/jenkinsci/plugins/tokenmacro/Parser.java @@ -10,59 +10,54 @@ import org.jenkinsci.plugins.tokenmacro.transform.BeginningOrEndMatchTransorm; import org.jenkinsci.plugins.tokenmacro.transform.ContentLengthTransform; import org.jenkinsci.plugins.tokenmacro.transform.SubstringTransform; -import org.parboiled.*; -import org.parboiled.annotations.SuppressSubnodes; -import org.parboiled.parserunners.ReportingParseRunner; -import org.parboiled.support.Var; import java.io.IOException; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; import java.util.*; +import java.util.stream.Collectors; /** * Created by acearl on 3/6/2016. */ -public class Parser extends BaseParser { +public class Parser { private static final int MAX_RECURSION_LEVEL = 10; - private Stack transforms = new Stack(); - private StringBuffer output; + private Stack transforms = new Stack<>(); + private StringBuilder output; private Run run; private FilePath workspace; private TaskListener listener; private boolean throwException; - private List privateTokens; private String stringWithMacro; private int recursionLevel; + private List privateTokens; + private Stack argInfoStack = new Stack<>(); + private int tokenStartIndex; private String tokenName; private ListMultimap args; - public Parser(Run run, FilePath workspace, TaskListener listener, String stringWithMacro) { + public Parser(Run run, FilePath workspace, TaskListener listener, String stringWithMacro, boolean throwException) { this.run = run; this.workspace = workspace; this.listener = listener; this.stringWithMacro = stringWithMacro; - this.output = new StringBuffer(); + this.output = new StringBuilder(); + this.throwException = throwException; this.recursionLevel = 0; } - public Parser(Run run, FilePath workspace, TaskListener listener, String stringWithMacro, int recursionLevel) { + public Parser(Run run, FilePath workspace, TaskListener listener, String stringWithMacro, boolean throwException, int recursionLevel) { this.run = run; this.workspace = workspace; this.listener = listener; this.stringWithMacro = stringWithMacro; - this.output = new StringBuffer(); - this.recursionLevel = recursionLevel; - } - - public void setThrowException(boolean throwException) { + this.output = new StringBuilder(); this.throwException = throwException; - } - - public void setPrivateTokens(List privateTokens) { - this.privateTokens = privateTokens; + this.recursionLevel = recursionLevel; } public static String process(AbstractBuild build, TaskListener listener, String stringWithMacro, boolean throwException, List privateTokens) throws MacroEvaluationException { @@ -76,219 +71,363 @@ public static String process(Run run, FilePath workspace, TaskListener lis private static String process(Run run, FilePath workspace, TaskListener listener, String stringWithMacro, boolean throwException, List privateTokens, int recursionLevel) throws MacroEvaluationException { if ( StringUtils.isBlank( stringWithMacro ) ) return stringWithMacro; - Parser p = Parboiled.createParser(Parser.class, run, workspace, listener, stringWithMacro, recursionLevel); - p.setThrowException(throwException); - p.setPrivateTokens(privateTokens); + Parser p = new Parser(run, workspace, listener, stringWithMacro, throwException); + p.parse(privateTokens); + return p.output.toString(); + } + + private void parse(List privateTokens) throws MacroEvaluationException { + this.privateTokens = privateTokens; + CharacterIterator c = new StringCharacterIterator(stringWithMacro); try { - new ReportingParseRunner(p.Text()).run(stringWithMacro); - } catch(Exception e) { - if(e.getCause() instanceof MacroEvaluationException) + while (c.current() != CharacterIterator.DONE) { + if (c.current() == '$') { // some sort of token? + tokenStartIndex = c.getIndex(); + parseToken(c); + } else { + output.append(c.current()); + c.next(); + } + } + } catch(Throwable e) { + if(e.getCause() instanceof MacroEvaluationException) { throw (MacroEvaluationException)e.getCause(); + } throw new MacroEvaluationException("Error processing tokens", e); } - - return p.output.toString(); } - public Rule Text() throws InterruptedException, MacroEvaluationException, IOException { - return Sequence( - ZeroOrMore( - FirstOf( - Token(), - Sequence(ANY, appendOutput()))), - EOI); - } + private void parseToken(CharacterIterator c) throws MacroEvaluationException, IOException, InterruptedException { + if(c.current() != '$') { + throw new MacroEvaluationException("Missing $ in macro usage"); + } - public Rule Token() throws InterruptedException, MacroEvaluationException, IOException { - return FirstOf( - EscapedToken(), - DelimitedToken(), - NonDelimitedToken()); + c.next(); + if(c.current() == '$') { + parseEscapedToken(c); + } else if(c.current() == '{') { + parseDelimitedToken(c); + } else { + parseNonDelimitedToken(c); + } } - Rule DelimitedToken() throws InterruptedException, MacroEvaluationException, IOException { - return Sequence('$', '{', - Optional(Sequence('#', addTransform(new ContentLengthTransform()))), - Sequence(Identifier(), startToken()), - Optional(Expansion()), - Optional(Arguments()), - Optional(Spacing()), - '}', - processToken()); - } + private void parseEscapedToken(CharacterIterator c) throws MacroEvaluationException { + if(c.current() != '$') { + throw new MacroEvaluationException("Missing $ in escaped macro"); + } - Rule WhiteSpace() { - return ZeroOrMore(AnyOf(" \t\f")); + output.append(c.current()); + c.next(); + if(c.current() == '{') { + parseEscapedDelimitedToken(c); + } else { + parseEscapedNonDelimitedToken(c); + } } - Rule EscapedToken() { - return FirstOf(EscapedDelimitedToken(), EscapedNonDelimitedToken()); + private void parseDelimitedToken(CharacterIterator c) throws MacroEvaluationException, IOException, InterruptedException { + if(c.current() != '{') { + throw new MacroEvaluationException("Missing { in delimited macro"); + } + + c.next(); + if(c.current() == '#') { + addTransform(new ContentLengthTransform()); + c.next(); + } + + String token = parseIdentifier(c); + startToken(token); + if(c.current() == ':') { + parseSubstringExpansion(c); + } else if(c.current() == '#') { + parseBeginningMatchExpansion(c); + } else if(c.current() == '%') { + parseEndingMatchExpansion(c); + } + + while(Character.isSpaceChar(c.current())) { + c.next(); + } + + if(c.current() == ',') { + parseArguments(c); + } + + if(c.current() != '}') { + throw new MacroEvaluationException("Missing } in macro usage"); + } + + processToken(c.getIndex()); + c.next(); } - Rule EscapedDelimitedToken() { - return Sequence('$', Sequence('$', '{', Identifier(), ZeroOrMore(TestNot('}')), '}'), appendOutput()); + private void parseNonDelimitedToken(CharacterIterator c) throws MacroEvaluationException, IOException, InterruptedException { + String token = parseIdentifier(c); + if(StringUtils.isNotBlank(token)) { + startToken(token); + processToken(c.getIndex()); + } } - Rule EscapedNonDelimitedToken() { - return Sequence('$', Sequence('$', Identifier()), appendOutput()); + private void parseEscapedDelimitedToken(CharacterIterator c) throws MacroEvaluationException { + if(c.current() != '{') { + throw new MacroEvaluationException("Missing { in macro"); + } + + while(c.current() != '}') { + output.append(c.current()); + c.next(); + } + output.append(c.current()); + c.next(); } - Rule NonDelimitedToken() throws InterruptedException, MacroEvaluationException, IOException { - return Sequence('$', Sequence(Identifier(), startToken()), processToken()); + private void parseEscapedNonDelimitedToken(CharacterIterator c) throws MacroEvaluationException { + output.append(parseIdentifier(c)); } - Rule Expansion() { - return FirstOf(SubstringExpansion(), BeginningMatchExpansion(), EndingMatchExpansion()); + private String parseIdentifier(CharacterIterator c) throws MacroEvaluationException { + StringBuilder builder = new StringBuilder(); + + if(Character.isDigit(c.current()) || c.current() == '.' || c.current() == '-') { + // we have a number + output.append('$'); + output.append(parseNumericalValue(c)); + return ""; + } + + if(!Character.isLetter(c.current()) && c.current() != '_') { + throw new MacroEvaluationException("Invalid identifier in macro"); + } + + while(Character.isLetter(c.current()) || Character.isDigit((c.current())) || c.current() == '_') { + builder.append(c.current()); + c.next(); + } + return builder.toString(); } /** * Rule for substring expansion, which is of the form ${TOKEN:offset:length}, where length is optional. offset and * length can be negative, which then operates from the end of the string. */ - Rule SubstringExpansion() { - final Var offset = new Var(0); - final Var length = new Var(Integer.MAX_VALUE); - final Var isOffsetNegative = new Var(false); - final Var isLengthNegative = new Var(false); - return Sequence( - ':', - Sequence(Optional(' ', '-', isOffsetNegative.set(true)), IntegerValue(), offset.set(Integer.parseInt(((String)pop()).trim()))), - Optional(':', Sequence(Optional('-', isLengthNegative.set(true)), IntegerValue(), length.set(Integer.parseInt(((String)pop()).trim())))), - new Action() { - @Override - public boolean run(Context context) { - return addTransform(new SubstringTransform((isOffsetNegative.get() ? -1 : 1) * offset.get(), (isLengthNegative.get() ? -1 : 1) * length.get())); - } - } - ); + private void parseSubstringExpansion(CharacterIterator c) throws MacroEvaluationException { + if(c.current() != ':') { + throw new MacroEvaluationException("Missing : in substring expansion for macro: " + tokenName); + } + + boolean isOffsetNegative = false; + c.next(); + if(c.current() == ' ') { + // we should have a negative number + c.next(); + if(c.current() != '-') { + throw new MacroEvaluationException("Invalid negative offset in substring expansion for macro: " + tokenName); + } + isOffsetNegative = true; + c.next(); + } + + int offset = (isOffsetNegative ? -1 : 1) * Integer.parseInt(parseNumericalValue(c)); + boolean isLengthNegative = false; + int length = Integer.MAX_VALUE; + if(c.current() == ':') { + c.next(); + if(c.current() == '-') { + isLengthNegative = true; + c.next(); + } + length = (isLengthNegative ? -1 : 1) * Integer.parseInt(parseNumericalValue(c)); + } + addTransform(new SubstringTransform(offset, length)); } /** * Rule for beginning match expansion, which is of the form${TOKEN#pattern}, where pattern is a regular expression. * Will check for match at the beginning of the string, and if matched remove the matching text. */ - Rule BeginningMatchExpansion() { - return Sequence('#', - ZeroOrMore( - FirstOf( - NoneOf("\\\"\r\n},"), - Sequence('\\', FirstOf(Sequence(Optional('\r'), '\n'), ANY)) - ) - ), - addTransform(new BeginningOrEndMatchTransorm(match(), true))); - } - - - Rule EndingMatchExpansion() { - return Sequence('%', - ZeroOrMore( - FirstOf( - NoneOf("\\\"\r\n},"), - Sequence('\\', FirstOf(Sequence(Optional('\r'), '\n'), ANY)) - ) - ), - addTransform(new BeginningOrEndMatchTransorm(match(), false))); - } - - Rule Arguments() { - return ZeroOrMore(Sequence(Spacing(), ',', Spacing(), Sequence(Identifier(), push(match())), Spacing(), '=', Spacing(), ArgumentValue()), addArg()); - } + private void parseBeginningMatchExpansion(CharacterIterator c) throws MacroEvaluationException { + if(c.current() != '#') { + throw new MacroEvaluationException("Missing # in beginning match expansion for macro: " + tokenName); + } + c.next(); - Rule Spacing() { - return ZeroOrMore(AnyOf(" \t")); + String match = parseBeginningEndMatchExpansion(c); + addTransform(new BeginningOrEndMatchTransorm(match, true)); } - Rule ArgumentValue() { - return FirstOf(FloatValue(), IntegerValue(), StringValue(), BooleanValue()); - } + private void parseEndingMatchExpansion(CharacterIterator c) throws MacroEvaluationException { + if(c.current() != '%') { + throw new MacroEvaluationException("Missing % in ending match expansion for macro: " + tokenName); + } + c.next(); - Rule IntegerValue() { - return Sequence(FirstOf(HexNumeral(), OctalNumeral(), DecimalNumeral()), push(match())); + String match = parseBeginningEndMatchExpansion(c); + addTransform(new BeginningOrEndMatchTransorm(match, false)); } - Rule HexNumeral() { - return Sequence('0', IgnoreCase('x'), OneOrMore(HexDigit())); + private String parseBeginningEndMatchExpansion(CharacterIterator c) { + StringBuilder match = new StringBuilder(); + while(true) { + if(c.current() == '}' || c.current() == ',') { + break; + } else if(c.current() == '\\') { + c.next(); + if (c.current() != '}' && c.current() != ',') { + match.append('\\'); + } + match.append(c.current()); + } else { + match.append(c.current()); + } + c.next(); + } + return match.toString(); } - Rule OctalNumeral() { - return Sequence('0', OneOrMore(CharRange('0', '7'))); - } + private void parseArguments(CharacterIterator c) throws MacroEvaluationException { + while(c.current() != '}') { + if (c.current() != ',') { + throw new MacroEvaluationException("Missing , for arguments in macro"); + } - Rule DecimalNumeral() { - return FirstOf('0', Sequence(CharRange('1', '9'), ZeroOrMore(Digit()))); - } + c.next(); - Rule DecimalFloat() { - return FirstOf( - Sequence(OneOrMore(Digit()), '.', ZeroOrMore(Digit()), Optional(Exponent())), - Sequence('.', OneOrMore(Digit()), Optional(Exponent())), - Sequence(OneOrMore(Digit()), Exponent()), - Sequence(OneOrMore(Digit()), Optional(Exponent())) - ); - } + while(Character.isSpaceChar(c.current())) { + c.next(); + } - Rule Exponent() { - return Sequence(AnyOf("eE"), Optional(AnyOf("+-")), OneOrMore(Digit())); - } + String argName = parseIdentifier(c); + argInfoStack.push(argName); + while (Character.isSpaceChar(c.current())) { + c.next(); + } - Rule Digit() { - return CharRange('0', '9'); - } + if (c.current() != '=') { + throw new MacroEvaluationException("Missing = for argument in macro"); + } - @SuppressSubnodes - Rule HexFloat() { - return Sequence(HexSignificant(), BinaryExponent()); - } + c.next(); + while (Character.isSpaceChar(c.current())) { + c.next(); + } + parseArgumentValue(c); + addArg(); - Rule HexSignificant() { - return FirstOf( - Sequence(FirstOf("0x", "0X"), ZeroOrMore(HexDigit()), '.', OneOrMore(HexDigit())), - Sequence(HexNumeral(), Optional('.')) - ); + while(Character.isSpaceChar(c.current())) { + c.next(); + } + } } - Rule BinaryExponent() { - return Sequence(AnyOf("pP"), Optional(AnyOf("+-")), OneOrMore(Digit())); + private void parseArgumentValue(CharacterIterator c) throws MacroEvaluationException { + if(c.current() == '"') { + parseStringValue(c); + } else if(c.current() == 't' || c.current() == 'f' || c.current() == 'T' || c.current() == 'F') { + parseBooleanValue(c); + } else { + argInfoStack.push(parseNumericalValue(c)); + } } + private void parseStringValue(CharacterIterator c) throws MacroEvaluationException { + StringBuilder builder = new StringBuilder(); + if(c.current() != '"') { + throw new MacroEvaluationException("Missing \" in argument value for macro: " + tokenName); + } + c.next(); + + boolean escaped = false; + while(true) { + if((c.current() == '\n' || c.current() == '\r') && !escaped) { + throw new MacroEvaluationException("Newlines are not allowed in string arguments for macro: " + tokenName); + } else if(c.current() == '\\') { + escaped = true; + builder.append(c.current()); + c.next(); + } else if(c.current() == '"' && !escaped) { + c.next(); + break; + } else { + builder.append(c.current()); + c.next(); + escaped = false; + } + } - Rule StringValue() { - return Sequence( - '"', - ZeroOrMore( - FirstOf( - NoneOf("\\\"\r\n"), - Sequence('\\', FirstOf(Sequence(Optional('\r'), '\n'), ANY)) - ) - ), - push(unescapeString(match())), - '"' - ); + argInfoStack.push(unescapeString(builder.toString())); } - Rule HexDigit() { - return FirstOf(CharRange('a', 'f'), CharRange('A', 'F'), CharRange('0', '9')); - } + private void parseBooleanValue(CharacterIterator c) throws MacroEvaluationException { + char[] matches = null; + if(Character.toLowerCase(c.current()) == 't') { + matches = new char[] { 't', 'r', 'u', 'e' }; + } else if(Character.toLowerCase(c.current()) == 'f') { + matches = new char[] { 'f', 'a', 'l', 's', 'e' }; + } - Rule BooleanValue() { - return Sequence(FirstOf(String("true"), String("false")), push(match())); - } + if(matches == null) { + throw new MacroEvaluationException("Invalid boolean value for argument for macro: " + tokenName); + } - Rule FloatValue() { - return Sequence(FirstOf(HexFloat(), DecimalFloat()), push(match())); + for(int i = 0; i < matches.length; ++i) { + if(c.current() != matches[i]) { + throw new MacroEvaluationException("Invalid boolean value in macro: " + tokenName); + } + c.next(); + } + argInfoStack.push(new String(matches)); } - Rule Identifier() { - return Sequence(Letter(), ZeroOrMore(LetterOrDigit())); - } + private String parseNumericalValue(CharacterIterator c) throws MacroEvaluationException { + StringBuilder builder = new StringBuilder(); + if(c.current() == '-') { + builder.append(c.current()); + c.next(); + } - Rule Letter() { - return FirstOf(CharRange('a', 'z'), CharRange('A', 'Z'), '_'); - } + if(c.current() != '0') { + if(!Character.isDigit(c.current())) { + throw new MacroEvaluationException("Invalid number value in macro: " + tokenName); + } - Rule LetterOrDigit() { - return FirstOf(CharRange('a', 'z'), CharRange('A', 'Z'), CharRange('0', '9'), '_'); + // we must have a decimal number + while(Character.isDigit(c.current())) { + builder.append(c.current()); + c.next(); + } + } else { + // we could have a decimal, octal or hex number + builder.append(c.current()); + c.next(); + if(c.current() == 'x' || c.current() == 'X') { + // we have a hex number + while(Character.isDigit (c.current()) || (c.current() >= 'a' && c.current() <= 'f') || (c.current() >= 'A' && c.current() <= 'F')) { + builder.append(c.current()); + c.next(); + } + } else if(Character.isDigit(c.current()) && c.current() >= '0' && c.current() <= '7') { + // we have an octal number + while(Character.isDigit(c.current()) && c.current() >= '0' && c.current() <= '7') { + builder.append(c.current()); + c.next(); + } + } else if(Character.isDigit(c.current())) { + // decimal number + boolean foundDecimal = false; + while(Character.isDigit(c.current()) || (c.current() == '.' && !foundDecimal)) { + if(c.current() == '.') { + foundDecimal = true; + } + builder.append(c.current()); + c.next(); + } + } + } + return builder.toString(); } boolean addTransform(Transform t) { @@ -296,16 +435,12 @@ boolean addTransform(Transform t) { return true; } - boolean processToken() throws IOException, InterruptedException, MacroEvaluationException { + boolean processToken(int currentIndex) throws IOException, InterruptedException, MacroEvaluationException { String replacement = null; List all = new ArrayList(TokenMacro.all()); if(privateTokens!=null) { - for(TokenMacro t : privateTokens) { - if(t != null) { - all.add(t); - } - } + all.addAll(privateTokens.stream().filter(x -> x != null).collect(Collectors.toList())); } Map map = new HashMap(); @@ -342,7 +477,7 @@ boolean processToken() throws IOException, InterruptedException, MacroEvaluation throw new MacroEvaluationException(String.format("Unrecognized macro '%s' in '%s'", tokenName, stringWithMacro)); if (replacement == null && !throwException) { // just put the token back in since we don't want to throw the exception - output.append(getContext().getInputBuffer().extract(getContext().getStartIndex(), getContext().getCurrentIndex())); + output.append(stringWithMacro.substring(tokenStartIndex, currentIndex+1)); } else if (replacement != null) { while(transforms != null && transforms.size() > 0) { Transform t = transforms.pop(); @@ -357,15 +492,10 @@ boolean processToken() throws IOException, InterruptedException, MacroEvaluation return true; } - boolean appendOutput() { - output.append(match()); - return true; - } - - boolean startToken() { - tokenName = match(); + boolean startToken(String tokenName) { + this.tokenName = tokenName; if(args == null) { - args = Multimaps.newListMultimap(new TreeMap>(), ArrayList::new); + args = Multimaps.newListMultimap(new TreeMap<>(), () -> new ArrayList()); } else { args.clear(); } @@ -373,8 +503,8 @@ boolean startToken() { } boolean addArg() { - String value = (String)pop(); - String name = (String)pop(); + String value = argInfoStack.pop(); + String name = argInfoStack.pop(); args.put(name, value); return true; } diff --git a/src/main/java/org/jenkinsci/plugins/tokenmacro/impl/EnvironmentVariableMacro.java b/src/main/java/org/jenkinsci/plugins/tokenmacro/impl/EnvironmentVariableMacro.java index db4eca9..c47b629 100644 --- a/src/main/java/org/jenkinsci/plugins/tokenmacro/impl/EnvironmentVariableMacro.java +++ b/src/main/java/org/jenkinsci/plugins/tokenmacro/impl/EnvironmentVariableMacro.java @@ -68,8 +68,8 @@ private String getEnvVarFromWorkflowRun(Run run) { return vars.get(var); } } - } catch(Exception e) { - // we don't need to do anything here... + } catch (Exception e) { + // don't do anything here } return ""; } diff --git a/src/test/java/org/jenkinsci/plugins/tokenmacro/DataBoundTokenMacroTest.java b/src/test/java/org/jenkinsci/plugins/tokenmacro/DataBoundTokenMacroTest.java index 7172f31..280c6a0 100644 --- a/src/test/java/org/jenkinsci/plugins/tokenmacro/DataBoundTokenMacroTest.java +++ b/src/test/java/org/jenkinsci/plugins/tokenmacro/DataBoundTokenMacroTest.java @@ -75,7 +75,7 @@ public void testRecursionLimit() throws Exception { FreeStyleProject p = j.createFreeStyleProject("foo"); FreeStyleBuild b = p.scheduleBuild2(0).get(); - assertEquals("0 1 2 3 4 5 6 7 8 9 10 $RECURSIVE11", TokenMacro.expand(b, TaskListener.NULL, "$RECURSIVE0")); + assertEquals("0 1 2 3 4 5 6 7 8 9 10 DONE!", TokenMacro.expand(b, TaskListener.NULL, "$RECURSIVE0")); } @TestExtension diff --git a/src/test/java/org/jenkinsci/plugins/tokenmacro/PipelineTest.java b/src/test/java/org/jenkinsci/plugins/tokenmacro/PipelineTest.java index e8f7a36..a40f2cc 100644 --- a/src/test/java/org/jenkinsci/plugins/tokenmacro/PipelineTest.java +++ b/src/test/java/org/jenkinsci/plugins/tokenmacro/PipelineTest.java @@ -1,171 +1,171 @@ -package org.jenkinsci.plugins.tokenmacro; - -import com.google.common.collect.ListMultimap; -import hudson.FilePath; -import hudson.model.AbstractBuild; -import hudson.model.Label; -import hudson.model.Run; -import hudson.model.TaskListener; -import hudson.util.StreamTaskListener; -import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; -import org.jenkinsci.plugins.workflow.job.WorkflowJob; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.jvnet.hudson.test.JenkinsRule; -import org.jvnet.hudson.test.TestExtension; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static junit.framework.TestCase.assertEquals; -import static org.junit.Assert.fail; - -/** - * Created by acearl on 6/14/2016. - */ -public class PipelineTest { - private StreamTaskListener listener; - - @Rule - public final JenkinsRule j = new JenkinsRule(); - - @Before - public void setup() throws Exception { - j.createOnlineSlave(Label.get("agents")); - } - - @Test - public void testEnvironmentVariables() throws Exception { - WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); - job.setDefinition(new CpsFlowDefinition("pipeline {\n" + - " agent any\n" + - " environment {\n" + - " VERSION = \"1.0.0\"\n" + - " }\n\n" + - " stages {\n" + - " stage('Cool') {\n" + - " steps {\n" + - " echo \"VERSION=\" + tm('${ENV, var=\"VERSION\"}')\n" + - " }\n" + - " }\n" + - " }\n" + - "}", true)); - Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); - j.assertLogContains("VERSION=1.0.0", run); - } - - @Test - public void testEscapedExpandAll() throws Exception { - WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); - - job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); - Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); - - listener = StreamTaskListener.fromStdout(); - assertEquals(j.jenkins.getRootUrl()+"job/foo/1/",TokenMacro.expand(run,null,listener,"${BUILD_URL}")); - assertEquals(j.jenkins.getRootUrl()+"job/foo/1/",TokenMacro.expand(run,null,listener,"$BUILD_URL")); - - assertEquals("{abc=[def, ghi], jkl=[true]}",TokenMacro.expand(run,null,listener,"${TEST,abc=\"def\",abc=\"ghi\",jkl=true}")); - } - - @Test - public void testException() throws Exception { - WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); - job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); - Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); - - listener = StreamTaskListener.fromStdout(); - - try { - TokenMacro.expand(run,null,listener,"${TEST_NESTEDX}"); - fail(); - } catch(MacroEvaluationException e) { - // do nothing, just want to catch the exception when it occurs - } - - assertEquals(" ${TEST_NESTEDX}", TokenMacro.expand(run,null,listener," ${TEST_NESTEDX}",false,null)); - assertEquals("${TEST_NESTEDX,abc=\"def\",abc=\"ghi\",jkl=true}", TokenMacro.expand(run,null,listener,"${TEST_NESTEDX,abc=\"def\",abc=\"ghi\",jkl=true}",false,null)); - } - - @Test - public void testUnconvertedMacro() throws Exception { - WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); - job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); - Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); - - listener = StreamTaskListener.fromStdout(); - - assertEquals("TEST_2 is not supported in this context", TokenMacro.expand(run,null,listener,"${TEST_2}")); - } - - @TestExtension - public static class TestMacro extends TokenMacro { - private static final String MACRO_NAME = "TEST"; - - @Override - public boolean acceptsMacroName(String macroName) { - return macroName.equals(MACRO_NAME); - } - - @Override - public List getAcceptedMacroNames() { - return Collections.singletonList(MACRO_NAME); - } - - @Override - public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { - return evaluate(context,null,listener,macroName,arguments,argumentMultimap); - } - - @Override - public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { - return argumentMultimap.toString(); - } - } - - @TestExtension - public static class NestedTestMacro extends TokenMacro { - private static final String MACRO_NAME = "TEST_NESTED"; - - @Override - public boolean acceptsMacroName(String macroName) { return macroName.equals(MACRO_NAME); } - - @Override - public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { - return "${TEST,abc=\"def\",abc=\"ghi\",jkl=true}"; - } - - @Override - public List getAcceptedMacroNames() { - return Collections.singletonList(MACRO_NAME); - } - - @Override - public boolean hasNestedContent() { - return true; - } - } - - @TestExtension - public static class TestMacro2 extends TokenMacro { - private static final String MACRO_NAME = "TEST_2"; - - @Override - public boolean acceptsMacroName(String macroName) { - return macroName.equals(MACRO_NAME); - } - - @Override - public List getAcceptedMacroNames() { - return Collections.singletonList(MACRO_NAME); - } - - @Override - public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { - return argumentMultimap.toString(); - } - } -} +package org.jenkinsci.plugins.tokenmacro; + +import com.google.common.collect.ListMultimap; +import hudson.FilePath; +import hudson.model.AbstractBuild; +import hudson.model.Label; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.util.StreamTaskListener; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.fail; + +/** + * Created by acearl on 6/14/2016. + */ +public class PipelineTest { + private StreamTaskListener listener; + + @Rule + public final JenkinsRule j = new JenkinsRule(); + + @Before + public void setup() throws Exception { + j.createOnlineSlave(Label.get("agents")); + } + + @Test + public void testEnvironmentVariables() throws Exception { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); + job.setDefinition(new CpsFlowDefinition("pipeline {\n" + + " agent any\n" + + " environment {\n" + + " VERSION = \"1.0.0\"\n" + + " }\n\n" + + " stages {\n" + + " stage('Cool') {\n" + + " steps {\n" + + " echo \"VERSION=\" + tm('${ENV, var=\"VERSION\"}')\n" + + " }\n" + + " }\n" + + " }\n" + + "}", true)); + Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + j.assertLogContains("VERSION=1.0.0", run); + } + + @Test + public void testEscapedExpandAll() throws Exception { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); + + job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); + Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + + listener = StreamTaskListener.fromStdout(); + assertEquals(j.jenkins.getRootUrl()+"job/foo/1/",TokenMacro.expand(run,null,listener,"${BUILD_URL}")); + assertEquals(j.jenkins.getRootUrl()+"job/foo/1/",TokenMacro.expand(run,null,listener,"$BUILD_URL")); + + assertEquals("{abc=[def, ghi], jkl=[true]}",TokenMacro.expand(run,null,listener,"${TEST,abc=\"def\",abc=\"ghi\",jkl=true}")); + } + + @Test + public void testException() throws Exception { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); + job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); + Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + + listener = StreamTaskListener.fromStdout(); + + try { + TokenMacro.expand(run,null,listener,"${TEST_NESTEDX}"); + fail(); + } catch(MacroEvaluationException e) { + // do nothing, just want to catch the exception when it occurs + } + + assertEquals(" ${TEST_NESTEDX}", TokenMacro.expand(run,null,listener," ${TEST_NESTEDX}",false,null)); + assertEquals("${TEST_NESTEDX,abc=\"def\",abc=\"ghi\",jkl=true}", TokenMacro.expand(run,null,listener,"${TEST_NESTEDX,abc=\"def\",abc=\"ghi\",jkl=true}",false,null)); + } + + @Test + public void testUnconvertedMacro() throws Exception { + WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "foo"); + job.setDefinition(new CpsFlowDefinition("node('agents') {\n\techo 'Hello, world'\n}", true)); + Run run = j.assertBuildStatusSuccess(job.scheduleBuild2(0)); + + listener = StreamTaskListener.fromStdout(); + + assertEquals("TEST_2 is not supported in this context", TokenMacro.expand(run,null,listener,"${TEST_2}")); + } + + @TestExtension + public static class TestMacro extends TokenMacro { + private static final String MACRO_NAME = "TEST"; + + @Override + public boolean acceptsMacroName(String macroName) { + return macroName.equals(MACRO_NAME); + } + + @Override + public List getAcceptedMacroNames() { + return Collections.singletonList(MACRO_NAME); + } + + @Override + public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { + return evaluate(context,null,listener,macroName,arguments,argumentMultimap); + } + + @Override + public String evaluate(Run run, FilePath workspace, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { + return argumentMultimap.toString(); + } + } + + @TestExtension + public static class NestedTestMacro extends TokenMacro { + private static final String MACRO_NAME = "TEST_NESTED"; + + @Override + public boolean acceptsMacroName(String macroName) { return macroName.equals(MACRO_NAME); } + + @Override + public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { + return "${TEST,abc=\"def\",abc=\"ghi\",jkl=true}"; + } + + @Override + public List getAcceptedMacroNames() { + return Collections.singletonList(MACRO_NAME); + } + + @Override + public boolean hasNestedContent() { + return true; + } + } + + @TestExtension + public static class TestMacro2 extends TokenMacro { + private static final String MACRO_NAME = "TEST_2"; + + @Override + public boolean acceptsMacroName(String macroName) { + return macroName.equals(MACRO_NAME); + } + + @Override + public List getAcceptedMacroNames() { + return Collections.singletonList(MACRO_NAME); + } + + @Override + public String evaluate(AbstractBuild context, TaskListener listener, String macroName, Map arguments, ListMultimap argumentMultimap) throws MacroEvaluationException, IOException, InterruptedException { + return argumentMultimap.toString(); + } + } +} diff --git a/src/test/java/org/jenkinsci/plugins/tokenmacro/TokenMacroTest.java b/src/test/java/org/jenkinsci/plugins/tokenmacro/TokenMacroTest.java index 311a973..f874cfc 100644 --- a/src/test/java/org/jenkinsci/plugins/tokenmacro/TokenMacroTest.java +++ b/src/test/java/org/jenkinsci/plugins/tokenmacro/TokenMacroTest.java @@ -76,15 +76,23 @@ public void testVeryLongStringArg() throws Exception { } @Test - public void testMultilineStringArgs() throws Exception { + public void testMultilineStringArg() throws Exception { FreeStyleProject p = j.createFreeStyleProject("foo"); FreeStyleBuild b = p.scheduleBuild2(0).get(); listener = StreamTaskListener.fromStdout(); - assertEquals("{arg=[a \n b \r\n c]}\n",TokenMacro.expand(b, listener, "${TEST, arg = \"a \\\n b \\\r\n c\"}\n")); + assertEquals("{arg=[a \n b \r\n c]}\n",TokenMacro.expand(b, listener, "${TEST, arg = \"a \\\n b \\\r\\\n c\"}\n")); + } + + @Test(expected = MacroEvaluationException.class) + public void testInvalidMultilineStringArg() throws Exception { + FreeStyleProject p = j.createFreeStyleProject("foo"); + FreeStyleBuild b = p.scheduleBuild2(0).get(); + + listener = StreamTaskListener.fromStdout(); - assertEquals("${TEST, arg = \"a \n b \r\n c\"}\n",TokenMacro.expand(b, listener, "${TEST, arg = \"a \n b \r\n c\"}\n")); + TokenMacro.expand(b, listener, "${TEST, arg = \"a \n b \r\n c\"}\n"); } @Test