1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
4  *   SPDX-License-Identifier: GPL-3.0-or-later
5  *
6  *
7  *   Calamares is Free Software: see the License-Identifier above.
8  *
9  *
10  */
11 
12 #include "CalamaresUtilsSystem.h"
13 #include "Entropy.h"
14 #include "Logger.h"
15 #include "RAII.h"
16 #include "String.h"
17 #include "Traits.h"
18 #include "UMask.h"
19 #include "Variant.h"
20 #include "Yaml.h"
21 
22 #include "GlobalStorage.h"
23 #include "JobQueue.h"
24 
25 #include <QTemporaryFile>
26 
27 #include <QtTest/QtTest>
28 
29 #include <fcntl.h>
30 #include <sys/stat.h>
31 #include <unistd.h>
32 
33 class LibCalamaresTests : public QObject
34 {
35     Q_OBJECT
36 public:
37     LibCalamaresTests();
38     ~LibCalamaresTests() override;
39 
40 private Q_SLOTS:
41     void initTestCase();
42     void testDebugLevels();
43 
44     void testLoadSaveYaml();  // Just settings.conf
45     void testLoadSaveYamlExtended();  // Do a find() in the src dir
46 
47     void testCommands();
48 
49     /** @section Test that all the UMask objects work correctly. */
50     void testUmask();
51 
52     /** @section Tests the entropy functions. */
53     void testEntropy();
54     void testPrintableEntropy();
55     void testOddSizedPrintable();
56 
57     /** @section Tests the RAII bits. */
58     void testPointerSetter();
59 
60     /** @section Tests the Traits bits. */
61     void testTraits();
62 
63     /** @section Testing the variants-methods */
64     void testVariantStringListCode();
65     void testVariantStringListYAMLDashed();
66     void testVariantStringListYAMLBracketed();
67 
68     /** @section Test smart string truncation. */
69     void testStringTruncation();
70     void testStringTruncationShorter();
71     void testStringTruncationDegenerate();
72 
73 private:
74     void recursiveCompareMap( const QVariantMap& a, const QVariantMap& b, int depth );
75 };
76 
LibCalamaresTests()77 LibCalamaresTests::LibCalamaresTests() {}
78 
~LibCalamaresTests()79 LibCalamaresTests::~LibCalamaresTests() {}
80 
81 void
initTestCase()82 LibCalamaresTests::initTestCase()
83 {
84 }
85 
86 void
testDebugLevels()87 LibCalamaresTests::testDebugLevels()
88 {
89     Logger::setupLogLevel( Logger::LOG_DISABLE );
90 
91     QCOMPARE( Logger::logLevel(), static_cast< unsigned int >( Logger::LOG_DISABLE ) );
92 
93     for ( unsigned int level = 0; level <= Logger::LOGVERBOSE; ++level )
94     {
95         Logger::setupLogLevel( level );
96         QCOMPARE( Logger::logLevel(), level );
97         QVERIFY( Logger::logLevelEnabled( level ) );
98 
99         for ( unsigned int xlevel = 0; xlevel <= Logger::LOGVERBOSE; ++xlevel )
100         {
101             QCOMPARE( Logger::logLevelEnabled( xlevel ), xlevel <= level );
102         }
103     }
104 }
105 
106 void
testLoadSaveYaml()107 LibCalamaresTests::testLoadSaveYaml()
108 {
109     Logger::setupLogLevel( Logger::LOGDEBUG );
110 
111     QFile f( "settings.conf" );
112     // Find the nearest settings.conf to read
113     for ( unsigned int up = 0; !f.exists() && ( up < 4 ); ++up )
114     {
115         f.setFileName( QString( "../" ) + f.fileName() );
116     }
117     cDebug() << QDir().absolutePath() << f.fileName() << f.exists();
118     QVERIFY( f.exists() );
119 
120     auto map = CalamaresUtils::loadYaml( f.fileName() );
121     QVERIFY( map.contains( "sequence" ) );
122     QCOMPARE( map[ "sequence" ].type(), QVariant::List );
123 
124     // The source-repo example `settings.conf` has a show and an exec phase
125     auto sequence = map[ "sequence" ].toList();
126     cDebug() << "Loaded example `settings.conf` sequence:";
127     for ( const auto& v : sequence )
128     {
129         cDebug() << Logger::SubEntry << v;
130         QCOMPARE( v.type(), QVariant::Map );
131         QVERIFY( v.toMap().contains( "show" ) || v.toMap().contains( "exec" ) );
132     }
133 
134     CalamaresUtils::saveYaml( "out.yaml", map );
135 
136     auto other_map = CalamaresUtils::loadYaml( "out.yaml" );
137     CalamaresUtils::saveYaml( "out2.yaml", other_map );
138     QCOMPARE( map, other_map );
139 
140     QFile::remove( "out.yaml" );
141     QFile::remove( "out2.yaml" );
142 }
143 
144 static QStringList
findConf(const QDir & d)145 findConf( const QDir& d )
146 {
147     QStringList mine;
148     if ( d.exists() )
149     {
150         QString path = d.absolutePath();
151         path.append( d.separator() );
152         for ( const auto& confname : d.entryList( { "*.conf" } ) )
153             mine.append( path + confname );
154         for ( const auto& subdirname : d.entryList( QDir::AllDirs | QDir::NoDotAndDotDot ) )
155         {
156             QDir subdir( d );
157             subdir.cd( subdirname );
158             mine.append( findConf( subdir ) );
159         }
160     }
161     return mine;
162 }
163 
164 void
recursiveCompareMap(const QVariantMap & a,const QVariantMap & b,int depth)165 LibCalamaresTests::recursiveCompareMap( const QVariantMap& a, const QVariantMap& b, int depth )
166 {
167     cDebug() << "Comparing depth" << depth << a.count() << b.count();
168     QCOMPARE( a.keys(), b.keys() );
169     for ( const auto& k : a.keys() )
170     {
171         cDebug() << Logger::SubEntry << k;
172         const auto& av = a[ k ];
173         const auto& bv = b[ k ];
174 
175         if ( av.typeName() != bv.typeName() )
176         {
177             cDebug() << Logger::SubEntry << "a type" << av.typeName() << av;
178             cDebug() << Logger::SubEntry << "b type" << bv.typeName() << bv;
179         }
180         QCOMPARE( av.typeName(), bv.typeName() );
181         if ( av.canConvert< QVariantMap >() )
182         {
183             recursiveCompareMap( av.toMap(), bv.toMap(), depth + 1 );
184         }
185         else
186         {
187             QCOMPARE( av, bv );
188         }
189     }
190 }
191 
192 
193 void
testLoadSaveYamlExtended()194 LibCalamaresTests::testLoadSaveYamlExtended()
195 {
196     Logger::setupLogLevel( Logger::LOGDEBUG );
197     bool loaded_ok;
198     for ( const auto& confname : findConf( QDir( "../src" ) ) )
199     {
200         loaded_ok = true;
201         cDebug() << "Testing" << confname;
202         auto map = CalamaresUtils::loadYaml( confname, &loaded_ok );
203         QVERIFY( loaded_ok );
204         QVERIFY( CalamaresUtils::saveYaml( "out.yaml", map ) );
205         auto othermap = CalamaresUtils::loadYaml( "out.yaml", &loaded_ok );
206         QVERIFY( loaded_ok );
207         QCOMPARE( map.keys(), othermap.keys() );
208         recursiveCompareMap( map, othermap, 0 );
209         QCOMPARE( map, othermap );
210     }
211     QFile::remove( "out.yaml" );
212 }
213 
214 void
testCommands()215 LibCalamaresTests::testCommands()
216 {
217     using CalamaresUtils::System;
218     auto r = System::runCommand( System::RunLocation::RunInHost, { "/bin/ls", "/tmp" } );
219 
220     QVERIFY( r.getExitCode() == 0 );
221 
222     QTemporaryFile tf( "/tmp/calamares-test-XXXXXX" );
223     QVERIFY( tf.open() );
224     QVERIFY( !tf.fileName().isEmpty() );
225 
226     QFileInfo tfn( tf.fileName() );
227     QVERIFY( !r.getOutput().contains( tfn.fileName() ) );
228 
229     // Run ls again, now that the file exists
230     r = System::runCommand( System::RunLocation::RunInHost, { "/bin/ls", "/tmp" } );
231     QVERIFY( r.getOutput().contains( tfn.fileName() ) );
232 
233     // .. and without a working directory set, assume builddir != /tmp
234     r = System::runCommand( System::RunLocation::RunInHost, { "/bin/ls" } );
235     QVERIFY( !r.getOutput().contains( tfn.fileName() ) );
236 
237     r = System::runCommand( System::RunLocation::RunInHost, { "/bin/ls" }, "/tmp" );
238     QVERIFY( r.getOutput().contains( tfn.fileName() ) );
239 }
240 
241 void
testUmask()242 LibCalamaresTests::testUmask()
243 {
244     struct stat mystat;
245 
246     QTemporaryFile ft;
247     QVERIFY( ft.open() );
248 
249     // m gets the previous value of the mask (depends on the environment the
250     // test is run in, might be 002, might be 077), ..
251     mode_t m = CalamaresUtils::setUMask( 022 );
252     QCOMPARE( CalamaresUtils::setUMask( m ), mode_t( 022 ) );  // But now most recently set was 022
253 
254     for ( mode_t i = 0; i <= 0777 /* octal! */; ++i )
255     {
256         QByteArray name = ( ft.fileName() + QChar( '.' ) + QString::number( i, 8 ) ).toLatin1();
257         CalamaresUtils::UMask um( i );
258         int fd = creat( name, 0777 );
259         QVERIFY( fd >= 0 );
260         close( fd );
261         QFileInfo fi( name );
262         QVERIFY( fi.exists() );
263         QCOMPARE( stat( name, &mystat ), 0 );
264         QCOMPARE( mystat.st_mode & 0777, 0777 & ~i );
265         QCOMPARE( unlink( name ), 0 );
266     }
267     QCOMPARE( CalamaresUtils::setUMask( 022 ), m );
268     QCOMPARE( CalamaresUtils::setUMask( m ), mode_t( 022 ) );
269 }
270 
271 void
testEntropy()272 LibCalamaresTests::testEntropy()
273 {
274     QByteArray data;
275 
276     auto r0 = CalamaresUtils::getEntropy( 0, data );
277     QCOMPARE( CalamaresUtils::EntropySource::None, r0 );
278     QCOMPARE( data.size(), 0 );
279 
280     auto r1 = CalamaresUtils::getEntropy( 16, data );
281     QVERIFY( r1 != CalamaresUtils::EntropySource::None );
282     QCOMPARE( data.size(), 16 );
283     // This can randomly fail (but not often)
284     QVERIFY( data.at( data.size() - 1 ) != char( 0xcb ) );
285 
286     auto r2 = CalamaresUtils::getEntropy( 8, data );
287     QVERIFY( r2 != CalamaresUtils::EntropySource::None );
288     QCOMPARE( data.size(), 8 );
289     QCOMPARE( r1, r2 );
290     // This can randomly fail (but not often)
291     QVERIFY( data.at( data.size() - 1 ) != char( 0xcb ) );
292 }
293 
294 void
testPrintableEntropy()295 LibCalamaresTests::testPrintableEntropy()
296 {
297     QString s;
298 
299     auto r0 = CalamaresUtils::getPrintableEntropy( 0, s );
300     QCOMPARE( CalamaresUtils::EntropySource::None, r0 );
301     QCOMPARE( s.length(), 0 );
302 
303     auto r1 = CalamaresUtils::getPrintableEntropy( 16, s );
304     QVERIFY( r1 != CalamaresUtils::EntropySource::None );
305     QCOMPARE( s.length(), 16 );
306     for ( QChar c : s )
307     {
308         QVERIFY( c.isPrint() );
309         QCOMPARE( c.row(), uchar( 0 ) );
310         QVERIFY( c.cell() > 32 );  // ASCII SPACE
311         QVERIFY( c.cell() < 127 );
312     }
313 }
314 
315 void
testOddSizedPrintable()316 LibCalamaresTests::testOddSizedPrintable()
317 {
318     QString s;
319     for ( int l = 0; l <= 37; ++l )
320     {
321         auto r = CalamaresUtils::getPrintableEntropy( l, s );
322         if ( l == 0 )
323         {
324             QCOMPARE( r, CalamaresUtils::EntropySource::None );
325         }
326         else
327         {
328             QVERIFY( r != CalamaresUtils::EntropySource::None );
329         }
330         QCOMPARE( s.length(), l );
331 
332         for ( QChar c : s )
333         {
334             QVERIFY( c.isPrint() );
335             QCOMPARE( c.row(), uchar( 0 ) );
336             QVERIFY( c.cell() > 32 );  // ASCII SPACE
337             QVERIFY( c.cell() < 127 );
338         }
339     }
340 }
341 
342 void
testPointerSetter()343 LibCalamaresTests::testPointerSetter()
344 {
345     int special = 17;
346 
347     QCOMPARE( special, 17 );
348     {
349         cScopedAssignment p( &special );
350     }
351     QCOMPARE( special, 17 );
352     {
353         cScopedAssignment p( &special );
354         p = 18;
355     }
356     QCOMPARE( special, 18 );
357     {
358         cScopedAssignment p( &special );
359         p = 20;
360         p = 3;
361     }
362     QCOMPARE( special, 3 );
363     {
364         cScopedAssignment< int > p( nullptr );
365     }
366     QCOMPARE( special, 3 );
367     {
368         // "don't do this" .. order of destructors is important
369         cScopedAssignment p( &special );
370         cScopedAssignment q( &special );
371         p = 17;
372     }
373     QCOMPARE( special, 17 );
374     {
375         // "don't do this" .. order of destructors is important
376         cScopedAssignment p( &special );
377         cScopedAssignment q( &special );
378         p = 34;
379         q = 2;
380         // q destroyed first, then p
381     }
382     QCOMPARE( special, 34 );
383 }
384 
385 
386 /* Demonstration of Traits support for has-a-method or not.
387  *
388  * We have two classes, c1 and c2; one has a method do_the_thing() and the
389  * other does not. A third class, Thinginator, has a method thingify(),
390  * which should call do_the_thing() of its argument if it exists.
391  */
392 
393 struct c1
394 {
do_the_thingc1395     int do_the_thing() { return 2; }
396 };
397 struct c2
398 {
399 };
400 
401 DECLARE_HAS_METHOD( do_the_thing )
402 
403 struct Thinginator
404 {
405 public:
406     /// When class T has function do_the_thing()
407     template < class T >
thingifyThinginator408     int thingify( T& t, const std::true_type& )
409     {
410         return t.do_the_thing();
411     }
412 
413     template < class T >
thingifyThinginator414     int thingify( T&, const std::false_type& )
415     {
416         return -1;
417     }
418 
419     template < class T >
thingifyThinginator420     int thingify( T& t )
421     {
422         return thingify( t, has_do_the_thing< T > {} );
423     }
424 };
425 
426 
427 void
testTraits()428 LibCalamaresTests::testTraits()
429 {
430     has_do_the_thing< c1 > x {};
431     has_do_the_thing< c2 > y {};
432 
433     QVERIFY( x );
434     QVERIFY( !y );
435 
436     c1 c1 {};
437     c2 c2 {};
438 
439     QCOMPARE( c1.do_the_thing(), 2 );
440 
441     Thinginator t;
442     QCOMPARE( t.thingify( c1 ), 2 );  // Calls c1::do_the_thing()
443     QCOMPARE( t.thingify( c2 ), -1 );
444 }
445 
446 void
testVariantStringListCode()447 LibCalamaresTests::testVariantStringListCode()
448 {
449     using namespace CalamaresUtils;
450     const QString key( "strings" );
451     {
452         // Things that are not stringlists
453         QVariantMap m;
454         QCOMPARE( getStringList( m, key ), QStringList {} );
455         m.insert( key, 17 );
456         QCOMPARE( getStringList( m, key ), QStringList {} );
457         m.insert( key, QVariant {} );
458         QCOMPARE( getStringList( m, key ), QStringList {} );
459     }
460 
461     {
462         // Things that are **like** stringlists
463         QVariantMap m;
464         m.insert( key, QString( "astring" ) );
465         QCOMPARE( getStringList( m, key ).count(), 1 );
466         QCOMPARE( getStringList( m, key ),
467                   QStringList { "astring" } );  // A single string **can** be considered a stringlist!
468         m.insert( key, QString( "more strings" ) );
469         QCOMPARE( getStringList( m, key ).count(), 1 );
470         QCOMPARE( getStringList( m, key ), QStringList { "more strings" } );
471         m.insert( key, QString() );
472         QCOMPARE( getStringList( m, key ).count(), 1 );
473         QCOMPARE( getStringList( m, key ), QStringList { QString() } );
474     }
475 
476     {
477         // Things that are definitely stringlists
478         QVariantMap m;
479         m.insert( key, QStringList { "aap", "noot" } );
480         QCOMPARE( getStringList( m, key ).count(), 2 );
481         QVERIFY( getStringList( m, key ).contains( "aap" ) );
482         QVERIFY( !getStringList( m, key ).contains( "mies" ) );
483     }
484 }
485 
486 void
testVariantStringListYAMLDashed()487 LibCalamaresTests::testVariantStringListYAMLDashed()
488 {
489     using namespace CalamaresUtils;
490     const QString key( "strings" );
491 
492     // Looks like a stringlist to me
493     QTemporaryFile f;
494     QVERIFY( f.open() );
495     f.write( R"(---
496 strings:
497     - aap
498     - noot
499     - mies
500 )" );
501     f.close();
502     bool ok = false;
503     QVariantMap m = loadYaml( f.fileName(), &ok );
504 
505     QVERIFY( ok );
506     QCOMPARE( m.count(), 1 );
507     QVERIFY( m.contains( key ) );
508 
509     QVERIFY( getStringList( m, key ).contains( "aap" ) );
510     QVERIFY( getStringList( m, key ).contains( "mies" ) );
511     QVERIFY( !getStringList( m, key ).contains( "lam" ) );
512 }
513 
514 void
testVariantStringListYAMLBracketed()515 LibCalamaresTests::testVariantStringListYAMLBracketed()
516 {
517     using namespace CalamaresUtils;
518     const QString key( "strings" );
519 
520     // Looks like a stringlist to me
521     QTemporaryFile f;
522     QVERIFY( f.open() );
523     f.write( R"(---
524 strings: [ aap, noot, mies ]
525 )" );
526     f.close();
527     bool ok = false;
528     QVariantMap m = loadYaml( f.fileName(), &ok );
529 
530     QVERIFY( ok );
531     QCOMPARE( m.count(), 1 );
532     QVERIFY( m.contains( key ) );
533 
534     QVERIFY( getStringList( m, key ).contains( "aap" ) );
535     QVERIFY( getStringList( m, key ).contains( "mies" ) );
536     QVERIFY( !getStringList( m, key ).contains( "lam" ) );
537 }
538 
539 void
testStringTruncation()540 LibCalamaresTests::testStringTruncation()
541 {
542     Logger::setupLogLevel( Logger::LOGDEBUG );
543 
544     using namespace CalamaresUtils;
545 
546     const QString longString( R"(---
547 --- src/libcalamares/utils/String.h
548 +++ src/libcalamares/utils/String.h
549 @@ -62,15 +62,22 @@ DLLEXPORT QString removeDiacritics( const QString& string );
550   */
551  DLLEXPORT QString obscure( const QString& string );
552 
553 +/** @brief Parameter for counting lines at beginning and end of string
554 + *
555 + * This is used by truncateMultiLine() to indicate how many lines from
556 + * the beginning and how many from the end should be kept.
557 + */
558  struct LinesStartEnd
559  {
560 -    int atStart;
561 -    int atEnd;
562 +    int atStart = 0;
563 +    int atEnd = 0;
564 )" );
565 
566     const int sufficientLength = 812;
567     // There's 18 lines in all
568     QCOMPARE( longString.count( '\n' ), 18 );
569     QVERIFY( longString.length() < sufficientLength );
570 
571     // If we ask for more, we get everything back
572     QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 20, 0 }, CharCount { sufficientLength } ) );
573     QCOMPARE( longString, truncateMultiLine( longString, LinesStartEnd { 0, 20 }, CharCount { sufficientLength } ) );
574 
575     // If we ask for no lines, only characters, we get that
576     {
577         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { 4 } );
578         QCOMPARE( s.length(), 4 );
579         QCOMPARE( s, QString( "---\n" ) );
580     }
581     {
582         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 0 }, CharCount { sufficientLength } );
583         QCOMPARE( s, longString );
584     }
585 
586     // Lines at the start
587     {
588         auto s = truncateMultiLine( longString, LinesStartEnd { 4, 0 }, CharCount { sufficientLength } );
589         QVERIFY( s.length() > 1 );
590         QVERIFY( longString.startsWith( s ) );
591         cDebug() << "Result-line" << Logger::Quote << s;
592         QCOMPARE( s.count( '\n' ), 4 );
593     }
594 
595     // Lines at the end
596     {
597         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 4 }, CharCount { sufficientLength } );
598         QVERIFY( s.length() > 1 );
599         QVERIFY( longString.endsWith( s ) );
600         cDebug() << "Result-line" << Logger::Quote << s;
601         QCOMPARE( s.count( '\n' ), 4 );
602     }
603 
604     // Lines at both ends
605     {
606         auto s = truncateMultiLine( longString, LinesStartEnd { 2, 2 }, CharCount { sufficientLength } );
607         QVERIFY( s.length() > 1 );
608         cDebug() << "Result-line" << Logger::Quote << s;
609         QCOMPARE( s.count( '\n' ), 4 );
610 
611         auto firsttwo = truncateMultiLine( s, LinesStartEnd { 2, 0 }, CharCount { sufficientLength } );
612         auto lasttwo = truncateMultiLine( s, LinesStartEnd { 0, 2 }, CharCount { sufficientLength } );
613         QCOMPARE( firsttwo + lasttwo, s );
614         QCOMPARE( firsttwo.count( '\n' ), 2 );
615         QVERIFY( longString.startsWith( firsttwo ) );
616         QVERIFY( longString.endsWith( lasttwo ) );
617     }
618 }
619 
620 void
testStringTruncationShorter()621 LibCalamaresTests::testStringTruncationShorter()
622 {
623     Logger::setupLogLevel( Logger::LOGDEBUG );
624 
625     using namespace CalamaresUtils;
626 
627     const QString longString( R"(Some strange string artifacts appeared, leading to `{1?}` being
628 displayed in various user-facing messages. These have been removed
629 and the translations updated.)" );
630     const char NEWLINE = '\n';
631 
632     const int insufficientLength = 42;
633     // There's 2 newlines in all, no trailing newline
634     QVERIFY( !longString.endsWith( NEWLINE ) );
635     QCOMPARE( longString.count( NEWLINE ), 2 );
636     QVERIFY( longString.length() > insufficientLength );
637     // Even the first line must be more than the insufficientLength
638     QVERIFY( longString.indexOf( NEWLINE ) > insufficientLength );
639 
640     // Grab first line, untruncated
641     {
642         auto s = truncateMultiLine( longString, LinesStartEnd { 1, 0 } );
643         QVERIFY( s.length() > 1 );
644         QVERIFY( longString.startsWith( s ) );
645         QVERIFY( s.endsWith( NEWLINE ) );
646         QVERIFY( s.endsWith( "being\n" ) );
647         QVERIFY( s.startsWith( "Some " ) );
648     }
649 
650     // Grab last line, untruncated
651     {
652         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 1 } );
653         QVERIFY( s.length() > 1 );
654         QVERIFY( longString.endsWith( s ) );
655         QVERIFY( !s.endsWith( NEWLINE ) );
656         QVERIFY( s.endsWith( "updated." ) );
657         QCOMPARE( s.count( NEWLINE ), 0 );  // Because last line doesn't end with a newline
658         QVERIFY( s.startsWith( "and the " ) );
659     }
660 
661     // Grab last two lines, untruncated
662     {
663         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 2 } );
664         QVERIFY( s.length() > 1 );
665         QVERIFY( longString.endsWith( s ) );
666         QVERIFY( !s.endsWith( NEWLINE ) );
667         QVERIFY( s.endsWith( "updated." ) );
668         QCOMPARE( s.count( NEWLINE ), 1 );  // Because last line doesn't end with a newline
669         QVERIFY( s.startsWith( "displayed in " ) );
670     }
671 
672     // First line, truncated
673     {
674         auto s = truncateMultiLine( longString, LinesStartEnd { 1, 0 }, CharCount { insufficientLength } );
675         cDebug() << "Result-line" << Logger::Quote << s;
676         QVERIFY( s.length() > 1 );
677         QVERIFY( s.endsWith( NEWLINE ) );
678         QVERIFY( s.startsWith( "Some " ) );
679         // Because the first line has a newline, the truncated version does too,
680         //   but that makes it one longer than requested.
681         QCOMPARE( s.length(), insufficientLength + 1 );
682         QVERIFY( longString.startsWith( s.left( insufficientLength ) ) );
683     }
684 
685     // Last line, truncated; this line is quite short
686     {
687         const int quiteShort = 8;
688         QVERIFY( longString.lastIndexOf( NEWLINE ) < longString.length() - quiteShort );
689 
690         auto s = truncateMultiLine( longString, LinesStartEnd { 0, 1 }, CharCount { quiteShort } );
691         cDebug() << "Result-line" << Logger::Quote << s;
692         QVERIFY( s.length() > 1 );
693         QVERIFY( !s.endsWith( NEWLINE ) );  // Because the original doesn't either
694         QVERIFY( s.startsWith( "upda" ) );
695         QCOMPARE( s.length(), quiteShort );  // No extra newlines
696         QVERIFY( longString.endsWith( s ) );
697     }
698 
699     // First and last, but both truncated
700     {
701         const int quiteShort = 16;
702         QVERIFY( longString.indexOf( NEWLINE ) > quiteShort );
703         QVERIFY( longString.lastIndexOf( NEWLINE ) < longString.length() - quiteShort );
704 
705         auto s = truncateMultiLine( longString, LinesStartEnd { 1, 1 }, CharCount { quiteShort } );
706         cDebug() << "Result-line" << Logger::Quote << s;
707         QVERIFY( s.length() > 1 );
708         QVERIFY( !s.endsWith( NEWLINE ) );  // Because the original doesn't either
709         QVERIFY( s.startsWith( "Some " ) );
710         QVERIFY( s.endsWith( "updated." ) );
711         QCOMPARE( s.length(), quiteShort + 1 );  // Newline between front and back part
712     }
713 }
714 
715 void
testStringTruncationDegenerate()716 LibCalamaresTests::testStringTruncationDegenerate()
717 {
718     Logger::setupLogLevel( Logger::LOGDEBUG );
719 
720     using namespace CalamaresUtils;
721 
722     // This is quite long, 1 line only, with no newlines
723     const QString longString( "The portscout new distfile checker has detected that one or more of your "
724                               "ports appears to be out of date. Please take the opportunity to check "
725                               "each of the ports listed below, and if possible and appropriate, "
726                               "submit/commit an update. If any ports have already been updated, you can "
727                               "safely ignore the entry." );
728 
729     const char NEWLINE = '\n';
730     const int quiteShort = 16;
731     QVERIFY( longString.length() > quiteShort );
732     QVERIFY( !longString.contains( NEWLINE ) );
733     QVERIFY( longString.indexOf( NEWLINE ) < 0 );
734 
735     {
736         auto s = truncateMultiLine( longString, LinesStartEnd { 1, 0 }, CharCount { quiteShort } );
737         cDebug() << "Result-line" << Logger::Quote << s;
738         QVERIFY( s.length() > 1 );
739         QCOMPARE( s.length(), quiteShort );  // No newline between front and back part
740         QVERIFY( s.startsWith( "The port" ) );  // 8, which is quiteShort / 2
741         QVERIFY( s.endsWith( "e entry." ) );  // also 8 chars
742 
743         auto t = truncateMultiLine( longString, LinesStartEnd { 2, 2 }, CharCount { quiteShort } );
744         QCOMPARE( s, t );
745     }
746 }
747 
748 
749 QTEST_GUILESS_MAIN( LibCalamaresTests )
750 
751 #include "utils/moc-warnings.h"
752 
753 #include "Tests.moc"
754