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