From de3db2d3bc06ca85826179d6990b67d485784eca Mon Sep 17 00:00:00 2001 From: Uesli Almeida Date: Thu, 3 Dec 2020 00:29:32 -0300 Subject: [PATCH 1/2] Logic to get the test data and send it through Jira API. --- README.md | 18 ++++ composer.json | 47 +++++----- src/Extension/JiraExtension.php | 158 ++++++++++++++++++++++++++++---- 3 files changed, 182 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e14ec6e..0b8764e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,21 @@ jira-codeception-extension ============================= This package provides an extension for Codeception to create Jira issues automatically when a test fails. + +### Configuration Example + +This extension creates a Jira issue after a test failure. To use this extension a valid Jira configuration is required. + +Configuration 'codeception.yml' example: + + extensions: + enabled: + - Codeception\Extension\JiraExtension + config: + Codeception\Extension\JiraExtension: + host: https://yourdomain.atlassian.net + user: email@mail.com + token: Tg7womaGGFpn9EC16qD3L7T6 + projectKey: JE + issueType: Bug + debugMode: false diff --git a/composer.json b/composer.json index 5f8a008..b99742b 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,24 @@ { - "name": "ueslialmeida/jira-codeception-extension", - "description": "This package provides an extension for Codeception to create Jira issues automatically when a test fails.", - "keywords": [ - "jira", - "issue", - "codeception", - "extension" - ], - "authors": [ - { - "name": "Uesli Almeida", - "email": "almeida.uesli@gmail.com", - "homepage": "https://github.com/ueslialmeida", - "role": "Tester/Developer" - } - ], - "autoload": { - "psr-4": { - "Codeception\\": "src" - } - }, - "require": {} - } - \ No newline at end of file + "name": "ueslialmeida/simple-jira-codeception", + "description": "This package provides an extension for Codeception to create Jira issues automatically when a test fails.", + "keywords": [ + "jira", + "issue", + "codeception", + "extension" + ], + "authors": [ + { + "name": "Uesli Almeida", + "email": "almeida.uesli@gmail.com", + "homepage": "https://github.com/ueslialmeida", + "role": "Tester/Developer" + } + ], + "autoload": { + "psr-4": { + "Codeception\\": "src" + } + }, + "require": {} +} diff --git a/src/Extension/JiraExtension.php b/src/Extension/JiraExtension.php index 142800b..e8fe850 100644 --- a/src/Extension/JiraExtension.php +++ b/src/Extension/JiraExtension.php @@ -4,40 +4,164 @@ use Codeception\Events; use Codeception\Exception\ExtensionException; -use Codeception\Extension; +/** + * @author Uesli Almeida + * + * This extension creates a Jira issue after a test failure. + * + * To use this extension a valid Jira configuration is required. + * + * Configuration 'codeception.yml' example: + * + * extensions: + * enabled: + * - Codeception\Extension\JiraExtension + * config: + * Codeception\Extension\JiraExtension: + * host: https://yourdomain.atlassian.net + * user: email@mail.com + * token: Tg7womaGGFpn9EC16qD3L7T6 + * projectKey: JE + * issueType: Bug + * debugMode: false + */ class JiraExtension extends \Codeception\Extension { + const STRING_LIMIT = 1000; + + /** + * Configuration properties. + */ + protected $host; + protected $user; + protected $token; + protected $projectKey; + protected $issueType; + protected $debug; + + /** + * Jira issue properties. + */ + private $failedStep; + // list events to listen to // Codeception\Events constants used to set the event - public static $events = array( - Events::SUITE_AFTER => 'afterSuite', - Events::TEST_BEFORE => 'beforeTest', - Events::STEP_BEFORE => 'beforeStep', + Events::STEP_AFTER => 'afterStep', Events::TEST_FAIL => 'testFailed', - Events::RESULT_PRINT_AFTER => 'print', ); - // methods that handle events + public function _initialize() + { + if (!isset($this->config['host']) or empty($this->config['host'])) { + throw new ExtensionException($this, "Configuration for 'host' is missing."); + } - public function afterSuite(\Codeception\Event\SuiteEvent $e) { - echo('### THIS IS THE AFTER SUITE EVENT ###'); - } + $this->host = $this->config['host']; + + if (!isset($this->config['user']) or empty($this->config['user'])) { + throw new ExtensionException($this, "Configuration for 'user' is missing."); + } + + $this->user = $this->config['user']; + + if (!isset($this->config['token']) or empty($this->config['token'])) { + throw new ExtensionException($this, "Configuration for 'token' is missing."); + } + + $this->token = $this->config['token']; + + if (!isset($this->config['projectKey']) or empty($this->config['projectKey'])) { + throw new ExtensionException($this, "Configuration for 'project key' is missing."); + } + + $this->projectKey = $this->config['projectKey']; + + if (!isset($this->config['issueType']) or empty($this->config['issueType'])) { + throw new ExtensionException($this, "Configuration for 'issue type' is missing."); + } - public function beforeTest(\Codeception\Event\TestEvent $e) { - echo('@@@ THIS IS THE BEFORE TEST EVENT @@@'); + $this->issueType = $this->config['issueType']; + + if (!isset($this->config['debugMode'])) { + throw new ExtensionException($this, "Configuration for 'debug mode' is missing. Possible values are true or false"); + } + + $this->debug = $this->config['debugMode']; } - public function beforeStep(\Codeception\Event\StepEvent $e) { - echo('%%% THIS IS THE BEFORE STEP EVENT %%%'); + /** + * This method is fired when the event 'step.after' occurs. + * @param \Codeception\Event\StepEvent $e + */ + public function afterStep(\Codeception\Event\StepEvent $e) { + if ($e->getStep()->hasFailed()) { + $this->failedStep = $e->getStep()->toString(self::STRING_LIMIT); + } } + /** + * This method is fired when the event 'test.fail' occurs. + * @param \Codeception\Event\FailEvent $e + */ public function testFailed(\Codeception\Event\FailEvent $e) { - echo('$$$ THIS IS THE TEST FAILED EVENT $$$'); + if (!$this->debug) { + $trace = $e->getFail()->getTraceAsString(); + $message = $e->getFail()->getMessage(); + $fileName = $e->getTest()->getMetadata()->getFilename(); + $testName = $e->getTest()->getMetadata()->getName(); + + $this->createIssue($trace, $message, $fileName, $testName); + } + else { + echo "Debug mode is active, no Jira issue will be created.\n\n"; + } + } + + private function createIssue($trace, $message, $fileName, $testName) { + echo("CREATING JIRA ISSUE\n"); + + $cleanFileName = $this->removeFilePath($fileName); + + $jiraAPI = $this->host . '/rest/api/2/issue'; + + $issue = json_encode([ + 'fields' => [ + 'project' => ['key' => "$this->projectKey"], + 'summary' => $cleanFileName . ' : ' . $testName, + 'description' => " + Test Name: $testName \n + Failed Message: $message \n + Failed Step: I $this->failedStep \n + File Name: $fileName \n + Stack Trace:\n $trace", + 'issuetype' => ['name' => $this->issueType], + 'assignee' => ['name' => 'uesli@zoocha.com'], + ] + ]); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $jiraAPI); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($curl, CURLOPT_HTTPHEADER, [ + 'Authorization: Basic ' . base64_encode($this->user . ':' . $this->token), + 'Content-Type: application/json', + ]); + curl_setopt($curl, CURLOPT_POSTFIELDS, $issue); + + $response = curl_exec($curl); + echo "Jira response: $response \n\n"; } - public function print(\Codeception\Event\PrintResultEvent $e) { - echo('&&& THIS IS THE RESULT PRINT AFTER EVENT &&&'); + private function removeFilePath($filePath) { + $pattern = "/[a-zA-Z\d]+\.[php]+/"; + $path = explode('/', $filePath); + $fileName = implode(preg_grep($pattern, $path)); + + return $fileName; } } \ No newline at end of file From 7f8745247ae09a95984436b564bad47fbd437566 Mon Sep 17 00:00:00 2001 From: Uesli Almeida Date: Fri, 4 Dec 2020 00:02:22 -0300 Subject: [PATCH 2/2] Improved README.md file. Improved readability of the code. --- README.md | 22 ++++++++- src/Extension/JiraExtension.php | 81 ++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 0b8764e..5906720 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,28 @@ jira-codeception-extension ============================= -This package provides an extension for Codeception to create Jira issues automatically when a test fails. +This package provides an extension for Codeception to create issues in Jira automatically when a test fails. + +### How it works? + +When you run a test and it fails the extension will connect with your Jira instance through the Jira API and create an issue containing the data generated by Codeception at the moment of the failure. The number of issues created will vary depending on the number of failed tests, if two tests failed then two separated issues will be created. + +The issue will contain the following data: +- Test Name +- Failure Message +- Failed Step +- File Name +- Stack Trace ### Configuration Example -This extension creates a Jira issue after a test failure. To use this extension a valid Jira configuration is required. +This extension creates a Jira issue after a test failure. To use this extension a valid Jira configuration is required. + +- host: A Jira instance. +- user: A valid user that has permission to create issues in the specified project. +- token: A valid token for the specified user. The API will not accept the user password so a token is required. You can create a token in the user configuration panel, for more information follow the Jira official documentation [here](https://confluence.atlassian.com/cloud/api-tokens-938839638.html). +- projectKey: A valid Jira project key (e.g. TA, ZTE, ETC). +- issueType: Usually the issue is created as a Bug but you can change it for Task or another valid issue type available in your Jira instance. +- debugMode: In case you are creating tests or debugging tests you may not want to create issues (and I don't recommend it) so setting this config to true the extension will not create issues in production set it back to false. Configuration 'codeception.yml' example: diff --git a/src/Extension/JiraExtension.php b/src/Extension/JiraExtension.php index e8fe850..8e9e333 100644 --- a/src/Extension/JiraExtension.php +++ b/src/Extension/JiraExtension.php @@ -41,9 +41,13 @@ class JiraExtension extends \Codeception\Extension protected $debug; /** - * Jira issue properties. + * Issue fields. */ private $failedStep; + private $testName; + private $failureMessage; + private $fileName; + private $stackTrace; // list events to listen to // Codeception\Events constants used to set the event @@ -79,13 +83,13 @@ public function _initialize() $this->projectKey = $this->config['projectKey']; if (!isset($this->config['issueType']) or empty($this->config['issueType'])) { - throw new ExtensionException($this, "Configuration for 'issue type' is missing."); + throw new ExtensionException($this, "Configuration for 'issue type' is missing. Recommended using 'Bug'."); } $this->issueType = $this->config['issueType']; if (!isset($this->config['debugMode'])) { - throw new ExtensionException($this, "Configuration for 'debug mode' is missing. Possible values are true or false"); + throw new ExtensionException($this, "Configuration for 'debug mode' is missing. Possible values are 'true' or 'false'."); } $this->debug = $this->config['debugMode']; @@ -107,54 +111,57 @@ public function afterStep(\Codeception\Event\StepEvent $e) { */ public function testFailed(\Codeception\Event\FailEvent $e) { if (!$this->debug) { - $trace = $e->getFail()->getTraceAsString(); - $message = $e->getFail()->getMessage(); - $fileName = $e->getTest()->getMetadata()->getFilename(); - $testName = $e->getTest()->getMetadata()->getName(); + $this->stackTrace = $e->getFail()->getTraceAsString(); + $this->failureMessage = $e->getFail()->getMessage(); + $this->fileName = $e->getTest()->getMetadata()->getFilename(); + $this->testName = $e->getTest()->getMetadata()->getName(); - $this->createIssue($trace, $message, $fileName, $testName); + $this->createIssue(); } else { - echo "Debug mode is active, no Jira issue will be created.\n\n"; + echo("Debug mode is active, no issue will be created in Jira.\n\n"); } } - private function createIssue($trace, $message, $fileName, $testName) { - echo("CREATING JIRA ISSUE\n"); - - $cleanFileName = $this->removeFilePath($fileName); + private function createIssue() { + echo("Creating issue in Jira...\n"); $jiraAPI = $this->host . '/rest/api/2/issue'; - $issue = json_encode([ + $issue = json_encode($this->getIssueData()); + + $request = curl_init(); + curl_setopt($request, CURLOPT_URL, $jiraAPI); + curl_setopt($request, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($request, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($request, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($request, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($request, CURLOPT_HTTPHEADER, [ + 'Authorization: Basic ' . base64_encode($this->user . ':' . $this->token), + 'Content-Type: application/json', + ]); + curl_setopt($request, CURLOPT_POSTFIELDS, $issue); + + $response = curl_exec($request); + echo("Jira response: $response \n\n"); + } + + private function getIssueData() { + $cleanFileName = $this->removeFilePath($this->fileName); + + return [ 'fields' => [ 'project' => ['key' => "$this->projectKey"], - 'summary' => $cleanFileName . ' : ' . $testName, + 'summary' => $cleanFileName . ' : ' . $this->testName, 'description' => " - Test Name: $testName \n - Failed Message: $message \n + Test Name: $this->testName \n + Failure Message: $this->failureMessage \n Failed Step: I $this->failedStep \n - File Name: $fileName \n - Stack Trace:\n $trace", + File Name: $this->fileName \n + Stack Trace:\n $this->stackTrace", 'issuetype' => ['name' => $this->issueType], - 'assignee' => ['name' => 'uesli@zoocha.com'], ] - ]); - - $curl = curl_init(); - curl_setopt($curl, CURLOPT_URL, $jiraAPI); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($curl, CURLOPT_HTTPHEADER, [ - 'Authorization: Basic ' . base64_encode($this->user . ':' . $this->token), - 'Content-Type: application/json', - ]); - curl_setopt($curl, CURLOPT_POSTFIELDS, $issue); - - $response = curl_exec($curl); - echo "Jira response: $response \n\n"; + ]; } private function removeFilePath($filePath) { @@ -164,4 +171,4 @@ private function removeFilePath($filePath) { return $fileName; } -} \ No newline at end of file +}