This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 4146521  RestClient tests.
4146521 is described below

commit 41465213e58b7a02f2a36cc670654a599a827dc3
Author: JamesBognar <[email protected]>
AuthorDate: Tue Jun 16 16:52:37 2020 -0400

    RestClient tests.
---
 .../org/apache/juneau/utils/StringUtilsTest.java   |  31 ++++
 .../juneau/assertions/FluentStringAssertion.java   |  73 +++++++--
 .../org/apache/juneau/internal/StringUtils.java    |  48 ++++++
 .../apache/juneau/rest/client2/RestClientTest.java | 158 ++++++++++++++++++--
 .../org/apache/juneau/rest/client2/RestClient.java |  34 ++++-
 .../juneau/rest/client2/RestClientBuilder.java     |  39 +++++
 .../apache/juneau/rest/client2/RestResponse.java   |   4 +-
 .../org/apache/juneau/rest/mock2/MockConsole.java  | 164 +++++++++++++++++++++
 .../org/apache/juneau/rest/mock2/MockLogger.java   | 147 ++++++++++++------
 9 files changed, 629 insertions(+), 69 deletions(-)

diff --git 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
 
b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
index 7c1cf3a..db0980a 100755
--- 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
+++ 
b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
@@ -1034,4 +1034,35 @@ public class StringUtilsTest {
                assertEquals("foo%7Bbar%7Dbaz", fixUrl("foo{bar}baz"));
                assertEquals("%7Dfoo%7Bbar%7Dbaz%7B", fixUrl("}foo{bar}baz{"));
        }
+
+       
//====================================================================================================
+       // diffPosition(String,String)
+       
//====================================================================================================
+       @Test
+       public void testDiffPosition() throws Exception {
+               assertEquals(-1, diffPosition("a", "a"));
+               assertEquals(-1, diffPosition(null, null));
+               assertEquals(0, diffPosition("a", "b"));
+               assertEquals(1, diffPosition("aa", "ab"));
+               assertEquals(1, diffPosition("aaa", "ab"));
+               assertEquals(1, diffPosition("aa", "abb"));
+               assertEquals(0, diffPosition("a", null));
+               assertEquals(0, diffPosition(null, "b"));
+       }
+
+       
//====================================================================================================
+       // diffPositionIc(String,String)
+       
//====================================================================================================
+       @Test
+       public void testDiffPositionIc() throws Exception {
+               assertEquals(-1, diffPositionIc("a", "a"));
+               assertEquals(-1, diffPositionIc("a", "A"));
+               assertEquals(-1, diffPositionIc(null, null));
+               assertEquals(0, diffPositionIc("a", "b"));
+               assertEquals(1, diffPositionIc("aa", "Ab"));
+               assertEquals(1, diffPositionIc("aaa", "Ab"));
+               assertEquals(1, diffPositionIc("aa", "Abb"));
+               assertEquals(0, diffPositionIc("a", null));
+               assertEquals(0, diffPositionIc(null, "b"));
+       }
 }
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
index 2b13f4f..d359bd5 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
@@ -28,7 +28,7 @@ import org.apache.juneau.internal.*;
  *     client
  *             .get(<jsf>URL</jsf>)
  *             .run()
- *             .assertBody().equals(<js>"OK"</js>);
+ *             .assertBody().is(<js>"OK"</js>);
  * </p>
  * @param <R> The return type.
  */
