Merge branch 'master' into gtasks

pull/14/head
Tim Su 15 years ago
commit 1e9cda7aaa

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid"
android:versionName="3.3.4 (bug fix release)"
android:versionCode="159">
android:versionName="3.3.5 (bug fix release)"
android:versionCode="160">
<!-- widgets, alarms, and services will break if Astrid is installed on SD card -->
<!-- android:installLocation="internalOnly"> -->

@ -3,12 +3,13 @@
*/
package com.todoroo.astrid.alarms;
import java.util.Date;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.text.format.DateUtils;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.AstridApiConstants;
@ -60,10 +61,16 @@ public class AlarmDetailExposer extends BroadcastReceiver {
if(nextTime == -1)
return null;
CharSequence durationString = DateUtils.getRelativeDateTimeString(context,
nextTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS,
DateUtils.FORMAT_ABBREV_ALL);
return context.getString(R.string.alarm_ADE_detail, durationString);
int flags = DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME;
Date today = new Date();
Date alarm = new Date(nextTime);
if(today.getYear() == alarm.getYear())
flags |= DateUtils.FORMAT_NO_YEAR;
if(alarm.getTime() - today.getTime() > DateUtilities.ONE_DAY)
flags |= DateUtils.FORMAT_SHOW_DATE;
CharSequence durationString = DateUtils.formatDateTime(context, nextTime,
flags);
return "<img src='silk_clock'/> " + durationString; //$NON-NLS-1$
} finally {
cursor.close();
}

