tempFile = tempnam(sys_get_temp_dir(), 'csvreader_test_'); } protected function tearDown(): void { if (file_exists($this->tempFile)) { unlink($this->tempFile); } } private function write(string $content): void { file_put_contents($this->tempFile, $content); } private function reader(int $headerLine = 1, string $delimiter = ';', bool $hasBom = false): CsvReader { return new CsvReader( $this->tempFile, ['inputDelimiter' => $delimiter, 'headerLine' => $headerLine, 'hasBom' => $hasBom] ); } // ------------------------------------------------------------------------- // readCsvData – basic parsing // ------------------------------------------------------------------------- public function testReadCsvDataSimple(): void { $this->write("Name;Age\nAlice;30\nBob;25\n"); $data = $this->reader()->readCsvData(); $this->assertCount(2, $data); $this->assertSame('Alice', $data[0]['Name']); $this->assertSame('30', $data[0]['Age']); $this->assertSame('Bob', $data[1]['Name']); } public function testReadCsvDataTrimsWhitespace(): void { $this->write("Name ; Age\n Alice ; 30 \n"); $data = $this->reader()->readCsvData(); $this->assertSame('Alice', $data[0]['Name']); $this->assertSame('30', $data[0]['Age']); } // ------------------------------------------------------------------------- // headerLine offset // ------------------------------------------------------------------------- public function testHeaderLineOffset(): void { // Lines 1+2 are metadata, header is line 3 $this->write("Meta1\nMeta2\nColA;ColB\nval1;val2\n"); $data = $this->reader(3)->readCsvData(); $this->assertCount(1, $data); $this->assertSame('val1', $data[0]['ColA']); $this->assertSame('val2', $data[0]['ColB']); } // ------------------------------------------------------------------------- // readMetadataLines // ------------------------------------------------------------------------- public function testReadMetadataLines(): void { $this->write("Line1\nLine2\nHeader;Col\nData;Row\n"); $meta = $this->reader(3)->readMetadataLines(); $this->assertCount(2, $meta); $this->assertSame('Line1', $meta[0]); $this->assertSame('Line2', $meta[1]); } public function testReadMetadataLinesHeaderOnLine1IsEmpty(): void { $this->write("Name;Age\nAlice;30\n"); $meta = $this->reader(1)->readMetadataLines(); $this->assertSame([], $meta); } public function testReadMetadataLinesHeaderOnLine2ReturnsOneLine(): void { $this->write("MetaInfo\nName;Age\nAlice;30\n"); $meta = $this->reader(2)->readMetadataLines(); $this->assertCount(1, $meta); $this->assertSame('MetaInfo', $meta[0]); } // ------------------------------------------------------------------------- // maxDataRows limit // ------------------------------------------------------------------------- public function testMaxDataRowsLimit(): void { $this->write("Name;Age\nAlice;30\nBob;25\nCarol;20\n"); $data = $this->reader()->readCsvData(2); $this->assertCount(2, $data); $this->assertSame('Alice', $data[0]['Name']); $this->assertSame('Bob', $data[1]['Name']); } public function testMaxDataRowsZeroMeansAll(): void { $this->write("Name;Age\nAlice;30\nBob;25\nCarol;20\n"); $data = $this->reader()->readCsvData(0); $this->assertCount(3, $data); } // ------------------------------------------------------------------------- // Empty line skipping // ------------------------------------------------------------------------- public function testEmptyLinesAreSkipped(): void { $this->write("Name;Age\nAlice;30\n\nBob;25\n\n"); $data = $this->reader()->readCsvData(); $this->assertCount(2, $data); } // ------------------------------------------------------------------------- // BOM removal // ------------------------------------------------------------------------- public function testBomIsRemovedFromFirstColumnName(): void { // UTF-8 BOM followed immediately by the header $this->write("\xEF\xBB\xBFName;Age\nAlice;30\n"); $data = $this->reader(1, ';', true)->readCsvData(); $this->assertCount(1, $data); // The column must be 'Name', not the BOM-prefixed version $this->assertArrayHasKey('Name', $data[0]); $this->assertSame('Alice', $data[0]['Name']); } public function testNoBomFlagLeavesHeaderIntact(): void { // If hasBom is false, BOM bytes stay and the key will be mangled — we only // assert that the clean path (hasBom=true) works, tested above. // Here we just verify normal CSV without BOM also works when hasBom=false. $this->write("Name;Age\nAlice;30\n"); $data = $this->reader(1, ';', false)->readCsvData(); $this->assertArrayHasKey('Name', $data[0]); } // ------------------------------------------------------------------------- // getHeaders // ------------------------------------------------------------------------- public function testGetHeaders(): void { $this->write("Col1;Col2;Col3\nA;B;C\n"); $headers = $this->reader()->getHeaders(); $this->assertSame(['Col1', 'Col2', 'Col3'], $headers); } // ------------------------------------------------------------------------- // Row with fewer columns than headers // ------------------------------------------------------------------------- public function testShortRowPaddedWithEmpty(): void { $this->write("A;B;C\n1;2\n"); $data = $this->reader()->readCsvData(); $this->assertCount(1, $data); $this->assertSame('1', $data[0]['A']); $this->assertSame('2', $data[0]['B']); $this->assertSame('', $data[0]['C']); } // ------------------------------------------------------------------------- // Error cases // ------------------------------------------------------------------------- public function testFileNotFoundThrowsRuntimeException(): void { $this->expectException(\RuntimeException::class); $reader = new CsvReader('/nonexistent/path/file.csv', ['inputDelimiter' => ';', 'headerLine' => 1]); $reader->readLines(); } public function testHeaderLineBeyondFileLengthThrows(): void { $this->write("Name;Age\nAlice;30\n"); $this->expectException(\RuntimeException::class); $this->reader(99)->readCsvData(); } }