Moved sync tests to test-sync project

pull/14/head
Tim Su 13 years ago
parent 25f52266f2
commit 282c619cfb

@ -192,7 +192,7 @@ public class Astrid3ContentProvider extends ContentProvider {
throw new UnsupportedOperationException("Unknown URI " + uri);
}
static void setDatabaseOverride(AbstractDatabase override) {
public static void setDatabaseOverride(AbstractDatabase override) {
databaseOverride = override;
}

@ -2,8 +2,11 @@
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry combineaccessrules="false" kind="src" path="/astrid"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry combineaccessrules="false" kind="src" path="/astrid"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry combineaccessrules="false" kind="src" path="/astridApi"/>
<classpathentry combineaccessrules="false" kind="src" path="/facebook"/>
<classpathentry combineaccessrules="false" kind="src" path="/GreenDroid"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

@ -1,19 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid.test"
android:versionCode="1"
android:versionName="1.0" >
package="com.todoroo.astrid.sync.tests"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="7" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.timsu.astrid" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
</manifest>
<!--
This declares that this application uses the instrumentation test runner targeting
the package of the parent app. To run the tests use the command:
"adb shell am instrument -w com.xxx.xxx.tests/android.test.InstrumentationTestRunner"
-->
<instrumentation android:name="com.zutubi.android.junitreport.JUnitReportTestRunner"
android:targetPackage="com.timsu.astrid"
android:label="Tests for Astrid"/>
</manifest>

@ -9,3 +9,5 @@
# Project target.
target=android-14
android.library.reference.1=../api
android.library.reference.2=../facebook/facebook

@ -0,0 +1,414 @@
package com.todoroo.astrid.sync.repeats;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import android.content.Intent;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.Weekday;
import com.google.ical.values.WeekdayNum;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.service.StartupService;
import com.todoroo.astrid.test.DatabaseTestCase;
import com.todoroo.astrid.utility.Flags;
abstract public class AbstractSyncRepeatTests<REMOTE_MODEL> extends DatabaseTestCase {
@Autowired
protected TaskDao taskDao;
@Autowired
protected MetadataDao metadataDao;
@Override
protected void setUp() throws Exception {
super.setUp();
Preferences.setStringFromInteger(R.string.p_default_urgency_key, 0);
RepeatTaskCompleteListener.setSkipActFmCheck(true);
}
private void saveAndTriggerRepeatListener(Task task) {
Flags.set(Flags.ACTFM_SUPPRESS_SYNC);
if(task.isSaved())
taskDao.saveExisting(task);
else
taskDao.createNew(task);
Intent intent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED);
intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
new RepeatTaskCompleteListener().onReceive(getContext(), intent);
}
protected void waitAndSync() {
// Subclasses can override this to insert sync functionality
}
/**
* @param t
* @param expectedDueDate
*/
protected REMOTE_MODEL assertTaskExistsRemotely(Task t, long expectedDueDate) {
// Subclasses can override this to check the existence of remote objects
return null;
}
/**
* @param t task
*/
protected void assertTaskCompletedRemotely(Task t) {
// Subclasses can override this to check the status of the corresponding remote task
}
/**
* @param remoteModel
*/
protected long setCompletionDate(boolean completeBefore, Task t,
REMOTE_MODEL remoteModel, long dueDate) {
long completionDate;
if (completeBefore)
completionDate = dueDate - DateUtilities.ONE_DAY;
else
completionDate = dueDate + DateUtilities.ONE_DAY;
t.setValue(Task.COMPLETION_DATE, completionDate);
saveAndTriggerRepeatListener(t);
return completionDate;
}
protected void assertTimesMatch(long expectedTime, long newDueDate) {
assertTrue(String.format("Expected %s, was %s", new Date(expectedTime), new Date(newDueDate)),
Math.abs(expectedTime - newDueDate) < 5000);
}
/*
* Tests for no sync
*/
public void testNoRepeat() {
Task t = new Task();
t.setValue(Task.TITLE, "no repeat");
taskDao.save(t);
t.setValue(Task.COMPLETION_DATE, DateUtilities.now());
saveAndTriggerRepeatListener(t);
TodorooCursor<Task> cursor = taskDao.query(Query.select(Task.ID));
try {
assertEquals(1, cursor.getCount());
} finally {
cursor.close();
}
}
protected void testRepeating(boolean completeBefore, boolean fromCompletion,
RRule rrule, Frequency frequency, String title) {
for (int i = 0; i < StartupService.INTRO_TASK_SIZE; i++) { // Create startup tasks so sync services don't miss the test tasks
Task temp = new Task();
temp.setValue(Task.TITLE, "" + i);
taskDao.save(temp);
}
Task t = new Task();
t.setValue(Task.TITLE, title);
long dueDate = DateUtilities.now() + DateUtilities.ONE_DAY * 3;
dueDate = (dueDate / 1000L) * 1000L; // Strip milliseconds
if (fromCompletion)
t.setFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION, true);
t.setValue(Task.DUE_DATE, dueDate);
if (rrule == null) {
rrule = new RRule();
rrule.setFreq(frequency);
int interval = 2;
rrule.setInterval(interval);
}
t.setValue(Task.RECURRENCE, rrule.toIcal());
taskDao.save(t);
waitAndSync();
t = taskDao.fetch(t.getId(), Task.PROPERTIES); // Refetch
REMOTE_MODEL remoteModel = assertTaskExistsRemotely(t, dueDate);
long completionDate = setCompletionDate(completeBefore, t, remoteModel, dueDate);
waitAndSync();
assertTaskCompletedRemotely(t);
TodorooCursor<Task> cursor = taskDao.query(Query.select(Task.PROPERTIES).where(TaskCriteria.notDeleted()));
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
Task task = new Task(cursor);
System.err.println("Task: " + task.getValue(Task.TITLE) + ", due: " + task.getValue(Task.DUE_DATE));
}
assertEquals(StartupService.INTRO_TASK_SIZE + 2, cursor.getCount());
cursor.moveToFirst();
for (int i = 0; i < StartupService.INTRO_TASK_SIZE; i++) {
cursor.moveToNext();
}
t.readFromCursor(cursor);
assertEquals(title, t.getValue(Task.TITLE));
assertEquals(dueDate, (long)t.getValue(Task.DUE_DATE));
assertTrue(t.isCompleted());
cursor.moveToNext();
t.readFromCursor(cursor);
assertEquals(title, t.getValue(Task.TITLE));
assertFalse(t.isCompleted());
long newDueDate = t.getValue(Task.DUE_DATE);
assertTrue(t.hasDueTime());
long fromDate = (fromCompletion? completionDate : dueDate);
long expectedTime = computeNextDueDateFromDate(fromDate, rrule, fromCompletion);
assertTaskExistsRemotely(t, expectedTime);
assertTimesMatch(expectedTime, newDueDate);
} finally {
cursor.close();
}
}
private long computeWeeklyCaseDueDate(long fromDate, RRule rrule, boolean fromCompletion) {
long result = fromDate;
Frequency frequency = rrule.getFreq();
assertTrue(frequency.equals(Frequency.WEEKLY));
List<WeekdayNum> weekdayNums = rrule.getByDay();
if (weekdayNums.size() == 0) {
result += DateUtilities.ONE_WEEK * rrule.getInterval();
return result;
}
HashSet<Weekday> weekdays = new HashSet<Weekday>();
for (WeekdayNum curr : weekdayNums) {
weekdays.add(curr.wday);
}
Weekday[] allWeekdays = Weekday.values();
result -= DateUtilities.ONE_DAY;
Date date = new Date(result);
Weekday start = allWeekdays[date.getDay()];
int i;
for (i = 0; i < allWeekdays.length; i++) {
if (start == allWeekdays[i]) break;
}
int index = i;
int daysToAdd = 0;
Weekday next = null;
for (i = index + 1; i < allWeekdays.length; i++) {
Weekday curr = allWeekdays[i];
daysToAdd++;
if (weekdays.contains(curr)) {
next = curr;
break;
}
}
if (next == null) {
for (i = 0; i < index + 1; i++) {
Weekday curr = allWeekdays[i];
daysToAdd++;
if (weekdays.contains(curr)) {
next = curr;
break;
}
}
}
if (fromCompletion) {
result += DateUtilities.ONE_WEEK * (rrule.getInterval() - 1);
}
result += DateUtilities.ONE_DAY * daysToAdd;
return result;
}
/** Advanced weekly repeating tests */
protected long computeNextDueDateFromDate(long fromDate, RRule rrule, boolean fromCompletion) {
long expectedTime = fromDate;
Frequency frequency = rrule.getFreq();
int interval = rrule.getInterval();
if (frequency.equals(Frequency.MINUTELY)) {
expectedTime += DateUtilities.ONE_MINUTE * interval;
} else if (frequency.equals(Frequency.HOURLY)) {
expectedTime += DateUtilities.ONE_HOUR * interval;
} else if (frequency.equals(Frequency.DAILY)) {
expectedTime += DateUtilities.ONE_DAY * interval;
} else if (frequency.equals(Frequency.WEEKLY)) {
expectedTime = computeWeeklyCaseDueDate(fromDate, rrule, fromCompletion);
} else if (frequency.equals(Frequency.MONTHLY)) {
Date originalDate = new Date(expectedTime);
for (int i = 0; i < interval; i++) {
int month = originalDate.getMonth();
if (month == 11) { // Roll over the year and set the month to January
originalDate.setYear(originalDate.getYear() + 1);
originalDate.setMonth(0);
} else {
originalDate.setMonth(originalDate.getMonth() + 1);
}
}
expectedTime = originalDate.getTime();
} else if (frequency.equals(Frequency.YEARLY)) {
Date originalCompleteDate = new Date(expectedTime);
originalCompleteDate.setYear(originalCompleteDate.getYear() + interval);
expectedTime = originalCompleteDate.getTime();
}
return expectedTime;
}
private void testFromDueDate(boolean completeBefore, Frequency frequency, String title) {
testRepeating(completeBefore, false, null, frequency, title);
}
private void testFromCompletionDate(boolean completeBefore, Frequency frequency, String title) {
testRepeating(completeBefore, true, null, frequency, title);
}
/** Tests for repeating from due date */
public void testRepeatMinutelyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.MINUTELY, "minutely-before");
}
public void testRepeatMinutelyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.MINUTELY, "minutely-after");
}
public void testRepeatHourlyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.HOURLY, "hourly-before");
}
public void testRepeatHourlyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.HOURLY, "hourly-after");
}
public void testRepeatDailyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.DAILY, "daily-before");
}
public void testRepeatDailyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.DAILY, "daily-after");
}
public void testRepeatWeeklyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.WEEKLY, "weekly-before");
}
public void testRepeatWeeklyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.WEEKLY, "weekly-after");
}
public void testRepeatMonthlyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.MONTHLY, "monthly-before");
}
public void testRepeatMonthlyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.MONTHLY, "monthly-after");
}
public void testRepeatYearlyFromDueDateCompleteBefore() {
testFromDueDate(true, Frequency.YEARLY, "yearly-before");
}
public void testRepeatYearlyFromDueDateCompleteAfter() {
testFromDueDate(false, Frequency.YEARLY, "yearly-after");
}
/** Tests for repeating from completionDate */
public void testRepeatMinutelyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.MINUTELY, "minutely-before");
}
public void testRepeatMinutelyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.MINUTELY, "minutely-after");
}
public void testRepeatHourlyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.HOURLY, "hourly-before");
}
public void testRepeatHourlyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.HOURLY, "hourly-after");
}
public void testRepeatDailyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.DAILY, "daily-before");
}
public void testRepeatDailyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.DAILY, "daily-after");
}
public void testRepeatWeeklyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.WEEKLY, "weekly-before");
}
public void testRepeatWeeklyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.WEEKLY, "weekly-after");
}
public void testRepeatMonthlyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.MONTHLY, "monthly-before");
}
public void testRepeatMonthlyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.MONTHLY, "monthly-after");
}
public void testRepeatYearlyFromCompleteDateCompleteBefore() {
testFromCompletionDate(true, Frequency.YEARLY, "yearly-before");
}
public void testRepeatYearlyFromCompleteDateCompleteAfter() {
testFromCompletionDate(false, Frequency.YEARLY, "yearly-after");
}
private void testAdvancedWeeklyFromCompleteDate(boolean completeBefore, String title) {
RRule rrule = new RRule();
rrule.setFreq(Frequency.WEEKLY);
int interval = 1;
rrule.setInterval(interval);
List<WeekdayNum> weekdays = new ArrayList<WeekdayNum>();
weekdays.add(new WeekdayNum(0, Weekday.MO));
weekdays.add(new WeekdayNum(0, Weekday.WE));
rrule.setByDay(weekdays);
testRepeating(completeBefore, true, rrule, Frequency.WEEKLY, title);
}
// disabled until test can be fixed
public void testAdvancedRepeatWeeklyFromDueDateCompleteBefore() {
// testAdvancedWeeklyFromDueDate(true, "advanced-weekly-before");
}
public void testAdvancedRepeatWeeklyFromDueDateCompleteAfter() {
// testAdvancedWeeklyFromDueDate(false, "advanced-weekly-after");
}
public void testAdvancedRepeatWeeklyFromCompleteDateCompleteBefore() {
testAdvancedWeeklyFromCompleteDate(true, "advanced-weekly-before");
}
public void testAdvancedRepeatWeeklyFromCompleteDateCompleteAfter() {
testAdvancedWeeklyFromCompleteDate(false, "advanced-weekly-after");
}
}

