zeppelin-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From zjf...@apache.org
Subject zeppelin git commit: [ZEPPELIN-1967] Passing Z variables to Shell and SQL Interpreters
Date Wed, 28 Mar 2018 01:20:11 GMT
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.dasgupta@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.dasgupta@gmail.com>
Authored: Mon Mar 26 13:31:09 2018 +0530
Committer: Jeff Zhang <zjffdu@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));
+  }
+
+}


Mime
View raw message