@@ -416,6 +416,74 @@ public function testCSVExportImportCompatibility()
416416 }
417417 }
418418
419+ /**
420+ * Test that CSV parsing handles trailing empty lines gracefully.
421+ * Trailing empty lines in CSV files produce rows like [''] which have
422+ * a different count than headers, previously causing:
423+ * "CSV row does not match the number of header columns."
424+ */
425+ public function testCSVParsingHandlesTrailingEmptyLines (): void
426+ {
427+ $ filepath = self ::RESOURCES_DIR . 'trailing_empty_lines.csv ' ;
428+ $ stream = fopen ($ filepath , 'r ' );
429+ $ this ->assertNotFalse ($ stream );
430+
431+ $ headers = fgetcsv ($ stream , 0 , ', ' , '" ' , '" ' );
432+ $ this ->assertSame (['id ' , 'name ' , 'age ' ], $ headers );
433+
434+ $ rows = [];
435+ while (($ row = fgetcsv ($ stream , 0 , ', ' , '" ' , '" ' )) !== false ) {
436+ // Simulate the fixed behavior: skip empty rows
437+ if (\count ($ row ) === 1 && \trim ($ row [0 ]) === '' ) {
438+ continue ;
439+ }
440+ $ rows [] = $ row ;
441+ }
442+ fclose ($ stream );
443+
444+ // Should have exactly 2 data rows, trailing empty line should be skipped
445+ $ this ->assertCount (2 , $ rows );
446+ $ this ->assertSame (['1 ' , 'Alice ' , '23 ' ], $ rows [0 ]);
447+ $ this ->assertSame (['2 ' , 'Bob ' , '30 ' ], $ rows [1 ]);
448+ }
449+
450+ /**
451+ * Test that CSV parsing handles rows with fewer columns than headers.
452+ * Short rows should be padded with empty strings rather than throwing.
453+ */
454+ public function testCSVParsingHandlesShortRows (): void
455+ {
456+ $ filepath = self ::RESOURCES_DIR . 'short_rows.csv ' ;
457+ $ stream = fopen ($ filepath , 'r ' );
458+ $ this ->assertNotFalse ($ stream );
459+
460+ $ headers = fgetcsv ($ stream , 0 , ', ' , '" ' , '" ' );
461+ $ this ->assertSame (['id ' , 'name ' , 'age ' ], $ headers );
462+ $ headerCount = \count ($ headers );
463+
464+ $ rows = [];
465+ while (($ row = fgetcsv ($ stream , 0 , ', ' , '" ' , '" ' )) !== false ) {
466+ if (\count ($ row ) === 1 && \trim ($ row [0 ]) === '' ) {
467+ continue ;
468+ }
469+ // Simulate the fixed behavior: pad short rows
470+ if (\count ($ row ) < $ headerCount ) {
471+ $ row = \array_pad ($ row , $ headerCount , '' );
472+ }
473+ $ rows [] = \array_combine ($ headers , $ row );
474+ }
475+ fclose ($ stream );
476+
477+ $ this ->assertCount (3 , $ rows );
478+ $ this ->assertSame ('Alice ' , $ rows [0 ]['name ' ]);
479+ $ this ->assertSame ('23 ' , $ rows [0 ]['age ' ]);
480+ // Short row should have been padded
481+ $ this ->assertSame ('Bob ' , $ rows [1 ]['name ' ]);
482+ $ this ->assertSame ('' , $ rows [1 ]['age ' ]); // Padded with empty string
483+ $ this ->assertSame ('Charlie ' , $ rows [2 ]['name ' ]);
484+ $ this ->assertSame ('25 ' , $ rows [2 ]['age ' ]);
485+ }
486+
419487 private function recursiveDelete (string $ dir ): void
420488 {
421489 if (is_dir ($ dir )) {
0 commit comments