<?php
namespace Tlf\BigDb;
/**
* Base class for testing:
*
* - Create, set props directly, and save
* - Create Orm, set props from db, modify, save
* - Delete a saved article
* - Create, set props from form, and save
* - Fail when setting from form with invalid Data
* - Fail when setting from form without appropriate access level.
* - Phad form can be loaded & printed
* - Phad view can be loaded & printed
* - Cannot be read if access is forbidden (such as articles that are private)
* - Search-related entries are created/deleted/etc
* - Cannot be created when invalid state exists (i.e. can't CREATE author page for a user who already has one, but CAN modify existing author entry)
* - Data validation via phad form (crud should inadvertently test this, but we should also make this an explicit test)
*
*/
abstract class OrmTester extends \Tlf\Tester {
/**
* Active test information
* TODO: Make this into a struct (value object bc PHP doesn't have structs!!!)
*/
protected array $active_test = [];
public string $orm_class;
/**
* Rows to insert before running this class's tests. Each table will be emptied before inserts
* array<string table_name, array meta_data>
* meta_data array <columns: array of strings, rows: array of arrays>
* columns array<int index, string col_name> array of column names
* rows array<int row_num, array row> array of rows (row_num is not used)
* row array<int index, mixed col_value>
*
*/
public array $create_before_test = [];
public array $valid_form_data;
public array $invalid_form_data;
/**
* array<int index, string method_name> Methods to call after Orm is initialized from database.
* These should initialize non-database properties like an article body that is stored on disk and lazy-loaded.
*/
public array $call_after_dbload = [];
abstract public function get_bigdb(\PDO $pdo): \Tlf\BigDb;
/**
* The seed to use for mt_srand(), which determines the random datasets generated
*/
public int $seed;
/**
* The number of times mt_rand() has been called in total during this execution AFTER setting the seed
*/
public int $rand_called_count = 0;
/**
* Set true to add more verbose debug output
*/
public bool $verbose = false;
public function prepare(){
if (is_file($this->file('test/bootstrap.php'))){
require($this->file('test/bootstrap.php'));
}
$this->create_before_test($this->create_before_test);
if (!isset($this->options['seed'])){
$this->options['seed'] = mt_rand();
}
$this->seed = (int)$this->options['seed'];
mt_srand($this->options['seed']);
if (isset($this->options['rand_count'])){
$c = (int)$this->options['rand_count'];
while ($c-- > 0){
$this->rand_called_count++;
mt_rand();
}
}
if (isset($this->options['verbose'])){
$this->verbose = true;
}
}
/**
* @param $table_rows_to_insert see self::create_before_test
*/
protected function create_before_test(array $table_rows_to_insert){
$pdo = $this->get_pdo();
foreach ($table_rows_to_insert as $table => $metadata){
$column_names = $metadata['columns'];
$table_rows = $metadata['rows'];
$pdo->exec("DELETE FROM `$table`");
if (count($table_rows)==0 || count($column_names) == 0)continue;
$keyed_rows = array_map(function($row) use ($column_names){return array_combine($column_names, $row);}, $table_rows);
$this->dbInsertAll($pdo, $table, $keyed_rows);
}
}
public function get_pdo(): \PDO {
return $this->getPdoFromSettingsFile($this->file('config/secret.json'));
}
public function get_orm(\PDO $pdo, \Tlf\BigDb $db): \Tlf\BigOrm {
$orm_class = $this->orm_class;
$orm = new $orm_class($db);
return $orm;
}
/**
* Get a random set of data from an array of data-options.
*
*
* @param $data_options array<string key, array options>
* @param $data_indices array<string key, int index_within_options>
*
*
* @var options array<int index, mixed value_option>
* @var value_option mixed any value
*
* @return array<string key, mixed value_option>
*/
public function get_random_data(array $data_options, ?array &$data_indices): array {
$data_indices = [];
$out_data = [];
foreach ($data_options as $key=>$options){
$start = 0;
$end = count($options) - 1;
$this->rand_called_count++;
$index = mt_rand($start,$end);
$out_data[$key] = $options[$index];
$data_indices[$key] = $index;
}
return $out_data;
}
public function get_data_entries(array $data_options, array $data_indices){
}
/**
*
* @param $data_options array<string key, array data_options>
* @param $dataset_indices string like "key:index, key2:index2, key3:index3"
*
* @return array<string key, array data_options> except data_options will have only one entry for each key, as determined by dataset_indices
*/
public function get_data_options_from_dataset_string(array $data_options, string $dataset_indices): array {
$list = explode(", ",$dataset_indices);
$return_data = [];
foreach ($list as $entry){
$parts = explode(":", $entry);
$key = $parts[0];
$index = $parts[1];
$return_data[$key] = [$data_options[$key][$index]];
}
return $return_data;
}
/**
* Run tests on a semi-random set of data, of the given size, built from $valid_data_options, and tested by $test_function.
* Outputs useful error information
*
* CLI Option -num_tests will overwrite $default_num_datasets_to_test
* CLI Option -data "..." will cause only one test to run, for the given dataset only
* When you run tests, failed datasets will print their -data option.
*
* The passed $test_function should call `$this->pass_if(statement_that_should_be_true, "Message Describing Test")`
*
* @param $valid_data_options array<string key, array value_options> an options set of data to build individual semi-random datasets from.
* @param $default_num_datasets_to_test The number of datasets to generate. Can be overridden with cli option -num_tests
* @param $test_function function(\Tlf\BigDb $db, \Tlf\BigFormOrm $orm, array $data_row_to_test, string $id_column_name)
*/
public function run_valid_data_tests(array $valid_data_options, int $default_num_datasets_to_test, callable $test_function){
if (empty($valid_data_options)){
throw new \Exception("Your valid test data array is empty. property \$valid_form_data should be set when using default tests.");
}
$num_datasets_to_test = $this->options['num_tests'] ?? $default_num_datasets_to_test;
if (isset($this->options['data'])){
$num_datasets_to_test = 1;
$valid_data_options = $this->get_data_options_from_dataset_string($valid_data_options, $this->options['data']);
}
$pdo = $this->get_pdo();
$db = $this->get_bigdb($pdo);
//$pass_count = 0;
//$fail_count = 0;
$dataset_results = [];
$i=-1;
while (++$i < $num_datasets_to_test){
$failed_assertions_count = $this->assertions['fail'];
$sample_dataset_rand_count = $this->rand_called_count;
$dataset = $this->get_random_data($valid_data_options, $dataset_indices);
$sample_dataset_info = '"'.implode(", ",
array_map(
function(string $k, string $v): string { return "$k:$v"; },
array_keys($dataset_indices),
$dataset_indices
)
).'"';
echo "\n### Test $i ###"
."\n Dataset: ".$sample_dataset_info;
if ($this->verbose){
echo "\n\n Dataset being tested: \n";
print_r($dataset);
echo "\n\n\n --- END dataset being tested --- \n\n\n";
}
echo "\n\n";
$this->active_test =
[
'data' => $dataset,
'starting_rand_count' => $sample_dataset_rand_count,
'indices' => $dataset_indices,
'status' => 'initialized',
'messages'=> '',
'test_num'=> $i,
'passed_tests'=>[],
'failed_tests'=>[],
];
try {
echo "\nTest Loop $i";
$orm = $this->get_orm($pdo, $db);
$test_function($db, $orm, $dataset, 'id');
} catch (\Exception $e){
$this->active_test['failed_tests'][] = 'Exception: '.$e->getMessage()."\n\n".$e->getTraceAsString();
}
if ($failed_assertions_count != $this->assertions['fail']
//&& count($this->active_test['failed_tests']) == 0
){
$this->active_test['failed_tests'][] = 'ASSERTION FAILURE';
}
$testresult = $this->active_test;
unset($testresult['data']);
$dataset_results[] = $this->active_test;
}
$pass_count = 0;
$fail_count = 0;
$failure_descriptions_list = [];
$failed_sets_list = [];
foreach ($dataset_results as $dataset_test){
$passed = false;
if (count($dataset_test['failed_tests']) > 0){
$passed = false;
} else if (count($dataset_test['passed_tests'])==0){
$passed = false;
} else {
$passed = true;
}
if ($passed)$pass_count++;
else $fail_count++;
$this->handleDidPass($passed);
if (!$passed){
$indices = [];
foreach ($dataset_test['indices'] as $key=>$index){
$indices[] = "$key:$index";
}
$test_num = $dataset_test['test_num'];
$sample_dataset_rand_count = $dataset_test['starting_rand_count'];
$sample_dataset_info = '"'.implode(", ", $indices).'"';
$failed_sets_list[] = 'Test '.$test_num.': '.$sample_dataset_info. " -rand_count ".$sample_dataset_rand_count;
//print_r($dataset_test['failed_tests']);
$failure_descriptions_list[] =
'Test '.$test_num.': '
.implode(", ", $dataset_test['failed_tests']);
}
}
echo "\n\n ### RESULTS ###"
. "\n Random Dataset Seed: ".$this->seed
. "\n Random Called Count: ".$this->rand_called_count
. "\n Datasets Tested: ".count($dataset_results)
. "\n Num Passed: ". $pass_count
. "\n Num Failed: ". $fail_count
. "\n Failed sets: "
. "\n " . implode("\n ",$failed_sets_list)
. "\n Failed Test Descriptions: "
. "\n " . implode("\n ", $failure_descriptions_list)
."\n\n";
$current_test_name = substr($this->current_test,4); // remove 'test' from the beginning
$current_test_class = basename(str_replace("\\","/",get_class($this)));
echo "\n\nRe-run a specific dataset with: \n";
echo " phptest -class $current_test_class -test $current_test_name -data $sample_dataset_info";
echo "\nRe-run this exact test with: \n";
echo " phptest -class $current_test_class -test $current_test_name -seed ".$this->seed;
echo "\nRe-run a specific dataset WITH a specified seed: \n";
echo " phptest -class $current_test_class -test $current_test_name -data $sample_dataset_info -seed ".$this->seed." -rand_count ".$sample_dataset_rand_count;
echo "\n";
}
public function pass_if(bool $condition_is_true, string $test_description){
if ($condition_is_true){
$this->active_test['passed_tests'][] = $test_description;
} else {
$this->active_test['failed_tests'][] = $test_description;
}
}
/**
* For the created_at and updated_at properties on `$orm`, set the milliseconds to zero, if those properties are DateTime objects.
*/
public function remove_datetime_milliseconds(\Tlf\BigOrm $orm){
// remove millisecondss from DateTime properties bc MySql throws away millis.
if (isset($orm->created_at)&&$orm->created_at instanceof \DateTime){
$ca = $orm->created_at;
$ca->setTime($ca->format('H'), $ca->format('i'), $ca->format('s'), 0);
}
if (isset($orm->updated_at)&&$orm->updated_at instanceof \DateTime){
$ca = $orm->updated_at;
$ca->setTime($ca->format('H'), $ca->format('i'), $ca->format('s'), 0);
}
}
public function testDeleteRowViaOrm(){
$this->disable();
echo "\n\nThis test has not been implemented\n\n";
}
public function testDeleteRowCreatedFromFormData(){
$pdo = $this->get_pdo();
$db = $this->get_bigdb($pdo);
$this->run_valid_data_tests(
$this->valid_form_data,
$default_test_count = 60,
function(\Tlf\BigDb $db, \Tlf\BigFormOrm $orm, array $data_row_to_test, string $id_column_name) {
$orm->set_from_form($data_row_to_test);
$orm->save();
$id = $orm->$id_column_name;
$loaded_orm_list = $db->get($orm->table(),[$id_column_name=>$id]);
$this->pass_if(($count=count($loaded_orm_list))==1, "Load 1 ORM after save. (Loaded $count)");
$orm->delete();
$this->pass_if(!$orm->is_saved(), "Deleted ORM->is_saved() should return false");
$loaded_orm_list = $db->get($orm->table(),[$id_column_name=>$id]);
$this->pass_if(($count=count($loaded_orm_list))==0, "Load zero ORMs after delete. (Loaded $count)");
}
);
}
public function testUpdateRowFromForm(){
$pdo = $this->get_pdo();
$db = $this->get_bigdb($pdo);
$row_index = -1;
$this->run_valid_data_tests(
$this->valid_form_data,
$default_test_count = 60,
function(\Tlf\BigDb $db, \Tlf\BigFormOrm $orm, array $data_row_to_test, string $id_column_name) use ($row_index) {
do {
$random_data_for_creation = $this->get_random_data($this->valid_form_data, $random_data_indice);
}
while ($random_data_for_creation == $data_row_to_test);
if ($this->verbose){
echo "\n\n Random Dataset used to create initial ORM:
\n (Note: The Dataset being tested (above), will MODIFY the orm/row created by this random dataset)
\n";
print_r($random_data_for_creation);
echo "\n\n\n --- END Random Dataset --- \n\n\n";
}
$orm->set_from_form($random_data_for_creation);
$orm->save();
$this->remove_datetime_milliseconds($orm);
$id = $orm->$id_column_name;
$loaded_orm_list = $db->get($orm->table(),[$id_column_name=>$id]);
$loaded_orm = $loaded_orm_list[0];
foreach ($this->call_after_dbload as $method){
$loaded_orm->$method();
}
if ($this->verbose){
echo "\n\n The database row created from the Random Dataset
\n (Note: This data comes from calling \$orm->get_db_row(). The \$orm is hydraded from the actual database row that was created from the random data.)
\n";
print_r($random_data_for_creation);
echo "\n\n\n --- END Random dataset's database row --- \n\n\n";
}
$this->test("Check if orm instantiated from array matches orm loaded from database");
$orms_match = $this->compare_object_properties($orm, $loaded_orm);
$this->pass_if($orms_match, "Initial ORM (from random data) is identical to loaded orm after it is saved");
$loaded_orm->set_from_form($data_row_to_test);
$loaded_orm->save();
$this->remove_datetime_milliseconds($loaded_orm);
$loaded_orm_list2 = $db->get($orm->table(),[$id_column_name=>$id]);
$loaded_orm2 = $loaded_orm_list2[0];
foreach ($this->call_after_dbload as $method){
$loaded_orm2->$method();
}
if ($this->verbose){
echo "\n\n The final database row after UPDATE.
\n (Note: Random data creates the row. Test Data modifies the row. THIS is the end result after modification)
\n (Note: This data comes from calling \$orm->get_db_row(). The \$orm is hydraded from the actual database row that was created from the random data.)
\n";
print_r($random_data_for_creation);
echo "\n\n\n --- END Modified Database Row --- \n\n\n";
}
$this->test("Initial ORM (from random data) is DIFFERENT from the Orm loaded after UPDATE");
$this->invert();
$orms_are_same = $this->compare_object_properties($orm, $loaded_orm2);
$this->invert();
$this->pass_if(!$orms_are_same, "Initial ORM (from random data) is DIFFERENT from the Orm loaded after UPDATE");
$this->pass_if($this->compare_object_properties($loaded_orm, $loaded_orm2), "Loaded Orm 1 (which was modified by form data) is identical to Loaded Orm 2 (which was loaded from the database after UPDATE)");
}
);
}
public function testUpdateRowBySettingOrmProperties(){
$this->disable();
}
public function testReadIntoOrmFromDb(){
$this->disable();
}
public function testReadIntoFormFromDb(){
$this->disable();
}
public function testCreateRowFromForm(){
$pdo = $this->get_pdo();
$db = $this->get_bigdb($pdo);
$this->run_valid_data_tests(
$this->valid_form_data,
$default_test_count = 60,
function(\Tlf\BigDb $db, \Tlf\BigFormOrm $orm, array $data_row_to_test, string $id_column_name) {
//print_r($data_row_to_test);
$orm->set_from_form($data_row_to_test);
$orm->save();
$this->pass_if($orm->is_saved(), 'Orm Saved');
$loaded_orm_list = $db->get($orm->table(),[$id_column_name=>$orm->$id_column_name]);
$this->pass_if(($count=count($loaded_orm_list))==1, "Load 1 ORM after save. (Loaded $count)");
$this->remove_datetime_milliseconds($orm);
foreach ($this->call_after_dbload as $method){
if (!isset($loaded_orm_list[0]))break;
$loaded_orm_list[0]->$method();
}
$this->pass_if(
$this->compare_object_properties($orm, $loaded_orm_list[0]),
"Loaded object properties match saved object properties",
);
}
);
}
public function testCreateRowBySettingOrmProperties(){
$this->disable();
// HOW? Some properties MUST be coerced, like DateTimes
// Perhaps I can have an interface for get_property_value_options()
// Then the default implementation could just get them from this->good_property_values
// But if you need something non-static, then you can override get_property_value_options()
// Or I could support some automatic coersion.
// get valid orm properties
// instantiate orms
// set properties directly
// save to db
// READ from db
// Load DB row into Orm
// Read same properties from Orm
// Ensure they match original properties
}
}