@@ -67,13 +67,40 @@ public class FluentStringAssertion<R> extends 
FluentAssertion<R> {
        /**
         * Asserts that the text equals the specified value.
         *
-        * @param value The value to check against.
+        * <h5 class='section'>Example:</h5>
+        * <p class='bcode w800'>
+        *      <jc>// Validates the response body of an HTTP call is the text 
"OK".</jc>
+        *      client
+        *              .get(<jsf>URL</jsf>)
+        *              .run()
+        *              .assertBody().equals(<js>"OK"</js>);
+        * </p>
+        *
+        * <p>
+        * Multiple values can be passed in to represent multiple lines of 
output like so:
+        *
+        * <p class='bcode w800'>
+        *      <jc>// Validates the response body of an HTTP call is the text 
"OK".</jc>
+        *      client
+        *              .get(<jsf>URL</jsf>)
+        *              .run()
+        *              .assertBody().equals(
+        *                      <js>"Line 1"</js>,
+        *                      <js>"Line 2"</js>,
+        *                      <js>"Line 3"</js>
+        *              );
+        * </p>
+        *
+        * @param value
+        *      The value to check against.
+        *      <br>If multiple values are specified, they are concatenated 
with newlines.
         * @return The response object (for method chaining).
         * @throws AssertionError If assertion failed.
         */
-       public R equals(String value) throws AssertionError {
-               if (! isEquals(value, text))
-                       throw error("Text did not equal 
expected.\n\tExpected=[{0}]\n\tActual=[{1}]", fix(value), fix(text));
+       public R equals(String...value) throws AssertionError {
+               String v = join(value, '\n');
+               if (! isEquals(v, text))
+                       throw error("Text differed at position 
{0}.\n\tExpected=[{1}]\n\tActual=[{2}]", diffPosition(v, text), fix(v), 
fix(text));
                return returns();
        }
 
@@ -81,13 +108,39 @@ public class FluentStringAssertion<R> extends 
FluentAssertion<R> {
         * Asserts that the text equals the specified value.
         *
         * <p>
-        * Equivalent to {@link #equals(String)}.
+        * Equivalent to {@link #equals(String...)}.
         *
-        * @param value The value to check against.
+        * <h5 class='section'>Example:</h5>
+        * <p class='bcode w800'>
+        *      <jc>// Validates the response body of an HTTP call is the text 
"OK".</jc>
+        *      client
+        *              .get(<jsf>URL</jsf>)
+        *              .run()
+        *              .assertBody().is(<js>"OK"</js>);
+        * </p>
+        *
+        * <p>
+        * Multiple values can be passed in to represent multiple lines of 
output like so:
+        *
+        * <p class='bcode w800'>
+        *      <jc>// Validates the response body of an HTTP call is the text 
"OK".</jc>
+        *      client
+        *              .get(<jsf>URL</jsf>)
+        *              .run()
+        *              .assertBody().is(
+        *                      <js>"Line 1"</js>,
+        *                      <js>"Line 2"</js>,
+        *                      <js>"Line 3"</js>
+        *              );
+        * </p>
+        *
+        * @param value
+        *      The value to check against.
+        *      <br>If multiple values are specified, they are concatenated 
with newlines.
         * @return The response object (for method chaining).
         * @throws AssertionError If assertion failed.
         */
-       public R is(String value) throws AssertionError {
+       public R is(String...value) throws AssertionError {
                return equals(value);
        }
 
@@ -101,7 +154,7 @@ public class FluentStringAssertion<R> extends 
FluentAssertion<R> {
        public R urlDecodedIs(String value) throws AssertionError {
                String t = urlDecode(text);
                if (! isEqualsIc(value, t))
-                       throw error("Text did not equal 
expected.\n\tExpected=[{0}]\n\tActual=[{1}]", fix(value), fix(t));
+                       throw error("Text differed at position 
{0}.\n\tExpected=[{1}]\n\tActual=[{2}]", diffPosition(value, text), fix(value), 
fix(t));
                return returns();
        }
 
@@ -114,7 +167,7 @@ public class FluentStringAssertion<R> extends 
FluentAssertion<R> {
         */
        public R equalsIc(String value) throws AssertionError {
                if (! isEqualsIc(value, text))
-                       throw error("Text did not equal 
expected.\n\tExpected=[{0}]\n\tActual=[{1}]", fix(value), fix(text));
+                       throw error("Text differed at position 
{0}.\n\tExpected=[{1}]\n\tActual=[{2}]", diffPositionIc(value, text), 
fix(value), fix(text));
                return returns();
        }
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
index b852403..4699807 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
@@ -434,6 +434,8 @@ public final class StringUtils {
        public static String join(Object[] tokens, char d) {
                if (tokens == null)
                        return null;
+               if (tokens.length == 1)
+                       return emptyIfNull(stringify(tokens[0]));
                return join(tokens, d, new StringBuilder()).toString();
        }
 
@@ -1161,6 +1163,52 @@ public final class StringUtils {
        }
 
        /**
+        * Finds the position where the two strings differ.
+        * 
+        * @param s1 The first string.
+        * @param s2 The second string.
+        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
+        */
+       public static int diffPosition(String s1, String s2) {
+               s1 = emptyIfNull(s1);
+               s2 = emptyIfNull(s2);
+               int i = 0;
+               int len = Math.min(s1.length(), s2.length());
+               while (i < len) {
+                       int j = s1.charAt(i) - s2.charAt(i);
+                       if (j != 0)
+                               return i;
+                       i++;
+               }
+               if (i == len && s1.length() == s2.length())
+                       return -1;
+               return i;
+       }
+
+       /**
+        * Finds the position where the two strings differ ignoring case.
+        * 
+        * @param s1 The first string.
+        * @param s2 The second string.
+        * @return The position where the two strings differ, or <c>-1</c> if 
they're equal.
+        */
+       public static int diffPositionIc(String s1, String s2) {
+               s1 = emptyIfNull(s1);
+               s2 = emptyIfNull(s2);
+               int i = 0;
+               int len = Math.min(s1.length(), s2.length());
+               while (i < len) {
+                       int j = Character.toLowerCase(s1.charAt(i)) - 
Character.toLowerCase(s2.charAt(i));
+                       if (j != 0)
+                               return i;
+                       i++;
+               }
+               if (i == len && s1.length() == s2.length())
+                       return -1;
+               return i;
+       }
+
+       /**
         * Tests two strings for case-insensitive equality, but gracefully 
handles nulls.
         *
         * @param s1 String 1.
diff --git 
a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientTest.java
 
b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientTest.java
index 4781abb..b80713c 100644
--- 
a/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientTest.java
+++ 
b/juneau-rest/juneau-rest-client-utest/src/test/java/org/apache/juneau/rest/client2/RestClientTest.java
@@ -842,38 +842,128 @@ public class RestClientTest {
        
//------------------------------------------------------------------------------------------------------------------
        // Logging
        
//------------------------------------------------------------------------------------------------------------------
+
        @Test
        public void b01_logToConsole() throws Exception {
+               MockConsole c = MockConsole.create();
+               MockLogger l = MockLogger.create();
+
+               MockRestClient
+                       .create(A.class)
+                       .simpleJson()
+                       .logRequests(DetailLevel.NONE, Level.SEVERE)
+                       .logToConsole()
+                       .logger(l)
+                       .console(c)
+                       .build()
+                       .post("/bean", bean)
+                       .complete();
+
+               c.assertContents().is("");
+
+               c.reset();
+
                MockRestClient
                        .create(A.class)
                        .simpleJson()
                        .logRequests(DetailLevel.SIMPLE, Level.SEVERE)
                        .logToConsole()
+                       .logger(l)
+                       .console(c)
+                       .build()
+                       .post("/bean", bean)
+                       .complete();
+
+               c.assertContents().is("HTTP POST http://localhost/bean, 
HTTP/1.1 200 \n");
+
+               c.reset();
+
+               MockRestClient
+                       .create(A.class)
+                       .simpleJson()
+                       .logRequests(DetailLevel.FULL, Level.SEVERE)
+                       .logToConsole()
+                       .logger(l)
+                       .console(c)
                        .build()
                        .post("/bean", bean)
                        .complete();
+
+               c.assertContents().is(
+                       "",
+                       "=== HTTP Call (outgoing) 
======================================================",
+                       "=== REQUEST ===",
+                       "POST http://localhost/bean";,
+                       "---request headers---",
+                       "       Accept: application/json+simple",
+                       "---request entity---",
+                       "       Content-Type: application/json+simple",
+                       "---request content---",
+                       "{f:1}",
+                       "=== RESPONSE ===",
+                       "HTTP/1.1 200 ",
+                       "---response headers---",
+                       "       Content-Type: application/json",
+                       "---response content---",
+                       "{f:1}",
+                       "=== END 
=======================================================================",
+                       ""
+               );
        }
 
        @Test
        public void b02_logTo() throws Exception {
-               MockLogger ml = new MockLogger();
+               MockLogger l = MockLogger.create();
+
                MockRestClient
                        .create(A.class)
                        .simpleJson()
-                       .logger(ml)
-                       .logRequests(DetailLevel.FULL, Level.SEVERE)
+                       .logRequests(DetailLevel.NONE, Level.SEVERE)
+                       .logToConsole()
+                       .logger(l)
                        .build()
                        .post("/bean", bean)
                        .complete();
-               ml.assertLevel(Level.SEVERE);
-               ml.assertMessageContains(
+
+               l.assertContents().is("");
+               l.assertRecordCount().is(0);
+
+               l.reset();
+
+               MockRestClient
+                       .create(A.class)
+                       .simpleJson()
+                       .logger(l)
+                       .logRequests(DetailLevel.SIMPLE, Level.WARNING)
+                       .build()
+                       .post("/bean", bean)
+                       .complete();
+
+               l.assertLastLevel(Level.WARNING);
+               l.assertLastMessage().stderr().is("HTTP POST 
http://localhost/bean, HTTP/1.1 200 ");
+               l.assertContents().is("WARNING: HTTP POST 
http://localhost/bean, HTTP/1.1 200 \n");
+
+               l.reset();
+
+               MockRestClient
+                       .create(A.class)
+                       .simpleJson()
+                       .logger(l)
+                       .logRequests(DetailLevel.FULL, Level.WARNING)
+                       .build()
+                       .post("/bean", bean)
+                       .complete();
+
+               l.assertLastLevel(Level.WARNING);
+               l.assertLastMessage().is(
+                       "",
                        "=== HTTP Call (outgoing) 
======================================================",
                        "=== REQUEST ===",
                        "POST http://localhost/bean";,
                        "---request headers---",
                        "       Accept: application/json+simple",
                        "---request entity---",
-                       "application/json+simple",
+                       "       Content-Type: application/json+simple",
                        "---request content---",
                        "{f:1}",
                        "=== RESPONSE ===",
@@ -884,6 +974,26 @@ public class RestClientTest {
                        "{f:1}",
                        "=== END 
======================================================================="
                );
+               l.assertContents().stderr().javaStrings().is(
+                       "WARNING: ",
+                       "=== HTTP Call (outgoing) 
======================================================",
+                       "=== REQUEST ===",
+                       "POST http://localhost/bean";,
+                       "---request headers---",
+                       "       Accept: application/json+simple",
+                       "---request entity---",
+                       "       Content-Type: application/json+simple",
+                       "---request content---",
+                       "{f:1}",
+                       "=== RESPONSE ===",
+                       "HTTP/1.1 200 ",
+                       "---response headers---",
+                       "       Content-Type: application/json",
+                       "---response content---",
+                       "{f:1}",
+                       "=== END 
=======================================================================",
+                       ""
+               );
        }
 
        public static class B02a extends BasicRestCallInterceptor {
@@ -891,26 +1001,52 @@ public class RestClientTest {
                public void onConnect(RestRequest req, RestResponse res) throws 
Exception {
                        super.onConnect(req, res);
                        req.log(Level.WARNING, "Foo");
-                       req.log(Level.WARNING, new RuntimeException(), "Foo");
-                       res.log(Level.WARNING, "Foo");
-                       res.log(Level.WARNING, new RuntimeException(), "Foo");
+                       req.log(Level.WARNING, new RuntimeException(), "Bar");
+                       res.log(Level.WARNING, "Baz");
+                       res.log(Level.WARNING, new RuntimeException(), "Qux");
+                       req.log(Level.WARNING, (Throwable)null, "Quux");
                }
        }
 
        @Test
        public void b02a_loggingOther() throws Exception {
-               MockLogger ml = new MockLogger();
+               MockLogger ml = MockLogger.create();
+               MockConsole mc = MockConsole.create();
+
+               MockRestClient
+                       .create(A.class)
+                       .simpleJson()
+                       .logger(ml)
+                       .interceptors(B02a.class)
+                       .build()
+                       .post("/bean", bean)
+                       .complete();
+
+               ml.assertRecordCount().is(5);
+
+               ml.reset();
 
                MockRestClient
                        .create(A.class)
                        .simpleJson()
                        .logger(ml)
+                       .logToConsole()
+                       .console(mc)
                        .interceptors(B02a.class)
                        .build()
                        .post("/bean", bean)
                        .complete();
 
-               ml.assertCount(4);
+               ml.assertRecordCount().is(5);
+
+               ml.assertContents().contains(
+                       "WARNING: Foo",
+                       "WARNING: Bar",
+                       "WARNING: Baz",
+                       "WARNING: Qux",
+                       "WARNING: Quux",
+                       "at org.apache.juneau"
+               );
        }
 
        public static class B03 extends RestClient {
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java
index 2b2d7ae..698297c 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClient.java
@@ -1004,6 +1004,32 @@ public class RestClient extends BeanContext implements 
HttpClient, Closeable, Re
        public static final String RESTCLIENT_callHandler = PREFIX + 
"callHandler.o";
 
        /**
+        * Configuration property:  Console print stream
+        *
+        * <h5 class='section'>Property:</h5>
+        * <ul class='spaced-list'>
+        *      <li><b>ID:</b>  {@link 
org.apache.juneau.rest.client2.RestClient#RESTCLIENT_console RESTCLIENT_console}
+        *      <li><b>Name:</b>  <js>"RestClient.console.o"</js>
+        *      <li><b>System property:</b>  <c>RestClient.console</c>
+        *      <li><b>Data type:</b>
+        *      <ul>
+        *              <li><b>Data type:</b>  <c>Class&lt;? <jk>extends</jk> 
{@link java.io.PrintStream}&gt; | {@link java.io.PrintStream}</c>
+        *      </ul>
+        *      <li><b>Default:</b>  <c>System.<jsf>out</jsf></c>
+        *      <li><b>Methods:</b>
+        *              <ul>
+        *                      <li class='jm'>{@link 
org.apache.juneau.rest.client2.RestClientBuilder#console(PrintStream)}
+        *                      <li class='jm'>{@link 
org.apache.juneau.rest.client2.RestClientBuilder#console(Class)}
+        *              </ul>
+        * </ul>
+        *
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * Allows you to redirect the console output to a different print 
stream.
+        */
+       public static final String RESTCLIENT_console = PREFIX + "console.o";
+
+       /**
         * Configuration property:  Error codes predicate.
         *
         * <h5 class='section'>Property:</h5>
@@ -1860,6 +1886,7 @@ public class RestClient extends BeanContext implements 
HttpClient, Closeable, Re
        final Level logRequestsLevel;
        final boolean ignoreErrors;
        private final boolean logToConsole;
+       private final PrintStream console;
        private StackTraceElement[] closedStack;
        private static final ConcurrentHashMap<Class<?>,Context> 
requestContexts = new ConcurrentHashMap<>();
 
@@ -1913,6 +1940,7 @@ public class RestClient extends BeanContext implements 
HttpClient, Closeable, Re
                this.logRequests = getInstanceProperty(RESTCLIENT_logRequests, 
DetailLevel.class, isDebug() ? DetailLevel.FULL : DetailLevel.NONE);
                this.logRequestsLevel = 
getInstanceProperty(RESTCLIENT_logRequestsLevel, Level.class, isDebug() ? 
Level.WARNING : Level.OFF);
                this.logToConsole = getBooleanProperty(RESTCLIENT_logToConsole, 
isDebug());
+               this.console = getInstanceProperty(RESTCLIENT_console, 
PrintStream.class, System.err);
                this.logRequestsPredicate = 
getInstanceProperty(RESTCLIENT_logRequestsPredicate, BiPredicate.class, 
LOG_REQUESTS_PREDICATE_DEFAULT);
 
                SerializerGroupBuilder sgb = SerializerGroup.create();
@@ -3237,9 +3265,9 @@ public class RestClient extends BeanContext implements 
HttpClient, Closeable, Re
        protected void log(Level level, Throwable t, String msg, Object...args) 
{
                logger.log(level, t, msg(msg, args));
                if (logToConsole) {
-                       System.err.println(msg(msg, args).get());
+                       console.println(msg(msg, args).get());
                        if (t != null)
-                               t.printStackTrace();
+                               t.printStackTrace(console);
                }
        }
 
@@ -3253,7 +3281,7 @@ public class RestClient extends BeanContext implements 
HttpClient, Closeable, Re
        protected void log(Level level, String msg, Object...args) {
                logger.log(level, msg(msg, args));
                if (logToConsole)
-                       System.err.println(msg(msg, args).get());
+                       console.println(msg(msg, args).get());
        }
 
        private Supplier<String> msg(String msg, Object...args) {
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClientBuilder.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClientBuilder.java
index dddc00b..19717da 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClientBuilder.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestClientBuilder.java
@@ -22,6 +22,7 @@ import static org.apache.juneau.serializer.WriterSerializer.*;
 import static org.apache.juneau.oapi.OpenApiCommon.*;
 import static org.apache.juneau.uon.UonSerializer.*;
 
+import java.io.*;
 import java.lang.annotation.*;
 import java.lang.reflect.*;
 import java.net.*;
@@ -2536,6 +2537,44 @@ public class RestClientBuilder extends 
BeanContextBuilder {
        }
 
        /**
+        * <i><l>RestClient</l> configuration property:  Console print stream
+        *
+        * <p>
+        * Allows you to redirect the console output to a different print 
stream.
+        *
+        * <ul class='seealso'>
+        *      <li class='jf'>{@link RestClient#RESTCLIENT_console}
+        * </ul>
+        *
+        * @param value
+        *      The new value for this setting.
+        * @return This object (for method chaining).
+        */
+       @FluentSetter
+       public RestClientBuilder console(Class<? extends PrintStream> value) {
+               return set(RESTCLIENT_console, value);
+       }
+
+       /**
+        * <i><l>RestClient</l> configuration property:  Console print stream
+        *
+        * <p>
+        * Allows you to redirect the console output to a different print 
stream.
+        *
+        * <ul class='seealso'>
+        *      <li class='jf'>{@link RestClient#RESTCLIENT_console}
+        * </ul>
+        *
+        * @param value
+        *      The new value for this setting.
+        * @return This object (for method chaining).
+        */
+       @FluentSetter
+       public RestClientBuilder console(PrintStream value) {
+               return set(RESTCLIENT_console, value);
+       }
+
+       /**
         * <i><l>RestClient</l> configuration property:</i>  Errors codes 
predicate.
         *
         * <p>
diff --git 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
index fcab39b..a027d76 100644
--- 
a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
+++ 
b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client2/RestResponse.java
@@ -972,9 +972,9 @@ public class RestResponse implements HttpResponse {
                                                sb.append("\nEntity is null");
                                        else {
                                                if (e.getContentType() != null)
-                                                       
sb.append("\n").append(e.getContentType());
+                                                       
sb.append("\n\t").append(e.getContentType());
                                                if (e.getContentEncoding() != 
null)
-                                                       
sb.append("\n").append(e.getContentEncoding());
+                                                       
sb.append("\n\t").append(e.getContentEncoding());
                                                if (e.isRepeatable()) {
                                                        try {
                                                                
sb.append("\n---request content---\n").append(EntityUtils.toString(e));
diff --git 
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockConsole.java
 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockConsole.java
new file mode 100644
index 0000000..3a2bfdb
--- /dev/null
+++ 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockConsole.java
@@ -0,0 +1,164 @@
+// 
***************************************************************************************************************************
+// * 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.juneau.rest.mock2;
+
+import java.io.*;
+import org.apache.juneau.assertions.*;
+import org.apache.juneau.rest.client2.*;
+
+/**
+ * A capturing {@link PrintStream} that allows you to easily capture console 
output.
+ *
+ * <p>
+ * Stores output into an internal {@link ByteArrayOutputStream}.  Note that 
this means you could run into memory
+ * constraints if you heavily use this class.
+ *
+ * <p>
+ * Typically used in conjunction with the {@link 
RestClientBuilder#console(PrintStream)} to capture console output for
+ * testing purposes.
+ *
+ * <h5 class='figure'>Example:</h5>
+ * <p class='bcode w800'>
+ *     <jc>// A simple REST API that echos a posted bean.</jc>
+ *     <ja>@Rest</ja>
+ *     <jk>public class</jk> MyRest <jk>extends</jk> BasicRest {
+ *             <ja>@RestMethod</ja>(path=<js>"/bean"</js>)
+ *             <jk>public</jk> Bean postBean(<ja>@Body</ja> Bean b) {
+ *                     <jk>return</jk> b;
+ *             }
+ *     }
+ *
+ *     <jc>// Our mock console.</jc>
+ *     MockConsole console = MockConsole.<jsm>create</jsm>();
+ *
+ *     <jc>// Make a call against our REST API and log the call.</jc>
+ *     MockRestClient
+ *             .<jsm>create</jsm>(MyRest.<jk>class</jk>)
+ *             .simpleJson()
+ *             .logRequests(DetailLevel.<jsf>FULL</jsf>, 
Level.<jsf>SEVERE</jsf>)
+ *             .logToConsole()
+ *             .console(console)
+ *             .build()
+ *             .post(<js>"/bean"</js>, bean)
+ *             .run();
+ *
+ *     console.assertContents().is(
+ *             <js>""</js>,
+ *             <js>"=== HTTP Call (outgoing) 
======================================================"</js>,
+ *             <js>"=== REQUEST ==="</js>,
+ *             <js>"POST http://localhost/bean";</js>,
+ *             <js>"---request headers---"</js>,
+ *             <js>"   Accept: application/json+simple"</js>,
+ *             <js>"---request entity---"</js>,
+ *             <js>"   Content-Type: application/json+simple"</js>,
+ *             <js>"---request content---"</js>,
+ *             <js>"{f:1}"</js>,
+ *             <js>"=== RESPONSE ==="</js>,
+ *             <js>"HTTP/1.1 200 "</js>,
+ *             <js>"---response headers---"</js>,
+ *             <js>"   Content-Type: application/json"</js>,
+ *             <js>"---response content---"</js>,
+ *             <js>"{f:1}"</js>,
+ *             <js>"=== END 
======================================================================="</js>,
+ *             <js>""</js>
+ *     );
+ * </p>
+ */
+public class MockConsole extends PrintStream {
+
+       private static final ByteArrayOutputStream baos = new 
ByteArrayOutputStream();
+
+       /**
+        * Constructor.
+        */
+       public MockConsole() {
+               super(baos);
+       }
+
+       /**
+        * Creator.
+        *
+        * @return A new {@link MockConsole} object.
+        */
+       public static MockConsole create() {
+               return new MockConsole();
+       }
+
+       /**
+        * Resets the contents of this buffer.
+        *
+        * @return This object (for method chaining).
+        */
+       public synchronized MockConsole reset() {
+               baos.reset();
+               return this;
+       }
+
+       /**
+        * Allows you to perform fluent-style assertions on the contents of 
this buffer.
+        *
+        * <h5 class='figure'>Example:</h5>
+        * <p class='bcode w800'>
+        *      MockConsole console = MockConsole.<jsf>create</jsf>();
+        *
+        *      MockRestClient
+        *              .<jsm>create</jsm>(MyRest.<jk>class</jk>)
+        *              .console(console)
+        *              .debug()
+        *              .simpleJson()
+        *              .build()
+        *              .get(<js>"/url"</js>)
+        *              .run();
+        *
+        *      console.assertContents().contains(<js>"HTTP GET /url"</js>);
+        * </p>
+        *
+        * @return A new fluent-style assertion object.
+        */
+       public synchronized FluentStringAssertion<MockConsole> assertContents() 
{
+               return new FluentStringAssertion<>(toString(), this);
+       }
+
+       /**
+        * Allows you to perform fluent-style assertions on the size of this 
buffer.
+        *
+        * <h5 class='figure'>Example:</h5>
+        * <p class='bcode w800'>
+        *      MockConsole console = MockConsole.<jsf>create</jsf>();
+        *
+        *      MockRestClient
+        *              .<jsm>create</jsm>(MyRest.<jk>class</jk>)
+        *              .console(console)
+        *              .debug()
+        *              .simpleJson()
+        *              .build()
+        *              .get(<js>"/url"</js>)
+        *              .run();
+        *
+        *      console.assertSize().isGreaterThan(0);
+        * </p>
+        *
+        * @return A new fluent-style assertion object.
+        */
+       public synchronized FluentIntegerAssertion<MockConsole> assertSize() {
+               return new FluentIntegerAssertion<>(baos.size(), this);
+       }
+
+       /**
+        * Returns the contents of this buffer as a string.
+        */
+       @Override
+       public String toString() {
+               return baos.toString();
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockLogger.java
 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockLogger.java
index ecf6868..a27539c 100644
--- 
a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockLogger.java
+++ 
b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock2/MockLogger.java
@@ -12,8 +12,12 @@
 // 
***************************************************************************************************************************
 package org.apache.juneau.rest.mock2;
 
+import java.io.*;
 import java.util.*;
 import java.util.logging.*;
+import java.util.logging.Formatter;
+
+import org.apache.juneau.assertions.*;
 
 /**
  * Simplified logger for intercepting and asserting logging messages.
@@ -21,28 +25,28 @@ import java.util.logging.*;
  * <h5 class='figure'>Example:</h5>
  * <p class='bcode w800'>
  *     <jc>// Instantiate a mock logger.</jc>
- *     MockLogger ml = <jk>new</jk> MockLogger();
+ *     MockLogger logger = <jk>new</jk> MockLogger();
  *
  *     <jc>// Associate it with a MockRestClient.</jc>
  *     MockRestClient
  *             .<jsm>create</jsm>(MyRestResource.<jk>class</jk>)
  *             .simpleJson()
- *             .logger(ml)
+ *             .logger(logger)
  *             .logRequests(DetailLevel.<jsf>FULL</jsf>, 
Level.<jsf>SEVERE</jsf>)
  *             .build()
  *             .post(<js>"/bean"</js>, bean)
  *             .complete();
  *
  *     <jc>// Assert that logging occurred.</jc>
- *     ml.assertLevel(Level.<jsf>SEVERE</jsf>);
- *     ml.assertMessageContains(
+ *     logger.assertLastLevel(Level.<jsf>SEVERE</jsf>);
+ *     logger.assertLastMessage().is(
  *             <js>"=== HTTP Call (outgoing) 
======================================================"</js>,
  *             <js>"=== REQUEST ==="</js>,
  *             <js>"POST http://localhost/bean";</js>,
  *             <js>"---request headers---"</js>,
  *             <js>"   Accept: application/json+simple"</js>,
  *             <js>"---request entity---"</js>,
- *             <js>"application/json+simple"</js>,
+ *             <js>"   Content-Type: application/json+simple"</js>,
  *             <js>"---request content---"</js>,
  *             <js>"{f:1}"</js>,
  *             <js>"=== RESPONSE ==="</js>,
@@ -51,13 +55,19 @@ import java.util.logging.*;
  *             <js>"   Content-Type: application/json"</js>,
  *             <js>"---response content---"</js>,
  *             <js>"{f:1}"</js>,
- *             <js>"=== END 
======================================================================="</js>
+ *             <js>"=== END 
======================================================================="</js>,
+ *             <js>""</js>
  *     );
  * </p>
  */
 public class MockLogger extends Logger {
 
-       private volatile List<LogRecord> logRecords = new ArrayList<>();
+       private static final String FORMAT_PROPERTY = 
"java.util.logging.SimpleFormatter.format";
+
+       private final List<LogRecord> logRecords = new ArrayList<>();
+       private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+       private volatile Formatter formatter;
+       private volatile String format = "%4$s: %5$s%6$s%n";
 
        /**
         * Constructor.
@@ -66,9 +76,77 @@ public class MockLogger extends Logger {
                super("Mock", null);
        }
 
+       /**
+        * Creator.
+        *
+        * @return A new {@link MockLogger} object.
+        */
+       public static MockLogger create() {
+               return new MockLogger();
+       }
+
        @Override /* Logger */
        public synchronized void log(LogRecord record) {
                logRecords.add(record);
+               try {
+                       
baos.write(getFormatter().format(record).getBytes("UTF-8"));
+               } catch (Exception e) {
+                       throw new RuntimeException(e);
+               }
+       }
+
+       private Formatter getFormatter() {
+               if (formatter == null) {
+                       synchronized(this) {
+                               String oldFormat = 
System.getProperty(FORMAT_PROPERTY);
+                               System.setProperty(FORMAT_PROPERTY, format);
+                               formatter = new SimpleFormatter();
+                               if (oldFormat == null)
+                                       System.clearProperty(FORMAT_PROPERTY);
+                               else
+                                       System.setProperty(FORMAT_PROPERTY, 
oldFormat);
+                       }
+               }
+               return formatter;
+       }
+
+       /**
+        * Sets the level for this logger.
+        *
+        * @param level The new level for this logger.
+        * @return This object (for method chaining).
+        */
+       public synchronized MockLogger level(Level level) {
+               super.setLevel(level);
+               return this;
+       }
+
+       /**
+        * Specifies the format for messages sent to the log file.
+        *
+        * <p>
+        * See {@link SimpleFormatter#format(LogRecord)} for the syntax of this 
string.
+        *
+        * @param format The format string.
+        * @return This object (for method chaining).
+        */
+       public synchronized MockLogger format(String format) {
+               this.format = format;
+               return this;
+       }
+
+       /**
+        * Overrides the formatter to use for formatting messages.
+        *
+        * <p>
+        * The default uses {@link SimpleFormatter}.
+        *
+        * @param formatter The log record formatter.
+        * @return This object (for method chaining).
+        */
+       public synchronized MockLogger formatter(Formatter formatter) {
+               this.formatter = formatter;
+               return this;
        }
 
        /**
@@ -78,6 +156,7 @@ public class MockLogger extends Logger {
         */
        public synchronized MockLogger reset() {
                logRecords.clear();
+               baos.reset();
                return this;
        }
 
@@ -98,7 +177,7 @@ public class MockLogger extends Logger {
         * @param level The level to match against.
         * @return This object (for method chaining).
         */
-       public synchronized MockLogger assertLevel(Level level) {
+       public synchronized MockLogger assertLastLevel(Level level) {
                assertLogged();
                if (last().getLevel() != level)
                        throw new AssertionError("Message logged at [" + 
last().getLevel() + "] instead of [" + level + "]");
@@ -108,55 +187,29 @@ public class MockLogger extends Logger {
        /**
         * Asserts that the last message matched the specified message.
         *
-        * @param message The message to search for.
-        * @return This object (for method chaining).
-        */
-       public synchronized MockLogger assertMessage(String message) {
-               assertLogged();
-               if (! last().getMessage().equals(message))
-                       throw new AssertionError("Message was not [" + message 
+ "].  Message=[" + last().getMessage() + "]");
-               return this;
-       }
-
-       /**
-        * Asserts that the last message contained the specified text.
-        *
-        * @param messages The messages to search for.
         * @return This object (for method chaining).
         */
-       public synchronized MockLogger assertMessageContains(String...messages) 
{
+       public synchronized FluentStringAssertion<MockLogger> 
assertLastMessage() {
                assertLogged();
-               for (String m : messages)
-                       if (! last().getMessage().contains(m))
-                               throw new AssertionError("Message did not 
contain [" + m + "].  Message=[" + last().getMessage() + "]");
-               return this;
+               return new FluentStringAssertion<>(last().getMessage(), this);
        }
 
        /**
-        * Asserts that the last message doesn't contained the specified text.
+        * Asserts that the specified number of messages have been logged.
         *
-        * @param messages The messages to search for.
         * @return This object (for method chaining).
         */
-       public synchronized MockLogger 
assertMessageNotContains(String...messages) {
-               assertLogged();
-               for (String m : messages)
-                       if (last().getMessage().contains(m))
-                               throw new AssertionError("Message contained [" 
+ m + "].  Message=[" + last().getMessage() + "]");
-               return this;
+       public synchronized FluentIntegerAssertion<MockLogger> 
assertRecordCount() {
+               return new FluentIntegerAssertion<>(logRecords.size(), this);
        }
 
        /**
-        * Asserts that the specified number of messages have been logged.
+        * Allows you to perform fluent-style assertions on the contents of the 
log file.
         *
-        * @param count Expected number of messages logged.
-        * @return This object (for method chaining).
+        * @return A new fluent-style assertion object.
         */
-       public synchronized MockLogger assertCount(int count) {
-               assertLogged();
-               if (logRecords.size() != count)
-                       throw new AssertionError("Wrong number of messages.  
Expected=["+count+"], Actual=["+logRecords.size()+"]");
-               return this;
+       public synchronized FluentStringAssertion<MockLogger> assertContents() {
+               return new FluentStringAssertion<>(baos.toString(), this);
        }
 
        private LogRecord last() {
@@ -164,4 +217,12 @@ public class MockLogger extends Logger {
                        throw new AssertionError("Message not logged");
                return logRecords.get(logRecords.size()-1);
        }
+
+       /**
+        * Returns the contents of this log file as a string.
+        */
+       @Override
+       public String toString() {
+               return baos.toString();
+       }
 }

Reply via email to