Merge branch 'feature/unit-test-views' into dev

pull/77/merge
Alinson S. Xavier 10 years ago
commit e0b637e84d

1
.gitignore vendored

@ -33,3 +33,4 @@ build/
*.iml
art/
*.actual.png

@ -0,0 +1,5 @@
#!/bin/bash
P=/sdcard/Android/data/org.isoron.uhabits/cache/Failed/
adb pull $P Failed/
adb shell rm -r $P

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,87 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.models.HabitFixtures;
import org.isoron.uhabits.views.CheckmarkView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkViewTest extends ViewTest
{
private CheckmarkView view;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createNonDailyHabit();
view = new CheckmarkView(targetContext);
view.setHabit(habit);
measureView(dpToPixels(100), dpToPixels(200), view);
}
@Test
public void render_checked() throws IOException
{
assertRenders(view, "CheckmarkView/checked.png");
}
@Test
public void render_unchecked() throws IOException
{
habit.repetitions.toggle(DateHelper.getStartOfToday());
view.refreshData();
assertRenders(view, "CheckmarkView/unchecked.png");
}
@Test
public void render_implicitlyChecked() throws IOException
{
long today = DateHelper.getStartOfToday();
long day = DateHelper.millisecondsInOneDay;
habit.repetitions.toggle(today);
habit.repetitions.toggle(today - day);
habit.repetitions.toggle(today - 2 * day);
view.refreshData();
assertRenders(view, "CheckmarkView/implicitly_checked.png");
}
@Test
public void render_largeSize() throws IOException
{
measureView(dpToPixels(300), dpToPixels(300), view);
assertRenders(view, "CheckmarkView/large_size.png");
}
}

@ -0,0 +1,78 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.views.RingView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RingViewTest extends ViewTest
{
private RingView view;
@Before
public void setup()
{
super.setup();
view = new RingView(targetContext);
view.setLabel("Hello world");
view.setPercentage(0.6f);
view.setColor(ColorHelper.palette[0]);
view.setMaxDiameter(dpToPixels(100));
}
@Test
public void render_base() throws IOException
{
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "RingView/render.png");
}
@Test
public void render_withLongLabel() throws IOException
{
view.setLabel("The quick brown fox jumps over the lazy fox");
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "RingView/renderLongLabel.png");
}
@Test
public void render_withDifferentParams() throws IOException
{
view.setLabel("Habit Strength");
view.setPercentage(0.25f);
view.setMaxDiameter(dpToPixels(50));
view.setColor(ColorHelper.palette[5]);
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "RingView/renderDifferentParams.png");
}
}

