url_fetcher_unittest.cc revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2010 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 "base/message_loop_proxy.h" 6#include "base/thread.h" 7#include "base/waitable_event.h" 8#include "chrome/common/chrome_plugin_lib.h" 9#include "chrome/common/net/url_fetcher.h" 10#include "chrome/common/net/url_fetcher_protect.h" 11#include "chrome/common/net/url_request_context_getter.h" 12#include "net/http/http_response_headers.h" 13#include "net/url_request/url_request_unittest.h" 14#include "net/test/test_server.h" 15#include "testing/gtest/include/gtest/gtest.h" 16 17using base::Time; 18using base::TimeDelta; 19 20// TODO(eroman): Add a regression test for http://crbug.com/40505. 21 22namespace { 23 24const FilePath::CharType kDocRoot[] = FILE_PATH_LITERAL("chrome/test/data"); 25 26class TestURLRequestContextGetter : public URLRequestContextGetter { 27 public: 28 explicit TestURLRequestContextGetter( 29 base::MessageLoopProxy* io_message_loop_proxy) 30 : io_message_loop_proxy_(io_message_loop_proxy) { 31 } 32 virtual URLRequestContext* GetURLRequestContext() { 33 if (!context_) 34 context_ = new TestURLRequestContext(); 35 return context_; 36 } 37 virtual scoped_refptr<base::MessageLoopProxy> GetIOMessageLoopProxy() { 38 return io_message_loop_proxy_; 39 } 40 41 protected: 42 scoped_refptr<base::MessageLoopProxy> io_message_loop_proxy_; 43 44 private: 45 ~TestURLRequestContextGetter() {} 46 47 scoped_refptr<URLRequestContext> context_; 48}; 49 50class URLFetcherTest : public testing::Test, public URLFetcher::Delegate { 51 public: 52 URLFetcherTest() : fetcher_(NULL) { } 53 54 // Creates a URLFetcher, using the program's main thread to do IO. 55 virtual void CreateFetcher(const GURL& url); 56 57 // URLFetcher::Delegate 58 virtual void OnURLFetchComplete(const URLFetcher* source, 59 const GURL& url, 60 const URLRequestStatus& status, 61 int response_code, 62 const ResponseCookies& cookies, 63 const std::string& data); 64 65 scoped_refptr<base::MessageLoopProxy> io_message_loop_proxy() { 66 return io_message_loop_proxy_; 67 } 68 69 protected: 70 virtual void SetUp() { 71 testing::Test::SetUp(); 72 73 io_message_loop_proxy_ = base::MessageLoopProxy::CreateForCurrentThread(); 74 75 // Ensure that any plugin operations done by other tests are cleaned up. 76 ChromePluginLib::UnloadAllPlugins(); 77 } 78 79 // URLFetcher is designed to run on the main UI thread, but in our tests 80 // we assume that the current thread is the IO thread where the URLFetcher 81 // dispatches its requests to. When we wish to simulate being used from 82 // a UI thread, we dispatch a worker thread to do so. 83 MessageLoopForIO io_loop_; 84 scoped_refptr<base::MessageLoopProxy> io_message_loop_proxy_; 85 86 URLFetcher* fetcher_; 87}; 88 89// Version of URLFetcherTest that does a POST instead 90class URLFetcherPostTest : public URLFetcherTest { 91 public: 92 virtual void CreateFetcher(const GURL& url); 93 94 // URLFetcher::Delegate 95 virtual void OnURLFetchComplete(const URLFetcher* source, 96 const GURL& url, 97 const URLRequestStatus& status, 98 int response_code, 99 const ResponseCookies& cookies, 100 const std::string& data); 101}; 102 103// Version of URLFetcherTest that tests headers. 104class URLFetcherHeadersTest : public URLFetcherTest { 105 public: 106 // URLFetcher::Delegate 107 virtual void OnURLFetchComplete(const URLFetcher* source, 108 const GURL& url, 109 const URLRequestStatus& status, 110 int response_code, 111 const ResponseCookies& cookies, 112 const std::string& data); 113}; 114 115// Version of URLFetcherTest that tests overload protection. 116class URLFetcherProtectTest : public URLFetcherTest { 117 public: 118 virtual void CreateFetcher(const GURL& url); 119 // URLFetcher::Delegate 120 virtual void OnURLFetchComplete(const URLFetcher* source, 121 const GURL& url, 122 const URLRequestStatus& status, 123 int response_code, 124 const ResponseCookies& cookies, 125 const std::string& data); 126 private: 127 Time start_time_; 128}; 129 130// Version of URLFetcherTest that tests overload protection, when responses 131// passed through. 132class URLFetcherProtectTestPassedThrough : public URLFetcherTest { 133 public: 134 virtual void CreateFetcher(const GURL& url); 135 // URLFetcher::Delegate 136 virtual void OnURLFetchComplete(const URLFetcher* source, 137 const GURL& url, 138 const URLRequestStatus& status, 139 int response_code, 140 const ResponseCookies& cookies, 141 const std::string& data); 142 private: 143 Time start_time_; 144}; 145 146// Version of URLFetcherTest that tests bad HTTPS requests. 147class URLFetcherBadHTTPSTest : public URLFetcherTest { 148 public: 149 URLFetcherBadHTTPSTest(); 150 151 // URLFetcher::Delegate 152 virtual void OnURLFetchComplete(const URLFetcher* source, 153 const GURL& url, 154 const URLRequestStatus& status, 155 int response_code, 156 const ResponseCookies& cookies, 157 const std::string& data); 158 159 private: 160 FilePath cert_dir_; 161}; 162 163// Version of URLFetcherTest that tests request cancellation on shutdown. 164class URLFetcherCancelTest : public URLFetcherTest { 165 public: 166 virtual void CreateFetcher(const GURL& url); 167 // URLFetcher::Delegate 168 virtual void OnURLFetchComplete(const URLFetcher* source, 169 const GURL& url, 170 const URLRequestStatus& status, 171 int response_code, 172 const ResponseCookies& cookies, 173 const std::string& data); 174 175 void CancelRequest(); 176}; 177 178// Version of TestURLRequestContext that posts a Quit task to the IO 179// thread once it is deleted. 180class CancelTestURLRequestContext : public TestURLRequestContext { 181 virtual ~CancelTestURLRequestContext() { 182 // The d'tor should execute in the IO thread. Post the quit task to the 183 // current thread. 184 MessageLoop::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 185 } 186}; 187 188class CancelTestURLRequestContextGetter : public URLRequestContextGetter { 189 public: 190 explicit CancelTestURLRequestContextGetter( 191 base::MessageLoopProxy* io_message_loop_proxy) 192 : io_message_loop_proxy_(io_message_loop_proxy), 193 context_created_(false, false) { 194 } 195 virtual URLRequestContext* GetURLRequestContext() { 196 if (!context_) { 197 context_ = new CancelTestURLRequestContext(); 198 context_created_.Signal(); 199 } 200 return context_; 201 } 202 virtual scoped_refptr<base::MessageLoopProxy> GetIOMessageLoopProxy() { 203 return io_message_loop_proxy_; 204 } 205 void WaitForContextCreation() { 206 context_created_.Wait(); 207 } 208 209 private: 210 ~CancelTestURLRequestContextGetter() {} 211 212 scoped_refptr<base::MessageLoopProxy> io_message_loop_proxy_; 213 base::WaitableEvent context_created_; 214 scoped_refptr<URLRequestContext> context_; 215}; 216 217// Wrapper that lets us call CreateFetcher() on a thread of our choice. We 218// could make URLFetcherTest refcounted and use PostTask(FROM_HERE.. ) to call 219// CreateFetcher() directly, but the ownership of the URLFetcherTest is a bit 220// confusing in that case because GTest doesn't know about the refcounting. 221// It's less confusing to just do it this way. 222class FetcherWrapperTask : public Task { 223 public: 224 FetcherWrapperTask(URLFetcherTest* test, const GURL& url) 225 : test_(test), url_(url) { } 226 virtual void Run() { 227 test_->CreateFetcher(url_); 228 } 229 230 private: 231 URLFetcherTest* test_; 232 GURL url_; 233}; 234 235void URLFetcherTest::CreateFetcher(const GURL& url) { 236 fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 237 fetcher_->set_request_context(new TestURLRequestContextGetter( 238 io_message_loop_proxy())); 239 fetcher_->Start(); 240} 241 242void URLFetcherTest::OnURLFetchComplete(const URLFetcher* source, 243 const GURL& url, 244 const URLRequestStatus& status, 245 int response_code, 246 const ResponseCookies& cookies, 247 const std::string& data) { 248 EXPECT_TRUE(status.is_success()); 249 EXPECT_EQ(200, response_code); // HTTP OK 250 EXPECT_FALSE(data.empty()); 251 252 delete fetcher_; // Have to delete this here and not in the destructor, 253 // because the destructor won't necessarily run on the 254 // same thread that CreateFetcher() did. 255 256 io_message_loop_proxy()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 257 // If the current message loop is not the IO loop, it will be shut down when 258 // the main loop returns and this thread subsequently goes out of scope. 259} 260 261void URLFetcherPostTest::CreateFetcher(const GURL& url) { 262 fetcher_ = new URLFetcher(url, URLFetcher::POST, this); 263 fetcher_->set_request_context(new TestURLRequestContextGetter( 264 io_message_loop_proxy())); 265 fetcher_->set_upload_data("application/x-www-form-urlencoded", 266 "bobsyeruncle"); 267 fetcher_->Start(); 268} 269 270void URLFetcherPostTest::OnURLFetchComplete(const URLFetcher* source, 271 const GURL& url, 272 const URLRequestStatus& status, 273 int response_code, 274 const ResponseCookies& cookies, 275 const std::string& data) { 276 EXPECT_EQ(std::string("bobsyeruncle"), data); 277 URLFetcherTest::OnURLFetchComplete(source, url, status, response_code, 278 cookies, data); 279} 280 281void URLFetcherHeadersTest::OnURLFetchComplete( 282 const URLFetcher* source, 283 const GURL& url, 284 const URLRequestStatus& status, 285 int response_code, 286 const ResponseCookies& cookies, 287 const std::string& data) { 288 std::string header; 289 EXPECT_TRUE(source->response_headers()->GetNormalizedHeader("cache-control", 290 &header)); 291 EXPECT_EQ("private", header); 292 URLFetcherTest::OnURLFetchComplete(source, url, status, response_code, 293 cookies, data); 294} 295 296void URLFetcherProtectTest::CreateFetcher(const GURL& url) { 297 fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 298 fetcher_->set_request_context(new TestURLRequestContextGetter( 299 io_message_loop_proxy())); 300 start_time_ = Time::Now(); 301 fetcher_->Start(); 302} 303 304void URLFetcherProtectTest::OnURLFetchComplete(const URLFetcher* source, 305 const GURL& url, 306 const URLRequestStatus& status, 307 int response_code, 308 const ResponseCookies& cookies, 309 const std::string& data) { 310 const TimeDelta one_second = TimeDelta::FromMilliseconds(1000); 311 if (response_code >= 500) { 312 // Now running ServerUnavailable test. 313 // It takes more than 1 second to finish all 11 requests. 314 EXPECT_TRUE(Time::Now() - start_time_ >= one_second); 315 EXPECT_TRUE(status.is_success()); 316 EXPECT_FALSE(data.empty()); 317 delete fetcher_; 318 io_message_loop_proxy()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 319 } else { 320 // Now running Overload test. 321 static int count = 0; 322 count++; 323 if (count < 20) { 324 fetcher_->Start(); 325 } else { 326 // We have already sent 20 requests continuously. And we expect that 327 // it takes more than 1 second due to the overload pretection settings. 328 EXPECT_TRUE(Time::Now() - start_time_ >= one_second); 329 URLFetcherTest::OnURLFetchComplete(source, url, status, response_code, 330 cookies, data); 331 } 332 } 333} 334 335void URLFetcherProtectTestPassedThrough::CreateFetcher(const GURL& url) { 336 fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 337 fetcher_->set_request_context(new TestURLRequestContextGetter( 338 io_message_loop_proxy())); 339 fetcher_->set_automatically_retry_on_5xx(false); 340 start_time_ = Time::Now(); 341 fetcher_->Start(); 342} 343 344void URLFetcherProtectTestPassedThrough::OnURLFetchComplete( 345 const URLFetcher* source, 346 const GURL& url, 347 const URLRequestStatus& status, 348 int response_code, 349 const ResponseCookies& cookies, 350 const std::string& data) { 351 const TimeDelta one_minute = TimeDelta::FromMilliseconds(60000); 352 if (response_code >= 500) { 353 // Now running ServerUnavailable test. 354 // It should get here on the first attempt, so almost immediately and 355 // *not* to attempt to execute all 11 requests (2.5 minutes). 356 EXPECT_TRUE(Time::Now() - start_time_ < one_minute); 357 EXPECT_TRUE(status.is_success()); 358 // Check that suggested back off time is bigger than 0. 359 EXPECT_GT(fetcher_->backoff_delay().InMicroseconds(), 0); 360 EXPECT_FALSE(data.empty()); 361 } else { 362 // We should not get here! 363 ADD_FAILURE(); 364 } 365 366 delete fetcher_; 367 io_message_loop_proxy()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 368} 369 370 371URLFetcherBadHTTPSTest::URLFetcherBadHTTPSTest() { 372 PathService::Get(base::DIR_SOURCE_ROOT, &cert_dir_); 373 cert_dir_ = cert_dir_.AppendASCII("chrome"); 374 cert_dir_ = cert_dir_.AppendASCII("test"); 375 cert_dir_ = cert_dir_.AppendASCII("data"); 376 cert_dir_ = cert_dir_.AppendASCII("ssl"); 377 cert_dir_ = cert_dir_.AppendASCII("certificates"); 378} 379 380// The "server certificate expired" error should result in automatic 381// cancellation of the request by 382// URLRequest::Delegate::OnSSLCertificateError. 383void URLFetcherBadHTTPSTest::OnURLFetchComplete( 384 const URLFetcher* source, 385 const GURL& url, 386 const URLRequestStatus& status, 387 int response_code, 388 const ResponseCookies& cookies, 389 const std::string& data) { 390 // This part is different from URLFetcherTest::OnURLFetchComplete 391 // because this test expects the request to be cancelled. 392 EXPECT_EQ(URLRequestStatus::CANCELED, status.status()); 393 EXPECT_EQ(net::ERR_ABORTED, status.os_error()); 394 EXPECT_EQ(-1, response_code); 395 EXPECT_TRUE(cookies.empty()); 396 EXPECT_TRUE(data.empty()); 397 398 // The rest is the same as URLFetcherTest::OnURLFetchComplete. 399 delete fetcher_; 400 io_message_loop_proxy()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 401} 402 403void URLFetcherCancelTest::CreateFetcher(const GURL& url) { 404 fetcher_ = new URLFetcher(url, URLFetcher::GET, this); 405 CancelTestURLRequestContextGetter* context_getter = 406 new CancelTestURLRequestContextGetter(io_message_loop_proxy()); 407 fetcher_->set_request_context(context_getter); 408 fetcher_->Start(); 409 // We need to wait for the creation of the URLRequestContext, since we 410 // rely on it being destroyed as a signal to end the test. 411 context_getter->WaitForContextCreation(); 412 CancelRequest(); 413} 414 415void URLFetcherCancelTest::OnURLFetchComplete(const URLFetcher* source, 416 const GURL& url, 417 const URLRequestStatus& status, 418 int response_code, 419 const ResponseCookies& cookies, 420 const std::string& data) { 421 // We should have cancelled the request before completion. 422 ADD_FAILURE(); 423 delete fetcher_; 424 io_message_loop_proxy()->PostTask(FROM_HERE, new MessageLoop::QuitTask()); 425} 426 427void URLFetcherCancelTest::CancelRequest() { 428 delete fetcher_; 429 // The URLFetcher's test context will post a Quit task once it is 430 // deleted. So if this test simply hangs, it means cancellation 431 // did not work. 432} 433 434TEST_F(URLFetcherTest, SameThreadsTest) { 435 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 436 ASSERT_TRUE(test_server.Start()); 437 438 // Create the fetcher on the main thread. Since IO will happen on the main 439 // thread, this will test URLFetcher's ability to do everything on one 440 // thread. 441 CreateFetcher(test_server.GetURL("defaultresponse")); 442 443 MessageLoop::current()->Run(); 444} 445 446TEST_F(URLFetcherTest, DifferentThreadsTest) { 447 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 448 ASSERT_TRUE(test_server.Start()); 449 450 // Create a separate thread that will create the URLFetcher. The current 451 // (main) thread will do the IO, and when the fetch is complete it will 452 // terminate the main thread's message loop; then the other thread's 453 // message loop will be shut down automatically as the thread goes out of 454 // scope. 455 base::Thread t("URLFetcher test thread"); 456 ASSERT_TRUE(t.Start()); 457 t.message_loop()->PostTask(FROM_HERE, new FetcherWrapperTask(this, 458 test_server.GetURL("defaultresponse"))); 459 460 MessageLoop::current()->Run(); 461} 462 463TEST_F(URLFetcherPostTest, Basic) { 464 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 465 ASSERT_TRUE(test_server.Start()); 466 467 CreateFetcher(test_server.GetURL("echo")); 468 MessageLoop::current()->Run(); 469} 470 471TEST_F(URLFetcherHeadersTest, Headers) { 472 net::TestServer test_server(net::TestServer::TYPE_HTTP, 473 FilePath(FILE_PATH_LITERAL("net/data/url_request_unittest"))); 474 ASSERT_TRUE(test_server.Start()); 475 476 CreateFetcher(test_server.GetURL("files/with-headers.html")); 477 MessageLoop::current()->Run(); 478 // The actual tests are in the URLFetcherHeadersTest fixture. 479} 480 481TEST_F(URLFetcherProtectTest, Overload) { 482 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 483 ASSERT_TRUE(test_server.Start()); 484 485 GURL url(test_server.GetURL("defaultresponse")); 486 487 // Registers an entry for test url. It only allows 3 requests to be sent 488 // in 200 milliseconds. 489 URLFetcherProtectManager* manager = URLFetcherProtectManager::GetInstance(); 490 URLFetcherProtectEntry* entry = 491 new URLFetcherProtectEntry(200, 3, 11, 1, 2.0, 0, 256); 492 manager->Register(url.host(), entry); 493 494 CreateFetcher(url); 495 496 MessageLoop::current()->Run(); 497} 498 499TEST_F(URLFetcherProtectTest, ServerUnavailable) { 500 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 501 ASSERT_TRUE(test_server.Start()); 502 503 GURL url(test_server.GetURL("files/server-unavailable.html")); 504 505 // Registers an entry for test url. The backoff time is calculated by: 506 // new_backoff = 2.0 * old_backoff + 0 507 // and maximum backoff time is 256 milliseconds. 508 // Maximum retries allowed is set to 11. 509 URLFetcherProtectManager* manager = URLFetcherProtectManager::GetInstance(); 510 URLFetcherProtectEntry* entry = 511 new URLFetcherProtectEntry(200, 3, 11, 1, 2.0, 0, 256); 512 manager->Register(url.host(), entry); 513 514 CreateFetcher(url); 515 516 MessageLoop::current()->Run(); 517} 518 519TEST_F(URLFetcherProtectTestPassedThrough, ServerUnavailablePropagateResponse) { 520 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 521 ASSERT_TRUE(test_server.Start()); 522 523 GURL url(test_server.GetURL("files/server-unavailable.html")); 524 525 // Registers an entry for test url. The backoff time is calculated by: 526 // new_backoff = 2.0 * old_backoff + 0 527 // and maximum backoff time is 256 milliseconds. 528 // Maximum retries allowed is set to 11. 529 URLFetcherProtectManager* manager = URLFetcherProtectManager::GetInstance(); 530 // Total time if *not* for not doing automatic backoff would be 150s. 531 // In reality it should be "as soon as server responds". 532 URLFetcherProtectEntry* entry = 533 new URLFetcherProtectEntry(200, 3, 11, 100, 2.0, 0, 150000); 534 manager->Register(url.host(), entry); 535 536 CreateFetcher(url); 537 538 MessageLoop::current()->Run(); 539} 540 541 542TEST_F(URLFetcherBadHTTPSTest, BadHTTPSTest) { 543 net::TestServer test_server(net::TestServer::TYPE_HTTPS_EXPIRED_CERTIFICATE, 544 FilePath(kDocRoot)); 545 ASSERT_TRUE(test_server.Start()); 546 547 CreateFetcher(test_server.GetURL("defaultresponse")); 548 MessageLoop::current()->Run(); 549} 550 551TEST_F(URLFetcherCancelTest, ReleasesContext) { 552 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 553 ASSERT_TRUE(test_server.Start()); 554 555 GURL url(test_server.GetURL("files/server-unavailable.html")); 556 557 // Registers an entry for test url. The backoff time is calculated by: 558 // new_backoff = 2.0 * old_backoff + 0 559 // The initial backoff is 2 seconds and maximum backoff is 4 seconds. 560 // Maximum retries allowed is set to 2. 561 URLFetcherProtectManager* manager = URLFetcherProtectManager::GetInstance(); 562 URLFetcherProtectEntry* entry = 563 new URLFetcherProtectEntry(200, 3, 2, 2000, 2.0, 0, 4000); 564 manager->Register(url.host(), entry); 565 566 // Create a separate thread that will create the URLFetcher. The current 567 // (main) thread will do the IO, and when the fetch is complete it will 568 // terminate the main thread's message loop; then the other thread's 569 // message loop will be shut down automatically as the thread goes out of 570 // scope. 571 base::Thread t("URLFetcher test thread"); 572 ASSERT_TRUE(t.Start()); 573 t.message_loop()->PostTask(FROM_HERE, new FetcherWrapperTask(this, url)); 574 575 MessageLoop::current()->Run(); 576} 577 578TEST_F(URLFetcherCancelTest, CancelWhileDelayedStartTaskPending) { 579 net::TestServer test_server(net::TestServer::TYPE_HTTP, FilePath(kDocRoot)); 580 ASSERT_TRUE(test_server.Start()); 581 582 GURL url(test_server.GetURL("files/server-unavailable.html")); 583 584 // Register an entry for test url. 585 // 586 // Ideally we would mock URLFetcherProtectEntry to return XXX seconds 587 // in response to entry->UpdateBackoff(SEND). 588 // 589 // Unfortunately this function is time sensitive, so we fudge some numbers 590 // to make it at least somewhat likely to have a non-zero deferred 591 // delay when running. 592 // 593 // Using a sliding window of 2 seconds, and max of 1 request, under a fast 594 // run we expect to have a 4 second delay when posting the Start task. 595 URLFetcherProtectManager* manager = URLFetcherProtectManager::GetInstance(); 596 URLFetcherProtectEntry* entry = 597 new URLFetcherProtectEntry(2000, 1, 2, 2000, 2.0, 0, 4000); 598 EXPECT_EQ(0, entry->UpdateBackoff(URLFetcherProtectEntry::SEND)); 599 entry->UpdateBackoff(URLFetcherProtectEntry::SEND); // Returns about 2000. 600 manager->Register(url.host(), entry); 601 602 // The next request we try to send will be delayed by ~4 seconds. 603 // The slower the test runs, the less the delay will be (since it takes the 604 // time difference from now). 605 606 base::Thread t("URLFetcher test thread"); 607 ASSERT_TRUE(t.Start()); 608 t.message_loop()->PostTask(FROM_HERE, new FetcherWrapperTask(this, url)); 609 610 MessageLoop::current()->Run(); 611} 612 613} // namespace. 614