Files
Enelix-EMS/tests/V4DataCapture/native_checks.php
T

62 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
// ALL IPS functions below are mocks. Never execute this test in a real Symcon kernel.
if (PHP_SAPI !== 'cli') throw new RuntimeException('CLI isolation required');
require_once __DIR__ . '/../../examples/V4DataCapture/install.php';
$count=0;$objects=[];$values=[];$scripts=[];$timers=[];$nextId=60000;$locks=[];$timerCalls=[];
function yes(bool $v,string $s):void{global$count;if(!$v)throw new RuntimeException('FAIL '.$s);$count++;echo 'PASS '.$s."\n";}
function IPS_GetKernelVersion(){return '8.0';}
function IPS_GetObject($id){return $GLOBALS['objects'][$id]??throw new RuntimeException('Missing object');}
function IPS_GetChildrenIDs($id){return array_keys(array_filter($GLOBALS['objects'],static fn($o)=>$o['ParentID']===$id));}
function IPS_InstanceExists($id){return $id===17004;}
function IPS_GetInstance($id){return ['ModuleInfo'=>['ModuleID'=>'{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}']];}
function IPS_VariableExists($id){return isset($GLOBALS['values'][$id]);}
function IPS_GetVariable($id){$v=$GLOBALS['values'][$id];return ['VariableType'=>is_string($v)?3:2,'VariableUpdated'=>time(),'VariableChanged'=>time()-10];}
function GetValue($id){return $GLOBALS['values'][$id];}
function SetValue($id,$v){if($id<60000||IPS_GetObject($id)['ObjectIdent']!=='CaptureStatus')throw new RuntimeException('Foreign variable write');$GLOBALS['values'][$id]=$v;return true;}
function IPS_SemaphoreEnter($key,$wait){if($GLOBALS['locks'][$key]??false)return false;$GLOBALS['locks'][$key]=true;return true;}
function IPS_SemaphoreLeave($key){$GLOBALS['locks'][$key]=false;}
function makeObj($type){$id=$GLOBALS['nextId']++;$GLOBALS['objects'][$id]=['ObjectType'=>$type,'ObjectIdent'=>'','ParentID'=>0,'ObjectInfo'=>''];return $id;}
function IPS_CreateCategory(){return makeObj(0);}
function IPS_CreateScript($type){$id=makeObj(3);$GLOBALS['scripts'][$id]='<?php';return $id;}
function IPS_CreateVariable($type){if($type!==3)throw new RuntimeException('Only string status allowed');$id=makeObj(2);$GLOBALS['values'][$id]='';return $id;}
function setObj($id,$key,$v){if($id<60000)throw new RuntimeException('Existing object modified');$GLOBALS['objects'][$id][$key]=$v;return true;}
function IPS_SetParent($id,$v){return setObj($id,'ParentID',$v);}
function IPS_SetIdent($id,$v){return setObj($id,'ObjectIdent',$v);}
function IPS_SetName($id,$v){return setObj($id,'ObjectName',$v);}
function IPS_SetInfo($id,$v){return setObj($id,'ObjectInfo',$v);}
function IPS_GetScriptContent($id){return $GLOBALS['scripts'][$id];}
function IPS_SetScriptContent($id,$v){if($id<60000)throw new RuntimeException('Foreign script write');$GLOBALS['scripts'][$id]=$v;return true;}
function IPS_SetScriptTimer($id,$seconds){if($id<60000||IPS_GetObject($id)['ObjectIdent']!=='RawCapture')throw new RuntimeException('Foreign timer changed');$GLOBALS['timers'][$id]=$seconds;$GLOBALS['timerCalls'][]=[$id,$seconds];return true;}
// A separate fake stage with real candidate code and known synthetic numeric sources.
$base=sys_get_temp_dir().'/v4-native-capture-'.bin2hex(random_bytes(5));mkdir($base,0700);mkdir($base.'/data',0700);
$repo=dirname(__DIR__,2);$names=['libs/NetzfahrplanV4Bilanzierung.php','libs/NetzfahrplanV4Messaufnahme.php','examples/V4DataCapture/runtime.php','examples/V4DataCapture/install.php'];
$hashes=[];
foreach($names as $name){$p=$base.'/source/'.$name;if(!is_dir(dirname($p)))mkdir(dirname($p),0700,true);copy($repo.'/'.$name,$p);$hashes['source/'.$name]=hash_file('sha256',$p);}
$info=['source'=>'synthetic_test','physicalKwh'=>322,'evKwh'=>160];file_put_contents($base.'/USER_REPORTED_INVENTORY.json',json_encode($info));
$c=['version'=>1,'installationId'=>'e3a08f9e-af12-4695-99bd-8b51c0520021','managerId'=>17004,'reportedInventorySha256'=>hash_file('sha256',$base.'/USER_REPORTED_INVENTORY.json'),
'extraSources'=>[],'accounting'=>['version'=>1,'maxSkewSeconds'=>30,'splitToleranceW'=>100.,'sources'=>[]]];
foreach(['grid','pv','physical_storage','controlled_virtual','external_virtual']as$i=>$role){$id=101+$i;$c['accounting']['sources'][]=['key'=>'x'.$i,'variableId'=>$id,'parentId'=>200,'ident'=>'v'.$i,'role'=>$role,'factorToW'=>1,'maxAgeSeconds'=>60,'dependsOn'=>[]];$objects[$id]=['ParentID'=>200,'ObjectIdent'=>'v'.$i,'ObjectType'=>2,'ObjectInfo'=>'original'];$values[$id]=[1000,2000,500,500,0][$i];}
file_put_contents($base.'/config.json',json_encode($c));$hashes['config.json']=hash_file('sha256',$base.'/config.json');$hashes['USER_REPORTED_INVENTORY.json']=hash_file('sha256',$base.'/USER_REPORTED_INVENTORY.json');
file_put_contents($base.'/MANIFEST.json',json_encode(['scope'=>'passive_raw_capture_only','hashes'=>$hashes]));
$before=$objects;$beforeValues=$values;
try{
ob_start();$r=v4captureInstall($base);ob_end_clean();
yes($r['status']==='capture_installed','passive installation completes');
yes(count($r['createdObjects'])===4,'only category, two scripts and one status variable');
yes($timers[$r['scriptId']]===30,'only new capture timer runs at 30 seconds');
yes($r['firstCapture']['status']==='recorded'&&count(file($r['firstCapture']['journal']))===1,'first capture actually written before activation');
yes($r['firstCapture']['accountingQuality']==='consistent_candidate','candidate is not a training permit');
foreach($before as$id=>$o){yes($objects[$id]===$o && $values[$id]===$beforeValues[$id],'existing source preserved '.$id);}
$size=count($objects);ob_start();$again=v4captureInstall($base);ob_end_clean();yes(count($objects)===$size&&$again['createdObjects']===[],'installation idempotent, no duplicate timers');
$bad=$scripts[$r['scriptId']];$scripts[$r['scriptId']]="<?php // concurrent edit\n";ob_start();$err=v4captureInstall($base);ob_end_clean();yes($err['status']==='needs_review'&&$scripts[$r['scriptId']]!==$bad,'parallel script edit refused');$scripts[$r['scriptId']]=$bad;
$values[101]=null;$raw=v4captureRun($base,$r['categoryId']);yes($raw['status']==='recorded'&&$raw['accountingQuality']==='unavailable','missing measurement still records an explicit gap');$values[101]=1000;
v4captureStop($base,$r['categoryId']);yes($timers[$r['scriptId']]===0,'stop affects capture timer only');yes(file_exists($r['firstCapture']['journal']),'stop retains original journal');
ob_start();$resume=v4captureInstall($base);ob_end_clean();yes($resume['status']==='capture_installed'&&$timers[$r['scriptId']]===30,'restart by installer retains data');
$objects[$r['categoryId']]['ObjectInfo']='unrelated';ob_start();$err=v4captureInstall($base);ob_end_clean();yes($err['status']==='needs_review','foreign category identity not taken over');
foreach(['runtime.php','install.php']as$f){$s=file_get_contents($repo.'/examples/V4DataCapture/'.$f);yes(!preg_match('/\b(?:RequestAction|IPS_RequestAction|IPS_SetProperty|IPS_ApplyChanges|MC_ReloadModule|AC_SetLoggingStatus|curl_exec|shell_exec)\s*\(/',$s),'no control/network/module reload in '.$f);}
}finally{foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST)as$p){$p->isDir()&&!$p->isLink()?rmdir($p->getPathname()):unlink($p->getPathname());}rmdir($base);}
echo "TOTAL $count native-mock checks passed; no Symcon kernel calls.\n";