@ -0,0 +1,193 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.test.InstrumentationRegistry;
import android.view.View;
import org.isoron.uhabits.helpers.DialogHelper;
import org.junit.Before;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import static junit.framework.Assert.fail;
public class ViewTest
{
protected static final double SIMILARITY_CUTOFF = 0.02;
public static final int HISTOGRAM_BIN_SIZE = 8;
protected Context testContext;
protected Context targetContext;
@Before
public void setup()
{
targetContext = InstrumentationRegistry.getTargetContext();
testContext = InstrumentationRegistry.getContext();
}
protected void measureView(int width, int height, View view)
{
int specWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int specHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
protected void assertRenders(View view, String expectedImagePath) throws IOException
{
StringBuilder errorMessage = new StringBuilder();
expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap actual = view.getDrawingCache();
Bitmap expected = getBitmapFromAssets(expectedImagePath);
int width = actual.getWidth();
int height = actual.getHeight();
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true);
double distance;
boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
{
similarEnough = false;
errorMessage.append(String.format(
"Rendered image has wrong histogram (distance=%f). ",
distance));
}
if(!similarEnough)
{
saveBitmap(expectedImagePath, ".scaledExpected", scaledExpected);
String path = saveBitmap(expectedImagePath, ".actual", actual);
errorMessage.append(String.format("Actual rendered image " + "saved to %s", path));
fail(errorMessage.toString());
}
actual.recycle();
expected.recycle();
scaledExpected.recycle();
}
private Bitmap getBitmapFromAssets(String path) throws IOException
{
InputStream stream = testContext.getAssets().open(path);
return BitmapFactory.decodeStream(stream);
}
private String getVersionedViewAssetPath(String path)
{
String result = null;
if (android.os.Build.VERSION.SDK_INT >= 21)
{
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
}
catch (IOException e)
{
// ignored
}
}
if(result == null)
result = "views/" + path;
return result;
}
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
{
String absolutePath = String.format("%s/Failed/%s", targetContext.getExternalCacheDir(),
filename.replaceAll("\\.png$", suffix + ".png"));
new File(absolutePath).getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(absolutePath);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
return absolutePath;
}
private int[][] getHistogram(Bitmap bitmap)
{
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE];
for(int x = 0; x < bitmap.getWidth(); x++)
{
for(int y = 0; y < bitmap.getHeight(); y++)
{
int color = bitmap.getPixel(x, y);
int[] argb = new int[]{
(color >> 24) & 0xff, //alpha
(color >> 16) & 0xff, //red
(color >> 8) & 0xff, //green
(color ) & 0xff //blue
};
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
histogram[1][argb[1] / HISTOGRAM_BIN_SIZE]++;
histogram[2][argb[2] / HISTOGRAM_BIN_SIZE]++;
histogram[3][argb[3] / HISTOGRAM_BIN_SIZE]++;
}
}
return histogram;
}
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
{
long diff = 0;
long total = 0;
for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++)
{
diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]);
diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]);
diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]);
diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]);
total += actualHistogram[0][i];
total += actualHistogram[1][i];
total += actualHistogram[2][i];
total += actualHistogram[3][i];
}
return (double) diff / total / 2;
}
protected int dpToPixels(int dp)
{
return (int) DialogHelper.dpToPixels(targetContext, dp);
}
}

@ -30,10 +30,11 @@ import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.models.Habit;
public class CheckmarkView extends View
@ -48,9 +49,9 @@ public class CheckmarkView extends View
private int width;
private int height;
private int leftMargin;
private int topMargin;
private int padding;
private float leftMargin;
private float topMargin;
private float padding;
private String label;
private String fa_check;
@ -65,6 +66,7 @@ public class CheckmarkView extends View
private Rect rect;
private TextPaint textPaint;
private StaticLayout labelLayout;
private Habit habit;
public CheckmarkView(Context context)
{
@ -114,11 +116,8 @@ public class CheckmarkView extends View
public void setHabit(Habit habit)
{
this.check_status = habit.checkmarks.getTodayValue();
this.star_status = habit.scores.getTodayStarStatus();
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
this.label = habit.name;
updateLabel();
this.habit = habit;
refreshData();
}
@Override
@ -152,8 +151,6 @@ public class CheckmarkView extends View
pIcon.setTextSize(width * 0.5f);
pIcon.getTextBounds(text, 0, 1, rect);
// canvas.drawLine(0, 0.67f * height, width, 0.67f * height, pIcon);
int y = (int) ((0.67f * height - rect.bottom - rect.top) / 2);
canvas.drawText(text, width / 2, y, pIcon);
}
@ -188,18 +185,25 @@ public class CheckmarkView extends View
this.width = getMeasuredWidth();
this.height = getMeasuredHeight();
leftMargin = (int) (width * 0.015);
topMargin = (int) (height * 0.015);
leftMargin = (width * 0.015f);
topMargin = (height * 0.015f);
padding = 8 * leftMargin;
textPaint.setTextSize(0.15f * width);
updateLabel();
refreshData();
}
private void updateLabel()
public void refreshData()
{
this.check_status = habit.checkmarks.getTodayValue();
this.star_status = habit.scores.getTodayStarStatus();
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color),
Color.blue(habit.color));
this.label = habit.name;
textPaint.setColor(Color.WHITE);
labelLayout = new StaticLayout(label, textPaint, width - 2 * leftMargin - 2 * padding,
labelLayout = new StaticLayout(label, textPaint,
(int) (width - 2 * leftMargin - 2 * padding),
Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false);
}

