1 /* 2 * Copyright (C) 2009, 2010, 2011 Nicolas Bonnefon and other contributors 3 * 4 * This file is part of glogg. 5 * 6 * glogg is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * glogg is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with glogg. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 #include <QSignalSpy> 21 #include <QMutexLocker> 22 #include <QFile> 23 24 #include "testlogfiltereddata.h" 25 #include "logdata.h" 26 #include "logfiltereddata.h" 27 28 #if !defined( TMPDIR ) 29 #define TMPDIR "/tmp" 30 #endif 31 32 static const qint64 ML_NB_LINES = 15000LL; 33 static const char* ml_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line\t\t%06d\n"; 34 static const int ML_VISIBLE_LINE_LENGTH = (76+8+4+6); // Without the final '\n' ! 35 36 static const qint64 SL_NB_LINES = 2000LL; 37 static const char* sl_format="LOGDATA is a part of glogg, we are going to test it thoroughly, this is line %06d\n"; 38 39 static const char* partial_line_begin = "123... beginning of line."; 40 static const char* partial_line_end = " end of line 123.\n"; 41 42 static const char* partial_nonmatching_line_begin = "Beginning of line."; 43 44 TestLogFilteredData::TestLogFilteredData() : QObject(), 45 loadingFinishedMutex_(), 46 searchProgressedMutex_(), 47 loadingFinishedCondition_(), 48 searchProgressedCondition_() 49 { 50 loadingFinished_received_ = false; 51 loadingFinished_read_ = false; 52 searchProgressed_received_ = false; 53 searchProgressed_read_ = false; 54 } 55 56 void TestLogFilteredData::initTestCase() 57 { 58 QVERIFY( generateDataFiles() ); 59 } 60 61 void TestLogFilteredData::simpleSearch() 62 { 63 logData_ = new LogData(); 64 65 // Register for notification file is loaded 66 connect( logData_, SIGNAL( loadingFinished( bool ) ), 67 this, SLOT( loadingFinished() ) ); 68 69 filteredData_ = logData_->getNewFilteredData(); 70 connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), 71 this, SLOT( searchProgressed( int, int ) ) ); 72 73 QFuture<void> future = QtConcurrent::run(this, &TestLogFilteredData::simpleSearchTest); 74 75 QApplication::exec(); 76 77 disconnect( filteredData_, 0 ); 78 disconnect( logData_, 0 ); 79 80 delete filteredData_; 81 delete logData_; 82 } 83 84 void TestLogFilteredData::simpleSearchTest() 85 { 86 // First load the tests file 87 logData_->attachFile( TMPDIR "/mediumlog.txt" ); 88 // Wait for the loading to be done 89 waitLoadingFinished(); 90 QCOMPARE( logData_->getNbLine(), ML_NB_LINES ); 91 signalLoadingFinishedRead(); 92 93 // Now perform a simple search 94 qint64 matches[] = { 0, 15, 20, 135 }; 95 QBENCHMARK { 96 // Start the search 97 filteredData_->runSearch( QRegExp( "123" ) ); 98 99 // And check we receive data in 4 chunks (the first being empty) 100 for ( int i = 0; i < 4; i++ ) { 101 std::pair<int,int> progress = waitSearchProgressed(); 102 // FIXME: The test for this is unfortunately not reliable 103 // (race conditions) 104 // QCOMPARE( (qint64) progress.first, matches[i] ); 105 signalSearchProgressedRead(); 106 } 107 } 108 109 QCOMPARE( filteredData_->getNbLine(), matches[3] ); 110 // Check the search 111 QCOMPARE( filteredData_->isLineInMatchingList( 123 ), true ); 112 QCOMPARE( filteredData_->isLineInMatchingList( 124 ), false ); 113 QCOMPARE( filteredData_->getMaxLength(), ML_VISIBLE_LINE_LENGTH ); 114 QCOMPARE( filteredData_->getLineLength( 12 ), ML_VISIBLE_LINE_LENGTH ); 115 QCOMPARE( filteredData_->getNbLine(), 135LL ); 116 // Line beyond limit 117 QCOMPARE( filteredData_->isLineInMatchingList( 60000 ), false ); 118 QCOMPARE( filteredData_->getMatchingLineNumber( 0 ), 123LL ); 119 120 // Now let's try interrupting a search 121 filteredData_->runSearch( QRegExp( "123" ) ); 122 // ... wait for two chunks. 123 waitSearchProgressed(); 124 signalSearchProgressedRead(); 125 // and interrupt! 126 filteredData_->interruptSearch(); 127 128 { 129 std::pair<int,int> progress; 130 do { 131 progress = waitSearchProgressed(); 132 signalSearchProgressedRead(); 133 } while ( progress.second < 100 ); 134 135 // (because there is no guarantee when the search is 136 // interrupted, we are not sure how many chunk of result 137 // we will get.) 138 } 139 140 QApplication::quit(); 141 } 142 143 void TestLogFilteredData::multipleSearch() 144 { 145 logData_ = new LogData(); 146 147 // Register for notification file is loaded 148 connect( logData_, SIGNAL( loadingFinished( bool ) ), 149 this, SLOT( loadingFinished() ) ); 150 151 filteredData_ = logData_->getNewFilteredData(); 152 connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), 153 this, SLOT( searchProgressed( int, int ) ) ); 154 155 QFuture<void> future = QtConcurrent::run(this, &TestLogFilteredData::multipleSearchTest); 156 157 QApplication::exec(); 158 159 disconnect( filteredData_, 0 ); 160 disconnect( logData_, 0 ); 161 162 delete filteredData_; 163 delete logData_; 164 } 165 166 void TestLogFilteredData::multipleSearchTest() 167 { 168 // First load the tests file 169 logData_->attachFile( TMPDIR "/smalllog.txt" ); 170 // Wait for the loading to be done 171 waitLoadingFinished(); 172 QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); 173 signalLoadingFinishedRead(); 174 175 // Performs two searches in a row 176 // Start the search, and immediately another one 177 // (the second call should block until the first search is done) 178 filteredData_->runSearch( QRegExp( "1234" ) ); 179 filteredData_->runSearch( QRegExp( "123" ) ); 180 181 for ( int i = 0; i < 3; i++ ) { 182 waitSearchProgressed(); 183 signalSearchProgressedRead(); 184 } 185 186 // We should have the result for the 2nd search after the last chunk 187 waitSearchProgressed(); 188 QCOMPARE( filteredData_->getNbLine(), 12LL ); 189 signalSearchProgressedRead(); 190 191 // Now a tricky one: we run a search and immediately attach a new file 192 /* FIXME: sometimes we receive loadingFinished before searchProgressed 193 * -> deadlock in the test. 194 filteredData_->runSearch( QRegExp( "123" ) ); 195 waitSearchProgressed(); 196 signalSearchProgressedRead(); 197 logData_->attachFile( TMPDIR "/mediumlog.txt" ); 198 199 // We don't expect meaningful results but it should not crash! 200 for ( int i = 0; i < 1; i++ ) { 201 waitSearchProgressed(); 202 signalSearchProgressedRead(); 203 } 204 */ 205 206 sleep(10); 207 208 QApplication::quit(); 209 } 210 211 void TestLogFilteredData::updateSearch() 212 { 213 logData_ = new LogData(); 214 215 // Register for notification file is loaded 216 connect( logData_, SIGNAL( loadingFinished( bool ) ), 217 this, SLOT( loadingFinished() ) ); 218 219 filteredData_ = logData_->getNewFilteredData(); 220 connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), 221 this, SLOT( searchProgressed( int, int ) ) ); 222 223 QFuture<void> future = QtConcurrent::run(this, &TestLogFilteredData::updateSearchTest); 224 225 QApplication::exec(); 226 227 disconnect( filteredData_, 0 ); 228 disconnect( logData_, 0 ); 229 230 delete filteredData_; 231 delete logData_; 232 } 233 234 void TestLogFilteredData::updateSearchTest() 235 { 236 // First load the tests file 237 logData_->attachFile( TMPDIR "/smalllog.txt" ); 238 // Wait for the loading to be done 239 waitLoadingFinished(); 240 QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); 241 signalLoadingFinishedRead(); 242 243 // Perform a first search 244 filteredData_->runSearch( QRegExp( "123" ) ); 245 246 for ( int i = 0; i < 2; i++ ) { 247 waitSearchProgressed(); 248 signalSearchProgressedRead(); 249 } 250 251 // Check the result 252 QCOMPARE( filteredData_->getNbLine(), 12LL ); 253 254 sleep(1); 255 256 QWARN("Starting stage 2"); 257 258 // Add some data to the file 259 char newLine[90]; 260 QFile file( TMPDIR "/smalllog.txt" ); 261 if ( file.open( QIODevice::Append ) ) { 262 for (int i = 0; i < 3000; i++) { 263 snprintf(newLine, 89, sl_format, i); 264 file.write( newLine, qstrlen(newLine) ); 265 } 266 // To test the edge case when the final line is not complete and matching 267 file.write( partial_line_begin, qstrlen( partial_line_begin ) ); 268 } 269 file.close(); 270 271 // Let the system do the update (there might be several ones) 272 do { 273 waitLoadingFinished(); 274 signalLoadingFinishedRead(); 275 } while ( logData_->getNbLine() < 5001LL ); 276 277 sleep(1); 278 279 // Start an update search 280 filteredData_->updateSearch(); 281 282 for ( int i = 0; i < 2; i++ ) { 283 waitSearchProgressed(); 284 signalSearchProgressedRead(); 285 } 286 287 // Check the result 288 QCOMPARE( logData_->getNbLine(), 5001LL ); 289 QCOMPARE( filteredData_->getNbLine(), 26LL ); 290 291 QWARN("Starting stage 3"); 292 293 // Add a couple more lines, including the end of the unfinished one. 294 if ( file.open( QIODevice::Append ) ) { 295 file.write( partial_line_end, qstrlen( partial_line_end ) ); 296 for (int i = 0; i < 20; i++) { 297 snprintf(newLine, 89, sl_format, i); 298 file.write( newLine, qstrlen(newLine) ); 299 } 300 // To test the edge case when the final line is not complete and not matching 301 file.write( partial_nonmatching_line_begin, 302 qstrlen( partial_nonmatching_line_begin ) ); 303 } 304 file.close(); 305 306 // Let the system do the update 307 waitLoadingFinished(); 308 signalLoadingFinishedRead(); 309 310 // Start an update search 311 filteredData_->updateSearch(); 312 313 for ( int i = 0; i < 2; i++ ) { 314 waitSearchProgressed(); 315 signalSearchProgressedRead(); 316 } 317 318 // Check the result 319 QCOMPARE( logData_->getNbLine(), 5022LL ); 320 QCOMPARE( filteredData_->getNbLine(), 26LL ); 321 322 QWARN("Starting stage 4"); 323 324 // Now test the case where a match is found at the end of an updated line. 325 if ( file.open( QIODevice::Append ) ) { 326 file.write( partial_line_end, qstrlen( partial_line_end ) ); 327 for (int i = 0; i < 20; i++) { 328 snprintf(newLine, 89, sl_format, i); 329 file.write( newLine, qstrlen(newLine) ); 330 } 331 } 332 file.close(); 333 334 // Let the system do the update 335 waitLoadingFinished(); 336 signalLoadingFinishedRead(); 337 338 // Start an update search 339 filteredData_->updateSearch(); 340 341 for ( int i = 0; i < 2; i++ ) { 342 waitSearchProgressed(); 343 signalSearchProgressedRead(); 344 } 345 346 // Check the result 347 QCOMPARE( logData_->getNbLine(), 5042LL ); 348 QCOMPARE( filteredData_->getNbLine(), 27LL ); 349 350 QApplication::quit(); 351 } 352 353 void TestLogFilteredData::marks() 354 { 355 logData_ = new LogData(); 356 357 // Register for notification file is loaded 358 connect( logData_, SIGNAL( loadingFinished( bool ) ), 359 this, SLOT( loadingFinished() ) ); 360 361 filteredData_ = logData_->getNewFilteredData(); 362 connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), 363 this, SLOT( searchProgressed( int, int ) ) ); 364 365 QFuture<void> future = QtConcurrent::run(this, &TestLogFilteredData::marksTest); 366 367 QApplication::exec(); 368 369 disconnect( filteredData_, 0 ); 370 disconnect( logData_, 0 ); 371 372 delete filteredData_; 373 delete logData_; 374 } 375 376 void TestLogFilteredData::marksTest() 377 { 378 // First load the tests file 379 logData_->attachFile( TMPDIR "/smalllog.txt" ); 380 // Wait for the loading to be done 381 waitLoadingFinished(); 382 QCOMPARE( logData_->getNbLine(), SL_NB_LINES ); 383 signalLoadingFinishedRead(); 384 385 // First check no line is marked 386 for ( int i = 0; i < SL_NB_LINES; i++ ) 387 QVERIFY( filteredData_->isLineMarked( i ) == false ); 388 389 // Try to create some "out of limit" marks 390 filteredData_->addMark( -10 ); 391 filteredData_->addMark( SL_NB_LINES + 25 ); 392 393 // Check no line is marked still 394 for ( int i = 0; i < SL_NB_LINES; i++ ) 395 QVERIFY( filteredData_->isLineMarked( i ) == false ); 396 397 // Create a couple of unnamed marks 398 filteredData_->addMark( 10 ); 399 filteredData_->addMark( 44 ); // This one will also be a match 400 filteredData_->addMark( 25 ); 401 402 // Check they are marked 403 QVERIFY( filteredData_->isLineMarked( 10 ) ); 404 QVERIFY( filteredData_->isLineMarked( 25 ) ); 405 QVERIFY( filteredData_->isLineMarked( 44 ) ); 406 407 // But others are not 408 QVERIFY( filteredData_->isLineMarked( 15 ) == false ); 409 QVERIFY( filteredData_->isLineMarked( 20 ) == false ); 410 411 QCOMPARE( filteredData_->getNbLine(), 3LL ); 412 413 // Performs a search 414 QSignalSpy progressSpy( filteredData_, 415 SIGNAL( searchProgressed( int, int ) ) ); 416 filteredData_->runSearch( QRegExp( "0000.4" ) ); 417 418 for ( int i = 0; i < 1; i++ ) { 419 waitSearchProgressed(); 420 signalSearchProgressedRead(); 421 } 422 423 // We should have the result of the search and the marks 424 waitSearchProgressed(); 425 QCOMPARE( filteredData_->getNbLine(), 12LL ); 426 signalSearchProgressedRead(); 427 428 QString startline = "LOGDATA is a part of glogg, we are going to test it thoroughly, this is line "; 429 430 QCOMPARE( filteredData_->getLineString(0), startline + "000004" ); 431 QCOMPARE( filteredData_->getLineString(1), startline + "000010" ); 432 QCOMPARE( filteredData_->getLineString(2), startline + "000014" ); 433 QCOMPARE( filteredData_->getLineString(3), startline + "000024" ); 434 QCOMPARE( filteredData_->getLineString(4), startline + "000025" ); 435 QCOMPARE( filteredData_->getLineString(5), startline + "000034" ); 436 QCOMPARE( filteredData_->getLineString(6), startline + "000044" ); 437 QCOMPARE( filteredData_->getLineString(7), startline + "000054" ); 438 QCOMPARE( filteredData_->getLineString(8), startline + "000064" ); 439 QCOMPARE( filteredData_->getLineString(9), startline + "000074" ); 440 QCOMPARE( filteredData_->getLineString(10), startline + "000084" ); 441 QCOMPARE( filteredData_->getLineString(11), startline + "000094" ); 442 443 filteredData_->setVisibility( LogFilteredData::MatchesOnly ); 444 445 QCOMPARE( filteredData_->getNbLine(), 10LL ); 446 QCOMPARE( filteredData_->getLineString(0), startline + "000004" ); 447 QCOMPARE( filteredData_->getLineString(1), startline + "000014" ); 448 QCOMPARE( filteredData_->getLineString(2), startline + "000024" ); 449 QCOMPARE( filteredData_->getLineString(3), startline + "000034" ); 450 QCOMPARE( filteredData_->getLineString(4), startline + "000044" ); 451 QCOMPARE( filteredData_->getLineString(5), startline + "000054" ); 452 QCOMPARE( filteredData_->getLineString(6), startline + "000064" ); 453 QCOMPARE( filteredData_->getLineString(7), startline + "000074" ); 454 QCOMPARE( filteredData_->getLineString(8), startline + "000084" ); 455 QCOMPARE( filteredData_->getLineString(9), startline + "000094" ); 456 457 filteredData_->setVisibility( LogFilteredData::MarksOnly ); 458 459 QCOMPARE( filteredData_->getNbLine(), 3LL ); 460 QCOMPARE( filteredData_->getLineString(0), startline + "000010" ); 461 QCOMPARE( filteredData_->getLineString(1), startline + "000025" ); 462 QCOMPARE( filteredData_->getLineString(2), startline + "000044" ); 463 464 // Another test with marks only 465 filteredData_->clearSearch(); 466 filteredData_->clearMarks(); 467 filteredData_->setVisibility( LogFilteredData::MarksOnly ); 468 469 filteredData_->addMark(18); 470 filteredData_->addMark(19); 471 filteredData_->addMark(20); 472 473 QCOMPARE( filteredData_->getMatchingLineNumber(0), 18LL ); 474 QCOMPARE( filteredData_->getMatchingLineNumber(1), 19LL ); 475 QCOMPARE( filteredData_->getMatchingLineNumber(2), 20LL ); 476 477 QApplication::quit(); 478 } 479 480 void TestLogFilteredData::lineLength() 481 { 482 logData_ = new LogData(); 483 484 // Register for notification file is loaded 485 connect( logData_, SIGNAL( loadingFinished( bool ) ), 486 this, SLOT( loadingFinished() ) ); 487 488 filteredData_ = logData_->getNewFilteredData(); 489 connect( filteredData_, SIGNAL( searchProgressed( int, int ) ), 490 this, SLOT( searchProgressed( int, int ) ) ); 491 492 QFuture<void> future = QtConcurrent::run(this, &TestLogFilteredData::lineLengthTest); 493 494 QApplication::exec(); 495 496 disconnect( filteredData_, 0 ); 497 disconnect( logData_, 0 ); 498 499 delete filteredData_; 500 delete logData_; 501 } 502 503 void TestLogFilteredData::lineLengthTest() 504 { 505 // Line length tests 506 507 logData_->attachFile( TMPDIR "/length_test.txt" ); 508 // Wait for the loading to be done 509 waitLoadingFinished(); 510 QCOMPARE( logData_->getNbLine(), 4LL ); 511 signalLoadingFinishedRead(); 512 513 // Performs a search (the two middle lines matche) 514 filteredData_->setVisibility( LogFilteredData::MatchesOnly ); 515 filteredData_->runSearch( QRegExp( "longer" ) ); 516 517 std::pair<int,int> progress; 518 do { 519 progress = waitSearchProgressed(); 520 signalSearchProgressedRead(); 521 QWARN("progress"); 522 } while ( progress.second < 100 ); 523 524 filteredData_->addMark( 3 ); 525 526 QCOMPARE( filteredData_->getNbLine(), 2LL ); 527 QCOMPARE( filteredData_->getMaxLength(), 40 ); 528 529 filteredData_->setVisibility( LogFilteredData::MarksAndMatches ); 530 QCOMPARE( filteredData_->getNbLine(), 3LL ); 531 QCOMPARE( filteredData_->getMaxLength(), 103 ); 532 533 filteredData_->setVisibility( LogFilteredData::MarksOnly ); 534 QCOMPARE( filteredData_->getNbLine(), 1LL ); 535 QCOMPARE( filteredData_->getMaxLength(), 103 ); 536 537 filteredData_->addMark( 0 ); 538 QCOMPARE( filteredData_->getNbLine(), 2LL ); 539 QCOMPARE( filteredData_->getMaxLength(), 103 ); 540 filteredData_->deleteMark( 3 ); 541 QCOMPARE( filteredData_->getNbLine(), 1LL ); 542 QCOMPARE( filteredData_->getMaxLength(), 27 ); 543 544 filteredData_->setVisibility( LogFilteredData::MarksAndMatches ); 545 QCOMPARE( filteredData_->getMaxLength(), 40 ); 546 547 QApplication::quit(); 548 } 549 550 // 551 // Private functions 552 // 553 void TestLogFilteredData::loadingFinished() 554 { 555 QMutexLocker locker( &loadingFinishedMutex_ ); 556 557 QWARN("loadingFinished"); 558 loadingFinished_received_ = true; 559 loadingFinished_read_ = false; 560 561 loadingFinishedCondition_.wakeOne(); 562 563 // Wait for the test thread to read the signal 564 while ( ! loadingFinished_read_ ) 565 loadingFinishedCondition_.wait( locker.mutex() ); 566 } 567 568 void TestLogFilteredData::searchProgressed( int nbMatches, int completion ) 569 { 570 QMutexLocker locker( &searchProgressedMutex_ ); 571 572 QWARN("searchProgressed"); 573 searchProgressed_received_ = true; 574 searchProgressed_read_ = false; 575 576 searchLastMatches_ = nbMatches; 577 searchLastProgress_ = completion; 578 579 searchProgressedCondition_.wakeOne(); 580 581 // Wait for the test thread to read the signal 582 while ( ! searchProgressed_read_ ) 583 searchProgressedCondition_.wait( locker.mutex() ); 584 } 585 586 std::pair<int,int> TestLogFilteredData::waitSearchProgressed() 587 { 588 QMutexLocker locker( &searchProgressedMutex_ ); 589 590 while ( ! searchProgressed_received_ ) 591 searchProgressedCondition_.wait( locker.mutex() ); 592 593 QWARN("searchProgressed Received"); 594 595 return std::pair<int,int>(searchLastMatches_, searchLastProgress_); 596 } 597 598 void TestLogFilteredData::waitLoadingFinished() 599 { 600 QMutexLocker locker( &loadingFinishedMutex_ ); 601 602 while ( ! loadingFinished_received_ ) 603 loadingFinishedCondition_.wait( locker.mutex() ); 604 605 QWARN("loadingFinished Received"); 606 } 607 608 void TestLogFilteredData::signalSearchProgressedRead() 609 { 610 QMutexLocker locker( &searchProgressedMutex_ ); 611 612 searchProgressed_received_ = false; 613 searchProgressed_read_ = true; 614 searchProgressedCondition_.wakeOne(); 615 } 616 617 void TestLogFilteredData::signalLoadingFinishedRead() 618 { 619 QMutexLocker locker( &loadingFinishedMutex_ ); 620 621 loadingFinished_received_ = false; 622 loadingFinished_read_ = true; 623 loadingFinishedCondition_.wakeOne(); 624 } 625 626 bool TestLogFilteredData::generateDataFiles() 627 { 628 char newLine[90]; 629 630 QFile file( TMPDIR "/mediumlog.txt" ); 631 if ( file.open( QIODevice::WriteOnly ) ) { 632 for (int i = 0; i < ML_NB_LINES; i++) { 633 snprintf(newLine, 89, ml_format, i); 634 file.write( newLine, qstrlen(newLine) ); 635 } 636 } 637 else { 638 return false; 639 } 640 file.close(); 641 642 file.setFileName( TMPDIR "/smalllog.txt" ); 643 if ( file.open( QIODevice::WriteOnly ) ) { 644 for (int i = 0; i < SL_NB_LINES; i++) { 645 snprintf(newLine, 89, sl_format, i); 646 file.write( newLine, qstrlen(newLine) ); 647 } 648 } 649 else { 650 return false; 651 } 652 file.close(); 653 654 file.setFileName( TMPDIR "/length_test.txt" ); 655 if ( file.open( QIODevice::WriteOnly ) ) { 656 file.write( "This line is 27 characters.\n" ); 657 file.write( "This line is longer: 36 characters.\n" ); 658 file.write( "This line is even longer: 40 characters.\n" ); 659 file.write( "This line is very long, it's actually hard to count but it is\ 660 probably something around 103 characters.\n" ); 661 } 662 file.close(); 663 664 return true; 665 } 666