@ -23,12 +23,11 @@ import com.todoroo.astrid.actfm.sync.ActFmSyncService;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.repeats.NewRepeatTests;
import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StartupService;
public class RepeatTestsActFmSync extends NewRepeatTests<Task> {
public class RepeatTestsActFmSync extends AbstractSyncRepeatTests<Task> {
@Autowired MetadataService metadataService;
@Autowired ActFmDataService actFmDataService;

@ -26,11 +26,10 @@ import com.todoroo.astrid.gtasks.api.GtasksApiUtilities;
import com.todoroo.astrid.gtasks.api.GtasksInvoker;
import com.todoroo.astrid.gtasks.auth.GtasksTokenValidator;
import com.todoroo.astrid.gtasks.sync.GtasksSyncProvider;
import com.todoroo.astrid.repeats.NewRepeatTests;
import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.service.MetadataService;
public class RepeatTestsGtasksSync extends NewRepeatTests<com.google.api.services.tasks.model.Task> {
public class RepeatTestsGtasksSync extends AbstractSyncRepeatTests<com.google.api.services.tasks.model.Task> {
@Autowired MetadataService metadataService;
@Autowired GtasksMetadataService gtasksMetadataService;

@ -1,7 +1,5 @@
package com.todoroo.astrid.sync.repeats;
import static com.todoroo.astrid.sync.repeats.RepeatTestsGtasksSync.DEFAULT_LIST;
import java.io.IOException;
import java.util.Date;

@ -14,12 +14,11 @@ import com.todoroo.astrid.producteev.api.ProducteevInvoker;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.producteev.sync.ProducteevTask;
import com.todoroo.astrid.repeats.NewRepeatTests;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.tags.TagService;
public class RepeatTestsProducteevSync extends NewRepeatTests<JSONObject> {
public class RepeatTestsProducteevSync extends AbstractSyncRepeatTests<JSONObject> {
private static boolean initialized = false;

@ -0,0 +1,63 @@
package com.todoroo.astrid.test;
import java.io.File;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.provider.Astrid3ContentProvider;
import com.todoroo.astrid.service.AstridDependencyInjector;
/**
* Test case that automatically sets up and tears down a test database
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class DatabaseTestCase extends TodorooTestCaseWithInjector {
static {
AstridDependencyInjector.initialize();
}
public static Database database = new TestDatabase();
@Override
protected void addInjectables() {
testInjector.addInjectable("database", database);
}
@Override
protected void setUp() throws Exception {
// call upstream setup, which invokes dependency injector
super.setUp();
// empty out test databases
database.clear();
database.openForWriting();
Astrid3ContentProvider.setDatabaseOverride(database);
}
/**
* Helper to delete a database by name
* @param database
*/
protected void deleteDatabase(String database) {
File db = getContext().getDatabasePath(database);
if(db.exists())
db.delete();
}
@Override
protected void tearDown() throws Exception {
database.close();
super.tearDown();
}
public static class TestDatabase extends Database {
@Override
public String getName() {
return "databasetest";
}
}
}

@ -0,0 +1,48 @@
package com.todoroo.astrid.test;
import com.todoroo.andlib.service.AbstractDependencyInjector;
import com.todoroo.andlib.service.DependencyInjectionService;
public class TestDependencyInjector extends AbstractDependencyInjector {
private String name;
public TestDependencyInjector(String name) {
this.name = name;
}
public void addInjectable(String field, Object injection) {
injectables.put(field, injection);
}
@Override
protected void addInjectables() {
// do nothing, we populate injectables via the addInjectable method
}
@Override
public String toString() {
return "TestDI:" + name;
}
// --- static stuff
/**
* Install TestDependencyInjector above other injectors
*/
public synchronized static TestDependencyInjector initialize(String name) {
TestDependencyInjector instance = new TestDependencyInjector(name);
DependencyInjectionService.getInstance().addInjector(instance);
return instance;
}
/**
* Remove an installed TestDependencyInjector
* @param string
*/
public static void deinitialize(TestDependencyInjector instance) {
DependencyInjectionService.getInstance().removeInjector(instance);
}
}

@ -0,0 +1,25 @@
package com.todoroo.astrid.test;
/**
* Utility methods used in unit tests
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class TestUtilities {
/**
* Sleep, suppressing exceptions
*
* @param millis
*/
public static void sleepDeep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
// do nothing
}
}
}

@ -0,0 +1,66 @@
package com.todoroo.astrid.test;
import java.util.Locale;
import android.content.res.Configuration;
import android.test.AndroidTestCase;
import android.util.DisplayMetrics;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.astrid.service.AstridDependencyInjector;
/**
* Base test case for Astrid tests
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class TodorooTestCase extends AndroidTestCase {
static {
AstridDependencyInjector.initialize();
}
@Override
protected void setUp() throws Exception {
super.setUp();
ContextManager.setContext(this.getContext());
AstridDependencyInjector.flush();
DependencyInjectionService.getInstance().inject(this);
setLocale(Locale.ENGLISH);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
setLocale(Locale.getDefault());
}
/**
* Loop through each locale and call runnable
* @param r
*/
public void forEachLocale(Runnable r) {
Locale[] locales = Locale.getAvailableLocales();
for(Locale locale : locales) {
setLocale(locale);
r.run();
}
}
/**
* Sets locale
* @param locale
*/
private void setLocale(Locale locale) {
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
getContext().getResources().updateConfiguration(config, metrics);
}
}

@ -0,0 +1,31 @@
package com.todoroo.astrid.test;
/**
* Base test case for Astrid tests that need a separate injection context
*
* @author Tim Su <tim@todoroo.com>
*
*/
abstract public class TodorooTestCaseWithInjector extends TodorooTestCase {
protected TestDependencyInjector testInjector;
abstract protected void addInjectables();
@Override
protected void setUp() throws Exception {
testInjector = TestDependencyInjector.initialize("test");
addInjectables();
super.setUp();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
TestDependencyInjector.deinitialize(testInjector);
}
}

@ -0,0 +1,246 @@
/*
* Copyright (C) 2010 Zutubi Pty Ltd
*
* 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.zutubi.android.junitreport;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import org.xmlpull.v1.XmlSerializer;
import android.content.Context;
import android.util.Log;
import android.util.Xml;
/**
* Custom test listener that outputs test results to a single XML file. The file
* uses a similar format the to Ant JUnit task XML formatter, with a couple of
* caveats:
* <ul>
* <li>Multiple suites are all placed in a single file under a root
* &lt;testsuites&gt; element.</li>
* <li>Redundant information about the number of nested cases within a suite is
* omitted.</li>
* <li>Neither standard output nor system properties are included.</li>
* </ul>
* The differences mainly revolve around making this reporting as lightweight as
* possible. The report is streamed as the tests run, making it impossible to,
* e.g. include the case count in a &lt;testsuite&gt; element.
*/
public class JUnitReportListener implements TestListener {
private static final String LOG_TAG = "JUnitReportListener";
private static final String ENCODING_UTF_8 = "utf-8";
private static final String TAG_SUITES = "testsuites";
private static final String TAG_SUITE = "testsuite";
private static final String TAG_CASE = "testcase";
private static final String TAG_ERROR = "error";
private static final String TAG_FAILURE = "failure";
private static final String ATTRIBUTE_NAME = "name";
private static final String ATTRIBUTE_CLASS = "classname";
private static final String ATTRIBUTE_TYPE = "type";
private static final String ATTRIBUTE_MESSAGE = "message";
private static final String ATTRIBUTE_TIME = "time";
// With thanks to org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner.
// Trimmed some entries, added others for Android.
private static final String[] DEFAULT_TRACE_FILTERS = new String[] {
"junit.framework.TestCase", "junit.framework.TestResult",
"junit.framework.TestSuite",
"junit.framework.Assert.", // don't filter AssertionFailure
"java.lang.reflect.Method.invoke(", "sun.reflect.",
// JUnit 4 support:
"org.junit.", "junit.framework.JUnit4TestAdapter", " more",
// Added for Android
"android.test.", "android.app.Instrumentation",
"java.lang.reflect.Method.invokeNative",
};
private Context mContext;
private String mReportFilePath;
private boolean mFilterTraces;
private FileOutputStream mOutputStream;
private XmlSerializer mSerializer;
private String mCurrentSuite;
// simple time tracking
private boolean timeAlreadyWritten = false;
private long testStart;
/**
* Creates a new listener.
*
* @param context context of the target application under test
* @param reportFilePath path of the report file to create (under the
* context using {@link Context#openFileOutput(String, int)}).
* @param filterTraces if true, stack traces will have common noise (e.g.
* framework methods) omitted for clarity
*/
public JUnitReportListener(Context context, String reportFilePath, boolean filterTraces) {
this.mContext = context;
this.mReportFilePath = reportFilePath;
this.mFilterTraces = filterTraces;
}
public void startTest(Test test) {
try {
openIfRequired(test);
if (test instanceof TestCase) {
TestCase testCase = (TestCase) test;
checkForNewSuite(testCase);
testStart = System.currentTimeMillis();
timeAlreadyWritten = false;
mSerializer.startTag("", TAG_CASE);
mSerializer.attribute("", ATTRIBUTE_CLASS, mCurrentSuite);
mSerializer.attribute("", ATTRIBUTE_NAME, testCase.getName());
}
} catch (IOException e) {
Log.e(LOG_TAG, safeMessage(e));
}
}
private void checkForNewSuite(TestCase testCase) throws IOException {
String suiteName = testCase.getClass().getName();
if (mCurrentSuite == null || !mCurrentSuite.equals(suiteName)) {
if (mCurrentSuite != null) {
mSerializer.endTag("", TAG_SUITE);
}
mSerializer.startTag("", TAG_SUITE);
mSerializer.attribute("", ATTRIBUTE_NAME, suiteName);
mCurrentSuite = suiteName;
}
}
private void openIfRequired(Test test) throws IOException {
if (mOutputStream == null) {
mOutputStream = mContext.openFileOutput(mReportFilePath, 0);
mSerializer = Xml.newSerializer();
mSerializer.setOutput(mOutputStream, ENCODING_UTF_8);
mSerializer.startDocument(ENCODING_UTF_8, true);
mSerializer.startTag("", TAG_SUITES);
}
}
public void addError(Test test, Throwable error) {
addProblem(TAG_ERROR, error);
}
public void addFailure(Test test, AssertionFailedError error) {
addProblem(TAG_FAILURE, error);
}
private void addProblem(String tag, Throwable error) {
try {
recordTestTime();
mSerializer.startTag("", tag);
mSerializer.attribute("", ATTRIBUTE_MESSAGE, safeMessage(error));
mSerializer.attribute("", ATTRIBUTE_TYPE, error.getClass().getName());
StringWriter w = new StringWriter();
error.printStackTrace(mFilterTraces ? new FilteringWriter(w) : new PrintWriter(w));
mSerializer.text(w.toString());
mSerializer.endTag("", tag);
} catch (IOException e) {
Log.e(LOG_TAG, safeMessage(e));
}
}
private void recordTestTime() throws IOException {
if(!timeAlreadyWritten) {
timeAlreadyWritten = true;
mSerializer.attribute("", ATTRIBUTE_TIME,
String.format("%.3f", (System.currentTimeMillis() - testStart) / 1000.));
}
}
public void endTest(Test test) {
try {
if (test instanceof TestCase) {
recordTestTime();
mSerializer.endTag("", TAG_CASE);
}
} catch (IOException e) {
Log.e(LOG_TAG, safeMessage(e));
}
}
/**
* Releases all resources associated with this listener. Must be called
* when the listener is finished with.
*/
public void close() {
if (mSerializer != null) {
try {
if (mCurrentSuite != null) {
mSerializer.endTag("", TAG_SUITE);
}
mSerializer.endTag("", TAG_SUITES);
mSerializer.endDocument();
mSerializer = null;
} catch (IOException e) {
Log.e(LOG_TAG, safeMessage(e));
}
}
if (mOutputStream != null) {
try {
mOutputStream.close();
mOutputStream = null;
} catch (IOException e) {
Log.e(LOG_TAG, safeMessage(e));
}
}
}
private String safeMessage(Throwable error) {
String message = error.getMessage();
return error.getClass().getName() + ": " + (message == null ? "<null>" : message);
}
/**
* Wrapper around a print writer that filters out common noise from stack
* traces, making it easier to see the actual failure.
*/
private static class FilteringWriter extends PrintWriter {
public FilteringWriter(Writer out) {
super(out);
}
@Override
public void println(String s) {
for (String filtered : DEFAULT_TRACE_FILTERS) {
if (s.contains(filtered)) {
return;
}
}
super.println(s);
}
}
}

@ -0,0 +1,98 @@
/*
* Copyright (C) 2010 Zutubi Pty Ltd
*
* 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.zutubi.android.junitreport;
import android.os.Bundle;
import android.test.AndroidTestRunner;
import android.test.InstrumentationTestRunner;
/**
* Custom test runner that adds a {@link JUnitReportListener} to the underlying
* test runner in order to capture test results in an XML report. You may use
* this class in place of {@link InstrumentationTestRunner} in your test
* project's manifest, and/or specify it to your Ant build using the test.runner
* property.
* <p/>
* This runner behaves identically to the default, with the added side-effect of
* producing a JUnit XML report. The report format is similar to that produced
* by the Ant JUnit task's XML formatter, making it compatible with existing
* tools that can process that format. See {@link JUnitReportListener} for
* further details.
* <p/>
* This runner accepts the following arguments:
* <ul>
* <li>reportFilePath: path of the file to write the XML report to, in the
* target application's data area (default: junit-report.xml).</li>
* <li>filterTraces: if true, stack traces in test failure reports will be
* filtered to remove noise such as framework methods (default: true)</li>
* </ul>
* These arguments may be specified as follows:
*
* <pre>
* {@code adb shell am instrument -w -e reportFile my-report-file.xml}
* </pre>
*/
public class JUnitReportTestRunner extends InstrumentationTestRunner {
/**
* Path, relative to the target applications file root, at which to write the report file.
*/
private static final String ARG_REPORT_FILE_PATH = "reportFilePath";
/**
* If true, stack traces in the report will be filtered to remove common noise (e.g. framework
* methods).
*/
private static final String ARG_FILTER_TRACES = "filterTraces";
/**
* Default path of the report file.
*/
private static final String DEFAULT_REPORT_FILE = "junit-report.xml";
private JUnitReportListener mListener;
private String mReportFilePath;
private boolean mFilterTraces = true;
@Override
public void onCreate(Bundle arguments) {
if (arguments != null) {
mReportFilePath = arguments.getString(ARG_REPORT_FILE_PATH);
mFilterTraces = arguments.getBoolean(ARG_FILTER_TRACES, true);
}
if (mReportFilePath == null) {
mReportFilePath = DEFAULT_REPORT_FILE;
}
super.onCreate(arguments);
}
@Override
protected AndroidTestRunner getAndroidTestRunner() {
AndroidTestRunner runner = new AndroidTestRunner();
mListener = new JUnitReportListener(getTargetContext(), mReportFilePath, mFilterTraces);
runner.addTestListener(mListener);
return runner;
}
@Override
public void finish(int resultCode, Bundle results) {
if (mListener != null) {
mListener.close();
}
super.finish(resultCode, results);
}
}

@ -1,34 +0,0 @@
/*
* 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.
*/
package com.todoroo.astrid;
import junit.framework.Test;
import junit.framework.TestSuite;
import android.test.suitebuilder.TestSuiteBuilder;
/**
* A test suite containing activity-related tests
*/
public class ActivityTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(ActivityTests.class)
.includePackages("com.todoroo.astrid.activity")
.build();
}
}