@ -29,11 +29,11 @@ import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DialogHelper;
public class RingView extends View
{
@ -48,10 +48,16 @@ public class RingView extends View
private int width;
private int height;
private float diameter;
private float maxDiameter;
private float textSize;
private int fadedTextColor;
public RingView(Context context)
{
super(context);
init();
}
public RingView(Context context, AttributeSet attrs)
{
@ -59,25 +65,28 @@ public class RingView extends View
this.label = DialogHelper.getAttribute(context, attrs, "label");
this.maxDiameter = DialogHelper.getFloatAttribute(context, attrs, "maxDiameter");
this.maxDiameter = DialogHelper.dpToPixels(context, maxDiameter);
this.textSize = getResources().getDimension(R.dimen.smallTextSize);
this.color = ColorHelper.palette[7];
this.percentage = 0.75f;
init();
}
public void setColor(int color)
{
this.color = color;
pRing.setColor(color);
postInvalidate();
}
public void setMaxDiameter(float maxDiameter)
{
this.maxDiameter = maxDiameter;
}
public void setLabel(String label)
{
this.label = label;
}
public void setPercentage(float percentage)
{
this.percentage = percentage;
postInvalidate();
}
private void init()
@ -88,6 +97,8 @@ public class RingView extends View
pRing.setTextAlign(Paint.Align.CENTER);
fadedTextColor = getResources().getColor(R.color.fadedTextColor);
textSize = getResources().getDimension(R.dimen.smallTextSize);
Log.d("RingView", String.format("textSize=%f", textSize));
rect = new RectF();
}

@ -11,8 +11,6 @@ test:
parallel: true
- circle-android wait-for-boot
- adb shell input keyevent 82
- ./gradlew -PdisablePreDex connectedAndroidTest
- ./run_tests
- cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok
- cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok
- adb logcat -d > $CIRCLE_TEST_REPORTS/logcat.txt
- bash <(curl -s https://codecov.io/bash)
- bash <(curl -s https://codecov.io/bash)

@ -0,0 +1,53 @@
#!/bin/bash
PACKAGE_NAME=org.isoron.uhabits
OUTPUT_DIR=app/build/outputs
LOG=${OUTPUT_DIR}/test.log
info() {
local COLOR='\033[1;32m'
local NC='\033[0m'
echo -e " $COLOR*$NC $1"
}
info "Cleaning output directory..."
rm -rf ${OUTPUT_DIR}
mkdir -p ${OUTPUT_DIR}
info "Building and installing APK..."
./gradlew assembleDebug assembleAndroidTest >> $LOG 2>> $LOG || exit 1
adb install -r ${OUTPUT_DIR}/apk/app-debug.apk >> $LOG 2>> $LOG || exit 1
adb install -r ${OUTPUT_DIR}/apk/app-debug-androidTest-unaligned.apk \
>> $LOG 2>> $LOG || exit 1
info "Granting permission to disable animations..."
adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE \
>> $LOG 2>> $LOG || exit 1
info "Running tests..."
adb shell am instrument \
-e coverage true $* \
-w ${PACKAGE_NAME}.test/android.support.test.runner.AndroidJUnitRunner \
| tee ${OUTPUT_DIR}/runner.txt \
| tee -a $LOG
grep -q "FAILURES\!\!\!" ${OUTPUT_DIR}/runner.txt && failed=1
info "Fetching failed generated files..."
mkdir -p ${OUTPUT_DIR}/failed
adb pull /sdcard/Android/data/${PACKAGE_NAME}/cache/Failed \
${OUTPUT_DIR}/failed >> $LOG 2>> $LOG
adb shell rm -r /sdcard/Android/data/${PACKAGE_NAME}/cache/ >> $LOG 2>> $LOG
info "Fetching logcat..."
adb logcat -d > ${OUTPUT_DIR}/logcat.txt
info "Building coverage report..."
mkdir -p ${OUTPUT_DIR}/code-coverage/connected/
adb pull /data/data/${PACKAGE_NAME}/files/coverage.ec \
${OUTPUT_DIR}/code-coverage/connected/ >> $LOG 2>> $LOG
./gradlew app:createDebugCoverageReport \
-x app:connectedDebugAndroidTest >> $LOG 2>> $LOG
info "Uninstalling test APK..."
adb uninstall ${PACKAGE_NAME}.test >> $LOG 2>> $LOG
exit $failed
Loading…
Cancel
Save