diff --git a/.pydevproject b/.pydevproject index 831a2ca..9452bc3 100644 --- a/.pydevproject +++ b/.pydevproject @@ -1,13 +1,17 @@ - + + Default - + + python interpreter - + + /${PROJECT_DIR_NAME}/src /${PROJECT_DIR_NAME} - + + diff --git a/Jenkinsfile b/Jenkinsfile index ba3de93..5a0144c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,7 +6,13 @@ // You should have received a copy of the license along with this // work. If not, see . - +import groovy.xml.XmlUtil +import static javax.xml.xpath.XPathConstants.* +import javax.xml.xpath.* +import groovy.xml.DOMBuilder +import groovy.xml.dom.DOMCategory +import java.math.RoundingMode +import java.math.BigDecimal // configurable settings: // use to send email if workflow problem @@ -21,10 +27,17 @@ def _bDraft = false // release content / changelog management def _bAutoChangelog = true //Not supported yet def _ReleaseContent_Title = "_CI/CD Automatic Release_" +def bPushMasterOnPypi = true // full rebuild toogle def _bFullRebuilt = true def _MkDocsWebURL = "dabauto--mkdocs-web.dmz.chacha.home/mkdocs-web/" def _MkDocsWebCredentials = "2c5b684e-3787-4b37-8aca-b3dd4a383fe2" +def _PypiCredentials = "Pypi" + + +def badge_coverage = addEmbeddableBadgeConfiguration(id: "coverage", subject: "coverage") +def badge_maintainability = addEmbeddableBadgeConfiguration(id: "maintainability", subject: "maintainability") +def badge_quality = addEmbeddableBadgeConfiguration(id: "quality", subject: "quality score") // commands Helper: /!\ Made for GITEA /!\ String determineRepoUserName() { @@ -61,6 +74,46 @@ String ExtractBaseVersion(inVersion) { return matcher[0][1] } +int GetCoverageValue(String CoverageFilePath,String XPath) +{ + //File file = new File(CoverageFilePath) + //coverageReportRaw = file.getText('UTF-8') + coverageReportRaw = readFile(CoverageFilePath) + coverageReport = DOMBuilder.parse(new StringReader(coverageReportRaw), false, false) + coverageReportRoot = coverageReport.documentElement + + def xpath = XPathFactory.newInstance().newXPath() + res = xpath.evaluate(XPath,coverageReportRoot,NUMBER) + return res +} + +String getColorScale(BigDecimal value) +{ + if( value >9) { return "Goldenrod"} + else if( value >6) { return "seagreen"} + else if( value >4) { return "orange"} + else if( value >2) { return "darkred"} + else { return "dimgrey"} +} + +String getColorScale_reversed(BigDecimal value) +{ + if( value >9) { return "dimgrey"} + else if( value >6) { return "darkred"} + else if( value >4) { return "orange"} + else if( value >2) { return "seagreen"} + else { return "Goldenrod"} +} + +int GetCoverageValue_lines_valid(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@lines-valid") } +int GetCoverageValue_lines_covered(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@lines-covered") } +int GetCoverageValue_line_rate(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@line-rate") } +int GetCoverageValue_branches_valid(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branches-valid") } +int GetCoverageValue_branches_covered(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branches-covered") } +int GetCoverageValue_branch_rate(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@branch-rate") } +int GetCoverageValue_complexity(String CoverageFilePath) { return GetCoverageValue(CoverageFilePath,"/coverage/@complexity") } + + pipeline { // for Docker based build (preferable) @@ -131,11 +184,11 @@ pipeline { sh(". ~/BUILD_ENV/bin/activate && pip install --upgrade setuptools build pip copier jinja2-slug toml") - sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests") + sh(". ~/TOOLS_ENV/bin/activate && pip install simple_rest_client requests twine") script { if(_PROJECT_NAME!="pygitversionhelper") { - sh(". ~/TOOLS_ENV/bin/activate && pip install git+https://chacha.ddns.net/gitea/chacha/pygitversionhelper.git@master") + sh(". ~/TOOLS_ENV/bin/activate && pip install pygitversionhelper") } else { @@ -145,6 +198,7 @@ pipeline { } sh("git config --global user.email $_MaintainerEmail") sh("git config --global user.name $_MaintainerName") + sh("git config --global init.defaultBranch master") } } @@ -321,7 +375,7 @@ pipeline { def wheelPath = findFiles(glob: "**/dist/*.whl")[0] echo "wheel artifact path: $wheelPath" // install the package, with *test* optionnal packages, as user - sh(". ~/TEST_ENV/bin/activate && pip install --find-links dist/ ${PY_PROJECT_NAME} .[test,coverage-check,quality-check,type-check,doc-gen]") + sh(". ~/TEST_ENV/bin/activate && pip install --find-links dist/ ${PY_PROJECT_NAME} .[test,coverage-check,quality-check,type-check,doc-gen,complexity-check]") } } } @@ -331,6 +385,14 @@ pipeline { steps { dir("gitrepo") { sh(". ~/TEST_ENV/bin/activate && python -m helpers --type-check --quality-check") + script { + def jsonObj = readJSON file: "helpers-results/quality_check/metrics.json" + quality_score = new BigDecimal(jsonObj["GlobalScore"]) + sz_quality_score = quality_score.setScale(2, RoundingMode.HALF_EVEN).toString() + badge_quality.setStatus(sz_quality_score) + badge_quality.setColor(getColorScale(quality_score)) + } + sh(". ~/TEST_ENV/bin/activate && python -m helpers --complexity-check") } } post { @@ -385,6 +447,14 @@ pipeline { style: 'stackedArea', keepRecords: true, numBuilds: '']) + + plot([ csvFileName: 'plot-4ceb9ee2-ca78-11ed-afa1-0242ac120002.csv', + csvSeries: [[ file: 'gitrepo/helpers-results/complexity_check/MI.csv', inclusionFlag: 'INCLUDE_BY_STRING',exclusionValues: 'MeanMaintainability', url: '']], + group: 'metrics', + title: 'maintainability', + style: 'stackedArea', + keepRecords: true, + numBuilds: '']) } } @@ -397,6 +467,36 @@ pipeline { println unit_test_full_name__html unit_test_full_name__xml=findFiles(glob: "helpers-results/unit_test_full/*.xml")[0].getName() println unit_test_full_name__xml + + coverage_report_path = "helpers-results/unit_test_coverage/test_coverage.xml" + println GetCoverageValue_lines_valid(coverage_report_path) + println GetCoverageValue_lines_covered(coverage_report_path) + println GetCoverageValue_line_rate(coverage_report_path) + println GetCoverageValue_branches_valid(coverage_report_path) + println GetCoverageValue_branches_covered(coverage_report_path) + println GetCoverageValue_branch_rate(coverage_report_path) + println GetCoverageValue_complexity(coverage_report_path) + + full_rate = new BigDecimal( 10*(GetCoverageValue_line_rate(coverage_report_path) + GetCoverageValue_branch_rate(coverage_report_path)) / 2 ) + sz_full_rate = full_rate.setScale(2, RoundingMode.HALF_EVEN).toString() + badge_coverage.setStatus(sz_full_rate) + badge_coverage.setColor(getColorScale(full_rate)) + + //complexity = new BigDecimal( 10*GetCoverageValue_complexity(coverage_report_path)) + //sz_complexity = complexity.setScale(2, RoundingMode.HALF_EVEN).toString() + //badge_complexity.setStatus(sz_complexity) + //badge_quality.setColor(getColorScale_reversed(complexity)) + + //badge_maintainability + records = readCSV file: 'helpers-results/complexity_check/MI.csv' + maintainability = records[1][1] + badge_maintainability.setStatus(maintainability) + + if ( maintainability == 'D') { badge_maintainability.setColor( "dimgrey")} + else if( maintainability == 'C') { badge_maintainability.setColor( "darkred")} + else if( maintainability == 'B') { badge_maintainability.setColor( "orange")} + else if( maintainability == 'A') { badge_maintainability.setColor( "seagreen")} + else if( maintainability == 'A+') { badge_maintainability.setColor( "Goldenrod")} } } } @@ -554,6 +654,14 @@ pipeline { |__EOWRAPPER__ """.stripMargin()) } + if((_GIT_BRANCH=="master") && (bPushMasterOnPypi)) { + withCredentials([usernamePassword( credentialsId: _PypiCredentials, passwordVariable: 'PYPI_PASSWORD', usernameVariable: 'PYPI_USERNAME')]) { + sh(script: """#!/bin/sh - + |. ~/TOOLS_ENV/bin/activate + |exec twine upload -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --non-interactive --disable-progress-bar dist/* + """.stripMargin()) + } + } } } } diff --git a/README.md b/README.md index 51f5acd..94685df 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,17 @@ +![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=status&status=active&color=seagreen) +![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=doc&status=MkDocs&color=blue) ![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=jenkins-unittest&job={{repository}}-{{branch}}) -![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=licence&status=CC%20BY-NC-SA%204.0&color=blue) +![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=coverage) +![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=maintainability) +![](https://chacha.ddns.net/jenkins/buildStatus/icon?job={{repository}}-{{branch}}&build=0&config=quality) +![](https://chacha.ddns.net/jenkins/buildStatus/icon?subject=licence&status=CC%20BY-NC-SA%204.0&color=teal) ![](docs-static/Library.jpg) -# Python project template +# pyChangeLogHelper -A nice template to start blank python projets. - -This template automate a lot of handy things and allow CI/CD automatic releases generation. +A simple changelog formater that consume merged commit message and produce nice pre-formated changelogs -It is also collectings data to feed Jenkins build. - -Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychachadummyproject/{{branch}}/latest/). +Checkout [Latest Documentation](https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/master/latest/). ## Features - -### Generic pipeline skeleton: - - Prepare - - GetCode - - BuildPackage - - Install - - CheckCode - - PlotMetrics - - RunUnitTests - - GenDOC - - PostRelease - -### CI/CD Environment - - Jenkins - - Gitea (with patch for dynamic Readme variables: https://chacha.ddns.net/gitea/chacha/GiteaMarkupVariable) - - Docker - - MkDocsWeb - -### CI/CD Helper libs - - VirtualEnv - - Changelog generation based on commits - - copier - - pylint + pylint_json2html - - mypy - - unittest + xmlrunner + junitparser + junit2htmlreport - - mkdocs - -### Python project - - Full .toml implementation - - .whl automatic generation - - dynamic versionning using git repository - - embedded unit-test \ No newline at end of file diff --git a/docs-static/usage.md b/docs-static/usage.md index 7763bbd..06f0375 100644 --- a/docs-static/usage.md +++ b/docs-static/usage.md @@ -1,16 +1,24 @@ # Usage -## Pulvinar dolor -Donec dapibus est fermentum justo volutpat condimentum. Integer quis nunc neque. Donec dictum vehicula justo, in facilisis ex tincidunt in. -Vivamus sollicitudin sem dui, id mollis orci facilisis ut. Proin sed pulvinar dolor. Donec volutpat commodo urna imperdiet pulvinar. Fusce eget aliquam risus. -Vivamus viverra luctus ex, in finibus mi. Nullam elementum dapibus mollis. Ut suscipit volutpat ex, quis feugiat lacus consectetur eu. +## Installation -## Condimentum faucibus -Quisque auctor egestas sem, luctus suscipit ex maximus vitae. Duis facilisis augue et condimentum faucibus. -Donec cursus, enim a sagittis egestas, lectus lorem eleifend libero, at tincidunt leo magna at libero. -Nunc eros velit, suscipit luctus tempor vel, finibus et est. Curabitur efficitur pretium pulvinar. -Donec urna lectus, vulputate quis turpis sed, placerat congue urna. Phasellus aliquet fermentum quam, non auctor elit porta nec. Morbi eu ligula at nisl ultricies condimentum vitae id ante. +From pypi repository (prefered): -## Aliquam lacinia -In volutpat lorem ex, et fringilla nibh faucibus quis. Mauris et arcu elementum, auctor dui vitae, egestas arcu. Duis sit amet aliquam quam. -Phasellus a odio turpis. Etiam tristique mi eu enim varius, eget facilisis est vestibulum. Aliquam lacinia nec purus sed luctus. Cras at laoreet erat. \ No newline at end of file + python -m pip install pychangelogfactory + +From downloaded .whl file: + + python -m pip install pychangelogfactory--py3-none-any.whl + +From master git repository: + + python -m pip install git+https://chacha.ddns.net/gitea/chacha/pychangelogfactory.git@master + + + + +## Import in your project + +Add this line on the top of your python script: + + from pychangelogfactory import ChangeLogFormater diff --git a/helpers/__main__.py b/helpers/__main__.py index 8539d81..b4dd963 100644 --- a/helpers/__main__.py +++ b/helpers/__main__.py @@ -16,89 +16,101 @@ import os import logging import sys -if __package__=="helpers": +if __package__ == "helpers": # when calling the module from: > python -m helpers - from .types_check import types_check - from .quality_check import quality_check - from .unit_test import unit_test - from .doc_gen import doc_gen - from .changelog_gen import changelog_gen -else: + from .types_check import types_check + from .quality_check import quality_check + from .unit_test import unit_test + from .doc_gen import doc_gen + from .changelog_gen import changelog_gen + from .complexity_check import complexity_check +else: # when calling the __main__.py file (from IDE) - from helpers.types_check import types_check - from helpers.quality_check import quality_check - from helpers.unit_test import unit_test - from helpers.doc_gen import doc_gen - from helpers.changelog_gen import changelog_gen - + from helpers.types_check import types_check + from helpers.quality_check import quality_check + from helpers.unit_test import unit_test + from helpers.doc_gen import doc_gen + from helpers.changelog_gen import changelog_gen + from helpers.complexity_check import complexity_check + logging.getLogger().setLevel(logging.INFO) if __name__ == "__main__": - project_rootdir_path=Path(__file__).parent.parent.absolute() + project_rootdir_path = Path(__file__).parent.parent.absolute() with open(project_rootdir_path / "pyproject.toml", mode="rb") as fp: pyproject = tomli.load(fp) - - parser = argparse.ArgumentParser( prog = 'continuous-integration-helper', - description = 'A tiny set of scripts to help continous integration on python') - - parser.add_argument('-tc', '--type-check', dest='typecheck', action='store_true', help='enable static typing check') - - parser.add_argument('-ut', '--unit-test', dest='unittest', action='store_true', help='enable unit-test') - parser.add_argument('-cc', '--coverage-check', dest='coveragecheck', action='store_true', help='enable unit-test coverage check (requires unit-test)') - parser.add_argument('-qc', '--quality-check', dest='qualitycheck', action='store_true', help='enable code quality check') - - parser.add_argument('-dg', '--doc-gen', dest='docgen', action='store_true', help='enable documentation generation using MkDoc') - parser.add_argument('-pdf', '--doc-gen-pdf', dest='docgenpdf', action='store_true', help='enable pdf documentation export (requires doc-gen)') - - parser.add_argument('-clg', '--changelog-gen', dest='changeloggen', action='store_true', help='enable changelog generation') - + parser = argparse.ArgumentParser( + prog="continuous-integration-helper", description="A tiny set of scripts to help continous integration on python" + ) + + parser.add_argument("-tc", "--type-check", dest="typecheck", action="store_true", help="enable static typing check") + + parser.add_argument("-ut", "--unit-test", dest="unittest", action="store_true", help="enable unit-test") + parser.add_argument( + "-cc", "--coverage-check", dest="coveragecheck", action="store_true", help="enable unit-test coverage check (requires unit-test)" + ) + + parser.add_argument("-qc", "--quality-check", dest="qualitycheck", action="store_true", help="enable code quality check") + + parser.add_argument("-dg", "--doc-gen", dest="docgen", action="store_true", help="enable documentation generation using MkDoc") + parser.add_argument( + "-pdf", "--doc-gen-pdf", dest="docgenpdf", action="store_true", help="enable pdf documentation export (requires doc-gen)" + ) + + parser.add_argument("-clg", "--changelog-gen", dest="changeloggen", action="store_true", help="enable changelog generation") + + parser.add_argument("-cpc", "--complexity-check", dest="complexitycheck", action="store_true", help="enable complexity check") + args = parser.parse_args() - + ################################## # Dev / Debug forced toogles # - #-------------------------------- + # -------------------------------- # - #args.typecheck = True - #args.qualitycheck = True - #args.unittest = True - #args.coveragecheck = True - #args.docgen = True - #args.docgenpdf = True - #args.changeloggen = True - + # args.typecheck = True + # args.qualitycheck = True + # args.unittest = True + # args.coveragecheck = True + # args.docgen = True + # args.docgenpdf = True + # args.changeloggen = True + # args.complexitycheck = True + helpers = [] - if args.typecheck == True: + if args.typecheck == True: helpers.append(types_check) - - if args.unittest == True: + + if args.unittest == True: helpers.append(unit_test) - - if args.coveragecheck == True: + + if args.coveragecheck == True: if args.unittest == True: unit_test.enable_coverage_check = True else: raise RuntimeError("unit-test is required to enable coverage-check") - - if args.qualitycheck == True: + + if args.qualitycheck == True: helpers.append(quality_check) - - if args.docgen == True: + + if args.docgen == True: helpers.append(doc_gen) - - if args.docgenpdf==True: + + if args.docgenpdf == True: if args.docgen == True: doc_gen.enable_gen_pdf = True else: raise RuntimeError("doc-gen is required to enable doc-gen-pdf") - - if args.changeloggen == True: + + if args.changeloggen == True: helpers.append(changelog_gen) - + + if args.complexitycheck == True: + helpers.append(complexity_check) + for helper in helpers: - helper.set_context(project_rootdir_path,pyproject) + helper.set_context(project_rootdir_path, pyproject) helper.reset_result_dir() helper.do_job() - diff --git a/helpers/changelog_gen.py b/helpers/changelog_gen.py index a0b5734..f4a15cb 100644 --- a/helpers/changelog_gen.py +++ b/helpers/changelog_gen.py @@ -9,16 +9,15 @@ from __future__ import annotations from typing import TYPE_CHECKING -#from pathlib import Path -#import os -#import datetime +# from pathlib import Path +# import os +# import datetime from .helper_base import helper_base -class changelog_gen(helper_base): - - @classmethod - def do_job(cls): - pass \ No newline at end of file +class changelog_gen(helper_base): + @classmethod + def do_job(cls): + pass diff --git a/helpers/complexity_check.py b/helpers/complexity_check.py new file mode 100644 index 0000000..529e872 --- /dev/null +++ b/helpers/complexity_check.py @@ -0,0 +1,70 @@ +# pyChaChaDummyProject (c) by chacha +# +# pyChaChaDummyProject is licensed under a +# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Unported License. +# +# You should have received a copy of the license along with this +# work. If not, see . + +from __future__ import annotations +from typing import TYPE_CHECKING + +# from pathlib import Path +# import os +import statistics +import csv +from json import loads as JSON_LOADS +from radon.complexity import cc_rank, SCORE +from radon.cli import Config +from radon.cli.harvest import CCHarvester, HCHarvester, MIHarvester + +from .helper_base import helper_withresults_base +from pprint import pprint + + +class complexity_check(helper_withresults_base): + @classmethod + def do_job(cls): + config = Config( + exclude="__init__\.py", + ignore=None, + order=SCORE, + show_closures=False, + no_assert=True, + min="A", + max="F", + multi=False, + ) + + h = MIHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json() + res = JSON_LOADS(h) + + with open(cls.get_result_dir() / "MI.json", "w", newline="") as oFile: + oFile.write(h) + + mean = statistics.mean(_["mi"] for _ in res.values()) + + if mean >= 65: + rank = "A+" + elif mean >= 20: + rank = "A" + elif mean >= 10: + rank = "B" + else: + rank = "C" + + RES_MI = {"MeanMaintainability": mean, "MaintainabilityIndex": rank} + with open(cls.get_result_dir() / "MI.csv", "w", newline="") as oFile: + writer = csv.DictWriter(oFile, fieldnames=RES_MI.keys()) + writer.writeheader() + writer.writerow(RES_MI) + + config = Config(exclude=None, ignore=None, order=SCORE, show_closures=False, no_assert=True, min="A", max="F", multi=False) + h = CCHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json() + with open(cls.get_result_dir() / "CC.json", "w", newline="") as oFile: + oFile.write(h) + + config = Config(exclude=None, ignore=None, order=SCORE, show_closures=False, no_assert=True, min="A", max="F", by_function=None) + h = HCHarvester([str(_) for _ in sorted((cls.project_rootdir_path / "src").rglob("*.py"))], config).as_json() + with open(cls.get_result_dir() / "HC.json", "w", newline="") as oFile: + oFile.write(h) diff --git a/helpers/doc_gen.py b/helpers/doc_gen.py index 05dcb38..155db4d 100644 --- a/helpers/doc_gen.py +++ b/helpers/doc_gen.py @@ -17,6 +17,7 @@ from pathlib import Path from distutils.dir_util import copy_tree import yaml + try: from yaml import CLoader as Loader, CDumper as Dumper except ImportError: @@ -24,35 +25,34 @@ except ImportError: from .helper_base import helper_withresults_base -class doc_gen(helper_withresults_base): + +class doc_gen(helper_withresults_base): enable_gen_pdf: bool = False - - @classmethod - def do_job(cls): - print(cls.project_rootdir_path) - print() - + + @classmethod + def do_job(cls): + # create doc root dir - doc_path = cls.project_rootdir_path/"docs" + doc_path = cls.project_rootdir_path / "docs" cls._reset_dir(doc_path) - - site_path = cls.get_result_dir()/"site" + + site_path = cls.get_result_dir() / "site" cls._reset_dir(site_path) - + # copy files from main project dir - shutil.copyfile(str(cls.project_rootdir_path/"README.md"),str(doc_path/"README.md")) - shutil.copyfile(str(cls.project_rootdir_path/"LICENSE.md"),str(doc_path/"LICENSE.md")) - + shutil.copyfile(str(cls.project_rootdir_path / "README.md"), str(doc_path / "README.md")) + shutil.copyfile(str(cls.project_rootdir_path / "LICENSE.md"), str(doc_path / "LICENSE.md")) + # copy files from static-doc dir - copy_tree(str(cls.project_rootdir_path/"docs-static"),str(doc_path)) - + copy_tree(str(cls.project_rootdir_path / "docs-static"), str(doc_path)) + # generating API doc + nav from python docstrings reference_path = doc_path / "reference" cls._reset_dir(reference_path) - + for path in sorted((cls.project_rootdir_path / "src").rglob("*.py")): - module_path = path.relative_to(cls.project_rootdir_path/ "src").with_suffix("") - doc_path = path.relative_to(cls.project_rootdir_path/ "src").with_suffix(".md") + module_path = path.relative_to(cls.project_rootdir_path / "src").with_suffix("") + doc_path = path.relative_to(cls.project_rootdir_path / "src").with_suffix(".md") full_doc_path = Path(reference_path, doc_path) parts = list(module_path.parts) @@ -61,43 +61,44 @@ class doc_gen(helper_withresults_base): parts = parts[:-1] elif parts[-1] == "__main__": continue - + cls._reset_dir(os.path.dirname(full_doc_path)) with open(full_doc_path, "w+") as fd: - identifier = "src."+".".join(parts) + identifier = "src." + ".".join(parts) print("::: " + identifier, file=fd) - - - cmdopts = [f"{sys.executable}","-m","mkdocs","-v","build","--site-dir",str(site_path),"--clean"] - + + cmdopts = [f"{sys.executable}", "-m", "mkdocs", "-v", "build", "--site-dir", str(site_path), "--clean"] + # little hack here, to enable / disable pdf generation using own class config - # => reason is mkdocs seems to try loading the plugin even if we disable it, so we need to + # => reason is mkdocs seems to try loading the plugin even if we disable it, so we need to # manually process the configuration file. - mkdocsCfg=None - with open(cls.project_rootdir_path / "mkdocs.yml",'r') as mkdocsCfgFile: + mkdocsCfg = None + with open(cls.project_rootdir_path / "mkdocs.yml", "r") as mkdocsCfgFile: mkdocsCfg = yaml.load(mkdocsCfgFile, Loader=yaml.SafeLoader) - - if cls.enable_gen_pdf==True: - mkdocsCfg['plugins'].append({ 'with-pdf':{ - 'cover_subtitle': 'User Manual', - 'cover_logo': str(cls.project_rootdir_path / 'docs-static' / 'Library.jpg'), - 'verbose': False, - 'media_type': 'print', - 'exclude_pages': ['LICENSE'], - 'output_path': str(site_path / 'pdf' / 'manual.pdf') - }}) + + if cls.enable_gen_pdf == True: + mkdocsCfg["plugins"].append( + { + "with-pdf": { + "cover_subtitle": "User Manual", + "cover_logo": str(cls.project_rootdir_path / "docs-static" / "Library.jpg"), + "verbose": False, + "media_type": "print", + "exclude_pages": ["LICENSE"], + "output_path": str(site_path / "pdf" / "manual.pdf"), + } + } + ) else: - for subelem in mkdocsCfg['plugins']: - if isinstance(subelem,dict) : - if 'with-pdf' in subelem.keys(): - mkdocsCfg['plugins'].remove(subelem) - break - - with open(cls.project_rootdir_path / "mkdocs.yml",'w') as mkdocsCfgFile: - mkdocsCfgFile.write(yaml.dump(mkdocsCfg, Dumper=Dumper,default_flow_style=False, sort_keys=False)) - - - res=cls.run_cmd(cmdopts) + for subelem in mkdocsCfg["plugins"]: + if isinstance(subelem, dict): + if "with-pdf" in subelem.keys(): + mkdocsCfg["plugins"].remove(subelem) + break + + with open(cls.project_rootdir_path / "mkdocs.yml", "w") as mkdocsCfgFile: + mkdocsCfgFile.write(yaml.dump(mkdocsCfg, Dumper=Dumper, default_flow_style=False, sort_keys=False)) + + res = cls.run_cmd(cmdopts) print(res.decode()) - print(' !! done') - + print(" !! done") diff --git a/helpers/helper_base.py b/helpers/helper_base.py index 74cf0e8..294ca80 100644 --- a/helpers/helper_base.py +++ b/helpers/helper_base.py @@ -9,64 +9,66 @@ from __future__ import annotations from typing import TYPE_CHECKING -from abc import ABC,abstractmethod +from abc import ABC, abstractmethod import os from pathlib import Path import subprocess if TYPE_CHECKING: # Only imports the below statements during type checking - from typing import Union + from typing import Union + class helper_base(ABC): - project_rootdir_path: Union[Path,None] = None - pyproject: Union[dict,None] = None - current_dir: Union[Path,None] = None - - @classmethod - def set_context(cls,project_rootdir_path:Path, pyproject:dict): - cls.project_rootdir_path = project_rootdir_path - cls.pyproject = pyproject - cls.current_dir = Path(__file__).parent.absolute() - - @classmethod + project_rootdir_path: Union[Path, None] = None + pyproject: Union[dict, None] = None + current_dir: Union[Path, None] = None + + @classmethod + def set_context(cls, project_rootdir_path: Path, pyproject: dict): + cls.project_rootdir_path = project_rootdir_path + cls.pyproject = pyproject + cls.current_dir = Path(__file__).parent.absolute() + + @classmethod def get_result_dir(cls): return None - - @staticmethod - def _reset_dir(dirpath:Path): + + @staticmethod + def _reset_dir(dirpath: Path): dirpath = Path(dirpath) if not os.path.exists(dirpath): os.makedirs(dirpath) - [f.unlink() for f in Path(dirpath).glob("*") if f.is_file()] - - @classmethod + [f.unlink() for f in Path(dirpath).glob("*") if f.is_file()] + + @classmethod def reset_result_dir(cls): result_dir = cls.get_result_dir() if result_dir != None: cls._reset_dir(result_dir) - - @classmethod + + @classmethod @abstractmethod - def do_job(cls): + def do_job(cls): raise NotImplementedError() - @classmethod + @classmethod def run_cmd_(cls, cmdarray): process = subprocess.run(cmdarray, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, check=True) return process.stdout - - @classmethod + + @classmethod def run_cmd(cls, cmdarray): p = subprocess.run(cmdarray, capture_output=True) print(p.stdout) print(p.stderr) return p.stdout - + + class helper_withresults_base(helper_base): - helper_results_dir: Union[Path,None] = None - - @classmethod + helper_results_dir: Union[Path, None] = None + + @classmethod def get_result_dir(cls): - if cls.helper_results_dir==None: - cls.helper_results_dir=cls.__name__ - return Path(__file__).parent.parent.absolute() / "helpers-results" / cls.helper_results_dir \ No newline at end of file + if cls.helper_results_dir == None: + cls.helper_results_dir = cls.__name__ + return Path(__file__).parent.parent.absolute() / "helpers-results" / cls.helper_results_dir diff --git a/helpers/quality_check.py b/helpers/quality_check.py index 7616d5f..d57c523 100644 --- a/helpers/quality_check.py +++ b/helpers/quality_check.py @@ -25,197 +25,222 @@ import pylint_json2html from .helper_base import helper_withresults_base + class PyLintMetricNotFound(Warning): pass -class quality_check(helper_withresults_base): - PylintMessageList=dict() - - @classmethod + +class quality_check(helper_withresults_base): + PylintMessageList = dict() + + @classmethod def GetPylintMessageList(cls): - Messagelist=dict() - regex = r"^:([a-zA-Z-]+) \(([^\)]+)\)" - for line in cls.run_cmd([sys.executable,"-m","pylint","--list-msgs"]).splitlines(): - if res:=re.search(regex,line.decode()): - Messagelist[res.group(1)]=res.group(2) + Messagelist = dict() + regex = r"^:([a-zA-Z-]+) \(([^\)]+)\)" + for line in cls.run_cmd([sys.executable, "-m", "pylint", "--list-msgs"]).splitlines(): + if res := re.search(regex, line.decode()): + Messagelist[res.group(1)] = res.group(2) cls.PylintMessageList = Messagelist @staticmethod - def TryExtractPYReportMetric(line: str,tag: str): - regex=f"^(?:\|{tag}\s*\|)(\d+)(?=\s*|)" - if res:=re.search(regex,line): + def TryExtractPYReportMetric(line: str, tag: str): + regex = f"^(?:\|{tag}\s*\|)(\d+)(?=\s*|)" + if res := re.search(regex, line): return float(res.group(1)) raise PyLintMetricNotFound() - @classmethod - def do_job(cls): + @classmethod + def do_job(cls): print("checking code quality ...") cls.GetPylintMessageList() - - RES_all=dict() + + RES_all = dict() with StringIO() as StdOutput: - JsonContent="" + JsonContent = "" with redirect_stdout(StdOutput): - pylint_Run(['--output-format=json,parseable', - '--disable=invalid-name', - '--ignore=_version.py', - '--reports=y', - '--score=yes', - '--max-line-length=140', - 'src.' + cls.pyproject['project']['name']], exit=False) - - with open(cls.get_result_dir()/"report.json","w+", encoding='utf-8') as Outfile: + pylint_Run( + [ + "--load-plugins=pylint.extensions.mccabe", + "--output-format=json,parseable", + "--disable=invalid-name", + "--ignore=_version.py", + "--reports=y", + "--score=yes", + "--max-line-length=140", + "src." + cls.pyproject["project"]["name"], + ], + exit=False, + ) + + with open(cls.get_result_dir() / "report.json", "w+", encoding="utf-8") as Outfile: # hacky way of exctracting json + having overall score... class TScanState(Enum): - TEXT_REPORT = 1 - JSON_REPORT = 2 - OTHER_REPORT_START = 3 - OTHER_REPORT_STATISTICS = 4 - OTHER_REPORT_METRICS = 5 - OTHER_REPORT_DUPLICATION = 6 - OTHER_REPORT_MESSAGES_CAT = 7 - OTHER_REPORT_MESSAGES = 8 - OTHER_REPORT_END = 99 - - RES_all['Statistics'] = dict() - RES_all['RawMetrics'] = dict() - RES_all['RawMetricsPercent'] = dict() - RES_all['Duplication'] = dict() - RES_all['MessagesCat'] = dict() - RES_all['Messages'] = dict() - RES_all['GlobalScore'] = -999 - RES_all['NbAnalysedStatments'] = -999 - RES_all['NbAnalysedLines'] = -999 - + TEXT_REPORT = 1 + JSON_REPORT = 2 + OTHER_REPORT_START = 3 + OTHER_REPORT_STATISTICS = 4 + OTHER_REPORT_METRICS = 5 + OTHER_REPORT_DUPLICATION = 6 + OTHER_REPORT_MESSAGES_CAT = 7 + OTHER_REPORT_MESSAGES = 8 + OTHER_REPORT_END = 99 + + RES_all["Statistics"] = dict() + RES_all["RawMetrics"] = dict() + RES_all["RawMetricsPercent"] = dict() + RES_all["Duplication"] = dict() + RES_all["MessagesCat"] = dict() + RES_all["Messages"] = dict() + RES_all["GlobalScore"] = -999 + RES_all["NbAnalysedStatments"] = -999 + RES_all["NbAnalysedLines"] = -999 + ScanState = TScanState.TEXT_REPORT - for line in StdOutput.getvalue().split('\n'): - print(line) - if ScanState == TScanState.TEXT_REPORT: + for line in StdOutput.getvalue().split("\n"): + print(line) + if ScanState == TScanState.TEXT_REPORT: # ignoring this part, we need json - if line=='[': - JsonContent+=line + if line == "[": + JsonContent += line ScanState = TScanState.JSON_REPORT - elif line=='[]': - JsonContent+=line + elif line == "[]": + JsonContent += line ScanState = TScanState.OTHER_REPORT_START - - elif ScanState == TScanState.JSON_REPORT: - JsonContent+=line - if line==']': + + elif ScanState == TScanState.JSON_REPORT: + JsonContent += line + if line == "]": ScanState = TScanState.OTHER_REPORT_START - - elif ScanState == TScanState.OTHER_REPORT_START: - if res:=re.search(r"^(\d+)(?= statements analysed.)",line): - RES_all['NbAnalysedStatments'] = float(res.group(1)) + + elif ScanState == TScanState.OTHER_REPORT_START: + if res := re.search(r"^(\d+)(?= statements analysed.)", line): + RES_all["NbAnalysedStatments"] = float(res.group(1)) if line == "Statistics by type": ScanState = TScanState.OTHER_REPORT_STATISTICS - - elif ScanState == TScanState.OTHER_REPORT_STATISTICS: - if res:=re.search(r"^(\d+)(?= lines have been analyzed)",line): - RES_all['NbAnalysedLines' ]= float(res.group(1)) + + elif ScanState == TScanState.OTHER_REPORT_STATISTICS: + if res := re.search(r"^(\d+)(?= lines have been analyzed)", line): + RES_all["NbAnalysedLines"] = float(res.group(1)) elif line == "Raw metrics": ScanState = TScanState.OTHER_REPORT_METRICS else: - with suppress(PyLintMetricNotFound): RES_all['Statistics']['module'] = cls.TryExtractPYReportMetric(line,"module") - with suppress(PyLintMetricNotFound): RES_all['Statistics']['class'] = cls.TryExtractPYReportMetric(line,"class") - with suppress(PyLintMetricNotFound): RES_all['Statistics']['method'] = cls.TryExtractPYReportMetric(line,"method") - with suppress(PyLintMetricNotFound): RES_all['Statistics']['function'] = cls.TryExtractPYReportMetric(line,"function") - - elif ScanState == TScanState.OTHER_REPORT_METRICS: + with suppress(PyLintMetricNotFound): + RES_all["Statistics"]["module"] = cls.TryExtractPYReportMetric(line, "module") + with suppress(PyLintMetricNotFound): + RES_all["Statistics"]["class"] = cls.TryExtractPYReportMetric(line, "class") + with suppress(PyLintMetricNotFound): + RES_all["Statistics"]["method"] = cls.TryExtractPYReportMetric(line, "method") + with suppress(PyLintMetricNotFound): + RES_all["Statistics"]["function"] = cls.TryExtractPYReportMetric(line, "function") + + elif ScanState == TScanState.OTHER_REPORT_METRICS: if line == "Duplication": - RES_all['RawMetricsPercent']['code'] = RES_all['RawMetrics']['code'] / RES_all['NbAnalysedLines' ] - RES_all['RawMetricsPercent']['docstring'] = RES_all['RawMetrics']['docstring'] / RES_all['NbAnalysedLines' ] - RES_all['RawMetricsPercent']['comment'] = RES_all['RawMetrics']['comment'] / RES_all['NbAnalysedLines' ] - RES_all['RawMetricsPercent']['empty'] = RES_all['RawMetrics']['empty'] / RES_all['NbAnalysedLines' ] + RES_all["RawMetricsPercent"]["code"] = RES_all["RawMetrics"]["code"] / RES_all["NbAnalysedLines"] + RES_all["RawMetricsPercent"]["docstring"] = RES_all["RawMetrics"]["docstring"] / RES_all["NbAnalysedLines"] + RES_all["RawMetricsPercent"]["comment"] = RES_all["RawMetrics"]["comment"] / RES_all["NbAnalysedLines"] + RES_all["RawMetricsPercent"]["empty"] = RES_all["RawMetrics"]["empty"] / RES_all["NbAnalysedLines"] ScanState = TScanState.OTHER_REPORT_DUPLICATION else: - with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['code'] = cls.TryExtractPYReportMetric(line,"code") - with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['docstring'] = cls.TryExtractPYReportMetric(line,"docstring") - with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['comment'] = cls.TryExtractPYReportMetric(line,"comment") - with suppress(PyLintMetricNotFound): RES_all['RawMetrics']['empty'] = cls.TryExtractPYReportMetric(line,"empty") - - elif ScanState == TScanState.OTHER_REPORT_DUPLICATION: + with suppress(PyLintMetricNotFound): + RES_all["RawMetrics"]["code"] = cls.TryExtractPYReportMetric(line, "code") + with suppress(PyLintMetricNotFound): + RES_all["RawMetrics"]["docstring"] = cls.TryExtractPYReportMetric(line, "docstring") + with suppress(PyLintMetricNotFound): + RES_all["RawMetrics"]["comment"] = cls.TryExtractPYReportMetric(line, "comment") + with suppress(PyLintMetricNotFound): + RES_all["RawMetrics"]["empty"] = cls.TryExtractPYReportMetric(line, "empty") + + elif ScanState == TScanState.OTHER_REPORT_DUPLICATION: if line == "Messages by category": ScanState = TScanState.OTHER_REPORT_MESSAGES_CAT else: - with suppress(PyLintMetricNotFound): RES_all['Duplication']['NbDupLines'] = cls.TryExtractPYReportMetric(line,"nb duplicated lines") - with suppress(PyLintMetricNotFound): RES_all['Duplication']['PersentDuplicatedLines'] = cls.TryExtractPYReportMetric(line,"percent duplicated lines") - - elif ScanState == TScanState.OTHER_REPORT_MESSAGES_CAT: + with suppress(PyLintMetricNotFound): + RES_all["Duplication"]["NbDupLines"] = cls.TryExtractPYReportMetric(line, "nb duplicated lines") + with suppress(PyLintMetricNotFound): + RES_all["Duplication"]["PersentDuplicatedLines"] = cls.TryExtractPYReportMetric( + line, "percent duplicated lines" + ) + + elif ScanState == TScanState.OTHER_REPORT_MESSAGES_CAT: if line == "Messages": ScanState = TScanState.OTHER_REPORT_MESSAGES else: - with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Convention'] = cls.TryExtractPYReportMetric(line,"convention") - with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Refactor'] = cls.TryExtractPYReportMetric(line,"refactor") - with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Warning'] = cls.TryExtractPYReportMetric(line,"warning") - with suppress(PyLintMetricNotFound): RES_all['MessagesCat']['Error'] = cls.TryExtractPYReportMetric(line,"error") - - elif ScanState == TScanState.OTHER_REPORT_MESSAGES: + with suppress(PyLintMetricNotFound): + RES_all["MessagesCat"]["Convention"] = cls.TryExtractPYReportMetric(line, "convention") + with suppress(PyLintMetricNotFound): + RES_all["MessagesCat"]["Refactor"] = cls.TryExtractPYReportMetric(line, "refactor") + with suppress(PyLintMetricNotFound): + RES_all["MessagesCat"]["Warning"] = cls.TryExtractPYReportMetric(line, "warning") + with suppress(PyLintMetricNotFound): + RES_all["MessagesCat"]["Error"] = cls.TryExtractPYReportMetric(line, "error") + + elif ScanState == TScanState.OTHER_REPORT_MESSAGES: # approx match because the number of '-' depend on screen width.. if line.startswith("--------"): ScanState = TScanState.OTHER_REPORT_END else: for PylintMessage in cls.PylintMessageList.keys(): - with suppress(PyLintMetricNotFound): RES_all['Messages'][PylintMessage] = cls.TryExtractPYReportMetric(line,PylintMessage) - - elif ScanState == TScanState.OTHER_REPORT_END: - if res:=re.search(r"(?<=Your code has been rated at )(\d+(?:\.\d+)?)/10",line): - RES_all['GlobalScore']=float(res.group(1)) - print(RES_all['GlobalScore']) + with suppress(PyLintMetricNotFound): + RES_all["Messages"][PylintMessage] = cls.TryExtractPYReportMetric(line, PylintMessage) + + elif ScanState == TScanState.OTHER_REPORT_END: + if res := re.search(r"(?<=Your code has been rated at )(\d+(?:\.\d+)?)/10", line): + RES_all["GlobalScore"] = float(res.group(1)) + print(RES_all["GlobalScore"]) else: raise RuntimeError("Invalid ScanState") Outfile.write(JsonContent) - - with open(cls.get_result_dir()/"metrics.json", 'w') as json_file: + + with open(cls.get_result_dir() / "metrics.json", "w") as json_file: json.dump(RES_all, json_file) - - # exporting all Data in one csv, unused atm because jenkins seems not able to select columns from csv an keep displaying all... + + # exporting all Data in one csv, unused atm because jenkins seems not able to select columns from csv an keep displaying all... # => to export a working full csv we need to a 'flat' dict (no more nested dict) RES_all_trim = copy.deepcopy(RES_all) del RES_all_trim["Messages"] - flat_RES_all=pandas.json_normalize(RES_all_trim, sep='_').to_dict(orient='records')[0] - - with open(cls.get_result_dir()/"metrics.csv", 'w', newline='') as csv_file: - writer = csv.DictWriter(csv_file,fieldnames=flat_RES_all.keys()) + flat_RES_all = pandas.json_normalize(RES_all_trim, sep="_").to_dict(orient="records")[0] + + with open(cls.get_result_dir() / "metrics.csv", "w", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=flat_RES_all.keys()) writer.writeheader() writer.writerow(flat_RES_all) - + # splited csv exports for jenkins plots: RawMetricsPercent RES_all_percent = RES_all["RawMetricsPercent"] - with open(cls.get_result_dir()/"metrics_rawpercent.csv", 'w', newline='') as csv_file: - writer = csv.DictWriter(csv_file,fieldnames=RES_all_percent.keys()) + with open(cls.get_result_dir() / "metrics_rawpercent.csv", "w", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=RES_all_percent.keys()) writer.writeheader() writer.writerow(RES_all_percent) # splited csv exports for jenkins plots: Statistics + Duplication + NbAnalysedStatments + NbAnalysedLines RES_all_stats = copy.deepcopy(RES_all["Statistics"]) - RES_all_stats["NbDupLines"] = RES_all['Duplication']['NbDupLines'] - RES_all_stats["PersentDuplicatedLines"] = RES_all['Duplication']['PersentDuplicatedLines'] - RES_all_stats["NbAnalysedStatments"] = RES_all['NbAnalysedStatments' ] - RES_all_stats["NbAnalysedLines"] = RES_all['NbAnalysedLines' ] - with open(cls.get_result_dir()/"metrics_Statistics.csv", 'w', newline='') as csv_file: - writer = csv.DictWriter(csv_file,fieldnames=RES_all_stats.keys()) + RES_all_stats["NbDupLines"] = RES_all["Duplication"]["NbDupLines"] + RES_all_stats["PersentDuplicatedLines"] = RES_all["Duplication"]["PersentDuplicatedLines"] + RES_all_stats["NbAnalysedStatments"] = RES_all["NbAnalysedStatments"] + RES_all_stats["NbAnalysedLines"] = RES_all["NbAnalysedLines"] + with open(cls.get_result_dir() / "metrics_Statistics.csv", "w", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=RES_all_stats.keys()) writer.writeheader() writer.writerow(RES_all_stats) - + # splited csv exports for jenkins plots: Statistics + Duplication - RES_all_MessagesCat = RES_all['MessagesCat'] - with open(cls.get_result_dir()/"metrics_MessagesCat.csv", 'w', newline='') as csv_file: - writer = csv.DictWriter(csv_file,fieldnames=RES_all_MessagesCat.keys()) + RES_all_MessagesCat = RES_all["MessagesCat"] + with open(cls.get_result_dir() / "metrics_MessagesCat.csv", "w", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=RES_all_MessagesCat.keys()) writer.writeheader() writer.writerow(RES_all_MessagesCat) - + # splited csv exports for jenkins plots: GlobalScore - RES_GlobalScore = {'GlobalScore': RES_all['GlobalScore']} - with open(cls.get_result_dir()/"metrics_GlobalScore.csv", 'w', newline='') as csv_file: - writer = csv.DictWriter(csv_file,fieldnames=RES_GlobalScore.keys()) + RES_GlobalScore = {"GlobalScore": RES_all["GlobalScore"]} + with open(cls.get_result_dir() / "metrics_GlobalScore.csv", "w", newline="") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=RES_GlobalScore.keys()) writer.writeheader() writer.writerow(RES_GlobalScore) - + # converting the report using pylint_json2html (/!\ internal API, but as their is no leading '_' ...) - with open(cls.get_result_dir()/"report.html","w+", encoding='utf-8') as Outfile: + with open(cls.get_result_dir() / "report.html", "w+", encoding="utf-8") as Outfile: raw_data = json.loads(JsonContent) - report=pylint_json2html.Report( raw_data) + report = pylint_json2html.Report(raw_data) Outfile.write(report.render()) - + print("Done") diff --git a/helpers/types_check.py b/helpers/types_check.py index 4816e0c..6179581 100644 --- a/helpers/types_check.py +++ b/helpers/types_check.py @@ -16,36 +16,46 @@ from mypy import api from .helper_base import helper_withresults_base -class types_check(helper_withresults_base): +class types_check(helper_withresults_base): JUnitReportName = "junit.xml" - - @classmethod - def do_job(cls): + + @classmethod + def do_job(cls): print("checking code typing ...") - result = api.run([ # project path - "-m", - "src." + str(cls.pyproject['project']['name']), - # analysis configuration - "--ignore-missing-imports", - "--strict-equality", - # reports generation - "--cobertura-xml-report", str(cls.get_result_dir()), - "--html-report", str(cls.get_result_dir()), - "--linecount-report", str(cls.get_result_dir()), - "--linecoverage-report", str(cls.get_result_dir()), - "--lineprecision-report", str(cls.get_result_dir()), - "--txt-report", str(cls.get_result_dir()), - "--xml-report", str(cls.get_result_dir()), - "--junit-xml", str(cls.get_result_dir()) + "/" + cls.JUnitReportName - ]) - + result = api.run( + [ # project path + "-m", + "src." + str(cls.pyproject["project"]["name"]), + # analysis configuration + "--ignore-missing-imports", + "--strict-equality", + # reports generation + "--cobertura-xml-report", + str(cls.get_result_dir()), + "--html-report", + str(cls.get_result_dir()), + "--linecount-report", + str(cls.get_result_dir()), + "--linecoverage-report", + str(cls.get_result_dir()), + "--lineprecision-report", + str(cls.get_result_dir()), + "--txt-report", + str(cls.get_result_dir()), + "--xml-report", + str(cls.get_result_dir()), + "--junit-xml", + str(cls.get_result_dir()) + "/" + cls.JUnitReportName, + ] + ) + if result[0]: - print('\nType checking report:\n') + print("\nType checking report:\n") print(result[0]) # stdout - + if result[1]: - print('\nError report:\n') + print("\nError report:\n") print(result[1]) # stderr - - print('\nExit status:', result[2]) - print("Done") \ No newline at end of file + + print("\nExit status:", result[2]) + print("Done") diff --git a/helpers/unit_test.py b/helpers/unit_test.py index 571f2da..4d9d6a3 100644 --- a/helpers/unit_test.py +++ b/helpers/unit_test.py @@ -21,34 +21,36 @@ from junit2htmlreport import parser as junit2html_parser from .helper_base import helper_withresults_base -class unit_test(helper_withresults_base): - enable_coverage_check: bool = False - enable_xml_export: bool = True +class unit_test(helper_withresults_base): + enable_coverage_check: bool = False + enable_xml_export: bool = True enable_full_xml_export: bool = True - FullReportName: str = "full_report" - CoverageReportName: str = "test_coverage" - - @classmethod - def do_job(cls): - if cls.enable_coverage_check==True: + FullReportName: str = "full_report" + CoverageReportName: str = "test_coverage" + + @classmethod + def do_job(cls): + if cls.enable_coverage_check == True: import coverage - + # preparing unittest framework test_loader = unittest.TestLoader() - + if cls.enable_coverage_check == True: - #we start coverage now because module files discovery is part of the coverage measurement - CoverageReportPath = Path(str(cls.get_result_dir())+"_coverage") + # we start coverage now because module files discovery is part of the coverage measurement + CoverageReportPath = Path(str(cls.get_result_dir()) + "_coverage") cls._reset_dir(CoverageReportPath) - cov = coverage.Coverage(cover_pylib=False,branch=True,source_pkgs=["src."+cls.pyproject['project']['name']]) + cov = coverage.Coverage(cover_pylib=False, branch=True, source_pkgs=["src." + cls.pyproject["project"]["name"]]) cov.start() - - package_tests = test_loader.discover(start_dir=str(cls.project_rootdir_path / "test"),top_level_dir=str(cls.project_rootdir_path / "test")) + + package_tests = test_loader.discover( + start_dir=str(cls.project_rootdir_path / "test"), top_level_dir=str(cls.project_rootdir_path / "test") + ) if cls.enable_xml_export: testRunner = xmlrunner.XMLTestRunner(output=str(str(cls.get_result_dir()))) else: testRunner = unittest.TextTestRunner() - + # running the test testRunner.run(package_tests) @@ -57,23 +59,23 @@ class unit_test(helper_withresults_base): cov.stop() cov.save() cov.html_report(directory=str(CoverageReportPath)) - cov.xml_report(outfile=(CoverageReportPath/f"{cls.CoverageReportName}.xml")) - + cov.xml_report(outfile=(CoverageReportPath / f"{cls.CoverageReportName}.xml")) + # computing results (Only if xml available) if cls.enable_full_xml_export == True: print("Full reports generation...") - FullReportPath = Path(str(cls.get_result_dir())+"_full") + FullReportPath = Path(str(cls.get_result_dir()) + "_full") cls._reset_dir(FullReportPath) - + FullJUnitReport = JUnitXml() - for fname in [fname for fname in os.listdir(cls.get_result_dir()) if fname.endswith('.xml')]: - FullJUnitReport+=JUnitXml.fromfile(str(cls.get_result_dir() / fname)) - + for fname in [fname for fname in os.listdir(cls.get_result_dir()) if fname.endswith(".xml")]: + FullJUnitReport += JUnitXml.fromfile(str(cls.get_result_dir() / fname)) + current_datetime = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") full_report_base_name = f'{cls.pyproject["project"]["name"]}-{cls.FullReportName}-{current_datetime}' - FullJUnitReport.write(str(FullReportPath / f'{full_report_base_name}.xml')) - report = junit2html_parser.Junit(FullReportPath/ f'{full_report_base_name}.xml') + FullJUnitReport.write(str(FullReportPath / f"{full_report_base_name}.xml")) + report = junit2html_parser.Junit(FullReportPath / f"{full_report_base_name}.xml") html = report.html() - with open(FullReportPath/ f'{full_report_base_name}.html', "wb") as outfile: - outfile.write(html.encode('utf-8')) - print("Done") \ No newline at end of file + with open(FullReportPath / f"{full_report_base_name}.html", "wb") as outfile: + outfile.write(html.encode("utf-8")) + print("Done") diff --git a/mkdocs.yml b/mkdocs.yml index 2a90602..f8fa45d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,6 @@ # You should have received a copy of the license along with this # work. If not, see . - docs_dir: docs site_name: pychangelogfactory site_url: https://chacha.ddns.net/mkdocs-web/chacha/pychangelogfactory/latest/ @@ -14,49 +13,80 @@ site_description: A simple changelog builder that you can feed with your reposit site_author: chacha repo_url: https://chacha.ddns.net/gitea/chacha/pychangelogfactory use_directory_urls: false +copyright: CC BY-NC-SA 4.0 theme: name: material features: - - navigation.instant - - navigation.tracking - - navigation.tabs - - navigation.tabs.sticky - - toc.integrate - - navigation.top + - navigation.instant + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - toc.integrate + - navigation.top palette: - - media: '(prefers-color-scheme: dark)' - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to system preference - - media: (prefers-color-scheme) - toggle: - icon: material/brightness-auto - name: Switch to light mode - - media: '(prefers-color-scheme: light)' - scheme: default - toggle: - icon: material/brightness-7 - name: Switch to dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + - media: (prefers-color-scheme) + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode plugins: -- search -- localsearch -- autorefs -- mkdocstrings: - default_handler: python - handlers: - python: - selection: - filters: - - '!^_(?!_init__)' - inherited_members: true - rendering: - show_root_heading: false - show_root_toc_entry: false - show_root_full_path: false - show_if_no_docstring: true - show_signature_annotations: true - show_source: false - heading_level: 2 - group_by_category: true - show_category_heading: true + - search + - markdownextradata + - mermaid2 + - localsearch + - autorefs + - mkdocstrings: + default_handler: python + handlers: + python: + selection: + filters: + - '!^_(?!_init__)' + inherited_members: true + rendering: + show_root_heading: false + show_root_toc_entry: false + show_root_full_path: false + show_if_no_docstring: true + show_signature_annotations: true + show_source: false + heading_level: 2 + group_by_category: true + show_category_heading: true +markdown_extensions: + - def_list + - tables + - attr_list + - abbr + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.critic + - pymdownx.details + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.keys + - pymdownx.mark + - pymdownx.progressbar + - pymdownx.smartsymbols + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + - footnotes + +extra: + branch: master + repository: pygitversionhelper \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5b4672f..f689a2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,10 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] version_scheme= "post-release" -# tag_regex="^(?:v)?(?P\\d+\\.\\d+\\.\\d+)([\\.\\-\\+])?(?:.*)?" [project] name = "pychangelogfactory" -description = "A simple changelog builder that you can feed with your repository change history" +description = "A simple changelog builder that you can feed with your repository change history." readme = "README.md" requires-python = ">=3.9" keywords = ["chacha","chacha","template","pychangelogfactory"] @@ -57,9 +56,10 @@ Tracker = "https://chacha.ddns.net/gitea/chacha/pychangelogfactory/issue [project.optional-dependencies] test = ["junitparser>=2.8","junit2html>=30.1","xmlrunner>=1.7","mypy>=0.99" ] coverage-check = ["coverage>=7.0"] +complexity-check = ["radon>=5.1"] quality-check = ["pylint>=2.15","pylint-json2html>=0.4","pandas>=1.5"] type-check = ["mypy[reports]>=0.99" ] -doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0"] +doc-gen = ["mkdocs>=1.4.0", "mkdocs-material>=8.5", "mkdocs-localsearch>=0.9.0", "mkdocstrings[python]>=0.19", "mkdocs-with-pdf>=0.9.3","pyyaml>=6.0","pymdown-extensions>=9","mkdocs-markdownextradata-plugin","mkdocs-mermaid2-plugin"] #[project.scripts] #my-script = "my_package.module:function" diff --git a/src/pychangelogfactory/__init__.py b/src/pychangelogfactory/__init__.py index 80cc636..2babfbc 100644 --- a/src/pychangelogfactory/__init__.py +++ b/src/pychangelogfactory/__init__.py @@ -14,9 +14,10 @@ from importlib.metadata import version, PackageNotFoundError try: # pragma: no cover __version__ = version("pychangelogfactory") -except PackageNotFoundError: # pragma: no cover +except PackageNotFoundError: # pragma: no cover import warnings + warnings.warn("can not read __version__, assuming local test context, setting it to ?.?.?") __version__ = "?.?.?" -from pychangelogfactory.changelogfactory import ChangeLogFormater +from .changelogfactory import ChangeLogFormater diff --git a/src/pychangelogfactory/changelogfactory.py b/src/pychangelogfactory/changelogfactory.py index 61cbf99..99ab8bb 100644 --- a/src/pychangelogfactory/changelogfactory.py +++ b/src/pychangelogfactory/changelogfactory.py @@ -9,7 +9,7 @@ # You should have received a copy of the license along with this # work. If not, see . -"""Phasellus tellus lectus, volutpat eu dapibus ut, suscipit vel augue. +""" A simple changelog formater that consume merged commit message and produce nice pre-formated changelogs """ @@ -18,127 +18,212 @@ from __future__ import annotations import re from abc import ABC -def ChangeLogFormaterRecordType(Klass:type) -> type: + +def ChangeLogFormaterRecordType(Klass: type) -> type: + """decorator helper function to register interface implementation""" ChangeLogFormater.ar_Klass.append(Klass) return Klass -class ChangeLogFormater(ABC): - ar_Klass:list[ChangeLogFormater] = [] - ar_LinesResult:list[ChangeLogFormater] = [] - prefix:str = '' - title:str = 'Others :' - keywords:list[str] = [] - priority:int = 0 - def __init__(self,scope:str | None, ChangelogString:str): +class ChangeLogFormater(ABC): + """the main changelog class that define nearly everythings. + This was supposed to be a very shorty script this is why it is all-in-one... + Factory and base-object are mixed. + """ + + ar_Klass: list[ChangeLogFormater] = [] + ar_LinesResult: list[ChangeLogFormater] = [] + prefix: str = "" + title: str = "Others :" + keywords: list[str] = [] + priority: int = 0 + + def __init__(self, scope: str | None, ChangelogString: str): self._scope = scope self._ChangelogString = ChangelogString.strip() def RenderLine(self): + """return a rendered line""" return self._ChangelogString.strip() - @classmethod + @classmethod def RenderLines(cls) -> str: + """render all lines""" changelog_category: str = "" lines = cls.GetLines() - if len(lines)>0: + if len(lines) > 0: changelog_category = f"#### {cls.title}\n" for line in lines: changelog_category = changelog_category + f"> {line.RenderLine()}" - if (scope:=line.GetScope()) != "": + if (scope := line.GetScope()) != "": changelog_category = changelog_category + f"\t*[{scope}]*" changelog_category = changelog_category + "\n" return changelog_category - + def GetScope(self) -> str: - return self._scope if self._scope != None else "" + """return the current scope (category)""" + return self._scope if self._scope is not None else "" @classmethod def Clear(cls) -> None: - ChangeLogFormater.ar_LinesResult=[] + """clear internal memory""" + ChangeLogFormater.ar_LinesResult = [] @classmethod - def _CheckLine(cls,content:str) -> bool: - regex=re.compile(f"^(?:-\s+)?(?:{cls.prefix})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)") - return regex.match(content) - + def CheckLine(cls, content: str) -> bool: + """check if a line is in the current scope (lazy identification)""" + regex = re.compile(r"^(?:-\s+)?(?:{0})(?:\((.*)\))?(?::)(?:\s*)([^\s].+)".format(cls.prefix)) + _match = regex.match(content) + return _match + @classmethod - def _CheckLine_keywords(cls,content:str) -> bool: - keyword_list = (cls.keywords + list((cls.prefix,))) + def CheckLine_keywords(cls, content: str) -> bool: + """check if a line is in the current scope (deeper in-word identification)""" + keyword_list = cls.keywords for _keyword in keyword_list: - if ( _keyword != "") and re.search(_keyword,content): + if (_keyword != "") and re.search(_keyword, content): return True return False - - @classmethod - def FactoryProcessLine(cls,RawChangelogLine:str) -> ChangeLogFormater: - - for Klass in sorted(ChangeLogFormater.ar_Klass,key=lambda x: x.priority): - if content:=Klass._CheckLine(RawChangelogLine): - return Klass(content.group(1),content.group(2)) - - for Klass in sorted(ChangeLogFormater.ar_Klass,key=lambda x: x.priority,reverse=True): - if content:=Klass._CheckLine_keywords(RawChangelogLine): - return Klass(None,RawChangelogLine) - - return ChangeLogFormater_others(None,RawChangelogLine) @classmethod - def FactoryProcessFullChangelog(cls,RawChangelogMessage:str) -> list[ChangeLogFormater]: - LinesResult=[] - for line in RawChangelogMessage.split('\n'): - if(line.strip()!=""): - LinesResult.append(cls.FactoryProcessLine(line)) - ChangeLogFormater.ar_LinesResult=LinesResult - return LinesResult + def FactoryProcessLineMain(cls, RawChangelogLine: str) -> ChangeLogFormater: + """Process a line and look for identified ones""" + for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority): + content = Klass.CheckLine(RawChangelogLine) + if content is not None: + return Klass(content.group(1), content.group(2)) + return ChangeLogFormater_others(None, RawChangelogLine) @classmethod - def GetLinesOfType(cls,Klass:type) -> list[ChangeLogFormater]: - return [_ for _ in ChangeLogFormater.ar_LinesResult if type(_)==Klass] + def FactoryProcessLineSecond(cls, RawChangelogLine: str) -> ChangeLogFormater: + """Process a line and look for non-identified ones""" + for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True): + if Klass.CheckLine_keywords(RawChangelogLine): + return Klass(None, RawChangelogLine) + + return ChangeLogFormater_others(None, RawChangelogLine) + + @classmethod + def FactoryProcessFullChangelog(cls, RawChangelogMessage: str) -> list[ChangeLogFormater]: + """Process all input lines""" + LinesResult = [] + Lines2ndRound = [] + + for line in RawChangelogMessage.split("\n"): + if line.strip() != "": + res = cls.FactoryProcessLineMain(line) + if res is not ChangeLogFormater_others: + LinesResult.append(res) + else: + Lines2ndRound.append(line) + + for line in Lines2ndRound: + LinesResult.append(cls.FactoryProcessLineSecond(line)) + + ChangeLogFormater.ar_LinesResult = LinesResult + return ChangeLogFormater.ar_LinesResult + + @classmethod + def GetLinesOfType(cls, Klass: type) -> list[ChangeLogFormater]: + """retrieve all lines of specified type""" + return [_ for _ in ChangeLogFormater.ar_LinesResult if isinstance(_, Klass)] @classmethod def GetLines(cls) -> list[ChangeLogFormater]: + """retrieve all lines for the current formater""" return ChangeLogFormater.GetLinesOfType(cls) @classmethod def RenderFullChangelog(cls) -> str: + """render the main changelog""" full_changelog = "" - for Klass in ChangeLogFormater.ar_Klass: + for Klass in sorted(ChangeLogFormater.ar_Klass, key=lambda x: x.priority, reverse=True): full_changelog = full_changelog + Klass.RenderLines() return full_changelog + +# to avoid writing class, they are initialized with the following structure: # creating category classes: '': (priority, ['',...], '
') -for RecordType, Config in { 'breaking': (20, ["break",], ':rotating_light: Breaking changes :rotating_light::'), - 'feat': (20, ["feat","new","create"], 'Features :sparkles::'), - 'fix': (10, ["issue","problem"], 'Fixes :wrench::'), - 'security': (20, ["safe","leak"], 'Security :shield::'), - 'chore': (20, ["task","refactor","build","better"], 'Chore :building_construction::'), - 'perf': (0, ["fast",], 'Performance Enhancements :rocket::'), - 'wip': (0, ["temp",], 'Work in progress changes :construction::'), - 'docs': (0, ["doc",], 'Documentations :book::'), - 'style': (30, ["beautify",], 'Style :art::'), - 'refactor': (0, [], 'Refactorings :recycle::'), - 'ci': (0, [""], 'Continuous Integration :cyclone::'), - 'test': (15, ["unittest","check"], 'Testings :vertical_traffic_light::'), - 'build': (0, ["compile","version"], 'Builds :package:') - }.items(): - name=f"ChangeLogFormater_{RecordType}" - tmp = globals()[name] = type(name, (ChangeLogFormater,),{"prefix": RecordType,"title": Config[2],"keywords":Config[1],"priority":Config[0] }) +for RecordType, Config in { + "break": ( + 20, + [], + ":rotating_light: Breaking changes :rotating_light::", + ), + "feat": (20, ["feat", "new", "create", "add"], "Features :sparkles::"), + "fix": (10, ["issue", "problem"], "Fixes :wrench::"), + "security": (20, ["safe", "leak"], "Security :shield::"), + "chore": ( + 20, + ["task", "refactor", "build", "better", "improve"], + "Chore :building_construction::", + ), + "perf": ( + 0, + [ + "fast", + ], + "Performance Enhancements :rocket::", + ), + "wip": ( + 0, + [ + "temp", + ], + "Work in progress changes :construction::", + ), + "docs": ( + 0, + [ + "doc", + ], + "Documentations :book::", + ), + "style": ( + 5, + [ + "beautify", + ], + "Style :art::", + ), + "refactor": (0, [], "Refactorings :recycle::"), + "ci": (0, ["jenkins", "git"], "Continuous Integration :cyclone::"), + "test": (15, ["unittest", "check", r"^(?:\s)*test(?:\s)*$"], "Testings :vertical_traffic_light::"), + "build": (0, ["compile", "version"], "Builds :package:"), +}.items(): + # then we instantiate all of them + name = f"ChangeLogFormater_{RecordType}" + tmp = globals()[name] = type( + name, + (ChangeLogFormater,), + { + "prefix": RecordType, + "title": Config[2], + "keywords": Config[1], + "priority": Config[0], + }, + ) ChangeLogFormater.ar_Klass.append(tmp) + @ChangeLogFormaterRecordType class ChangeLogFormater_revert(ChangeLogFormater): - prefix:str = 'revert' - title:str = 'Reverts :back::' - keywords:list[str] = ["fallback"] - priority:int = 0 + """revert scope formater""" + + prefix: str = "revert" + title: str = "Reverts :back::" + keywords: list[str] = ["fallback"] + priority: int = 0 def RenderLine(self) -> str: - return "~~"+super().RenderLine()+"~~" + return "~~" + super().RenderLine() + "~~" + @ChangeLogFormaterRecordType class ChangeLogFormater_others(ChangeLogFormater): - prefix:str = 'other' - title:str = 'Others :question::' - keywords:list[str] = [""] - priority:int = -20 \ No newline at end of file + """others / unknown scope formater""" + + prefix: str = "other" + title: str = "Others :question::" + keywords: list[str] = [""] + priority: int = -20 diff --git a/test/test_changelogfactory.py b/test/test_changelogfactory.py index 75de808..a9e1301 100644 --- a/test/test_changelogfactory.py +++ b/test/test_changelogfactory.py @@ -7,62 +7,93 @@ # work. If not, see . import unittest -from io import StringIO -from contextlib import redirect_stdout,redirect_stderr - -print(__name__) -print(__package__) from src import pychangelogfactory - -test_commitlog_1=""" -breaking: test1 -feat: test2 -chore: test3 -security: test4 -style:test5 -fix: test6 -wip: test7 -perf: test8 -refactor: test9 -ci: test10 -docs: test11 -test: test12 -build: test13 -- fix: test14 -revert: test15 -toto:un autre cas -fdsfdsfdsfsdfsdff -""" - - - - - class Testtest_module(unittest.TestCase): - def test_simplegeneration_full(self): - pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(test_commitlog_1) + def simplegeneration(self, inputstr, teststrs: list[str]): + pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(inputstr) changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog() - print(changelog) - self.assertIn("test1",changelog) - self.assertIn("test2",changelog) - self.assertIn("test3",changelog) - self.assertIn("test4",changelog) - self.assertIn("test5",changelog) - self.assertIn("test6",changelog) - self.assertIn("test7",changelog) - self.assertIn("test8",changelog) - self.assertIn("test9",changelog) - self.assertIn("test10",changelog) - self.assertIn("test11",changelog) - self.assertIn("test12",changelog) - self.assertIn("test13",changelog) - self.assertIn("test14",changelog) - self.assertIn("test15",changelog) - self.assertIn("un autre cas",changelog) - self.assertIn("fdsfdsfdsfsdfsdff",changelog) + for test in teststrs: + self.assertIn(test, changelog) + def test_simplegeneration_order(self): + raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle" + "\n" + "test: testtest" + pychangelogfactory.ChangeLogFormater.FactoryProcessFullChangelog(raw) + changelog = pychangelogfactory.ChangeLogFormater.RenderFullChangelog().splitlines() + self.assertIn("testbreak", changelog[1]) + self.assertIn("testtest", changelog[3]) + self.assertIn("teststyle", changelog[5]) + self.assertIn("testdoc", changelog[7]) - \ No newline at end of file + def test_simplegeneration_multiple(self): + raw = "break: testbreak" + "\n" + "docs: testdoc" + "\n" + "style: teststyle" + + self.simplegeneration(raw, ["testbreak", "testdoc", "teststyle"]) + + def test_simplegeneration_breaking(self): + self.simplegeneration("break: teststring", ["teststring"]) + self.simplegeneration("test break", ["test break"]) + + def test_simplegeneration_features(self): + self.simplegeneration("feat: teststring", ["teststring"]) + self.simplegeneration("test feat", ["test feat"]) + self.simplegeneration("test new", ["test new"]) + self.simplegeneration("test create", ["test create"]) + self.simplegeneration("test add", ["test add"]) + + def test_simplegeneration_fix(self): + self.simplegeneration("fix: teststring", ["teststring"]) + self.simplegeneration("test fix", ["test fix"]) + self.simplegeneration("test issue", ["test issue"]) + self.simplegeneration("test problem", ["test problem"]) + + def test_simplegeneration_security(self): + self.simplegeneration("security: teststring", ["teststring"]) + self.simplegeneration("test safe", ["test safe"]) + self.simplegeneration("test leak", ["test leak"]) + + def test_simplegeneration_task(self): + self.simplegeneration("task: teststring", ["teststring"]) + self.simplegeneration("test refactor", ["test refactor"]) + self.simplegeneration("test build", ["test build"]) + self.simplegeneration("test better", ["test better"]) + self.simplegeneration("test improve", ["test improve"]) + + def test_simplegeneration_perf(self): + self.simplegeneration("perf: teststring", ["teststring"]) + self.simplegeneration("test fast", ["test fast"]) + + def test_simplegeneration_wip(self): + self.simplegeneration("wip: teststring", ["teststring"]) + self.simplegeneration("test temp", ["test temp"]) + + def test_simplegeneration_docs(self): + self.simplegeneration("docs: teststring", ["teststring"]) + self.simplegeneration("test doc", ["test doc"]) + + def test_simplegeneration_style(self): + self.simplegeneration("style: teststring", ["teststring"]) + self.simplegeneration("test beautify", ["test beautify"]) + + def test_simplegeneration_refactor(self): + self.simplegeneration("refactor: teststring", ["teststring"]) + + def test_simplegeneration_ci(self): + self.simplegeneration("ci: teststring", ["teststring"]) + self.simplegeneration("test jenkins", ["test jenkins"]) + self.simplegeneration("test git", ["test git"]) + + def test_simplegeneration_test(self): + self.simplegeneration("test: teststring", ["teststring"]) + self.simplegeneration("test unittest", ["test unittest"]) + self.simplegeneration("test check", ["test check"]) + + def test_simplegeneration_build(self): + self.simplegeneration("build: teststring", ["teststring"]) + self.simplegeneration("test compile", ["test compile"]) + self.simplegeneration("test version", ["test version"]) + + def test_simplegeneration_revert(self): + self.simplegeneration("revert: teststring", ["~~teststring~~"])