master 593a0d6ab9cd cached
16 files
57.3 KB
14.6k tokens
25 symbols
1 requests
Download .txt
Repository: adiralashiva8/robotframework-metrics
Branch: master
Commit: 593a0d6ab9cd
Files: 16
Total size: 57.3 KB

Directory structure:
gitextract_e0lyx8s4/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── robotframework_metrics/
│   ├── __init__.py
│   ├── dashboard_stats.py
│   ├── details.py
│   ├── keyword_results.py
│   ├── keyword_times.py
│   ├── robotmetrics.py
│   ├── runner.py
│   ├── suite_results.py
│   ├── templates/
│   │   └── index.html
│   ├── test_results.py
│   └── version.py
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================

# Created by https://www.gitignore.io/api/python
# Edit at https://www.gitignore.io/?templates=python

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

### Python Patch ###
.venv/

# End of https://www.gitignore.io/api/python
*.xml
metrics.html

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Shiva Prasad Adirala

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: MANIFEST.in
================================================
recursive-include robotframework_metrics/templates *
include MANIFEST.in

================================================
FILE: README.md
================================================
<div align="center">
  <h1>Robot Framework Metrics</h1>
  <p>
     Custom HTML report (dashboard view) by parsing robotframework output.xml file
  </p>

<!-- Badges -->
<p>
  <a href="https://github.com/adiralashiva8/robotframework-metrics/graphs/contributors">
    <img src="https://img.shields.io/github/contributors/adiralashiva8/robotframework-metrics" alt="contributors" />
  </a>
  <a href="">
    <img src="https://img.shields.io/github/last-commit/adiralashiva8/robotframework-metrics" alt="last update" />
  </a>
  <a href="https://github.com/adiralashiva8/robotframework-metrics/network/members">
    <img src="https://img.shields.io/github/forks/adiralashiva8/robotframework-metrics" alt="forks" />
  </a>
  <a href="https://github.com/adiralashiva8/robotframework-metrics/stargazers">
    <img src="https://img.shields.io/github/stars/adiralashiva8/robotframework-metrics" alt="stars" />
  </a>
  <a href="https://pypi.org/project/robotframework-metrics/">
    <img src="https://img.shields.io/pypi/dm/robotframework-metrics.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333" alt="downloads" />
  </a>
  <a href="https://github.com/adiralashiva8/robotframework-metrics/issues/">
    <img src="https://img.shields.io/github/issues/adiralashiva8/robotframework-metrics" alt="open issues" />
  </a>
  <a href="https://github.com/adiralashiva8/robotframework-metrics/blob/master/LICENSE">
    <img src="https://img.shields.io/github/license/adiralashiva8/robotframework-metrics.svg" alt="license" />
  </a>
</p>

<h4>
    <a href="https://robotmetrics37.netlify.app/" target="_blank">View Demo</a>
  <span> · </span>
    <a href="https://github.com/adiralashiva8/robotframework-metrics/blob/master/README.md">Documentation</a>
  <span> · </span>
    <a href="https://github.com/adiralashiva8/robotframework-metrics/issues/">Report Bug</a>
  <span> · </span>
    <a href="https://github.com/adiralashiva8/robotframework-metrics/issues/">Request Feature</a>
  </h4>
</div>

<br />

<!-- Table of Contents -->
# 📔 Table of Contents

