Source code for Muscat.Helpers.Check

# -*- coding: utf-8 -*-
#
# This file is subject to the terms and conditions defined in
# file 'LICENSE.txt', which is part of this source code package.
#

"""Testing infrastructure for Muscat and extra modules

    Developer documentation:

    all the import statements are placed after the treatment of the
    user option "-y" (dont_write_bytecode). So no pyc are created prior
    the default behavior

"""

from typing import Callable, List, Tuple, Dict, Optional
import sys
import traceback
import time
import getopt
import os

Test_Help_String = """
python  Muscat.Helper.Check.py -c -f -s -e <extraModules> -m <moduleFilter>
python -m Muscat.Helper.Check -c -f -s -e <extraModules> -m <moduleFilter>

options :
    -f    Full output for all the test
    -v    Activate maximal level of verbosity (implied -f)
    -s    Stop at first error
    -e    To test extra Modules (-e can be repeated)
    -m    To filter modules to test by this string (-m can be repeated) (for example -m .IO. )
    -k    Skip test base on string
    -d    Dry run do not execute anything, only show what will be executed
    -c    To activate coverage and generate a html report
          -cb (coverage with browser and local file index.html generated)
          for correct platform dependent coverage you can use the following
          comment to disconnect coverage in some platform:
          # only nt coverage
          # only posix coverage
    -l    Generation of the coverage (html) report locally (current path)
    -b    Launch browser after the coverage report generation
    -p    Activate profiling
    -t    use mypy to check the typing of every module
    -y    Generate .pyc when importing modules (default False)
    -L    Output Locally, Use the current path for all the outputs
    -P <dir> Set temporary output directory to
    -g    to use the Froze decorator during testing
"""


def __RunAndCheck(lis, bp, stopAtFirstError, dryRun, profiling, typing) -> Dict:  # pragma: no cover

    res: dict = {}
    from Muscat.Helpers.TextFormatHelper import TFormat

    for name in lis:
        bp.Print(TFormat.InBlue(TFormat.Center("Running Test " + name, width=80)))
        if lis[name] is None:
            bp.Print(TFormat().InRed(TFormat().GetIndent() + 'Sub Module "' + name + '" does not have the CheckIntegrity function'))
            r = 'Not OK'
            if stopAtFirstError:
                raise (Exception(TFormat().GetIndent() + 'Sub Module "' + name + '" does not have the CheckIntegrity function'))
        try:
            start_time: float = time.time()
            stop_time: float = time.time()
            if dryRun:
                r = "Dry Run "
            else:
                testStr = """
from {} import CheckIntegrity
res = CheckIntegrity()""".format(name)
                local = {}

                if profiling:
                    import cProfile
                    import pstats
                    from io import StringIO
                    pr = cProfile.Profile()
                    pr.enable()
                    exec(testStr, {}, local)
                    r = local["res"]
                    pr.disable()
                    s = StringIO()
                    sortby = 'cumulative'
                    pstats.Stats(pr, stream=s).sort_stats(pstats.SortKey.CUMULATIVE).print_stats(50)
                    print(s.getvalue())
                else:
                    exec(testStr, {}, local)
                    r = local["res"]
            sys.stdout.flush()
            sys.stderr.flush()
            stop_time = time.time()
            res[name] = r
            if not isinstance(r, str):
                bp.Print(TFormat.InRed(TFormat().GetIndent() + "Please add a correct return statement in the CheckIntegrity of the module" + name))
                #raise Exception()
                r = 'Not OK'

            if typing:
                import subprocess
                subprocess.call(["mypy", "--ignore-missing-imports", "-m", name])

        except UserWarning as e:
            sys.stdout.flush()
            sys.stderr.flush()
            bp.Print("Unexpected Warning:" + str(sys.exc_info()[0]))
            res[name] = "error"
            traceback.print_exc(file=bp.stdout_)
            r = 'Not OK'
        except KeyboardInterrupt as e:
            sys.stdout.flush()
            sys.stderr.flush()
            bp.Print("User interruption ")
            res[name] = "interrupted"
            r = 'Interrupted'
            bp.Restore()
            raise
        except:
            sys.stdout.flush()
            sys.stderr.flush()
            bp.Print("Unexpected Error:" + str(sys.exc_info()[0]))
            res[name] = "error"
            traceback.print_exc(file=bp.stdout_)

            r = 'Not OK'
            if stopAtFirstError:
                bp.Restore()
                raise

        if r.lower().startswith('ok'):
            bp.Print(TFormat.InGreen(f"OK {name} : {stop_time -start_time:.3f} seconds "))
        elif r.lower().startswith('skip'):
            bp.Print(TFormat.InYellow(str(r) + " !!!! " + name))
        else:
            bp.Print(TFormat.InRed(str(r) + " !!!! " + name))

    return res