@ -46,10 +46,6 @@ public class AllTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(AllTests.class)
.excludePackages(
"com.todoroo.astrid.gtasks",
"com.todoroo.astrid.producteev",
"com.todoroo.astrid.sync.repeats")
.build();
}
}

@ -1,55 +0,0 @@
/*
* 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.
*/
package com.todoroo.astrid;
import junit.framework.Test;
import junit.framework.TestSuite;
import android.test.suitebuilder.TestSuiteBuilder;
/**
* A test suite containing all tests for ApiDemos.
*
* To run all suites found in this apk:
* $ adb shell am instrument -w \
* com.example.android.apis.tests/android.test.InstrumentationTestRunner
*
* To run just this suite from the command line:
* $ adb shell am instrument -w \
* -e class com.example.android.apis.AllTests \
* com.example.android.apis.tests/android.test.InstrumentationTestRunner
*
* To run an individual test case, e.g. {@link com.example.android.apis.os.MorseCodeConverterTest}:
* $ adb shell am instrument -w \
* -e class com.example.android.apis.os.MorseCodeConverterTest \
* com.example.android.apis.tests/android.test.InstrumentationTestRunner
*
* To run an individual test, e.g. {@link com.example.android.apis.os.MorseCodeConverterTest#testCharacterS()}:
* $ adb shell am instrument -w \
* -e class com.example.android.apis.os.MorseCodeConverterTest#testCharacterS \
* com.example.android.apis.tests/android.test.InstrumentationTestRunner
*/
public class ContinuousTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(ContinuousTests.class)
.excludePackages(
"com.todoroo.astrid.gtasks",
"com.todoroo.astrid.producteev",
"com.todoroo.astrid.sync.repeats")
.build();
}
}

@ -1,35 +0,0 @@
/*
* 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.
*/
package com.todoroo.astrid;
import junit.framework.Test;
import junit.framework.TestSuite;
import android.test.suitebuilder.TestSuiteBuilder;
/**
* A test suite containing activity-related tests
*/
public class NightlyTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(NightlyTests.class)
.includeAllPackagesUnderHere()
.build();
}
}
Loading…
Cancel
Save