* @package case * @version $Id: model.php 5108 2013-07-12 01:59:04Z chencongzhi520@gmail.com $ * @link https://www.zentao.net */ ?> loadModel('qa')->setMenu($productID, $branch); } /** * 创建一个用例。 * Create a case. * * @param object $case * @access public * @return bool|int */ public function create(object $case): bool|int { /* 插入测试用例。 */ /* Insert testcase. */ $this->testcaseTao->doCreate($case); if(dao::isError()) return false; $caseID = $this->dao->lastInsertID(); /* 记录动态。*/ /* Record log. */ $this->loadModel('action'); $this->action->create('case', $caseID, 'Opened'); if($case->status == 'wait') $this->action->create('case', $caseID, 'submitReview'); /* 存储上传的文件。 */ /* Save upload files. */ $this->config->dangers = ''; $this->loadModel('file')->saveUpload('testcase', $caseID, 'autoscript', 'script', 'scriptName'); $files = $this->loadModel('file')->saveUpload('testcase', $caseID); $this->testcaseTao->doCreateSpec($caseID, $case, $files); $this->loadModel('score')->create('testcase', 'create', $caseID); /* 插入用例步骤。 */ /* Insert testcase steps. */ $this->testcaseTao->insertSteps($caseID, $case->steps, $case->expects, $case->stepType); if(isset($case->auto) && $case->auto != 'auto') { ob_start(); setcookie('onlyAutoCase', '0'); } if(dao::isError()) return false; return $caseID; } /** * 获取模块的用例。 * Get cases of modules. * * @param int|array $productID * @param int|string $branch * @param int|array $moduleIdList * @param string $browseType * @param string $auto no|unit * @param string $caseType * @param string $orderBy * @param object $pager * @param string $from * @access public * @return array */ public function getModuleCases(int|array $productID, int|string $branch = 0, int|array $moduleIdList = 0, string $browseType = '', string $auto = 'no', string $caseType = '', string $orderBy = 'id_desc', ?object $pager = null, string $from = 'testcase'): array { $isProjectTab = $this->app->tab == 'project' && $from != 'doc'; $isExecutionTab = $this->app->tab == 'execution' && $from != 'doc'; $stmt = $this->dao->select('t1.*, t2.title as storyTitle, t2.deleted as storyDeleted')->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id'); if($isProjectTab) $stmt = $stmt->leftJoin(TABLE_PROJECTCASE)->alias('t3')->on('t1.id=t3.case'); if($isExecutionTab) $stmt = $stmt->leftJoin(TABLE_PROJECTCASE)->alias('t3')->on('t1.id=t3.case'); return $stmt ->where('t1.product')->in($productID) ->beginIF($isProjectTab)->andWhere('t3.project')->eq($this->session->project)->fi() ->beginIF($isExecutionTab)->andWhere('t3.project')->eq($this->session->execution)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($moduleIdList)->andWhere('t1.module')->in($moduleIdList)->fi() ->beginIF($browseType == 'wait')->andWhere('t1.status')->eq($browseType)->fi() ->beginIF($browseType == 'needconfirm') ->andWhere('t2.version > t1.storyVersion') ->andWhere('t2.status')->eq('active') ->fi() ->beginIF($auto == 'auto' || $auto == 'unit')->andWhere('t1.auto')->eq($auto)->fi() ->beginIF($auto != 'auto' && $auto != 'unit')->andWhere('t1.auto')->ne('unit')->fi() ->beginIF($caseType)->andWhere('t1.type')->eq($caseType)->fi() ->andWhere('t1.deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } /** * 获取执行的用例。 * Get execution cases. * * @param string $browseType all|wait|needconfirm * @param int $executionID * @param int $productID * @param int $branchID * @param int $moduleID * @param int $paramID * @param string $orderBy * @param object $pager * @access public * @return array */ public function getExecutionCases(string $browseType = 'all', int $executionID = 0, int $productID = 0, int|string $branchID = 0, int $moduleID = 0, int $paramID = 0, string $orderBy = 'id_desc', ?object $pager = null): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCases(); if(strpos($orderBy, 'id_') !== false) $orderBy = str_replace('id_', 't2.id_', $orderBy); if(strpos($orderBy, 'order_') !== false) $orderBy = str_replace('order_', 't2.order_', $orderBy); if(strpos($orderBy, 'version_') !== false) $orderBy = str_replace('version_', 't2.version_', $orderBy); if($browseType == 'needconfirm') { return $this->dao->select('distinct t1.*, t2.*')->from(TABLE_PROJECTCASE)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case = t2.id') ->leftJoin(TABLE_STORY)->alias('t3')->on('t2.story = t3.id') ->leftJoin(TABLE_MODULE)->alias('t4')->on('t2.module = t4.id') ->where('t1.project')->eq($executionID) ->andWhere('t2.deleted')->eq('0') ->andWhere('t3.version > t2.storyVersion') ->andWhere("t3.status")->eq('active') ->beginIF($productID)->andWhere('t1.product')->eq($productID)->fi() ->beginIF($productID && $branchID !== 'all')->andWhere('t2.branch')->eq($branchID)->fi() ->beginIF($moduleID)->andWhere('t4.path')->like("%,$moduleID,%")->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } if($browseType == 'bysearch') return $this->testcaseTao->getExecutionCasesBySearch($executionID, $productID, $branchID, $paramID, $orderBy, $pager); return $this->dao->select('distinct t1.*, t2.*')->from(TABLE_PROJECTCASE)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case = t2.id') ->leftJoin(TABLE_MODULE)->alias('t3')->on('t2.module = t3.id') ->where('t1.project')->eq($executionID) ->andWhere('t2.deleted')->eq('0') ->beginIF($browseType != 'all' && $browseType != 'byModule')->andWhere('t2.status')->eq($browseType)->fi() ->beginIF($productID)->andWhere('t1.product')->eq($productID)->fi() ->beginIF($productID && $branchID !== 'all')->andWhere('t2.branch')->eq($branchID)->fi() ->beginIF($moduleID)->andWhere('t3.path')->like("%,$moduleID,%")->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } /** * Get case info by ID. * * @param int $caseID * @param int $version * @access public * @return object|bool */ public function getById(int $caseID, int $version = 0): object|bool { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCase(); $case = $this->dao->findById($caseID)->from(TABLE_CASE)->fetch(); if(!$case) return false; $case = $this->loadModel('file')->replaceImgURL($case, 'steps'); $case = $this->processDateField($case); /* Get project and execution. */ if($this->app->tab == 'project') { $case->project = $this->session->project; } elseif($this->app->tab == 'execution') { $case->execution = $this->session->execution; $case->project = $this->dao->select('project')->from(TABLE_PROJECT)->where('id')->eq($case->execution)->fetch('project'); } else { $objects = $this->dao->select('t1.project as objectID, t2.type')->from(TABLE_PROJECTCASE)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id') ->where('t1.case')->eq($caseID) ->fetchPairs(); foreach($objects as $objectID => $objectType) { if($objectType == 'project') $case->project = $objectID; if(in_array($objectType, array('sprint', 'stage', 'kanban'))) $case->execution = $objectID; } } /* Set related variables. */ $toBugs = $this->dao->select('id, title, severity, openedDate, status')->from(TABLE_BUG)->where('`case`')->eq($caseID)->fetchAll(); $case->toBugs = array(); $case->fromBugData = array(); $case->linkCaseTitles = array(); foreach($toBugs as $toBug) $case->toBugs[$toBug->id] = $toBug; if($case->story) { $story = $this->dao->findById($case->story)->from(TABLE_STORY)->fields('title, status, version')->fetch(); $case->storyTitle = $story->title; $case->storyStatus = $story->status; $case->latestStoryVersion = $story->version; } if($case->fromBug) $case->fromBugData = $this->dao->findById($case->fromBug)->from(TABLE_BUG)->fields('id, title, severity, openedDate, status')->fetch(); if($case->linkCase || $case->fromCaseID) $case->linkCaseTitles = $this->dao->select('id,title')->from(TABLE_CASE)->where('id')->in($case->linkCase)->orWhere('id')->eq($case->fromCaseID)->fetchPairs(); $case->currentVersion = $version ? $version : $case->version; $case->files = $this->loadModel('file')->getByObject('testcase', $caseID); $case->steps = $this->testcaseTao->getSteps($caseID, $case->currentVersion); $spec = $this->dao->select('title,precondition,files')->from(TABLE_CASESPEC)->where('case')->eq($caseID)->andWhere('version')->eq($version)->fetch(); if($spec) { $case->title = $spec->title; $case->precondition = $spec->precondition; $case->files = $this->file->getByIdList($spec->files); } return $case; } /** * 测试通过 id 列表和查询语句获取用例。 * Get cases by id list and query string. * * @param array $caseIdList * @param string $query * @access public * @return array */ public function getByList(array $caseIdList, string $query = ''): array { if(!$caseIdList) return array(); return $this->dao->select('*,precondition')->from(TABLE_CASE) ->where('deleted')->eq(0) ->andWhere('id')->in($caseIdList) ->beginIF($query)->andWhere($query)->fi() ->fetchAll('id', false); } /** * 获取用例列表。 * Get test cases. * * @param int|array $productID * @param int|string $branch * @param string $browseType * @param int $queryID * @param int $moduleID * @param string $caseType * @param string $auto no|unit * @param string $orderBy * @param object $pager * @param string $from * @access public * @return array */ public function getTestCases(int|array $productID, string|int $branch, string $browseType, int $queryID, int $moduleID, string $caseType = '', string $auto = 'no', string $orderBy = 'id_desc', ?object $pager = null, string $from = 'testcase'): array { if(common::isTutorialMode()) return $this->loadModel('tutorial')->getCases(); $modules = $moduleID ? $this->loadModel('tree')->getAllChildId($moduleID) : array(); $browseType = ($browseType == 'bymodule' and $this->session->caseBrowseType and $this->session->caseBrowseType != 'bysearch') ? $this->session->caseBrowseType : $browseType; $auto = $this->cookie->onlyAutoCase ? 'auto' : $auto; if($browseType == 'bymodule' || $browseType == 'all' || $browseType == 'wait') { if($this->app->tab == 'project' && $from != 'doc') return $this->testcaseTao->getModuleProjectCases($productID, $branch, $modules, $browseType, $auto, $caseType, $orderBy, $pager); return $this->getModuleCases($productID, $branch, $modules, $browseType, $auto, $caseType, $orderBy, $pager, $from); } if($browseType == 'needconfirm') return $this->testcaseTao->getNeedConfirmList($productID, $branch, $modules, $auto, $caseType, $orderBy, $pager); if($browseType == 'bysuite') return $this->testcaseTao->getBySuite($productID, $branch, (int)$queryID, $modules, $auto, $orderBy, $pager); if($browseType == 'bysearch') return $this->getBySearch($productID, $branch, (int)$queryID, $auto, $orderBy, $pager); return array(); } /** * 搜索用例。 * Get cases by search. * * @param int $productID * @param int|string $branch * @param int $queryID * @param string $auto no|unit * @param string $orderBy * @param object $pager * @access public * @return array */ public function getBySearch(int $productID, int|string $branch = 0, int $queryID = 0, string $auto = 'no', string $orderBy = 'id_desc', ?object $pager = null): array { $queryID = (int)$queryID; if($queryID) { $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('testcaseQuery', $query->sql); $this->session->set('testcaseForm', $query->form); } } if($this->session->testcaseQuery == false) $this->session->set('testcaseQuery', ' 1 = 1'); $caseQuery = '(' . $this->session->testcaseQuery; // 将caseQuery中的字段替换成t1.字段 $caseQuery = preg_replace('/`(.*?)`/', 't1.`$1`', $caseQuery); /* 处理用例查询中的产品条件。*/ /* Process product condition in case query. */ $queryProductID = $productID; if(strpos($this->session->testcaseQuery, "`product` = 'all'") !== false) { $caseQuery = str_replace("t1.`product` = 'all'", '1', $caseQuery); $caseQuery .= ' AND t1.`product` ' . helper::dbIN($this->app->user->view->products); $queryProductID = 'all'; } if($this->app->tab == 'project') $caseQuery = str_replace('t1.`product`', 't2.`product`', $caseQuery); /* 处理用例查询中的产品分支条件。*/ /* Process branch condition in case query. */ if($branch !== 'all' && strpos($caseQuery, '`branch` =') === false) $caseQuery .= " AND t1.`branch` in ('$branch')"; if(strpos($caseQuery, "`branch` = 'all'") !== false) $caseQuery = str_replace("t1.`branch` = 'all'", '1 = 1', $caseQuery); $caseQuery .= ')'; /* Search criteria under compatible project. */ $sql = $this->dao->select('DISTINCT t1.*,t3.title as storyTitle')->from(TABLE_CASE)->alias('t1'); if($this->app->tab == 'project') $sql->leftJoin(TABLE_PROJECTCASE)->alias('t2')->on('t1.id = t2.case'); $sql->leftJoin(TABLE_STORY)->alias('t3')->on('t1.story = t3.id'); return $sql->where($caseQuery) ->beginIF($this->app->tab == 'project' && $this->config->systemMode == 'ALM')->andWhere('t2.project')->eq($this->session->project)->fi() ->beginIF($this->app->tab == 'project' && !empty($productID) && $queryProductID != 'all')->andWhere('t2.product')->eq($productID)->fi() ->beginIF($this->app->tab != 'project' && !empty($productID) && $queryProductID != 'all')->andWhere('t1.product')->eq($productID)->fi() ->beginIF($auto == 'auto' || $auto == 'unit')->andWhere('t1.auto')->eq($auto)->fi() ->beginIF($auto != 'auto' && $auto != 'unit')->andWhere('t1.auto')->ne('unit')->fi() ->andWhere('t1.deleted')->eq('0') ->orderBy($orderBy) ->page($pager, 't1.id') ->fetchAll('id', false); } /** * 根据指派给获取用例。 * Get cases by assignedTo. * * @param string $account * @param string $auto no|unit|skip * @param string $orderBy * @param object $pager * @access public * @return array */ public function getByAssignedTo(string $account, string $auto = 'no', string $orderBy = 'id_desc', ?object $pager = null): array { return $this->dao->select('t1.id AS run, t1.task, t1.case, t1.version, t1.assignedTo, t1.lastRunner, t1.lastRunDate, t1.lastRunResult, t1.status AS lastRunStatus, t2.id AS id, t2.project, t2.pri, t2.title, t2.type, t2.openedBy, t2.color, t2.product, t2.lib, t2.branch, t2.module, t2.status, t2.auto, t2.story, t2.storyVersion, t3.name AS taskName')->from(TABLE_TESTRUN)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case = t2.id') ->leftJoin(TABLE_TESTTASK)->alias('t3')->on('t1.task = t3.id') ->where('t1.assignedTo')->eq($account) ->andWhere('t3.deleted')->eq('0') ->andWhere('t2.deleted')->eq('0') ->andWhere('t3.status')->ne('done') ->beginIF(strpos($auto, 'skip') === false && $auto != 'unit')->andWhere('t2.auto')->ne('unit')->fi() ->beginIF($auto == 'unit')->andWhere('t2.auto')->eq('unit')->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll(strpos($auto, 'run') !== false ? 'run' : 'id'); } /** * 根据创建者获取用例。 * Get cases by openedBy * * @param string $account * @param string $auto no|unit|skip * @param string $orderBy * @param object $pager * @access public * @return array */ public function getByOpenedBy(string $account, string $auto = 'no', string $orderBy = 'id_desc', ?object $pager = null): array { return $this->dao->findByOpenedBy($account)->from(TABLE_CASE) ->andWhere('deleted')->eq('0') ->beginIF($auto != 'skip')->andWhere('product')->ne(0)->fi() ->beginIF($auto != 'skip' && $auto != 'unit')->andWhere('auto')->ne('unit')->fi() ->beginIF($auto == 'unit')->andWhere('auto')->eq('unit')->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } /** * 根据状态获取用例。 * Get cases by status. * * @param int $productID * @param int|string $branch * @param string $type all|needconfirm * @param string $status all|normal|blocked|investigate * @param int $moduleID * @param string $auto no|unit|skip * @param string $orderBy * @param object $pager * @access public * @return array */ public function getByStatus(int $productID = 0, int|string $branch = 0, string $type = 'all', string $status = 'all', int $moduleID = 0, string $auto = 'no', string $orderBy = 'id_desc', ?object $pager = null): array { $modules = $moduleID ? $this->loadModel('tree')->getAllChildId($moduleID) : '0'; $cases = $this->dao->select('t1.*, t2.title AS storyTitle')->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->where('1=1') ->beginIF($productID)->andWhere('t1.product')->eq((int) $productID)->fi() ->beginIF(!$productID)->andWhere('t1.product')->in($this->app->user->view->products)->fi() ->beginIF($branch)->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('t1.module')->in($modules)->fi() ->beginIF($type == 'needconfirm') ->andWhere('t2.status')->eq('active') ->andWhere('t2.version > t1.storyVersion') ->fi() ->beginIF($status != 'all')->andWhere('t1.status')->eq($status)->fi() ->beginIF($auto == 'unit')->andWhere('t1.auto')->eq('unit')->fi() ->beginIF($auto != 'unit')->andWhere('t1.auto')->ne('unit')->fi() ->andWhere('t1.deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); return $this->appendData($cases); } /** * 根据产品获取用例。 * Get cases by product id. * * @param int $productID * @access public * @return array */ public function getByProduct(int $productID): array { return $this->dao->select('*')->from(TABLE_CASE)->where('deleted')->eq('0')->andWhere('product')->eq($productID)->fetchAll('id', false); } /** * 通过产品 id 和分支获取用例键对。 * Get case pairs by product id and branch. * * @param int $productID * @param int|array $branch * @param string $search * @param int $limit * @access public * @return array */ public function getPairsByProduct(int $productID, int|array $branch = 0, string $search = '', int $limit = 0): array { return $this->dao->select("id, concat_ws(':', id, title) as title")->from(TABLE_CASE) ->where('deleted')->eq(0) ->andWhere('product')->eq($productID) ->beginIF($branch)->andWhere('branch')->in($branch)->fi() ->beginIF(strlen(trim($search)))->andWhere('title')->like('%' . $search . '%')->fi() ->orderBy('id_desc') ->beginIF($limit)->limit($limit)->fi() ->fetchPairs(); } /** * 获取关联需求的用例。 * Get cases of a story. * * @param int $storyID * @access public * @return array */ public function getStoryCases(int $storyID): array { return $this->dao->select('id, project, title, pri, type, status, lastRunner, lastRunDate, lastRunResult') ->from(TABLE_CASE) ->where('story')->eq((int)$storyID) ->andWhere('deleted')->eq(0) ->fetchAll('id'); } /** * 获取需求列表关联的用例数量数组。 * Get counts of some stories' cases. * * @param array $stories * @access public * @return array */ public function getStoryCaseCounts(array $stories): array { if(empty($stories)) return array(); $caseCounts = $this->dao->select('story, COUNT(1) AS cases') ->from(TABLE_CASE) ->where('story')->in($stories) ->andWhere('deleted')->eq(0) ->groupBy('story') ->fetchPairs(); foreach($stories as $storyID) { if(!isset($caseCounts[$storyID])) $caseCounts[$storyID] = 0; } return $caseCounts; } /** * 获取导出的用例。 * Get cases to export. * * @param string $exportType * @param int $taskID * @param string $orderBy * @param int $limit * @access public * @return array */ public function getCasesToExport(string $exportType, int $taskID, string $orderBy, int $limit): array { if(strpos($orderBy, 'case') !== false) { list($field, $sort) = explode('_', $orderBy); $orderBy = '`' . $field . '`_' . $sort; } $queryCondition = preg_replace("/AND\s+t[0-9]\.scene\s+=\s+'0'/i", '', $this->session->testcaseQueryCondition); if($this->session->testcaseOnlyCondition) { $caseIdList = array(); if($taskID) $caseIdList = $this->dao->select('`case`')->from(TABLE_TESTRUN)->where('task')->eq($taskID)->fetchPairs(); return $this->dao->select('*')->from(TABLE_CASE)->where($queryCondition) ->beginIF($taskID)->andWhere('id')->in($caseIdList)->fi() ->beginIF($exportType == 'selected')->andWhere('id')->in($this->cookie->checkedItem)->fi() ->orderBy($orderBy) ->beginIF($limit)->limit($limit)->fi() ->fetchAll('id', false); } $cases = array(); $orderBy = " ORDER BY " . str_replace(array('|', '^A', '_'), ' ', $orderBy); $stmt = $this->dao->query($queryCondition . $orderBy . ($limit ? ' LIMIT ' . $limit : '')); while($row = $stmt->fetch()) { $caseID = isset($row->case) ? $row->case : $row->id; if($exportType == 'selected' && $taskID && strpos(",{$this->cookie->checkedItem},", ",$row->id,") === false) continue; if($exportType == 'selected' && !$taskID && strpos(",{$this->cookie->checkedItem},", ",$caseID,") === false) continue; $row->id = $caseID; $cases[$caseID] = $row; } return $cases; } /** * 获取导出的用例的结果。 * Get case results for export. * * @param array $caseIdList * @param int $taskID * @access public * @return array */ public function getCaseResultsForExport(array $caseIdList, int $taskID = 0): array { $stmt = $this->dao->select('t1.*')->from(TABLE_TESTRESULT)->alias('t1') ->leftJoin(TABLE_TESTRUN)->alias('t2')->on('t1.run = t2.id') ->where('t1.`case`')->in($caseIdList) ->beginIF($taskID)->andWhere('t2.task')->eq($taskID)->fi() ->beginIF($this->app->tab == 'devops')->andWhere('t1.deploy')->eq($this->session->deployID)->fi() ->orderBy('id_desc') ->query(); $results = array(); while($result = $stmt->fetch()) { if(!isset($results[$result->case])) $results[$result->case] = unserialize($result->stepResults); } return $results; } /** * 更新用例。 * Update a case. * * @param object $case * @param object $oldCase * @param array $testtasks * @access public * @return bool|array */ public function update(object $case, object $oldCase, array $testtasks = array()): bool|array { $this->testcaseTao->doUpdate($case); if(dao::isError()) return false; $this->testcaseTao->updateCase2Project($oldCase, $case); if($oldCase->version != $case->version || !empty($case->stepChanged)) $this->testcaseTao->updateStep($case, $oldCase); if(isset($oldCase->toBugs) && isset($case->linkBug)) $this->testcaseTao->linkBugs($oldCase->id, array_keys($oldCase->toBugs), $case); if($case->branch && !empty($testtasks)) $this->testcaseTao->unlinkCaseFromTesttask($oldCase->id, $case->branch, $testtasks); $this->loadModel('file')->processFileDiffsForObject('testcase', $oldCase, $case); if($oldCase->version != $case->version) $this->testcaseTao->doCreateSpec($oldCase->id, $case, $case->files); /* Join the steps to diff. */ if(!empty($case->stepChanged) && $case->steps) { $oldCase->steps = $this->joinStep($oldCase->steps); $case->steps = $this->joinStep($this->getByID($oldCase->id, $case->version)->steps); } else { unset($oldCase->steps); unset($case->steps); } return common::createChanges($oldCase, $case); } /** * 评审用例。 * Review case. * * @param object $case * @param object $oldCase * @access public * @return bool */ public function review(object $case, object $oldCase): bool { $this->dao->update(TABLE_CASE)->data($case, 'result,comment')->autoCheck()->checkFlow()->where('id')->eq($oldCase->id)->exec(); if(dao::isError()) return false; $changes = common::createChanges($oldCase, $case); $actionID = $this->loadModel('action')->create('case', $oldCase->id, 'Reviewed', $this->post->comment, ucfirst($case->result)); $this->action->logHistory($actionID, $changes); return true; } /** * Batch review cases. * * @param array $caseIdList * @param string $result * @access public * @return bool */ public function batchReview(array $caseIdList, string $result): bool { $caseIdList = array_filter($caseIdList); if(!$caseIdList) return false; $oldCases = $this->getByList($caseIdList, "status = 'wait'"); if(!$oldCases) return false; $now = helper::now(); $case = new stdClass(); $case->reviewedBy = $this->app->user->account; $case->reviewedDate = substr($now, 0, 10); $case->lastEditedBy = $this->app->user->account; $case->lastEditedDate = $now; if($result == 'pass') $case->status = 'normal'; $this->dao->update(TABLE_CASE)->data($case)->autoCheck()->where('id')->in(array_keys($oldCases))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldCases as $oldCase) { $changes = common::createChanges($oldCase, $case); if($changes) { $actionID = $this->action->create('case', $oldCase->id, 'Reviewed', '', ucfirst($result)); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * 获取可关联的用例。 * Get cases to link. * * @param int $caseID * @param string $browseType * @param int $queryID * @access public * @return array */ public function getCases2Link(int $caseID, string $browseType = 'bySearch', int $queryID = 0): array { if($browseType != 'bySearch') return array(); $case = $this->getByID($caseID); $cases2Link = $this->getBySearch($case->product, $case->branch, (int)$queryID, $auto = 'no', $orderBy = 'id'); foreach($cases2Link as $key => $case2Link) { if($case2Link->id == $caseID) unset($cases2Link[$key]); if(in_array($case2Link->id, explode(',', $case->linkCase))) unset($cases2Link[$key]); } return $cases2Link; } /** * 获取可关联的 bug。 * Get bugs to link. * * @param int $caseID * @param string $browseType * @param int $queryID * @param string $orderBy * @access public * @return array */ public function getBugs2Link(int $caseID, string $browseType = 'bySearch', int $queryID = 0, string $orderBy = 'id_asc'): array { if($browseType != 'bySearch') return array(); $case = $this->getByID($caseID); $bugs2Link = $this->loadModel('bug')->getBySearch('bug', $case->product, (string)$case->branch, 0, 0, (int)$queryID, '', $orderBy); foreach($bugs2Link as $key => $bug2Link) { if($bug2Link->case != 0) unset($bugs2Link[$key]); } return $bugs2Link; } /** * 批量删除用例和场景。 * Batch delete cases and scenes. * * @param array $caseIdList * @param array $sceneIdList * @access public * @return bool */ public function batchDelete(array $caseIdList, array $sceneIdList): bool { /* 过滤用例和场景。 */ /* Filter cases and scenes. */ $caseIdList = array_filter($caseIdList); $sceneIdList = array_filter($sceneIdList); if(!$caseIdList && !$sceneIdList) return false; $this->loadModel('action'); /* 删除用例。 */ /* Delete cases. */ if($caseIdList) { $this->dao->update(TABLE_CASE)->set('deleted')->eq('1')->where('id')->in($caseIdList)->exec(); foreach($caseIdList as $caseID) $this->action->create('case', (int)$caseID, 'deleted', '', actionModel::CAN_UNDELETED); } /* 删除场景。 */ /* Delete scenes. */ if($sceneIdList) { $this->dao->update(TABLE_SCENE)->set('deleted')->eq('1')->where('id')->in($sceneIdList)->exec(); foreach($sceneIdList as $sceneID) $this->action->create('scene', $sceneID, 'deleted', '', actionModel::CAN_UNDELETED); } return !dao::isError(); } /** * Batch change branch of cases and scenes. * * @param array $caseIdList * @param array $sceneIdList * @param int $branchID * @access public * @return bool */ public function batchChangeBranch(array $caseIdList, array $sceneIdList, int $branchID): bool { if($branchID < 0 || $branchID > 16777215) return false; // The branch column's data type is mediumint unsigned and its range is 0-16777215. $caseIdList = array_filter($caseIdList); $sceneIdList = array_filter($sceneIdList); if(!$caseIdList && !$sceneIdList) return false; if($caseIdList) $this->batchChangeCaseBranch($caseIdList, $branchID); if($sceneIdList) $this->batchChangeSceneBranch($sceneIdList, $branchID); return !dao::isError(); } /** * Batch change branch of cases. * * @param array $caseIdList * @param int $branchID * @access public * @return bool */ public function batchChangeCaseBranch(array $caseIdList, int $branchID): bool { if(!$caseIdList) return false; if($branchID < 0 || $branchID > 16777215) return false; // The branch column's data type is mediumint unsigned and its range is 0-16777215. $oldCases = $this->getByList($caseIdList, "branch != '{$branchID}'"); if(!$oldCases) return false; $case = new stdclass(); $case->branch = $branchID; $case->lastEditedBy = $this->app->user->account; $case->lastEditedDate = helper::now(); $this->dao->update(TABLE_CASE)->data($case)->where('id')->in(array_keys($oldCases))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldCases as $oldCase) { $changes = common::createChanges($oldCase, $case); if($changes) { $actionID = $this->action->create('case', $oldCase->id, 'edited'); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * Batch change branch of scenes. * * @param array $sceneIdList * @param int $branchID * @access public * @return bool */ public function batchChangeSceneBranch(array $sceneIdList, int $branchID): bool { if(!$sceneIdList) return false; if($branchID < 0 || $branchID > 16777215) return false; // The branch column's data type is mediumint unsigned and its range is 0-16777215. $oldScenes = $this->getScenesByList($sceneIdList, "branch != '{$branchID}'"); if(!$oldScenes) return false; $scene = new stdclass(); $scene->branch = $branchID; $scene->lastEditedBy = $this->app->user->account; $scene->lastEditedDate = helper::now(); $this->dao->update(TABLE_SCENE)->data($scene)->where('id')->in(array_keys($oldScenes))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldScenes as $oldScene) { $changes = common::createChanges($oldScene, $scene); if($changes) { $actionID = $this->action->create('scene', $oldScene->id, 'edited'); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * Batch change module of cases and scenes. * * @param array $caseIdList * @param array $sceneIdList * @param int $moduleID * @access public * @return bool */ public function batchChangeModule(array $caseIdList, array $sceneIdList, int $moduleID): bool { if($moduleID < 0 || $moduleID > 16777215) return false; // The module column's data type is mediumint unsigned and its range is 0-16777215. $caseIdList = array_filter($caseIdList); $sceneIdList = array_filter($sceneIdList); if(!$caseIdList && !$sceneIdList) return false; if($caseIdList) $this->batchChangeCaseModule($caseIdList, $moduleID); if($sceneIdList) $this->batchChangeSceneModule($sceneIdList, $moduleID); return !dao::isError(); } /** * Batch change module of cases. * * @param array $caseIdList * @param int $moduleID * @access public * @return bool */ public function batchChangeCaseModule(array $caseIdList, int $moduleID): bool { if(!$caseIdList) return false; if($moduleID < 0 || $moduleID > 16777215) return false; // The module column's data type is mediumint unsigned and its range is 0-16777215. $oldCases = $this->getByList($caseIdList, "module != '{$moduleID}'"); if(!$oldCases) return false; $case = new stdclass(); $case->module = $moduleID; $case->lastEditedBy = $this->app->user->account; $case->lastEditedDate = helper::now(); $this->dao->update(TABLE_CASE)->data($case)->where('id')->in(array_keys($oldCases))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldCases as $oldCase) { $changes = common::createChanges($oldCase, $case); if($changes) { $actionID = $this->action->create('case', $oldCase->id, 'edited'); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * Batch change module of scenes. * * @param array $sceneIdList * @param int $moduleID * @access public * @return bool */ public function batchChangeSceneModule(array $sceneIdList, int $moduleID): bool { if(!$sceneIdList) return false; if($moduleID < 0 || $moduleID > 16777215) return false; // The module column's data type is mediumint unsigned and its range is 0-16777215. $oldScenes = $this->getScenesByList($sceneIdList, "module != '{$moduleID}'"); if(!$oldScenes) return false; $scene = new stdclass(); $scene->module = $moduleID; $scene->lastEditedBy = $this->app->user->account; $scene->lastEditedDate = helper::now(); $this->dao->update(TABLE_SCENE)->data($scene)->where('id')->in(array_keys($oldScenes))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldScenes as $oldScene) { $changes = common::createChanges($oldScene, $scene); if($changes) { $actionID = $this->action->create('scene', $oldScene->id, 'edited'); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * Batch change type of cases. * * @param array $caseIdList * @param string $type * @access public * @return bool */ public function batchChangeType(array $caseIdList, string $type): bool { if(!$type) return false; $caseIdList = array_filter($caseIdList); if(!$caseIdList) return false; $oldCases = $this->getByList($caseIdList, "type != '{$type}'"); if(!$oldCases) return false; $case = new stdClass(); $case->type = $type; $case->lastEditedBy = $this->app->user->account; $case->lastEditedDate = helper::now(); $this->dao->update(TABLE_CASE)->data($case)->autoCheck()->where('id')->in(array_keys($oldCases))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldCases as $oldCase) { $changes = common::createChanges($oldCase, $case); if($changes) { $actionID = $this->action->create('case', $oldCase->id, 'Edited', '', ucfirst($type)); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * Batch confirm story change of cases. * * @param array $caseIdList * @access public * @return bool */ public function batchConfirmStoryChange(array $caseIdList): bool { $caseIdList = array_filter($caseIdList); if(!$caseIdList) return false; $cases = $this->getByList($caseIdList); if(!$cases) return false; $storyIdList = array_unique(array_filter(array_map(function($case){return $case->story;}, $cases))); if(!$storyIdList) return false; $stories = $this->dao->select('id, version')->from(TABLE_STORY)->where('id')->in($storyIdList)->fetchPairs(); $this->loadModel('action'); foreach($cases as $case) { $storyVersion = zget($stories, $case->story, 0); if(!$storyVersion) continue; $this->dao->update(TABLE_CASE)->set('storyVersion')->eq($storyVersion)->where('id')->eq($case->id)->exec(); $this->action->create('case', $case->id, 'confirmed', '', $storyVersion); } return !dao::isError(); } /** * 将步骤转为字符串,并且区分它们。 * Join steps to a string, thus can diff them. * * @param array $steps * @access public * @return string */ public function joinStep(array $steps): string { $return = ''; if(empty($steps)) return $return; foreach($steps as $step) $return .= $step->desc . ' EXPECT:' . $step->expect . "\n"; return $return; } /** * 从 bug 的步骤创建用例步骤。 * Create case steps from a bug's step. * * @param string $steps * @access public * @return array */ public function createStepsFromBug(string $steps): array { /* 初始化步骤相关变量,获取步骤的描述、结果和期望,以及他们在字符串中的位置。 */ /* Initializes the step variable, and get the desc, result and expect of the step and its position. */ $steps = strip_tags($steps); $caseSteps = array((object)array('name' => '1', 'desc' => $steps, 'step' => $steps, 'expect' => '')); // the default steps before parse. $lblStep = strip_tags($this->lang->bug->tplStep); $lblResult = strip_tags($this->lang->bug->tplResult); $lblExpect = strip_tags($this->lang->bug->tplExpect); $lblStepPos = strpos($steps, $lblStep); $lblResultPos = strpos($steps, $lblResult); $lblExpectPos = strpos($steps, $lblExpect); /* 如果 bug 的步骤没有描述、结果或者期望,返回默认步骤。 */ /* If steps don't have desc, result or expect, return default steps. */ if($lblStepPos === false || $lblResultPos === false || $lblExpectPos === false) return $caseSteps; /* 计算描述和期望。 */ /* Process desc and expect. */ $caseSteps = substr($steps, $lblStepPos + strlen($lblStep), $lblResultPos - strlen($lblStep) - $lblStepPos); $caseExpect = substr($steps, $lblExpectPos + strlen($lblExpect)); $caseSteps = trim($caseSteps); $caseExpect = trim($caseExpect); /* 计算步骤。 */ /* Process steps. */ $caseSteps = explode("\n", trim($caseSteps)); $stepCount = count($caseSteps); foreach($caseSteps as $key => $caseStep) { $expect = $key + 1 == $stepCount ? $caseExpect : ''; $caseSteps[$key] = (object)array('name' => '1', 'desc' => trim($caseStep), 'step' => trim($caseStep), 'expect' => $expect, 'type' => 'item'); } return $caseSteps; } /** * 判断当前动作是否可以点击。 * Adjust the action is clickable. * * @param object $case * @param string $action * @access public * @return bool */ public static function isClickable(object $case, string $action): bool { $canBeChanged = common::canBeChanged('case', $case); if(!$canBeChanged) return false; global $config; $action = strtolower($action); if($action == 'runcase') return (empty($case->lib) || !empty($case->product)) && $case->auto == 'no' && $case->status != 'wait'; if($action == 'runresult') return !$case->lib || !empty($case->product); if($action == 'importtolib') return !$case->lib || !empty($case->product); if($action == 'ztfrun') return $case->auto == 'auto'; if($action == 'confirmchange') return isset($case->caseStatus) && isset($case->caseVersion) && $case->caseStatus != 'wait' && $case->version < $case->caseVersion; if($action == 'confirmstorychange') return !empty($case->needconfirm) || (isset($case->browseType) && $case->browseType == 'needconfirm'); if($action == 'createbug') return isset($case->caseFails) && $case->caseFails > 0; if($action == 'create') return !$case->lib || !empty($case->product); if($action == 'review') return ($config->testcase->needReview || !empty($config->testcase->forceReview)) && (isset($case->caseStatus) ? $case->caseStatus == 'wait' : $case->status == 'wait'); if($action == 'showscript') return $case->auto == 'auto'; if($action == 'createcase') return !isset($case->lib) || ($case->lib && empty($case->product)); /* 判断确认撤销操作按钮的权限。 */ /* Check confirmdemandretract priv. */ if($action == 'confirmdemandretract') return !empty($case->confirmeActionType) && $case->confirmeActionType == 'confirmedretract'; /* 判断确认移除操作按钮的权限。 */ /* Check confirmdemandunlink priv. */ if($action == 'confirmdemandunlink') return !empty($case->confirmeActionType) && $case->confirmeActionType == 'confirmedunlink'; return true; } /** * 获取导入的字段。 * Get fields for import. * * @param int $productID * @access public * @return array */ public function getImportFields(int $productID = 0): array { /* If type of the product isn't normal, set language item of branch. */ /* 如果产品是多分支产品,设置分支语言项。 */ $product = $this->loadModel('product')->getById($productID); if($product && $product->type != 'normal') $this->lang->testcase->branch = $this->lang->product->branchName[$product->type]; $caseLang = $this->lang->testcase; $caseConfig = $this->config->testcase; $fields = explode(',', $caseConfig->exportFields); foreach($fields as $key => $fieldName) { /* 设置字段的语言项。 */ /* Set language item of the fieldName. */ $fieldName = trim($fieldName); $fields[$fieldName] = isset($caseLang->{$fieldName}) ? $caseLang->{$fieldName} : $fieldName; unset($fields[$key]); } return $fields; } /** * 导入测试用例到用例库。 * Import cases to lib. * * @param array $cases * @param array $steps * @param array $files * @access public * @return bool */ public function importToLib(array $cases, array $steps, array $files): bool { $this->loadModel('action'); $caseIdList = array_column($cases, 'id'); $oldCaseList = $this->getByList($caseIdList); foreach($cases as $case) { /* 如果用例没有 ID,插入用例。 */ /* If case id is not exist, insert it. */ if(!isset($case->id)) { $this->testcaseTao->doCreate($case); if(!dao::isError()) { $caseID = $this->dao->lastInsertID(); $this->action->create('case', $caseID, 'tolib', '', $case->fromCaseID); } } /* 如果用例有 ID,更新用例。 */ /* If case id is exist, update it. */ else { $caseID = $case->id; $oldCase = isset($oldCaseList[$caseID]) ? $oldCaseList[$caseID] : null; $this->testcaseTao->doUpdate($case); $this->action->create('case', $caseID, 'updatetolib', '', $case->fromCaseID); $this->dao->delete()->from(TABLE_CASESTEP)->where('`case`')->eq($caseID)->exec(); $removeFiles = $this->dao->select('id,pathname')->from(TABLE_FILE)->where('`objectID`')->eq($caseID)->andWhere('objectType')->eq('testcase')->fetchAll('id'); $this->dao->delete()->from(TABLE_FILE)->where('`objectID`')->eq($caseID)->andWhere('objectType')->eq('testcase')->exec(); foreach($removeFiles as $fileID => $file) { if(empty($file->pathname)) continue; $filePath = pathinfo($file->pathname, PATHINFO_BASENAME); $datePath = substr($file->pathname, 0, 6); $filePath = $this->app->getAppRoot() . "www/data/upload/{$this->app->company->id}/" . "{$datePath}/" . $filePath; if(file_exists($filePath)) unlink($filePath); } if(!empty($oldCase) && $oldCase->lib && empty($oldCase->product)) { $fromcaseVersion = $this->dao->select('fromCaseVersion')->from(TABLE_CASE)->where('fromCaseID')->eq($case->id)->fetch('fromCaseVersion'); $fromcaseVersion = (int)$fromcaseVersion + 1; $this->dao->update(TABLE_CASE)->set('`fromCaseVersion`')->eq($fromcaseVersion)->where('`fromCaseID`')->eq($case->id)->exec(); } } if(isset($caseID)) { $this->testcaseTao->importSteps($caseID, zget($steps, $case->fromCaseID, array())); $this->testcaseTao->importFiles($caseID, zget($files, $case->fromCaseID, array())); } } return !dao::isError(); } /** * 导入用例关联的模块。 * Import case related modules. * * @param int $libID * @param int $oldModuleID * @param int $maxOrder * @access public * @return int */ public function importCaseRelatedModules(int $libID, int $oldModuleID = 0, int $maxOrder = 0): int { /* If module has been imported, return imported module id. */ $moduleID = $this->checkModuleImported($libID, $oldModuleID); if($moduleID) return $moduleID; /* Build old module, and insert it. */ $oldModule = $this->dao->select('name, parent, grade, `order`, short')->from(TABLE_MODULE)->where('id')->eq($oldModuleID)->fetch(); $oldModule->root = $libID; $oldModule->from = $oldModuleID; $oldModule->type = 'caselib'; if(!empty($maxOrder)) $oldModule->order = $maxOrder + $oldModule->order; $this->dao->insert(TABLE_MODULE)->data($oldModule)->autoCheck()->exec(); if(!dao::isError()) { /* Get new module id. */ $newModuleID = $this->dao->lastInsertID(); /* Set path and parent. */ if($oldModule->parent) { /* If old module has parent module, import parent module. */ $parentModuleID = $this->importCaseRelatedModules($libID, $oldModule->parent, !empty($maxOrder) ? $maxOrder : 0); $parentModule = $this->dao->select('id, path')->from(TABLE_MODULE)->where('id')->eq($parentModuleID)->fetch(); $parent = $parentModule->id; $path = $parentModule->path . "$newModuleID,"; } else { $path = ",$newModuleID,"; $parent = 0; } /* Update path and parent. */ $this->dao->update(TABLE_MODULE)->set('parent')->eq($parent)->set('path')->eq($path)->where('id')->eq($newModuleID)->exec(); /* Return new module id. */ return $newModuleID; } return 0; } /** * 检查模块是否可以导入。 * Adjust module is can import. * * @param int $libID * @param int $oldModule * @access public * @return int */ public function checkModuleImported(int $libID, int $oldModule = 0): int { /* Get module if from is oldModule and root is libID. */ $module = $this->dao->select('id')->from(TABLE_MODULE) ->where('root')->eq($libID) ->andWhere('`from`')->eq($oldModule) ->andWhere('type')->eq('caselib') ->andWhere('deleted')->eq(0) ->fetch(); /* If module isn't exist, return 0. */ if(!$module) return 0; /* Return module id. */ return $module->id; } /** * 追加 bug 和用例执行结果信息 * Append bugs and results. * * @param array $cases * @param string $type * @param array $caseIdList * @access public * @return void */ public function appendData(array $cases, string $type = 'case', array $caseIdList = array()): array { if(empty($caseIdList)) $caseIdList = array_keys($cases); /* 查询用例的 bugs 和结果。 */ /* Get bugs and results. */ $queryField = $type == 'case' ? '`case`' : '`result`'; $caseBugs = $this->dao->select('COUNT(1) AS count, `case`')->from(TABLE_BUG)->where($queryField)->in($caseIdList)->andWhere('deleted')->eq(0)->groupBy('`case`')->fetchPairs('case', 'count'); $results = $this->dao->select('COUNT(1) AS count, `case`')->from(TABLE_TESTRESULT)->where('`case`')->in($caseIdList)->groupBy('`case`')->fetchPairs('case', 'count'); /* 查询用例的失败结果。 */ /* Get result fails of the the testcases. */ if($type != 'case') $queryField = '`run`'; $caseFails = $this->dao->select('COUNT(1) AS count, `case`')->from(TABLE_TESTRESULT) ->where('caseResult')->eq('fail') ->andWhere($queryField)->in($caseIdList) ->groupBy('`case`') ->fetchPairs('case','count'); /* 查询用例的步骤。 */ /* Get the the testcase steps. */ $queryTable = $type == 'case' ? TABLE_CASE : TABLE_TESTRUN; $queryOn = $type == 'case' ? 't1.`case`=t2.`id`' : 't1.`case`=t2.`case`'; $queryField = $type == 'case' ? 't1.`case`' : 't2.`id`'; $steps = $this->dao->select('COUNT(DISTINCT t1.id) AS count, t1.`case`')->from(TABLE_CASESTEP)->alias('t1') ->leftJoin($queryTable)->alias('t2')->on($queryOn) ->where($queryField)->in($caseIdList) ->andWhere('t1.type')->ne('group') ->andWhere('t1.version=t2.version') ->groupBy('t1.`case`') ->fetchPairs('case', 'count'); /* 设置测试用例的 bugs 执行结果和步骤。 */ /* Set related bugs, results and steps of the testcases. */ foreach($cases as $case) { $caseID = $type == 'case' ? $case->id : $case->case; $case->bugs = zget($caseBugs, $caseID, 0); $case->results = zget($results, $caseID, 0); $case->caseFails = zget($caseFails, $caseID, 0); $case->stepNumber = zget($steps, $caseID, 0); $case = $this->processDateField($case); } return $cases; } /** * 检查是否不需要评审。 * Check whether force not review. * * @access public * @return bool */ public function forceNotReview(): bool { if(!$this->config->testcase->needReview) { if(!isset($this->config->testcase->forceReview)) return true; if(strpos(",{$this->config->testcase->forceReview},", ",{$this->app->user->account},") === false) return true; } elseif(isset($this->config->testcase->forceNotReview) && strpos(",{$this->config->testcase->forceNotReview},", ",{$this->app->user->account},") !== false) { return true; } return false; } /** * 获取用例总结。 * Summary cases. * * @param array $cases * @access public * @return string */ public function summary(array $cases): string { $executed = 0; foreach($cases as $case) { if($case->lastRunResult != '') $executed ++; } return sprintf($this->lang->testcase->summary, count($cases), $executed); } /** * 同步用例到项目中。 * Sync case to project. * * @param object $case * @param int $caseID * @access public * @return bool */ public function syncCase2Project(object $case, int $caseID): bool { $projects = array(); if(!empty($case->story)) { $projects = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($case->story)->fetchPairs(); } elseif($this->app->tab == 'project' && empty($case->story) && empty($case->project)) { $projects = array($this->session->project); } elseif($this->app->tab == 'execution' && empty($case->story) && empty($case->execution)) { $projects = array($this->session->execution); } if(!empty($case->project)) $projects[] = $case->project; if(!empty($case->execution)) $projects[] = $case->execution; if(empty($projects)) return false; $this->loadModel('action'); $projects = array_filter(array_unique($projects)); $objectInfo = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($projects)->fetchAll('id', false); foreach($projects as $projectID) { if(!isset($objectInfo[$projectID])) continue; $lastOrder = (int)$this->dao->select('*')->from(TABLE_PROJECTCASE)->where('project')->eq($projectID)->orderBy('order_desc')->limit(1)->fetch('order'); $lastOrder++; $data = new stdclass(); $data->project = $projectID; $data->product = $case->product; $data->case = $caseID; $data->version = 1; $data->order = $lastOrder; $this->dao->insert(TABLE_PROJECTCASE)->data($data)->exec(); $object = $objectInfo[$projectID]; $objectType = $object->type; if($objectType == 'project') $this->action->create('case', $caseID, 'linked2project', '', $projectID); if(in_array($objectType, array('sprint', 'stage')) && $object->multiple) $this->action->create('case', $caseID, 'linked2execution', '', $projectID); } return !dao::isError(); } /** * processDatas * * @param array $datas * @access public * @return void */ public function processDatas($datas) { if(isset($datas->datas)) $datas = $datas->datas; $stepData = array(); $stepVars = 0; foreach($datas as $row => $cellValue) { foreach($cellValue as $field => $value) { if($field != 'stepDesc' and $field != 'stepExpect') continue; $value = (string)$value; $steps = $value; if(strpos($value, "\n")) { $steps = explode("\n", $value); } elseif(strpos($value, "\r")) { $steps = explode("\r", $value); } if(is_string($steps)) $steps = explode("\n", $steps); $stepKey = str_replace('step', '', strtolower($field)); $caseStep = array(); foreach($steps as $step) { $trimmedStep = trim($step); if(empty($trimmedStep)) continue; preg_match('/^((([0-9]+)[.]([0-9]+))[.]([0-9]+))[.、](.*)$/Uu', $trimmedStep, $out); if(!$out) preg_match('/^(([0-9]+)[.]([0-9]+))[.、](.*)$/Uu', $trimmedStep, $out); if(!$out) preg_match('/^([0-9]+)[.、](.*)$/Uu', $trimmedStep, $out); if($out && !empty(trim($out[1]))) { $count = count($out); $num = $out[1]; $parent = $count > 4 ? $out[2] : '0'; $grand = $count > 6 ? $out[3] : '0'; $step = trim($out[2]); if($count > 4) $step = $count > 6 ? trim($out[6]) : trim($out[4]); $caseStep[$num]['content'] = $step; $caseStep[$num]['number'] = $num; $caseStep[$num]['type'] = $count > 4 ? 'item' : 'step'; if(!empty($parent)) $caseStep[$parent]['type'] = 'group'; if(!empty($grand)) $caseStep[$grand]['type'] = 'group'; } elseif(isset($num)) { $caseStep[$num]['content'] = isset($caseStep[$num]['content']) ? "{$caseStep[$num]['content']}\n{$step}" : "\n{$step}"; } elseif($field == 'stepDesc') { $num = 1; $caseStep[$num]['content'] = $step; $caseStep[$num]['type'] = 'step'; $caseStep[$num]['number'] = $num; } elseif($field == 'stepExpect' && isset($stepData[$row]['desc'])) { end($stepData[$row]['desc']); $num = key($stepData[$row]['desc']); $caseStep[$num]['content'] = $step; $caseStep[$num]['number'] = $num; } } unset($num); unset($sign); if($stepKey == 'expect' && !empty($stepData[$row]['desc'])) { foreach($stepData[$row]['desc'] as $stepDescValue) { if(empty($stepDescValue['number'])) continue; $caseNumber = $stepDescValue['number']; if($stepDescValue && !isset($caseStep[$caseNumber]) || empty($caseStep[$caseNumber]['content'])) $caseStep[$caseNumber] = ''; } } $stepVars += count($caseStep, COUNT_RECURSIVE) - count($caseStep); $stepData[$row][$stepKey] = $caseStep; } } return $stepData; } /** * Process date field when date is empty. * * @param object $case * @access public * @return object */ public function processDateField(object $case): object { foreach($case as $key => $value) { if(strpos($key, 'Date') !== false && helper::isZeroDate($value)) $case->$key = ''; } return $case; } /** * 处理批量表单内的用例步骤或预期。 * Process steps or expects in batch form. * * @param array $steps * @access protected * @return array */ public function processStepsOrExpects(string $steps): array { $caseSteps = array(); $stepTypes = array(); $steps = explode("\n", trim($steps)); $preParent = $preGrand = $preNum = '0'; foreach($steps as $step) { $step = trim($step); if(empty($step)) continue; $appendToPre = false; preg_match('/^((([0-9]+)[.]([0-9]+)[.]([0-9]+))[.]([0-9]+))[.、](.*)$/Uu', $step, $out); if($out) $appendToPre = true; // 如果层级大于3级,追加到目前的步骤中 preg_match('/^((([0-9]+)[.]([0-9]+))[.]([0-9]+))[.、](.*)$/Uu', $step, $out); if(!$out) preg_match('/^(([0-9]+)[.]([0-9]+))[.、](.*)$/Uu', $step, $out); if(!$out) preg_match('/^([0-9]+)[.、](.*)$/Uu', $step, $out); $grand = $parent = $num = '0'; if(!$appendToPre && $out && isset($caseSteps[$out[1]])) { $appendToPre = true; // 如果已经设置过,追加到目前的步骤中 } elseif(!$appendToPre && $out) { $count = count($out); if($count > 6) { $grand = $out[3]; $parent = $out[4]; $num = $out[5]; } elseif($count > 4) { $grand = $out[2]; $parent = $out[3]; } else { $grand = $out[1]; } $appendToPre = !(($grand == $preGrand && $parent == $preParent && $num == $preNum + 1) || ($grand == $preGrand && $parent == $preParent + 1 && $num == '0') || ($grand == $preGrand + 1 && $parent == '0' && $num == '0')); } if(!$appendToPre && $out) { $preGrand = $grand; $preParent = $parent; $preNum = $num; $parent = $count > 4 ? $out[2] : '0'; $step = trim($out[2]); $code = $out[1]; if($count > 4) $step = $count > 6 ? trim($out[6]) : trim($out[4]); $caseSteps[$code] = $step; $stepTypes[$code] = $count > 4 ? 'item' : 'step'; if($count > 4 && !empty($parent)) $stepTypes[$parent] = 'group'; if($count > 6 && !empty($grand)) $stepTypes[$grand] = 'group'; } elseif($appendToPre && isset($code)) { $caseSteps[$code] = isset($caseSteps[$code]) ? "{$caseSteps[$code]}\n{$step}" : "\n{$step}"; } elseif(!$out && isset($code)) { $caseSteps[$code] = isset($caseSteps[$code]) ? "{$caseSteps[$code]}\n{$step}" : "\n{$step}"; } } if(empty($caseSteps) && !empty($steps)) { $caseSteps[] = implode("\n", $steps); $stepTypes[] = 'step'; } return array($caseSteps, $stepTypes); } /** * 判断步骤是否变更。 * Judge if steps changed. * * @param object $case * @param array $oldStep * @access protected * @return bool */ protected function processStepsChanged(object $case, array $oldStep): bool { $newSteps = array_filter($case->steps); $stepChanged = (count($oldStep) != count($newSteps)); if(!$stepChanged) { $desc = array_values($case->steps); $expect = array_values($case->expects); $stepType = array_values($case->stepType); foreach($oldStep as $index => $step) { if($stepChanged) break; if(!isset($desc[$index]) || !isset($expect[$index]) || $step->desc != $desc[$index] || $step->expect != $expect[$index] || $step->type != $stepType[$index]) $stepChanged = true; } } return $stepChanged; } /** * 为 datatable 获取模块。 * Get modules for datatable. * * @param int $productID * @access public * @return void */ public function getDatatableModules(int $productID): array { $branches = $this->loadModel('branch')->getPairs($productID); $modules = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0); if(count($branches) <= 1) return $modules; foreach($branches as $branchID => $branchName) $modules += $this->tree->getOptionMenu($productID, 'case', 0, (string)$branchID); return $modules; } /** * Batch change scene. * * @param array $caseIdList * @param int $sceneID * @access public * @return bool */ public function batchChangeScene(array $caseIdList, int $sceneID): bool { if($sceneID < 0 || $sceneID > 16777215) return false; // The scene column's data type is mediumint unsigned and its range is 0-16777215. $caseIdList = array_filter($caseIdList); if(!$caseIdList) return false; $oldCases = $this->getByList($caseIdList, "scene != '{$sceneID}'"); if(!$oldCases) return false; $case = new stdclass(); $case->scene = $sceneID; $case->lastEditedBy = $this->app->user->account; $case->lastEditedDate = helper::now(); $this->dao->update(TABLE_CASE)->data($case)->where('id')->in(array_keys($oldCases))->exec(); if(dao::isError()) return false; $this->loadModel('action'); foreach($oldCases as $oldCase) { $changes = common::createChanges($oldCase, $case); if($changes) { $actionID = $this->action->create('case', $oldCase->id, 'edited'); $this->action->logHistory($actionID, $changes); } } return !dao::isError(); } /** * 为构建场景菜单获取场景。 * Get scenes for menu. * * @param int $productID * @param int $moduleID * @param int $startScene * @param string $branch * @param int $currentScene * @access public * @return array */ public function getScenesForMenu(int $productID, int $moduleID, int $startScene = 0, string $branch = 'all', int $currentScene = 0): array { /* Set the start scene. */ $startScenePath = ''; if($startScene > 0) { $startScene = $this->getSceneByID($startScene); if($startScene) $startScenePath = $startScene->path . '%'; } $currentScenePath = ''; if($currentScene > 0) { $currentScene = $this->getSceneByID($currentScene); if($currentScene) $currentScenePath = $currentScene->path . '%'; } /* Return scenes. */ return $this->dao->select('*')->from(TABLE_SCENE) ->where('deleted')->eq(0) ->beginIF($productID)->andWhere('product')->eq($productID)->fi() ->beginIF($moduleID > 0)->andWhere('module')->eq($moduleID)->fi() ->beginIF($startScenePath)->andWhere('path')->like($startScenePath)->fi() ->beginIF($currentScenePath)->andWhere('path')->notlike($currentScenePath)->fi() ->beginIF($branch !== 'all' && $branch !== '')->andWhere('branch')->eq((int)$branch)->fi() ->orderBy('grade desc, sort') ->fetchAll('id', false); } /** * 构建树数组。 * Build tree array. * * @param array $treeMenu * @param array $scenes * @param object $scene * @param string $sceneName * @access public * @return void */ public function buildTreeArray(array &$treeMenu, array $scenes, object $scene, string $sceneName = '/'): void { $parentScenes = explode(',', $scene->path); foreach($parentScenes as $parentSceneID) { if(empty($parentSceneID)) continue; if(empty($scenes[$parentSceneID])) continue; $sceneName .= $scenes[$parentSceneID]->title . '/'; } $sceneName = rtrim($sceneName, '/'); $sceneName .= "|$scene->id\n"; if(isset($treeMenu[$scene->id]) and !empty($treeMenu[$scene->id])) { if(isset($treeMenu[$scene->parent])) { $treeMenu[$scene->parent] .= $sceneName; } else { $treeMenu[$scene->parent] = $sceneName; } $treeMenu[$scene->parent] .= $treeMenu[$scene->id]; } else { if(isset($treeMenu[$scene->parent]) and !empty($treeMenu[$scene->parent])) { $treeMenu[$scene->parent] .= $sceneName; } else { $treeMenu[$scene->parent] = $sceneName; } } } /** * 根据 ID 获取一个场景。 * Get a scene by id. * * @param int $sceneID * @access public * @return object */ public function getSceneByID(int $sceneID): object|bool { return $this->dao->select('*')->from(TABLE_SCENE)->where('id')->eq($sceneID)->fetch(); } /** * 创建一个场景。 * Create a scene. * * @access public * @return bool */ public function createScene(object $scene): bool { $product = zget($scene, 'product', 0); $this->dao->insert(TABLE_SCENE)->data($scene) ->autoCheck() ->batchCheck($this->config->testcase->createscene->requiredFields, 'notempty') ->checkIF($product, 'title', 'unique', "product = '{$product}'") ->checkFlow() ->exec(); if(dao::isError()) return false; $sceneID = $this->dao->lastInsertID(); $scene->sort = $sceneID; $scene->path = ',' . $sceneID . ','; $scene->grade = 1; if(!empty($scene->parent)) { $parent = $this->getSceneByID($scene->parent); if($parent) { $scene->path = $parent->path . $sceneID . ','; $scene->grade = ++$parent->grade; $scene->product = $parent->product; $scene->branch = $parent->branch; $scene->module = $parent->module; } else { $scene->parent = 0; } } $this->dao->update(TABLE_SCENE)->data($scene)->where('id')->eq($sceneID)->exec(); if(dao::isError()) return false; $this->loadModel('action')->create('scene', $sceneID, 'Opened'); return !dao::isError(); } /** * 获取所有的子场景 id。 * Get all children id. * * @param int $sceneID * @access public * @return array */ public function getAllChildId(int $sceneID): array { if($sceneID == 0) return array(); $scene = $this->dao->findById($sceneID)->from(TABLE_SCENE)->fetch(); if(empty($scene)) return array(); return $this->dao->select('id')->from(TABLE_SCENE) ->where('path')->like($scene->path . '%') ->andWhere('deleted')->eq(0) ->fetchPairs(); } /** * 通过 ID 列表和查询语句获取场景。 * Get scenes by id list and query string. * * @param array $sceneIdList * @param string $query * @access public * @return array */ public function getScenesByList(array $sceneIdList, string $query = ''): array { if(!$sceneIdList) return array(); return $this->dao->select('*')->from(TABLE_SCENE) ->where('deleted')->eq('0') ->andWhere('id')->in($sceneIdList) ->beginIF($query)->andWhere($query)->fi() ->fetchAll('id', false); } /** * 获取包含子场景的场景列表。 * Get scene list include sub scenes. * * @param int|array $productID * @param string $branch * @param int $moduleID * @param string $orderBy * @param object $pager * @access public * @return array */ public function getSceneGroups(int|array $productID, string $branch = '', int $moduleID = 0, string $orderBy = 'id_desc', ?object $pager = null): array { $modules = []; if($moduleID) { $modules = $this->loadModel('tree')->getAllChildId($moduleID); if(!$modules) return []; } $topScenes = $this->dao->select('*')->from(TABLE_SCENE) ->where('deleted')->eq('0') ->andWhere('grade')->eq(1) ->andWhere('product')->in($productID) ->beginIF($branch !== 'all')->andWhere('branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('module')->in($modules)->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); if(!$topScenes) return array(); $subScenes = $this->dao->select('*')->from(TABLE_SCENE) ->where('deleted')->eq('0') ->andWhere('grade')->gt(1) ->andWhere('product')->in($productID) ->beginIF($branch !== 'all')->andWhere('branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('module')->in($modules)->fi() ->orderBy($orderBy) ->fetchAll('id', false); foreach($subScenes as $id => $scene) { $path = trim($scene->path, ','); $root = substr($path, 0, strpos($path, ',')); if(!isset($topScenes[$root])) unset($subScenes[$id]); } return $subScenes + $topScenes; } /** * 获取用场景 ID 分组的用例。 * Get cases by scene id. * * @param int|array $productID * @param string $branch * @param array $modules * @param string $caseType * @param string $orderBy * @access public * @return array */ public function getSceneGroupCases(int|array $productID, string $branch, array $modules, string $caseType, string $orderBy): array { $browseType = $this->session->caseBrowseType && $this->session->caseBrowseType != 'bysearch' ? $this->session->caseBrowseType : 'all'; $stmt = $this->dao->select('t1.*,t3.title as storyTitle')->from(TABLE_CASE)->alias('t1'); if($this->app->tab == 'project') $stmt = $stmt->leftJoin(TABLE_PROJECTCASE)->alias('t2')->on('t1.id=t2.case'); $stmt->leftJoin(TABLE_STORY)->alias('t3')->on('t1.story = t3.id'); $caseList = $stmt->where('t1.deleted')->eq('0') ->andWhere('t1.scene')->ne(0) ->andWhere('t1.product')->in($productID) ->beginIF($this->app->tab == 'project')->andWhere('t2.project')->eq($this->session->project)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() ->beginIF($modules)->andWhere('t1.module')->in($modules)->fi() ->beginIF($browseType == 'wait')->andWhere('t1.status')->eq($browseType)->fi() ->beginIF($this->cookie->onlyAutoCase)->andWhere('t1.auto')->eq('auto')->fi() ->beginIF(!$this->cookie->onlyAutoCase)->andWhere('t1.auto')->ne('unit')->fi() ->beginIF($caseType)->andWhere('t1.type')->eq($caseType)->fi() ->orderBy($orderBy) ->fetchAll('id', false); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'testcase', false); $caseList = $this->loadModel('story')->checkNeedConfirm($caseList); $caseList = $this->appendData($caseList); $cases = array(); foreach($caseList as $case) $cases[$case->scene][$case->id] = $case; return $cases; } /** * 获取场景菜单。 * Get scene menu. * * @param int $productID * @param int $moduleID * @param string $branch * @param int $startScene * @param int $currentScene * @param bool $emptyMenu * @access public * @return array */ public function getSceneMenu(int $productID, int $moduleID = 0, string $branch = 'all', int $startScene = 0, int $currentScene = 0, bool $emptyMenu = false): array { $branches = array($branch => ''); if($branch != 'all') { $product = $this->loadModel('product')->getById($productID); if($product && $product->type != 'normal') { $branchPairs = $this->loadModel('branch')->getPairs($productID, 'all'); $branches = array($branch => $branchPairs[$branch]); } elseif($product && $product->type == 'normal') { $branches = array('0' => ''); } } $treeMenu = array(); foreach($branches as $branchID => $branch) { $scenes = $this->getScenesForMenu($productID, $moduleID, $startScene, (string)$branchID, $currentScene); foreach($scenes as $scene) { $branchName = !empty($product) && $product->type != 'normal' && $scene->branch === BRANCH_MAIN ? $this->lang->branch->main : $branch; $this->buildTreeArray($treeMenu, $scenes, $scene, (empty($branchName)) ? '/' : "/$branchName/"); } } ksort($treeMenu); $topMenu = @array_shift($treeMenu); $topMenu = explode("\n", trim((string)$topMenu)); $lastMenu[] = '/'; foreach($topMenu as $menu) { if(!strpos($menu, '|')) continue; list($label, $sceneID) = explode('|', $menu); $lastMenu[$sceneID] = $label; } /* Attach empty option. */ if($emptyMenu) $lastMenu['null'] = $this->lang->null; return $lastMenu; } /** * 获取场景的名称。 * Get scene name. * * @param array $sceneIDList * @param bool $fullPath * @access public * @return array */ public function getScenesName(array $sceneIDList, bool $fullPath = true): array { if(!$fullPath) return $this->dao->select('id, title')->from(TABLE_SCENE)->where('deleted')->eq('0')->andWhere('id')->in($sceneIDList)->fetchPairs(); $scenes = $this->dao->select('id, title, path')->from(TABLE_SCENE)->where('deleted')->eq('0')->andWhere('id')->in($sceneIDList)->fetchAll('path'); $allScenes = $this->dao->select('id, title')->from(TABLE_SCENE)->where('deleted')->eq('0')->andWhere('id')->in(implode(',', array_keys($scenes)))->fetchPairs(); $scenePairs = array(); foreach($scenes as $scene) { $title = ''; $path = explode(',', trim($scene->path, ',')); foreach($path as $sceneID) $title .= '/' . $allScenes[$sceneID]; $scenePairs[$scene->id] = $title; } return $scenePairs; } /** * 编辑一个场景和它的子场景。 * Update a scene and its children. * * @param object $scene * @access public * @return bool */ public function updateScene(object $scene, object $oldScene): bool { $this->dao->update(TABLE_SCENE)->data($scene) ->autoCheck() ->batchCheck($this->config->testcase->editscene->requiredFields, 'notempty') ->check('title', 'unique', "product = '{$scene->product}' AND id != '{$oldScene->id}'") ->where('id')->eq($oldScene->id) ->checkFlow() ->exec(); if(dao::isError()) return false; if(isset($scene->parent) && $scene->parent != $oldScene->parent) { if($scene->parent) { $parent = $this->getSceneByID($scene->parent); $scene->path = $parent->path . $oldScene->id . ','; $scene->grade = ++$parent->grade; $scene->product = $parent->product; $scene->branch = $parent->branch; $scene->module = $parent->module; } else { $scene->path = ',' . $oldScene->id . ','; $scene->grade = 1; } $this->dao->update(TABLE_SCENE)->data($scene)->where('id')->eq($oldScene->id)->exec(); $this->dao->update(TABLE_SCENE)->set("path = REPLACE(path, '{$oldScene->path}', '{$scene->path}')") ->where('id')->ne($oldScene->id) ->andWhere('path')->like("{$oldScene->path}%") ->exec(); } $changes = common::createChanges($oldScene, $scene); if($changes) { $actionID = $this->loadModel('action')->create('scene', $oldScene->id, 'edited'); $this->action->logHistory($actionID, $changes); } return true; } /** * 获取 xmind 文件内容。 * Get xmind file content. * * @param string $fileName * @access public * @return string */ public function getXmindImport(string $fileName): string { $xmlNode = simplexml_load_file($fileName); $testData = $this->xmlToArray($xmlNode); return json_encode($testData); } /** * 保存 xmind 文件内容。 * Save xmind file content to database. * * @param array $scenes * @param array $testcases * @access public * @return array */ public function saveXmindImport(array $scenes, array $testcases): array { try { $this->dao->begin(); $sceneList = array_combine(array_map(function($scene){return $scene['tmpId'];}, $scenes), array_map(function($scene){return (array)$scene;}, $scenes)); $sceneIDList = array(); foreach($sceneList as $key => &$scene) { $result = $this->testcaseTao->saveScene($scene, $sceneList); if($result['result'] == 'fail') throw new Exception($result['message']); $scene['id'] = $result['sceneID']; $sceneIDList[$key]['id'] = $result['sceneID']; } foreach($testcases as $testcase) { $result = $this->saveTestcase($testcase, $sceneIDList); if($result['result'] == 'fail') throw new Exception($result['message']); } $this->dao->commit(); return array('result' => 'success', 'message' => $this->lang->saveSuccess); } catch (Exception $e) { $this->dao->rollBack(); return array('result' => 'fail', 'message' => $e->getMessage()); } } /** * 导入 xmind 时保存其中的测试用例。 * Save the test cases in xmind when importing it. * * @param object $testcase * @param array $sceneIdList * @access public * @return array */ public function saveTestcase(object $testcase, array $sceneIdList): array { $scene = 0; $nodeID = $testcase->tmpPId; if(isset($sceneIdList[$nodeID]['id'])) $scene = $sceneIdList[$nodeID]['id']; unset($testcase->tmpPId); $testcase->scene = $scene; if(empty($testcase->id)) { $this->create($testcase); } else { $oldCase = $this->getById($testcase->id); $changes = $this->update($testcase, $oldCase); if($changes) { $actionID = $this->loadModel('action')->create('case', $testcase->id, 'edited'); $this->action->logHistory($actionID, $changes); } } if(dao::isError()) return array('result' => 'fail', 'message' => dao::getError(true)); return array('result' => 'success', 'message' => 1, 'testcaseID' => zget($testcase, 'id', 0)); } /** * 把导入 xmind 时 POST 提交的 stepList 属性转换为创建用例和编辑用例表单提交的 steps、expects 和 stepType 属性。 * Convert the stepList attribute submitted by POST when importing xmind into the steps, expects, and stepType attributes submitted by the form for creating and editing use cases. * * @param object $case * @param object $testcase * @access protected * @return object */ protected function processCaseSteps(object $case, object $testcase): object { $case->steps = array(); $case->expects = array(); $case->stepType = array(); if(isset($testcase->stepList)) { $index = 1; $postSteps = $testcase->stepList; $stepList = array_combine(array_map(function($step){return $step['tmpId'];}, $postSteps), array_map(function($step){return (object)$step;}, $postSteps)); foreach($stepList as $step) { if(isset($stepList[$step->tmpPId])) { $parentStep = $stepList[$step->tmpPId]; $parentStep->children = isset($parentStep->children) ? $parentStep->children : array(); $step->index = $parentStep->index . '.' . (count($parentStep->children) + 1); $parentStep->children[] = $step; } else { $step->index = $index; $step->children = array(); $index++; } $case->steps[$step->index] = zget($step, 'desc', ''); $case->expects[$step->index] = zget($step, 'expect', ''); $case->stepType[$step->index] = zget($step, 'type', 'item'); } } return $case; } /** * 导出 xmind 格式的用例时获取用例列表。 * Get case list for export xmind. * * @param int $productID * @param int $moduleID * @param string $branch * @access public * @return array */ public function getCaseListForXmindExport(int $productID, int $moduleID, string $branch = ''): array { $fields = 't1.id AS testcaseID, t1.title AS `name`, t1.pri, t2.id AS productID, t2.`name` AS productName, t3.id AS moduleID, t3.`name` AS moduleName, t4.id AS sceneID, t4.title AS sceneName, t1.precondition'; return $this->dao->select($fields)->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->leftJoin(TABLE_MODULE)->alias('t3')->on('t1.module = t3.id') ->leftJoin(TABLE_SCENE)->alias('t4')->on('t1.scene = t4.id') ->where('t1.deleted')->eq('0') ->andWhere('t1.product')->eq($productID) ->beginIF($moduleID)->andWhere('t1.module')->eq($moduleID)->fi() ->beginIF($branch !== '' && $branch !== 'all')->andWhere('t1.branch')->eq($branch)->fi() ->fetchAll(); } /** * 通过产品和模块获取步骤信息。 * Get step by product and module. * * @param int $productID * @param int $moduleID * @access public * @return array */ public function getStepByProductAndModule(int $productID, int $moduleID): array { $fields = "t1.id as testcaseID," . "t2.id as stepID," . "t2.type," . "t2.parent as parentID," . "t2.`desc`," . "t2.expect"; $stepList = $this->dao->select($fields)->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_CASESTEP)->alias('t2')->on('t1.id = t2.`case` and t1.version = t2.version') ->where('t1.deleted')->eq(0) ->andWhere('t1.product')->eq($productID) ->andWhere('t2.id')->gt('0') ->beginIF($moduleID > 0)->andWhere('t1.module')->eq($moduleID)->fi() ->fetchAll(); return $stepList; } /** * 分组获取用例的步骤信息。 * Get steps grouped by case id. * * @param array $caseID * @param string $status * @access public * @return array */ public function getStepGroupByIdList(array $caseIdList, $status = ''): array { if(!$caseIdList) return array(); return $this->dao->select('t1.*')->from(TABLE_CASESTEP)->alias('t1') ->leftJoin(TABLE_CASE)->alias('t2')->on('t1.case=t2.id') ->where('t2.id')->in($caseIdList) ->andWhere('t1.version=t2.version') ->beginIF($status != 'all')->andWhere('t2.status')->ne('wait')->fi() ->fetchGroup('case', 'id'); } /** * 通过产品和模块获取场景。 * Get scene by product and module. * * @param int $productID * @param int $moduleID * @access public * @return array */ public function getSceneByProductAndModule($productID, $moduleID): array { /* Get scenes by product and module. */ $sceneList = $this->dao->select('id as sceneID, title as sceneName, path, parent as parentID, product as productID, module as moduleID') ->from(TABLE_SCENE) ->where('deleted')->eq(0) ->andWhere('product')->eq($productID) ->beginIF($moduleID > 0)->andWhere('module')->eq($moduleID)->fi() ->fetchAll(); $sceneMaps = array(); $topScenes = array(); foreach($sceneList as $one) { /* If scene parent is zero, append it to topScenes. */ if($one->parentID == 0) $topScenes[] = $one; /* Append it to sceneMaps. */ $sceneMaps[$one->sceneID] = $one; } return array('sceneMaps' => $sceneMaps,'topScenes' => $topScenes); } /** * 存储 mind 配置。 * Save mind config. * * @param array $configList * @access public * @return array */ public function saveMindConfig(string $type, array $configList): array { $this->dao->begin(); $this->dao->delete()->from(TABLE_CONFIG) ->where('owner')->eq($this->app->user->account) ->andWhere('module')->eq('testcase') ->andWhere('section')->eq($type) ->exec(); foreach($configList as $one) { $config = new stdclass(); $config->module = 'testcase'; $config->section = $type; $config->key = $one['key']; $config->value = $one['value']; $config->owner = $this->app->user->account; $this->dao->insert(TABLE_CONFIG)->data($config)->autoCheck()->exec(); if($this->dao->isError()) { $this->dao->rollBack(); return array('result' => 'fail', 'message' => $this->dao->getError(true)); } } $this->dao->commit(); return array("result" => "success", "message" => 1); } /** * 获取 xmind 配置。 * Get xmind config. * * @access public * @return array */ public function getMindConfig(string $type): array { $configItems = $this->dao->select("`key`,value")->from(TABLE_CONFIG) ->where('owner')->eq($this->app->user->account) ->andWhere('module')->eq('testcase') ->andWhere('section')->eq($type) ->fetchAll(); $config = array(); foreach($configItems as $item) $config[$item->key] = $item->value; if(!isset($config['module'])) $config['module'] = 'M'; if(!isset($config['scene'])) $config['scene'] = 'S'; if(!isset($config['case'])) $config['case'] = 'C'; if(!isset($config['precondition'])) $config['precondition'] = 'pre'; if(!isset($config['pri'])) $config['pri'] = 'P'; if(!isset($config['group'])) $config['group'] = 'G'; return $config; } /** * 将 xml 内容转为数组。 * Convert xml to array. * * @param object $xml * @param array $options * @access private * @return array */ private function xmlToArray(object $xml, array $options = array()): array { $defaults = array( 'namespaceRecursive' => false, // Get XML doc namespaces recursively 'removeNamespace' => true, // Remove namespace from resulting keys 'namespaceSeparator' => ':', // Change separator to something other than a colon 'attributePrefix' => '', // Distinguish between attributes and nodes with the same name 'alwaysArray' => array(), // Array of XML tag names which should always become arrays 'autoArray' => true, // Create arrays for tags which appear more than once 'textContent' => 'text', // Key used for the text content of elements 'autoText' => true, // Skip textContent key if node has no attributes or child nodes 'keySearch' => false, // (Optional) search and replace on tag and attribute names 'keyReplace' => false, // (Optional) replace values for above search values ); $options = array_merge($defaults, $options); $namespaces = $xml->getDocNamespaces($options['namespaceRecursive']); $namespaces[''] = null; // Add empty base namespace /* Get attributes from all namespaces. */ $attributesArray = array(); foreach($namespaces as $prefix => $namespace) { if($options['removeNamespace']) $prefix = ''; foreach($xml->attributes($namespace) as $attributeName => $attribute) { // (Optional) replace characters in attribute name if($options['keySearch']) $attributeName = str_replace($options['keySearch'], $options['keyReplace'], $attributeName); $attributeKey = $options['attributePrefix'] . ($prefix ? $prefix . $options['namespaceSeparator'] : '') . $attributeName; $attributesArray[$attributeKey] = (string) $attribute; } } // Get child nodes from all namespaces $tagsArray = $this->getXmlTagsArray($xml, $namespaces, $options); // Get text content of node $textContentArray = array(); $plainText = trim((string) $xml); if($plainText !== '') $textContentArray[$options['textContent']] = $plainText; // Stick it all together $propertiesArray = !$options['autoText'] || $attributesArray || $tagsArray || $plainText === '' ? array_merge($attributesArray, $tagsArray, $textContentArray) : $plainText; // Return node as array return array($xml->getName() => $propertiesArray); } /** * 获取 xml 标签数组。 * Get xml tags array. * * @param object $xml * @param array $namespaces * @param array $options * @access public * @return array */ public function getXmlTagsArray(object $xml, array $namespaces, array $options): array { $tagsArray = array(); foreach($namespaces as $prefix => $namespace) { if($options['removeNamespace']) $prefix = ''; foreach($xml->children($namespace) as $childXml) { // Recurse into child nodes $childArray = $this->xmlToArray($childXml, $options); $childTagName = key($childArray); $childProperties = current($childArray); // Replace characters in tag name if($options['keySearch']) $childTagName = str_replace($options['keySearch'], $options['keyReplace'], $childTagName); // Add namespace prefix, if any if($prefix) $childTagName = $prefix . $options['namespaceSeparator'] . $childTagName; if(!isset($tagsArray[$childTagName])) { // Only entry with this key // Test if tags of this type should always be arrays, no matter the element count $tagsArray[$childTagName] = in_array($childTagName, $options['alwaysArray'], true) || !$options['autoArray'] ? array($childProperties) : $childProperties; } elseif(is_array($tagsArray[$childTagName]) && array_keys($tagsArray[$childTagName]) === range(0, count($tagsArray[$childTagName]) - 1)) { // Key already exists and is integer indexed array $tagsArray[$childTagName][] = $childProperties; } else { // Key exists so convert to integer indexed array with previous value in position 0 $tagsArray[$childTagName] = array($tagsArray[$childTagName], $childProperties); } } } return $tagsArray; } /** * 追加用例执行失败次数。 * Append case fails. * * @param object $case * @param string $from * @param int $taskID * @access public * @return object */ public function appendCaseFails(object $case, string $from, int $taskID): object { $caseFails = $this->dao->select('COUNT(1) AS count')->from(TABLE_TESTRESULT) ->where('caseResult')->eq('fail') ->andwhere('`case`')->eq($case->id) ->beginIF($from == 'testtask')->andwhere('`run`')->eq($taskID)->fi() ->fetch('count'); $case->caseFails = $caseFails; return $case; } /** * 添加步骤。 * Append steps. * * @param array $steps * @param int $count * @access public * @return array */ public function appendSteps(array $steps, int $count = 0): array { if($count == 0) $count = $this->config->testcase->defaultSteps; if(count($steps) < $count) { $step = new stdclass(); $step->step = ''; $step->desc = ''; $step->expect = ''; $step->type = 'step'; for($i = count($steps) + 1; $i <= $count; $i ++) { $data = clone $step; $data->name = (string)$i; $steps[] = $data; } } return $steps; } /** * 获取可以导入的测试用例。 * Get can import cases. * * @param int $productID * @param int $libID * @param string $orderBy * @param object $pager * @param int $queryID * @access public * @return array */ public function getCanImportCases(int $productID, int $libID, string $orderBy = 'id_desc', ?object $pager = null, string $browseType = '', int $queryID = 0): array { $query = ''; if($browseType == 'bysearch') { $queryID = (int)$queryID; if($queryID) { $this->session->set('testsuiteQuery', ' 1 = 1'); $query = $this->loadModel('search')->getQuery($queryID); if($query) { $this->session->set('testsuiteQuery', $query->sql); $this->session->set('testsuiteForm', $query->form); } } else { if($this->session->testsuiteQuery === false) $this->session->set('testsuiteQuery', ' 1 = 1'); } $query = $this->session->testsuiteQuery; $allLib = "`lib` = 'all'"; $query = strpos($query, $allLib) !== false ? str_replace($allLib, '1 = 1', $query) : "{$query} AND `lib` = '{$libID}'"; } $this->loadModel('branch'); $this->loadModel('tree'); $product = $this->loadModel('product')->getById($productID); $branches = $product->type != 'normal' ? array(BRANCH_MAIN => $this->lang->branch->main) + $this->branch->getPairs($productID, 'active') : array(0); $branches = array_keys($branches); $branchModules = array(); foreach($branches as $branch) $branchModules[$branch] = $this->tree->getOptionMenu($productID, 'case', 0, (string)$branch); $caseModuleCount = $this->dao->select('fromCaseID,count(module) AS moduleCount') ->from(TABLE_CASE)->alias('t1') ->leftJoin(TABLE_MODULE)->alias('t2')->on('t1.module=t2.id and t1.product = t2.root') ->where('t1.product')->eq($productID) ->andWhere('t1.lib')->eq($libID) ->andWhere('t1.fromCaseID')->ne('0') ->andWhere('t1.deleted')->eq('0') ->andWhere('((t2.type')->in('story,case')->andWhere('t2.deleted')->eq('0')->markRight(1) ->orWhere('t1.module')->eq('0')->markRight(1) ->groupBy('t1.fromCaseID') ->fetchPairs(); $branchModuleCount = $this->dao->select('branch,count(id) + 1 as moduleCount') ->from(TABLE_MODULE) ->where('root')->eq($productID) ->andWhere('type')->in('story,case') ->andWhere('deleted')->eq('0') ->groupBy('branch') ->fetchPairs(); foreach($branches as $branch) { if(!isset($branchModuleCount[$branch])) $branchModuleCount[$branch] = 0; if(!empty($branch) && isset($branchModuleCount[0])) $branchModuleCount[$branch] += $branchModuleCount[0]; } $maxCount = array_sum($branchModuleCount); $canImport = $canNotImport = array(); $libCases = $this->loadModel('caselib')->getLibCases($libID, 'all'); foreach($libCases as $caseID => $case) { $caseModuleCount = zget($caseModuleCount, $caseID, 0); if(!empty($caseModuleCount) && $caseModuleCount >= $maxCount) { $canNotImport[$caseID] = $caseID; } else { $canImport[$caseID] = $caseID; } } return $this->dao->select('*')->from(TABLE_CASE) ->where('deleted')->eq('0') ->beginIF($browseType != 'bysearch')->andWhere('lib')->eq($libID)->fi() ->beginIF($browseType == 'bysearch')->andWhere($query)->fi() ->beginIF(count($canImport) <= count($canNotImport))->andWhere('id')->in($canImport)->fi() ->beginIF(count($canImport) > count($canNotImport) && !empty($canNotImport))->andWhere('id')->notIn($canNotImport)->fi() ->andWhere('product')->eq(0) ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } /** * 获取已经导入的用例模块。 * Get imported case modules. * * @param int $productID * @param int $libID * @param int|string $branch * @param string $returnType * @access public * @return array */ public function getCanImportedModules(int $productID, int $libID, int|string $branch, string $returnType = 'pairs', array $libCases = array()): array { $importedModules = $this->dao->select('fromCaseID,module')->from(TABLE_CASE) ->where('product')->eq($productID) ->beginIF($branch != 'all')->andWhere('branch')->eq($branch)->fi() ->andWhere('fromCaseID')->ne('0') ->andWhere('deleted')->eq('0') ->fetchGroup('fromCaseID', 'module'); foreach($importedModules as $fromCaseID => $modules) $importedModules[$fromCaseID] = array_combine(array_keys($modules), array_keys($modules)); if(empty($libCases)) $libCases = $this->loadModel('caselib')->getLibCases($libID, 'all'); $modules = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0, (string)$branch); if($returnType == 'items') { $moduleItems = array(); foreach($modules as $moduleID => $moduleName) $moduleItems[$moduleID] = array('text' => $moduleName, 'value' => $moduleID); } $canImportModules = array(); foreach($libCases as $caseID => $case) { $caseModules = !empty($importedModules[$caseID]) ? $importedModules[$caseID] : array(); $canImportModules[$caseID] = $returnType == 'pairs' ? array_diff_key($modules, $caseModules) : array_diff_key($moduleItems, $caseModules); if(!empty($canImportModules[$caseID])) { if($returnType == 'pairs') { $canImportModules[$caseID]['ditto'] = $this->lang->testcase->ditto; } else { $canImportModules[$caseID]['ditto'] = array('text' => $this->lang->testcase->ditto, 'value' => 'ditto'); $canImportModules[$caseID] = array_values($canImportModules[$caseID]); } } if(empty($canImportModules[$caseID])) unset($canImportModules[$caseID]); } return $canImportModules; } /** * Build search form. * * @param int $productID * @param array $products * @param int $queryID * @param string $actionURL * @param int $projectID * @param int $moduleID * @param string $branch * @access public * @return void */ public function buildSearchForm(int $productID, array $products, int $queryID, string $actionURL, int $projectID = 0, int $moduleID = 0, string $branch = 'all'): void { /* 获取产品列表。Get productList. */ if($this->app->tab == 'project' && !$productID) { if($projectID) { $products = $this->loadModel('product')->getProducts($projectID); $productList = array(0 => ''); foreach($products as $product) $productList[$product->id] = $product->name; $productList['all'] = $this->lang->product->allProductsOfProject; } else { $productList = $products; } $this->config->testcase->search['params']['story']['values'] = $this->loadModel('story')->getExecutionStoryPairs($projectID, 0, 'all', $moduleID, 'full', 'active'); } else { $productList = array(0 => ''); $productList['all'] = $this->lang->all; if(isset($products[$productID])) $productList[$productID] = $products[$productID]; if(empty($productID) && empty($products)) { $products = $this->loadModel('product')->getPairs('', 0, '', 'all'); $productList += $products; } $this->config->testcase->search['params']['story']['values'] = $this->loadModel('story')->getProductStoryPairs($productID, $branch, array(), 'active,reviewing', 'id_desc', 0, '', 'story', false); } /* 获取模块列表。*/ /* Get moduleList. */ if($productID) { $modules = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0, $branch); } else { $modules = array(); foreach($products as $id => $name) $modules += $this->loadModel('tree')->getOptionMenu($id, 'case', 0); } $this->config->testcase->search['params']['product']['values'] = $productList; $this->config->testcase->search['params']['module']['values'] = $modules; $this->config->testcase->search['params']['scene']['values'] = $this->getSceneMenu($productID, $moduleID, $branch, 0, 0, true); $this->config->testcase->search['params']['lib']['values'] = $this->loadModel('caselib')->getLibraries(); $product = $this->loadModel('product')->getByID($productID ? $productID : (int)key($products)); if((isset($product->type) && $product->type == 'normal') || $this->app->tab == 'project') { unset($this->config->testcase->search['fields']['branch']); unset($this->config->testcase->search['params']['branch']); } else { $branches = $this->loadModel('branch')->getPairs($productID, '', $projectID); $this->config->testcase->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->testcase->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches + array('all' => $this->lang->branch->all); } if(!$this->config->testcase->needReview) unset($this->config->testcase->search['params']['status']['values']['wait']); $this->config->testcase->search['actionURL'] = $actionURL; $this->config->testcase->search['queryID'] = $queryID; $this->config->testcase->search['module'] = 'testcase'; $this->loadModel('search')->setSearchParams($this->config->testcase->search); } /** * 构建搜索配置。 * Build search config. * * @param int $productID * @param string $branch * @access public * @return array */ public function buildSearchConfig(int $productID, string $branch = 'all'): array { $this->config->testcase->search['params']['story']['values'] = $this->loadModel('story')->getProductStoryPairs($productID, $branch, array(), 'active,reviewing', 'id_desc', 0, '', 'story', false); /* 获取模块列表。*/ /* Get moduleList. */ $modules = $this->loadModel('tree')->getOptionMenu($productID, 'case', 0, $branch); $this->config->testcase->search['params']['module']['values'] = $modules; $this->config->testcase->search['params']['scene']['values'] = $this->getSceneMenu($productID, 0, $branch, 0, 0, true); $this->config->testcase->search['params']['lib']['values'] = $this->loadModel('caselib')->getLibraries(); unset($this->config->testcase->search['fields']['product']); unset($this->config->testcase->search['params']['product']); $product = $this->loadModel('product')->getByID($productID ? $productID : 0); if($productID) { if(isset($product->type) && $product->type == 'normal') { unset($this->config->testcase->search['fields']['branch']); unset($this->config->testcase->search['params']['branch']); } else { $branches = $this->loadModel('branch')->getPairs($productID, '', 0); $this->config->testcase->search['fields']['branch'] = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); $this->config->testcase->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches + array('all' => $this->lang->branch->all); } } if(!$this->config->testcase->needReview) unset($this->config->testcase->search['params']['status']['values']['wait']); $_SESSION['searchParams']['module'] = 'testcase'; $searchConfig = $this->loadModel('search')->processBuildinFields('testcase', $this->config->testcase->search); $searchConfig['params'] = $this->search->setDefaultParams('testcase', $searchConfig['fields'], $searchConfig['params']); return $searchConfig; } /** * 过滤自动测试用例的ID列表。 * Ignore auto testcase id list. * * @param array $caseIdList * @access public * @return array */ public function ignoreAutoCaseIdList(array $caseIdList): array { if(empty($caseIdList)) return array(); return $this->dao->select('id')->from(TABLE_CASE)->where('id')->in($caseIdList)->andWhere('auto')->ne('auto')->fetchPairs('id', 'id'); } }