def __tryImportRecursive(subModule, toCheck, stopAtFirstError, modulesToTreat, modulesToSkip) -> None:  # pragma: no cover

    import importlib
    try:
        sm = importlib.import_module(subModule)
    except ImportError as e:
        print(e)
        print("please check the value of the _test variable ")
        if stopAtFirstError:
            raise
        sm = None

    except Exception:
        sm = None
        if stopAtFirstError:
            raise

    cif = getattr(sm, "CheckIntegrity", None)
    if cif is not None:
        toCheck[subModule] = cif
    else:
        try:
            subSubModules = []
            try:
                listToTest = getattr(sm, "_test", None)
                subSubModules = [subModule + '.' + x for x in listToTest]
            except Exception as e:
                print(e)
                print('Error Loading module : ' + subModule + ' (Current folder'+os.getcwd()+')')
                print('-*-*-*-*-*-*> missing _test variable ??? <*-*-*-*-*-*--')
                raise

            for subSubModule in subSubModules:
                if any([x.lower() in ("."+subSubModule.lower()+".") for x in modulesToSkip]):
                    print("skip module :" + str(subSubModule))
                    continue
                try:
                    __tryImportRecursive(subSubModule, toCheck, stopAtFirstError, modulesToTreat, modulesToSkip)
                except Exception as e:
                    print(e)
                    print('Error Loading module : ' + subSubModule + ' (Current folder'+os.getcwd()+')')
                    print('-*-*-*-*-*-*> missing CheckIntegrity()??? <*-*-*-*-*-*--')
                    raise
        except:
            toCheck[subModule] = None
            if stopAtFirstError:
                raise


def __tryImport(noduleName, bp, stopAtFirstError, modulesToTreat, modulesToSkip) -> Dict:  # pragma: no cover

    toCheck = {}
    try:
        __tryImportRecursive(noduleName, toCheck, stopAtFirstError, modulesToTreat, modulesToSkip)
    except:
        print("Error loading module '" + noduleName + "'")
        print("This module will not be tested ")

        sys.stdout.flush()
        sys.stderr.flush()
        bp.Print("Unexpected Error:" + str(sys.exc_info()[0]))
        traceback.print_exc(file=bp.stdout_)

        if stopAtFirstError:
            raise
    return toCheck


