diff options
| -rw-r--r-- | Android.mk | 4 | ||||
| -rw-r--r-- | proguard.flags | 4 | ||||
| -rw-r--r-- | src/com/android/calculator2/BoundedRational.java | 10 | ||||
| -rw-r--r-- | tests/Android.mk | 20 | ||||
| -rw-r--r-- | tests/AndroidManifest.xml | 33 | ||||
| -rw-r--r-- | tests/README.txt | 21 | ||||
| -rw-r--r-- | tests/src/com/android/calculator2/BRTest.java | 195 | ||||
| -rw-r--r-- | tests/src/com/android/calculator2/CalculatorHitSomeButtons.java | 164 |
8 files changed, 448 insertions, 3 deletions
@@ -27,6 +27,10 @@ LOCAL_SDK_VERSION := current 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 @@ public class BoundedRational { } // 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 class BoundedRational { } public int signum() { - return mDen.signum() * mDen.signum(); + return mNum.signum() * mDen.signum(); } public boolean equals(BoundedRational r) { @@ -184,7 +188,7 @@ public class BoundedRational { 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(); + } +} + |
