From 1e34aaf536cdb1f92f49205a0f9df55f38d97a3d Mon Sep 17 00:00:00 2001
From: Matthew Hague <matthew.hague@rhul.ac.uk>
Date: Wed, 9 Dec 2020 19:32:46 +0000
Subject: [PATCH] Add call and callStatic that check null return

You can define if a null return value is considered an error. Useful if
you need to use the returned object later on in the test.
---
 .../uk/ac/rhul/cs/javatester/CodeTester.java  | 129 +++++++++++++++---
 .../uk/ac/rhul/cs/javatester/UnitTests.java   |  30 ++++
 .../cs/javatester/solution/NullPtrMethod.java |   4 +
 3 files changed, 147 insertions(+), 16 deletions(-)

diff --git a/src/main/java/uk/ac/rhul/cs/javatester/CodeTester.java b/src/main/java/uk/ac/rhul/cs/javatester/CodeTester.java
index d6968a9..2ecad55 100644
--- a/src/main/java/uk/ac/rhul/cs/javatester/CodeTester.java
+++ b/src/main/java/uk/ac/rhul/cs/javatester/CodeTester.java
@@ -302,7 +302,6 @@ public class CodeTester {
         return array;
     }
 
-
     /**
      * Convenience for constructMsg with default messages
      */
@@ -325,6 +324,23 @@ public class CodeTester {
         return constructMsg(msg, invokeMsg, className, params);
     }
 
+    /**
+     * Convenience for callMsgAcceptNull with acceptNull set to true
+     */
+    public Object callMsg(String msg,
+                          String invocationMsg,
+                          Object o,
+                          String methodName,
+                          Object... params) throws
+                  BaseTester.FailedTestException {
+        return callMsgAcceptNull(msg,
+                                 invocationMsg,
+                                 true,
+                                 o,
+                                 methodName,
+                                 params);
+    }
+
     /**
      * Attempts to call an object method
      *
@@ -332,20 +348,24 @@ public class CodeTester {
      * msg and invocationMsg can contain LOG_EXPAND which will be replaced by
      * getIndentedLinesString in the exception.
      *
+     * Can specify whether to cause a failure if the method returns null
+     *
      * @param msg the failure message
      * @param invocationMsg failure message prefix in case method call
      * throws exception (student code fails)
+     * @param acceptNull whether the result of the call can be null
      * @param o the object to call the method on
      * @param String name method name
      * @param parameterTypes the arguments (cannot be null else type
      * can't be inferred, with throw NPE)
      * @return the result object
      */
