change package name to uiuios
This commit is contained in:
43
tests/src/com/android/uiuios/util/Condition.java
Normal file
43
tests/src/com/android/uiuios/util/Condition.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.android.uiuios.util;
|
||||
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
|
||||
import com.android.uiuios.MainThreadExecutor;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public interface Condition {
|
||||
|
||||
boolean isTrue() throws Throwable;
|
||||
|
||||
/**
|
||||
* Converts the condition to be run on UI thread.
|
||||
*/
|
||||
static Condition runOnUiThread(final Condition condition) {
|
||||
final MainThreadExecutor executor = new MainThreadExecutor();
|
||||
return () -> {
|
||||
final AtomicBoolean value = new AtomicBoolean(false);
|
||||
final Throwable[] exceptions = new Throwable[1];
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
value.set(condition.isTrue());
|
||||
} catch (Throwable e) {
|
||||
exceptions[0] = e;
|
||||
}
|
||||
|
||||
});
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
if (exceptions[0] != null) {
|
||||
throw exceptions[0];
|
||||
}
|
||||
return value.get();
|
||||
};
|
||||
}
|
||||
|
||||
static Condition minChildCount(final UiObject2 obj, final int childCount) {
|
||||
return () -> obj.getChildCount() >= childCount;
|
||||
}
|
||||
}
|
||||
172
tests/src/com/android/uiuios/util/LauncherLayoutBuilder.java
Normal file
172
tests/src/com/android/uiuios/util/LauncherLayoutBuilder.java
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (C) 2019 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.android.uiuios.util;
|
||||
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.util.Xml;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Helper class to build xml for Launcher Layout
|
||||
*/
|
||||
public class LauncherLayoutBuilder {
|
||||
|
||||
// Object Tags
|
||||
private static final String TAG_WORKSPACE = "workspace";
|
||||
private static final String TAG_AUTO_INSTALL = "autoinstall";
|
||||
private static final String TAG_FOLDER = "folder";
|
||||
private static final String TAG_APPWIDGET = "appwidget";
|
||||
private static final String TAG_EXTRA = "extra";
|
||||
|
||||
private static final String ATTR_CONTAINER = "container";
|
||||
private static final String ATTR_RANK = "rank";
|
||||
|
||||
private static final String ATTR_PACKAGE_NAME = "packageName";
|
||||
private static final String ATTR_CLASS_NAME = "className";
|
||||
private static final String ATTR_TITLE = "title";
|
||||
private static final String ATTR_SCREEN = "screen";
|
||||
|
||||
// x and y can be specified as negative integers, in which case -1 represents the
|
||||
// last row / column, -2 represents the second last, and so on.
|
||||
private static final String ATTR_X = "x";
|
||||
private static final String ATTR_Y = "y";
|
||||
private static final String ATTR_SPAN_X = "spanX";
|
||||
private static final String ATTR_SPAN_Y = "spanY";
|
||||
|
||||
private static final String ATTR_CHILDREN = "children";
|
||||
|
||||
|
||||
// Style attrs -- "Extra"
|
||||
private static final String ATTR_KEY = "key";
|
||||
private static final String ATTR_VALUE = "value";
|
||||
|
||||
private static final String CONTAINER_DESKTOP = "desktop";
|
||||
private static final String CONTAINER_HOTSEAT = "hotseat";
|
||||
|
||||
private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
|
||||
|
||||
public Location atHotseat(int rank) {
|
||||
Location l = new Location();
|
||||
l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
|
||||
l.items.put(ATTR_RANK, Integer.toString(rank));
|
||||
return l;
|
||||
}
|
||||
|
||||
public Location atWorkspace(int x, int y, int screen) {
|
||||
Location l = new Location();
|
||||
l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
|
||||
l.items.put(ATTR_X, Integer.toString(x));
|
||||
l.items.put(ATTR_Y, Integer.toString(y));
|
||||
l.items.put(ATTR_SCREEN, Integer.toString(screen));
|
||||
return l;
|
||||
}
|
||||
|
||||
public String build() throws IOException {
|
||||
StringWriter writer = new StringWriter();
|
||||
build(writer);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
public void build(Writer writer) throws IOException {
|
||||
XmlSerializer serializer = Xml.newSerializer();
|
||||
serializer.setOutput(writer);
|
||||
|
||||
serializer.startDocument("UTF-8", true);
|
||||
serializer.startTag(null, TAG_WORKSPACE);
|
||||
writeNodes(serializer, mNodes);
|
||||
serializer.endTag(null, TAG_WORKSPACE);
|
||||
serializer.endDocument();
|
||||
serializer.flush();
|
||||
}
|
||||
|
||||
private static void writeNodes(XmlSerializer serializer,
|
||||
ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
|
||||
for (Pair<String, HashMap<String, Object>> node : nodes) {
|
||||
ArrayList<Pair<String, HashMap<String, Object>>> children = null;
|
||||
|
||||
serializer.startTag(null, node.first);
|
||||
for (Map.Entry<String, Object> attr : node.second.entrySet()) {
|
||||
if (ATTR_CHILDREN.equals(attr.getKey())) {
|
||||
children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
|
||||
} else {
|
||||
serializer.attribute(null, attr.getKey(), (String) attr.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (children != null) {
|
||||
writeNodes(serializer, children);
|
||||
}
|
||||
serializer.endTag(null, node.first);
|
||||
}
|
||||
}
|
||||
|
||||
public class Location {
|
||||
|
||||
final HashMap<String, Object> items = new HashMap<>();
|
||||
|
||||
public LauncherLayoutBuilder putApp(String packageName, String className) {
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
|
||||
mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
|
||||
public LauncherLayoutBuilder putWidget(String packageName, String className,
|
||||
int spanX, int spanY) {
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, className);
|
||||
items.put(ATTR_SPAN_X, Integer.toString(spanX));
|
||||
items.put(ATTR_SPAN_Y, Integer.toString(spanY));
|
||||
mNodes.add(Pair.create(TAG_APPWIDGET, items));
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
|
||||
public FolderBuilder putFolder(int titleResId) {
|
||||
FolderBuilder folderBuilder = new FolderBuilder();
|
||||
items.put(ATTR_TITLE, Integer.toString(titleResId));
|
||||
items.put(ATTR_CHILDREN, folderBuilder.mChildren);
|
||||
mNodes.add(Pair.create(TAG_FOLDER, items));
|
||||
return folderBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
public class FolderBuilder {
|
||||
|
||||
final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
|
||||
|
||||
public FolderBuilder addApp(String packageName, String className) {
|
||||
HashMap<String, Object> items = new HashMap<>();
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
|
||||
mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
|
||||
return this;
|
||||
}
|
||||
|
||||
public LauncherLayoutBuilder build() {
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
488
tests/src/com/android/uiuios/util/RaceConditionReproducer.java
Normal file
488
tests/src/com/android/uiuios/util/RaceConditionReproducer.java
Normal file
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.android.uiuios.util;
|
||||
|
||||
import static com.android.uiuios.util.RaceConditionTracker.ENTER_POSTFIX;
|
||||
import static com.android.uiuios.util.RaceConditionTracker.EXIT_POSTFIX;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Event processor for reliably reproducing multithreaded apps race conditions in tests.
|
||||
*
|
||||
* The app notifies us about “events” that happen in its threads. The race condition test runs the
|
||||
* test action multiple times (aka iterations), trying to generate all possible permutations of
|
||||
* these events. It keeps a set of all seen event sequences and steers the execution towards
|
||||
* executing events in previously unseen order. It does it by postponing execution of threads that
|
||||
* would lead to an already seen sequence.
|
||||
*
|
||||
* If an event A occurs before event B in the sequence, this is how execution order looks like:
|
||||
* Events: ... A ... B ...
|
||||
* Events and instructions, guaranteed order:
|
||||
* (instructions executed prior to A) A ... B (instructions executed after B)
|
||||
*
|
||||
* Each iteration has 3 parts (phases).
|
||||
* Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen
|
||||
* continuations. Reproducing this sequence by pausing threads that would lead to other sequences.
|
||||
* Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need
|
||||
* one new event after that sequence. All threads leading to seen continuations will be postponed
|
||||
* for some short period of time. The phase ends once the new event is registered, or after the
|
||||
* period of time ends (in which case we declare that the sequence can’t have new continuations).
|
||||
* Phase 3. Releasing all threads and letting the test iteration run till its end.
|
||||
*
|
||||
* The iterations end when all seen paths have been declared “uncontinuable”.
|
||||
*
|
||||
* When we register event XXX:enter, we hold all other events until we register XXX:exit.
|
||||
*/
|
||||
public class RaceConditionReproducer implements RaceConditionTracker.EventProcessor {
|
||||
private static final String TAG = "RaceConditionReproducer";
|
||||
private static final long SHORT_TIMEOUT_MS = 2000;
|
||||
private static final long LONG_TIMEOUT_MS = 60000;
|
||||
// Handler used to resume postponed events.
|
||||
private static final Handler POSTPONED_EVENT_RESUME_HANDLER = createEventResumeHandler();
|
||||
|
||||
private static Handler createEventResumeHandler() {
|
||||
final HandlerThread thread = new HandlerThread("RaceConditionEventResumer");
|
||||
thread.start();
|
||||
return new Handler(thread.getLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Event in a particular sequence of events. A node in the prefix tree of all seen event
|
||||
* sequences.
|
||||
*/
|
||||
private class EventNode {
|
||||
// Events that were seen just after this event.
|
||||
private final Map<String, EventNode> mNextEvents = new HashMap<>();
|
||||
// Whether we believe that further iterations will not be able to add more events to
|
||||
// mNextEvents.
|
||||
private boolean mStoppedAddingChildren = true;
|
||||
|
||||
private void debugDump(StringBuilder sb, int indent, String name) {
|
||||
for (int i = 0; i < indent; ++i) sb.append('.');
|
||||
sb.append(!mStoppedAddingChildren ? "+" : "-");
|
||||
sb.append(" : ");
|
||||
sb.append(name);
|
||||
if (mLastRegisteredEvent == this) sb.append(" <");
|
||||
sb.append('\n');
|
||||
|
||||
for (String key : mNextEvents.keySet()) {
|
||||
mNextEvents.get(key).debugDump(sb, indent + 2, key);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of leaves in the subtree with this node as a root. */
|
||||
private int numberOfLeafNodes() {
|
||||
if (mNextEvents.isEmpty()) return 1;
|
||||
|
||||
int leaves = 0;
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
leaves += mNextEvents.get(event).numberOfLeafNodes();
|
||||
}
|
||||
return leaves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we believe that further iterations will not be able add nodes to the subtree with
|
||||
* this node as a root.
|
||||
*/
|
||||
private boolean stoppedAddingChildrenToTree() {
|
||||
if (!mStoppedAddingChildren) return false;
|
||||
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the subtree with this node as a root, tries finding a node where we may have a
|
||||
* chance to add new children.
|
||||
* If succeeds, returns true and fills 'path' with the sequence of events to that node;
|
||||
* otherwise returns false.
|
||||
*/
|
||||
private boolean populatePathToGrowthPoint(List<String> path) {
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
if (mNextEvents.get(event).populatePathToGrowthPoint(path)) {
|
||||
path.add(0, event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!mStoppedAddingChildren) {
|
||||
// Mark that we have finished adding children. It will remain true if no new
|
||||
// children are added, or will be set to false upon adding a new child.
|
||||
mStoppedAddingChildren = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Starting point of all event sequences; the root of the prefix tree representation all
|
||||
// sequences generated by test iterations. A test iteration can add nodes int it.
|
||||
private EventNode mRoot = new EventNode();
|
||||
// During a test iteration, the last event that was registered.
|
||||
private EventNode mLastRegisteredEvent;
|
||||
// Length of the current sequence of registered events for the current test iteration.
|
||||
private int mRegisteredEventCount = 0;
|
||||
// During the first part of a test iteration, we go to a specific node under mRoot by
|
||||
// 'playing back' mSequenceToFollow. During this part, all events that don't belong to this
|
||||
// sequence get postponed.
|
||||
private List<String> mSequenceToFollow = new ArrayList<>();
|
||||
// Collection of events that got postponed, with corresponding wait objects used to let them go.
|
||||
private Map<String, Semaphore> mPostponedEvents = new HashMap<>();
|
||||
// Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently
|
||||
// postponed events.
|
||||
private Runnable mResumeAllEventsCallback;
|
||||
// String representation of the sequence of events registered so far for the current test
|
||||
// iteration. After registering any event, we output it to the log. The last output before
|
||||
// the test failure can be later played back to reliable reproduce the exact sequence of
|
||||
// events that broke the test.
|
||||
// Format: EV1|EV2|...\EVN
|
||||
private StringBuilder mCurrentSequence;
|
||||
// When not null, we are in a repro mode. We run only one test iteration, and are trying to
|
||||
// reproduce the event sequence represented by this string. The format is same as for
|
||||
// mCurrentSequence.
|
||||
private final String mReproString;
|
||||
|
||||
/* Constructor for a normal test. */
|
||||
public RaceConditionReproducer() {
|
||||
mReproString = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for reliably reproducing a race condition failure. The developer should find in
|
||||
* the log the latest "Repro sequence:" record and locally modify the test by passing that
|
||||
* string to the constructor. Running the test will have only one iteration that will reliably
|
||||
* "play back" that sequence.
|
||||
*/
|
||||
public RaceConditionReproducer(String reproString) {
|
||||
mReproString = reproString;
|
||||
}
|
||||
|
||||
public RaceConditionReproducer(String... reproSequence) {
|
||||
this(String.join("|", reproSequence));
|
||||
}
|
||||
|
||||
public synchronized String getCurrentSequenceString() {
|
||||
return mCurrentSequence.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this
|
||||
* call will be ignored.
|
||||
*/
|
||||
public synchronized void startIteration() {
|
||||
mLastRegisteredEvent = mRoot;
|
||||
mRegisteredEventCount = 0;
|
||||
mCurrentSequence = new StringBuilder();
|
||||
Log.d(TAG, "Repro sequence: " + mCurrentSequence);
|
||||
mSequenceToFollow = mReproString != null ?
|
||||
parseReproString(mReproString) : generateSequenceToFollowLocked();
|
||||
Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
|
||||
checkIfCompletedSequenceToFollowLocked();
|
||||
RaceConditionTracker.setEventProcessor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call
|
||||
* will be ignored.
|
||||
* Returns whether we need more iterations.
|
||||
*/
|
||||
public synchronized boolean finishIteration() {
|
||||
RaceConditionTracker.setEventProcessor(null);
|
||||
runResumeAllEventsCallbackLocked();
|
||||
assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
|
||||
assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
|
||||
|
||||
// No events came after mLastRegisteredEvent. It doesn't make sense to come to it again
|
||||
// because we won't see new continuations.
|
||||
mLastRegisteredEvent.mStoppedAddingChildren = true;
|
||||
Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked());
|
||||
if (mReproString != null) {
|
||||
assertTrue("Repro mode: failed to reproduce the sequence",
|
||||
mCurrentSequence.toString().startsWith(mReproString));
|
||||
}
|
||||
// If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree
|
||||
// has prospective growth points.
|
||||
return mReproString == null && !mRoot.stoppedAddingChildrenToTree();
|
||||
}
|
||||
|
||||
private static List<String> parseReproString(String reproString) {
|
||||
return Arrays.asList(reproString.split("\\|"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the app issues an event.
|
||||
*/
|
||||
@Override
|
||||
public void onEvent(String event) {
|
||||
final Semaphore waitObject = tryRegisterEvent(event);
|
||||
if (waitObject != null) {
|
||||
waitUntilCanRegister(event, waitObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit.
|
||||
*/
|
||||
private boolean canRegisterEventNowLocked(String event) {
|
||||
final String lastEventAsEnter = lastEventAsEnter();
|
||||
final String thisEventAsExit = eventAsExit(event);
|
||||
|
||||
if (lastEventAsEnter != null) {
|
||||
if (!lastEventAsEnter.equals(thisEventAsExit)) {
|
||||
assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null);
|
||||
// Last event was :enter, but this event is not :exit.
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Previous event was not :enter.
|
||||
assertTrue(":exit after a non-enter event", thisEventAsExit == null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event issued by the app and returns null or decides that the event must be
|
||||
* postponed, and returns an object to wait on.
|
||||
*/
|
||||
private synchronized Semaphore tryRegisterEvent(String event) {
|
||||
Log.d(TAG, "Event issued by the app: " + event);
|
||||
|
||||
if (!canRegisterEventNowLocked(event)) {
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
|
||||
if (mRegisteredEventCount < mSequenceToFollow.size()) {
|
||||
// We are in the first part of the iteration. We only register events that follow the
|
||||
// mSequenceToFollow and postponing all other events.
|
||||
if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) {
|
||||
// The event is the next one expected in the sequence. Register it.
|
||||
registerEventLocked(event);
|
||||
|
||||
// If there are postponed events that could continue the sequence, register them.
|
||||
while (mRegisteredEventCount < mSequenceToFollow.size() &&
|
||||
mPostponedEvents.containsKey(
|
||||
mSequenceToFollow.get(mRegisteredEventCount))) {
|
||||
registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount));
|
||||
}
|
||||
|
||||
// Perhaps we just completed the required sequence...
|
||||
checkIfCompletedSequenceToFollowLocked();
|
||||
} else {
|
||||
// The event is not the next one in the sequence. Postpone it.
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
} else if (mRegisteredEventCount == mSequenceToFollow.size()) {
|
||||
// The second phase of the iteration. We have just registered the whole
|
||||
// mSequenceToFollow, and want to add previously not seen continuations for the last
|
||||
// node in the sequence aka 'growth point'.
|
||||
if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) {
|
||||
// The event was never seen as a continuation for the current node.
|
||||
// Or we are in repro mode, in which case we are not in business of generating
|
||||
// new sequences after we've played back the required sequence.
|
||||
// Register it immediately.
|
||||
registerEventLocked(event);
|
||||
} else {
|
||||
// The event was seen as a continuation for the current node. Postpone it, hoping
|
||||
// that a new event will come from other threads.
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
} else {
|
||||
// The third phase of the iteration. We are past the growth point and register
|
||||
// everything that comes.
|
||||
registerEventLocked(event);
|
||||
// Register events that may have been postponed while waiting for an :exit event
|
||||
// during the third phase. We don't do this if just registered event is :enter.
|
||||
if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) {
|
||||
registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Called when there are chances that we just have registered the whole mSequenceToFollow. */
|
||||
private void checkIfCompletedSequenceToFollowLocked() {
|
||||
if (mRegisteredEventCount == mSequenceToFollow.size()) {
|
||||
// We just entered the second phase of the iteration. We have just registered the
|
||||
// whole mSequenceToFollow, and want to add previously not seen continuations for the
|
||||
// last node in the sequence aka 'growth point'. All seen continuations will be
|
||||
// postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go.
|
||||
scheduleResumeAllEventsLocked();
|
||||
|
||||
// Among the events that were postponed during the first stage, there may be an event
|
||||
// that wasn't seen after the current. If so, register it immediately because this
|
||||
// creates a new sequence.
|
||||
final Set<String> keys = new HashSet<>(mPostponedEvents.keySet());
|
||||
keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet());
|
||||
if (!keys.isEmpty()) {
|
||||
registerPostponedEventLocked(keys.iterator().next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Semaphore createWaitObjectForPostponedEventLocked(String event) {
|
||||
final Semaphore waitObject = new Semaphore(0);
|
||||
assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event));
|
||||
mPostponedEvents.put(event, waitObject);
|
||||
return waitObject;
|
||||
}
|
||||
|
||||
private void waitUntilCanRegister(String event, Semaphore waitObject) {
|
||||
try {
|
||||
assertTrue("Never registered event: " + event,
|
||||
waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
fail("Wait was interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
/** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */
|
||||
private void scheduleResumeAllEventsLocked() {
|
||||
assertTrue(mResumeAllEventsCallback == null);
|
||||
mResumeAllEventsCallback = this::allEventsResumeCallback;
|
||||
POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private synchronized void allEventsResumeCallback() {
|
||||
assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null);
|
||||
mResumeAllEventsCallback = null;
|
||||
registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
|
||||
}
|
||||
|
||||
private void registerPostponedEventsLocked(Collection<String> events) {
|
||||
for (String event : events) {
|
||||
registerPostponedEventLocked(event);
|
||||
if (eventAsEnter(event) != null) {
|
||||
// Once :enter is registered, switch to waiting for :exit to come. Won't register
|
||||
// other postponed events.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPostponedEventLocked(String event) {
|
||||
mPostponedEvents.remove(event).release();
|
||||
registerEventLocked(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the last registered event was XXX:enter, returns XXX, otherwise, null.
|
||||
*/
|
||||
private String lastEventAsEnter() {
|
||||
return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:postfix, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String prefixFromPostfixedEvent(String event, String postfix) {
|
||||
final int columnPos = event.indexOf(':');
|
||||
if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) {
|
||||
return event.substring(0, columnPos);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:enter, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String eventAsEnter(String event) {
|
||||
return prefixFromPostfixedEvent(event, ENTER_POSTFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:exit, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String eventAsExit(String event) {
|
||||
return prefixFromPostfixedEvent(event, EXIT_POSTFIX);
|
||||
}
|
||||
|
||||
private void registerEventLocked(String event) {
|
||||
assertTrue(canRegisterEventNowLocked(event));
|
||||
|
||||
Log.d(TAG, "Actually registering event: " + event);
|
||||
EventNode next = mLastRegisteredEvent.mNextEvents.get(event);
|
||||
if (next == null) {
|
||||
// This event wasn't seen after mLastRegisteredEvent.
|
||||
next = new EventNode();
|
||||
mLastRegisteredEvent.mNextEvents.put(event, next);
|
||||
// The fact that we've added a new event after the previous one means that the
|
||||
// previous event is still a growth point, unless this event is :exit, which means
|
||||
// that the previous event is :enter.
|
||||
mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null;
|
||||
}
|
||||
|
||||
mLastRegisteredEvent = next;
|
||||
mRegisteredEventCount++;
|
||||
|
||||
if (mCurrentSequence.length() > 0) mCurrentSequence.append("|");
|
||||
mCurrentSequence.append(event);
|
||||
Log.d(TAG, "Repro sequence: " + mCurrentSequence);
|
||||
}
|
||||
|
||||
private void runResumeAllEventsCallbackLocked() {
|
||||
if (mResumeAllEventsCallback != null) {
|
||||
POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback);
|
||||
mResumeAllEventsCallback.run();
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence dumpStateLocked() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Sequence to follow: ");
|
||||
for (String event : mSequenceToFollow) sb.append(" " + event);
|
||||
sb.append(".\n");
|
||||
sb.append("Registered event count: " + mRegisteredEventCount);
|
||||
|
||||
sb.append("\nPostponed events: ");
|
||||
for (String event : mPostponedEvents.keySet()) sb.append(" " + event);
|
||||
sb.append(".");
|
||||
|
||||
sb.append("\nNodes: \n");
|
||||
mRoot.debugDump(sb, 0, "");
|
||||
return sb;
|
||||
}
|
||||
|
||||
public int numberOfLeafNodes() {
|
||||
return mRoot.numberOfLeafNodes();
|
||||
}
|
||||
|
||||
private List<String> generateSequenceToFollowLocked() {
|
||||
ArrayList<String> sequence = new ArrayList<>();
|
||||
mRoot.populatePathToGrowthPoint(sequence);
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.android.uiuios.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RaceConditionReproducerTest {
|
||||
private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3";
|
||||
|
||||
private static int factorial(int n) {
|
||||
int res = 1;
|
||||
for (int i = 2; i <= n; ++i) res *= i;
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void run3_3_TestAction() throws InterruptedException {
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
RaceConditionTracker.onEvent("B3");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2");
|
||||
RaceConditionTracker.onEvent("A3");
|
||||
|
||||
tb.join();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads, 3 events each.
|
||||
public void test3_3() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
boolean sawTheValidSequence = false;
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
run3_3_TestAction();
|
||||
final boolean needMoreIterations = eventProcessor.finishIteration();
|
||||
|
||||
sawTheValidSequence = sawTheValidSequence ||
|
||||
SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString());
|
||||
|
||||
if (!needMoreIterations) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(3 + 3) / (factorial(3) * factorial(3)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
assertTrue(sawTheValidSequence);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads, 3 events, including enter-exit pairs each.
|
||||
public void test3_3_enter_exit() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
boolean sawTheValidSequence = false;
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1:enter");
|
||||
RaceConditionTracker.onEvent("B1:exit");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
RaceConditionTracker.onEvent("B3:enter");
|
||||
RaceConditionTracker.onEvent("B3:exit");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2:enter");
|
||||
RaceConditionTracker.onEvent("A2:exit");
|
||||
RaceConditionTracker.onEvent("A3:enter");
|
||||
RaceConditionTracker.onEvent("A3:exit");
|
||||
|
||||
tb.join();
|
||||
final boolean needMoreIterations = eventProcessor.finishIteration();
|
||||
|
||||
sawTheValidSequence = sawTheValidSequence ||
|
||||
"B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit".
|
||||
equals(eventProcessor.getCurrentSequenceString());
|
||||
|
||||
if (!needMoreIterations) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(3 + 3) / (factorial(3) * factorial(3)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
assertTrue(sawTheValidSequence);
|
||||
}
|
||||
|
||||
@Test
|
||||
// 2 threads, 3 events each; reproducing a particular event sequence.
|
||||
public void test3_3_ReproMode() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(
|
||||
SOME_VALID_SEQUENCE_3_3);
|
||||
|
||||
eventProcessor.startIteration();
|
||||
run3_3_TestAction();
|
||||
assertTrue(!eventProcessor.finishIteration());
|
||||
assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString());
|
||||
|
||||
assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads with 2 events; 1 thread with 1 event.
|
||||
public void test2_1_2() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
Thread tc = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("C1");
|
||||
});
|
||||
tc.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2");
|
||||
|
||||
tb.join();
|
||||
tc.join();
|
||||
|
||||
if (!eventProcessor.finishIteration()) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
|
||||
public void test2_1_2_enter_exit() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1:enter");
|
||||
RaceConditionTracker.onEvent("B1:exit");
|
||||
RaceConditionTracker.onEvent("B2:enter");
|
||||
RaceConditionTracker.onEvent("B2:exit");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
Thread tc = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("C1:enter");
|
||||
RaceConditionTracker.onEvent("C1:exit");
|
||||
});
|
||||
tc.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1:enter");
|
||||
RaceConditionTracker.onEvent("A1:exit");
|
||||
RaceConditionTracker.onEvent("A2:enter");
|
||||
RaceConditionTracker.onEvent("A2:exit");
|
||||
|
||||
tb.join();
|
||||
tc.join();
|
||||
|
||||
if (!eventProcessor.finishIteration()) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
}
|
||||
58
tests/src/com/android/uiuios/util/TestUtil.java
Normal file
58
tests/src/com/android/uiuios/util/TestUtil.java
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.android.uiuios.util;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getContext;
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class TestUtil {
|
||||
public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
|
||||
|
||||
public static void installDummyApp() throws IOException {
|
||||
// Copy apk from resources to a local file and install from there.
|
||||
final Resources resources = getContext().getResources();
|
||||
final InputStream in = resources.openRawResource(
|
||||
resources.getIdentifier("aardwolf_dummy_app",
|
||||
"raw", getContext().getPackageName()));
|
||||
final String apkFilename = getInstrumentation().getTargetContext().
|
||||
getFilesDir().getPath() + "/dummy_app.apk";
|
||||
|
||||
final FileOutputStream out = new FileOutputStream(apkFilename);
|
||||
byte[] buff = new byte[1024];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buff)) > 0) {
|
||||
out.write(buff, 0, read);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand("pm install " + apkFilename);
|
||||
}
|
||||
|
||||
public static void uninstallDummyApp() throws IOException {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
|
||||
"pm uninstall " + DUMMY_PACKAGE);
|
||||
}
|
||||
}
|
||||
41
tests/src/com/android/uiuios/util/Wait.java
Normal file
41
tests/src/com/android/uiuios/util/Wait.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.android.uiuios.util;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
* A utility class for waiting for a condition to be true.
|
||||
*/
|
||||
public class Wait {
|
||||
|
||||
private static final long DEFAULT_SLEEP_MS = 200;
|
||||
|
||||
public static void atMost(String message, Condition condition, long timeout) {
|
||||
atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
|
||||
}
|
||||
|
||||
public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
|
||||
long endTime = SystemClock.uptimeMillis() + timeout;
|
||||
while (SystemClock.uptimeMillis() < endTime) {
|
||||
try {
|
||||
if (condition.isTrue()) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
SystemClock.sleep(sleepMillis);
|
||||
}
|
||||
|
||||
// Check once more before returning false.
|
||||
try {
|
||||
if (condition.isTrue()) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
Assert.fail(message);
|
||||
}
|
||||
}
|
||||
59
tests/src/com/android/uiuios/util/rule/FailureWatcher.java
Normal file
59
tests/src/com/android/uiuios/util/rule/FailureWatcher.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.android.uiuios.util.rule;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FailureWatcher extends TestWatcher {
|
||||
private static final String TAG = "FailureWatcher";
|
||||
private static int sScreenshotCount = 0;
|
||||
final private UiDevice mDevice;
|
||||
|
||||
public FailureWatcher(UiDevice device) {
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
private void dumpViewHierarchy() {
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
try {
|
||||
mDevice.dumpWindowHierarchy(stream);
|
||||
stream.flush();
|
||||
stream.close();
|
||||
for (String line : stream.toString().split("\\r?\\n")) {
|
||||
Log.e(TAG, line.trim());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "error dumping XML to logcat", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed(Throwable e, Description description) {
|
||||
if (mDevice == null) return;
|
||||
final String pathname = getInstrumentation().getTargetContext().
|
||||
getFilesDir().getPath() + "/TaplTestScreenshot" + sScreenshotCount++ + ".png";
|
||||
Log.e(TAG, "Failed test " + description.getMethodName() +
|
||||
", screenshot will be saved to " + pathname +
|
||||
", track trace is below, UI object dump is further below:\n" +
|
||||
Log.getStackTraceString(e));
|
||||
dumpViewHierarchy();
|
||||
|
||||
try {
|
||||
final String dumpsysResult = mDevice.executeShellCommand(
|
||||
"dumpsys activity service TouchInteractionService");
|
||||
Log.d(TAG, "TouchInteractionService: " + dumpsysResult);
|
||||
} catch (IOException ex) {
|
||||
}
|
||||
|
||||
mDevice.takeScreenshot(new File(pathname));
|
||||
}
|
||||
}
|
||||
124
tests/src/com/android/uiuios/util/rule/LauncherActivityRule.java
Normal file
124
tests/src/com/android/uiuios/util/rule/LauncherActivityRule.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.android.uiuios.util.rule;
|
||||
|
||||
import static com.android.uiuios.tapl.TestHelpers.getHomeIntentInPackage;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.os.Bundle;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import com.android.uiuios.Launcher;
|
||||
import com.android.uiuios.Workspace.ItemOperator;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Test rule to get the current Launcher activity.
|
||||
*/
|
||||
public class LauncherActivityRule implements TestRule {
|
||||
|
||||
private Launcher mActivity;
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new MyStatement(base);
|
||||
}
|
||||
|
||||
public Launcher getActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
public Callable<Boolean> itemExists(final ItemOperator op) {
|
||||
return new Callable<Boolean>() {
|
||||
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
Launcher launcher = getActivity();
|
||||
if (launcher == null) {
|
||||
return false;
|
||||
}
|
||||
return launcher.getWorkspace().getFirstMatch(op) != null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the launcher activity in the target package.
|
||||
*/
|
||||
public void startLauncher() {
|
||||
getInstrumentation().startActivitySync(getHomeIntentInPackage(getTargetContext()));
|
||||
}
|
||||
|
||||
private class MyStatement extends Statement implements ActivityLifecycleCallbacks {
|
||||
|
||||
private final Statement mBase;
|
||||
|
||||
public MyStatement(Statement base) {
|
||||
mBase = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
Application app = (Application)
|
||||
InstrumentationRegistry.getTargetContext().getApplicationContext();
|
||||
app.registerActivityLifecycleCallbacks(this);
|
||||
try {
|
||||
mBase.evaluate();
|
||||
} finally {
|
||||
app.unregisterActivityLifecycleCallbacks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle bundle) {
|
||||
if (activity instanceof Launcher) {
|
||||
mActivity = (Launcher) activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
if (activity == mActivity) {
|
||||
mActivity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
tests/src/com/android/uiuios/util/rule/ShellCommandRule.java
Normal file
90
tests/src/com/android/uiuios/util/rule/ShellCommandRule.java
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.android.uiuios.util.rule;
|
||||
|
||||
import static com.android.uiuios.tapl.TestHelpers.getLauncherInMyProcess;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.ActivityInfo;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
/**
|
||||
* Test rule which executes a shell command at the start of the test.
|
||||
*/
|
||||
public class ShellCommandRule implements TestRule {
|
||||
|
||||
private final String mCmd;
|
||||
private final String mRevertCommand;
|
||||
|
||||
public ShellCommandRule(String cmd, @Nullable String revertCommand) {
|
||||
mCmd = cmd;
|
||||
mRevertCommand = revertCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
|
||||
try {
|
||||
base.evaluate();
|
||||
} finally {
|
||||
if (mRevertCommand != null) {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(mRevertCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the launcher permission to bind widgets.
|
||||
*/
|
||||
public static ShellCommandRule grantWidgetBind() {
|
||||
return new ShellCommandRule("appwidget grantbind --package "
|
||||
+ InstrumentationRegistry.getTargetContext().getPackageName(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target launcher as default launcher.
|
||||
*/
|
||||
public static ShellCommandRule setDefaultLauncher() {
|
||||
return new ShellCommandRule(getLauncherCommand(getLauncherInMyProcess()), null);
|
||||
}
|
||||
|
||||
public static String getLauncherCommand(ActivityInfo launcher) {
|
||||
return "cmd package set-home-activity " +
|
||||
new ComponentName(launcher.packageName, launcher.name).flattenToString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables heads up notification for the duration of the test
|
||||
*/
|
||||
public static ShellCommandRule disableHeadsUpNotification() {
|
||||
return new ShellCommandRule("settings put global heads_up_notifications_enabled 0",
|
||||
"settings put global heads_up_notifications_enabled 1");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user