Completed
Push — master ( 056c6e...0740de )
by Maxence
01:57
created

Test::testLoadingPlatform()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
/**
3
 * FullTextSearch - Full text search framework for Nextcloud
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Maxence Lange <[email protected]>
9
 * @copyright 2018
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\FullTextSearch\Command;
28
29
use Exception;
30
use OCA\FullTextSearch\Exceptions\InterruptException;
31
use OCA\FullTextSearch\Exceptions\ProviderDoesNotExistException;
32
use OCA\FullTextSearch\Exceptions\ProviderIsNotCompatibleException;
33
use OCA\FullTextSearch\Exceptions\ProviderIsNotUniqueException;
34
use OCA\FullTextSearch\Exceptions\RunnerAlreadyUpException;
35
use OCA\FullTextSearch\Exceptions\TickDoesNotExistException;
36
use OCA\FullTextSearch\IFullTextSearchPlatform;
37
use OCA\FullTextSearch\IFullTextSearchProvider;
38
use OCA\FullTextSearch\Model\DocumentAccess;
39
use OCA\FullTextSearch\Model\ExtendedBase;
40
use OCA\FullTextSearch\Model\IndexOptions;
41
use OCA\FullTextSearch\Model\Runner;
42
use OCA\FullTextSearch\Model\SearchRequest;
43
use OCA\FullTextSearch\Model\SearchResult;
44
use OCA\FullTextSearch\Provider\TestProvider;
45
use OCA\FullTextSearch\Service\IndexService;
46
use OCA\FullTextSearch\Service\MiscService;
47
use OCA\FullTextSearch\Service\PlatformService;
48
use OCA\FullTextSearch\Service\ProviderService;
49
use OCA\FullTextSearch\Service\RunningService;
50
use OCA\FullTextSearch\Service\TestService;
51
use OCP\AppFramework\QueryException;
52
use Symfony\Component\Console\Input\InputInterface;
53
use Symfony\Component\Console\Input\InputOption;
54
use Symfony\Component\Console\Output\OutputInterface;
55
56
57
class Test extends ExtendedBase {
58
59
	const DELAY_STABILIZE_PLATFORM = 3;
60
61
	/** @var RunningService */
62
	private $runningService;
63
64
	/** @var PlatformService */
65
	private $platformService;
66
67
	/** @var ProviderService */
68
	private $providerService;
69
70
	/** @var IndexService */
71
	private $indexService;
72
73
	/** @var TestService */
74
	private $testService;
75
76
	/** @var MiscService */
77
	private $miscService;
78
79
80
	/** @var Runner */
81
	private $runner;
82
83
84
	/** @var boolean */
85
	private $isJson = false;
86
87
	/**
88
	 * Index constructor.
89
	 *
90
	 * @param RunningService $runningService
91
	 * @param ProviderService $providerService
92
	 * @param IndexService $indexService
93
	 * @param PlatformService $platformService
94
	 * @param TestService $testService
95
	 * @param MiscService $miscService
96
	 */
97 View Code Duplication
	public function __construct(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
98
		RunningService $runningService, PlatformService $platformService,
99
		ProviderService $providerService, IndexService $indexService, TestService $testService,
100
		MiscService $miscService
101
	) {
102
		parent::__construct();
103
104
		$this->runningService = $runningService;
105
		$this->platformService = $platformService;
106
		$this->providerService = $providerService;
107
		$this->indexService = $indexService;
108
		$this->testService = $testService;
109
		$this->miscService = $miscService;
110
	}
111
112
113
	/**
114
	 *
115
	 */
116
	protected function configure() {
117
		parent::configure();
118
		$this->setName('fulltextsearch:test')
119
			 ->setDescription('Testing the platform setup')
120
			 ->addOption('json', 'j', InputOption::VALUE_NONE, 'return result as JSON')
121
			 ->addOption(
122
				 'platform_delay', 'd', InputOption::VALUE_REQUIRED,
123
				 'change DELAY_STABILIZE_PLATFORM'
124
			 );
125
	}
126
127
128
	/**
129
	 * @param InputInterface $input
130
	 * @param OutputInterface $output
131
	 *
132
	 * @return int|null|void
133
	 * @throws Exception
134
	 */