- [About the Project](#-about-the-project)
  * [Screenshots](#-screenshots)
  * [Tech Stack](#-tech-stack)
  * [Features](#-features)
- [Getting Started](#-getting-started)
  * [Installation](#-installation)
- [Usage](#usage)
  * [Continuous Integration (CI) Setup](#-cisetup)
- [Contact](#-contact)
- [Acknowledgements](#-acknowledgements)

<!-- About the Project -->
## 🌟 About the Project

`Robot Framework Metrics` is a tool designed to generate comprehensive `HTML reports` from Robot Framework's `output.xml` files. These reports provide a __dashboard view__, offering detailed insights into your test executions, including __suite__ statistics, __test case__ results, and __keyword__ performance.

<!-- Screenshots -->
### 📷 Screenshots

![Metrics Report](https://github.com/adiralashiva8/robotframework-metrics/blob/master/metrics.png)

<!-- TechStack -->
### 🛠️ Tech Stack

<details>
  <ul>
    <li><a href="https://www.python.org/">Python</a></li>
    <li><a href="https://robot-framework.readthedocs.io/en/stable/autodoc/robot.result.html">Robotframework results api</a></li>
    <li><a href="https://pandas.pydata.org/docs/getting_started/index.html">Pandas</a></li>
    <li><a href="https://jinja.palletsprojects.com/en/2.10.x/">Jinja2</a></li>
  </ul>
</details>

<!-- Features -->
### 🎯 Features

- *Custom HTML Report:* Create visually appealing and informative dashboard.
- *Detailed Metrics:* Access suite, test case, keyword statistics, status, and elapsed time.
- *Support for RF7:* Fully compatible with Robot Framework 7 (from v3.5.0 onwards).
- *Command-Line Interface:* Easy-to-use CLI for report generation.


<!-- Getting Started -->
## 🧰 Getting Started

<!-- Installation -->
### ⚙️ Installation

You can install `robotframework-metrics` using one of the following methods:


__Method 1__: Latest Development Version  (**Recommended**) (for the latest features and RF7 support)
```
pip install git+https://github.com/adiralashiva8/robotframework-metrics
```

__Method 2__: Using pip
```
pip install robotframework-metrics==3.7.0
```

__Method 3__: From Source (clone the repository and install using setup.py)
```
git clone https://github.com/adiralashiva8/robotframework-metrics.git
cd robotframework-metrics
python setup.py install
```


<!-- Usage -->
## 👀 Usage

After executing your Robot Framework tests, you can generate a metrics report by running:

__Default Configuration__: If `output.xml` is in the current directory
```
robotmetrics
```

__Custom Path__: If `output.xml` is located in a different directory
```
robotmetrics --inputpath ./Result/ --output output1.xml
```

For more options:
```
robotmetrics --help
```

### 🧪 Continuous Integration (CI) Setup

To automate report generation in CI/CD pipelines, add the following steps to your pipeline configuration:

1. Run tests with Robot Framework
2. Generate the metrics report
   ```
   robot test.robot &
   robotmetrics [:options]
   ```
   > & is used to execute multiple command's in .bat file

<!-- Contact -->
## 🤝 Contact

For any questions, suggestions, or feedback, please contact:

- Email: <a href="mailto:adiralashiva8@gmail.com?Subject=Robotframework%20Metrics" target="_blank">`adiralashiva8@gmail.com`</a> 

<!-- Acknowledgments -->
## 💎 Acknowledgements

Special thanks to the following individuals for their guidance, contributions, and feedback:

*Idea, Guidance and Support:*
 - Steve Fisher
 - Goutham Duduka

*Contributors:*
1. [Pekka Klarck](https://www.linkedin.com/in/pekkaklarck/) [Author of robotframework]
2. [Ruud Prijs](https://www.linkedin.com/in/ruudprijs/)
3. [Jesse Zacharias](https://www.linkedin.com/in/jesse-zacharias-7926ba50/)
4. [Bassam Khouri](https://www.linkedin.com/in/bassamkhouri/)
5. [Francesco Spegni](https://www.linkedin.com/in/francesco-spegni-34b39b61/)
6. [Sreelesh Kunnath](https://www.linkedin.com/in/kunnathsree/)

*Feedback:*
1. [Mantri Sri](https://www.linkedin.com/in/mantri-sri-4a0196133/)
2. [Prasad Ozarkar](https://www.linkedin.com/in/prasad-ozarkar-b4a61017/)
3. [Suresh Parimi](https://www.linkedin.com/in/sparimi/)
4. [Amit Lohar](https://github.com/amitlohar)
5. [Robotframework community users](https://groups.google.com/forum/#!forum/robotframework-users)

---

⭐ Star this repository if you find it useful! (it motivates)

---


================================================
FILE: robotframework_metrics/__init__.py
================================================


================================================
FILE: robotframework_metrics/dashboard_stats.py
================================================
import pandas as pd
from datetime import datetime
import numpy as np


class Dashboard:

    def __init__(self):
        pass

    @classmethod
    def get_suite_statistics(self, suite_list):
        suite_data_frame = pd.DataFrame.from_records(suite_list)
        suite_stats = {
            "Total" : (suite_data_frame.Name).count(),
            "Pass"  : (suite_data_frame.Status == 'PASS').sum(),
            "Fail"  : (suite_data_frame.Status == 'FAIL').sum(),
            "Skip"  : (suite_data_frame.Status == 'SKIP').sum(),
            "Time"  : (suite_data_frame.Time).sum(),
            "Min"  : (suite_data_frame.Time).min(),
            "Max"  : (suite_data_frame.Time).max(),
            "Avg"  : (suite_data_frame.Time).mean()
        }
        return suite_stats
    
    @classmethod
    def get_test_statistics(self, test_list):
        test_data_frame = pd.DataFrame.from_records(test_list)
        test_stats = {
            "Total" : (test_data_frame.Status).count(),
            "Pass"  : (test_data_frame.Status == 'PASS').sum(),
            "Fail"  : (test_data_frame.Status == 'FAIL').sum(),
            "Skip"  : (test_data_frame.Status == 'SKIP').sum(),
            "Time"  : (test_data_frame.Time).sum(),
            "Min"  : (test_data_frame.Time).min(),
            "Max"  : (test_data_frame.Time).max(),
            "Avg"  : (test_data_frame.Time).mean()
        }
        return test_stats

    @classmethod
    def get_keyword_statistics(self, kw_list):
        kw_data_frame = pd.DataFrame.from_records(kw_list)
        if not kw_data_frame.empty:
            kw_stats = {
                "Total" : (kw_data_frame.Status).count(),
                "Pass"  : (kw_data_frame.Status == 'PASS').sum(),
                "Fail"  : (kw_data_frame.Status == 'FAIL').sum(),
                "Skip"  : (kw_data_frame.Status == 'SKIP').sum()
            }
        else:
            kw_stats = {
                "Total" : 0,
                "Pass"  : 0,
                "Fail"  : 0,
                "Skip"  : 0,
            }
        return kw_stats

    def suite_error_statistics(self, suite_list):
        suite_data_frame = pd.DataFrame.from_records(suite_list)
        required_data_frame = pd.DataFrame(suite_data_frame, columns = ['Name', 'Total', 'Fail'])
        required_data_frame['percent'] = (required_data_frame['Fail'] / required_data_frame['Total'])*100
        filtered_data_frame = required_data_frame[required_data_frame['Fail'] > 0]
        # print(required_data_frame)
        return filtered_data_frame.sort_values(by = ['Fail'], ascending = [False], ignore_index=True).head(10).reset_index(drop=True)

    def get_execution_info(self, test_list):
        data_frame = pd.DataFrame.from_records(test_list)
        data_frame['start_time'] = pd.to_datetime(data_frame['start_time'])
        data_frame['end_time'] = pd.to_datetime(data_frame['end_time'])
        initial_start_time = data_frame['start_time'].min()
        final_end_time = data_frame['end_time'].max()
        overall_execution_time = final_end_time - initial_start_time
        return [initial_start_time, final_end_time, overall_execution_time]

    def get_test_execution_trends(self, test_list):
        data_frame = pd.DataFrame.from_records(test_list)
        num_bins = 10
        min_time = round(data_frame['Time'].min()/60000, 2)
        max_time = round(data_frame['Time'].max()/60000, 2)
        if max_time == min_time:
            max_time += 0.1
        bins = np.linspace(min_time, max_time, num_bins + 1)
        labels = [f'{round(bins[i], 0)} - {round(bins[i+1], 0)} min' for i in range(len(bins)-1)]
        data_frame['time_group'] = pd.cut(round(data_frame['Time']/60000,2), bins=bins, labels=labels, include_lowest=True, ordered=False)
        result = data_frame.groupby('time_group').size().reset_index(name='test_case_count')
        return result


================================================
FILE: robotframework_metrics/details.py
================================================
from robot.api import ExecutionResult, ResultVisitor
from datetime import timedelta
from robot.result.model import Keyword


class SuiteReportVisitor(ResultVisitor):
    def __init__(self, details_list):
        self.test_report = details_list

    def visit_suite(self, suite):

        self.tests = []
        self.keywords = []
        # Traverse each test in the suite
        for test in suite.tests:

            # Traverse each keyword in the test
            for keyword in test.body:
                if isinstance(keyword, Keyword):
                    _current_keyword = {
                        'keyword_name': keyword.name,
                        'keyword_status': keyword.status,
                        'keyword_start_time': keyword.starttime,
                        'keyword_end_time': keyword.endtime,
                        'keyword_elapsed_time': str(timedelta(milliseconds=keyword.elapsedtime)),
                        'keyword_documentation': keyword.doc,
                        'keyword_message': keyword.message if keyword.message else "",
                    }
                    self.keywords.append(_current_keyword)

            _current_test = {
                'test_name': test.name,
                'test_id': test.id,
                'start_time': test.starttime,
                'end_time': test.endtime,
                'elapsed_time': str(timedelta(milliseconds=test.elapsedtime)),
                'status': test.status,
                'tags': ", ".join(test.tags),
                'documentation': test.doc,
                'message': test.message if test.message else "",
                'keywords': self.keywords
            }
            self.tests.append(_current_test)


        tests_info = {
            'suite_name': suite.longname,
            'suite_id': suite.id,
            'start_time': suite.starttime,
            'end_time': suite.endtime,
            'elapsed_time': str(timedelta(milliseconds=suite.elapsedtime)),
            'status': suite.status,
            'pass_count': suite.statistics.passed,
            'fail_count': suite.statistics.failed,
            'skip_count': suite.statistics.skipped,
            'total': suite.statistics.total,
            'message': suite.message if suite.message else "",
            'tests': self.tests
        }

        self.test_report.append(tests_info)

        # Recursively visit nested suites
        for child_suite in suite.suites:
            child_suite.visit(self)


================================================
FILE: robotframework_metrics/keyword_results.py
================================================
from robot.api import ResultVisitor


class KeywordResults(ResultVisitor):

    def __init__(self, kw_list, ignore_library, ignore_type):
        self.kw_list = kw_list
        self.ignore_library = ignore_library
        self.ignore_type = ignore_type
        
    def start_keyword(self, kw):
        if (kw.libname not in self.ignore_library) and (kw.type not in self.ignore_type):
            kw_json = {
                "Name" : kw.name,
                "Status" : kw.status,
                "Time" : kw.elapsedtime
            }
            self.kw_list.append(kw_json)

================================================
FILE: robotframework_metrics/keyword_times.py
================================================
import pandas as pd

class KeywordTimes():

    def get_keyword_times(self, kw_list):
        keywords_data_frame = pd.DataFrame.from_records(kw_list)
        if not keywords_data_frame.empty:
            kw_times = (keywords_data_frame.groupby("Name").agg(times = ("Time", "count"), time_min = ("Time", "min"),
            time_max = ("Time", "max"), time_mean = ("Time", "mean"), fail_count=("Status", lambda x: (x == "FAIL").sum())).reset_index())
        else:
            kw_times = keywords_data_frame
        return kw_times


================================================
FILE: robotframework_metrics/robotmetrics.py
================================================
import os
import logging
import codecs
from datetime import datetime
from robot.api import ExecutionResult
from jinja2 import Environment, FileSystemLoader, Template
from .suite_results import SuiteResults
from .test_results import TestResults
from .keyword_results import KeywordResults
from .keyword_times import KeywordTimes
from .dashboard_stats import Dashboard
from .details import SuiteReportVisitor

templates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
file_loader = FileSystemLoader(templates_dir)
env = Environment( loader = file_loader )
template = env.get_template('index.html')

IGNORE_LIBRARIES = ["SeleniumLibrary", "BuiltIn",
 "Collections", "DateTime", "Dialogs", "OperatingSystem"
 "Process", "Screenshot", "String", "Telnet", "XML"]

IGNORE_TYPES = ['FOR ITERATION', 'FOR', 'for', 'foritem']

suite_list, test_list, kw_list, kw_times, details_list = [], [], [], [], []

def generate_report(opts):
    logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)

    # Ignores following library keywords in metrics report
    ignore_library = IGNORE_LIBRARIES
    if opts.ignore:
        ignore_library.extend(opts.ignore)

    # Ignores following type keywords in metrics report
    ignore_type = IGNORE_TYPES
    if opts.ignoretype:
        ignore_type.extend(opts.ignoretype)

    # Report to support file location as arguments
    path = os.path.abspath(os.path.expanduser(opts.path))

    # output.xml files
    output_names = []
    # support "*.xml" of output files
    if ( opts.output == "*.xml" ):
        for item in os.listdir(path):
            item = os.path.join(path, item)
            if os.path.isfile(item) and item.endswith('.xml'):
                output_names.append(item)
    else:
        for curr_name in opts.output.split(","):
            curr_path = os.path.join(path, curr_name)
            output_names.append(curr_path)

    log_name = opts.log_name

    # copy the list of output_names onto the one of required_files; the latter may (in the future)
    # contain files that should not be processed as output_names
    required_files = list(output_names)
    missing_files = [filename for filename in required_files if not os.path.exists(filename)]
    if missing_files:
        # We have files missing.
        exit("output.xml file is missing: {}".format(", ".join(missing_files)))

    mt_time = datetime.now().strftime('%Y%m%d-%H%M%S')

    # Output result file location
    if opts.metrics_report_name:
        result_file_name = opts.metrics_report_name
    else:
        result_file_name = 'metrics-' + mt_time + '.html'
    result_file = os.path.join(path, result_file_name)

    logging.info(" Converting .xml to .html file. This may take few minutes...")
    # Read output.xml file
    result = ExecutionResult(*output_names)

    logging.info(" 1 of 4: Capturing suite metrics")
    result.visit(SuiteResults(suite_list))

    logging.info(" 2 of 4: Capturing test metrics")
    result.visit(TestResults(test_list))

    # if opts.showkeyword == "True":
    #     logging.info(" 3 of 4: Capturing keyword metrics")
    #     result.visit(KeywordResults(kw_list, IGNORE_LIBRARIES))
    #     hide_keyword_menu = ""
    # else:
    #     logging.info(" 3 of 4: Ignoring keyword metrics")
    #     result.visit(KeywordResults([], IGNORE_LIBRARIES))
    #     hide_keyword_menu = "hide"

    if opts.showkwtimes == "True":
        logging.info(" 3 of 4: Capturing keyword times metrics")
        result.visit(KeywordResults(kw_list, ignore_library, ignore_type))
        kw_times = KeywordTimes().get_keyword_times(kw_list)
        hide_kw_times_menu = ""
    else:
        kw_times = KeywordTimes().get_keyword_times([])
        hide_kw_times_menu = "hide"

    if opts.showtags == "True":
        hide_tags = ""
    else:
        hide_tags = "hide"

    if opts.showdocs == "True":
        hide_docs = ""
    else:
        hide_docs = "hide"

    logging.info(" 4 of 4: Capturing details")
    result.visit(SuiteReportVisitor(details_list))

    logging.info(" Preparing data for dashboard")
    dashboard_obj = Dashboard()
    suite_stats = dashboard_obj.get_suite_statistics(suite_list)
    test_stats = dashboard_obj.get_test_statistics(test_list)
    kw_stats = dashboard_obj.get_keyword_statistics(kw_list)
    suite_error_stats = dashboard_obj.suite_error_statistics(suite_list)
    execution_stats = dashboard_obj.get_execution_info(test_list)
    test_time_group = dashboard_obj.get_test_execution_trends(test_list)

    logging.info(" Writing results to html file")
    with codecs.open(result_file,'w','utf-8') as fh:
        fh.write(template.render(
            hide_tags = hide_tags,
            hide_docs = hide_docs,
            # hide_keyword_menu = hide_keyword_menu,
            hide_kw_times_menu = hide_kw_times_menu,
            suite_stats = suite_stats,
            log_name = log_name,
            test_stats = test_stats,
            kw_stats = kw_stats,
            suites = suite_list,
            tests = test_list,
            # keywords = kw_list,
            keyword_times = kw_times,
            # error_stats = error_stats,
            suite_error_stats = suite_error_stats,
            suites_list = details_list,
            execution_stats=execution_stats,
            test_time_group=test_time_group,
        ))
    logging.info(" Results file created successfully and can be found at {}".format(result_file))


================================================
FILE: robotframework_metrics/runner.py
================================================
import os
import argparse
from .robotmetrics import generate_report
from .robotmetrics import IGNORE_LIBRARIES
from .robotmetrics import IGNORE_TYPES
from .version import __version__


def parse_options():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    general = parser.add_argument_group("General")
    parser.add_argument(
        '-v', '--version',
        action='store_true',
        dest='version',
        help='Display application version information'
    )

    general.add_argument(
        '--ignorelib',
        dest='ignore',
        default=IGNORE_LIBRARIES,
        nargs="+",
        help="Ignore keywords of specified library in report"
    )

    general.add_argument(
        '--ignoretype',
        dest='ignoretype',
        default=IGNORE_TYPES,
        nargs="+",
        help="Ignore keywords of specified type in report"
    )

    general.add_argument(
        '-I', '--inputpath',
        dest='path',
        default=os.path.curdir,
        help="Path of result files"
    )

    general.add_argument(
        '-M', '--metrics-report-name',
        dest='metrics_report_name',
        help="Output name of the generate metrics report"
    )

    general.add_argument(
        '-O', '--output',
        dest='output',
        default="output.xml",
        help="Name of output.xml"
    )

    # general.add_argument(
    #     '-sk', '--showkeyword',
    #     dest='showkeyword',
    #     default="True",
    #     help="Display keywords in metrics report"
    # )

    general.add_argument(
        '-skt', '--showkwtimes',
        dest='showkwtimes',
        default="True",
        help="Display keyword times in metrics report"
    )

    general.add_argument(
        '-t', '--showtags',
        dest='showtags',
        default="False",
        help="Display test case tags in test metrics"
    )

    general.add_argument(
        '-d', '--showdocs',
        dest='showdocs',
        default="False",
        help="Display test case documentation in test metrics"
    )

    general.add_argument(
        '-L', '--log',
        dest='log_name',
        default='log.html',
        help="Name of log.html"
    )

    args = parser.parse_args()
    return args


def main():
    args = parse_options()

    if args.version:
        print(__version__)
        exit(0)

    generate_report(args)

================================================
FILE: robotframework_metrics/suite_results.py
================================================
from robot.api import ResultVisitor
from robot.utils.markuputils import html_format


class SuiteResults(ResultVisitor):

    def __init__(self, suite_list):
        self.suite_list = suite_list
    
    def start_suite(self, suite):
        if suite.tests:
            try:
                stats = suite.statistics.all
            except:
                stats = suite.statistics
            
            try:
                skipped = stats.skipped
            except:
                skipped = 0

            suite_json = {
                "Name" : suite.longname,
                "Id" : suite.id,
                "Status" : suite.status,
                "Documentation" : html_format(suite.doc),
                "Total" : stats.total,
                "Pass" : stats.passed,
                "Fail" : stats.failed,
                "Skip" : skipped,
                "Time" : suite.elapsedtime,
            }
            self.suite_list.append(suite_json)

================================================
FILE: robotframework_metrics/templates/index.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Robot Metrics</title>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
  <link rel="stylesheet" href="https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css">
  <link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.1.0/css/buttons.dataTables.min.css">
  <style>
    body {
      font-family: -apple-system, sans-serif;
      background-color: #eeeeee;
      display: flex;
    }

    .tablecard {
      background-color: white;
      font-size: 14px;
    }

    .sidebar {
      width: 80px;
      background-color: #343a40;
      color: white;
      min-height: 100vh;
      text-align: center;
      position: fixed;
    }

    .section {
      padding-left: 80px;
    }

    .sidebar .brand {
      font-size: 16px;
      padding: 15px 0;
      color: #fff;
      font-weight: bold;
    }

    .sidebar .nav-link {
      color: #ccc;
      padding: 15px 0;
      cursor: pointer;
    }

    .sidebar .nav-link:hover,
    .sidebar .nav-link.active {
      color: white;
      background-color: #495057;
    }

    .sidebar .icon {
      font-size: 24px;
    }

    .sidebar .count {
      font-size: 12px;
      color: #ccc;
    }

    .content {
      flex-grow: 1;
      padding: 20px;
    }

    .loader {
      position: fixed;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      z-index: 9999;
      background-color: rgb(249, 249, 249);
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .loader i {
      font-size: 60px;
      color: #333;
    }

    .hide {
      display: none;
    }

    tfoot input {
      width: 100%;
      padding: 3px;
      box-sizing: border-box;
    }

    tfoot {
      display: table-header-group;
    }

    .dt-button {
      border: none;
      color: #fff;
      margin: 5px;
      border-radius: 12px;
      cursor: pointer;
      padding-left: 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
    }

    .dt-button.copyButton {
      background-color: seashell;
    }

    .dt-button.csvButton {
      background-color: cornsilk;
    }

    .dt-button.excelButton {
      background-color: lavender;
    }

    .dt-button.printButton {
      background-color: whitesmoke;
    }

    .dt-button.colviButton {
      background-color: gainsboro;
    }

    th,
    td {
      text-align: center;
      max-width: 100px;
    }

    .dt-buttons {
      margin-left: 5px;
    }

    .row {
      padding: 5px;
    }

    .rowcard {
      padding: 10px;
      border-radius: 15px;
      background-color: white;
    }

    .card-header-new {
      font-weight: bold;
      color: gray;
      padding-left: 5px;
    }

    .card-table,
    .suite-table,
    .ecard-table,
    .table {
      width: 100%;
    }

    .suite-table th {
      width: 25%;
      color: #666;
    }

    .ecard-table tr,
    .table tr {
      height: 25px;
      padding-left: 5px;
    }

    .table td,
    .table tr {
      font-style: italic;
      font-size: 14px;
    }

    .ecard-table tr:nth-child(even) {
      background-color: #f2f2f2;
    }

    .card-table tr {
      height: 25px;
    }

    .card-table td {
      width: 50%;
    }

    .card-table tr:first-child {
      font-size: 30px;
    }

    .card-table tr:last-child {
      font-size: 10px;
      color: gray;
    }

    .total {
      color: brown;
    }

    .pass,
    .text-success {
      color: green;
    }

    .fail,
    .text-danger {
      color: red;
    }

    .skip,
    .text-warning {
      color: orange;
    }

    .td_left {
      word-wrap: break-word;
      max-width: 250px;
      white-space: normal;
      text-align: left;
    }

    .suite-list,
    .suite-details {
      padding: 10px;
    }

    .scroll {
      height: 800px;
      border: 1px solid #ccc;
      background-color: #fff;
      overflow-y: auto;
      padding: 10px;
    }

    .suite-card,
    .test-item {
      border: 1px solid #ddd;
      border-radius: 8px;
      margin-bottom: 15px;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
      transition: transform 0.2s ease;
      cursor: pointer;
    }

    .suite-card:hover,
    .test-item:hover {
      transform: translateY(-5px);
    }

    .card-body {
      padding: 15px;
    }

    .suite-card.pass .card-body,
    .test-item.pass .card-body {
      border-left: 5px solid green;
    }

    .suite-card.fail .card-body,
    .test-item.fail .card-body {
      border-left: 5px solid red;
    }

    .suite-card.skip .card-body,
    .test-item.skip .card-body {
      border-left: 5px solid orange;
    }

    .test-details p {
      margin: 0;
      font-size: 0.9rem;
      color: #666;
    }

    .keyword-item {
      padding-left: 20px;
      font-size: 0.85rem;
      color: #d9534f;
    }

    .col-md-4,
    .col-md-8 {
      overflow-y: auto;
    }

    .suite-tracker {
      border: 1px solid #ddd;
      border-radius: 8px;
      background-color: #fff;
      padding: 15px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .suite-card {
      margin-bottom: 10px;
      padding: 15px;
      border-radius: 5px;
      cursor: pointer;
      transition: background-color 0.3s ease;
    }

    .suite-card.pass {
      background-color: #e6f4ea;
      color: #2e7d32;
    }

    .suite-card.fail {
      background-color: #ffebee;
      color: #c62828;
    }

    .suite-card.skip {
      background-color: #fff3e0;
      color: #ef6c00;
    }

    .filter-buttons button {
      padding: 8px 12px;
      margin-right: 5px;
      border: 1px solid #ddd;
      background-color: #f0f0f0;
      border-radius: 4px;
      cursor: pointer;
    }

    .filter-buttons button:hover {
      background-color: #ddd;
    }

    .search-container input {
      width: 100%;
      padding: 10px;
      border-radius: 4px;
      border: 1px solid #ccc;
      margin-bottom: 15px;
    }

    .error-message-box {
      border-left: 5px solid #f5c2c7;
      padding: 10px;
      margin: 10px 0;
      border-radius: 4px;
      color: #842029;
      font-size: 0.85rem;
    }

    .title {
      word-wrap: break-word;
    }
  </style>

</head>

<body>
  <div class="loader">
    <i class="fa fa-spinner fa-spin"></i>
  </div>

  <!-- Sidebar -->
  <div class="sidebar d-flex flex-column">
    <div class="brand" style="color:darkgoldenrod">Metrics</div>
    <a class="nav-link" data-target="dashboard" href="#dashboard" style="color:teal">
      <i class="icon fas fa-tachometer-alt"></i>
      <div class="count">Dashboard</div>
    </a>
    <a class="nav-link" data-target="suite" href="#suite" style="color: skyblue">
      <i class="icon fas fa-folder"></i>
      <div class="count">Suite</div>
    </a>
    <a class="nav-link" data-target="test" href="#test" style="color:green">
      <i class="icon fas fa-vial"></i>
      <div class="count">Test</div>
    </a>
    <a class="nav-link" data-target="kwtimes" href="#kwtimes" style="color:tomato">
      <i class="icon fas fa-stopwatch"></i>
      <div class="count">KW Times</div>
    </a>
    <a class="nav-link" data-target="details" href="#details" style="color:burlywood">
      <i class="icon fas fa-info-circle"></i>
      <div class="count">Details</div>
    </a>
  </div>

  <!-- Content Area -->
  <div class="content">

    <div class="section" id="dashboard">
      <h2 style="color:silver">Dashboard</h2>
      <div class="row">
        <div class="col-md-4">
          <div class="col-md-12 rowcard">
            <span class="card-header-new">Test Status:</span>
            <div class="col-md-12" style="display: flex; align-items: center; justify-content: center;">
              <div id="testPie"></div>
            </div>
            <div class="col-md-12">
              <table class="card-table">
                <tr>
                  <td class="total">{{test_stats['Total']}}</td>
                  <td class="pass">{{test_stats['Pass']}}</td>
                </tr>
                <tr>
                  <td>Total</td>
                  <td>Pass</td>
                </tr>
              </table>
              <table class="card-table">
                <tr>
                  <td class="fail">{{test_stats['Fail']}}</td>
                  <td class="skip">{{test_stats['Skip']}}</td>
                </tr>
                <tr>
                  <td>Fail</td>
                  <td>Skip</td>
                </tr>
              </table>
            </div>
          </div>
        </div>

        <div class="col-md-4">
          <div class="col-md-12">
            <div class="col-md-12 rowcard">
              <span class="card-header-new">Suite Status:</span>
              <div class="col-md-12">
                <div id="suitePie"></div>
              </div>
            </div>
          </div>
          <div class="row"></div>

          <div class="col-md-12">
            <div class="col-md-12 rowcard">
              <span class="card-header-new">Keyword Status:</span>
              <div class="col-md-12">
                <div id="keywordPie"></div>
              </div>
            </div>
          </div>
        </div>

        <div class="col-md-4">
          <div class="col-md-12 rowcard">
            <span class="card-header-new">Execution Duration (m):</span>
            <table class="table">
              <tr>
                <th>Type</th>
                <th>Min</th>
                <th>Max</th>
                <th>Avg</th>
              </tr>
              <tr>
                <td class="suite-list" style="text-align: left;">Suite</td>
                <td class="suite-list">{{(suite_stats['Min']/60000)|round(2)}}</td>
                <td class="suite-list">{{(suite_stats['Max']/60000)|round(2)}}</td>
                <td class="suite-list">{{(suite_stats['Avg']/60000)|round(2)}}</td>
              </tr>
              <tr>
                <td class="suite-list" style="text-align: left;">Test</td>
                <td class="suite-list">{{(test_stats['Min']/60000)|round(2)}}</td>
                <td class="suite-list">{{(test_stats['Max']/60000)|round(2)}}</td>
                <td class="suite-list">{{(test_stats['Avg']/60000)|round(2)}}</td>
              </tr>
            </table>
          </div>
          <div class="row"></div>
          <div class="col-md-12 rowcard">
            <span class="card-header-new">Execution Info:</span>
            <table class="table">
              <tr>
                <th>Action</th>
                <th>Time</th>
              </tr>
              <tr>
                <td class="suite-list" style="text-align: left;">Start Time</td>
                <td>{{ execution_stats[0] }}</td>
              </tr>
              <tr>
                <td class="suite-list" style="text-align: left;">End Time</td>
                <td>{{ execution_stats[1] }}</td>
              </tr>
              <tr>
                <td class="suite-list" style="text-align: left;">Duration</td>
                <td>{{ execution_stats[2] }}</td>
              </tr>
            </table>
          </div>
        </div>


        <div class="row"></div>

      </div>
      <div class="row"></div>
      <div class="row">
        <div class="col-md-6">
          <div class="col-md-12 rowcard">
            <span class="card-header-new">Top 10 Failed Suites:</span>
            <div class="col-md-12">
              <div id="suiteFailureLineID"></div>
            </div>
          </div>
        </div>
        <div class="col-md-6">
          <div class="col-md-12 rowcard">
            <span class="card-header-new">Test Count By Elapsed Time:</span>
            <div class="col-md-12">
              <div id="testExecutionTrends"></div>
            </div>
          </div>
        </div>
        <div class="row"></div>
      </div>
    </div>

    <!-- Suite Data -->
    <div class="section" id="suite">
      <h2 style="color:silver">Suite Metrics</h2>
      <div class="row"></div>
      <table id="sm" class="display tablecard" style="width:100%">
        <thead>
          <tr>
            <th>Name</th>
            <th>Status</th>
            <th>Total</th>
            <th>Pass</th>
            <th>Fail</th>
            <th>Skip</th>
            <th>Time (s)</th>
          </tr>
        </thead>
        <tfoot>
          <tr>
            <th><input type="text" placeholder="Search Name" /></th>
            <th><input type="text" placeholder="Search Status" /></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
          </tr>
        </tfoot>
        <tbody>
          {% for suite in suites %}
          <tr>
            <td class="td_left" data-toggle="tooltip" title="{{ suite['Name'] }}"
              onclick="openInNewTab('{{ log_name }}#{{ suite['Id'] }}','#{{ suite['Id'] }}')"
              style="cursor: pointer; color:blue;">
              {{ suite['Name'] }}</td>
            {% if (suite['Status'] == "PASS") %}
            <td style="color: green">{{ suite['Status'] }}</td>
            {% elif (suite['Status'] == "FAIL") %}
            <td style="color: red">{{ suite['Status'] }}</td>
            {% else %}
            <td style="color: orange">{{ suite['Status'] }}</td>
            {% endif %}
            <td>{{ suite['Total'] }}</td>
            <td style="color: green">{{ suite['Pass'] }}</td>
            <td style="color: red">{{ suite['Fail'] }}</td>
            <td style="color: orange">{{ suite['Skip'] }}</td>
            <td>{{ (suite['Time']/1000)|round(2) }}</td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>

    <!-- Test Case Data -->
    <div class="section" id="test">
      <h2 style="color:silver">Test Metrics</h2>
      <div class="row"></div>
      <table id="tm" class="display tablecard" style="width:100%">
        <thead>
          <tr>
            <th style="width: 20%;">Suite Name</th>
            <th style="width: 20%;">Test Name</th>
            <th style="width: 10%;">Status</th>
            <th style="width: 10%;">Time (s)</th>
            <th style="width: 20%;">Message</th>
            <th style="width: 20%;" class="{{hide_tags}}">Tags</th>
          </tr>
        </thead>
        <tfoot>
          <tr>
            <th><input type="text" placeholder="Search Suite" /></th>
            <th><input type="text" placeholder="Search Name" /></th>
            <th><input type="text" placeholder="Search Status" /></th>
            <th></th>
            <th><input type="text" placeholder="Search Message" /></th>
            <th class="{{hide_tags}}"></th>
          </tr>
        </tfoot>
        <tbody>
          {% for test in tests %}
          <tr>
            <td class="td_left" style="width: 200px;">{{ test['Suite Name'] }}</td>
            <td class="td_left" data-toggle="tooltip" title="{{ test['Test Name'] }}"
              onclick="openInNewTab('{{ log_name }}#{{ test['Test Id'] }}','#{{ test['Test Id'] }}')"
              style="cursor: pointer; color:blue;width: 200px;">
              {{ test['Test Name'] }}</td>
            {% if (test['Status'] == "PASS") %}
            <td style="color:green">{{ test['Status'] }}</td>
            {% elif (test['Status'] == "FAIL") %}
            <td style="color:red">{{ test['Status'] }}</td>
            {% else %}
            <td style="color:orange">{{ test['Status'] }}</td>
            {% endif %}
            <td>{{ (test['Time']/1000)|round(2) }}</td>
            <td class="td_left" style="font-size: 12px">{{
              test['Message'] }}</td>
            <td class="{{hide_tags}} td_left">{{ test['Tags'] }}
            </td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>

    <!-- Keyword Average -->
    <div class="section" id="kwtimes">
      <h2 style="color:silver">KW Times Metrics</h2>
      <div class="row"></div>
      <table id="kmt" class="display tablecard" style="width:100%">
        <thead>
          <tr>
            <th>Keyword Name</th>
            <th>Times</th>
            <th>Fail Count</th>
            <th>Min Duration(s)</th>
            <th>Max Duration(s)</th>
            <th>Average Duration(s)</th>
          </tr>
        </thead>
        <tbody>
          {% if not keyword_times.empty %}
          {% for key, value in keyword_times.iterrows() %}
          <tr>
            <td class="td_left">{{ value['Name'] }}</td>
            <td>{{ value['times'] }}</td>
            <td>{{ value['fail_count'] }}</td>
            <td>{{ (value['time_min']/1000)|round(2) }}</td>
            <td>{{ (value['time_max']/1000)|round(2) }}</td>
            <td>{{ (value['time_mean']/1000)|round(2) }}</td>
          </tr>
          {% endfor %}
          {% endif %}
        </tbody>
      </table>
    </div>

    <div class="section" id="details">
      <h2 style="color:silver">Details</h2>
      <div class="search-container">
        <input type="text" hidden id="suiteSearch" placeholder="Search suites..." onkeyup="filterSuites()">
      </div>

      <!-- Status Filter Buttons -->
      <div class="filter-buttons">
        <button class="total" onclick="filterByStatus('all')">All</button>
        <button class="pass" onclick="filterByStatus('pass')">Pass</button>
        <button class="fail" onclick="filterByStatus('fail')">Fail</button>
        <button class="skip" onclick="filterByStatus('skip')">Skip</button>
      </div>

      <div class="row">
        <div class="col-md-4 scroll suite-tracker">
          <div class="suite-list" id="suiteList">
            {% for suite in suites_list %}
            {% if suite['tests'] %}
            <div class="card suite-card {{ suite['status']|lower }}" id="{{ suite['suite_id'] }}"
              data-status="{{ suite['status']|lower }}" onclick="updateDetails('{{ suite['suite_id'] }}')">
              <div class="title">{{ suite["suite_name"] }}</div>
            </div>
            {% endif %}
            {% endfor %}
          </div>
        </div>

        <div class="col-md-8 scroll">
          <div class="suite-details">
            <div id="suite-description">
              {% for suite in suites_list %}
              {% if suite['tests'] %}
              <div class="suite-content" id="{{ suite['suite_id'] }}_details" style="display: none;">
                <h4 class="title" style="color:gray">{{ suite['suite_name'] }}</h4>
                <table class="suite-table">
                  <thead>
                    <tr>
                      <th>Total</th>
                      <th>Pass</th>
                      <th>Fail</th>
                      <th>Skip</th>
                      <th>Duration</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td><b>{{ suite['total'] }}</b></td>
                      <td class="text-success"><b>{{ suite['pass_count'] }}</b></td>
                      <td class="text-danger"><b>{{ suite['fail_count'] }}</b></td>
                      <td class="text-warning"><b>{{ suite['skip_count'] }}</b></td>
                      <td>{{ suite['elapsed_time'] }}</td>
                    </tr>
                  </tbody>
                </table>
                <div class="row"></div>
                {% for test in suite['tests'] %}
                <div class="card test-item">
                  <div class="card-body">
                    <h5 class="test-name {{ test['status']|lower }} title">{{ test["test_name"] }}</h5>
                    <div class="test-details">
                      <p><b>Duration:</b> {{ test['elapsed_time'] }}</p>
                      <p><b>Tags:</b> {{ test['tags'] }}</p>
                      {% if test['status'] == "FAIL" %}
                      <p><b>Message:</b></p>
                      <div class="error-message-box">
                        <p> {{ test['message'] }}</p>
                      </div>
                      <div>
                        <p><b>Failed Keywords:</b></p>
                        <div class="card-body">
                          {% for keyword in test['keywords'] %}
                          {% if keyword['keyword_status'] == "FAIL" %}
                          <p class="text-danger title">{{ keyword['keyword_name'] }}</p>
                          {% endif %}
                          {% endfor %}
                        </div>
                      </div>
                      {% endif %}
                    </div>
                  </div>
                </div>
                {% endfor %}
              </div>
              {% endif %}
              {% endfor %}
            </div>
          </div>
        </div>
      </div>
    </div>




    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/buttons/2.1.0/js/dataTables.buttons.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
    <script src="https://cdn.datatables.net/buttons/1.5.2/js/buttons.html5.min.js"></script>
    <script src="https://cdn.datatables.net/buttons/1.5.2/js/buttons.print.min.js"></script>
    <script src="https://cdn.datatables.net/buttons/1.6.1/js/buttons.colVis.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>




    <script>

      function showSection(targetId) {
        // Remove active class from all links and sections
        document.querySelectorAll('.sidebar .nav-link').forEach(link => link.classList.remove('active'));
        document.querySelectorAll('.content .section').forEach(section => section.style.display = 'none');

        // Add active class to the clicked link and show the corresponding section
        document.querySelector(`.sidebar .nav-link[href="#${targetId}"]`).classList.add('active');
        document.getElementById(targetId).style.display = 'block';

      }

      // Event listener for hash change and initial load
      window.addEventListener('hashchange', () => {
        const targetId = window.location.hash.replace('#', '') || 'dashboard';
        showSection(targetId);
      });

      // Initial load based on hash or default to 'dashboard'
      document.addEventListener('DOMContentLoaded', () => {
        const targetId = window.location.hash.replace('#', '') || 'dashboard';
        showSection(targetId);
      });

      // Initialize DataTables
      $(document).ready(function () {
        // Initialize DataTables
        $('#sm, #tm, #kmt').DataTable({
          dom: '<"top"lfB>rtip',
          order: [[2, 'desc']],
          buttons: [
            {
              extend: 'copy',
              className: 'copyButton dt-button',
              text: 'Copy',
              filename: function () {
                return "Metrics" + '-' + new Date().toLocaleString();
              },
              exportOptions: {
                columns: ':visible'
              }
            },
            {
              extend: 'csv',
              className: 'csvButton dt-button',
              text: 'CSV',
              filename: function () {
                return "Metrics" + '-' + new Date().toLocaleString();
              },
              exportOptions: {
                columns: ':visible'
              }
            },
            {
              extend: 'excel',
              className: 'excelButton dt-button',
              text: 'Excel',
              filename: function () {
                return "Metrics" + '-' + new Date().toLocaleString();
              },
              exportOptions: {
                columns: ':visible',
              }
            },
            {
              extend: 'print',
              className: 'printButton dt-button',
              text: 'Print',
              filename: function () {
                return "Metrics" + '-' + new Date().toLocaleString();
              },
              exportOptions: {
                columns: ':visible',
                alignment: 'left',
              }
            },
            {
              extend: 'colvis',
              className: 'colviButton dt-button',
              text: 'Hide',
              postfixButtons: ['colvisRestore']
            }
          ],
          lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, "All"]],
          responsive: true,
          initComplete: function () {
            this.api().columns().every(function () {
              var column = this;
              $('input', this.footer()).on('keyup change', function () {
                if (column.search() !== this.value) {
                  column.search(this.value).draw();
                }
              });
            });
          }
        });
      });
    </script>

    <script>
      window.onload = function () {
        barChart('#suitePie', "{{suite_stats['Pass']}}", "{{suite_stats['Fail']}}", "{{suite_stats['Skip']}}");
        pieChart('#testPie', "{{test_stats['Pass']}}", "{{test_stats['Fail']}}", "{{test_stats['Skip']}}");
        barChart('#keywordPie', "{{kw_stats['Pass']}}", "{{kw_stats['Fail']}}", "{{kw_stats['Skip']}}");
      };
    </script>
    <script>
      function pieChart(chartID, passed, failed, skipped) {
        var options = {
          series: [parseInt(passed), parseInt(failed), parseInt(skipped)],
          chart: {
            type: 'donut',
            width: 300,
          },
          labels: ["Pass", "Fail", "Skip"],
          legend: {
            show: false,
          },
          colors: ['#2ecc71', '#fc6666', '#ffa500'],
        };

        var chart = new ApexCharts(document.querySelector(chartID), options);
        chart.render();
      }
    </script>
    <script>
      function barChart(chartID, passed, failed, skipped) {
        var options = {
          series: [{
            name: 'Pass',
            data: [parseInt(passed)]
          },
          {
            name: 'Fail',
            data: [parseInt(failed)]
          },
          {
            name: 'Skip',
            data: [parseInt(skipped)]
          }],
          chart: {
            type: 'bar',
            height: 120,
            stacked: true,
            toolbar: {
              show: false
            },
          },
          plotOptions: {
            bar: {
              horizontal: true,
              dataLabels: {
                total: {
                  enabled: true,
                  offsetX: 0,
                  style: {
                    fontSize: '12px',
                    fontWeight: 100
                  }
                }
              }
            },
          },
          stroke: {
            width: 2,
            colors: ['#fff']
          },
          labels: [""],
          legend: {
            show: false,
          },
          colors: ['#2ecc71', '#fc6666', '#ffa500'],
        };

        var chart = new ApexCharts(document.querySelector(chartID), options);
        chart.render();
      }
    </script>
    <script>
      var passArray = [];
      var failArray = [];
      var catgArray = [];

      {% for key, value in suite_error_stats.iterrows() %}
      {% if (value['Name'] != "") %}
      catgArray.push("{{value['Name']}}");
      passArray.push({{ value['percent']| round(2) }});
      failArray.push({{ value['Fail']}});
      {% endif %}
      {% endfor %}

      var options = {
        series: [{
          name: 'Fail Percentage',
          data: passArray,
          type: "line"
        }, {
          name: 'Fail',
          data: failArray,
          type: "column",
        }],
        chart: {
          type: 'line',
          height: 350,
          // stacked: true,
        },

        plotOptions: {
          bar: {
            dataLabels: {
              position: 'center',
              hideOverflowingLabels: true
            }
          },
        },
        colors: ['#ea9999', '#fc6666'],
        xaxis: {
          categories: catgArray,
          tickPlacement: 'off',
          labels: {
            show: false,
            trim: true
          }
        },
        yaxis: [{
          title: {
            text: 'Suite Fail Percentage',
          },

        }, {
          opposite: true,
          title: {
            text: 'Suite Fail Count'
          }
        }],
        fill: {
          opacity: 0.9
        },
        tooltip: {
          y: {
            formatter: function (val) {
              return val
            }
          },
          x: {
            show: true
          }
        }
      };

      var chart = new ApexCharts(document.querySelector("#suiteFailureLineID"), options);
      chart.render();

    </script>
    <script>
      var time_group = [];
      var test_count = [];

      {% for key, value in test_time_group.iterrows() %}
      time_group.push("{{value['time_group']}}");
      test_count.push({{ value['test_case_count']}});
      {% endfor %}

      var options = {
        series: [{
          name: "Test Count",
          data: test_count
        }],
        chart: {
          type: 'area',
          height: 350
        },
        plotOptions: {
          bar: {
            borderRadius: 4,
            borderRadiusApplication: 'end',
            horizontal: true,
          }
        },
        dataLabels: {
          enabled: false
        },
        xaxis: {
          categories: time_group,
        }
      };

      var chart = new ApexCharts(document.querySelector("#testExecutionTrends"), options);
      chart.render();

    </script>
    <script>
      function openInNewTab(url, element_id) {
        var element_id = element_id;
        var win = window.open(url, '_blank');
        win.focus();
        $('body').scrollTo(element_id);
      }
    </script>
    <script>
      $(window).on('load', function () { $('.loader').fadeOut(); });
    </script>
    <script>
      function updateDetails(selectedSuite) {
        // Hide all issue content divs
        const contents = document.querySelectorAll('.suite-content');
        contents.forEach(content => {
          content.style.display = 'none';
        });

        // Show the selected suite details
        const selectedContent = document.getElementById(selectedSuite + '_details');
        if (selectedContent) {
          selectedContent.style.display = 'block';
        }

      }
    </script>
    <script>
      function filterSuites() {
        const searchInput = document.getElementById('suiteSearch').value.toLowerCase();
        const suites = document.querySelectorAll('.suite-card');
        suites.forEach(suite => {
          const suiteName = suite.innerText.toLowerCase();
          suite.style.display = suiteName.includes(searchInput) && isStatusVisible(suite) ? "" : "none";
        });
      }

      function filterByStatus(status) {
        document.querySelector('.filter-buttons').dataset.status = status;
        filterSuites(); // Reapply filters to update visibility
      }

      function isStatusVisible(suite) {
        const status = document.querySelector('.filter-buttons').dataset.status;
        return status === 'all' || suite.dataset.status === status;
      }
    </script>

</body>

</html>


================================================
FILE: robotframework_metrics/test_results.py
================================================
from robot.api import ResultVisitor
from robot.utils.markuputils import html_format


class TestResults(ResultVisitor):

    def __init__(self, test_list):
        self.test_list = test_list
    
    def visit_test(self, test):
        suite_name = test.parent if test.parent else test.parent.name
        test_json = {
            "Suite Name" : suite_name,
            "Test Name" : test.name,
            "Test Id" : test.id,
            "Status" : test.status,
            "Documentation" : html_format(test.doc),
            "Time" : test.elapsedtime,
            # "Message" : html_format(test.message),
            "Message" : str(test.message).replace("*HTML*",""),
            "Tags" : test.tags,
            'start_time': test.starttime,
            'end_time': test.endtime,
        }
        self.test_list.append(test_json)


================================================
FILE: robotframework_metrics/version.py
================================================
__version__ = "3.6.0"


================================================
FILE: setup.py
================================================
from setuptools import setup, find_packages

setup(
      name='robotframework-metrics',
      version="3.6.0",
      description='Custom report for robot framework',
      long_description='Custom html report generator using robot.result api',
      classifiers=[
          'Framework :: Robot Framework',
          'Programming Language :: Python',
          'Topic :: Software Development :: Testing',
      ],
      keywords='robotframework report',
      author='Shiva Prasad Adirala',
      author_email='adiralashiva8@gmail.com',
      url='https://github.com/adiralashiva8/robotframework-metrics',
      license='MIT',
      
      packages=find_packages(),
      include_package_data= True,
      zip_safe=False,
      
      install_requires=[
          'robotframework',
          'jinja2',
          'pandas',
      ],
      entry_points={
          'console_scripts': [
              'robotmetrics=robotframework_metrics.runner:main',
          ]
      },
      )
Download .txt
gitextract_e0lyx8s4/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── robotframework_metrics/
│   ├── __init__.py
│   ├── dashboard_stats.py
│   ├── details.py
│   ├── keyword_results.py
│   ├── keyword_times.py
│   ├── robotmetrics.py
│   ├── runner.py
│   ├── suite_results.py
│   ├── templates/
│   │   └── index.html
│   ├── test_results.py
│   └── version.py
└── setup.py
Download .txt
SYMBOL INDEX (25 symbols across 8 files)

FILE: robotframework_metrics/dashboard_stats.py
  class Dashboard (line 6) | class Dashboard:
    method __init__ (line 8) | def __init__(self):
    method get_suite_statistics (line 12) | def get_suite_statistics(self, suite_list):
    method get_test_statistics (line 27) | def get_test_statistics(self, test_list):
    method get_keyword_statistics (line 42) | def get_keyword_statistics(self, kw_list):
    method suite_error_statistics (line 60) | def suite_error_statistics(self, suite_list):
    method get_execution_info (line 68) | def get_execution_info(self, test_list):
    method get_test_execution_trends (line 77) | def get_test_execution_trends(self, test_list):

FILE: robotframework_metrics/details.py
  class SuiteReportVisitor (line 6) | class SuiteReportVisitor(ResultVisitor):
    method __init__ (line 7) | def __init__(self, details_list):
    method visit_suite (line 10) | def visit_suite(self, suite):

FILE: robotframework_metrics/keyword_results.py
  class KeywordResults (line 4) | class KeywordResults(ResultVisitor):
    method __init__ (line 6) | def __init__(self, kw_list, ignore_library, ignore_type):
    method start_keyword (line 11) | def start_keyword(self, kw):

FILE: robotframework_metrics/keyword_times.py
  class KeywordTimes (line 3) | class KeywordTimes():
    method get_keyword_times (line 5) | def get_keyword_times(self, kw_list):

FILE: robotframework_metrics/robotmetrics.py
  function generate_report (line 27) | def generate_report(opts):

FILE: robotframework_metrics/runner.py
  function parse_options (line 9) | def parse_options():
  function main (line 95) | def main():

FILE: robotframework_metrics/suite_results.py
  class SuiteResults (line 5) | class SuiteResults(ResultVisitor):
    method __init__ (line 7) | def __init__(self, suite_list):
    method start_suite (line 10) | def start_suite(self, suite):

FILE: robotframework_metrics/test_results.py
  class TestResults (line 5) | class TestResults(ResultVisitor):
    method __init__ (line 7) | def __init__(self, test_list):
    method visit_test (line 10) | def visit_test(self, test):
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (62K chars).
[
  {
    "path": ".gitignore",
    "chars": 1539,
    "preview": "\n# Created by https://www.gitignore.io/api/python\n# Edit at https://www.gitignore.io/?templates=python\n\n### Python ###\n#"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "MIT License\n\nCopyright (c) 2019 Shiva Prasad Adirala\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "MANIFEST.in",
    "chars": 72,
    "preview": "recursive-include robotframework_metrics/templates *\ninclude MANIFEST.in"
  },
  {
    "path": "README.md",
    "chars": 6329,
    "preview": "<div align=\"center\">\n  <h1>Robot Framework Metrics</h1>\n  <p>\n     Custom HTML report (dashboard view) by parsing robotf"
  },
  {
    "path": "robotframework_metrics/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "robotframework_metrics/dashboard_stats.py",
    "chars": 3879,
    "preview": "import pandas as pd\nfrom datetime import datetime\nimport numpy as np\n\n\nclass Dashboard:\n\n    def __init__(self):\n       "
  },
  {
    "path": "robotframework_metrics/details.py",
    "chars": 2481,
    "preview": "from robot.api import ExecutionResult, ResultVisitor\nfrom datetime import timedelta\nfrom robot.result.model import Keywo"
  },
  {
    "path": "robotframework_metrics/keyword_results.py",
    "chars": 575,
    "preview": "from robot.api import ResultVisitor\n\n\nclass KeywordResults(ResultVisitor):\n\n    def __init__(self, kw_list, ignore_libra"
  },
  {
    "path": "robotframework_metrics/keyword_times.py",
    "chars": 532,
    "preview": "import pandas as pd\n\nclass KeywordTimes():\n\n    def get_keyword_times(self, kw_list):\n        keywords_data_frame = pd.D"
  },
  {
    "path": "robotframework_metrics/robotmetrics.py",
    "chars": 5455,
    "preview": "import os\nimport logging\nimport codecs\nfrom datetime import datetime\nfrom robot.api import ExecutionResult\nfrom jinja2 i"
  },
  {
    "path": "robotframework_metrics/runner.py",
    "chars": 2378,
    "preview": "import os\nimport argparse\nfrom .robotmetrics import generate_report\nfrom .robotmetrics import IGNORE_LIBRARIES\nfrom .rob"
  },
  {
    "path": "robotframework_metrics/suite_results.py",
    "chars": 955,
    "preview": "from robot.api import ResultVisitor\nfrom robot.utils.markuputils import html_format\n\n\nclass SuiteResults(ResultVisitor):"
  },
  {
    "path": "robotframework_metrics/templates/index.html",
    "chars": 31588,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, i"
  },
  {
    "path": "robotframework_metrics/test_results.py",
    "chars": 837,
    "preview": "from robot.api import ResultVisitor\nfrom robot.utils.markuputils import html_format\n\n\nclass TestResults(ResultVisitor):\n"
  },
  {
    "path": "robotframework_metrics/version.py",
    "chars": 22,
    "preview": "__version__ = \"3.6.0\"\n"
  },
  {
    "path": "setup.py",
    "chars": 977,
    "preview": "from setuptools import setup, find_packages\n\nsetup(\n      name='robotframework-metrics',\n      version=\"3.6.0\",\n      de"
  }
]

About this extraction

This page contains the full source code of the adiralashiva8/robotframework-metrics GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (57.3 KB), approximately 14.6k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!