1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/chromeos/drive/search_metadata.h" 6 7 #include "base/file_util.h" 8 #include "base/files/scoped_temp_dir.h" 9 #include "base/i18n/string_search.h" 10 #include "base/message_loop/message_loop_proxy.h" 11 #include "base/run_loop.h" 12 #include "base/strings/utf_string_conversions.h" 13 #include "chrome/browser/chromeos/drive/fake_free_disk_space_getter.h" 14 #include "chrome/browser/chromeos/drive/file_cache.h" 15 #include "chrome/browser/chromeos/drive/file_system_util.h" 16 #include "chrome/browser/chromeos/drive/test_util.h" 17 #include "content/public/test/test_browser_thread_bundle.h" 18 #include "testing/gtest/include/gtest/gtest.h" 19 20 namespace drive { 21 namespace internal { 22 23 namespace { 24 25 const int kDefaultAtMostNumMatches = 10; 26 27 // A simple wrapper for testing FindAndHighlightWrapper(). It just converts the 28 // query text parameter to FixedPatternStringSearchIgnoringCaseAndAccents. 29 bool FindAndHighlightWrapper( 30 const std::string& text, 31 const std::string& query_text, 32 std::string* highlighted_text) { 33 base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents query( 34 base::UTF8ToUTF16(query_text)); 35 return FindAndHighlight(text, &query, highlighted_text); 36 } 37 38 } // namespace 39 40 class SearchMetadataTest : public testing::Test { 41 protected: 42 virtual void SetUp() OVERRIDE { 43 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); 44 fake_free_disk_space_getter_.reset(new FakeFreeDiskSpaceGetter); 45 46 metadata_storage_.reset(new ResourceMetadataStorage( 47 temp_dir_.path(), base::MessageLoopProxy::current().get())); 48 ASSERT_TRUE(metadata_storage_->Initialize()); 49 50 cache_.reset(new FileCache(metadata_storage_.get(), 51 temp_dir_.path(), 52 base::MessageLoopProxy::current().get(), 53 fake_free_disk_space_getter_.get())); 54 ASSERT_TRUE(cache_->Initialize()); 55 56 resource_metadata_.reset( 57 new ResourceMetadata(metadata_storage_.get(), 58 cache_.get(), 59 base::MessageLoopProxy::current())); 60 ASSERT_EQ(FILE_ERROR_OK, resource_metadata_->Initialize()); 61 62 AddEntriesToMetadata(); 63 } 64 65 void AddEntriesToMetadata() { 66 base::FilePath temp_file; 67 EXPECT_TRUE(base::CreateTemporaryFileInDir(temp_dir_.path(), &temp_file)); 68 const std::string temp_file_md5 = "md5"; 69 70 ResourceEntry entry; 71 std::string local_id; 72 73 // drive/root 74 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->GetIdByPath( 75 util::GetDriveMyDriveRootPath(), &local_id)); 76 const std::string root_local_id = local_id; 77 78 // drive/root/Directory 1 79 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetDirectoryEntry( 80 "Directory 1", "dir1", 1, root_local_id), &local_id)); 81 const std::string dir1_local_id = local_id; 82 83 // drive/root/Directory 1/SubDirectory File 1.txt 84 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( 85 "SubDirectory File 1.txt", "file1a", 2, dir1_local_id), &local_id)); 86 EXPECT_EQ(FILE_ERROR_OK, cache_->Store( 87 local_id, temp_file_md5, temp_file, FileCache::FILE_OPERATION_COPY)); 88 89 // drive/root/Directory 1/Shared To The Account Owner.txt 90 entry = GetFileEntry( 91 "Shared To The Account Owner.txt", "file1b", 3, dir1_local_id); 92 entry.set_shared_with_me(true); 93 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); 94 95 // drive/root/Directory 2 excludeDir-test 96 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetDirectoryEntry( 97 "Directory 2 excludeDir-test", "dir2", 4, root_local_id), &local_id)); 98 99 // drive/root/Slash \xE2\x88\x95 in directory 100 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry( 101 GetDirectoryEntry("Slash \xE2\x88\x95 in directory", "dir3", 5, 102 root_local_id), &local_id)); 103 const std::string dir3_local_id = local_id; 104 105 // drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt 106 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( 107 "Slash SubDir File.txt", "file3a", 6, dir3_local_id), &local_id)); 108 109 // drive/root/File 2.txt 110 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(GetFileEntry( 111 "File 2.txt", "file2", 7, root_local_id), &local_id)); 112 EXPECT_EQ(FILE_ERROR_OK, cache_->Store( 113 local_id, temp_file_md5, temp_file, FileCache::FILE_OPERATION_COPY)); 114 115 // drive/root/Document 1 excludeDir-test 116 entry = GetFileEntry( 117 "Document 1 excludeDir-test", "doc1", 8, root_local_id); 118 entry.mutable_file_specific_info()->set_is_hosted_document(true); 119 entry.mutable_file_specific_info()->set_document_extension(".gdoc"); 120 EXPECT_EQ(FILE_ERROR_OK, resource_metadata_->AddEntry(entry, &local_id)); 121 122 } 123 124 ResourceEntry GetFileEntry(const std::string& name, 125 const std::string& resource_id, 126 int64 last_accessed, 127 const std::string& parent_local_id) { 128 ResourceEntry entry; 129 entry.set_title(name); 130 entry.set_resource_id(resource_id); 131 entry.set_parent_local_id(parent_local_id); 132 entry.mutable_file_info()->set_last_accessed(last_accessed); 133 return entry; 134 } 135 136 ResourceEntry GetDirectoryEntry(const std::string& name, 137 const std::string& resource_id, 138 int64 last_accessed, 139 const std::string& parent_local_id) { 140 ResourceEntry entry; 141 entry.set_title(name); 142 entry.set_resource_id(resource_id); 143 entry.set_parent_local_id(parent_local_id); 144 entry.mutable_file_info()->set_last_accessed(last_accessed); 145 entry.mutable_file_info()->set_is_directory(true); 146 return entry; 147 } 148 149 content::TestBrowserThreadBundle thread_bundle_; 150 base::ScopedTempDir temp_dir_; 151 scoped_ptr<FakeFreeDiskSpaceGetter> fake_free_disk_space_getter_; 152 scoped_ptr<ResourceMetadataStorage, 153 test_util::DestroyHelperForTests> metadata_storage_; 154 scoped_ptr<ResourceMetadata, test_util::DestroyHelperForTests> 155 resource_metadata_; 156 scoped_ptr<FileCache, test_util::DestroyHelperForTests> cache_; 157 }; 158 159 TEST_F(SearchMetadataTest, SearchMetadata_ZeroMatches) { 160 FileError error = FILE_ERROR_FAILED; 161 scoped_ptr<MetadataSearchResultVector> result; 162 163 SearchMetadata(base::MessageLoopProxy::current(), 164 resource_metadata_.get(), 165 "NonExistent", 166 SEARCH_METADATA_ALL, 167 kDefaultAtMostNumMatches, 168 google_apis::test_util::CreateCopyResultCallback( 169 &error, &result)); 170 base::RunLoop().RunUntilIdle(); 171 EXPECT_EQ(FILE_ERROR_OK, error); 172 ASSERT_TRUE(result); 173 ASSERT_EQ(0U, result->size()); 174 } 175 176 TEST_F(SearchMetadataTest, SearchMetadata_RegularFile) { 177 FileError error = FILE_ERROR_FAILED; 178 scoped_ptr<MetadataSearchResultVector> result; 179 180 SearchMetadata(base::MessageLoopProxy::current(), 181 resource_metadata_.get(), 182 "SubDirectory File 1.txt", 183 SEARCH_METADATA_ALL, 184 kDefaultAtMostNumMatches, 185 google_apis::test_util::CreateCopyResultCallback( 186 &error, &result)); 187 base::RunLoop().RunUntilIdle(); 188 EXPECT_EQ(FILE_ERROR_OK, error); 189 ASSERT_TRUE(result); 190 ASSERT_EQ(1U, result->size()); 191 EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", 192 result->at(0).path.AsUTF8Unsafe()); 193 } 194 195 // This test checks if |FindAndHighlightWrapper| does case-insensitive search. 196 // Tricker test cases for |FindAndHighlightWrapper| can be found below. 197 TEST_F(SearchMetadataTest, SearchMetadata_CaseInsensitiveSearch) { 198 FileError error = FILE_ERROR_FAILED; 199 scoped_ptr<MetadataSearchResultVector> result; 200 201 // The query is all in lower case. 202 SearchMetadata(base::MessageLoopProxy::current(), 203 resource_metadata_.get(), 204 "subdirectory file 1.txt", 205 SEARCH_METADATA_ALL, 206 kDefaultAtMostNumMatches, 207 google_apis::test_util::CreateCopyResultCallback( 208 &error, &result)); 209 base::RunLoop().RunUntilIdle(); 210 EXPECT_EQ(FILE_ERROR_OK, error); 211 ASSERT_TRUE(result); 212 ASSERT_EQ(1U, result->size()); 213 EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", 214 result->at(0).path.AsUTF8Unsafe()); 215 } 216 217 TEST_F(SearchMetadataTest, SearchMetadata_RegularFiles) { 218 FileError error = FILE_ERROR_FAILED; 219 scoped_ptr<MetadataSearchResultVector> result; 220 221 SearchMetadata(base::MessageLoopProxy::current(), 222 resource_metadata_.get(), 223 "SubDir", 224 SEARCH_METADATA_ALL, 225 kDefaultAtMostNumMatches, 226 google_apis::test_util::CreateCopyResultCallback( 227 &error, &result)); 228 base::RunLoop().RunUntilIdle(); 229 EXPECT_EQ(FILE_ERROR_OK, error); 230 ASSERT_TRUE(result); 231 ASSERT_EQ(2U, result->size()); 232 233 // All base names should contain "File". The results should be sorted by the 234 // last accessed time in descending order. 235 EXPECT_EQ("drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt", 236 result->at(0).path.AsUTF8Unsafe()); 237 EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", 238 result->at(1).path.AsUTF8Unsafe()); 239 } 240 241 TEST_F(SearchMetadataTest, SearchMetadata_AtMostOneFile) { 242 FileError error = FILE_ERROR_FAILED; 243 scoped_ptr<MetadataSearchResultVector> result; 244 245 // There are two files matching "SubDir" but only one file should be 246 // returned. 247 SearchMetadata(base::MessageLoopProxy::current(), 248 resource_metadata_.get(), 249 "SubDir", 250 SEARCH_METADATA_ALL, 251 1, // at_most_num_matches 252 google_apis::test_util::CreateCopyResultCallback( 253 &error, &result)); 254 base::RunLoop().RunUntilIdle(); 255 EXPECT_EQ(FILE_ERROR_OK, error); 256 ASSERT_TRUE(result); 257 ASSERT_EQ(1U, result->size()); 258 EXPECT_EQ("drive/root/Slash \xE2\x88\x95 in directory/Slash SubDir File.txt", 259 result->at(0).path.AsUTF8Unsafe()); 260 } 261 262 TEST_F(SearchMetadataTest, SearchMetadata_Directory) { 263 FileError error = FILE_ERROR_FAILED; 264 scoped_ptr<MetadataSearchResultVector> result; 265 266 SearchMetadata(base::MessageLoopProxy::current(), 267 resource_metadata_.get(), 268 "Directory 1", 269 SEARCH_METADATA_ALL, 270 kDefaultAtMostNumMatches, 271 google_apis::test_util::CreateCopyResultCallback( 272 &error, &result)); 273 base::RunLoop().RunUntilIdle(); 274 EXPECT_EQ(FILE_ERROR_OK, error); 275 ASSERT_TRUE(result); 276 ASSERT_EQ(1U, result->size()); 277 EXPECT_EQ("drive/root/Directory 1", result->at(0).path.AsUTF8Unsafe()); 278 } 279 280 TEST_F(SearchMetadataTest, SearchMetadata_HostedDocument) { 281 FileError error = FILE_ERROR_FAILED; 282 scoped_ptr<MetadataSearchResultVector> result; 283 284 SearchMetadata(base::MessageLoopProxy::current(), 285 resource_metadata_.get(), 286 "Document", 287 SEARCH_METADATA_ALL, 288 kDefaultAtMostNumMatches, 289 google_apis::test_util::CreateCopyResultCallback( 290 &error, &result)); 291 base::RunLoop().RunUntilIdle(); 292 EXPECT_EQ(FILE_ERROR_OK, error); 293 ASSERT_TRUE(result); 294 ASSERT_EQ(1U, result->size()); 295 296 EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", 297 result->at(0).path.AsUTF8Unsafe()); 298 } 299 300 TEST_F(SearchMetadataTest, SearchMetadata_ExcludeHostedDocument) { 301 FileError error = FILE_ERROR_FAILED; 302 scoped_ptr<MetadataSearchResultVector> result; 303 304 SearchMetadata(base::MessageLoopProxy::current(), 305 resource_metadata_.get(), 306 "Document", 307 SEARCH_METADATA_EXCLUDE_HOSTED_DOCUMENTS, 308 kDefaultAtMostNumMatches, 309 google_apis::test_util::CreateCopyResultCallback( 310 &error, &result)); 311 base::RunLoop().RunUntilIdle(); 312 EXPECT_EQ(FILE_ERROR_OK, error); 313 ASSERT_TRUE(result); 314 ASSERT_EQ(0U, result->size()); 315 } 316 317 TEST_F(SearchMetadataTest, SearchMetadata_SharedWithMe) { 318 FileError error = FILE_ERROR_FAILED; 319 scoped_ptr<MetadataSearchResultVector> result; 320 321 SearchMetadata(base::MessageLoopProxy::current(), 322 resource_metadata_.get(), 323 "", 324 SEARCH_METADATA_SHARED_WITH_ME, 325 kDefaultAtMostNumMatches, 326 google_apis::test_util::CreateCopyResultCallback( 327 &error, &result)); 328 base::RunLoop().RunUntilIdle(); 329 EXPECT_EQ(FILE_ERROR_OK, error); 330 ASSERT_TRUE(result); 331 ASSERT_EQ(1U, result->size()); 332 EXPECT_EQ("drive/root/Directory 1/Shared To The Account Owner.txt", 333 result->at(0).path.AsUTF8Unsafe()); 334 } 335 336 TEST_F(SearchMetadataTest, SearchMetadata_FileAndDirectory) { 337 FileError error = FILE_ERROR_FAILED; 338 scoped_ptr<MetadataSearchResultVector> result; 339 340 SearchMetadata(base::MessageLoopProxy::current(), 341 resource_metadata_.get(), 342 "excludeDir-test", 343 SEARCH_METADATA_ALL, 344 kDefaultAtMostNumMatches, 345 google_apis::test_util::CreateCopyResultCallback( 346 &error, &result)); 347 348 base::RunLoop().RunUntilIdle(); 349 EXPECT_EQ(FILE_ERROR_OK, error); 350 ASSERT_TRUE(result); 351 ASSERT_EQ(2U, result->size()); 352 353 EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", 354 result->at(0).path.AsUTF8Unsafe()); 355 EXPECT_EQ("drive/root/Directory 2 excludeDir-test", 356 result->at(1).path.AsUTF8Unsafe()); 357 } 358 359 TEST_F(SearchMetadataTest, SearchMetadata_ExcludeDirectory) { 360 FileError error = FILE_ERROR_FAILED; 361 scoped_ptr<MetadataSearchResultVector> result; 362 363 SearchMetadata(base::MessageLoopProxy::current(), 364 resource_metadata_.get(), 365 "excludeDir-test", 366 SEARCH_METADATA_EXCLUDE_DIRECTORIES, 367 kDefaultAtMostNumMatches, 368 google_apis::test_util::CreateCopyResultCallback( 369 &error, &result)); 370 371 base::RunLoop().RunUntilIdle(); 372 EXPECT_EQ(FILE_ERROR_OK, error); 373 ASSERT_TRUE(result); 374 ASSERT_EQ(1U, result->size()); 375 376 EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", 377 result->at(0).path.AsUTF8Unsafe()); 378 } 379 380 // "drive", "drive/root", "drive/other" should be excluded. 381 TEST_F(SearchMetadataTest, SearchMetadata_ExcludeSpecialDirectories) { 382 const char* kQueries[] = { "drive", "root", "other" }; 383 for (size_t i = 0; i < arraysize(kQueries); ++i) { 384 FileError error = FILE_ERROR_FAILED; 385 scoped_ptr<MetadataSearchResultVector> result; 386 387 const std::string query = kQueries[i]; 388 SearchMetadata(base::MessageLoopProxy::current(), 389 resource_metadata_.get(), 390 query, 391 SEARCH_METADATA_ALL, 392 kDefaultAtMostNumMatches, 393 google_apis::test_util::CreateCopyResultCallback( 394 &error, &result)); 395 396 base::RunLoop().RunUntilIdle(); 397 EXPECT_EQ(FILE_ERROR_OK, error); 398 ASSERT_TRUE(result); 399 ASSERT_TRUE(result->empty()) << ": " << query << " should not match"; 400 } 401 } 402 403 TEST_F(SearchMetadataTest, SearchMetadata_Offline) { 404 FileError error = FILE_ERROR_FAILED; 405 scoped_ptr<MetadataSearchResultVector> result; 406 407 SearchMetadata(base::MessageLoopProxy::current(), 408 resource_metadata_.get(), 409 "", 410 SEARCH_METADATA_OFFLINE, 411 kDefaultAtMostNumMatches, 412 google_apis::test_util::CreateCopyResultCallback( 413 &error, &result)); 414 base::RunLoop().RunUntilIdle(); 415 EXPECT_EQ(FILE_ERROR_OK, error); 416 ASSERT_EQ(3U, result->size()); 417 418 // This is not included in the cache but is a hosted document. 419 EXPECT_EQ("drive/root/Document 1 excludeDir-test.gdoc", 420 result->at(0).path.AsUTF8Unsafe()); 421 422 EXPECT_EQ("drive/root/File 2.txt", 423 result->at(1).path.AsUTF8Unsafe()); 424 EXPECT_EQ("drive/root/Directory 1/SubDirectory File 1.txt", 425 result->at(2).path.AsUTF8Unsafe()); 426 } 427 428 TEST(SearchMetadataSimpleTest, FindAndHighlight_ZeroMatches) { 429 std::string highlighted_text; 430 EXPECT_FALSE(FindAndHighlightWrapper("text", "query", &highlighted_text)); 431 } 432 433 TEST(SearchMetadataSimpleTest, FindAndHighlight_EmptyText) { 434 std::string highlighted_text; 435 EXPECT_FALSE(FindAndHighlightWrapper("", "query", &highlighted_text)); 436 } 437 438 TEST(SearchMetadataSimpleTest, FindAndHighlight_FullMatch) { 439 std::string highlighted_text; 440 EXPECT_TRUE(FindAndHighlightWrapper("hello", "hello", &highlighted_text)); 441 EXPECT_EQ("<b>hello</b>", highlighted_text); 442 } 443 444 TEST(SearchMetadataSimpleTest, FindAndHighlight_StartWith) { 445 std::string highlighted_text; 446 EXPECT_TRUE(FindAndHighlightWrapper("hello, world", "hello", 447 &highlighted_text)); 448 EXPECT_EQ("<b>hello</b>, world", highlighted_text); 449 } 450 451 TEST(SearchMetadataSimpleTest, FindAndHighlight_EndWith) { 452 std::string highlighted_text; 453 EXPECT_TRUE(FindAndHighlightWrapper("hello, world", "world", 454 &highlighted_text)); 455 EXPECT_EQ("hello, <b>world</b>", highlighted_text); 456 } 457 458 TEST(SearchMetadataSimpleTest, FindAndHighlight_InTheMiddle) { 459 std::string highlighted_text; 460 EXPECT_TRUE(FindAndHighlightWrapper("yo hello, world", "hello", 461 &highlighted_text)); 462 EXPECT_EQ("yo <b>hello</b>, world", highlighted_text); 463 } 464 465 TEST(SearchMetadataSimpleTest, FindAndHighlight_MultipeMatches) { 466 std::string highlighted_text; 467 EXPECT_TRUE(FindAndHighlightWrapper("yoyoyoyoy", "yoy", &highlighted_text)); 468 // Only the first match is highlighted. 469 EXPECT_EQ("<b>yoy</b>oyoyoy", highlighted_text); 470 } 471 472 TEST(SearchMetadataSimpleTest, FindAndHighlight_IgnoreCase) { 473 std::string highlighted_text; 474 EXPECT_TRUE(FindAndHighlightWrapper("HeLLo", "hello", &highlighted_text)); 475 EXPECT_EQ("<b>HeLLo</b>", highlighted_text); 476 } 477 478 TEST(SearchMetadataSimpleTest, FindAndHighlight_IgnoreCaseNonASCII) { 479 std::string highlighted_text; 480 481 // Case and accent ignorance in Greek. Find "socra" in "Socra'tes". 482 EXPECT_TRUE(FindAndHighlightWrapper( 483 "\xCE\xA3\xCF\x89\xCE\xBA\xCF\x81\xCE\xAC\xCF\x84\xCE\xB7\xCF\x82", 484 "\xCF\x83\xCF\x89\xCE\xBA\xCF\x81\xCE\xB1", &highlighted_text)); 485 EXPECT_EQ( 486 "<b>\xCE\xA3\xCF\x89\xCE\xBA\xCF\x81\xCE\xAC</b>\xCF\x84\xCE\xB7\xCF\x82", 487 highlighted_text); 488 489 // In Japanese characters. 490 // Find Hiragana "pi" + "(small)ya" in Katakana "hi" + semi-voiced-mark + "ya" 491 EXPECT_TRUE(FindAndHighlightWrapper( 492 "\xE3\x81\xB2\xE3\x82\x9A\xE3\x82\x83\xE3\x83\xBC", 493 "\xE3\x83\x94\xE3\x83\xA4", 494 &highlighted_text)); 495 EXPECT_EQ( 496 "<b>\xE3\x81\xB2\xE3\x82\x9A\xE3\x82\x83</b>\xE3\x83\xBC", 497 highlighted_text); 498 } 499 500 TEST(SearchMetadataSimpleTest, MultiTextBySingleQuery) { 501 base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents query( 502 base::UTF8ToUTF16("hello")); 503 504 std::string highlighted_text; 505 EXPECT_TRUE(FindAndHighlight("hello", &query, &highlighted_text)); 506 EXPECT_EQ("<b>hello</b>", highlighted_text); 507 EXPECT_FALSE(FindAndHighlight("goodbye", &query, &highlighted_text)); 508 EXPECT_TRUE(FindAndHighlight("1hello2", &query, &highlighted_text)); 509 EXPECT_EQ("1<b>hello</b>2", highlighted_text); 510 } 511 512 TEST(SearchMetadataSimpleTest, FindAndHighlight_MetaChars) { 513 std::string highlighted_text; 514 EXPECT_TRUE(FindAndHighlightWrapper("<hello>", "hello", &highlighted_text)); 515 EXPECT_EQ("<<b>hello</b>>", highlighted_text); 516 } 517 518 TEST(SearchMetadataSimpleTest, FindAndHighlight_MoreMetaChars) { 519 std::string highlighted_text; 520 EXPECT_TRUE(FindAndHighlightWrapper("a&b&c&d", "b&c", &highlighted_text)); 521 EXPECT_EQ("a&<b>b&c</b>&d", highlighted_text); 522 } 523 524 } // namespace internal 525 } // namespace drive 526