/* * Copyright 2013-present Facebook, Inc. * * 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.facebook.buck.android; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import com.facebook.buck.android.StringResources.Gender; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.TestExecutionContext; import com.facebook.buck.testutil.FakeProjectFilesystem; import com.facebook.buck.testutil.integration.TestDataHelper; import com.facebook.buck.util.XmlDomParser; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.io.Files; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import org.easymock.EasyMockSupport; import org.junit.Before; import org.junit.Test; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class CompileStringsStepTest extends EasyMockSupport { private static final String XML_HEADER = "<?xml version='1.0' encoding='utf-8'?>"; private Path testdataDir; private Path firstFile; private Path secondFile; private Path thirdFile; private Path fourthFile; private Path fifthFile; @Before public void findTestData() { testdataDir = TestDataHelper.getTestDataDirectory(this).resolve("compile_strings"); firstFile = testdataDir.resolve("first/res/values-es/strings.xml"); secondFile = testdataDir.resolve("second/res/values-es/strings.xml"); thirdFile = testdataDir.resolve("third/res/values-pt/strings.xml"); fourthFile = testdataDir.resolve("third/res/values-pt-rBR/strings.xml"); fifthFile = testdataDir.resolve("third/res/values/strings.xml"); } @Test public void testStringFilePattern() { testStringPathRegex("res/values-es/strings.xml", true, "es", null); testStringPathRegex("/one/res/values-es/strings.xml", true, "es", null); testStringPathRegex("/two/res/values-es-rUS/strings.xml", true, "es", "US"); // Not matching strings. testStringPathRegex("/one/res/values/strings.xml", false, null, null); testStringPathRegex("/one/res/values-/strings.xml", false, null, null); testStringPathRegex("/one/res/values-e/strings.xml", false, null, null); testStringPathRegex("/one/res/values-esc/strings.xml", false, null, null); testStringPathRegex("/one/res/values-es-rU/strings.xml", false, null, null); testStringPathRegex("/one/res/values-es-rUSA/strings.xml", false, null, null); testStringPathRegex("/one/res/values-es-RUS/strings.xml", false, null, null); testStringPathRegex("/one/res/values-rUS/strings.xml", false, null, null); } private void testStringPathRegex(String input, boolean matches, String locale, String country) { Matcher matcher = CompileStringsStep.NON_ENGLISH_STRING_FILE_PATTERN.matcher(input); assertEquals(matches, matcher.matches()); if (!matches) { return; } assertEquals(locale, matcher.group(1)); assertEquals(country, matcher.group(2)); } @Test public void testRDotTxtContentsPattern() { testContentRegex(" int string r_name 0xdeadbeef", false, null, null, null); testContentRegex("int string r_name 0xdeadbeef ", false, null, null, null); testContentRegex("int string r_name 0xdeadbeef", true, "string", "r_name", "deadbeef"); testContentRegex("int string r_name 0x", false, null, null, null); testContentRegex("int array r_name 0xdead", true, "array", "r_name", "dead"); testContentRegex("int plurals r_name 0xdead", true, "plurals", "r_name", "dead"); testContentRegex("int plural r_name 0xdead", false, null, null, null); testContentRegex("int plurals r name 0xdead", false, null, null, null); testContentRegex("int[] string r_name 0xdead", false, null, null, null); } private void testContentRegex( String input, boolean matches, String resourceType, String resourceName, String resourceId) { Matcher matcher = CompileStringsStep.R_DOT_TXT_STRING_RESOURCE_PATTERN.matcher(input); assertEquals(matches, matcher.matches()); if (!matches) { return; } assertEquals("Resource type does not match.", resourceType, matcher.group(1)); assertEquals("Resource name does not match.", resourceName, matcher.group(2)); assertEquals("Resource id does not match.", resourceId, matcher.group(3)); } @Test public void testGroupFilesByLocale() { Path path0 = Paths.get("project/dir/res/values-da/strings.xml"); Path path1 = Paths.get("project/dir/res/values-da-rAB/strings.xml"); Path path2 = Paths.get("project/dir/res/values/strings.xml"); Path path3 = Paths.get("project/groupme/res/values-da/strings.xml"); Path path4 = Paths.get("project/groupmetoo/res/values-da-rAB/strings.xml"); Path path5 = Paths.get("project/foreveralone/res/values-es/strings.xml"); ImmutableList<Path> files = ImmutableList.of(path0, path1, path2, path3, path4, path5); ImmutableMultimap<String, Path> groupedByLocale = createNonExecutingStep().groupFilesByLocale(ImmutableList.copyOf(files)); ImmutableMultimap<String, Path> expectedMap = ImmutableMultimap.<String, Path>builder() .putAll("da", ImmutableSet.of(path0, path3)) .putAll("da_AB", ImmutableSet.of(path1, path4)) .putAll("es", ImmutableSet.of(path5)) .putAll("en", ImmutableSet.of(path2)) .build(); assertEquals( "Result of CompileStringsStep.groupFilesByLocale() should match the expected value.", expectedMap, groupedByLocale); } @Test public void testScrapeStringNodes() throws IOException, SAXException { String xmlInput = "<string name='name1' gender='unknown'>Value1</string>" + "<string name='name1_f1gender' gender='female'>Value1_f1</string>" + "<string name='name2' gender='unknown'>Value with space</string>" + "<string name='name2_m2gender' gender='male'>Value with space m2</string>" + "<string name='name3' gender='unknown'>Value with \"quotes\"</string>" + "<string name='name4' gender='unknown'></string>" + // ignored because "name3" already found "<string name='name3' gender='unknown'>IGNORE</string>" + "<string name='name5' gender='unknown'>Value with %1$s</string>"; NodeList stringNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("string"); EnumMap<Gender, String> map1 = Maps.newEnumMap(Gender.class); map1.put(Gender.unknown, "Value1"); map1.put(Gender.female, "Value1_f1"); EnumMap<Gender, String> map2 = Maps.newEnumMap(Gender.class); map2.put(Gender.unknown, "Value with space"); map2.put(Gender.male, "Value with space m2"); EnumMap<Gender, String> map3 = Maps.newEnumMap(Gender.class); map3.put(Gender.unknown, "Value with \"quotes\""); EnumMap<Gender, String> map4 = Maps.newEnumMap(Gender.class); map4.put(Gender.unknown, ""); EnumMap<Gender, String> map5 = Maps.newEnumMap(Gender.class); map5.put(Gender.unknown, "Value with %1$s"); Map<Integer, EnumMap<Gender, String>> stringsMap = new HashMap<>(); CompileStringsStep step = createNonExecutingStep(); step.addStringResourceNameToIdMap( ImmutableMap.of( "name1", 1, "name2", 2, "name3", 3, "name4", 4, "name5", 5)); step.scrapeStringNodes(stringNodes, stringsMap); assertEquals( "Incorrect map of resource id to string values.", ImmutableMap.of( 1, map1, 2, map2, 3, map3, 4, map4, 5, map5), stringsMap); } @Test public void testScrapePluralsNodes() throws IOException, SAXException { String xmlInput = "<plurals name='name1' gender='unknown'>" + "<item quantity='zero'>%d people saw this</item>" + "<item quantity='one'>%d person saw this</item>" + "<item quantity='many'>%d people saw this</item>" + "</plurals>" + "<plurals name='name1_f1gender' gender='female'>" + "<item quantity='zero'>%d people saw this f1</item>" + "<item quantity='one'>%d person saw this f1</item>" + "<item quantity='many'>%d people saw this f1</item>" + "</plurals>" + "<plurals name='name2' gender='unknown'>" + "<item quantity='zero'>%d people ate this</item>" + "<item quantity='many'>%d people ate this</item>" + "</plurals>" + "<plurals name='name2_m2gender' gender='male'>" + "<item quantity='zero'>%d people ate this m2</item>" + "<item quantity='many'>%d people ate this m2</item>" + "</plurals>" + "<plurals name='name3' gender='unknown'></plurals>" + // Test empty array. // Ignored since "name2" already found. "<plurals name='name2' gender='unknown'></plurals>"; NodeList pluralsNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("plurals"); EnumMap<Gender, ImmutableMap<String, String>> map1 = Maps.newEnumMap(Gender.class); map1.put( Gender.unknown, ImmutableMap.of( "zero", "%d people saw this", "one", "%d person saw this", "many", "%d people saw this")); map1.put( Gender.female, ImmutableMap.of( "zero", "%d people saw this f1", "one", "%d person saw this f1", "many", "%d people saw this f1")); EnumMap<Gender, ImmutableMap<String, String>> map2 = Maps.newEnumMap(Gender.class); map2.put( Gender.unknown, ImmutableMap.of( "zero", "%d people ate this", "many", "%d people ate this")); map2.put( Gender.male, ImmutableMap.of( "zero", "%d people ate this m2", "many", "%d people ate this m2")); EnumMap<Gender, ImmutableMap<String, String>> map3 = Maps.newEnumMap(Gender.class); map3.put(Gender.unknown, ImmutableMap.of()); Map<Integer, EnumMap<Gender, ImmutableMap<String, String>>> pluralsMap = new HashMap<>(); CompileStringsStep step = createNonExecutingStep(); step.addPluralsResourceNameToIdMap( ImmutableMap.of( "name1", 1, "name2", 2, "name3", 3)); step.scrapePluralsNodes(pluralsNodes, pluralsMap); assertEquals( "Incorrect map of resource id to plural values.", ImmutableMap.of( 1, map1, 2, map2, 3, map3), pluralsMap); } @Test public void testScrapeStringArrayNodes() throws IOException, SAXException { String xmlInput = "<string-array name='name1' gender='unknown'>" + "<item>Value12</item>" + "<item>Value11</item>" + "</string-array>" + "<string-array name='name1_f1gender' gender='female'>" + "<item>Value12 f1</item>" + "<item>Value11 f1</item>" + "</string-array>" + "<string-array name='name2' gender='unknown'>" + "<item>Value21</item>" + "</string-array>" + "<string-array name='name2_m2gender' gender='male'>" + "<item>Value21 m2</item>" + "</string-array>" + "<string-array name='name3' gender='unknown'></string-array>" + "<string-array name='name2' gender='unknown'>" + "<item>ignored</item>" + // Ignored because "name2" already found above. "</string-array>"; EnumMap<Gender, List<String>> map1 = Maps.newEnumMap(Gender.class); map1.put(Gender.unknown, ImmutableList.of("Value12", "Value11")); map1.put(Gender.female, ImmutableList.of("Value12 f1", "Value11 f1")); EnumMap<Gender, List<String>> map2 = Maps.newEnumMap(Gender.class); map2.put(Gender.unknown, ImmutableList.of("Value21")); map2.put(Gender.male, ImmutableList.of("Value21 m2")); NodeList arrayNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("string-array"); Map<Integer, EnumMap<Gender, ImmutableList<String>>> arraysMap = new TreeMap<>(); CompileStringsStep step = createNonExecutingStep(); step.addArrayResourceNameToIdMap( ImmutableMap.of( "name1", 1, "name2", 2, "name3", 3)); step.scrapeStringArrayNodes(arrayNodes, arraysMap); assertEquals( "Incorrect map of resource id to string arrays.", ImmutableMap.of(1, map1, 2, map2), arraysMap); } @Test public void testScrapeNodesWithSameName() throws IOException, SAXException { String xmlInput = "<string name='name1' gender='unknown'>1</string>" + "<string name='name1_f1gender' gender='female'>1 f1</string>" + "<plurals name='name1' gender='unknown'>" + "<item quantity='one'>2</item>" + "<item quantity='other'>3</item>" + "</plurals>" + "<plurals name='name1_f1gender' gender='female'>" + "<item quantity='one'>2 f1</item>" + "<item quantity='other'>3 f1</item>" + "</plurals>" + "<string-array name='name1' gender='unknown'>" + "<item>4</item>" + "<item>5</item>" + "</string-array>" + "<string-array name='name1_f1gender' gender='female'>" + "<item>4 f1</item>" + "<item>5 f1</item>" + "</string-array>"; NodeList stringNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("string"); NodeList pluralsNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("plurals"); NodeList arrayNodes = XmlDomParser.parse(createResourcesXml(xmlInput)).getElementsByTagName("string-array"); Map<Integer, EnumMap<Gender, String>> stringMap = new TreeMap<>(); Map<Integer, EnumMap<Gender, ImmutableMap<String, String>>> pluralsMap = new TreeMap<>(); Map<Integer, EnumMap<Gender, ImmutableList<String>>> arraysMap = new TreeMap<>(); EnumMap<Gender, String> map1 = Maps.newEnumMap(Gender.class); map1.put(Gender.unknown, "1"); map1.put(Gender.female, "1 f1"); EnumMap<Gender, Map<String, String>> map2 = Maps.newEnumMap(Gender.class); map2.put(Gender.unknown, ImmutableMap.of("one", "2", "other", "3")); map2.put(Gender.female, ImmutableMap.of("one", "2 f1", "other", "3 f1")); EnumMap<Gender, ImmutableList<String>> map3 = Maps.newEnumMap(Gender.class); map3.put(Gender.unknown, ImmutableList.of("4", "5")); map3.put(Gender.female, ImmutableList.of("4 f1", "5 f1")); CompileStringsStep step = createNonExecutingStep(); step.addStringResourceNameToIdMap(ImmutableMap.of("name1", 1)); step.addPluralsResourceNameToIdMap(ImmutableMap.of("name1", 2)); step.addArrayResourceNameToIdMap(ImmutableMap.of("name1", 3)); step.scrapeStringNodes(stringNodes, stringMap); step.scrapePluralsNodes(pluralsNodes, pluralsMap); step.scrapeStringArrayNodes(arrayNodes, arraysMap); assertEquals("Incorrect map of resource id to string.", ImmutableMap.of(1, map1), stringMap); assertEquals("Incorrect map of resource id to plurals.", ImmutableMap.of(2, map2), pluralsMap); assertEquals( "Incorrect map of resource id to string arrays.", ImmutableMap.of(3, map3), arraysMap); } private CompileStringsStep createNonExecutingStep() { return new CompileStringsStep( new FakeProjectFilesystem(), ImmutableList.of(), createMock(Path.class), locale -> { throw new UnsupportedOperationException(); }); } private String createResourcesXml(String contents) { return XML_HEADER + "<resources>" + contents + "</resources>"; } @Test public void testSuccessfulStepExecution() throws InterruptedException, IOException { final Path destinationDir = Paths.get(""); Path rDotJavaSrcDir = Paths.get(""); ExecutionContext context = TestExecutionContext.newInstance(); FakeProjectFileSystem fileSystem = new FakeProjectFileSystem(); ImmutableList<Path> stringFiles = ImmutableList.of(firstFile, secondFile, thirdFile, fourthFile, fifthFile); replayAll(); CompileStringsStep step = new CompileStringsStep( fileSystem, stringFiles, rDotJavaSrcDir.resolve("R.txt"), input -> destinationDir.resolve(input + PackageStringAssets.STRING_ASSET_FILE_EXTENSION)); assertEquals(0, step.execute(context).getExitCode()); Map<String, byte[]> fileContentsMap = fileSystem.getFileContents(); assertEquals("Incorrect number of string files written.", 4, fileContentsMap.size()); for (Map.Entry<String, byte[]> entry : fileContentsMap.entrySet()) { File expectedFile = testdataDir.resolve(entry.getKey()).toFile(); assertArrayEquals(createBinaryStream(expectedFile), fileContentsMap.get(entry.getKey())); } verifyAll(); } private byte[] createBinaryStream(File expectedFile) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream stream = new DataOutputStream(bos)) { for (String line : Files.readLines(expectedFile, Charset.defaultCharset())) { for (String token : Splitter.on('|').split(line)) { char dataType = token.charAt(0); String value = token.substring(2); switch (dataType) { case 'i': stream.writeInt(Integer.parseInt(value)); break; case 's': stream.writeShort(Integer.parseInt(value)); break; case 'b': stream.writeByte(Integer.parseInt(value)); break; case 't': stream.write(value.getBytes(StandardCharsets.UTF_8)); break; default: throw new RuntimeException("Unexpected data type in .fbstr file: " + dataType); } } } return bos.toByteArray(); } } private class FakeProjectFileSystem extends ProjectFilesystem { private ImmutableMap.Builder<String, byte[]> fileContentsMapBuilder = ImmutableMap.builder(); public FakeProjectFileSystem() throws InterruptedException { super(Paths.get(".").toAbsolutePath()); } @Override public List<String> readLines(Path path) throws IOException { Path fullPath = testdataDir.resolve(path); return Files.readLines(fullPath.toFile(), Charset.defaultCharset()); } @Override public void writeBytesToPath(byte[] content, Path path, FileAttribute<?>... attrs) { fileContentsMapBuilder.put(path.getFileName().toString(), content); } public Map<String, byte[]> getFileContents() { return fileContentsMapBuilder.build(); } } }