135
	protected function execute(InputInterface $input, OutputInterface $output) {
136
		$this->isJson = ($input->getOption('json') === true);
137
		$platformDelay = ($input->getOption('platform_delay') > 0) ? $input->getOption(
138
			'platform_delay'
139
		) : self::DELAY_STABILIZE_PLATFORM;
140
141
		$this->output($output, '.Testing your current setup:');
142
143
		try {
144
			$testProvider = $this->testCreatingProvider($output);
145
			$this->testMockedProvider($output, $testProvider);
146
			$testPlatform = $this->testLoadingPlatform($output);
147
			$this->testLockingProcess($output, $testPlatform, $testProvider);
148
		} catch (Exception $e) {
149
			$this->output($output, false);
150
			throw $e;
151
		}
152
153
		try {
154
			$this->testResetTest($output, $testProvider);
155
			$this->pause($output, $platformDelay);
156
			$this->testInitIndexing($output, $testPlatform);
157
			$this->testIndexingDocuments($output, $testPlatform, $testProvider);
158
			$this->pause($output, $platformDelay);
159
			$this->testContentLicense($output, $testPlatform);
160
			$this->testSearchSimple($output, $testPlatform, $testProvider);
161
162
			$this->testUpdatingDocumentsAccess($output, $testPlatform, $testProvider);
163
			$this->pause($output, $platformDelay);
164
			$this->testSearchAccess($output, $testPlatform, $testProvider);
165
			$this->testSearchShare($output, $testPlatform, $testProvider);
166
167
			$this->testResetTest($output, $testProvider);
168
			$this->testUnlockingProcess($output);
169
		} catch (Exception $e) {
170
			$this->output($output, false);
171
			$this->output($output, 'Error detected, unlocking process');
172
			$this->runner->stop();
173
			$this->output($output, true);
174
175
			throw $e;
176
		}
177
178
		$this->output($output, '', true);
179
	}
180
181
182
	/**
183
	 * @return IFullTextSearchProvider
184
	 * @throws ProviderIsNotCompatibleException
185
	 * @throws QueryException
186
	 * @throws ProviderDoesNotExistException
187
	 * @throws ProviderIsNotUniqueException
188
	 */
189
	private function generateMockProvider() {
190
		$this->providerService->loadProvider('OCA\FullTextSearch\Provider\TestProvider');
191
192
		return $this->providerService->getProvider(TestProvider::TEST_PROVIDER_ID);
193
	}
194
195
196
	/**
197
	 * @param OutputInterface $output
198
	 * @param string|bool $line
199
	 * @param bool $isNewLine
200
	 */
201
	private function output(OutputInterface $output, $line, $isNewLine = true) {
202
		$line = $this->convertBoolToLine($line, $isNewLine);
203
		if ($isNewLine) {
204
			$output->write(' ', true);
205
		}
206
207
		$output->write($line . ' ', false);
208
	}
209
210
211
	/**
212
	 * @param string|bool $line
213
	 * @param $isNewLine
214
	 *
215
	 * @return string
216
	 */
217
	private function convertBoolToLine($line, &$isNewLine) {
218
		if (!is_bool($line)) {
219
			return $line;
220
		}
221
222
		$isNewLine = false;
223
		if ($line === false) {
224
			return '<error>fail</error>';
225
		}
226
227
		return '<info>ok</info>';
228
	}
229
230
231
	/**
232
	 * @param $output
233
	 *
234
	 * @return IFullTextSearchProvider
235
	 * @throws ProviderDoesNotExistException
236
	 * @throws ProviderIsNotCompatibleException
237
	 * @throws ProviderIsNotUniqueException
238
	 * @throws QueryException
239
	 */
240
	private function testCreatingProvider($output) {
241
		$this->output($output, 'Creating mocked content provider.');
242
		$testProvider = $this->generateMockProvider();
243
		$this->output($output, true);
244
245
		return $testProvider;
246
	}
247
248
249
	/**
250
	 * @param $output
251
	 * @param IFullTextSearchProvider $testProvider
252
	 */
253
	private function testMockedProvider($output, IFullTextSearchProvider $testProvider) {
254
		$this->output($output, 'Testing mocked provider: get indexable documents.');
255
		$indexableDocuments =
256
			$testProvider->generateIndexableDocuments(TestService::DOCUMENT_USER1);
257
		$this->output($output, '(' . sizeof($indexableDocuments) . ' items)', false);
258
		$this->output($output, true);
259
	}
260
261
262
	/**
263
	 * @param $output
264
	 *
265
	 * @return IFullTextSearchPlatform
266
	 * @throws Exception
267
	 */
