[PATCH 00/10] FVT testcases base framework

From: Archana Singh <archus@linux.vnet.ibm.com> Patch for adding FVT testcases base framework which can be used across plugins. The changes are: 1) Added fvt package inside tests directory. 2) Added config file inside fvt package to have session details. 3) restapilib.py to have common classes/methods for REST API calls. 4) fvt_base.py, a base test class to take care of doing common setup and treadown required for any fvt test cases like creating/destroying session using config file and restapilib.py. 5) run_test.sh.in script to install all the dependencies and to run all FVT. 6) make file changes to have 'make check-fvt' for running all the FVT. Archana Singh (10): Package for functional verification testcases Wok level config file to have sections required for functional verification test common across plugins. Lists all dependecies for fvt testcases. Common classes/methods for API calls as per config file configuration. Base test class, takes care common actions required for any FVT test cases like creating/destoring session, authorization using wok level fvt config file, creating JSON validator and logging. Install all the dependencies from requirements.txt in python virtualenv and runs all the FVT test cases. Makefile needed for build and run fvt. Added fvt as subdirs and check-fvt to run fvt testcases using make. Added FVT Makefile in AC_CONFIG_FILES list. Added check-fvt to run FVT testcases using make and venv dir to be cleaned. Makefile.am | 8 +- configure.ac | 1 + tests/Makefile.am | 6 + tests/fvt/Makefile.am | 43 +++ tests/fvt/__init__.py | 18 ++ tests/fvt/config | 7 + tests/fvt/fvt_base.py | 92 ++++++ tests/fvt/requirements.txt | 23 ++ tests/fvt/restapilib.py | 760 +++++++++++++++++++++++++++++++++++++++++++++ tests/fvt/run_tests.sh.in | 92 ++++++ 10 files changed, 1048 insertions(+), 2 deletions(-) create mode 100644 tests/fvt/Makefile.am create mode 100644 tests/fvt/__init__.py create mode 100644 tests/fvt/config create mode 100644 tests/fvt/fvt_base.py create mode 100644 tests/fvt/requirements.txt create mode 100644 tests/fvt/restapilib.py create mode 100755 tests/fvt/run_tests.sh.in -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/fvt/__init__.py diff --git a/tests/fvt/__init__.py b/tests/fvt/__init__.py new file mode 100644 index 0000000..784a988 --- /dev/null +++ b/tests/fvt/__init__.py @@ -0,0 +1,18 @@ +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/config | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/fvt/config diff --git a/tests/fvt/config b/tests/fvt/config new file mode 100644 index 0000000..2f681a9 --- /dev/null +++ b/tests/fvt/config @@ -0,0 +1,7 @@ +[Session] +user : +passwd : +host : localhost +port : 8001 +logfile : kimchi-api-test-suite.log +loglevel : DEBUG \ No newline at end of file -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/requirements.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/fvt/requirements.txt diff --git a/tests/fvt/requirements.txt b/tests/fvt/requirements.txt new file mode 100644 index 0000000..92d201c --- /dev/null +++ b/tests/fvt/requirements.txt @@ -0,0 +1,23 @@ +# This requirements file lists all dependecies for fvt testcases. +# Run 'pip install -r requirements.txt' to install these dependencies +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA + +jsonschema +requests -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/restapilib.py | 760 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 tests/fvt/restapilib.py diff --git a/tests/fvt/restapilib.py b/tests/fvt/restapilib.py new file mode 100644 index 0000000..ab7eb62 --- /dev/null +++ b/tests/fvt/restapilib.py @@ -0,0 +1,760 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA + +import ConfigParser +import jsonschema +import json +import requests +import logging +import os + +requests.packages.urllib3.disable_warnings() + +__all__ = ['APIError', 'APIRequestError', 'APISession', 'SessionParameters'] + +# DEFAULT_CONNECTION_TIMEOUT = 5 # Default connection timeout, in secs +# DEFAULT_REQUEST_TIMEOUT = 300 # Default request timeout, in secs +DEFAULT_CONF = os.path.dirname(os.path.abspath(__file__)) + 'config' + +_HTTP_SUCCESS_STATUS_RANGE = range(200, 299) # All of the 2XX status codes + + +class APIError(Exception): + """Base class for exceptions raised by this module.""" + + pass + + +class APISession(object): + """Represents an API session with the host Web Services API""" + + def __init__(self, conffile=DEFAULT_CONF): + """ Creates an APISession object for use with the specified Host. + """ + self._session = None + self._base_uri = None + self.username = None + self.passwd = None + self.host = None + self.port = None + self._session = self.session() # Holds the session from requests + self.sessionparams = SessionParameters(conffile=conffile) + self.logging = self.sessionparams.getlogger() + # base URI of connected machine + self._base_uri = self.create_base_uri() + + def session(self): + """ + Create the session object for API Request if no existing session. + """ + if self._session is not None: + raise APIError('Already have a session') + + self._session = requests.Session() + return self._session + + def auth(self): + """ + Set authentication to API session. + + \param username + The username name to use for this session. + + \param password + The password associated with username. + + """ + + if self._session is None: + self.session() + + self.username, self.passwd = self.sessionparams.getcredentials() + + if self.username is None: + raise APIError('Username is None') + if self.passwd is None: + raise APIError('password is None') + # Set auth at session level + self._session.auth = (self.username, self.passwd) + + # set password expiration currently it default value + # self._session.expires = '1432091741' + # For now keeping it False to disable SSL verification + self._session.verify = False + return + + def create_base_uri(self): + """ + Create the base URI using host and port. + if no host is provided _base_uri is None. + if port is None then _base_uri is just host + if both is provided then append port to host. + """ + + if self._base_uri is not None: + return self._base_uri + + self.host, self.port = self.sessionparams.geturlparams() + if self.host is None: + return None + if self.port is None: + return self.host + + self._base_uri = 'https://' + self.host + ':' + self.port + return self._base_uri + + def end_session(self): + """Ends an API session""" + + if self._session is None: + return None # Silently ignore + # The session to be terminated by setting None in session object we + # have. + self._session = None + return + + def request( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, ): + """ + Issue an WS API request and return the response body, if any. + + \param method + The HTTP method to issue (eg. GET, PUT, POST, DELETE). + + \param uri + The URI path and query parameter string for the request. + + \param body + The request body that is input to the request, as a Unicode + or String, or None if there is no input body. + + \param expected_status_values + The HTTP status code that is expected on completion of the request. + If this parameter is specified, the function raises an exception + if the HTTP status from the request was not exactly as specified. + Otherwise, it raises an error if the HTTP status is not 2XX. + + \param expected_reason + The API reason code tha tis expected on completion of the request. + If this parameter is specified in addition to the + expected_status_values patameter, this function raises an exception + if either the status or reason is not as specified. + Otherwise, the reason code from the request is not considered. + + \param headers + Request headers for this request, in the form of a Python Dict. + Optional. This function automatically augments these headers + with the headers needed to specify the API session. Note that if + input headers are provided, the supplied Dict object will be + modified by this function. + """ + + # Start with the headers supplied by caller, if any + + if headers is not None: + hdrs = headers # Use caller's dict, not a copy + else: + hdrs = dict() + + # If no session then throw error. + + if self._session is None: + raise APIError('You have no session') + + resp = self._session.request(method, uri, data=body, headers=hdrs) + + # If an expected status was specified, check that the status exactly + # matches wbat was expected, otherwise check that the status is one of + # the success Statuses. + + if expected_status_values is not None: + raise_exc = resp.status_code not in expected_status_values + else: + raise_exc = resp.status_code not in _HTTP_SUCCESS_STATUS_RANGE + + # Raise an exceptoin if the result is not as intended. + + if raise_exc: + + # If the request fails in some way WEb Services API response + # will usually include a standard error response body + # in JSON format that includes a more detailed reason + # code (and message) for the failure. It provides this data + # in JSON format even if the request would return some other + # format if the request had been successful. + # So if the request has failed, grab that additional info + # for use in raising exceptions below. + + failure_reason = 0 + failure_code = None + # if response.status_code not in _HTTP_SUCCESS_STATUS_RANGE: + + # The API provides the JSON error response in all usual error + # cases. But for certain less common errors this does not occur + # because the error is caught higher in the processing stack. + # So try to interpret the response as a JSON response body, but + # just silently ignore problems if we can't do this. + + try: + error_resp = json.loads(resp.text) + failure_reason = error_resp['reason'] + failure_code = error_resp['code'] + except (ValueError, KeyError): + pass + + raise APIRequestError(resp.status_code, failure_reason, + failure_code) + + # Return the response + return resp + + def request_octet( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + + # Start with the request headers the caller supplied if any, + # otherwise start with an empty set of headers. + + if headers is not None: + hdrs = headers # Using caller's dict, not a copy + else: + hdrs = dict() + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for not not set.) + + body_json = body + if body is not None: + body_json = json.dumps(body) + + # Supply an HTTP Accepts header indicating we accept/expect that any + # response body be JSON. The Accepts header is optional, and the API + # will defautl to supplying JSON for any operation defined in API V1.1 + # to return JSON (even if in the future support for other formats would + # be added). But its a good idea to specify this header as a safeguard + # in case this function were called for an URI that is not capable of + # returning JSON, as this function as currently coded would choke on + # non-JSON responses. + + hdrs['Accept'] = 'application/json' + + # Now issue the request, and retrieve the response object and the + # response body if provided. + + resp = self.request( + method, + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=hdrs, + ) + + return resp + + def request_json( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """ + Issue an WS API request that is defined to take JSON input and + produce JSON output. (Nearly all WS API requests are like this.) + + Input and output bodies are specified as Python Dict or List objects, + which are converted to/from JSON by this function. + + \param method + The HTTP method to issue (eg. GET, PUT, POST, DELETE). + + \param uri + The URI path and query parameter string for the request. + + \param body + The request body that is input to the request, in the form of a + Python Dict or List object. This object is automatically converted + to corresponding JSON by this function. Optional. + + \param expected_status_values + The HTTP status code that is expected on completion of the request. + See corresponding parameter on request() method for more details. + + \param expected_reason + The API reason code that is expected on completion of the request. + See corresponding parameter on request() method for more details. + + \param headers + Request headers for this request, in the form of a Python Dict. + Optional. This function automatically augments these headers + with the headers needed to specify the JSON content type for input + and output, and to specify the API session. Note that if input + headers are provided, the supplied Dict object will be modified + by this function. + + """ + + # Start with the request headers the caller supplied if any, + # otherwise start with an empty set of headers. + + if headers is not None: + hdrs = headers # Using caller's dict, not a copy + else: + hdrs = dict() + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for now have not set.) + + body_json = None + if body is not None: + body_json = json.dumps(body) + hdrs['Content-Type'] = 'application/json' + + # Supply an HTTP Accepts header indicating we accept/expect that any + # response body be JSON. The Accepts header is optional, and the API + # will default to supplying JSON for any operation defined in API V1.1 + # to return JSON (even if in the future support for other formats would + # be added). But its a good idea to specify this header as a safeguard + # in case this function were called for an URI that is not capable of + # returning JSON, as this function as currently coded would choke on + # non-JSON responses. + + hdrs['Accept'] = 'application/json' + + # Now issue the request, and retrieve the response object and the + # response body if provided. + + response_body = self.request( + method, + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=hdrs, + ) + + # If a response body was returned, we presume it is JSON and + # convert it into a Python dict we will return. + + resp_json = None + if response_body is not None: + try: + resp_json = response_body.json() + except ValueError, err: + print 'response_body %s' % response_body + print 'response_body headers %s' % response_body.headers + raise APIError('Response body expected to be JSON but is' + + 'not valid %s', err) + return resp_json + + def request_get_json( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request that returns JSON.""" + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json('GET', uri, + expected_status_values=expected_status_values, + headers=headers) + + def request_put_json( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request that returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'PUT', + uri, + body=body, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_post_json( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a POST request that + provides/returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'POST', + uri, + body=body, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_delete_json( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a DELETE request that returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'DELETE', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_get( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request('GET', uri, + expected_status_values=expected_status_values, + headers=headers) + + def request_put( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a PUT request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request( + 'PUT', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_post( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a POST request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for now have not set.) + + body_json = None + if body is not None: + body_json = json.dumps(body) + + if headers is None: + headers = dict() + headers['Content-Type'] = 'application/json' + headers['Accept'] = 'application/json' + + return self.request( + 'POST', + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_delete( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a DELETE request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request( + 'DELETE', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + +class APIRequestError(APIError): + """ + Raised when an API request ends in error or not as expected. + + Attributes: + \param status + HTTP status code from the request + + \param reason + API reason code from the request + + \param message + API diagnostic message from the request + + \param stack + Internal diagnostic info for selected Status 500 errors + """ + + def __init__( + self, + status, + reason=None, + code=None, + ): + self.status = status + self.reason = reason + self.code = code + + def __str__(self): + if self.code is not None: + s = 'Request ended with status %s-%s (%s)' \ + % (self.status, self.reason, self.code) + else: + s = 'Request ended with status %s-%s' % (self.status, + self.reason) + return s + + +class BadJSONResponse(APIError): + """exceptions raised for Bad Json Response.""" + + def __init__( + self, + error, + ): + self.error = error + + def __str__(self): + s = 'Bad response: %s' % self.error + return s + + +class InvalidInput(APIError): + """exceptions raised for Invalid Input.""" + + def __init__( + self, + error, + ): + self.error = error + + def __str__(self): + s = 'Bad response: %s' % self.error + return s + + +class SessionParameters(object): + """ + Represents functionalities that could help in getting the + session information. + """ + + def __init__( + self, + conffile=DEFAULT_CONF): + """ + Attributes: + \param conffile + config file which contains all configuration + information with sections + """ + self.conffile = conffile + self.username = None + self.passwd = None + self.host = '127.0.0.1' + self.port = '8001' + self.params = 'config' + self.logfile = 'kimchi-api-test-suite.log' + self.session_section = 'Session' + self.logging = logging + self.loglevel = 'ERROR' + self.LEVELS = {'INFO': self.logging.INFO, + 'DEBUG': self.logging.DEBUG, + 'WARNING': self.logging.WARNING, + 'ERROR': self.logging.ERROR, + 'CRITICAL': self.logging.CRITICAL} + if self.conffile is None: + print 'Configuration file required %s' \ + % self.conffile + else: + print 'Reading configuration file %s' % self.conffile + self.params = ConfigParser.ConfigParser() + print self.params.read(self.conffile) + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'logfile'): + self.logfile = self.params.get( + self.session_section, 'logfile') + + if self.params.has_option(self.session_section, 'loglevel'): + self.loglevel = self.params.get( + self.session_section, 'loglevel') + else: + print "Section %s is not available in the config file " \ + % self.session_section + + self.logging.basicConfig(format='%(asctime)s %(levelname)s: \ + %(message)s', + datefmt='%m-%d-%Y %I:%M:%S %p', + filename=self.logfile, + level=self.LEVELS[self.loglevel]) + + def getcredentials(self): + """ + Returns user and password details for a session obtained from config + file + """ + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'user'): + self.username = self.params.get(self.session_section, 'user') + + if self.params.has_option(self.session_section, 'passwd'): + self.passwd = self.params.get(self.session_section, 'passwd') + else: + print "Section %s is not available in the config file " \ + % self.session_section + raise APIError('Session section not present in config') + + return self.username, self.passwd + + def geturlparams(self): + """ + Returns host and port details for a session obtained from config file + """ + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'host'): + self.host = self.params.get(self.session_section, 'host') + + if self.params.has_option(self.session_section, 'port'): + self.port = self.params.get(self.session_section, 'port') + else: + print "Section %s is not available in the config file " \ + % self.session_section + + return self.host, self.port + + def getlogger(self): + return self.logging + + +class Validator(object): + """ + validator class having different validate methods + """ + + def __init__(self, logger=None): + """ + :param logger: + :return: + """ + if logger is not None: + self.logging = logger + + def validate_json(self, jsn, schma): + """ + validates json against schema using jsonschema.validate library + + :param jsn: JSON to validate + :param schma: Schema against which JSON should get validated + :return: + """ + if logging is not None: + self.logging.info('--> validate_json()') + if logging is not None: + self.logging.debug( + 'validate_json(): jsn:%s' % jsn) + self.logging.debug( + 'validate_json(): schma:%s' % schma) + jsonschema.validate(jsn, schma) + if logging is not None: + self.logging.debug( + 'validate_json(): jsn is valid') + if logging is not None: + self.logging.info('<-- validate_json()') -- 2.1.0

