Commit 3ed31ecf authored by Brian Sheedy's avatar Brian Sheedy Committed by Commit Bot

Switch RenderTests to use SkiaGold

Switches all uses of RenderTestRule to use the SkiaGoldBuilder method
of constructing it, which results in all Android RenderTests being
backed by Skia Gold.

Bug: 1057851
Change-Id: I63d40faef7863dddb5edaa93aef8559eb4d3696d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2248742
Commit-Queue: Brian Sheedy <bsheedy@chromium.org>
Reviewed-by: default avatarTibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#780931}
parent a40effe9
...@@ -979,7 +979,6 @@ class LocalDeviceInstrumentationTestRun( ...@@ -979,7 +979,6 @@ class LocalDeviceInstrumentationTestRun(
if not self._render_tests_device_output_dir: if not self._render_tests_device_output_dir:
return return
self._ProcessSkiaGoldRenderTestResults(device, results) self._ProcessSkiaGoldRenderTestResults(device, results)
self._ProcessLocalRenderTestResults(device, results)
def _ProcessSkiaGoldRenderTestResults(self, device, results): def _ProcessSkiaGoldRenderTestResults(self, device, results):
gold_dir = posixpath.join(self._render_tests_device_output_dir, gold_dir = posixpath.join(self._render_tests_device_output_dir,
...@@ -1107,62 +1106,6 @@ class LocalDeviceInstrumentationTestRun( ...@@ -1107,62 +1106,6 @@ class LocalDeviceInstrumentationTestRun(
'Given unhandled SkiaGoldSession StatusCode %s with error %s', 'Given unhandled SkiaGoldSession StatusCode %s with error %s',
status, error) status, error)
def _ProcessLocalRenderTestResults(self, device, results):
failure_images_device_dir = posixpath.join(
self._render_tests_device_output_dir, 'failures')
if not device.FileExists(failure_images_device_dir):
return
diff_images_device_dir = posixpath.join(
self._render_tests_device_output_dir, 'diffs')
golden_images_device_dir = posixpath.join(
self._render_tests_device_output_dir, 'goldens')
for failure_filename in device.ListDirectory(failure_images_device_dir):
with self._env.output_manager.ArchivedTempfile(
'fail_%s' % failure_filename, 'render_tests',
output_manager.Datatype.PNG) as failure_image_host_file:
device.PullFile(
posixpath.join(failure_images_device_dir, failure_filename),
failure_image_host_file.name)
failure_link = failure_image_host_file.Link()
golden_image_device_file = posixpath.join(
golden_images_device_dir, failure_filename)
if device.PathExists(golden_image_device_file):
with self._env.output_manager.ArchivedTempfile(
'golden_%s' % failure_filename, 'render_tests',
output_manager.Datatype.PNG) as golden_image_host_file:
device.PullFile(
golden_image_device_file, golden_image_host_file.name)
golden_link = golden_image_host_file.Link()
else:
golden_link = ''
diff_image_device_file = posixpath.join(
diff_images_device_dir, failure_filename)
if device.PathExists(diff_image_device_file):
with self._env.output_manager.ArchivedTempfile(
'diff_%s' % failure_filename, 'render_tests',
output_manager.Datatype.PNG) as diff_image_host_file:
device.PullFile(
diff_image_device_file, diff_image_host_file.name)
diff_link = diff_image_host_file.Link()
else:
diff_link = ''
processed_template_output = _GenerateRenderTestHtml(
failure_filename, failure_link, golden_link, diff_link)
with self._env.output_manager.ArchivedTempfile(
'%s.html' % failure_filename, 'render_tests',
output_manager.Datatype.HTML) as html_results:
html_results.write(processed_template_output)
html_results.flush()
_SetLinkOnResults(results, failure_filename, html_results.Link())
#override #override
def _ShouldRetry(self, test, result): def _ShouldRetry(self, test, result):
# We've tried to disable retries in the past with mixed results. # We've tried to disable retries in the past with mixed results.
......
...@@ -230,8 +230,6 @@ public class StartSurfaceLayoutTest { ...@@ -230,8 +230,6 @@ public class StartSurfaceLayoutTest {
ChromeTabUtils.switchTabInCurrentTabModel(cta, 0); ChromeTabUtils.switchTabInCurrentTabModel(cta, 0);
enterTabSwitcher(cta); enterTabSwitcher(cta);
// See crbug.com/1063619
mRenderTestRule.setPixelDiffThreshold(2);
mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "3_web_tabs"); mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "3_web_tabs");
} }
...@@ -251,8 +249,6 @@ public class StartSurfaceLayoutTest { ...@@ -251,8 +249,6 @@ public class StartSurfaceLayoutTest {
ChromeTabUtils.switchTabInCurrentTabModel(cta, 0); ChromeTabUtils.switchTabInCurrentTabModel(cta, 0);
enterTabSwitcher(cta); enterTabSwitcher(cta);
// See crbug.com/1063619
mRenderTestRule.setPixelDiffThreshold(2);
mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "10_web_tabs"); mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "10_web_tabs");
} }
...@@ -268,8 +264,6 @@ public class StartSurfaceLayoutTest { ...@@ -268,8 +264,6 @@ public class StartSurfaceLayoutTest {
prepareTabs(10, 0, "about:blank"); prepareTabs(10, 0, "about:blank");
assertEquals(9, cta.getTabModelSelector().getCurrentModel().index()); assertEquals(9, cta.getTabModelSelector().getCurrentModel().index());
enterGTSWithThumbnailRetry(); enterGTSWithThumbnailRetry();
// See crbug.com/1063619
mRenderTestRule.setPixelDiffThreshold(2);
// Make sure the grid tab switcher is scrolled down to show the selected tab. // Make sure the grid tab switcher is scrolled down to show the selected tab.
mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "10_web_tabs-select_last"); mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "10_web_tabs-select_last");
} }
...@@ -291,8 +285,6 @@ public class StartSurfaceLayoutTest { ...@@ -291,8 +285,6 @@ public class StartSurfaceLayoutTest {
ChromeTabUtils.switchTabInCurrentTabModel(cta, 0); ChromeTabUtils.switchTabInCurrentTabModel(cta, 0);
enterTabSwitcher(cta); enterTabSwitcher(cta);
// See crbug.com/1063619
mRenderTestRule.setPixelDiffThreshold(2);
mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "3_incognito_web_tabs"); mRenderTestRule.render(cta.findViewById(R.id.tab_list_view), "3_incognito_web_tabs");
} }
......
...@@ -334,7 +334,6 @@ public class TabSelectionEditorTest { ...@@ -334,7 +334,6 @@ public class TabSelectionEditorTest {
mRobot.resultRobot.verifyTabSelectionEditorIsVisible(); mRobot.resultRobot.verifyTabSelectionEditorIsVisible();
ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout); ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout);
mRenderTestRule.setPixelDiffThreshold(5);
mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view"); mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view");
} }
...@@ -356,7 +355,6 @@ public class TabSelectionEditorTest { ...@@ -356,7 +355,6 @@ public class TabSelectionEditorTest {
mRobot.resultRobot.verifyTabSelectionEditorIsVisible(); mRobot.resultRobot.verifyTabSelectionEditorIsVisible();
ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout); ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout);
mRenderTestRule.setPixelDiffThreshold(5);
mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_one_selected_tab"); mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_one_selected_tab");
} }
...@@ -378,7 +376,6 @@ public class TabSelectionEditorTest { ...@@ -378,7 +376,6 @@ public class TabSelectionEditorTest {
mRobot.resultRobot.verifyTabSelectionEditorIsVisible(); mRobot.resultRobot.verifyTabSelectionEditorIsVisible();
ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout); ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout);
mRenderTestRule.setPixelDiffThreshold(5);
mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_one_pre_selected_tab"); mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_one_pre_selected_tab");
} }
...@@ -400,7 +397,6 @@ public class TabSelectionEditorTest { ...@@ -400,7 +397,6 @@ public class TabSelectionEditorTest {
mRobot.resultRobot.verifyTabSelectionEditorIsVisible(); mRobot.resultRobot.verifyTabSelectionEditorIsVisible();
ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout); ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout);
mRenderTestRule.setPixelDiffThreshold(5);
mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_two_pre_selected_tab"); mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_two_pre_selected_tab");
} }
...@@ -422,7 +418,6 @@ public class TabSelectionEditorTest { ...@@ -422,7 +418,6 @@ public class TabSelectionEditorTest {
mRobot.resultRobot.verifyTabSelectionEditorIsVisible(); mRobot.resultRobot.verifyTabSelectionEditorIsVisible();
ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout); ChromeRenderTestRule.sanitize(mTabSelectionEditorLayout);
mRenderTestRule.setPixelDiffThreshold(5);
mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_all_pre_selected_tab"); mRenderTestRule.render(mTabSelectionEditorLayout, "grid_view_all_pre_selected_tab");
} }
......
...@@ -66,8 +66,7 @@ public class ModalDialogViewRenderTest extends DummyUiActivityTestCase { ...@@ -66,8 +66,7 @@ public class ModalDialogViewRenderTest extends DummyUiActivityTestCase {
private TextView mCustomTextView2; private TextView mCustomTextView2;
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("chrome/test/data/android/render_tests");
public ModalDialogViewRenderTest(boolean nightModeEnabled) { public ModalDialogViewRenderTest(boolean nightModeEnabled) {
// Sets a fake background color to make the screenshots easier to compare with bare eyes. // Sets a fake background color to make the screenshots easier to compare with bare eyes.
......
...@@ -53,8 +53,7 @@ public class PaymentRequestFreeShippingTest implements MainActivityStartCallback ...@@ -53,8 +53,7 @@ public class PaymentRequestFreeShippingTest implements MainActivityStartCallback
new PaymentRequestTestRule("payment_request_free_shipping_test.html", this, true); new PaymentRequestTestRule("payment_request_free_shipping_test.html", this, true);
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("components/test/data/payments/render_tests");
@BeforeClass @BeforeClass
public static void setUpBeforeActivityLaunched() { public static void setUpBeforeActivityLaunched() {
......
...@@ -50,8 +50,7 @@ public class PaymentRequestRetryTest implements MainActivityStartCallback { ...@@ -50,8 +50,7 @@ public class PaymentRequestRetryTest implements MainActivityStartCallback {
new PaymentRequestTestRule("payment_request_retry.html", this); new PaymentRequestTestRule("payment_request_retry.html", this);
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("components/test/data/payments/render_tests");
@Override @Override
public void onMainActivityStarted() throws TimeoutException { public void onMainActivityStarted() throws TimeoutException {
......
...@@ -14,7 +14,6 @@ import static org.chromium.chrome.browser.tasks.ReturnToChromeExperimentsUtil.TA ...@@ -14,7 +14,6 @@ import static org.chromium.chrome.browser.tasks.ReturnToChromeExperimentsUtil.TA
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.text.TextUtils; import android.text.TextUtils;
...@@ -581,12 +580,6 @@ public class ReturnToChromeTest { ...@@ -581,12 +580,6 @@ public class ReturnToChromeTest {
assertEquals(10, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount()); assertEquals(10, mActivityTestRule.getActivity().getTabModelSelector().getTotalTabCount());
assertEquals(9, mActivityTestRule.getActivity().getCurrentTabModel().index()); assertEquals(9, mActivityTestRule.getActivity().getCurrentTabModel().index());
// See crbug.com/1063619
mRenderTestRule.setPixelDiffThreshold(2);
if (Build.VERSION.SDK_INT == VERSION_CODES.P) {
// See crbug.com/1025241
mRenderTestRule.setPixelDiffThreshold(16);
}
// Make sure the grid tab switcher is scrolled down to show the selected tab. // Make sure the grid tab switcher is scrolled down to show the selected tab.
mRenderTestRule.render(mActivityTestRule.getActivity().findViewById( mRenderTestRule.render(mActivityTestRule.getActivity().findViewById(
org.chromium.chrome.tab_ui.R.id.tab_list_view), org.chromium.chrome.tab_ui.R.id.tab_list_view),
......
...@@ -39,11 +39,13 @@ import org.chromium.ui.test.util.RenderTestRule; ...@@ -39,11 +39,13 @@ import org.chromium.ui.test.util.RenderTestRule;
* </pre> * </pre>
*/ */
public class ChromeRenderTestRule extends RenderTestRule { public class ChromeRenderTestRule extends RenderTestRule {
/**
* Constructor using {@code "chrome/test/data/android/render_tests"} as default golden folder.
*/
public ChromeRenderTestRule() { public ChromeRenderTestRule() {
super("chrome/test/data/android/render_tests"); super();
}
protected ChromeRenderTestRule(int revision, @RenderTestRule.Corpus String corpus,
String description, boolean failOnUnsupportedConfigs) {
super(revision, corpus, description, failOnUnsupportedConfigs);
} }
/** /**
...@@ -53,4 +55,15 @@ public class ChromeRenderTestRule extends RenderTestRule { ...@@ -53,4 +55,15 @@ public class ChromeRenderTestRule extends RenderTestRule {
public static void sanitize(View view) { public static void sanitize(View view) {
TestThreadUtils.runOnUiThreadBlocking(() -> RenderTestRule.sanitize(view)); TestThreadUtils.runOnUiThreadBlocking(() -> RenderTestRule.sanitize(view));
} }
/**
* Builder to create a ChromeRenderTestRule for use with Skia Gold.
*/
public static class SkiaGoldBuilder extends RenderTestRule.SkiaGoldBuilder {
@Override
public ChromeRenderTestRule build() {
return new ChromeRenderTestRule(
mRevision, mCorpus, mDescription, mFailOnUnsupportedConfigs);
}
}
} }
...@@ -42,8 +42,7 @@ public class RadioButtonRenderTest extends DummyUiActivityTestCase { ...@@ -42,8 +42,7 @@ public class RadioButtonRenderTest extends DummyUiActivityTestCase {
new NightModeTestUtils.NightModeParams().getParameters(); new NightModeTestUtils.NightModeParams().getParameters();
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("chrome/test/data/android/render_tests");
private RadioButtonWithDescriptionLayout mLayout; private RadioButtonWithDescriptionLayout mLayout;
......
...@@ -44,8 +44,7 @@ public class ListMenuRenderTest extends DummyUiActivityTestCase { ...@@ -44,8 +44,7 @@ public class ListMenuRenderTest extends DummyUiActivityTestCase {
new NightModeTestUtils.NightModeParams().getParameters(); new NightModeTestUtils.NightModeParams().getParameters();
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("chrome/test/data/android/render_tests");
private View mView; private View mView;
......
...@@ -48,8 +48,7 @@ public class PromoCardViewRenderTest extends DummyUiActivityTestCase { ...@@ -48,8 +48,7 @@ public class PromoCardViewRenderTest extends DummyUiActivityTestCase {
new NightModeTestUtils.NightModeParams().getParameters(); new NightModeTestUtils.NightModeParams().getParameters();
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("chrome/test/data/android/render_tests");
public PromoCardViewRenderTest(boolean nightModeEnabled) { public PromoCardViewRenderTest(boolean nightModeEnabled) {
NightModeTestUtils.setUpNightModeForDummyUiActivity(nightModeEnabled); NightModeTestUtils.setUpNightModeForDummyUiActivity(nightModeEnabled);
......
...@@ -43,8 +43,7 @@ public class ColorPickerDialogRenderTest extends DummyUiActivityTestCase { ...@@ -43,8 +43,7 @@ public class ColorPickerDialogRenderTest extends DummyUiActivityTestCase {
new NightModeTestUtils.NightModeParams().getParameters(); new NightModeTestUtils.NightModeParams().getParameters();
@Rule @Rule
public RenderTestRule mRenderTestRule = public RenderTestRule mRenderTestRule = new RenderTestRule();
new RenderTestRule("chrome/test/data/android/render_tests");
private View mView; private View mView;
......
# Render Tests # Render Tests
## Fixing a failing Render Test Render tests are the way of performing pixel diff/image comparison tests in
Chromium's Android instrumentation tests. They are backed by the Skia team's
Which section applies to the test you are investigating is determined by whether Gold image diffing service, which means that baselines (golden images) are
the test class is manually creating a RenderTestRule or using the stored outside of the repo. Image triage (approval/rejection) is handled via the
SkiaGoldBuilder. Gold web UI, located [here](https://chrome-gold.skia.org/). The UI can also be
used to look at what images are currently being produced for tests.
### Skia Gold Comparison
The newer form of pixel comparison backed by ## Fixing a failing Render Test
[Skia Gold](https://skia.org/dev/testing/skiagold). If a test is running in
this mode, there will be mentions of "Skia Gold" in the reported failure.
#### Failing on trybots ### Failing on trybots
Anytime a patchset produces new golden images, Gold should automatically Anytime a patchset produces new golden images, Gold should automatically
comment on your CL with a link to the triage page. If it fails to do so (e.g. comment on your CL with a link to the triage page. If it fails to do so (e.g.
...@@ -29,9 +26,12 @@ corresponding to the renders mentioned in the failure stack trace. The links ...@@ -29,9 +26,12 @@ corresponding to the renders mentioned in the failure stack trace. The links
will be named "Skia Gold triage link for entire CL". will be named "Skia Gold triage link for entire CL".
Once on the triage page, make sure you are logged in at the top-right. Once on the triage page, make sure you are logged in at the top-right.
Currently, only @google.com accounts work, but other domains such as Currently, only @google.com and @chromium.org accounts work, but other domains
chromium.org can be whitelisted if requested. You should then be able to such as @opera.com can be allowed if requested. Any domain that can log into
triage any newly produced images. using the Google login flow (e.g. what's used to log into crbug.com) should be
able to be allowed. @microsoft.com accounts are supposed to work, but currently
don't due to some issues. You should then be able to triage any newly produced
images.
If the newly generated golden images are "breaking", i.e. it would be a If the newly generated golden images are "breaking", i.e. it would be a
regression if Chrome continued to produce the old golden images (such as due regression if Chrome continued to produce the old golden images (such as due
...@@ -51,7 +51,7 @@ that test class), so you may have to re-triage additional images. If there ...@@ -51,7 +51,7 @@ that test class), so you may have to re-triage additional images. If there
are many images that need to be triaged, you can use the "Bulk Triage" option are many images that need to be triaged, you can use the "Bulk Triage" option
in Gold under the "ACTIONS" menu item. in Gold under the "ACTIONS" menu item.
#### Failing on CI bots ### Failing on CI bots
If a test is failing on the CI bots, i.e. after a CL has already been merged, If a test is failing on the CI bots, i.e. after a CL has already been merged,
you can perform the same steps as in the above section with the following you can perform the same steps as in the above section with the following
...@@ -61,76 +61,15 @@ differences: ...@@ -61,76 +61,15 @@ differences:
comment to. Alternatively, you can check for untriaged images directly in the comment to. Alternatively, you can check for untriaged images directly in the
[gold instance](https://chrome-gold.skia.org). [gold instance](https://chrome-gold.skia.org).
2. Triage links are for specific images instead of for an entire CL, and are 2. Triage links are for specific images instead of for an entire CL, and are
thus named after the the render name. thus named after the render name.
#### Failing locally ### Failing locally
Skia Gold does not allow you to update golden images from local runs. You will Skia Gold does not allow you to update golden images from local runs. You will
still have access to the generated image, the closest golden image, and the diff still have access to the generated image, the closest golden image, and the diff
between them in the test results, but this is purely for local debugging. New between them in the test results, but this is purely for local debugging. New
golden images must come from either trybots or CI bots. golden images must come from either trybots or CI bots.
### Legacy/Local Pixel Comparison
The older form of pixel comparison that does everything locally. If a test is
running in this mode, there will be no mention of "Skia Gold" in the reported
failure.
#### Failing on trybots
To investigate why a Render Test is failing on the trybots:
1. On the failed trybot run, locate and follow the `results_details` link under
the `chrome_public_test_apk` step to go to the **Suites Summary** page.
2. On the **Suites Summary** page, follow the link to the test suite that is
failing.
3. On the **Test Results of Suite** page, follow the links in the **log** column
corresponding to the renders mentioned in the failure stack trace. The links
will be of the form `<test class>.<render id>.<device details>.png`.
Now you will see a **Render Results** page, showing:
* Some useful links.
* The **Failure** image, what the rendered Views look like on the test device.
* The **Golden** image, what the rendered Views should look like, according to
the golden files checked into the repository.
* A **Diff** image to help compare.
At this point, decide whether the UI change was intentional. If it was, follow
the steps below to update the golden files stored in the repository. If not, go
and fix your code! If there's some other error or flakiness, file a bug to
`peconn@chromium.org`.
1. Use the `Link to Golden` link to determine where in the repository the golden
was stored.
2. Right click on the `Download Failure Image` link to save the failure image in
the appropriate place in your local repository.
3. Run the script
`//chrome/test/data/android/manage_render_test_goldens.py upload` to upload the
new goldens to Google Storage and update the hashes used to download them.
4. Reupload the CL and run it through the trybots again.
When putting a change up for review that changes goldens, please include links
to the results_details/Render Results pages that you grabbed the new goldens
from. This will help reviewers confirm that the changes to the goldens are
acceptable.
If you add a new device/SDK combination that you expect golden images for, be
sure to add it to `ALLOWED_DEVICE_SDK_COMBINATIONS` in
`//chrome/test/data/android/manage_render_test_goldens.py`, otherwise the
goldens for it will not be uploaded.
#### Failing locally
Follow the steps in [*Running the tests locally*](#running-the-tests-locally)
below to generate renders.
You can rename the renders as appropriate and move them to the correct place in
the repository, or you can open the locally generated **Render Results** pages
and follow steps 2-3 in the second part of the
[*Failing on trybots*](#failing-on-trybots) section.
## Writing a new Render Test ## Writing a new Render Test
### Writing the test ### Writing the test
...@@ -139,18 +78,6 @@ To write a new test, start with the example in the javadoc for ...@@ -139,18 +78,6 @@ To write a new test, start with the example in the javadoc for
[RenderTestRule](https://cs.chromium.org/chromium/src/ui/android/javatests/src/org/chromium/ui/test/util/RenderTestRule.java) [RenderTestRule](https://cs.chromium.org/chromium/src/ui/android/javatests/src/org/chromium/ui/test/util/RenderTestRule.java)
or [ChromeRenderTestRule](https://cs.chromium.org/chromium/src/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeRenderTestRule.java). or [ChromeRenderTestRule](https://cs.chromium.org/chromium/src/chrome/test/android/javatests/src/org/chromium/chrome/test/util/ChromeRenderTestRule.java).
To enable use of Skia Gold for managing golden images, use
RenderTestRule.SkiaGoldBuilder instead of creating a
RenderTestRule manually. This will become the default eventually, but is still
going through the experimental stage. If you want maximum stability, prefer the
older approach for now. If you want an easier rebaselining process in exchange
for potentially running into some early growing pains, prefer the use of Skia
Gold.
Rebaselining the old way requires downloading all new goldens locally, running
a script to upload them to a Google Storage bucket, and committing the updated
SHA1 files. Rebaselining via Gold is done entirely through a web UI.
If you want to separate your baselines from the default `android-render-tests` If you want to separate your baselines from the default `android-render-tests`
corpus in Gold, you can call `setCorpus()` on your corpus in Gold, you can call `setCorpus()` on your
`SkiaGoldBuilder` instance before calling `build()`. `SkiaGoldBuilder` instance before calling `build()`.
...@@ -174,38 +101,6 @@ failed renders, eg: ...@@ -174,38 +101,6 @@ failed renders, eg:
./out/Debug/bin/run_chrome_public_test_apk -A Feature=RenderTest --local-output ./out/Debug/bin/run_chrome_public_test_apk -A Feature=RenderTest --local-output
``` ```
The golden images should be downloaded as part of the `gclient sync` process,
but if there appear to be goldens missing that should be there, try running
`//chrome/test/data/android/manage_render_test_goldens.py download` to ensure
that the downloaded goldens are current for the git revision.
### Generating golden images locally
**Note that this section only applies to tests running in the legacy/local pixel
comparison mode**
New golden images may be downloaded from the trybots or retrieved locally. This
section elaborates how to do the latter.
You should always create your reference images on the same device type as the
one running the tests. This is because each device/API version may produce a
slightly different image, eg. due to different screen dimensions, DPI setting,
or styling used across OS versions. This is also why each golden image name
includes the device name and API version.
When running a test with no goldens on the correct device, your tests should
fail with an exception:
```
RenderTest Goldens missing for: <reference>. See RENDER_TESTS.md for how to fix this failure.
```
You will be able to find the images the device captured on the device's SD card.
```
adb -d shell ls /sdcard/chromium_tests_root/chrome/test/data/android/render_tests/failures
```
## Implementation Details ## Implementation Details
### Supported devices ### Supported devices
...@@ -216,10 +111,11 @@ that occur on the trybots, otherwise the golden files will get out of date as ...@@ -216,10 +111,11 @@ that occur on the trybots, otherwise the golden files will get out of date as
changes occur and render tests will either fail on the Testers with no warning, changes occur and render tests will either fail on the Testers with no warning,
or be useless. or be useless.
Currently, `chrome_public_test_apk` is only run on Nexus 5s running Android Currently, the render tests are only run on the CQ on Nexus 5Xs running
Lollipop, so that is the only model/sdk combination for which we store goldens. Android Marshmallow, so that is the only model/sdk combination for which we
There [is work](https://crbug.com/731759) to expand this to include Nexus 5Xs expect golden images to be maintained. The tests run on other devices and OS
running Marshmallow as well. versions, but the results are made available mostly as an FYI, and a comparison
failure on these other configurations will not result in a test failure.
### Sanitizing Views ### Sanitizing Views
......
...@@ -5,12 +5,9 @@ ...@@ -5,12 +5,9 @@
package org.chromium.ui.test.util; package org.chromium.ui.test.util;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
...@@ -26,10 +23,8 @@ import org.junit.rules.TestWatcher; ...@@ -26,10 +23,8 @@ import org.junit.rules.TestWatcher;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.chromium.base.CommandLine; import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.ui.UiUtils; import org.chromium.ui.UiUtils;
import java.io.File; import java.io.File;
...@@ -38,26 +33,20 @@ import java.io.IOException; ...@@ -38,26 +33,20 @@ import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
/** /**
* A TestRule for creating Render Tests. An exception will be thrown after the test method completes * A TestRule for creating Render Tests. The comparison is performed using the Skia Gold image
* if the test fails. * diffing service on the host.
* *
* Skia Gold/newer diffing approach: * General usage:
* *
* <pre> * <pre>
* {@code * {@code
* *
* @RunWith(BaseJUnit4ClassRunner.class) * @RunWith(BaseJUnit4ClassRunner.class)
* public class MyTest extends DummyUiActivityTestCase > * public class MyTest extends DummyUiActivityTestCase {
* @Rule * @Rule
* public RenderTestRule mRenderTestRule = new RenderTestRule.SkiaGoldBuilder() * public RenderTestRule mRenderTestRule = new RenderTestRule.SkiaGoldBuilder()
* // Optional, only necessary if you want your results to be kept in a different * // Optional, only necessary if you want your results to be kept in a different
...@@ -73,7 +62,7 @@ import java.util.concurrent.Callable; ...@@ -73,7 +62,7 @@ import java.util.concurrent.Callable;
* .build(); * .build();
* *
* @Test * @Test
* // "RenderTest" feature still required. * // "RenderTest" feature required.
* @Feature({"RenderTest"}) * @Feature({"RenderTest"})
* public void testViewAppearance() { * public void testViewAppearance() {
* // Setup the UI. * // Setup the UI.
...@@ -88,64 +77,17 @@ import java.util.concurrent.Callable; ...@@ -88,64 +77,17 @@ import java.util.concurrent.Callable;
* } * }
* </pre> * </pre>
* *
* Legacy/local diffing approach:
*
* <pre>
* {@code
*
* @RunWith(BaseJUnit4ClassRunner.class)
* public class MyTest extends DummyUiActivityTestCase {
* // Provide RenderTestRule with the path from src/ to the golden directory.
* @Rule
* public RenderTestRule mRenderTestRule =
* new RenderTestRule("components/myfeature/test/data/android/render_tests");
*
* @Test
* // The test must have the feature "RenderTest" for the bots to display renders.
* @Feature({"RenderTest"})
* public void testViewAppearance() {
* // Setup the UI.
* ...
*
* // Render UI Elements.
* mRenderTestRule.render(bigWidgetView, "big_widget");
* mRenderTestRule.render(smallWidgetView, "small_widget");
* }
* }
*
* }
* </pre>
*
*/ */
public class RenderTestRule extends TestWatcher { public class RenderTestRule extends TestWatcher {
private static final String TAG = "RenderTest"; private static final String TAG = "RenderTest";
private static final String DIFF_FOLDER_RELATIVE = "/diffs";
private static final String FAILURE_FOLDER_RELATIVE = "/failures";
private static final String GOLDEN_FOLDER_RELATIVE = "/goldens";
private static final String SKIA_GOLD_FOLDER_RELATIVE = "/skia_gold"; private static final String SKIA_GOLD_FOLDER_RELATIVE = "/skia_gold";
/**
* This is a list of model-SDK version identifiers for devices we maintain golden images for.
* If render tests are being run on a device of a model-sdk on this list, goldens should exist.
*
* This should be kept in sync with the RENDER_TEST_MODEL_SDK_CONFIGS dict in
* local_device_instrumentation_test_run.py.
* TODO(https://crbug.com/1060245): Re-add Nexus_5-19 when chrome_public_test_apk is back on the
* KitKat bot on the CQ.
*/
private static final String[] RENDER_TEST_MODEL_SDK_PAIRS = {"Nexus_5X-23"};
private enum ComparisonResult { MATCH, MISMATCH, GOLDEN_NOT_FOUND }
// State for a test class. // State for a test class.
private final String mOutputFolder; private final String mOutputFolder;
private final String mGoldenFolder;
// State for a test method. // State for a test method.
private String mTestClassName; private String mTestClassName;
private List<String> mMismatchIds = new LinkedList<>();
private List<String> mGoldenMissingIds = new LinkedList<>();
private boolean mHasRenderTestFeature; private boolean mHasRenderTestFeature;
/** Parameterized tests have a prefix inserted at the front of the test description. */ /** Parameterized tests have a prefix inserted at the front of the test description. */
...@@ -154,12 +96,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -154,12 +96,6 @@ public class RenderTestRule extends TestWatcher {
/** Prefix on the render test images that describes light/dark mode. */ /** Prefix on the render test images that describes light/dark mode. */
private String mNightModePrefix; private String mNightModePrefix;
// How much a channel must differ when comparing pixels in order to be considered different.
private int mPixelDiffThreshold;
private Map<String, DiffReport> mMismatchReports = new HashMap<>();
private boolean mUseSkiaGold;
private String mSkiaGoldCorpus; private String mSkiaGoldCorpus;
private int mSkiaGoldRevision; private int mSkiaGoldRevision;
private String mSkiaGoldRevisionDescription; private String mSkiaGoldRevisionDescription;
...@@ -175,21 +111,14 @@ public class RenderTestRule extends TestWatcher { ...@@ -175,21 +111,14 @@ public class RenderTestRule extends TestWatcher {
} }
/** /**
* An exception thrown after a Render Test if images do not match the goldens or goldens are * Default constructor, which is equivalent to using the SkiaGoldBuilder without setting any
* missing on a render test device. * additional configurations on it.
*
* If any such configurations are desired, e.g. bumping the image revision, please switch to
* using the builder instead.
*/ */
public static class RenderTestException extends RuntimeException { public RenderTestRule() {
public RenderTestException(String message) { this(0, null, null, false);
super(message);
}
}
public RenderTestRule(String goldenFolder) {
// |goldenFolder| is relative to the src directory in the repository. |mGoldenFolder| will
// be the folder on the test device.
mGoldenFolder = UrlUtils.getIsolatedTestFilePath(goldenFolder);
// The output folder can be overridden with the --render-test-output-dir command.
mOutputFolder = CommandLine.getInstance().getSwitchValue("render-test-output-dir");
} }
// Skia Gold-specific constructor used by the builder. // Skia Gold-specific constructor used by the builder.
...@@ -200,7 +129,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -200,7 +129,6 @@ public class RenderTestRule extends TestWatcher {
boolean failOnUnsupportedConfigs) { boolean failOnUnsupportedConfigs) {
assert revision >= 0; assert revision >= 0;
mUseSkiaGold = true;
mSkiaGoldCorpus = (corpus == null) ? Corpus.ANDROID_RENDER_TESTS : corpus; mSkiaGoldCorpus = (corpus == null) ? Corpus.ANDROID_RENDER_TESTS : corpus;
mSkiaGoldRevisionDescription = description; mSkiaGoldRevisionDescription = description;
mSkiaGoldRevision = revision; mSkiaGoldRevision = revision;
...@@ -208,8 +136,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -208,8 +136,6 @@ public class RenderTestRule extends TestWatcher {
// The output folder can be overridden with the --render-test-output-dir command. // The output folder can be overridden with the --render-test-output-dir command.
mOutputFolder = CommandLine.getInstance().getSwitchValue("render-test-output-dir"); mOutputFolder = CommandLine.getInstance().getSwitchValue("render-test-output-dir");
// Unused when using Skia Gold, but compiler complains about it being uninitialized.
mGoldenFolder = null;
} }
@Override @Override
...@@ -217,19 +143,18 @@ public class RenderTestRule extends TestWatcher { ...@@ -217,19 +143,18 @@ public class RenderTestRule extends TestWatcher {
// desc.getClassName() gets the fully qualified name. // desc.getClassName() gets the fully qualified name.
mTestClassName = desc.getTestClass().getSimpleName(); mTestClassName = desc.getTestClass().getSimpleName();
mMismatchIds.clear();
mGoldenMissingIds.clear();
Feature feature = desc.getAnnotation(Feature.class); Feature feature = desc.getAnnotation(Feature.class);
mHasRenderTestFeature = mHasRenderTestFeature =
(feature != null && Arrays.asList(feature.value()).contains("RenderTest")); (feature != null && Arrays.asList(feature.value()).contains("RenderTest"));
} }
/** /**
* Renders the |view| and compares it to the golden view with the |id|. The RenderTestRule will * Renders the |view| and compares it to the golden view with the |id|. Image comparison is
* throw an exception after the test method has completed if the view does not match the * performed on the host after the test has finished running. Comparison will fail if the given
* golden or if a golden is missing on a device it should be present (see * image does not exactly match one of the images in Gold and the image came from a device that
* {@link RenderTestRule#RENDER_TEST_MODEL_SDK_PAIRS}). * should have baselines maintained (see the RENDER_TEST_MODEL_SDK_CONFIGS constant in the
* Python test runner code at
* //build/android/pylib/local/device/local_device_instrumentation_test_run.py).
* *
* @throws IOException if the rendered image cannot be saved to the device. * @throws IOException if the rendered image cannot be saved to the device.
*/ */
...@@ -254,10 +179,11 @@ public class RenderTestRule extends TestWatcher { ...@@ -254,10 +179,11 @@ public class RenderTestRule extends TestWatcher {
} }
/** /**
* Compares the given |testBitmap| to the golden with the |id|. The RenderTestRule will throw * Compares the given |testBitmap| to the images in Gold for |id|. Image comparison is performed
* an exception after the test method has completed if the view does not match the golden or if * on the host after the test has finished running. Comparison will fail if the given image
* a golden is missing on a device it should be present (see * does not exactly match one of the images in Gold and the image came from a device that should
* {@link RenderTestRule#RENDER_TEST_MODEL_SDK_PAIRS}). * have baselines maintained (see the RENDER_TEST_MODEL_SDK_CONFIGS constant in the Python test
* runner code at //build/android/pylib/local/device/local_device_instrumentation_test_run.py).
* *
* Tests should prefer {@link RenderTestRule#render(View, String) render} to this if possible. * Tests should prefer {@link RenderTestRule#render(View, String) render} to this if possible.
* *
...@@ -266,14 +192,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -266,14 +192,6 @@ public class RenderTestRule extends TestWatcher {
public void compareForResult(Bitmap testBitmap, String id) throws IOException { public void compareForResult(Bitmap testBitmap, String id) throws IOException {
Assert.assertTrue("Render Tests must have the RenderTest feature.", mHasRenderTestFeature); Assert.assertTrue("Render Tests must have the RenderTest feature.", mHasRenderTestFeature);
if (mUseSkiaGold) {
compareForResultSkiaGold(testBitmap, id);
} else {
compareForResultLocal(testBitmap, id);
}
}
public void compareForResultSkiaGold(Bitmap testBitmap, String id) throws IOException {
// Save the image and its metadata to a location where it can be pulled by the test runner // Save the image and its metadata to a location where it can be pulled by the test runner
// for comparison after the test finishes. // for comparison after the test finishes.
String imageName = getImageName(mTestClassName, mVariantPrefix, id); String imageName = getImageName(mTestClassName, mVariantPrefix, id);
...@@ -293,80 +211,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -293,80 +211,6 @@ public class RenderTestRule extends TestWatcher {
saveString(goldKeys.toString(), createOutputPath(SKIA_GOLD_FOLDER_RELATIVE, jsonName)); saveString(goldKeys.toString(), createOutputPath(SKIA_GOLD_FOLDER_RELATIVE, jsonName));
} }
public void compareForResultLocal(Bitmap testBitmap, String id) throws IOException {
String filename = getImageName(mTestClassName, mVariantPrefix, id);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = testBitmap.getConfig();
File goldenFile = createGoldenPath(filename);
Bitmap goldenBitmap = BitmapFactory.decodeFile(goldenFile.getAbsolutePath(), options);
Pair<DiffReport, Bitmap> result = compareBitmapToGolden(testBitmap, goldenBitmap, id);
Log.i(TAG, "RenderTest %s %s", id, result.first.toString());
// Save the result and any interesting images.
switch (result.first.getResult()) {
case MATCH:
// We don't do anything with the matches.
break;
case GOLDEN_NOT_FOUND:
mGoldenMissingIds.add(id);
saveBitmap(testBitmap, createOutputPath(FAILURE_FOLDER_RELATIVE, filename));
break;
case MISMATCH:
mMismatchIds.add(id);
mMismatchReports.put(id, result.first);
saveBitmap(testBitmap, createOutputPath(FAILURE_FOLDER_RELATIVE, filename));
saveBitmap(goldenBitmap, createOutputPath(GOLDEN_FOLDER_RELATIVE, filename));
saveBitmap(result.second, createOutputPath(DIFF_FOLDER_RELATIVE, filename));
break;
}
}
/**
* Appends the DiffReports if exists.
*/
private void maybeAppendDiffReports(StringBuilder sb) {
for (String key : mMismatchIds) {
if (mMismatchReports.containsKey(key)) {
sb.append(mMismatchReports.get(key));
}
}
}
@Override
protected void finished(Description desc) {
if (!onRenderTestDevice() && !mGoldenMissingIds.isEmpty()) {
Log.d(TAG, "RenderTest missing goldens, but we are not on a render test device.");
mGoldenMissingIds.clear();
}
if (mGoldenMissingIds.isEmpty() && mMismatchIds.isEmpty()) {
// Everything passed!
return;
}
StringBuilder sb = new StringBuilder();
if (!mGoldenMissingIds.isEmpty()) {
sb.append("RenderTest Goldens missing for: ");
sb.append(TextUtils.join(", ", mGoldenMissingIds));
sb.append(".");
}
if (!mMismatchIds.isEmpty()) {
if (sb.length() != 0) sb.append(" ");
sb.append("RenderTest Mismatches for: ");
sb.append(TextUtils.join(", ", mMismatchIds));
sb.append(".");
}
sb.append(" See RENDER_TESTS.md for how to fix this failure.\n");
maybeAppendDiffReports(sb);
throw new RenderTestException(sb.toString());
}
/** /**
* Searches the View hierarchy and modifies the Views to provide better stability in tests. For * Searches the View hierarchy and modifies the Views to provide better stability in tests. For
* example it will disable the blinking cursor in EditTexts. * example it will disable the blinking cursor in EditTexts.
...@@ -389,16 +233,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -389,16 +233,6 @@ public class RenderTestRule extends TestWatcher {
} }
} }
/**
* Returns whether goldens should exist for the current device.
*/
private static boolean onRenderTestDevice() {
for (String model : RENDER_TEST_MODEL_SDK_PAIRS) {
if (model.equals(modelSdkIdentifier())) return true;
}
return false;
}
/** /**
* Sets a string that will be inserted at the start of the description in the golden image name. * Sets a string that will be inserted at the start of the description in the golden image name.
* This is used to create goldens for multiple different variants of the UI. * This is used to create goldens for multiple different variants of the UI.
...@@ -414,15 +248,6 @@ public class RenderTestRule extends TestWatcher { ...@@ -414,15 +248,6 @@ public class RenderTestRule extends TestWatcher {
mNightModePrefix = nightModeEnabled ? "NightModeEnabled" : "NightModeDisabled"; mNightModePrefix = nightModeEnabled ? "NightModeEnabled" : "NightModeDisabled";
} }
/**
* Sets the threshold that a pixel must differ by when comparing channels in order to be
* considered different.
*/
public void setPixelDiffThreshold(int threshold) {
assert threshold >= 0;
mPixelDiffThreshold = threshold;
}
/** /**
* Creates an image name combining the image description with details about the device * Creates an image name combining the image description with details about the device
* (eg model, current orientation). * (eg model, current orientation).
...@@ -455,12 +280,8 @@ public class RenderTestRule extends TestWatcher { ...@@ -455,12 +280,8 @@ public class RenderTestRule extends TestWatcher {
desc = variantPrefix + "-" + desc; desc = variantPrefix + "-" + desc;
} }
if (mUseSkiaGold) { return String.format(
return String.format( "%s.%s.%s.rev_%s", testClass, desc, modelSdkIdentifier(), mSkiaGoldRevision);
"%s.%s.%s.rev_%s", testClass, desc, modelSdkIdentifier(), mSkiaGoldRevision);
}
return String.format("%s.%s.%s", testClass, desc, modelSdkIdentifier());
} }
/** /**
...@@ -491,19 +312,12 @@ public class RenderTestRule extends TestWatcher { ...@@ -491,19 +312,12 @@ public class RenderTestRule extends TestWatcher {
} }
} }
/**
* Convenience method to create a File pointing to |filename| in |mGoldenFolder|.
*/
private File createGoldenPath(String filename) throws IOException {
return createPath(mGoldenFolder, filename);
}
/** /**
* Convenience method to create a File pointing to |filename| in the |subfolder| in * Convenience method to create a File pointing to |filename| in the |subfolder| in
* |mOutputFolder|. * |mOutputFolder|.
*/ */
private File createOutputPath(String subfolder, String filename) throws IOException { private File createOutputPath(String subfolder, String filename) throws IOException {
String folder = mOutputFolder != null ? mOutputFolder : mGoldenFolder; String folder = mOutputFolder;
return createPath(folder + subfolder, filename); return createPath(folder + subfolder, filename);
} }
...@@ -521,10 +335,10 @@ public class RenderTestRule extends TestWatcher { ...@@ -521,10 +335,10 @@ public class RenderTestRule extends TestWatcher {
* Builder to create a RenderTestRule for use with Skia Gold. * Builder to create a RenderTestRule for use with Skia Gold.
*/ */
public static class SkiaGoldBuilder { public static class SkiaGoldBuilder {
private int mRevision; protected int mRevision;
private @Corpus String mCorpus; protected @Corpus String mCorpus;
private String mDescription; protected String mDescription;
private boolean mFailOnUnsupportedConfigs; protected boolean mFailOnUnsupportedConfigs;
/** /**
* Sets the revision that will be appended to the test name reported to Gold. This should * Sets the revision that will be appended to the test name reported to Gold. This should
...@@ -554,7 +368,9 @@ public class RenderTestRule extends TestWatcher { ...@@ -554,7 +368,9 @@ public class RenderTestRule extends TestWatcher {
/** /**
* Sets whether failures should still be reported on unsupported hardware/software configs. * Sets whether failures should still be reported on unsupported hardware/software configs.
* Supported configurations are listed under RENDER_TEST_MODEL_SDK_PAIRS. * Supported configurations are listed in the Python test runner code in
* //build/android/pylib/local/device/local_device_instrumentation_test_run.py under the
* RENDER_TEST_MODEL_SDK_CONFIGS constant.
*/ */
public SkiaGoldBuilder setFailOnUnsupportedConfigs(boolean fail) { public SkiaGoldBuilder setFailOnUnsupportedConfigs(boolean fail) {
mFailOnUnsupportedConfigs = fail; mFailOnUnsupportedConfigs = fail;
...@@ -565,345 +381,4 @@ public class RenderTestRule extends TestWatcher { ...@@ -565,345 +381,4 @@ public class RenderTestRule extends TestWatcher {
return new RenderTestRule(mRevision, mCorpus, mDescription, mFailOnUnsupportedConfigs); return new RenderTestRule(mRevision, mCorpus, mDescription, mFailOnUnsupportedConfigs);
} }
} }
/**
* Compares two Bitmaps.
* @return A pair of DiffReport and Bitmap. If the DiffReport.mResult is MISMATCH or MATCH,
* the Bitmap will be a generated pixel-by-pixel difference.
*/
private Pair<DiffReport, Bitmap> compareBitmapToGolden(
Bitmap render, Bitmap golden, String id) {
if (golden == null) return Pair.create(DiffReport.newGoldenNotFoundDiffReport(), null);
// This comparison is much, much faster than doing a pixel-by-pixel comparison, so try this
// first and only fall back to the pixel comparison if it fails.
if (render.sameAs(golden)) return Pair.create(DiffReport.newMatchDiffReport(), null);
Bitmap diff = Bitmap.createBitmap(Math.max(render.getWidth(), golden.getWidth()),
Math.max(render.getHeight(), golden.getHeight()), render.getConfig());
// Assume that the majority of the pixels will be the same and set the diff image to
// transparent by default.
diff.eraseColor(Color.TRANSPARENT);
int maxWidth = Math.max(render.getWidth(), golden.getWidth());
int maxHeight = Math.max(render.getHeight(), golden.getHeight());
int minWidth = Math.min(render.getWidth(), golden.getWidth());
int minHeight = Math.min(render.getHeight(), golden.getHeight());
DiffReport.Builder report =
comparePixels(render, golden, diff, mPixelDiffThreshold, 0, minWidth, 0, minHeight);
report.addPixelDiffCount(compareSizes(diff, minWidth, maxWidth, minHeight, maxHeight));
report.setID(id);
return Pair.create(report.build(), diff);
}
/**
* The class represents a read only copy of comparison result between one test rendered image
* and its golden image.
*/
static class DiffReport {
// The maximum value difference in the red channel.
private final int mMaxRDiff;
// The maximum value difference in the green channel.
private final int mMaxGDiff;
// The maximum value difference in the blue channel.
private final int mMaxBDiff;
// The maximum value difference in the alpha channel.
private final int mMaxADiff;
// Only store first 20 pixel coordinates.
private final List<Pair<Integer, Integer>> mDiffPixelLocations = new ArrayList<>();
// The count of pixels being considered diff.
private final int mDiffPixelCount;
// The comparison result such as Match, Mismatch, and Golden_not_found.
private final ComparisonResult mResult;
// The ID used to identify the golden image.
private final String mID;
// The Match DiffReport with no ID associated to it.
private static final DiffReport MATCH_DIFF_REPORT =
new DiffReport("", 0, 0, 0, 0, null, 0, ComparisonResult.MATCH);
// The GoldenNotFound DiffReport with no ID associated to it.
private static final DiffReport GOLDEN_NOT_FOUND_DIFF_REPORT =
new DiffReport("", 0, 0, 0, 0, null, 0, ComparisonResult.GOLDEN_NOT_FOUND);
// The maximum number of entries in mDiffPixelCount
private static final int MAXIMUM_TOTAL_PIXEL_LOCATIONS = 20;
private DiffReport(String id, int rDiff, int gDiff, int bDiff, int aDiff,
List<Pair<Integer, Integer>> diffLocations, int diffPixelCount) {
this(id, rDiff, gDiff, bDiff, aDiff, diffLocations, diffPixelCount,
ComparisonResult.MISMATCH);
}
private DiffReport(String id, int rDiff, int gDiff, int bDiff, int aDiff,
List<Pair<Integer, Integer>> diffLocations, int diffPixelCount,
ComparisonResult result) {
mID = id;
mMaxRDiff = rDiff;
mMaxGDiff = gDiff;
mMaxBDiff = bDiff;
mMaxADiff = aDiff;
if (diffLocations != null) {
for (Pair<Integer, Integer> loc : diffLocations) {
mDiffPixelLocations.add(loc);
if (mDiffPixelLocations.size() >= MAXIMUM_TOTAL_PIXEL_LOCATIONS) {
break;
}
}
}
mDiffPixelCount = diffPixelCount;
switch (result) {
case MISMATCH:
if (isMismatch()) {
mResult = ComparisonResult.MISMATCH;
} else {
// If the isMismatch() check fails, it is a Match.
mResult = ComparisonResult.MATCH;
}
break;
case MATCH:
case GOLDEN_NOT_FOUND:
mResult = result;
break;
default:
mResult = ComparisonResult.MISMATCH;
}
}
static DiffReport newMatchDiffReport() {
return MATCH_DIFF_REPORT;
}
static DiffReport newGoldenNotFoundDiffReport() {
return GOLDEN_NOT_FOUND_DIFF_REPORT;
}
/**
* Retrieves a new Builder.
*/
static DiffReport.Builder newBuilder() {
return new Builder();
}
/**
* Generates a string format of the DiffReport.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("====DiffReport====\n");
sb.append(String.format(Locale.getDefault(), "id : %s\n", mID));
sb.append(String.format(Locale.getDefault(),
"There are %d pixels rendered differently.\n", mDiffPixelCount));
sb.append(String.format(Locale.getDefault(),
"The maximum difference in RGBA channels are {R:%d, G:%d, B:%d, A:%d}\n",
mMaxRDiff, mMaxGDiff, mMaxBDiff, mMaxADiff));
sb.append(String.format(Locale.getDefault(),
"The different pixels are located at (limited to first %d entries):\n",
MAXIMUM_TOTAL_PIXEL_LOCATIONS));
for (Pair<Integer, Integer> loc : mDiffPixelLocations) {
sb.append(String.format(Locale.getDefault(), "(%d, %d)\n", loc.first, loc.second));
}
sb.append("====End of DiffReport====\n");
return sb.toString();
}
boolean isMismatch() {
return mDiffPixelCount > 0;
}
ComparisonResult getResult() {
return mResult;
}
/**
* Allows constructing a new DiffReport with read-only properties.
*/
static class Builder {
private int mMaxRedDiff;
private int mMaxGreenDiff;
private int mMaxBlueDiff;
private int mMaxAlphaDiff;
private int mDiffPixelCount;
private String mID;
private List<Pair<Integer, Integer>> mDiffPixelLocations = new ArrayList<>();
Builder setMaxRedDiff(int value) {
mMaxRedDiff = value;
return this;
}
Builder setMaxGreenDiff(int value) {
mMaxGreenDiff = value;
return this;
}
Builder setMaxBlueDiff(int value) {
mMaxBlueDiff = value;
return this;
}
Builder setMaxAlphaDiff(int value) {
mMaxAlphaDiff = value;
return this;
}
Builder addPixelLocation(Pair<Integer, Integer> loc) {
mDiffPixelLocations.add(loc);
return this;
}
Builder setPixelDiffCount(int count) {
mDiffPixelCount = count;
return this;
}
Builder addPixelDiffCount(int count) {
mDiffPixelCount += count;
return this;
}
Builder setID(String id) {
mID = id;
return this;
}
DiffReport build() {
return new DiffReport(mID, mMaxRedDiff, mMaxGreenDiff, mMaxBlueDiff, mMaxAlphaDiff,
mDiffPixelLocations, mDiffPixelCount);
}
}
}
/**
* Compares two bitmaps pixel-wise.
*
* @param testImage Bitmap of test image.
*
* @param goldenImage Bitmap of golden image.
*
* @param diffImage This is an output argument. Function will set pixels in the |diffImage| to
* either transparent or red depending on whether that pixel differed in the golden and test
* bitmaps. diffImage should have its width and height be the max width and height of the
* golden and test bitmaps.
*
* @param diffThreshold Threshold for when to consider two color values as different. These
* values are 8 bit (256) so this threshold value should be in range 0-256.
*
* @param startWidth Start x-coord to start diffing the Bitmaps.
*
* @param endWidth End x-coord to start diffing the Bitmaps.
*
* @param startHeight Start y-coord to start diffing the Bitmaps.
*
* @param endHeight End x-coord to start diffing the Bitmaps.
*
* @return Returns a DiffReport.Builder that is used to create a DiffReport.
*/
private static DiffReport.Builder comparePixels(Bitmap testImage, Bitmap goldenImage,
Bitmap diffImage, int diffThreshold, int startWidth, int endWidth, int startHeight,
int endHeight) {
int diffPixels = 0;
// Get copies of the pixels and compare using that instead of repeatedly calling getPixel,
// as that's significantly faster since we don't need to repeatedly hop through JNI.
int diffWidth = endWidth - startWidth;
int diffHeight = endHeight - startHeight;
int[] goldenPixels =
writeBitmapToArray(goldenImage, startWidth, startHeight, diffWidth, diffHeight);
int[] testPixels =
writeBitmapToArray(testImage, startWidth, startHeight, diffWidth, diffHeight);
int maxRedDiff = 0;
int maxGreenDiff = 0;
int maxBlueDiff = 0;
int maxAlphaDiff = 0;
DiffReport.Builder builder = DiffReport.newBuilder();
int diffArea = diffHeight * diffWidth;
for (int i = 0; i < diffArea; ++i) {
if (goldenPixels[i] == testPixels[i]) continue;
int goldenColor = goldenPixels[i];
int testColor = testPixels[i];
int redDiff = Math.abs(Color.red(goldenColor) - Color.red(testColor));
int greenDiff = Math.abs(Color.green(goldenColor) - Color.green(testColor));
int blueDiff = Math.abs(Color.blue(goldenColor) - Color.blue(testColor));
int alphaDiff = Math.abs(Color.alpha(goldenColor) - Color.alpha(testColor));
if (redDiff > maxRedDiff) {
maxRedDiff = redDiff;
}
if (blueDiff > maxBlueDiff) {
maxBlueDiff = blueDiff;
}
if (greenDiff > maxGreenDiff) {
maxGreenDiff = greenDiff;
}
if (alphaDiff > maxAlphaDiff) {
maxAlphaDiff = alphaDiff;
}
if (redDiff > diffThreshold || blueDiff > diffThreshold || greenDiff > diffThreshold
|| alphaDiff > diffThreshold) {
builder.addPixelLocation(Pair.create(i % diffWidth, i / diffWidth));
diffPixels++;
diffImage.setPixel(i % diffWidth, i / diffWidth, Color.RED);
}
}
return builder.setMaxRedDiff(maxRedDiff)
.setMaxGreenDiff(maxGreenDiff)
.setMaxBlueDiff(maxBlueDiff)
.setMaxAlphaDiff(maxAlphaDiff)
.setPixelDiffCount(diffPixels);
}
/**
* Compares two bitmaps size.
*
* @param diffImage This is an output argument. Function will set pixels in the |diffImage| to
* either transparent or red depending on whether that pixel coordinate occurs in the
* dimensions of the golden and not the test bitmap or vice-versa.
*
* @param minWidth Min width of golden and test bitmaps.
*
* @param maxWidth Max width of golden and test bitmaps.
*
* @param minHeight Min height of golden and test bitmaps.
*
* @param maxHeight Max height of golden and test bitmaps.
*
* @return Returns number of pixels that differ between |goldenImage| and |testImage| due to
* their size.
*/
private static int compareSizes(
Bitmap diffImage, int minWidth, int maxWidth, int minHeight, int maxHeight) {
int diffPixels = 0;
if (maxWidth > minWidth) {
int diffWidth = maxWidth - minWidth;
int totalPixels = diffWidth * maxHeight;
// Filling an array of pixels then bulk-setting is faster than looping through each
// individual pixel and setting it.
int[] pixels = new int[totalPixels];
Arrays.fill(pixels, 0, totalPixels, Color.RED);
diffImage.setPixels(pixels, 0 /* offset */, diffWidth /* stride */, minWidth /* x */,
0 /* y */, diffWidth /* width */, maxHeight /* height */);
diffPixels += totalPixels;
}
if (maxHeight > minHeight) {
int diffHeight = maxHeight - minHeight;
int totalPixels = diffHeight * minWidth;
int[] pixels = new int[totalPixels];
Arrays.fill(pixels, 0, totalPixels, Color.RED);
diffImage.setPixels(pixels, 0 /* offset */, minWidth /* stride */, 0 /* x */,
minHeight /* y */, minWidth /* width */, diffHeight /* height */);
diffPixels += totalPixels;
}
return diffPixels;
}
private static int[] writeBitmapToArray(Bitmap bitmap, int x, int y, int width, int height) {
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0 /* offset */, width /* stride */, x, y, width, height);
return pixels;
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment