[
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/python\n# Edit at https://www.gitignore.io/?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n### Python Patch ###\n.venv/\n\n# End of https://www.gitignore.io/api/python\n*.xml\nmetrics.html"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Shiva Prasad Adirala\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-include robotframework_metrics/templates *\ninclude MANIFEST.in"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <h1>Robot Framework Metrics</h1>\n  <p>\n     Custom HTML report (dashboard view) by parsing robotframework output.xml file\n  </p>\n\n<!-- Badges -->\n<p>\n  <a href=\"https://github.com/adiralashiva8/robotframework-metrics/graphs/contributors\">\n    <img src=\"https://img.shields.io/github/contributors/adiralashiva8/robotframework-metrics\" alt=\"contributors\" />\n  </a>\n  <a href=\"\">\n    <img src=\"https://img.shields.io/github/last-commit/adiralashiva8/robotframework-metrics\" alt=\"last update\" />\n  </a>\n  <a href=\"https://github.com/adiralashiva8/robotframework-metrics/network/members\">\n    <img src=\"https://img.shields.io/github/forks/adiralashiva8/robotframework-metrics\" alt=\"forks\" />\n  </a>\n  <a href=\"https://github.com/adiralashiva8/robotframework-metrics/stargazers\">\n    <img src=\"https://img.shields.io/github/stars/adiralashiva8/robotframework-metrics\" alt=\"stars\" />\n  </a>\n  <a href=\"https://pypi.org/project/robotframework-metrics/\">\n    <img src=\"https://img.shields.io/pypi/dm/robotframework-metrics.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333\" alt=\"downloads\" />\n  </a>\n  <a href=\"https://github.com/adiralashiva8/robotframework-metrics/issues/\">\n    <img src=\"https://img.shields.io/github/issues/adiralashiva8/robotframework-metrics\" alt=\"open issues\" />\n  </a>\n  <a href=\"https://github.com/adiralashiva8/robotframework-metrics/blob/master/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/adiralashiva8/robotframework-metrics.svg\" alt=\"license\" />\n  </a>\n</p>\n\n<h4>\n    <a href=\"https://robotmetrics37.netlify.app/\" target=\"_blank\">View Demo</a>\n  <span> · </span>\n    <a href=\"https://github.com/adiralashiva8/robotframework-metrics/blob/master/README.md\">Documentation</a>\n  <span> · </span>\n    <a href=\"https://github.com/adiralashiva8/robotframework-metrics/issues/\">Report Bug</a>\n  <span> · </span>\n    <a href=\"https://github.com/adiralashiva8/robotframework-metrics/issues/\">Request Feature</a>\n  </h4>\n</div>\n\n<br />\n\n<!-- Table of Contents -->\n# 📔 Table of Contents\n\n- [About the Project](#-about-the-project)\n  * [Screenshots](#-screenshots)\n  * [Tech Stack](#-tech-stack)\n  * [Features](#-features)\n- [Getting Started](#-getting-started)\n  * [Installation](#-installation)\n- [Usage](#usage)\n  * [Continuous Integration (CI) Setup](#-cisetup)\n- [Contact](#-contact)\n- [Acknowledgements](#-acknowledgements)\n\n<!-- About the Project -->\n## 🌟 About the Project\n\n`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.\n\n<!-- Screenshots -->\n### 📷 Screenshots\n\n![Metrics Report](https://github.com/adiralashiva8/robotframework-metrics/blob/master/metrics.png)\n\n<!-- TechStack -->\n### 🛠️ Tech Stack\n\n<details>\n  <ul>\n    <li><a href=\"https://www.python.org/\">Python</a></li>\n    <li><a href=\"https://robot-framework.readthedocs.io/en/stable/autodoc/robot.result.html\">Robotframework results api</a></li>\n    <li><a href=\"https://pandas.pydata.org/docs/getting_started/index.html\">Pandas</a></li>\n    <li><a href=\"https://jinja.palletsprojects.com/en/2.10.x/\">Jinja2</a></li>\n  </ul>\n</details>\n\n<!-- Features -->\n### 🎯 Features\n\n- *Custom HTML Report:* Create visually appealing and informative dashboard.\n- *Detailed Metrics:* Access suite, test case, keyword statistics, status, and elapsed time.\n- *Support for RF7:* Fully compatible with Robot Framework 7 (from v3.5.0 onwards).\n- *Command-Line Interface:* Easy-to-use CLI for report generation.\n\n\n<!-- Getting Started -->\n## 🧰 Getting Started\n\n<!-- Installation -->\n### ⚙️ Installation\n\nYou can install `robotframework-metrics` using one of the following methods:\n\n\n__Method 1__: Latest Development Version  (**Recommended**) (for the latest features and RF7 support)\n```\npip install git+https://github.com/adiralashiva8/robotframework-metrics\n```\n\n__Method 2__: Using pip\n```\npip install robotframework-metrics==3.7.0\n```\n\n__Method 3__: From Source (clone the repository and install using setup.py)\n```\ngit clone https://github.com/adiralashiva8/robotframework-metrics.git\ncd robotframework-metrics\npython setup.py install\n```\n\n\n<!-- Usage -->\n## 👀 Usage\n\nAfter executing your Robot Framework tests, you can generate a metrics report by running:\n\n__Default Configuration__: If `output.xml` is in the current directory\n```\nrobotmetrics\n```\n\n__Custom Path__: If `output.xml` is located in a different directory\n```\nrobotmetrics --inputpath ./Result/ --output output1.xml\n```\n\nFor more options:\n```\nrobotmetrics --help\n```\n\n### 🧪 Continuous Integration (CI) Setup\n\nTo automate report generation in CI/CD pipelines, add the following steps to your pipeline configuration:\n\n1. Run tests with Robot Framework\n2. Generate the metrics report\n   ```\n   robot test.robot &\n   robotmetrics [:options]\n   ```\n   > & is used to execute multiple command's in .bat file\n\n<!-- Contact -->\n## 🤝 Contact\n\nFor any questions, suggestions, or feedback, please contact:\n\n- Email: <a href=\"mailto:adiralashiva8@gmail.com?Subject=Robotframework%20Metrics\" target=\"_blank\">`adiralashiva8@gmail.com`</a> \n\n<!-- Acknowledgments -->\n## 💎 Acknowledgements\n\nSpecial thanks to the following individuals for their guidance, contributions, and feedback:\n\n*Idea, Guidance and Support:*\n - Steve Fisher\n - Goutham Duduka\n\n*Contributors:*\n1. [Pekka Klarck](https://www.linkedin.com/in/pekkaklarck/) [Author of robotframework]\n2. [Ruud Prijs](https://www.linkedin.com/in/ruudprijs/)\n3. [Jesse Zacharias](https://www.linkedin.com/in/jesse-zacharias-7926ba50/)\n4. [Bassam Khouri](https://www.linkedin.com/in/bassamkhouri/)\n5. [Francesco Spegni](https://www.linkedin.com/in/francesco-spegni-34b39b61/)\n6. [Sreelesh Kunnath](https://www.linkedin.com/in/kunnathsree/)\n\n*Feedback:*\n1. [Mantri Sri](https://www.linkedin.com/in/mantri-sri-4a0196133/)\n2. [Prasad Ozarkar](https://www.linkedin.com/in/prasad-ozarkar-b4a61017/)\n3. [Suresh Parimi](https://www.linkedin.com/in/sparimi/)\n4. [Amit Lohar](https://github.com/amitlohar)\n5. [Robotframework community users](https://groups.google.com/forum/#!forum/robotframework-users)\n\n---\n\n⭐ Star this repository if you find it useful! (it motivates)\n\n---\n"
  },
  {
    "path": "robotframework_metrics/__init__.py",
    "content": ""
  },
  {
    "path": "robotframework_metrics/dashboard_stats.py",
    "content": "import pandas as pd\nfrom datetime import datetime\nimport numpy as np\n\n\nclass Dashboard:\n\n    def __init__(self):\n        pass\n\n    @classmethod\n    def get_suite_statistics(self, suite_list):\n        suite_data_frame = pd.DataFrame.from_records(suite_list)\n        suite_stats = {\n            \"Total\" : (suite_data_frame.Name).count(),\n            \"Pass\"  : (suite_data_frame.Status == 'PASS').sum(),\n            \"Fail\"  : (suite_data_frame.Status == 'FAIL').sum(),\n            \"Skip\"  : (suite_data_frame.Status == 'SKIP').sum(),\n            \"Time\"  : (suite_data_frame.Time).sum(),\n            \"Min\"  : (suite_data_frame.Time).min(),\n            \"Max\"  : (suite_data_frame.Time).max(),\n            \"Avg\"  : (suite_data_frame.Time).mean()\n        }\n        return suite_stats\n    \n    @classmethod\n    def get_test_statistics(self, test_list):\n        test_data_frame = pd.DataFrame.from_records(test_list)\n        test_stats = {\n            \"Total\" : (test_data_frame.Status).count(),\n            \"Pass\"  : (test_data_frame.Status == 'PASS').sum(),\n            \"Fail\"  : (test_data_frame.Status == 'FAIL').sum(),\n            \"Skip\"  : (test_data_frame.Status == 'SKIP').sum(),\n            \"Time\"  : (test_data_frame.Time).sum(),\n            \"Min\"  : (test_data_frame.Time).min(),\n            \"Max\"  : (test_data_frame.Time).max(),\n            \"Avg\"  : (test_data_frame.Time).mean()\n        }\n        return test_stats\n\n    @classmethod\n    def get_keyword_statistics(self, kw_list):\n        kw_data_frame = pd.DataFrame.from_records(kw_list)\n        if not kw_data_frame.empty:\n            kw_stats = {\n                \"Total\" : (kw_data_frame.Status).count(),\n                \"Pass\"  : (kw_data_frame.Status == 'PASS').sum(),\n                \"Fail\"  : (kw_data_frame.Status == 'FAIL').sum(),\n                \"Skip\"  : (kw_data_frame.Status == 'SKIP').sum()\n            }\n        else:\n            kw_stats = {\n                \"Total\" : 0,\n                \"Pass\"  : 0,\n                \"Fail\"  : 0,\n                \"Skip\"  : 0,\n            }\n        return kw_stats\n\n    def suite_error_statistics(self, suite_list):\n        suite_data_frame = pd.DataFrame.from_records(suite_list)\n        required_data_frame = pd.DataFrame(suite_data_frame, columns = ['Name', 'Total', 'Fail'])\n        required_data_frame['percent'] = (required_data_frame['Fail'] / required_data_frame['Total'])*100\n        filtered_data_frame = required_data_frame[required_data_frame['Fail'] > 0]\n        # print(required_data_frame)\n        return filtered_data_frame.sort_values(by = ['Fail'], ascending = [False], ignore_index=True).head(10).reset_index(drop=True)\n\n    def get_execution_info(self, test_list):\n        data_frame = pd.DataFrame.from_records(test_list)\n        data_frame['start_time'] = pd.to_datetime(data_frame['start_time'])\n        data_frame['end_time'] = pd.to_datetime(data_frame['end_time'])\n        initial_start_time = data_frame['start_time'].min()\n        final_end_time = data_frame['end_time'].max()\n        overall_execution_time = final_end_time - initial_start_time\n        return [initial_start_time, final_end_time, overall_execution_time]\n\n    def get_test_execution_trends(self, test_list):\n        data_frame = pd.DataFrame.from_records(test_list)\n        num_bins = 10\n        min_time = round(data_frame['Time'].min()/60000, 2)\n        max_time = round(data_frame['Time'].max()/60000, 2)\n        if max_time == min_time:\n            max_time += 0.1\n        bins = np.linspace(min_time, max_time, num_bins + 1)\n        labels = [f'{round(bins[i], 0)} - {round(bins[i+1], 0)} min' for i in range(len(bins)-1)]\n        data_frame['time_group'] = pd.cut(round(data_frame['Time']/60000,2), bins=bins, labels=labels, include_lowest=True, ordered=False)\n        result = data_frame.groupby('time_group').size().reset_index(name='test_case_count')\n        return result\n"
  },
  {
    "path": "robotframework_metrics/details.py",
    "content": "from robot.api import ExecutionResult, ResultVisitor\nfrom datetime import timedelta\nfrom robot.result.model import Keyword\n\n\nclass SuiteReportVisitor(ResultVisitor):\n    def __init__(self, details_list):\n        self.test_report = details_list\n\n    def visit_suite(self, suite):\n\n        self.tests = []\n        self.keywords = []\n        # Traverse each test in the suite\n        for test in suite.tests:\n\n            # Traverse each keyword in the test\n            for keyword in test.body:\n                if isinstance(keyword, Keyword):\n                    _current_keyword = {\n                        'keyword_name': keyword.name,\n                        'keyword_status': keyword.status,\n                        'keyword_start_time': keyword.starttime,\n                        'keyword_end_time': keyword.endtime,\n                        'keyword_elapsed_time': str(timedelta(milliseconds=keyword.elapsedtime)),\n                        'keyword_documentation': keyword.doc,\n                        'keyword_message': keyword.message if keyword.message else \"\",\n                    }\n                    self.keywords.append(_current_keyword)\n\n            _current_test = {\n                'test_name': test.name,\n                'test_id': test.id,\n                'start_time': test.starttime,\n                'end_time': test.endtime,\n                'elapsed_time': str(timedelta(milliseconds=test.elapsedtime)),\n                'status': test.status,\n                'tags': \", \".join(test.tags),\n                'documentation': test.doc,\n                'message': test.message if test.message else \"\",\n                'keywords': self.keywords\n            }\n            self.tests.append(_current_test)\n\n\n        tests_info = {\n            'suite_name': suite.longname,\n            'suite_id': suite.id,\n            'start_time': suite.starttime,\n            'end_time': suite.endtime,\n            'elapsed_time': str(timedelta(milliseconds=suite.elapsedtime)),\n            'status': suite.status,\n            'pass_count': suite.statistics.passed,\n            'fail_count': suite.statistics.failed,\n            'skip_count': suite.statistics.skipped,\n            'total': suite.statistics.total,\n            'message': suite.message if suite.message else \"\",\n            'tests': self.tests\n        }\n\n        self.test_report.append(tests_info)\n\n        # Recursively visit nested suites\n        for child_suite in suite.suites:\n            child_suite.visit(self)\n"
  },
  {
    "path": "robotframework_metrics/keyword_results.py",
    "content": "from robot.api import ResultVisitor\n\n\nclass KeywordResults(ResultVisitor):\n\n    def __init__(self, kw_list, ignore_library, ignore_type):\n        self.kw_list = kw_list\n        self.ignore_library = ignore_library\n        self.ignore_type = ignore_type\n        \n    def start_keyword(self, kw):\n        if (kw.libname not in self.ignore_library) and (kw.type not in self.ignore_type):\n            kw_json = {\n                \"Name\" : kw.name,\n                \"Status\" : kw.status,\n                \"Time\" : kw.elapsedtime\n            }\n            self.kw_list.append(kw_json)"
  },
  {
    "path": "robotframework_metrics/keyword_times.py",
    "content": "import pandas as pd\n\nclass KeywordTimes():\n\n    def get_keyword_times(self, kw_list):\n        keywords_data_frame = pd.DataFrame.from_records(kw_list)\n        if not keywords_data_frame.empty:\n            kw_times = (keywords_data_frame.groupby(\"Name\").agg(times = (\"Time\", \"count\"), time_min = (\"Time\", \"min\"),\n            time_max = (\"Time\", \"max\"), time_mean = (\"Time\", \"mean\"), fail_count=(\"Status\", lambda x: (x == \"FAIL\").sum())).reset_index())\n        else:\n            kw_times = keywords_data_frame\n        return kw_times\n"
  },
  {
    "path": "robotframework_metrics/robotmetrics.py",
    "content": "import os\nimport logging\nimport codecs\nfrom datetime import datetime\nfrom robot.api import ExecutionResult\nfrom jinja2 import Environment, FileSystemLoader, Template\nfrom .suite_results import SuiteResults\nfrom .test_results import TestResults\nfrom .keyword_results import KeywordResults\nfrom .keyword_times import KeywordTimes\nfrom .dashboard_stats import Dashboard\nfrom .details import SuiteReportVisitor\n\ntemplates_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')\nfile_loader = FileSystemLoader(templates_dir)\nenv = Environment( loader = file_loader )\ntemplate = env.get_template('index.html')\n\nIGNORE_LIBRARIES = [\"SeleniumLibrary\", \"BuiltIn\",\n \"Collections\", \"DateTime\", \"Dialogs\", \"OperatingSystem\"\n \"Process\", \"Screenshot\", \"String\", \"Telnet\", \"XML\"]\n\nIGNORE_TYPES = ['FOR ITERATION', 'FOR', 'for', 'foritem']\n\nsuite_list, test_list, kw_list, kw_times, details_list = [], [], [], [], []\n\ndef generate_report(opts):\n    logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)\n\n    # Ignores following library keywords in metrics report\n    ignore_library = IGNORE_LIBRARIES\n    if opts.ignore:\n        ignore_library.extend(opts.ignore)\n\n    # Ignores following type keywords in metrics report\n    ignore_type = IGNORE_TYPES\n    if opts.ignoretype:\n        ignore_type.extend(opts.ignoretype)\n\n    # Report to support file location as arguments\n    path = os.path.abspath(os.path.expanduser(opts.path))\n\n    # output.xml files\n    output_names = []\n    # support \"*.xml\" of output files\n    if ( opts.output == \"*.xml\" ):\n        for item in os.listdir(path):\n            item = os.path.join(path, item)\n            if os.path.isfile(item) and item.endswith('.xml'):\n                output_names.append(item)\n    else:\n        for curr_name in opts.output.split(\",\"):\n            curr_path = os.path.join(path, curr_name)\n            output_names.append(curr_path)\n\n    log_name = opts.log_name\n\n    # copy the list of output_names onto the one of required_files; the latter may (in the future)\n    # contain files that should not be processed as output_names\n    required_files = list(output_names)\n    missing_files = [filename for filename in required_files if not os.path.exists(filename)]\n    if missing_files:\n        # We have files missing.\n        exit(\"output.xml file is missing: {}\".format(\", \".join(missing_files)))\n\n    mt_time = datetime.now().strftime('%Y%m%d-%H%M%S')\n\n    # Output result file location\n    if opts.metrics_report_name:\n        result_file_name = opts.metrics_report_name\n    else:\n        result_file_name = 'metrics-' + mt_time + '.html'\n    result_file = os.path.join(path, result_file_name)\n\n    logging.info(\" Converting .xml to .html file. This may take few minutes...\")\n    # Read output.xml file\n    result = ExecutionResult(*output_names)\n\n    logging.info(\" 1 of 4: Capturing suite metrics\")\n    result.visit(SuiteResults(suite_list))\n\n    logging.info(\" 2 of 4: Capturing test metrics\")\n    result.visit(TestResults(test_list))\n\n    # if opts.showkeyword == \"True\":\n    #     logging.info(\" 3 of 4: Capturing keyword metrics\")\n    #     result.visit(KeywordResults(kw_list, IGNORE_LIBRARIES))\n    #     hide_keyword_menu = \"\"\n    # else:\n    #     logging.info(\" 3 of 4: Ignoring keyword metrics\")\n    #     result.visit(KeywordResults([], IGNORE_LIBRARIES))\n    #     hide_keyword_menu = \"hide\"\n\n    if opts.showkwtimes == \"True\":\n        logging.info(\" 3 of 4: Capturing keyword times metrics\")\n        result.visit(KeywordResults(kw_list, ignore_library, ignore_type))\n        kw_times = KeywordTimes().get_keyword_times(kw_list)\n        hide_kw_times_menu = \"\"\n    else:\n        kw_times = KeywordTimes().get_keyword_times([])\n        hide_kw_times_menu = \"hide\"\n\n    if opts.showtags == \"True\":\n        hide_tags = \"\"\n    else:\n        hide_tags = \"hide\"\n\n    if opts.showdocs == \"True\":\n        hide_docs = \"\"\n    else:\n        hide_docs = \"hide\"\n\n    logging.info(\" 4 of 4: Capturing details\")\n    result.visit(SuiteReportVisitor(details_list))\n\n    logging.info(\" Preparing data for dashboard\")\n    dashboard_obj = Dashboard()\n    suite_stats = dashboard_obj.get_suite_statistics(suite_list)\n    test_stats = dashboard_obj.get_test_statistics(test_list)\n    kw_stats = dashboard_obj.get_keyword_statistics(kw_list)\n    suite_error_stats = dashboard_obj.suite_error_statistics(suite_list)\n    execution_stats = dashboard_obj.get_execution_info(test_list)\n    test_time_group = dashboard_obj.get_test_execution_trends(test_list)\n\n    logging.info(\" Writing results to html file\")\n    with codecs.open(result_file,'w','utf-8') as fh:\n        fh.write(template.render(\n            hide_tags = hide_tags,\n            hide_docs = hide_docs,\n            # hide_keyword_menu = hide_keyword_menu,\n            hide_kw_times_menu = hide_kw_times_menu,\n            suite_stats = suite_stats,\n            log_name = log_name,\n            test_stats = test_stats,\n            kw_stats = kw_stats,\n            suites = suite_list,\n            tests = test_list,\n            # keywords = kw_list,\n            keyword_times = kw_times,\n            # error_stats = error_stats,\n            suite_error_stats = suite_error_stats,\n            suites_list = details_list,\n            execution_stats=execution_stats,\n            test_time_group=test_time_group,\n        ))\n    logging.info(\" Results file created successfully and can be found at {}\".format(result_file))\n"
  },
  {
    "path": "robotframework_metrics/runner.py",
    "content": "import os\nimport argparse\nfrom .robotmetrics import generate_report\nfrom .robotmetrics import IGNORE_LIBRARIES\nfrom .robotmetrics import IGNORE_TYPES\nfrom .version import __version__\n\n\ndef parse_options():\n    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n\n    general = parser.add_argument_group(\"General\")\n    parser.add_argument(\n        '-v', '--version',\n        action='store_true',\n        dest='version',\n        help='Display application version information'\n    )\n\n    general.add_argument(\n        '--ignorelib',\n        dest='ignore',\n        default=IGNORE_LIBRARIES,\n        nargs=\"+\",\n        help=\"Ignore keywords of specified library in report\"\n    )\n\n    general.add_argument(\n        '--ignoretype',\n        dest='ignoretype',\n        default=IGNORE_TYPES,\n        nargs=\"+\",\n        help=\"Ignore keywords of specified type in report\"\n    )\n\n    general.add_argument(\n        '-I', '--inputpath',\n        dest='path',\n        default=os.path.curdir,\n        help=\"Path of result files\"\n    )\n\n    general.add_argument(\n        '-M', '--metrics-report-name',\n        dest='metrics_report_name',\n        help=\"Output name of the generate metrics report\"\n    )\n\n    general.add_argument(\n        '-O', '--output',\n        dest='output',\n        default=\"output.xml\",\n        help=\"Name of output.xml\"\n    )\n\n    # general.add_argument(\n    #     '-sk', '--showkeyword',\n    #     dest='showkeyword',\n    #     default=\"True\",\n    #     help=\"Display keywords in metrics report\"\n    # )\n\n    general.add_argument(\n        '-skt', '--showkwtimes',\n        dest='showkwtimes',\n        default=\"True\",\n        help=\"Display keyword times in metrics report\"\n    )\n\n    general.add_argument(\n        '-t', '--showtags',\n        dest='showtags',\n        default=\"False\",\n        help=\"Display test case tags in test metrics\"\n    )\n\n    general.add_argument(\n        '-d', '--showdocs',\n        dest='showdocs',\n        default=\"False\",\n        help=\"Display test case documentation in test metrics\"\n    )\n\n    general.add_argument(\n        '-L', '--log',\n        dest='log_name',\n        default='log.html',\n        help=\"Name of log.html\"\n    )\n\n    args = parser.parse_args()\n    return args\n\n\ndef main():\n    args = parse_options()\n\n    if args.version:\n        print(__version__)\n        exit(0)\n\n    generate_report(args)"
  },
  {
    "path": "robotframework_metrics/suite_results.py",
    "content": "from robot.api import ResultVisitor\nfrom robot.utils.markuputils import html_format\n\n\nclass SuiteResults(ResultVisitor):\n\n    def __init__(self, suite_list):\n        self.suite_list = suite_list\n    \n    def start_suite(self, suite):\n        if suite.tests:\n            try:\n                stats = suite.statistics.all\n            except:\n                stats = suite.statistics\n            \n            try:\n                skipped = stats.skipped\n            except:\n                skipped = 0\n\n            suite_json = {\n                \"Name\" : suite.longname,\n                \"Id\" : suite.id,\n                \"Status\" : suite.status,\n                \"Documentation\" : html_format(suite.doc),\n                \"Total\" : stats.total,\n                \"Pass\" : stats.passed,\n                \"Fail\" : stats.failed,\n                \"Skip\" : skipped,\n                \"Time\" : suite.elapsedtime,\n            }\n            self.suite_list.append(suite_json)"
  },
  {
    "path": "robotframework_metrics/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Robot Metrics</title>\n  <link href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css\" rel=\"stylesheet\">\n  <link href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css\" rel=\"stylesheet\">\n  <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/1.11.5/css/jquery.dataTables.min.css\">\n  <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/buttons/2.1.0/css/buttons.dataTables.min.css\">\n  <style>\n    body {\n      font-family: -apple-system, sans-serif;\n      background-color: #eeeeee;\n      display: flex;\n    }\n\n    .tablecard {\n      background-color: white;\n      font-size: 14px;\n    }\n\n    .sidebar {\n      width: 80px;\n      background-color: #343a40;\n      color: white;\n      min-height: 100vh;\n      text-align: center;\n      position: fixed;\n    }\n\n    .section {\n      padding-left: 80px;\n    }\n\n    .sidebar .brand {\n      font-size: 16px;\n      padding: 15px 0;\n      color: #fff;\n      font-weight: bold;\n    }\n\n    .sidebar .nav-link {\n      color: #ccc;\n      padding: 15px 0;\n      cursor: pointer;\n    }\n\n    .sidebar .nav-link:hover,\n    .sidebar .nav-link.active {\n      color: white;\n      background-color: #495057;\n    }\n\n    .sidebar .icon {\n      font-size: 24px;\n    }\n\n    .sidebar .count {\n      font-size: 12px;\n      color: #ccc;\n    }\n\n    .content {\n      flex-grow: 1;\n      padding: 20px;\n    }\n\n    .loader {\n      position: fixed;\n      left: 0;\n      top: 0;\n      width: 100%;\n      height: 100%;\n      z-index: 9999;\n      background-color: rgb(249, 249, 249);\n      display: flex;\n      justify-content: center;\n      align-items: center;\n    }\n\n    .loader i {\n      font-size: 60px;\n      color: #333;\n    }\n\n    .hide {\n      display: none;\n    }\n\n    tfoot input {\n      width: 100%;\n      padding: 3px;\n      box-sizing: border-box;\n    }\n\n    tfoot {\n      display: table-header-group;\n    }\n\n    .dt-button {\n      border: none;\n      color: #fff;\n      margin: 5px;\n      border-radius: 12px;\n      cursor: pointer;\n      padding-left: 20px;\n      text-align: center;\n      text-decoration: none;\n      display: inline-block;\n    }\n\n    .dt-button.copyButton {\n      background-color: seashell;\n    }\n\n    .dt-button.csvButton {\n      background-color: cornsilk;\n    }\n\n    .dt-button.excelButton {\n      background-color: lavender;\n    }\n\n    .dt-button.printButton {\n      background-color: whitesmoke;\n    }\n\n    .dt-button.colviButton {\n      background-color: gainsboro;\n    }\n\n    th,\n    td {\n      text-align: center;\n      max-width: 100px;\n    }\n\n    .dt-buttons {\n      margin-left: 5px;\n    }\n\n    .row {\n      padding: 5px;\n    }\n\n    .rowcard {\n      padding: 10px;\n      border-radius: 15px;\n      background-color: white;\n    }\n\n    .card-header-new {\n      font-weight: bold;\n      color: gray;\n      padding-left: 5px;\n    }\n\n    .card-table,\n    .suite-table,\n    .ecard-table,\n    .table {\n      width: 100%;\n    }\n\n    .suite-table th {\n      width: 25%;\n      color: #666;\n    }\n\n    .ecard-table tr,\n    .table tr {\n      height: 25px;\n      padding-left: 5px;\n    }\n\n    .table td,\n    .table tr {\n      font-style: italic;\n      font-size: 14px;\n    }\n\n    .ecard-table tr:nth-child(even) {\n      background-color: #f2f2f2;\n    }\n\n    .card-table tr {\n      height: 25px;\n    }\n\n    .card-table td {\n      width: 50%;\n    }\n\n    .card-table tr:first-child {\n      font-size: 30px;\n    }\n\n    .card-table tr:last-child {\n      font-size: 10px;\n      color: gray;\n    }\n\n    .total {\n      color: brown;\n    }\n\n    .pass,\n    .text-success {\n      color: green;\n    }\n\n    .fail,\n    .text-danger {\n      color: red;\n    }\n\n    .skip,\n    .text-warning {\n      color: orange;\n    }\n\n    .td_left {\n      word-wrap: break-word;\n      max-width: 250px;\n      white-space: normal;\n      text-align: left;\n    }\n\n    .suite-list,\n    .suite-details {\n      padding: 10px;\n    }\n\n    .scroll {\n      height: 800px;\n      border: 1px solid #ccc;\n      background-color: #fff;\n      overflow-y: auto;\n      padding: 10px;\n    }\n\n    .suite-card,\n    .test-item {\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      margin-bottom: 15px;\n      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n      transition: transform 0.2s ease;\n      cursor: pointer;\n    }\n\n    .suite-card:hover,\n    .test-item:hover {\n      transform: translateY(-5px);\n    }\n\n    .card-body {\n      padding: 15px;\n    }\n\n    .suite-card.pass .card-body,\n    .test-item.pass .card-body {\n      border-left: 5px solid green;\n    }\n\n    .suite-card.fail .card-body,\n    .test-item.fail .card-body {\n      border-left: 5px solid red;\n    }\n\n    .suite-card.skip .card-body,\n    .test-item.skip .card-body {\n      border-left: 5px solid orange;\n    }\n\n    .test-details p {\n      margin: 0;\n      font-size: 0.9rem;\n      color: #666;\n    }\n\n    .keyword-item {\n      padding-left: 20px;\n      font-size: 0.85rem;\n      color: #d9534f;\n    }\n\n    .col-md-4,\n    .col-md-8 {\n      overflow-y: auto;\n    }\n\n    .suite-tracker {\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      background-color: #fff;\n      padding: 15px;\n      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n    }\n\n    .suite-card {\n      margin-bottom: 10px;\n      padding: 15px;\n      border-radius: 5px;\n      cursor: pointer;\n      transition: background-color 0.3s ease;\n    }\n\n    .suite-card.pass {\n      background-color: #e6f4ea;\n      color: #2e7d32;\n    }\n\n    .suite-card.fail {\n      background-color: #ffebee;\n      color: #c62828;\n    }\n\n    .suite-card.skip {\n      background-color: #fff3e0;\n      color: #ef6c00;\n    }\n\n    .filter-buttons button {\n      padding: 8px 12px;\n      margin-right: 5px;\n      border: 1px solid #ddd;\n      background-color: #f0f0f0;\n      border-radius: 4px;\n      cursor: pointer;\n    }\n\n    .filter-buttons button:hover {\n      background-color: #ddd;\n    }\n\n    .search-container input {\n      width: 100%;\n      padding: 10px;\n      border-radius: 4px;\n      border: 1px solid #ccc;\n      margin-bottom: 15px;\n    }\n\n    .error-message-box {\n      border-left: 5px solid #f5c2c7;\n      padding: 10px;\n      margin: 10px 0;\n      border-radius: 4px;\n      color: #842029;\n      font-size: 0.85rem;\n    }\n\n    .title {\n      word-wrap: break-word;\n    }\n  </style>\n\n</head>\n\n<body>\n  <div class=\"loader\">\n    <i class=\"fa fa-spinner fa-spin\"></i>\n  </div>\n\n  <!-- Sidebar -->\n  <div class=\"sidebar d-flex flex-column\">\n    <div class=\"brand\" style=\"color:darkgoldenrod\">Metrics</div>\n    <a class=\"nav-link\" data-target=\"dashboard\" href=\"#dashboard\" style=\"color:teal\">\n      <i class=\"icon fas fa-tachometer-alt\"></i>\n      <div class=\"count\">Dashboard</div>\n    </a>\n    <a class=\"nav-link\" data-target=\"suite\" href=\"#suite\" style=\"color: skyblue\">\n      <i class=\"icon fas fa-folder\"></i>\n      <div class=\"count\">Suite</div>\n    </a>\n    <a class=\"nav-link\" data-target=\"test\" href=\"#test\" style=\"color:green\">\n      <i class=\"icon fas fa-vial\"></i>\n      <div class=\"count\">Test</div>\n    </a>\n    <a class=\"nav-link\" data-target=\"kwtimes\" href=\"#kwtimes\" style=\"color:tomato\">\n      <i class=\"icon fas fa-stopwatch\"></i>\n      <div class=\"count\">KW Times</div>\n    </a>\n    <a class=\"nav-link\" data-target=\"details\" href=\"#details\" style=\"color:burlywood\">\n      <i class=\"icon fas fa-info-circle\"></i>\n      <div class=\"count\">Details</div>\n    </a>\n  </div>\n\n  <!-- Content Area -->\n  <div class=\"content\">\n\n    <div class=\"section\" id=\"dashboard\">\n      <h2 style=\"color:silver\">Dashboard</h2>\n      <div class=\"row\">\n        <div class=\"col-md-4\">\n          <div class=\"col-md-12 rowcard\">\n            <span class=\"card-header-new\">Test Status:</span>\n            <div class=\"col-md-12\" style=\"display: flex; align-items: center; justify-content: center;\">\n              <div id=\"testPie\"></div>\n            </div>\n            <div class=\"col-md-12\">\n              <table class=\"card-table\">\n                <tr>\n                  <td class=\"total\">{{test_stats['Total']}}</td>\n                  <td class=\"pass\">{{test_stats['Pass']}}</td>\n                </tr>\n                <tr>\n                  <td>Total</td>\n                  <td>Pass</td>\n                </tr>\n              </table>\n              <table class=\"card-table\">\n                <tr>\n                  <td class=\"fail\">{{test_stats['Fail']}}</td>\n                  <td class=\"skip\">{{test_stats['Skip']}}</td>\n                </tr>\n                <tr>\n                  <td>Fail</td>\n                  <td>Skip</td>\n                </tr>\n              </table>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"col-md-4\">\n          <div class=\"col-md-12\">\n            <div class=\"col-md-12 rowcard\">\n              <span class=\"card-header-new\">Suite Status:</span>\n              <div class=\"col-md-12\">\n                <div id=\"suitePie\"></div>\n              </div>\n            </div>\n          </div>\n          <div class=\"row\"></div>\n\n          <div class=\"col-md-12\">\n            <div class=\"col-md-12 rowcard\">\n              <span class=\"card-header-new\">Keyword Status:</span>\n              <div class=\"col-md-12\">\n                <div id=\"keywordPie\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"col-md-4\">\n          <div class=\"col-md-12 rowcard\">\n            <span class=\"card-header-new\">Execution Duration (m):</span>\n            <table class=\"table\">\n              <tr>\n                <th>Type</th>\n                <th>Min</th>\n                <th>Max</th>\n                <th>Avg</th>\n              </tr>\n              <tr>\n                <td class=\"suite-list\" style=\"text-align: left;\">Suite</td>\n                <td class=\"suite-list\">{{(suite_stats['Min']/60000)|round(2)}}</td>\n                <td class=\"suite-list\">{{(suite_stats['Max']/60000)|round(2)}}</td>\n                <td class=\"suite-list\">{{(suite_stats['Avg']/60000)|round(2)}}</td>\n              </tr>\n              <tr>\n                <td class=\"suite-list\" style=\"text-align: left;\">Test</td>\n                <td class=\"suite-list\">{{(test_stats['Min']/60000)|round(2)}}</td>\n                <td class=\"suite-list\">{{(test_stats['Max']/60000)|round(2)}}</td>\n                <td class=\"suite-list\">{{(test_stats['Avg']/60000)|round(2)}}</td>\n              </tr>\n            </table>\n          </div>\n          <div class=\"row\"></div>\n          <div class=\"col-md-12 rowcard\">\n            <span class=\"card-header-new\">Execution Info:</span>\n            <table class=\"table\">\n              <tr>\n                <th>Action</th>\n                <th>Time</th>\n              </tr>\n              <tr>\n                <td class=\"suite-list\" style=\"text-align: left;\">Start Time</td>\n                <td>{{ execution_stats[0] }}</td>\n              </tr>\n              <tr>\n                <td class=\"suite-list\" style=\"text-align: left;\">End Time</td>\n                <td>{{ execution_stats[1] }}</td>\n              </tr>\n              <tr>\n                <td class=\"suite-list\" style=\"text-align: left;\">Duration</td>\n                <td>{{ execution_stats[2] }}</td>\n              </tr>\n            </table>\n          </div>\n        </div>\n\n\n        <div class=\"row\"></div>\n\n      </div>\n      <div class=\"row\"></div>\n      <div class=\"row\">\n        <div class=\"col-md-6\">\n          <div class=\"col-md-12 rowcard\">\n            <span class=\"card-header-new\">Top 10 Failed Suites:</span>\n            <div class=\"col-md-12\">\n              <div id=\"suiteFailureLineID\"></div>\n            </div>\n          </div>\n        </div>\n        <div class=\"col-md-6\">\n          <div class=\"col-md-12 rowcard\">\n            <span class=\"card-header-new\">Test Count By Elapsed Time:</span>\n            <div class=\"col-md-12\">\n              <div id=\"testExecutionTrends\"></div>\n            </div>\n          </div>\n        </div>\n        <div class=\"row\"></div>\n      </div>\n    </div>\n\n    <!-- Suite Data -->\n    <div class=\"section\" id=\"suite\">\n      <h2 style=\"color:silver\">Suite Metrics</h2>\n      <div class=\"row\"></div>\n      <table id=\"sm\" class=\"display tablecard\" style=\"width:100%\">\n        <thead>\n          <tr>\n            <th>Name</th>\n            <th>Status</th>\n            <th>Total</th>\n            <th>Pass</th>\n            <th>Fail</th>\n            <th>Skip</th>\n            <th>Time (s)</th>\n          </tr>\n        </thead>\n        <tfoot>\n          <tr>\n            <th><input type=\"text\" placeholder=\"Search Name\" /></th>\n            <th><input type=\"text\" placeholder=\"Search Status\" /></th>\n            <th></th>\n            <th></th>\n            <th></th>\n            <th></th>\n            <th></th>\n          </tr>\n        </tfoot>\n        <tbody>\n          {% for suite in suites %}\n          <tr>\n            <td class=\"td_left\" data-toggle=\"tooltip\" title=\"{{ suite['Name'] }}\"\n              onclick=\"openInNewTab('{{ log_name }}#{{ suite['Id'] }}','#{{ suite['Id'] }}')\"\n              style=\"cursor: pointer; color:blue;\">\n              {{ suite['Name'] }}</td>\n            {% if (suite['Status'] == \"PASS\") %}\n            <td style=\"color: green\">{{ suite['Status'] }}</td>\n            {% elif (suite['Status'] == \"FAIL\") %}\n            <td style=\"color: red\">{{ suite['Status'] }}</td>\n            {% else %}\n            <td style=\"color: orange\">{{ suite['Status'] }}</td>\n            {% endif %}\n            <td>{{ suite['Total'] }}</td>\n            <td style=\"color: green\">{{ suite['Pass'] }}</td>\n            <td style=\"color: red\">{{ suite['Fail'] }}</td>\n            <td style=\"color: orange\">{{ suite['Skip'] }}</td>\n            <td>{{ (suite['Time']/1000)|round(2) }}</td>\n          </tr>\n          {% endfor %}\n        </tbody>\n      </table>\n    </div>\n\n    <!-- Test Case Data -->\n    <div class=\"section\" id=\"test\">\n      <h2 style=\"color:silver\">Test Metrics</h2>\n      <div class=\"row\"></div>\n      <table id=\"tm\" class=\"display tablecard\" style=\"width:100%\">\n        <thead>\n          <tr>\n            <th style=\"width: 20%;\">Suite Name</th>\n            <th style=\"width: 20%;\">Test Name</th>\n            <th style=\"width: 10%;\">Status</th>\n            <th style=\"width: 10%;\">Time (s)</th>\n            <th style=\"width: 20%;\">Message</th>\n            <th style=\"width: 20%;\" class=\"{{hide_tags}}\">Tags</th>\n          </tr>\n        </thead>\n        <tfoot>\n          <tr>\n            <th><input type=\"text\" placeholder=\"Search Suite\" /></th>\n            <th><input type=\"text\" placeholder=\"Search Name\" /></th>\n            <th><input type=\"text\" placeholder=\"Search Status\" /></th>\n            <th></th>\n            <th><input type=\"text\" placeholder=\"Search Message\" /></th>\n            <th class=\"{{hide_tags}}\"></th>\n          </tr>\n        </tfoot>\n        <tbody>\n          {% for test in tests %}\n          <tr>\n            <td class=\"td_left\" style=\"width: 200px;\">{{ test['Suite Name'] }}</td>\n            <td class=\"td_left\" data-toggle=\"tooltip\" title=\"{{ test['Test Name'] }}\"\n              onclick=\"openInNewTab('{{ log_name }}#{{ test['Test Id'] }}','#{{ test['Test Id'] }}')\"\n              style=\"cursor: pointer; color:blue;width: 200px;\">\n              {{ test['Test Name'] }}</td>\n            {% if (test['Status'] == \"PASS\") %}\n            <td style=\"color:green\">{{ test['Status'] }}</td>\n            {% elif (test['Status'] == \"FAIL\") %}\n            <td style=\"color:red\">{{ test['Status'] }}</td>\n            {% else %}\n            <td style=\"color:orange\">{{ test['Status'] }}</td>\n            {% endif %}\n            <td>{{ (test['Time']/1000)|round(2) }}</td>\n            <td class=\"td_left\" style=\"font-size: 12px\">{{\n              test['Message'] }}</td>\n            <td class=\"{{hide_tags}} td_left\">{{ test['Tags'] }}\n            </td>\n          </tr>\n          {% endfor %}\n        </tbody>\n      </table>\n    </div>\n\n    <!-- Keyword Average -->\n    <div class=\"section\" id=\"kwtimes\">\n      <h2 style=\"color:silver\">KW Times Metrics</h2>\n      <div class=\"row\"></div>\n      <table id=\"kmt\" class=\"display tablecard\" style=\"width:100%\">\n        <thead>\n          <tr>\n            <th>Keyword Name</th>\n            <th>Times</th>\n            <th>Fail Count</th>\n            <th>Min Duration(s)</th>\n            <th>Max Duration(s)</th>\n            <th>Average Duration(s)</th>\n          </tr>\n        </thead>\n        <tbody>\n          {% if not keyword_times.empty %}\n          {% for key, value in keyword_times.iterrows() %}\n          <tr>\n            <td class=\"td_left\">{{ value['Name'] }}</td>\n            <td>{{ value['times'] }}</td>\n            <td>{{ value['fail_count'] }}</td>\n            <td>{{ (value['time_min']/1000)|round(2) }}</td>\n            <td>{{ (value['time_max']/1000)|round(2) }}</td>\n            <td>{{ (value['time_mean']/1000)|round(2) }}</td>\n          </tr>\n          {% endfor %}\n          {% endif %}\n        </tbody>\n      </table>\n    </div>\n\n    <div class=\"section\" id=\"details\">\n      <h2 style=\"color:silver\">Details</h2>\n      <div class=\"search-container\">\n        <input type=\"text\" hidden id=\"suiteSearch\" placeholder=\"Search suites...\" onkeyup=\"filterSuites()\">\n      </div>\n\n      <!-- Status Filter Buttons -->\n      <div class=\"filter-buttons\">\n        <button class=\"total\" onclick=\"filterByStatus('all')\">All</button>\n        <button class=\"pass\" onclick=\"filterByStatus('pass')\">Pass</button>\n        <button class=\"fail\" onclick=\"filterByStatus('fail')\">Fail</button>\n        <button class=\"skip\" onclick=\"filterByStatus('skip')\">Skip</button>\n      </div>\n\n      <div class=\"row\">\n        <div class=\"col-md-4 scroll suite-tracker\">\n          <div class=\"suite-list\" id=\"suiteList\">\n            {% for suite in suites_list %}\n            {% if suite['tests'] %}\n            <div class=\"card suite-card {{ suite['status']|lower }}\" id=\"{{ suite['suite_id'] }}\"\n              data-status=\"{{ suite['status']|lower }}\" onclick=\"updateDetails('{{ suite['suite_id'] }}')\">\n              <div class=\"title\">{{ suite[\"suite_name\"] }}</div>\n            </div>\n            {% endif %}\n            {% endfor %}\n          </div>\n        </div>\n\n        <div class=\"col-md-8 scroll\">\n          <div class=\"suite-details\">\n            <div id=\"suite-description\">\n              {% for suite in suites_list %}\n              {% if suite['tests'] %}\n              <div class=\"suite-content\" id=\"{{ suite['suite_id'] }}_details\" style=\"display: none;\">\n                <h4 class=\"title\" style=\"color:gray\">{{ suite['suite_name'] }}</h4>\n                <table class=\"suite-table\">\n                  <thead>\n                    <tr>\n                      <th>Total</th>\n                      <th>Pass</th>\n                      <th>Fail</th>\n                      <th>Skip</th>\n                      <th>Duration</th>\n                    </tr>\n                  </thead>\n                  <tbody>\n                    <tr>\n                      <td><b>{{ suite['total'] }}</b></td>\n                      <td class=\"text-success\"><b>{{ suite['pass_count'] }}</b></td>\n                      <td class=\"text-danger\"><b>{{ suite['fail_count'] }}</b></td>\n                      <td class=\"text-warning\"><b>{{ suite['skip_count'] }}</b></td>\n                      <td>{{ suite['elapsed_time'] }}</td>\n                    </tr>\n                  </tbody>\n                </table>\n                <div class=\"row\"></div>\n                {% for test in suite['tests'] %}\n                <div class=\"card test-item\">\n                  <div class=\"card-body\">\n                    <h5 class=\"test-name {{ test['status']|lower }} title\">{{ test[\"test_name\"] }}</h5>\n                    <div class=\"test-details\">\n                      <p><b>Duration:</b> {{ test['elapsed_time'] }}</p>\n                      <p><b>Tags:</b> {{ test['tags'] }}</p>\n                      {% if test['status'] == \"FAIL\" %}\n                      <p><b>Message:</b></p>\n                      <div class=\"error-message-box\">\n                        <p> {{ test['message'] }}</p>\n                      </div>\n                      <div>\n                        <p><b>Failed Keywords:</b></p>\n                        <div class=\"card-body\">\n                          {% for keyword in test['keywords'] %}\n                          {% if keyword['keyword_status'] == \"FAIL\" %}\n                          <p class=\"text-danger title\">{{ keyword['keyword_name'] }}</p>\n                          {% endif %}\n                          {% endfor %}\n                        </div>\n                      </div>\n                      {% endif %}\n                    </div>\n                  </div>\n                </div>\n                {% endfor %}\n              </div>\n              {% endif %}\n              {% endfor %}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n\n\n\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js\"></script>\n    <script src=\"https://code.jquery.com/jquery-3.6.0.min.js\"></script>\n    <script src=\"https://cdn.datatables.net/1.11.5/js/jquery.dataTables.min.js\"></script>\n    <script src=\"https://cdn.datatables.net/buttons/2.1.0/js/dataTables.buttons.min.js\"></script>\n    <script src=\"https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js\"></script>\n    <script src=\"https://cdn.datatables.net/buttons/1.5.2/js/buttons.html5.min.js\"></script>\n    <script src=\"https://cdn.datatables.net/buttons/1.5.2/js/buttons.print.min.js\"></script>\n    <script src=\"https://cdn.datatables.net/buttons/1.6.1/js/buttons.colVis.min.js\"></script>\n    <script src=\"https://cdn.jsdelivr.net/npm/apexcharts\"></script>\n\n\n\n\n    <script>\n\n      function showSection(targetId) {\n        // Remove active class from all links and sections\n        document.querySelectorAll('.sidebar .nav-link').forEach(link => link.classList.remove('active'));\n        document.querySelectorAll('.content .section').forEach(section => section.style.display = 'none');\n\n        // Add active class to the clicked link and show the corresponding section\n        document.querySelector(`.sidebar .nav-link[href=\"#${targetId}\"]`).classList.add('active');\n        document.getElementById(targetId).style.display = 'block';\n\n      }\n\n      // Event listener for hash change and initial load\n      window.addEventListener('hashchange', () => {\n        const targetId = window.location.hash.replace('#', '') || 'dashboard';\n        showSection(targetId);\n      });\n\n      // Initial load based on hash or default to 'dashboard'\n      document.addEventListener('DOMContentLoaded', () => {\n        const targetId = window.location.hash.replace('#', '') || 'dashboard';\n        showSection(targetId);\n      });\n\n      // Initialize DataTables\n      $(document).ready(function () {\n        // Initialize DataTables\n        $('#sm, #tm, #kmt').DataTable({\n          dom: '<\"top\"lfB>rtip',\n          order: [[2, 'desc']],\n          buttons: [\n            {\n              extend: 'copy',\n              className: 'copyButton dt-button',\n              text: 'Copy',\n              filename: function () {\n                return \"Metrics\" + '-' + new Date().toLocaleString();\n              },\n              exportOptions: {\n                columns: ':visible'\n              }\n            },\n            {\n              extend: 'csv',\n              className: 'csvButton dt-button',\n              text: 'CSV',\n              filename: function () {\n                return \"Metrics\" + '-' + new Date().toLocaleString();\n              },\n              exportOptions: {\n                columns: ':visible'\n              }\n            },\n            {\n              extend: 'excel',\n              className: 'excelButton dt-button',\n              text: 'Excel',\n              filename: function () {\n                return \"Metrics\" + '-' + new Date().toLocaleString();\n              },\n              exportOptions: {\n                columns: ':visible',\n              }\n            },\n            {\n              extend: 'print',\n              className: 'printButton dt-button',\n              text: 'Print',\n              filename: function () {\n                return \"Metrics\" + '-' + new Date().toLocaleString();\n              },\n              exportOptions: {\n                columns: ':visible',\n                alignment: 'left',\n              }\n            },\n            {\n              extend: 'colvis',\n              className: 'colviButton dt-button',\n              text: 'Hide',\n              postfixButtons: ['colvisRestore']\n            }\n          ],\n          lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, \"All\"]],\n          responsive: true,\n          initComplete: function () {\n            this.api().columns().every(function () {\n              var column = this;\n              $('input', this.footer()).on('keyup change', function () {\n                if (column.search() !== this.value) {\n                  column.search(this.value).draw();\n                }\n              });\n            });\n          }\n        });\n      });\n    </script>\n\n    <script>\n      window.onload = function () {\n        barChart('#suitePie', \"{{suite_stats['Pass']}}\", \"{{suite_stats['Fail']}}\", \"{{suite_stats['Skip']}}\");\n        pieChart('#testPie', \"{{test_stats['Pass']}}\", \"{{test_stats['Fail']}}\", \"{{test_stats['Skip']}}\");\n        barChart('#keywordPie', \"{{kw_stats['Pass']}}\", \"{{kw_stats['Fail']}}\", \"{{kw_stats['Skip']}}\");\n      };\n    </script>\n    <script>\n      function pieChart(chartID, passed, failed, skipped) {\n        var options = {\n          series: [parseInt(passed), parseInt(failed), parseInt(skipped)],\n          chart: {\n            type: 'donut',\n            width: 300,\n          },\n          labels: [\"Pass\", \"Fail\", \"Skip\"],\n          legend: {\n            show: false,\n          },\n          colors: ['#2ecc71', '#fc6666', '#ffa500'],\n        };\n\n        var chart = new ApexCharts(document.querySelector(chartID), options);\n        chart.render();\n      }\n    </script>\n    <script>\n      function barChart(chartID, passed, failed, skipped) {\n        var options = {\n          series: [{\n            name: 'Pass',\n            data: [parseInt(passed)]\n          },\n          {\n            name: 'Fail',\n            data: [parseInt(failed)]\n          },\n          {\n            name: 'Skip',\n            data: [parseInt(skipped)]\n          }],\n          chart: {\n            type: 'bar',\n            height: 120,\n            stacked: true,\n            toolbar: {\n              show: false\n            },\n          },\n          plotOptions: {\n            bar: {\n              horizontal: true,\n              dataLabels: {\n                total: {\n                  enabled: true,\n                  offsetX: 0,\n                  style: {\n                    fontSize: '12px',\n                    fontWeight: 100\n                  }\n                }\n              }\n            },\n          },\n          stroke: {\n            width: 2,\n            colors: ['#fff']\n          },\n          labels: [\"\"],\n          legend: {\n            show: false,\n          },\n          colors: ['#2ecc71', '#fc6666', '#ffa500'],\n        };\n\n        var chart = new ApexCharts(document.querySelector(chartID), options);\n        chart.render();\n      }\n    </script>\n    <script>\n      var passArray = [];\n      var failArray = [];\n      var catgArray = [];\n\n      {% for key, value in suite_error_stats.iterrows() %}\n      {% if (value['Name'] != \"\") %}\n      catgArray.push(\"{{value['Name']}}\");\n      passArray.push({{ value['percent']| round(2) }});\n      failArray.push({{ value['Fail']}});\n      {% endif %}\n      {% endfor %}\n\n      var options = {\n        series: [{\n          name: 'Fail Percentage',\n          data: passArray,\n          type: \"line\"\n        }, {\n          name: 'Fail',\n          data: failArray,\n          type: \"column\",\n        }],\n        chart: {\n          type: 'line',\n          height: 350,\n          // stacked: true,\n        },\n\n        plotOptions: {\n          bar: {\n            dataLabels: {\n              position: 'center',\n              hideOverflowingLabels: true\n            }\n          },\n        },\n        colors: ['#ea9999', '#fc6666'],\n        xaxis: {\n          categories: catgArray,\n          tickPlacement: 'off',\n          labels: {\n            show: false,\n            trim: true\n          }\n        },\n        yaxis: [{\n          title: {\n            text: 'Suite Fail Percentage',\n          },\n\n        }, {\n          opposite: true,\n          title: {\n            text: 'Suite Fail Count'\n          }\n        }],\n        fill: {\n          opacity: 0.9\n        },\n        tooltip: {\n          y: {\n            formatter: function (val) {\n              return val\n            }\n          },\n          x: {\n            show: true\n          }\n        }\n      };\n\n      var chart = new ApexCharts(document.querySelector(\"#suiteFailureLineID\"), options);\n      chart.render();\n\n    </script>\n    <script>\n      var time_group = [];\n      var test_count = [];\n\n      {% for key, value in test_time_group.iterrows() %}\n      time_group.push(\"{{value['time_group']}}\");\n      test_count.push({{ value['test_case_count']}});\n      {% endfor %}\n\n      var options = {\n        series: [{\n          name: \"Test Count\",\n          data: test_count\n        }],\n        chart: {\n          type: 'area',\n          height: 350\n        },\n        plotOptions: {\n          bar: {\n            borderRadius: 4,\n            borderRadiusApplication: 'end',\n            horizontal: true,\n          }\n        },\n        dataLabels: {\n          enabled: false\n        },\n        xaxis: {\n          categories: time_group,\n        }\n      };\n\n      var chart = new ApexCharts(document.querySelector(\"#testExecutionTrends\"), options);\n      chart.render();\n\n    </script>\n    <script>\n      function openInNewTab(url, element_id) {\n        var element_id = element_id;\n        var win = window.open(url, '_blank');\n        win.focus();\n        $('body').scrollTo(element_id);\n      }\n    </script>\n    <script>\n      $(window).on('load', function () { $('.loader').fadeOut(); });\n    </script>\n    <script>\n      function updateDetails(selectedSuite) {\n        // Hide all issue content divs\n        const contents = document.querySelectorAll('.suite-content');\n        contents.forEach(content => {\n          content.style.display = 'none';\n        });\n\n        // Show the selected suite details\n        const selectedContent = document.getElementById(selectedSuite + '_details');\n        if (selectedContent) {\n          selectedContent.style.display = 'block';\n        }\n\n      }\n    </script>\n    <script>\n      function filterSuites() {\n        const searchInput = document.getElementById('suiteSearch').value.toLowerCase();\n        const suites = document.querySelectorAll('.suite-card');\n        suites.forEach(suite => {\n          const suiteName = suite.innerText.toLowerCase();\n          suite.style.display = suiteName.includes(searchInput) && isStatusVisible(suite) ? \"\" : \"none\";\n        });\n      }\n\n      function filterByStatus(status) {\n        document.querySelector('.filter-buttons').dataset.status = status;\n        filterSuites(); // Reapply filters to update visibility\n      }\n\n      function isStatusVisible(suite) {\n        const status = document.querySelector('.filter-buttons').dataset.status;\n        return status === 'all' || suite.dataset.status === status;\n      }\n    </script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "robotframework_metrics/test_results.py",
    "content": "from robot.api import ResultVisitor\nfrom robot.utils.markuputils import html_format\n\n\nclass TestResults(ResultVisitor):\n\n    def __init__(self, test_list):\n        self.test_list = test_list\n    \n    def visit_test(self, test):\n        suite_name = test.parent if test.parent else test.parent.name\n        test_json = {\n            \"Suite Name\" : suite_name,\n            \"Test Name\" : test.name,\n            \"Test Id\" : test.id,\n            \"Status\" : test.status,\n            \"Documentation\" : html_format(test.doc),\n            \"Time\" : test.elapsedtime,\n            # \"Message\" : html_format(test.message),\n            \"Message\" : str(test.message).replace(\"*HTML*\",\"\"),\n            \"Tags\" : test.tags,\n            'start_time': test.starttime,\n            'end_time': test.endtime,\n        }\n        self.test_list.append(test_json)\n"
  },
  {
    "path": "robotframework_metrics/version.py",
    "content": "__version__ = \"3.6.0\"\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, find_packages\n\nsetup(\n      name='robotframework-metrics',\n      version=\"3.6.0\",\n      description='Custom report for robot framework',\n      long_description='Custom html report generator using robot.result api',\n      classifiers=[\n          'Framework :: Robot Framework',\n          'Programming Language :: Python',\n          'Topic :: Software Development :: Testing',\n      ],\n      keywords='robotframework report',\n      author='Shiva Prasad Adirala',\n      author_email='adiralashiva8@gmail.com',\n      url='https://github.com/adiralashiva8/robotframework-metrics',\n      license='MIT',\n      \n      packages=find_packages(),\n      include_package_data= True,\n      zip_safe=False,\n      \n      install_requires=[\n          'robotframework',\n          'jinja2',\n          'pandas',\n      ],\n      entry_points={\n          'console_scripts': [\n              'robotmetrics=robotframework_metrics.runner:main',\n          ]\n      },\n      )\n"
  }
]