-    public Object callMsg(String msg,
-                          String invocationMsg,
-                          Object o,
-                          String methodName,
-                          Object... params) throws
+    public Object callMsgAcceptNull(String msg,
+                                    String invocationMsg,
+                                    boolean acceptNull,
+                                    Object o,
+                                    String methodName,
+                                    Object... params) throws
                   BaseTester.FailedTestException {
         Class<?>[] parameterTypes
             = Stream.of(params).map(p -> p.getClass())
@@ -355,6 +375,13 @@ public class CodeTester {
 
             Object r = invokeMethod(invocationMsg, m, o, params);
 
+            if (r == null && !acceptNull) {
+                throw new BaseTester.FailedTestException(
+                    expandMsg(invocationMsg) + "\n\n" +
+                    "The method call returned null but a non-null value was expected."
+                );
+            }
+
             recordCall(r, o, m, params);
 
             return r;
@@ -373,13 +400,27 @@ public class CodeTester {
         throw new BaseTester.FailedTestException(expandMsg(msg));
     }
 
+
     /**
      * Convenience for callMsg with default messages.
+     *
+     * Will allow method to return null.
      */
     public Object call(Object o,
                        String methodName,
                        Object... params) throws
                   BaseTester.FailedTestException {
+        return callAcceptNull(true, o, methodName, params);
+    }
+
+    /**
+     * Convenience for callMsgAcceptNull with default messages.
+     */
+    public Object callAcceptNull(boolean acceptNull,
+                                 Object o,
+                                 String methodName,
+                                 Object... params) throws
+                  BaseTester.FailedTestException {
         String base;
 
         if (getLines().size() > 0) {
@@ -394,7 +435,30 @@ public class CodeTester {
         String msg = base + "could not be made.";
         String invokeMsg = base + "went wrong.";
 
-        return callMsg(msg, invokeMsg, o, methodName, params);
+        return callMsgAcceptNull(msg,
+                                 invokeMsg,
+                                 acceptNull,
+                                 o,
+                                 methodName,
+                                 params);
+    }
+
+
+    /**
+     * Convenience for callStaticMsgAcceptNull with acceptNull as true
+     */
+    public Object callStaticMsg(String msg,
+                                String invocationMsg,
+                                Class<?> klass,
+                                String methodName,
+                                Object... params) throws
+                  BaseTester.FailedTestException {
+        return callStaticMsgAcceptNull(msg,
+                                       invocationMsg,
+                                       true,
+                                       klass,
+                                       methodName,
+                                       params);
     }
 
     /**
@@ -404,19 +468,23 @@ public class CodeTester {
      * msg and invocationMsg can contain LOG_EXPAND which will be replaced by
      * getIndentedLinesString in the exception.
      *
+     * Can specify whether a null return value is an error.
+     *
      * @param msg the failure message
      * @param invocationMsg failure message prefix in case method call
      * throws exception (student code fails)
+     * @param acceptNull whether null is an ok return value
      * @param klass the class having the method
      * @param String name method name
      * @param parameterTypes the arguments
      * @return the result object
      */
-    public Object callStaticMsg(String msg,
-                                String invocationMsg,
-                                Class<?> klass,
-                                String methodName,
-                                Object... params) throws
+    public Object callStaticMsgAcceptNull(String msg,
+                                          String invocationMsg,
+                                          boolean acceptNull,
+                                          Class<?> klass,
+                                          String methodName,
+                                          Object... params) throws
                   BaseTester.FailedTestException {
         Class<?>[] parameterTypes
             = Stream.of(params).map(p -> p.getClass())
@@ -433,6 +501,13 @@ public class CodeTester {
 
             Object r = invokeMethod(invocationMsg, m, null, params);
 
+            if (r == null && !acceptNull) {
+                throw new BaseTester.FailedTestException(
+                    expandMsg(invocationMsg) + "\n\n" +
+                    "The method call returned null but a non-null value was expected."
+                );
+            }
+
             recordStaticCall(r, klass, m, params);
 
             return r;
@@ -443,21 +518,40 @@ public class CodeTester {
                 Utils.makeThrowableMsg(e.getCause())
             );
         }
+        catch (BaseTester.FailedTestException e) {
+            throw e;
+        }
         catch (Throwable e) {
             /* fall through */
+            System.out.println(e);
         }
 
         // failures fall through here
         throw new BaseTester.FailedTestException(expandMsg(msg));
     }
 
+
     /**
-     * Convenience for callMsg with default messages.
+     * Convenience for callMsgAcceptNull with acceptNull as true
      */
     public Object callStatic(Class<?> klass,
                              String methodName,
                              Object... params) throws
                   BaseTester.FailedTestException {
+        return callStaticAcceptNull(true,
+                                    klass,
+                                    methodName,
+                                    params);
+    }
+
+    /**
+     * Convenience for callStaticMsgAcceptNull with default messages.
+     */
+    public Object callStaticAcceptNull(boolean acceptNull,
+                                       Class<?> klass,
+                                       String methodName,
+                                       Object... params) throws
+                  BaseTester.FailedTestException {
         String base;
 
         if (getLines().size() > 0) {
@@ -472,10 +566,14 @@ public class CodeTester {
         String msg = base + "could not be made.";
         String invokeMsg = base + "went wrong.";
 
-        return callStaticMsg(msg, invokeMsg, klass, methodName, params);
+        return callStaticMsgAcceptNull(msg,
+                                       invokeMsg,
+                                       acceptNull,
+                                       klass,
+                                       methodName,
+                                       params);
     }
 
-
     /**
      * If a name is recorded for the object, return it, else null
      */
@@ -483,7 +581,6 @@ public class CodeTester {
         return objectNames.get(System.identityHashCode(o));
     }
 
-
     /**
      * Expand a string with log and arguments.
      *
diff --git a/src/test/java/uk/ac/rhul/cs/javatester/UnitTests.java b/src/test/java/uk/ac/rhul/cs/javatester/UnitTests.java
index 34e0db3..a05098f 100644
--- a/src/test/java/uk/ac/rhul/cs/javatester/UnitTests.java
+++ b/src/test/java/uk/ac/rhul/cs/javatester/UnitTests.java
@@ -315,4 +315,34 @@ public class UnitTests {
         };
         assertFalse(tester.runTests());
     }
+
+    @Test
+    public void testCallMethodNoNull() {
+        BaseTester tester = new BaseTester(NULL_PTR_CLASS) {
+            @SuppressWarnings("unused")
+            public boolean testCallMethod()
+                    throws BaseTester.FailedTestException {
+                CodeTester ct = new CodeTester();
+                Object o = ct.construct(NULL_PTR_CLASS);
+                ct.callAcceptNull(false, o, "getNull");
+                return true;
+            }
+        };
+        assertFalse(tester.runTests());
+    }
+
+    @Test
+    public void testCallMethodStaticNoNull() {
+        BaseTester tester = new BaseTester(NULL_PTR_CLASS) {
+            @SuppressWarnings("unused")
+            public boolean testCallMethod()
+                    throws BaseTester.FailedTestException {
+                CodeTester ct = new CodeTester();
+                Class<?> klass = ct.loadClass(NULL_PTR_CLASS);
+                ct.callStaticAcceptNull(false, klass, "getNull");
+                return true;
+            }
+        };
+        assertFalse(tester.runTests());
+    }
 }
diff --git a/src/test/java/uk/ac/rhul/cs/javatester/solution/NullPtrMethod.java b/src/test/java/uk/ac/rhul/cs/javatester/solution/NullPtrMethod.java
index 1788f9f..6ecdff3 100644
--- a/src/test/java/uk/ac/rhul/cs/javatester/solution/NullPtrMethod.java
+++ b/src/test/java/uk/ac/rhul/cs/javatester/solution/NullPtrMethod.java
@@ -16,4 +16,8 @@ public class NullPtrMethod {
         String n = null;
         n.length();
     }
+
+    public static String getNull() {
+        return null;
+    }
 }
-- 
GitLab