Skip to content

Instantly share code, notes, and snippets.

@mrjazz
Last active February 6, 2023 17:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrjazz/6fdaee15b089f982871bec32a8ac4fc6 to your computer and use it in GitHub Desktop.
Save mrjazz/6fdaee15b089f982871bec32a8ac4fc6 to your computer and use it in GitHub Desktop.
<?php
require('testrail.php');
// error_reporting(0);
function traverse($xml, $fn, $level = 0) {
$result = $fn($xml, $level);
if ($result !== false) {
return $result;
}
if ($xml->count() > 0) {
foreach ($xml->children() as $child) {
$result = traverse($child, $fn, $level+2);
if ($result !== false) {
return $result;
}
}
}
return false;
}
function print_as_tree($xml) {
traverse($xml, function ($node, $level) {
if (trim($node['TEXT']) !== '') {
echo str_repeat(' ', $level) . $node['TEXT'] . "\n";
}
return false;
});
}
function find_node($xml, $searchNode) {
return traverse($xml, function ($node, $level) use ($searchNode) {
$curNode = new FlowNode($node['TEXT']);
// var_dump($curNode);
if ($curNode->getTitle() === $searchNode) {
return $node;
}
return false;
});
}
class FlowNode
{
const TYPE_END = 1;
const TYPE_REF = 2;
const TYPE_CASE = 3;
private $nodeTitle;
private $title;
private $type;
private $ref;
public function __construct($nodeTitle) {
$this->nodeTitle = $nodeTitle;
$title = trim(preg_replace('/[\(\[].*$/', '', empty($nodeTitle) ? '' : $nodeTitle));
$this->title = $title;
if (strtoupper($title) === 'END') {
$this->type = self::TYPE_END;
} elseif (stripos($title, 'GOTO:') === 0) {
$this->type = self::TYPE_REF;
$this->ref = trim(substr($title, strlen('GOTO:')));
} else {
$this->type = self::TYPE_CASE;
}
}
/**
* @return mixed
*/
public function getNodeTitle() {
return $this->nodeTitle;
}
public function getType() {
return $this->type;
}
/**
* @return string
*/
public function getTitle() {
return $this->title;
}
/**
* @return bool|string
*/
public function getRef() {
return $this->ref;
}
}
class TestCaseDetector {
private $rootXml; // XML of flo that has to be processed
private $xml; // full XML
private $cyclesAllowed = 1;
const RESULT_OK = 'Ok';
const RESULT_NOT_TERMINATED = 'NotTerminated';
const RESULT_CYCLES = 'Cycles';
private $resultCycles = [];
private $resultOk = [];
private $resultNotTerminated = [];
public function __construct($rootXml, $cyclesAllowed, $xml) {
$this->rootXml = $rootXml;
$this->cyclesAllowed = $cyclesAllowed;
$this->xml = $xml;
}
public function process() {
$this->resultCycles = [];
$this->resultOk = [];
$this->resultNotTerminated = [];
$this->collectTestCases();
return [
self::RESULT_CYCLES => $this->resultCycles,
self::RESULT_OK => $this->resultOk,
self::RESULT_NOT_TERMINATED => $this->resultNotTerminated
];
}
private function collectTestCases($xml = null, $out = []) {
if ($xml === null) {
$xml = $this->rootXml;
}
if (!isset($xml['TEXT'])) {
// ignore node if it has no text
return;
}
$out[] = (string) $xml['TEXT'];
if ($xml->count() > 0) {
foreach ($xml->children() as $child) {
$this->collectTestCases($child, $out);
}
} else {
$this->validateTestCase($out);
}
}
private function validateTestCase($testCases) {
// echo "\n--\n";
// echo implode("\n", $testCases);
// var_dump($testCases);
if (count($testCases) === 0) {
throw new Exception("Testcase is empty");
return;
}
$flowNodes = [];
foreach ($testCases as $nodeTitle) {
$flowNode = new FlowNode($nodeTitle);
if (!isset($flowNodes[$flowNode->getTitle()])) {
$flowNodes[$flowNode->getTitle()] = 1;
} else {
$flowNodes[$flowNode->getTitle()] += 1;
}
if ($flowNodes[$flowNode->getTitle()] > $this->cyclesAllowed) {
echo "WARN: Cycle found\n";
$this->resultCycles[] = $testCases;
return;
}
}
$lastNode = new FlowNode($testCases[count($testCases) - 1]);
if ($lastNode->getType() === FlowNode::TYPE_REF) {
$node = find_node($this->xml, $lastNode->getRef());
if ($node === false) {
echo "ERROR: Unknown reference: {$lastNode->getNodeTitle()}\n";
throw new Exception("Unknown reference: {$lastNode->getNodeTitle()}");
}
$this->collectTestCases($node, $testCases);
} elseif ($lastNode->getType() !== FlowNode::TYPE_END) {
echo "ERROR: Flow was not terminated properly in {$lastNode->getTitle()}\n";
$this->resultNotTerminated[] = $testCases;
} else {
$this->resultOk[] = array_filter($testCases, function($case) {
$flowNode = new FlowNode($case);
return !in_array($flowNode->getType(), [FlowNode::TYPE_END, FlowNode::TYPE_REF]);
});
// $this->resultOk[] = $testCases;
}
}
}
class TestCaseParser
{
/**
* How many recursive cycles should be generated. I.e. test every feature once, two, three times, etc
*/
private $cycles = 1;
private function generateTestCases($xml, $rootNode)
{
$flowNode = find_node($xml, $rootNode);
if ($flowNode === null) {
echo 'Flow node not found';
return;
}
$detector = new TestCaseDetector($flowNode, $this->cycles, $xml);
$result = $detector->process();
$output = [];
foreach ($result[TestCaseDetector::RESULT_OK] as $k => $testCase) {
$caseTitle = "TestCase #" . ($k + 1) . " - {$testCase[0]}";
$output[$caseTitle] = $testCase;
}
return $output;
}
public function parseMindMap($file, $rootNode)
{
// echo "Parse {$file}...\n";
$doc = file_get_contents($file);
$xml = new SimpleXMLElement($doc);
return $this->generateTestCases($xml, $rootNode);
}
}
class TestRail
{
/**
* @var TestRailAPIClient
*/
private $client;
/**
* $var mixed - configuration
*/
private $config = [];
public function __construct($config)
{
$this->config = $config;
$this->client = new TestRailAPIClient($config['TESTRAIL_URL']);
$this->client->set_user($config['TESTRAIL_EMAIL']);
$this->client->set_password($config['TESTRAIL_PASSWORD']);
}
public function getCases()
{
return $this->client->send_get("get_cases/{$this->config['TESTRAIL_PROJECT_ID']}");
}
public function addCase($title, $steps, $section)
{
return $this->client->send_post("add_case/$section", [
'title' => $title,
'custom_steps_separated' => array_slice($steps, 1)
]);
}
public function updateCase($caseId, $title, $steps)
{
return $this->client->send_post("update_case/$caseId", [
'title' => $title,
'custom_steps_separated' => array_slice($steps, 1)
]);
}
public function deleteCase($caseId)
{
return $this->client->send_post("delete_case/{$caseId}", []);
}
public function getSections()
{
return $this->client->send_get("get_sections/{$this->config['TESTRAIL_PROJECT_ID']}");
}
public function getAddSection($name)
{
return $this->client->send_post("add_section/{$this->config['TESTRAIL_PROJECT_ID']}", [
'name' => $name
]);
}
}
function testCaseByTitle($title, $testcases)
{
foreach ($testcases['cases'] as $case) {
if ($case['title'] == $title) {
return $case;
}
}
return false;
}
function sectionByTitle($name, $sections)
{
foreach ($sections['sections'] as $section) {
if ($section['name'] == $name) {
return $section;
}
}
return false;
}
function printCases($cases)
{
foreach ($cases as $title => $steps) {
echo "$title\n - ";
echo implode(
"\n - ",
array_slice($steps, 1) // skip the first step because it's title
);
echo "\n\n";
}
}
if (!is_file('config')) {
echo 'Script can\'t find config file for initialization.';
exit();
}
$config = parse_ini_file('config');
if (isset($_SERVER['argv']) && count($_SERVER['argv']) < 2) {
echo 'No arguments. Valid arguments: print SECTION, publish SECTION';
}
if ($_SERVER['argv'][1] == 'print') {
$parser = new TestCaseParser();
$out = $parser->parseMindMap($config['MINDMAP'], $_SERVER['argv'][2]);
printCases($out);
exit();
}
if ($_SERVER['argv'][1] == 'publish') {
$parser = new TestCaseParser();
$section = $_SERVER['argv'][2];
$out = $parser->parseMindMap($config['MINDMAP'], $section);
$testRail = new TestRail($config);
$testRailSections = $testRail->getSections();
$testRailSection = sectionByTitle($section, $testRailSections);
if (empty($testRailSection)) {
$testRailSection = $testRail->getAddSection($section);
}
$testRailCases = $testRail->getCases();
foreach ($out as $caseName => $testCase) {
$steps = [];
foreach ($testCase as $step) {
$parts = explode('->', $step);
$steps[] = [
'content' => trim($parts[0]),
'expected' => count($parts) > 1 ? trim($parts[1]) : ''
];
}
$oldCase = testCaseByTitle($caseName, $testRailCases);
if (empty($oldCase)) {
echo "Create case $caseName\n";
$newCase = $testRail->addCase($caseName, $steps, $testRailSection['id']);
} else {
echo "Update case $caseName\n";
$testRail->updateCase($oldCase['id'], $caseName, $steps);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment