462 lines
15 KiB
PHP
462 lines
15 KiB
PHP
<?php
|
|
|
|
namespace LoretaAudit\Tests;
|
|
|
|
use Exception;
|
|
use SimpleXMLElement;
|
|
use ZipArchive;
|
|
|
|
class ExcelDataExtractForTestingValues
|
|
{
|
|
/**
|
|
* Global Temp folder
|
|
*/
|
|
private $globalExtractionFolder;
|
|
|
|
/**
|
|
* Temp folder for single file
|
|
*/
|
|
private $extractionFolder;
|
|
|
|
/**
|
|
* Active sheet file
|
|
*/
|
|
private $activeSheetId;
|
|
|
|
/**
|
|
* Default sheet file
|
|
*/
|
|
private $defaultActiveSheetId = 1;
|
|
|
|
/**
|
|
* Workbook XML file path within zip
|
|
*/
|
|
private $workbookXmlFile = '/xl/workbook.xml';
|
|
|
|
/**
|
|
* SharedStrings XML file path within zip
|
|
*/
|
|
private $sharedStringsFile = '/xl/sharedStrings.xml';
|
|
|
|
/**
|
|
* Sheet XML file path within zip
|
|
*/
|
|
private $SheetBaseFile = '/xl/worksheets/sheet%d.xml';
|
|
|
|
/**
|
|
* SharedStrings XML object
|
|
* SimpleXMLElement $sharedStringsObject
|
|
*/
|
|
private $sharedStringsObject = null;
|
|
|
|
/**
|
|
* SharedStrings XML object
|
|
* SimpleXMLElement $activeSheetObject
|
|
*/
|
|
private $activeSheetObject = null;
|
|
|
|
private const ERROR_NODE_NOT_FOUND = 10;
|
|
|
|
public function __construct(string $tempDirectory)
|
|
{
|
|
$this->globalExtractionFolder = $tempDirectory;
|
|
if (!strlen($this->globalExtractionFolder)) {
|
|
throw new Exception("No Temp extraction directory specified!");
|
|
}
|
|
|
|
// Set shared directory
|
|
if (!file_exists($this->globalExtractionFolder)) {
|
|
mkdir($this->globalExtractionFolder, 0777, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $file
|
|
* @return bool|null
|
|
* @throws Exception
|
|
*/
|
|
public function loadFileForExtraction(string $file): ?bool
|
|
{
|
|
// Fail on invalid file
|
|
if (!file_exists($file) || !is_file($file)) {
|
|
throw new Exception("Base extraction file not found!");
|
|
}
|
|
|
|
$this->activeSheetObject = null;
|
|
$this->sharedStringsObject = null;
|
|
|
|
// Set sheet ID
|
|
$this->activeSheetId = $this->defaultActiveSheetId;
|
|
|
|
// Create file temp folder
|
|
$fileInfo = pathinfo($file);
|
|
$this->extractionFolder = $this->globalExtractionFolder . '/' . $fileInfo['filename'] . '_data_';
|
|
|
|
$i = 1;
|
|
while (file_exists($this->extractionFolder . $i)) {
|
|
$i++;
|
|
}
|
|
$this->extractionFolder .= $i;
|
|
|
|
// Copy base file as zip
|
|
mkdir($this->extractionFolder, 0777, true);
|
|
$newFile = $this->globalExtractionFolder . '/' . $fileInfo['filename'] . '.zip';
|
|
copy($file, $newFile);
|
|
|
|
// Extract xls file data
|
|
$zip = new ZipArchive();
|
|
$res = $zip->open($newFile);
|
|
if ($res === true) {
|
|
$zip->extractTo($this->extractionFolder);
|
|
$zip->close();
|
|
} else {
|
|
throw new Exception("Data extraction from base file has failed!");
|
|
}
|
|
|
|
// Validate that we got base xml files
|
|
if (
|
|
!file_exists($this->extractionFolder . $this->workbookXmlFile)
|
|
|| !file_exists($this->extractionFolder . $this->sharedStringsFile)
|
|
|| !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId))
|
|
) {
|
|
throw new Exception("Structure files are missing!");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param string $sheetTitle
|
|
* @return int
|
|
* @throws Exception
|
|
*/
|
|
public function getSheetIdByTitle(string $sheetTitle): int
|
|
{
|
|
if (
|
|
strlen($sheetTitle) == 0
|
|
|| !file_exists($this->extractionFolder . $this->workbookXmlFile)
|
|
) {
|
|
throw new Exception("Sheet title or base workbook file is missing!");
|
|
}
|
|
|
|
$xmlObject = simplexml_load_file($this->extractionFolder . $this->workbookXmlFile);
|
|
if ($xmlObject instanceof SimpleXMLElement) {
|
|
foreach ($xmlObject->sheets->sheet as $sheet) {
|
|
if ($sheet['name'] == $sheetTitle) {
|
|
return (int)$sheet['sheetId'];
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Exception("Excel '$sheetTitle' sheet not found!");
|
|
}
|
|
|
|
/**
|
|
* @param int $sheetId
|
|
* @return bool|null
|
|
* @throws Exception
|
|
*/
|
|
public function setActiveWorkingSheetById(int $sheetId): ?bool
|
|
{
|
|
if (
|
|
$sheetId <= 0
|
|
|| !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $sheetId))
|
|
) {
|
|
throw new Exception("Sheet ID or sheet XML file is missing!");
|
|
}
|
|
|
|
if ($this->activeSheetId != $sheetId) {
|
|
$this->activeSheetId = $sheetId;
|
|
$this->activeSheetObject = null;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param int $x
|
|
* @param int $y
|
|
* @return string|null
|
|
* @throws Exception
|
|
*/
|
|
public function extractTextFromCell(int $x, int $y): ?string
|
|
{
|
|
if ($x <= 0 || $y <= 0) {
|
|
throw new Exception("Invalid coordinates!");
|
|
}
|
|
|
|
if (
|
|
$this->activeSheetId <= 0
|
|
|| !file_exists(sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId))
|
|
) {
|
|
throw new Exception("Sheet ID or sheet XML file is missing!");
|
|
}
|
|
|
|
$value = null;
|
|
|
|
// Load sheet XMl if not loaded
|
|
if (is_null($this->activeSheetObject)) {
|
|
$this->activeSheetObject = simplexml_load_file(
|
|
sprintf($this->extractionFolder . $this->SheetBaseFile, $this->activeSheetId)
|
|
);
|
|
|
|
// Without namespace XPath queries won't work
|
|
if ($this->activeSheetObject instanceof SimpleXMLElement) {
|
|
$namespaces = $this->activeSheetObject->getDocNamespaces();
|
|
|
|
if (is_array($namespaces)) {
|
|
foreach ($namespaces as $prefix => $namespace) {
|
|
$this->activeSheetObject->registerXPathNamespace($prefix ?: 'default', $namespace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for data
|
|
if ($this->activeSheetObject instanceof SimpleXMLElement) {
|
|
$reqCell = $this->activeSheetObject->xpath(
|
|
'//default:sheetData/default:row[@r="' . $y . '"]/default:c[@r="' . self::getAddress($x, $y) . '"]'
|
|
);
|
|
|
|
|
|
if (!is_array($reqCell) || count($reqCell) <= 0) {
|
|
$message = "Cell " . self::getAddress($x, $y) . " not found in '" .
|
|
basename(sprintf($this->SheetBaseFile, $this->activeSheetId)) . "'!";
|
|
throw new Exception($message, self::ERROR_NODE_NOT_FOUND);
|
|
}
|
|
$reqCell = $reqCell[0];
|
|
|
|
// Get value
|
|
$value = $reqCell->{'v'};
|
|
|
|
// Check if formula exists - then return formula
|
|
if (isset($reqCell->{'f'})) {
|
|
$value = $reqCell->{'f'};
|
|
}
|
|
|
|
// Check if we got rich text set in cell
|
|
if (isset($reqCell['t']) && $reqCell['t'] == 'inlineStr') {
|
|
$value = $reqCell->{'is'}->{'r'}->{'t'};
|
|
}
|
|
|
|
// Check if we got a string from sharedStrings.xml and retrieve text
|
|
if (isset($reqCell['t']) && $reqCell['t'] == 's') {
|
|
// Load shared strings if not loaded
|
|
if (is_null($this->sharedStringsObject)) {
|
|
$this->sharedStringsObject = simplexml_load_file(
|
|
$this->extractionFolder . $this->sharedStringsFile
|
|
);
|
|
|
|
// Without namespace XPath queries won't work
|
|
if ($this->sharedStringsObject instanceof SimpleXMLElement) {
|
|
$namespaces = $this->sharedStringsObject->getDocNamespaces();
|
|
if (is_array($namespaces)) {
|
|
foreach ($namespaces as $prefix => $namespace) {
|
|
$this->sharedStringsObject->registerXPathNamespace($prefix ?: 'default', $namespace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get string by index
|
|
if ($this->sharedStringsObject instanceof SimpleXMLElement) {
|
|
$reqCell = $this->sharedStringsObject->xpath('//default:si[' . ((int)$value + 1) . ']/default:t');
|
|
if ($reqCell === false || !is_array($reqCell) || count($reqCell) <= 0) {
|
|
throw new Exception("Shared string not found in '" . basename($this->sharedStringsFile) . "'!");
|
|
}
|
|
|
|
$value = $reqCell[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @param int $x
|
|
* @param int $y
|
|
* @return string
|
|
*/
|
|
public static function getAddress(int $x, int $y): string
|
|
{
|
|
$x = $x <= 0 ? 1 : $x;
|
|
$y = $y <= 0 ? 1 : $y;
|
|
|
|
$sk = 0;
|
|
$iX = $x;
|
|
$address = '';
|
|
|
|
while (1) {
|
|
if ($iX > 26) {
|
|
while ($iX > 26) {
|
|
$iX -= 26;
|
|
$sk++;
|
|
}
|
|
|
|
$address = Chr(64 + $iX) . $address;
|
|
$iX = $sk;
|
|
$sk = 0;
|
|
} else {
|
|
return chr(64 + $iX) . $address . $y;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param BasePhpUnitSeleniumCase $baseTestCase
|
|
* @param string $file
|
|
* @param array<string, array<int, array<int, string>>> $excelValueCfg -
|
|
* ['sheet_title' => ['row_index' => ['col_index 0 based' => 'assertion value']]]
|
|
* @param bool $assertByStringContainsString
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function assertionsOnExcelSheetData(
|
|
BasePhpUnitSeleniumCase $baseTestCase,
|
|
string $file = '',
|
|
array $excelValueCfg = [],
|
|
bool $assertByStringContainsString = false
|
|
): void {
|
|
if (count($excelValueCfg) == 0) {
|
|
throw new Exception("Validation configuration is missing!");
|
|
}
|
|
|
|
try {
|
|
$this->loadFileForExtraction($file);
|
|
|
|
// Loop cfg & assert values within Excel sheets
|
|
foreach ($excelValueCfg as $sheetTitle => $rows) {
|
|
$this->setActiveWorkingSheetById(
|
|
$this->getSheetIdByTitle($sheetTitle)
|
|
);
|
|
|
|
foreach ($rows as $rowExactIndex => $values) {
|
|
foreach ($values as $k => $val) {
|
|
// Check is formula - remove = sign since formulas are stored without it
|
|
if (mb_substr($val, 0, 1) == '=') {
|
|
$val = mb_substr($val, 1);
|
|
}
|
|
|
|
// Find cell in xml & do the assertion
|
|
try {
|
|
$rawActual = (string)$this->extractTextFromCell($k + 1, $rowExactIndex);
|
|
$rawExpected = (string)$val;
|
|
|
|
$errorMessage = "Error within '$sheetTitle' sheet cell '" .
|
|
self::getAddress($k + 1, $rowExactIndex) . "'!" .
|
|
"\nexpected: $val \n actual: $rawActual";
|
|
|
|
if ($assertByStringContainsString) {
|
|
$baseTestCase->assertStringContainsString($val, $rawActual, $errorMessage);
|
|
} else {
|
|
if (is_numeric($rawActual) && is_numeric($rawExpected)) {
|
|
if (!$this->excelNumbersEqual($rawActual, $rawExpected, 3)) {
|
|
$baseTestCase->fail($errorMessage);
|
|
}
|
|
} else {
|
|
$baseTestCase->assertEquals($rawExpected, $rawActual, $errorMessage);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Expect empty string assertions to fail on xml lookup - since empty cells are not stored
|
|
if (
|
|
strlen($val) == 0 &&
|
|
$e->getCode() == self::ERROR_NODE_NOT_FOUND
|
|
) {
|
|
continue;
|
|
} else {
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$baseTestCase->fail($e->getMessage());
|
|
}
|
|
|
|
$baseTestCase->clearTempFolder($this->globalExtractionFolder);
|
|
}
|
|
|
|
private function excelNumbersEqual($a, $b, int $precision = 3): bool
|
|
{
|
|
if (!is_numeric($a) || !is_numeric($b)) {
|
|
return false;
|
|
}
|
|
|
|
// Round precision
|
|
$a = round((float)$a, $precision);
|
|
$b = round((float)$b, $precision);
|
|
|
|
return bccomp((string)$a, (string)$b, $precision) === 0;
|
|
}
|
|
|
|
/**
|
|
* Generate a PHP expected array file directly from Excel XML (1:1)
|
|
*
|
|
* @param string $excelFilePath Path to a source Excel file
|
|
* @param string $outputPhpFile Path where a PHP file will be written
|
|
* @param string $sheetTitle Excel sheet title
|
|
* @param int $startRow First row index (Excel-based)
|
|
* @param int $endRow Last row index (inclusive)
|
|
* @param int $startCol First column index (Excel-based)
|
|
* @param int $endCol Last column index (inclusive)
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function generateExpectedPhpArrayFile(
|
|
string $excelFilePath,
|
|
string $outputPhpFile,
|
|
string $sheetTitle,
|
|
int $startRow,
|
|
int $endRow,
|
|
int $startCol,
|
|
int $endCol
|
|
): void {
|
|
if ($startRow <= 0 || $endRow < $startRow) {
|
|
throw new Exception('Invalid row range');
|
|
}
|
|
|
|
if ($startCol <= 0 || $endCol < $startCol) {
|
|
throw new Exception('Invalid column range');
|
|
}
|
|
|
|
$this->loadFileForExtraction($excelFilePath);
|
|
|
|
$sheetId = $this->getSheetIdByTitle($sheetTitle);
|
|
$this->setActiveWorkingSheetById($sheetId);
|
|
|
|
$result = [];
|
|
|
|
for ($row = $startRow; $row <= $endRow; $row++) {
|
|
$rowData = [];
|
|
|
|
for ($col = $startCol; $col <= $endCol; $col++) {
|
|
try {
|
|
$rowData[] = (string)$this->extractTextFromCell($col, $row);
|
|
} catch (Exception $e) {
|
|
// Empty cells are not stored in XML
|
|
if ($e->getCode() === self::ERROR_NODE_NOT_FOUND) {
|
|
$rowData[] = '';
|
|
} else {
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
|
|
$result[$row] = $rowData;
|
|
}
|
|
|
|
$php = "<?php\n\n";
|
|
$php .= "/**\n";
|
|
$php .= " * AUTO-GENERATED FILE\n";
|
|
$php .= " * Source: " . basename($excelFilePath) . "\n";
|
|
$php .= " */\n\n";
|
|
$php .= "return " . var_export($result, true) . ";\n";
|
|
|
|
if (file_put_contents($outputPhpFile, $php) === false) {
|
|
throw new Exception("Failed to write output file: $outputPhpFile");
|
|
}
|
|
}
|
|
}
|