268
	private function testLoadingPlatform($output) {
269
		$this->output($output, 'Loading search platform.');
270
		$testPlatform = $this->platformService->getPlatform();
271
		$this->output($output, '(' . $testPlatform->getName() . ')', false);
272
		$this->output($output, true);
273
274
		$this->output($output, 'Testing search platform.');
275
		if (!$testPlatform->testPlatform()) {
276
			throw new Exception ('Search platform (' . $testPlatform->getName() . ') down ?');
277
		}
278
		$this->output($output, true);
279
280
		return $testPlatform;
281
	}
282
283
	/**
284
	 * @param OutputInterface $output
285
	 * @param IFullTextSearchPlatform $testPlatform
286
	 * @param IFullTextSearchProvider $testProvider
287
	 *
288
	 * @throws RunnerAlreadyUpException
289
	 */
290
	private function testLockingProcess(
291
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
292
		IFullTextSearchProvider $testProvider
293
	) {
294
		$this->output($output, 'Locking process');
295
		$this->runner = new Runner($this->runningService, 'test');
296
		$this->runner->start(true);
297
		$this->indexService->setRunner($this->runner);
298
		$testPlatform->setRunner($this->runner);
299
		$testProvider->setRunner($this->runner);
300
		$this->output($output, true);
301
	}
302
303
304
	/**
305
	 * @param OutputInterface $output
306
	 * @param IFullTextSearchProvider $testProvider
307
	 *
308
	 * @throws Exception
309
	 */
310
	private function testResetTest(OutputInterface $output, IFullTextSearchProvider $testProvider
311
	) {
312
		$this->output($output, 'Removing test.');
313
		$this->indexService->resetIndex($testProvider->getId());
314
		$this->output($output, true);
315
	}
316
317
318
	/**
319
	 * @param OutputInterface $output
320
	 * @param IFullTextSearchPlatform $testPlatform
321
	 */
322
	private function testInitIndexing(OutputInterface $output, IFullTextSearchPlatform $testPlatform
323
	) {
324
		$this->output($output, 'Initializing index mapping.');
325
		$testPlatform->initializeIndex();
326
		$this->output($output, true);
327
	}
328
329
330
	/**
331
	 * @param OutputInterface $output
332
	 * @param IFullTextSearchPlatform $testPlatform
333
	 * @param IFullTextSearchProvider $testProvider
334
	 *
335
	 * @throws InterruptException
336
	 * @throws TickDoesNotExistException
337
	 */
338
	private function testIndexingDocuments(
339
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
340
		IFullTextSearchProvider $testProvider
341
	) {
342
		$this->output($output, 'Indexing generated documents.');
343
		$options = new IndexOptions(
344
			[
345
				'provider' => TestProvider::TEST_PROVIDER_ID
346
			]
347
		);
348
		$this->indexService->indexProviderContentFromUser(
349
			$testPlatform, $testProvider, TestService::DOCUMENT_USER1, $options
350
		);
351
		$this->output($output, true);
352
	}
353
354
355
	/**
356
	 * @param OutputInterface $output
357
	 * @param IFullTextSearchPlatform $testPlatform
358
	 *
359
	 * @throws Exception
360
	 */
361
	private function testContentLicense(
362
		OutputInterface $output, IFullTextSearchPlatform $testPlatform
363
	) {
364
365
		try {
366
			$this->output($output, 'Retreiving content from a big index (license).');
367
			$indexDocument = $testPlatform->getDocument(
0 ignored issues
show
Bug introduced by
The method getDocument() does not seem to exist on object<OCA\FullTextSearc...FullTextSearchPlatform>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
368
				TestProvider::TEST_PROVIDER_ID, TestService::DOCUMENT_TYPE_LICENSE
369
			);
370
371
			$this->output(
372
				$output, '(size: ' . $indexDocument->getContentSize() . ')', false
373
			);
374
			$this->output($output, true);
375
		} catch (Exception $e) {
376
			throw new Exception(
377
				"Issue while getting test document '" . TestService::DOCUMENT_TYPE_LICENSE
378
				. "' from search platform: " . $e->getMessage()
379
			);
380
		}
381
382
		$this->output($output, 'Comparing document with source.');
383
		$this->testService->compareIndexDocument(
384
			$this->testService->generateIndexDocumentContentLicense(), $indexDocument
385
		);
386
		$this->output($output, true);
387
	}
388
389
390
	/**
391
	 * @param OutputInterface $output
392
	 * @param IFullTextSearchPlatform $testPlatform
393
	 *
394
	 * @param IFullTextSearchProvider $testProvider
395
	 *
396
	 * @throws Exception
397
	 */
