| /* |
| * Copyright (C) 2013 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.tools.idea.rendering; |
| |
| import com.android.sdklib.devices.Device; |
| import com.android.tools.idea.configurations.Configuration; |
| import com.android.tools.idea.configurations.ConfigurationManager; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiManager; |
| import org.jetbrains.android.AndroidTestCase; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.imageio.ImageIO; |
| import java.awt.*; |
| import java.awt.image.BufferedImage; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import static java.awt.image.BufferedImage.TYPE_INT_ARGB; |
| import static java.io.File.separator; |
| import static java.io.File.separatorChar; |
| |
| /** |
| * Base for unit tests which perform rendering; this test can generate configurations and perform |
| * rendering, then check that the rendered result matches a known thumbnail (by a certain maximum |
| * percentage difference). The test will generate the required thumbnail if it does not exist, |
| * so to create a new render test just call {@link #checkRenderedImage(java.awt.image.BufferedImage, String)} |
| * and run the test once; then verify that the thumbnail looks fine, and if so, check it in; the test |
| * will now check that subsequent renders are similar. |
| * <p> |
| * The reason the test checks for similarity is that whenever rendering includes fonts, there are some |
| * platform differences in text rendering etc which does not give us a pixel for pixel match. |
| */ |
| public abstract class RenderTestBase extends AndroidTestCase { |
| protected static final String DEFAULT_DEVICE_ID = "Nexus 4"; |
| private static final String DEFAULT_THEME_STYLE = "@android:style/Theme.Holo"; |
| private static final float MAX_PERCENT_DIFFERENT = 5.0f; |
| |
| @Override |
| protected boolean requireRecentSdk() { |
| return true; |
| } |
| |
| protected RenderTask createRenderTask(VirtualFile file) throws Exception { |
| Configuration configuration = getConfiguration(file, DEFAULT_DEVICE_ID, DEFAULT_THEME_STYLE); |
| return createRenderTask(file, configuration); |
| } |
| |
| protected Configuration getConfiguration(VirtualFile file, String deviceId) { |
| AndroidFacet facet = AndroidFacet.getInstance(myModule); |
| assertNotNull(facet); |
| ConfigurationManager configurationManager = facet.getConfigurationManager(); |
| assertNotNull(configurationManager); |
| Configuration configuration = configurationManager.getConfiguration(file); |
| configuration.setDevice(findDeviceById(configurationManager, deviceId), false); |
| |
| return configuration; |
| } |
| |
| protected Configuration getConfiguration(VirtualFile file, String deviceId, String themeStyle) { |
| Configuration configuration = getConfiguration(file, deviceId); |
| configuration.setTheme(themeStyle); |
| return configuration; |
| } |
| |
| protected RenderTask createRenderTask(VirtualFile file, Configuration configuration) throws IOException { |
| AndroidFacet facet = AndroidFacet.getInstance(myModule); |
| PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file); |
| assertNotNull(psiFile); |
| assertNotNull(facet); |
| RenderService renderService = RenderService.get(facet); |
| RenderLogger logger = renderService.createLogger(); |
| final RenderTask task = renderService.createTask(psiFile, configuration, logger, null); |
| assertNotNull(task); |
| return task; |
| } |
| |
| protected void checkRendering(RenderTask task, String thumbnailPath) throws IOException { |
| // Next try a render |
| RenderResult result = task.render(); |
| RenderResult render = renderOnSeparateThread(task); |
| assertNotNull(render); |
| |
| assertNotNull(result); |
| RenderedImage image = result.getImage(); |
| assertNotNull(image); |
| image.setMaxSize(200, 200); |
| image.setDeviceFrameEnabled(false); |
| @SuppressWarnings("UndesirableClassUsage") // Don't want Retina images in unit tests |
| BufferedImage thumbnail = new BufferedImage(image.getRequiredWidth(), image.getRequiredHeight(), TYPE_INT_ARGB); |
| Graphics graphics = thumbnail.getGraphics(); |
| image.paint(graphics, 0, 0); |
| graphics.dispose(); |
| checkRenderedImage(thumbnail, "render" + separator + "thumbnails" + separator + thumbnailPath.replace('/', separatorChar)); |
| } |
| |
| @Nullable |
| public static RenderResult renderOnSeparateThread(@NotNull final RenderTask task) { |
| // Ensure that we don't render on the read lock (since we want to test that all parts of the |
| // rendering system which needs a read lock asks for one!) |
| final AtomicReference<RenderResult> holder = new AtomicReference<RenderResult>(); |
| Thread thread = new Thread() { |
| @Override |
| public void run() { |
| holder.set(task.render()); |
| } |
| }; |
| thread.start(); |
| try { |
| thread.join(); |
| } |
| catch (InterruptedException e) { |
| fail("Interrupted"); |
| } |
| |
| return holder.get(); |
| } |
| |
| @NotNull |
| protected static Device findDeviceById(ConfigurationManager manager, String id) { |
| for (Device device : manager.getDevices()) { |
| if (device.getId().equals(id)) { |
| return device; |
| } |
| } |
| fail("Can't find device " + id); |
| throw new IllegalStateException(); |
| } |
| |
| protected void checkRenderedImage(BufferedImage image, String relativePath) throws IOException { |
| relativePath = relativePath.replace('/', separatorChar); |
| |
| final String testDataPath = getTestDataPath(); |
| assert testDataPath != null : "test data path not specified"; |
| |
| File fromFile = new File(testDataPath + "/" + relativePath); |
| System.out.println("fromFile=" + fromFile); |
| |
| if (fromFile.exists()) { |
| BufferedImage goldenImage = ImageIO.read(fromFile); |
| assertImageSimilar(relativePath, goldenImage, image, MAX_PERCENT_DIFFERENT); |
| } else { |
| File dir = fromFile.getParentFile(); |
| assertNotNull(dir); |
| if (!dir.exists()) { |
| boolean ok = dir.mkdirs(); |
| assertTrue(dir.getPath(), ok); |
| } |
| ImageIO.write(image, "PNG", fromFile); |
| fail("File did not exist, created " + fromFile); |
| } |
| } |
| |
| public static void assertImageSimilar(String imageName, BufferedImage goldenImage, |
| BufferedImage image, double maxPercentDifferent) throws IOException { |
| assertEquals("Only TYPE_INT_ARGB image types are supported", TYPE_INT_ARGB, image.getType()); |
| |
| if (goldenImage.getType() != TYPE_INT_ARGB) { |
| @SuppressWarnings("UndesirableClassUsage") // Don't want Retina images in unit tests |
| BufferedImage temp = new BufferedImage(goldenImage.getWidth(), goldenImage.getHeight(), |
| TYPE_INT_ARGB); |
| temp.getGraphics().drawImage(goldenImage, 0, 0, null); |
| goldenImage = temp; |
| } |
| assertEquals(TYPE_INT_ARGB, goldenImage.getType()); |
| |
| int imageWidth = Math.min(goldenImage.getWidth(), image.getWidth()); |
| int imageHeight = Math.min(goldenImage.getHeight(), image.getHeight()); |
| |
| // Blur the images to account for the scenarios where there are pixel |
| // differences |
| // in where a sharp edge occurs |
| // goldenImage = blur(goldenImage, 6); |
| // image = blur(image, 6); |
| |
| int width = 3 * imageWidth; |
| @SuppressWarnings("UnnecessaryLocalVariable") |
| int height = imageHeight; // makes code more readable |
| @SuppressWarnings("UndesirableClassUsage") // Don't want Retina images in unit tests |
| BufferedImage deltaImage = new BufferedImage(width, height, TYPE_INT_ARGB); |
| Graphics g = deltaImage.getGraphics(); |
| |
| // Compute delta map |
| long delta = 0; |
| for (int y = 0; y < imageHeight; y++) { |
| for (int x = 0; x < imageWidth; x++) { |
| int goldenRgb = goldenImage.getRGB(x, y); |
| int rgb = image.getRGB(x, y); |
| if (goldenRgb == rgb) { |
| deltaImage.setRGB(imageWidth + x, y, 0x00808080); |
| continue; |
| } |
| |
| // If the pixels have no opacity, don't delta colors at all |
| if (((goldenRgb & 0xFF000000) == 0) && (rgb & 0xFF000000) == 0) { |
| deltaImage.setRGB(imageWidth + x, y, 0x00808080); |
| continue; |
| } |
| |
| int deltaR = ((rgb & 0xFF0000) >>> 16) - ((goldenRgb & 0xFF0000) >>> 16); |
| int newR = 128 + deltaR & 0xFF; |
| int deltaG = ((rgb & 0x00FF00) >>> 8) - ((goldenRgb & 0x00FF00) >>> 8); |
| int newG = 128 + deltaG & 0xFF; |
| int deltaB = (rgb & 0x0000FF) - (goldenRgb & 0x0000FF); |
| int newB = 128 + deltaB & 0xFF; |
| |
| int avgAlpha = ((((goldenRgb & 0xFF000000) >>> 24) |
| + ((rgb & 0xFF000000) >>> 24)) / 2) << 24; |
| |
| int newRGB = avgAlpha | newR << 16 | newG << 8 | newB; |
| deltaImage.setRGB(imageWidth + x, y, newRGB); |
| |
| delta += Math.abs(deltaR); |
| delta += Math.abs(deltaG); |
| delta += Math.abs(deltaB); |
| } |
| } |
| |
| // 3 different colors, 256 color levels |
| long total = imageHeight * imageWidth * 3L * 256L; |
| float percentDifference = (float) (delta * 100 / (double) total); |
| |
| String error = null; |
| if (percentDifference > maxPercentDifferent) { |
| error = String.format("Images differ (by %.1f%%)", percentDifference); |
| } else if (Math.abs(goldenImage.getWidth() - image.getWidth()) >= 2) { |
| error = "Widths differ too much for " + imageName + ": " + goldenImage.getWidth() + "x" + goldenImage.getHeight() + |
| "vs" + image.getWidth() + "x" + image.getHeight(); |
| } else if (Math.abs(goldenImage.getHeight() - image.getHeight()) >= 2) { |
| error = "Heights differ too much for " + imageName + ": " + goldenImage.getWidth() + "x" + goldenImage.getHeight() + |
| "vs" + image.getWidth() + "x" + image.getHeight(); |
| } |
| |
| assertEquals(TYPE_INT_ARGB, image.getType()); |
| if (error != null) { |
| // Expected on the left |
| // Golden on the right |
| g.drawImage(goldenImage, 0, 0, null); |
| g.drawImage(image, 2 * imageWidth, 0, null); |
| |
| // Labels |
| if (imageWidth > 80) { |
| g.setColor(Color.RED); |
| g.drawString("Expected", 10, 20); |
| g.drawString("Actual", 2 * imageWidth + 10, 20); |
| } |
| |
| File output = new File(getTempDir(), "delta-"+ imageName.replace(separatorChar, '_')); |
| if (output.exists()) { |
| boolean deleted = output.delete(); |
| assertTrue(deleted); |
| } |
| ImageIO.write(deltaImage, "PNG", output); |
| error += " - see details in " + output.getPath(); |
| System.out.println(error); |
| fail(error); |
| } |
| |
| g.dispose(); |
| } |
| |
| @NotNull |
| public static File getTempDir() { |
| if (System.getProperty("os.name").equals("Mac OS X")) { |
| return new File("/tmp"); //$NON-NLS-1$ |
| } |
| |
| return new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ |
| } |
| } |