diff --git a/tools/clientSimulator/README.md b/tools/clientSimulator/README.md index 2a7c9b6bc..8898c33c7 100644 --- a/tools/clientSimulator/README.md +++ b/tools/clientSimulator/README.md @@ -40,7 +40,9 @@ configuration functional tests. ## How to run tests -You can run tests using pfClientSimulator.py. +You can run tests using pfClientSimulator.py. `test-platform` and +`remote-process` need to be in the PATH (e.g. by installing the Parameter +Framework - see the main README file). You have to run the script with at least the test directory argument: @@ -71,41 +73,32 @@ to learn what it is about. A test directory should contains a `conf.json` file containing: -- The desired command prefix (optional; e.g. `adb shell` in order to execute - tests on an android board or empty to execute locally). +- The desired command prefix (e.g. `adb shell` in order to execute tests on an + android board or empty to execute locally). +- The port on which the test-platform should be started. - The criterion file path (see [this README](https://github.com/01org/parameter-framework/tree/master/tools/xmlGenerator#domaingeneratorpy)). -- The absolute path to the Parameter Framework toplevel configuration file. +- The path to the Parameter Framework toplevel configuration file. - The path to the directory containing the scenario files. - The path to the scripts definitions file (optional) (see below). - The path to the actions definitions (aka "ActionGatherer") file (optional) (see below). -- The absolute path to the log output file (optional). -- A setup script (inline shell) (optional). -- The absolute paths to test-platform and remote-process executables (or the - executables' names if they are in the PATH). -- The host and port on which the test-platform and the Parameter Framework - instance are listening for commands. -- The absolute path to the directory containing the coverage generation tool +- The path to the log output file (optional but needed for coverage). +- The path to the directory containing the coverage generation tool (optional; for coverage only). - The path to the html coverage output file (optional; for coverage only). - The path to the Parameter Framework domain configuration file (optional; for coverage only). -All these *must* be defined in `conf.json`; when marked "optional", it means -that they *may be empty but still need to be defined*. - -Unless otherwise noted, paths in `conf.json` should be relative and will be -evaluated *relative to the test directory*. When we mention absolute path, you -may still fill a relative path but it will be evaluated *relative to the -working directory*. - +Relative paths in `conf.json` will be evaluated *relative to the test +directory*. ## Example Client Simulator configuration file ```{.json} { "PrefixCommand" : "adb shell", + "TestPlatformPort" : "5001", "CriterionFile" : "MyCriteria.txt", "PfwConfFile" : "/home/user/tests/TopLevel.xml", @@ -116,13 +109,6 @@ working directory*. "LogFile" : "tests.log", - "SetupScript" : "echo 'bouh'", - - "TestPlatformCommand" : "test-platform", - "RemoteProcessCommand" : "remote-process", - "TestPlatformHost" : "localhost 5001", - "ParameterFramworkHost" : "localhost 5000", - "CoverageDir" : "/home/user/parameter-framework/tools/coverage", "CoverageFile" : "coverage.html", "PfwDomainConfFile" : "MyConfigurableDomains.xml" diff --git a/tools/clientSimulator/clientsimulator/configuration/ConfigParser.py b/tools/clientSimulator/clientsimulator/configuration/ConfigParser.py index 6cc6b6420..8a513ffde 100644 --- a/tools/clientSimulator/clientsimulator/configuration/ConfigParser.py +++ b/tools/clientSimulator/clientsimulator/configuration/ConfigParser.py @@ -41,17 +41,23 @@ def __init__(self, confFileName, testsDirectory, consoleLogger): with open(confFileName, "r") as testFile: self.__conf = json.load(testFile) - # Preparing files and directory paths + # Preparing mandatory files and directory paths for key in ["CriterionFile", - "ScriptsFile", - "SetupScript", + "PfwConfFile", + "ScenariosDirectory"]: + self.__conf[key] = os.path.join(testsDirectory, self.__conf[key]) + + # Preparing optional files and directory paths + for key in ["ScriptsFile", "ActionGathererFile", - "ScenariosDirectory", "LogFile", "CoverageFile", "CoverageDir", "PfwDomainConfFile"]: - self.__conf[key] = os.path.join(testsDirectory, self.__conf[key]) + try: + self.__conf[key] = os.path.join(testsDirectory, self.__conf[key]) + except KeyError as e: + self.__conf[key] = "" self.__logger = logging.getLogger(__name__) self.__logger.addHandler(consoleLogger) diff --git a/tools/clientSimulator/clientsimulator/scenario/Scenario.py b/tools/clientSimulator/clientsimulator/scenario/Scenario.py index 6284eba56..965fbe948 100644 --- a/tools/clientSimulator/clientsimulator/scenario/Scenario.py +++ b/tools/clientSimulator/clientsimulator/scenario/Scenario.py @@ -28,7 +28,6 @@ import json import logging -import os class Scenario: @@ -100,30 +99,19 @@ def __parseScenarioActions(self, scenarioFileName, actionGathererFileName): # Parsing the action Gatherer file which allows defining new # actions types - with open(actionGathererFileName, "r") as actionGathererFile: - scenarioGatheredActions = json.load(actionGathererFile) + scenarioGatheredActions = {} + if actionGathererFileName: + with open(actionGathererFileName, "r") as actionGathererFile: + scenarioGatheredActions = json.load(actionGathererFile) for action in scenarioActions: + actionDefinedType = self.__getActionType(action) + if actionDefinedType in self.__actionTypeBehaviour.keys(): + continue + try: - actionDefinedType = self.__getActionType(action) - if actionDefinedType not in self.__actionTypeBehaviour.keys(): - actionValue = action.pop(actionDefinedType) - actionGatherer = scenarioGatheredActions[actionDefinedType] - - if self.__getActionType(actionGatherer) == "script": - raise UngatherableTypeException( - "Unable to redefine {} type, please edit your {} file".format( - self.__getActionType(actionGatherer), - actionGathererFileName)) - - # Fusion of gathered Actions and other desired actions which - # are directly writed in the scenario's file - actionValue.update( - self.__getActionValue(actionGatherer)) - - # Change the user defined key which was previously popped - # by the known one - action[self.__getActionType(actionGatherer)] = actionValue + actionValue = action.pop(actionDefinedType) + actionGatherer = scenarioGatheredActions[actionDefinedType] except KeyError as e: self.__logger.error( "Actions {} from {} file is not valid".format( @@ -131,6 +119,20 @@ def __parseScenarioActions(self, scenarioFileName, actionGathererFileName): scenarioFileName)) raise e + if self.__getActionType(actionGatherer) == "script": + raise UngatherableTypeException( + "Unable to redefine {} type, please edit your {} file".format( + self.__getActionType(actionGatherer), + actionGathererFileName)) + + # Fusion of gathered Actions and other desired actions which + # are directly writed in the scenario's file + actionValue.update(self.__getActionValue(actionGatherer)) + + # Change the user defined key which was previously popped + # by the known one + action[self.__getActionType(actionGatherer)] = actionValue + return scenarioActions def __getActionType(self, action): diff --git a/tools/clientSimulator/clientsimulator/testGenerator/TestLauncher.py b/tools/clientSimulator/clientsimulator/testGenerator/TestLauncher.py index 6cd0c47d9..eb68bfb74 100644 --- a/tools/clientSimulator/clientsimulator/testGenerator/TestLauncher.py +++ b/tools/clientSimulator/clientsimulator/testGenerator/TestLauncher.py @@ -58,18 +58,21 @@ def __init__(self, self.__configParser = configParser # Prepare basic commands - halCommand = [configParser["RemoteProcessCommand"], - configParser["TestPlatformHost"]] + halCommand = ["remote-process", + "localhost", + configParser["TestPlatformPort"]] setCriteriaCommand = halCommand + ["setCriterionState"] - testPlatformHostCommand = [configParser["RemoteProcessCommand"], - configParser["TestPlatformHost"]] + testPlatformHostCommand = ["remote-process", + "localhost", + configParser["TestPlatformPort"]] self.__logFileName = configParser["LogFile"] # Commands self.__startTestPlatformCmd = [configParser["PrefixCommand"], - configParser["TestPlatformCommand"], - configParser["PfwConfFile"]] + "test-platform", + configParser["PfwConfFile"], + configParser["TestPlatformPort"]] self.__createCriterionCmd = [configParser["PrefixCommand"]] self.__createCriterionCmd.extend(testPlatformHostCommand) @@ -85,12 +88,10 @@ def __init__(self, self.__applyConfigurationsCmd.extend(halCommand) self.__applyConfigurationsCmd.append("applyConfigurations") - self.__setupScript = [configParser["SetupScript"]] - # Command used to generate coverage self.__coverageCmd = [ "eval", - configParser["CoverageDir"] + "/aplog2coverage.sh", + os.path.join(configParser["CoverageDir"], "aplog2coverage.sh"), "-d", configParser["PfwDomainConfFile"], "-e.", @@ -102,8 +103,10 @@ def __init__(self, # Prepare script Commands # Loading possible scripts - with open(configParser["ScriptsFile"], 'r') as scriptFile: - self.__rawScripts = json.load(scriptFile) + self.__rawScripts = {} + if configParser["ScriptsFile"]: + with open(configParser["ScriptsFile"], 'r') as scriptFile: + self.__rawScripts = json.load(scriptFile) self.__availableLaunchType = ["asynchronous", "synchronous"] @@ -118,11 +121,6 @@ def scripts(self): def init(self, criterionClasses, isVerbose): """ Initialise the Pseudo HAL """ - # Use user script to setup environment as requested before to do - # anything - self.__logger.info("Launching Setup script") - self.__call_process(self.__setupScript) - self.__logger.info("Pseudo Hal Initialisation") # Test platform is launched asynchronously and not as script self.__call_process(self.__startTestPlatformCmd, True) @@ -175,9 +173,10 @@ def executeScript(self, scriptName): launchType = self.__availableLaunchType[0] # Create and launch the command to use the desired script + # A script's path is absolute or relative to the "ScriptsFile" file. self.__call_process( - ["eval", "{}/{}".format( - os.path.split(self.__configParser["ScriptsFile"])[0], + ["eval", os.path.join( + os.path.dirname(self.__configParser["ScriptsFile"]), script)], launchType == self.__availableLaunchType[0], True) diff --git a/tools/clientSimulator/clientsimulator/testGenerator/TestVector.py b/tools/clientSimulator/clientsimulator/testGenerator/TestVector.py deleted file mode 100644 index 97663870f..000000000 --- a/tools/clientSimulator/clientsimulator/testGenerator/TestVector.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2014-2015, Intel Corporation -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation and/or -# other materials provided with the distribution. -# -# 3. Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -class TestVector: - - """ This class describe a test vector which can be launched by a TestLauncher object """ - - def __init__(self, name, criterions, testType): - self.__name = name - self.__testType = testType - self.__criterions = criterions - - @property - def criterions(self): - return self.__criterions - - @property - def testType(self): - return self.__testType - - @testType.setter - def testType(self, value): - self.__testType = value - - def __str__(self): - toString = "Test Type : {}\n".format(self.__testType) - for criterion in self.__criterions: - toString += (str(criterion) + '\n') - return toString - - -class InvalidTestTypeValueException(Exception): - - """ Exception raised in case of problem with the test type """ - - def __init__(self, msg): - self.__msg = msg - - def __str__(self): - return "Invalid Test Type Error : " + self.__msg diff --git a/tools/clientSimulator/clientsimulator/userInteraction/UserInteractor.py b/tools/clientSimulator/clientsimulator/userInteraction/UserInteractor.py index df7833f11..d695705c7 100644 --- a/tools/clientSimulator/clientsimulator/userInteraction/UserInteractor.py +++ b/tools/clientSimulator/clientsimulator/userInteraction/UserInteractor.py @@ -51,28 +51,38 @@ def __init__(self, testLauncher, criterions): self.__criterions = criterions @classmethod - def getMenu(cls, options): + def getMenu(cls, options, cancelSentence="Go Back"): """ Dynamic Menu Generator : - :param options: dictionnary containing, the invite string - and the function to launch - :type options: dict + :param options: list containing tuples of a) the invite string and + b) the function to launch + :type options: list + :param cancelSentence: title of the menu entry that will be + appended after the provided options, in order to exit the menu. For + top-level menus, it is advised to pass "Quit" as argument. + :type cancelSentence: string """ - testQuit = True - - options[len(options)] = \ - ("Go Back", lambda: False) - while testQuit: + while True: print("\nPlease Make a choice : ") - for numMenu, (sentenceMenu, fonc) in sorted(options.items()): + for numMenu, (sentenceMenu, fonc) in enumerate(options): print("\t{}. {}".format(numMenu, sentenceMenu)) - choice = input("Your Choice : ") + # Lastly, append an option to go to the previous menu/quit + print("\t{}. {}".format(len(options), cancelSentence)) + choice = input("Your Choice : ") try: - testQuit = options[int(choice)][1]() + choice = int(choice) + if choice == len(options): + # The user has selected the "cancel" option + break + if choice < 0: + # Negative values make no sense + raise KeyError(choice) + + options[choice][1]() except (KeyError, ValueError) as e: print("Invalid Choice : {}".format(e)) @@ -81,13 +91,13 @@ def launchInteractiveMode(self): Interactive Mode : Set up a menu which allow users to personnalize a Test and to Launch it """ - optionsMenu = { - 0: ("Edit Vector", self.__editVector), - 1: ("Apply Configuration", self.__applyConfiguration), - 2: ("Launch Script", self.__launchScript) - } + optionsMenu = [ + ("Edit Vector", self.__editVector), + ("Apply Configuration", self.__applyConfiguration), + ("Launch Script", self.__launchScript) + ] - UserInteractor.getMenu(optionsMenu) + UserInteractor.getMenu(optionsMenu, "Quit") def __applyConfiguration(self): """ @@ -105,11 +115,11 @@ def __launchScript(self): script to run. """ - optionScript = { - num: ("{} scripts".format(script), + optionScript = [ + ("Execute {}".format(script), DynamicCallHelper(self.__testLauncher.executeScript, script)) - for num, script in enumerate(self.__testLauncher.scripts) - } + for script in self.__testLauncher.scripts + ] UserInteractor.getMenu(optionScript) @@ -129,37 +139,34 @@ def __editCriterion(self, criterion): :type criterion: Criterion """ - optionEditCriterion = {} + optionEditCriterion = [] for possibleValue in [x for x in criterion.allowedValues() if not x in criterion.currentValue and not x == criterion.noValue]: - optionEditCriterion[ - len(optionEditCriterion)] = ( - "Set {}".format(possibleValue), - DynamicCallHelper( - self.__setCriterion, - criterion, - possibleValue)) + optionEditCriterion.append( + ("Set {}".format(possibleValue), + DynamicCallHelper( + self.__setCriterion, + criterion, + possibleValue))) if InclusiveCriterion in criterion.__class__.__bases__: # Inclusive criterion : display unset value (default when empty) for possibleValue in criterion.currentValue: - optionEditCriterion[ - len(optionEditCriterion)] = ( - "Unset {}".format(possibleValue), - DynamicCallHelper( - self.__removeCriterionValue, - criterion, - possibleValue)) + optionEditCriterion.append( + ("Unset {}".format(possibleValue), + DynamicCallHelper( + self.__removeCriterionValue, + criterion, + possibleValue))) else: # Exclusive criterion : display default value - optionEditCriterion[ - len(optionEditCriterion)] = ( - "Set Default", - DynamicCallHelper( - self.__setCriterion, - criterion, - criterion.noValue)) + optionEditCriterion.append( + ("Set Default", + DynamicCallHelper( + self.__setCriterion, + criterion, + criterion.noValue))) UserInteractor.getMenu(optionEditCriterion) @@ -170,11 +177,11 @@ def __editVector(self): Allow to change the value of several criterions through a menu. """ - optionEdit = { - num: ("Edit {}".format(cri.__class__.__name__), + optionEdit = [ + ("Edit {}".format(cri.__class__.__name__), DynamicCallHelper(self.__editCriterion, cri)) - for num, cri in enumerate(self.__criterions) - } + for cri in self.__criterions + ] UserInteractor.getMenu(optionEdit) diff --git a/tools/clientSimulator/pfClientSimulator.py b/tools/clientSimulator/pfClientSimulator.py index 28d080f65..f58c8386b 100755 --- a/tools/clientSimulator/pfClientSimulator.py +++ b/tools/clientSimulator/pfClientSimulator.py @@ -37,8 +37,6 @@ from clientsimulator.userInteraction.UserInteractor import UserInteractor from clientsimulator.userInteraction.DynamicCallHelper import DynamicCallHelper import argparse -import threading -import signal import time import logging import os @@ -100,11 +98,18 @@ def main(): parser.add_argument("test_directory", type=str, default=None, help="precise a test directory (required).") - parser.add_argument("-s", "--scenario", type=int, default=None, - help="precise a scenario to launch.") + parser.add_argument("-s", "--scenario", type=int, default=None, nargs='+', + help="precise one or more scenarios to launch.") - parser.add_argument("--interactive", action='store_true', - help="run in interactive mode.") + interactiveness = parser.add_mutually_exclusive_group() + interactiveness.add_argument("--no-exit", action='store_true', + help="lets you interactively select more scenarios (This is" + " implicit if neither '--scenario' nor '--interactive' are " + " passed).") + + interactiveness.add_argument("--interactive", action='store_true', + help="run in interactive mode (lets you select actions and scripts" + " to run).") parser.add_argument( "-v", @@ -136,12 +141,18 @@ def main(): args.test_directory)) exit(1) - configParser = ConfigParser( - os.path.join( + try: + configParser = ConfigParser( + os.path.join( + args.test_directory, + "conf.json"), args.test_directory, - "conf.json"), - args.test_directory, - consoleLogger) + consoleLogger) + except KeyError as e: + logger.error( + "Missing mandatory configuration item {} in the" + " conf.json file".format(e)) + exit(1) # Always write all log in the file logging.basicConfig(level=logging.DEBUG, @@ -175,34 +186,33 @@ def main(): testLauncher, testFactory.generateTestVector()).launchInteractiveMode() else: - while True: - scenarioOptions = { - scenarioNumber: - (scenarioFileName, - DynamicCallHelper( - launchScenario, - logger, - consoleLogger, - configParser["ActionGathererFile"], - os.path.join( - configParser["ScenariosDirectory"], scenarioFileName), - testFactory, - testLauncher - )) - for scenarioNumber, scenarioFileName in enumerate( - [file for file in sorted(os.listdir( - configParser["ScenariosDirectory"]))]) - } - if args.scenario is not None: - scenarioOptions[args.scenario][1]() - # Let the user choose other scenario after the one choosed by command line - args.scenario = None - else: - UserInteractor.getMenu(scenarioOptions) + scenarioOptions = [ + (scenarioFileName, + DynamicCallHelper( + launchScenario, + logger, + consoleLogger, + configParser["ActionGathererFile"], + os.path.join( + configParser["ScenariosDirectory"], scenarioFileName), + testFactory, + testLauncher + )) + for scenarioFileName in sorted(os.listdir(configParser["ScenariosDirectory"])) + ] + if args.scenario is not None: + for elem in args.scenario: + scenarioOptions[elem][1]() + if (args.scenario is None) or args.no_exit: + # Let the user choose more scenarios after the ones chosen by command line + # or if none was given on the command line. + UserInteractor.getMenu(scenarioOptions, "Quit") except KeyboardInterrupt as e: close(logger, testLauncher, args.coverage) + else: + close(logger, testLauncher, args.coverage) if __name__ == "__main__": - """ Execute main if the script is running as main """ + # Execute main if the script is running as main main()