398
	private function testSearchSimple(
399
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
400
		IFullTextSearchProvider $testProvider
401
	) {
402
403
		$this->output($output, 'Searching basic keywords:');
404
405
		$access = new DocumentAccess();
406
		$access->setViewerId(TestService::DOCUMENT_USER1);
407
408
		$this->search(
409
			$output, $testPlatform, $testProvider, $access, 'test',
410
			[TestService::DOCUMENT_TYPE_SIMPLE]
411
		);
412
		$this->search(
413
			$output, $testPlatform, $testProvider, $access, 'document is a simple test',
414
//			[TestService::DOCUMENT_TYPE_SIMPLE]
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
415
			[TestService::DOCUMENT_TYPE_SIMPLE, TestService::DOCUMENT_TYPE_LICENSE]
416
		);
417
		$this->search(
418
			$output, $testPlatform, $testProvider, $access, '"document is a test"',
419
			[]
420
		);
421
		$this->search(
422
			$output, $testPlatform, $testProvider, $access, '"document is a simple test"',
423
			[TestService::DOCUMENT_TYPE_SIMPLE]
424
		);
425
		$this->search(
426
			$output, $testPlatform, $testProvider, $access, 'document is a simple -test',
427
			[TestService::DOCUMENT_TYPE_LICENSE]
428
		);
429
		$this->search(
430
			$output, $testPlatform, $testProvider, $access, 'document is a simple +test',
431
			[TestService::DOCUMENT_TYPE_SIMPLE]
432
		);
433
		$this->search(
434
			$output, $testPlatform, $testProvider, $access, '-document is a simple test',
435
			[]
436
		);
437
	}
438
439
440
	/**
441
	 * @param OutputInterface $output
442
	 * @param IFullTextSearchPlatform $testPlatform
443
	 * @param IFullTextSearchProvider $testProvider
444
	 *
445
	 * @throws InterruptException
446
	 * @throws TickDoesNotExistException
447
	 */
448
	private function testUpdatingDocumentsAccess(
449
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
450
		IFullTextSearchProvider $testProvider
451
	) {
452
		$this->output($output, 'Updating documents access.');
453
		$options = new IndexOptions(
454
			[
455
				'provider'                            => TestProvider::TEST_PROVIDER_ID,
456
				TestService::DOCUMENT_INDEXING_OPTION => TestService::DOCUMENT_INDEXING_ACCESS
457
			]
458
		);
459
		$testProvider->setIndexOptions($options);
460
		$this->indexService->indexProviderContentFromUser(
461
			$testPlatform, $testProvider, TestService::DOCUMENT_USER1, $options
462
		);
463
		$this->output($output, true);
464
	}
465
466
467
	/**
468
	 * @param OutputInterface $output
469
	 * @param IFullTextSearchPlatform $platform
470
	 *
471
	 * @param IFullTextSearchProvider $provider
472
	 *
473
	 * @throws Exception
474
	 */
475
	private function testSearchAccess(
476
		OutputInterface $output, IFullTextSearchPlatform $platform,
477
		IFullTextSearchProvider $provider
478
	) {
479
480
		$this->output($output, 'Searching with group access rights:');
481
482
		$this->searchGroups($output, $platform, $provider, [], []);
483
		$this->searchGroups(
484
			$output, $platform, $provider, [TestService::DOCUMENT_GROUP1],
485
			[TestService::DOCUMENT_TYPE_LICENSE]
486
		);
487
		$this->searchGroups(
488
			$output, $platform, $provider,
489
			[TestService::DOCUMENT_GROUP1, TestService::DOCUMENT_GROUP2],
490
			[TestService::DOCUMENT_TYPE_LICENSE]
491
		);
492
		$this->searchGroups(
493
			$output, $platform, $provider,
494
			[TestService::DOCUMENT_NOTGROUP, TestService::DOCUMENT_GROUP2],
495
			[TestService::DOCUMENT_TYPE_LICENSE]
496
		);
497
		$this->searchGroups($output, $platform, $provider, [TestService::DOCUMENT_NOTGROUP], []);
498
	}
499
500
501
	/**
502
	 * @param OutputInterface $output
503
	 * @param IFullTextSearchPlatform $platform
504
	 *
505
	 * @param IFullTextSearchProvider $provider
506
	 *
507
	 * @throws Exception
508
	 */
509
	private function testSearchShare(
510
		OutputInterface $output, IFullTextSearchPlatform $platform,
511
		IFullTextSearchProvider $provider
512
	) {
513
514
		$this->output($output, 'Searching with share rights:');
515
516
		$this->searchUsers($output, $platform, $provider, TestService::DOCUMENT_NOTUSER, []);
517
		$this->searchUsers($output, $platform, $provider, TestService::DOCUMENT_USER2, ['license']);
518
		$this->searchUsers($output, $platform, $provider, TestService::DOCUMENT_USER3, ['license']);
519
	}