@ -25,6 +25,7 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.reminders.Notifications;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.utility.Constants;
/**
* Provides operations for working with alerts
@ -77,7 +78,10 @@ public class AlarmService {
metadata.add(item);
}
return service.synchronizeMetadata(taskId, metadata, Metadata.KEY.eq(AlarmFields.METADATA_KEY)) > 0;
boolean changed = service.synchronizeMetadata(taskId, metadata, Metadata.KEY.eq(AlarmFields.METADATA_KEY)) > 0;
if(changed)
scheduleAlarms(taskId);
return changed;
}
// --- alarm scheduling
@ -99,7 +103,7 @@ public class AlarmService {
* @return todoroo cursor. PLEASE CLOSE THIS CURSOR!
*/
private TodorooCursor<Metadata> getAlarmsForTask(long taskId) {
return PluginServices.getMetadataService().query(Query.select(AlarmFields.TIME).
return PluginServices.getMetadataService().query(Query.select(Metadata.TASK, AlarmFields.TIME).
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
where(Criterion.and(TaskCriteria.isActive(),
MetadataCriteria.byTaskAndwithKey(taskId, AlarmFields.METADATA_KEY))));
@ -129,8 +133,8 @@ public class AlarmService {
* Schedules alarms for a single task
* @param task
*/
public void scheduleAlarms(Task task) {
TodorooCursor<Metadata> cursor = getAlarmsForTask(task.getId());
public void scheduleAlarms(long taskId) {
TodorooCursor<Metadata> cursor = getAlarmsForTask(taskId);
try {
Metadata alarm = new Metadata();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
@ -138,7 +142,7 @@ public class AlarmService {
scheduleAlarm(alarm);
}
} catch (Exception e) {
// suppress
Log.i("astrid-alarms", "Error scheduling alarm", e); //$NON-NLS-1$ //$NON-NLS-2$
} finally {
cursor.close();
}
@ -173,6 +177,7 @@ public class AlarmService {
if(time == 0 || time == NO_ALARM)
am.cancel(pendingIntent);
else if(time > DateUtilities.now()) {
if(Constants.DEBUG)
Log.e("Astrid", "Alarm (" + taskId + ", " + type +
") set for " + new Date(time));
am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent);

@ -19,6 +19,8 @@
*/
package com.todoroo.astrid.producteev;
import java.util.TimeZone;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
@ -27,8 +29,10 @@ import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.flurry.android.FlurryAgent;
@ -78,6 +82,21 @@ public class ProducteevLoginActivity extends Activity {
final EditText emailEditText = (EditText) findViewById(R.id.email);
final EditText passwordEditText = (EditText) findViewById(R.id.password);
final View newUserLayout = findViewById(R.id.newUserLayout);
final Spinner timezoneList = (Spinner) findViewById(R.id.timezoneList);
String[] timezoneIds = TimeZone.getAvailableIDs();
String defaultTimeZone = TimeZone.getDefault().getID();
int selected = 0;
for(int i = 0; i < timezoneIds.length; i++) {
if(timezoneIds[i].equals(defaultTimeZone))
selected = i;
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, timezoneIds);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
timezoneList.setAdapter(adapter);
timezoneList.setSelection(selected);
Button signIn = (Button) findViewById(R.id.signIn);
signIn.setOnClickListener(new OnClickListener() {
@ -111,6 +130,7 @@ public class ProducteevLoginActivity extends Activity {
Editable password = passwordEditText.getText();
Editable firstName = ((EditText)findViewById(R.id.firstName)).getText();
Editable lastName = ((EditText)findViewById(R.id.lastName)).getText();
String timezone = timezoneList.getSelectedItem().toString();
if(email.length() == 0 || password.length() == 0 ||
firstName.length() == 0 ||
lastName.length() == 0) {
@ -119,7 +139,7 @@ public class ProducteevLoginActivity extends Activity {
return;
}
performSignup(email.toString(), password.toString(),
firstName.toString(), lastName.toString());
firstName.toString(), lastName.toString(), timezone);
}
}
});
@ -144,7 +164,7 @@ public class ProducteevLoginActivity extends Activity {
Preferences.setString(R.string.producteev_PPr_password, password);
ProducteevUtilities.INSTANCE.setToken(invoker.getToken());
FlurryAgent.onEvent("producteev-login");
FlurryAgent.onEvent("producteev-login"); //$NON-NLS-1$
synchronize();
} catch (ApiAuthenticationException e) {
@ -167,7 +187,7 @@ public class ProducteevLoginActivity extends Activity {
}
private void performSignup(final String email, final String password,
final String firstName, final String lastName) {
final String firstName, final String lastName, final String timezone) {
final ProgressDialog dialog = DialogUtilities.progressDialog(this,
getString(R.string.DLG_wait));
final TextView errors = (TextView) findViewById(R.id.error);
@ -178,7 +198,7 @@ public class ProducteevLoginActivity extends Activity {
ProducteevInvoker invoker = ProducteevSyncProvider.getInvoker();
final StringBuilder errorMessage = new StringBuilder();
try {
invoker.usersSignUp(email, firstName, lastName, password, null);
invoker.usersSignUp(email, firstName, lastName, password, timezone, null);
invoker.authenticate(email, password);
Preferences.setString(R.string.producteev_PPr_email, email);
@ -220,7 +240,10 @@ public class ProducteevLoginActivity extends Activity {
FlurryAgent.onStartSession(this, Constants.FLURRY_KEY);
}
private void onEn() {
@Override
protected void onStop() {
super.onStop();
FlurryAgent.onEndSession(this);
}
}

@ -86,12 +86,13 @@ public class ProducteevInvoker {
* Sign up as the given user
*/
public JSONObject usersSignUp(String email, String firstName, String lastName, String
password, Long fbUid) throws IOException, ApiServiceException {
password, String timezone, Long fbUid) throws IOException, ApiServiceException {
return invokeGet("users/signup.json",
"email", email,
"firstname", firstName,
"lastname", lastName,
"password", password,
"timezone", timezone,
"fbuid", fbUid);
}
@ -254,6 +255,19 @@ public class ProducteevInvoker {
"all_day", allDay);
}
/**
* unset a deadline
*
* @param idTask
*
* @return array tasks/view
*/
public JSONObject tasksUnsetDeadline(long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_deadline.json",
"token", token,
"id_task", idTask);
}
/**
* set a workspace
*

@ -527,8 +527,12 @@ public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer
invoker.tasksSetTitle(idTask, local.task.getValue(Task.TITLE));
if(shouldTransmit(local, Task.IMPORTANCE, remote))
invoker.tasksSetStar(idTask, createStars(local.task));
if(shouldTransmit(local, Task.DUE_DATE, remote) && local.task.hasDueDate()) // temporary can't unset deadline
if(shouldTransmit(local, Task.DUE_DATE, remote)) {
if(local.task.hasDueDate())
invoker.tasksSetDeadline(idTask, createDeadline(local.task), local.task.hasDueTime() ? 0 : 1);
else
invoker.tasksUnsetDeadline(idTask);
}
if(shouldTransmit(local, Task.COMPLETION_DATE, remote))
invoker.tasksSetStatus(idTask, local.task.isCompleted() ? 2 : 1);

@ -356,7 +356,7 @@ public final class ReminderService {
time = DateUtilities.now() + 5000L;
if(Constants.DEBUG)
Log.e("Astrid", "Alarm (" + task.getId() + ", " + type +
Log.e("Astrid", "Reminder (" + task.getId() + ", " + type +
") set for " + new Date(time));
am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="@drawable/pdv_body">
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -12,7 +12,6 @@
android:paddingBottom="5dip"
android:scaleType="fitCenter"
android:src="@drawable/pdv_logo" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -21,8 +20,8 @@
android:textSize="16sp"
android:textColor="#ffffff"
android:text="@string/producteev_PLA_body" />
<TextView android:id="@+id/error"
<TextView
android:id="@+id/error"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
@ -31,60 +30,67 @@
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
<EditText android:id="@+id/email"
<EditText
android:id="@+id/email"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_email"
android:contentDescription="E-Mail with which you registered to Producteev-service"
android:inputType="textEmailAddress"/>
<EditText android:id="@+id/password"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/password"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="Password for your Producteev account"
android:hint="@string/producteev_PLA_password"
android:inputType="textPassword"/>
<LinearLayout android:id="@+id/newUserLayout"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:inputType="textPassword" />
<LinearLayout
android:id="@+id/newUserLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="20dip"
android:visibility="gone">
<EditText android:id="@+id/firstName"
<EditText
android:id="@+id/firstName"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_firstName"
android:inputType="textPersonName"/>
<EditText android:id="@+id/lastName"
android:inputType="textPersonName" />
<EditText
android:id="@+id/lastName"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_lastName"
android:inputType="textPersonName"/>
android:inputType="textPersonName" />
<Spinner
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:id="@+id/timezoneList"
android:contentDescription="@string/producteev_PLA_timezone"
android:prompt="@string/producteev_PLA_timezone" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="5dip"
android:baselineAligned="false">
<Button android:id="@+id/signIn" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="1"
<Button
android:id="@+id/signIn"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/producteev_PLA_signIn" />
<Button android:id="@+id/createNew" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="1"
<Button
android:id="@+id/createNew"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/producteev_PLA_createNew" />
</LinearLayout>
<TextView android:id="@+id/terms"
<TextView
android:id="@+id/terms"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
@ -93,5 +99,4 @@
android:textColor="#0000ff"
android:linksClickable="true"
android:text="@string/producteev_PLA_terms" />
</LinearLayout>

@ -10,9 +10,6 @@
<!-- Task Edit Activity: Add New Alarm -->
<string name="alarm_ACS_button">Add an Alarm</string>
<!-- Task Detail for Alarms (%s -> time)-->
<string name="alarm_ADE_detail">Alarm %s</string>
<string-array name="reminders_alarm">
<!-- reminders related to alarm -->
<item>Alarm!</item>

@ -72,6 +72,9 @@
<!-- Password Label -->
<string name="producteev_PLA_password">Password</string>
<!-- Timezone Spinner -->
<string name="producteev_PLA_timezone">Timezone</string>
<!-- Confirm Password Label -->
<string name="producteev_PLA_confirmPassword">Confirm Password</string>

@ -106,6 +106,11 @@ public final class UpgradeService {
"If you liked the old version, you can also go back by " +
"<a href='http://bit.ly/oldastrid'>clicking here</a>",
});
if(from >= V3_3_0)
newVersionString(changeLog, "3.3.5 (9/27/10)", new String[] {
"Restored alarm functionality",
"Producteev: sync can now remove due dates in Producteev",
});
if(from >= V3_0_0 && from < V3_3_0)
newVersionString(changeLog, "3.3.0 (9/17/10)", new String[] {
"Fixed some RTM duplicated tasks issues",

1
tests/.gitignore vendored

@ -2,3 +2,4 @@ bin/
instrumented/
coverage.em
coverage/
reports/

@ -19,7 +19,7 @@
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="android.test.InstrumentationTestRunner"
<instrumentation android:name="com.zutubi.android.junitreport.JUnitReportTestRunner"
android:targetPackage="com.timsu.astrid"
android:label="Tests for Astrid"/>
</manifest>

@ -24,3 +24,8 @@ out.dir=bin
# because we pull in from multiple source folders
source.dir=${out.dir}/source
# test runner: junit report test runner
test.runner=com.zutubi.android.junitreport.JUnitReportTestRunner
# reports output directory
reports.dir=reports

@ -64,7 +64,14 @@
-->
<setup />
<target name="coverage">
<target name="coverage" depends="-prepare-coverage-source">
<antcall target="android-coverage">
<param name="source.dir" value="bin/source" />
</antcall>
<antcall target="pull-junit" />
</target>
<target name="-prepare-coverage-source">
<subant target="help">
<fileset file="${tested.project.absolute.dir}/build.xml" />
</subant>
@ -77,9 +84,78 @@
<copy todir="${source.dir}">
<fileset dir="src" />
</copy>
<antcall target="android_test_rules.coverage">
<param name="source.dir" value="bin/source" />
</antcall>
</target>
<target name="pull-junit">
<exec executable="${adb}" failonerror="true">
<arg line="${adb.device.arg}"/>
<arg value="pull" />
<arg value="/data/data/${tested.manifest.package}/files/junit-report.xml" />
<arg value="${reports.dir}/junit-report.xml" />
</exec>
</target>
<!-- clone of android coverage target with extra purposes:
1. generate emma xml report
2. use adb device arg to pull coverage file
-->
<target name="android-coverage" depends="-set-coverage-classpath, -install-instrumented, install"
description="Runs the tests against the instrumented code and generates
code coverage report">
<run-tests-helper emma.enabled="true">
<extra-instrument-args>
<arg value="-e" />
<arg value="coverageFile" />
<arg value="${emma.dump.file}" />
</extra-instrument-args>
</run-tests-helper>
<echo>Downloading coverage file into project directory...</echo>
<exec executable="${adb}" failonerror="true">
<arg line="${adb.device.arg}"/>
<arg value="pull" />
<arg value="${emma.dump.file}" />
<arg value="coverage.ec" />
</exec>
<echo>Extracting coverage report...</echo>
<mkdir dir="${reports.dir}" />
<emma>
<report sourcepath="${tested.project.absolute.dir}/${source.dir}"
verbosity="${verbosity}">
<!-- TODO: report.dir or something like should be introduced if necessary -->
<infileset dir=".">
<include name="coverage.ec" />
<include name="coverage.em" />
</infileset>
<!-- TODO: reports in other, indicated by user formats -->
<html outfile="${reports.dir}/coverage.html" encoding="UTF-8" />
<xml outfile="${reports.dir}/coverage.xml" />
</report>
</emma>
<echo>Cleaning up temporary files...</echo>
<delete dir="${instrumentation.absolute.dir}" />
<delete file="coverage.ec" />
<delete file="coverage.em" />
<echo>Saving the report file in ${basedir}/coverage/coverage.html</echo>
</target>
<macrodef name="run-tests-helper">
<attribute name="emma.enabled" default="false" />
<element name="extra-instrument-args" optional="yes" />
<sequential>
<echo>Running tests ...</echo>
<exec executable="${adb}" failonerror="true">
<arg line="${adb.device.arg}"/>
<arg value="shell" />
<arg value="am" />
<arg value="instrument" />
<arg value="-w" />
<arg value="-e" />
<arg value="coverage" />
<arg value="@{emma.enabled}" />
<extra-instrument-args />
<arg value="${manifest.package}/${test.runner}" />
</exec>
</sequential>
</macrodef>
</project>

@ -4,8 +4,10 @@ package com.todoroo.andlib.test;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import android.content.res.Configuration;
@ -265,11 +267,18 @@ abstract public class TranslationTests extends TodorooTestCase {
*/
public int[] getResourceIds(Class<?> resources) throws Exception {
Field[] fields = resources.getDeclaredFields();
int[] ids = new int[fields.length];
List<Integer> ids = new ArrayList<Integer>(fields.length);
for(int i = 0; i < fields.length; i++) {
ids[i] = fields[i].getInt(null);
try {
ids.add(fields[i].getInt(null));
} catch (Exception e) {
// not a field we care about
}
}
return ids;
int[] idsAsIntArray = new int[ids.size()];
for(int i = 0; i < ids.size(); i++)
idsAsIntArray[i] = ids.get(i);
return idsAsIntArray;
}
/**

@ -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);
}
}
Loading…
Cancel
Save