diff options
author | 2024-12-16 21:45:41 -0500 | |
---|---|---|
committer | 2025-01-11 14:17:35 -0500 | |
commit | e9a0e66716dab4dd3184d009d8920de1961efdfa (patch) | |
tree | 02dcc096643d74645bf28459c2834c3d4a2ad7f2 /tests/src/org/kelar/inputmethod/latin | |
parent | fb3b9360d70596d7e921de8bf7d3ca99564a077e (diff) | |
download | latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip |
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'tests/src/org/kelar/inputmethod/latin')
64 files changed, 14284 insertions, 0 deletions
diff --git a/tests/src/org/kelar/inputmethod/latin/AppWorkaroundsTests.java b/tests/src/org/kelar/inputmethod/latin/AppWorkaroundsTests.java new file mode 100644 index 000000000..038b38735 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/AppWorkaroundsTests.java @@ -0,0 +1,75 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build.VERSION_CODES; +import android.view.inputmethod.EditorInfo; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.settings.Settings; + +@LargeTest +public class AppWorkaroundsTests extends InputTestsBase { + String packageNameOfAppBeforeJellyBean; + String packageNameOfAppAfterJellyBean; + + @Override + protected void setUp() throws Exception { + // NOTE: this will fail if there is no app installed that targets an SDK + // before Jelly Bean. For the moment, it's fine. + final PackageManager pm = getContext().getPackageManager(); + for (ApplicationInfo ai : pm.getInstalledApplications(0 /* flags */)) { + if (ai.targetSdkVersion < VERSION_CODES.JELLY_BEAN) { + packageNameOfAppBeforeJellyBean = ai.packageName; + } else { + packageNameOfAppAfterJellyBean = ai.packageName; + } + } + super.setUp(); + } + + // We want to test if the app package info is correctly retrieved by LatinIME. Since it + // asks this information to the package manager from the package name, and that it takes + // the package name from the EditorInfo, all we have to do it put the correct package + // name in the editor info. + // To this end, our base class InputTestsBase offers a hook for us to touch the EditorInfo. + // We override this hook to write the package name that we need. + @Override + protected EditorInfo enrichEditorInfo(final EditorInfo ei) { + if ("testBeforeJellyBeanTrue".equals(getName())) { + ei.packageName = packageNameOfAppBeforeJellyBean; + } else if ("testBeforeJellyBeanFalse".equals(getName())) { + ei.packageName = packageNameOfAppAfterJellyBean; + } + return ei; + } + + public void testBeforeJellyBeanTrue() { + assertTrue("Couldn't successfully detect this app targets < Jelly Bean (package is " + + packageNameOfAppBeforeJellyBean + ")", + Settings.getInstance().getCurrent().isBeforeJellyBean()); + } + + public void testBeforeJellyBeanFalse() { + assertFalse("Couldn't successfully detect this app targets >= Jelly Bean (package is " + + packageNameOfAppAfterJellyBean + ")", + Settings.getInstance().getCurrent().isBeforeJellyBean()); + } +}
\ No newline at end of file diff --git a/tests/src/org/kelar/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/org/kelar/inputmethod/latin/BinaryDictionaryTests.java new file mode 100644 index 000000000..a44c0bce7 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/BinaryDictionaryTests.java @@ -0,0 +1,913 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.text.TextUtils; +import android.util.Pair; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.NgramContext.WordInfo; +import org.kelar.inputmethod.latin.common.CodePointUtils; +import org.kelar.inputmethod.latin.common.FileUtils; +import org.kelar.inputmethod.latin.makedict.DictionaryHeader; +import org.kelar.inputmethod.latin.makedict.FormatSpec; +import org.kelar.inputmethod.latin.makedict.WeightedString; +import org.kelar.inputmethod.latin.makedict.WordProperty; +import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Random; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class BinaryDictionaryTests { + private static final String TEST_DICT_FILE_EXTENSION = ".testDict"; + private static final String TEST_LOCALE = "test"; + private static final String DICTIONARY_ID = "TestBinaryDictionary"; + + private HashSet<File> mDictFilesToBeDeleted = new HashSet<>(); + + @Before + public void setUp() throws Exception { + mDictFilesToBeDeleted.clear(); + } + + @After + public void tearDown() throws Exception { + for (final File dictFile : mDictFilesToBeDeleted) { + dictFile.delete(); + } + mDictFilesToBeDeleted.clear(); + } + + private File createEmptyDictionaryAndGetFile(final int formatVersion) { + return createEmptyDictionaryWithAttributesAndGetFile(formatVersion, + new HashMap<String, String>()); + } + + private File createEmptyDictionaryWithAttributesAndGetFile(final int formatVersion, + final HashMap<String, String> attributeMap) { + try { + final File dictFile = createEmptyVer4DictionaryAndGetFile(formatVersion, + attributeMap); + mDictFilesToBeDeleted.add(dictFile); + return dictFile; + } catch (final IOException e) { + fail(e.toString()); + } + return null; + } + + private File createEmptyVer4DictionaryAndGetFile(final int formatVersion, + final HashMap<String, String> attributeMap) throws IOException { + final File file = File.createTempFile(DICTIONARY_ID, TEST_DICT_FILE_EXTENSION, + InstrumentationRegistry.getTargetContext().getCacheDir()); + file.delete(); + file.mkdir(); + if (BinaryDictionaryUtils.createEmptyDictFile(file.getAbsolutePath(), formatVersion, + Locale.ENGLISH, attributeMap)) { + return file; + } + throw new IOException("Empty dictionary " + file.getAbsolutePath() + + " cannot be created. Format version: " + formatVersion); + } + + private static BinaryDictionary getBinaryDictionary(final File dictFile) { + return new BinaryDictionary(dictFile.getAbsolutePath(), + 0 /* offset */, dictFile.length(), true /* useFullEditDistance */, + Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */); + } + + private BinaryDictionary getEmptyBinaryDictionary(final int formatVersion) { + final File dictFile = createEmptyDictionaryAndGetFile(formatVersion); + return new BinaryDictionary(dictFile.getAbsolutePath(), + 0 /* offset */, dictFile.length(), true /* useFullEditDistance */, + Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */); + } + + @Test + public void testIsValidDictionary() { + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + assertTrue("binaryDictionary must be valid for existing valid dictionary file.", + binaryDictionary.isValidDictionary()); + binaryDictionary.close(); + assertFalse("binaryDictionary must be invalid after closing.", + binaryDictionary.isValidDictionary()); + FileUtils.deleteRecursively(dictFile); + binaryDictionary = getBinaryDictionary(dictFile); + assertFalse("binaryDictionary must be invalid for not existing dictionary file.", + binaryDictionary.isValidDictionary()); + binaryDictionary.close(); + } + + @Test + public void testConstructingDictionaryOnMemory() { + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + FileUtils.deleteRecursively(dictFile); + assertFalse(dictFile.exists()); + final BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(), + true /* useFullEditDistance */, Locale.getDefault(), TEST_LOCALE, + FormatSpec.VERSION403, new HashMap<String, String>()); + assertTrue(binaryDictionary.isValidDictionary()); + assertEquals(FormatSpec.VERSION403, binaryDictionary.getFormatVersion()); + final int probability = 100; + addUnigramWord(binaryDictionary, "word", probability); + assertEquals(probability, binaryDictionary.getFrequency("word")); + assertFalse(dictFile.exists()); + binaryDictionary.flush(); + assertTrue(dictFile.exists()); + assertTrue(binaryDictionary.isValidDictionary()); + assertEquals(FormatSpec.VERSION403, binaryDictionary.getFormatVersion()); + assertEquals(probability, binaryDictionary.getFrequency("word")); + binaryDictionary.close(); + } + + @Test + public void testAddTooLongWord() { + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + final StringBuffer stringBuilder = new StringBuffer(); + for (int i = 0; i < BinaryDictionary.DICTIONARY_MAX_WORD_LENGTH; i++) { + stringBuilder.append('a'); + } + final String validLongWord = stringBuilder.toString(); + stringBuilder.append('a'); + final String invalidLongWord = stringBuilder.toString(); + final int probability = 100; + addUnigramWord(binaryDictionary, "aaa", probability); + addUnigramWord(binaryDictionary, validLongWord, probability); + addUnigramWord(binaryDictionary, invalidLongWord, probability); + // Too long short cut. + binaryDictionary.addUnigramEntry("a", probability, false /* isBeginningOfSentence */, + false /* isNotAWord */, false /* isPossiblyOffensive */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); + addUnigramWord(binaryDictionary, "abc", probability); + final int updatedProbability = 200; + // Update. + addUnigramWord(binaryDictionary, validLongWord, updatedProbability); + addUnigramWord(binaryDictionary, invalidLongWord, updatedProbability); + addUnigramWord(binaryDictionary, "abc", updatedProbability); + + assertEquals(probability, binaryDictionary.getFrequency("aaa")); + assertEquals(updatedProbability, binaryDictionary.getFrequency(validLongWord)); + assertEquals(Dictionary.NOT_A_PROBABILITY, binaryDictionary.getFrequency(invalidLongWord)); + assertEquals(updatedProbability, binaryDictionary.getFrequency("abc")); + } + + private static void addUnigramWord(final BinaryDictionary binaryDictionary, final String word, + final int probability) { + binaryDictionary.addUnigramEntry(word, probability, + false /* isBeginningOfSentence */, false /* isNotAWord */, + false /* isPossiblyOffensive */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + } + + private static void addBigramWords(final BinaryDictionary binaryDictionary, final String word0, + final String word1, final int probability) { + binaryDictionary.addNgramEntry(new NgramContext(new WordInfo(word0)), word1, probability, + BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + } + + private static void addTrigramEntry(final BinaryDictionary binaryDictionary, final String word0, + final String word1, final String word2, final int probability) { + binaryDictionary.addNgramEntry( + new NgramContext(new WordInfo(word1), new WordInfo(word0)), word2, + probability, BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + } + + private static boolean isValidBigram(final BinaryDictionary binaryDictionary, + final String word0, final String word1) { + return binaryDictionary.isValidNgram(new NgramContext(new WordInfo(word0)), word1); + } + + private static int getBigramProbability(final BinaryDictionary binaryDictionary, + final String word0, final String word1) { + return binaryDictionary.getNgramProbability(new NgramContext(new WordInfo(word0)), word1); + } + + private static int getTrigramProbability(final BinaryDictionary binaryDictionary, + final String word0, final String word1, final String word2) { + return binaryDictionary.getNgramProbability( + new NgramContext(new WordInfo(word1), new WordInfo(word0)), word2); + } + + @Test + public void testAddUnigramWord() { + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + final int probability = 100; + addUnigramWord(binaryDictionary, "aaa", probability); + // Reallocate and create. + addUnigramWord(binaryDictionary, "aab", probability); + // Insert into children. + addUnigramWord(binaryDictionary, "aac", probability); + // Make terminal. + addUnigramWord(binaryDictionary, "aa", probability); + // Create children. + addUnigramWord(binaryDictionary, "aaaa", probability); + // Reallocate and make termianl. + addUnigramWord(binaryDictionary, "a", probability); + + final int updatedProbability = 200; + // Update. + addUnigramWord(binaryDictionary, "aaa", updatedProbability); + + assertEquals(probability, binaryDictionary.getFrequency("aab")); + assertEquals(probability, binaryDictionary.getFrequency("aac")); + assertEquals(probability, binaryDictionary.getFrequency("aa")); + assertEquals(probability, binaryDictionary.getFrequency("aaaa")); + assertEquals(probability, binaryDictionary.getFrequency("a")); + assertEquals(updatedProbability, binaryDictionary.getFrequency("aaa")); + } + + @Test + public void testRandomlyAddUnigramWord() { + final int wordCount = 1000; + final int codePointSetSize = 50; + final long seed = System.currentTimeMillis(); + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + + final HashMap<String, Integer> probabilityMap = new HashMap<>(); + // Test a word that isn't contained within the dictionary. + final Random random = new Random(seed); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + for (int i = 0; i < wordCount; ++i) { + final String word = CodePointUtils.generateWord(random, codePointSet); + probabilityMap.put(word, random.nextInt(0xFF)); + } + for (String word : probabilityMap.keySet()) { + addUnigramWord(binaryDictionary, word, probabilityMap.get(word)); + } + for (String word : probabilityMap.keySet()) { + assertEquals(word, (int)probabilityMap.get(word), binaryDictionary.getFrequency(word)); + } + } + + @Test + public void testAddBigramWords() { + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + + final int unigramProbability = 100; + final int bigramProbability = 150; + final int updatedBigramProbability = 200; + addUnigramWord(binaryDictionary, "aaa", unigramProbability); + addUnigramWord(binaryDictionary, "abb", unigramProbability); + addUnigramWord(binaryDictionary, "bcc", unigramProbability); + addBigramWords(binaryDictionary, "aaa", "abb", bigramProbability); + addBigramWords(binaryDictionary, "aaa", "bcc", bigramProbability); + addBigramWords(binaryDictionary, "abb", "aaa", bigramProbability); + addBigramWords(binaryDictionary, "abb", "bcc", bigramProbability); + + assertTrue(isValidBigram(binaryDictionary, "aaa", "abb")); + assertTrue(isValidBigram(binaryDictionary, "aaa", "bcc")); + assertTrue(isValidBigram(binaryDictionary, "abb", "aaa")); + assertTrue(isValidBigram(binaryDictionary, "abb", "bcc")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc")); + + addBigramWords(binaryDictionary, "aaa", "abb", updatedBigramProbability); + assertEquals(updatedBigramProbability, + getBigramProbability(binaryDictionary, "aaa", "abb")); + + assertFalse(isValidBigram(binaryDictionary, "bcc", "aaa")); + assertFalse(isValidBigram(binaryDictionary, "bcc", "bbc")); + assertFalse(isValidBigram(binaryDictionary, "aaa", "aaa")); + assertEquals(Dictionary.NOT_A_PROBABILITY, + getBigramProbability(binaryDictionary, "bcc", "aaa")); + assertEquals(Dictionary.NOT_A_PROBABILITY, + getBigramProbability(binaryDictionary, "bcc", "bbc")); + assertEquals(Dictionary.NOT_A_PROBABILITY, + getBigramProbability(binaryDictionary, "aaa", "aaa")); + + // Testing bigram link. + addUnigramWord(binaryDictionary, "abcde", unigramProbability); + addUnigramWord(binaryDictionary, "fghij", unigramProbability); + addBigramWords(binaryDictionary, "abcde", "fghij", bigramProbability); + addUnigramWord(binaryDictionary, "fgh", unigramProbability); + addUnigramWord(binaryDictionary, "abc", unigramProbability); + addUnigramWord(binaryDictionary, "f", unigramProbability); + + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abcde", "fghij")); + assertEquals(Dictionary.NOT_A_PROBABILITY, + getBigramProbability(binaryDictionary, "abcde", "fgh")); + addBigramWords(binaryDictionary, "abcde", "fghij", updatedBigramProbability); + assertEquals(updatedBigramProbability, + getBigramProbability(binaryDictionary, "abcde", "fghij")); + } + + @Test + public void testRandomlyAddBigramWords() { + final int wordCount = 100; + final int bigramCount = 1000; + final int codePointSetSize = 50; + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + + final ArrayList<String> words = new ArrayList<>(); + final ArrayList<Pair<String, String>> bigramWords = new ArrayList<>(); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + final HashMap<String, Integer> unigramProbabilities = new HashMap<>(); + final HashMap<Pair<String, String>, Integer> bigramProbabilities = new HashMap<>(); + + for (int i = 0; i < wordCount; ++i) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + unigramProbabilities.put(word, unigramProbability); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + + for (int i = 0; i < bigramCount; i++) { + final String word0 = words.get(random.nextInt(wordCount)); + final String word1 = words.get(random.nextInt(wordCount)); + if (TextUtils.equals(word0, word1)) { + continue; + } + final Pair<String, String> bigram = new Pair<>(word0, word1); + bigramWords.add(bigram); + final int unigramProbability = unigramProbabilities.get(word1); + final int bigramProbability = + unigramProbability + random.nextInt(0xFF - unigramProbability); + bigramProbabilities.put(bigram, bigramProbability); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + } + + for (final Pair<String, String> bigram : bigramWords) { + final int bigramProbability = bigramProbabilities.get(bigram); + assertEquals(bigramProbability != Dictionary.NOT_A_PROBABILITY, + isValidBigram(binaryDictionary, bigram.first, bigram.second)); + assertEquals(bigramProbability, + getBigramProbability(binaryDictionary, bigram.first, bigram.second)); + } + } + + @Test + public void testAddTrigramWords() { + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + final int unigramProbability = 100; + final int trigramProbability = 150; + final int updatedTrigramProbability = 200; + addUnigramWord(binaryDictionary, "aaa", unigramProbability); + addUnigramWord(binaryDictionary, "abb", unigramProbability); + addUnigramWord(binaryDictionary, "bcc", unigramProbability); + + addBigramWords(binaryDictionary, "abb", "bcc", 10); + addBigramWords(binaryDictionary, "abb", "aaa", 10); + + addTrigramEntry(binaryDictionary, "aaa", "abb", "bcc", trigramProbability); + addTrigramEntry(binaryDictionary, "bcc", "abb", "aaa", trigramProbability); + + assertEquals(trigramProbability, + getTrigramProbability(binaryDictionary, "aaa", "abb", "bcc")); + assertEquals(trigramProbability, + getTrigramProbability(binaryDictionary, "bcc", "abb", "aaa")); + assertFalse(isValidBigram(binaryDictionary, "aaa", "abb")); + + addTrigramEntry(binaryDictionary, "bcc", "abb", "aaa", updatedTrigramProbability); + assertEquals(updatedTrigramProbability, + getTrigramProbability(binaryDictionary, "bcc", "abb", "aaa")); + } + + @Test + public void testFlushDictionary() { + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + + final int probability = 100; + addUnigramWord(binaryDictionary, "aaa", probability); + addUnigramWord(binaryDictionary, "abcd", probability); + // Close without flushing. + binaryDictionary.close(); + + binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(), + 0 /* offset */, dictFile.length(), true /* useFullEditDistance */, + Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */); + + assertEquals(Dictionary.NOT_A_PROBABILITY, binaryDictionary.getFrequency("aaa")); + assertEquals(Dictionary.NOT_A_PROBABILITY, binaryDictionary.getFrequency("abcd")); + + addUnigramWord(binaryDictionary, "aaa", probability); + addUnigramWord(binaryDictionary, "abcd", probability); + binaryDictionary.flush(); + binaryDictionary.close(); + + binaryDictionary = getBinaryDictionary(dictFile); + assertEquals(probability, binaryDictionary.getFrequency("aaa")); + assertEquals(probability, binaryDictionary.getFrequency("abcd")); + addUnigramWord(binaryDictionary, "bcde", probability); + binaryDictionary.flush(); + binaryDictionary.close(); + + binaryDictionary = getBinaryDictionary(dictFile); + assertEquals(probability, binaryDictionary.getFrequency("bcde")); + binaryDictionary.close(); + } + + @Test + public void testFlushWithGCDictionary() { + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + final int unigramProbability = 100; + final int bigramProbability = 150; + addUnigramWord(binaryDictionary, "aaa", unigramProbability); + addUnigramWord(binaryDictionary, "abb", unigramProbability); + addUnigramWord(binaryDictionary, "bcc", unigramProbability); + addBigramWords(binaryDictionary, "aaa", "abb", bigramProbability); + addBigramWords(binaryDictionary, "aaa", "bcc", bigramProbability); + addBigramWords(binaryDictionary, "abb", "aaa", bigramProbability); + addBigramWords(binaryDictionary, "abb", "bcc", bigramProbability); + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + + binaryDictionary = getBinaryDictionary(dictFile); + assertEquals(unigramProbability, binaryDictionary.getFrequency("aaa")); + assertEquals(unigramProbability, binaryDictionary.getFrequency("abb")); + assertEquals(unigramProbability, binaryDictionary.getFrequency("bcc")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa")); + assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc")); + assertFalse(isValidBigram(binaryDictionary, "bcc", "aaa")); + assertFalse(isValidBigram(binaryDictionary, "bcc", "bbc")); + assertFalse(isValidBigram(binaryDictionary, "aaa", "aaa")); + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + } + + @Test + public void testAddBigramWordsAndFlashWithGC() { + final int wordCount = 100; + final int bigramCount = 1000; + final int codePointSetSize = 30; + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + + final ArrayList<String> words = new ArrayList<>(); + final ArrayList<Pair<String, String>> bigramWords = new ArrayList<>(); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + final HashMap<String, Integer> unigramProbabilities = new HashMap<>(); + final HashMap<Pair<String, String>, Integer> bigramProbabilities = new HashMap<>(); + + for (int i = 0; i < wordCount; ++i) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + unigramProbabilities.put(word, unigramProbability); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + + for (int i = 0; i < bigramCount; i++) { + final String word0 = words.get(random.nextInt(wordCount)); + final String word1 = words.get(random.nextInt(wordCount)); + if (TextUtils.equals(word0, word1)) { + continue; + } + final Pair<String, String> bigram = new Pair<>(word0, word1); + bigramWords.add(bigram); + final int unigramProbability = unigramProbabilities.get(word1); + final int bigramProbability = + unigramProbability + random.nextInt(0xFF - unigramProbability); + bigramProbabilities.put(bigram, bigramProbability); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + } + + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + binaryDictionary = getBinaryDictionary(dictFile); + + for (final Pair<String, String> bigram : bigramWords) { + final int bigramProbability = bigramProbabilities.get(bigram); + assertEquals(bigramProbability != Dictionary.NOT_A_PROBABILITY, + isValidBigram(binaryDictionary, bigram.first, bigram.second)); + assertEquals(bigramProbability, + getBigramProbability(binaryDictionary, bigram.first, bigram.second)); + } + } + + @Test + public void testRandomOperationsAndFlashWithGC() { + final int maxUnigramCount = 5000; + final int maxBigramCount = 10000; + final HashMap<String, String> attributeMap = new HashMap<>(); + attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY, String.valueOf(maxUnigramCount)); + attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY, String.valueOf(maxBigramCount)); + + final int flashWithGCIterationCount = 50; + final int operationCountInEachIteration = 200; + final int initialUnigramCount = 100; + final float addUnigramProb = 0.5f; + final float addBigramProb = 0.8f; + final int codePointSetSize = 30; + + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final File dictFile = createEmptyDictionaryWithAttributesAndGetFile(FormatSpec.VERSION403, + attributeMap); + BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + + final ArrayList<String> words = new ArrayList<>(); + final ArrayList<Pair<String, String>> bigramWords = new ArrayList<>(); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + final HashMap<String, Integer> unigramProbabilities = new HashMap<>(); + final HashMap<Pair<String, String>, Integer> bigramProbabilities = new HashMap<>(); + for (int i = 0; i < initialUnigramCount; ++i) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + unigramProbabilities.put(word, unigramProbability); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + + for (int gcCount = 0; gcCount < flashWithGCIterationCount; gcCount++) { + binaryDictionary = getBinaryDictionary(dictFile); + for (int opCount = 0; opCount < operationCountInEachIteration; opCount++) { + // Add unigram. + if (random.nextFloat() < addUnigramProb) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + unigramProbabilities.put(word, unigramProbability); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + // Add bigram. + if (random.nextFloat() < addBigramProb && words.size() > 2) { + final int word0Index = random.nextInt(words.size()); + int word1Index = random.nextInt(words.size() - 1); + if (word0Index <= word1Index) { + word1Index++; + } + final String word0 = words.get(word0Index); + final String word1 = words.get(word1Index); + if (TextUtils.equals(word0, word1)) { + continue; + } + final int unigramProbability = unigramProbabilities.get(word1); + final int bigramProbability = + unigramProbability + random.nextInt(0xFF - unigramProbability); + final Pair<String, String> bigram = new Pair<>(word0, word1); + bigramWords.add(bigram); + bigramProbabilities.put(bigram, bigramProbability); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + } + } + + // Test whether the all unigram operations are collectlly handled. + for (int i = 0; i < words.size(); i++) { + final String word = words.get(i); + final int unigramProbability = unigramProbabilities.get(word); + assertEquals(word, unigramProbability, binaryDictionary.getFrequency(word)); + } + // Test whether the all bigram operations are collectlly handled. + for (int i = 0; i < bigramWords.size(); i++) { + final Pair<String, String> bigram = bigramWords.get(i); + final int probability; + if (bigramProbabilities.containsKey(bigram)) { + probability = bigramProbabilities.get(bigram); + } else { + probability = Dictionary.NOT_A_PROBABILITY; + } + + assertEquals(probability, + getBigramProbability(binaryDictionary, bigram.first, bigram.second)); + assertEquals(probability != Dictionary.NOT_A_PROBABILITY, + isValidBigram(binaryDictionary, bigram.first, bigram.second)); + } + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + } + } + + @Test + public void testAddManyUnigramsAndFlushWithGC() { + final int flashWithGCIterationCount = 3; + final int codePointSetSize = 50; + + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + + final ArrayList<String> words = new ArrayList<>(); + final HashMap<String, Integer> unigramProbabilities = new HashMap<>(); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + + BinaryDictionary binaryDictionary; + for (int i = 0; i < flashWithGCIterationCount; i++) { + binaryDictionary = getBinaryDictionary(dictFile); + while(!binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + unigramProbabilities.put(word, unigramProbability); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + + for (int j = 0; j < words.size(); j++) { + final String word = words.get(j); + final int unigramProbability = unigramProbabilities.get(word); + assertEquals(word, unigramProbability, binaryDictionary.getFrequency(word)); + } + + binaryDictionary.flushWithGC(); + binaryDictionary.close(); + } + } + + @Test + public void testUnigramAndBigramCount() { + final int maxUnigramCount = 5000; + final int maxBigramCount = 10000; + final HashMap<String, String> attributeMap = new HashMap<>(); + attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY, String.valueOf(maxUnigramCount)); + attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY, String.valueOf(maxBigramCount)); + + final int flashWithGCIterationCount = 10; + final int codePointSetSize = 50; + final int unigramCountPerIteration = 1000; + final int bigramCountPerIteration = 2000; + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final File dictFile = createEmptyDictionaryWithAttributesAndGetFile(FormatSpec.VERSION403, + attributeMap); + + final ArrayList<String> words = new ArrayList<>(); + final HashSet<Pair<String, String>> bigrams = new HashSet<>(); + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + + BinaryDictionary binaryDictionary; + for (int i = 0; i < flashWithGCIterationCount; i++) { + binaryDictionary = getBinaryDictionary(dictFile); + for (int j = 0; j < unigramCountPerIteration; j++) { + final String word = CodePointUtils.generateWord(random, codePointSet); + words.add(word); + final int unigramProbability = random.nextInt(0xFF); + addUnigramWord(binaryDictionary, word, unigramProbability); + } + for (int j = 0; j < bigramCountPerIteration; j++) { + final String word0 = words.get(random.nextInt(words.size())); + final String word1 = words.get(random.nextInt(words.size())); + if (TextUtils.equals(word0, word1)) { + continue; + } + bigrams.add(new Pair<>(word0, word1)); + final int bigramProbability = random.nextInt(0xF); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + } + assertEquals(new HashSet<>(words).size(), Integer.parseInt( + binaryDictionary.getPropertyForGettingStats( + BinaryDictionary.UNIGRAM_COUNT_QUERY))); + assertEquals(new HashSet<>(bigrams).size(), Integer.parseInt( + binaryDictionary.getPropertyForGettingStats( + BinaryDictionary.BIGRAM_COUNT_QUERY))); + binaryDictionary.flushWithGC(); + assertEquals(new HashSet<>(words).size(), Integer.parseInt( + binaryDictionary.getPropertyForGettingStats( + BinaryDictionary.UNIGRAM_COUNT_QUERY))); + assertEquals(new HashSet<>(bigrams).size(), Integer.parseInt( + binaryDictionary.getPropertyForGettingStats( + BinaryDictionary.BIGRAM_COUNT_QUERY))); + binaryDictionary.close(); + } + } + + @Test + public void testGetWordProperties() { + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final int UNIGRAM_COUNT = 1000; + final int BIGRAM_COUNT = 1000; + final int codePointSetSize = 20; + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + final File dictFile = createEmptyDictionaryAndGetFile(FormatSpec.VERSION403); + final BinaryDictionary binaryDictionary = getBinaryDictionary(dictFile); + + final WordProperty invalidWordProperty = binaryDictionary.getWordProperty("dummyWord", + false /* isBeginningOfSentence */); + assertFalse(invalidWordProperty.isValid()); + + final ArrayList<String> words = new ArrayList<>(); + final HashMap<String, Integer> wordProbabilities = new HashMap<>(); + final HashMap<String, HashSet<String>> bigrams = new HashMap<>(); + final HashMap<Pair<String, String>, Integer> bigramProbabilities = new HashMap<>(); + + for (int i = 0; i < UNIGRAM_COUNT; i++) { + final String word = CodePointUtils.generateWord(random, codePointSet); + final int unigramProbability = random.nextInt(0xFF); + final boolean isNotAWord = random.nextBoolean(); + final boolean isPossiblyOffensive = random.nextBoolean(); + // TODO: Add tests for historical info. + binaryDictionary.addUnigramEntry(word, unigramProbability, + false /* isBeginningOfSentence */, isNotAWord, isPossiblyOffensive, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + binaryDictionary.flushWithGC(); + } + words.add(word); + wordProbabilities.put(word, unigramProbability); + final WordProperty wordProperty = binaryDictionary.getWordProperty(word, + false /* isBeginningOfSentence */); + assertEquals(word, wordProperty.mWord); + assertTrue(wordProperty.isValid()); + assertEquals(isNotAWord, wordProperty.mIsNotAWord); + assertEquals(isPossiblyOffensive, wordProperty.mIsPossiblyOffensive); + assertEquals(false, wordProperty.mHasNgrams); + assertEquals(unigramProbability, wordProperty.mProbabilityInfo.mProbability); + } + + for (int i = 0; i < BIGRAM_COUNT; i++) { + final int word0Index = random.nextInt(wordProbabilities.size()); + final int word1Index = random.nextInt(wordProbabilities.size()); + if (word0Index == word1Index) { + continue; + } + final String word0 = words.get(word0Index); + final String word1 = words.get(word1Index); + final int unigramProbability = wordProbabilities.get(word1); + final int bigramProbability = + unigramProbability + random.nextInt(0xFF - unigramProbability); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + binaryDictionary.flushWithGC(); + } + if (!bigrams.containsKey(word0)) { + final HashSet<String> bigramWord1s = new HashSet<>(); + bigrams.put(word0, bigramWord1s); + } + bigrams.get(word0).add(word1); + bigramProbabilities.put(new Pair<>(word0, word1), bigramProbability); + } + + for (int i = 0; i < words.size(); i++) { + final String word0 = words.get(i); + if (!bigrams.containsKey(word0)) { + continue; + } + final HashSet<String> bigramWord1s = bigrams.get(word0); + final WordProperty wordProperty = binaryDictionary.getWordProperty(word0, + false /* isBeginningOfSentence */); + assertEquals(bigramWord1s.size(), wordProperty.mNgrams.size()); + // TODO: Support ngram. + for (final WeightedString bigramTarget : wordProperty.getBigrams()) { + final String word1 = bigramTarget.mWord; + assertTrue(bigramWord1s.contains(word1)); + final int bigramProbability = bigramProbabilities.get(new Pair<>(word0, word1)); + assertEquals(bigramProbability, bigramTarget.getProbability()); + } + } + } + + @Test + public void testIterateAllWords() { + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final int UNIGRAM_COUNT = 1000; + final int BIGRAM_COUNT = 1000; + final int codePointSetSize = 20; + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + + final WordProperty invalidWordProperty = binaryDictionary.getWordProperty("dummyWord", + false /* isBeginningOfSentence */); + assertFalse(invalidWordProperty.isValid()); + + final ArrayList<String> words = new ArrayList<>(); + final HashMap<String, Integer> wordProbabilitiesToCheckLater = new HashMap<>(); + final HashMap<String, HashSet<String>> bigrams = new HashMap<>(); + final HashMap<Pair<String, String>, Integer> bigramProbabilitiesToCheckLater = + new HashMap<>(); + + for (int i = 0; i < UNIGRAM_COUNT; i++) { + final String word = CodePointUtils.generateWord(random, codePointSet); + final int unigramProbability = random.nextInt(0xFF); + addUnigramWord(binaryDictionary, word, unigramProbability); + if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + binaryDictionary.flushWithGC(); + } + words.add(word); + wordProbabilitiesToCheckLater.put(word, unigramProbability); + } + + for (int i = 0; i < BIGRAM_COUNT; i++) { + final int word0Index = random.nextInt(wordProbabilitiesToCheckLater.size()); + final int word1Index = random.nextInt(wordProbabilitiesToCheckLater.size()); + if (word0Index == word1Index) { + continue; + } + final String word0 = words.get(word0Index); + final String word1 = words.get(word1Index); + final int unigramProbability = wordProbabilitiesToCheckLater.get(word1); + final int bigramProbability = + unigramProbability + random.nextInt(0xFF - unigramProbability); + addBigramWords(binaryDictionary, word0, word1, bigramProbability); + if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + binaryDictionary.flushWithGC(); + } + if (!bigrams.containsKey(word0)) { + final HashSet<String> bigramWord1s = new HashSet<>(); + bigrams.put(word0, bigramWord1s); + } + bigrams.get(word0).add(word1); + bigramProbabilitiesToCheckLater.put(new Pair<>(word0, word1), bigramProbability); + } + + final HashSet<String> wordSet = new HashSet<>(words); + final HashSet<Pair<String, String>> bigramSet = + new HashSet<>(bigramProbabilitiesToCheckLater.keySet()); + int token = 0; + do { + final BinaryDictionary.GetNextWordPropertyResult result = + binaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + final String word0 = wordProperty.mWord; + assertEquals((int)wordProbabilitiesToCheckLater.get(word0), + wordProperty.mProbabilityInfo.mProbability); + wordSet.remove(word0); + final HashSet<String> bigramWord1s = bigrams.get(word0); + // TODO: Support ngram. + if (wordProperty.mHasNgrams) { + for (final WeightedString bigramTarget : wordProperty.getBigrams()) { + final String word1 = bigramTarget.mWord; + assertTrue(bigramWord1s.contains(word1)); + final Pair<String, String> bigram = new Pair<>(word0, word1); + final int bigramProbability = bigramProbabilitiesToCheckLater.get(bigram); + assertEquals(bigramProbability, bigramTarget.getProbability()); + bigramSet.remove(bigram); + } + } + token = result.mNextToken; + } while (token != 0); + assertTrue(wordSet.isEmpty()); + assertTrue(bigramSet.isEmpty()); + } + + @Test + public void testPossiblyOffensiveAttributeMaintained() { + final BinaryDictionary binaryDictionary = + getEmptyBinaryDictionary(FormatSpec.VERSION403); + binaryDictionary.addUnigramEntry("ddd", 100, false, true, true, 0); + WordProperty wordProperty = binaryDictionary.getWordProperty("ddd", false); + assertEquals(true, wordProperty.mIsPossiblyOffensive); + } + + @Test + public void testBeginningOfSentence() { + final BinaryDictionary binaryDictionary = getEmptyBinaryDictionary(FormatSpec.VERSION403); + final int dummyProbability = 0; + final NgramContext beginningOfSentenceContext = NgramContext.BEGINNING_OF_SENTENCE; + final int bigramProbability = 200; + addUnigramWord(binaryDictionary, "aaa", dummyProbability); + binaryDictionary.addNgramEntry(beginningOfSentenceContext, "aaa", bigramProbability, + BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + assertEquals(bigramProbability, + binaryDictionary.getNgramProbability(beginningOfSentenceContext, "aaa")); + binaryDictionary.addNgramEntry(beginningOfSentenceContext, "aaa", bigramProbability, + BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + addUnigramWord(binaryDictionary, "bbb", dummyProbability); + binaryDictionary.addNgramEntry(beginningOfSentenceContext, "bbb", bigramProbability, + BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */); + binaryDictionary.flushWithGC(); + assertEquals(bigramProbability, + binaryDictionary.getNgramProbability(beginningOfSentenceContext, "aaa")); + assertEquals(bigramProbability, + binaryDictionary.getNgramProbability(beginningOfSentenceContext, "bbb")); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/BlueUnderlineTests.java b/tests/src/org/kelar/inputmethod/latin/BlueUnderlineTests.java new file mode 100644 index 000000000..7835f4497 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/BlueUnderlineTests.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin; + +import android.text.style.SuggestionSpan; +import android.text.style.UnderlineSpan; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.common.Constants; + +@LargeTest +public class BlueUnderlineTests extends InputTestsBase { + + public void testBlueUnderline() { + final String STRING_TO_TYPE = "tgis"; + final int EXPECTED_SPAN_START = 0; + final int EXPECTED_SPAN_END = 4; + type(STRING_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + final SpanGetter span = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + assertEquals("show blue underline, span start", EXPECTED_SPAN_START, span.mStart); + assertEquals("show blue underline, span end", EXPECTED_SPAN_END, span.mEnd); + assertEquals("show blue underline, span color", true, span.isAutoCorrectionIndicator()); + } + + public void testBlueUnderlineDisappears() { + final String STRING_1_TO_TYPE = "tqis"; + final String STRING_2_TO_TYPE = "g"; + final int EXPECTED_SPAN_START = 0; + final int EXPECTED_SPAN_END = 5; + type(STRING_1_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + type(STRING_2_TO_TYPE); + // We haven't have time to look into the dictionary yet, so the line should still be + // blue to avoid any flicker. + final SpanGetter spanBefore = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + assertEquals("extend blue underline, span start", EXPECTED_SPAN_START, spanBefore.mStart); + assertEquals("extend blue underline, span end", EXPECTED_SPAN_END, spanBefore.mEnd); + assertTrue("extend blue underline, span color", spanBefore.isAutoCorrectionIndicator()); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + // Now we have been able to re-evaluate the word, there shouldn't be an auto-correction span + final SpanGetter spanAfter = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + assertNull("hide blue underline", spanAfter.mSpan); + } + + public void testBlueUnderlineOnBackspace() { + final String STRING_TO_TYPE = "tgis"; + final int typedLength = STRING_TO_TYPE.length(); + final int EXPECTED_UNDERLINE_SPAN_START = 0; + final int EXPECTED_UNDERLINE_SPAN_END = 3; + type(STRING_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + type(Constants.CODE_SPACE); + // typedLength + 1 because we also typed a space + mLatinIME.onUpdateSelection(0, 0, typedLength + 1, typedLength + 1, -1, -1); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + type(Constants.CODE_DELETE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + type(Constants.CODE_DELETE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + final SpanGetter suggestionSpan = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + assertFalse("show no blue underline after backspace, span should not be the auto-" + + "correction indicator", suggestionSpan.isAutoCorrectionIndicator()); + final SpanGetter underlineSpan = new SpanGetter(mEditText.getText(), UnderlineSpan.class); + assertEquals("should be composing, so should have an underline span", + EXPECTED_UNDERLINE_SPAN_START, underlineSpan.mStart); + assertEquals("should be composing, so should have an underline span", + EXPECTED_UNDERLINE_SPAN_END, underlineSpan.mEnd); + } + + public void testBlueUnderlineDisappearsWhenCursorMoved() { + final String STRING_TO_TYPE = "tgis"; + final int typedLength = STRING_TO_TYPE.length(); + final int NEW_CURSOR_POSITION = 0; + type(STRING_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + // Simulate the onUpdateSelection() event + mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, -1, -1); + runMessages(); + // Here the blue underline has been set. testBlueUnderline() is testing for this already, + // so let's not test it here again. + // Now simulate the user moving the cursor. + mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION); + mLatinIME.onUpdateSelection(typedLength, typedLength, + NEW_CURSOR_POSITION, NEW_CURSOR_POSITION, -1, -1); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + final SpanGetter span = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + assertFalse("blue underline removed when cursor is moved", + span.isAutoCorrectionIndicator()); + } + + public void testComposingStopsOnSpace() { + final String STRING_TO_TYPE = "this "; + type(STRING_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + // Simulate the onUpdateSelection() event + mLatinIME.onUpdateSelection(0, 0, STRING_TO_TYPE.length(), STRING_TO_TYPE.length(), -1, -1); + runMessages(); + // Here the blue underline has been set. testBlueUnderline() is testing for this already, + // so let's not test it here again. + // Now simulate the user moving the cursor. + SpanGetter span = new SpanGetter(mEditText.getText(), UnderlineSpan.class); + assertNull("should not be composing, so should not have an underline span", span.mSpan); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/ContactsContentObserverTest.java b/tests/src/org/kelar/inputmethod/latin/ContactsContentObserverTest.java new file mode 100644 index 000000000..8e21324cb --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/ContactsContentObserverTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.validateMockitoUsage; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.provider.ContactsContract.Contacts; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; + +/** + * Tests for {@link ContactsContentObserver}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContactsContentObserverTest { + private static final int UPDATED_CONTACT_COUNT = 10; + private static final int STALE_CONTACT_COUNT = 8; + private static final ArrayList<String> STALE_NAMES_LIST = new ArrayList<>(); + private static final ArrayList<String> UPDATED_NAMES_LIST = new ArrayList<>(); + + static { + STALE_NAMES_LIST.add("Larry Page"); + STALE_NAMES_LIST.add("Roger Federer"); + UPDATED_NAMES_LIST.add("Larry Page"); + UPDATED_NAMES_LIST.add("Roger Federer"); + UPDATED_NAMES_LIST.add("Barak Obama"); + } + + @Mock private ContactsManager mMockManager; + @Mock private Context mContext; + + private ContactsContentObserver mObserver; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mObserver = new ContactsContentObserver(mMockManager, mContext); + } + + @After + public void tearDown() { + validateMockitoUsage(); + } + + @Test + public void testHaveContentsChanged_NoChange() { + when(mMockManager.getContactCount()).thenReturn(STALE_CONTACT_COUNT); + when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT); + when(mMockManager.getValidNames(eq(Contacts.CONTENT_URI))).thenReturn(STALE_NAMES_LIST); + when(mMockManager.getHashCodeAtLastRebuild()).thenReturn(STALE_NAMES_LIST.hashCode()); + assertFalse(mObserver.haveContentsChanged()); + } + @Test + public void testHaveContentsChanged_UpdatedCount() { + when(mMockManager.getContactCount()).thenReturn(UPDATED_CONTACT_COUNT); + when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT); + assertTrue(mObserver.haveContentsChanged()); + } + + @Test + public void testHaveContentsChanged_HashUpdate() { + when(mMockManager.getContactCount()).thenReturn(STALE_CONTACT_COUNT); + when(mMockManager.getContactCountAtLastRebuild()).thenReturn(STALE_CONTACT_COUNT); + when(mMockManager.getValidNames(eq(Contacts.CONTENT_URI))).thenReturn(UPDATED_NAMES_LIST); + when(mMockManager.getHashCodeAtLastRebuild()).thenReturn(STALE_NAMES_LIST.hashCode()); + assertTrue(mObserver.haveContentsChanged()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/ContactsDictionaryUtilsTest.java b/tests/src/org/kelar/inputmethod/latin/ContactsDictionaryUtilsTest.java new file mode 100644 index 000000000..79f13a979 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/ContactsDictionaryUtilsTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +/** + * Tests for {@link ContactsDictionaryUtils} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContactsDictionaryUtilsTest { + + @Test + public void testGetWordEndPosition() { + final String testString1 = "Larry Page"; + assertEquals(5, ContactsDictionaryUtils.getWordEndPosition( + testString1, testString1.length(), 0 /* startIndex */)); + + assertEquals(10, ContactsDictionaryUtils.getWordEndPosition( + testString1, testString1.length(), 6 /* startIndex */)); + + final String testString2 = "Larry-Page"; + assertEquals(10, ContactsDictionaryUtils.getWordEndPosition( + testString2, testString1.length(), 0 /* startIndex */)); + + final String testString3 = "Larry'Page"; + assertEquals(10, ContactsDictionaryUtils.getWordEndPosition( + testString3, testString1.length(), 0 /* startIndex */)); + } + + @Test + public void testUseFirstLastBigramsForLocale() { + assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.ENGLISH)); + assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.US)); + assertTrue(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.UK)); + assertFalse(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.CHINA)); + assertFalse(ContactsDictionaryUtils.useFirstLastBigramsForLocale(Locale.GERMAN)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/ContactsManagerTest.java b/tests/src/org/kelar/inputmethod/latin/ContactsManagerTest.java new file mode 100644 index 000000000..d912ec266 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/ContactsManagerTest.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentProvider; +import android.test.mock.MockContentResolver; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link ContactsManager} + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ContactsManagerTest { + + private ContactsManager mManager; + private FakeContactsContentProvider mFakeContactsContentProvider; + private MatrixCursor mMatrixCursor; + + private final static float EPSILON = 0.00001f; + + @Before + public void setUp() throws Exception { + // Fake content provider + mFakeContactsContentProvider = new FakeContactsContentProvider(); + mMatrixCursor = new MatrixCursor(ContactsDictionaryConstants.PROJECTION); + // Add the fake content provider to fake content resolver. + final MockContentResolver contentResolver = new MockContentResolver(); + contentResolver.addProvider(ContactsContract.AUTHORITY, mFakeContactsContentProvider); + // Add the fake content resolver to a fake context. + final ContextWithMockContentResolver context = + new ContextWithMockContentResolver(InstrumentationRegistry.getTargetContext()); + context.setContentResolver(contentResolver); + + mManager = new ContactsManager(context); + } + + @Test + public void testGetValidNames() { + final String contactName1 = "firstname last-name"; + final String contactName2 = "larry"; + mMatrixCursor.addRow(new Object[] { 1, contactName1, 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 2, null /* null name */, 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 3, contactName2, 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */, 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 5, "news-group" /* invalid name */, 0, 0, 0 }); + mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor); + + final ArrayList<String> validNames = mManager.getValidNames(Contacts.CONTENT_URI); + assertEquals(2, validNames.size()); + assertEquals(contactName1, validNames.get(0)); + assertEquals(contactName2, validNames.get(1)); + } + + @Test + public void testGetValidNamesAffinity() { + final long now = System.currentTimeMillis(); + final long month_ago = now - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS); + for (int i = 0; i < ContactsManager.MAX_CONTACT_NAMES + 10; ++i) { + mMatrixCursor.addRow(new Object[] { i, "name" + i, i, now, 1 }); + } + mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor); + + final ArrayList<String> validNames = mManager.getValidNames(Contacts.CONTENT_URI); + assertEquals(ContactsManager.MAX_CONTACT_NAMES, validNames.size()); + for (int i = 0; i < 10; ++i) { + assertFalse(validNames.contains("name" + i)); + } + for (int i = 10; i < ContactsManager.MAX_CONTACT_NAMES + 10; ++i) { + assertTrue(validNames.contains("name" + i)); + } + } + + @Test + public void testComputeAffinity() { + final long now = System.currentTimeMillis(); + final long month_ago = now - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS); + mMatrixCursor.addRow(new Object[] { 1, "name", 1, month_ago, 1 }); + mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor); + + Cursor cursor = mFakeContactsContentProvider.query(Contacts.CONTENT_URI, + ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null); + cursor.moveToFirst(); + ContactsManager.RankedContact contact = new ContactsManager.RankedContact(cursor); + contact.computeAffinity(1, month_ago); + assertEquals(contact.getAffinity(), 1.0f, EPSILON); + contact.computeAffinity(2, now); + assertEquals(contact.getAffinity(), (2.0f/3.0f + (float)Math.pow(0.5, 3) + 1.0f) / 3, + EPSILON); + } + + @Test + public void testGetCount() { + mMatrixCursor.addRow(new Object[] { 1, "firstname", 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 2, null /* null name */, 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 3, "larry", 0, 0, 0 }); + mMatrixCursor.addRow(new Object[] { 4, "floopy@example.com" /* invalid name */, 0, 0, 0 }); + mFakeContactsContentProvider.addQueryResult(Contacts.CONTENT_URI, mMatrixCursor); + assertEquals(4, mManager.getContactCount()); + } + + + static class ContextWithMockContentResolver extends RenamingDelegatingContext { + private ContentResolver contentResolver; + + public void setContentResolver(final ContentResolver contentResolver) { + this.contentResolver = contentResolver; + } + + public ContextWithMockContentResolver(final Context targetContext) { + super(targetContext, "test"); + } + + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } + } + + static class FakeContactsContentProvider extends MockContentProvider { + private final HashMap<String, MatrixCursor> mQueryCursorMapForTestExpectations = + new HashMap<>(); + + @Override + public Cursor query(final Uri uri, final String[] projection, final String selection, + final String[] selectionArgs, final String sortOrder) { + return mQueryCursorMapForTestExpectations.get(uri.toString()); + } + + public void reset() { + mQueryCursorMapForTestExpectations.clear(); + } + + public void addQueryResult(final Uri uri, final MatrixCursor cursor) { + mQueryCursorMapForTestExpectations.put(uri.toString(), cursor); + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java b/tests/src/org/kelar/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java new file mode 100644 index 000000000..ed9928a65 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/DictionaryFacilitatorLruCacheTests.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class DictionaryFacilitatorLruCacheTests { + + @Test + public void testGetFacilitator() { + final DictionaryFacilitatorLruCache cache = + new DictionaryFacilitatorLruCache(InstrumentationRegistry.getTargetContext(), ""); + + final DictionaryFacilitator dictionaryFacilitatorEnUs = cache.get(Locale.US); + assertNotNull(dictionaryFacilitatorEnUs); + assertTrue(dictionaryFacilitatorEnUs.isForLocale(Locale.US)); + + final DictionaryFacilitator dictionaryFacilitatorFr = cache.get(Locale.FRENCH); + assertNotNull(dictionaryFacilitatorEnUs); + assertTrue(dictionaryFacilitatorFr.isForLocale(Locale.FRENCH)); + + final DictionaryFacilitator dictionaryFacilitatorDe = cache.get(Locale.GERMANY); + assertNotNull(dictionaryFacilitatorDe); + assertTrue(dictionaryFacilitatorDe.isForLocale(Locale.GERMANY)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/InputLogicTests.java b/tests/src/org/kelar/inputmethod/latin/InputLogicTests.java new file mode 100644 index 000000000..689ee88bb --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/InputLogicTests.java @@ -0,0 +1,786 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin; + +import static android.test.MoreAsserts.assertNotEqual; + +import android.text.TextUtils; +import android.view.inputmethod.BaseInputConnection; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.define.DecoderSpecificConstants; +import org.kelar.inputmethod.latin.settings.Settings; + +@LargeTest +public class InputLogicTests extends InputTestsBase { + + private boolean mNextWordPrediction; + + @Override + public void setUp() throws Exception { + super.setUp(); + mNextWordPrediction = getBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, true); + } + + @Override + public void tearDown() throws Exception { + setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, mNextWordPrediction, true); + super.tearDown(); + } + + public void testTypeWord() { + final String WORD_TO_TYPE = "abcd"; + type(WORD_TO_TYPE); + assertEquals("type word", WORD_TO_TYPE, mEditText.getText().toString()); + } + + public void testPickSuggestionThenBackspace() { + final String WORD_TO_TYPE = "this"; + final String EXPECTED_RESULT = "thi"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + sendUpdateForCursorMoveTo(WORD_TO_TYPE.length()); + type(Constants.CODE_DELETE); + assertEquals("press suggestion then backspace", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testPickAutoCorrectionThenBackspace() { + final String WORD_TO_TYPE = "tgis"; + final String WORD_TO_PICK = "this"; + final String EXPECTED_RESULT = "thi"; + type(WORD_TO_TYPE); + // Choose the auto-correction. For "tgis", the auto-correction should be "this". + pickSuggestionManually(WORD_TO_PICK); + sendUpdateForCursorMoveTo(WORD_TO_TYPE.length()); + assertEquals("pick typed word over auto-correction then backspace", WORD_TO_PICK, + mEditText.getText().toString()); + type(Constants.CODE_DELETE); + assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testPickTypedWordOverAutoCorrectionThenBackspace() { + final String WORD_TO_TYPE = "tgis"; + final String EXPECTED_RESULT = "tgi"; + type(WORD_TO_TYPE); + // Choose the typed word. + pickSuggestionManually(WORD_TO_TYPE); + sendUpdateForCursorMoveTo(WORD_TO_TYPE.length()); + assertEquals("pick typed word over auto-correction then backspace", WORD_TO_TYPE, + mEditText.getText().toString()); + type(Constants.CODE_DELETE); + assertEquals("pick typed word over auto-correction then backspace", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testPickDifferentSuggestionThenBackspace() { + final String WORD_TO_TYPE = "tgis"; + final String WORD_TO_PICK = "thus"; + final String EXPECTED_RESULT = "thu"; + type(WORD_TO_TYPE); + // Choose the second suggestion, which should be "thus" when "tgis" is typed. + pickSuggestionManually(WORD_TO_PICK); + sendUpdateForCursorMoveTo(WORD_TO_TYPE.length()); + assertEquals("pick different suggestion then backspace", WORD_TO_PICK, + mEditText.getText().toString()); + type(Constants.CODE_DELETE); + assertEquals("pick different suggestion then backspace", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeleteSelection() { + final String STRING_TO_TYPE = "some text delete me some text"; + final int typedLength = STRING_TO_TYPE.length(); + final int SELECTION_START = 10; + final int SELECTION_END = 19; + final String EXPECTED_RESULT = "some text some text"; + type(STRING_TO_TYPE); + // Don't use the sendUpdateForCursorMove* family of methods here because they + // don't handle selections. + // Send once to simulate the cursor actually responding to the move caused by typing. + // This is necessary because LatinIME is bookkeeping to avoid confusing a real cursor + // move with a move triggered by LatinIME inputting stuff. + mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, -1, -1); + mInputConnection.setSelection(SELECTION_START, SELECTION_END); + // And now we simulate the user actually selecting some text. + mLatinIME.onUpdateSelection(typedLength, typedLength, + SELECTION_START, SELECTION_END, -1, -1); + type(Constants.CODE_DELETE); + assertEquals("delete selection", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testDeleteSelectionTwice() { + final String STRING_TO_TYPE = "some text delete me some text"; + final int typedLength = STRING_TO_TYPE.length(); + final int SELECTION_START = 10; + final int SELECTION_END = 19; + final String EXPECTED_RESULT = "some text some text"; + type(STRING_TO_TYPE); + // Don't use the sendUpdateForCursorMove* family of methods here because they + // don't handle selections. + // Send once to simulate the cursor actually responding to the move caused by typing. + // This is necessary because LatinIME is bookkeeping to avoid confusing a real cursor + // move with a move triggered by LatinIME inputting stuff. + mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, -1, -1); + mInputConnection.setSelection(SELECTION_START, SELECTION_END); + // And now we simulate the user actually selecting some text. + mLatinIME.onUpdateSelection(typedLength, typedLength, + SELECTION_START, SELECTION_END, -1, -1); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + assertEquals("delete selection twice", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrect() { + final String STRING_TO_TYPE = "tgis "; + final String EXPECTED_RESULT = "this "; + type(STRING_TO_TYPE); + assertEquals("simple auto-correct", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectWithQuote() { + final String STRING_TO_TYPE = "didn' "; + final String EXPECTED_RESULT = "didn't "; + type(STRING_TO_TYPE); + assertEquals("auto-correct with quote", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectWithPeriod() { + final String STRING_TO_TYPE = "tgis."; + final String EXPECTED_RESULT = "this."; + type(STRING_TO_TYPE); + assertEquals("auto-correct with period", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectWithPeriodThenRevert() { + final String STRING_TO_TYPE = "tgis."; + final String EXPECTED_RESULT = "tgis."; + type(STRING_TO_TYPE); + sendUpdateForCursorMoveTo(STRING_TO_TYPE.length()); + type(Constants.CODE_DELETE); + assertEquals("auto-correct with period then revert", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testAutoCorrectWithSpaceThenRevert() { + // Backspacing to cancel the "tgis"->"this" autocorrection should result in + // a "phantom space": if the user presses space immediately after, + // only one space will be inserted in total. + final String STRING_TO_TYPE = "tgis "; + final String EXPECTED_RESULT = "tgis"; + type(STRING_TO_TYPE); + sendUpdateForCursorMoveTo(STRING_TO_TYPE.length()); + type(Constants.CODE_DELETE); + assertEquals("auto-correct with space then revert", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testAutoCorrectWithSpaceThenRevertThenTypeMore() { + final String STRING_TO_TYPE_FIRST = "tgis "; + final String STRING_TO_TYPE_SECOND = "a"; + final String EXPECTED_RESULT = "tgis a"; + type(STRING_TO_TYPE_FIRST); + sendUpdateForCursorMoveTo(STRING_TO_TYPE_FIRST.length()); + type(Constants.CODE_DELETE); + + type(STRING_TO_TYPE_SECOND); + sendUpdateForCursorMoveTo(STRING_TO_TYPE_FIRST.length() - 1 + + STRING_TO_TYPE_SECOND.length()); + assertEquals("auto-correct with space then revert then type more", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testAutoCorrectToSelfDoesNotRevert() { + final String STRING_TO_TYPE = "this "; + final String EXPECTED_RESULT = "this"; + type(STRING_TO_TYPE); + sendUpdateForCursorMoveTo(STRING_TO_TYPE.length()); + type(Constants.CODE_DELETE); + assertEquals("auto-correct with space does not revert", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDoubleSpace() { + // U+1F607 is an emoji + final String[] STRINGS_TO_TYPE = + new String[] { "this ", "a+ ", "\u1F607 ", ".. ", ") ", "( ", "% " }; + final String[] EXPECTED_RESULTS = + new String[] { "this. ", "a+. ", "\u1F607. ", ".. ", "). ", "( ", "%. " }; + verifyDoubleSpace(STRINGS_TO_TYPE, EXPECTED_RESULTS); + } + + public void testDoubleSpaceHindi() { + changeLanguage("hi"); + // U+1F607 is an emoji + final String[] STRINGS_TO_TYPE = + new String[] { "this ", "a+ ", "\u1F607 ", "|| ", ") ", "( ", "% " }; + final String[] EXPECTED_RESULTS = + new String[] { "this| ", "a+| ", "\u1F607| ", "|| ", ")| ", "( ", "%| " }; + verifyDoubleSpace(STRINGS_TO_TYPE, EXPECTED_RESULTS); + } + + private void verifyDoubleSpace(String[] stringsToType, String[] expectedResults) { + // Set default pref just in case + setBooleanPreference(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true, true); + for (int i = 0; i < stringsToType.length; ++i) { + mEditText.setText(""); + type(stringsToType[i]); + assertEquals("double space processing", expectedResults[i], + mEditText.getText().toString()); + } + } + + public void testCancelDoubleSpaceEnglish() { + final String STRING_TO_TYPE = "this "; + final String EXPECTED_RESULT = "this "; + type(STRING_TO_TYPE); + type(Constants.CODE_DELETE); + assertEquals("double space make a period", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testCancelDoubleSpaceHindi() { + changeLanguage("hi"); + final String STRING_TO_TYPE = "this "; + final String EXPECTED_RESULT = "this "; + type(STRING_TO_TYPE); + type(Constants.CODE_DELETE); + assertEquals("double space make a period", EXPECTED_RESULT, mEditText.getText().toString()); + } + + private void testDoubleSpacePeriodWithSettings(final boolean expectsPeriod, + final Object... settingsKeysValues) { + final Object[] oldSettings = new Object[settingsKeysValues.length / 2]; + final String STRING_WITHOUT_PERIOD = "this "; + final String STRING_WITH_PERIOD = "this. "; + final String EXPECTED_RESULT = expectsPeriod ? STRING_WITH_PERIOD : STRING_WITHOUT_PERIOD; + try { + for (int i = 0; i < settingsKeysValues.length; i += 2) { + if (settingsKeysValues[i + 1] instanceof String) { + oldSettings[i / 2] = setStringPreference((String)settingsKeysValues[i], + (String)settingsKeysValues[i + 1], "0"); + } else { + oldSettings[i / 2] = setBooleanPreference((String)settingsKeysValues[i], + (Boolean)settingsKeysValues[i + 1], false); + } + } + mLatinIME.loadSettings(); + mEditText.setText(""); + type(STRING_WITHOUT_PERIOD); + assertEquals("double-space-to-period with specific settings " + + TextUtils.join(" ", settingsKeysValues), + EXPECTED_RESULT, mEditText.getText().toString()); + } finally { + // Restore old settings + for (int i = 0; i < settingsKeysValues.length; i += 2) { + if (null == oldSettings[i / 2]) { + break; + } if (oldSettings[i / 2] instanceof String) { + setStringPreference((String)settingsKeysValues[i], (String)oldSettings[i / 2], + ""); + } else { + setBooleanPreference((String)settingsKeysValues[i], (Boolean)oldSettings[i / 2], + false); + } + } + } + } + + public void testDoubleSpacePeriod() { + // Reset settings to default, else these tests will go flaky. + setBooleanPreference(Settings.PREF_SHOW_SUGGESTIONS, true, true); + setBooleanPreference(Settings.PREF_AUTO_CORRECTION, true, true); + setBooleanPreference(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true, true); + testDoubleSpacePeriodWithSettings(true); + // "Suggestion visibility" to off + testDoubleSpacePeriodWithSettings(true, Settings.PREF_SHOW_SUGGESTIONS, false); + // "Suggestion visibility" to on + testDoubleSpacePeriodWithSettings(true, Settings.PREF_SHOW_SUGGESTIONS, true); + + // "Double-space period" to "off" + testDoubleSpacePeriodWithSettings(false, Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, false); + + // "Auto-correction" to "off" + testDoubleSpacePeriodWithSettings(true, Settings.PREF_AUTO_CORRECTION, false); + // "Auto-correction" to "on" + testDoubleSpacePeriodWithSettings(true, Settings.PREF_AUTO_CORRECTION, true); + + // "Suggestion visibility" to "always hide" and "Auto-correction" to "off" + testDoubleSpacePeriodWithSettings(true, Settings.PREF_SHOW_SUGGESTIONS, false, + Settings.PREF_AUTO_CORRECTION, false); + // "Suggestion visibility" to "always hide" and "Auto-correction" to "off" + testDoubleSpacePeriodWithSettings(false, Settings.PREF_SHOW_SUGGESTIONS, false, + Settings.PREF_AUTO_CORRECTION, false, + Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, false); + } + + public void testBackspaceAtStartAfterAutocorrect() { + final String STRING_TO_TYPE = "tgis "; + final int typedLength = STRING_TO_TYPE.length(); + final String EXPECTED_RESULT = "this "; + final int NEW_CURSOR_POSITION = 0; + type(STRING_TO_TYPE); + sendUpdateForCursorMoveTo(typedLength); + mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION); + sendUpdateForCursorMoveTo(NEW_CURSOR_POSITION); + type(Constants.CODE_DELETE); + assertEquals("auto correct then move cursor to start of line then backspace", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectThenMoveCursorThenBackspace() { + final String STRING_TO_TYPE = "and tgis "; + final int typedLength = STRING_TO_TYPE.length(); + final String EXPECTED_RESULT = "andthis "; + final int NEW_CURSOR_POSITION = STRING_TO_TYPE.indexOf('t'); + type(STRING_TO_TYPE); + sendUpdateForCursorMoveTo(typedLength); + mInputConnection.setSelection(NEW_CURSOR_POSITION, NEW_CURSOR_POSITION); + sendUpdateForCursorMoveTo(NEW_CURSOR_POSITION); + type(Constants.CODE_DELETE); + assertEquals("auto correct then move cursor then backspace", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testNoSpaceAfterManualPick() { + final String WORD_TO_TYPE = "this"; + final String EXPECTED_RESULT = WORD_TO_TYPE; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + assertEquals("no space after manual pick", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManualPickThenType() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_TYPE = "is"; + final String EXPECTED_RESULT = "this is"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("manual pick then type", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManualPickThenSeparator() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_TYPE = "!"; + final String EXPECTED_RESULT = "this!"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("manual pick then separator", EXPECTED_RESULT, mEditText.getText().toString()); + } + + // This test matches testClusteringPunctuationForFrench. + // In some non-English languages, ! and ? are clustering punctuation signs. + public void testClusteringPunctuation() { + final String WORD1_TO_TYPE = "test"; + final String WORD2_TO_TYPE = "!!?!:!"; + final String EXPECTED_RESULT = "test!!?!:!"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("clustering punctuation", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManualPickThenStripperThenPick() { + final String WORD_TO_TYPE = "this"; + final String STRIPPER = "\n"; + final String EXPECTED_RESULT = "this\nthis"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + type(STRIPPER); + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + assertEquals("manual pick then \\n then manual pick", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManualPickThenSpaceThenType() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_TYPE = " is"; + final String EXPECTED_RESULT = "this is"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("manual pick then space then type", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManualPickThenManualPick() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_PICK = "is"; + final String EXPECTED_RESULT = "this is"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + // Here we fake picking a word through bigram prediction. + pickSuggestionManually(WORD2_TO_PICK); + assertEquals("manual pick then manual pick", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeleteWholeComposingWord() { + final String WORD_TO_TYPE = "this"; + type(WORD_TO_TYPE); + for (int i = 0; i < WORD_TO_TYPE.length(); ++i) { + type(Constants.CODE_DELETE); + } + assertEquals("delete whole composing word", "", mEditText.getText().toString()); + } + + public void testResumeSuggestionOnBackspace() { + final String STRING_TO_TYPE = "and this "; + final int typedLength = STRING_TO_TYPE.length(); + type(STRING_TO_TYPE); + assertEquals("resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + sendUpdateForCursorMoveTo(typedLength); + type(Constants.CODE_DELETE); + assertEquals("resume suggestion on backspace", 4, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("resume suggestion on backspace", 8, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + private void helperTestComposing(final String wordToType, final boolean shouldBeComposing) { + mEditText.setText(""); + type(wordToType); + assertEquals("start composing inside text", shouldBeComposing ? 0 : -1, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("start composing inside text", shouldBeComposing ? wordToType.length() : -1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testStartComposing() { + // Should start composing on a letter + helperTestComposing("a", true); + type(" "); // To reset the composing state + // Should not start composing on quote + helperTestComposing("'", false); + type(" "); + helperTestComposing("'-", false); + type(" "); + // Should not start composing on dash + helperTestComposing("-", false); + type(" "); + helperTestComposing("-'", false); + type(" "); + helperTestComposing("a-", true); + type(" "); + helperTestComposing("a'", true); + } + + // TODO: Add some tests for non-BMP characters + + public void testAutoCorrectByUserHistory() { + type("qpmz"); + type(Constants.CODE_SPACE); + + int startIndex = mEditText.getText().length(); + type("qpmx"); + type(Constants.CODE_SPACE); + int endIndex = mEditText.getText().length(); + assertEquals("auto-corrected by user history", + "qpmz ", mEditText.getText().subSequence(startIndex, endIndex).toString()); + } + + public void testPredictionsAfterSpace() { + final String WORD_TO_TYPE = "Barack "; + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + // Test the first prediction is displayed + final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("predictions after space", "Obama", + suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null); + } + + public void testPredictionsWithDoubleSpaceToPeriod() { + mLatinIME.clearPersonalizedDictionariesForTest(); + final String WORD_TO_TYPE = "Barack "; + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + + type(Constants.CODE_DELETE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + + SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("predictions after cancel double-space-to-period", "Obama", + mLatinIME.getSuggestedWordsForTest().getWord(0)); + } + + public void testPredictionsAfterManualPick() { + final String WORD_TO_TYPE = "Barack"; + type(WORD_TO_TYPE); + // Choose the auto-correction. For "Barack", the auto-correction should be "Barack". + pickSuggestionManually(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + // Test the first prediction is displayed + final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("predictions after manual pick", "Obama", + suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null); + } + + public void testPredictionsAfterPeriod() { + mLatinIME.clearPersonalizedDictionariesForTest(); + final String WORD_TO_TYPE = "Barack. "; + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + + SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertFalse(mLatinIME.getSuggestedWordsForTest().isEmpty()); + } + + public void testPredictionsAfterRecorrection() { + final String PREFIX = "A "; + final String WORD_TO_TYPE = "Barack"; + final String FIRST_NON_TYPED_SUGGESTION = "Barrack"; + final int endOfPrefix = PREFIX.length(); + final int endOfWord = endOfPrefix + WORD_TO_TYPE.length(); + final int endOfSuggestion = endOfPrefix + FIRST_NON_TYPED_SUGGESTION.length(); + final int indexForManualCursor = endOfPrefix + 3; // +3 because it's after "Bar" in "Barack" + type(PREFIX); + sendUpdateForCursorMoveTo(endOfPrefix); + type(WORD_TO_TYPE); + pickSuggestionManually(FIRST_NON_TYPED_SUGGESTION); + sendUpdateForCursorMoveTo(endOfSuggestion); + runMessages(); + type(" "); + sendUpdateForCursorMoveBy(1); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + // Simulate a manual cursor move + mInputConnection.setSelection(indexForManualCursor, indexForManualCursor); + sendUpdateForCursorMoveTo(indexForManualCursor); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + pickSuggestionManually(WORD_TO_TYPE); + sendUpdateForCursorMoveTo(endOfWord); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + // Test the first prediction is displayed + final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("predictions after recorrection", "Obama", + suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null); + } + + public void testComposingMultipleBackspace() { + final String WORD_TO_TYPE = "radklro"; + final int TIMES_TO_TYPE = 3; + final int TIMES_TO_BACKSPACE = 8; + type(WORD_TO_TYPE); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + type(WORD_TO_TYPE); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + type(WORD_TO_TYPE); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + type(Constants.CODE_DELETE); + assertEquals("composing with multiple backspace", + WORD_TO_TYPE.length() * TIMES_TO_TYPE - TIMES_TO_BACKSPACE, + mEditText.getText().length()); + } + + public void testManySingleQuotes() { + final String WORD_TO_AUTOCORRECT = "i"; + final String WORD_AUTOCORRECTED = "I"; + final String QUOTES = "''''''''''''''''''''"; + final String WORD_TO_TYPE = WORD_TO_AUTOCORRECT + QUOTES + " "; + final String EXPECTED_RESULT = WORD_AUTOCORRECTED + QUOTES + " "; + type(WORD_TO_TYPE); + assertEquals("auto-correct with many trailing single quotes", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManySingleQuotesOneByOne() { + final String WORD_TO_AUTOCORRECT = "i"; + final String WORD_AUTOCORRECTED = "I"; + final String QUOTES = "''''''''''''''''''''"; + final String WORD_TO_TYPE = WORD_TO_AUTOCORRECT + QUOTES + " "; + final String EXPECTED_RESULT = WORD_AUTOCORRECTED + QUOTES + " "; + + for (int i = 0; i < WORD_TO_TYPE.length(); ++i) { + type(WORD_TO_TYPE.substring(i, i+1)); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + } + assertEquals("type many trailing single quotes one by one", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testTypingSingleQuotesOneByOne() { + final String WORD_TO_TYPE = "it's "; + final String EXPECTED_RESULT = WORD_TO_TYPE; + for (int i = 0; i < WORD_TO_TYPE.length(); ++i) { + type(WORD_TO_TYPE.substring(i, i+1)); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + } + assertEquals("type words letter by letter", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testBasicGesture() { + gesture("this"); + assertEquals("this", mEditText.getText().toString()); + } + + public void testGestureGesture() { + gesture("got"); + gesture("milk"); + assertEquals("got milk", mEditText.getText().toString()); + } + + public void testGestureBackspaceGestureAgain() { + gesture("this"); + type(Constants.CODE_DELETE); + assertEquals("gesture then backspace", "", mEditText.getText().toString()); + gesture("this"); + if (DecoderSpecificConstants.SHOULD_REMOVE_PREVIOUSLY_REJECTED_SUGGESTION) { + assertNotEqual("this", mEditText.getText().toString()); + } else { + assertEquals("this", mEditText.getText().toString()); + } + } + + private void typeOrGestureWordAndPutCursorInside(final boolean gesture, final String word, + final int startPos) { + final int END_OF_WORD = startPos + word.length(); + final int NEW_CURSOR_POSITION = startPos + word.length() / 2; + if (gesture) { + gesture(word); + } else { + type(word); + } + sendUpdateForCursorMoveTo(END_OF_WORD); + runMessages(); + sendUpdateForCursorMoveTo(NEW_CURSOR_POSITION); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + } + + private void typeWordAndPutCursorInside(final String word, final int startPos) { + typeOrGestureWordAndPutCursorInside(false /* gesture */, word, startPos); + } + + private void gestureWordAndPutCursorInside(final String word, final int startPos) { + typeOrGestureWordAndPutCursorInside(true /* gesture */, word, startPos); + } + + private void ensureComposingSpanPos(final String message, final int from, final int to) { + assertEquals(message, from, BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals(message, to, BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testTypeWithinComposing() { + final String WORD_TO_TYPE = "something"; + final String EXPECTED_RESULT = "some thing"; + typeWordAndPutCursorInside(WORD_TO_TYPE, 0 /* startPos */); + type(" "); + ensureComposingSpanPos("space while in the middle of a word cancels composition", -1, -1); + assertEquals("space in the middle of a composing word", EXPECTED_RESULT, + mEditText.getText().toString()); + int cursorPos = sendUpdateForCursorMoveToEndOfLine(); + runMessages(); + type(" "); + assertEquals("mbo", "some thing ", mEditText.getText().toString()); + typeWordAndPutCursorInside(WORD_TO_TYPE, cursorPos + 1 /* startPos */); + type(Constants.CODE_DELETE); + ensureComposingSpanPos("delete while in the middle of a word cancels composition", -1, -1); + } + + public void testTypeWithinGestureComposing() { + final String WORD_TO_TYPE = "something"; + final String EXPECTED_RESULT = "some thing"; + gestureWordAndPutCursorInside(WORD_TO_TYPE, 0 /* startPos */); + type(" "); + ensureComposingSpanPos("space while in the middle of a word cancels composition", -1, -1); + assertEquals("space in the middle of a composing word", EXPECTED_RESULT, + mEditText.getText().toString()); + int cursorPos = sendUpdateForCursorMoveToEndOfLine(); + runMessages(); + type(" "); + typeWordAndPutCursorInside(WORD_TO_TYPE, cursorPos + 1 /* startPos */); + type(Constants.CODE_DELETE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + ensureComposingSpanPos("delete while in the middle of a word cancels composition", -1, -1); + } + + public void testManualPickThenSeparatorForFrench() { + final String WORD1_TO_TYPE = "test"; + final String WORD2_TO_TYPE = "!"; + final String EXPECTED_RESULT = "test !"; + changeLanguage("fr"); + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("manual pick then separator for French", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testClusteringPunctuationForFrench() { + final String WORD1_TO_TYPE = "test"; + final String WORD2_TO_TYPE = "!!?!:!"; + // In English, the expected result would be "test!!?!:!" + final String EXPECTED_RESULT = "test !!?! : !"; + changeLanguage("fr"); + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + type(WORD2_TO_TYPE); + assertEquals("clustering punctuation for French", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testWordThenSpaceThenPunctuationFromStripTwice() { + setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, false, true); + + final String WORD_TO_TYPE = "test "; + final String PUNCTUATION_FROM_STRIP = "!"; + final String EXPECTED_RESULT = "test!! "; + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + assertTrue("type word then type space should display punctuation strip", + mLatinIME.getSuggestedWordsForTest().isPunctuationSuggestions()); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + assertEquals(EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testWordThenSpaceDisplaysPredictions() { + final String WORD_TO_TYPE = "Barack "; + final String EXPECTED_RESULT = "Obama"; + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("type word then type space yields predictions for French", + EXPECTED_RESULT, suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/InputLogicTestsDeadKeys.java b/tests/src/org/kelar/inputmethod/latin/InputLogicTestsDeadKeys.java new file mode 100644 index 000000000..0c4662b1f --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/InputLogicTestsDeadKeys.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.event.Event; +import org.kelar.inputmethod.latin.common.Constants; + +import java.util.ArrayList; + +@LargeTest +public class InputLogicTestsDeadKeys extends InputTestsBase { + // A helper class for readability + static class EventList extends ArrayList<Event> { + public EventList addCodePoint(final int codePoint, final boolean isDead) { + final Event event; + if (isDead) { + event = Event.createDeadEvent(codePoint, Event.NOT_A_KEY_CODE, null /* next */); + } else { + event = Event.createSoftwareKeypressEvent(codePoint, Event.NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */); + } + add(event); + return this; + } + + public EventList addKey(final int keyCode) { + add(Event.createSoftwareKeypressEvent(Event.NOT_A_CODE_POINT, keyCode, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */)); + return this; + } + } + + public void testDeadCircumflexSimple() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final String EXPECTED_RESULT = "aê"; + final EventList events = new EventList() + .addCodePoint('a', false) + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true) + .addCodePoint('e', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("simple dead circumflex", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testDeadCircumflexBackspace() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final String EXPECTED_RESULT = "ae"; + final EventList events = new EventList() + .addCodePoint('a', false) + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true) + .addKey(Constants.CODE_DELETE) + .addCodePoint('e', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead circumflex backspace", EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testDeadCircumflexFeedback() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final String EXPECTED_RESULT = "a\u02C6"; + final EventList events = new EventList() + .addCodePoint('a', false) + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead circumflex gives feedback", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeadDiaeresisSpace() { + final int MODIFIER_LETTER_DIAERESIS = 0xA8; + final String EXPECTED_RESULT = "a\u00A8e\u00A8i"; + final EventList events = new EventList() + .addCodePoint('a', false) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addCodePoint(Constants.CODE_SPACE, false) + .addCodePoint('e', false) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addCodePoint(Constants.CODE_ENTER, false) + .addCodePoint('i', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead diaeresis space commits the dead char", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeadAcuteLetterBackspace() { + final int MODIFIER_LETTER_ACUTE = 0xB4; + final String EXPECTED_RESULT1 = "aá"; + final String EXPECTED_RESULT2 = "a"; + final EventList events = new EventList() + .addCodePoint('a', false) + .addCodePoint(MODIFIER_LETTER_ACUTE, true) + .addCodePoint('a', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead acute on a typed", EXPECTED_RESULT1, mEditText.getText().toString()); + mLatinIME.onEvent(Event.createSoftwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */)); + assertEquals("a with acute deleted", EXPECTED_RESULT2, mEditText.getText().toString()); + } + + public void testFinnishStroke() { + final int MODIFIER_LETTER_STROKE = '-'; + final String EXPECTED_RESULT = "x\u0110\u0127"; + final EventList events = new EventList() + .addCodePoint('x', false) + .addCodePoint(MODIFIER_LETTER_STROKE, true) + .addCodePoint('D', false) + .addCodePoint(MODIFIER_LETTER_STROKE, true) + .addCodePoint('h', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("Finnish dead stroke", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDoubleDeadOgonek() { + final int MODIFIER_LETTER_OGONEK = 0x02DB; + final String EXPECTED_RESULT = "txǫs\u02DBfk"; + final EventList events = new EventList() + .addCodePoint('t', false) + .addCodePoint('x', false) + .addCodePoint(MODIFIER_LETTER_OGONEK, true) + .addCodePoint('o', false) + .addCodePoint('s', false) + .addCodePoint(MODIFIER_LETTER_OGONEK, true) + .addCodePoint(MODIFIER_LETTER_OGONEK, true) + .addCodePoint('f', false) + .addCodePoint(MODIFIER_LETTER_OGONEK, true) + .addCodePoint(MODIFIER_LETTER_OGONEK, true) + .addKey(Constants.CODE_DELETE) + .addCodePoint('k', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("double dead ogonek, and backspace", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeadCircumflexDeadDiaeresis() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final int MODIFIER_LETTER_DIAERESIS = 0xA8; + final String EXPECTED_RESULT = "r̂̈"; + + final EventList events = new EventList() + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addCodePoint('r', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("both circumflex and diaeresis on r", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeadCircumflexDeadDiaeresisBackspace() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final int MODIFIER_LETTER_DIAERESIS = 0xA8; + final String EXPECTED_RESULT = "û"; + + final EventList events = new EventList() + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addKey(Constants.CODE_DELETE) + .addCodePoint('u', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead circumflex, dead diaeresis, backspace, u", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testDeadCircumflexDoubleDeadDiaeresisBackspace() { + final int MODIFIER_LETTER_CIRCUMFLEX_ACCENT = 0x02C6; + final int MODIFIER_LETTER_DIAERESIS = 0xA8; + final String EXPECTED_RESULT = "\u02C6u"; + + final EventList events = new EventList() + .addCodePoint(MODIFIER_LETTER_CIRCUMFLEX_ACCENT, true) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addCodePoint(MODIFIER_LETTER_DIAERESIS, true) + .addKey(Constants.CODE_DELETE) + .addCodePoint('u', false); + for (final Event event : events) { + mLatinIME.onEvent(event); + } + assertEquals("dead circumflex, double dead diaeresis, backspace, u", EXPECTED_RESULT, + mEditText.getText().toString()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java b/tests/src/org/kelar/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java new file mode 100644 index 000000000..9b4e661be --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java @@ -0,0 +1,135 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import android.view.inputmethod.BaseInputConnection; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.common.Constants; + +@LargeTest +public class InputLogicTestsLanguageWithoutSpaces extends InputTestsBase { + public void testAutoCorrectForLanguageWithoutSpaces() { + final String STRING_TO_TYPE = "tgis is"; + final String EXPECTED_RESULT = "thisis"; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(STRING_TO_TYPE); + assertEquals("simple auto-correct for language without spaces", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testRevertAutoCorrectForLanguageWithoutSpaces() { + final String STRING_TO_TYPE = "tgis "; + final String EXPECTED_INTERMEDIATE_RESULT = "this"; + final String EXPECTED_FINAL_RESULT = "tgis"; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(STRING_TO_TYPE); + assertEquals("simple auto-correct for language without spaces", + EXPECTED_INTERMEDIATE_RESULT, mEditText.getText().toString()); + type(Constants.CODE_DELETE); + assertEquals("simple auto-correct for language without spaces", + EXPECTED_FINAL_RESULT, mEditText.getText().toString()); + // Check we are back to composing the word + assertEquals("don't resume suggestion on backspace", 0, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("don't resume suggestion on backspace", 4, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testDontResumeSuggestionOnBackspace() { + final String WORD_TO_TYPE = "and this "; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(WORD_TO_TYPE); + assertEquals("don't resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("don't resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + type(" "); + type(Constants.CODE_DELETE); + assertEquals("don't resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("don't resume suggestion on backspace", -1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testStartComposingInsideText() { + final String WORD_TO_TYPE = "abcdefgh "; + final int typedLength = WORD_TO_TYPE.length() - 1; // -1 because space gets eaten + final int CURSOR_POS = 4; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(WORD_TO_TYPE); + mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, -1, -1); + mInputConnection.setSelection(CURSOR_POS, CURSOR_POS); + mLatinIME.onUpdateSelection(typedLength, typedLength, + CURSOR_POS, CURSOR_POS, -1, -1); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + assertEquals("start composing inside text", -1, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("start composing inside text", -1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + type("xxxx"); + assertEquals("start composing inside text", 4, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("start composing inside text", 8, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testMovingCursorInsideWordAndType() { + final String WORD_TO_TYPE = "abcdefgh"; + final int typedLength = WORD_TO_TYPE.length(); + final int CURSOR_POS = 4; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(WORD_TO_TYPE); + mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, 0, typedLength); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + mInputConnection.setSelection(CURSOR_POS, CURSOR_POS); + mLatinIME.onUpdateSelection(typedLength, typedLength, + CURSOR_POS, CURSOR_POS, 0, typedLength); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + assertEquals("move cursor inside text", 0, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("move cursor inside text", typedLength, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + type("x"); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + assertEquals("start typing while cursor inside composition", CURSOR_POS, + BaseInputConnection.getComposingSpanStart(mEditText.getText())); + assertEquals("start typing while cursor inside composition", CURSOR_POS + 1, + BaseInputConnection.getComposingSpanEnd(mEditText.getText())); + } + + public void testPredictions() { + final String WORD_TO_TYPE = "Barack "; + changeKeyboardLocaleAndDictLocale("th", "en_US"); + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + // Make sure there is no space + assertEquals("predictions in lang without spaces", "Barack", + mEditText.getText().toString()); + // Test the first prediction is displayed + final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest(); + assertEquals("predictions in lang without spaces", "Obama", + suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/InputTestsBase.java b/tests/src/org/kelar/inputmethod/latin/InputTestsBase.java new file mode 100644 index 000000000..e57b6823f --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/InputTestsBase.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin; + +import android.content.Context; +import android.content.SharedPreferences; +import android.graphics.Point; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.test.ServiceTestCase; +import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.style.CharacterStyle; +import android.text.style.SuggestionSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.EditText; +import android.widget.FrameLayout; + +import org.kelar.inputmethod.compat.InputMethodSubtypeCompatUtils; +import org.kelar.inputmethod.event.Event; +import org.kelar.inputmethod.keyboard.Key; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.latin.Dictionary.PhonyDictionary; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.InputPointers; +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.common.StringUtils; +import org.kelar.inputmethod.latin.settings.DebugSettings; +import org.kelar.inputmethod.latin.settings.Settings; +import org.kelar.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +public class InputTestsBase extends ServiceTestCase<LatinIMEForTests> { + private static final String TAG = InputTestsBase.class.getSimpleName(); + + // Default value for auto-correction threshold. This is the string representation of the + // index in the resources array of auto-correction threshold settings. + private static final boolean DEFAULT_AUTO_CORRECTION = true; + + // The message that sets the underline is posted with a 500 ms delay + protected static final int DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS = 500; + // The message that sets predictions is posted with a 200 ms delay + protected static final int DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS = 200; + // We wait for gesture computation for this delay + protected static final int DELAY_TO_WAIT_FOR_GESTURE_MILLIS = 200; + // If a dictionary takes longer to load, we could have serious problems. + private final int TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS = 5; + + // Type for a test phony dictionary + private static final String TYPE_TEST = "test"; + private static final PhonyDictionary DICTIONARY_TEST = new PhonyDictionary(TYPE_TEST); + + protected LatinIME mLatinIME; + protected Keyboard mKeyboard; + protected MyEditText mEditText; + protected View mInputView; + protected InputConnection mInputConnection; + private boolean mPreviousAutoCorrectSetting; + private boolean mPreviousBigramPredictionSettings; + + // A helper class to ease span tests + public static class SpanGetter { + final SpannableStringBuilder mInputText; + final CharacterStyle mSpan; + final int mStart; + final int mEnd; + // The supplied CharSequence should be an instance of SpannableStringBuilder, + // and it should contain exactly zero or one span. Otherwise, an exception + // is thrown. + public SpanGetter(final CharSequence inputText, + final Class<? extends CharacterStyle> spanType) { + mInputText = (SpannableStringBuilder)inputText; + final CharacterStyle[] spans = + mInputText.getSpans(0, mInputText.length(), spanType); + if (0 == spans.length) { + mSpan = null; + mStart = -1; + mEnd = -1; + } else if (1 == spans.length) { + mSpan = spans[0]; + mStart = mInputText.getSpanStart(mSpan); + mEnd = mInputText.getSpanEnd(mSpan); + } else { + throw new RuntimeException("Expected one span, found " + spans.length); + } + } + public SuggestionSpan getSpan() { + return (SuggestionSpan) mSpan; + } + public boolean isAutoCorrectionIndicator() { + return (mSpan instanceof SuggestionSpan) && + 0 != (SuggestionSpan.FLAG_AUTO_CORRECTION & getSpan().getFlags()); + } + public String[] getSuggestions() { + return getSpan().getSuggestions(); + } + } + + // A helper class to increase control over the EditText + public static class MyEditText extends EditText { + public Locale mCurrentLocale; + public MyEditText(final Context c) { + super(c); + } + + @Override + public void onAttachedToWindow() { + // Make onAttachedToWindow "public" + super.onAttachedToWindow(); + } + + // overriding hidden API in EditText + public Locale getTextServicesLocale() { + // This method is necessary because EditText is asking this method for the language + // to check the spell in. If we don't override this, the spell checker will run in + // whatever language the keyboard is currently set on the test device, ignoring any + // settings we do inside the tests. + return mCurrentLocale; + } + + // overriding hidden API in EditText + public Locale getSpellCheckerLocale() { + // This method is necessary because EditText is asking this method for the language + // to check the spell in. If we don't override this, the spell checker will run in + // whatever language the keyboard is currently set on the test device, ignoring any + // settings we do inside the tests. + return mCurrentLocale; + } + + } + + public InputTestsBase() { + super(LatinIMEForTests.class); + } + + protected boolean setBooleanPreference(final String key, final boolean value, + final boolean defaultValue) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); + final boolean previousSetting = prefs.getBoolean(key, defaultValue); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(key, value); + editor.apply(); + return previousSetting; + } + + protected boolean getBooleanPreference(final String key, final boolean defaultValue) { + return PreferenceManager.getDefaultSharedPreferences(mLatinIME) + .getBoolean(key, defaultValue); + } + + protected String setStringPreference(final String key, final String value, + final String defaultValue) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); + final String previousSetting = prefs.getString(key, defaultValue); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putString(key, value); + editor.apply(); + return previousSetting; + } + + protected void setDebugMode(final boolean value) { + setBooleanPreference(DebugSettings.PREF_DEBUG_MODE, value, false); + setBooleanPreference(Settings.PREF_KEY_IS_INTERNAL, value, false); + } + + protected EditorInfo enrichEditorInfo(final EditorInfo ei) { + // Some tests that inherit from us need to add some data in the EditorInfo (see + // AppWorkaroundsTests#enrichEditorInfo() for a concrete example of this). Since we + // control the EditorInfo, we supply a hook here for children to override. + return ei; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + mEditText = new MyEditText(getContext()); + final int inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE; + mEditText.setInputType(inputType); + mEditText.setEnabled(true); + mLastCursorPos = 0; + if (null == Looper.myLooper()) { + Looper.prepare(); + } + setupService(); + mLatinIME = getService(); + setDebugMode(true); + mPreviousBigramPredictionSettings = setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, + true, true /* defaultValue */); + mPreviousAutoCorrectSetting = setBooleanPreference(Settings.PREF_AUTO_CORRECTION, + DEFAULT_AUTO_CORRECTION, DEFAULT_AUTO_CORRECTION); + mLatinIME.onCreate(); + EditorInfo ei = new EditorInfo(); + final InputConnection ic = mEditText.onCreateInputConnection(ei); + final LayoutInflater inflater = + (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + final ViewGroup vg = new FrameLayout(getContext()); + mInputView = inflater.inflate(R.layout.input_view, vg); + ei = enrichEditorInfo(ei); + mLatinIME.onCreateInputMethodInterface().startInput(ic, ei); + mLatinIME.setInputView(mInputView); + mLatinIME.onBindInput(); + mLatinIME.onCreateInputView(); + mLatinIME.onStartInputView(ei, false); + mInputConnection = ic; + changeLanguage("en_US"); + // Run messages to avoid the messages enqueued by startInputView() and its friends + // to run on a later call and ruin things. We need to wait first because some of them + // can be posted with a delay (notably, MSG_RESUME_SUGGESTIONS) + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + } + + @Override + protected void tearDown() throws Exception { + mLatinIME.onFinishInputView(true); + mLatinIME.onFinishInput(); + runMessages(); + mLatinIME.mHandler.removeAllMessages(); + setBooleanPreference(Settings.PREF_BIGRAM_PREDICTIONS, mPreviousBigramPredictionSettings, + true /* defaultValue */); + setBooleanPreference(Settings.PREF_AUTO_CORRECTION, mPreviousAutoCorrectSetting, + DEFAULT_AUTO_CORRECTION); + setDebugMode(false); + mLatinIME.recycle(); + super.tearDown(); + mLatinIME = null; + } + + // We need to run the messages added to the handler from LatinIME. The only way to do + // that is to call Looper#loop() on the right looper, so we're going to get the looper + // object and call #loop() here. The messages in the handler actually run on the UI + // thread of the keyboard by design of the handler, so we want to call it synchronously + // on the same thread that the tests are running on to mimic the actual environment as + // closely as possible. + // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method + // is called, which has a lot of bad side effects. We can however just throw an exception + // in the runnable which will unwind the stack and allow us to exit. + final class InterruptRunMessagesException extends RuntimeException { + // Empty class + } + protected void runMessages() { + mLatinIME.mHandler.post(new Runnable() { + @Override + public void run() { + throw new InterruptRunMessagesException(); + } + }); + try { + Looper.loop(); + } catch (InterruptRunMessagesException e) { + // Resume normal operation + } + } + + // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME. + protected void typeInternal(final int codePoint, final boolean isKeyRepeat) { + // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the + // code (although multitouch/slide input and other factors make the sequencing complicated). + // They are supposed to be entirely deconnected from the input logic from LatinIME point of + // view and only delegates to the parts of the code that care. So we don't include them here + // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies, + // but keep them in mind if something breaks. Commenting them out as is should work. + //mLatinIME.onPressKey(codePoint, 0 /* repeatCount */, true /* isSinglePointer */); + final Key key = mKeyboard.getKey(codePoint); + final Event event; + if (key == null) { + event = Event.createSoftwareKeypressEvent(codePoint, Event.NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat); + } else { + final int x = key.getX() + key.getWidth() / 2; + final int y = key.getY() + key.getHeight() / 2; + event = LatinIME.createSoftwareKeypressEvent(codePoint, x, y, isKeyRepeat); + } + mLatinIME.onEvent(event); + // Also see the comment at the top of this function about onReleaseKey + //mLatinIME.onReleaseKey(codePoint, false /* withSliding */); + } + + protected void type(final int codePoint) { + typeInternal(codePoint, false /* isKeyRepeat */); + } + + protected void repeatKey(final int codePoint) { + typeInternal(codePoint, true /* isKeyRepeat */); + } + + protected void type(final String stringToType) { + for (int i = 0; i < stringToType.length(); i = stringToType.offsetByCodePoints(i, 1)) { + type(stringToType.codePointAt(i)); + } + } + + protected Point getXY(final int codePoint) { + final Key key = mKeyboard.getKey(codePoint); + if (key == null) { + throw new RuntimeException("Code point not on the keyboard"); + } + return new Point(key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2); + } + + protected void gesture(final String stringToGesture) { + if (StringUtils.codePointCount(stringToGesture) < 2) { + throw new RuntimeException("Can't gesture strings less than 2 chars long"); + } + + mLatinIME.onStartBatchInput(); + final int startCodePoint = stringToGesture.codePointAt(0); + Point oldPoint = getXY(startCodePoint); + int timestamp = 0; // In milliseconds since the start of the gesture + final InputPointers pointers = new InputPointers(Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + pointers.addPointer(oldPoint.x, oldPoint.y, 0 /* pointerId */, timestamp); + + for (int i = Character.charCount(startCodePoint); i < stringToGesture.length(); + i = stringToGesture.offsetByCodePoints(i, 1)) { + final Point newPoint = getXY(stringToGesture.codePointAt(i)); + // Arbitrarily 0.5s between letters and 0.1 between events. Refine this later if needed. + final int STEPS = 5; + for (int j = 0; j < STEPS; ++j) { + timestamp += 100; + pointers.addPointer(oldPoint.x + ((newPoint.x - oldPoint.x) * j) / STEPS, + oldPoint.y + ((newPoint.y - oldPoint.y) * j) / STEPS, + 0 /* pointerId */, timestamp); + } + oldPoint.x = newPoint.x; + oldPoint.y = newPoint.y; + mLatinIME.onUpdateBatchInput(pointers); + } + mLatinIME.onEndBatchInput(pointers); + sleep(DELAY_TO_WAIT_FOR_GESTURE_MILLIS); + runMessages(); + } + + protected void waitForDictionariesToBeLoaded() { + try { + mLatinIME.waitForLoadingDictionaries( + TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted during waiting for loading main dictionary.", e); + } + } + + protected void changeLanguage(final String locale) { + changeLanguage(locale, null); + } + + protected void changeLanguage(final String locale, final String combiningSpec) { + changeLanguageWithoutWait(locale, combiningSpec); + waitForDictionariesToBeLoaded(); + } + + protected void changeLanguageWithoutWait(final String locale, final String combiningSpec) { + mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale); + // TODO: this is forcing a QWERTY keyboard for all locales, which is wrong. + // It's still better than using whatever keyboard is the current one, but we + // should actually use the default keyboard for this locale. + // TODO: Use {@link InputMethodSubtype.InputMethodSubtypeBuilder} directly or indirectly so + // that {@link InputMethodSubtype#isAsciiCapable} can return the correct value. + final String EXTRA_VALUE_FOR_TEST = + "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY + + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE + + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE + + null == combiningSpec ? "" : ("," + combiningSpec); + final InputMethodSubtype subtype = InputMethodSubtypeCompatUtils.newInputMethodSubtype( + R.string.subtype_no_language_qwerty, + R.drawable.ic_ime_switcher_dark, + locale, + Constants.Subtype.KEYBOARD_MODE, + EXTRA_VALUE_FOR_TEST, + false /* isAuxiliary */, + false /* overridesImplicitlyEnabledSubtype */, + 0 /* id */); + RichInputMethodManager.forceSubtype(subtype); + mLatinIME.onCurrentInputMethodSubtypeChanged(subtype); + runMessages(); + mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard(); + mLatinIME.clearPersonalizedDictionariesForTest(); + } + + protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale, + final String dictLocale) { + changeLanguage(keyboardLocale); + if (!keyboardLocale.equals(dictLocale)) { + mLatinIME.replaceDictionariesForTest(LocaleUtils.constructLocaleFromString(dictLocale)); + } + waitForDictionariesToBeLoaded(); + } + + protected void pickSuggestionManually(final String suggestion) { + mLatinIME.pickSuggestionManually(new SuggestedWordInfo(suggestion, + "" /* prevWordsContext */, 1 /* score */, + SuggestedWordInfo.KIND_CORRECTION, DICTIONARY_TEST, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); + } + + // Helper to avoid writing the try{}catch block each time + protected static void sleep(final int milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) {} + } + + // Some helper methods to manage the mock cursor position + // DO NOT CALL LatinIME#onUpdateSelection IF YOU WANT TO USE THOSE + int mLastCursorPos = 0; + /** + * Move the cached cursor position to the passed position and send onUpdateSelection to LatinIME + */ + protected int sendUpdateForCursorMoveTo(final int position) { + mInputConnection.setSelection(position, position); + mLatinIME.onUpdateSelection(mLastCursorPos, mLastCursorPos, position, position, -1, -1); + mLastCursorPos = position; + return position; + } + + /** + * Move the cached cursor position by the passed amount and send onUpdateSelection to LatinIME + */ + protected int sendUpdateForCursorMoveBy(final int offset) { + final int lastPos = mEditText.getText().length(); + final int requestedPosition = mLastCursorPos + offset; + if (requestedPosition < 0) { + return sendUpdateForCursorMoveTo(0); + } else if (requestedPosition > lastPos) { + return sendUpdateForCursorMoveTo(lastPos); + } else { + return sendUpdateForCursorMoveTo(requestedPosition); + } + } + + /** + * Move the cached cursor position to the end of the line and send onUpdateSelection to LatinIME + */ + protected int sendUpdateForCursorMoveToEndOfLine() { + final int lastPos = mEditText.getText().length(); + return sendUpdateForCursorMoveTo(lastPos); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/LatinIMEForTests.java b/tests/src/org/kelar/inputmethod/latin/LatinIMEForTests.java new file mode 100644 index 000000000..f9e975556 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/LatinIMEForTests.java @@ -0,0 +1,36 @@ +/* + * 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 org.kelar.inputmethod.latin; + +public class LatinIMEForTests extends LatinIME { + @Override + public boolean isInputViewShown() { + return true; + } + + private boolean deallocateMemoryWasPerformed = false; + + @Override + protected void deallocateMemory() { + super.deallocateMemory(); + deallocateMemoryWasPerformed = true; + } + + public boolean getDeallocateMemoryWasPerformed() { + return deallocateMemoryWasPerformed; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/LatinImeStressTests.java b/tests/src/org/kelar/inputmethod/latin/LatinImeStressTests.java new file mode 100644 index 000000000..6b9f65931 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/LatinImeStressTests.java @@ -0,0 +1,62 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.common.CodePointUtils; + +import java.util.Random; + +@LargeTest +public class LatinImeStressTests extends InputTestsBase { + public void testSwitchLanguagesAndInputLatinRandomCodePoints() { + final String[] locales = {"en_US", "de", "el", "es", "fi", "it", "nl", "pt", "ru"}; + final int switchCount = 50; + final int maxWordCountToTypeInEachIteration = 20; + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final int[] codePointSet = CodePointUtils.LATIN_ALPHABETS_LOWER; + for (int i = 0; i < switchCount; ++i) { + changeLanguageWithoutWait(locales[random.nextInt(locales.length)], + null /* combiningSpec */); + final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration); + for (int j = 0; j < wordCount; ++j) { + final String word = CodePointUtils.generateWord(random, codePointSet); + type(word); + } + } + } + public void testSwitchLanguagesAndInputRandomCodePoints() { + final String[] locales = {"en_US", "de", "el", "es", "fi", "it", "nl", "pt", "ru"}; + final int switchCount = 50; + final int maxWordCountToTypeInEachIteration = 20; + final long seed = System.currentTimeMillis(); + final Random random = new Random(seed); + final int codePointSetSize = 30; + final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random); + for (int i = 0; i < switchCount; ++i) { + changeLanguageWithoutWait(locales[random.nextInt(locales.length)], + null /* combiningSpec */); + final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration); + for (int j = 0; j < wordCount; ++j) { + final String word = CodePointUtils.generateWord(random, codePointSet); + type(word); + } + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/LatinImeTests.java b/tests/src/org/kelar/inputmethod/latin/LatinImeTests.java new file mode 100644 index 000000000..5d3d04783 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/LatinImeTests.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import androidx.test.filters.LargeTest; + +@LargeTest +public class LatinImeTests extends InputTestsBase { + public void testDeferredDeallocation_doesntHappenBeforeTimeout() { + mLatinIME.mHandler.onFinishInputView(true); + runMessages(); + sleep(1000); // 1s + runMessages(); + assertFalse("memory deallocation performed before timeout passed", + ((LatinIMEForTests)mLatinIME).getDeallocateMemoryWasPerformed()); + } + + public void testDeferredDeallocation_doesHappenAfterTimeout() { + mLatinIME.mHandler.onFinishInputView(true); + runMessages(); + sleep(11000); // 11s (timeout is at 10s) + runMessages(); + assertTrue("memory deallocation not performed although timeout passed", + ((LatinIMEForTests)mLatinIME).getDeallocateMemoryWasPerformed()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/NgramContextTests.java b/tests/src/org/kelar/inputmethod/latin/NgramContextTests.java new file mode 100644 index 000000000..07b66271d --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/NgramContextTests.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.NgramContext.WordInfo; +import org.kelar.inputmethod.latin.settings.SpacingAndPunctuations; +import org.kelar.inputmethod.latin.utils.NgramContextUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NgramContextTests { + + @Test + public void testConstruct() { + assertEquals(new NgramContext(new WordInfo("a")), new NgramContext(new WordInfo("a"))); + assertEquals(new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO), + new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO)); + assertEquals(new NgramContext(WordInfo.EMPTY_WORD_INFO), + new NgramContext(WordInfo.EMPTY_WORD_INFO)); + assertEquals(new NgramContext(WordInfo.EMPTY_WORD_INFO), + new NgramContext(WordInfo.EMPTY_WORD_INFO)); + } + + @Test + public void testIsBeginningOfSentenceContext() { + assertFalse(new NgramContext().isBeginningOfSentenceContext()); + assertTrue(new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO) + .isBeginningOfSentenceContext()); + assertTrue(NgramContext.BEGINNING_OF_SENTENCE.isBeginningOfSentenceContext()); + assertFalse(new NgramContext(new WordInfo("a")).isBeginningOfSentenceContext()); + assertFalse(new NgramContext(new WordInfo("")).isBeginningOfSentenceContext()); + assertFalse(new NgramContext(WordInfo.EMPTY_WORD_INFO).isBeginningOfSentenceContext()); + assertTrue(new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO, new WordInfo("a")) + .isBeginningOfSentenceContext()); + assertFalse(new NgramContext(new WordInfo("a"), WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO) + .isBeginningOfSentenceContext()); + assertFalse(new NgramContext( + WordInfo.EMPTY_WORD_INFO, WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO) + .isBeginningOfSentenceContext()); + } + + @Test + public void testGetNextNgramContext() { + final NgramContext ngramContext_a = new NgramContext(new WordInfo("a")); + final NgramContext ngramContext_b_a = + ngramContext_a.getNextNgramContext(new WordInfo("b")); + assertEquals("b", ngramContext_b_a.getNthPrevWord(1)); + assertEquals("a", ngramContext_b_a.getNthPrevWord(2)); + final NgramContext ngramContext_bos_b = + ngramContext_b_a.getNextNgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + assertTrue(ngramContext_bos_b.isBeginningOfSentenceContext()); + assertEquals("b", ngramContext_bos_b.getNthPrevWord(2)); + final NgramContext ngramContext_c_bos = + ngramContext_b_a.getNextNgramContext(new WordInfo("c")); + assertEquals("c", ngramContext_c_bos.getNthPrevWord(1)); + } + + @Test + public void testExtractPrevWordsContextTest() { + final NgramContext ngramContext_bos = + new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + assertEquals("<S>", ngramContext_bos.extractPrevWordsContext()); + final NgramContext ngramContext_a = new NgramContext(new WordInfo("a")); + final NgramContext ngramContext_b_a = + ngramContext_a.getNextNgramContext(new WordInfo("b")); + assertEquals("b", ngramContext_b_a.getNthPrevWord(1)); + assertEquals("a", ngramContext_b_a.getNthPrevWord(2)); + assertEquals("a b", ngramContext_b_a.extractPrevWordsContext()); + + final NgramContext ngramContext_bos_b = + ngramContext_b_a.getNextNgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + assertTrue(ngramContext_bos_b.isBeginningOfSentenceContext()); + assertEquals("b", ngramContext_bos_b.getNthPrevWord(2)); + assertEquals("a b <S>", ngramContext_bos_b.extractPrevWordsContext()); + + final NgramContext ngramContext_empty = new NgramContext(WordInfo.EMPTY_WORD_INFO); + assertEquals("", ngramContext_empty.extractPrevWordsContext()); + final NgramContext ngramContext_a_empty = + ngramContext_empty.getNextNgramContext(new WordInfo("a")); + assertEquals("a", ngramContext_a_empty.getNthPrevWord(1)); + assertEquals("a", ngramContext_a_empty.extractPrevWordsContext()); + } + + @Test + public void testExtractPrevWordsContextArray() { + final NgramContext ngramContext_bos = + new NgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + assertEquals("<S>", ngramContext_bos.extractPrevWordsContext()); + assertEquals(1, ngramContext_bos.extractPrevWordsContextArray().length); + final NgramContext ngramContext_a = new NgramContext(new WordInfo("a")); + final NgramContext ngramContext_b_a = + ngramContext_a.getNextNgramContext(new WordInfo("b")); + assertEquals(2, ngramContext_b_a.extractPrevWordsContextArray().length); + assertEquals("b", ngramContext_b_a.getNthPrevWord(1)); + assertEquals("a", ngramContext_b_a.getNthPrevWord(2)); + assertEquals("a", ngramContext_b_a.extractPrevWordsContextArray()[0]); + assertEquals("b", ngramContext_b_a.extractPrevWordsContextArray()[1]); + + final NgramContext ngramContext_bos_b = + ngramContext_b_a.getNextNgramContext(WordInfo.BEGINNING_OF_SENTENCE_WORD_INFO); + assertTrue(ngramContext_bos_b.isBeginningOfSentenceContext()); + assertEquals(3, ngramContext_bos_b.extractPrevWordsContextArray().length); + assertEquals("b", ngramContext_bos_b.getNthPrevWord(2)); + assertEquals("a", ngramContext_bos_b.extractPrevWordsContextArray()[0]); + assertEquals("b", ngramContext_bos_b.extractPrevWordsContextArray()[1]); + assertEquals("<S>", ngramContext_bos_b.extractPrevWordsContextArray()[2]); + + final NgramContext ngramContext_empty = new NgramContext(WordInfo.EMPTY_WORD_INFO); + assertEquals(0, ngramContext_empty.extractPrevWordsContextArray().length); + final NgramContext ngramContext_a_empty = + ngramContext_empty.getNextNgramContext(new WordInfo("a")); + assertEquals(1, ngramContext_a_empty.extractPrevWordsContextArray().length); + assertEquals("a", ngramContext_a_empty.extractPrevWordsContextArray()[0]); + } + + @Test + public void testGetNgramContextFromNthPreviousWord() { + SpacingAndPunctuations spacingAndPunctuations = new SpacingAndPunctuations( + InstrumentationRegistry.getTargetContext().getResources()); + assertEquals("<S>", NgramContextUtils.getNgramContextFromNthPreviousWord("", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertEquals("<S> b", NgramContextUtils.getNgramContextFromNthPreviousWord("a. b ", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertEquals("<S> b", NgramContextUtils.getNgramContextFromNthPreviousWord("a? b ", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertEquals("<S> b", NgramContextUtils.getNgramContextFromNthPreviousWord("a! b ", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertEquals("<S> b", NgramContextUtils.getNgramContextFromNthPreviousWord("a\nb ", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertEquals("<S> a b", NgramContextUtils.getNgramContextFromNthPreviousWord("a b ", + spacingAndPunctuations, 1).extractPrevWordsContext()); + assertFalse(NgramContextUtils + .getNgramContextFromNthPreviousWord("a b c d e", spacingAndPunctuations, 1) + .extractPrevWordsContext().startsWith("<S>")); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/PunctuationTests.java b/tests/src/org/kelar/inputmethod/latin/PunctuationTests.java new file mode 100644 index 000000000..c8337430d --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/PunctuationTests.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin; + +import android.provider.Settings.Secure; + +import androidx.test.filters.LargeTest; + +@LargeTest +public class PunctuationTests extends InputTestsBase { + + final String NEXT_WORD_PREDICTION_OPTION = "next_word_prediction"; + + public void testWordThenSpaceThenPunctuationFromStripTwice() { + final String WORD_TO_TYPE = "this "; + final String PUNCTUATION_FROM_STRIP = "!"; + final String EXPECTED_RESULT = "this!! "; + final boolean defaultNextWordPredictionOption = + mLatinIME.getResources().getBoolean(R.bool.config_default_next_word_prediction); + final boolean previousNextWordPredictionOption = + setBooleanPreference(NEXT_WORD_PREDICTION_OPTION, false, + defaultNextWordPredictionOption); + try { + mLatinIME.loadSettings(); + type(WORD_TO_TYPE); + sleep(DELAY_TO_WAIT_FOR_UNDERLINE_MILLIS); + runMessages(); + assertTrue("type word then type space should display punctuation strip", + mLatinIME.getSuggestedWordsForTest().isPunctuationSuggestions()); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + assertEquals("type word then type space then punctuation from strip twice", + EXPECTED_RESULT, mEditText.getText().toString()); + } finally { + setBooleanPreference(NEXT_WORD_PREDICTION_OPTION, previousNextWordPredictionOption, + defaultNextWordPredictionOption); + } + } + + public void testWordThenSpaceThenPunctuationFromKeyboardTwice() { + final String WORD_TO_TYPE = "this !!"; + final String EXPECTED_RESULT = "this !!"; + type(WORD_TO_TYPE); + assertEquals("manual pick then space then punctuation from keyboard twice", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManualPickThenPunctuationFromStripTwiceThenType() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_TYPE = "is"; + final String PUNCTUATION_FROM_STRIP = "!"; + final String EXPECTED_RESULT = "this!! is"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + pickSuggestionManually(PUNCTUATION_FROM_STRIP); + type(WORD2_TO_TYPE); + assertEquals("pick word then pick punctuation twice then type", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManualPickThenManualPickWithPunctAtStart() { + final String WORD1_TO_TYPE = "this"; + final String WORD2_TO_PICK = "!is"; + final String EXPECTED_RESULT = "this!is"; + type(WORD1_TO_TYPE); + pickSuggestionManually(WORD1_TO_TYPE); + pickSuggestionManually(WORD2_TO_PICK); + assertEquals("manual pick then manual pick a word with punct at start", EXPECTED_RESULT, + mEditText.getText().toString()); + } + + public void testManuallyPickedWordThenColon() { + final String WORD_TO_TYPE = "this"; + final String PUNCTUATION = ":"; + final String EXPECTED_RESULT = "this:"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + type(PUNCTUATION); + assertEquals("manually pick word then colon", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManuallyPickedWordThenOpenParen() { + final String WORD_TO_TYPE = "this"; + final String PUNCTUATION = "("; + final String EXPECTED_RESULT = "this ("; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + type(PUNCTUATION); + assertEquals("manually pick word then open paren", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManuallyPickedWordThenCloseParen() { + final String WORD_TO_TYPE = "this"; + final String PUNCTUATION = ")"; + final String EXPECTED_RESULT = "this)"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + type(PUNCTUATION); + assertEquals("manually pick word then close paren", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManuallyPickedWordThenSmiley() { + final String WORD_TO_TYPE = "this"; + final String SPECIAL_KEY = ":-)"; + final String EXPECTED_RESULT = "this :-)"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + mLatinIME.onTextInput(SPECIAL_KEY); + assertEquals("manually pick word then press the smiley key", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testManuallyPickedWordThenDotCom() { + final String WORD_TO_TYPE = "this"; + final String SPECIAL_KEY = ".com"; + final String EXPECTED_RESULT = "this.com"; + type(WORD_TO_TYPE); + pickSuggestionManually(WORD_TO_TYPE); + mLatinIME.onTextInput(SPECIAL_KEY); + assertEquals("manually pick word then press the .com key", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testTypeWordTypeDotThenPressDotCom() { + final String WORD_TO_TYPE = "this."; + final String SPECIAL_KEY = ".com"; + final String EXPECTED_RESULT = "this.com"; + type(WORD_TO_TYPE); + mLatinIME.onTextInput(SPECIAL_KEY); + assertEquals("type word type dot then press the .com key", + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectionWithSingleQuoteInside() { + final String WORD_TO_TYPE = "you'f "; + final String EXPECTED_RESULT = "you'd "; + type(WORD_TO_TYPE); + assertEquals("auto-correction with single quote inside. ID = " + + Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID) + + " ; Suggestions = " + mLatinIME.getSuggestedWordsForTest(), + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoCorrectionWithSingleQuotesAround() { + final String WORD_TO_TYPE = "'tgis' "; + final String EXPECTED_RESULT = "'this' "; + type(WORD_TO_TYPE); + assertEquals("auto-correction with single quotes around. ID = " + + Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID) + + " ; Suggestions = " + mLatinIME.getSuggestedWordsForTest(), + EXPECTED_RESULT, mEditText.getText().toString()); + } + + public void testAutoSpaceWithDoubleQuotes() { + final String STRING_TO_TYPE = "He said\"hello\"to me. I replied,\"hi\"." + + "Then, 5\"passed. He said\"bye\"and left."; + final String EXPECTED_RESULT = "He said \"hello\" to me. I replied, \"hi\". " + + "Then, 5\" passed. He said \"bye\" and left. \""; + // Split by double quote, so that we can type the double quotes individually. + for (final String partToType : STRING_TO_TYPE.split("\"")) { + // Split at word boundaries. This regexp means "anywhere that is preceded + // by a word character but not followed by a word character, OR that is not + // preceded by a word character but followed by a word character". + // We need to input word by word because auto-spaces are only active when + // manually picking or gesturing (which we can't simulate yet), but only words + // can be picked. + final String[] wordsToType = partToType.split("(?<=\\w)(?!\\w)|(?<!\\w)(?=\\w)"); + for (final String wordToType : wordsToType) { + type(wordToType); + if (wordToType.matches("^\\w+$")) { + // Only pick selection if that was a word, because if that was not a word, + // then we don't have a composition. + pickSuggestionManually(wordToType); + } + } + type("\""); + } + assertEquals("auto-space with double quotes", + EXPECTED_RESULT, mEditText.getText().toString()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/RichInputConnectionAndTextRangeTests.java b/tests/src/org/kelar/inputmethod/latin/RichInputConnectionAndTextRangeTests.java new file mode 100644 index 000000000..cf4a349d5 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/RichInputConnectionAndTextRangeTests.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.res.Resources; +import android.inputmethodservice.InputMethodService; +import android.os.Parcel; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.StringUtils; +import org.kelar.inputmethod.latin.settings.SpacingAndPunctuations; +import org.kelar.inputmethod.latin.utils.NgramContextUtils; +import org.kelar.inputmethod.latin.utils.RunInLocale; +import org.kelar.inputmethod.latin.utils.ScriptUtils; +import org.kelar.inputmethod.latin.utils.TextRange; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RichInputConnectionAndTextRangeTests { + + // The following is meant to be a reasonable default for + // the "word_separators" resource. + private SpacingAndPunctuations mSpacingAndPunctuations; + + @Before + public void setUp() throws Exception { + final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(final Resources res) { + return new SpacingAndPunctuations(res); + } + }; + final Resources res = InstrumentationRegistry.getTargetContext().getResources(); + mSpacingAndPunctuations = job.runInLocale(res, Locale.ENGLISH); + } + + private class MockConnection extends InputConnectionWrapper { + final CharSequence mTextBefore; + final CharSequence mTextAfter; + final ExtractedText mExtractedText; + + public MockConnection(final CharSequence text, final int cursorPosition) { + super(null, false); + // Interaction of spans with Parcels is completely non-trivial, but in the actual case + // the CharSequences do go through Parcels because they go through IPC. There + // are some significant differences between the behavior of Spanned objects that + // have and that have not gone through parceling, so it's much easier to simulate + // the environment with Parcels than try to emulate things by hand. + final Parcel p = Parcel.obtain(); + TextUtils.writeToParcel(text.subSequence(0, cursorPosition), p, 0 /* flags */); + TextUtils.writeToParcel(text.subSequence(cursorPosition, text.length()), p, + 0 /* flags */); + final byte[] marshalled = p.marshall(); + p.unmarshall(marshalled, 0, marshalled.length); + p.setDataPosition(0); + mTextBefore = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); + mTextAfter = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); + mExtractedText = null; + p.recycle(); + } + + public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) { + super(null, false); + mTextBefore = textBefore; + mTextAfter = textAfter; + mExtractedText = extractedText; + } + + public int cursorPos() { + return mTextBefore.length(); + } + + /* (non-Javadoc) + * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int) + */ + @Override + public CharSequence getTextBeforeCursor(int n, int flags) { + return mTextBefore; + } + + /* (non-Javadoc) + * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int) + */ + @Override + public CharSequence getTextAfterCursor(int n, int flags) { + return mTextAfter; + } + + /* (non-Javadoc) + * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText( + * ExtractedTextRequest, int) + */ + @Override + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { + return mExtractedText; + } + + @Override + public boolean beginBatchEdit() { + return true; + } + + @Override + public boolean endBatchEdit() { + return true; + } + + @Override + public boolean finishComposingText() { + return true; + } + } + + static class MockInputMethodService extends InputMethodService { + private MockConnection mMockConnection; + public void setInputConnection(final MockConnection mockConnection) { + mMockConnection = mockConnection; + } + public int cursorPos() { + return mMockConnection.cursorPos(); + } + @Override + public InputConnection getCurrentInputConnection() { + return mMockConnection; + } + } + + /************************** Tests ************************/ + + /** + * Test for getting previous word (for bigram suggestions) + */ + @Test + public void testGetPreviousWord() { + // If one of the following cases breaks, the bigram suggestions won't work. + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc. def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); + + assertFalse(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); + assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); + + // For n-gram + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(2), "abc"); + assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 2).isNthPrevWordBeginningOfSentence(2)); + + // The following tests reflect the current behavior of the function + // RichInputConnection#getNthPreviousWord. + // TODO: However at this time, the code does never go + // into such a path, so it should be safe to change the behavior of + // this function if needed - especially since it does not seem very + // logical. These tests are just there to catch any unintentional + // changes in the behavior of the RichInputConnection#getPreviousWord method. + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def ", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def.", mSpacingAndPunctuations, 2).getNthPrevWord(1), "abc"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def .", mSpacingAndPunctuations, 2).getNthPrevWord(1), "def"); + assertTrue(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc ", mSpacingAndPunctuations, 2).isBeginningOfSentenceContext()); + + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def ", mSpacingAndPunctuations, 1).getNthPrevWord(1), "def"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc 'def", mSpacingAndPunctuations, 1).getNthPrevWord(1), "'def"); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def.", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc def .", mSpacingAndPunctuations, 1), NgramContext.BEGINNING_OF_SENTENCE); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc, def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO); + // question mark is treated as the end of the sentence. Hence, beginning of the + // sentence is expected. + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc? def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); + // Exclamation mark is treated as the end of the sentence. Hence, beginning of the + // sentence is expected. + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc! def", mSpacingAndPunctuations, 2), NgramContext.BEGINNING_OF_SENTENCE); + assertEquals(NgramContextUtils.getNgramContextFromNthPreviousWord( + "abc 'def", mSpacingAndPunctuations, 2), NgramContext.EMPTY_PREV_WORDS_INFO); + } + + @Test + public void testGetWordRangeAtCursor() { + /** + * Test logic in getting the word range at the cursor. + */ + final SpacingAndPunctuations SPACE = new SpacingAndPunctuations( + mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE }); + final SpacingAndPunctuations TAB = new SpacingAndPunctuations( + mSpacingAndPunctuations, new int[] { Constants.CODE_TAB }); + // A character that needs surrogate pair to represent its code point (U+2008A). + final String SUPPLEMENTARY_CHAR_STRING = "\uD840\uDC8A"; + final SpacingAndPunctuations SUPPLEMENTARY_CHAR = new SpacingAndPunctuations( + mSpacingAndPunctuations, StringUtils.toSortedCodePointArray( + SUPPLEMENTARY_CHAR_STRING)); + final String HIRAGANA_WORD = "\u3042\u3044\u3046\u3048\u304A"; // あいうえお + final String GREEK_WORD = "\u03BA\u03B1\u03B9"; // και + + ExtractedText et = new ExtractedText(); + final MockInputMethodService mockInputMethodService = new MockInputMethodService(); + final RichInputConnection ic = new RichInputConnection(mockInputMethodService); + mockInputMethodService.setInputConnection(new MockConnection("word wo", "rd", et)); + et.startOffset = 0; + et.selectionStart = 7; + TextRange r; + + ic.beginBatchEdit(); + // basic case + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + assertTrue(TextUtils.equals("word", r.mWord)); + + // tab character instead of space + mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et)); + ic.beginBatchEdit(); + r = ic.getWordRangeAtCursor(TAB, ScriptUtils.SCRIPT_LATIN); + ic.endBatchEdit(); + assertTrue(TextUtils.equals("word", r.mWord)); + + // splitting on supplementary character + mockInputMethodService.setInputConnection( + new MockConnection("one word" + SUPPLEMENTARY_CHAR_STRING + "wo", "rd", et)); + ic.beginBatchEdit(); + r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN); + ic.endBatchEdit(); + assertTrue(TextUtils.equals("word", r.mWord)); + + // split on chars outside the specified script + mockInputMethodService.setInputConnection( + new MockConnection(HIRAGANA_WORD + "wo", "rd" + GREEK_WORD, et)); + ic.beginBatchEdit(); + r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_LATIN); + ic.endBatchEdit(); + assertTrue(TextUtils.equals("word", r.mWord)); + + // likewise for greek + mockInputMethodService.setInputConnection( + new MockConnection("text" + GREEK_WORD, "text", et)); + ic.beginBatchEdit(); + r = ic.getWordRangeAtCursor(SUPPLEMENTARY_CHAR, ScriptUtils.SCRIPT_GREEK); + ic.endBatchEdit(); + assertTrue(TextUtils.equals(GREEK_WORD, r.mWord)); + } + + /** + * Test logic in getting the word range at the cursor. + */ + @Test + public void testGetSuggestionSpansAtWord() { + helpTestGetSuggestionSpansAtWord(10); + helpTestGetSuggestionSpansAtWord(12); + helpTestGetSuggestionSpansAtWord(15); + helpTestGetSuggestionSpansAtWord(16); + } + + private void helpTestGetSuggestionSpansAtWord(final int cursorPos) { + final SpacingAndPunctuations SPACE = new SpacingAndPunctuations( + mSpacingAndPunctuations, new int[] { Constants.CODE_SPACE }); + final MockInputMethodService mockInputMethodService = new MockInputMethodService(); + final RichInputConnection ic = new RichInputConnection(mockInputMethodService); + + final String[] SUGGESTIONS1 = { "swing", "strong" }; + final String[] SUGGESTIONS2 = { "storing", "strung" }; + + // Test the usual case. + SpannableString text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + TextRange r; + SuggestionSpan[] suggestions; + + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 1); + assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); + + // Test the case with 2 suggestion spans in the same place. + text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 2); + assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); + assertEquals(suggestions[1].getSuggestions(), SUGGESTIONS2); + + // Test a case with overlapping spans, 2nd extending past the start of the word + text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), + 5 /* start */, 16 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 1); + assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); + + // Test a case with overlapping spans, 2nd extending past the end of the word + text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), + 10 /* start */, 20 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 1); + assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); + + // Test a case with overlapping spans, 2nd extending past both ends of the word + text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 10 /* start */, 16 /* end */, 0 /* flags */); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), + 5 /* start */, 20 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 1); + assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1); + + // Test a case with overlapping spans, none right on the word + text = new SpannableString("This is a string for test"); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS1, 0 /* flags */), + 5 /* start */, 16 /* end */, 0 /* flags */); + text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */), + 5 /* start */, 20 /* end */, 0 /* flags */); + mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos)); + r = ic.getWordRangeAtCursor(SPACE, ScriptUtils.SCRIPT_LATIN); + suggestions = r.getSuggestionSpansAtWord(); + assertEquals(suggestions.length, 0); + } + + @Test + public void testCursorTouchingWord() { + final MockInputMethodService ims = new MockInputMethodService(); + final RichInputConnection ic = new RichInputConnection(ims); + final SpacingAndPunctuations sap = mSpacingAndPunctuations; + + ims.setInputConnection(new MockConnection("users", 5)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true /* checkTextAfter */)); + + ims.setInputConnection(new MockConnection("users'", 5)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("users'", 6)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("'users'", 6)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("'users'", 7)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("users '", 6)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("users '", 7)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("re-", 3)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("re--", 4)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("-", 1)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection("--", 2)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" -", 2)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" --", 3)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" users '", 1)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" users '", 3)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" users '", 7)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" users are", 7)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertTrue(ic.isCursorTouchingWord(sap, true)); + + ims.setInputConnection(new MockConnection(" users 'are", 7)); + ic.resetCachesUponCursorMoveAndReturnSuccess(ims.cursorPos(), ims.cursorPos(), true); + assertFalse(ic.isCursorTouchingWord(sap, true)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/RichInputMethodSubtypeTests.java b/tests/src/org/kelar/inputmethod/latin/RichInputMethodSubtypeTests.java new file mode 100644 index 000000000..f28ba53cd --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/RichInputMethodSubtypeTests.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.content.res.Resources; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.utils.AdditionalSubtypeUtils; +import org.kelar.inputmethod.latin.utils.RunInLocale; +import org.kelar.inputmethod.latin.utils.SubtypeLocaleUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RichInputMethodSubtypeTests { + // All input method subtypes of LatinIME. + private final ArrayList<RichInputMethodSubtype> mSubtypesList = new ArrayList<>(); + + private RichInputMethodManager mRichImm; + private Resources mRes; + private InputMethodSubtype mSavedAddtionalSubtypes[]; + + RichInputMethodSubtype EN_US; + RichInputMethodSubtype EN_GB; + RichInputMethodSubtype ES_US; + RichInputMethodSubtype FR; + RichInputMethodSubtype FR_CA; + RichInputMethodSubtype FR_CH; + RichInputMethodSubtype DE; + RichInputMethodSubtype DE_CH; + RichInputMethodSubtype HI; + RichInputMethodSubtype SR; + RichInputMethodSubtype ZZ; + RichInputMethodSubtype DE_QWERTY; + RichInputMethodSubtype FR_QWERTZ; + RichInputMethodSubtype EN_US_AZERTY; + RichInputMethodSubtype EN_UK_DVORAK; + RichInputMethodSubtype ES_US_COLEMAK; + RichInputMethodSubtype ZZ_AZERTY; + RichInputMethodSubtype ZZ_PC; + + // These are preliminary subtypes and may not exist. + RichInputMethodSubtype HI_LATN; // Hinglish + RichInputMethodSubtype SR_LATN; // Serbian Latin + RichInputMethodSubtype HI_LATN_DVORAK; + RichInputMethodSubtype SR_LATN_QWERTY; + + @Before + public void setUp() throws Exception { + final Context context = InstrumentationRegistry.getTargetContext(); + mRes = context.getResources(); + RichInputMethodManager.init(context); + mRichImm = RichInputMethodManager.getInstance(); + + // Save and reset additional subtypes + mSavedAddtionalSubtypes = mRichImm.getAdditionalSubtypes(); + final InputMethodSubtype[] predefinedAddtionalSubtypes = + AdditionalSubtypeUtils.createAdditionalSubtypesArray( + AdditionalSubtypeUtils.createPrefSubtypes( + mRes.getStringArray(R.array.predefined_subtypes))); + mRichImm.setAdditionalInputMethodSubtypes(predefinedAddtionalSubtypes); + + final InputMethodInfo imi = mRichImm.getInputMethodInfoOfThisIme(); + final int subtypeCount = imi.getSubtypeCount(); + for (int index = 0; index < subtypeCount; index++) { + final InputMethodSubtype subtype = imi.getSubtypeAt(index); + mSubtypesList.add(new RichInputMethodSubtype(subtype)); + } + + EN_US = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.US.toString(), "qwerty")); + EN_GB = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.UK.toString(), "qwerty")); + ES_US = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "es_US", "spanish")); + FR = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.FRENCH.toString(), "azerty")); + FR_CA = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.CANADA_FRENCH.toString(), "qwerty")); + FR_CH = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "fr_CH", "swiss")); + DE = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.GERMAN.toString(), "qwertz")); + DE_CH = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "de_CH", "swiss")); + HI = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "hi", "hindi")); + SR = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "sr", "south_slavic")); + ZZ = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, "qwerty")); + DE_QWERTY = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.GERMAN.toString(), "qwerty")); + FR_QWERTZ = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.FRENCH.toString(), "qwertz")); + EN_US_AZERTY = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.US.toString(), "azerty")); + EN_UK_DVORAK = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.UK.toString(), "dvorak")); + ES_US_COLEMAK = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "es_US", "colemak")); + ZZ_AZERTY = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, "azerty")); + ZZ_PC = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty")); + + final InputMethodSubtype hiLatn = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "hi_ZZ", "qwerty"); + if (hiLatn != null) { + HI_LATN = new RichInputMethodSubtype(hiLatn); + HI_LATN_DVORAK = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "hi_ZZ", "dvorak")); + } + final InputMethodSubtype srLatn = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "sr_ZZ", "serbian_qwertz"); + if (srLatn != null) { + SR_LATN = new RichInputMethodSubtype(srLatn); + SR_LATN_QWERTY = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "sr_ZZ", "qwerty")); + } + } + + @After + public void tearDown() throws Exception { + // Restore additional subtypes. + mRichImm.setAdditionalInputMethodSubtypes(mSavedAddtionalSubtypes); + } + + @Test + public void testAllFullDisplayNameForSpacebar() { + for (final RichInputMethodSubtype subtype : mSubtypesList) { + final String subtypeName = SubtypeLocaleUtils + .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype()); + final String spacebarText = subtype.getFullDisplayName(); + final String languageName = SubtypeLocaleUtils + .getSubtypeLocaleDisplayName(subtype.getLocale().toString()); + if (subtype.isNoLanguage()) { + assertFalse(subtypeName, spacebarText.contains(languageName)); + } else { + assertTrue(subtypeName, spacebarText.contains(languageName)); + } + } + } + + @Test + public void testAllMiddleDisplayNameForSpacebar() { + for (final RichInputMethodSubtype subtype : mSubtypesList) { + final String subtypeName = SubtypeLocaleUtils + .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype()); + final Locale locale = subtype.getLocale(); + final Locale displayLocale = SubtypeLocaleUtils.getDisplayLocaleOfSubtypeLocale( + locale.toString()); + if (Locale.ROOT.equals(displayLocale)) { + // Skip test because the language part of this locale string doesn't represent + // the locale to be displayed on the spacebar (for example Hinglish). + continue; + } + final String spacebarText = subtype.getMiddleDisplayName(); + if (subtype.isNoLanguage()) { + assertEquals(subtypeName, SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName( + subtype.getRawSubtype()), spacebarText); + } else { + assertEquals(subtypeName, + SubtypeLocaleUtils.getSubtypeLanguageDisplayName(locale.toString()), + spacebarText); + } + } + } + + // InputMethodSubtype's display name for spacebar text in its locale. + // isAdditionalSubtype (T=true, F=false) + // locale layout | Middle Full + // ------ -------------- - --------- ---------------------- + // en_US qwerty F English English (US) exception + // en_GB qwerty F English English (UK) exception + // es_US spanish F Español Español (EE.UU.) exception + // fr azerty F Français Français + // fr_CA qwerty F Français Français (Canada) + // fr_CH swiss F Français Français (Suisse) + // de qwertz F Deutsch Deutsch + // de_CH swiss F Deutsch Deutsch (Schweiz) + // hi hindi F हिन्दी हिन्दी + // hi_ZZ qwerty F Hinglish Hinglish exception + // sr south_slavic F Српски Српски + // sr_ZZ serbian_qwertz F Srpski Srpski exception + // zz qwerty F QWERTY QWERTY + // fr qwertz T Français Français + // de qwerty T Deutsch Deutsch + // en_US azerty T English English (US) + // en_GB dvorak T English English (UK) + // hi_ZZ dvorak T Hinglish Hinglish exception + // sr_ZZ qwerty T Srpski Srpski exception + // zz azerty T AZERTY AZERTY + + private final RunInLocale<Void> testsPredefinedSubtypesForSpacebar = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("en_US", "English (US)", EN_US.getFullDisplayName()); + assertEquals("en_GB", "English (UK)", EN_GB.getFullDisplayName()); + assertEquals("es_US", "Español (EE.UU.)", ES_US.getFullDisplayName()); + assertEquals("fr", "Français", FR.getFullDisplayName()); + assertEquals("fr_CA", "Français (Canada)", FR_CA.getFullDisplayName()); + assertEquals("fr_CH", "Français (Suisse)", FR_CH.getFullDisplayName()); + assertEquals("de", "Deutsch", DE.getFullDisplayName()); + assertEquals("de_CH", "Deutsch (Schweiz)", DE_CH.getFullDisplayName()); + assertEquals("hi", "हिन्दी", HI.getFullDisplayName()); + assertEquals("sr", "Српски", SR.getFullDisplayName()); + assertEquals("zz", "QWERTY", ZZ.getFullDisplayName()); + + assertEquals("en_US", "English", EN_US.getMiddleDisplayName()); + assertEquals("en_GB", "English", EN_GB.getMiddleDisplayName()); + assertEquals("es_US", "Español", ES_US.getMiddleDisplayName()); + assertEquals("fr", "Français", FR.getMiddleDisplayName()); + assertEquals("fr_CA", "Français", FR_CA.getMiddleDisplayName()); + assertEquals("fr_CH", "Français", FR_CH.getMiddleDisplayName()); + assertEquals("de", "Deutsch", DE.getMiddleDisplayName()); + assertEquals("de_CH", "Deutsch", DE_CH.getMiddleDisplayName()); + assertEquals("zz", "QWERTY", ZZ.getMiddleDisplayName()); + + // These are preliminary subtypes and may not exist. + if (HI_LATN != null) { + assertEquals("hi_ZZ", "Hinglish", HI_LATN.getFullDisplayName()); + assertEquals("hi_ZZ", "Hinglish", HI_LATN.getMiddleDisplayName()); + } + if (SR_LATN != null) { + assertEquals("sr_ZZ", "Srpski", SR_LATN.getFullDisplayName()); + assertEquals("sr_ZZ", "Srpski", SR_LATN.getMiddleDisplayName()); + } + return null; + } + }; + + private final RunInLocale<Void> testsAdditionalSubtypesForSpacebar = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("fr qwertz", "Français", FR_QWERTZ.getFullDisplayName()); + assertEquals("de qwerty", "Deutsch", DE_QWERTY.getFullDisplayName()); + assertEquals("en_US azerty", "English (US)", EN_US_AZERTY.getFullDisplayName()); + assertEquals("en_UK dvorak", "English (UK)", EN_UK_DVORAK.getFullDisplayName()); + assertEquals("es_US colemak", "Español (EE.UU.)", ES_US_COLEMAK.getFullDisplayName()); + assertEquals("zz azerty", "AZERTY", ZZ_AZERTY.getFullDisplayName()); + assertEquals("zz pc", "PC", ZZ_PC.getFullDisplayName()); + + assertEquals("fr qwertz", "Français", FR_QWERTZ.getMiddleDisplayName()); + assertEquals("de qwerty", "Deutsch", DE_QWERTY.getMiddleDisplayName()); + assertEquals("en_US azerty", "English", EN_US_AZERTY.getMiddleDisplayName()); + assertEquals("en_UK dvorak", "English", EN_UK_DVORAK.getMiddleDisplayName()); + assertEquals("es_US colemak", "Español", ES_US_COLEMAK.getMiddleDisplayName()); + assertEquals("zz azerty", "AZERTY", ZZ_AZERTY.getMiddleDisplayName()); + assertEquals("zz pc", "PC", ZZ_PC.getMiddleDisplayName()); + + // These are preliminary subtypes and may not exist. + if (HI_LATN_DVORAK != null) { + assertEquals("hi_ZZ dvorak", "Hinglish", HI_LATN_DVORAK.getFullDisplayName()); + assertEquals("hi_ZZ dvorak", "Hinglish", HI_LATN_DVORAK.getMiddleDisplayName()); + } + if (SR_LATN_QWERTY != null) { + assertEquals("sr_ZZ qwerty", "Srpski", SR_LATN_QWERTY.getFullDisplayName()); + assertEquals("sr_ZZ qwerty", "Srpski", SR_LATN_QWERTY.getMiddleDisplayName()); + } + return null; + } + }; + + @Test + public void testPredefinedSubtypesForSpacebarInEnglish() { + testsPredefinedSubtypesForSpacebar.runInLocale(mRes, Locale.ENGLISH); + } + + @Test + public void testAdditionalSubtypeForSpacebarInEnglish() { + testsAdditionalSubtypesForSpacebar.runInLocale(mRes, Locale.ENGLISH); + } + + @Test + public void testPredefinedSubtypesForSpacebarInFrench() { + testsPredefinedSubtypesForSpacebar.runInLocale(mRes, Locale.FRENCH); + } + + @Test + public void testAdditionalSubtypeForSpacebarInFrench() { + testsAdditionalSubtypesForSpacebar.runInLocale(mRes, Locale.FRENCH); + } + + @Test + public void testRichInputMethodSubtypeForNullInputMethodSubtype() { + RichInputMethodSubtype subtype = RichInputMethodSubtype.getRichInputMethodSubtype(null); + assertNotNull(subtype); + assertEquals("zz", subtype.getRawSubtype().getLocale()); + assertEquals("keyboard", subtype.getRawSubtype().getMode()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/ShiftModeTests.java b/tests/src/org/kelar/inputmethod/latin/ShiftModeTests.java new file mode 100644 index 000000000..a036e5d7d --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/ShiftModeTests.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin; + +import android.text.TextUtils; +import android.view.inputmethod.EditorInfo; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.common.Constants; + +@LargeTest +public class ShiftModeTests extends InputTestsBase { + + @Override + protected EditorInfo enrichEditorInfo(final EditorInfo ei) { + ei.inputType |= TextUtils.CAP_MODE_SENTENCES; + ei.initialCapsMode = TextUtils.CAP_MODE_SENTENCES; + return ei; + } + + private boolean isCapsModeAutoShifted() { + return mLatinIME.mKeyboardSwitcher.getKeyboardShiftMode() + == WordComposer.CAPS_MODE_AUTO_SHIFTED; + } + + public void testTypicalSentence() { + assertTrue("Initial auto caps state", isCapsModeAutoShifted()); + type("Test"); + assertFalse("Caps after letter", isCapsModeAutoShifted()); + type(" "); + assertFalse("Caps after space", isCapsModeAutoShifted()); + type("some,"); + assertFalse("Caps after comma", isCapsModeAutoShifted()); + type(" "); + assertFalse("Caps after comma space", isCapsModeAutoShifted()); + type("words."); + assertFalse("Caps directly after period", isCapsModeAutoShifted()); + type(" "); + assertTrue("Caps after period space", isCapsModeAutoShifted()); + } + + public void testBackspace() { + assertTrue("Initial auto caps state", isCapsModeAutoShifted()); + type("A"); + assertFalse("Caps state after one letter", isCapsModeAutoShifted()); + type(Constants.CODE_DELETE); + assertTrue("Auto caps state at start after delete", isCapsModeAutoShifted()); + } + + public void testRepeatingBackspace() { + final String SENTENCE_TO_TYPE = "Test sentence. Another."; + final int BACKSPACE_COUNT = + SENTENCE_TO_TYPE.length() - SENTENCE_TO_TYPE.lastIndexOf(' ') - 1; + + type(SENTENCE_TO_TYPE); + assertFalse("Caps after typing \"" + SENTENCE_TO_TYPE + "\"", isCapsModeAutoShifted()); + type(Constants.CODE_DELETE); + for (int i = 1; i < BACKSPACE_COUNT; ++i) { + repeatKey(Constants.CODE_DELETE); + } + assertFalse("Caps immediately after repeating Backspace a lot", isCapsModeAutoShifted()); + sleep(DELAY_TO_WAIT_FOR_PREDICTIONS_MILLIS); + runMessages(); + assertTrue("Caps after a while after repeating Backspace a lot", isCapsModeAutoShifted()); + } + + public void testAutoCapsAfterDigitsPeriod() { + changeLanguage("en"); + type("On 22.11."); + assertFalse("(English) Auto caps after digits-period", isCapsModeAutoShifted()); + type(" "); + assertTrue("(English) Auto caps after digits-period-whitespace", isCapsModeAutoShifted()); + mEditText.setText(""); + changeLanguage("fr"); + type("Le 22."); + assertFalse("(French) Auto caps after digits-period", isCapsModeAutoShifted()); + type(" "); + assertTrue("(French) Auto caps after digits-period-whitespace", isCapsModeAutoShifted()); + mEditText.setText(""); + changeLanguage("de"); + type("Am 22."); + assertFalse("(German) Auto caps after digits-period", isCapsModeAutoShifted()); + type(" "); + // For German, no auto-caps in this case + assertFalse("(German) Auto caps after digits-period-whitespace", isCapsModeAutoShifted()); + } + + public void testAutoCapsAfterInvertedMarks() { + changeLanguage("es"); + assertTrue("(Spanish) Auto caps at start", isCapsModeAutoShifted()); + type("Hey. ¿"); + assertTrue("(Spanish) Auto caps after inverted what", isCapsModeAutoShifted()); + mEditText.setText(""); + type("¡"); + assertTrue("(Spanish) Auto caps after inverted bang", isCapsModeAutoShifted()); + } + + public void testOtherSentenceSeparators() { + changeLanguage("hy_AM"); + assertTrue("(Armenian) Auto caps at start", isCapsModeAutoShifted()); + type("Hey. "); + assertFalse("(Armenian) No auto-caps after latin period", isCapsModeAutoShifted()); + type("Hey\u0589"); + assertFalse("(Armenian) No auto-caps directly after armenian period", + isCapsModeAutoShifted()); + type(" "); + assertTrue("(Armenian) Auto-caps after armenian period-whitespace", + isCapsModeAutoShifted()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/SuggestedWordsTests.java b/tests/src/org/kelar/inputmethod/latin/SuggestedWordsTests.java new file mode 100644 index 000000000..1d3df9335 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/SuggestedWordsTests.java @@ -0,0 +1,186 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SuggestedWordsTests { + + /** + * Helper method to create a placeholder {@link SuggestedWordInfo} with specifying + * {@link SuggestedWordInfo#KIND_TYPED}. + * + * @param word the word to be used to create {@link SuggestedWordInfo}. + * @return a new instance of {@link SuggestedWordInfo}. + */ + private static SuggestedWordInfo createTypedWordInfo(final String word) { + // Use 100 as the frequency because the numerical value does not matter as + // long as it's > 1 and < INT_MAX. + return new SuggestedWordInfo(word, "" /* prevWordsContext */, 100 /* score */, + SuggestedWordInfo.KIND_TYPED, + null /* sourceDict */, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + 1 /* autoCommitFirstWordConfidence */); + } + + /** + * Helper method to create a placeholder {@link SuggestedWordInfo} with specifying + * {@link SuggestedWordInfo#KIND_CORRECTION}. + * + * @param word the word to be used to create {@link SuggestedWordInfo}. + * @return a new instance of {@link SuggestedWordInfo}. + */ + private static SuggestedWordInfo createCorrectionWordInfo(final String word) { + return new SuggestedWordInfo(word, "" /* prevWordsContext */, 1 /* score */, + SuggestedWordInfo.KIND_CORRECTION, + null /* sourceDict */, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + } + + private static ArrayList<SuggestedWordInfo> createCorrectionWordInfos(final String... words) { + final ArrayList<SuggestedWordInfo> infos = new ArrayList<>(); + for (final String word : words) { + infos.add(createCorrectionWordInfo(word)); + } + return infos; + } + + // Helper for testGetTransformedWordInfo + private static SuggestedWordInfo transformWordInfo(final String info, + final int trailingSingleQuotesCount) { + final SuggestedWordInfo suggestedWordInfo = createTypedWordInfo(info); + final SuggestedWordInfo returnedWordInfo = + Suggest.getTransformedSuggestedWordInfo(suggestedWordInfo, + Locale.ENGLISH, false /* isAllUpperCase */, false /* isFirstCharCapitalized */, + trailingSingleQuotesCount); + assertEquals(suggestedWordInfo.mAutoCommitFirstWordConfidence, + returnedWordInfo.mAutoCommitFirstWordConfidence); + return returnedWordInfo; + } + + @Test + public void testRemoveDupesNoDupes() { + final ArrayList<SuggestedWordInfo> infos = createCorrectionWordInfos("a", "c"); + assertEquals(-1, SuggestedWordInfo.removeDups("b", infos)); + assertEquals(2, infos.size()); + } + + @Test + public void testRemoveDupesTypedWordNotDupe() { + final ArrayList<SuggestedWordInfo> infos = createCorrectionWordInfos("a", "a", "c"); + assertEquals(-1, SuggestedWordInfo.removeDups("b", infos)); + assertEquals(2, infos.size()); + } + + @Test + public void testRemoveDupesTypedWordOnlyDupe() { + final ArrayList<SuggestedWordInfo> infos = createCorrectionWordInfos("a", "b", "c"); + assertEquals(1, SuggestedWordInfo.removeDups("b", infos)); + assertEquals(2, infos.size()); + } + + @Test + public void testRemoveDupesTypedWordNotOnlyDupe() { + final ArrayList<SuggestedWordInfo> infos = createCorrectionWordInfos("a", "b", "b", "c"); + assertEquals(1, SuggestedWordInfo.removeDups("b", infos)); + assertEquals(2, infos.size()); + } + + @Test + public void testGetTransformedSuggestedWordInfo() { + SuggestedWordInfo result = transformWordInfo("word", 0); + assertEquals(result.mWord, "word"); + result = transformWordInfo("word", 1); + assertEquals(result.mWord, "word'"); + result = transformWordInfo("word", 3); + assertEquals(result.mWord, "word'''"); + result = transformWordInfo("didn't", 0); + assertEquals(result.mWord, "didn't"); + result = transformWordInfo("didn't", 1); + assertEquals(result.mWord, "didn't"); + result = transformWordInfo("didn't", 3); + assertEquals(result.mWord, "didn't''"); + } + + @Test + public void testGetTypedWordInfoOrNull() { + final String TYPED_WORD = "typed"; + final SuggestedWordInfo TYPED_WORD_INFO = createTypedWordInfo(TYPED_WORD); + final int NUMBER_OF_ADDED_SUGGESTIONS = 5; + final ArrayList<SuggestedWordInfo> list = new ArrayList<>(); + list.add(TYPED_WORD_INFO); + for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) { + list.add(createCorrectionWordInfo(Integer.toString(i))); + } + + // Make sure getTypedWordInfoOrNull() returns non-null object. + final SuggestedWords wordsWithTypedWord = new SuggestedWords( + list, null /* rawSuggestions */, + TYPED_WORD_INFO, + false /* typedWordValid */, + false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_NONE, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); + final SuggestedWordInfo typedWord = wordsWithTypedWord.getTypedWordInfoOrNull(); + assertNotNull(typedWord); + assertEquals(TYPED_WORD, typedWord.mWord); + + // Make sure getTypedWordInfoOrNull() returns null when no typed word. + list.remove(0); + final SuggestedWords wordsWithoutTypedWord = new SuggestedWords( + list, null /* rawSuggestions */, + null /* typedWord */, + false /* typedWordValid */, + false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_NONE, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); + assertNull(wordsWithoutTypedWord.getTypedWordInfoOrNull()); + + // Make sure getTypedWordInfoOrNull() returns null. + assertNull(SuggestedWords.getEmptyInstance().getTypedWordInfoOrNull()); + + final SuggestedWords emptySuggestedWords = new SuggestedWords( + new ArrayList<SuggestedWordInfo>(), null /* rawSuggestions */, + null /* typedWord */, + false /* typedWordValid */, + false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, + SuggestedWords.INPUT_STYLE_NONE, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); + assertNull(emptySuggestedWords.getTypedWordInfoOrNull()); + + assertNull(SuggestedWords.getEmptyInstance().getTypedWordInfoOrNull()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/WordComposerTests.java b/tests/src/org/kelar/inputmethod/latin/WordComposerTests.java new file mode 100644 index 000000000..f13458526 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/WordComposerTests.java @@ -0,0 +1,133 @@ +/* + * 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 org.kelar.inputmethod.latin; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.CoordinateUtils; +import org.kelar.inputmethod.latin.common.StringUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for WordComposer. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class WordComposerTests { + + @Test + public void testMoveCursor() { + final WordComposer wc = new WordComposer(); + // BMP is the Basic Multilingual Plane, as defined by Unicode. This includes + // most characters for most scripts, including all Roman alphabet languages, + // CJK, Arabic, Hebrew. Notable exceptions include some emoji and some + // very rare Chinese ideograms. BMP characters can be encoded on 2 bytes + // in UTF-16, whereas those outside the BMP need 4 bytes. + // http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane + final String STR_WITHIN_BMP = "abcdef"; + final int[] CODEPOINTS_WITHIN_BMP = StringUtils.toCodePointArray(STR_WITHIN_BMP); + final int[] COORDINATES_WITHIN_BMP = + CoordinateUtils.newCoordinateArray(CODEPOINTS_WITHIN_BMP.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + wc.setComposingWord(CODEPOINTS_WITHIN_BMP, COORDINATES_WITHIN_BMP); + assertEquals(wc.size(), STR_WITHIN_BMP.codePointCount(0, STR_WITHIN_BMP.length())); + assertFalse(wc.isCursorFrontOrMiddleOfComposingWord()); + wc.setCursorPositionWithinWord(2); + assertTrue(wc.isCursorFrontOrMiddleOfComposingWord()); + // Move the cursor to after the 'd' + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(2)); + assertTrue(wc.isCursorFrontOrMiddleOfComposingWord()); + // Move the cursor to after the 'e' + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1)); + assertTrue(wc.isCursorFrontOrMiddleOfComposingWord()); + assertEquals(wc.size(), 6); + // Move the cursor to after the 'f' + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1)); + assertFalse(wc.isCursorFrontOrMiddleOfComposingWord()); + // Move the cursor past the end of the word + assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(1)); + assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(15)); + // Do what LatinIME does when the cursor is moved outside of the word, + // and check the behavior is correct. + wc.reset(); + + // \uD861\uDED7 is 𨛗, a character outside the BMP + final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh"; + final int[] CODEPOINTS_WITH_SUPPLEMENTARY_CHAR = + StringUtils.toCodePointArray(STR_WITH_SUPPLEMENTARY_CHAR); + final int[] COORDINATES_WITH_SUPPLEMENTARY_CHAR = + CoordinateUtils.newCoordinateArray(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + assertEquals(wc.size(), CODEPOINTS_WITH_SUPPLEMENTARY_CHAR.length); + assertFalse(wc.isCursorFrontOrMiddleOfComposingWord()); + wc.setCursorPositionWithinWord(3); + assertTrue(wc.isCursorFrontOrMiddleOfComposingWord()); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(6)); + assertTrue(wc.isCursorFrontOrMiddleOfComposingWord()); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1)); + assertFalse(wc.isCursorFrontOrMiddleOfComposingWord()); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + wc.setCursorPositionWithinWord(3); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + wc.setCursorPositionWithinWord(3); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + wc.setCursorPositionWithinWord(3); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-3)); + assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-1)); + + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + wc.setCursorPositionWithinWord(3); + assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-9)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-10)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-11)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0)); + + wc.setComposingWord(CODEPOINTS_WITH_SUPPLEMENTARY_CHAR, + COORDINATES_WITH_SUPPLEMENTARY_CHAR); + wc.setCursorPositionWithinWord(2); + assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/accounts/AccountsChangedReceiverTests.java b/tests/src/org/kelar/inputmethod/latin/accounts/AccountsChangedReceiverTests.java new file mode 100644 index 000000000..96e522182 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/accounts/AccountsChangedReceiverTests.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.accounts; + +import static org.junit.Assert.assertEquals; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.settings.LocalSettingsConstants; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link AccountsChangedReceiver}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AccountsChangedReceiverTests { + private static final String ACCOUNT_1 = "account1@example.com"; + private static final String ACCOUNT_2 = "account2@example.com"; + + private SharedPreferences mPrefs; + private String mLastKnownAccount = null; + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + @Before + public void setUp() throws Exception { + mPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + // Keep track of the current account so that we restore it when the test finishes. + mLastKnownAccount = mPrefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null); + } + + @After + public void tearDown() throws Exception { + // Restore the account that was present before running the test. + updateAccountName(mLastKnownAccount); + } + + @Test + public void testUnknownIntent() { + updateAccountName(ACCOUNT_1); + AccountsChangedReceiver reciever = new AccountsChangedReceiver(); + reciever.onReceive(getContext(), new Intent("some-random-action")); + // Account should *not* be removed from preferences. + assertAccountName(ACCOUNT_1); + } + + @Test + public void testAccountRemoved() { + updateAccountName(ACCOUNT_1); + AccountsChangedReceiver reciever = new AccountsChangedReceiver() { + @Override + protected String[] getAccountsForLogin(Context context) { + return new String[] {ACCOUNT_2}; + } + }; + reciever.onReceive(getContext(), new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION)); + // Account should be removed from preferences. + assertAccountName(null); + } + + @Test + public void testAccountRemoved_noAccounts() { + updateAccountName(ACCOUNT_2); + AccountsChangedReceiver reciever = new AccountsChangedReceiver() { + @Override + protected String[] getAccountsForLogin(Context context) { + return new String[0]; + } + }; + reciever.onReceive(getContext(), new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION)); + // Account should be removed from preferences. + assertAccountName(null); + } + + @Test + public void testAccountNotRemoved() { + updateAccountName(ACCOUNT_2); + AccountsChangedReceiver reciever = new AccountsChangedReceiver() { + @Override + protected String[] getAccountsForLogin(Context context) { + return new String[] {ACCOUNT_1, ACCOUNT_2}; + } + }; + reciever.onReceive(getContext(), new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION)); + // Account should *not* be removed from preferences. + assertAccountName(ACCOUNT_2); + } + + private void updateAccountName(String accountName) { + if (accountName == null) { + mPrefs.edit().remove(LocalSettingsConstants.PREF_ACCOUNT_NAME).apply(); + } else { + mPrefs.edit().putString(LocalSettingsConstants.PREF_ACCOUNT_NAME, accountName).apply(); + } + } + + private void assertAccountName(String expectedAccountName) { + assertEquals(expectedAccountName, + mPrefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/common/InputPointersTests.java b/tests/src/org/kelar/inputmethod/latin/common/InputPointersTests.java new file mode 100644 index 000000000..e0b98dd13 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/common/InputPointersTests.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class InputPointersTests { + private static final int DEFAULT_CAPACITY = 48; + + @Test + public void testNewInstance() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + assertEquals("new instance size", 0, src.getPointerSize()); + assertNotNull("new instance xCoordinates", src.getXCoordinates()); + assertNotNull("new instance yCoordinates", src.getYCoordinates()); + assertNotNull("new instance pointerIds", src.getPointerIds()); + assertNotNull("new instance times", src.getTimes()); + } + + @Test + public void testReset() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int[] xCoordinates = src.getXCoordinates(); + final int[] yCoordinates = src.getXCoordinates(); + final int[] pointerIds = src.getXCoordinates(); + final int[] times = src.getXCoordinates(); + + src.reset(); + assertEquals("size after reset", 0, src.getPointerSize()); + assertNotSame("xCoordinates after reset", xCoordinates, src.getXCoordinates()); + assertNotSame("yCoordinates after reset", yCoordinates, src.getYCoordinates()); + assertNotSame("pointerIds after reset", pointerIds, src.getPointerIds()); + assertNotSame("times after reset", times, src.getTimes()); + } + + @Test + public void testAdd() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int limit = src.getXCoordinates().length * 2 + 10; + for (int i = 0; i < limit; i++) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + src.addPointer(x, y, pointerId, time); + assertEquals("size after add " + i, i + 1, src.getPointerSize()); + } + for (int i = 0; i < limit; i++) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + assertEquals("xCoordinates at " + i, x, src.getXCoordinates()[i]); + assertEquals("yCoordinates at " + i, y, src.getYCoordinates()[i]); + assertEquals("pointerIds at " + i, pointerId, src.getPointerIds()[i]); + assertEquals("times at " + i, time, src.getTimes()[i]); + } + } + + @Test + public void testAddAt() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int limit = 1000, step = 100; + for (int i = 0; i < limit; i += step) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + src.addPointerAt(i, x, y, pointerId, time); + assertEquals("size after add at " + i, i + 1, src.getPointerSize()); + } + for (int i = 0; i < limit; i += step) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + assertEquals("xCoordinates at " + i, x, src.getXCoordinates()[i]); + assertEquals("yCoordinates at " + i, y, src.getYCoordinates()[i]); + assertEquals("pointerIds at " + i, pointerId, src.getPointerIds()[i]); + assertEquals("times at " + i, time, src.getTimes()[i]); + } + } + + @Test + public void testSet() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int limit = src.getXCoordinates().length * 2 + 10; + for (int i = 0; i < limit; i++) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + src.addPointer(x, y, pointerId, time); + } + final InputPointers dst = new InputPointers(DEFAULT_CAPACITY); + dst.set(src); + assertEquals("size after set", dst.getPointerSize(), src.getPointerSize()); + assertSame("xCoordinates after set", dst.getXCoordinates(), src.getXCoordinates()); + assertSame("yCoordinates after set", dst.getYCoordinates(), src.getYCoordinates()); + assertSame("pointerIds after set", dst.getPointerIds(), src.getPointerIds()); + assertSame("times after set", dst.getTimes(), src.getTimes()); + } + + @Test + public void testCopy() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int limit = 100; + for (int i = 0; i < limit; i++) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + src.addPointer(x, y, pointerId, time); + } + final InputPointers dst = new InputPointers(DEFAULT_CAPACITY); + dst.copy(src); + assertEquals("size after copy", dst.getPointerSize(), src.getPointerSize()); + assertNotSame("xCoordinates after copy", dst.getXCoordinates(), src.getXCoordinates()); + assertNotSame("yCoordinates after copy", dst.getYCoordinates(), src.getYCoordinates()); + assertNotSame("pointerIds after copy", dst.getPointerIds(), src.getPointerIds()); + assertNotSame("times after copy", dst.getTimes(), src.getTimes()); + final int size = dst.getPointerSize(); + assertIntArrayEquals("xCoordinates values after copy", + dst.getXCoordinates(), 0, src.getXCoordinates(), 0, size); + assertIntArrayEquals("yCoordinates values after copy", + dst.getYCoordinates(), 0, src.getYCoordinates(), 0, size); + assertIntArrayEquals("pointerIds values after copy", + dst.getPointerIds(), 0, src.getPointerIds(), 0, size); + assertIntArrayEquals("times values after copy", + dst.getTimes(), 0, src.getTimes(), 0, size); + } + + @Test + public void testAppend() { + final int dstLength = 50; + final InputPointers dst = new InputPointers(DEFAULT_CAPACITY); + for (int i = 0; i < dstLength; i++) { + final int x = i * 4; + final int y = i * 3; + final int pointerId = i * 2; + final int time = i; + dst.addPointer(x, y, pointerId, time); + } + final InputPointers dstCopy = new InputPointers(DEFAULT_CAPACITY); + dstCopy.copy(dst); + + final ResizableIntArray srcXCoords = new ResizableIntArray(DEFAULT_CAPACITY); + final ResizableIntArray srcYCoords = new ResizableIntArray(DEFAULT_CAPACITY); + final ResizableIntArray srcPointerIds = new ResizableIntArray(DEFAULT_CAPACITY); + final ResizableIntArray srcTimes = new ResizableIntArray(DEFAULT_CAPACITY); + final int srcLength = 100; + final int srcPointerId = 10; + for (int i = 0; i < srcLength; i++) { + final int x = i; + final int y = i * 2; + // The time value must be larger than <code>dst</code>. + final int time = i * 4 + dstLength; + srcXCoords.add(x); + srcYCoords.add(y); + srcPointerIds.add(srcPointerId); + srcTimes.add(time); + } + + final int startPos = 0; + dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, + startPos, 0 /* length */); + assertEquals("size after append zero", dstLength, dst.getPointerSize()); + assertIntArrayEquals("xCoordinates after append zero", + dstCopy.getXCoordinates(), startPos, dst.getXCoordinates(), startPos, dstLength); + assertIntArrayEquals("yCoordinates after append zero", + dstCopy.getYCoordinates(), startPos, dst.getYCoordinates(), startPos, dstLength); + assertIntArrayEquals("pointerIds after append zero", + dstCopy.getPointerIds(), startPos, dst.getPointerIds(), startPos, dstLength); + assertIntArrayEquals("times after append zero", + dstCopy.getTimes(), startPos, dst.getTimes(), startPos, dstLength); + + dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, + startPos, srcLength); + assertEquals("size after append", dstLength + srcLength, dst.getPointerSize()); + assertTrue("primitive length after append", + dst.getPointerIds().length >= dstLength + srcLength); + assertIntArrayEquals("original xCoordinates values after append", + dstCopy.getXCoordinates(), startPos, dst.getXCoordinates(), startPos, dstLength); + assertIntArrayEquals("original yCoordinates values after append", + dstCopy.getYCoordinates(), startPos, dst.getYCoordinates(), startPos, dstLength); + assertIntArrayEquals("original pointerIds values after append", + dstCopy.getPointerIds(), startPos, dst.getPointerIds(), startPos, dstLength); + assertIntArrayEquals("original times values after append", + dstCopy.getTimes(), startPos, dst.getTimes(), startPos, dstLength); + assertIntArrayEquals("appended xCoordinates values after append", + srcXCoords.getPrimitiveArray(), startPos, dst.getXCoordinates(), + dstLength, srcLength); + assertIntArrayEquals("appended yCoordinates values after append", + srcYCoords.getPrimitiveArray(), startPos, dst.getYCoordinates(), + dstLength, srcLength); + assertIntArrayEquals("appended pointerIds values after append", + srcPointerIds.getPrimitiveArray(), startPos, dst.getPointerIds(), + dstLength, srcLength); + assertIntArrayEquals("appended times values after append", + srcTimes.getPrimitiveArray(), startPos, dst.getTimes(), dstLength, srcLength); + } + + @Test + public void testAppendResizableIntArray() { + final int dstLength = 50; + final InputPointers dst = new InputPointers(DEFAULT_CAPACITY); + for (int i = 0; i < dstLength; i++) { + final int x = i * 4; + final int y = i * 3; + final int pointerId = i * 2; + final int time = i; + dst.addPointer(x, y, pointerId, time); + } + final InputPointers dstCopy = new InputPointers(DEFAULT_CAPACITY); + dstCopy.copy(dst); + + final int srcLength = 100; + final int srcPointerId = 1; + final int[] srcPointerIds = new int[srcLength]; + Arrays.fill(srcPointerIds, srcPointerId); + final ResizableIntArray srcTimes = new ResizableIntArray(DEFAULT_CAPACITY); + final ResizableIntArray srcXCoords = new ResizableIntArray(DEFAULT_CAPACITY); + final ResizableIntArray srcYCoords= new ResizableIntArray(DEFAULT_CAPACITY); + for (int i = 0; i < srcLength; i++) { + // The time value must be larger than <code>dst</code>. + final int time = i * 2 + dstLength; + final int x = i * 3; + final int y = i * 4; + srcTimes.add(time); + srcXCoords.add(x); + srcYCoords.add(y); + } + + dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, 0); + assertEquals("size after append zero", dstLength, dst.getPointerSize()); + assertIntArrayEquals("xCoordinates after append zero", + dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLength); + assertIntArrayEquals("yCoordinates after append zero", + dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLength); + assertIntArrayEquals("pointerIds after append zero", + dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLength); + assertIntArrayEquals("times after append zero", + dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLength); + + dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, srcLength); + assertEquals("size after append", dstLength + srcLength, dst.getPointerSize()); + assertTrue("primitive length after append", + dst.getPointerIds().length >= dstLength + srcLength); + assertIntArrayEquals("original xCoordinates values after append", + dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLength); + assertIntArrayEquals("original yCoordinates values after append", + dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLength); + assertIntArrayEquals("original pointerIds values after append", + dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLength); + assertIntArrayEquals("original times values after append", + dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLength); + assertIntArrayEquals("appended xCoordinates values after append", + srcXCoords.getPrimitiveArray(), 0, dst.getXCoordinates(), dstLength, srcLength); + assertIntArrayEquals("appended yCoordinates values after append", + srcYCoords.getPrimitiveArray(), 0, dst.getYCoordinates(), dstLength, srcLength); + assertIntArrayEquals("appended pointerIds values after append", + srcPointerIds, 0, dst.getPointerIds(), dstLength, srcLength); + assertIntArrayEquals("appended times values after append", + srcTimes.getPrimitiveArray(), 0, dst.getTimes(), dstLength, srcLength); + } + + // TODO: Consolidate this method with + // {@link ResizableIntArrayTests#assertIntArrayEquals(String,int[],int,int[],int,int)}. + private static void assertIntArrayEquals(final String message, final int[] expecteds, + final int expectedPos, final int[] actuals, final int actualPos, final int length) { + if (expecteds == actuals) { + return; + } + if (expecteds == null || actuals == null) { + assertEquals(message, Arrays.toString(expecteds), Arrays.toString(actuals)); + return; + } + if (expecteds.length < expectedPos + length || actuals.length < actualPos + length) { + fail(message + ": insufficient length: expecteds=" + Arrays.toString(expecteds) + + " actuals=" + Arrays.toString(actuals)); + return; + } + for (int i = 0; i < length; i++) { + assertEquals(message + " [" + i + "]", + expecteds[i + expectedPos], actuals[i + actualPos]); + } + } + + @Test + public void testShift() { + final InputPointers src = new InputPointers(DEFAULT_CAPACITY); + final int limit = 100; + final int shiftAmount = 20; + for (int i = 0; i < limit; i++) { + final int x = i; + final int y = i * 2; + final int pointerId = i * 3; + final int time = i * 4; + src.addPointer(x, y, pointerId, time); + } + src.shift(shiftAmount); + assertEquals("length after shift", src.getPointerSize(), limit - shiftAmount); + for (int i = 0; i < limit - shiftAmount; ++i) { + final int oldIndex = i + shiftAmount; + final int x = oldIndex; + final int y = oldIndex * 2; + final int pointerId = oldIndex * 3; + final int time = oldIndex * 4; + assertEquals("xCoordinates at " + i, x, src.getXCoordinates()[i]); + assertEquals("yCoordinates at " + i, y, src.getYCoordinates()[i]); + assertEquals("pointerIds at " + i, pointerId, src.getPointerIds()[i]); + assertEquals("times at " + i, time, src.getTimes()[i]); + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/common/ResizableIntArrayTests.java b/tests/src/org/kelar/inputmethod/latin/common/ResizableIntArrayTests.java new file mode 100644 index 000000000..d2a7a651f --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/common/ResizableIntArrayTests.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ResizableIntArrayTests { + private static final int DEFAULT_CAPACITY = 48; + + @Test + public void testNewInstance() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int[] array = src.getPrimitiveArray(); + assertEquals("new instance length", 0, src.getLength()); + assertNotNull("new instance array", array); + assertEquals("new instance array length", DEFAULT_CAPACITY, array.length); + } + + @Test + public void testAdd() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int[] array = src.getPrimitiveArray(); + int[] array2 = null, array3 = null; + final int limit = DEFAULT_CAPACITY * 2 + 10; + for (int i = 0; i < limit; i++) { + final int value = i; + src.add(value); + assertEquals("length after add " + i, i + 1, src.getLength()); + if (i == DEFAULT_CAPACITY) { + array2 = src.getPrimitiveArray(); + } + if (i == DEFAULT_CAPACITY * 2) { + array3 = src.getPrimitiveArray(); + } + if (i < DEFAULT_CAPACITY) { + assertSame("array after add " + i, array, src.getPrimitiveArray()); + } else if (i < DEFAULT_CAPACITY * 2) { + assertSame("array after add " + i, array2, src.getPrimitiveArray()); + } else if (i < DEFAULT_CAPACITY * 3) { + assertSame("array after add " + i, array3, src.getPrimitiveArray()); + } + } + for (int i = 0; i < limit; i++) { + final int value = i; + assertEquals("value at " + i, value, src.get(i)); + } + } + + @Test + public void testAddAt() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int limit = DEFAULT_CAPACITY * 10, step = DEFAULT_CAPACITY * 2; + for (int i = 0; i < limit; i += step) { + final int value = i; + src.addAt(i, value); + assertEquals("length after add at " + i, i + 1, src.getLength()); + } + for (int i = 0; i < limit; i += step) { + final int value = i; + assertEquals("value at " + i, value, src.get(i)); + } + } + + @Test + public void testGet() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + try { + src.get(0); + fail("get(0) shouldn't succeed"); + } catch (ArrayIndexOutOfBoundsException e) { + // success + } + try { + src.get(DEFAULT_CAPACITY); + fail("get(DEFAULT_CAPACITY) shouldn't succeed"); + } catch (ArrayIndexOutOfBoundsException e) { + // success + } + + final int index = DEFAULT_CAPACITY / 2; + final int valueAddAt = 100; + src.addAt(index, valueAddAt); + assertEquals("legth after add at " + index, index + 1, src.getLength()); + assertEquals("value after add at " + index, valueAddAt, src.get(index)); + assertEquals("value after add at 0", 0, src.get(0)); + try { + src.get(src.getLength()); + fail("get(length) shouldn't succeed"); + } catch (ArrayIndexOutOfBoundsException e) { + // success + } + } + + @Test + public void testReset() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int[] array = src.getPrimitiveArray(); + for (int i = 0; i < DEFAULT_CAPACITY; i++) { + final int value = i; + src.add(value); + assertEquals("length after add " + i, i + 1, src.getLength()); + } + + final int smallerLength = DEFAULT_CAPACITY / 2; + src.reset(smallerLength); + final int[] array2 = src.getPrimitiveArray(); + assertEquals("length after reset", 0, src.getLength()); + assertNotSame("array after reset", array, array2); + + int[] array3 = null; + for (int i = 0; i < DEFAULT_CAPACITY; i++) { + final int value = i; + src.add(value); + assertEquals("length after add " + i, i + 1, src.getLength()); + if (i == smallerLength) { + array3 = src.getPrimitiveArray(); + } + if (i < smallerLength) { + assertSame("array after add " + i, array2, src.getPrimitiveArray()); + } else if (i < smallerLength * 2) { + assertSame("array after add " + i, array3, src.getPrimitiveArray()); + } + } + } + + @Test + public void testSetLength() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int[] array = src.getPrimitiveArray(); + for (int i = 0; i < DEFAULT_CAPACITY; i++) { + final int value = i; + src.add(value); + assertEquals("length after add " + i, i + 1, src.getLength()); + } + + final int largerLength = DEFAULT_CAPACITY * 2; + src.setLength(largerLength); + final int[] array2 = src.getPrimitiveArray(); + assertEquals("length after larger setLength", largerLength, src.getLength()); + assertNotSame("array after larger setLength", array, array2); + assertEquals("array length after larger setLength", largerLength, array2.length); + for (int i = 0; i < largerLength; i++) { + final int value = i; + if (i < DEFAULT_CAPACITY) { + assertEquals("value at " + i, value, src.get(i)); + } else { + assertEquals("value at " + i, 0, src.get(i)); + } + } + + final int smallerLength = DEFAULT_CAPACITY / 2; + src.setLength(smallerLength); + final int[] array3 = src.getPrimitiveArray(); + assertEquals("length after smaller setLength", smallerLength, src.getLength()); + assertSame("array after smaller setLength", array2, array3); + assertEquals("array length after smaller setLength", largerLength, array3.length); + for (int i = 0; i < smallerLength; i++) { + final int value = i; + assertEquals("value at " + i, value, src.get(i)); + } + } + + @Test + public void testSet() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int limit = DEFAULT_CAPACITY * 2 + 10; + for (int i = 0; i < limit; i++) { + final int value = i; + src.add(value); + } + + final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY); + dst.set(src); + assertEquals("length after set", dst.getLength(), src.getLength()); + assertSame("array after set", dst.getPrimitiveArray(), src.getPrimitiveArray()); + } + + @Test + public void testCopy() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + for (int i = 0; i < DEFAULT_CAPACITY; i++) { + final int value = i; + src.add(value); + } + + final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY); + final int[] array = dst.getPrimitiveArray(); + dst.copy(src); + assertEquals("length after copy", dst.getLength(), src.getLength()); + assertSame("array after copy", array, dst.getPrimitiveArray()); + assertNotSame("array after copy", dst.getPrimitiveArray(), src.getPrimitiveArray()); + assertIntArrayEquals("values after copy", + dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength()); + + final int smallerLength = DEFAULT_CAPACITY / 2; + dst.reset(smallerLength); + final int[] array2 = dst.getPrimitiveArray(); + dst.copy(src); + final int[] array3 = dst.getPrimitiveArray(); + assertEquals("length after copy to smaller", dst.getLength(), src.getLength()); + assertNotSame("array after copy to smaller", array2, array3); + assertNotSame("array after copy to smaller", array3, src.getPrimitiveArray()); + assertIntArrayEquals("values after copy to smaller", + dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength()); + } + + @Test + public void testAppend() { + final int srcLength = DEFAULT_CAPACITY; + final ResizableIntArray src = new ResizableIntArray(srcLength); + for (int i = 0; i < srcLength; i++) { + final int value = i; + src.add(value); + } + final ResizableIntArray dst = new ResizableIntArray(DEFAULT_CAPACITY * 2); + final int[] array = dst.getPrimitiveArray(); + final int dstLength = DEFAULT_CAPACITY / 2; + for (int i = 0; i < dstLength; i++) { + final int value = -i - 1; + dst.add(value); + } + final ResizableIntArray dstCopy = new ResizableIntArray(dst.getLength()); + dstCopy.copy(dst); + + final int startPos = 0; + dst.append(src, startPos, 0 /* length */); + assertEquals("length after append zero", dstLength, dst.getLength()); + assertSame("array after append zero", array, dst.getPrimitiveArray()); + assertIntArrayEquals("values after append zero", dstCopy.getPrimitiveArray(), startPos, + dst.getPrimitiveArray(), startPos, dstLength); + + dst.append(src, startPos, srcLength); + assertEquals("length after append", dstLength + srcLength, dst.getLength()); + assertSame("array after append", array, dst.getPrimitiveArray()); + assertTrue("primitive length after append", + dst.getPrimitiveArray().length >= dstLength + srcLength); + assertIntArrayEquals("original values after append", dstCopy.getPrimitiveArray(), startPos, + dst.getPrimitiveArray(), startPos, dstLength); + assertIntArrayEquals("appended values after append", src.getPrimitiveArray(), startPos, + dst.getPrimitiveArray(), dstLength, srcLength); + + dst.append(src, startPos, srcLength); + assertEquals("length after 2nd append", dstLength + srcLength * 2, dst.getLength()); + assertNotSame("array after 2nd append", array, dst.getPrimitiveArray()); + assertTrue("primitive length after 2nd append", + dst.getPrimitiveArray().length >= dstLength + srcLength * 2); + assertIntArrayEquals("original values after 2nd append", + dstCopy.getPrimitiveArray(), startPos, dst.getPrimitiveArray(), startPos, + dstLength); + assertIntArrayEquals("appended values after 2nd append", + src.getPrimitiveArray(), startPos, dst.getPrimitiveArray(), dstLength, + srcLength); + assertIntArrayEquals("appended values after 2nd append", + src.getPrimitiveArray(), startPos, dst.getPrimitiveArray(), dstLength + srcLength, + srcLength); + } + + @Test + public void testFill() { + final int srcLength = DEFAULT_CAPACITY; + final ResizableIntArray src = new ResizableIntArray(srcLength); + for (int i = 0; i < srcLength; i++) { + final int value = i; + src.add(value); + } + final int[] array = src.getPrimitiveArray(); + + final int startPos = srcLength / 3; + final int length = srcLength / 3; + final int endPos = startPos + length; + assertTrue(startPos >= 1); + final int fillValue = 123; + try { + src.fill(fillValue, -1 /* startPos */, length); + fail("fill from -1 shouldn't succeed"); + } catch (IllegalArgumentException e) { + // success + } + try { + src.fill(fillValue, startPos, -1 /* length */); + fail("fill negative length shouldn't succeed"); + } catch (IllegalArgumentException e) { + // success + } + + src.fill(fillValue, startPos, length); + assertEquals("length after fill", srcLength, src.getLength()); + assertSame("array after fill", array, src.getPrimitiveArray()); + for (int i = 0; i < srcLength; i++) { + final int value = i; + if (i >= startPos && i < endPos) { + assertEquals("new values after fill at " + i, fillValue, src.get(i)); + } else { + assertEquals("unmodified values after fill at " + i, value, src.get(i)); + } + } + + final int length2 = srcLength * 2 - startPos; + final int largeEnd = startPos + length2; + assertTrue(largeEnd > srcLength); + final int fillValue2 = 456; + src.fill(fillValue2, startPos, length2); + assertEquals("length after large fill", largeEnd, src.getLength()); + assertNotSame("array after large fill", array, src.getPrimitiveArray()); + for (int i = 0; i < largeEnd; i++) { + final int value = i; + if (i >= startPos && i < largeEnd) { + assertEquals("new values after large fill at " + i, fillValue2, src.get(i)); + } else { + assertEquals("unmodified values after large fill at " + i, value, src.get(i)); + } + } + + final int startPos2 = largeEnd + length2; + final int endPos2 = startPos2 + length2; + final int fillValue3 = 789; + src.fill(fillValue3, startPos2, length2); + assertEquals("length after disjoint fill", endPos2, src.getLength()); + for (int i = 0; i < endPos2; i++) { + final int value = i; + if (i >= startPos2 && i < endPos2) { + assertEquals("new values after disjoint fill at " + i, fillValue3, src.get(i)); + } else if (i >= startPos && i < largeEnd) { + assertEquals("unmodified values after disjoint fill at " + i, + fillValue2, src.get(i)); + } else if (i < startPos) { + assertEquals("unmodified values after disjoint fill at " + i, value, src.get(i)); + } else { + assertEquals("gap values after disjoint fill at " + i, 0, src.get(i)); + } + } + } + + private static void assertIntArrayEquals(final String message, final int[] expecteds, + final int expectedPos, final int[] actuals, final int actualPos, final int length) { + if (expecteds == actuals) { + return; + } + if (expecteds == null || actuals == null) { + assertEquals(message, Arrays.toString(expecteds), Arrays.toString(actuals)); + return; + } + if (expecteds.length < expectedPos + length || actuals.length < actualPos + length) { + fail(message + ": insufficient length: expecteds=" + Arrays.toString(expecteds) + + " actuals=" + Arrays.toString(actuals)); + return; + } + for (int i = 0; i < length; i++) { + assertEquals(message + " [" + i + "]", + expecteds[i + expectedPos], actuals[i + actualPos]); + } + } + + @Test + public void testShift() { + final ResizableIntArray src = new ResizableIntArray(DEFAULT_CAPACITY); + final int limit = DEFAULT_CAPACITY * 10; + final int shiftAmount = 20; + for (int i = 0; i < limit; ++i) { + final int value = i; + src.addAt(i, value); + assertEquals("length after add at " + i, i + 1, src.getLength()); + } + src.shift(shiftAmount); + for (int i = 0; i < limit - shiftAmount; ++i) { + final int oldValue = i + shiftAmount; + assertEquals("value at " + i, oldValue, src.get(i)); + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/common/StringUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/common/StringUtilsTests.java new file mode 100644 index 000000000..016819ce8 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/common/StringUtilsTests.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class StringUtilsTests { + private static final Locale US = Locale.US; + private static final Locale GERMAN = Locale.GERMAN; + private static final Locale TURKEY = new Locale("tr", "TR"); + private static final Locale GREECE = new Locale("el", "GR"); + + private static void assert_toTitleCaseOfKeyLabel(final Locale locale, + final String lowerCase, final String expected) { + assertEquals(lowerCase + " in " + locale, expected, + StringUtils.toTitleCaseOfKeyLabel(lowerCase, locale)); + } + + @Test + public void test_toTitleCaseOfKeyLabel() { + assert_toTitleCaseOfKeyLabel(US, null, null); + assert_toTitleCaseOfKeyLabel(US, "", ""); + assert_toTitleCaseOfKeyLabel(US, "aeiou", "AEIOU"); + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+00C0: "À" LATIN CAPITAL LETTER A WITH GRAVE + // U+00C8: "È" LATIN CAPITAL LETTER E WITH GRAVE + // U+00CE: "Î" LATIN CAPITAL LETTER I WITH CIRCUMFLEX + // U+00D6: "Ö" LATIN CAPITAL LETTER O WITH DIAERESIS + // U+016A: "Ū" LATIN CAPITAL LETTER U WITH MACRON + // U+00D1: "Ñ" LATIN CAPITAL LETTER N WITH TILDE + // U+00C7: "Ç" LATIN CAPITAL LETTER C WITH CEDILLA + assert_toTitleCaseOfKeyLabel(US, + "\u00E0\u00E8\u00EE\u00F6\u016B\u00F1\u00E7", + "\u00C0\u00C8\u00CE\u00D6\u016A\u00D1\u00C7"); + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+015A: "Ś" LATIN CAPITAL LETTER S WITH ACUTE + // U+0160: "Š" LATIN CAPITAL LETTER S WITH CARONZ + assert_toTitleCaseOfKeyLabel(GERMAN, + "\u00DF\u015B\u0161", + "SS\u015A\u0160"); + // U+0259: "ə" LATIN SMALL LETTER SCHWA + // U+0069: "i" LATIN SMALL LETTER I + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+018F: "Ə" LATIN SMALL LETTER SCHWA + // U+0130: "İ" LATIN SMALL LETTER I WITH DOT ABOVE + // U+0049: "I" LATIN SMALL LETTER I + assert_toTitleCaseOfKeyLabel(TURKEY, + "\u0259\u0069\u0131", + "\u018F\u0130\u0049"); + // U+03C3: "σ" GREEK SMALL LETTER SIGMA + // U+03C2: "ς" GREEK SMALL LETTER FINAL SIGMA + // U+03A3: "Σ" GREEK CAPITAL LETTER SIGMA + assert_toTitleCaseOfKeyLabel(GREECE, + "\u03C3\u03C2", + "\u03A3\u03A3"); + // U+03AC: "ά" GREEK SMALL LETTER ALPHA WITH TONOS + // U+03AD: "έ" GREEK SMALL LETTER EPSILON WITH TONOS + // U+03AE: "ή" GREEK SMALL LETTER ETA WITH TONOS + // U+03AF: "ί" GREEK SMALL LETTER IOTA WITH TONOS + // U+03CC: "ό" GREEK SMALL LETTER OMICRON WITH TONOS + // U+03CD: "ύ" GREEK SMALL LETTER UPSILON WITH TONOS + // U+03CE: "ώ" GREEK SMALL LETTER OMEGA WITH TONOS + // U+0386: "Ά" GREEK CAPITAL LETTER ALPHA WITH TONOS + // U+0388: "Έ" GREEK CAPITAL LETTER EPSILON WITH TONOS + // U+0389: "Ή" GREEK CAPITAL LETTER ETA WITH TONOS + // U+038A: "Ί" GREEK CAPITAL LETTER IOTA WITH TONOS + // U+038C: "Ό" GREEK CAPITAL LETTER OMICRON WITH TONOS + // U+038E: "Ύ" GREEK CAPITAL LETTER UPSILON WITH TONOS + // U+038F: "Ώ" GREEK CAPITAL LETTER OMEGA WITH TONOS + assert_toTitleCaseOfKeyLabel(GREECE, + "\u03AC\u03AD\u03AE\u03AF\u03CC\u03CD\u03CE", + "\u0386\u0388\u0389\u038A\u038C\u038E\u038F"); + // U+03CA: "ϊ" GREEK SMALL LETTER IOTA WITH DIALYTIKA + // U+03CB: "ϋ" GREEK SMALL LETTER UPSILON WITH DIALYTIKA + // U+0390: "ΐ" GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS + // U+03B0: "ΰ" GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS + // U+03AA: "Ϊ" GREEK CAPITAL LETTER IOTA WITH DIALYTIKA + // U+03AB: "Ϋ" GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA + // U+0399: "Ι" GREEK CAPITAL LETTER IOTA + // U+03A5: "Υ" GREEK CAPITAL LETTER UPSILON + // U+0308: COMBINING DIAERESIS + // U+0301: COMBINING GRAVE ACCENT + assert_toTitleCaseOfKeyLabel(GREECE, + "\u03CA\u03CB\u0390\u03B0", + "\u03AA\u03AB\u0399\u0308\u0301\u03A5\u0308\u0301"); + } + + private static void assert_toTitleCaseOfKeyCode(final Locale locale, final int lowerCase, + final int expected) { + assertEquals(lowerCase + " in " + locale, expected, + StringUtils.toTitleCaseOfKeyCode(lowerCase, locale)); + } + + @Test + public void test_toTitleCaseOfKeyCode() { + assert_toTitleCaseOfKeyCode(US, Constants.CODE_ENTER, Constants.CODE_ENTER); + assert_toTitleCaseOfKeyCode(US, Constants.CODE_SPACE, Constants.CODE_SPACE); + assert_toTitleCaseOfKeyCode(US, Constants.CODE_COMMA, Constants.CODE_COMMA); + // U+0069: "i" LATIN SMALL LETTER I + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+0130: "İ" LATIN SMALL LETTER I WITH DOT ABOVE + // U+0049: "I" LATIN SMALL LETTER I + assert_toTitleCaseOfKeyCode(US, 0x0069, 0x0049); // i -> I + assert_toTitleCaseOfKeyCode(US, 0x0131, 0x0049); // ı -> I + assert_toTitleCaseOfKeyCode(TURKEY, 0x0069, 0x0130); // i -> İ + assert_toTitleCaseOfKeyCode(TURKEY, 0x0131, 0x0049); // ı -> I + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // The title case of "ß" is "SS". + assert_toTitleCaseOfKeyCode(US, 0x00DF, Constants.CODE_UNSPECIFIED); + // U+03AC: "ά" GREEK SMALL LETTER ALPHA WITH TONOS + // U+0386: "Ά" GREEK CAPITAL LETTER ALPHA WITH TONOS + assert_toTitleCaseOfKeyCode(GREECE, 0x03AC, 0x0386); + // U+03CA: "ϊ" GREEK SMALL LETTER IOTA WITH DIALYTIKA + // U+03AA: "Ϊ" GREEK CAPITAL LETTER IOTA WITH DIALYTIKA + assert_toTitleCaseOfKeyCode(GREECE, 0x03CA, 0x03AA); + // U+03B0: "ΰ" GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS + // The title case of "ΰ" is "\u03A5\u0308\u0301". + assert_toTitleCaseOfKeyCode(GREECE, 0x03B0, Constants.CODE_UNSPECIFIED); + } + + private static void assert_capitalizeFirstCodePoint(final Locale locale, final String text, + final String expected) { + assertEquals(text + " in " + locale, expected, + StringUtils.capitalizeFirstCodePoint(text, locale)); + } + + @Test + public void test_capitalizeFirstCodePoint() { + assert_capitalizeFirstCodePoint(US, "", ""); + assert_capitalizeFirstCodePoint(US, "a", "A"); + assert_capitalizeFirstCodePoint(US, "à", "À"); + assert_capitalizeFirstCodePoint(US, "ß", "SS"); + assert_capitalizeFirstCodePoint(US, "text", "Text"); + assert_capitalizeFirstCodePoint(US, "iGoogle", "IGoogle"); + assert_capitalizeFirstCodePoint(TURKEY, "iyi", "İyi"); + assert_capitalizeFirstCodePoint(TURKEY, "ısırdı", "Isırdı"); + assert_capitalizeFirstCodePoint(GREECE, "ά", "Ά"); + assert_capitalizeFirstCodePoint(GREECE, "άνεση", "Άνεση"); + } + + private static void assert_capitalizeFirstAndDowncaseRest(final Locale locale, + final String text, final String expected) { + assertEquals(text + " in " + locale, expected, + StringUtils.capitalizeFirstAndDowncaseRest(text, locale)); + } + + @Test + public void test_capitalizeFirstAndDowncaseRest() { + assert_capitalizeFirstAndDowncaseRest(US, "", ""); + assert_capitalizeFirstAndDowncaseRest(US, "a", "A"); + assert_capitalizeFirstAndDowncaseRest(US, "à", "À"); + assert_capitalizeFirstAndDowncaseRest(US, "ß", "SS"); + assert_capitalizeFirstAndDowncaseRest(US, "text", "Text"); + assert_capitalizeFirstAndDowncaseRest(US, "iGoogle", "Igoogle"); + assert_capitalizeFirstAndDowncaseRest(US, "invite", "Invite"); + assert_capitalizeFirstAndDowncaseRest(US, "INVITE", "Invite"); + assert_capitalizeFirstAndDowncaseRest(TURKEY, "iyi", "İyi"); + assert_capitalizeFirstAndDowncaseRest(TURKEY, "İYİ", "İyi"); + assert_capitalizeFirstAndDowncaseRest(TURKEY, "ısırdı", "Isırdı"); + assert_capitalizeFirstAndDowncaseRest(TURKEY, "ISIRDI", "Isırdı"); + assert_capitalizeFirstAndDowncaseRest(GREECE, "ά", "Ά"); + assert_capitalizeFirstAndDowncaseRest(GREECE, "άνεση", "Άνεση"); + assert_capitalizeFirstAndDowncaseRest(GREECE, "ΆΝΕΣΗ", "Άνεση"); + } + + @Test + public void testContainsInArray() { + assertFalse("empty array", StringUtils.containsInArray("key", new String[0])); + assertFalse("not in 1 element", StringUtils.containsInArray("key", new String[] { + "key1" + })); + assertFalse("not in 2 elements", StringUtils.containsInArray("key", new String[] { + "key1", "key2" + })); + + assertTrue("in 1 element", StringUtils.containsInArray("key", new String[] { + "key" + })); + assertTrue("in 2 elements", StringUtils.containsInArray("key", new String[] { + "key1", "key" + })); + } + + @Test + public void testContainsInCommaSplittableText() { + assertFalse("null", StringUtils.containsInCommaSplittableText("key", null)); + assertFalse("empty", StringUtils.containsInCommaSplittableText("key", "")); + assertFalse("not in 1 element", + StringUtils.containsInCommaSplittableText("key", "key1")); + assertFalse("not in 2 elements", + StringUtils.containsInCommaSplittableText("key", "key1,key2")); + + assertTrue("in 1 element", StringUtils.containsInCommaSplittableText("key", "key")); + assertTrue("in 2 elements", StringUtils.containsInCommaSplittableText("key", "key1,key")); + } + + @Test + public void testRemoveFromCommaSplittableTextIfExists() { + assertEquals("null", "", StringUtils.removeFromCommaSplittableTextIfExists("key", null)); + assertEquals("empty", "", StringUtils.removeFromCommaSplittableTextIfExists("key", "")); + + assertEquals("not in 1 element", "key1", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key1")); + assertEquals("not in 2 elements", "key1,key2", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key2")); + + assertEquals("in 1 element", "", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key")); + assertEquals("in 2 elements at position 1", "key2", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key,key2")); + assertEquals("in 2 elements at position 2", "key1", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key")); + assertEquals("in 3 elements at position 2", "key1,key3", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key,key3")); + + assertEquals("in 3 elements at position 1,2,3", "", + StringUtils.removeFromCommaSplittableTextIfExists("key", "key,key,key")); + assertEquals("in 5 elements at position 2,4", "key1,key3,key5", + StringUtils.removeFromCommaSplittableTextIfExists( + "key", "key1,key,key3,key,key5")); + } + + @Test + public void testCapitalizeFirstCodePoint() { + assertEquals("SSaa", + StringUtils.capitalizeFirstCodePoint("ßaa", Locale.GERMAN)); + assertEquals("Aßa", + StringUtils.capitalizeFirstCodePoint("aßa", Locale.GERMAN)); + assertEquals("Iab", + StringUtils.capitalizeFirstCodePoint("iab", Locale.ENGLISH)); + assertEquals("CAmElCaSe", + StringUtils.capitalizeFirstCodePoint("cAmElCaSe", Locale.ENGLISH)); + assertEquals("İab", + StringUtils.capitalizeFirstCodePoint("iab", new Locale("tr"))); + assertEquals("AİB", + StringUtils.capitalizeFirstCodePoint("AİB", new Locale("tr"))); + assertEquals("A", + StringUtils.capitalizeFirstCodePoint("a", Locale.ENGLISH)); + assertEquals("A", + StringUtils.capitalizeFirstCodePoint("A", Locale.ENGLISH)); + } + + @Test + public void testCapitalizeFirstAndDowncaseRest() { + assertEquals("SSaa", + StringUtils.capitalizeFirstAndDowncaseRest("ßaa", Locale.GERMAN)); + assertEquals("Aßa", + StringUtils.capitalizeFirstAndDowncaseRest("aßa", Locale.GERMAN)); + assertEquals("Iab", + StringUtils.capitalizeFirstAndDowncaseRest("iab", Locale.ENGLISH)); + assertEquals("Camelcase", + StringUtils.capitalizeFirstAndDowncaseRest("cAmElCaSe", Locale.ENGLISH)); + assertEquals("İab", + StringUtils.capitalizeFirstAndDowncaseRest("iab", new Locale("tr"))); + assertEquals("Aib", + StringUtils.capitalizeFirstAndDowncaseRest("AİB", new Locale("tr"))); + assertEquals("A", + StringUtils.capitalizeFirstAndDowncaseRest("a", Locale.ENGLISH)); + assertEquals("A", + StringUtils.capitalizeFirstAndDowncaseRest("A", Locale.ENGLISH)); + } + + @Test + public void testGetCapitalizationType() { + assertEquals(StringUtils.CAPITALIZE_NONE, + StringUtils.getCapitalizationType("capitalize")); + assertEquals(StringUtils.CAPITALIZE_NONE, + StringUtils.getCapitalizationType("cApITalize")); + assertEquals(StringUtils.CAPITALIZE_NONE, + StringUtils.getCapitalizationType("capitalizE")); + assertEquals(StringUtils.CAPITALIZE_NONE, + StringUtils.getCapitalizationType("__c a piu$@tali56ze")); + assertEquals(StringUtils.CAPITALIZE_FIRST, + StringUtils.getCapitalizationType("A__c a piu$@tali56ze")); + assertEquals(StringUtils.CAPITALIZE_FIRST, + StringUtils.getCapitalizationType("Capitalize")); + assertEquals(StringUtils.CAPITALIZE_FIRST, + StringUtils.getCapitalizationType(" Capitalize")); + assertEquals(StringUtils.CAPITALIZE_ALL, + StringUtils.getCapitalizationType("CAPITALIZE")); + assertEquals(StringUtils.CAPITALIZE_ALL, + StringUtils.getCapitalizationType(" PI26LIE")); + assertEquals(StringUtils.CAPITALIZE_NONE, + StringUtils.getCapitalizationType("")); + } + + @Test + public void testIsIdenticalAfterUpcaseIsIdenticalAfterDowncase() { + assertFalse(StringUtils.isIdenticalAfterUpcase("capitalize")); + assertTrue(StringUtils.isIdenticalAfterDowncase("capitalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase("cApITalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase("cApITalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase("capitalizE")); + assertFalse(StringUtils.isIdenticalAfterDowncase("capitalizE")); + assertFalse(StringUtils.isIdenticalAfterUpcase("__c a piu$@tali56ze")); + assertTrue(StringUtils.isIdenticalAfterDowncase("__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterUpcase("A__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterDowncase("A__c a piu$@tali56ze")); + assertFalse(StringUtils.isIdenticalAfterUpcase("Capitalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase("Capitalize")); + assertFalse(StringUtils.isIdenticalAfterUpcase(" Capitalize")); + assertFalse(StringUtils.isIdenticalAfterDowncase(" Capitalize")); + assertTrue(StringUtils.isIdenticalAfterUpcase("CAPITALIZE")); + assertFalse(StringUtils.isIdenticalAfterDowncase("CAPITALIZE")); + assertTrue(StringUtils.isIdenticalAfterUpcase(" PI26LIE")); + assertFalse(StringUtils.isIdenticalAfterDowncase(" PI26LIE")); + assertTrue(StringUtils.isIdenticalAfterUpcase("")); + assertTrue(StringUtils.isIdenticalAfterDowncase("")); + } + + private static void checkCapitalize(final String src, final String dst, + final int[] sortedSeparators, final Locale locale) { + assertEquals(dst, StringUtils.capitalizeEachWord(src, sortedSeparators, locale)); + assert(src.equals(dst) + == StringUtils.isIdenticalAfterCapitalizeEachWord(src, sortedSeparators)); + } + + private static final int[] SPACE = { Constants.CODE_SPACE }; + private static final int[] SPACE_PERIOD = StringUtils.toSortedCodePointArray(" ."); + private static final int[] SENTENCE_SEPARATORS = + StringUtils.toSortedCodePointArray(" \n.!?*()&"); + private static final int[] WORD_SEPARATORS = StringUtils.toSortedCodePointArray(" \n.!?*,();&"); + + @Test + public void testCapitalizeEachWord() { + checkCapitalize("", "", SPACE, Locale.ENGLISH); + checkCapitalize("test", "Test", SPACE, Locale.ENGLISH); + checkCapitalize(" test", " Test", SPACE, Locale.ENGLISH); + checkCapitalize("Test", "Test", SPACE, Locale.ENGLISH); + checkCapitalize(" Test", " Test", SPACE, Locale.ENGLISH); + checkCapitalize(".Test", ".test", SPACE, Locale.ENGLISH); + checkCapitalize(".Test", ".Test", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("test and retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test and retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test And Retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test And.Retest ", "Test And.Retest ", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test And.retest ", "Test And.Retest ", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test And.retest ", "Test And.retest ", SPACE, Locale.ENGLISH); + checkCapitalize("Test And.Retest ", "Test And.retest ", SPACE, Locale.ENGLISH); + checkCapitalize("test and ietest", "Test And İetest", SPACE_PERIOD, new Locale("tr")); + checkCapitalize("test and ietest", "Test And Ietest", SPACE_PERIOD, Locale.ENGLISH); + checkCapitalize("Test&Retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH); + checkCapitalize("Test&retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH); + checkCapitalize("test&Retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH); + checkCapitalize("rest\nrecreation! And in the end...", + "Rest\nRecreation! And In The End...", WORD_SEPARATORS, Locale.ENGLISH); + checkCapitalize("lorem ipsum dolor sit amet", "Lorem Ipsum Dolor Sit Amet", + WORD_SEPARATORS, Locale.ENGLISH); + checkCapitalize("Lorem!Ipsum (Dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet", + WORD_SEPARATORS, Locale.ENGLISH); + checkCapitalize("Lorem!Ipsum (dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet", + WORD_SEPARATORS, Locale.ENGLISH); + } + + @Test + public void testLooksLikeURL() { + assertTrue(StringUtils.lastPartLooksLikeURL("http://www.google.")); + assertFalse(StringUtils.lastPartLooksLikeURL("word wo")); + assertTrue(StringUtils.lastPartLooksLikeURL("/etc/foo")); + assertFalse(StringUtils.lastPartLooksLikeURL("left/right")); + assertTrue(StringUtils.lastPartLooksLikeURL("www.goo")); + assertTrue(StringUtils.lastPartLooksLikeURL("www.")); + assertFalse(StringUtils.lastPartLooksLikeURL("U.S.A")); + assertFalse(StringUtils.lastPartLooksLikeURL("U.S.A.")); + assertTrue(StringUtils.lastPartLooksLikeURL("rtsp://foo.")); + assertTrue(StringUtils.lastPartLooksLikeURL("://")); + assertFalse(StringUtils.lastPartLooksLikeURL("abc/")); + assertTrue(StringUtils.lastPartLooksLikeURL("abc.def/ghi")); + assertFalse(StringUtils.lastPartLooksLikeURL("abc.def")); + // TODO: ideally this would not look like a URL, but to keep down the complexity of the + // code for now True is acceptable. + assertTrue(StringUtils.lastPartLooksLikeURL("abc./def")); + // TODO: ideally this would not look like a URL, but to keep down the complexity of the + // code for now True is acceptable. + assertTrue(StringUtils.lastPartLooksLikeURL(".abc/def")); + } + + @Test + public void testHexStringUtils() { + final byte[] bytes = new byte[] { (byte)0x01, (byte)0x11, (byte)0x22, (byte)0x33, + (byte)0x55, (byte)0x88, (byte)0xEE }; + final String bytesStr = StringUtils.byteArrayToHexString(bytes); + final byte[] bytes2 = StringUtils.hexStringToByteArray(bytesStr); + for (int i = 0; i < bytes.length; ++i) { + assertTrue(bytes[i] == bytes2[i]); + } + final String bytesStr2 = StringUtils.byteArrayToHexString(bytes2); + assertTrue(bytesStr.equals(bytesStr2)); + } + + @Test + public void testToCodePointArray() { + final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh\u0000\u2002\u2003\u3000xx"; + final int[] EXPECTED_RESULT = new int[] { 'a', 'b', 'c', 'd', 'e', 0x286D7, 'f', 'g', 'h', + 0, 0x2002, 0x2003, 0x3000, 'x', 'x'}; + final int[] codePointArray = StringUtils.toCodePointArray(STR_WITH_SUPPLEMENTARY_CHAR, 0, + STR_WITH_SUPPLEMENTARY_CHAR.length()); + assertEquals("toCodePointArray, size matches", codePointArray.length, + EXPECTED_RESULT.length); + for (int i = 0; i < EXPECTED_RESULT.length; ++i) { + assertEquals("toCodePointArray position " + i, codePointArray[i], EXPECTED_RESULT[i]); + } + } + + @Test + public void testCopyCodePointsAndReturnCodePointCount() { + final String STR_WITH_SUPPLEMENTARY_CHAR = "AbcDE\uD861\uDED7fGh\u0000\u2002\u3000あx"; + final int[] EXPECTED_RESULT = new int[] { 'A', 'b', 'c', 'D', 'E', 0x286D7, + 'f', 'G', 'h', 0, 0x2002, 0x3000, 'あ', 'x'}; + final int[] EXPECTED_RESULT_DOWNCASE = new int[] { 'a', 'b', 'c', 'd', 'e', 0x286D7, + 'f', 'g', 'h', 0, 0x2002, 0x3000, 'あ', 'x'}; + + int[] codePointArray = new int[50]; + int codePointCount = StringUtils.copyCodePointsAndReturnCodePointCount(codePointArray, + STR_WITH_SUPPLEMENTARY_CHAR, 0, + STR_WITH_SUPPLEMENTARY_CHAR.length(), false /* downCase */); + assertEquals("copyCodePointsAndReturnCodePointCount, size matches", codePointCount, + EXPECTED_RESULT.length); + for (int i = 0; i < codePointCount; ++i) { + assertEquals("copyCodePointsAndReturnCodePointCount position " + i, codePointArray[i], + EXPECTED_RESULT[i]); + } + + codePointCount = StringUtils.copyCodePointsAndReturnCodePointCount(codePointArray, + STR_WITH_SUPPLEMENTARY_CHAR, 0, + STR_WITH_SUPPLEMENTARY_CHAR.length(), true /* downCase */); + assertEquals("copyCodePointsAndReturnCodePointCount downcase, size matches", codePointCount, + EXPECTED_RESULT_DOWNCASE.length); + for (int i = 0; i < codePointCount; ++i) { + assertEquals("copyCodePointsAndReturnCodePointCount position " + i, codePointArray[i], + EXPECTED_RESULT_DOWNCASE[i]); + } + + final int JAVA_CHAR_COUNT = 8; + final int CODEPOINT_COUNT = 7; + codePointCount = StringUtils.copyCodePointsAndReturnCodePointCount(codePointArray, + STR_WITH_SUPPLEMENTARY_CHAR, 0, JAVA_CHAR_COUNT, false /* downCase */); + assertEquals("copyCodePointsAndReturnCodePointCount, size matches", codePointCount, + CODEPOINT_COUNT); + for (int i = 0; i < codePointCount; ++i) { + assertEquals("copyCodePointsAndReturnCodePointCount position " + i, codePointArray[i], + EXPECTED_RESULT[i]); + } + + boolean exceptionHappened = false; + codePointArray = new int[5]; + try { + codePointCount = StringUtils.copyCodePointsAndReturnCodePointCount(codePointArray, + STR_WITH_SUPPLEMENTARY_CHAR, 0, JAVA_CHAR_COUNT, false /* downCase */); + } catch (ArrayIndexOutOfBoundsException e) { + exceptionHappened = true; + } + assertTrue("copyCodePointsAndReturnCodePointCount throws when array is too small", + exceptionHappened); + } + + @Test + public void testGetTrailingSingleQuotesCount() { + assertEquals(0, StringUtils.getTrailingSingleQuotesCount("")); + assertEquals(1, StringUtils.getTrailingSingleQuotesCount("'")); + assertEquals(5, StringUtils.getTrailingSingleQuotesCount("'''''")); + assertEquals(0, StringUtils.getTrailingSingleQuotesCount("a")); + assertEquals(0, StringUtils.getTrailingSingleQuotesCount("'this")); + assertEquals(1, StringUtils.getTrailingSingleQuotesCount("'word'")); + assertEquals(0, StringUtils.getTrailingSingleQuotesCount("I'm")); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/common/UnicodeSurrogateTests.java b/tests/src/org/kelar/inputmethod/latin/common/UnicodeSurrogateTests.java new file mode 100644 index 000000000..7fad18588 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/common/UnicodeSurrogateTests.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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 org.kelar.inputmethod.latin.common; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class UnicodeSurrogateTests { + + @Test + public void testIsLowSurrogate() { + assertFalse(UnicodeSurrogate.isLowSurrogate('\uD7FF')); + assertTrue(UnicodeSurrogate.isLowSurrogate('\uD83D')); + assertFalse(UnicodeSurrogate.isLowSurrogate('\uDC00')); + } + + @Test + public void testIsHighSurrogate() { + assertFalse(UnicodeSurrogate.isHighSurrogate('\uDBFF')); + assertTrue(UnicodeSurrogate.isHighSurrogate('\uDE25')); + assertFalse(UnicodeSurrogate.isHighSurrogate('\uE000')); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/AbstractDictDecoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/AbstractDictDecoder.java new file mode 100644 index 000000000..978e6cd3a --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/AbstractDictDecoder.java @@ -0,0 +1,104 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.TreeMap; + +/** + * A base class of the binary dictionary decoder. + */ +public abstract class AbstractDictDecoder implements DictDecoder { + private static final int SUCCESS = 0; + private static final int ERROR_CANNOT_READ = 1; + private static final int ERROR_WRONG_FORMAT = 2; + + @Override @UsedForTesting + public int getTerminalPosition(final String word) + throws IOException, UnsupportedFormatException { + if (!isDictBufferOpen()) { + openDictBuffer(); + } + return BinaryDictIOUtils.getTerminalPosition(this, word); + } + + @Override @UsedForTesting + public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, + final TreeMap<Integer, Integer> frequencies, + final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) + throws IOException, UnsupportedFormatException { + if (!isDictBufferOpen()) { + openDictBuffer(); + } + BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams); + } + + /** + * Check whether the header contains the expected information. This is a no-error method, + * that will return an error code and never throw a checked exception. + * @return an error code, either ERROR_* or SUCCESS. + */ + private int checkHeader() { + try { + readHeader(); + } catch (IOException e) { + return ERROR_CANNOT_READ; + } catch (UnsupportedFormatException e) { + return ERROR_WRONG_FORMAT; + } + return SUCCESS; + } + + @Override + public boolean hasValidRawBinaryDictionary() { + return checkHeader() == SUCCESS; + } + + // Placeholder implementations below. These are actually unused. + @Override + public void openDictBuffer() throws FileNotFoundException, IOException, + UnsupportedFormatException { + } + + @Override + public boolean isDictBufferOpen() { + return false; + } + + @Override + public PtNodeInfo readPtNode(final int ptNodePos) { + return null; + } + + @Override + public void setPosition(int newPos) { + } + + @Override + public int getPosition() { + return 0; + } + + @Override + public int readPtNodeCount() { + return 0; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java new file mode 100644 index 000000000..659df6859 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java @@ -0,0 +1,675 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.makedict; + +import android.test.AndroidTestCase; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import org.kelar.inputmethod.latin.BinaryDictionary; +import org.kelar.inputmethod.latin.common.CodePointUtils; +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNode; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; +import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils; +import org.kelar.inputmethod.latin.utils.ByteArrayDictBuffer; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.TreeMap; + +/** + * Unit tests for BinaryDictDecoderUtils and BinaryDictEncoderUtils. + */ +public class BinaryDictDecoderEncoderTests extends AndroidTestCase { + private static final String TAG = BinaryDictDecoderEncoderTests.class.getSimpleName(); + private static final int DEFAULT_MAX_UNIGRAMS = 300; + private static final int DEFAULT_CODE_POINT_SET_SIZE = 50; + private static final int LARGE_CODE_POINT_SET_SIZE = 300; + private static final int UNIGRAM_FREQ = 10; + private static final int BIGRAM_FREQ = 50; + private static final int TOLERANCE_OF_BIGRAM_FREQ = 5; + + private static final ArrayList<String> sWords = new ArrayList<>(); + private static final ArrayList<String> sWordsWithVariousCodePoints = new ArrayList<>(); + private static final SparseArray<List<Integer>> sEmptyBigrams = new SparseArray<>(); + private static final SparseArray<List<Integer>> sStarBigrams = new SparseArray<>(); + private static final SparseArray<List<Integer>> sChainBigrams = new SparseArray<>(); + + final Random mRandom; + + public BinaryDictDecoderEncoderTests() { + this(System.currentTimeMillis(), DEFAULT_MAX_UNIGRAMS); + } + + public BinaryDictDecoderEncoderTests(final long seed, final int maxUnigrams) { + super(); + BinaryDictionaryUtils.setCurrentTimeForTest(0); + Log.e(TAG, "Testing dictionary: seed is " + seed); + mRandom = new Random(seed); + sWords.clear(); + sWordsWithVariousCodePoints.clear(); + generateWords(maxUnigrams, mRandom); + + for (int i = 0; i < sWords.size(); ++i) { + sChainBigrams.put(i, new ArrayList<Integer>()); + if (i > 0) { + sChainBigrams.get(i - 1).add(i); + } + } + + sStarBigrams.put(0, new ArrayList<Integer>()); + // MAX - 1 because we added one above already + final int maxBigrams = Math.min(sWords.size(), FormatSpec.MAX_BIGRAMS_IN_A_PTNODE - 1); + for (int i = 1; i < maxBigrams; ++i) { + sStarBigrams.get(0).add(i); + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + BinaryDictionaryUtils.setCurrentTimeForTest(0); + } + + @Override + protected void tearDown() throws Exception { + // Quit test mode. + BinaryDictionaryUtils.setCurrentTimeForTest(-1); + super.tearDown(); + } + + private static void generateWords(final int number, final Random random) { + final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE, + random); + final Set<String> wordSet = new HashSet<>(); + while (wordSet.size() < number) { + wordSet.add(CodePointUtils.generateWord(random, codePointSet)); + } + sWords.addAll(wordSet); + + final int[] largeCodePointSet = CodePointUtils.generateCodePointSet( + LARGE_CODE_POINT_SET_SIZE, random); + wordSet.clear(); + while (wordSet.size() < number) { + wordSet.add(CodePointUtils.generateWord(random, largeCodePointSet)); + } + sWordsWithVariousCodePoints.addAll(wordSet); + } + + /** + * Adds unigrams to the dictionary. + */ + private static void addUnigrams(final int number, final FusionDictionary dict, + final List<String> words) { + for (int i = 0; i < number; ++i) { + final String word = words.get(i); + final ArrayList<WeightedString> shortcuts = new ArrayList<>(); + dict.add(word, new ProbabilityInfo(UNIGRAM_FREQ), false /* isNotAWord */, + false /* isPossiblyOffensive */); + } + } + + private static void addBigrams(final FusionDictionary dict, + final List<String> words, + final SparseArray<List<Integer>> bigrams) { + for (int i = 0; i < bigrams.size(); ++i) { + final int w1 = bigrams.keyAt(i); + for (int w2 : bigrams.valueAt(i)) { + dict.setBigram(words.get(w1), words.get(w2), new ProbabilityInfo(BIGRAM_FREQ)); + } + } + } + +// The following is useful to dump the dictionary into a textual file, but it can't compile +// on-device, so it's commented out. +// private void dumpToCombinedFileForDebug(final FusionDictionary dict, final String filename) +// throws IOException { +// org.kelar.inputmethod.latin.dicttool.CombinedInputOutput.writeDictionaryCombined( +// new java.io.FileWriter(new File(filename)), dict); +// } + + private static long timeWritingDictToFile(final File file, final FusionDictionary dict, + final FormatSpec.FormatOptions formatOptions) { + + long now = -1, diff = -1; + + try { + final DictEncoder dictEncoder = BinaryDictUtils.getDictEncoder(file, formatOptions); + + now = System.currentTimeMillis(); + // If you need to dump the dict to a textual file, uncomment the line below and the + // function above + // dumpToCombinedFileForDebug(file, "/tmp/foo"); + dictEncoder.writeDictionary(dict, formatOptions); + diff = System.currentTimeMillis() - now; + } catch (IOException e) { + Log.e(TAG, "IO exception while writing file", e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "UnsupportedFormatException", e); + } + + return diff; + } + + private static void checkDictionary(final FusionDictionary dict, final List<String> words, + final SparseArray<List<Integer>> bigrams) { + assertNotNull(dict); + + // check unigram + for (final String word : words) { + final PtNode ptNode = FusionDictionary.findWordInTree(dict.mRootNodeArray, word); + assertNotNull(ptNode); + } + + // check bigram + for (int i = 0; i < bigrams.size(); ++i) { + final int w1 = bigrams.keyAt(i); + for (final int w2 : bigrams.valueAt(i)) { + final PtNode ptNode = FusionDictionary.findWordInTree(dict.mRootNodeArray, + words.get(w1)); + assertNotNull(words.get(w1) + "," + words.get(w2), ptNode.getBigram(words.get(w2))); + } + } + } + + private static String outputOptions(final int bufferType, + final FormatSpec.FormatOptions formatOptions) { + final String result = " : buffer type = " + + ((bufferType == BinaryDictUtils.USE_BYTE_BUFFER) ? "byte buffer" : "byte array"); + return result + " : version = " + formatOptions.mVersion; + } + + // Tests for readDictionaryBinary and writeDictionaryBinary + + private static long timeReadingAndCheckDict(final File file, final List<String> words, + final SparseArray<List<Integer>> bigrams, final int bufferType) { + long now, diff = -1; + + FusionDictionary dict = null; + try { + final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(file, 0, file.length(), + bufferType); + now = System.currentTimeMillis(); + dict = dictDecoder.readDictionaryBinary(false /* deleteDictIfBroken */); + diff = System.currentTimeMillis() - now; + } catch (IOException e) { + Log.e(TAG, "IOException while reading dictionary", e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "Unsupported format", e); + } + + checkDictionary(dict, words, bigrams); + return diff; + } + + // Tests for readDictionaryBinary and writeDictionaryBinary + private String runReadAndWrite(final List<String> words, + final SparseArray<List<Integer>> bigrams, + final int bufferType, final FormatSpec.FormatOptions formatOptions, + final String message) { + + final String dictName = "runReadAndWrite"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, formatOptions, + getContext().getCacheDir()); + + final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(words.size(), dict, words); + addBigrams(dict, words, bigrams); + checkDictionary(dict, words, bigrams); + + final long write = timeWritingDictToFile(file, dict, formatOptions); + final long read = timeReadingAndCheckDict(file, words, bigrams, bufferType); + + return "PROF: read=" + read + "ms, write=" + write + "ms :" + message + + " : " + outputOptions(bufferType, formatOptions); + } + + private void runReadAndWriteTests(final List<String> results, final int bufferType, + final FormatSpec.FormatOptions formatOptions) { + results.add(runReadAndWrite(sWords, sEmptyBigrams, bufferType, + formatOptions, "unigram")); + results.add(runReadAndWrite(sWords, sChainBigrams, bufferType, + formatOptions, "chain")); + results.add(runReadAndWrite(sWords, sStarBigrams, bufferType, + formatOptions, "star")); + results.add(runReadAndWrite(sWords, sEmptyBigrams, bufferType, formatOptions, + "unigram with shortcuts")); + results.add(runReadAndWrite(sWords, sChainBigrams, bufferType, formatOptions, + "chain with shortcuts")); + results.add(runReadAndWrite(sWords, sStarBigrams, bufferType, formatOptions, + "star with shortcuts")); + results.add(runReadAndWrite(sWordsWithVariousCodePoints, sEmptyBigrams, + bufferType, formatOptions, + "unigram with various code points")); + } + + public void testCharacterTableIsPresent() throws IOException, UnsupportedFormatException { + final String[] wordSource = {"words", "used", "for", "testing", "a", "code point", "table"}; + final List<String> words = Arrays.asList(wordSource); + final String correctCodePointTable = "toesdrniawuplgfcb "; + final String dictName = "codePointTableTest"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final String codePointTableAttribute = DictionaryHeader.CODE_POINT_TABLE_KEY; + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, + BinaryDictUtils.STATIC_OPTIONS, getContext().getCacheDir()); + + // Write a test dictionary + final DictEncoder dictEncoder = new Ver2DictEncoder(file, + Ver2DictEncoder.CODE_POINT_TABLE_ON); + final FormatSpec.FormatOptions formatOptions = + new FormatSpec.FormatOptions( + FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION); + final FusionDictionary sourcedict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(words.size(), sourcedict, words); + dictEncoder.writeDictionary(sourcedict, formatOptions); + + // Read the dictionary + final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(file, 0, file.length(), + DictDecoder.USE_BYTEARRAY); + final DictionaryHeader fileHeader = dictDecoder.readHeader(); + // Check if codePointTable is present + assertTrue("codePointTable is not present", + fileHeader.mDictionaryOptions.mAttributes.containsKey(codePointTableAttribute)); + final String codePointTable = + fileHeader.mDictionaryOptions.mAttributes.get(codePointTableAttribute); + // Check if codePointTable is correct + assertEquals("codePointTable is incorrect", codePointTable, correctCodePointTable); + } + + // Unit test for CharEncoding.readString and CharEncoding.writeString. + public void testCharEncoding() { + // the max length of a word in sWords is less than 50. + // See generateWords. + final byte[] buffer = new byte[50 * 3]; + final DictBuffer dictBuffer = new ByteArrayDictBuffer(buffer); + for (final String word : sWords) { + Arrays.fill(buffer, (byte) 0); + CharEncoding.writeString(buffer, 0, word, null); + dictBuffer.position(0); + final String str = CharEncoding.readString(dictBuffer); + assertEquals(word, str); + } + } + + public void testReadAndWriteWithByteBuffer() { + final List<String> results = new ArrayList<>(); + + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER, + BinaryDictUtils.STATIC_OPTIONS); + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER, + BinaryDictUtils.DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP); + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER, + BinaryDictUtils.DYNAMIC_OPTIONS_WITH_TIMESTAMP); + for (final String result : results) { + Log.d(TAG, result); + } + } + + public void testReadAndWriteWithByteArray() { + final List<String> results = new ArrayList<>(); + + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY, + BinaryDictUtils.STATIC_OPTIONS); + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY, + BinaryDictUtils.DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP); + runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY, + BinaryDictUtils.DYNAMIC_OPTIONS_WITH_TIMESTAMP); + + for (final String result : results) { + Log.d(TAG, result); + } + } + + // Tests for readUnigramsAndBigramsBinary + + private static void checkWordMap(final List<String> expectedWords, + final SparseArray<List<Integer>> expectedBigrams, + final TreeMap<Integer, String> resultWords, + final TreeMap<Integer, Integer> resultFrequencies, + final TreeMap<Integer, ArrayList<PendingAttribute>> resultBigrams, + final boolean checkProbability) { + // check unigrams + final Set<String> actualWordsSet = new HashSet<>(resultWords.values()); + final Set<String> expectedWordsSet = new HashSet<>(expectedWords); + assertEquals(actualWordsSet, expectedWordsSet); + if (checkProbability) { + for (int freq : resultFrequencies.values()) { + assertEquals(freq, UNIGRAM_FREQ); + } + } + + // check bigrams + final HashMap<String, Set<String>> expBigrams = new HashMap<>(); + for (int i = 0; i < expectedBigrams.size(); ++i) { + final String word1 = expectedWords.get(expectedBigrams.keyAt(i)); + for (int w2 : expectedBigrams.valueAt(i)) { + if (expBigrams.get(word1) == null) { + expBigrams.put(word1, new HashSet<String>()); + } + expBigrams.get(word1).add(expectedWords.get(w2)); + } + } + + final HashMap<String, Set<String>> actBigrams = new HashMap<>(); + for (Entry<Integer, ArrayList<PendingAttribute>> entry : resultBigrams.entrySet()) { + final String word1 = resultWords.get(entry.getKey()); + final int unigramFreq = resultFrequencies.get(entry.getKey()); + for (PendingAttribute attr : entry.getValue()) { + final String word2 = resultWords.get(attr.mAddress); + if (actBigrams.get(word1) == null) { + actBigrams.put(word1, new HashSet<String>()); + } + actBigrams.get(word1).add(word2); + + if (checkProbability) { + final int bigramFreq = BinaryDictIOUtils.reconstructBigramFrequency( + unigramFreq, attr.mFrequency); + assertTrue(Math.abs(bigramFreq - BIGRAM_FREQ) < TOLERANCE_OF_BIGRAM_FREQ); + } + } + } + assertEquals(actBigrams, expBigrams); + } + + private static long timeAndCheckReadUnigramsAndBigramsBinary(final File file, + final List<String> words, final SparseArray<List<Integer>> bigrams, + final int bufferType, final boolean checkProbability) { + final TreeMap<Integer, String> resultWords = new TreeMap<>(); + final TreeMap<Integer, ArrayList<PendingAttribute>> resultBigrams = new TreeMap<>(); + final TreeMap<Integer, Integer> resultFreqs = new TreeMap<>(); + + long now = -1, diff = -1; + try { + final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(file, 0, file.length(), + bufferType); + now = System.currentTimeMillis(); + dictDecoder.readUnigramsAndBigramsBinary(resultWords, resultFreqs, resultBigrams); + diff = System.currentTimeMillis() - now; + } catch (IOException e) { + Log.e(TAG, "IOException", e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "UnsupportedFormatException", e); + } + + checkWordMap(words, bigrams, resultWords, resultFreqs, resultBigrams, checkProbability); + return diff; + } + + private String runReadUnigramsAndBigramsBinary(final ArrayList<String> words, + final SparseArray<List<Integer>> bigrams, final int bufferType, + final FormatSpec.FormatOptions formatOptions, final String message) { + final String dictName = "runReadUnigrams"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, formatOptions, + getContext().getCacheDir()); + + // making the dictionary from lists of words. + final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(words.size(), dict, words); + addBigrams(dict, words, bigrams); + + timeWritingDictToFile(file, dict, formatOptions); + + // Caveat: Currently, the Java code to read a v4 dictionary doesn't calculate the + // probability when there's a timestamp for the entry. + // TODO: Abandon the Java code, and implement the v4 dictionary reading code in native. + long wordMap = timeAndCheckReadUnigramsAndBigramsBinary(file, words, bigrams, bufferType, + !formatOptions.mHasTimestamp /* checkProbability */); + long fullReading = timeReadingAndCheckDict(file, words, bigrams, + bufferType); + + return "readDictionaryBinary=" + fullReading + ", readUnigramsAndBigramsBinary=" + wordMap + + " : " + message + " : " + outputOptions(bufferType, formatOptions); + } + + private void runReadUnigramsAndBigramsTests(final ArrayList<String> results, + final int bufferType, final FormatSpec.FormatOptions formatOptions) { + results.add(runReadUnigramsAndBigramsBinary(sWords, sEmptyBigrams, bufferType, + formatOptions, "unigram")); + results.add(runReadUnigramsAndBigramsBinary(sWords, sChainBigrams, bufferType, + formatOptions, "chain")); + results.add(runReadUnigramsAndBigramsBinary(sWords, sStarBigrams, bufferType, + formatOptions, "star")); + } + + public void testReadUnigramsAndBigramsBinaryWithByteBuffer() { + final ArrayList<String> results = new ArrayList<>(); + + runReadUnigramsAndBigramsTests(results, BinaryDictUtils.USE_BYTE_BUFFER, + BinaryDictUtils.STATIC_OPTIONS); + + for (final String result : results) { + Log.d(TAG, result); + } + } + + public void testReadUnigramsAndBigramsBinaryWithByteArray() { + final ArrayList<String> results = new ArrayList<>(); + + runReadUnigramsAndBigramsTests(results, BinaryDictUtils.USE_BYTE_ARRAY, + BinaryDictUtils.STATIC_OPTIONS); + + for (final String result : results) { + Log.d(TAG, result); + } + } + + // Tests for getTerminalPosition + private static String getWordFromBinary(final DictDecoder dictDecoder, final int address) { + if (dictDecoder.getPosition() != 0) dictDecoder.setPosition(0); + + DictionaryHeader fileHeader = null; + try { + fileHeader = dictDecoder.readHeader(); + } catch (IOException e) { + return null; + } catch (UnsupportedFormatException e) { + return null; + } + if (fileHeader == null) return null; + return BinaryDictDecoderUtils.getWordAtPosition(dictDecoder, fileHeader.mBodyOffset, + address).mWord; + } + + private static long checkGetTerminalPosition(final DictDecoder dictDecoder, final String word, + final boolean contained) { + long diff = -1; + int position = -1; + try { + final long now = System.nanoTime(); + position = dictDecoder.getTerminalPosition(word); + diff = System.nanoTime() - now; + } catch (IOException e) { + Log.e(TAG, "IOException while getTerminalPosition", e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "UnsupportedFormatException while getTerminalPosition", e); + } + + assertEquals(FormatSpec.NOT_VALID_WORD != position, contained); + if (contained) assertEquals(getWordFromBinary(dictDecoder, position), word); + return diff; + } + + private void runGetTerminalPosition(final ArrayList<String> words, + final SparseArray<List<Integer>> bigrams, final int bufferType, + final FormatOptions formatOptions, final String message) { + final String dictName = "testGetTerminalPosition"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, formatOptions, + getContext().getCacheDir()); + + final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(sWords.size(), dict, sWords); + addBigrams(dict, words, bigrams); + timeWritingDictToFile(file, dict, formatOptions); + + final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(file, 0, file.length(), + DictDecoder.USE_BYTEARRAY); + try { + dictDecoder.openDictBuffer(); + } catch (IOException e) { + Log.e(TAG, "IOException while opening the buffer", e); + } catch (UnsupportedFormatException e) { + Log.e(TAG, "IOException while opening the buffer", e); + } + assertTrue("Can't get the buffer", dictDecoder.isDictBufferOpen()); + + try { + // too long word + final String longWord = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; + assertEquals(FormatSpec.NOT_VALID_WORD, dictDecoder.getTerminalPosition(longWord)); + + // null + assertEquals(FormatSpec.NOT_VALID_WORD, dictDecoder.getTerminalPosition(null)); + + // empty string + assertEquals(FormatSpec.NOT_VALID_WORD, dictDecoder.getTerminalPosition("")); + } catch (IOException e) { + } catch (UnsupportedFormatException e) { + } + + // Test a word that is contained within the dictionary. + long sum = 0; + for (int i = 0; i < sWords.size(); ++i) { + final long time = checkGetTerminalPosition(dictDecoder, sWords.get(i), true); + sum += time == -1 ? 0 : time; + } + Log.d(TAG, "per search : " + (((double)sum) / sWords.size() / 1000000) + " : " + message + + " : " + outputOptions(bufferType, formatOptions)); + + // Test a word that isn't contained within the dictionary. + final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE, + mRandom); + for (int i = 0; i < 1000; ++i) { + final String word = CodePointUtils.generateWord(mRandom, codePointSet); + if (sWords.indexOf(word) != -1) continue; + checkGetTerminalPosition(dictDecoder, word, false); + } + } + + private void runGetTerminalPositionTests(final int bufferType, + final FormatOptions formatOptions) { + runGetTerminalPosition(sWords, sEmptyBigrams, bufferType, formatOptions, "unigram"); + } + + public void testGetTerminalPosition() { + final ArrayList<String> results = new ArrayList<>(); + + runGetTerminalPositionTests(BinaryDictUtils.USE_BYTE_ARRAY, + BinaryDictUtils.STATIC_OPTIONS); + runGetTerminalPositionTests(BinaryDictUtils.USE_BYTE_BUFFER, + BinaryDictUtils.STATIC_OPTIONS); + + for (final String result : results) { + Log.d(TAG, result); + } + } + + public void testVer2DictGetWordProperty() { + final FormatOptions formatOptions = BinaryDictUtils.STATIC_OPTIONS; + final ArrayList<String> words = sWords; + final String dictName = "testGetWordProperty"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(words.size(), dict, words); + addBigrams(dict, words, sEmptyBigrams); + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, formatOptions, + getContext().getCacheDir()); + file.delete(); + timeWritingDictToFile(file, dict, formatOptions); + final BinaryDictionary binaryDictionary = new BinaryDictionary(file.getAbsolutePath(), + 0 /* offset */, file.length(), true /* useFullEditDistance */, + Locale.ENGLISH, dictName, false /* isUpdatable */); + for (final String word : words) { + final WordProperty wordProperty = binaryDictionary.getWordProperty(word, + false /* isBeginningOfSentence */); + assertEquals(word, wordProperty.mWord); + assertEquals(UNIGRAM_FREQ, wordProperty.getProbability()); + } + } + + public void testVer2DictIteration() { + final FormatOptions formatOptions = BinaryDictUtils.STATIC_OPTIONS; + final ArrayList<String> words = sWords; + final SparseArray<List<Integer>> bigrams = sEmptyBigrams; + final String dictName = "testGetWordProperty"; + final String dictVersion = Long.toString(System.currentTimeMillis()); + final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), + BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions)); + addUnigrams(words.size(), dict, words); + addBigrams(dict, words, bigrams); + final File file = BinaryDictUtils.getDictFile(dictName, dictVersion, formatOptions, + getContext().getCacheDir()); + timeWritingDictToFile(file, dict, formatOptions); + Log.d(TAG, file.getAbsolutePath()); + final BinaryDictionary binaryDictionary = new BinaryDictionary(file.getAbsolutePath(), + 0 /* offset */, file.length(), true /* useFullEditDistance */, + Locale.ENGLISH, dictName, false /* isUpdatable */); + + final HashSet<String> wordSet = new HashSet<>(words); + final HashSet<Pair<String, String>> bigramSet = new HashSet<>(); + + for (int i = 0; i < words.size(); i++) { + final List<Integer> bigramList = bigrams.get(i); + if (bigramList != null) { + for (final Integer word1Index : bigramList) { + final String word1 = words.get(word1Index); + bigramSet.add(new Pair<>(words.get(i), word1)); + } + } + } + int token = 0; + do { + final BinaryDictionary.GetNextWordPropertyResult result = + binaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + final String word0 = wordProperty.mWord; + assertEquals(UNIGRAM_FREQ, wordProperty.mProbabilityInfo.mProbability); + wordSet.remove(word0); + if (wordProperty.mHasNgrams) { + for (final WeightedString bigramTarget : wordProperty.getBigrams()) { + final String word1 = bigramTarget.mWord; + final Pair<String, String> bigram = new Pair<>(word0, word1); + assertTrue(bigramSet.contains(bigram)); + bigramSet.remove(bigram); + } + } + token = result.mNextToken; + } while (token != 0); + assertTrue(wordSet.isEmpty()); + assertTrue(bigramSet.isEmpty()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderUtils.java new file mode 100644 index 000000000..18199ae21 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictDecoderUtils.java @@ -0,0 +1,425 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.LinkedList; + +import javax.annotation.Nonnull; + +/** + * Decodes binary files for a FusionDictionary. + * + * All the methods in this class are static. + * + * TODO: Move this file to makedict/internal. + * TODO: Rename this class to DictDecoderUtils. + */ +public final class BinaryDictDecoderUtils { + private BinaryDictDecoderUtils() { + // This utility class is not publicly instantiable. + } + + @UsedForTesting + public interface DictBuffer { + public int readUnsignedByte(); + public int readUnsignedShort(); + public int readUnsignedInt24(); + public int readInt(); + public int position(); + public void position(int newPosition); + @UsedForTesting + public void put(final byte b); + public int limit(); + @UsedForTesting + public int capacity(); + } + + public static final class ByteBufferDictBuffer implements DictBuffer { + private ByteBuffer mBuffer; + + public ByteBufferDictBuffer(final ByteBuffer buffer) { + mBuffer = buffer; + } + + @Override + public int readUnsignedByte() { + return mBuffer.get() & 0xFF; + } + + @Override + public int readUnsignedShort() { + return mBuffer.getShort() & 0xFFFF; + } + + @Override + public int readUnsignedInt24() { + final int retval = readUnsignedByte(); + return (retval << 16) + readUnsignedShort(); + } + + @Override + public int readInt() { + return mBuffer.getInt(); + } + + @Override + public int position() { + return mBuffer.position(); + } + + @Override + public void position(int newPos) { + mBuffer.position(newPos); + } + + @Override + public void put(final byte b) { + mBuffer.put(b); + } + + @Override + public int limit() { + return mBuffer.limit(); + } + + @Override + public int capacity() { + return mBuffer.capacity(); + } + } + + /** + * A class grouping utility function for our specific character encoding. + */ + static final class CharEncoding { + + /** + * Helper method to find out whether this code fits on one byte + */ + private static boolean fitsOnOneByte(final int character, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int codePoint = character; + if (codePointToOneByteCodeMap != null) { + if (codePointToOneByteCodeMap.containsKey(character)) { + codePoint = codePointToOneByteCodeMap.get(character); + } + } + return codePoint >= FormatSpec.MINIMAL_ONE_BYTE_CHARACTER_VALUE + && codePoint <= FormatSpec.MAXIMAL_ONE_BYTE_CHARACTER_VALUE; + } + + /** + * Compute the size of a character given its character code. + * + * Char format is: + * 1 byte = bbbbbbbb match + * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte + * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because + * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with + * 00011111 would be outside unicode. + * else: iso-latin-1 code + * This allows for the whole unicode range to be encoded, including chars outside of + * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control + * characters which should never happen anyway (and still work, but take 3 bytes). + * + * @param character the character code. + * @return the size in binary encoded-form, either 1 or 3 bytes. + */ + static int getCharSize(final int character, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + // See char encoding in FusionDictionary.java + if (fitsOnOneByte(character, codePointToOneByteCodeMap)) return 1; + if (FormatSpec.INVALID_CHARACTER == character) return 1; + return 3; + } + + /** + * Compute the byte size of a character array. + */ + static int getCharArraySize(final int[] chars, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int size = 0; + for (int character : chars) size += getCharSize(character, codePointToOneByteCodeMap); + return size; + } + + /** + * Writes a char array to a byte buffer. + * + * @param codePoints the code point array to write. + * @param buffer the byte buffer to write to. + * @param fromIndex the index in buffer to write the character array to. + * @param codePointToOneByteCodeMap the map to convert the code point. + * @return the index after the last character. + */ + static int writeCharArray(final int[] codePoints, final byte[] buffer, final int fromIndex, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int index = fromIndex; + for (int codePoint : codePoints) { + if (codePointToOneByteCodeMap != null) { + if (codePointToOneByteCodeMap.containsKey(codePoint)) { + // Convert code points + codePoint = codePointToOneByteCodeMap.get(codePoint); + } + } + if (1 == getCharSize(codePoint, codePointToOneByteCodeMap)) { + buffer[index++] = (byte)codePoint; + } else { + buffer[index++] = (byte)(0xFF & (codePoint >> 16)); + buffer[index++] = (byte)(0xFF & (codePoint >> 8)); + buffer[index++] = (byte)(0xFF & codePoint); + } + } + return index; + } + + /** + * Writes a string with our character format to a byte buffer. + * + * This will also write the terminator byte. + * + * @param buffer the byte buffer to write to. + * @param origin the offset to write from. + * @param word the string to write. + * @return the size written, in bytes. + */ + static int writeString(final byte[] buffer, final int origin, final String word, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + final int length = word.length(); + int index = origin; + for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { + int codePoint = word.codePointAt(i); + if (codePointToOneByteCodeMap != null) { + if (codePointToOneByteCodeMap.containsKey(codePoint)) { + // Convert code points + codePoint = codePointToOneByteCodeMap.get(codePoint); + } + } + if (1 == getCharSize(codePoint, codePointToOneByteCodeMap)) { + buffer[index++] = (byte)codePoint; + } else { + buffer[index++] = (byte)(0xFF & (codePoint >> 16)); + buffer[index++] = (byte)(0xFF & (codePoint >> 8)); + buffer[index++] = (byte)(0xFF & codePoint); + } + } + buffer[index++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; + return index - origin; + } + + /** + * Writes a string with our character format to an OutputStream. + * + * This will also write the terminator byte. + * + * @param stream the OutputStream to write to. + * @param word the string to write. + * @return the size written, in bytes. + */ + static int writeString(final OutputStream stream, final String word, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) throws IOException { + final int length = word.length(); + int written = 0; + for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { + final int codePoint = word.codePointAt(i); + final int charSize = getCharSize(codePoint, codePointToOneByteCodeMap); + if (1 == charSize) { + stream.write((byte) codePoint); + } else { + stream.write((byte) (0xFF & (codePoint >> 16))); + stream.write((byte) (0xFF & (codePoint >> 8))); + stream.write((byte) (0xFF & codePoint)); + } + written += charSize; + } + stream.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR); + written += FormatSpec.PTNODE_TERMINATOR_SIZE; + return written; + } + + /** + * Reads a string from a DictBuffer. This is the converse of the above method. + */ + static String readString(final DictBuffer dictBuffer) { + final StringBuilder s = new StringBuilder(); + int character = readChar(dictBuffer); + while (character != FormatSpec.INVALID_CHARACTER) { + s.appendCodePoint(character); + character = readChar(dictBuffer); + } + return s.toString(); + } + + /** + * Reads a character from the buffer. + * + * This follows the character format documented earlier in this source file. + * + * @param dictBuffer the buffer, positioned over an encoded character. + * @return the character code. + */ + static int readChar(final DictBuffer dictBuffer) { + int character = dictBuffer.readUnsignedByte(); + if (!fitsOnOneByte(character, null)) { + if (FormatSpec.PTNODE_CHARACTERS_TERMINATOR == character) { + return FormatSpec.INVALID_CHARACTER; + } + character <<= 16; + character += dictBuffer.readUnsignedShort(); + } + return character; + } + } + + /** + * Reads and returns the PtNode count out of a buffer and forwards the pointer. + */ + /* package */ static int readPtNodeCount(final DictBuffer dictBuffer) { + final int msb = dictBuffer.readUnsignedByte(); + if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= msb) { + return msb; + } + return ((FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT & msb) << 8) + + dictBuffer.readUnsignedByte(); + } + + /** + * Finds, as a string, the word at the position passed as an argument. + * + * @param dictDecoder the dict decoder. + * @param headerSize the size of the header. + * @param pos the position to seek. + * @return the word with its frequency, as a weighted string. + */ + @UsedForTesting + /* package for tests */ static WeightedString getWordAtPosition(final DictDecoder dictDecoder, + final int headerSize, final int pos) { + final WeightedString result; + final int originalPos = dictDecoder.getPosition(); + dictDecoder.setPosition(pos); + result = getWordAtPositionWithoutParentAddress(dictDecoder, headerSize, pos); + dictDecoder.setPosition(originalPos); + return result; + } + + private static WeightedString getWordAtPositionWithoutParentAddress( + final DictDecoder dictDecoder, final int headerSize, final int pos) { + dictDecoder.setPosition(headerSize); + final int count = dictDecoder.readPtNodeCount(); + int groupPos = dictDecoder.getPosition(); + final StringBuilder builder = new StringBuilder(); + WeightedString result = null; + + PtNodeInfo last = null; + for (int i = count - 1; i >= 0; --i) { + PtNodeInfo info = dictDecoder.readPtNode(groupPos); + groupPos = info.mEndAddress; + if (info.mOriginalAddress == pos) { + builder.append(new String(info.mCharacters, 0, info.mCharacters.length)); + result = new WeightedString(builder.toString(), info.mProbabilityInfo); + break; // and return + } + if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { + if (info.mChildrenAddress > pos) { + if (null == last) continue; + builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); + dictDecoder.setPosition(last.mChildrenAddress); + i = dictDecoder.readPtNodeCount(); + groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); + last = null; + continue; + } + last = info; + } + if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) { + builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); + dictDecoder.setPosition(last.mChildrenAddress); + i = dictDecoder.readPtNodeCount(); + groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); + last = null; + continue; + } + } + return result; + } + + /** + * Helper method that brutally decodes a header from a byte array. + * + * @param headerBuffer a buffer containing the bytes of the header. + * @return a hashmap of the attributes stored in the header + */ + @Nonnull + public static HashMap<String, String> decodeHeaderAttributes(@Nonnull final byte[] headerBuffer) + throws UnsupportedFormatException { + final StringBuilder sb = new StringBuilder(); + final LinkedList<String> keyValues = new LinkedList<>(); + int index = 0; + while (index < headerBuffer.length) { + if (headerBuffer[index] == FormatSpec.PTNODE_CHARACTERS_TERMINATOR) { + keyValues.add(sb.toString()); + sb.setLength(0); + } else if (CharEncoding.fitsOnOneByte(headerBuffer[index] & 0xFF, + null /* codePointTable */)) { + sb.appendCodePoint(headerBuffer[index] & 0xFF); + } else { + sb.appendCodePoint(((headerBuffer[index] & 0xFF) << 16) + + ((headerBuffer[index + 1] & 0xFF) << 8) + + (headerBuffer[index + 2] & 0xFF)); + index += 2; + } + index += 1; + } + if ((keyValues.size() & 1) != 0) { + throw new UnsupportedFormatException("Odd number of attributes"); + } + final HashMap<String, String> attributes = new HashMap<>(); + for (int i = 0; i < keyValues.size(); i += 2) { + attributes.put(keyValues.get(i), keyValues.get(i + 1)); + } + return attributes; + } + + /** + * Helper method to pass a file name instead of a File object to isBinaryDictionary. + */ + public static boolean isBinaryDictionary(final String filename) { + final File file = new File(filename); + return isBinaryDictionary(file); + } + + /** + * Basic test to find out whether the file is a binary dictionary or not. + * + * @param file The file to test. + * @return true if it's a binary dictionary, false otherwise + */ + public static boolean isBinaryDictionary(final File file) { + final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(file, 0, file.length()); + if (dictDecoder == null) { + return false; + } + return dictDecoder.hasValidRawBinaryDictionary(); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictEncoderUtils.java new file mode 100644 index 000000000..3b35288d7 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictEncoderUtils.java @@ -0,0 +1,839 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNode; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * Encodes binary files for a FusionDictionary. + * + * All the methods in this class are static. + * + * TODO: Rename this class to DictEncoderUtils. + */ +public class BinaryDictEncoderUtils { + + private static final boolean DBG = MakedictLog.DBG; + + private BinaryDictEncoderUtils() { + // This utility class is not publicly instantiable. + } + + // Arbitrary limit to how much passes we consider address size compression should + // terminate in. At the time of this writing, our largest dictionary completes + // compression in five passes. + // If the number of passes exceeds this number, makedict bails with an exception on + // suspicion that a bug might be causing an infinite loop. + private static final int MAX_PASSES = 24; + + /** + * Compute the binary size of the character array. + * + * If only one character, this is the size of this character. If many, it's the sum of their + * sizes + 1 byte for the terminator. + * + * @param characters the character array + * @return the size of the char array, including the terminator if any + */ + static int getPtNodeCharactersSize(final int[] characters, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int size = CharEncoding.getCharArraySize(characters, codePointToOneByteCodeMap); + if (characters.length > 1) size += FormatSpec.PTNODE_TERMINATOR_SIZE; + return size; + } + + /** + * Compute the binary size of the character array in a PtNode + * + * If only one character, this is the size of this character. If many, it's the sum of their + * sizes + 1 byte for the terminator. + * + * @param ptNode the PtNode + * @return the size of the char array, including the terminator if any + */ + private static int getPtNodeCharactersSize(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + return getPtNodeCharactersSize(ptNode.mChars, codePointToOneByteCodeMap); + } + + /** + * Compute the binary size of the PtNode count for a node array. + * @param nodeArray the nodeArray + * @return the size of the PtNode count, either 1 or 2 bytes. + */ + private static int getPtNodeCountSize(final PtNodeArray nodeArray) { + return BinaryDictIOUtils.getPtNodeCountSize(nodeArray.mData.size()); + } + + /** + * Compute the maximum size of a PtNode, assuming 3-byte addresses for everything. + * + * @param ptNode the PtNode to compute the size of. + * @return the maximum size of the PtNode. + */ + private static int getPtNodeMaximumSize(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int size = getNodeHeaderSize(ptNode, codePointToOneByteCodeMap); + if (ptNode.isTerminal()) { + // If terminal, one byte for the frequency. + size += FormatSpec.PTNODE_FREQUENCY_SIZE; + } + size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address + if (null != ptNode.mBigrams) { + size += (FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE + + FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE) + * ptNode.mBigrams.size(); + } + return size; + } + + /** + * Compute the maximum size of each PtNode of a PtNode array, assuming 3-byte addresses for + * everything, and caches it in the `mCachedSize' member of the nodes; deduce the size of + * the containing node array, and cache it it its 'mCachedSize' member. + * + * @param ptNodeArray the node array to compute the maximum size of. + */ + private static void calculatePtNodeArrayMaximumSize(final PtNodeArray ptNodeArray, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int size = getPtNodeCountSize(ptNodeArray); + for (PtNode node : ptNodeArray.mData) { + final int nodeSize = getPtNodeMaximumSize(node, codePointToOneByteCodeMap); + node.mCachedSize = nodeSize; + size += nodeSize; + } + ptNodeArray.mCachedSize = size; + } + + /** + * Compute the size of the header (flag + [parent address] + characters size) of a PtNode. + * + * @param ptNode the PtNode of which to compute the size of the header + */ + private static int getNodeHeaderSize(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + return FormatSpec.PTNODE_FLAGS_SIZE + getPtNodeCharactersSize(ptNode, + codePointToOneByteCodeMap); + } + + /** + * Compute the size, in bytes, that an address will occupy. + * + * This can be used either for children addresses (which are always positive) or for + * attribute, which may be positive or negative but + * store their sign bit separately. + * + * @param address the address + * @return the byte size. + */ + static int getByteSize(final int address) { + assert(address <= FormatSpec.UINT24_MAX); + if (!BinaryDictIOUtils.hasChildrenAddress(address)) { + return 0; + } else if (Math.abs(address) <= FormatSpec.UINT8_MAX) { + return 1; + } else if (Math.abs(address) <= FormatSpec.UINT16_MAX) { + return 2; + } else { + return 3; + } + } + + static int writeUIntToBuffer(final byte[] buffer, final int fromPosition, final int value, + final int size) { + int position = fromPosition; + switch(size) { + case 4: + buffer[position++] = (byte) ((value >> 24) & 0xFF); + /* fall through */ + case 3: + buffer[position++] = (byte) ((value >> 16) & 0xFF); + /* fall through */ + case 2: + buffer[position++] = (byte) ((value >> 8) & 0xFF); + /* fall through */ + case 1: + buffer[position++] = (byte) (value & 0xFF); + break; + default: + /* nop */ + } + return position; + } + + static void writeUIntToStream(final OutputStream stream, final int value, final int size) + throws IOException { + switch(size) { + case 4: + stream.write((value >> 24) & 0xFF); + /* fall through */ + case 3: + stream.write((value >> 16) & 0xFF); + /* fall through */ + case 2: + stream.write((value >> 8) & 0xFF); + /* fall through */ + case 1: + stream.write(value & 0xFF); + break; + default: + /* nop */ + } + } + + // End utility methods + + // This method is responsible for finding a nice ordering of the nodes that favors run-time + // cache performance and dictionary size. + /* package for tests */ static ArrayList<PtNodeArray> flattenTree( + final PtNodeArray rootNodeArray) { + final int treeSize = FusionDictionary.countPtNodes(rootNodeArray); + MakedictLog.i("Counted nodes : " + treeSize); + final ArrayList<PtNodeArray> flatTree = new ArrayList<>(treeSize); + return flattenTreeInner(flatTree, rootNodeArray); + } + + private static ArrayList<PtNodeArray> flattenTreeInner(final ArrayList<PtNodeArray> list, + final PtNodeArray ptNodeArray) { + // Removing the node is necessary if the tails are merged, because we would then + // add the same node several times when we only want it once. A number of places in + // the code also depends on any node being only once in the list. + // Merging tails can only be done if there are no attributes. Searching for attributes + // in LatinIME code depends on a total breadth-first ordering, which merging tails + // breaks. If there are no attributes, it should be fine (and reduce the file size) + // to merge tails, and removing the node from the list would be necessary. However, + // we don't merge tails because breaking the breadth-first ordering would result in + // extreme overhead at bigram lookup time (it would make the search function O(n) instead + // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty + // high). + // If no nodes are ever merged, we can't have the same node twice in the list, hence + // searching for duplicates in unnecessary. It is also very performance consuming, + // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making + // this simple list.remove operation O(n*n) overall. On Android this overhead is very + // high. + // For future reference, the code to remove duplicate is a simple : list.remove(node); + list.add(ptNodeArray); + final ArrayList<PtNode> branches = ptNodeArray.mData; + for (PtNode ptNode : branches) { + if (null != ptNode.mChildren) flattenTreeInner(list, ptNode.mChildren); + } + return list; + } + + /** + * Get the offset from a position inside a current node array to a target node array, during + * update. + * + * If the current node array is before the target node array, the target node array has not + * been updated yet, so we should return the offset from the old position of the current node + * array to the old position of the target node array. If on the other hand the target is + * before the current node array, it already has been updated, so we should return the offset + * from the new position in the current node array to the new position in the target node + * array. + * + * @param currentNodeArray node array containing the PtNode where the offset will be written + * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray + * @param targetNodeArray the target node array to get the offset to + * @return the offset to the target node array + */ + private static int getOffsetToTargetNodeArrayDuringUpdate(final PtNodeArray currentNodeArray, + final int offsetFromStartOfCurrentNodeArray, final PtNodeArray targetNodeArray) { + final boolean isTargetBeforeCurrent = (targetNodeArray.mCachedAddressBeforeUpdate + < currentNodeArray.mCachedAddressBeforeUpdate); + if (isTargetBeforeCurrent) { + return targetNodeArray.mCachedAddressAfterUpdate + - (currentNodeArray.mCachedAddressAfterUpdate + + offsetFromStartOfCurrentNodeArray); + } + return targetNodeArray.mCachedAddressBeforeUpdate + - (currentNodeArray.mCachedAddressBeforeUpdate + offsetFromStartOfCurrentNodeArray); + } + + /** + * Get the offset from a position inside a current node array to a target PtNode, during + * update. + * + * @param currentNodeArray node array containing the PtNode where the offset will be written + * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray + * @param targetPtNode the target PtNode to get the offset to + * @return the offset to the target PtNode + */ + // TODO: is there any way to factorize this method with the one above? + private static int getOffsetToTargetPtNodeDuringUpdate(final PtNodeArray currentNodeArray, + final int offsetFromStartOfCurrentNodeArray, final PtNode targetPtNode) { + final int oldOffsetBasePoint = currentNodeArray.mCachedAddressBeforeUpdate + + offsetFromStartOfCurrentNodeArray; + final boolean isTargetBeforeCurrent = (targetPtNode.mCachedAddressBeforeUpdate + < oldOffsetBasePoint); + // If the target is before the current node array, then its address has already been + // updated. We can use the AfterUpdate member, and compare it to our own member after + // update. Otherwise, the AfterUpdate member is not updated yet, so we need to use the + // BeforeUpdate member, and of course we have to compare this to our own address before + // update. + if (isTargetBeforeCurrent) { + final int newOffsetBasePoint = currentNodeArray.mCachedAddressAfterUpdate + + offsetFromStartOfCurrentNodeArray; + return targetPtNode.mCachedAddressAfterUpdate - newOffsetBasePoint; + } + return targetPtNode.mCachedAddressBeforeUpdate - oldOffsetBasePoint; + } + + /** + * Computes the actual node array size, based on the cached addresses of the children nodes. + * + * Each node array stores its tentative address. During dictionary address computing, these + * are not final, but they can be used to compute the node array size (the node array size + * depends on the address of the children because the number of bytes necessary to store an + * address depends on its numeric value. The return value indicates whether the node array + * contents (as in, any of the addresses stored in the cache fields) have changed with + * respect to their previous value. + * + * @param ptNodeArray the node array to compute the size of. + * @param dict the dictionary in which the word/attributes are to be found. + * @return false if none of the cached addresses inside the node array changed, true otherwise. + */ + private static boolean computeActualPtNodeArraySize(final PtNodeArray ptNodeArray, + final FusionDictionary dict, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + boolean changed = false; + int size = getPtNodeCountSize(ptNodeArray); + for (PtNode ptNode : ptNodeArray.mData) { + ptNode.mCachedAddressAfterUpdate = ptNodeArray.mCachedAddressAfterUpdate + size; + if (ptNode.mCachedAddressAfterUpdate != ptNode.mCachedAddressBeforeUpdate) { + changed = true; + } + int nodeSize = getNodeHeaderSize(ptNode, codePointToOneByteCodeMap); + if (ptNode.isTerminal()) { + nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE; + } + if (null != ptNode.mChildren) { + nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray, + nodeSize + size, ptNode.mChildren)); + } + if (null != ptNode.mBigrams) { + for (WeightedString bigram : ptNode.mBigrams) { + final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray, + nodeSize + size + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE, + FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord)); + nodeSize += getByteSize(offset) + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; + } + } + ptNode.mCachedSize = nodeSize; + size += nodeSize; + } + if (ptNodeArray.mCachedSize != size) { + ptNodeArray.mCachedSize = size; + changed = true; + } + return changed; + } + + /** + * Initializes the cached addresses of node arrays and their containing nodes from their size. + * + * @param flatNodes the list of node arrays. + * @return the byte size of the entire stack. + */ + private static int initializePtNodeArraysCachedAddresses( + final ArrayList<PtNodeArray> flatNodes) { + int nodeArrayOffset = 0; + for (final PtNodeArray nodeArray : flatNodes) { + nodeArray.mCachedAddressBeforeUpdate = nodeArrayOffset; + int nodeCountSize = getPtNodeCountSize(nodeArray); + int nodeffset = 0; + for (final PtNode ptNode : nodeArray.mData) { + ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate = + nodeCountSize + nodeArrayOffset + nodeffset; + nodeffset += ptNode.mCachedSize; + } + nodeArrayOffset += nodeArray.mCachedSize; + } + return nodeArrayOffset; + } + + /** + * Updates the cached addresses of node arrays after recomputing their new positions. + * + * @param flatNodes the list of node arrays. + */ + private static void updatePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes) { + for (final PtNodeArray nodeArray : flatNodes) { + nodeArray.mCachedAddressBeforeUpdate = nodeArray.mCachedAddressAfterUpdate; + for (final PtNode ptNode : nodeArray.mData) { + ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate; + } + } + } + + /** + * Compute the addresses and sizes of an ordered list of PtNode arrays. + * + * This method takes a list of PtNode arrays and will update their cached address and size + * values so that they can be written into a file. It determines the smallest size each of the + * PtNode arrays can be given the addresses of its children and attributes, and store that into + * each PtNode. + * The order of the PtNode is given by the order of the array. This method makes no effort + * to find a good order; it only mechanically computes the size this order results in. + * + * @param dict the dictionary + * @param flatNodes the ordered list of PtNode arrays + * @return the same array it was passed. The nodes have been updated for address and size. + */ + /* package */ static ArrayList<PtNodeArray> computeAddresses(final FusionDictionary dict, + final ArrayList<PtNodeArray> flatNodes, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + // First get the worst possible sizes and offsets + for (final PtNodeArray n : flatNodes) { + calculatePtNodeArrayMaximumSize(n, codePointToOneByteCodeMap); + } + final int offset = initializePtNodeArraysCachedAddresses(flatNodes); + + MakedictLog.i("Compressing the array addresses. Original size : " + offset); + MakedictLog.i("(Recursively seen size : " + offset + ")"); + + int passes = 0; + boolean changesDone = false; + do { + changesDone = false; + int ptNodeArrayStartOffset = 0; + for (final PtNodeArray ptNodeArray : flatNodes) { + ptNodeArray.mCachedAddressAfterUpdate = ptNodeArrayStartOffset; + final int oldNodeArraySize = ptNodeArray.mCachedSize; + final boolean changed = computeActualPtNodeArraySize(ptNodeArray, dict, + codePointToOneByteCodeMap); + final int newNodeArraySize = ptNodeArray.mCachedSize; + if (oldNodeArraySize < newNodeArraySize) { + throw new RuntimeException("Increased size ?!"); + } + ptNodeArrayStartOffset += newNodeArraySize; + changesDone |= changed; + } + updatePtNodeArraysCachedAddresses(flatNodes); + ++passes; + if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug"); + } while (changesDone); + + final PtNodeArray lastPtNodeArray = flatNodes.get(flatNodes.size() - 1); + MakedictLog.i("Compression complete in " + passes + " passes."); + MakedictLog.i("After address compression : " + + (lastPtNodeArray.mCachedAddressAfterUpdate + lastPtNodeArray.mCachedSize)); + + return flatNodes; + } + + /** + * Validity-checking method. + * + * This method checks a list of PtNode arrays for juxtaposition, that is, it will do + * nothing if each node array's cached address is actually the previous node array's address + * plus the previous node's size. + * If this is not the case, it will throw an exception. + * + * @param arrays the list of node arrays to check + */ + /* package */ static void checkFlatPtNodeArrayList(final ArrayList<PtNodeArray> arrays) { + int offset = 0; + int index = 0; + for (final PtNodeArray ptNodeArray : arrays) { + // BeforeUpdate and AfterUpdate addresses are the same here, so it does not matter + // which we use. + if (ptNodeArray.mCachedAddressAfterUpdate != offset) { + throw new RuntimeException("Wrong address for node " + index + + " : expected " + offset + ", got " + + ptNodeArray.mCachedAddressAfterUpdate); + } + ++index; + offset += ptNodeArray.mCachedSize; + } + } + + /** + * Helper method to write a children position to a file. + * + * @param buffer the buffer to write to. + * @param fromIndex the index in the buffer to write the address to. + * @param position the position to write. + * @return the size in bytes the address actually took. + */ + /* package */ static int writeChildrenPosition(final byte[] buffer, final int fromIndex, + final int position) { + int index = fromIndex; + switch (getByteSize(position)) { + case 1: + buffer[index++] = (byte)position; + return 1; + case 2: + buffer[index++] = (byte)(0xFF & (position >> 8)); + buffer[index++] = (byte)(0xFF & position); + return 2; + case 3: + buffer[index++] = (byte)(0xFF & (position >> 16)); + buffer[index++] = (byte)(0xFF & (position >> 8)); + buffer[index++] = (byte)(0xFF & position); + return 3; + case 0: + return 0; + default: + throw new RuntimeException("Position " + position + " has a strange size"); + } + } + + /** + * Makes the flag value for a PtNode. + * + * @param hasMultipleChars whether the PtNode has multiple chars. + * @param isTerminal whether the PtNode is terminal. + * @param childrenAddressSize the size of a children address. + * @param hasBigrams whether the PtNode has bigrams. + * @param isNotAWord whether the PtNode is not a word. + * @param isPossiblyOffensive whether the PtNode is a possibly offensive entry. + * @return the flags + */ + static int makePtNodeFlags(final boolean hasMultipleChars, final boolean isTerminal, + final int childrenAddressSize, final boolean hasBigrams, + final boolean isNotAWord, final boolean isPossiblyOffensive) { + byte flags = 0; + if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS; + if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL; + switch (childrenAddressSize) { + case 1: + flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE; + break; + case 2: + flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES; + break; + case 3: + flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES; + break; + case 0: + flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS; + break; + default: + throw new RuntimeException("Node with a strange address"); + } + if (hasBigrams) flags |= FormatSpec.FLAG_HAS_BIGRAMS; + if (isNotAWord) flags |= FormatSpec.FLAG_IS_NOT_A_WORD; + if (isPossiblyOffensive) flags |= FormatSpec.FLAG_IS_POSSIBLY_OFFENSIVE; + return flags; + } + + /* package */ static byte makePtNodeFlags(final PtNode node, final int childrenOffset) { + return (byte) makePtNodeFlags(node.mChars.length > 1, node.isTerminal(), + getByteSize(childrenOffset), + node.mBigrams != null && !node.mBigrams.isEmpty(), + node.mIsNotAWord, node.mIsPossiblyOffensive); + } + + /** + * Makes the flag value for a bigram. + * + * @param more whether there are more bigrams after this one. + * @param offset the offset of the bigram. + * @param bigramFrequency the frequency of the bigram, 0..255. + * @param unigramFrequency the unigram frequency of the same word, 0..255. + * @param word the second bigram, for debugging purposes + * @return the flags + */ + /* package */ static int makeBigramFlags(final boolean more, final int offset, + final int bigramFrequency, final int unigramFrequency, final String word) { + int bigramFlags = (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0) + + (offset < 0 ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0); + switch (getByteSize(offset)) { + case 1: + bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE; + break; + case 2: + bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES; + break; + case 3: + bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES; + break; + default: + throw new RuntimeException("Strange offset size"); + } + final int frequency; + if (unigramFrequency > bigramFrequency) { + MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word + + "\". Bigram freq is " + bigramFrequency + ", unigram freq for " + + word + " is " + unigramFrequency); + frequency = unigramFrequency; + } else { + frequency = bigramFrequency; + } + bigramFlags += getBigramFrequencyDiff(unigramFrequency, frequency) + & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY; + return bigramFlags; + } + + public static int getBigramFrequencyDiff(final int unigramFrequency, + final int bigramFrequency) { + // We compute the difference between 255 (which means probability = 1) and the + // unigram score. We split this into a number of discrete steps. + // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15 + // represents an increase of 16 steps: a value of 15 will be interpreted as the median + // value of the 16th step. In all justice, if the bigram frequency is low enough to be + // rounded below the first step (which means it is less than half a step higher than the + // unigram frequency) then the unigram frequency itself is the best approximation of the + // bigram freq that we could possibly supply, hence we should *not* include this bigram + // in the file at all. + // until this is done, we'll write 0 and slightly overestimate this case. + // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step + // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to + // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the + // step size. Then we compute the start of the first step (the one where value 0 starts) + // by adding half-a-step to the unigramFrequency. From there, we compute the integer + // number of steps to the bigramFrequency. One last thing: we want our steps to include + // their lower bound and exclude their higher bound so we need to have the first step + // start at exactly 1 unit higher than floor(unigramFreq + half a step). + // Note : to reconstruct the score, the dictionary reader will need to divide + // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step, + // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best + // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the + // step pointed by the discretized frequency. + final float stepSize = + (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) + / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); + final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f); + final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize); + // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1 + // here. The best approximation would be the unigram freq itself, so we should not + // include this bigram in the dictionary. For now, register as 0, and live with the + // small over-estimation that we get in this case. TODO: actually remove this bigram + // if discretizedFrequency < 0. + return discretizedFrequency > 0 ? discretizedFrequency : 0; + } + + /* package */ static int getChildrenPosition(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + int positionOfChildrenPosField = ptNode.mCachedAddressAfterUpdate + + getNodeHeaderSize(ptNode, codePointToOneByteCodeMap); + if (ptNode.isTerminal()) { + // A terminal node has the frequency. + // If positionOfChildrenPosField is incorrect, we may crash when jumping to the children + // position. + positionOfChildrenPosField += FormatSpec.PTNODE_FREQUENCY_SIZE; + } + return null == ptNode.mChildren ? FormatSpec.NO_CHILDREN_ADDRESS + : ptNode.mChildren.mCachedAddressAfterUpdate - positionOfChildrenPosField; + } + + /** + * Write a PtNodeArray. The PtNodeArray is expected to have its final position cached. + * + * @param dict the dictionary the node array is a part of (for relative offsets). + * @param dictEncoder the dictionary encoder. + * @param ptNodeArray the node array to write. + * @param codePointToOneByteCodeMap the map to convert the code points. + */ + /* package */ static void writePlacedPtNodeArray(final FusionDictionary dict, + final DictEncoder dictEncoder, final PtNodeArray ptNodeArray, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + // TODO: Make the code in common with BinaryDictIOUtils#writePtNode + dictEncoder.setPosition(ptNodeArray.mCachedAddressAfterUpdate); + + final int ptNodeCount = ptNodeArray.mData.size(); + dictEncoder.writePtNodeCount(ptNodeCount); + for (int i = 0; i < ptNodeCount; ++i) { + final PtNode ptNode = ptNodeArray.mData.get(i); + if (dictEncoder.getPosition() != ptNode.mCachedAddressAfterUpdate) { + throw new RuntimeException("Bug: write index is not the same as the cached address " + + "of the node : " + dictEncoder.getPosition() + " <> " + + ptNode.mCachedAddressAfterUpdate); + } + // Validity checks. + if (DBG && ptNode.getProbability() > FormatSpec.MAX_TERMINAL_FREQUENCY) { + throw new RuntimeException("A node has a frequency > " + + FormatSpec.MAX_TERMINAL_FREQUENCY + + " : " + ptNode.mProbabilityInfo.toString()); + } + dictEncoder.writePtNode(ptNode, dict, codePointToOneByteCodeMap); + } + if (dictEncoder.getPosition() != ptNodeArray.mCachedAddressAfterUpdate + + ptNodeArray.mCachedSize) { + throw new RuntimeException("Not the same size : written " + + (dictEncoder.getPosition() - ptNodeArray.mCachedAddressAfterUpdate) + + " bytes from a node that should have " + ptNodeArray.mCachedSize + " bytes"); + } + } + + /** + * Dumps a collection of useful statistics about a list of PtNode arrays. + * + * This prints purely informative stuff, like the total estimated file size, the + * number of PtNode arrays, of PtNodes, the repartition of each address size, etc + * + * @param ptNodeArrays the list of PtNode arrays. + */ + /* package */ static void showStatistics(ArrayList<PtNodeArray> ptNodeArrays) { + int firstTerminalAddress = Integer.MAX_VALUE; + int lastTerminalAddress = Integer.MIN_VALUE; + int size = 0; + int ptNodes = 0; + int maxNodes = 0; + int maxRuns = 0; + for (final PtNodeArray ptNodeArray : ptNodeArrays) { + if (maxNodes < ptNodeArray.mData.size()) maxNodes = ptNodeArray.mData.size(); + for (final PtNode ptNode : ptNodeArray.mData) { + ++ptNodes; + if (ptNode.mChars.length > maxRuns) maxRuns = ptNode.mChars.length; + if (ptNode.isTerminal()) { + if (ptNodeArray.mCachedAddressAfterUpdate < firstTerminalAddress) + firstTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; + if (ptNodeArray.mCachedAddressAfterUpdate > lastTerminalAddress) + lastTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; + } + } + if (ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize > size) { + size = ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize; + } + } + final int[] ptNodeCounts = new int[maxNodes + 1]; + final int[] runCounts = new int[maxRuns + 1]; + for (final PtNodeArray ptNodeArray : ptNodeArrays) { + ++ptNodeCounts[ptNodeArray.mData.size()]; + for (final PtNode ptNode : ptNodeArray.mData) { + ++runCounts[ptNode.mChars.length]; + } + } + + MakedictLog.i("Statistics:\n" + + " Total file size " + size + "\n" + + " " + ptNodeArrays.size() + " node arrays\n" + + " " + ptNodes + " PtNodes (" + ((float)ptNodes / ptNodeArrays.size()) + + " PtNodes per node)\n" + + " First terminal at " + firstTerminalAddress + "\n" + + " Last terminal at " + lastTerminalAddress + "\n" + + " PtNode stats : max = " + maxNodes); + } + + /** + * Writes a file header to an output stream. + * + * @param destination the stream to write the file header to. + * @param dict the dictionary to write. + * @param formatOptions file format options. + * @param codePointOccurrenceArray code points ordered by occurrence count. + * @return the size of the header. + */ + /* package */ static int writeDictionaryHeader(final OutputStream destination, + final FusionDictionary dict, final FormatOptions formatOptions, + final ArrayList<Entry<Integer, Integer>> codePointOccurrenceArray) + throws IOException, UnsupportedFormatException { + final int version = formatOptions.mVersion; + if ((version >= FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION && + version <= FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION) || ( + version >= FormatSpec.MINIMUM_SUPPORTED_DYNAMIC_VERSION && + version <= FormatSpec.MAXIMUM_SUPPORTED_DYNAMIC_VERSION)) { + // Dictionary is valid + } else { + throw new UnsupportedFormatException("Requested file format version " + version + + ", but this implementation only supports static versions " + + FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION + " through " + + FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION + " and dynamic versions " + + FormatSpec.MINIMUM_SUPPORTED_DYNAMIC_VERSION + " through " + + FormatSpec.MAXIMUM_SUPPORTED_DYNAMIC_VERSION); + } + + ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256); + + // The magic number in big-endian order. + // Magic number for all versions. + headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 24))); + headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 16))); + headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 8))); + headerBuffer.write((byte) (0xFF & FormatSpec.MAGIC_NUMBER)); + // Dictionary version. + headerBuffer.write((byte) (0xFF & (version >> 8))); + headerBuffer.write((byte) (0xFF & version)); + + // Options flags + // TODO: Remove this field. + final int options = 0; + headerBuffer.write((byte) (0xFF & (options >> 8))); + headerBuffer.write((byte) (0xFF & options)); + final int headerSizeOffset = headerBuffer.size(); + // Placeholder to be written later with header size. + for (int i = 0; i < 4; ++i) { + headerBuffer.write(0); + } + // Write out the options. + for (final String key : dict.mOptions.mAttributes.keySet()) { + final String value = dict.mOptions.mAttributes.get(key); + CharEncoding.writeString(headerBuffer, key, null); + CharEncoding.writeString(headerBuffer, value, null); + } + // Write out the codePointTable if there is codePointOccurrenceArray. + if (codePointOccurrenceArray != null) { + final String codePointTableString = + encodeCodePointTable(codePointOccurrenceArray); + CharEncoding.writeString(headerBuffer, DictionaryHeader.CODE_POINT_TABLE_KEY, null); + CharEncoding.writeString(headerBuffer, codePointTableString, null); + } + final int size = headerBuffer.size(); + final byte[] bytes = headerBuffer.toByteArray(); + // Write out the header size. + bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24)); + bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16)); + bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8)); + bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0)); + destination.write(bytes); + + headerBuffer.close(); + return size; + } + + static final class CodePointTable { + final HashMap<Integer, Integer> mCodePointToOneByteCodeMap; + final ArrayList<Entry<Integer, Integer>> mCodePointOccurrenceArray; + + // Let code point table empty for version 200 dictionary which used in test + CodePointTable() { + mCodePointToOneByteCodeMap = null; + mCodePointOccurrenceArray = null; + } + + CodePointTable(final HashMap<Integer, Integer> codePointToOneByteCodeMap, + final ArrayList<Entry<Integer, Integer>> codePointOccurrenceArray) { + mCodePointToOneByteCodeMap = codePointToOneByteCodeMap; + mCodePointOccurrenceArray = codePointOccurrenceArray; + } + } + + private static String encodeCodePointTable( + final ArrayList<Entry<Integer, Integer>> codePointOccurrenceArray) { + final StringBuilder codePointTableString = new StringBuilder(); + int currentCodePointTableIndex = FormatSpec.MINIMAL_ONE_BYTE_CHARACTER_VALUE; + for (final Entry<Integer, Integer> entry : codePointOccurrenceArray) { + // Native reads the table as a string + codePointTableString.appendCodePoint(entry.getKey()); + if (FormatSpec.MAXIMAL_ONE_BYTE_CHARACTER_VALUE < ++currentCodePointTableIndex) { + break; + } + } + return codePointTableString.toString(); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictIOUtils.java b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictIOUtils.java new file mode 100644 index 000000000..055d6492d --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.define.DecoderSpecificConstants; +import org.kelar.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Map; +import java.util.Stack; + +public final class BinaryDictIOUtils { + private static final boolean DBG = false; + + private BinaryDictIOUtils() { + // This utility class is not publicly instantiable. + } + + /** + * Returns new dictionary decoder. + * + * @param dictFile the dictionary file. + * @param bufferType The type of buffer, as one of USE_* in DictDecoder. + * @return new dictionary decoder if the dictionary file exists, otherwise null. + */ + public static DictDecoder getDictDecoder(final File dictFile, final long offset, + final long length, final int bufferType) { + return new Ver4DictDecoder(dictFile); + } + + public static DictDecoder getDictDecoder(final File dictFile, final long offset, + final long length, final DictionaryBufferFactory factory) { + return new Ver4DictDecoder(dictFile); + } + + public static DictDecoder getDictDecoder(final File dictFile, final long offset, + final long length) { + return getDictDecoder(dictFile, offset, length, DictDecoder.USE_READONLY_BYTEBUFFER); + } + + private static final class Position { + public static final int NOT_READ_PTNODE_COUNT = -1; + + public int mAddress; + public int mNumOfPtNode; + public int mPosition; + public int mLength; + + public Position(int address, int length) { + mAddress = address; + mLength = length; + mNumOfPtNode = NOT_READ_PTNODE_COUNT; + } + } + + /** + * Retrieves all node arrays without recursive call. + */ + private static void readUnigramsAndBigramsBinaryInner(final DictDecoder dictDecoder, + final int bodyOffset, final Map<Integer, String> words, + final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams) { + int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1]; + + Stack<Position> stack = new Stack<>(); + int index = 0; + + Position initPos = new Position(bodyOffset, 0); + stack.push(initPos); + + while (!stack.empty()) { + Position p = stack.peek(); + + if (DBG) { + MakedictLog.d("read: address=" + p.mAddress + ", numOfPtNode=" + + p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength); + } + + if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress); + if (index != p.mLength) index = p.mLength; + + if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) { + p.mNumOfPtNode = dictDecoder.readPtNodeCount(); + p.mAddress = dictDecoder.getPosition(); + p.mPosition = 0; + } + if (p.mNumOfPtNode == 0) { + stack.pop(); + continue; + } + final PtNodeInfo ptNodeInfo = dictDecoder.readPtNode(p.mAddress); + for (int i = 0; i < ptNodeInfo.mCharacters.length; ++i) { + pushedChars[index++] = ptNodeInfo.mCharacters[i]; + } + p.mPosition++; + if (ptNodeInfo.isTerminal()) {// found word + words.put(ptNodeInfo.mOriginalAddress, new String(pushedChars, 0, index)); + frequencies.put( + ptNodeInfo.mOriginalAddress, ptNodeInfo.mProbabilityInfo.mProbability); + if (ptNodeInfo.mBigrams != null) { + bigrams.put(ptNodeInfo.mOriginalAddress, ptNodeInfo.mBigrams); + } + } + + if (p.mPosition == p.mNumOfPtNode) { + stack.pop(); + } else { + // The PtNode array has more PtNodes. + p.mAddress = dictDecoder.getPosition(); + } + + if (hasChildrenAddress(ptNodeInfo.mChildrenAddress)) { + final Position childrenPos = new Position(ptNodeInfo.mChildrenAddress, index); + stack.push(childrenPos); + } + } + } + + /** + * Reads unigrams and bigrams from the binary file. + * Doesn't store a full memory representation of the dictionary. + * + * @param dictDecoder the dict decoder. + * @param words the map to store the address as a key and the word as a value. + * @param frequencies the map to store the address as a key and the frequency as a value. + * @param bigrams the map to store the address as a key and the list of address as a value. + * @throws IOException if the file can't be read. + * @throws UnsupportedFormatException if the format of the file is not recognized. + */ + /* package */ static void readUnigramsAndBigramsBinary(final DictDecoder dictDecoder, + final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, + UnsupportedFormatException { + // Read header + final DictionaryHeader header = dictDecoder.readHeader(); + readUnigramsAndBigramsBinaryInner(dictDecoder, header.mBodyOffset, words, + frequencies, bigrams); + } + + /** + * Gets the address of the last PtNode of the exact matching word in the dictionary. + * If no match is found, returns NOT_VALID_WORD. + * + * @param dictDecoder the dict decoder. + * @param word the word we search for. + * @return the address of the terminal node. + * @throws IOException if the file can't be read. + * @throws UnsupportedFormatException if the format of the file is not recognized. + */ + @UsedForTesting + /* package */ static int getTerminalPosition(final DictDecoder dictDecoder, + final String word) throws IOException, UnsupportedFormatException { + if (word == null) return FormatSpec.NOT_VALID_WORD; + dictDecoder.setPosition(0); + dictDecoder.readHeader(); + int wordPos = 0; + final int wordLen = word.codePointCount(0, word.length()); + for (int depth = 0; depth < DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { + if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD; + + do { + final int ptNodeCount = dictDecoder.readPtNodeCount(); + boolean foundNextPtNode = false; + for (int i = 0; i < ptNodeCount; ++i) { + final int ptNodePos = dictDecoder.getPosition(); + final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos); + boolean same = true; + for (int p = 0, j = word.offsetByCodePoints(0, wordPos); + p < currentInfo.mCharacters.length; + ++p, j = word.offsetByCodePoints(j, 1)) { + if (wordPos + p >= wordLen + || word.codePointAt(j) != currentInfo.mCharacters[p]) { + same = false; + break; + } + } + + if (same) { + // found the PtNode matches the word. + if (wordPos + currentInfo.mCharacters.length == wordLen) { + return currentInfo.isTerminal() ? ptNodePos : FormatSpec.NOT_VALID_WORD; + } + wordPos += currentInfo.mCharacters.length; + if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { + return FormatSpec.NOT_VALID_WORD; + } + foundNextPtNode = true; + dictDecoder.setPosition(currentInfo.mChildrenAddress); + break; + } + } + if (foundNextPtNode) break; + return FormatSpec.NOT_VALID_WORD; + } while(true); + } + return FormatSpec.NOT_VALID_WORD; + } + + /** + * Writes a PtNodeCount to the stream. + * + * @param destination the stream to write. + * @param ptNodeCount the count. + * @return the size written in bytes. + */ + @UsedForTesting + static int writePtNodeCount(final OutputStream destination, final int ptNodeCount) + throws IOException { + final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); + // the count must fit on one byte or two bytes. + // Please see comments in FormatSpec. + if (countSize != 1 && countSize != 2) { + throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize); + } + final int encodedPtNodeCount = (countSize == 2) ? + (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; + BinaryDictEncoderUtils.writeUIntToStream(destination, encodedPtNodeCount, countSize); + return countSize; + } + + /** + * Helper method to hide the actual value of the no children address. + */ + public static boolean hasChildrenAddress(final int address) { + return FormatSpec.NO_CHILDREN_ADDRESS != address; + } + + /** + * Compute the binary size of the node count + * @param count the node count + * @return the size of the node count, either 1 or 2 bytes. + */ + public static int getPtNodeCountSize(final int count) { + if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= count) { + return 1; + } else if (FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY >= count) { + return 2; + } else { + throw new RuntimeException("Can't have more than " + + FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY + " PtNode in a PtNodeArray (found " + + count + ")"); + } + } + + static int getChildrenAddressSize(final int optionFlags) { + switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { + case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: + return 1; + case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: + return 2; + case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: + return 3; + case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: + default: + return 0; + } + } + + /** + * Calculate bigram frequency from compressed value + * + * @param unigramFrequency + * @param bigramFrequency compressed frequency + * @return approximate bigram frequency + */ + @UsedForTesting + public static int reconstructBigramFrequency(final int unigramFrequency, + final int bigramFrequency) { + final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) + / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); + final float resultFreqFloat = unigramFrequency + stepSize * (bigramFrequency + 1.0f); + return (int)resultFreqFloat; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictUtils.java b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictUtils.java new file mode 100644 index 000000000..48e3d95f2 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/BinaryDictUtils.java @@ -0,0 +1,80 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; + +import java.io.File; +import java.util.HashMap; + +public class BinaryDictUtils { + public static final int USE_BYTE_ARRAY = 1; + public static final int USE_BYTE_BUFFER = 2; + + public static final String TEST_DICT_FILE_EXTENSION = ".testDict"; + + public static final FormatSpec.FormatOptions STATIC_OPTIONS = + new FormatSpec.FormatOptions(FormatSpec.VERSION202); + public static final FormatSpec.FormatOptions DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP = + new FormatSpec.FormatOptions(FormatSpec.VERSION4, false /* hasTimestamp */); + public static final FormatSpec.FormatOptions DYNAMIC_OPTIONS_WITH_TIMESTAMP = + new FormatSpec.FormatOptions(FormatSpec.VERSION4, true /* hasTimestamp */); + + public static DictionaryOptions makeDictionaryOptions(final String id, final String version, + final FormatSpec.FormatOptions formatOptions) { + final DictionaryOptions options = new DictionaryOptions(new HashMap<String, String>()); + options.mAttributes.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, "en_US"); + options.mAttributes.put(DictionaryHeader.DICTIONARY_ID_KEY, id); + options.mAttributes.put(DictionaryHeader.DICTIONARY_VERSION_KEY, version); + if (formatOptions.mHasTimestamp) { + options.mAttributes.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + options.mAttributes.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + } + return options; + } + + public static File getDictFile(final String name, final String version, + final FormatOptions formatOptions, final File directory) { + if (formatOptions.mVersion == FormatSpec.VERSION2 + || formatOptions.mVersion == FormatSpec.VERSION201 + || formatOptions.mVersion == FormatSpec.VERSION202) { + return new File(directory, name + "." + version + TEST_DICT_FILE_EXTENSION); + } else if (formatOptions.mVersion == FormatSpec.VERSION4) { + return new File(directory, name + "." + version); + } else { + throw new RuntimeException("the format option has a wrong version : " + + formatOptions.mVersion); + } + } + + public static DictEncoder getDictEncoder(final File file, final FormatOptions formatOptions) { + if (formatOptions.mVersion == FormatSpec.VERSION4) { + if (!file.isDirectory()) { + file.mkdir(); + } + return new Ver4DictEncoder(file); + } else if (formatOptions.mVersion == FormatSpec.VERSION202) { + return new Ver2DictEncoder(file, Ver2DictEncoder.CODE_POINT_TABLE_OFF); + } else { + throw new RuntimeException("The format option has a wrong version : " + + formatOptions.mVersion); + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/DictDecoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/DictDecoder.java new file mode 100644 index 000000000..97fb3596b --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/DictDecoder.java @@ -0,0 +1,222 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; +import org.kelar.inputmethod.latin.utils.ByteArrayDictBuffer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.TreeMap; + +/** + * An interface of binary dictionary decoders. + */ +// TODO: Straighten out responsibility for the buffer's file pointer. +public interface DictDecoder { + + /** + * Reads and returns the file header. + */ + public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException; + + /** + * Reads PtNode from ptNodePos. + * @param ptNodePos the position of PtNode. + * @return PtNodeInfo. + */ + public PtNodeInfo readPtNode(final int ptNodePos); + + /** + * Reads a buffer and returns the memory representation of the dictionary. + * + * This high-level method takes a buffer and reads its contents, populating a + * FusionDictionary structure. + * + * @param deleteDictIfBroken a flag indicating whether this method should remove the broken + * dictionary or not. + * @return the created dictionary. + */ + @UsedForTesting + public FusionDictionary readDictionaryBinary(final boolean deleteDictIfBroken) + throws FileNotFoundException, IOException, UnsupportedFormatException; + + /** + * Gets the address of the last PtNode of the exact matching word in the dictionary. + * If no match is found, returns NOT_VALID_WORD. + * + * @param word the word we search for. + * @return the address of the terminal node. + * @throws IOException if the file can't be read. + * @throws UnsupportedFormatException if the format of the file is not recognized. + */ + @UsedForTesting + public int getTerminalPosition(final String word) + throws IOException, UnsupportedFormatException; + + /** + * Reads unigrams and bigrams from the binary file. + * Doesn't store a full memory representation of the dictionary. + * + * @param words the map to store the address as a key and the word as a value. + * @param frequencies the map to store the address as a key and the frequency as a value. + * @param bigrams the map to store the address as a key and the list of address as a value. + * @throws IOException if the file can't be read. + * @throws UnsupportedFormatException if the format of the file is not recognized. + */ + @UsedForTesting + public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, + final TreeMap<Integer, Integer> frequencies, + final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) + throws IOException, UnsupportedFormatException; + + /** + * Sets the position of the buffer to the given value. + * + * @param newPos the new position + */ + public void setPosition(final int newPos); + + /** + * Gets the position of the buffer. + * + * @return the position + */ + public int getPosition(); + + /** + * Reads and returns the PtNode count out of a buffer and forwards the pointer. + */ + public int readPtNodeCount(); + + /** + * Opens the dictionary file and makes DictBuffer. + */ + @UsedForTesting + public void openDictBuffer() throws FileNotFoundException, IOException, + UnsupportedFormatException; + @UsedForTesting + public boolean isDictBufferOpen(); + + // Constants for DictionaryBufferFactory. + public static final int USE_READONLY_BYTEBUFFER = 0x01000000; + public static final int USE_BYTEARRAY = 0x02000000; + public static final int USE_WRITABLE_BYTEBUFFER = 0x03000000; + public static final int MASK_DICTBUFFER = 0x0F000000; + + public interface DictionaryBufferFactory { + public DictBuffer getDictionaryBuffer(final File file) + throws FileNotFoundException, IOException; + } + + /** + * Creates DictionaryBuffer using a ByteBuffer + * + * This class uses less memory than DictionaryBufferFromByteArrayFactory, + * but doesn't perform as fast. + * When operating on a big dictionary, this class is preferred. + */ + public static final class DictionaryBufferFromReadOnlyByteBufferFactory + implements DictionaryBufferFactory { + @Override + public DictBuffer getDictionaryBuffer(final File file) + throws FileNotFoundException, IOException { + FileInputStream inStream = null; + ByteBuffer buffer = null; + try { + inStream = new FileInputStream(file); + buffer = inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, + 0, file.length()); + } finally { + if (inStream != null) { + inStream.close(); + } + } + if (buffer != null) { + return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); + } + return null; + } + } + + /** + * Creates DictionaryBuffer using a byte array + * + * This class performs faster than other classes, but consumes more memory. + * When operating on a small dictionary, this class is preferred. + */ + public static final class DictionaryBufferFromByteArrayFactory + implements DictionaryBufferFactory { + @Override + public DictBuffer getDictionaryBuffer(final File file) + throws FileNotFoundException, IOException { + FileInputStream inStream = null; + try { + inStream = new FileInputStream(file); + final byte[] array = new byte[(int) file.length()]; + inStream.read(array); + return new ByteArrayDictBuffer(array); + } finally { + if (inStream != null) { + inStream.close(); + } + } + } + } + + /** + * Creates DictionaryBuffer using a writable ByteBuffer and a RandomAccessFile. + * + * This class doesn't perform as fast as other classes, + * but this class is the only option available for destructive operations (insert or delete) + * on a dictionary. + */ + @UsedForTesting + public static final class DictionaryBufferFromWritableByteBufferFactory + implements DictionaryBufferFactory { + @Override + public DictBuffer getDictionaryBuffer(final File file) + throws FileNotFoundException, IOException { + RandomAccessFile raFile = null; + ByteBuffer buffer = null; + try { + raFile = new RandomAccessFile(file, "rw"); + buffer = raFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length()); + } finally { + if (raFile != null) { + raFile.close(); + } + } + if (buffer != null) { + return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); + } + return null; + } + } + + /** + * @return whether this decoder has a valid binary dictionary that it can decode. + */ + public boolean hasValidRawBinaryDictionary(); +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/DictEncoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/DictEncoder.java new file mode 100644 index 000000000..d637b6d2a --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/DictEncoder.java @@ -0,0 +1,39 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNode; + +import java.io.IOException; +import java.util.HashMap; + +/** + * An interface of binary dictionary encoder. + */ +public interface DictEncoder { + @UsedForTesting + public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) + throws IOException, UnsupportedFormatException; + + public void setPosition(final int position); + public int getPosition(); + public void writePtNodeCount(final int ptNodeCount); + public void writePtNode(final PtNode ptNode, final FusionDictionary dict, + final HashMap<Integer, Integer> codePointToOneByteCodeMap); +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/FusionDictionary.java b/tests/src/org/kelar/inputmethod/latin/makedict/FusionDictionary.java new file mode 100644 index 000000000..817d69f99 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/FusionDictionary.java @@ -0,0 +1,646 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.define.DecoderSpecificConstants; +import org.kelar.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * A dictionary that can fusion heads and tails of words for more compression. + */ +@UsedForTesting +public final class FusionDictionary implements Iterable<WordProperty> { + private static final boolean DBG = MakedictLog.DBG; + + private static int CHARACTER_NOT_FOUND_INDEX = -1; + + /** + * A node array of the dictionary, containing several PtNodes. + * + * A PtNodeArray is but an ordered array of PtNodes, which essentially contain all the + * real information. + * This class also contains fields to cache size and address, to help with binary + * generation. + */ + public static final class PtNodeArray { + ArrayList<PtNode> mData; + // To help with binary generation + int mCachedSize = Integer.MIN_VALUE; + // mCachedAddressBefore/AfterUpdate are helpers for binary dictionary generation. They + // always hold the same value except between dictionary address compression, during which + // the update process needs to know about both values at the same time. Updating will + // update the AfterUpdate value, and the code will move them to BeforeUpdate before + // the next update pass. + int mCachedAddressBeforeUpdate = Integer.MIN_VALUE; + int mCachedAddressAfterUpdate = Integer.MIN_VALUE; + int mCachedParentAddress = 0; + + public PtNodeArray() { + mData = new ArrayList<>(); + } + public PtNodeArray(ArrayList<PtNode> data) { + Collections.sort(data, PTNODE_COMPARATOR); + mData = data; + } + } + + /** + * PtNode is a group of characters, with probability information, shortcut targets, bigrams, + * and children (Pt means Patricia Trie). + * + * This is the central class of the in-memory representation. A PtNode is what can + * be seen as a traditional "trie node", except it can hold several characters at the + * same time. A PtNode essentially represents one or several characters in the middle + * of the trie tree; as such, it can be a terminal, and it can have children. + * In this in-memory representation, whether the PtNode is a terminal or not is represented + * by mProbabilityInfo. The PtNode is a terminal when the mProbabilityInfo is not null and the + * PtNode is not a terminal when the mProbabilityInfo is null. A terminal may have non-null + * shortcuts and/or bigrams, but a non-terminal may not. Moreover, children, if present, + * are non-null. + */ + public static final class PtNode { + private static final int NOT_A_TERMINAL = -1; + final int mChars[]; + ArrayList<WeightedString> mBigrams; + // null == mProbabilityInfo indicates this is not a terminal. + ProbabilityInfo mProbabilityInfo; + int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal. + PtNodeArray mChildren; + boolean mIsNotAWord; // Only a shortcut + boolean mIsPossiblyOffensive; + // mCachedSize and mCachedAddressBefore/AfterUpdate are helpers for binary dictionary + // generation. Before and After always hold the same value except during dictionary + // address compression, where the update process needs to know about both values at the + // same time. Updating will update the AfterUpdate value, and the code will move them + // to BeforeUpdate before the next update pass. + // The update process does not need two versions of mCachedSize. + int mCachedSize; // The size, in bytes, of this PtNode. + int mCachedAddressBeforeUpdate; // The address of this PtNode (before update) + int mCachedAddressAfterUpdate; // The address of this PtNode (after update) + + public PtNode(final int[] chars, final ArrayList<WeightedString> bigrams, + final ProbabilityInfo probabilityInfo, final boolean isNotAWord, + final boolean isPossiblyOffensive) { + mChars = chars; + mProbabilityInfo = probabilityInfo; + mTerminalId = probabilityInfo == null ? NOT_A_TERMINAL : probabilityInfo.mProbability; + mBigrams = bigrams; + mChildren = null; + mIsNotAWord = isNotAWord; + mIsPossiblyOffensive = isPossiblyOffensive; + } + + public PtNode(final int[] chars, final ArrayList<WeightedString> bigrams, + final ProbabilityInfo probabilityInfo, final boolean isNotAWord, + final boolean isPossiblyOffensive, final PtNodeArray children) { + mChars = chars; + mProbabilityInfo = probabilityInfo; + mBigrams = bigrams; + mChildren = children; + mIsNotAWord = isNotAWord; + mIsPossiblyOffensive = isPossiblyOffensive; + } + + public void addChild(PtNode n) { + if (null == mChildren) { + mChildren = new PtNodeArray(); + } + mChildren.mData.add(n); + } + + public int getTerminalId() { + return mTerminalId; + } + + public boolean isTerminal() { + return mProbabilityInfo != null; + } + + public int getProbability() { + return isTerminal() ? mProbabilityInfo.mProbability : NOT_A_TERMINAL; + } + + public boolean getIsNotAWord() { + return mIsNotAWord; + } + + public boolean getIsPossiblyOffensive() { + return mIsPossiblyOffensive; + } + + public ArrayList<WeightedString> getBigrams() { + // We don't want write permission to escape outside the package, so we return a copy + if (null == mBigrams) return null; + final ArrayList<WeightedString> copyOfBigrams = new ArrayList<>(mBigrams); + return copyOfBigrams; + } + + public boolean hasSeveralChars() { + assert(mChars.length > 0); + return 1 < mChars.length; + } + + /** + * Adds a word to the bigram list. Updates the probability information if the word already + * exists. + */ + public void addBigram(final String word, final ProbabilityInfo probabilityInfo) { + if (mBigrams == null) { + mBigrams = new ArrayList<>(); + } + WeightedString bigram = getBigram(word); + if (bigram != null) { + bigram.mProbabilityInfo = probabilityInfo; + } else { + bigram = new WeightedString(word, probabilityInfo); + mBigrams.add(bigram); + } + } + + /** + * Gets the bigram for the given word. + * Returns null if the word is not in the bigrams list. + */ + public WeightedString getBigram(final String word) { + // TODO: Don't do a linear search + if (mBigrams != null) { + final int size = mBigrams.size(); + for (int i = 0; i < size; ++i) { + WeightedString bigram = mBigrams.get(i); + if (bigram.mWord.equals(word)) { + return bigram; + } + } + } + return null; + } + + /** + * Updates the PtNode with the given properties. Adds the shortcut and bigram lists to + * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only + * updated if they are higher than the existing ones. + */ + void update(final ProbabilityInfo probabilityInfo, + final ArrayList<WeightedString> bigrams, + final boolean isNotAWord, final boolean isPossiblyOffensive) { + mProbabilityInfo = ProbabilityInfo.max(mProbabilityInfo, probabilityInfo); + if (bigrams != null) { + if (mBigrams == null) { + mBigrams = bigrams; + } else { + final int size = bigrams.size(); + for (int i = 0; i < size; ++i) { + final WeightedString bigram = bigrams.get(i); + final WeightedString existingBigram = getBigram(bigram.mWord); + if (existingBigram == null) { + mBigrams.add(bigram); + } else { + existingBigram.mProbabilityInfo = ProbabilityInfo.max( + existingBigram.mProbabilityInfo, bigram.mProbabilityInfo); + } + } + } + } + mIsNotAWord = isNotAWord; + mIsPossiblyOffensive = isPossiblyOffensive; + } + } + + public final DictionaryOptions mOptions; + public final PtNodeArray mRootNodeArray; + + public FusionDictionary(final PtNodeArray rootNodeArray, final DictionaryOptions options) { + mRootNodeArray = rootNodeArray; + mOptions = options; + } + + public void addOptionAttribute(final String key, final String value) { + mOptions.mAttributes.put(key, value); + } + + /** + * Helper method to convert a String to an int array. + */ + static int[] getCodePoints(final String word) { + // TODO: this is a copy-paste of the old contents of StringUtils.toCodePointArray, + // which is not visible from the makedict package. Factor this code. + final int length = word.length(); + if (length <= 0) return new int[] {}; + final char[] characters = word.toCharArray(); + final int[] codePoints = new int[Character.codePointCount(characters, 0, length)]; + int codePoint = Character.codePointAt(characters, 0); + int dsti = 0; + for (int srci = Character.charCount(codePoint); + srci < length; srci += Character.charCount(codePoint), ++dsti) { + codePoints[dsti] = codePoint; + codePoint = Character.codePointAt(characters, srci); + } + codePoints[dsti] = codePoint; + return codePoints; + } + + /** + * Helper method to add a word as a string. + * + * This method adds a word to the dictionary with the given frequency. Optional + * lists of bigrams can be passed here. For each word inside, + * they will be added to the dictionary as necessary. + * @param word the word to add. + * @param probabilityInfo probability information of the word. + * @param isNotAWord true if this should not be considered a word (e.g. shortcut only) + * @param isPossiblyOffensive true if this word is possibly offensive + */ + public void add(final String word, final ProbabilityInfo probabilityInfo, + final boolean isNotAWord, final boolean isPossiblyOffensive) { + add(getCodePoints(word), probabilityInfo, isNotAWord, isPossiblyOffensive); + } + + /** + * Validity check for a PtNode array. + * + * This method checks that all PtNodes in a node array are ordered as expected. + * If they are, nothing happens. If they aren't, an exception is thrown. + */ + private static void checkStack(PtNodeArray ptNodeArray) { + ArrayList<PtNode> stack = ptNodeArray.mData; + int lastValue = -1; + for (int i = 0; i < stack.size(); ++i) { + int currentValue = stack.get(i).mChars[0]; + if (currentValue <= lastValue) { + throw new RuntimeException("Invalid stack"); + } + lastValue = currentValue; + } + } + + /** + * Helper method to add a new bigram to the dictionary. + * + * @param word0 the previous word of the context + * @param word1 the next word of the context + * @param probabilityInfo the bigram probability info + */ + public void setBigram(final String word0, final String word1, + final ProbabilityInfo probabilityInfo) { + PtNode ptNode0 = findWordInTree(mRootNodeArray, word0); + if (ptNode0 != null) { + final PtNode ptNode1 = findWordInTree(mRootNodeArray, word1); + if (ptNode1 == null) { + add(getCodePoints(word1), new ProbabilityInfo(0), false /* isNotAWord */, + false /* isPossiblyOffensive */); + // The PtNode for the first word may have moved by the above insertion, + // if word1 and word2 share a common stem that happens not to have been + // a cutting point until now. In this case, we need to refresh ptNode. + ptNode0 = findWordInTree(mRootNodeArray, word0); + } + ptNode0.addBigram(word1, probabilityInfo); + } else { + throw new RuntimeException("First word of bigram not found " + word0); + } + } + + /** + * Add a word to this dictionary. + * + * The shortcuts, if any, have to be in the dictionary already. If they aren't, + * an exception is thrown. + * @param word the word, as an int array. + * @param probabilityInfo the probability information of the word. + * @param isNotAWord true if this is not a word for spellchecking purposes (shortcut only or so) + * @param isPossiblyOffensive true if this word is possibly offensive + */ + private void add(final int[] word, final ProbabilityInfo probabilityInfo, + final boolean isNotAWord, final boolean isPossiblyOffensive) { + assert(probabilityInfo.mProbability <= FormatSpec.MAX_TERMINAL_FREQUENCY); + if (word.length >= DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH) { + MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length); + return; + } + + PtNodeArray currentNodeArray = mRootNodeArray; + int charIndex = 0; + + PtNode currentPtNode = null; + int differentCharIndex = 0; // Set by the loop to the index of the char that differs + int nodeIndex = findIndexOfChar(mRootNodeArray, word[charIndex]); + while (CHARACTER_NOT_FOUND_INDEX != nodeIndex) { + currentPtNode = currentNodeArray.mData.get(nodeIndex); + differentCharIndex = compareCharArrays(currentPtNode.mChars, word, charIndex); + if (ARRAYS_ARE_EQUAL != differentCharIndex + && differentCharIndex < currentPtNode.mChars.length) break; + if (null == currentPtNode.mChildren) break; + charIndex += currentPtNode.mChars.length; + if (charIndex >= word.length) break; + currentNodeArray = currentPtNode.mChildren; + nodeIndex = findIndexOfChar(currentNodeArray, word[charIndex]); + } + + if (CHARACTER_NOT_FOUND_INDEX == nodeIndex) { + // No node at this point to accept the word. Create one. + final int insertionIndex = findInsertionIndex(currentNodeArray, word[charIndex]); + final PtNode newPtNode = new PtNode(Arrays.copyOfRange(word, charIndex, word.length), + null /* bigrams */, probabilityInfo, isNotAWord, + isPossiblyOffensive); + currentNodeArray.mData.add(insertionIndex, newPtNode); + if (DBG) checkStack(currentNodeArray); + } else { + // There is a word with a common prefix. + if (differentCharIndex == currentPtNode.mChars.length) { + if (charIndex + differentCharIndex >= word.length) { + // The new word is a prefix of an existing word, but the node on which it + // should end already exists as is. Since the old PtNode was not a terminal, + // make it one by filling in its frequency and other attributes + currentPtNode.update(probabilityInfo, null, isNotAWord, + isPossiblyOffensive); + } else { + // The new word matches the full old word and extends past it. + // We only have to create a new node and add it to the end of this. + final PtNode newNode = new PtNode( + Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length), + null /* bigrams */, probabilityInfo, + isNotAWord, isPossiblyOffensive); + currentPtNode.mChildren = new PtNodeArray(); + currentPtNode.mChildren.mData.add(newNode); + } + } else { + if (0 == differentCharIndex) { + // Exact same word. Update the frequency if higher. This will also add the + // new shortcuts to the existing shortcut list if it already exists. + currentPtNode.update(probabilityInfo, null, + currentPtNode.mIsNotAWord && isNotAWord, + currentPtNode.mIsPossiblyOffensive || isPossiblyOffensive); + } else { + // Partial prefix match only. We have to replace the current node with a node + // containing the current prefix and create two new ones for the tails. + PtNodeArray newChildren = new PtNodeArray(); + final PtNode newOldWord = new PtNode( + Arrays.copyOfRange(currentPtNode.mChars, differentCharIndex, + currentPtNode.mChars.length), + currentPtNode.mBigrams, currentPtNode.mProbabilityInfo, + currentPtNode.mIsNotAWord, currentPtNode.mIsPossiblyOffensive, + currentPtNode.mChildren); + newChildren.mData.add(newOldWord); + + final PtNode newParent; + if (charIndex + differentCharIndex >= word.length) { + newParent = new PtNode( + Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), + null /* bigrams */, probabilityInfo, + isNotAWord, isPossiblyOffensive, newChildren); + } else { + newParent = new PtNode( + Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), + null /* bigrams */, null /* probabilityInfo */, + false /* isNotAWord */, false /* isPossiblyOffensive */, + newChildren); + final PtNode newWord = new PtNode(Arrays.copyOfRange(word, + charIndex + differentCharIndex, word.length), + null /* bigrams */, probabilityInfo, + isNotAWord, isPossiblyOffensive); + final int addIndex = word[charIndex + differentCharIndex] + > currentPtNode.mChars[differentCharIndex] ? 1 : 0; + newChildren.mData.add(addIndex, newWord); + } + currentNodeArray.mData.set(nodeIndex, newParent); + } + if (DBG) checkStack(currentNodeArray); + } + } + } + + private static int ARRAYS_ARE_EQUAL = 0; + + /** + * Custom comparison of two int arrays taken to contain character codes. + * + * This method compares the two arrays passed as an argument in a lexicographic way, + * with an offset in the dst string. + * This method does NOT test for the first character. It is taken to be equal. + * I repeat: this method starts the comparison at 1 <> dstOffset + 1. + * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the + * strings are equal. This works BECAUSE we don't look at the first character. + * + * @param src the left-hand side string of the comparison. + * @param dst the right-hand side string of the comparison. + * @param dstOffset the offset in the right-hand side string. + * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't. + */ + private static int compareCharArrays(final int[] src, final int[] dst, int dstOffset) { + // We do NOT test the first char, because we come from a method that already + // tested it. + for (int i = 1; i < src.length; ++i) { + if (dstOffset + i >= dst.length) return i; + if (src[i] != dst[dstOffset + i]) return i; + } + if (dst.length > src.length) return src.length; + return ARRAYS_ARE_EQUAL; + } + + /** + * Helper class that compares and sorts two PtNodes according to their + * first element only. I repeat: ONLY the first element is considered, the rest + * is ignored. + * This comparator imposes orderings that are inconsistent with equals. + */ + static final class PtNodeComparator implements java.util.Comparator<PtNode> { + @Override + public int compare(PtNode p1, PtNode p2) { + if (p1.mChars[0] == p2.mChars[0]) return 0; + return p1.mChars[0] < p2.mChars[0] ? -1 : 1; + } + } + final static PtNodeComparator PTNODE_COMPARATOR = new PtNodeComparator(); + + /** + * Finds the insertion index of a character within a node array. + */ + private static int findInsertionIndex(final PtNodeArray nodeArray, int character) { + final ArrayList<PtNode> data = nodeArray.mData; + final PtNode reference = new PtNode(new int[] { character }, + null /* bigrams */, null /* probabilityInfo */, + false /* isNotAWord */, false /* isPossiblyOffensive */); + int result = Collections.binarySearch(data, reference, PTNODE_COMPARATOR); + return result >= 0 ? result : -result - 1; + } + + /** + * Find the index of a char in a node array, if it exists. + * + * @param nodeArray the node array to search in. + * @param character the character to search for. + * @return the position of the character if it's there, or CHARACTER_NOT_FOUND_INDEX = -1 else. + */ + private static int findIndexOfChar(final PtNodeArray nodeArray, int character) { + final int insertionIndex = findInsertionIndex(nodeArray, character); + if (nodeArray.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND_INDEX; + return character == nodeArray.mData.get(insertionIndex).mChars[0] ? insertionIndex + : CHARACTER_NOT_FOUND_INDEX; + } + + /** + * Helper method to find a word in a given branch. + */ + public static PtNode findWordInTree(final PtNodeArray rootNodeArray, final String string) { + PtNodeArray nodeArray = rootNodeArray; + int index = 0; + final StringBuilder checker = DBG ? new StringBuilder() : null; + final int[] codePoints = getCodePoints(string); + + PtNode currentPtNode; + do { + int indexOfGroup = findIndexOfChar(nodeArray, codePoints[index]); + if (CHARACTER_NOT_FOUND_INDEX == indexOfGroup) return null; + currentPtNode = nodeArray.mData.get(indexOfGroup); + + if (codePoints.length - index < currentPtNode.mChars.length) return null; + int newIndex = index; + while (newIndex < codePoints.length && newIndex - index < currentPtNode.mChars.length) { + if (currentPtNode.mChars[newIndex - index] != codePoints[newIndex]) return null; + newIndex++; + } + index = newIndex; + + if (DBG) { + checker.append(new String(currentPtNode.mChars, 0, currentPtNode.mChars.length)); + } + if (index < codePoints.length) { + nodeArray = currentPtNode.mChildren; + } + } while (null != nodeArray && index < codePoints.length); + + if (index < codePoints.length) return null; + if (!currentPtNode.isTerminal()) return null; + if (DBG && !string.equals(checker.toString())) return null; + return currentPtNode; + } + + /** + * Helper method to find out whether a word is in the dict or not. + */ + public boolean hasWord(final String s) { + if (null == s || "".equals(s)) { + throw new RuntimeException("Can't search for a null or empty string"); + } + return null != findWordInTree(mRootNodeArray, s); + } + + /** + * Recursively count the number of PtNodes in a given branch of the trie. + * + * @param nodeArray the parent node. + * @return the number of PtNodes in all the branch under this node. + */ + public static int countPtNodes(final PtNodeArray nodeArray) { + final int nodeSize = nodeArray.mData.size(); + int size = nodeSize; + for (int i = nodeSize - 1; i >= 0; --i) { + PtNode ptNode = nodeArray.mData.get(i); + if (null != ptNode.mChildren) + size += countPtNodes(ptNode.mChildren); + } + return size; + } + + /** + * Iterator to walk through a dictionary. + * + * This is purely for convenience. + */ + public static final class DictionaryIterator implements Iterator<WordProperty> { + private static final class Position { + public Iterator<PtNode> pos; + public int length; + public Position(ArrayList<PtNode> ptNodes) { + pos = ptNodes.iterator(); + length = 0; + } + } + final StringBuilder mCurrentString; + final LinkedList<Position> mPositions; + + public DictionaryIterator(ArrayList<PtNode> ptRoot) { + mCurrentString = new StringBuilder(); + mPositions = new LinkedList<>(); + final Position rootPos = new Position(ptRoot); + mPositions.add(rootPos); + } + + @Override + public boolean hasNext() { + for (Position p : mPositions) { + if (p.pos.hasNext()) { + return true; + } + } + return false; + } + + @Override + public WordProperty next() { + Position currentPos = mPositions.getLast(); + mCurrentString.setLength(currentPos.length); + + do { + if (currentPos.pos.hasNext()) { + final PtNode currentPtNode = currentPos.pos.next(); + currentPos.length = mCurrentString.length(); + for (int i : currentPtNode.mChars) { + mCurrentString.append(Character.toChars(i)); + } + if (null != currentPtNode.mChildren) { + currentPos = new Position(currentPtNode.mChildren.mData); + currentPos.length = mCurrentString.length(); + mPositions.addLast(currentPos); + } + if (currentPtNode.isTerminal()) { + return new WordProperty(mCurrentString.toString(), + currentPtNode.mProbabilityInfo, currentPtNode.mBigrams, + currentPtNode.mIsNotAWord, currentPtNode.mIsPossiblyOffensive); + } + } else { + mPositions.removeLast(); + currentPos = mPositions.getLast(); + mCurrentString.setLength(mPositions.getLast().length); + } + } while (true); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Unsupported yet"); + } + + } + + /** + * Method to return an iterator. + * + * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x + * and say : for (Word w : x) {} + */ + @Override + public Iterator<WordProperty> iterator() { + return new DictionaryIterator(mRootNodeArray.mData); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/MakedictLog.java b/tests/src/org/kelar/inputmethod/latin/makedict/MakedictLog.java new file mode 100644 index 000000000..07270d1d0 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/MakedictLog.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.makedict; + +/** + * Wrapper to redirect log events to the right output medium. + */ +public class MakedictLog { + public static final boolean DBG = true; + + private static void print(String message) { + System.out.println(message); + } + + public static void d(String message) { + print(message); + } + + public static void i(String message) { + print(message); + } + + public static void w(String message) { + print(message); + } + + public static void e(String message) { + print(message); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/PendingAttribute.java b/tests/src/org/kelar/inputmethod/latin/makedict/PendingAttribute.java new file mode 100644 index 000000000..0abce4848 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/PendingAttribute.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.makedict; + +/** + * A not-yet-resolved attribute. + * + * An attribute is either a bigram or a shortcut. + * All instances of this class are always immutable. + */ +public final class PendingAttribute { + public final int mFrequency; + public final int mAddress; + public PendingAttribute(final int frequency, final int address) { + mFrequency = frequency; + mAddress = address; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/PtNodeInfo.java b/tests/src/org/kelar/inputmethod/latin/makedict/PtNodeInfo.java new file mode 100644 index 000000000..65209a832 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/PtNodeInfo.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.makedict; + +import java.util.ArrayList; + +/** + * Raw PtNode info straight out of a file. This will contain numbers for addresses. + */ +public final class PtNodeInfo { + public final int mOriginalAddress; + public final int mEndAddress; + public final int mFlags; + public final int[] mCharacters; + public final ProbabilityInfo mProbabilityInfo; + public final int mChildrenAddress; + public final ArrayList<WeightedString> mShortcutTargets; + public final ArrayList<PendingAttribute> mBigrams; + + public PtNodeInfo(final int originalAddress, final int endAddress, final int flags, + final int[] characters, final ProbabilityInfo probabilityInfo, + final int childrenAddress, final ArrayList<WeightedString> shortcutTargets, + final ArrayList<PendingAttribute> bigrams) { + mOriginalAddress = originalAddress; + mEndAddress = endAddress; + mFlags = flags; + mCharacters = characters; + mProbabilityInfo = probabilityInfo; + mChildrenAddress = childrenAddress; + mShortcutTargets = shortcutTargets; + mBigrams = bigrams; + } + + public boolean isTerminal() { + return mProbabilityInfo != null; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/Ver2DictEncoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/Ver2DictEncoder.java new file mode 100644 index 000000000..8b3636208 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/Ver2DictEncoder.java @@ -0,0 +1,280 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; +import org.kelar.inputmethod.latin.makedict.BinaryDictEncoderUtils.CodePointTable; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNode; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Objects; + +/** + * An implementation of DictEncoder for version 2 binary dictionary. + */ +@UsedForTesting +public class Ver2DictEncoder implements DictEncoder { + + private final File mDictFile; + private OutputStream mOutStream; + private byte[] mBuffer; + private int mPosition; + private final int mCodePointTableMode; + public static final int CODE_POINT_TABLE_OFF = 0; + public static final int CODE_POINT_TABLE_ON = 1; + + @UsedForTesting + public Ver2DictEncoder(final File dictFile, final int codePointTableMode) { + mDictFile = dictFile; + mOutStream = null; + mBuffer = null; + mCodePointTableMode = codePointTableMode; + } + + // This constructor is used only by BinaryDictOffdeviceUtilsTests. + // If you want to use this in the production code, you should consider keeping consistency of + // the interface of Ver3DictDecoder by using factory. + @UsedForTesting + public Ver2DictEncoder(final OutputStream outStream) { + mDictFile = null; + mOutStream = outStream; + mCodePointTableMode = CODE_POINT_TABLE_OFF; + } + + private void openStream() throws FileNotFoundException { + mOutStream = new FileOutputStream(mDictFile); + } + + private void close() throws IOException { + if (mOutStream != null) { + mOutStream.close(); + mOutStream = null; + } + } + + // Package for testing + static CodePointTable makeCodePointTable(final FusionDictionary dict) { + final HashMap<Integer, Integer> codePointOccurrenceCounts = new HashMap<>(); + for (final WordProperty word : dict) { + // Store per code point occurrence + final String wordString = word.mWord; + for (int i = 0; i < wordString.length(); ++i) { + final int codePoint = Character.codePointAt(wordString, i); + if (codePointOccurrenceCounts.containsKey(codePoint)) { + codePointOccurrenceCounts.put(codePoint, + codePointOccurrenceCounts.get(codePoint) + 1); + } else { + codePointOccurrenceCounts.put(codePoint, 1); + } + } + } + final ArrayList<Entry<Integer, Integer>> codePointOccurrenceArray = + new ArrayList<>(codePointOccurrenceCounts.entrySet()); + // Descending order sort by occurrence (value side) + Collections.sort(codePointOccurrenceArray, new Comparator<Entry<Integer, Integer>>() { + @Override + public int compare(final Entry<Integer, Integer> a, final Entry<Integer, Integer> b) { + if (!Objects.equals(a.getValue(), b.getValue())) { + return b.getValue().compareTo(a.getValue()); + } + return b.getKey().compareTo(a.getKey()); + } + }); + int currentCodePointTableIndex = FormatSpec.MINIMAL_ONE_BYTE_CHARACTER_VALUE; + // Temporary map for writing of nodes + final HashMap<Integer, Integer> codePointToOneByteCodeMap = new HashMap<>(); + for (final Entry<Integer, Integer> entry : codePointOccurrenceArray) { + // Put a relation from the original code point to the one byte code. + codePointToOneByteCodeMap.put(entry.getKey(), currentCodePointTableIndex); + if (FormatSpec.MAXIMAL_ONE_BYTE_CHARACTER_VALUE < ++currentCodePointTableIndex) { + break; + } + } + // codePointToOneByteCodeMap for writing the trie + // codePointOccurrenceArray for writing the header + return new CodePointTable(codePointToOneByteCodeMap, codePointOccurrenceArray); + } + + @Override + public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) + throws IOException, UnsupportedFormatException { + // We no longer support anything but the latest version of v2. + if (formatOptions.mVersion != FormatSpec.VERSION202) { + throw new UnsupportedFormatException( + "The given format options has wrong version number : " + + formatOptions.mVersion); + } + + if (mOutStream == null) { + openStream(); + } + + // Make code point conversion table ordered by occurrence of code points + // Version 201 or later have codePointTable + final CodePointTable codePointTable; + if (mCodePointTableMode == CODE_POINT_TABLE_OFF || formatOptions.mVersion + < FormatSpec.MINIMUM_SUPPORTED_VERSION_OF_CODE_POINT_TABLE) { + codePointTable = new CodePointTable(); + } else { + codePointTable = makeCodePointTable(dict); + } + + BinaryDictEncoderUtils.writeDictionaryHeader(mOutStream, dict, formatOptions, + codePointTable.mCodePointOccurrenceArray); + + // Addresses are limited to 3 bytes, but since addresses can be relative to each node + // array, the structure itself is not limited to 16MB. However, if it is over 16MB deciding + // the order of the PtNode arrays becomes a quite complicated problem, because though the + // dictionary itself does not have a size limit, each node array must still be within 16MB + // of all its children and parents. As long as this is ensured, the dictionary file may + // grow to any size. + + // Leave the choice of the optimal node order to the flattenTree function. + MakedictLog.i("Flattening the tree..."); + ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); + + MakedictLog.i("Computing addresses..."); + BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, + codePointTable.mCodePointToOneByteCodeMap); + MakedictLog.i("Checking PtNode array..."); + if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); + + // Create a buffer that matches the final dictionary size. + final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); + final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; + mBuffer = new byte[bufferSize]; + + MakedictLog.i("Writing file..."); + + for (PtNodeArray nodeArray : flatNodes) { + BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, + codePointTable.mCodePointToOneByteCodeMap); + } + if (MakedictLog.DBG) BinaryDictEncoderUtils.showStatistics(flatNodes); + mOutStream.write(mBuffer, 0, mPosition); + + MakedictLog.i("Done"); + close(); + } + + @Override + public void setPosition(final int position) { + if (mBuffer == null || position < 0 || position >= mBuffer.length) return; + mPosition = position; + } + + @Override + public int getPosition() { + return mPosition; + } + + @Override + public void writePtNodeCount(final int ptNodeCount) { + final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); + if (countSize != 1 && countSize != 2) { + throw new RuntimeException("Strange size from getGroupCountSize : " + countSize); + } + final int encodedPtNodeCount = (countSize == 2) ? + (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, encodedPtNodeCount, + countSize); + } + + private void writePtNodeFlags(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, + codePointToOneByteCodeMap); + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, + BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos), + FormatSpec.PTNODE_FLAGS_SIZE); + } + + private void writeCharacters(final int[] codePoints, final boolean hasSeveralChars, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + mPosition = CharEncoding.writeCharArray(codePoints, mBuffer, mPosition, + codePointToOneByteCodeMap); + if (hasSeveralChars) { + mBuffer[mPosition++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; + } + } + + private void writeFrequency(final int frequency) { + if (frequency >= 0) { + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, frequency, + FormatSpec.PTNODE_FREQUENCY_SIZE); + } + } + + private void writeChildrenPosition(final PtNode ptNode, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, + codePointToOneByteCodeMap); + mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, + childrenPos); + } + + /** + * Write a bigram attributes list to mBuffer. + * + * @param bigrams the bigram attributes list. + * @param dict the dictionary the node array is a part of (for relative offsets). + */ + private void writeBigrams(final ArrayList<WeightedString> bigrams, + final FusionDictionary dict) { + if (bigrams == null) return; + + final Iterator<WeightedString> bigramIterator = bigrams.iterator(); + while (bigramIterator.hasNext()) { + final WeightedString bigram = bigramIterator.next(); + final PtNode target = + FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); + final int addressOfBigram = target.mCachedAddressAfterUpdate; + final int unigramFrequencyForThisWord = target.getProbability(); + final int offset = addressOfBigram + - (mPosition + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); + final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), + offset, bigram.getProbability(), unigramFrequencyForThisWord, bigram.mWord); + mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, bigramFlags, + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); + mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, + Math.abs(offset)); + } + } + + @Override + public void writePtNode(final PtNode ptNode, final FusionDictionary dict, + final HashMap<Integer, Integer> codePointToOneByteCodeMap) { + writePtNodeFlags(ptNode, codePointToOneByteCodeMap); + writeCharacters(ptNode.mChars, ptNode.hasSeveralChars(), codePointToOneByteCodeMap); + writeFrequency(ptNode.getProbability()); + writeChildrenPosition(ptNode, codePointToOneByteCodeMap); + writeBigrams(ptNode.mBigrams, dict); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictDecoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictDecoder.java new file mode 100644 index 000000000..351fd72c4 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictDecoder.java @@ -0,0 +1,104 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.BinaryDictionary; +import org.kelar.inputmethod.latin.common.FileUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; + +/** + * An implementation of binary dictionary decoder for version 4 binary dictionary. + */ +@UsedForTesting +public class Ver4DictDecoder extends AbstractDictDecoder { + final File mDictDirectory; + + @UsedForTesting + /* package */ Ver4DictDecoder(final File dictDirectory) { + mDictDirectory = dictDirectory; + + } + + @Override + public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException { + // dictType is not being used in dicttool. Passing an empty string. + final BinaryDictionary binaryDictionary= new BinaryDictionary( + mDictDirectory.getAbsolutePath(), 0 /* offset */, 0 /* length */, + true /* useFullEditDistance */, null /* locale */, + "" /* dictType */, true /* isUpdatable */); + final DictionaryHeader header = binaryDictionary.getHeader(); + binaryDictionary.close(); + if (header == null) { + throw new IOException("Cannot read the dictionary header."); + } + return header; + } + + @Override + public FusionDictionary readDictionaryBinary(final boolean deleteDictIfBroken) + throws FileNotFoundException, IOException, UnsupportedFormatException { + // dictType is not being used in dicttool. Passing an empty string. + final BinaryDictionary binaryDictionary = new BinaryDictionary( + mDictDirectory.getAbsolutePath(), 0 /* offset */, 0 /* length */, + true /* useFullEditDistance */, null /* locale */, + "" /* dictType */, true /* isUpdatable */); + final DictionaryHeader header = readHeader(); + final FusionDictionary fusionDict = + new FusionDictionary(new FusionDictionary.PtNodeArray(), header.mDictionaryOptions); + int token = 0; + final ArrayList<WordProperty> wordProperties = new ArrayList<>(); + do { + final BinaryDictionary.GetNextWordPropertyResult result = + binaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + if (wordProperty == null) { + binaryDictionary.close(); + if (deleteDictIfBroken) { + FileUtils.deleteRecursively(mDictDirectory); + } + return null; + } + wordProperties.add(wordProperty); + token = result.mNextToken; + } while (token != 0); + + // Insert unigrams into the fusion dictionary. + for (final WordProperty wordProperty : wordProperties) { + fusionDict.add(wordProperty.mWord, wordProperty.mProbabilityInfo, + wordProperty.mIsNotAWord, + wordProperty.mIsPossiblyOffensive); + } + // Insert bigrams into the fusion dictionary. + // TODO: Support ngrams. + for (final WordProperty wordProperty : wordProperties) { + if (!wordProperty.mHasNgrams) { + continue; + } + final String word0 = wordProperty.mWord; + for (final WeightedString bigram : wordProperty.getBigrams()) { + fusionDict.setBigram(word0, bigram.mWord, bigram.mProbabilityInfo); + } + } + binaryDictionary.close(); + return fusionDict; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictEncoder.java b/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictEncoder.java new file mode 100644 index 000000000..2eb3010c2 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/makedict/Ver4DictEncoder.java @@ -0,0 +1,133 @@ +/* + * 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 org.kelar.inputmethod.latin.makedict; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.BinaryDictionary; +import org.kelar.inputmethod.latin.Dictionary; +import org.kelar.inputmethod.latin.NgramContext; +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import org.kelar.inputmethod.latin.makedict.FusionDictionary.PtNode; +import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; + +/** + * An implementation of DictEncoder for version 4 binary dictionary. + */ +@UsedForTesting +public class Ver4DictEncoder implements DictEncoder { + private final File mDictPlacedDir; + + @UsedForTesting + public Ver4DictEncoder(final File dictPlacedDir) { + mDictPlacedDir = dictPlacedDir; + } + + // TODO: This builds a FusionDictionary first and iterates it to add words to the binary + // dictionary. However, it is possible to just add words directly to the binary dictionary + // instead. + // In the long run, when we stop supporting version 2, FusionDictionary will become deprecated + // and we can remove it. Then we'll be able to just call BinaryDictionary directly. + @Override + public void writeDictionary(FusionDictionary dict, FormatOptions formatOptions) + throws IOException, UnsupportedFormatException { + if (formatOptions.mVersion != FormatSpec.VERSION4) { + throw new UnsupportedFormatException("File header has a wrong version number : " + + formatOptions.mVersion); + } + if (!mDictPlacedDir.isDirectory()) { + throw new UnsupportedFormatException("Given path is not a directory."); + } + if (!BinaryDictionaryUtils.createEmptyDictFile(mDictPlacedDir.getAbsolutePath(), + FormatSpec.VERSION4, LocaleUtils.constructLocaleFromString( + dict.mOptions.mAttributes.get(DictionaryHeader.DICTIONARY_LOCALE_KEY)), + dict.mOptions.mAttributes)) { + throw new IOException("Cannot create dictionary file : " + + mDictPlacedDir.getAbsolutePath()); + } + final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(), + 0l, mDictPlacedDir.length(), true /* useFullEditDistance */, + LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get( + DictionaryHeader.DICTIONARY_LOCALE_KEY)), + Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */, + true /* isUpdatable */); + if (!binaryDict.isValidDictionary()) { + // Somehow createEmptyDictFile returned true, but the file was not created correctly + throw new IOException("Cannot create dictionary file"); + } + for (final WordProperty wordProperty : dict) { + if (!binaryDict.addUnigramEntry(wordProperty.mWord, wordProperty.getProbability(), + wordProperty.mIsBeginningOfSentence, wordProperty.mIsNotAWord, + wordProperty.mIsPossiblyOffensive, 0 /* timestamp */)) { + MakedictLog.e("Cannot add unigram entry for " + wordProperty.mWord); + } + if (binaryDict.needsToRunGC(true /* mindsBlockByGC */)) { + if (!binaryDict.flushWithGC()) { + MakedictLog.e("Cannot flush dict with GC."); + return; + } + } + } + for (final WordProperty word0Property : dict) { + if (!word0Property.mHasNgrams) continue; + // TODO: Support ngram. + for (final WeightedString word1 : word0Property.getBigrams()) { + final NgramContext ngramContext = + new NgramContext(new NgramContext.WordInfo(word0Property.mWord)); + if (!binaryDict.addNgramEntry(ngramContext, word1.mWord, + word1.getProbability(), 0 /* timestamp */)) { + MakedictLog.e("Cannot add n-gram entry for " + + ngramContext + " -> " + word1.mWord); + return; + } + if (binaryDict.needsToRunGC(true /* mindsBlockByGC */)) { + if (!binaryDict.flushWithGC()) { + MakedictLog.e("Cannot flush dict with GC."); + return; + } + } + } + } + if (!binaryDict.flushWithGC()) { + MakedictLog.e("Cannot flush dict with GC."); + return; + } + binaryDict.close(); + } + + @Override + public void setPosition(int position) { + } + + @Override + public int getPosition() { + return 0; + } + + @Override + public void writePtNodeCount(int ptNodeCount) { + } + + @Override + public void writePtNode(PtNode ptNode, FusionDictionary dict, + HashMap<Integer, Integer> codePointToOneByteCodeMap) { + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/network/BlockingHttpClientTests.java b/tests/src/org/kelar/inputmethod/latin/network/BlockingHttpClientTests.java new file mode 100644 index 000000000..941abc606 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/network/BlockingHttpClientTests.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.network; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.network.BlockingHttpClient.ResponseProcessor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.util.Arrays; +import java.util.Random; + +/** + * Tests for {@link BlockingHttpClient}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BlockingHttpClientTests { + @Mock HttpURLConnection mMockHttpConnection; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testError_badGateway() throws IOException, AuthException { + when(mMockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_GATEWAY); + final BlockingHttpClient client = new BlockingHttpClient(mMockHttpConnection); + final FakeErrorResponseProcessor processor = new FakeErrorResponseProcessor(); + + try { + client.execute(null /* empty request */, processor); + fail("Expecting an HttpException"); + } catch (HttpException e) { + // expected HttpException + assertEquals(HttpURLConnection.HTTP_BAD_GATEWAY, e.getHttpStatusCode()); + } + } + + @Test + public void testError_clientTimeout() throws Exception { + when(mMockHttpConnection.getResponseCode()).thenReturn( + HttpURLConnection.HTTP_CLIENT_TIMEOUT); + final BlockingHttpClient client = new BlockingHttpClient(mMockHttpConnection); + final FakeErrorResponseProcessor processor = new FakeErrorResponseProcessor(); + + try { + client.execute(null /* empty request */, processor); + fail("Expecting an HttpException"); + } catch (HttpException e) { + // expected HttpException + assertEquals(HttpURLConnection.HTTP_CLIENT_TIMEOUT, e.getHttpStatusCode()); + } + } + + @Test + public void testError_forbiddenWithRequest() throws Exception { + final OutputStream mockOutputStream = Mockito.mock(OutputStream.class); + when(mMockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN); + when(mMockHttpConnection.getOutputStream()).thenReturn(mockOutputStream); + final BlockingHttpClient client = new BlockingHttpClient(mMockHttpConnection); + final FakeErrorResponseProcessor processor = new FakeErrorResponseProcessor(); + + try { + client.execute(new byte[100], processor); + fail("Expecting an HttpException"); + } catch (HttpException e) { + assertEquals(HttpURLConnection.HTTP_FORBIDDEN, e.getHttpStatusCode()); + } + verify(mockOutputStream).write(any(byte[].class), eq(0), eq(100)); + } + + @Test + public void testSuccess_emptyRequest() throws Exception { + final Random rand = new Random(); + byte[] response = new byte[100]; + rand.nextBytes(response); + when(mMockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockHttpConnection.getInputStream()).thenReturn(new ByteArrayInputStream(response)); + final BlockingHttpClient client = new BlockingHttpClient(mMockHttpConnection); + final FakeSuccessResponseProcessor processor = + new FakeSuccessResponseProcessor(response); + + client.execute(null /* empty request */, processor); + assertTrue("ResponseProcessor was not invoked", processor.mInvoked); + } + + @Test + public void testSuccess() throws Exception { + final OutputStream mockOutputStream = Mockito.mock(OutputStream.class); + final Random rand = new Random(); + byte[] response = new byte[100]; + rand.nextBytes(response); + when(mMockHttpConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mMockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mMockHttpConnection.getInputStream()).thenReturn(new ByteArrayInputStream(response)); + final BlockingHttpClient client = new BlockingHttpClient(mMockHttpConnection); + final FakeSuccessResponseProcessor processor = + new FakeSuccessResponseProcessor(response); + + client.execute(new byte[100], processor); + assertTrue("ResponseProcessor was not invoked", processor.mInvoked); + } + + static class FakeErrorResponseProcessor implements ResponseProcessor<Void> { + @Override + public Void onSuccess(InputStream response) { + fail("Expected an error but received success"); + return null; + } + } + + private static class FakeSuccessResponseProcessor implements ResponseProcessor<Void> { + private final byte[] mExpectedResponse; + + boolean mInvoked; + + FakeSuccessResponseProcessor(byte[] expectedResponse) { + mExpectedResponse = expectedResponse; + } + + @Override + public Void onSuccess(InputStream response) { + try { + mInvoked = true; + BufferedInputStream in = new BufferedInputStream(response); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int read = 0; + while ((read = in.read()) != -1) { + buffer.write(read); + } + byte[] actualResponse = buffer.toByteArray(); + in.close(); + assertTrue("Response doesn't match", + Arrays.equals(mExpectedResponse, actualResponse)); + } catch (IOException ex) { + fail("IOException in onSuccess"); + } + return null; + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/network/HttpUrlConnectionBuilderTests.java b/tests/src/org/kelar/inputmethod/latin/network/HttpUrlConnectionBuilderTests.java new file mode 100644 index 000000000..63a5f6ff0 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/network/HttpUrlConnectionBuilderTests.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.network; + +import static org.kelar.inputmethod.latin.network.HttpUrlConnectionBuilder.MODE_BI_DIRECTIONAL; +import static org.kelar.inputmethod.latin.network.HttpUrlConnectionBuilder.MODE_DOWNLOAD_ONLY; +import static org.kelar.inputmethod.latin.network.HttpUrlConnectionBuilder.MODE_UPLOAD_ONLY; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; + +/** + * Tests for {@link HttpUrlConnectionBuilder}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class HttpUrlConnectionBuilderTests { + @Test + public void testSetUrl_malformed() { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + try { + builder.setUrl("dadasd!@%@!:11"); + fail("Expected a MalformedURLException."); + } catch (MalformedURLException e) { + // Expected + } + } + + @Test + public void testSetConnectTimeout_invalid() { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + try { + builder.setConnectTimeout(-1); + fail("Expected an IllegalArgumentException."); + } catch (IllegalArgumentException e) { + // Expected + } + } + + @Test + public void testSetConnectTimeout() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("https://www.example.com"); + builder.setConnectTimeout(8765); + HttpURLConnection connection = builder.build(); + assertEquals(8765, connection.getConnectTimeout()); + } + + @Test + public void testSetReadTimeout_invalid() { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + try { + builder.setReadTimeout(-1); + fail("Expected an IllegalArgumentException."); + } catch (IllegalArgumentException e) { + // Expected + } + } + + @Test + public void testSetReadTimeout() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("https://www.example.com"); + builder.setReadTimeout(8765); + HttpURLConnection connection = builder.build(); + assertEquals(8765, connection.getReadTimeout()); + } + + @Test + public void testAddHeader() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("http://www.example.com"); + builder.addHeader("some-random-key", "some-random-value"); + HttpURLConnection connection = builder.build(); + assertEquals("some-random-value", connection.getRequestProperty("some-random-key")); + } + + @Test + public void testSetUseCache_notSet() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("http://www.example.com"); + HttpURLConnection connection = builder.build(); + assertFalse(connection.getUseCaches()); + } + + @Test + public void testSetUseCache_false() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("http://www.example.com"); + HttpURLConnection connection = builder.build(); + connection.setUseCaches(false); + assertFalse(connection.getUseCaches()); + } + + @Test + public void testSetUseCache_true() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("http://www.example.com"); + HttpURLConnection connection = builder.build(); + connection.setUseCaches(true); + assertTrue(connection.getUseCaches()); + } + + @Test + public void testSetMode_uploadOnly() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("http://www.example.com"); + builder.setMode(MODE_UPLOAD_ONLY); + HttpURLConnection connection = builder.build(); + assertTrue(connection.getDoInput()); + assertFalse(connection.getDoOutput()); + } + + @Test + public void testSetMode_downloadOnly() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("https://www.example.com"); + builder.setMode(MODE_DOWNLOAD_ONLY); + HttpURLConnection connection = builder.build(); + assertFalse(connection.getDoInput()); + assertTrue(connection.getDoOutput()); + } + + @Test + public void testSetMode_bidirectional() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("https://www.example.com"); + builder.setMode(MODE_BI_DIRECTIONAL); + HttpURLConnection connection = builder.build(); + assertTrue(connection.getDoInput()); + assertTrue(connection.getDoOutput()); + } + + @Test + public void testSetAuthToken() throws IOException { + HttpUrlConnectionBuilder builder = new HttpUrlConnectionBuilder(); + builder.setUrl("https://www.example.com"); + builder.setAuthToken("some-random-auth-token"); + HttpURLConnection connection = builder.build(); + assertEquals("some-random-auth-token", + connection.getRequestProperty(HttpUrlConnectionBuilder.HTTP_HEADER_AUTHORIZATION)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTests.java new file mode 100644 index 000000000..9e6a18a13 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTests.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.personalization; + +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.ExpandableBinaryDictionary; +import org.kelar.inputmethod.latin.utils.BinaryDictionaryUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.Locale; +import java.util.Random; + +/** + * Unit tests for UserHistoryDictionary + */ +@LargeTest +@RunWith(AndroidJUnit4.class) +public class UserHistoryDictionaryTests { + private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName(); + private static final int WAIT_FOR_WRITING_FILE_IN_MILLISECONDS = 3000; + private static final String TEST_ACCOUNT = "account@example.com"; + + private int mCurrentTime = 0; + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + private static void printAllFiles(final File dir) { + Log.d(TAG, dir.getAbsolutePath()); + for (final File file : dir.listFiles()) { + Log.d(TAG, " " + file.getName()); + } + } + + private static void assertDictionaryExists(final UserHistoryDictionary dict, + final File dictFile) { + Log.d(TAG, "waiting for writing ..."); + dict.waitAllTasksForTests(); + if (!dictFile.exists()) { + try { + Log.d(TAG, dictFile + " is not existing. Wait " + + WAIT_FOR_WRITING_FILE_IN_MILLISECONDS + " ms for writing."); + printAllFiles(dictFile.getParentFile()); + Thread.sleep(WAIT_FOR_WRITING_FILE_IN_MILLISECONDS); + } catch (final InterruptedException e) { + Log.e(TAG, "Interrupted during waiting for writing the dict file."); + } + } + assertTrue("Following dictionary file doesn't exist: " + dictFile, dictFile.exists()); + } + + @Before + public void setUp() throws Exception { + resetCurrentTimeForTestMode(); + UserHistoryDictionaryTestsHelper.removeAllTestDictFiles( + UserHistoryDictionaryTestsHelper.TEST_LOCALE_PREFIX, getContext()); + } + + @After + public void tearDown() throws Exception { + UserHistoryDictionaryTestsHelper.removeAllTestDictFiles( + UserHistoryDictionaryTestsHelper.TEST_LOCALE_PREFIX, getContext()); + stopTestModeInNativeCode(); + } + + private void resetCurrentTimeForTestMode() { + mCurrentTime = 0; + setCurrentTimeForTestMode(mCurrentTime); + } + + private static int setCurrentTimeForTestMode(final int currentTime) { + return BinaryDictionaryUtils.setCurrentTimeForTest(currentTime); + } + + private static int stopTestModeInNativeCode() { + return BinaryDictionaryUtils.setCurrentTimeForTest(-1); + } + + /** + * Clear all entries in the user history dictionary. + * @param dict the user history dictionary. + */ + private static void clearHistory(final UserHistoryDictionary dict) { + dict.waitAllTasksForTests(); + dict.clear(); + dict.close(); + dict.waitAllTasksForTests(); + } + + private void doTestRandomWords(final String testAccount) { + Log.d(TAG, "This test can be used for profiling."); + Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true."); + final Locale fakeLocale = UserHistoryDictionaryTestsHelper.getFakeLocale("random_words"); + final String dictName = UserHistoryDictionary.getUserHistoryDictName( + UserHistoryDictionary.NAME, fakeLocale, + null /* dictFile */, + testAccount /* account */); + final File dictFile = ExpandableBinaryDictionary.getDictFile( + getContext(), dictName, null /* dictFile */); + final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary( + getContext(), fakeLocale, testAccount); + clearHistory(dict); + + final int numberOfWords = 1000; + final Random random = new Random(123456); + assertTrue(UserHistoryDictionaryTestsHelper.addAndWriteRandomWords( + dict, numberOfWords, random, true /* checksContents */, mCurrentTime)); + assertDictionaryExists(dict, dictFile); + } + + @Test + public void testRandomWords_NullAccount() { + doTestRandomWords(null /* testAccount */); + } + + @Test + public void testRandomWords() { + doTestRandomWords(TEST_ACCOUNT); + } + + @Test + public void testStressTestForSwitchingLanguagesAndAddingWords() { + doTestStressTestForSwitchingLanguagesAndAddingWords(TEST_ACCOUNT); + } + + @Test + public void testStressTestForSwitchingLanguagesAndAddingWords_NullAccount() { + doTestStressTestForSwitchingLanguagesAndAddingWords(null /* testAccount */); + } + + private void doTestStressTestForSwitchingLanguagesAndAddingWords(final String testAccount) { + final int numberOfLanguages = 2; + final int numberOfLanguageSwitching = 80; + final int numberOfWordsInsertedForEachLanguageSwitch = 100; + + final File dictFiles[] = new File[numberOfLanguages]; + final UserHistoryDictionary dicts[] = new UserHistoryDictionary[numberOfLanguages]; + + try { + final Random random = new Random(123456); + + // Create filename suffixes for this test. + for (int i = 0; i < numberOfLanguages; i++) { + final Locale fakeLocale = + UserHistoryDictionaryTestsHelper.getFakeLocale("switching_languages" + i); + final String dictName = UserHistoryDictionary.getUserHistoryDictName( + UserHistoryDictionary.NAME, fakeLocale, null /* dictFile */, + testAccount /* account */); + dictFiles[i] = ExpandableBinaryDictionary.getDictFile( + getContext(), dictName, null /* dictFile */); + dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(), + fakeLocale, testAccount); + clearHistory(dicts[i]); + } + + final long start = System.currentTimeMillis(); + + for (int i = 0; i < numberOfLanguageSwitching; i++) { + final int index = i % numberOfLanguages; + // Switch to dicts[index]. + assertTrue(UserHistoryDictionaryTestsHelper.addAndWriteRandomWords(dicts[index], + numberOfWordsInsertedForEachLanguageSwitch, + random, + false /* checksContents */, + mCurrentTime)); + } + + final long end = System.currentTimeMillis(); + Log.d(TAG, "testStressTestForSwitchingLanguageAndAddingWords took " + + (end - start) + " ms"); + } finally { + for (int i = 0; i < numberOfLanguages; i++) { + assertDictionaryExists(dicts[i], dictFiles[i]); + } + } + } + + @Test + public void testAddManyWords() { + doTestAddManyWords(TEST_ACCOUNT); + } + + @Test + public void testAddManyWords_NullAccount() { + doTestAddManyWords(null /* testAccount */); + } + + private void doTestAddManyWords(final String testAccount) { + final Locale fakeLocale = + UserHistoryDictionaryTestsHelper.getFakeLocale("many_random_words"); + final String dictName = UserHistoryDictionary.getUserHistoryDictName( + UserHistoryDictionary.NAME, fakeLocale, null /* dictFile */, testAccount); + final File dictFile = ExpandableBinaryDictionary.getDictFile( + getContext(), dictName, null /* dictFile */); + final int numberOfWords = 10000; + final Random random = new Random(123456); + final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary( + getContext(), fakeLocale, testAccount); + clearHistory(dict); + assertTrue(UserHistoryDictionaryTestsHelper.addAndWriteRandomWords(dict, + numberOfWords, random, true /* checksContents */, mCurrentTime)); + assertDictionaryExists(dict, dictFile); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTestsHelper.java b/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTestsHelper.java new file mode 100644 index 000000000..11e2a7ae5 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/personalization/UserHistoryDictionaryTestsHelper.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.personalization; + +import android.content.Context; + +import org.kelar.inputmethod.latin.BinaryDictionary; +import org.kelar.inputmethod.latin.NgramContext; +import org.kelar.inputmethod.latin.NgramContext.WordInfo; +import org.kelar.inputmethod.latin.common.FileUtils; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * Utility class for helping while running tests involving {@link UserHistoryDictionary}. + */ +public class UserHistoryDictionaryTestsHelper { + + /** + * Locale prefix for generating placeholder locales for tests. + */ + public static final String TEST_LOCALE_PREFIX = "test-"; + + /** + * Characters for generating random words. + */ + private static final String[] CHARACTERS = { + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", + "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" + }; + + /** + * Remove all the test dictionary files created for the given locale. + */ + public static void removeAllTestDictFiles(final String filter, final Context context) { + final FilenameFilter filenameFilter = new FilenameFilter() { + @Override + public boolean accept(final File dir, final String filename) { + return filename.startsWith(UserHistoryDictionary.NAME + "." + filter); + } + }; + FileUtils.deleteFilteredFiles(context.getFilesDir(), filenameFilter); + } + + /** + * Generates and writes random words to dictionary. Caller can be assured + * that the write tasks would be finished; and its success would be reflected + * in the returned boolean. + * + * @param dict {@link UserHistoryDictionary} to which words should be added. + * @param numberOfWords number of words to be added. + * @param random helps generate random words. + * @param checkContents if true, checks whether written words are actually in the dictionary. + * @param currentTime timestamp that would be used for adding the words. + * @returns true if all words have been written to dictionary successfully. + */ + public static boolean addAndWriteRandomWords(final UserHistoryDictionary dict, + final int numberOfWords, final Random random, final boolean checkContents, + final int currentTime) { + final List<String> words = generateWords(numberOfWords, random); + // Add random words to the user history dictionary. + addWordsToDictionary(dict, words, currentTime); + boolean success = true; + if (checkContents) { + dict.waitAllTasksForTests(); + for (int i = 0; i < numberOfWords; ++i) { + final String word = words.get(i); + if (!dict.isInDictionary(word)) { + success = false; + break; + } + } + } + // write to file. + dict.close(); + dict.waitAllTasksForTests(); + return success; + } + + private static void addWordsToDictionary(final UserHistoryDictionary dict, + final List<String> words, final int timestamp) { + NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext( + BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM); + for (final String word : words) { + UserHistoryDictionary.addToDictionary(dict, ngramContext, word, true, timestamp); + ngramContext = ngramContext.getNextNgramContext(new WordInfo(word)); + } + } + + /** + * Creates unique test locale for using within tests. + */ + public static Locale getFakeLocale(final String name) { + return new Locale(TEST_LOCALE_PREFIX + name + System.currentTimeMillis()); + } + + /** + * Generates random words. + * + * @param numberOfWords number of words to generate. + * @param random salt used for generating random words. + */ + public static List<String> generateWords(final int numberOfWords, final Random random) { + final HashSet<String> wordSet = new HashSet<>(); + while (wordSet.size() < numberOfWords) { + wordSet.add(generateWord(random.nextInt())); + } + return new ArrayList<>(wordSet); + } + + /** + * Generates a random word. + */ + private static String generateWord(final int value) { + final int lengthOfChars = CHARACTERS.length; + final StringBuilder builder = new StringBuilder(); + long lvalue = Math.abs((long)value); + while (lvalue > 0) { + builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]); + lvalue /= lengthOfChars; + } + return builder.toString(); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragmentTests.java b/tests/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragmentTests.java new file mode 100644 index 000000000..5eff614df --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/settings/AccountsSettingsFragmentTests.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.settings; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.view.View; +import android.widget.ListView; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.utils.ManagedProfileUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class AccountsSettingsFragmentTests { + private static final String FRAG_NAME = AccountsSettingsFragment.class.getName(); + private static final long TEST_TIMEOUT_MILLIS = 5000; + + @Mock private ManagedProfileUtils mManagedProfileUtils; + + private TestFragmentActivity mActivity; + private TestFragmentActivity getActivity() { + return mActivity; + } + + @Before + public void setUp() throws Exception { + // Initialize the mocks. + MockitoAnnotations.initMocks(this); + ManagedProfileUtils.setTestInstance(mManagedProfileUtils); + + final Intent intent = new Intent() + .setAction(Intent.ACTION_MAIN) + .setClass(InstrumentationRegistry.getTargetContext(), TestFragmentActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + .putExtra(TestFragmentActivity.EXTRA_SHOW_FRAGMENT, FRAG_NAME); + mActivity = (TestFragmentActivity) InstrumentationRegistry.getInstrumentation() + .startActivitySync(intent); + } + + @After + public void tearDown() throws Exception { + ManagedProfileUtils.setTestInstance(null); + mActivity = null; + } + + @Test + public void testEmptyAccounts() { + final AccountsSettingsFragment fragment = + (AccountsSettingsFragment) getActivity().mFragment; + try { + fragment.createAccountPicker(new String[0], null, null /* listener */); + fail("Expected IllegalArgumentException, never thrown"); + } catch (IllegalArgumentException expected) { + // Expected. + } + } + + private static class DialogHolder { + AlertDialog mDialog; + DialogHolder() {} + } + + @Test + public void testMultipleAccounts_noSettingsForManagedProfile() { + when(mManagedProfileUtils.hasWorkProfile(any(Context.class))).thenReturn(true); + + final AccountsSettingsFragment fragment = + (AccountsSettingsFragment) getActivity().mFragment; + final AlertDialog dialog = initDialog(fragment, null).mDialog; + final ListView lv = dialog.getListView(); + + // Nothing to check/uncheck. + assertNull(fragment.findPreference(AccountsSettingsFragment.PREF_ACCCOUNT_SWITCHER)); + } + + @Test + public void testMultipleAccounts_noCurrentAccount() { + when(mManagedProfileUtils.hasWorkProfile(any(Context.class))).thenReturn(false); + + final AccountsSettingsFragment fragment = + (AccountsSettingsFragment) getActivity().mFragment; + final AlertDialog dialog = initDialog(fragment, null).mDialog; + final ListView lv = dialog.getListView(); + + // The 1st account should be checked by default. + assertEquals("checked-item", 0, lv.getCheckedItemPosition()); + // There should be 4 accounts in the list. + assertEquals("count", 4, lv.getCount()); + // The sign-out button shouldn't exist + assertEquals(View.GONE, + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).getVisibility()); + assertEquals(View.VISIBLE, + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getVisibility()); + assertEquals(View.VISIBLE, + dialog.getButton(DialogInterface.BUTTON_POSITIVE).getVisibility()); + } + + @Test + public void testMultipleAccounts_currentAccount() { + when(mManagedProfileUtils.hasWorkProfile(any(Context.class))).thenReturn(false); + + final AccountsSettingsFragment fragment = + (AccountsSettingsFragment) getActivity().mFragment; + final AlertDialog dialog = initDialog(fragment, "3@example.com").mDialog; + final ListView lv = dialog.getListView(); + + // The 3rd account should be checked by default. + assertEquals("checked-item", 2, lv.getCheckedItemPosition()); + // There should be 4 accounts in the list. + assertEquals("count", 4, lv.getCount()); + // The sign-out button should be shown + assertEquals(View.VISIBLE, + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).getVisibility()); + assertEquals(View.VISIBLE, + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).getVisibility()); + assertEquals(View.VISIBLE, + dialog.getButton(DialogInterface.BUTTON_POSITIVE).getVisibility()); + } + + private DialogHolder initDialog( + final AccountsSettingsFragment fragment, + final String selectedAccount) { + final DialogHolder dialogHolder = new DialogHolder(); + final CountDownLatch latch = new CountDownLatch(1); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + final AlertDialog dialog = fragment.createAccountPicker( + new String[] { + "1@example.com", + "2@example.com", + "3@example.com", + "4@example.com"}, + selectedAccount, null /* positiveButtonListner */); + dialog.show(); + dialogHolder.mDialog = dialog; + latch.countDown(); + } + }); + + try { + latch.await(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException ex) { + fail(); + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + return dialogHolder; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuationsTests.java b/tests/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuationsTests.java new file mode 100644 index 000000000..45a303639 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/settings/SpacingAndPunctuationsTests.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.settings; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.content.res.Resources; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.utils.RunInLocale; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SpacingAndPunctuationsTests { + private static final int ARMENIAN_FULL_STOP = '\u0589'; + private static final int ARMENIAN_COMMA = '\u055D'; + + private int mScreenMetrics; + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + private boolean isPhone() { + return Constants.isPhone(mScreenMetrics); + } + + private boolean isTablet() { + return Constants.isTablet(mScreenMetrics); + } + + private SpacingAndPunctuations ENGLISH; + private SpacingAndPunctuations FRENCH; + private SpacingAndPunctuations GERMAN; + private SpacingAndPunctuations ARMENIAN; + private SpacingAndPunctuations THAI; + private SpacingAndPunctuations KHMER; + private SpacingAndPunctuations LAO; + private SpacingAndPunctuations ARABIC; + private SpacingAndPunctuations PERSIAN; + private SpacingAndPunctuations HEBREW; + + private SpacingAndPunctuations UNITED_STATES; + private SpacingAndPunctuations UNITED_KINGDOM; + private SpacingAndPunctuations CANADA_FRENCH; + private SpacingAndPunctuations SWISS_GERMAN; + private SpacingAndPunctuations INDIA_ENGLISH; + private SpacingAndPunctuations ARMENIA_ARMENIAN; + private SpacingAndPunctuations CAMBODIA_KHMER; + private SpacingAndPunctuations LAOS_LAO; + + @Before + public void setUp() throws Exception { + mScreenMetrics = Settings.readScreenMetrics(getContext().getResources()); + + // Language only + ENGLISH = getSpacingAndPunctuations(Locale.ENGLISH); + FRENCH = getSpacingAndPunctuations(Locale.FRENCH); + GERMAN = getSpacingAndPunctuations(Locale.GERMAN); + THAI = getSpacingAndPunctuations(new Locale("th")); + ARMENIAN = getSpacingAndPunctuations(new Locale("hy")); + KHMER = getSpacingAndPunctuations(new Locale("km")); + LAO = getSpacingAndPunctuations(new Locale("lo")); + ARABIC = getSpacingAndPunctuations(new Locale("ar")); + PERSIAN = getSpacingAndPunctuations(new Locale("fa")); + HEBREW = getSpacingAndPunctuations(new Locale("iw")); + + // Language and Country + UNITED_STATES = getSpacingAndPunctuations(Locale.US); + UNITED_KINGDOM = getSpacingAndPunctuations(Locale.UK); + CANADA_FRENCH = getSpacingAndPunctuations(Locale.CANADA_FRENCH); + SWISS_GERMAN = getSpacingAndPunctuations(new Locale("de", "CH")); + INDIA_ENGLISH = getSpacingAndPunctuations(new Locale("en", "IN")); + ARMENIA_ARMENIAN = getSpacingAndPunctuations(new Locale("hy", "AM")); + CAMBODIA_KHMER = getSpacingAndPunctuations(new Locale("km", "KH")); + LAOS_LAO = getSpacingAndPunctuations(new Locale("lo", "LA")); + } + + private SpacingAndPunctuations getSpacingAndPunctuations(final Locale locale) { + final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(Resources res) { + return new SpacingAndPunctuations(res); + } + }; + return job.runInLocale(getContext().getResources(), locale); + } + + private static void testingStandardWordSeparator(final SpacingAndPunctuations sp) { + assertTrue("Tab", sp.isWordSeparator('\t')); + assertTrue("Newline", sp.isWordSeparator('\n')); + assertTrue("Space", sp.isWordSeparator(' ')); + assertTrue("Exclamation", sp.isWordSeparator('!')); + assertTrue("Quotation", sp.isWordSeparator('"')); + assertFalse("Number", sp.isWordSeparator('#')); + assertFalse("Dollar", sp.isWordSeparator('$')); + assertFalse("Percent", sp.isWordSeparator('%')); + assertTrue("Ampersand", sp.isWordSeparator('&')); + assertFalse("Apostrophe", sp.isWordSeparator('\'')); + assertTrue("L Paren", sp.isWordSeparator('(')); + assertTrue("R Paren", sp.isWordSeparator(')')); + assertTrue("Asterisk", sp.isWordSeparator('*')); + assertTrue("Plus", sp.isWordSeparator('+')); + assertTrue("Comma", sp.isWordSeparator(',')); + assertFalse("Minus", sp.isWordSeparator('-')); + assertTrue("Period", sp.isWordSeparator('.')); + assertTrue("Slash", sp.isWordSeparator('/')); + assertTrue("Colon", sp.isWordSeparator(':')); + assertTrue("Semicolon", sp.isWordSeparator(';')); + assertTrue("L Angle", sp.isWordSeparator('<')); + assertTrue("Equal", sp.isWordSeparator('=')); + assertTrue("R Angle", sp.isWordSeparator('>')); + assertTrue("Question", sp.isWordSeparator('?')); + assertFalse("Atmark", sp.isWordSeparator('@')); + assertTrue("L S Bracket", sp.isWordSeparator('[')); + assertFalse("B Slash", sp.isWordSeparator('\\')); + assertTrue("R S Bracket", sp.isWordSeparator(']')); + assertFalse("Circumflex", sp.isWordSeparator('^')); + assertTrue("Underscore", sp.isWordSeparator('_')); + assertFalse("Grave", sp.isWordSeparator('`')); + assertTrue("L C Brace", sp.isWordSeparator('{')); + assertTrue("V Line", sp.isWordSeparator('|')); + assertTrue("R C Brace", sp.isWordSeparator('}')); + assertFalse("Tilde", sp.isWordSeparator('~')); + } + + @Test + public void testWordSeparator() { + testingStandardWordSeparator(ENGLISH); + testingStandardWordSeparator(FRENCH); + testingStandardWordSeparator(CANADA_FRENCH); + testingStandardWordSeparator(ARMENIA_ARMENIAN); + assertTrue(ARMENIA_ARMENIAN.isWordSeparator(ARMENIAN_FULL_STOP)); + assertTrue(ARMENIA_ARMENIAN.isWordSeparator(ARMENIAN_COMMA)); + // TODO: We should fix these. + testingStandardWordSeparator(ARMENIAN); + assertFalse(ARMENIAN.isWordSeparator(ARMENIAN_FULL_STOP)); + assertFalse(ARMENIAN.isWordSeparator(ARMENIAN_COMMA)); + } + + private static void testingStandardWordConnector(final SpacingAndPunctuations sp) { + assertFalse("Tab", sp.isWordConnector('\t')); + assertFalse("Newline", sp.isWordConnector('\n')); + assertFalse("Space", sp.isWordConnector(' ')); + assertFalse("Exclamation", sp.isWordConnector('!')); + assertFalse("Quotation", sp.isWordConnector('"')); + assertFalse("Number", sp.isWordConnector('#')); + assertFalse("Dollar", sp.isWordConnector('$')); + assertFalse("Percent", sp.isWordConnector('%')); + assertFalse("Ampersand", sp.isWordConnector('&')); + assertTrue("Apostrophe", sp.isWordConnector('\'')); + assertFalse("L Paren", sp.isWordConnector('(')); + assertFalse("R Paren", sp.isWordConnector(')')); + assertFalse("Asterisk", sp.isWordConnector('*')); + assertFalse("Plus", sp.isWordConnector('+')); + assertFalse("Comma", sp.isWordConnector(',')); + assertTrue("Minus", sp.isWordConnector('-')); + assertFalse("Period", sp.isWordConnector('.')); + assertFalse("Slash", sp.isWordConnector('/')); + assertFalse("Colon", sp.isWordConnector(':')); + assertFalse("Semicolon", sp.isWordConnector(';')); + assertFalse("L Angle", sp.isWordConnector('<')); + assertFalse("Equal", sp.isWordConnector('=')); + assertFalse("R Angle", sp.isWordConnector('>')); + assertFalse("Question", sp.isWordConnector('?')); + assertFalse("Atmark", sp.isWordConnector('@')); + assertFalse("L S Bracket", sp.isWordConnector('[')); + assertFalse("B Slash", sp.isWordConnector('\\')); + assertFalse("R S Bracket", sp.isWordConnector(']')); + assertFalse("Circumflex", sp.isWordConnector('^')); + assertFalse("Underscore", sp.isWordConnector('_')); + assertFalse("Grave", sp.isWordConnector('`')); + assertFalse("L C Brace", sp.isWordConnector('{')); + assertFalse("V Line", sp.isWordConnector('|')); + assertFalse("R C Brace", sp.isWordConnector('}')); + assertFalse("Tilde", sp.isWordConnector('~')); + + } + + @Test + public void testWordConnector() { + testingStandardWordConnector(ENGLISH); + testingStandardWordConnector(FRENCH); + testingStandardWordConnector(CANADA_FRENCH); + testingStandardWordConnector(ARMENIA_ARMENIAN); + } + + private static void testingCommonPrecededBySpace(final SpacingAndPunctuations sp) { + assertFalse("Tab", sp.isUsuallyPrecededBySpace('\t')); + assertFalse("Newline", sp.isUsuallyPrecededBySpace('\n')); + assertFalse("Space", sp.isUsuallyPrecededBySpace(' ')); + //assertFalse("Exclamation", sp.isUsuallyPrecededBySpace('!')); + assertFalse("Quotation", sp.isUsuallyPrecededBySpace('"')); + assertFalse("Number", sp.isUsuallyPrecededBySpace('#')); + assertFalse("Dollar", sp.isUsuallyPrecededBySpace('$')); + assertFalse("Percent", sp.isUsuallyPrecededBySpace('%')); + assertTrue("Ampersand", sp.isUsuallyPrecededBySpace('&')); + assertFalse("Apostrophe", sp.isUsuallyPrecededBySpace('\'')); + assertTrue("L Paren", sp.isUsuallyPrecededBySpace('(')); + assertFalse("R Paren", sp.isUsuallyPrecededBySpace(')')); + assertFalse("Asterisk", sp.isUsuallyPrecededBySpace('*')); + assertFalse("Plus", sp.isUsuallyPrecededBySpace('+')); + assertFalse("Comma", sp.isUsuallyPrecededBySpace(',')); + assertFalse("Minus", sp.isUsuallyPrecededBySpace('-')); + assertFalse("Period", sp.isUsuallyPrecededBySpace('.')); + assertFalse("Slash", sp.isUsuallyPrecededBySpace('/')); + //assertFalse("Colon", sp.isUsuallyPrecededBySpace(':')); + //assertFalse("Semicolon", sp.isUsuallyPrecededBySpace(';')); + assertFalse("L Angle", sp.isUsuallyPrecededBySpace('<')); + assertFalse("Equal", sp.isUsuallyPrecededBySpace('=')); + assertFalse("R Angle", sp.isUsuallyPrecededBySpace('>')); + //assertFalse("Question", sp.isUsuallyPrecededBySpace('?')); + assertFalse("Atmark", sp.isUsuallyPrecededBySpace('@')); + assertTrue("L S Bracket", sp.isUsuallyPrecededBySpace('[')); + assertFalse("B Slash", sp.isUsuallyPrecededBySpace('\\')); + assertFalse("R S Bracket", sp.isUsuallyPrecededBySpace(']')); + assertFalse("Circumflex", sp.isUsuallyPrecededBySpace('^')); + assertFalse("Underscore", sp.isUsuallyPrecededBySpace('_')); + assertFalse("Grave", sp.isUsuallyPrecededBySpace('`')); + assertTrue("L C Brace", sp.isUsuallyPrecededBySpace('{')); + assertFalse("V Line", sp.isUsuallyPrecededBySpace('|')); + assertFalse("R C Brace", sp.isUsuallyPrecededBySpace('}')); + assertFalse("Tilde", sp.isUsuallyPrecededBySpace('~')); + } + + private static void testingStandardPrecededBySpace(final SpacingAndPunctuations sp) { + testingCommonPrecededBySpace(sp); + assertFalse("Exclamation", sp.isUsuallyPrecededBySpace('!')); + assertFalse("Colon", sp.isUsuallyPrecededBySpace(':')); + assertFalse("Semicolon", sp.isUsuallyPrecededBySpace(';')); + assertFalse("Question", sp.isUsuallyPrecededBySpace('?')); + } + + @Test + public void testIsUsuallyPrecededBySpace() { + testingStandardPrecededBySpace(ENGLISH); + testingCommonPrecededBySpace(FRENCH); + assertTrue("Exclamation", FRENCH.isUsuallyPrecededBySpace('!')); + assertTrue("Colon", FRENCH.isUsuallyPrecededBySpace(':')); + assertTrue("Semicolon", FRENCH.isUsuallyPrecededBySpace(';')); + assertTrue("Question", FRENCH.isUsuallyPrecededBySpace('?')); + testingCommonPrecededBySpace(CANADA_FRENCH); + assertFalse("Exclamation", CANADA_FRENCH.isUsuallyPrecededBySpace('!')); + assertTrue("Colon", CANADA_FRENCH.isUsuallyPrecededBySpace(':')); + assertFalse("Semicolon", CANADA_FRENCH.isUsuallyPrecededBySpace(';')); + assertFalse("Question", CANADA_FRENCH.isUsuallyPrecededBySpace('?')); + testingStandardPrecededBySpace(ARMENIA_ARMENIAN); + } + + private static void testingStandardFollowedBySpace(final SpacingAndPunctuations sp) { + assertFalse("Tab", sp.isUsuallyFollowedBySpace('\t')); + assertFalse("Newline", sp.isUsuallyFollowedBySpace('\n')); + assertFalse("Space", sp.isUsuallyFollowedBySpace(' ')); + assertTrue("Exclamation", sp.isUsuallyFollowedBySpace('!')); + assertFalse("Quotation", sp.isUsuallyFollowedBySpace('"')); + assertFalse("Number", sp.isUsuallyFollowedBySpace('#')); + assertFalse("Dollar", sp.isUsuallyFollowedBySpace('$')); + assertFalse("Percent", sp.isUsuallyFollowedBySpace('%')); + assertTrue("Ampersand", sp.isUsuallyFollowedBySpace('&')); + assertFalse("Apostrophe", sp.isUsuallyFollowedBySpace('\'')); + assertFalse("L Paren", sp.isUsuallyFollowedBySpace('(')); + assertTrue("R Paren", sp.isUsuallyFollowedBySpace(')')); + assertFalse("Asterisk", sp.isUsuallyFollowedBySpace('*')); + assertFalse("Plus", sp.isUsuallyFollowedBySpace('+')); + assertTrue("Comma", sp.isUsuallyFollowedBySpace(',')); + assertFalse("Minus", sp.isUsuallyFollowedBySpace('-')); + assertTrue("Period", sp.isUsuallyFollowedBySpace('.')); + assertFalse("Slash", sp.isUsuallyFollowedBySpace('/')); + assertTrue("Colon", sp.isUsuallyFollowedBySpace(':')); + assertTrue("Semicolon", sp.isUsuallyFollowedBySpace(';')); + assertFalse("L Angle", sp.isUsuallyFollowedBySpace('<')); + assertFalse("Equal", sp.isUsuallyFollowedBySpace('=')); + assertFalse("R Angle", sp.isUsuallyFollowedBySpace('>')); + assertTrue("Question", sp.isUsuallyFollowedBySpace('?')); + assertFalse("Atmark", sp.isUsuallyFollowedBySpace('@')); + assertFalse("L S Bracket", sp.isUsuallyFollowedBySpace('[')); + assertFalse("B Slash", sp.isUsuallyFollowedBySpace('\\')); + assertTrue("R S Bracket", sp.isUsuallyFollowedBySpace(']')); + assertFalse("Circumflex", sp.isUsuallyFollowedBySpace('^')); + assertFalse("Underscore", sp.isUsuallyFollowedBySpace('_')); + assertFalse("Grave", sp.isUsuallyFollowedBySpace('`')); + assertFalse("L C Brace", sp.isUsuallyFollowedBySpace('{')); + assertFalse("V Line", sp.isUsuallyFollowedBySpace('|')); + assertTrue("R C Brace", sp.isUsuallyFollowedBySpace('}')); + assertFalse("Tilde", sp.isUsuallyFollowedBySpace('~')); + } + + @Test + public void testIsUsuallyFollowedBySpace() { + testingStandardFollowedBySpace(ENGLISH); + testingStandardFollowedBySpace(FRENCH); + testingStandardFollowedBySpace(CANADA_FRENCH); + testingStandardFollowedBySpace(ARMENIA_ARMENIAN); + assertTrue(ARMENIA_ARMENIAN.isUsuallyFollowedBySpace(ARMENIAN_FULL_STOP)); + assertTrue(ARMENIA_ARMENIAN.isUsuallyFollowedBySpace(ARMENIAN_COMMA)); + } + + private static void testingStandardSentenceSeparator(final SpacingAndPunctuations sp) { + assertFalse("Tab", sp.isUsuallyFollowedBySpace('\t')); + assertFalse("Newline", sp.isUsuallyFollowedBySpace('\n')); + assertFalse("Space", sp.isUsuallyFollowedBySpace(' ')); + assertFalse("Exclamation", sp.isUsuallyFollowedBySpace('!')); + assertFalse("Quotation", sp.isUsuallyFollowedBySpace('"')); + assertFalse("Number", sp.isUsuallyFollowedBySpace('#')); + assertFalse("Dollar", sp.isUsuallyFollowedBySpace('$')); + assertFalse("Percent", sp.isUsuallyFollowedBySpace('%')); + assertFalse("Ampersand", sp.isUsuallyFollowedBySpace('&')); + assertFalse("Apostrophe", sp.isUsuallyFollowedBySpace('\'')); + assertFalse("L Paren", sp.isUsuallyFollowedBySpace('(')); + assertFalse("R Paren", sp.isUsuallyFollowedBySpace(')')); + assertFalse("Asterisk", sp.isUsuallyFollowedBySpace('*')); + assertFalse("Plus", sp.isUsuallyFollowedBySpace('+')); + assertFalse("Comma", sp.isUsuallyFollowedBySpace(',')); + assertFalse("Minus", sp.isUsuallyFollowedBySpace('-')); + assertTrue("Period", sp.isUsuallyFollowedBySpace('.')); + assertFalse("Slash", sp.isUsuallyFollowedBySpace('/')); + assertFalse("Colon", sp.isUsuallyFollowedBySpace(':')); + assertFalse("Semicolon", sp.isUsuallyFollowedBySpace(';')); + assertFalse("L Angle", sp.isUsuallyFollowedBySpace('<')); + assertFalse("Equal", sp.isUsuallyFollowedBySpace('=')); + assertFalse("R Angle", sp.isUsuallyFollowedBySpace('>')); + assertFalse("Question", sp.isUsuallyFollowedBySpace('?')); + assertFalse("Atmark", sp.isUsuallyFollowedBySpace('@')); + assertFalse("L S Bracket", sp.isUsuallyFollowedBySpace('[')); + assertFalse("B Slash", sp.isUsuallyFollowedBySpace('\\')); + assertFalse("R S Bracket", sp.isUsuallyFollowedBySpace(']')); + assertFalse("Circumflex", sp.isUsuallyFollowedBySpace('^')); + assertFalse("Underscore", sp.isUsuallyFollowedBySpace('_')); + assertFalse("Grave", sp.isUsuallyFollowedBySpace('`')); + assertFalse("L C Brace", sp.isUsuallyFollowedBySpace('{')); + assertFalse("V Line", sp.isUsuallyFollowedBySpace('|')); + assertFalse("R C Brace", sp.isUsuallyFollowedBySpace('}')); + assertFalse("Tilde", sp.isUsuallyFollowedBySpace('~')); + } + + @Test + public void testIsSentenceSeparator() { + testingStandardSentenceSeparator(ENGLISH); + try { + testingStandardSentenceSeparator(ARMENIA_ARMENIAN); + fail("Armenian Sentence Separator"); + } catch (final AssertionFailedError e) { + assertEquals("Period", e.getMessage()); + } + assertTrue(ARMENIA_ARMENIAN.isSentenceSeparator(ARMENIAN_FULL_STOP)); + assertFalse(ARMENIA_ARMENIAN.isSentenceSeparator(ARMENIAN_COMMA)); + } + + @Test + public void testLanguageHasSpace() { + assertTrue(ENGLISH.mCurrentLanguageHasSpaces); + assertTrue(FRENCH.mCurrentLanguageHasSpaces); + assertTrue(GERMAN.mCurrentLanguageHasSpaces); + assertFalse(THAI.mCurrentLanguageHasSpaces); + assertFalse(CAMBODIA_KHMER.mCurrentLanguageHasSpaces); + assertFalse(LAOS_LAO.mCurrentLanguageHasSpaces); + // TODO: We should fix these. + assertTrue(KHMER.mCurrentLanguageHasSpaces); + assertTrue(LAO.mCurrentLanguageHasSpaces); + } + + @Test + public void testUsesAmericanTypography() { + assertTrue(ENGLISH.mUsesAmericanTypography); + assertTrue(UNITED_STATES.mUsesAmericanTypography); + assertTrue(UNITED_KINGDOM.mUsesAmericanTypography); + assertTrue(INDIA_ENGLISH.mUsesAmericanTypography); + assertFalse(FRENCH.mUsesAmericanTypography); + assertFalse(GERMAN.mUsesAmericanTypography); + assertFalse(SWISS_GERMAN.mUsesAmericanTypography); + } + + @Test + public void testUsesGermanRules() { + assertFalse(ENGLISH.mUsesGermanRules); + assertFalse(FRENCH.mUsesGermanRules); + assertTrue(GERMAN.mUsesGermanRules); + assertTrue(SWISS_GERMAN.mUsesGermanRules); + } + + // Punctuations for phone. + private static final String[] PUNCTUATION_LABELS_PHONE = { + "!", "?", ",", ":", ";", "\"", "(", ")", "'", "-", "/", "@", "_" + }; + private static final String[] PUNCTUATION_WORDS_PHONE_LTR = PUNCTUATION_LABELS_PHONE; + private static final String[] PUNCTUATION_WORDS_PHONE_HEBREW = { + "!", "?", ",", ":", ";", "\"", ")", "(", "'", "-", "/", "@", "_" + }; + // U+061F: "؟" ARABIC QUESTION MARK + // U+060C: "،" ARABIC COMMA + // U+061B: "؛" ARABIC SEMICOLON + private static final String[] PUNCTUATION_LABELS_PHONE_ARABIC_PERSIAN = { + "!", "\u061F", "\u060C", ":", "\u061B", "\"", "(", ")", "'", "-", "/", "@", "_" + }; + private static final String[] PUNCTUATION_WORDS_PHONE_ARABIC_PERSIAN = { + "!", "\u061F", "\u060C", ":", "\u061B", "\"", ")", "(", "'", "-", "/", "@", "_" + }; + + // Punctuations for tablet. + private static final String[] PUNCTUATION_LABELS_TABLET = { + ":", ";", "\"", "(", ")", "'", "-", "/", "@", "_" + }; + private static final String[] PUNCTUATION_WORDS_TABLET_LTR = PUNCTUATION_LABELS_TABLET; + private static final String[] PUNCTUATION_WORDS_TABLET_HEBREW = { + ":", ";", "\"", ")", "(", "'", "-", "/", "@", "_" + }; + private static final String[] PUNCTUATION_LABELS_TABLET_ARABIC_PERSIAN = { + "!", "\u061F", ":", "\u061B", "\"", "'", "(", ")", "-", "/", "@", "_" + }; + private static final String[] PUNCTUATION_WORDS_TABLET_ARABIC_PERSIAN = { + "!", "\u061F", ":", "\u061B", "\"", "'", ")", "(", "-", "/", "@", "_" + }; + + private static void testingStandardPunctuationSuggestions(final SpacingAndPunctuations sp, + final String[] punctuationLabels, final String[] punctuationWords) { + final SuggestedWords suggestedWords = sp.mSuggestPuncList; + assertFalse("typedWordValid", suggestedWords.mTypedWordValid); + assertFalse("willAutoCorrect", suggestedWords.mWillAutoCorrect); + assertTrue("isPunctuationSuggestions", suggestedWords.isPunctuationSuggestions()); + assertFalse("isObsoleteSuggestions", suggestedWords.mIsObsoleteSuggestions); + assertFalse("isPrediction", suggestedWords.isPrediction()); + assertEquals("size", punctuationLabels.length, suggestedWords.size()); + for (int index = 0; index < suggestedWords.size(); index++) { + assertEquals("punctuation label at " + index, + punctuationLabels[index], suggestedWords.getLabel(index)); + assertEquals("punctuation word at " + index, + punctuationWords[index], suggestedWords.getWord(index)); + } + } + + @Test + public void testPhonePunctuationSuggestions() { + if (!isPhone()) { + return; + } + testingStandardPunctuationSuggestions(ENGLISH, + PUNCTUATION_LABELS_PHONE, PUNCTUATION_WORDS_PHONE_LTR); + testingStandardPunctuationSuggestions(FRENCH, + PUNCTUATION_LABELS_PHONE, PUNCTUATION_WORDS_PHONE_LTR); + testingStandardPunctuationSuggestions(GERMAN, + PUNCTUATION_LABELS_PHONE, PUNCTUATION_WORDS_PHONE_LTR); + testingStandardPunctuationSuggestions(ARABIC, + PUNCTUATION_LABELS_PHONE_ARABIC_PERSIAN, PUNCTUATION_WORDS_PHONE_ARABIC_PERSIAN); + testingStandardPunctuationSuggestions(PERSIAN, + PUNCTUATION_LABELS_PHONE_ARABIC_PERSIAN, PUNCTUATION_WORDS_PHONE_ARABIC_PERSIAN); + testingStandardPunctuationSuggestions(HEBREW, + PUNCTUATION_LABELS_PHONE, PUNCTUATION_WORDS_PHONE_HEBREW); + } + + @Test + public void testTabletPunctuationSuggestions() { + if (!isTablet()) { + return; + } + testingStandardPunctuationSuggestions(ENGLISH, + PUNCTUATION_LABELS_TABLET, PUNCTUATION_WORDS_TABLET_LTR); + testingStandardPunctuationSuggestions(FRENCH, + PUNCTUATION_LABELS_TABLET, PUNCTUATION_WORDS_TABLET_LTR); + testingStandardPunctuationSuggestions(GERMAN, + PUNCTUATION_LABELS_TABLET, PUNCTUATION_WORDS_TABLET_LTR); + testingStandardPunctuationSuggestions(ARABIC, + PUNCTUATION_LABELS_TABLET_ARABIC_PERSIAN, PUNCTUATION_WORDS_TABLET_ARABIC_PERSIAN); + testingStandardPunctuationSuggestions(PERSIAN, + PUNCTUATION_LABELS_TABLET_ARABIC_PERSIAN, PUNCTUATION_WORDS_TABLET_ARABIC_PERSIAN); + testingStandardPunctuationSuggestions(HEBREW, + PUNCTUATION_LABELS_TABLET, PUNCTUATION_WORDS_TABLET_HEBREW); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java b/tests/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java new file mode 100644 index 000000000..e68670de0 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/spellcheck/AndroidSpellCheckerServiceTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012 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 org.kelar.inputmethod.latin.spellcheck; + +import android.text.style.SuggestionSpan; + +import androidx.test.filters.LargeTest; + +import org.kelar.inputmethod.latin.InputTestsBase; + +@LargeTest +public class AndroidSpellCheckerServiceTest extends InputTestsBase { + public void testSpellchecker() { + changeLanguage("en_US"); + mEditText.setText("tgis "); + mEditText.setSelection(mEditText.getText().length()); + mEditText.onAttachedToWindow(); + sleep(1000); + runMessages(); + sleep(1000); + + final SpanGetter span = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + // If no span, the following will crash + final String[] suggestions = span.getSuggestions(); + // For this test we consider "tgis" should yield at least 2 suggestions (at this moment + // it yields 5). + assertTrue(suggestions.length >= 2); + // We also assume the top suggestion should be "this". + assertEquals("Test basic spell checking", "this", suggestions[0]); + } + + public void testRussianSpellchecker() { + changeLanguage("ru"); + mEditText.onAttachedToWindow(); + mEditText.setText("годп "); + mEditText.setSelection(mEditText.getText().length()); + mEditText.onAttachedToWindow(); + sleep(1000); + runMessages(); + sleep(1000); + + final SpanGetter span = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + // We don't ship with Russian LM + assertNull(span.getSpan()); + } + + public void testSpellcheckWithPeriods() { + changeLanguage("en_US"); + mEditText.setText("I'm.sure "); + mEditText.setSelection(mEditText.getText().length()); + mEditText.onAttachedToWindow(); + sleep(1000); + runMessages(); + sleep(1000); + + final SpanGetter span = new SpanGetter(mEditText.getText(), SuggestionSpan.class); + // If no span, the following will crash + final String[] suggestions = span.getSuggestions(); + // The first suggestion should be "I'm sure". + assertEquals("Test spell checking of mistyped period for space", "I'm sure", + suggestions[0]); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelperTests.java b/tests/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelperTests.java new file mode 100644 index 000000000..e7f140f29 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/suggestions/SuggestionStripLayoutHelperTests.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.suggestions; + +import static junit.framework.TestCase.assertEquals; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.SuggestedWords; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SuggestionStripLayoutHelperTests { + private static void confirmShowTypedWord(final String message, final int inputType) { + assertFalse(message, SuggestionStripLayoutHelper.shouldOmitTypedWord( + inputType, + false /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse(message, SuggestionStripLayoutHelper.shouldOmitTypedWord( + inputType, + true /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse(message, SuggestionStripLayoutHelper.shouldOmitTypedWord( + inputType, + false /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + assertFalse(message, SuggestionStripLayoutHelper.shouldOmitTypedWord( + inputType, + true /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + } + + @Test + public void testShouldShowTypedWord() { + confirmShowTypedWord("no input style", + SuggestedWords.INPUT_STYLE_NONE); + confirmShowTypedWord("application specifed", + SuggestedWords.INPUT_STYLE_APPLICATION_SPECIFIED); + confirmShowTypedWord("recorrection", + SuggestedWords.INPUT_STYLE_RECORRECTION); + } + + @Test + public void testShouldOmitTypedWordWhileTyping() { + assertFalse("typing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TYPING, + false /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse("typing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TYPING, + true /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertTrue("typing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TYPING, + false /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + assertTrue("typing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TYPING, + true /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + } + + @Test + public void testShouldOmitTypedWordWhileGesturing() { + assertFalse("gesturing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_UPDATE_BATCH, + false /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse("gesturing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_UPDATE_BATCH, + true /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse("gesturing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_UPDATE_BATCH, + false /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + assertTrue("gesturing", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_UPDATE_BATCH, + true /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + } + + @Test + public void testShouldOmitTypedWordWhenGestured() { + assertFalse("gestured", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TAIL_BATCH, + false /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertFalse("gestured", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TAIL_BATCH, + true /* gestureFloatingPreviewTextEnabled */, + false /* shouldShowUiToAcceptTypedWord */)); + assertTrue("gestured", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TAIL_BATCH, + false /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + assertTrue("gestured", SuggestionStripLayoutHelper.shouldOmitTypedWord( + SuggestedWords.INPUT_STYLE_TAIL_BATCH, + true /* gestureFloatingPreviewTextEnabled */, + true /* shouldShowUiToAcceptTypedWord */)); + } + + // Note that this unit test assumes that the number of suggested words in the suggestion strip + // is 3. + private static final int POSITION_OMIT = -1; + private static final int POSITION_LEFT = 0; + private static final int POSITION_CENTER = 1; + private static final int POSITION_RIGHT = 2; + + @Test + public void testGetPositionInSuggestionStrip() { + assertEquals("1st word without auto correction", POSITION_CENTER, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_TYPED_WORD /* indexInSuggestedWords */, + false /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("2nd word without auto correction", POSITION_LEFT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_AUTO_CORRECTION /* indexInSuggestedWords */, + false /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("3rd word without auto correction", POSITION_RIGHT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 2 /* indexInSuggestedWords */, + false /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + + assertEquals("typed word with auto correction", POSITION_LEFT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_TYPED_WORD /* indexInSuggestedWords */, + true /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("2nd word with auto correction", POSITION_CENTER, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_AUTO_CORRECTION /* indexInSuggestedWords */, + true /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("3rd word with auto correction", POSITION_RIGHT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 2 /* indexInSuggestedWords */, + true /* willAutoCorrect */, + false /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + + assertEquals("1st word without auto correction", POSITION_OMIT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_TYPED_WORD /* indexInSuggestedWords */, + false /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("2nd word without auto correction", POSITION_CENTER, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_AUTO_CORRECTION /* indexInSuggestedWords */, + false /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("3rd word without auto correction", POSITION_LEFT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 2 /* indexInSuggestedWords */, + false /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("4th word without auto correction", POSITION_RIGHT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 3 /* indexInSuggestedWords */, + false /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + + assertEquals("typed word with auto correction", POSITION_OMIT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_TYPED_WORD /* indexInSuggestedWords */, + true /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("2nd word with auto correction", POSITION_CENTER, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + SuggestedWords.INDEX_OF_AUTO_CORRECTION /* indexInSuggestedWords */, + true /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("3rd word with auto correction", POSITION_LEFT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 2 /* indexInSuggestedWords */, + true /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + assertEquals("4th word with auto correction", POSITION_RIGHT, + SuggestionStripLayoutHelper.getPositionInSuggestionStrip( + 3 /* indexInSuggestedWords */, + true /* willAutoCorrect */, + true /* omitTypedWord */, + POSITION_CENTER /* centerPositionInStrip */, + POSITION_LEFT /* typedWordPositionWhenAutoCorrect */)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java b/tests/src/org/kelar/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java new file mode 100644 index 000000000..85eeaf86c --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/touchinputconsumer/NullGestureConsumerTests.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.touchinputconsumer; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for GestureConsumer.NULL_GESTURE_CONSUMER. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NullGestureConsumerTests { + /** + * Tests that GestureConsumer.NULL_GESTURE_CONSUMER indicates that it won't consume gesture data + * and that its methods don't raise exceptions even for invalid data. + */ + @Test + public void testNullGestureConsumer() { + assertFalse(GestureConsumer.NULL_GESTURE_CONSUMER.willConsume()); + GestureConsumer.NULL_GESTURE_CONSUMER.onInit(null, null); + GestureConsumer.NULL_GESTURE_CONSUMER.onGestureStarted(null, null); + GestureConsumer.NULL_GESTURE_CONSUMER.onGestureCanceled(); + GestureConsumer.NULL_GESTURE_CONSUMER.onGestureCompleted(null); + GestureConsumer.NULL_GESTURE_CONSUMER.onImeSuggestionsProcessed(null, -1, -1, null); + } + + /** + * Tests that newInstance returns NULL_GESTURE_CONSUMER for invalid input. + */ + @Test + public void testNewInstanceGivesNullGestureConsumerForInvalidInputs() { + assertSame(GestureConsumer.NULL_GESTURE_CONSUMER, + GestureConsumer.newInstance(null, null, null, null)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java new file mode 100644 index 000000000..7c929c7a1 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.kelar.inputmethod.latin.common.Constants.Subtype.ExtraValue.ASCII_CAPABLE; +import static org.kelar.inputmethod.latin.common.Constants.Subtype.ExtraValue.EMOJI_CAPABLE; +import static org.kelar.inputmethod.latin.common.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE; +import static org.kelar.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; +import static org.kelar.inputmethod.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME; +import static org.kelar.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.Build; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.compat.InputMethodSubtypeCompatUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AdditionalSubtypeUtilsTests { + + /** + * Predictable subtype ID for en_US dvorak layout. This is actually a hash code calculated as + * follows. + * <code> + * final boolean isAuxiliary = false; + * final boolean overrideImplicitlyEnabledSubtype = false; + * final int SUBTYPE_ID_EN_US_DVORAK = Arrays.hashCode(new Object[] { + * "en_US", + * "keyboard", + * "KeyboardLayoutSet=dvorak" + * + ",AsciiCapable" + * + ",UntranslatableReplacementStringInSubtypeName=Dvorak" + * + ",EmojiCapable" + * + ",isAdditionalSubtype", + * isAuxiliary, + * overrideImplicitlyEnabledSubtype }); + * </code> + */ + private static int SUBTYPE_ID_EN_US_DVORAK = 0xb3c0cc56; + private static String EXTRA_VALUE_EN_US_DVORAK_ICS = + "KeyboardLayoutSet=dvorak" + + ",AsciiCapable" + + ",isAdditionalSubtype"; + private static String EXTRA_VALUE_EN_US_DVORAK_JELLY_BEAN = + "KeyboardLayoutSet=dvorak" + + ",AsciiCapable" + + ",UntranslatableReplacementStringInSubtypeName=Dvorak" + + ",isAdditionalSubtype"; + private static String EXTRA_VALUE_EN_US_DVORAK_KITKAT = + "KeyboardLayoutSet=dvorak" + + ",AsciiCapable" + + ",UntranslatableReplacementStringInSubtypeName=Dvorak" + + ",EmojiCapable" + + ",isAdditionalSubtype"; + + /** + * Predictable subtype ID for azerty layout. This is actually a hash code calculated as follows. + * <code> + * final boolean isAuxiliary = false; + * final boolean overrideImplicitlyEnabledSubtype = false; + * final int SUBTYPE_ID_ZZ_AZERTY = Arrays.hashCode(new Object[] { + * "zz", + * "keyboard", + * "KeyboardLayoutSet=azerty" + * + ",AsciiCapable" + * + ",EmojiCapable" + * + ",isAdditionalSubtype", + * isAuxiliary, + * overrideImplicitlyEnabledSubtype }); + * </code> + */ + private static int SUBTYPE_ID_ZZ_AZERTY = 0x5b6be697; + private static String EXTRA_VALUE_ZZ_AZERTY_ICS = + "KeyboardLayoutSet=azerty" + + ",AsciiCapable" + + ",isAdditionalSubtype"; + private static String EXTRA_VALUE_ZZ_AZERTY_KITKAT = + "KeyboardLayoutSet=azerty" + + ",AsciiCapable" + + ",EmojiCapable" + + ",isAdditionalSubtype"; + + @Before + public void setUp() throws Exception { + final Context context = InstrumentationRegistry.getTargetContext(); + SubtypeLocaleUtils.init(context); + } + + private static void assertEnUsDvorak(InputMethodSubtype subtype) { + assertEquals("en_US", subtype.getLocale()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + assertEquals(EXTRA_VALUE_EN_US_DVORAK_KITKAT, subtype.getExtraValue()); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + assertEquals(EXTRA_VALUE_EN_US_DVORAK_JELLY_BEAN, subtype.getExtraValue()); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + assertEquals(EXTRA_VALUE_EN_US_DVORAK_ICS, subtype.getExtraValue()); + } + assertTrue(subtype.containsExtraValueKey(ASCII_CAPABLE)); + assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)); + // TODO: Enable following test + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapableWithAPI(subtype)); + // } + assertTrue(subtype.containsExtraValueKey(EMOJI_CAPABLE)); + assertTrue(subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE)); + assertEquals("dvorak", subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET)); + assertEquals("Dvorak", subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)); + assertEquals(KEYBOARD_MODE, subtype.getMode()); + assertEquals(SUBTYPE_ID_EN_US_DVORAK, subtype.hashCode()); + } + + private static void assertAzerty(InputMethodSubtype subtype) { + assertEquals("zz", subtype.getLocale()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + assertEquals(EXTRA_VALUE_ZZ_AZERTY_KITKAT, subtype.getExtraValue()); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + assertEquals(EXTRA_VALUE_ZZ_AZERTY_ICS, subtype.getExtraValue()); + } + assertTrue(subtype.containsExtraValueKey(ASCII_CAPABLE)); + assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)); + // TODO: Enable following test + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapableWithAPI(subtype)); + // } + assertTrue(subtype.containsExtraValueKey(EMOJI_CAPABLE)); + assertTrue(subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE)); + assertEquals("azerty", subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET)); + assertFalse(subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)); + assertEquals(KEYBOARD_MODE, subtype.getMode()); + assertEquals(SUBTYPE_ID_ZZ_AZERTY, subtype.hashCode()); + } + + @Test + public void testRestorable() { + final InputMethodSubtype EN_US_DVORAK = + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.US.toString(), "dvorak"); + final InputMethodSubtype ZZ_AZERTY = + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, "azerty"); + assertEnUsDvorak(EN_US_DVORAK); + assertAzerty(ZZ_AZERTY); + + // Make sure the subtype can be stored and restored in a deterministic manner. + final InputMethodSubtype[] subtypes = { EN_US_DVORAK, ZZ_AZERTY }; + final String prefSubtype = AdditionalSubtypeUtils.createPrefSubtypes(subtypes); + final InputMethodSubtype[] restoredSubtypes = + AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype); + assertEquals(2, restoredSubtypes.length); + final InputMethodSubtype restored_EN_US_DVORAK = restoredSubtypes[0]; + final InputMethodSubtype restored_ZZ_AZERTY = restoredSubtypes[1]; + + assertEnUsDvorak(restored_EN_US_DVORAK); + assertAzerty(restored_ZZ_AZERTY); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/AsyncResultHolderTests.java b/tests/src/org/kelar/inputmethod/latin/utils/AsyncResultHolderTests.java new file mode 100644 index 000000000..b85e4f38c --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/AsyncResultHolderTests.java @@ -0,0 +1,84 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; + +import android.util.Log; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class AsyncResultHolderTests { + static final String TAG = AsyncResultHolderTests.class.getSimpleName(); + + private static final int TIMEOUT_IN_MILLISECONDS = 500; + private static final int MARGIN_IN_MILLISECONDS = 250; + private static final int DEFAULT_VALUE = 2; + private static final int SET_VALUE = 1; + + private static <T> void setAfterGivenTime(final AsyncResultHolder<T> holder, final T value, + final long time) { + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + Log.d(TAG, "Exception while sleeping", e); + } + holder.set(value); + } + }).start(); + } + + @Test + public void testGetWithoutSet() { + final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test"); + final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS); + assertEquals(DEFAULT_VALUE, resultValue); + } + + @Test + public void testGetBeforeSet() { + final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test"); + setAfterGivenTime(holder, SET_VALUE, TIMEOUT_IN_MILLISECONDS + MARGIN_IN_MILLISECONDS); + final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS); + assertEquals(DEFAULT_VALUE, resultValue); + } + + @Test + public void testGetAfterSet() { + final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test"); + holder.set(SET_VALUE); + final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS); + assertEquals(SET_VALUE, resultValue); + } + + @Test + public void testGetBeforeTimeout() { + final AsyncResultHolder<Integer> holder = new AsyncResultHolder<>("Test"); + setAfterGivenTime(holder, SET_VALUE, TIMEOUT_IN_MILLISECONDS - MARGIN_IN_MILLISECONDS); + final int resultValue = holder.get(DEFAULT_VALUE, TIMEOUT_IN_MILLISECONDS); + assertEquals(SET_VALUE, resultValue); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/ByteArrayDictBuffer.java b/tests/src/org/kelar/inputmethod/latin/utils/ByteArrayDictBuffer.java new file mode 100644 index 000000000..6c0854824 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/ByteArrayDictBuffer.java @@ -0,0 +1,81 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import org.kelar.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; + +/** + * This class provides an implementation for the FusionDictionary buffer interface that is backed + * by a simpled byte array. It allows to create a binary dictionary in memory. + */ +public final class ByteArrayDictBuffer implements DictBuffer { + private byte[] mBuffer; + private int mPosition; + + public ByteArrayDictBuffer(final byte[] buffer) { + mBuffer = buffer; + mPosition = 0; + } + + @Override + public int readUnsignedByte() { + return mBuffer[mPosition++] & 0xFF; + } + + @Override + public int readUnsignedShort() { + final int retval = readUnsignedByte(); + return (retval << 8) + readUnsignedByte(); + } + + @Override + public int readUnsignedInt24() { + final int retval = readUnsignedShort(); + return (retval << 8) + readUnsignedByte(); + } + + @Override + public int readInt() { + final int retval = readUnsignedShort(); + return (retval << 16) + readUnsignedShort(); + } + + @Override + public int position() { + return mPosition; + } + + @Override + public void position(int position) { + mPosition = position; + } + + @Override + public void put(final byte b) { + mBuffer[mPosition++] = b; + } + + @Override + public int limit() { + return mBuffer.length - 1; + } + + @Override + public int capacity() { + return mBuffer.length; + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/CapsModeUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/CapsModeUtilsTests.java new file mode 100644 index 000000000..75463929f --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/CapsModeUtilsTests.java @@ -0,0 +1,162 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.res.Resources; +import android.text.TextUtils; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.settings.SpacingAndPunctuations; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CapsModeUtilsTests { + private static void onePathForCaps(final CharSequence cs, final int expectedResult, + final int mask, final SpacingAndPunctuations sp, final boolean hasSpaceBefore) { + final int oneTimeResult = expectedResult & mask; + assertEquals("After >" + cs + "<", oneTimeResult, + CapsModeUtils.getCapsMode(cs, mask, sp, hasSpaceBefore)); + } + + private static void allPathsForCaps(final CharSequence cs, final int expectedResult, + final SpacingAndPunctuations sp, final boolean hasSpaceBefore) { + final int c = TextUtils.CAP_MODE_CHARACTERS; + final int w = TextUtils.CAP_MODE_WORDS; + final int s = TextUtils.CAP_MODE_SENTENCES; + onePathForCaps(cs, expectedResult, c | w | s, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, w | s, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, c | s, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, c | w, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, c, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, w, sp, hasSpaceBefore); + onePathForCaps(cs, expectedResult, s, sp, hasSpaceBefore); + } + + @Test + public void testGetCapsMode() { + final int c = TextUtils.CAP_MODE_CHARACTERS; + final int w = TextUtils.CAP_MODE_WORDS; + final int s = TextUtils.CAP_MODE_SENTENCES; + final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(final Resources res) { + return new SpacingAndPunctuations(res); + } + }; + final Resources res = InstrumentationRegistry.getTargetContext().getResources(); + SpacingAndPunctuations sp = job.runInLocale(res, Locale.ENGLISH); + allPathsForCaps("", c | w | s, sp, false); + allPathsForCaps("Word", c, sp, false); + allPathsForCaps("Word.", c, sp, false); + allPathsForCaps("Word ", c | w, sp, false); + allPathsForCaps("Word. ", c | w | s, sp, false); + allPathsForCaps("Word..", c, sp, false); + allPathsForCaps("Word.. ", c | w | s, sp, false); + allPathsForCaps("Word... ", c | w | s, sp, false); + allPathsForCaps("Word ... ", c | w | s, sp, false); + allPathsForCaps("Word . ", c | w, sp, false); + allPathsForCaps("In the U.S ", c | w, sp, false); + allPathsForCaps("In the U.S. ", c | w, sp, false); + allPathsForCaps("Some stuff (e.g. ", c | w, sp, false); + allPathsForCaps("In the U.S.. ", c | w | s, sp, false); + allPathsForCaps("\"Word.\" ", c | w | s, sp, false); + allPathsForCaps("\"Word\". ", c | w | s, sp, false); + allPathsForCaps("\"Word\" ", c | w, sp, false); + + // Test for phantom space + allPathsForCaps("Word", c | w, sp, true); + allPathsForCaps("Word.", c | w | s, sp, true); + + // Tests after some whitespace + allPathsForCaps("Word\n", c | w | s, sp, false); + allPathsForCaps("Word\n", c | w | s, sp, true); + allPathsForCaps("Word\n ", c | w | s, sp, true); + allPathsForCaps("Word.\n", c | w | s, sp, false); + allPathsForCaps("Word.\n", c | w | s, sp, true); + allPathsForCaps("Word.\n ", c | w | s, sp, true); + + sp = job.runInLocale(res, Locale.FRENCH); + allPathsForCaps("\"Word.\" ", c | w, sp, false); + allPathsForCaps("\"Word\". ", c | w | s, sp, false); + allPathsForCaps("\"Word\" ", c | w, sp, false); + + // Test special case for German. German does not capitalize at the start of a + // line when the previous line starts with a comma. It does in other cases. + sp = job.runInLocale(res, Locale.GERMAN); + allPathsForCaps("Liebe Sara,\n", c | w, sp, false); + allPathsForCaps("Liebe Sara,\n", c | w, sp, true); + allPathsForCaps("Liebe Sara, \n ", c | w, sp, false); + allPathsForCaps("Liebe Sara \n ", c | w | s, sp, false); + allPathsForCaps("Liebe Sara.\n ", c | w | s, sp, false); + sp = job.runInLocale(res, Locale.ENGLISH); + allPathsForCaps("Liebe Sara,\n", c | w | s, sp, false); + allPathsForCaps("Liebe Sara,\n", c | w | s, sp, true); + allPathsForCaps("Liebe Sara, \n ", c | w | s, sp, false); + allPathsForCaps("Liebe Sara \n ", c | w | s, sp, false); + allPathsForCaps("Liebe Sara.\n ", c | w | s, sp, false); + + // Test armenian period + sp = job.runInLocale(res, LocaleUtils.constructLocaleFromString("hy_AM")); + assertTrue("Period is not sentence separator in Armenian", + !sp.isSentenceSeparator('.')); + assertTrue("Sentence separator is Armenian period in Armenian", + sp.isSentenceSeparator(0x589)); + // No space : capitalize only if MODE_CHARACTERS + allPathsForCaps("Word", c, sp, false); + allPathsForCaps("Word.", c, sp, false); + // Space, but no armenian period : capitalize if MODE_WORDS but not SENTENCES + allPathsForCaps("Word. ", c | w, sp, false); + // Armenian period : capitalize if MODE_SENTENCES + allPathsForCaps("Word\u0589 ", c | w | s, sp, false); + + // Test for sentence terminators + sp = job.runInLocale(res, Locale.ENGLISH); + allPathsForCaps("Word? ", c | w | s, sp, false); + allPathsForCaps("Word?", c | w | s, sp, true); + allPathsForCaps("Word?", c, sp, false); + allPathsForCaps("Word! ", c | w | s, sp, false); + allPathsForCaps("Word!", c | w | s, sp, true); + allPathsForCaps("Word!", c, sp, false); + allPathsForCaps("Word; ", c | w, sp, false); + allPathsForCaps("Word;", c | w, sp, true); + allPathsForCaps("Word;", c, sp, false); + // Test for sentence terminators in Greek + sp = job.runInLocale(res, LocaleUtils.constructLocaleFromString("el")); + allPathsForCaps("Word? ", c | w | s, sp, false); + allPathsForCaps("Word?", c | w | s, sp, true); + allPathsForCaps("Word?", c, sp, false); + allPathsForCaps("Word! ", c | w | s, sp, false); + allPathsForCaps("Word!", c | w | s, sp, true); + allPathsForCaps("Word!", c, sp, false); + // In Greek ";" is the question mark and it terminates the sentence + allPathsForCaps("Word; ", c | w | s, sp, false); + allPathsForCaps("Word;", c | w | s, sp, true); + allPathsForCaps("Word;", c, sp, false); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/CollectionUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/CollectionUtilsTests.java new file mode 100644 index 000000000..390e88828 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/CollectionUtilsTests.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.CollectionUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tests for {@link CollectionUtils}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CollectionUtilsTests { + /** + * Tests that {@link CollectionUtils#arrayAsList(Object[],int,int)} fails as expected + * with some invalid inputs. + */ + @Test + public void testArrayAsListFailure() { + final String[] array = { "0", "1" }; + // Negative start + try { + CollectionUtils.arrayAsList(array, -1, 1); + fail("Failed to catch start < 0"); + } catch (final IllegalArgumentException e) { + assertEquals("Invalid start: -1 end: 1 with array.length: 2", e.getMessage()); + } + // start > end + try { + CollectionUtils.arrayAsList(array, 1, -1); + fail("Failed to catch start > end"); + } catch (final IllegalArgumentException e) { + assertEquals("Invalid start: 1 end: -1 with array.length: 2", e.getMessage()); + } + // end > array.length + try { + CollectionUtils.arrayAsList(array, 1, 3); + fail("Failed to catch end > array.length"); + } catch (final IllegalArgumentException e) { + assertEquals("Invalid start: 1 end: 3 with array.length: 2", e.getMessage()); + } + } + + /** + * Tests that {@link CollectionUtils#arrayAsList(Object[],int,int)} gives the expected + * results for a few valid inputs. + */ + @Test + public void testArrayAsList() { + final ArrayList<String> empty = new ArrayList<>(); + assertEquals(empty, CollectionUtils.arrayAsList(new String[] {}, 0, 0)); + final String[] array = { "0", "1", "2", "3", "4" }; + assertEquals(empty, CollectionUtils.arrayAsList(array, 0, 0)); + assertEquals(empty, CollectionUtils.arrayAsList(array, 1, 1)); + assertEquals(empty, CollectionUtils.arrayAsList(array, array.length, array.length)); + final ArrayList<String> expected123 = new ArrayList<>(Arrays.asList("1", "2", "3")); + assertEquals(expected123, CollectionUtils.arrayAsList(array, 1, 4)); + } + + /** + * Tests that {@link CollectionUtils#isNullOrEmpty(java.util.Collection)} gives the expected + * results for a few cases. + */ + @Test + public void testIsNullOrEmpty() { + assertTrue(CollectionUtils.isNullOrEmpty((List<String>) null)); + assertTrue(CollectionUtils.isNullOrEmpty((Map<String, String>) null)); + assertTrue(CollectionUtils.isNullOrEmpty(new ArrayList<String>())); + assertTrue(CollectionUtils.isNullOrEmpty(new HashMap<String, String>())); + assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonList("Not empty"))); + assertFalse(CollectionUtils.isNullOrEmpty(Collections.singletonMap("Not", "empty"))); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/DictionaryInfoUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/DictionaryInfoUtilsTests.java new file mode 100644 index 000000000..4c30750f5 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/DictionaryInfoUtilsTests.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.res.Resources; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.settings.SpacingAndPunctuations; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DictionaryInfoUtilsTests { + @Test + public void testLooksValidForDictionaryInsertion() { + final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() { + @Override + protected SpacingAndPunctuations job(final Resources res) { + return new SpacingAndPunctuations(res); + } + }; + final Resources res = InstrumentationRegistry.getTargetContext().getResources(); + final SpacingAndPunctuations sp = job.runInLocale(res, Locale.ENGLISH); + assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("aochaueo", sp)); + assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("", sp)); + assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("ao-ch'aueo", sp)); + assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("2908743256", sp)); + assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("31aochaueo", sp)); + assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("akeo raeoch oerch .", + sp)); + assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("!!!", sp)); + } + + @Test + public void testGetMainDictId() { + assertEquals("main:en", + DictionaryInfoUtils.getMainDictId(LocaleUtils.constructLocaleFromString("en"))); + assertEquals("main:en_us", + DictionaryInfoUtils.getMainDictId(LocaleUtils.constructLocaleFromString("en_US"))); + assertEquals("main:en_gb", + DictionaryInfoUtils.getMainDictId(LocaleUtils.constructLocaleFromString("en_GB"))); + + assertEquals("main:es", + DictionaryInfoUtils.getMainDictId(LocaleUtils.constructLocaleFromString("es"))); + assertEquals("main:es_us", + DictionaryInfoUtils.getMainDictId(LocaleUtils.constructLocaleFromString("es_US"))); + + assertEquals("main:en_us_posix", DictionaryInfoUtils.getMainDictId( + LocaleUtils.constructLocaleFromString("en_US_POSIX"))); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/ExecutorUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/ExecutorUtilsTests.java new file mode 100644 index 000000000..440e527f1 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/ExecutorUtilsTests.java @@ -0,0 +1,65 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; + +import android.util.Log; + +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Unit tests for {@link ExecutorUtils}. + */ +@MediumTest +@RunWith(AndroidJUnit4.class) +public class ExecutorUtilsTests { + private static final String TAG = ExecutorUtilsTests.class.getSimpleName(); + + private static final int NUM_OF_TASKS = 10; + private static final int DELAY_FOR_WAITING_TASKS_MILLISECONDS = 500; + + @Test + public void testExecute() { + final ExecutorService executor = + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD); + final AtomicInteger v = new AtomicInteger(0); + for (int i = 0; i < NUM_OF_TASKS; ++i) { + executor.execute(new Runnable() { + @Override + public void run() { + v.incrementAndGet(); + } + }); + } + try { + executor.awaitTermination(DELAY_FOR_WAITING_TASKS_MILLISECONDS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Log.d(TAG, "Exception while sleeping.", e); + } + + assertEquals(NUM_OF_TASKS, v.get()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/ImportantNoticeUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/ImportantNoticeUtilsTests.java new file mode 100644 index 000000000..65abd16fd --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/ImportantNoticeUtilsTests.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.kelar.inputmethod.latin.utils.ImportantNoticeUtils.KEY_TIMESTAMP_OF_CONTACTS_NOTICE; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.settings.SettingsValues; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@MediumTest +@RunWith(AndroidJUnit4.class) +public class ImportantNoticeUtilsTests { + + private ImportantNoticePreferences mImportantNoticePreferences; + + @Mock private SettingsValues mMockSettingsValues; + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + private static class ImportantNoticePreferences { + private final SharedPreferences mPref; + + private Long mLastTime; + + public ImportantNoticePreferences(final Context context) { + mPref = ImportantNoticeUtils.getImportantNoticePreferences(context); + } + + private Integer getInt(final String key) { + if (mPref.contains(key)) { + return mPref.getInt(key, 0); + } + return null; + } + + public Long getLong(final String key) { + if (mPref.contains(key)) { + return mPref.getLong(key, 0); + } + return null; + } + + private void putInt(final String key, final Integer value) { + if (value == null) { + removePreference(key); + } else { + mPref.edit().putInt(key, value).apply(); + } + } + + private void putLong(final String key, final Long value) { + if (value == null) { + removePreference(key); + } else { + mPref.edit().putLong(key, value).apply(); + } + } + + private void removePreference(final String key) { + mPref.edit().remove(key).apply(); + } + + public void save() { + mLastTime = getLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE); + } + + public void restore() { + putLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE, mLastTime); + } + + public void clear() { + removePreference(KEY_TIMESTAMP_OF_CONTACTS_NOTICE); + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mImportantNoticePreferences = new ImportantNoticePreferences(getContext()); + mImportantNoticePreferences.save(); + when(mMockSettingsValues.isPersonalizationEnabled()).thenReturn(true); + } + + @After + public void tearDown() throws Exception { + mImportantNoticePreferences.restore(); + } + + @Test + public void testPersonalizationSetting() { + mImportantNoticePreferences.clear(); + + // Personalization enabled. + when(mMockSettingsValues.isPersonalizationEnabled()).thenReturn(true); + assertEquals("Current boolean with personalization enabled", true, + ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); + + // Personalization disabled. + when(mMockSettingsValues.isPersonalizationEnabled()).thenReturn(false); + assertEquals("Current boolean with personalization disabled", false, + ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/JsonUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/JsonUtilsTests.java new file mode 100644 index 000000000..5a323ef51 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/JsonUtilsTests.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class JsonUtilsTests { + @Test + public void testJsonUtils() { + final Object[] objs = new Object[] { 1, "aaa", "bbb", 3 }; + final List<Object> objArray = Arrays.asList(objs); + final String str = JsonUtils.listToJsonStr(objArray); + final List<Object> newObjArray = JsonUtils.jsonStrToList(str); + for (int i = 0; i < objs.length; ++i) { + assertEquals(objs[i], newObjArray.get(i)); + } + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/LanguageOnSpacebarUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/LanguageOnSpacebarUtilsTests.java new file mode 100644 index 000000000..007c69fd2 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/LanguageOnSpacebarUtilsTests.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2014 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 org.kelar.inputmethod.latin.utils; + +import static org.kelar.inputmethod.latin.utils.LanguageOnSpacebarUtils.FORMAT_TYPE_FULL_LOCALE; +import static org.kelar.inputmethod.latin.utils.LanguageOnSpacebarUtils.FORMAT_TYPE_LANGUAGE_ONLY; +import static org.kelar.inputmethod.latin.utils.LanguageOnSpacebarUtils.FORMAT_TYPE_NONE; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.RichInputMethodManager; +import org.kelar.inputmethod.latin.RichInputMethodSubtype; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Locale; + +import javax.annotation.Nonnull; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LanguageOnSpacebarUtilsTests { + private RichInputMethodManager mRichImm; + + RichInputMethodSubtype EN_US_QWERTY; + RichInputMethodSubtype EN_GB_QWERTY; + RichInputMethodSubtype FR_AZERTY; + RichInputMethodSubtype FR_CA_QWERTY; + RichInputMethodSubtype FR_CH_SWISS; + RichInputMethodSubtype FR_CH_QWERTY; + RichInputMethodSubtype FR_CH_QWERTZ; + RichInputMethodSubtype IW_HEBREW; + RichInputMethodSubtype ZZ_QWERTY; + + @Before + public void setUp() throws Exception { + final Context context = InstrumentationRegistry.getTargetContext(); + RichInputMethodManager.init(context); + mRichImm = RichInputMethodManager.getInstance(); + + EN_US_QWERTY = findSubtypeOf(Locale.US.toString(), "qwerty"); + EN_GB_QWERTY = findSubtypeOf(Locale.UK.toString(), "qwerty"); + FR_AZERTY = findSubtypeOf(Locale.FRENCH.toString(), "azerty"); + FR_CA_QWERTY = findSubtypeOf(Locale.CANADA_FRENCH.toString(), "qwerty"); + FR_CH_SWISS = findSubtypeOf("fr_CH", "swiss"); + FR_CH_QWERTZ = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype("fr_CH", "qwertz")); + FR_CH_QWERTY = new RichInputMethodSubtype( + AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype("fr_CH", "qwerty")); + IW_HEBREW = findSubtypeOf("iw", "hebrew"); + ZZ_QWERTY = findSubtypeOf(SubtypeLocaleUtils.NO_LANGUAGE, "qwerty"); + } + + @Nonnull + private RichInputMethodSubtype findSubtypeOf(final String localeString, + final String keyboardLayoutSetName) { + final InputMethodSubtype subtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + localeString, keyboardLayoutSetName); + if (subtype == null) { + throw new RuntimeException("Can't find subtype of " + localeString + " with " + + keyboardLayoutSetName); + } + return new RichInputMethodSubtype(subtype); + } + + private static void enableSubtypes(final RichInputMethodSubtype ... subtypes) { + final ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); + for (final RichInputMethodSubtype subtype : subtypes) { + enabledSubtypes.add(subtype.getRawSubtype()); + } + LanguageOnSpacebarUtils.setEnabledSubtypes(enabledSubtypes); + } + + private static void assertFormatType(final RichInputMethodSubtype subtype, + final boolean implicitlyEnabledSubtype, final Locale systemLocale, + final int expectedFormat) { + LanguageOnSpacebarUtils.onSubtypeChanged(subtype, implicitlyEnabledSubtype, systemLocale); + assertEquals(subtype.getLocale() + " implicitly=" + implicitlyEnabledSubtype + + " in " + systemLocale, expectedFormat, + LanguageOnSpacebarUtils.getLanguageOnSpacebarFormatType(subtype)); + } + + @Test + public void testOneSubtypeImplicitlyEnabled() { + enableSubtypes(EN_US_QWERTY); + assertFormatType(EN_US_QWERTY, true, Locale.US, FORMAT_TYPE_NONE); + + enableSubtypes(EN_GB_QWERTY); + assertFormatType(EN_GB_QWERTY, true, Locale.UK, FORMAT_TYPE_NONE); + + enableSubtypes(FR_AZERTY); + assertFormatType(FR_AZERTY, true, Locale.FRANCE, FORMAT_TYPE_NONE); + + enableSubtypes(FR_CA_QWERTY); + assertFormatType(FR_CA_QWERTY, true, Locale.CANADA_FRENCH, FORMAT_TYPE_NONE); + } + + @Test + public void testOneSubtypeExplicitlyEnabled() { + enableSubtypes(EN_US_QWERTY); + assertFormatType(EN_US_QWERTY, false, Locale.UK, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + + enableSubtypes(EN_GB_QWERTY); + assertFormatType(EN_GB_QWERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(EN_GB_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + + enableSubtypes(FR_AZERTY); + assertFormatType(FR_AZERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_AZERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + + enableSubtypes(FR_CA_QWERTY); + assertFormatType(FR_CA_QWERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + } + + @Test + public void testOneSubtypeImplicitlyEnabledWithNoLanguageSubtype() { + final Locale Locale_IW = new Locale("iw"); + enableSubtypes(IW_HEBREW, ZZ_QWERTY); + // TODO: Should this be FORMAT_TYPE_NONE? + assertFormatType(IW_HEBREW, true, Locale_IW, FORMAT_TYPE_LANGUAGE_ONLY); + // TODO: Should this be FORMAT_TYPE_NONE? + assertFormatType(ZZ_QWERTY, true, Locale_IW, FORMAT_TYPE_FULL_LOCALE); + } + + @Test + public void testTwoSubtypesExplicitlyEnabled() { + enableSubtypes(EN_US_QWERTY, FR_AZERTY); + assertFormatType(EN_US_QWERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_AZERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_AZERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(EN_US_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_AZERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + + enableSubtypes(EN_US_QWERTY, ZZ_QWERTY); + assertFormatType(EN_US_QWERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(ZZ_QWERTY, false, Locale.US, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(ZZ_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE); + + } + + @Test + public void testMultiSubtypeWithSameLanuageAndSameLayout() { + // Explicitly enable en_US, en_GB, fr_FR, and no language keyboards. + enableSubtypes(EN_US_QWERTY, EN_GB_QWERTY, FR_CA_QWERTY, ZZ_QWERTY); + + assertFormatType(EN_US_QWERTY, false, Locale.US, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(EN_GB_QWERTY, false, Locale.US, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CA_QWERTY, false, Locale.US, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(ZZ_QWERTY, false, Locale.US, FORMAT_TYPE_FULL_LOCALE); + + assertFormatType(EN_US_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(EN_GB_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(ZZ_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE); + } + + @Test + public void testMultiSubtypesWithSameLanguageButHaveDifferentLayout() { + enableSubtypes(FR_AZERTY, FR_CA_QWERTY, FR_CH_SWISS, FR_CH_QWERTZ); + + assertFormatType(FR_AZERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_SWISS, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTZ, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + + assertFormatType(FR_AZERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_SWISS, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTZ, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + + assertFormatType(FR_AZERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_SWISS, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTZ, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + } + + @Test + public void testMultiSubtypesWithSameLanguageAndMayHaveSameLayout() { + enableSubtypes(FR_AZERTY, FR_CA_QWERTY, FR_CH_SWISS, FR_CH_QWERTY, FR_CH_QWERTZ); + + assertFormatType(FR_AZERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_SWISS, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_QWERTZ, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY); + + assertFormatType(FR_AZERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_SWISS, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_QWERTZ, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY); + + assertFormatType(FR_AZERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_SWISS, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + assertFormatType(FR_CH_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE); + assertFormatType(FR_CH_QWERTZ, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/RecapitalizeStatusTests.java b/tests/src/org/kelar/inputmethod/latin/utils/RecapitalizeStatusTests.java new file mode 100644 index 000000000..1ce091b0b --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/RecapitalizeStatusTests.java @@ -0,0 +1,207 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.common.Constants; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RecapitalizeStatusTests { + private static final int[] SPACE = { Constants.CODE_SPACE }; + + @Test + public void testTrim() { + final RecapitalizeStatus status = new RecapitalizeStatus(); + status.start(30, 40, "abcdefghij", Locale.ENGLISH, SPACE); + status.trim(); + assertEquals("abcdefghij", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(40, status.getNewCursorEnd()); + + status.start(30, 44, " abcdefghij", Locale.ENGLISH, SPACE); + status.trim(); + assertEquals("abcdefghij", status.getRecapitalizedString()); + assertEquals(34, status.getNewCursorStart()); + assertEquals(44, status.getNewCursorEnd()); + + status.start(30, 40, "abcdefgh ", Locale.ENGLISH, SPACE); + status.trim(); + assertEquals("abcdefgh", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(38, status.getNewCursorEnd()); + + status.start(30, 45, " abcdefghij ", Locale.ENGLISH, SPACE); + status.trim(); + assertEquals("abcdefghij", status.getRecapitalizedString()); + assertEquals(33, status.getNewCursorStart()); + assertEquals(43, status.getNewCursorEnd()); + } + + @Test + public void testRotate() { + final RecapitalizeStatus status = new RecapitalizeStatus(); + status.start(29, 40, "abcd efghij", Locale.ENGLISH, SPACE); + status.rotate(); + assertEquals("Abcd Efghij", status.getRecapitalizedString()); + assertEquals(29, status.getNewCursorStart()); + assertEquals(40, status.getNewCursorEnd()); + status.rotate(); + assertEquals("ABCD EFGHIJ", status.getRecapitalizedString()); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("Abcd Efghij", status.getRecapitalizedString()); + + status.start(29, 40, "Abcd Efghij", Locale.ENGLISH, SPACE); + status.rotate(); + assertEquals("ABCD EFGHIJ", status.getRecapitalizedString()); + assertEquals(29, status.getNewCursorStart()); + assertEquals(40, status.getNewCursorEnd()); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("Abcd Efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("ABCD EFGHIJ", status.getRecapitalizedString()); + + status.start(29, 40, "ABCD EFGHIJ", Locale.ENGLISH, SPACE); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + assertEquals(29, status.getNewCursorStart()); + assertEquals(40, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Abcd Efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("ABCD EFGHIJ", status.getRecapitalizedString()); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + + status.start(29, 39, "AbCDefghij", Locale.ENGLISH, SPACE); + status.rotate(); + assertEquals("abcdefghij", status.getRecapitalizedString()); + assertEquals(29, status.getNewCursorStart()); + assertEquals(39, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Abcdefghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("ABCDEFGHIJ", status.getRecapitalizedString()); + status.rotate(); + assertEquals("AbCDefghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("abcdefghij", status.getRecapitalizedString()); + + status.start(29, 40, "Abcd efghij", Locale.ENGLISH, SPACE); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + assertEquals(29, status.getNewCursorStart()); + assertEquals(40, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Abcd Efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("ABCD EFGHIJ", status.getRecapitalizedString()); + status.rotate(); + assertEquals("Abcd efghij", status.getRecapitalizedString()); + status.rotate(); + assertEquals("abcd efghij", status.getRecapitalizedString()); + + status.start(30, 34, "grüß", Locale.GERMAN, SPACE); + status.rotate(); + assertEquals("Grüß", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(34, status.getNewCursorEnd()); + status.rotate(); + assertEquals("GRÜSS", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(35, status.getNewCursorEnd()); + status.rotate(); + assertEquals("grüß", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(34, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Grüß", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(34, status.getNewCursorEnd()); + + status.start(30, 33, "œuf", Locale.FRENCH, SPACE); + status.rotate(); + assertEquals("Œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("ŒUF", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + + status.start(30, 33, "œUf", Locale.FRENCH, SPACE); + status.rotate(); + assertEquals("œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("Œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("ŒUF", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("œUf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + status.rotate(); + assertEquals("œuf", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(33, status.getNewCursorEnd()); + + status.start(30, 35, "école", Locale.FRENCH, SPACE); + status.rotate(); + assertEquals("École", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(35, status.getNewCursorEnd()); + status.rotate(); + assertEquals("ÉCOLE", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(35, status.getNewCursorEnd()); + status.rotate(); + assertEquals("école", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(35, status.getNewCursorEnd()); + status.rotate(); + assertEquals("École", status.getRecapitalizedString()); + assertEquals(30, status.getNewCursorStart()); + assertEquals(35, status.getNewCursorEnd()); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/ResourceUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/ResourceUtilsTests.java new file mode 100644 index 000000000..7e7f24d49 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/ResourceUtilsTests.java @@ -0,0 +1,163 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ResourceUtilsTests { + @Test + public void testFindConstantForKeyValuePairsSimple() { + final HashMap<String,String> anyKeyValue = new HashMap<>(); + anyKeyValue.put("anyKey", "anyValue"); + final HashMap<String,String> nullKeyValue = null; + final HashMap<String,String> emptyKeyValue = new HashMap<>(); + + final String[] nullArray = null; + assertNull(ResourceUtils.findConstantForKeyValuePairs(anyKeyValue, nullArray)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, nullArray)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(nullKeyValue, nullArray)); + + final String[] emptyArray = {}; + assertNull(ResourceUtils.findConstantForKeyValuePairs(anyKeyValue, emptyArray)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, emptyArray)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(nullKeyValue, emptyArray)); + + final String HARDWARE_KEY = "HARDWARE"; + final String[] array = { + ",defaultValue", + "HARDWARE=grouper,0.3", + "HARDWARE=mako,0.4", + "HARDWARE=manta,0.2", + "HARDWARE=mako,0.5", + }; + + final HashMap<String,String> keyValues = new HashMap<>(); + keyValues.put(HARDWARE_KEY, "grouper"); + assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "mako"); + assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "manta"); + assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + + keyValues.clear(); + keyValues.put("hardware", "grouper"); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + + keyValues.clear(); + keyValues.put(HARDWARE_KEY, "MAKO"); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "mantaray"); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + + assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, array)); + } + + @Test + public void testFindConstantForKeyValuePairsCombined() { + final String HARDWARE_KEY = "HARDWARE"; + final String MODEL_KEY = "MODEL"; + final String MANUFACTURER_KEY = "MANUFACTURER"; + final String[] array = { + ",defaultValue", + "no_comma", + "error_pattern,0.1", + "HARDWARE=grouper:MANUFACTURER=asus,0.3", + "HARDWARE=mako:MODEL=Nexus 4,0.4", + "HARDWARE=manta:MODEL=Nexus 10:MANUFACTURER=samsung,0.2" + }; + final String[] failArray = { + ",defaultValue", + "HARDWARE=grouper:MANUFACTURER=ASUS,0.3", + "HARDWARE=mako:MODEL=Nexus_4,0.4", + "HARDWARE=mantaray:MODEL=Nexus 10:MANUFACTURER=samsung,0.2" + }; + + final HashMap<String,String> keyValues = new HashMap<>(); + keyValues.put(HARDWARE_KEY, "grouper"); + keyValues.put(MODEL_KEY, "Nexus 7"); + keyValues.put(MANUFACTURER_KEY, "asus"); + assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray)); + + keyValues.clear(); + keyValues.put(HARDWARE_KEY, "mako"); + keyValues.put(MODEL_KEY, "Nexus 4"); + keyValues.put(MANUFACTURER_KEY, "LGE"); + assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray)); + + keyValues.clear(); + keyValues.put(HARDWARE_KEY, "manta"); + keyValues.put(MODEL_KEY, "Nexus 10"); + keyValues.put(MANUFACTURER_KEY, "samsung"); + assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray)); + keyValues.put(HARDWARE_KEY, "mantaray"); + assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray)); + } + + @Test + public void testFindConstantForKeyValuePairsRegexp() { + final String HARDWARE_KEY = "HARDWARE"; + final String MODEL_KEY = "MODEL"; + final String MANUFACTURER_KEY = "MANUFACTURER"; + final String[] array = { + ",defaultValue", + "no_comma", + "HARDWARE=error_regexp:MANUFACTURER=error[regexp,0.1", + "HARDWARE=grouper|tilapia:MANUFACTURER=asus,0.3", + "HARDWARE=[mM][aA][kK][oO]:MODEL=Nexus 4,0.4", + "HARDWARE=manta.*:MODEL=Nexus 10:MANUFACTURER=samsung,0.2" + }; + + final HashMap<String,String> keyValues = new HashMap<>(); + keyValues.put(HARDWARE_KEY, "grouper"); + keyValues.put(MODEL_KEY, "Nexus 7"); + keyValues.put(MANUFACTURER_KEY, "asus"); + assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "tilapia"); + assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + + keyValues.clear(); + keyValues.put(HARDWARE_KEY, "mako"); + keyValues.put(MODEL_KEY, "Nexus 4"); + keyValues.put(MANUFACTURER_KEY, "LGE"); + assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "MAKO"); + assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + + keyValues.clear(); + keyValues.put(HARDWARE_KEY, "manta"); + keyValues.put(MODEL_KEY, "Nexus 10"); + keyValues.put(MANUFACTURER_KEY, "samsung"); + assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + keyValues.put(HARDWARE_KEY, "mantaray"); + assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array)); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/SpannableStringUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/SpannableStringUtilsTests.java new file mode 100644 index 000000000..f5546a04d --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/SpannableStringUtilsTests.java @@ -0,0 +1,244 @@ +/* + * 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.style.SuggestionSpan; +import android.text.style.URLSpan; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SpannableStringUtilsTests { + + private Context getContext() { + return InstrumentationRegistry.getTargetContext(); + } + + @Test + public void testConcatWithSuggestionSpansOnly() { + SpannableStringBuilder s = new SpannableStringBuilder("test string\ntest string\n" + + "test string\ntest string\ntest string\ntest string\ntest string\ntest string\n" + + "test string\ntest string\n"); + final int N = 10; + for (int i = 0; i < N; ++i) { + // Put a PARAGRAPH-flagged span that should not be found in the result. + s.setSpan(new SuggestionSpan(getContext(), + new String[] {"" + i}, Spanned.SPAN_PARAGRAPH), + i * 12, i * 12 + 12, Spanned.SPAN_PARAGRAPH); + // Put a normal suggestion span that should be found in the result. + s.setSpan(new SuggestionSpan(getContext(), new String[] {"" + i}, 0), i, i * 2, 0); + // Put a URL span than should not be found in the result. + s.setSpan(new URLSpan("http://a"), i, i * 2, 0); + } + + final CharSequence a = s.subSequence(0, 15); + final CharSequence b = s.subSequence(15, s.length()); + final Spanned result = + (Spanned)SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(a, b); + + Object[] spans = result.getSpans(0, result.length(), SuggestionSpan.class); + for (int i = 0; i < spans.length; i++) { + final int flags = result.getSpanFlags(spans[i]); + assertEquals("Should not find a span with PARAGRAPH flag", + flags & Spanned.SPAN_PARAGRAPH, 0); + assertTrue("Should be a SuggestionSpan", spans[i] instanceof SuggestionSpan); + } + } + + private static void assertSpanCount(final int expectedCount, final CharSequence cs) { + final int actualCount; + if (cs instanceof Spanned) { + final Spanned spanned = (Spanned) cs; + actualCount = spanned.getSpans(0, spanned.length(), Object.class).length; + } else { + actualCount = 0; + } + assertEquals(expectedCount, actualCount); + } + + private static void assertSpan(final CharSequence cs, final Object expectedSpan, + final int expectedStart, final int expectedEnd, final int expectedFlags) { + assertTrue(cs instanceof Spanned); + final Spanned spanned = (Spanned) cs; + final Object[] actualSpans = spanned.getSpans(0, spanned.length(), Object.class); + for (Object actualSpan : actualSpans) { + if (actualSpan == expectedSpan) { + final int actualStart = spanned.getSpanStart(actualSpan); + final int actualEnd = spanned.getSpanEnd(actualSpan); + final int actualFlags = spanned.getSpanFlags(actualSpan); + assertEquals(expectedStart, actualStart); + assertEquals(expectedEnd, actualEnd); + assertEquals(expectedFlags, actualFlags); + return; + } + } + assertTrue(false); + } + + @Test + public void testSplitCharSequenceWithSpan() { + // text: " a bcd efg hij " + // span1: ^^^^^^^ + // span2: ^^^^^ + // span3: ^ + final SpannableString spannableString = new SpannableString(" a bcd efg hij "); + final Object span1 = new Object(); + final Object span2 = new Object(); + final Object span3 = new Object(); + final int SPAN1_FLAGS = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE; + final int SPAN2_FLAGS = Spanned.SPAN_EXCLUSIVE_INCLUSIVE; + final int SPAN3_FLAGS = Spanned.SPAN_INCLUSIVE_INCLUSIVE; + spannableString.setSpan(span1, 0, 7, SPAN1_FLAGS); + spannableString.setSpan(span2, 0, 5, SPAN2_FLAGS); + spannableString.setSpan(span3, 12, 13, SPAN3_FLAGS); + final CharSequence[] charSequencesFromSpanned = SpannableStringUtils.split( + spannableString, " ", true /* preserveTrailingEmptySegmengs */); + final CharSequence[] charSequencesFromString = SpannableStringUtils.split( + spannableString.toString(), " ", true /* preserveTrailingEmptySegmengs */); + + + assertEquals(7, charSequencesFromString.length); + assertEquals(7, charSequencesFromSpanned.length); + + // text: "" + // span1: ^ + // span2: ^ + // span3: + assertEquals("", charSequencesFromString[0].toString()); + assertSpanCount(0, charSequencesFromString[0]); + assertEquals("", charSequencesFromSpanned[0].toString()); + assertSpanCount(2, charSequencesFromSpanned[0]); + assertSpan(charSequencesFromSpanned[0], span1, 0, 0, SPAN1_FLAGS); + assertSpan(charSequencesFromSpanned[0], span2, 0, 0, SPAN2_FLAGS); + + // text: "a" + // span1: ^ + // span2: ^ + // span3: + assertEquals("a", charSequencesFromString[1].toString()); + assertSpanCount(0, charSequencesFromString[1]); + assertEquals("a", charSequencesFromSpanned[1].toString()); + assertSpanCount(2, charSequencesFromSpanned[1]); + assertSpan(charSequencesFromSpanned[1], span1, 0, 1, SPAN1_FLAGS); + assertSpan(charSequencesFromSpanned[1], span2, 0, 1, SPAN2_FLAGS); + + // text: "bcd" + // span1: ^^^ + // span2: ^^ + // span3: + assertEquals("bcd", charSequencesFromString[2].toString()); + assertSpanCount(0, charSequencesFromString[2]); + assertEquals("bcd", charSequencesFromSpanned[2].toString()); + assertSpanCount(2, charSequencesFromSpanned[2]); + assertSpan(charSequencesFromSpanned[2], span1, 0, 3, SPAN1_FLAGS); + assertSpan(charSequencesFromSpanned[2], span2, 0, 2, SPAN2_FLAGS); + + // text: "efg" + // span1: + // span2: + // span3: + assertEquals("efg", charSequencesFromString[3].toString()); + assertSpanCount(0, charSequencesFromString[3]); + assertEquals("efg", charSequencesFromSpanned[3].toString()); + assertSpanCount(0, charSequencesFromSpanned[3]); + + // text: "hij" + // span1: + // span2: + // span3: ^ + assertEquals("hij", charSequencesFromString[4].toString()); + assertSpanCount(0, charSequencesFromString[4]); + assertEquals("hij", charSequencesFromSpanned[4].toString()); + assertSpanCount(1, charSequencesFromSpanned[4]); + assertSpan(charSequencesFromSpanned[4], span3, 1, 2, SPAN3_FLAGS); + + // text: "" + // span1: + // span2: + // span3: + assertEquals("", charSequencesFromString[5].toString()); + assertSpanCount(0, charSequencesFromString[5]); + assertEquals("", charSequencesFromSpanned[5].toString()); + assertSpanCount(0, charSequencesFromSpanned[5]); + + // text: "" + // span1: + // span2: + // span3: + assertEquals("", charSequencesFromString[6].toString()); + assertSpanCount(0, charSequencesFromString[6]); + assertEquals("", charSequencesFromSpanned[6].toString()); + assertSpanCount(0, charSequencesFromSpanned[6]); + } + + @Test + public void testSplitCharSequencePreserveTrailingEmptySegmengs() { + assertEquals(1, SpannableStringUtils.split("", " ", + false /* preserveTrailingEmptySegmengs */).length); + assertEquals(1, SpannableStringUtils.split(new SpannedString(""), " ", + false /* preserveTrailingEmptySegmengs */).length); + + assertEquals(1, SpannableStringUtils.split("", " ", + true /* preserveTrailingEmptySegmengs */).length); + assertEquals(1, SpannableStringUtils.split(new SpannedString(""), " ", + true /* preserveTrailingEmptySegmengs */).length); + + assertEquals(0, SpannableStringUtils.split(" ", " ", + false /* preserveTrailingEmptySegmengs */).length); + assertEquals(0, SpannableStringUtils.split(new SpannedString(" "), " ", + false /* preserveTrailingEmptySegmengs */).length); + + assertEquals(2, SpannableStringUtils.split(" ", " ", + true /* preserveTrailingEmptySegmengs */).length); + assertEquals(2, SpannableStringUtils.split(new SpannedString(" "), " ", + true /* preserveTrailingEmptySegmengs */).length); + + assertEquals(3, SpannableStringUtils.split("a b c ", " ", + false /* preserveTrailingEmptySegmengs */).length); + assertEquals(3, SpannableStringUtils.split(new SpannedString("a b c "), " ", + false /* preserveTrailingEmptySegmengs */).length); + + assertEquals(5, SpannableStringUtils.split("a b c ", " ", + true /* preserveTrailingEmptySegmengs */).length); + assertEquals(5, SpannableStringUtils.split(new SpannedString("a b c "), " ", + true /* preserveTrailingEmptySegmengs */).length); + + assertEquals(6, SpannableStringUtils.split("a b ", " ", + false /* preserveTrailingEmptySegmengs */).length); + assertEquals(6, SpannableStringUtils.split(new SpannedString("a b "), " ", + false /* preserveTrailingEmptySegmengs */).length); + + assertEquals(7, SpannableStringUtils.split("a b ", " ", + true /* preserveTrailingEmptySegmengs */).length); + assertEquals(7, SpannableStringUtils.split(new SpannedString("a b "), " ", + true /* preserveTrailingEmptySegmengs */).length); + } +} diff --git a/tests/src/org/kelar/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java b/tests/src/org/kelar/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java new file mode 100644 index 000000000..77b960a95 --- /dev/null +++ b/tests/src/org/kelar/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2011 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 org.kelar.inputmethod.latin.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.content.res.Resources; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.kelar.inputmethod.latin.R; +import org.kelar.inputmethod.latin.RichInputMethodManager; +import org.kelar.inputmethod.latin.RichInputMethodSubtype; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Locale; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SubtypeLocaleUtilsTests { + // All input method subtypes of LatinIME. + private final ArrayList<RichInputMethodSubtype> mSubtypesList = new ArrayList<>(); + + private RichInputMethodManager mRichImm; + private Resources mRes; + private InputMethodSubtype mSavedAddtionalSubtypes[]; + + InputMethodSubtype EN_US; + InputMethodSubtype EN_GB; + InputMethodSubtype ES_US; + InputMethodSubtype FR; + InputMethodSubtype FR_CA; + InputMethodSubtype FR_CH; + InputMethodSubtype DE; + InputMethodSubtype DE_CH; + InputMethodSubtype HI; + InputMethodSubtype SR; + InputMethodSubtype ZZ; + InputMethodSubtype DE_QWERTY; + InputMethodSubtype FR_QWERTZ; + InputMethodSubtype EN_US_AZERTY; + InputMethodSubtype EN_UK_DVORAK; + InputMethodSubtype ES_US_COLEMAK; + InputMethodSubtype ZZ_AZERTY; + InputMethodSubtype ZZ_PC; + + // These are preliminary subtypes and may not exist. + InputMethodSubtype HI_LATN; // Hinglish + InputMethodSubtype SR_LATN; // Serbian Latin + InputMethodSubtype HI_LATN_DVORAK; // Hinglis Dvorak + InputMethodSubtype SR_LATN_QWERTY; // Serbian Latin Qwerty + + @Before + public void setUp() throws Exception { + final Context context = InstrumentationRegistry.getTargetContext(); + mRes = context.getResources(); + RichInputMethodManager.init(context); + mRichImm = RichInputMethodManager.getInstance(); + + // Save and reset additional subtypes + mSavedAddtionalSubtypes = mRichImm.getAdditionalSubtypes(); + final InputMethodSubtype[] predefinedAddtionalSubtypes = + AdditionalSubtypeUtils.createAdditionalSubtypesArray( + AdditionalSubtypeUtils.createPrefSubtypes( + mRes.getStringArray(R.array.predefined_subtypes))); + mRichImm.setAdditionalInputMethodSubtypes(predefinedAddtionalSubtypes); + + final InputMethodInfo imi = mRichImm.getInputMethodInfoOfThisIme(); + final int subtypeCount = imi.getSubtypeCount(); + for (int index = 0; index < subtypeCount; index++) { + final InputMethodSubtype subtype = imi.getSubtypeAt(index); + mSubtypesList.add(new RichInputMethodSubtype(subtype)); + } + + EN_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.US.toString(), "qwerty"); + EN_GB = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.UK.toString(), "qwerty"); + ES_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "es_US", "spanish"); + FR = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.FRENCH.toString(), "azerty"); + FR_CA = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.CANADA_FRENCH.toString(), "qwerty"); + FR_CH = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "fr_CH", "swiss"); + DE = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + Locale.GERMAN.toString(), "qwertz"); + DE_CH = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "de_CH", "swiss"); + HI = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "hi", "hindi"); + SR = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + "sr", "south_slavic"); + ZZ = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, "qwerty"); + DE_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.GERMAN.toString(), "qwerty"); + FR_QWERTZ = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.FRENCH.toString(), "qwertz"); + EN_US_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.US.toString(), "azerty"); + EN_UK_DVORAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + Locale.UK.toString(), "dvorak"); + ES_US_COLEMAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "es_US", "colemak"); + ZZ_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, "azerty"); + ZZ_PC = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty"); + + HI_LATN = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet("hi_ZZ", "qwerty"); + if (HI_LATN != null) { + HI_LATN_DVORAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "hi_ZZ", "dvorak"); + } + SR_LATN = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet("sr_ZZ", "serbian_qwertz"); + if (SR_LATN != null) { + SR_LATN_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( + "sr_ZZ", "qwerty"); + } + } + + @After + public void tearDown() throws Exception { + // Restore additional subtypes. + mRichImm.setAdditionalInputMethodSubtypes(mSavedAddtionalSubtypes); + } + + @Test + public void testAllFullDisplayName() { + for (final RichInputMethodSubtype subtype : mSubtypesList) { + final String subtypeName = SubtypeLocaleUtils + .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype()); + if (subtype.isNoLanguage()) { + final String layoutName = SubtypeLocaleUtils + .getKeyboardLayoutSetDisplayName(subtype.getRawSubtype()); + assertTrue(subtypeName, subtypeName.contains(layoutName)); + } else { + final String languageName = SubtypeLocaleUtils + .getSubtypeLocaleDisplayNameInSystemLocale(subtype.getLocale().toString()); + assertTrue(subtypeName, subtypeName.contains(languageName)); + } + } + } + + @Test + public void testKeyboardLayoutSetName() { + assertEquals("en_US", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(EN_US)); + assertEquals("en_GB", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(EN_GB)); + assertEquals("es_US", "spanish", SubtypeLocaleUtils.getKeyboardLayoutSetName(ES_US)); + assertEquals("fr", "azerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(FR)); + assertEquals("fr_CA", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(FR_CA)); + assertEquals("fr_CH", "swiss", SubtypeLocaleUtils.getKeyboardLayoutSetName(FR_CH)); + assertEquals("de", "qwertz", SubtypeLocaleUtils.getKeyboardLayoutSetName(DE)); + assertEquals("de_CH", "swiss", SubtypeLocaleUtils.getKeyboardLayoutSetName(DE_CH)); + assertEquals("hi", "hindi", SubtypeLocaleUtils.getKeyboardLayoutSetName(HI)); + assertEquals("sr", "south_slavic", SubtypeLocaleUtils.getKeyboardLayoutSetName(SR)); + assertEquals("zz", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(ZZ)); + + assertEquals("de qwerty", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(DE_QWERTY)); + assertEquals("fr qwertz", "qwertz", SubtypeLocaleUtils.getKeyboardLayoutSetName(FR_QWERTZ)); + assertEquals("en_US azerty", "azerty", + SubtypeLocaleUtils.getKeyboardLayoutSetName(EN_US_AZERTY)); + assertEquals("en_UK dvorak", "dvorak", + SubtypeLocaleUtils.getKeyboardLayoutSetName(EN_UK_DVORAK)); + assertEquals("es_US colemak", "colemak", + SubtypeLocaleUtils.getKeyboardLayoutSetName(ES_US_COLEMAK)); + assertEquals("zz azerty", "azerty", + SubtypeLocaleUtils.getKeyboardLayoutSetName(ZZ_AZERTY)); + + // These are preliminary subtypes and may not exist. + if (HI_LATN != null) { + assertEquals("hi_ZZ", "qwerty", SubtypeLocaleUtils.getKeyboardLayoutSetName(HI_LATN)); + assertEquals("hi_ZZ dvorak", "dvorak", + SubtypeLocaleUtils.getKeyboardLayoutSetName(HI_LATN_DVORAK)); + } + if (SR_LATN != null) { + assertEquals("sr_ZZ", "serbian_qwertz", + SubtypeLocaleUtils.getKeyboardLayoutSetName(SR_LATN)); + assertEquals("sr_ZZ qwerty", "qwerty", + SubtypeLocaleUtils.getKeyboardLayoutSetName(SR_LATN_QWERTY)); + } + } + + // InputMethodSubtype's display name in system locale (en_US). + // isAdditionalSubtype (T=true, F=false) + // locale layout | display name + // ------ -------------- - ---------------------- + // en_US qwerty F English (US) exception + // en_GB qwerty F English (UK) exception + // es_US spanish F Spanish (US) exception + // fr azerty F French + // fr_CA qwerty F French (Canada) + // fr_CH swiss F French (Switzerland) + // de qwertz F German + // de_CH swiss F German (Switzerland) + // hi hindi F Hindi + // hi_ZZ qwerty F Hinglish exception + // sr south_slavic F Serbian + // sr_ZZ serbian_qwertz F Serbian (Latin) exception + // zz qwerty F Alphabet (QWERTY) + // fr qwertz T French (QWERTZ) + // de qwerty T German (QWERTY) + // en_US azerty T English (US) (AZERTY) exception + // en_UK dvorak T English (UK) (Dvorak) exception + // es_US colemak T Spanish (US) (Colemak) exception + // hi_ZZ dvorak T Hinglish (Dvorka) exception + // sr_ZZ qwerty T Serbian (QWERTY) exception + // zz pc T Alphabet (PC) + + @Test + public void testPredefinedSubtypesInEnglishSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("en_US", "English (US)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_US)); + assertEquals("en_GB", "English (UK)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_GB)); + assertEquals("es_US", "Spanish (US)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ES_US)); + assertEquals("fr", "French", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR)); + assertEquals("fr_CA", "French (Canada)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_CA)); + assertEquals("fr_CH", "French (Switzerland)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_CH)); + assertEquals("de", "German", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE)); + assertEquals("de_CH", "German (Switzerland)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE_CH)); + assertEquals("hi", "Hindi", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI)); + assertEquals("sr", "Serbian", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR)); + assertEquals("zz", "Alphabet (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ)); + // These are preliminary subtypes and may not exist. + if (HI_LATN != null) { + assertEquals("hi_ZZ", "Hinglish", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN)); + } + if (SR_LATN != null) { + assertEquals("sr_ZZ", "Serbian (Latin)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN)); + } + return null; + } + }; + tests.runInLocale(mRes, Locale.ENGLISH); + } + + @Test + public void testAdditionalSubtypesInEnglishSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("fr qwertz", "French (QWERTZ)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_QWERTZ)); + assertEquals("de qwerty", "German (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE_QWERTY)); + assertEquals("en_US azerty", "English (US) (AZERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_US_AZERTY)); + assertEquals("en_UK dvorak","English (UK) (Dvorak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_UK_DVORAK)); + assertEquals("es_US colemak", "Spanish (US) (Colemak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ES_US_COLEMAK)); + assertEquals("zz azerty", "Alphabet (AZERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ_AZERTY)); + assertEquals("zz pc", "Alphabet (PC)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ_PC)); + // These are preliminary subtypes and may not exist. + if (HI_LATN_DVORAK != null) { + assertEquals("hi_ZZ", "Hinglish (Dvorak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN_DVORAK)); + } + if (SR_LATN_QWERTY != null) { + assertEquals("sr_ZZ", "Serbian (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN_QWERTY)); + } + return null; + } + }; + tests.runInLocale(mRes, Locale.ENGLISH); + } + + // InputMethodSubtype's display name in system locale (fr). + // isAdditionalSubtype (T=true, F=false) + // locale layout | display name + // ------ ------- - ---------------------- + // en_US qwerty F Anglais (États-Unis) exception + // en_GB qwerty F Anglais (Royaume-Uni) exception + // es_US spanish F Espagnol (États-Unis) exception + // fr azerty F Français + // fr_CA qwerty F Français (Canada) + // fr_CH swiss F Français (Suisse) + // de qwertz F Allemand + // de_CH swiss F Allemand (Suisse) + // hi hindi F Hindi exception + // hi_ZZ qwerty F Hindi/Anglais exception + // sr south_slavic F Serbe exception + // sr_ZZ serbian_qwertz F Serbe (latin) exception + // zz qwerty F Alphabet latin (QWERTY) + // fr qwertz T Français (QWERTZ) + // de qwerty T Allemand (QWERTY) + // en_US azerty T Anglais (États-Unis) (AZERTY) exception + // en_UK dvorak T Anglais (Royaume-Uni) (Dvorak) exception + // es_US colemak T Espagnol (États-Unis) (Colemak) exception + // hi_ZZ dvorak T Hindi/Anglais (Dvorka) exception + // sr_ZZ qwerty T Serbe (QWERTY) exception + // zz pc T Alphabet latin (PC) + + @Test + public void testPredefinedSubtypesInFrenchSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("en_US", "Anglais (États-Unis)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_US)); + assertEquals("en_GB", "Anglais (Royaume-Uni)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_GB)); + assertEquals("es_US", "Espagnol (États-Unis)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ES_US)); + assertEquals("fr", "Français", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR)); + assertEquals("fr_CA", "Français (Canada)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_CA)); + assertEquals("fr_CH", "Français (Suisse)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_CH)); + assertEquals("de", "Allemand", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE)); + assertEquals("de_CH", "Allemand (Suisse)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE_CH)); + assertEquals("hi", "Hindi", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI)); + assertEquals("sr", "Serbe", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR)); + assertEquals("zz", "Alphabet latin (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ)); + // These are preliminary subtypes and may not exist. + if (HI_LATN != null) { + assertEquals("hi_ZZ", "Hindi/Anglais", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN)); + } + if (SR_LATN != null) { + assertEquals("sr_ZZ", "Serbe (latin)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN)); + } + return null; + } + }; + tests.runInLocale(mRes, Locale.FRENCH); + } + + @Test + public void testAdditionalSubtypesInFrenchSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job(final Resources res) { + assertEquals("fr qwertz", "Français (QWERTZ)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(FR_QWERTZ)); + assertEquals("de qwerty", "Allemand (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(DE_QWERTY)); + assertEquals("en_US azerty", "Anglais (États-Unis) (AZERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_US_AZERTY)); + assertEquals("en_UK dvorak", "Anglais (Royaume-Uni) (Dvorak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(EN_UK_DVORAK)); + assertEquals("es_US colemak", "Espagnol (États-Unis) (Colemak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ES_US_COLEMAK)); + assertEquals("zz azerty", "Alphabet latin (AZERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ_AZERTY)); + assertEquals("zz pc", "Alphabet latin (PC)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(ZZ_PC)); + // These are preliminary subtypes and may not exist. + if (HI_LATN_DVORAK != null) { + assertEquals("hi_ZZ", "Hindi/Anglais (Dvorak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN_DVORAK)); + } + if (SR_LATN_QWERTY != null) { + assertEquals("sr_ZZ", "Serbe (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN_QWERTY)); + } + return null; + } + }; + tests.runInLocale(mRes, Locale.FRENCH); + } + + // InputMethodSubtype's display name in system locale (hi). + // isAdditionalSubtype (T=true, F=false) + // locale layout | display name + // ------ ------- - ---------------------- + // hi hindi F हिन्दी + // hi_ZZ qwerty F हिंग्लिश + // hi_ZZ dvorak T हिंग्लिश (Dvorak) + + @Test + public void testHinglishSubtypesInHindiSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job (final Resources res) { + assertEquals("hi", "हिन्दी", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI)); + // These are preliminary subtypes and may not exist. + if (HI_LATN != null) { + assertEquals("hi_ZZ", "हिंग्लिश", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN)); + assertEquals("hi_ZZ", "हिंग्लिश (Dvorak)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(HI_LATN_DVORAK)); + } + return null; + } + }; + tests.runInLocale(mRes, new Locale("hi")); + } + + // InputMethodSubtype's display name in system locale (sr). + // isAdditionalSubtype (T=true, F=false) + // locale layout | display name + // ------ -------------- - ---------------------- + // sr south_slavic F Српски + // sr_ZZ serbian_qwertz F Српски (латиница) + // sr_ZZ qwerty T Српски (QWERTY) + + @Test + public void testSerbianLatinSubtypesInSerbianSystemLocale() { + final RunInLocale<Void> tests = new RunInLocale<Void>() { + @Override + protected Void job (final Resources res) { + assertEquals("sr", "Српски", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR)); + // These are preliminary subtypes and may not exist. + if (SR_LATN != null) { + assertEquals("sr_ZZ", "Српски (латиница)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN)); + assertEquals("sr_ZZ", "Српски (QWERTY)", + SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN_QWERTY)); + } + return null; + } + }; + tests.runInLocale(mRes, new Locale("sr")); + } + + @Test + public void testIsRtlLanguage() { + // Known Right-to-Left language subtypes. + final InputMethodSubtype ARABIC = mRichImm + .findSubtypeByLocaleAndKeyboardLayoutSet("ar", "arabic"); + assertNotNull("Arabic", ARABIC); + final InputMethodSubtype FARSI = mRichImm + .findSubtypeByLocaleAndKeyboardLayoutSet("fa", "farsi"); + assertNotNull("Farsi", FARSI); + final InputMethodSubtype HEBREW = mRichImm + .findSubtypeByLocaleAndKeyboardLayoutSet("iw", "hebrew"); + assertNotNull("Hebrew", HEBREW); + + for (final RichInputMethodSubtype subtype : mSubtypesList) { + final InputMethodSubtype rawSubtype = subtype.getRawSubtype(); + final String subtypeName = SubtypeLocaleUtils + .getSubtypeDisplayNameInSystemLocale(rawSubtype); + if (rawSubtype.equals(ARABIC) || rawSubtype.equals(FARSI) + || rawSubtype.equals(HEBREW)) { + assertTrue(subtypeName, subtype.isRtlSubtype()); + } else { + assertFalse(subtypeName, subtype.isRtlSubtype()); + } + } + } +} |