/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package libcore.java.nio.charset; import org.junit.Test; import org.junit.runner.RunWith; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Tests for the encoding behavior of charsets in {@link StandardCharsets}. */ @RunWith(DataProviderRunner.class) public class StandardCharsetsEncoderTest { private static final String DELIMITER = ":"; /** Big enough for a single codepoint */ private static final CharBuffer CHAR_BUFFER = CharBuffer.allocate(5); /** Big enough for the encoding for a single code point */ private static final ByteBuffer BYTE_BUFFER = ByteBuffer.allocate(5); /** * Returns the charsets to test. */ @DataProvider public static Charset[] getStandardCharsets() throws Exception { List<Charset> charsets = new ArrayList<>(); for (Field field : StandardCharsets.class.getFields()) { if (field.getType() == Charset.class) { charsets.add((Charset) field.get(null)); } } assertTrue(charsets.size() >= 6); return charsets.toArray(new Charset[0]); } @DataProvider public static Object[][] provider_getStandardCharsets() throws Exception { Charset[] charsets = getStandardCharsets(); int n = charsets.length; Object[][] result = new Object[n][1]; for (int i = 0; i < n; i++) { result[i][0] = charsets[i]; } return result; } /** * Tests recorded reference data against actual encoder behavior. * Reference data files can be created / updated using {@link Dumper} to dump existing Android * behavior. */ @UseDataProvider("provider_getStandardCharsets") @Test public void testCharset(Charset charset) throws Exception { String fileName = createFileName(charset); CharsetEncoder encoder = charset.newEncoder(); List<String> failures = new ArrayList<>(); try (BufferedReader reader = createAsciiReader(openResource(fileName))) { String expectedInfo; while ((expectedInfo = reader.readLine()) != null) { // Ignore comment lines if (expectedInfo.startsWith("#")) { continue; } String[] parts = expectedInfo.split(DELIMITER); int codePoint = Integer.parseInt(parts[0]); String actualInfo = createCodePointInfo(encoder, codePoint); if (!expectedInfo.equals(actualInfo)) { failures.add("Expected=" + expectedInfo + ", actual=" + actualInfo); } } } if (!failures.isEmpty()) { fail("Failures:\n" + failures); } } private static InputStream openResource(String fileName) { InputStream is = StandardCharsetsEncoderTest.class.getResourceAsStream(fileName); if (is == null) { fail("No resource found: " + fileName); } return is; } /** * Generates reference data for use by {@link #testCharset(Charset)}. Run the main method in * this class to obtain new reference files from current Android behavior. Pass the name of the * directory in which to create files. * * <p>For example: * <pre> * make vogar && make build-art-host && make core-tests * vogar --mode host --runner-type main \ * --classpath \ * ${ANDROID_PRODUCT_OUT}/obj/JAVA_LIBRARIES/core-tests_intermediates/javalib.jar \ * 'libcore.java.nio.charset.StandardCharsetsEncoderTest$Dumper' \ * -- libcore/luni/src/test/resources/libcore/java/nio/charset * </pre> */ public static class Dumper { public static void main(String[] args) throws Exception { String dir = args[0]; for (Charset charset : getStandardCharsets()) { CharsetEncoder coreEncoder = charset.newEncoder(); dumpEncodings(coreEncoder, dir + "/" + createFileName(charset)); } } /** * Generates a set of reference data from the current CharsetEncoder behavior into the * specified file. */ private static void dumpEncodings(CharsetEncoder encoder, String fileName) throws IOException { encoder.onMalformedInput(CodingErrorAction.IGNORE); encoder.onUnmappableCharacter(CodingErrorAction.IGNORE); try (BufferedWriter writer = createAsciiWriter(new FileOutputStream(fileName))) { writeLine(writer, "# Reference encodings for " + encoder.charset().name() + " generated by " + Dumper.class); writeLine(writer, "# Encodings are used by " + StandardCharsetsEncoderTest.class); writeLine(writer, "# {codepoint}:{canEncode}:{encoding bytes}"); for (int codePoint = 0; codePoint < 0xfffd; codePoint++) { String codePointInfo = createCodePointInfo(encoder, codePoint); writeLine(writer, codePointInfo); } } } private static void writeLine(BufferedWriter writer, String text) throws IOException { writer.append(text); writer.newLine(); } } private static String createCodePointInfo(CharsetEncoder encoder, int codePoint) { StringBuilder stringBuilder = new StringBuilder(); String utf16 = new String(Character.toChars(codePoint)); // Format: {codepoint:int}:{canEncode():bool}:{encode():bytes} stringBuilder.append(Integer.toString(codePoint)); stringBuilder.append(DELIMITER); stringBuilder.append(Boolean.toString(encoder.canEncode(utf16))); stringBuilder.append(DELIMITER); // Encode CHAR_BUFFER.append(utf16); CHAR_BUFFER.flip(); encoder.encode(CHAR_BUFFER, BYTE_BUFFER, true /* endOfInput */); encoder.reset(); BYTE_BUFFER.flip(); // Append the encoded bytes, if any. byte[] bytes = new byte[BYTE_BUFFER.limit()]; BYTE_BUFFER.get(bytes, 0, BYTE_BUFFER.limit()); stringBuilder.append(createBytesString(bytes)); CHAR_BUFFER.clear(); BYTE_BUFFER.clear(); return stringBuilder.toString(); } private static String createBytesString(byte[] bytes) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { int byteValues = Byte.toUnsignedInt(bytes[i]); builder.append(Integer.toString(byteValues)); if (i < bytes.length - 1) { builder.append(','); } } return builder.toString(); } private static BufferedWriter createAsciiWriter(OutputStream out) { return new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII)); } private static BufferedReader createAsciiReader(InputStream in) { return new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII)); } private static String createFileName(Charset charset) { return "encodings_" + charset.name() + ".txt"; } }