Repository: zeppelin Updated Branches: refs/heads/master 5f88452d6 -> 4e9d2c449
[ZEPPELIN-1967] Passing Z variables to Shell and SQL Interpreters ### What is this PR for? The code in this PR enables embedding/interpolating Z variables into command strings passed to Spark's SQL and Shell interpreters. It implements the functionality described in issue [ZEPPELIN-1967](https://issues.apache.org/jira/browse/ZEPPELIN-1967) This PR resumes a fresh effort while taking into consideration all of the discussion in the context of the earlier [PR-2502](https://github.com/apache/zeppelin/pull/2502). The earlier PR-2502 was closed due to a corruption in my repo that could not be corrected. The code in this PR resolves all of the discussion and suggestions in the body of the earlier [PR-2502](https://github.com/apache/zeppelin/pull/2502). The following description is a summary of the current implementation: Patterns of the form `{var-name}` within commands will be interpolated only if a predefined object of the specified name exists in `z`. Additionally, all such patterns within the command line should also be translatable for any interpolation to occur. Partial translation of a command line (where only some of the patterns are translated, and others are not) is never performed. Patterns of the form `{{any-text}}` are translated into `{any-text}`. This feature is an escaping mechanism that allows `{` and `}` characters to be passed into a command without invoking the interpolation mechanism. The translations described above are performed only when all occurrences of `{`, `}`, `{{`, and `}}` in any command string conform to one of the two forms described above. A command that contains `{` and/or `}` characters used in any other way (than `{var-name}` and `{{any-text}}` as described above) will be used as-is without attempting any translations whatsoever -- even if the command also contains legal, translate-able and/or escape-able, constructs of the above two forms. No error is flagged in any case. This behavior is identical to the implementation of a similar feature in Jupyter's shell invocation using the ! magic command. At present only the SQL and Shell interpreters support object interpolation. ### What type of PR is it? [Improvement] ### Todos * [ ] - Task ### What is the Jira issue? https://issues.apache.org/jira/browse/ZEPPELIN-1967 ### How should this be tested? A new unit-test class ZeppCtxtVariableTest.java (see below) has been added. The attached screenshots below also show tests of the functionality. ### Screenshots (if appropriate) ![figure-1](https://user-images.githubusercontent.com/477015/36956999-5f8cca92-2057-11e8-8b76-f4ccd2a21d50.png) ![figure-2](https://user-images.githubusercontent.com/477015/36957001-650f271c-2057-11e8-8e94-4805fd24e796.png) ![figure-3](https://user-images.githubusercontent.com/477015/36957005-6a747dec-2057-11e8-9c72-4ebef17b52db.png) ![figure-4](https://user-images.githubusercontent.com/477015/36957006-6df95ad2-2057-11e8-8585-3eb679e3a146.png) ![figure-5](https://user-images.githubusercontent.com/477015/36957011-7284be02-2057-11e8-9204-3774121397e6.png) ### Questions: * Does the licenses files need update? No * Is there breaking changes for older versions? No * Does this needs documentation? Yes, and detailed documentation has been added to the part describing ZeppelinContext variables (see file spark.md below). Author: Sanjay Dasgupta <sanjay.dasgu...@gmail.com> Closes #2834 from sanjaydasgupta/ZEPPELIN-1967 and squashes the following commits: 77738aa [Sanjay Dasgupta] Changes to comply with Felix Cheung's comment at https://github.com/apache/zeppelin/pull/2834#discussion_r176976263 and Jeff Zhang's subsequent clarification 5f8505b [Sanjay Dasgupta] Changes due to Felix Cheung's comments at https://github.com/apache/zeppelin/pull/2834#pullrequestreview-106738198 d600d86 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 cc3727f [Sanjay Dasgupta] Changes due the Jeff Zhang's comments at https://github.com/apache/zeppelin/pull/2834/files/1e2c87dd36dc091ca898baf8e9f178d6d1a5e600#r176930418 1e2c87d [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 3dd3dd8 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 a1703b8 [Sanjay Dasgupta] Changes suggested in Felix Cheung's review https://github.com/apache/zeppelin/pull/2834#pullrequestreview-104805661 b7ddf6b [Sanjay Dasgupta] Implementing configuration (global enable/disable interpolation) following https://github.com/apache/zeppelin/pull/2834#issuecomment-373948398 5268803 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 1718e79 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 3b30ea2 [Sanjay Dasgupta] Reversing previous incorrect update 3beebce [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 f43fd99 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 a3215fc [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 ced295c [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 b461c82 [Sanjay Dasgupta] Merge branch 'master' of https://github.com/apache/zeppelin into ZEPPELIN-1967 2868825 [Sanjay Dasgupta] ZEPPELIN-1967: Initial updates Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/4e9d2c44 Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/4e9d2c44 Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/4e9d2c44 Branch: refs/heads/master Commit: 4e9d2c449c071871753c8276ec0bcb1165515884 Parents: 5f88452 Author: Sanjay Dasgupta <sanjay.dasgu...@gmail.com> Authored: Mon Mar 26 13:31:09 2018 +0530 Committer: Jeff Zhang <zjf...@apache.org> Committed: Wed Mar 28 09:19:58 2018 +0800 ---------------------------------------------------------------------- docs/interpreter/shell.md | 29 ++- docs/interpreter/spark.md | 54 +++++ .../apache/zeppelin/shell/ShellInterpreter.java | 4 +- .../src/main/resources/interpreter-setting.json | 7 + .../zeppelin/spark/SparkSqlInterpreter.java | 4 +- .../src/main/resources/interpreter-setting.json | 7 + .../zeppelin/interpreter/Interpreter.java | 34 +++ .../interpreter/ZeppCtxtVariableTest.java | 212 +++++++++++++++++++ 8 files changed, 348 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/docs/interpreter/shell.md ---------------------------------------------------------------------- diff --git a/docs/interpreter/shell.md b/docs/interpreter/shell.md index d285cf4..25349aa 100644 --- a/docs/interpreter/shell.md +++ b/docs/interpreter/shell.md @@ -35,7 +35,7 @@ At the "Interpreters" menu in Zeppelin dropdown menu, you can set the property v <table class="table-configuration"> <tr> <th>Name</th> - <th>Value</th> + <th>Default</th> <th>Description</th> </tr> <tr> @@ -63,6 +63,11 @@ At the "Interpreters" menu in Zeppelin dropdown menu, you can set the property v <td></td> <td>The path to the keytab file</td> </tr> + <tr> + <td>zeppelin.shell.interpolation</td> + <td>false</td> + <td>Enable ZeppelinContext variable interpolation into paragraph text</td> + </tr> </table> ## Example @@ -82,3 +87,25 @@ export LAUNCH_KERBEROS_REFRESH_INTERVAL=4h # Change kinit number retries (default value is 5), which means if the kinit command fails for 5 retries consecutively it will close the interpreter. export KINIT_FAIL_THRESHOLD=10 ``` + +## Object Interpolation +The shell interpreter also supports interpolation of `ZeppelinContext` objects into the paragraph text. +The following example shows one use of this facility: + +####In Scala cell: +``` +z.put("dataFileName", "members-list-003.parquet") + // ... +val members = spark.read.parquet(z.get("dataFileName")) + // ... +``` + +####In later Shell cell: +``` +%sh rm -rf {dataFileName} +``` + +Object interpolation is disabled by default, and can be enabled (for the Shell interpreter) by +setting the value of the property `zeppelin.shell.interpolation` to `true` (see _Configuration_ above). +More details of this feature can be found in the Spark interpreter documentation under +[Object Interpolation](spark.html#object-interpolation) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/docs/interpreter/spark.md ---------------------------------------------------------------------- diff --git a/docs/interpreter/spark.md b/docs/interpreter/spark.md index 90b1608..3999e3a 100644 --- a/docs/interpreter/spark.md +++ b/docs/interpreter/spark.md @@ -146,6 +146,11 @@ You can also set other Spark properties which are not listed in the table. For a <td>Do not change - developer only setting, not for production use</td> </tr> <tr> + <td>zeppelin.spark.sql.interpolation</td> + <td>false</td> + <td>Enable ZeppelinContext variable interpolation into paragraph text</td> + </tr> + <tr> <td>zeppelin.spark.uiWebUrl</td> <td></td> <td>Overrides Spark UI default URL. Value should be a full URL (ex: http://{hostName}/{uniquePath}</td> @@ -365,6 +370,55 @@ myScalaDataFrame = DataFrame(z.get("myScalaDataFrame"), sqlContext) </div> </div> +### Object Interpolation +Some interpreters can interpolate object values from `z` into the paragraph text by using the +`{variable-name}` syntax. The value of any object previously `put` into `z` can be +interpolated into a paragraph text by using such a pattern containing the object's name. +The following example shows one use of this facility: + +####In Scala cell: +``` +z.put("minAge", 35) +``` + +####In later SQL cell: +``` +%sql select * from members where age >= {minAge} +``` + +The interpolation of a `{var-name}` pattern is performed only when `z` contains an object with the specified name. +But the pattern is left unchanged if the named object does not exist in `z`. +Further, all `{var-name}` patterns within the paragraph text must must be translatable for any interpolation to occur -- +translation of only some of the patterns in a paragraph text is never done. + +In some situations, it is necessary to use { and } characters in a paragraph text without invoking the +object interpolation mechanism. For these cases an escaping mechanism is available -- +doubled braces {{ and }} should be used. The following example shows the use of {{ and }} for passing a +regular expression containing just { and } into the paragraph text. + +``` +%sql select * from members where name rlike '[aeiou]{{3}}' +``` + +To summarize, patterns of the form `{var-name}` within the paragraph text will be interpolated only if a predefined +object of the specified name exists. Additionally, all such patterns within the paragraph text should also +be translatable for any interpolation to occur. Patterns of the form `{{any-text}}` are translated into `{any-text}`. +These translations are performed only when all occurrences of `{`, `}`, `{{`, and `}}` in the paragraph text conform +to one of the two forms described above. Paragraph text containing `{` and/or `}` characters used in any other way +(than `{var-name}` and `{{any-text}}`) is used as-is without any changes. +No error is flagged in any case. This behavior is identical to the implementation of a similar feature in +Jupyter's shell invocation using the `!` magic command. + +This feature is disabled by default, and must be explicitly turned on for each interpreter independently +by setting the value of an interpreter-specific property to `true`. +Consult the _Configuration_ section of each interpreter's documentation +to find out if object interpolation is implemented, and the name of the parameter that must be set to `true` to +enable the feature. The name of the parameter used to enable this feature it is different for each interpreter. +For example, the SparkSQL and Shell interpreters use the parameter names `zeppelin.spark.sql.interpolation` and +`zeppelin.shell.interpolation` respectively. + +At present only the SparkSQL and Shell interpreters support object interpolation. + ### Form Creation `ZeppelinContext` provides functions for creating forms. http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java ---------------------------------------------------------------------- diff --git a/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java b/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java index 9f6b11d..c686896 100644 --- a/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java +++ b/shell/src/main/java/org/apache/zeppelin/shell/ShellInterpreter.java @@ -85,7 +85,9 @@ public class ShellInterpreter extends KerberosInterpreter { @Override - public InterpreterResult interpret(String cmd, InterpreterContext contextInterpreter) { + public InterpreterResult interpret(String originalCmd, InterpreterContext contextInterpreter) { + String cmd = Boolean.parseBoolean(getProperty("zeppelin.shell.interpolation")) ? + interpolate(originalCmd, contextInterpreter.getResourcePool()) : originalCmd; LOGGER.debug("Run shell command '" + cmd + "'"); OutputStream outStream = new ByteArrayOutputStream(); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/shell/src/main/resources/interpreter-setting.json ---------------------------------------------------------------------- diff --git a/shell/src/main/resources/interpreter-setting.json b/shell/src/main/resources/interpreter-setting.json index 45a9719..a332134 100644 --- a/shell/src/main/resources/interpreter-setting.json +++ b/shell/src/main/resources/interpreter-setting.json @@ -38,6 +38,13 @@ "defaultValue": "", "description": "Kerberos principal", "type": "string" + }, + "zeppelin.shell.interpolation": { + "envName": null, + "propertyName": "zeppelin.shell.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" } }, "editor": { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java ---------------------------------------------------------------------- diff --git a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java index 9709f9e..90d7bc9 100644 --- a/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java +++ b/spark/interpreter/src/main/java/org/apache/zeppelin/spark/SparkSqlInterpreter.java @@ -115,7 +115,9 @@ public class SparkSqlInterpreter extends Interpreter { // to def sql(sqlText: String): DataFrame (1.3 and later). // Therefore need to use reflection to keep binary compatibility for all spark versions. Method sqlMethod = sqlc.getClass().getMethod("sql", String.class); - rdd = sqlMethod.invoke(sqlc, st); + String effectiveString = Boolean.parseBoolean(getProperty("zeppelin.spark.sql.interpolation")) ? + interpolate(st, context.getResourcePool()) : st; + rdd = sqlMethod.invoke(sqlc, effectiveString); } catch (InvocationTargetException ite) { if (Boolean.parseBoolean(getProperty("zeppelin.spark.sql.stacktrace"))) { throw new InterpreterException(ite); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/spark/interpreter/src/main/resources/interpreter-setting.json ---------------------------------------------------------------------- diff --git a/spark/interpreter/src/main/resources/interpreter-setting.json b/spark/interpreter/src/main/resources/interpreter-setting.json index 7e647d7..db3aebb 100644 --- a/spark/interpreter/src/main/resources/interpreter-setting.json +++ b/spark/interpreter/src/main/resources/interpreter-setting.json @@ -108,6 +108,13 @@ "description": "Show full exception stacktrace for SQL queries if set to true.", "type": "checkbox" }, + "zeppelin.spark.sql.interpolation": { + "envName": null, + "propertyName": "zeppelin.spark.sql.interpolation", + "defaultValue": false, + "description": "Enable ZeppelinContext variable interpolation into paragraph text", + "type": "checkbox" + }, "zeppelin.spark.maxResult": { "envName": "ZEPPELIN_SPARK_MAXRESULT", "propertyName": "zeppelin.spark.maxResult", http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java index 7b591e7..6075aea 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/Interpreter.java @@ -23,6 +23,8 @@ import org.apache.commons.lang.reflect.FieldUtils; import org.apache.zeppelin.annotation.Experimental; import org.apache.zeppelin.annotation.ZeppelinApi; import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion; +import org.apache.zeppelin.resource.Resource; +import org.apache.zeppelin.resource.ResourcePool; import org.apache.zeppelin.scheduler.Scheduler; import org.apache.zeppelin.scheduler.SchedulerFactory; import org.slf4j.Logger; @@ -37,6 +39,9 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Interface for interpreters. * If you want to implement new Zeppelin interpreter, extend this class @@ -78,6 +83,35 @@ public abstract class Interpreter { return null; } + protected String interpolate(String cmd, ResourcePool resourcePool) { + Pattern zVariablePattern = Pattern.compile("([^{}]*)([{]+[^{}]*[}]+)(.*)", Pattern.DOTALL); + StringBuilder sb = new StringBuilder(); + Matcher m; + String st = cmd; + while ((m = zVariablePattern.matcher(st)).matches()) { + sb.append(m.group(1)); + String varPat = m.group(2); + if (varPat.matches("[{][^{}]+[}]")) { + // substitute {variable} only if 'variable' has a value ... + Resource resource = resourcePool.get(varPat.substring(1, varPat.length() - 1)); + Object variableValue = resource == null ? null : resource.get(); + if (variableValue != null) + sb.append(variableValue); + else + return cmd; + } else if (varPat.matches("[{]{2}[^{}]+[}]{2}")) { + // escape {{text}} ... + sb.append("{").append(varPat.substring(2, varPat.length() - 2)).append("}"); + } else { + // mismatched {{ }} or more than 2 braces ... + return cmd; + } + st = m.group(3); + } + sb.append(st); + return sb.toString(); + } + /** * Run code and return result, in synchronous way. * http://git-wip-us.apache.org/repos/asf/zeppelin/blob/4e9d2c44/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java ---------------------------------------------------------------------- diff --git a/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java new file mode 100644 index 0000000..cf8daa3 --- /dev/null +++ b/zeppelin-interpreter/src/test/java/org/apache/zeppelin/interpreter/ZeppCtxtVariableTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF 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 org.apache.zeppelin.interpreter; + +import org.apache.zeppelin.resource.LocalResourcePool; +import org.apache.zeppelin.resource.ResourcePool; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.Before; +import org.junit.After; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertTrue; + +public class ZeppCtxtVariableTest { + + public static class TestInterpreter extends Interpreter { + + TestInterpreter(Properties property) { + super(property); + } + + @Override + public void open() { + } + + @Override + public void close() { + } + + @Override + public InterpreterResult interpret(String st, InterpreterContext context) { + return null; + } + + @Override + public void cancel(InterpreterContext context) { + } + + @Override + public FormType getFormType() { + return null; + } + + @Override + public int getProgress(InterpreterContext context) { + return 0; + } + } + + private Interpreter interpreter; + private ResourcePool resourcePool; + + @Before + public void setUp() throws Exception { + + resourcePool = new LocalResourcePool("ZeppelinContextVariableInterpolationTest"); + + InterpreterContext.set(new InterpreterContext("InterpolationTestNoteId", + "InterpolationTestParagraphTitle", + null, + "InterpolationTestParagraphTitle", + "InterpolationTestParagraphText", + new AuthenticationInfo("InterpolationTestUser", null, "testTicket"), + null, + null, + null, + null, + resourcePool, + null, + null)); + + interpreter = new TestInterpreter(new Properties()); + + resourcePool.put("PI", "3.1415"); + + } + + @After + public void tearDown() throws Exception { + InterpreterContext.remove(); + } + + @Test + public void stringWithoutPatterns() { + String result = interpreter.interpolate("The value of PI is not exactly 3.14", resourcePool); + assertTrue("String without patterns", "The value of PI is not exactly 3.14".equals(result)); + } + + @Test + public void substitutionInTheMiddle() { + String result = interpreter.interpolate("The value of {{PI}} is {PI} now", resourcePool); + assertTrue("Substitution in the middle", "The value of {PI} is 3.1415 now".equals(result)); + } + + @Test + public void substitutionAtTheEnds() { + String result = interpreter.interpolate("{{PI}} is now {PI}", resourcePool); + assertTrue("Substitution at the ends", "{PI} is now 3.1415".equals(result)); + } + + @Test + public void multiLineSubstitutionSuccessful1() { + String result = interpreter.interpolate("{{PI}}\n{PI}\n{{PI}}\n{PI}", resourcePool); + assertTrue("multiLineSubstitutionSuccessful1", "{PI}\n3.1415\n{PI}\n3.1415".equals(result)); + } + + + @Test + public void multiLineSubstitutionSuccessful2() { + String result = interpreter.interpolate("prefix {PI} {{PI\n}} suffix", resourcePool); + assertTrue("multiLineSubstitutionSuccessful2", "prefix 3.1415 {PI\n} suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionSuccessful3() { + String result = interpreter.interpolate("prefix {{\nPI}} {PI} suffix", resourcePool); + assertTrue("multiLineSubstitutionSuccessful3", "prefix {\nPI} 3.1415 suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionFailure2() { + String result = interpreter.interpolate("prefix {PI\n} suffix", resourcePool); + assertTrue("multiLineSubstitutionFailure2", "prefix {PI\n} suffix".equals(result)); + } + + + @Test + public void multiLineSubstitutionFailure3() { + String result = interpreter.interpolate("prefix {\nPI} suffix", resourcePool); + assertTrue("multiLineSubstitutionFailure3", "prefix {\nPI} suffix".equals(result)); + } + + @Test + public void noUndefinedVariableError() { + String result = interpreter.interpolate("This {pi} will pass silently", resourcePool); + assertTrue("No partial substitution", "This {pi} will pass silently".equals(result)); + } + + @Test + public void noPartialSubstitution() { + String result = interpreter.interpolate("A {PI} and a {PIE} are different", resourcePool); + assertTrue("No partial substitution", "A {PI} and a {PIE} are different".equals(result)); + } + + @Test + public void substitutionAndEscapeMixed() { + String result = interpreter.interpolate("A {PI} is not a {{PIE}}", resourcePool); + assertTrue("Substitution and escape mixed", "A 3.1415 is not a {PIE}".equals(result)); + } + + @Test + public void unbalancedBracesOne() { + String result = interpreter.interpolate("A {PI} and a {{PIE} remain unchanged", resourcePool); + assertTrue("Unbalanced braces - one", "A {PI} and a {{PIE} remain unchanged".equals(result)); + } + + @Test + public void unbalancedBracesTwo() { + String result = interpreter.interpolate("A {PI} and a {PIE}} remain unchanged", resourcePool); + assertTrue("Unbalanced braces - one", "A {PI} and a {PIE}} remain unchanged".equals(result)); + } + + @Test + public void tooManyBraces() { + String result = interpreter.interpolate("This {{{PI}}} remain unchanged", resourcePool); + assertTrue("Too many braces", "This {{{PI}}} remain unchanged".equals(result)); + } + + @Test + public void randomBracesOne() { + String result = interpreter.interpolate("A {{ starts an escaped sequence", resourcePool); + assertTrue("Random braces - one", "A {{ starts an escaped sequence".equals(result)); + } + + @Test + public void randomBracesTwo() { + String result = interpreter.interpolate("A }} ends an escaped sequence", resourcePool); + assertTrue("Random braces - two", "A }} ends an escaped sequence".equals(result)); + } + + @Test + public void randomBracesThree() { + String result = interpreter.interpolate("Paired { begin an escaped sequence", resourcePool); + assertTrue("Random braces - three", "Paired { begin an escaped sequence".equals(result)); + } + + @Test + public void randomBracesFour() { + String result = interpreter.interpolate("Paired } end an escaped sequence", resourcePool); + assertTrue("Random braces - four", "Paired } end an escaped sequence".equals(result)); + } + +}