Add tests, fix BoundedRational bugs

Add back some basic test infrastructure from the KitKat calculator,
updated to make it work with the current code base and asynchronous
expression evaluation.

Add BoundedRational tests, designed to check that we get all the
corner cases, particularly for degree-based trig functions, right.

Fix a couple of BoundedRational typos that caused these tests to fail.

Change-Id: I81d8f3d9bde6aa6c20f9958eabd62979babeff5b
diff --git a/Android.mk b/Android.mk
index d6b1c0c..2f3e84d 100644
--- a/Android.mk
+++ b/Android.mk
@@ -27,6 +27,10 @@
 
 LOCAL_PACKAGE_NAME := ExactCalculator
 
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
 LOCAL_AAPT_FLAGS := --rename-manifest-package com.android.exactcalculator
 
 include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..dfd6702
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,4 @@
+# Some small BoundedRational methods like equals() are not used by the
+# calculator, but crucial for testing.
+
+-keepclassmembers class com.android.calculator2.BoundedRational { *; }
diff --git a/src/com/android/calculator2/BoundedRational.java b/src/com/android/calculator2/BoundedRational.java
index 43572d5..4a71dee 100644
--- a/src/com/android/calculator2/BoundedRational.java
+++ b/src/com/android/calculator2/BoundedRational.java
@@ -62,9 +62,13 @@
     }
 
     // Debug or log messages only, not pretty.
+    public String toString() {
+        return mNum.toString() + "/" + mDen.toString();
+    }
+
     public static String toString(BoundedRational r) {
         if (r == null) return "not a small rational";
-        return r.mNum.toString() + "/" + r.mDen.toString();
+        return r.toString();
     }
 
     // Primarily for debugging; clearly not exact