On 11/25/2015 10:46 AM, archus@linux.vnet.ibm.com wrote:
From: Archana Singh <archus@linux.vnet.ibm.com>
Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/restapilib.py | 760 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 tests/fvt/restapilib.py
diff --git a/tests/fvt/restapilib.py b/tests/fvt/restapilib.py new file mode 100644 index 0000000..ab7eb62 --- /dev/null +++ b/tests/fvt/restapilib.py @@ -0,0 +1,760 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA + +import ConfigParser +import jsonschema +import json +import requests +import logging +import os + +requests.packages.urllib3.disable_warnings() + +__all__ = ['APIError', 'APIRequestError', 'APISession', 'SessionParameters'] + +# DEFAULT_CONNECTION_TIMEOUT = 5 # Default connection timeout, in secs +# DEFAULT_REQUEST_TIMEOUT = 300 # Default request timeout, in secs +DEFAULT_CONF = os.path.dirname(os.path.abspath(__file__)) + 'config' os separator(/) has missed in the path DEFAULT_CONF = os.path.dirname(os.path.abspath(__file__)) + os.sep + 'config' + +_HTTP_SUCCESS_STATUS_RANGE = range(200, 299) # All of the 2XX status codes + + +class APIError(Exception): + """Base class for exceptions raised by this module.""" + + pass + + +class APISession(object): + """Represents an API session with the host Web Services API""" + + def __init__(self, conffile=DEFAULT_CONF): + """ Creates an APISession object for use with the specified Host. + """ + self._session = None + self._base_uri = None + self.username = None + self.passwd = None + self.host = None + self.port = None + self._session = self.session() # Holds the session from requests + self.sessionparams = SessionParameters(conffile=conffile) + self.logging = self.sessionparams.getlogger() + # base URI of connected machine + self._base_uri = self.create_base_uri() + + def session(self): + """ + Create the session object for API Request if no existing session. + """ + if self._session is not None: + raise APIError('Already have a session') + + self._session = requests.Session() + return self._session + + def auth(self): + """ + Set authentication to API session. + + \param username + The username name to use for this session. + + \param password + The password associated with username. + + """ + + if self._session is None: + self.session() + + self.username, self.passwd = self.sessionparams.getcredentials() + + if self.username is None: + raise APIError('Username is None') + if self.passwd is None: + raise APIError('password is None') + # Set auth at session level + self._session.auth = (self.username, self.passwd) + + # set password expiration currently it default value + # self._session.expires = '1432091741' + # For now keeping it False to disable SSL verification + self._session.verify = False + return + + def create_base_uri(self): + """ + Create the base URI using host and port. + if no host is provided _base_uri is None. + if port is None then _base_uri is just host + if both is provided then append port to host. + """ + + if self._base_uri is not None: + return self._base_uri + + self.host, self.port = self.sessionparams.geturlparams() + if self.host is None: + return None + if self.port is None: + return self.host + + self._base_uri = 'https://' + self.host + ':' + self.port + return self._base_uri + + def end_session(self): + """Ends an API session""" + + if self._session is None: + return None # Silently ignore + # The session to be terminated by setting None in session object we + # have. + self._session = None + return + + def request( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, ): + """ + Issue an WS API request and return the response body, if any. + + \param method + The HTTP method to issue (eg. GET, PUT, POST, DELETE). + + \param uri + The URI path and query parameter string for the request. + + \param body + The request body that is input to the request, as a Unicode + or String, or None if there is no input body. + + \param expected_status_values + The HTTP status code that is expected on completion of the request. + If this parameter is specified, the function raises an exception + if the HTTP status from the request was not exactly as specified. + Otherwise, it raises an error if the HTTP status is not 2XX. + + \param expected_reason + The API reason code tha tis expected on completion of the request. + If this parameter is specified in addition to the + expected_status_values patameter, this function raises an exception + if either the status or reason is not as specified. + Otherwise, the reason code from the request is not considered. + + \param headers + Request headers for this request, in the form of a Python Dict. + Optional. This function automatically augments these headers + with the headers needed to specify the API session. Note that if + input headers are provided, the supplied Dict object will be + modified by this function. + """ + + # Start with the headers supplied by caller, if any + + if headers is not None: + hdrs = headers # Use caller's dict, not a copy + else: + hdrs = dict() + + # If no session then throw error. + + if self._session is None: + raise APIError('You have no session') + + resp = self._session.request(method, uri, data=body, headers=hdrs) + + # If an expected status was specified, check that the status exactly + # matches wbat was expected, otherwise check that the status is one of + # the success Statuses. + + if expected_status_values is not None: + raise_exc = resp.status_code not in expected_status_values + else: + raise_exc = resp.status_code not in _HTTP_SUCCESS_STATUS_RANGE + + # Raise an exceptoin if the result is not as intended. + + if raise_exc: + + # If the request fails in some way WEb Services API response + # will usually include a standard error response body + # in JSON format that includes a more detailed reason + # code (and message) for the failure. It provides this data + # in JSON format even if the request would return some other + # format if the request had been successful. + # So if the request has failed, grab that additional info + # for use in raising exceptions below. + + failure_reason = 0 + failure_code = None + # if response.status_code not in _HTTP_SUCCESS_STATUS_RANGE: + + # The API provides the JSON error response in all usual error + # cases. But for certain less common errors this does not occur + # because the error is caught higher in the processing stack. + # So try to interpret the response as a JSON response body, but + # just silently ignore problems if we can't do this. + + try: + error_resp = json.loads(resp.text) + failure_reason = error_resp['reason'] + failure_code = error_resp['code'] + except (ValueError, KeyError): + pass + + raise APIRequestError(resp.status_code, failure_reason, + failure_code) + + # Return the response + return resp + + def request_octet( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + + # Start with the request headers the caller supplied if any, + # otherwise start with an empty set of headers. + + if headers is not None: + hdrs = headers # Using caller's dict, not a copy + else: + hdrs = dict() + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for not not set.) + + body_json = body + if body is not None: + body_json = json.dumps(body) + + # Supply an HTTP Accepts header indicating we accept/expect that any + # response body be JSON. The Accepts header is optional, and the API + # will defautl to supplying JSON for any operation defined in API V1.1 + # to return JSON (even if in the future support for other formats would + # be added). But its a good idea to specify this header as a safeguard + # in case this function were called for an URI that is not capable of + # returning JSON, as this function as currently coded would choke on + # non-JSON responses. + + hdrs['Accept'] = 'application/json' + + # Now issue the request, and retrieve the response object and the + # response body if provided. + + resp = self.request( + method, + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=hdrs, + ) + + return resp + + def request_json( + self, + method, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """ + Issue an WS API request that is defined to take JSON input and + produce JSON output. (Nearly all WS API requests are like this.) + + Input and output bodies are specified as Python Dict or List objects, + which are converted to/from JSON by this function. + + \param method + The HTTP method to issue (eg. GET, PUT, POST, DELETE). + + \param uri + The URI path and query parameter string for the request. + + \param body + The request body that is input to the request, in the form of a + Python Dict or List object. This object is automatically converted + to corresponding JSON by this function. Optional. + + \param expected_status_values + The HTTP status code that is expected on completion of the request. + See corresponding parameter on request() method for more details. + + \param expected_reason + The API reason code that is expected on completion of the request. + See corresponding parameter on request() method for more details. + + \param headers + Request headers for this request, in the form of a Python Dict. + Optional. This function automatically augments these headers + with the headers needed to specify the JSON content type for input + and output, and to specify the API session. Note that if input + headers are provided, the supplied Dict object will be modified + by this function. + + """ + + # Start with the request headers the caller supplied if any, + # otherwise start with an empty set of headers. + + if headers is not None: + hdrs = headers # Using caller's dict, not a copy + else: + hdrs = dict() + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for now have not set.) + + body_json = None + if body is not None: + body_json = json.dumps(body) + hdrs['Content-Type'] = 'application/json' + + # Supply an HTTP Accepts header indicating we accept/expect that any + # response body be JSON. The Accepts header is optional, and the API + # will default to supplying JSON for any operation defined in API V1.1 + # to return JSON (even if in the future support for other formats would + # be added). But its a good idea to specify this header as a safeguard + # in case this function were called for an URI that is not capable of + # returning JSON, as this function as currently coded would choke on + # non-JSON responses. + + hdrs['Accept'] = 'application/json' + + # Now issue the request, and retrieve the response object and the + # response body if provided. + + response_body = self.request( + method, + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=hdrs, + ) + + # If a response body was returned, we presume it is JSON and + # convert it into a Python dict we will return. + + resp_json = None + if response_body is not None: + try: + resp_json = response_body.json() + except ValueError, err: + print 'response_body %s' % response_body + print 'response_body headers %s' % response_body.headers + raise APIError('Response body expected to be JSON but is' + + 'not valid %s', err) + return resp_json + + def request_get_json( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request that returns JSON.""" + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json('GET', uri, + expected_status_values=expected_status_values, + headers=headers) + + def request_put_json( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request that returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'PUT', + uri, + body=body, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_post_json( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a POST request that + provides/returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'POST', + uri, + body=body, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_delete_json( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a DELETE request that returns JSON.""" + + if self._base_uri is not None: + uri = self._base_uri + uri + + return self.request_json( + 'DELETE', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_get( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a GET request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request('GET', uri, + expected_status_values=expected_status_values, + headers=headers) + + def request_put( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a PUT request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request( + 'PUT', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_post( + self, + uri, + body=None, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a POST request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + # If a request body was specified, convert it from its assumed Dict + # or List form into JSON using the Python JSON library. Also, add in + # the required Content-Type HTTP message header to let the API know + # that we are supplying input in JSON form. (A Content-Length header + # is also needed to specify the body length, but for now have not set.) + + body_json = None + if body is not None: + body_json = json.dumps(body) + + if headers is None: + headers = dict() + headers['Content-Type'] = 'application/json' + headers['Accept'] = 'application/json' + + return self.request( + 'POST', + uri, + body=body_json, + expected_status_values=expected_status_values, + headers=headers, + ) + + def request_delete( + self, + uri, + expected_status_values=None, + headers=None, + ): + """Convenience function to do a DELETE request with header['Accept'] + as application/json that returns the response. + Does not convert the response to JSON + """ + + if self._base_uri is not None: + uri = self._base_uri + uri + + if headers is None: + headers = dict() + headers['Accept'] = 'application/json' + + return self.request( + 'DELETE', + uri, + expected_status_values=expected_status_values, + headers=headers, + ) + + +class APIRequestError(APIError): + """ + Raised when an API request ends in error or not as expected. + + Attributes: + \param status + HTTP status code from the request + + \param reason + API reason code from the request + + \param message + API diagnostic message from the request + + \param stack + Internal diagnostic info for selected Status 500 errors + """ + + def __init__( + self, + status, + reason=None, + code=None, + ): + self.status = status + self.reason = reason + self.code = code + + def __str__(self): + if self.code is not None: + s = 'Request ended with status %s-%s (%s)' \ + % (self.status, self.reason, self.code) + else: + s = 'Request ended with status %s-%s' % (self.status, + self.reason) + return s + + +class BadJSONResponse(APIError): + """exceptions raised for Bad Json Response.""" + + def __init__( + self, + error, + ): + self.error = error + + def __str__(self): + s = 'Bad response: %s' % self.error + return s + + +class InvalidInput(APIError): + """exceptions raised for Invalid Input.""" + + def __init__( + self, + error, + ): + self.error = error + + def __str__(self): + s = 'Bad response: %s' % self.error + return s + + +class SessionParameters(object): + """ + Represents functionalities that could help in getting the + session information. + """ + + def __init__( + self, + conffile=DEFAULT_CONF): + """ + Attributes: + \param conffile + config file which contains all configuration + information with sections + """ + self.conffile = conffile + self.username = None + self.passwd = None + self.host = '127.0.0.1' + self.port = '8001' + self.params = 'config' + self.logfile = 'kimchi-api-test-suite.log' + self.session_section = 'Session' + self.logging = logging + self.loglevel = 'ERROR' + self.LEVELS = {'INFO': self.logging.INFO, + 'DEBUG': self.logging.DEBUG, + 'WARNING': self.logging.WARNING, + 'ERROR': self.logging.ERROR, + 'CRITICAL': self.logging.CRITICAL} + if self.conffile is None: + print 'Configuration file required %s' \ + % self.conffile + else: + print 'Reading configuration file %s' % self.conffile + self.params = ConfigParser.ConfigParser() + print self.params.read(self.conffile) + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'logfile'): + self.logfile = self.params.get( + self.session_section, 'logfile') + + if self.params.has_option(self.session_section, 'loglevel'): + self.loglevel = self.params.get( + self.session_section, 'loglevel') + else: + print "Section %s is not available in the config file " \ + % self.session_section + + self.logging.basicConfig(format='%(asctime)s %(levelname)s: \ + %(message)s', + datefmt='%m-%d-%Y %I:%M:%S %p', + filename=self.logfile, + level=self.LEVELS[self.loglevel]) + + def getcredentials(self): + """ + Returns user and password details for a session obtained from config + file + """ + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'user'): + self.username = self.params.get(self.session_section, 'user') + + if self.params.has_option(self.session_section, 'passwd'): + self.passwd = self.params.get(self.session_section, 'passwd') + else: + print "Section %s is not available in the config file " \ + % self.session_section + raise APIError('Session section not present in config') + + return self.username, self.passwd + + def geturlparams(self): + """ + Returns host and port details for a session obtained from config file + """ + if self.params.has_section(self.session_section): + if self.params.has_option(self.session_section, 'host'): + self.host = self.params.get(self.session_section, 'host') + + if self.params.has_option(self.session_section, 'port'): + self.port = self.params.get(self.session_section, 'port') + else: + print "Section %s is not available in the config file " \ + % self.session_section + + return self.host, self.port + + def getlogger(self): + return self.logging + + +class Validator(object): + """ + validator class having different validate methods + """ + + def __init__(self, logger=None): + """ + :param logger: + :return: + """ + if logger is not None: + self.logging = logger + + def validate_json(self, jsn, schma): + """ + validates json against schema using jsonschema.validate library + + :param jsn: JSON to validate + :param schma: Schema against which JSON should get validated + :return: + """ + if logging is not None: + self.logging.info('--> validate_json()') + if logging is not None: + self.logging.debug( + 'validate_json(): jsn:%s' % jsn) + self.logging.debug( + 'validate_json(): schma:%s' % schma) + jsonschema.validate(jsn, schma) + if logging is not None: + self.logging.debug( + 'validate_json(): jsn is valid') + if logging is not None: + self.logging.info('<-- validate_json()')

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/fvt_base.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/fvt/fvt_base.py diff --git a/tests/fvt/fvt_base.py b/tests/fvt/fvt_base.py new file mode 100644 index 0000000..75ed140 --- /dev/null +++ b/tests/fvt/fvt_base.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA + +import unittest +import os +from restapilib import APISession, APIError, APIRequestError +from restapilib import Validator + +DEFAULT_CONF = os.path.dirname(os.path.abspath(__file__)) + 'config' + + +class TestBase(unittest.TestCase): + """Represents an API session setup with the host Web Services API""" + + session = None + + def __init__(self, method='runTest'): + """init with default method as runTest""" + super(TestBase, self).__init__(method) + + @classmethod + def setUpClass(cls): + """ + Hook method for setting up class fixture + before running tests in the class. + Create session and set auth to the session + """ + print '--> TestBase.setUpClass(): Create session ' + cls.session = APISession() + # Log on to the API. An exception will be raised if this fails. + cls.logging = cls.session.logging + cls.logging.debug('--> TestBase.setUpClass()') + cls.validator = Validator(cls.logging) + cls.logging.debug('TestBase.setUpClass(): Setting auth to session') + try: + cls.session.auth() + cls.logging.debug('TestBase.setUpClass(): Auth details set to ' + 'session and base URI created as %s' + % cls.session._base_uri) + except APIError, err: + print 'ERROR %s' % err + print err.__str__() + if cls.session is not None: + cls.logging.error('TestBase.setUpClass(): Ending session' + ' as API error happened') + cls.session.end_session() + finally: + cls.logging.debug('<-- TestBase.setUpClass()') + + def setUp(self): + """Hook method for setting up the test fixture before exercising it.""" + pass + + def tearDown(self): + """Hook method for deconstructing the test fixture after testing it.""" + pass + + @classmethod + def tearDownClass(cls): + """ + Hook method for deconstructing the class + fixture after running all tests in the class. + """ + if cls.session is not None: + print 'TestBase.tearDownClass(): Ending session' + cls.session.end_session() + + def get(self): + try: + resp_net = self.session.request_get_json('/') + print resp_net + except APIRequestError as err: + print 'ERROR %s' % err + print err.__str__() -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/run_tests.sh.in | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100755 tests/fvt/run_tests.sh.in diff --git a/tests/fvt/run_tests.sh.in b/tests/fvt/run_tests.sh.in new file mode 100755 index 0000000..aa8be67 --- /dev/null +++ b/tests/fvt/run_tests.sh.in @@ -0,0 +1,92 @@ +#!/bin/bash +# +# Project Wok +# +# Copyright IBM, Corp. 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301USA + +mkdir -p venv + +HAVE_UNITTEST=@HAVE_PYMOD_UNITTEST@ +PYTHON_VER=@PYTHON_VERSION@ + +# Verify if the required commands exists on the system +command -v virtualenv >/dev/null 2>&1 || { echo >&2 "virtualenv must be installed for your distribution. Aborting."; exit 1; } +command -v pip >/dev/null 2>&1 || { echo >&2 "pip must be installed for your distribution. Aborting."; exit 1; } + +# Get absolute path of this script +pushd `dirname $0` > /dev/null +SCRIPTPATH=`pwd -P` +reqfile=$SCRIPTPATH'/requirements.txt' + +# Start the virtual environment +virtualenv venv --no-site-packages + + +# Actiate the virtual environment +source venv/bin/activate + +while read line; do + + case "$line" in + \#*) + continue ;; # skip comments + "") + continue ;; # skip empty lines + *) + venv/bin/python$PYTHON_VER -c "import $line" > /dev/null 2>&1 + status=$? + if [ $status -ne 0 ]; then + pip install -r $reqfile # Install the required modules to run tests + break + fi + esac +done < $reqfile + +# Execute the test suite +#python registered_tests.py +#nosetests --with-html --html-file=test_report.html registered_tests.py + +if [ "$1" = "-v" ]; then + OPTS="-v" + shift +else + OPTS="" +fi + +if [ $# -ne 0 ]; then + ARGS="$@" +else + ARGS=`find -name "fvt_*.py" | xargs -I @ basename @ .py` +fi + +if [ "$HAVE_UNITTEST" != "yes" -o "$PYTHON_VER" == "2.6" ]; then + CMD="unit2" +else + CMD="python -m unittest" +fi + +LIST=($ARGS) +FVT_LIST=() +for ((i=0;i<${#LIST[@]};i++)); do + FVT_LIST+=(${LIST[$i]}) +done +PYTHONPATH=../ $CMD $OPTS ${FVT_LIST[@]} + +# Deativate the virtual environment +deactivate + +rm -fr venv -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/fvt/Makefile.am | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/fvt/Makefile.am diff --git a/tests/fvt/Makefile.am b/tests/fvt/Makefile.am new file mode 100644 index 0000000..3a88f43 --- /dev/null +++ b/tests/fvt/Makefile.am @@ -0,0 +1,43 @@ +# +# Project Wok +# +# Copyright IBM Corp, 2015 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +EXTRA_DIST = \ + Makefile.am \ + run_tests.sh.in \ + $(wildcard *.py) \ + $(NULL) + +noinst_SCRIPTS = run_tests.sh + +do_substitution = \ + sed -e 's,[@]HAVE_PYMOD_UNITTEST[@],$(HAVE_PYMOD_UNITTEST),g' \ + -e 's,[@]PYTHON_VERSION[@],$(PYTHON_VERSION),g' + +run_tests.sh: run_tests.sh.in Makefile + $(do_substitution) < $(srcdir)/run_tests.sh.in > run_tests.sh + chmod +x run_tests.sh + +CLEANFILES = run_tests.sh + +check-fvt: + ./run_tests.sh + rm -fr venv + +clean-local: + rm -fr venv -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- tests/Makefile.am | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Makefile.am b/tests/Makefile.am index e9800a5..cd66d90 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -17,6 +17,8 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +SUBDIRS = fvt + EXTRA_DIST = \ Makefile.am \ run_tests.sh.in \ @@ -46,5 +48,9 @@ check-local: $(MKDIR_P) $(top_srcdir)/data/screenshots ./run_tests.sh +check-fvt: + fvt/run_tests.sh + rm -fr venv + BUILT_SOURCES = test_config.py CLEANFILES = run_tests.sh test_config.py -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- configure.ac | 1 + 1 file changed, 1 insertion(+) diff --git a/configure.ac b/configure.ac index dd3955e..32f9d94 100644 --- a/configure.ac +++ b/configure.ac @@ -140,6 +140,7 @@ AC_CONFIG_FILES([ contrib/wok.spec.fedora contrib/wok.spec.suse tests/Makefile + tests/fvt/Makefile ],[ chmod +x po/gen-pot ]) -- 2.1.0

