1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.quicksearchbox;
18 
19 import com.android.quicksearchbox.MockTextAppearanceFactory.MockStyleSpan;
20 import com.android.quicksearchbox.util.LevenshteinDistance.Token;
21 
22 import android.test.AndroidTestCase;
23 import android.test.suitebuilder.annotation.SmallTest;
24 import android.text.Spanned;
25 
26 /**
27  * Tests for {@link LevenshteinSuggestionFormatter}.
28  */
29 @SmallTest
30 public class LevenshteinFormatterTest extends AndroidTestCase {
31 
32     private LevenshteinSuggestionFormatter mFormatter;
33     private int mSuggestedStyle;
34     private int mQueryStyle;
35 
36     @Override
setUp()37     protected void setUp() throws Exception {
38         mFormatter = new LevenshteinSuggestionFormatter(new MockTextAppearanceFactory());
39         mSuggestedStyle = MockTextAppearanceFactory.ID_SUGGESTED_TEXT;
40         mQueryStyle = MockTextAppearanceFactory.ID_QUERY_TEXT;
41     }
42 
verifyTokenizeResult(String input, String... output)43     private void verifyTokenizeResult(String input, String... output) {
44         Token[] tokens = mFormatter.tokenize(input);
45         assertEquals(output.length, tokens.length);
46         for (int i=0; i<output.length; ++i) {
47             assertEquals(output[i], tokens[i].toString());
48         }
49     }
50 
testTokenizeNoTokens()51     public void testTokenizeNoTokens() {
52         verifyTokenizeResult("");
53         verifyTokenizeResult("  ");
54     }
55 
testTokenizeSingleToken()56     public void testTokenizeSingleToken() {
57         verifyTokenizeResult("singleToken", "singleToken");
58     }
59 
testTokenizeTwoTokens()60     public void testTokenizeTwoTokens() {
61         verifyTokenizeResult("two tokens", "two", "tokens");
62     }
63 
testTokenizeLeadingSpaces()64     public void testTokenizeLeadingSpaces() {
65         verifyTokenizeResult(" evil kittens", "evil", "kittens");
66         verifyTokenizeResult("        furry lizards", "furry", "lizards");
67     }
68 
testTokenizeTrailingSpaces()69     public void testTokenizeTrailingSpaces() {
70         verifyTokenizeResult("mechanical elephant ", "mechanical", "elephant");
71         verifyTokenizeResult("disappointed dog       ", "disappointed", "dog");
72     }
73 
testTokenizeManySpaces()74     public void testTokenizeManySpaces() {
75         verifyTokenizeResult("happy     horses", "happy", "horses");
76     }
77 
testTokenizeLongSentence()78     public void testTokenizeLongSentence() {
79         verifyTokenizeResult("The fool looks at a finger that points at the sky",
80                 "The", "fool", "looks", "at", "a", "finger", "that", "points", "at", "the", "sky");
81     }
82 
testTokenizeWithPunctuation()83     public void testTokenizeWithPunctuation() {
84         verifyTokenizeResult("Hitchhiker's guide", "Hitchhiker's", "guide");
85         verifyTokenizeResult("full. stop. ", "full.", "stop.");
86         verifyTokenizeResult("' . ; . ..", "'", ".", ";", ".", "..");
87     }
88 
testTokenizeWithTabs()89     public void testTokenizeWithTabs() {
90         verifyTokenizeResult("paranoid\tandroid\t", "paranoid", "android");
91     }
92 
verifyFindMatches(String source, String target, String... newTokensInTarget)93     private void verifyFindMatches(String source, String target, String... newTokensInTarget) {
94         Token[] sourceTokens = mFormatter.tokenize(source);
95         Token[] targetTokens = mFormatter.tokenize(target);
96 
97         int[] matches = mFormatter.findMatches(sourceTokens, targetTokens);
98         assertEquals(targetTokens.length, matches.length);
99         int newTokenCount = 0;
100         int lastSourceToken = -1;
101         for (int i=0; i<targetTokens.length; ++i) {
102 
103             int sourceIdx = matches[i];
104             if (sourceIdx < 0) {
105                 String targetToken = targetTokens[i].toString();
106                 assertTrue("Unexpected new token '" + targetToken + "'",
107                         newTokenCount < newTokensInTarget.length);
108 
109                 assertEquals(newTokensInTarget[newTokenCount], targetToken);
110                 ++newTokenCount;
111             } else {
112                 assertTrue("Source token out of order", lastSourceToken < sourceIdx);
113                 Token srcToken = sourceTokens[sourceIdx];
114                 Token trgToken = targetTokens[i];
115                 assertTrue("'" + srcToken + "' is not a prefix of '" + trgToken + "'",
116                         srcToken.prefixOf(trgToken));
117                 lastSourceToken = sourceIdx;
118             }
119         }
120     }
121 
testFindMatchesSameTokens()122     public void testFindMatchesSameTokens() {
123         verifyFindMatches("", "");
124         verifyFindMatches("one", "one");
125         verifyFindMatches("one two three", "one two three");
126     }
127 
testFindMatchesNewTokens()128     public void testFindMatchesNewTokens() {
129         verifyFindMatches("", "one", "one");
130         verifyFindMatches("one", "one two", "two");
131         verifyFindMatches("one", "one two three", "two", "three");
132         verifyFindMatches("two", "one two three", "one", "three");
133         verifyFindMatches("pictures", "pictures of kittens", "of", "kittens");
134     }
135 
testFindMatchesReplacedTokens()136     public void testFindMatchesReplacedTokens() {
137         verifyFindMatches("one", "two", "two");
138         verifyFindMatches("one", "two three", "two", "three");
139         verifyFindMatches("two", "one three", "one", "three");
140         verifyFindMatches("pictures", "of kittens", "of", "kittens");
141     }
142 
testFindMatchesDuplicateTokens()143     public void testFindMatchesDuplicateTokens() {
144         verifyFindMatches("badger", "badger badger", "badger");
145         verifyFindMatches("badger", "badger badger badger", "badger", "badger");
146         verifyFindMatches("badger badger", "badger badger badger", "badger");
147         verifyFindMatches("badger badger badger", "badger badger badger");
148         // mushroom!
149     }
150 
verifyFormatSuggestion(String query, String suggestion, SpanFormat... spans)151     private void verifyFormatSuggestion(String query, String suggestion, SpanFormat... spans) {
152         Spanned s = mFormatter.formatSuggestion(query, suggestion);
153         for (SpanFormat span : spans) {
154             span.verify(s);
155         }
156     }
157 
testFormatSuggestionEmptyStrings()158     public void testFormatSuggestionEmptyStrings() {
159         verifyFormatSuggestion("", "");
160     }
161 
testFormatSuggestionEmptyQuery()162     public void testFormatSuggestionEmptyQuery() {
163         verifyFormatSuggestion("", "suggestion",
164                 new SpanFormat(0, "suggestion", mSuggestedStyle));
165     }
166 
testFormatSuggestionQuerySuggested()167     public void testFormatSuggestionQuerySuggested() {
168         verifyFormatSuggestion("query", "query",
169                 new SpanFormat(0, "query", mQueryStyle));
170     }
171 
testFormatSuggestionExtraWordsSuggested()172     public void testFormatSuggestionExtraWordsSuggested() {
173         verifyFormatSuggestion("query", "query suggested",
174                 new SpanFormat(0, "query", mQueryStyle),
175                 new SpanFormat(6, "suggested", mSuggestedStyle));
176 
177         verifyFormatSuggestion("pictures", "pictures of kittens",
178                 new SpanFormat(0,  "pictures", mQueryStyle),
179                 new SpanFormat(9,  "of", mSuggestedStyle),
180                 new SpanFormat(12, "kittens", mSuggestedStyle));
181 
182         verifyFormatSuggestion("pictures of", "pictures of kittens dying",
183                 new SpanFormat(0,  "pictures", mQueryStyle),
184                 new SpanFormat(9,  "of", mQueryStyle),
185                 new SpanFormat(12, "kittens", mSuggestedStyle),
186                 new SpanFormat(20, "dying", mSuggestedStyle));
187     }
188 
testFormatSuggestionExtraWordSuggestedAtStart()189     public void testFormatSuggestionExtraWordSuggestedAtStart() {
190         verifyFormatSuggestion("query", "suggested query",
191                 new SpanFormat(0, "suggested", mSuggestedStyle),
192                 new SpanFormat(10, "query", mQueryStyle));
193     }
194 
testFormatSuggestionAlternativeWordSuggested()195     public void testFormatSuggestionAlternativeWordSuggested() {
196         verifyFormatSuggestion("query", "suggested",
197                 new SpanFormat(0, "suggested", mSuggestedStyle));
198     }
199 
testFormatSuggestionDuplicateWords()200     public void testFormatSuggestionDuplicateWords() {
201         verifyFormatSuggestion("", "badger badger",
202                 new SpanFormat(0, "badger", mSuggestedStyle),
203                 new SpanFormat(7, "badger", mSuggestedStyle));
204 
205         verifyFormatSuggestion("badger", "badger badger",
206                 new SpanFormat(0, "badger", mQueryStyle),
207                 new SpanFormat(7, "badger", mSuggestedStyle));
208 
209         verifyFormatSuggestion("badger badger", "badger badger",
210                 new SpanFormat(0, "badger", mQueryStyle),
211                 new SpanFormat(7, "badger", mQueryStyle));
212 
213         verifyFormatSuggestion("badger badger", "badger badger badger",
214                 new SpanFormat(0, "badger", mQueryStyle),
215                 new SpanFormat(7, "badger", mQueryStyle),
216                 new SpanFormat(14, "badger", mSuggestedStyle));
217     }
218 
testFormatSuggestionDuplicateSequences()219     public void testFormatSuggestionDuplicateSequences() {
220         verifyFormatSuggestion("dem bones", "dem bones dem bones",
221                 new SpanFormat(0, "dem", mQueryStyle),
222                 new SpanFormat(4, "bones", mQueryStyle),
223                 new SpanFormat(10, "dem", mSuggestedStyle),
224                 new SpanFormat(14, "bones", mSuggestedStyle)
225         );
226 
227         verifyFormatSuggestion("dem bones", "dem dry bones dem bones",
228                 new SpanFormat(0, "dem", mQueryStyle),
229                 new SpanFormat(4, "dry", mSuggestedStyle),
230                 new SpanFormat(8, "bones", mQueryStyle),
231                 new SpanFormat(14, "dem", mSuggestedStyle),
232                 new SpanFormat(18, "bones", mSuggestedStyle)
233         );
234 
235         verifyFormatSuggestion("dem dry bones", "dry bones dem dry bones dem dry bones",
236                 new SpanFormat(0, "dry", mSuggestedStyle),
237                 new SpanFormat(4, "bones", mSuggestedStyle),
238                 new SpanFormat(10, "dem", mQueryStyle),
239                 new SpanFormat(14, "dry", mQueryStyle),
240                 new SpanFormat(18, "bones", mQueryStyle),
241                 new SpanFormat(24, "dem", mSuggestedStyle),
242                 new SpanFormat(28, "dry", mSuggestedStyle),
243                 new SpanFormat(32, "bones", mSuggestedStyle)
244         );
245     }
246 
testFormatSuggestionWordCompletion()247     public void testFormatSuggestionWordCompletion() {
248         verifyFormatSuggestion("hitch", "hitchhiker",
249                 new SpanFormat(0, "hitch", mQueryStyle),
250                 new SpanFormat(5, "hiker", mSuggestedStyle)
251         );
252 
253         verifyFormatSuggestion("hitch", "hitchhiker's guide",
254                 new SpanFormat(0, "hitch", mQueryStyle),
255                 new SpanFormat(5, "hiker's", mSuggestedStyle),
256                 new SpanFormat(13, "guide", mSuggestedStyle)
257         );
258 
259         verifyFormatSuggestion("hitchhiker's g", "hitchhiker's guide",
260                 new SpanFormat(0, "hitchhiker's", mQueryStyle),
261                 new SpanFormat(13, "g", mQueryStyle),
262                 new SpanFormat(14, "uide", mSuggestedStyle)
263         );
264 
265         verifyFormatSuggestion("hitchhiker's g", "hitchhiker's guide to the galaxy",
266                 new SpanFormat(0, "hitchhiker's", mQueryStyle),
267                 new SpanFormat(13, "g", mQueryStyle),
268                 new SpanFormat(14, "uide", mSuggestedStyle),
269                 new SpanFormat(19, "to", mSuggestedStyle),
270                 new SpanFormat(22, "the", mSuggestedStyle),
271                 new SpanFormat(26, "galaxy", mSuggestedStyle)
272         );
273     }
274 
testFormatSuggestionWordSplitting()275     public void testFormatSuggestionWordSplitting() {
276         verifyFormatSuggestion("dimsum", "dim sum",
277                 new SpanFormat(0, "dim", mSuggestedStyle),
278                 new SpanFormat(4, "sum", mSuggestedStyle)
279         );
280 
281         verifyFormatSuggestion("dimsum london", "dim sum london",
282                 new SpanFormat(0, "dim", mSuggestedStyle),
283                 new SpanFormat(4, "sum", mSuggestedStyle),
284                 new SpanFormat(8, "london", mQueryStyle)
285         );
286 
287         verifyFormatSuggestion("dimsum london", "dim sum london yummy",
288                 new SpanFormat(0, "dim", mSuggestedStyle),
289                 new SpanFormat(4, "sum", mSuggestedStyle),
290                 new SpanFormat(8, "london", mQueryStyle),
291                 new SpanFormat(15, "yummy", mSuggestedStyle)
292         );
293     }
294 
testFormatSuggestionWordCombining()295     public void testFormatSuggestionWordCombining() {
296         verifyFormatSuggestion("hos pital", "hospital",
297                 new SpanFormat(0, "hos", mQueryStyle),
298                 new SpanFormat(3, "pital", mSuggestedStyle)
299         );
300 
301         verifyFormatSuggestion("hos pital", "hospital waiting times",
302                 new SpanFormat(0, "hos", mQueryStyle),
303                 new SpanFormat(3, "pital", mSuggestedStyle),
304                 new SpanFormat(9, "waiting", mSuggestedStyle),
305                 new SpanFormat(17, "times", mSuggestedStyle)
306         );
307 
308         verifyFormatSuggestion("hos pital waiting", "hospital waiting",
309                 new SpanFormat(0, "hos", mQueryStyle),
310                 new SpanFormat(3, "pital", mSuggestedStyle),
311                 new SpanFormat(9, "waiting", mQueryStyle)
312         );
313 
314         verifyFormatSuggestion("hospital wait ing times", "hospital waiting times",
315                 new SpanFormat(0, "hospital", mQueryStyle),
316                 new SpanFormat(9, "wait", mQueryStyle),
317                 new SpanFormat(13, "ing", mSuggestedStyle),
318                 new SpanFormat(17, "times", mQueryStyle)
319         );
320     }
321 
testFormatSuggestionCapitalization()322     public void testFormatSuggestionCapitalization() {
323         verifyFormatSuggestion("Flay", "flay",
324                 new SpanFormat(0, "flay", mQueryStyle));
325 
326         verifyFormatSuggestion("STEERPI", "steerpike",
327                 new SpanFormat(0, "steerpi", mQueryStyle),
328                 new SpanFormat(7, "ke", mSuggestedStyle));
329 
330         verifyFormatSuggestion("STEErpi", "steerpike",
331                 new SpanFormat(0, "steerpi", mQueryStyle),
332                 new SpanFormat(7, "ke", mSuggestedStyle));
333 
334         verifyFormatSuggestion("TITUS", "titus groan",
335                 new SpanFormat(0, "titus", mQueryStyle),
336                 new SpanFormat(6, "groan", mSuggestedStyle));
337 }
338 
339     private class SpanFormat {
340         private final int mStart;
341         private final int mEnd;
342         private final String mExpectedText;
343         private final int mStyle;
SpanFormat(int start, String expectedText, int style)344         public SpanFormat(int start, String expectedText, int style) {
345             mStart = start;
346             mEnd = start + expectedText.length();
347             mExpectedText = expectedText;
348             mStyle = style;
349         }
verify(Spanned spanned)350         public void verify(Spanned spanned) {
351             String spannedText = spanned.subSequence(mStart, mEnd).toString();
352             assertEquals("Test error", mExpectedText, spannedText);
353             MockStyleSpan[] spans = spanned.getSpans(mStart, mEnd, MockStyleSpan.class);
354             assertEquals("Wrong number of spans in '" + spannedText + "'", 1, spans.length);
355             assertEquals("Wrong style for '" + spannedText + "' at position " + mStart,
356                     mStyle, spans[0].getId());
357         }
358     }
359 
360 }
361