faster spreadsheet exporter based on opensout and other export improvements (#5238)

This commit is contained in:
Kevin Papst
2024-12-22 18:36:47 +01:00
committed by GitHub
parent c7f0508707
commit 136104d4b0
80 changed files with 2023 additions and 1699 deletions

View File

@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\ArrayFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\ArrayFormatter
*/
class ArrayFormatterTest extends TestCase
{
public function testFormatValueReturnsCommaSeparatedStringForArray(): void
{
$formatter = new ArrayFormatter();
$result = $formatter->formatValue(['one', 'two', 'three']);
self::assertEquals('one, two, three', $result);
}
public function testFormatValueThrowsExceptionForNonArray(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only arrays are supported');
$formatter = new ArrayFormatter();
$formatter->formatValue('not an array');
}
public function testFormatValueReturnsEmptyStringForEmptyArray(): void
{
$formatter = new ArrayFormatter();
$result = $formatter->formatValue([]);
self::assertEquals('', $result);
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\BooleanFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\BooleanFormatter
*/
class BooleanFormatterTest extends TestCase
{
public function testFormatValueReturnsFalseForNull(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(null);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForTrue(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForFalse(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(false);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForNonZeroNumber(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(1);
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForZero(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue(0);
self::assertFalse($result);
}
public function testFormatValueReturnsTrueForNonEmptyString(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue('non-empty');
self::assertTrue($result);
}
public function testFormatValueReturnsFalseForEmptyString(): void
{
$formatter = new BooleanFormatter();
$result = $formatter->formatValue('');
self::assertFalse($result);
}
public function testFormatValueThrowsExceptionForNonScalar(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new BooleanFormatter();
$formatter->formatValue([]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DateFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DateFormatter
*/
class DateFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedDateForDateTime(): void
{
$formatter = new DateFormatter();
$date = new \DateTime('2023-10-01');
$result = $formatter->formatValue($date);
self::assertEquals('2023-10-01', $result);
}
public function testFormatValueReturnsNullForNullValue(): void
{
$formatter = new DateFormatter();
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonDateTime(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only DateTimeInterface can be formatted');
$formatter = new DateFormatter();
$formatter->formatValue('not a date');
}
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DefaultFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DefaultFormatter
*/
class DefaultFormatterTest extends TestCase
{
public function testFormatValueReturnsSameValueForScalar(): void
{
$formatter = new DefaultFormatter();
$result = $formatter->formatValue('string');
self::assertEquals('string', $result);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonScalar(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new DefaultFormatter();
$formatter->formatValue([]);
}
public function testFormatValueThrowsExceptionForObject(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only scalar values are supported');
$formatter = new DefaultFormatter();
$formatter->formatValue(new \stdClass());
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\DurationFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\DurationFormatter
*/
class DurationFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedDurationForNumericValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(7200);
self::assertEquals(2.00, $result);
}
public function testFormatValueReturnsZeroForNonNumericValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForFloatValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(4500.5);
self::assertEquals(1.25, $result);
}
public function testFormatValueReturnsZeroForNullValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(null);
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedDurationForNegativeValue(): void
{
$formatter = new DurationFormatter();
$result = $formatter->formatValue(-3600);
self::assertEquals(-1.00, $result);
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\RateFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\RateFormatter
*/
class RateFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedFloatForNumericValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(1234.5678);
self::assertEquals(1234.57, $result);
$result = $formatter->formatValue('1234.5678');
self::assertEquals(1234.57, $result);
}
public function testFormatValueThrowsForNonNumericValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$result = $formatter->formatValue('not a number');
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsZeroForNullValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(null);
self::assertEquals(0.0, $result);
}
public function testFormatValueReturnsFormattedFloatForNegativeNumericValue(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(-1234.5678);
self::assertEquals(-1234.57, $result);
}
public function testFormatValueReturnsFormattedFloatForFloatWithTwoDecimalPlaces(): void
{
$formatter = new RateFormatter();
$result = $formatter->formatValue(1234.56);
self::assertEquals(1234.56, $result);
}
public function testFormatValueThrowsExceptionForNonNumericNonNullValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$formatter->formatValue(new \stdClass());
}
public function testFormatValueThrowsExceptionFoArrayValue(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only numeric values be formatted');
$formatter = new RateFormatter();
$formatter->formatValue([]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\TextFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\TextFormatter
*/
class TextFormatterTest extends TestCase
{
public function testFormatValueReturnsSanitizedStringWhenSanitizeDdeIsTrue(): void
{
$formatter = new TextFormatter(true);
$result = $formatter->formatValue('=cmd|\' /C calc\'!A0');
self::assertEquals('\' =cmd|\' /C calc\'!A0', $result);
}
public function testFormatValueReturnsOriginalStringWhenSanitizeDdeIsFalse(): void
{
$formatter = new TextFormatter(false);
$result = $formatter->formatValue('=cmd|\' /C calc\'!A0');
self::assertEquals('=cmd|\' /C calc\'!A0', $result);
}
public function testFormatValueReturnsOriginalValueForNonStringWhenSanitizeDdeIsTrue(): void
{
$formatter = new TextFormatter(true);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
public function testFormatValueReturnsOriginalValueForNonStringWhenSanitizeDdeIsFalse(): void
{
$formatter = new TextFormatter(false);
$result = $formatter->formatValue(123);
self::assertEquals(123, $result);
$result = $formatter->formatValue(45.67);
self::assertEquals(45.67, $result);
$result = $formatter->formatValue(true);
self::assertTrue($result);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package\CellFormatter;
use App\Export\Package\CellFormatter\TimeFormatter;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\CellFormatter\TimeFormatter
*/
class TimeFormatterTest extends TestCase
{
public function testFormatValueReturnsFormattedTimeForDateTimeInterface(): void
{
$formatter = new TimeFormatter();
$dateTime = new \DateTime('2023-10-10 14:30:00');
$result = $formatter->formatValue($dateTime);
self::assertEquals('14:30', $result);
}
public function testFormatValueReturnsNullForNullValue(): void
{
$formatter = new TimeFormatter();
$result = $formatter->formatValue(null);
self::assertNull($result);
}
public function testFormatValueThrowsExceptionForNonDateTimeInterface(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Only DateTimeInterface can be formatted');
$formatter = new TimeFormatter();
$formatter->formatValue('not a DateTime');
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Entity\ExportableItem;
use App\Export\Package\CellFormatter\CellFormatterInterface;
use App\Export\Package\Column;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\Column
*/
class ColumnTest extends TestCase
{
public function testGetNameReturnsColumnName(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
self::assertEquals('testName', $column->getName());
}
public function withHeaderSetsHeader(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$column->withHeader('testHeader');
self::assertEquals('testHeader', $column->getHeader());
}
public function withExtractorSetsExtractor(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$extractor = function (ExportableItem $item) {
return $item->getId();
};
$column->withExtractor($extractor);
$exportableItem = $this->createMock(ExportableItem::class);
$exportableItem->method('getId')->willReturn(123);
self::assertEquals(123, $column->extract($exportableItem));
}
public function testExtractThrowsExceptionWhenExtractorIsNull(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Missing extractor on column: testName');
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$exportableItem = $this->createMock(ExportableItem::class);
$column->extract($exportableItem);
}
public function testGetValueReturnsFormattedValue(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$formatter->method('formatValue')->willReturn('formattedValue');
$column = new Column('testName', $formatter);
$extractor = function (ExportableItem $item) {
return 'rawValue';
};
$column->withExtractor($extractor);
$exportableItem = $this->createMock(ExportableItem::class);
self::assertEquals('formattedValue', $column->getValue($exportableItem));
}
public function testGetHeaderReturnsHeaderWhenSet(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
$column->withHeader('testHeader');
self::assertEquals('testHeader', $column->getHeader());
}
public function testGetHeaderReturnsNameWhenHeaderIsNull(): void
{
$formatter = $this->createMock(CellFormatterInterface::class);
$column = new Column('testName', $formatter);
self::assertEquals('testName', $column->getHeader());
}
}

View File

@@ -0,0 +1,121 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Export\Package\PhpOfficeSpreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\PhpOfficeSpreadsheet
*/
class PhpOfficeSpreadsheetTest extends TestCase
{
private string $filename;
private int $counter = 1;
protected function setUp(): void
{
$this->filename = realpath(__DIR__ . '/../../_data/') . '/test' . $this->counter++ . '.xlsx';
}
protected function tearDown(): void
{
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
public function testopenSetsFilename(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('filename');
$property->setAccessible(true);
self::assertEquals($this->filename, $property->getValue($spreadsheetPackage));
}
public function testsaveThrowsExceptionWhenFilenameIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Need to call open() first before save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->save();
}
public function testsaveWritesFile(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Foo', 'Bar']);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$spreadsheetPackage->addRow(['Data3', 'Data4']);
$spreadsheetPackage->save();
self::assertGreaterThan(0, filesize($this->filename));
}
public function testsaveThrowsExceptionWhenSpreadsheetIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot re-use spreadsheet after calling save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->save();
$spreadsheetPackage->save();
}
public function testsetHeaderSetsHeaderRow(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Column1', 'Column2']);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('worksheet');
$property->setAccessible(true);
/** @var Worksheet $worksheet */
$worksheet = $property->getValue($spreadsheetPackage);
self::assertNotNull($worksheet);
self::assertEquals('Column1', $worksheet->getCell('A1')->getValue());
self::assertEquals('Column2', $worksheet->getCell('B1')->getValue());
}
public function testaddRowAddsDataRow(): void
{
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$reflection = new \ReflectionClass($spreadsheetPackage);
$property = $reflection->getProperty('worksheet');
$property->setAccessible(true);
/** @var Worksheet $worksheet */
$worksheet = $property->getValue($spreadsheetPackage);
self::assertEquals('Data1', $worksheet->getCell('A1')->getValue());
self::assertEquals('Data2', $worksheet->getCell('B1')->getValue());
}
public function testaddRowThrowsExceptionWhenSpreadsheetIsNull(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Cannot re-use spreadsheet after calling save()');
$spreadsheetPackage = new PhpOfficeSpreadsheet();
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->save();
$spreadsheetPackage->addRow(['Data1', 'Data2']);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Tests\Export\Package;
use App\Export\Package\SpoutSpreadsheet;
use OpenSpout\Writer\CSV\Writer;
use PHPUnit\Framework\TestCase;
/**
* @covers \App\Export\Package\SpoutSpreadsheet
*/
class SpoutSpreadsheetTest extends TestCase
{
private string $filename;
private int $counter = 1;
protected function setUp(): void
{
$this->filename = realpath(__DIR__ . '/../../_data/') . '/test' . $this->counter++ . '.xlsx';
}
protected function tearDown(): void
{
if (file_exists($this->filename)) {
unlink($this->filename);
}
}
public function testsaveWritesFile(): void
{
$spreadsheetPackage = new SpoutSpreadsheet(new Writer());
$spreadsheetPackage->open($this->filename);
$spreadsheetPackage->setHeader(['Foo', 'Bar']);
$spreadsheetPackage->addRow(['Data1', 'Data2']);
$spreadsheetPackage->addRow(['Data3', 'Data4']);
$spreadsheetPackage->save();
self::assertGreaterThan(0, filesize($this->filename));
}
}