From: Archana Singh <archus@linux.vnet.ibm.com> Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- Makefile.am | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 027344f..426c2f3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,7 @@ EXTRA_DIST = \ $(NULL) -PEP8_BLACKLIST = *src/wok/plugins,*src/wok/config.py,*src/wok/i18n.py,*tests/test_config.py +PEP8_BLACKLIST = *src/wok/plugins,*src/wok/config.py,*src/wok/i18n.py,*tests/test_config.py,*venv/* SKIP_PYFLAKES_ERR = "\./src/wok/websocket\.py" @@ -51,11 +51,15 @@ check-local: find . -path './.git' -prune -type f -o \ -name '*.py' -o -name '*.py.in' | xargs $(PYFLAKES) | \ grep -w -v $(SKIP_PYFLAKES_ERR) | \ + grep -w -v "venv" | \ while read LINE; do echo "$$LINE"; false; done $(PEP8) --version $(PEP8) --filename '*.py,*.py.in' --exclude="$(PEP8_BLACKLIST)" . +check-fvt: + tests/fvt/run_tests.sh + rm -fr venv # Link built mo files in the source tree to enable use of translations from # within the source tree @@ -164,6 +168,6 @@ VERSION: clean-local: - rm -rf mo rpm + rm -rf mo rpm tests/fvt/venv CLEANFILES = wok.spec `find "$(top_srcdir)" -type f -name "*.pyc" -print` -- 2.1.0

Reviewed-by: Chandra Shekhar Reddy Potula <chandra@linux.vnet.ibm.com> On 25/11/15 10:46 AM, archus@linux.vnet.ibm.com wrote:
From: Archana Singh <archus@linux.vnet.ibm.com>
Signed-off-by: Archana Singh <archus@linux.vnet.ibm.com> --- Makefile.am | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/Makefile.am b/Makefile.am index 027344f..426c2f3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -38,7 +38,7 @@ EXTRA_DIST = \ $(NULL)
-PEP8_BLACKLIST = *src/wok/plugins,*src/wok/config.py,*src/wok/i18n.py,*tests/test_config.py +PEP8_BLACKLIST = *src/wok/plugins,*src/wok/config.py,*src/wok/i18n.py,*tests/test_config.py,*venv/*
SKIP_PYFLAKES_ERR = "\./src/wok/websocket\.py"
@@ -51,11 +51,15 @@ check-local: find . -path './.git' -prune -type f -o \ -name '*.py' -o -name '*.py.in' | xargs $(PYFLAKES) | \ grep -w -v $(SKIP_PYFLAKES_ERR) | \ + grep -w -v "venv" | \ while read LINE; do echo "$$LINE"; false; done
$(PEP8) --version $(PEP8) --filename '*.py,*.py.in' --exclude="$(PEP8_BLACKLIST)" .
+check-fvt: + tests/fvt/run_tests.sh + rm -fr venv
# Link built mo files in the source tree to enable use of translations from # within the source tree @@ -164,6 +168,6 @@ VERSION:
clean-local: - rm -rf mo rpm + rm -rf mo rpm tests/fvt/venv
CLEANFILES = wok.spec `find "$(top_srcdir)" -type f -name "*.pyc" -print`
participants (3)
-
archus@linux.vnet.ibm.com
-
Chandra Shekhar Reddy Potula
-
Jayavardhan Katta