2022-07-26 17:44:58 +03:00
|
|
|
<?php
|
|
|
|
namespace taskman;
|
|
|
|
use Amp;
|
2022-08-03 17:40:44 +03:00
|
|
|
use ClickHouseDB;
|
2022-08-05 18:23:05 +03:00
|
|
|
use Exception;
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
interface IATFDevicePool
|
|
|
|
{
|
|
|
|
function get();
|
|
|
|
}
|
|
|
|
|
|
|
|
class ATFApkInstaller
|
|
|
|
{
|
|
|
|
const MAX_INSTALLS_IN_PROGRESS = 5;
|
|
|
|
|
|
|
|
const ST_IN_PROGRESS = 1;
|
|
|
|
const ST_INSTALLED = 2;
|
|
|
|
|
|
|
|
private $apk_name;
|
|
|
|
private $installs = array();
|
|
|
|
private $override_conf;
|
|
|
|
|
|
|
|
function __construct($apk_name, $override_conf)
|
|
|
|
{
|
|
|
|
$this->apk_name = $apk_name;
|
|
|
|
$this->override_conf = $override_conf;
|
|
|
|
}
|
|
|
|
|
|
|
|
function countInstallsInProgress()
|
|
|
|
{
|
|
|
|
$c = 0;
|
|
|
|
foreach($this->installs as $status)
|
|
|
|
if($status == self::ST_IN_PROGRESS)
|
|
|
|
++$c;
|
|
|
|
return $c;
|
|
|
|
}
|
|
|
|
|
|
|
|
function installAsync($device)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device) {
|
|
|
|
|
|
|
|
while($this->countInstallsInProgress() >= self::MAX_INSTALLS_IN_PROGRESS)
|
|
|
|
yield Amp\delay(1000);
|
|
|
|
|
|
|
|
if(isset($this->installs[$device]))
|
|
|
|
return;
|
|
|
|
$this->installs[$device] = self::ST_IN_PROGRESS;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell am force-stop %{package_id}%", DEPLOY_OPT_ERR_OK);
|
|
|
|
if($this->apk_name !== null)
|
|
|
|
{
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device uninstall %{package_id}%", DEPLOY_OPT_ERR_OK, 30);
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device install -r ./{$this->apk_name}", 0, 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->installs[$device] = self::ST_INSTALLED;
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
atf_log("Error during install *$device*: " . $e->getMessage());
|
|
|
|
unset($this->installs[$device]);
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2022-08-18 20:37:36 +03:00
|
|
|
|
|
|
|
function getSize()
|
|
|
|
{
|
|
|
|
list($status, $lines) = atf_host_exec("ls -sd -- ./{$this->apk_name}", DEPLOY_OPT_ERR_OK|DEPLOY_OPT_SILENT);
|
|
|
|
if($status != 0)
|
|
|
|
return 0;
|
|
|
|
$items = explode(' ', $lines[0]);
|
|
|
|
//ls -s returns size in blocks where each block is 512 bytes long
|
|
|
|
return $items[0]*512;
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ATFAdbDevicePool implements IATFDevicePool
|
|
|
|
{
|
|
|
|
function get()
|
|
|
|
{
|
|
|
|
return atf_get_devices();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ATFFixedDevicePool implements IATFDevicePool
|
|
|
|
{
|
|
|
|
private $devices;
|
|
|
|
|
|
|
|
function __construct(array $devices)
|
|
|
|
{
|
|
|
|
$this->devices = $devices;
|
|
|
|
}
|
|
|
|
|
|
|
|
function get()
|
|
|
|
{
|
|
|
|
return $this->devices;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ATFCachedDevices implements IATFDevicePool
|
|
|
|
{
|
|
|
|
private $provider;
|
|
|
|
private $cached = null;
|
|
|
|
private $last_cache_time;
|
|
|
|
private $keep_cache_time;
|
|
|
|
|
|
|
|
function __construct(IATFDevicePool $provider, $keep_cache_time)
|
|
|
|
{
|
|
|
|
$this->provider = $provider;
|
|
|
|
$this->keep_cache_time = $keep_cache_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
function get()
|
|
|
|
{
|
|
|
|
if($this->cached === null || (time() - $this->last_cache_time) > $this->keep_cache_time)
|
|
|
|
{
|
|
|
|
$this->cached = $this->provider->get();
|
|
|
|
$this->last_cache_time = time();
|
|
|
|
}
|
|
|
|
return $this->cached;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ATFSession
|
|
|
|
{
|
|
|
|
public $guid;
|
|
|
|
public $version;
|
|
|
|
public $branch;
|
|
|
|
public $rev_hash;
|
|
|
|
|
|
|
|
public $plans = array();
|
|
|
|
|
|
|
|
public $ignored_devices = array();
|
|
|
|
public $bogus_device_count = array();
|
|
|
|
|
|
|
|
public $share_with_qa_chan = true;
|
|
|
|
public $shared_qa_errors = array();
|
|
|
|
|
|
|
|
function __construct()
|
|
|
|
{
|
|
|
|
$this->guid = atf_guid();
|
2022-09-23 08:04:25 +03:00
|
|
|
$this->branch = get('GAME_VERSION_BRANCH');
|
|
|
|
$this->version = get('GAME_VERSION');
|
|
|
|
$this->rev_hash = get('GAME_REVISION_HASH');
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function incBogusDevice($device)
|
|
|
|
{
|
|
|
|
if(!isset($this->bogus_device_count[$device]))
|
|
|
|
$this->bogus_device_count[$device] = 0;
|
|
|
|
return ++$this->bogus_device_count[$device];
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetBogusDeviceCount($device)
|
|
|
|
{
|
|
|
|
$this->bogus_device_count[$device] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetBogusDevices()
|
|
|
|
{
|
|
|
|
$this->bogus_device_count = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
function addPlan(ATFPlan $plan)
|
|
|
|
{
|
|
|
|
$plan->session = $this;
|
|
|
|
$this->plans[] = $plan;
|
|
|
|
}
|
|
|
|
|
|
|
|
function resetPlans()
|
|
|
|
{
|
|
|
|
$this->plans = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
function run($apk_path = null, $apk_reuse = false, $override_conf = false, $adb_reboot = true)
|
|
|
|
{
|
|
|
|
$apk_name = $apk_path === null ? null : basename($apk_path);
|
|
|
|
|
|
|
|
_atf_start_watchdog($this->guid);
|
|
|
|
|
|
|
|
//1. deploy an apk file to the atf host
|
|
|
|
if(!$apk_reuse && $apk_path !== null)
|
|
|
|
{
|
|
|
|
if(!is_file($apk_path))
|
|
|
|
throw new Exception("No such file '$apk_path'");
|
|
|
|
|
|
|
|
$local_crc = _atf_get_local_file_crc($apk_path);
|
|
|
|
$remote_crc = _atf_get_remote_file_crc($apk_name);
|
|
|
|
|
|
|
|
if($remote_crc === '' || $local_crc !== $remote_crc)
|
|
|
|
atf_host_put_file($apk_path, $apk_name);
|
|
|
|
else
|
|
|
|
atf_log("Skipping same .apk upload");
|
|
|
|
}
|
|
|
|
|
|
|
|
//2. reboot just in case
|
|
|
|
if($adb_reboot)
|
|
|
|
atf_adb_reboot();
|
|
|
|
|
|
|
|
|
|
|
|
$install = new ATFApkInstaller($apk_name, $override_conf);
|
2022-08-18 20:37:36 +03:00
|
|
|
$this->trySendApkStatsEvent($install->getSize());
|
|
|
|
|
2022-07-26 17:44:58 +03:00
|
|
|
foreach($this->plans as $plan)
|
|
|
|
$this->_runPlan($install, $plan);
|
|
|
|
|
|
|
|
foreach($this->plans as $plan)
|
|
|
|
{
|
|
|
|
if($plan->hasFatalProblems() ||
|
|
|
|
($plan->isOver() && !$plan->getDevices()))
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _runPlan(ATFApkInstaller $install, ATFPlan $plan)
|
|
|
|
{
|
|
|
|
atf_log("Starting " . $plan->getTitle() . "...");
|
|
|
|
|
|
|
|
//NOTE: hung_threshold must be larger than gone_threshold
|
|
|
|
Amp\Promise\wait($plan->runAsync($install, $sleep_time = 4, $hung_threshold = 80, $gone_threshold = 30, $stuck_threshold = 200));
|
|
|
|
}
|
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
function trySendStatsFromJzonAsync(ATFTask $task, $jzon)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $jzon) {
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
try
|
|
|
|
{
|
|
|
|
$data = jzon_parse(trim(str_replace('\"', '"', $jzon)));
|
|
|
|
$table = $data['table'];
|
|
|
|
unset($data['table']);
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
if(isset($data['deviceMemoryUsage']) && $data['deviceMemoryUsage'] === '')
|
|
|
|
{
|
|
|
|
$mem = yield atf_device_mem_async($task->device);
|
|
|
|
$data['deviceMemoryUsage'] = $mem['total'];
|
|
|
|
if($table === 'device_memory')
|
|
|
|
{
|
|
|
|
$data['deviceMemoryUsageNative'] = $mem['native'];
|
|
|
|
$data['deviceMemoryUsageSystem'] = $mem['system'];
|
|
|
|
$data['deviceMemoryUsageJava'] = $mem['java'];
|
|
|
|
$data['deviceMemoryUsageGraphics'] = $mem['graphics'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->trySendStats($task, $table, $data);
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
echo $e->getMessage() . "\n";
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-18 20:37:36 +03:00
|
|
|
function trySendApkStatsEvent($apk_size)
|
2022-08-03 17:40:44 +03:00
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2022-08-18 20:37:36 +03:00
|
|
|
$msg = "version $this->version($this->rev_hash) size: $apk_size";
|
2022-08-04 14:59:17 +03:00
|
|
|
atf_slack_post($msg, array('channel' => _atf_slack_chan_qa()));
|
2022-08-03 17:40:44 +03:00
|
|
|
|
2022-08-18 20:37:36 +03:00
|
|
|
$data['time'] = time();
|
2022-08-03 17:40:44 +03:00
|
|
|
$data['version'] = $this->version;
|
|
|
|
$data['revHash'] = $this->rev_hash;
|
2022-08-18 20:37:36 +03:00
|
|
|
$data['guid'] = $this->guid;
|
2022-08-03 17:40:44 +03:00
|
|
|
$data['branch'] = $this->branch;
|
|
|
|
$data['event'] = 'apk_size';
|
|
|
|
$data['value'] = $apk_size;
|
2022-08-18 20:37:36 +03:00
|
|
|
atf_stats_send('event_value', $data);
|
2022-08-03 17:40:44 +03:00
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
echo $e->getMessage() . "\n";
|
|
|
|
}
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
|
|
|
|
function trySendStats(ATFTask $task, $table, array $data)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
$data['guid'] = $this->guid;
|
|
|
|
$data['time'] = time();
|
|
|
|
$data['deviceId'] = $task->device;
|
|
|
|
atf_stats_send($table, $data);
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
echo $e->getMessage() . "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function trySendStatsEvent(ATFTask $task, $event, $value = 1)
|
|
|
|
{
|
|
|
|
$this->trySendStats(
|
|
|
|
$task,
|
|
|
|
'event_value',
|
|
|
|
array(
|
|
|
|
'event' => $event,
|
|
|
|
'value' => $value,
|
|
|
|
'branch' => $this->branch,
|
|
|
|
'version' => $this->version,
|
|
|
|
'revHash' => $this->rev_hash
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
function tryShareToQAChannel($msg_slack_id, $error)
|
|
|
|
{
|
|
|
|
if(!$this->share_with_qa_chan)
|
|
|
|
return;
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
//let's skip similar already shared errors
|
|
|
|
if($this->_calcSharedErrorSimilarity($error) > 80)
|
|
|
|
return;
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->shared_qa_errors[] = $error;
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
//let's share an exception message to goh-qa channel
|
|
|
|
$resp = atf_slack_get_permalink($msg_slack_id);
|
|
|
|
if(isset($resp['permalink']))
|
|
|
|
atf_slack_post($resp['permalink'], array('channel' => _atf_slack_chan_qa()));
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
function _calcSharedErrorSimilarity($error)
|
|
|
|
{
|
|
|
|
$max_perc = 0;
|
|
|
|
foreach($this->shared_qa_errors as $shared)
|
|
|
|
{
|
|
|
|
$perc = 0;
|
|
|
|
similar_text($shared, $error, $perc);
|
|
|
|
if($perc > $max_perc)
|
|
|
|
$max_perc = $perc;
|
|
|
|
}
|
|
|
|
return $max_perc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ATFPlan
|
|
|
|
{
|
|
|
|
const EXCEPTIONS_THRESHOLD = 5;
|
|
|
|
const BOGUS_DEVICE_COUNT_THRESHOLD = 5;
|
|
|
|
|
|
|
|
public $session;
|
|
|
|
public $name;
|
|
|
|
public $device_pool;
|
|
|
|
public $tasks = array();
|
|
|
|
public $slack_thread_ts;
|
|
|
|
|
|
|
|
function __construct($name, IATFDevicePool $device_pool)
|
|
|
|
{
|
|
|
|
$this->name = $name;
|
|
|
|
$this->device_pool = $device_pool;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTitle()
|
|
|
|
{
|
|
|
|
return "Testing plan '{$this->name}' devices:".sizeof($this->getDevices())."(n/a:".sizeof($this->session->ignored_devices).")";
|
|
|
|
}
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
function createSlackThread()
|
|
|
|
{
|
|
|
|
for($i=0;$i<5;++$i)
|
|
|
|
{
|
|
|
|
$resp = atf_slack_post($this->getTitle());
|
|
|
|
if(isset($resp['ok']))
|
|
|
|
{
|
|
|
|
$this->slack_thread_ts = $resp['ts'];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
sleep(1);
|
|
|
|
}
|
|
|
|
throw new Exception("Could not create Slack thread");
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateSlackThread()
|
|
|
|
{
|
|
|
|
$issues_summary = '';
|
|
|
|
foreach($this->getProblemsHistogram() as $code => $count)
|
|
|
|
{
|
|
|
|
if($code == ATFTask::CODE_GONE)
|
|
|
|
$issues_summary .= " gones:$count";
|
|
|
|
else if($code == ATFTask::CODE_NSTART)
|
|
|
|
$issues_summary .= " nstrts:$count";
|
|
|
|
else if($code == ATFTask::CODE_STUCK)
|
|
|
|
$issues_summary .= " stucks:$count";
|
|
|
|
else if($code == ATFTask::CODE_HUNG)
|
|
|
|
$issues_summary .= " hungs:$count";
|
|
|
|
else if($code == ATFTask::CODE_EXCEPTION)
|
|
|
|
$issues_summary .= " excepts:$count";
|
|
|
|
else if($code == ATFTask::CODE_WARN)
|
|
|
|
$issues_summary .= " warns:$count";
|
|
|
|
}
|
|
|
|
|
|
|
|
$running_count = 0;
|
|
|
|
$over_count = 0;
|
|
|
|
$total_progress = 0;
|
|
|
|
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
$total_progress += $task->getProgress();
|
|
|
|
if($task->isOver())
|
|
|
|
$over_count++;
|
|
|
|
if($task->device)
|
|
|
|
$running_count++;
|
|
|
|
}
|
|
|
|
$progress_txt = ' tasks:' . ($running_count > 0 ? $running_count . '/' : '') . $over_count . '/' . sizeof($this->tasks) . ' (' . round($total_progress/sizeof($this->tasks)*100, 2) . '%)';
|
|
|
|
|
|
|
|
atf_slack_update($this->slack_thread_ts,
|
|
|
|
array(array(
|
|
|
|
"text" => $this->getTitle() . $issues_summary . $progress_txt,
|
|
|
|
"color" => $this->_getThreadColor(),
|
|
|
|
"mrkdwn_in" => array("text")
|
|
|
|
))
|
|
|
|
);
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
function getProblemsHistogram()
|
|
|
|
{
|
|
|
|
$histogram = array();
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
foreach($task->status_codes as $item)
|
|
|
|
{
|
|
|
|
list($code, $_) = $item;
|
|
|
|
if(!ATFTask::isProblemCode($code))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if(!isset($histogram[$code]))
|
|
|
|
$histogram[$code] = 0;
|
|
|
|
++$histogram[$code];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $histogram;
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasStatusCode($code)
|
|
|
|
{
|
|
|
|
return $this->countStatusCode($code) > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function countStatusCode($code)
|
|
|
|
{
|
|
|
|
$c = 0;
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
$c += $task->countStatusCode($code);
|
|
|
|
return $c;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _getThreadColor()
|
|
|
|
{
|
|
|
|
if(!$this->getDevices())
|
|
|
|
return "#000000";
|
|
|
|
|
|
|
|
$has_gones = $this->hasStatusCode(ATFTask::CODE_GONE);
|
|
|
|
$has_hungs = $this->hasStatusCode(ATFTask::CODE_HUNG);
|
|
|
|
$has_stucks = $this->hasStatusCode(ATFTask::CODE_STUCK);
|
|
|
|
$has_nostarts = $this->hasStatusCode(ATFTask::CODE_NSTART);
|
|
|
|
$has_excepts = $this->hasStatusCode(ATFTask::CODE_EXCEPTION);
|
|
|
|
$has_warns = $this->hasStatusCode(ATFTask::CODE_WARN);
|
|
|
|
|
|
|
|
if($has_gones || $has_hungs || $has_warns || $has_excepts || $has_stucks || $has_nostarts)
|
|
|
|
{
|
|
|
|
if(!$has_excepts)
|
|
|
|
return $this->isOver() ? "warning" : "#FFCF9E";
|
|
|
|
else
|
|
|
|
return $this->isOver() ? "danger" : "#FFCCCB";
|
|
|
|
}
|
|
|
|
else if($this->isOver())
|
|
|
|
return "good";
|
|
|
|
else
|
|
|
|
return "#D3D3D3";
|
|
|
|
}
|
|
|
|
|
|
|
|
function _ignoreDevice($device, $reason)
|
|
|
|
{
|
|
|
|
$this->session->ignored_devices[] = $device;
|
|
|
|
atf_log("Ignoring device *$device*: $reason");
|
2022-08-04 14:59:17 +03:00
|
|
|
atf_slack_post("Ignoring device *$device*: $reason", array('thread_ts' => $this->slack_thread_ts));
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function getDevices()
|
|
|
|
{
|
|
|
|
$devices = array_diff($this->device_pool->get(), $this->session->ignored_devices);
|
|
|
|
return $devices;
|
|
|
|
}
|
|
|
|
|
|
|
|
function addTask(ATFTask $task)
|
|
|
|
{
|
|
|
|
$this->tasks[] = $task;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isOver()
|
|
|
|
{
|
|
|
|
if(!$this->getDevices())
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if($this->countStatusCode(ATFTask::CODE_EXCEPTION) >= self::EXCEPTIONS_THRESHOLD)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
if(!$task->isOver())
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasFatalProblems()
|
|
|
|
{
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
foreach($task->status_codes as $item)
|
|
|
|
{
|
|
|
|
if(ATFTask::isFatalCode($item[0]))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _findUnassignedTask()
|
|
|
|
{
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
if(!$task->device && !$task->isOver())
|
|
|
|
return $task;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function runAsync(ATFApkInstaller $install, $sleep_time, $hung_threshold, $gone_threshold, $stuck_threshold)
|
|
|
|
{
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->createSlackThread();
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
$this->session->resetBogusDevices();
|
|
|
|
|
|
|
|
$cs = array();
|
|
|
|
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
$cs[] = $this->_runTaskAsync($task, $install, $sleep_time, $hung_threshold, $gone_threshold, $stuck_threshold);
|
|
|
|
|
|
|
|
return Amp\Promise\all($cs);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _runTaskAsync(ATFTask $task, ATFApkInstaller $install, $sleep_time, $hung_threshold, $gone_threshold, $stuck_threshold)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $install, $sleep_time, $hung_threshold, $gone_threshold, $stuck_threshold) {
|
|
|
|
while(!$this->isOver() && !$task->isOver())
|
|
|
|
{
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->updateSlackThread();
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
yield Amp\delay((int)$sleep_time*1000);
|
|
|
|
|
|
|
|
if(!$task->device)
|
|
|
|
{
|
|
|
|
if(!yield $this->_findDeviceAndStartAsync($task, $install))
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$check_error = yield $this->_tryCheckExtStatusAsync($task, /*attempts*/3, /*timeout*/20);
|
|
|
|
if($check_error !== null)
|
|
|
|
{
|
|
|
|
$this->_ignoreDevice($device, $check_error);
|
2022-09-23 08:04:25 +03:00
|
|
|
if (!get('EXT_BOT_ERROR_PAUSE'))
|
2022-07-26 17:44:58 +03:00
|
|
|
$task->reschedule();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!$task->isOver())
|
|
|
|
yield $this->_checkHealthAsync($task, $hung_threshold, $gone_threshold, $stuck_threshold);
|
|
|
|
|
|
|
|
if($task->hasFatalProblem())
|
|
|
|
{
|
|
|
|
yield $this->_processFatalProblemAsync($task);
|
|
|
|
}
|
|
|
|
else if($task->isDone())
|
|
|
|
{
|
|
|
|
//let's reset hung stats for this device
|
|
|
|
$this->session->resetBogusDeviceCount($task->device);
|
|
|
|
//let's free the device
|
|
|
|
$task->device = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->updateSlackThread();
|
2022-07-26 17:44:58 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function _findDeviceAndStartAsync(ATFTask $task, ATFApkInstaller $install)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $install) {
|
|
|
|
|
|
|
|
$device = $this->_findFreeDevice();
|
|
|
|
if(!$device)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
//let's mark the device as occupied ASAP
|
|
|
|
$task->device = $device;
|
|
|
|
try
|
|
|
|
{
|
2022-08-04 14:59:17 +03:00
|
|
|
atf_slack_post("Preparing *{$task->device}*", array('thread_ts' => $this->slack_thread_ts));
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
yield $install->installAsync($device);
|
|
|
|
yield atf_start_ext_cmd_on_device_async($device, $task->getCmd(), $task->getCmdArgs());
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
$this->_ignoreDevice($device, $e->getMessage());
|
|
|
|
$task->reschedule();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$task->start($this->slack_thread_ts);
|
|
|
|
return true;
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function _processFatalProblemAsync(ATFTask $task)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task) {
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->_postScreenToSlack($task);
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
$fatal_msg = "Fatal problem ({$task->last_fatal_problem} - ".ATFTask::code2string($task->last_fatal_problem)."), attempt:{$task->attempts} *{$task->device}*";
|
|
|
|
|
|
|
|
atf_log("[FTL] $fatal_msg");
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
atf_slack_post($fatal_msg, array('thread_ts' => $task->slack_thread_ts));
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
if($task->last_fatal_problem == ATFTask::CODE_GONE ||
|
|
|
|
$task->last_fatal_problem == ATFTask::CODE_HUNG)
|
|
|
|
{
|
|
|
|
$app_log = atf_get_logcat_unity($task->device, 300);
|
|
|
|
$app_log = _atf_trim_start($app_log, 2000);
|
2022-08-04 14:59:17 +03:00
|
|
|
atf_slack_post("Last logs: ```$app_log``` *{$task->device}*", array('thread_ts' => $task->slack_thread_ts));
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
2022-09-23 08:04:25 +03:00
|
|
|
if (!get('EXT_BOT_ERROR_PAUSE'))
|
2022-07-26 17:44:58 +03:00
|
|
|
$task->reschedule();
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _findFreeDevice()
|
|
|
|
{
|
|
|
|
$available = array();
|
|
|
|
|
|
|
|
foreach($this->getDevices() as $device)
|
|
|
|
{
|
|
|
|
$busy = false;
|
|
|
|
foreach($this->tasks as $task)
|
|
|
|
{
|
|
|
|
if($task->device == $device)
|
|
|
|
{
|
|
|
|
$busy = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!$busy)
|
|
|
|
$available[] = $device;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!$available)
|
|
|
|
return null;
|
|
|
|
return $available[mt_rand(0, sizeof($available) - 1)];
|
|
|
|
}
|
|
|
|
|
|
|
|
function _tryCheckExtStatusAsync(ATFTask $task, $attempts, $timeout)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $attempts, $timeout) {
|
|
|
|
|
|
|
|
$last_error = null;
|
|
|
|
|
|
|
|
for($i=0;$i<$attempts;++$i)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
yield $this->_checkExtStatusAsync($task, $timeout);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
$last_error = $e->getMessage();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $last_error;
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function _checkExtStatusAsync(ATFTask $task, $timeout)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $timeout) {
|
|
|
|
|
|
|
|
$ext_status = yield atf_get_ext_status_async($task->device, $timeout);
|
|
|
|
if(!is_array($ext_status))
|
|
|
|
return;
|
|
|
|
|
|
|
|
$new_items = $this->_getExtStatusItemsSince($task->last_ext_status_item_time, $ext_status);
|
|
|
|
if(!$new_items)
|
|
|
|
return;
|
|
|
|
|
|
|
|
$task->last_ext_status_item_time = end($new_items)['time'];
|
|
|
|
|
|
|
|
foreach($new_items as $item)
|
|
|
|
yield $this->_analyzeExtStatusItemAsync($task, $item);
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _getExtStatusItemsSince($time, array $ext_status)
|
|
|
|
{
|
|
|
|
$new_items = array();
|
|
|
|
foreach($ext_status['entries'] as $item)
|
|
|
|
{
|
|
|
|
if($item['time'] > $time)
|
|
|
|
$new_items[] = $item;
|
|
|
|
}
|
|
|
|
return $new_items;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function _checkHealthAsync(ATFTask $task, $hung_threshold, $gone_threshold, $stuck_threshold)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $hung_threshold, $gone_threshold, $stuck_threshold) {
|
|
|
|
|
|
|
|
$device_is_bogus = false;
|
|
|
|
|
|
|
|
$not_alive_time = microtime(true) - $task->last_alive_check_time;
|
|
|
|
$stuck_time = microtime(true) - $task->last_stuck_check_time;
|
|
|
|
|
|
|
|
if($not_alive_time > $hung_threshold)
|
|
|
|
{
|
|
|
|
$task->addStatusCode(ATFTask::CODE_HUNG);
|
2022-08-03 17:40:44 +03:00
|
|
|
$this->session->trySendStatsEvent($task, 'hung');
|
2022-07-26 17:44:58 +03:00
|
|
|
atf_log("[HNG] No activity for $hung_threshold seconds *{$task->device}*");
|
|
|
|
$device_is_bogus = true;
|
|
|
|
}
|
|
|
|
else if($not_alive_time > $gone_threshold)
|
|
|
|
{
|
|
|
|
list($status, $_) = yield atf_host_exec_async("%{adb}% -s {$task->device} shell pidof %{package_id}%", DEPLOY_OPT_ERR_OK);
|
|
|
|
if($status != 0)
|
|
|
|
{
|
|
|
|
if($task->last_alive_check_time === $task->reset_time)
|
|
|
|
{
|
|
|
|
$task->addStatusCode(ATFTask::CODE_NSTART);
|
2022-08-03 17:40:44 +03:00
|
|
|
$this->session->trySendStatsEvent($task, 'nstart');
|
2022-07-26 17:44:58 +03:00
|
|
|
atf_log("[NFD] No app started after $hung_threshold seconds *{$task->device}*");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$task->addStatusCode(ATFTask::CODE_GONE);
|
2022-08-03 17:40:44 +03:00
|
|
|
$this->session->trySendStatsEvent($task, 'gone');
|
2022-07-26 17:44:58 +03:00
|
|
|
atf_log("[GNE] App is gone after $hung_threshold seconds *{$task->device}*");
|
|
|
|
}
|
|
|
|
$device_is_bogus = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
//let's tap the screen just in case app Activity is not in foreground
|
|
|
|
yield atf_host_exec_async("%{adb}% -s {$task->device} shell input tap 360 930", DEPLOY_OPT_ERR_OK);
|
|
|
|
}
|
|
|
|
else if($stuck_time > $stuck_threshold)
|
|
|
|
{
|
|
|
|
$task->addStatusCode(ATFTask::CODE_STUCK);
|
2022-08-03 17:40:44 +03:00
|
|
|
$this->session->trySendStatsEvent($task, 'stuck');
|
2022-07-26 17:44:58 +03:00
|
|
|
atf_log("[STK] Stuck for $stuck_threshold seconds *{$task->device}*");
|
|
|
|
}
|
|
|
|
|
|
|
|
if($device_is_bogus)
|
|
|
|
{
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->_reportErrorFromLogcatToSlack($task, 1000);
|
2022-07-26 17:44:58 +03:00
|
|
|
$this->_incAndCheckBogusDevice($task->device);
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function _incAndCheckBogusDevice($device)
|
|
|
|
{
|
|
|
|
if($this->session->incBogusDevice($device) >= self::BOGUS_DEVICE_COUNT_THRESHOLD)
|
|
|
|
{
|
|
|
|
$this->_ignoreDevice($device, "Too many N/As");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function _parseExtMessage($message)
|
|
|
|
{
|
|
|
|
$msg_code = null;
|
|
|
|
$msg_text = $message;
|
|
|
|
//example: [DBG] this a debug message
|
|
|
|
if(preg_match('~^(\[[^\]]+\])(.*)$~', $message, $matches))
|
|
|
|
{
|
|
|
|
$msg_code = $matches[1];
|
|
|
|
$msg_text = $matches[2];
|
|
|
|
}
|
|
|
|
return array($msg_code, $msg_text);
|
|
|
|
}
|
|
|
|
|
|
|
|
static function _printToShellExtItem(ATFTask $task, array $item)
|
|
|
|
{
|
|
|
|
$shell_msg = _atf_trim($item['message'], 200);
|
|
|
|
if(ATFTask::isProblemCode($item['error']))
|
|
|
|
$shell_msg = "[PROBLEM] Code:{$item['error']}, $shell_msg";
|
|
|
|
$shell_msg = "(".round($item['time'],1)."s) {$shell_msg} *{$task->device}*";
|
|
|
|
|
|
|
|
atf_log($shell_msg);
|
|
|
|
}
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
function _postToSlackExtStatusItem(ATFTask $task, array $item)
|
|
|
|
{
|
|
|
|
$orig_msg = _atf_trim($item['message'], 3000);
|
|
|
|
$slack_msg = $orig_msg;
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
if($item['error'] == ATFTask::CODE_EXCEPTION)
|
|
|
|
$slack_msg = "```$slack_msg```";
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
$slack_msg = '('.round($item['time'],1).'s) '.$slack_msg.' *'.$task->device.'*';
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
$resp = atf_slack_post($slack_msg, array('thread_ts' => $task->slack_thread_ts));
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
if(isset($resp['ok']) && $item['error'] == ATFTask::CODE_EXCEPTION)
|
|
|
|
$this->session->tryShareToQAChannel($resp['ts'], $orig_msg);
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
function _analyzeExtStatusItemAsync(ATFTask $task, array $item)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $item) {
|
|
|
|
|
|
|
|
self::_printToShellExtItem($task, $item);
|
|
|
|
|
|
|
|
$task->addStatusCode($item['error'], $item['message']);
|
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
if($item['error'] == ATFTask::CODE_EXCEPTION)
|
|
|
|
$this->session->trySendStatsEvent($task, 'exception');
|
2022-07-26 17:44:58 +03:00
|
|
|
|
|
|
|
list($msg_type, $msg_text) = self::_parseExtMessage($item['message']);
|
|
|
|
|
|
|
|
//NOTE: alive system messages are sent periodically by ExtBot itself,
|
|
|
|
// we'd like to ignore them for stuck detection
|
|
|
|
if($msg_type !== '[ALIVE]')
|
|
|
|
$task->last_stuck_check_time = microtime(true);
|
|
|
|
|
|
|
|
if($msg_type === '[DONE]')
|
|
|
|
{
|
|
|
|
$task->is_done = true;
|
|
|
|
$this->_postToSlackExtStatusItem($task, $item);
|
|
|
|
}
|
|
|
|
|
|
|
|
//NOTE: in case of any message from the device we update the alive check timestamp
|
|
|
|
$task->last_alive_check_time = microtime(true);
|
|
|
|
|
|
|
|
yield $this->_processExtStatusMessageAsync($task, $item, $msg_type, $msg_text);
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function _processExtStatusMessageAsync($task, $item, $msg_type, $msg_text)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($task, $item, $msg_type, $msg_text) {
|
|
|
|
|
|
|
|
if($msg_type === '[PRG]')
|
|
|
|
{
|
|
|
|
//let's reset hung stats since progress is going
|
|
|
|
$this->session->resetBogusDeviceCount($task->device);
|
|
|
|
$task->onProgress($msg_text);
|
|
|
|
}
|
2022-08-03 17:40:44 +03:00
|
|
|
else if($msg_type === '[STAT]')
|
|
|
|
yield $this->session->trySendStatsFromJzonAsync($task, $msg_text);
|
2022-07-26 17:44:58 +03:00
|
|
|
else if($msg_type === '[WRN]')
|
|
|
|
{
|
|
|
|
$task->addStatusCode(ATFTask::CODE_WARN);
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->_postToSlackExtStatusItem($task, $item);
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
else if($msg_type === null)
|
|
|
|
{
|
2022-08-04 14:59:17 +03:00
|
|
|
$this->_postToSlackExtStatusItem($task, $item);
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
function _reportErrorFromLogcatToSlack(ATFTask $task, $limit)
|
|
|
|
{
|
|
|
|
$errors_log = _atf_trim(atf_get_logcat_errors($task->device, $limit), 3000);
|
|
|
|
if($errors_log)
|
|
|
|
atf_slack_post("```$errors_log``` *{$task->device}*", array('thread_ts' => $task->slack_thread_ts));
|
|
|
|
}
|
|
|
|
|
|
|
|
function _postScreenToSlack(ATFTask $task)
|
|
|
|
{
|
|
|
|
global $GAME_ROOT;
|
|
|
|
|
|
|
|
$png_data = atf_screen($task->device);
|
|
|
|
if($png_data)
|
|
|
|
{
|
|
|
|
ensure_write("$GAME_ROOT/build/atf/screen.png", $png_data);
|
|
|
|
atf_slack_post_png("screen", "$GAME_ROOT/build/atf/screen.png", array('thread_ts' => $task->slack_thread_ts));
|
|
|
|
}
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ATFTask
|
|
|
|
{
|
|
|
|
const CODE_NONE = 0;
|
|
|
|
const CODE_EXCEPTION = 1;
|
|
|
|
const CODE_WARN = 2;
|
|
|
|
const CODE_NSTART = 124;
|
|
|
|
const CODE_STUCK = 125;
|
|
|
|
const CODE_GONE = 126;
|
|
|
|
const CODE_HUNG = 127;
|
|
|
|
const CODE_DONE = 128;
|
|
|
|
|
|
|
|
|
|
|
|
static function code2string($code)
|
|
|
|
{
|
|
|
|
switch($code)
|
|
|
|
{
|
|
|
|
case self::CODE_NONE:
|
|
|
|
return "none";
|
|
|
|
case self::CODE_EXCEPTION:
|
|
|
|
return "exception";
|
|
|
|
case self::CODE_WARN:
|
|
|
|
return "warning";
|
|
|
|
case self::CODE_STUCK:
|
|
|
|
return "stuck";
|
|
|
|
case self::CODE_NSTART:
|
|
|
|
return "nstart";
|
|
|
|
case self::CODE_GONE:
|
|
|
|
return "gone";
|
|
|
|
case self::CODE_HUNG:
|
|
|
|
return "hung";
|
|
|
|
case self::CODE_DONE:
|
|
|
|
return "done";
|
|
|
|
}
|
|
|
|
return "???";
|
|
|
|
}
|
|
|
|
|
|
|
|
public $cmd;
|
|
|
|
public $args;
|
|
|
|
public $title;
|
|
|
|
public $last_progress;
|
|
|
|
public $last_done_arg_idx;
|
|
|
|
public $slack_thread_ts;
|
|
|
|
public $reset_time = 0;
|
|
|
|
public $last_alive_check_time = 0;
|
|
|
|
public $last_stuck_check_time = 0;
|
|
|
|
public $last_ext_status_item_time = 0;
|
|
|
|
public $last_ext_input_req_id = 0;
|
|
|
|
public $status_codes = array();
|
|
|
|
public $is_done;
|
|
|
|
public $last_fatal_problem = 0;
|
|
|
|
public $attempts = 1;
|
|
|
|
public $device;
|
|
|
|
|
|
|
|
function __construct($cmd, array $args, $title = '')
|
|
|
|
{
|
|
|
|
$this->cmd = $cmd;
|
|
|
|
$this->args = $args;
|
|
|
|
$this->title = $title;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCmd()
|
|
|
|
{
|
|
|
|
return $this->cmd;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCmdArgs()
|
|
|
|
{
|
|
|
|
return $this->args;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getProgress()
|
|
|
|
{
|
|
|
|
return $this->last_progress;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function isProblemCode($code)
|
|
|
|
{
|
|
|
|
return $code > self::CODE_NONE && $code < self::CODE_DONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
static function isFatalCode($code)
|
|
|
|
{
|
|
|
|
return
|
|
|
|
$code == ATFTask::CODE_STUCK ||
|
|
|
|
$code == ATFTask::CODE_NSTART ||
|
|
|
|
$code == ATFTask::CODE_GONE ||
|
|
|
|
$code == ATFTask::CODE_HUNG ||
|
|
|
|
$code == ATFTask::CODE_EXCEPTION;
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasFatalProblem()
|
|
|
|
{
|
|
|
|
return $this->last_fatal_problem != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function hasStatusCode($code)
|
|
|
|
{
|
|
|
|
return $this->countStatusCode($code) > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function countStatusCode($code)
|
|
|
|
{
|
|
|
|
$c = 0;
|
|
|
|
foreach($this->status_codes as $item)
|
|
|
|
{
|
|
|
|
if($item[0] === $code)
|
|
|
|
$c++;
|
|
|
|
}
|
|
|
|
return $c;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isDone()
|
|
|
|
{
|
|
|
|
return $this->is_done;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isOver()
|
|
|
|
{
|
|
|
|
return $this->isDone() || $this->hasFatalProblem();
|
|
|
|
}
|
|
|
|
|
|
|
|
function addStatusCode($code, $msg = '')
|
|
|
|
{
|
|
|
|
$this->status_codes[] = array($code, $msg);
|
|
|
|
|
|
|
|
if($code == ATFTask::CODE_DONE)
|
|
|
|
$this->is_done = true;
|
|
|
|
else if(self::isFatalCode($code))
|
|
|
|
$this->last_fatal_problem = $code;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onProgress($jzon)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
$data = jzon_parse(trim(str_replace('\"', '"', $jzon)));
|
|
|
|
if(isset($data['p']))
|
|
|
|
$this->last_progress = floatval(str_replace(',', '.', str_replace('.', '', $data['p'])));
|
|
|
|
if(isset($data['arg_idx']))
|
|
|
|
$this->last_done_arg_idx = (int)$data['arg_idx'];
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
echo $e->getMessage() . "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function _resetCheckTimes()
|
|
|
|
{
|
|
|
|
$this->reset_time = microtime(true);
|
|
|
|
$this->last_alive_check_time = $this->reset_time;
|
|
|
|
$this->last_stuck_check_time = $this->reset_time;
|
|
|
|
$this->last_ext_status_item_time = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function reschedule()
|
|
|
|
{
|
|
|
|
++$this->attempts;
|
|
|
|
$this->device = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function start($slack_thread_ts)
|
|
|
|
{
|
|
|
|
$this->_resetCheckTimes();
|
|
|
|
$this->slack_thread_ts = $slack_thread_ts;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host_exec_async($cmd, $opts = 0, $timeout = 10) : Amp\Promise
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($cmd, $opts, $timeout) {
|
|
|
|
|
|
|
|
$timeout_cmd = "/usr/local/bin/timeout -k 5s $timeout $cmd";
|
|
|
|
|
|
|
|
$res = yield deploy_ssh_exec_async(atf_host(), $timeout_cmd, $opts);
|
|
|
|
|
|
|
|
list($status, $lines) = current($res);
|
|
|
|
//in case of timeout we set status to false explicitely
|
|
|
|
if($status === 124)
|
|
|
|
$status = false;
|
|
|
|
|
|
|
|
return array($status, $lines);
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host_exec($cmd, $opts = 0, $timeout = 10)
|
|
|
|
{
|
|
|
|
$timeout_cmd = "/usr/local/bin/timeout -k 5s $timeout $cmd";
|
|
|
|
|
|
|
|
$res = deploy_ssh_exec(atf_host(), $timeout_cmd, $opts);
|
|
|
|
|
|
|
|
list($status, $lines) = current($res);
|
|
|
|
//in case of timeout we set status to false explicitely
|
|
|
|
if($status === 124)
|
|
|
|
$status = false;
|
|
|
|
|
|
|
|
return array($status, $lines);
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host_get_file($file_name)
|
|
|
|
{
|
|
|
|
$file_data = current(deploy_get_file(atf_host(), $file_name));
|
|
|
|
return $file_data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host_put_file($local_path, $remote_path)
|
|
|
|
{
|
|
|
|
deploy_scp_put_file(atf_host(), $local_path, $remote_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host_put_file_async($local_path, $remote_path)
|
|
|
|
{
|
|
|
|
return deploy_scp_put_file_async(atf_host(), $local_path, $remote_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_slice(array $items, $max)
|
|
|
|
{
|
|
|
|
$sliced = array();
|
|
|
|
|
|
|
|
$offset = 0;
|
|
|
|
while(true)
|
|
|
|
{
|
|
|
|
$tmp = array_slice($items, $offset, $max);
|
|
|
|
if(!$tmp)
|
|
|
|
break;
|
|
|
|
$sliced[] = $tmp;
|
|
|
|
$offset += $max;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $sliced;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_get_remote_file_crc($path)
|
|
|
|
{
|
|
|
|
list($status, $lines) = atf_host_exec("crc32 $path", DEPLOY_OPT_ERR_OK);
|
|
|
|
if($status != 0)
|
|
|
|
return '';
|
|
|
|
return trim(implode("\n", $lines));
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_get_local_file_crc($path)
|
|
|
|
{
|
|
|
|
if(!is_file($path))
|
|
|
|
return '';
|
|
|
|
|
2022-09-23 08:04:25 +03:00
|
|
|
shell("crc32 $path", $out);
|
2022-07-26 17:44:58 +03:00
|
|
|
return trim($out[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_device_put_file_async($device, $data_or_file, $device_path, $is_file = false)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device, $data_or_file, $device_path, $is_file) {
|
|
|
|
|
|
|
|
if($device_path[0] !== "/")
|
|
|
|
$device_path = atf_package_dir() . $device_path;
|
|
|
|
|
|
|
|
$tmp_remote_file = uniqid(basename($device_path)."_");
|
|
|
|
|
|
|
|
if(!$is_file)
|
|
|
|
{
|
|
|
|
$local_path = tempnam("/tmp", "atf_");
|
|
|
|
ensure_write($local_path, $data_or_file);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
$local_path = $data_or_file;
|
|
|
|
|
|
|
|
try
|
|
|
|
{
|
|
|
|
//1. let's copy local file to the ATF host as temp one
|
|
|
|
yield atf_host_put_file_async($local_path, $tmp_remote_file);
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
if(!$is_file)
|
|
|
|
ensure_rm($local_path);
|
|
|
|
}
|
|
|
|
|
|
|
|
//2. let's push the temp file to the device
|
|
|
|
$device_dir = dirname($device_path);
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell mkdir -p $device_dir");
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device push $tmp_remote_file $device_path", 0, 30);
|
|
|
|
yield atf_host_exec_async("rm -rf $tmp_remote_file");
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_device_put_folder_async($device, $data, $device_path)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device, $data, $device_path) {
|
|
|
|
|
|
|
|
if($device_path[0] !== "/")
|
|
|
|
$device_path = atf_package_dir() . $device_path;
|
|
|
|
|
|
|
|
$device_dir = dirname($device_path);
|
|
|
|
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell rm -rf $device_path");
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell mkdir -p $device_dir");
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device push $data $device_path", 0, 400);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_device_del_file_async($device, $device_path, $opts = 0)
|
|
|
|
{
|
|
|
|
if($device_path[0] !== "/")
|
|
|
|
$device_path = atf_package_dir() . $device_path;
|
|
|
|
|
|
|
|
return atf_host_exec_async("%{adb}% -s $device shell rm -rf $device_path", $opts);
|
|
|
|
}
|
|
|
|
|
2022-08-18 20:37:36 +03:00
|
|
|
function atf_device_blink($device, $times = 5)
|
|
|
|
{
|
|
|
|
//disable auto brightness
|
|
|
|
atf_host_exec("%{adb}% -s $device shell settings put system screen_brightness_mode 0");
|
|
|
|
|
|
|
|
for($i=0;$i<$times;++$i)
|
|
|
|
{
|
|
|
|
atf_host_exec("%{adb}% -s $device shell settings put system screen_brightness 255");
|
|
|
|
sleep(1);
|
|
|
|
atf_host_exec("%{adb}% -s $device shell settings put system screen_brightness 5");
|
|
|
|
}
|
|
|
|
|
|
|
|
//enable auto brightness
|
|
|
|
atf_host_exec("%{adb}% -s $device shell settings put system screen_brightness 100");
|
|
|
|
atf_host_exec("%{adb}% -s $device shell settings put system screen_brightness_mode 1");
|
|
|
|
}
|
|
|
|
|
2022-07-26 17:44:58 +03:00
|
|
|
function atf_device_pull_file_async($device, $device_path, $timeout = 10, $throw_error_on_timeout = false)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device, $device_path, $throw_error_on_timeout, $timeout) {
|
|
|
|
|
|
|
|
if($device_path[0] !== "/")
|
|
|
|
$device_path = atf_package_dir() . $device_path;
|
|
|
|
|
|
|
|
$tmp_remote_file = uniqid(basename($device_path)."_");
|
|
|
|
|
|
|
|
list($status, $_) = yield atf_host_exec_async("%{adb}% -s $device pull $device_path $tmp_remote_file", DEPLOY_OPT_ERR_OK, $timeout);
|
|
|
|
|
|
|
|
if($status !== 0)
|
|
|
|
{
|
|
|
|
if($status === false && $throw_error_on_timeout)
|
|
|
|
throw new Exception("Could not pull from device '$device' file '$device_path' due to timeout");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$file_data = atf_host_get_file($tmp_remote_file);
|
|
|
|
yield atf_host_exec_async("rm -rf $tmp_remote_file");
|
|
|
|
return $file_data;
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_screen($device)
|
|
|
|
{
|
|
|
|
$screen_file_name = uniqid("screen_");
|
|
|
|
try
|
|
|
|
{
|
|
|
|
atf_host_exec("%{adb}% -s $device exec-out screencap -p > $screen_file_name");
|
|
|
|
}
|
|
|
|
catch(Exception $e)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
$data = atf_host_get_file($screen_file_name);
|
|
|
|
atf_host_exec("rm -rf $screen_file_name");
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_guid()
|
|
|
|
{
|
|
|
|
return uniqid();
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_start_watchdog($guid)
|
|
|
|
{
|
|
|
|
$script = dirname(__FILE__) . '/../gamectl atf_watchdog ' . getmypid() . ' ' . $guid;
|
|
|
|
exec("nohup $script > /dev/null &", $output, $ret);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_trim($txt, $max_len)
|
|
|
|
{
|
|
|
|
return strlen($txt) > $max_len ? substr($txt, 0, $max_len) . "..." : $txt;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_trim_start($txt, $max_len)
|
|
|
|
{
|
|
|
|
return strlen($txt) > $max_len ? "..." . substr($txt, strlen($txt) - $max_len) : $txt;
|
|
|
|
}
|
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
function _atf_db()
|
|
|
|
{
|
|
|
|
$config = array(
|
|
|
|
'host' => '172.19.179.148',
|
|
|
|
'port' => '8123',
|
|
|
|
'username' => get('ATF_CLICKHOUSE_USERNAME'),
|
|
|
|
'password' => get('ATF_CLICKHOUSE_PASSWORD')
|
|
|
|
);
|
|
|
|
$db = new ClickHouseDB\Client($config);
|
|
|
|
$db->database(get('ATF_CLICKHOUSE_DB'));
|
|
|
|
$db->setTimeout(10);
|
|
|
|
$db->setConnectTimeOut(5);
|
|
|
|
return $db;
|
|
|
|
}
|
|
|
|
|
2022-08-04 14:59:17 +03:00
|
|
|
|
2022-08-03 17:40:44 +03:00
|
|
|
function atf_stats_send($table, array $data)
|
|
|
|
{
|
2022-09-23 08:04:25 +03:00
|
|
|
return;
|
2022-08-04 14:59:17 +03:00
|
|
|
$db = _atf_db();
|
2022-08-03 17:40:44 +03:00
|
|
|
$db->insert($table, array(array_values($data)), array_keys($data));
|
|
|
|
}
|
2022-07-26 17:44:58 +03:00
|
|
|
|
2022-08-18 20:37:36 +03:00
|
|
|
function atf_device_mem_async($device)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device) {
|
|
|
|
list($code, $lines) = yield atf_host_exec_async("%{adb}% -s $device shell dumpsys meminfo -s %{package_id}%", DEPLOY_OPT_ERR_OK, 20);
|
|
|
|
|
|
|
|
$res = array(
|
|
|
|
'total' => 0,
|
|
|
|
'native' => 0,
|
|
|
|
'java' => 0,
|
|
|
|
'system' => 0,
|
|
|
|
'graphics' => 0,
|
|
|
|
);
|
|
|
|
|
|
|
|
if($code !== 0)
|
|
|
|
return $res;
|
|
|
|
|
|
|
|
foreach($lines as $idx => $line)
|
|
|
|
{
|
|
|
|
$items = preg_split('/\s+/', trim($line));
|
|
|
|
if($items && $items[0] === "TOTAL:")
|
|
|
|
$res['total'] = 1*$items[1];
|
|
|
|
else if($items && $items[0] === "TOTAL" && $items[1] === "PSS:")
|
|
|
|
$res['total'] = 1*$items[2];
|
|
|
|
else if($items && $items[0] === "Native" && $items[1] === "Heap:")
|
|
|
|
$res['native'] = 1*$items[2];
|
|
|
|
else if($items && $items[0] === "Java" && $items[1] === "Heap:")
|
|
|
|
$res['java'] = 1*$items[2];
|
|
|
|
else if($items && $items[0] === "System:")
|
|
|
|
$res['system'] = 1*$items[1];
|
|
|
|
else if($items && $items[0] === "Graphics:")
|
|
|
|
$res['graphics'] = 1*$items[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $res;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-26 17:44:58 +03:00
|
|
|
function atf_log($msg)
|
|
|
|
{
|
|
|
|
echo date("Y-m-d H:i:s") . " " . $msg . "\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_adb_reboot()
|
|
|
|
{
|
|
|
|
atf_host_exec("killall adb");
|
|
|
|
atf_host_exec("%{adb}% kill-server");
|
|
|
|
atf_host_exec("%{adb}% start-server");
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_reboot_devices(array $devices)
|
|
|
|
{
|
|
|
|
atf_log("Rebooting devices...");
|
|
|
|
//let's reboot devices
|
|
|
|
$ces = array();
|
|
|
|
foreach($devices as $device)
|
|
|
|
{
|
|
|
|
$ces[] = Amp\call(function() use($device) {
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device reboot", DEPLOY_OPT_ERR_OK);
|
|
|
|
yield Amp\delay(40*1000);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Amp\Promise\wait(Amp\Promise\all($ces));
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_get_devices($extended = false)
|
|
|
|
{
|
|
|
|
$devices = array();
|
|
|
|
|
|
|
|
list($_, $lines) = atf_host_exec("%{adb}% devices -l", DEPLOY_OPT_SILENT);
|
|
|
|
|
|
|
|
|
|
|
|
foreach($lines as $line)
|
|
|
|
{
|
|
|
|
if(preg_match('~^(\S+)\s+device(.*)~', $line, $m))
|
|
|
|
{
|
|
|
|
if($extended)
|
|
|
|
{
|
|
|
|
$devices[$m[1]] = array();
|
|
|
|
$items = explode(" ", trim($m[2]));
|
|
|
|
foreach($items as $item)
|
|
|
|
{
|
|
|
|
list($k, $v) = explode(":", $item);
|
|
|
|
$devices[$m[1]][$k] = $v;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
$devices[] = $m[1];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $devices;
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_get_ext_status_async($device, $timeout = 20)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device, $timeout) {
|
|
|
|
|
|
|
|
$file_data = yield atf_device_pull_file_async($device, "ext_status.js", $timeout, /*throw on timeout*/true);
|
|
|
|
if($file_data === null)
|
|
|
|
return null;
|
|
|
|
$file_data = preg_replace('/[[:cntrl:]]/', '', $file_data);
|
|
|
|
return json_decode($file_data, true);
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_update_ext_cmd_async($device, $main, array $args)
|
|
|
|
{
|
|
|
|
$ext_cmd = array(
|
|
|
|
"device_id" => $device,
|
|
|
|
"main" => $main,
|
|
|
|
"args" => $args,
|
|
|
|
);
|
|
|
|
|
|
|
|
return atf_device_put_file_async($device, json_encode($ext_cmd), 'ext_cmd.js');
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_package_dir()
|
|
|
|
{
|
|
|
|
return "/storage/emulated/0/Android/data/".deploy_get(atf_host(), 'package_id')."/files/";
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_get_locat_errors_since($device, $since_stamp)//not using
|
|
|
|
{
|
|
|
|
$time_spec = gmdate('m-d h:m:s.0', $since_stamp);
|
|
|
|
list($_, $lines) = atf_host_exec("%{adb}% -s $device logcat -s -v UTC -d -t '$time_spec' AndroidRuntime:E Unity:E", DEPLOY_OPT_SILENT | DEPLOY_OPT_ERR_OK);
|
|
|
|
_atf_filter_error_logs($lines);
|
|
|
|
return implode("\n", $lines);
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_get_logcat_errors($device, $limit = 0)
|
|
|
|
{
|
|
|
|
list($_, $lines) = atf_host_exec("%{adb}% -s $device logcat -s -d ".($limit > 0 ? "-t $limit" : "")." AndroidRuntime:E Unity:E", DEPLOY_OPT_SILENT | DEPLOY_OPT_ERR_OK);
|
|
|
|
_atf_filter_error_logs($lines);
|
|
|
|
return implode("\n", $lines);
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_get_logcat_unity($device, $limit = 0)
|
|
|
|
{
|
|
|
|
list($_, $lines) = atf_host_exec("%{adb}% -s $device logcat -s -d Unity:'*'", DEPLOY_OPT_ERR_OK);
|
|
|
|
if($limit > 0)
|
|
|
|
array_splice($lines, 0, sizeof($lines) - $limit);
|
|
|
|
return implode("\n", $lines);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_filter_error_logs(array &$lines)
|
|
|
|
{
|
|
|
|
$exception_lines = 0;
|
|
|
|
$filtered = array();
|
|
|
|
for($i=0;$i<sizeof($lines);++$i)
|
|
|
|
{
|
|
|
|
$line = $lines[$i];
|
|
|
|
if(strpos($line, 'E AndroidRuntime') !== false)
|
|
|
|
$filtered[] = $line;
|
|
|
|
else if(strpos($line, 'E Unity') !== false)
|
|
|
|
{
|
|
|
|
if(strpos($line, '[EXCEPTION]') !== false)
|
|
|
|
$exception_lines = 15;
|
|
|
|
|
|
|
|
if(--$exception_lines > 0)
|
|
|
|
$filtered[] = $line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$lines = $filtered;
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_start_ext_cmd_on_device_async($device, $cmd_main, array $cmd_args)
|
|
|
|
{
|
|
|
|
return Amp\call(function() use($device, $cmd_main, $cmd_args) {
|
|
|
|
|
|
|
|
yield atf_device_del_file_async($device, 'ext_status.js', DEPLOY_OPT_ERR_OK);
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell am force-stop %{package_id}%", DEPLOY_OPT_ERR_OK, 30);
|
|
|
|
yield atf_update_ext_cmd_async($device, $cmd_main, $cmd_args);
|
|
|
|
yield atf_host_exec_async("%{adb}% -s $device shell am start -n %{package_id}%/%{activity}%", 0, 30);
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function atf_host()
|
|
|
|
{
|
|
|
|
static $inited = false;
|
|
|
|
|
|
|
|
$node_name = 'atf';
|
|
|
|
if($inited)
|
|
|
|
return $node_name;
|
|
|
|
|
|
|
|
deploy_declare_node($node_name,
|
|
|
|
array(
|
2022-09-23 08:04:25 +03:00
|
|
|
'user' => get("ATF_USER"),
|
2022-07-26 17:44:58 +03:00
|
|
|
'dir' => '',
|
2022-09-23 08:04:25 +03:00
|
|
|
'ssh_key_str' => get("ATF_KEY"),
|
|
|
|
'hosts' => array(get("ATF_HOST")),
|
|
|
|
'adb' => get("ATF_HOST_ADB"),
|
|
|
|
'package_id' => get("PACKAGE_ID"),
|
|
|
|
'activity' => get("LAUNCHER_ACTIVITY")
|
2022-07-26 17:44:58 +03:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$inited = true;
|
|
|
|
|
|
|
|
return $node_name;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_opt(array &$args, $opt, $default, $conv_fn = null)
|
|
|
|
{
|
|
|
|
foreach($args as $idx => $arg)
|
|
|
|
{
|
|
|
|
if(strpos($arg, $opt) === 0)
|
|
|
|
{
|
|
|
|
$arg = substr($arg, strlen($opt));
|
|
|
|
unset($args[$idx]);
|
|
|
|
return $conv_fn === null ? $arg : $conv_fn($arg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _atf_opt_check_no_trailing(array $args)
|
|
|
|
{
|
|
|
|
foreach($args as $arg)
|
|
|
|
{
|
|
|
|
if(preg_match('~(--\w+)=.*~', $arg, $m))
|
|
|
|
throw new Exception("Unknown option '{$m[1]}'");
|
|
|
|
}
|
|
|
|
}
|