[docs] def TestAll(modulesToTreat: List[str] = ['ALL'], modulesToSkip: List = [], extraToolsBoxes: Optional[List[str]] = None, options=None) -> Dict: # pragma: no cover """Core function to test every module present Parameters ---------- modulesToTreat : list, optional List of modules to test , by default ['ALL'] means all the modules in the current toolbox modulesToSkip : list, optional List of modules to skip every entry is a string used in a find to filter the modules to test, by default [] stopAtFirstError : bool, optional Stop at first error encountered, by default False extraToolsBoxes : List[str], optional name of the extra libraries to test, by default None, this means "Muscat" dryRun : bool, optional to just print the code to be executed, by default False profiling : bool, optional activate the profiling of the tests, by default False coverage : Dict, optional argument to control the coverage checking, by default None typing : bool, optional activate the typing checking, by default False Returns ------- Dict _description_ """ print("") print("modulesToTreat : " + str(modulesToTreat)) print("modulesToSkip : " + str(modulesToSkip)) print("coverage : " + str(options)) print("extraToolsBoxes : " + str(extraToolsBoxes)) fullOutput = options["fullOutput"] cov = None if options["coverage"]: import coverage as modcoverage cov = modcoverage.Coverage(omit=['pyexpat', '*__init__.py']) linesToIgnore: List[str] = ["nt", "posix"] for line in linesToIgnore: if os.name == line: continue cov.exclude(f"#* only* {line}* coverage") cov.start() if options["-L"]: from Muscat.Helpers.IO.FileTools import TemporaryDirectory TemporaryDirectory.SetTempPath(os.getcwd()) if options["-P"]: print('Setting temp output directory to ' + options["-Parg"]) from Muscat.Helpers.IO.FileTools import TemporaryDirectory TemporaryDirectory.SetTempPath(options["-Parg"]) if options["-g"]: import Muscat.Helpers.Decorators as Decorators Decorators.useFroze_itDecorator = True print("Decorators.useFroze_itDecorator =", Decorators.useFroze_itDecorator) import Muscat.Helpers.Logger as Logger Logger.DefaultMuscatOutput() if options["-v"]: Logger.VerboseMuscatOutput() # calls to print, ie import module1 from Muscat.Helpers.PrintBypass import PrintBypass print("Running Tests : ") start_time = time.time() print("--- Begin Test ---") toCheck = {} with PrintBypass() as bp: if extraToolsBoxes is not None: for tool in extraToolsBoxes: toCheck.update(__tryImport(tool, bp, options["stopAtFirstError"], modulesToTreat, modulesToSkip)) if not fullOutput: bp.ToSink() bp.Print("Sending all the output of the tests to sink") #import Muscat.Helpers.Logger as Logger Logger.SilentAllMuscatOutput() if modulesToTreat[0] != 'ALL': filtered = dict((k, v) for k, v in toCheck.items() if any(s in k for s in modulesToTreat)) toCheck = filtered if len(modulesToSkip): filtered = dict((k, v) for k, v in toCheck.items() if not any(s in k for s in modulesToSkip)) toCheck = filtered res = __RunAndCheck(toCheck, bp, options["stopAtFirstError"], options["dryRun"], options["profiling"], options["typing"]) if options["coverage"]: cov.stop() cov.save() # create a temp file if options["localHtml"]: #tempdir = "./" tempdir = os.getcwd() + os.sep else: from Muscat.Helpers.IO.FileTools import TemporaryDirectory tempdir = TemporaryDirectory.GetTempPath() ss = [("*/"+os.sep.join(k.split("."))+"*") for k in toCheck] cov.html_report(directory=tempdir, include=ss, title="Coverage report of " + " and ".join(extraToolsBoxes)) print('Coverage Report in : ' + tempdir + "index.html") cov.report(include=ss) if options["launchBrowser"]: import webbrowser webbrowser.open(tempdir+"index.html") stop_time = time.time() bp.Print("Total Time : %.3f seconds " % (stop_time - start_time)) print("--- End Test ---") print(modulesToTreat) if len(modulesToTreat) == 1 and modulesToTreat[0] == "ALL": # we verified that all the python files in the repository are tested CheckIfAllThePresentFilesAreTested(extraToolsBoxes, res) return res
[docs] def CheckIfAllThePresentFilesAreTested(extraToolsBoxes, testedFiles) -> None: # pragma: no cover pythonPaths = os.environ.get('PYTHONPATH', os.getcwd()).split(os.pathsep) toIgnore = ['.git', # git file "__pycache__", # python 3 "__init__.py", # infra "setup.py", # compilation "docs/conf.py", # documentation "__main__.py", "Check.py", ] testedFiles = testedFiles.keys() presentFiles = [] for pythonPath in pythonPaths: for eTB in extraToolsBoxes: path = pythonPath + os.sep + eTB for dirname, dirnames, filenames in os.walk(path): cleanDirName = dirname.replace(pythonPath+os.sep, "") # .replace(os.sep,".") # print path to all filenames. for filename in filenames: if filename in toIgnore: continue if len(str(filename)) > 3 and str(filename)[-3:] == ".py": if len(cleanDirName) == 0: presentFiles.append(filename) else: presentFiles.append(cleanDirName+os.sep+filename) # Advanced usage: # editing the 'dirnames' list will stop os.walk() from recursing into there. for dti in toIgnore: if dti in dirnames: dirnames.remove(dti) for tf in testedFiles: if tf.replace(".", os.sep)+".py" in presentFiles: presentFiles.remove(tf.replace(".", os.sep) + ".py") if len(presentFiles) > 0: print("Files present in the repository but not tested:") for file in presentFiles: print(f" {file}")
[docs] def CheckIntegrity() -> str: return "Ok"
[docs] def GetDefaultOptions(**kwargs): res = {"-g":False, "-L": False, "-P":False, '-v':False, "coverage": False, "localHtml":False, "fullOutput":False, "typing":False, "dryRun":False, "profiling":False, "stopAtFirstError":False} res.update(kwargs) return res
[docs] def RunTests() -> int: # pragma: no cover """Base function to run all tests, all the option are captured from the command line arguments. this function is intended to be used by the user python Muscat.Helper.Check.py -c -f -s -e <extraModules> -m <moduleFilter> python -m Muscat.Helper.Check -c -f -s -e <extraModules> -m <moduleFilter> options : -f Full output for all the test -v Activate maximal level of verbosity (implied -f) -s Stop at first error -e To test extra Modules (-e can be repeated) -m To filter the output by this string (-m can be repeated) -k Skip test base on string -d Dry run do not execute anything, only show what will be executed -c To activate coverage and generate a html report -cb (coverage with browser and local file index.html generated) for correct platform dependent coverage you can use the following comment to disconnect coverage in some platform:: # only nt coverage # only posix coverage -l Generation of the coverage (html) report locally (current path) -b Launch browser after the coverage report generation -p Activate profiling -t use mypy to check the typing of every module -y Generate .pyc when importing modules (default False) -L Output Locally, Use the current path for all the outputs -P <dir> Set temporary output directory to -g to use the Froze decorator during testing Returns ------- int return the number of failed tests """ options = GetDefaultOptions() if len(sys.argv) == 1: res = TestAll(modulesToTreat=['ALL'], extraToolsBoxes=["Muscat"], options=options) # pragma: no cover else: try: opts, args = getopt.getopt(sys.argv[1:], "thcblfsdpvyLe:m:k:P:g") except getopt.GetoptError as e: print(e) print(Test_Help_String) sys.exit(2) coverage = False extraToolsBoxes = [] modulesToTreat = [] modulesToSkip = [] browser = False localHtml = False sys.dont_write_bytecode = True for opt, arg in opts: if opt == '-h': print(Test_Help_String) sys.exit() elif opt in ("-c"): coverage = True browser = False elif opt in ("-t"): options["typing"] = True os.environ["MYPYPATH"] = os.environ["PYTHONPATH"] elif opt in ("-l"): localHtml = True browser = False elif opt in ("-b"): browser = True elif opt in ("-f"): options["fullOutput"] = True elif opt in ("-s"): options["stopAtFirstError"] = True elif opt in ("-d"): options["dryRun"] = True elif opt in ("-v"): options["-v"] = True options["fullOutput"] = True elif opt in ("-p"): options["profiling"] = True elif opt in ("-e"): extraToolsBoxes.append(arg) elif opt in ("-m"): modulesToTreat.append(arg) elif opt in ("-k"): modulesToSkip.append(arg) elif opt in ("-P"): options["-P"] = True options["-Parg"] = arg elif opt in ("-y"): sys.dont_write_bytecode = False elif opt in ("-L"): options["-L"] = True elif opt in ("-g"): options["-g"] = True if len(modulesToTreat) == 0: modulesToTreat.append("ALL") if len(extraToolsBoxes) == 0: extraToolsBoxes.append("Muscat") options.update({"coverage": coverage, "localHtml": localHtml, "launchBrowser": browser} ) res = TestAll(modulesToTreat=modulesToTreat, modulesToSkip=modulesToSkip, options = options, extraToolsBoxes=extraToolsBoxes) errors = {} oks = {} skipped = {} for x, y in res.items(): if str(y).lower().startswith("ok"): oks[x] = y elif str(y).lower().startswith("skip"): skipped[x] = y else: errors[x] = y print("Number of test OK : " + str(len(oks))) print("Number of skipped test : " + str(len(skipped))) print("Number of test KO : " + str(len(errors))) nbOks = len(oks) nbErrors = len(errors) if nbOks == 0 and nbErrors == 0: nbOks = 1 print(f"Percentage of OK test : {(nbOks*100.0)/(nbOks+nbErrors):.2f} %") return errors
if __name__ == '__main__': # pragma: no cover errors = RunTests() sys.exit(len(errors))