Merge "Adding unit test to ReorderAlgorithm" into udc-dev

This commit is contained in:
Sebastián Franco
2023-05-03 20:13:15 +00:00
committed by Android (Google) Code Review
6 changed files with 2306 additions and 5 deletions

View File

@@ -85,6 +85,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
public class CellLayout extends ViewGroup {
@@ -891,7 +892,7 @@ public class CellLayout extends ViewGroup {
*
* @param result Array of 2 ints to hold the x and y coordinate of the point
*/
void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
public void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
cellToRect(cellX, cellY, spanX, spanY, mTempRect);
result[0] = mTempRect.centerX();
result[1] = mTempRect.centerY();
@@ -2340,7 +2341,16 @@ public class CellLayout extends ViewGroup {
}
Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
Rect r1 = new Rect();
for (View child: solution.map.keySet()) {
// The views need to be sorted so that the results are deterministic on the views positions
// and not by the views hash which is "random".
// The views are sorted twice, once for the X position and a second time for the Y position
// to ensure same order everytime.
Comparator comparator = Comparator.comparing(view ->
((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellX())
.thenComparing(view ->
((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellY());
List<View> views = solution.map.keySet().stream().sorted(comparator).toList();
for (View child : views) {
if (child == ignoreView) continue;
CellAndSpan c = solution.map.get(child);
CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();

File diff suppressed because it is too large Load Diff

View File

@@ -171,6 +171,8 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}
}
private HashSet<Character> mUsedWidgetTypes = new HashSet<>();
static final int INFINITE = 99999;
char[][] mWidget = new char[30][30];
@@ -182,6 +184,8 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
WidgetRect mMain = null;
int mWidth, mHeight;
CellLayoutBoard() {
for (int x = 0; x < mWidget.length; x++) {
for (int y = 0; y < mWidget[0].length; y++) {
@@ -190,6 +194,17 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}
}
CellLayoutBoard(int width, int height) {
mWidget = new char[width][height];
this.mWidth = width;
this.mHeight = height;
for (int x = 0; x < mWidget.length; x++) {
for (int y = 0; y < mWidget[0].length; y++) {
mWidget[x][y] = CellType.EMPTY;
}
}
}
public List<WidgetRect> getWidgets() {
return mWidgetsRects;
}
@@ -256,6 +271,16 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}).collect(Collectors.toList());
}
private char getNextWidgetType() {
for (char type = 'a'; type <= 'z'; type++) {
if (type == 'i') continue;
if (mUsedWidgetTypes.contains(type)) continue;
mUsedWidgetTypes.add(type);
return type;
}
return 'z';
}
public void addWidget(int x, int y, int spanX, int spanY, char type) {
Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y);
removeOverlappingItems(rect);
@@ -268,6 +293,10 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}
}
public void addWidget(int x, int y, int spanX, int spanY) {
addWidget(x, y, spanX, spanY, getNextWidgetType());
}
public void addIcon(int x, int y) {
Point iconCoord = new Point(x, y);
removeOverlappingItems(iconCoord);
@@ -367,6 +396,8 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}
}
}
board.mHeight = lines.length;
board.mWidth = lines[0].length();
board.mWidgetsRects = getRects(board.mWidget);
board.mWidgetsRects.forEach(widgetRect -> {
if (widgetRect.mType == CellType.MAIN_WIDGET) {
@@ -380,6 +411,11 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
public String toString(int maxX, int maxY) {
StringBuilder s = new StringBuilder();
s.append("board: ");
s.append(maxX);
s.append("x");
s.append(maxY);
s.append("\n");
maxX = Math.min(maxX, mWidget.length);
maxY = Math.min(maxY, mWidget[0].length);
for (int y = 0; y < maxY; y++) {
@@ -391,6 +427,11 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
return s.toString();
}
@Override
public String toString() {
return toString(mWidth, mHeight);
}
public static List<CellLayoutBoard> boardListFromString(String boardsStr) {
String[] lines = boardsStr.split("\n");
ArrayList<String> individualBoards = new ArrayList<>();
@@ -410,4 +451,12 @@ public class CellLayoutBoard implements Comparable<CellLayoutBoard> {
}
return boards;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
}

View File

@@ -24,12 +24,12 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import java.util.ArrayList;
import java.util.List;
public class CellLayoutTestUtils {
public static ArrayList<CellLayoutBoard> workspaceToBoards(Launcher launcher) {
ArrayList<CellLayoutBoard> boards = new ArrayList<>();
int widgetCount = 0;
for (CellLayout cellLayout : launcher.getWorkspace().mWorkspaceScreens) {
int count = cellLayout.getShortcutsAndWidgets().getChildCount();
@@ -52,11 +52,29 @@ public class CellLayoutTestUtils {
} else {
// is widget
board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
params.cellVSpan, (char) ('a' + widgetCount));
widgetCount++;
params.cellVSpan);
}
}
}
return boards;
}
public static CellLayoutBoard viewsToBoard(List<View> views, int width, int height) {
CellLayoutBoard board = new CellLayoutBoard();
board.mWidth = width;
board.mHeight = height;
for (View callView : views) {
CellLayoutLayoutParams params = (CellLayoutLayoutParams) callView.getLayoutParams();
// is icon
if (callView instanceof DoubleShadowBubbleTextView) {
board.addIcon(params.getCellX(), params.getCellY());
} else {
// is widget
board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
params.cellVSpan);
}
}
return board;
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright (C) 2023 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.launcher3.celllayout;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.View;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.util.ActivityContextWrapper;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class ReorderAlgorithmUnitTest {
private Context mApplicationContext;
private int mPrevNumColumns, mPrevNumRows;
@Test
public void testAllCases() throws IOException {
List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
"ReorderAlgorithmUnitTest/reorder_algorithm_test_cases");
mApplicationContext = new ActivityContextWrapper(getApplicationContext());
List<Integer> failingCases = new ArrayList<>();
for (int i = 0; i < testCases.size(); i++) {
try {
evaluateTestCase(testCases.get(i));
} catch (AssertionError e) {
e.printStackTrace();
failingCases.add(i);
}
}
assertEquals("Some test cases failed " + Arrays.toString(failingCases.toArray()), 0,
failingCases.size());
}
private void addViewInCellLayout(CellLayout cellLayout, int cellX, int cellY, int spanX,
int spanY, boolean isWidget) {
View cell = isWidget ? new View(mApplicationContext) : new DoubleShadowBubbleTextView(
mApplicationContext);
cell.setLayoutParams(new CellLayoutLayoutParams(cellX, cellY, spanX, spanY));
cellLayout.addViewToCellLayout(cell, -1, cell.getId(),
(CellLayoutLayoutParams) cell.getLayoutParams(), true);
}
public CellLayout createCellLayout(int width, int height) {
Context c = mApplicationContext;
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
// modify the device profile.
dp.inv.numColumns = width;
dp.inv.numRows = height;
CellLayout cl = new CellLayout(getWrappedContext(c, dp));
// I put a very large number for width and height so that all the items can fit, it doesn't
// need to be exact, just bigger than the sum of cell border
cl.measure(View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY));
cl.measure(View.MeasureSpec.makeMeasureSpec(cl.getDesiredWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(cl.getDesiredHeight(), View.MeasureSpec.EXACTLY));
return cl;
}
private Context getWrappedContext(Context context, DeviceProfile dp) {
return new ActivityContextWrapper(context) {
public DeviceProfile getDeviceProfile() {
return dp;
}
};
}
public CellLayout.ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
int spanY, int minSpanX, int minSpanY) {
CellLayout cl = createCellLayout(board.getWidth(), board.getHeight());
// The views have to be sorted or the result can vary
board.getIcons()
.stream()
.map(CellLayoutBoard.IconPoint::getCoord)
.sorted(Comparator.comparing(p -> ((Point) p).x).thenComparing(p -> ((Point) p).y))
.forEach(p -> addViewInCellLayout(cl, p.x, p.y, 1, 1, false));
board.getWidgets().stream()
.sorted(Comparator.comparing(CellLayoutBoard.WidgetRect::getCellX)
.thenComparing(CellLayoutBoard.WidgetRect::getCellY))
.forEach(widget -> addViewInCellLayout(cl, widget.getCellX(), widget.getCellY(),
widget.getSpanX(), widget.getSpanY(), true));
int[] testCaseXYinPixels = new int[2];
cl.regionToCenterPoint(x, y, spanX, spanY, testCaseXYinPixels);
CellLayout.ItemConfiguration solution = cl.createReorderAlgorithm().calculateReorder(
testCaseXYinPixels[0], testCaseXYinPixels[1], minSpanX, minSpanY, spanX, spanY,
null);
if (solution == null) {
solution = new CellLayout.ItemConfiguration();
solution.isSolution = false;
}
return solution;
}
public CellLayoutBoard boardFromSolution(CellLayout.ItemConfiguration solution, int width,
int height) {
// Update the views with solution value
solution.map.forEach((key, val) -> key.setLayoutParams(
new CellLayoutLayoutParams(val.cellX, val.cellY, val.spanX, val.spanY)));
CellLayoutBoard board = CellLayoutTestUtils.viewsToBoard(
new ArrayList<>(solution.map.keySet()), width, height);
board.addWidget(solution.cellX, solution.cellY, solution.spanX, solution.spanY,
'z');
return board;
}
public void evaluateTestCase(ReorderAlgorithmUnitTestCase testCase) {
CellLayout.ItemConfiguration solution = solve(testCase.startBoard, testCase.x,
testCase.y, testCase.spanX, testCase.spanY, testCase.minSpanX,
testCase.minSpanY);
assertEquals("should be a valid solution", solution.isSolution,
testCase.isValidSolution);
if (testCase.isValidSolution) {
CellLayoutBoard finishBoard = boardFromSolution(solution,
testCase.startBoard.getWidth(), testCase.startBoard.getHeight());
assertTrue("End result and test case result board doesn't match ",
finishBoard.compareTo(testCase.endBoard) == 0);
}
}
@Before
public void storePreviousValues() {
Context c = new ActivityContextWrapper(getApplicationContext());
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
mPrevNumColumns = dp.inv.numColumns;
mPrevNumRows = dp.inv.numColumns;
}
@After
public void restorePreviousValues() {
Context c = new ActivityContextWrapper(getApplicationContext());
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
dp.inv.numColumns = mPrevNumColumns;
dp.inv.numRows = mPrevNumRows;
}
@SuppressWarnings("UnusedMethod")
/**
* Utility function used to generate all the test cases
*/
private ReorderAlgorithmUnitTestCase generateRandomTestCase() {
ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
int width = getRandom(3, 8);
int height = getRandom(3, 8);
int targetWidth = getRandom(1, width - 2);
int targetHeight = getRandom(1, height - 2);
int minTargetWidth = getRandom(1, targetWidth);
int minTargetHeight = getRandom(1, targetHeight);
int x = getRandom(0, width - targetWidth);
int y = getRandom(0, height - targetHeight);
CellLayoutBoard board = generateBoard(new CellLayoutBoard(width, height),
new Rect(0, 0, width, height), targetWidth * targetHeight);
CellLayout.ItemConfiguration solution = solve(board, x, y, targetWidth, targetHeight,
minTargetWidth, minTargetHeight);
CellLayoutBoard finishBoard = solution.isSolution ? boardFromSolution(solution,
board.getWidth(), board.getHeight()) : new CellLayoutBoard(board.getWidth(),
board.getHeight());
testCase.startBoard = board;
testCase.endBoard = finishBoard;
testCase.isValidSolution = solution.isSolution;
testCase.x = x;
testCase.y = y;
testCase.spanX = targetWidth;
testCase.spanY = targetHeight;
testCase.minSpanX = minTargetWidth;
testCase.minSpanY = minTargetHeight;
testCase.type = solution.area() == 1 ? "icon" : "widget";
return testCase;
}
private int getRandom(int start, int end) {
int random = end == 0 ? 0 : new Random().nextInt(end);
return start + random;
}
private CellLayoutBoard generateBoard(CellLayoutBoard board, Rect area,
int emptySpaces) {
if (area.height() * area.width() <= 0) return board;
int width = getRandom(1, area.width() - 1);
int height = getRandom(1, area.height() - 1);
int x = area.left + getRandom(0, area.width() - width);
int y = area.top + getRandom(0, area.height() - height);
if (emptySpaces > 0) {
emptySpaces -= width * height;
} else if (width * height > 1) {
board.addWidget(x, y, width, height);
} else {
board.addIcon(x, y);
}
generateBoard(board,
new Rect(area.left, area.top, area.right, y), emptySpaces);
generateBoard(board,
new Rect(area.left, y, x, area.bottom), emptySpaces);
generateBoard(board,
new Rect(x, y + height, area.right, area.bottom), emptySpaces);
generateBoard(board,
new Rect(x + width, y, area.right, y + height), emptySpaces);
return board;
}
private static List<ReorderAlgorithmUnitTestCase> getTestCases(String testPath)
throws IOException {
List<ReorderAlgorithmUnitTestCase> cases = new ArrayList<>();
Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
while (iterableSection.hasNext()) {
cases.add(ReorderAlgorithmUnitTestCase.readNextCase(iterableSection));
}
return cases;
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2023 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.launcher3.celllayout;
import java.util.Iterator;
/**
* Represents a test case for {@code ReorderAlgorithmUnitTest}. The test cases are generated from
* text, an example of a test is the following:
*
* board: 10x8
* aaaaaaaaai
* bbbbbcciii
* ---------f
* ---------f
* ---------f
* ---------i
* iiddddddii
* iieeiiiiii
* arguments: 2 5 7 1 3 1 widget valid
* board: 10x8
* bbbbbbbbbi
* eeeeecciii
* ---------a
* ---------a
* ---------a
* --zzzzzzzi
* iiddddddii
* iiffiiiiii
*
*
* This represents a Workspace boards and a dragged widget that wants to be dropped on the
* workspace. The endBoard represents the result from such drag
* The first board is the startBoard, the arguments are as follow: cellX, cellY, widget spanX,
* widget spanY, minimum spanX, minimum spanX, type of object being drag (icon, widget, folder ),
* if the resulting board is a valid solution or not reorder was found.
*
* For more information on how to read the board please go to the text file
* reorder_algorithm_test_cases
*/
public class ReorderAlgorithmUnitTestCase {
CellLayoutBoard startBoard;
int x, y, spanX, spanY, minSpanX, minSpanY;
String type;
boolean isValidSolution;
CellLayoutBoard endBoard;
public static ReorderAlgorithmUnitTestCase readNextCase(
Iterator<CellLayoutTestCaseReader.TestSection> sections) {
ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
CellLayoutTestCaseReader.Board startBoard =
(CellLayoutTestCaseReader.Board) sections.next();
testCase.startBoard = CellLayoutBoard.boardFromString(startBoard.board);
CellLayoutTestCaseReader.Arguments arguments =
(CellLayoutTestCaseReader.Arguments) sections.next();
testCase.x = Integer.parseInt(arguments.arguments[0]);
testCase.y = Integer.parseInt(arguments.arguments[1]);
testCase.spanX = Integer.parseInt(arguments.arguments[2]);
testCase.spanY = Integer.parseInt(arguments.arguments[3]);
testCase.minSpanX = Integer.parseInt(arguments.arguments[4]);
testCase.minSpanY = Integer.parseInt(arguments.arguments[5]);
testCase.type = arguments.arguments[6];
testCase.isValidSolution = arguments.arguments[7].compareTo("valid") == 0;
CellLayoutTestCaseReader.Board endBoard = (CellLayoutTestCaseReader.Board) sections.next();
testCase.endBoard = CellLayoutBoard.boardFromString(endBoard.board);
return testCase;
}
public CellLayoutBoard getStartBoard() {
return startBoard;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getSpanX() {
return spanX;
}
public void setSpanX(int spanX) {
this.spanX = spanX;
}
public int getSpanY() {
return spanY;
}
public void setSpanY(int spanY) {
this.spanY = spanY;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public boolean isValidSolution() {
return isValidSolution;
}
public void setValidSolution(boolean validSolution) {
isValidSolution = validSolution;
}
public CellLayoutBoard getEndBoard() {
return endBoard;
}
public void setEndBoard(CellLayoutBoard endBoard) {
this.endBoard = endBoard;
}
@Override
public String toString() {
String valid = isValidSolution ? "valid" : "invalid";
return startBoard + "arguments: " + x + " " + y + " " + spanX + " " + spanY + " " + minSpanX
+ " " + minSpanY + " " + type + " " + valid + "\n" + endBoard;
}
}