@@ -113,7 +117,7 @@
     }
 
     public int signum() {
-        return mDen.signum() * mDen.signum();
+        return mNum.signum() * mDen.signum();
     }
 
     public boolean equals(BoundedRational r) {
@@ -184,7 +188,7 @@
         if (!num_sqrt.multiply(num_sqrt).equals(r.mNum)) return null;
         final BigInteger den_sqrt = BigInteger.valueOf(Math.round(Math.sqrt(
                                                    r.mDen.doubleValue())));
-        if (!num_sqrt.multiply(den_sqrt).equals(r.mDen)) return null;
+        if (!den_sqrt.multiply(den_sqrt).equals(r.mDen)) return null;
         return new BoundedRational(num_sqrt, den_sqrt);
     }
 
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..f4a73e7
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SDK_VERSION := current
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := ExactCalculatorTests
+
+LOCAL_INSTRUMENTATION_FOR := ExactCalculator
+
+LOCAL_AAPT_FLAGS := --rename-manifest-package com.android.exactcalculator.tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..a6183f0
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.calculator2.tests">
+
+    <uses-sdk
+        android:minSdkVersion="21" />
+
+    <instrumentation
+        android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.exactcalculator"
+        android:label="BoundedRational and Calculator Functional Test">
+    </instrumentation>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+</manifest>
diff --git a/tests/README.txt b/tests/README.txt
new file mode 100644
index 0000000..40b4f8a
--- /dev/null
+++ b/tests/README.txt
@@ -0,0 +1,21 @@
+Run on Android with
+
+1) Build the tests.
+2) Install the calculator with
+adb install <tree root>/out/target/product/generic/data/app/ExactCalculatorTests/ExactCalculator.apk
+3) adb install <tree root>/out/target/product/generic/data/app/ExactCalculatorTests/ExactCalculatorTests.apk
+4) adb shell am instrument -w com.android.exactcalculator.tests/android.test.InstrumentationTestRunner
+
+There are two kinds of tests:
+
+1. A superficial test of calculator functionality through the UI.
+This is a resurrected version of a test that appeared in KitKat.
+It's currently mostly a placeholder and some basic infrastructure for
+future tests.
+
+2. A test of the BoundedRationals library that mostly checks for agreement
+with the constructive reals (CR) package.  (The BoundedRationals package
+is used by the calculator mostly to identify exact results, i.e.
+terminating decimal expansions.  But it's also used to optimize CR
+computations, and bugs in BoundedRational could result in incorrect
+outputs.)
diff --git a/tests/src/com/android/calculator2/BRTest.java b/tests/src/com/android/calculator2/BRTest.java
new file mode 100644
index 0000000..bd7070a
--- /dev/null
+++ b/tests/src/com/android/calculator2/BRTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+// A test for BoundedRationals package.
+
+package com.android.calculator2;
+
+import com.hp.creals.CR;
+import com.hp.creals.UnaryCRFunction;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import java.math.BigInteger;
+
+public class BRTest extends TestCase {
+    private static void check(boolean x, String s) {
+        if (!x) throw new AssertionFailedError(s);
+    }
+    final static int TEST_PREC = -100; // 100 bits to the right of
+                                       // binary point.
+    private static void checkEq(BoundedRational x, CR y, String s) {
+        check(x.CRValue().compareTo(y, TEST_PREC) == 0, s);
+    }
+    private static void checkWeakEq(BoundedRational x, CR y, String s) {
+        if (x != null) checkEq(x, y, s);
+    }
+
+    private final static UnaryCRFunction ASIN = UnaryCRFunction.asinFunction;
+    private final static UnaryCRFunction ACOS = UnaryCRFunction.acosFunction;
+    private final static UnaryCRFunction ATAN = UnaryCRFunction.atanFunction;
+    private final static UnaryCRFunction TAN = UnaryCRFunction.tanFunction;
+    private final static BoundedRational BR_0 = new BoundedRational(0);
+    private final static BoundedRational BR_M1 = new BoundedRational(-1);
+    private final static BoundedRational BR_2 = new BoundedRational(2);
+    private final static BoundedRational BR_M2 = new BoundedRational(-2);
+    private final static BoundedRational BR_15 = new BoundedRational(15);
+    private final static BoundedRational BR_390 = new BoundedRational(390);
+    private final static BoundedRational BR_M390 = new BoundedRational(-390);
+    private final static CR CR_1 = CR.valueOf(1);
+
+    private final static CR RADIANS_PER_DEGREE = CR.PI.divide(CR.valueOf(180));
+    private final static CR DEGREES_PER_RADIAN = CR.valueOf(180).divide(CR.PI);
+    private final static CR LN10 = CR.valueOf(10).ln();
+
+    private static CR toRadians(CR x) {
+        return x.multiply(RADIANS_PER_DEGREE);
+    }
+
+    private static CR fromRadians(CR x) {
+        return x.multiply(DEGREES_PER_RADIAN);
+    }
+
+    // We assume that x is simple enough that we don't overflow bounds.
+    private static void checkBR(BoundedRational x) {
+        check(x != null, "test data should not be null");
+        CR xAsCR = x.CRValue();
+        checkEq(BoundedRational.add(x, BoundedRational.ONE), xAsCR.add(CR_1),
+                "add 1:" + x);
+        checkEq(BoundedRational.subtract(x, BoundedRational.MINUS_THIRTY),
+                xAsCR.subtract(CR.valueOf(-30)), "sub -30:" + x);
+        checkEq(BoundedRational.multiply(x, BR_15),
+                xAsCR.multiply(CR.valueOf(15)), "multiply 15:" + x);
+        checkEq(BoundedRational.divide(x, BR_15),
+                xAsCR.divide(CR.valueOf(15)), "divide 15:" + x);
+        checkWeakEq(BoundedRational.sin(x), xAsCR.sin(), "sin:" + x);
+        checkWeakEq(BoundedRational.cos(x), xAsCR.cos(), "cos:" + x);
+        checkWeakEq(BoundedRational.tan(x), TAN.execute(xAsCR), "tan:" + x);
+        checkWeakEq(BoundedRational.degreeSin(x), toRadians(xAsCR).sin(),
+                "degree sin:" + x);
+        checkWeakEq(BoundedRational.degreeCos(x), toRadians(xAsCR).cos(),
+                "degree cos:" + x);
+        BigInteger big_x = BoundedRational.asBigInteger(x);
+        long long_x = (big_x == null? 0 : big_x.longValue());
+        try {
+            checkWeakEq(BoundedRational.degreeTan(x),
+                        TAN.execute(toRadians(xAsCR)), "degree tan:" + x);
+            check((long_x - 90) % 180 != 0, "missed undefined tan: " + x);
+        } catch (ArithmeticException ignored) {
+            check((long_x - 90) % 180 == 0, "exception on defined tan: " + x);
+        }
+        if (x.compareTo(BoundedRational.ONE) <= 0
+                && x.compareTo(BoundedRational.MINUS_ONE) >= 0) {
+            checkWeakEq(BoundedRational.asin(x), ASIN.execute(xAsCR),
+                        "asin:" + x);
+            checkWeakEq(BoundedRational.acos(x), ACOS.execute(xAsCR),
+                        "acos:" + x);
+            checkWeakEq(BoundedRational.degreeAsin(x),
+                        fromRadians(ASIN.execute(xAsCR)), "degree asin:" + x);
+            checkWeakEq(BoundedRational.degreeAcos(x),
+                        fromRadians(ACOS.execute(xAsCR)), "degree acos:" + x);
+        }
+        checkWeakEq(BoundedRational.atan(x), fromRadians(ATAN.execute(xAsCR)),
+                    "atan:" + x);
+        checkWeakEq(BoundedRational.degreeAtan(x),
+                    fromRadians(ATAN.execute(xAsCR)), "degree atan:" + x);
+        if (x.signum() > 0) {
+            checkWeakEq(BoundedRational.ln(x), xAsCR.ln(), "ln:" + x);
+            checkWeakEq(BoundedRational.log(x), xAsCR.ln().divide(LN10),
+                        "log:" + x);
+            checkWeakEq(BoundedRational.sqrt(x), xAsCR.sqrt(), "sqrt:" + x);
+            checkEq(BoundedRational.pow(x, BR_15),
+                    xAsCR.ln().multiply(CR.valueOf(15)).exp(),
+                    "pow(x,15):" + x);
+        }
+    }
+
+    public void testBR() {
+        checkEq(BR_0, CR.valueOf(0), "0");
+        checkEq(BR_390, CR.valueOf(390), "390");
+        checkEq(BR_15, CR.valueOf(15), "15");
+        checkEq(BR_M390, CR.valueOf(-390), "-390");
+        checkEq(BR_M1, CR.valueOf(-1), "-1");
+        checkEq(BR_2, CR.valueOf(2), "2");
+        checkEq(BR_M2, CR.valueOf(-2), "-2");
+        check(BR_0.signum() == 0, "signum(0)");
+        check(BR_M1.signum() == -1, "signum(-1)");
+        check(BR_2.signum() == 1, "signum(2)");
+        // We check values that include all interesting degree values.
+        BoundedRational r = BR_M390;
+        while (!r.equals(BR_390)) {
+            check(r != null, "loop counter overflowed!");
+            checkBR(r);
+            r = BoundedRational.add(r, BR_15);
+        }
+        checkBR(BoundedRational.HALF);
+        checkBR(BoundedRational.MINUS_HALF);
+        checkBR(BoundedRational.ONE);
+        checkBR(BoundedRational.MINUS_ONE);
+        checkBR(new BoundedRational(1000));
+        checkBR(new BoundedRational(100));
+        checkBR(new BoundedRational(4,9));
+        check(BoundedRational.sqrt(new BoundedRational(4,9)) != null,
+              "sqrt(4/9) is null");
+        checkBR(BoundedRational.negate(new BoundedRational(4,9)));
+        checkBR(new BoundedRational(5,9));
+        checkBR(new BoundedRational(5,10));
+        checkBR(new BoundedRational(5,10));
+        checkBR(new BoundedRational(4,13));
+        checkBR(new BoundedRational(36));
+        checkBR(BoundedRational.negate(new BoundedRational(36)));
+        check(BoundedRational.pow(null, BR_15) == null, "pow(null, 15)");
+    }
+
+    public void testBRexceptions() {
+        try {
+            BoundedRational.ln(BR_M1);
+            check(false, "ln(-1)");
+        } catch (ArithmeticException ignored) {}
+        try {
+            BoundedRational.log(BR_M2);
+            check(false, "log(-2)");
+        } catch (ArithmeticException ignored) {}
+        try {
+            BoundedRational.sqrt(BR_M1);
+            check(false, "sqrt(-1)");
+        } catch (ArithmeticException ignored) {}
+        try {
+            BoundedRational.asin(BR_M2);
+            check(false, "asin(-2)");
+        } catch (ArithmeticException ignored) {}
+        try {
+            BoundedRational.degreeAcos(BR_2);
+            check(false, "degree acos(2)");
+        } catch (ArithmeticException ignored) {}
+    }
+
+    public void testBROverflow() {
+        BoundedRational sum = new BoundedRational(0);
+        long i;
+        for (i = 1; i < 1000; ++i) {
+             sum = BoundedRational.add(sum,
+                        BoundedRational.inverse(new BoundedRational(i)));
+             if (sum == null) break;
+        }
+        // Experimentally, this overflows at 139, which seems
+        // plausible based on the Wolfram Alpha result.
+        // This test is robust against minor changes in MAX_SIZE.
+        check(i > 100, "Harmonic series overflowed at " + i);
+        check(i < 1000, "Harmonic series didn't overflow");
+    }
+}
diff --git a/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
new file mode 100644
index 0000000..d26a5cb
--- /dev/null
+++ b/tests/src/com/android/calculator2/CalculatorHitSomeButtons.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2008, Google Inc.
+ *
+ * Licensed 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 com.android.calculator2;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.Instrumentation.ActivityMonitor;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.test.ActivityInstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.graphics.Rect;
+import android.test.TouchUtils;
+
+import com.android.calculator2.Calculator;
+import com.android.calculator2.R;
+import com.android.calculator2.CalculatorResult;
+
+/**
+ * Instrumentation tests for poking some buttons
+ *
+ */
+
+public class CalculatorHitSomeButtons extends ActivityInstrumentationTestCase <Calculator>{
+    public boolean setup = false;
+    private static final String TAG = "CalculatorTests";
+    Calculator mActivity = null;
+    Instrumentation mInst = null;
+
+    public CalculatorHitSomeButtons() {
+        super("com.android.calculator2", Calculator.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mActivity = getActivity();
+        mInst = getInstrumentation();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+/*
+    @LargeTest
+    public void testPressSomeKeys() {
+        Log.v(TAG, "Pressing some keys!");
+
+        // Make sure that we clear the output
+        press(KeyEvent.KEYCODE_ENTER);
+        press(KeyEvent.KEYCODE_CLEAR);
+
+        // 3 + 4 * 5 => 23
+        press(KeyEvent.KEYCODE_3);
+        press(KeyEvent.KEYCODE_PLUS);
+        press(KeyEvent.KEYCODE_4);
+        press(KeyEvent.KEYCODE_9 | KeyEvent.META_SHIFT_ON);
+        press(KeyEvent.KEYCODE_5);
+        press(KeyEvent.KEYCODE_ENTER);
+
+        checkDisplay("23");
+    }
+*/
+
+    @LargeTest
+    public void testTapSomeButtons() {
+        // TODO: This probably makes way too many hardcoded assumptions about locale.
+        // The calculator will need a routine to internationalize the output.
+        // We should use that here, too.
+        Log.v(TAG, "Tapping some buttons!");
+
+        // Make sure that we clear the output
+        tap(R.id.eq);
+        tap(R.id.del);
+
+        // 567 / 3 => 189
+        tap(R.id.digit_5);
+        tap(R.id.digit_6);
+        tap(R.id.digit_7);
+        tap(R.id.op_div);
+        tap(R.id.digit_3);
+        tap(R.id.eq);
+
+        checkDisplay("189");
+
+        // make sure we can continue calculations also
+        // 189 - 789 => -600
+        tap(R.id.op_sub);
+        tap(R.id.digit_7);
+        tap(R.id.digit_8);
+        tap(R.id.digit_9);
+        tap(R.id.eq);
+
+        // Careful: the first digit in the expected value is \u2212, not "-" (a hyphen)
+        checkDisplay(mActivity.getString(R.string.op_sub) + "600");
+    }
+
+    // helper functions
+    private void press(int keycode) {
+        mInst.sendKeyDownUpSync(keycode);
+    }
+
+    private boolean tap(int id) {
+        View view = mActivity.findViewById(id);
+        if(view != null) {
+            TouchUtils.clickView(this, view);
+            return true;
+        }
+        return false;
+    }
+
+    private void checkDisplay(final String s) {
+        mInst.waitForIdle(new Runnable () {
+            @Override
+            public void run() {
+                try {
+                    Thread.sleep(20); // Wait for background computation
+                } catch(InterruptedException ignored) {
+                    fail("Unexpected interrupt");
+                }
+                mInst.waitForIdle(new Runnable () {
+                    @Override
+                    public void run() {
+                        assertEquals(displayVal(), s);
+                    }
+                });
+            }
+        });
+    }
+
+    private String displayVal() {
+        CalculatorResult display = (CalculatorResult) mActivity.findViewById(R.id.result);
+        assertNotNull(display);
+
+        EditText box = (EditText) display;
+        assertNotNull(box);
+
+        return box.getText().toString();
+    }
+}
+