520
521
522
	/**
523
	 * @param OutputInterface $output
524
	 *
525
	 * @throws TickDoesNotExistException
526
	 */
527
	private function testUnlockingProcess(OutputInterface $output) {
528
		$this->output($output, 'Unlocking process');
529
		$this->runner->stop();
530
		$this->output($output, true);
531
	}
532
533
534
	/**
535
	 * @param OutputInterface $output
536
	 * @param IFullTextSearchPlatform $testPlatform
537
	 * @param IFullTextSearchProvider $testProvider
538
	 * @param DocumentAccess $access
539
	 * @param string $search
540
	 * @param array $expected
541
	 * @param string $moreOutput
542
	 *
543
	 * @throws Exception
544
	 */
545
	private function search(
546
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
547
		IFullTextSearchProvider $testProvider,
548
		DocumentAccess $access, $search, $expected, $moreOutput = ''
549
	) {
550
		$this->output(
551
			$output,
552
			" - '" . $search . "'" . (($moreOutput === '') ? '' : ' - ' . $moreOutput . ' - ')
553
		);
554
		$request = new SearchRequest();
555
556
		$request->setSearch($search);
557
		$searchResult = $testPlatform->searchDocuments($testProvider, $access, $request);
558
		$this->output(
559
			$output,
560
			'(result: ' . $searchResult->getCount() . ', expected: ' . json_encode($expected) . ')',
561
			false
562
		);
563
		$this->compareSearchResult($searchResult, $expected);
564
		$this->output($output, true);
565
	}
566
567
568
	/**
569
	 * @param OutputInterface $output
570
	 * @param IFullTextSearchPlatform $testPlatform
571
	 * @param IFullTextSearchProvider $testProvider
572
	 * @param array $groups
573
	 * @param array $expected
574
	 *
575
	 * @throws Exception
576
	 */
577 View Code Duplication
	private function searchGroups(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
578
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
579
		IFullTextSearchProvider $testProvider, $groups, $expected
580
	) {
581
582
		$access = new DocumentAccess();
583
		$access->setViewerId(TestService::DOCUMENT_NOTUSER);
584
		$access->setGroups($groups);
585
586
		$this->search(
587
			$output, $testPlatform, $testProvider, $access, 'license',
588
			$expected, json_encode($groups)
589
		);
590
	}
591
592
593
	/**
594
	 * @param OutputInterface $output
595
	 * @param IFullTextSearchPlatform $testPlatform
596
	 * @param IFullTextSearchProvider $testProvider
597
	 * @param string $user
598
	 * @param array $expected
599
	 *
600
	 * @throws Exception
601
	 */
602 View Code Duplication
	private function searchUsers(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
603
		OutputInterface $output, IFullTextSearchPlatform $testPlatform,
604
		IFullTextSearchProvider $testProvider, $user, $expected
605
	) {
606
		$access = new DocumentAccess();
607
		$access->setViewerId($user);
608
		$this->search(
609
			$output, $testPlatform, $testProvider, $access, 'license',
610
			$expected, $user
611
		);
612
	}
613
614
615
	/**
616
	 * @param SearchResult $searchResult
617
	 * @param $entries
618
	 *
619
	 * @throws Exception
620
	 */
621
	private function compareSearchResult(SearchResult $searchResult, $entries) {
622
		$documents = $searchResult->getDocuments();
623
		if (sizeof($documents) !== sizeof($entries)) {
624
			throw new \Exception('Unexpected SearchResult: ' . json_encode($searchResult));
625
		}
626
627
		foreach ($documents as $document) {
628
			if (!in_array($document->getId(), $entries)) {
629
				throw new \Exception('Unexpected Document: ' . json_encode($document));
630
			}
631
		}
632
	}
633
634
635
	/**
636
	 * @param OutputInterface $output
637
	 * @param int $s
638
	 *
639
	 * @throws InterruptException
640
	 */
641
	private function pause(OutputInterface $output, $s) {
642
		$this->output($output, 'Pausing ' . $s . ' seconds');
643
644
		for ($i = 1; $i <= $s; $i++) {
645
			if (time_nanosleep(1, 0) !== true) {
646
				throw new InterruptException('Interrupted by user');
647
			}
648
649
			$this->output($output, $i, false);
650
		}
651
652
		$this->output($output, true);
653
	}
654
655
}
656
657
658
659