diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 962df5138b5c00c40dec781fe7162713f5d8d2cb..54ea51d92095a82547eb541054ef2a81c53477aa 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -29,7 +29,7 @@ pipelines: - cp -af $HBP/user-scripts/config_files/platform_venv/* $VIRTUAL_ENV_PATH/lib/python2.7/site-packages/ # Build - - export IGNORE_LINT='platform_venv|config_files' + - export IGNORE_LINT='platform_venv|config_files|examples/integration_test/test_experiment_folder' - make verify_base || { if [ -f pylint.txt ]; then echo "----------"; echo "PYLINT.TXT"; echo "----------";cat pylint.txt; fi; if [ -f pep8.txt ]; then echo "----------"; echo "PEP8.TXT"; echo "----------";cat pep8.txt; fi; exit 1; } # Coverage check diff --git a/examples/integration_test/it.py b/examples/integration_test/it.py index b544d7c0b6f6869b56cfa1165fad08cd5d6ebf3e..acdac7be2cbec26ac6c00da4694208f3843de835 100644 --- a/examples/integration_test/it.py +++ b/examples/integration_test/it.py @@ -39,12 +39,12 @@ try: except ImportError: print - print 'Failed to import the VirtualCoach or access packages in the platform_venv! Aborting.' + print('Failed to import the VirtualCoach or access packages in the platform_venv! Aborting.') print - print 'Please make sure you have:' + print('Please make sure you have:') print - print '\t1. run "make devinstall" in the VirtualCoach repo' - print '\t2. use "cle-virtual-coach it.py" to run this script' + print('\t1. run "make devinstall" in the VirtualCoach repo') + print('\t2. use "cle-virtual-coach it.py" to run this script') print sys.exit(-1) @@ -110,7 +110,7 @@ def run(oidc_username, storage_username): # running array of test case results, unfortunately we have to hardcode the number of # test cases because the indeterminate progress bar is not helpful for the tester - NUM_TEST_CASES = 28 + NUM_TEST_CASES = 30 results = TestCases(NUM_TEST_CASES) try: @@ -174,6 +174,32 @@ def run(oidc_username, storage_username): raise TestCaseError('Deleting a cloned Experiment failed') results.done(True) + ## + ## Import an Experiment Folder + ## + + results.start('Importing an experiment folder') + response = vc.import_experiment('test_experiment_folder') + new_experiment_id = json.loads(response.text)['destFolderName'] + if new_experiment_id not in vc._VirtualCoach__get_experiment_list(cloned=True): + raise TestCaseError('Importing an experiment folder failed') + else: + vc.delete_cloned_experiment(new_experiment_id) + results.done(True) + + ## + ## Import an Experiment Zipped Folder + ## + + results.start('Importing a zipped experiment folder') + response = vc.import_experiment('test_experiment_folder.zip') + new_experiment_id = json.loads(response.text)['destFolderName'] + if new_experiment_id not in vc._VirtualCoach__get_experiment_list(cloned=True): + raise TestCaseError('Importing an experiment folder failed') + else: + vc.delete_cloned_experiment(new_experiment_id) + results.done(True) + ## ## Experiment Launch and Simulation State Interaction ## diff --git a/examples/integration_test/test_experiment_folder.zip b/examples/integration_test/test_experiment_folder.zip new file mode 100644 index 0000000000000000000000000000000000000000..ffb8e92506759a282c0ad578cc09427da0d7c6ec Binary files /dev/null and b/examples/integration_test/test_experiment_folder.zip differ diff --git a/examples/integration_test/test_experiment_folder/ExDTemplateHusky.exc b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.exc new file mode 100644 index 0000000000000000000000000000000000000000..3701f06494af9f50c303cd7f71dd9d5b4149d7ef --- /dev/null +++ b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.exc @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<ExD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig" + xsi:schemaLocation="http://schemas.humanbrainproject.eu/SP10/2014/ExDConfig ../ExDConfFile.xsd"> + <name>Template Husky in empty environment</name> + <thumbnail>ExDTemplateHusky.jpg</thumbnail> + <description>This experiment loads the Husky robot in an empty world, with an idle brain and basic transfer functions. You are free to edit it.</description> + <tags>template husky robotics empty</tags> + <timeout>840</timeout> + <configuration type="3d-settings" src="ExDTemplateHusky.ini"/> + <configuration type="brainvisualizer" src="brainvisualizer.json"/> + <configuration type="user-interaction-settings" src="ExDTemplateHusky.uis"/> + <maturity>production</maturity> + <environmentModel src="empty_world/empty_world.sdf"> + <robotPose robotId="husky" x="0.0" y="0.0" z="0.5" roll="0.0" pitch="-0.0" yaw="0.0"/> + </environmentModel> + <bibiConf src="template_husky.bibi"/> + <cameraPose> + <cameraPosition x="4.5" y="0" z="1.8"/> + <cameraLookAt x="0" y="0" z="0.6"/> + </cameraPose> +</ExD> diff --git a/examples/integration_test/test_experiment_folder/ExDTemplateHusky.ini b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.ini new file mode 100644 index 0000000000000000000000000000000000000000..0091df91f8d2f1d66bc2c12b8e4147a84626564a --- /dev/null +++ b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.ini @@ -0,0 +1 @@ +{"dynamicEnvMap":true,"shadows":true,"antiAliasing":true,"ssao":false,"ssaoDisplay":false,"ssaoClamp":"0.94","ssaoLumInfluence":"1","rgbCurve":{"red":[[0,0],[0.410400390625,0.4481201171875],[0.761962890625,0.7957763671875],[1,1]],"green":[[0,0],[0.261962890625,0.2801513671875],[0.718994140625,0.7371826171875],[1,1]],"blue":[[0,0],[0.265869140625,0.2606201171875],[0.840087890625,0.7645263671875],[1,1]]},"levelsInBlack":"0.09","levelsInGamma":0.52,"levelsInWhite":"0.8","levelsOutBlack":0,"levelsOutWhite":1,"skyBox":"softgradient","sun":"","bloom":false,"bloomStrength":"0.83","bloomRadius":"0.29","bloomThreshold":"0.91","fog":true,"fogDensity":"0.02","fogColor":"#97a2af","pbrMaterial":true,"shadowSettings":[{"lightName":"sun","mapSize":4096,"cameraBottom":-10,"cameraLeft":-10,"cameraRight":10,"cameraTop":25,"bias":0.0003,"radius":1.2}]} \ No newline at end of file diff --git a/examples/integration_test/test_experiment_folder/ExDTemplateHusky.jpg b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9838a4aa4f1e13bd6abcbcdf9eb72de33979104a Binary files /dev/null and b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.jpg differ diff --git a/examples/integration_test/test_experiment_folder/ExDTemplateHusky.uis b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.uis new file mode 100644 index 0000000000000000000000000000000000000000..cca085f4eef1f9fe3b28de95fe4feebb21adad18 --- /dev/null +++ b/examples/integration_test/test_experiment_folder/ExDTemplateHusky.uis @@ -0,0 +1,9 @@ +{ + "camera": { + "defaultMode": "free-camera", + "sensitivity": { + "translation": 1.0, + "rotation": 1.0 + } + } +} diff --git a/examples/integration_test/test_experiment_folder/all_neurons_spike_monitor.py b/examples/integration_test/test_experiment_folder/all_neurons_spike_monitor.py new file mode 100644 index 0000000000000000000000000000000000000000..34a270b2a084c06fa66316ac45b90ceb124dfd3e --- /dev/null +++ b/examples/integration_test/test_experiment_folder/all_neurons_spike_monitor.py @@ -0,0 +1,12 @@ +# Imported Python Transfer Function +# +import hbp_nrp_cle.tf_framework as nrp +# This specifies that the neurons 0 to 2 of the circuit population +# should be monitored. You can see them in the spike train widget +@nrp.NeuronMonitor(nrp.brain.record, nrp.spike_recorder) +def all_neurons_spike_monitor(t): + # Uncomment to log into the 'log-console' visible in the simulation + # clientLogger.info("Time: ", t) + return True +# + diff --git a/examples/integration_test/test_experiment_folder/brainvisualizer.json b/examples/integration_test/test_experiment_folder/brainvisualizer.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/examples/integration_test/test_experiment_folder/brainvisualizer.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/integration_test/test_experiment_folder/grab_image.py b/examples/integration_test/test_experiment_folder/grab_image.py new file mode 100644 index 0000000000000000000000000000000000000000..3209ff7a7a6ee2452668e006f1ee407f510eb348 --- /dev/null +++ b/examples/integration_test/test_experiment_folder/grab_image.py @@ -0,0 +1,12 @@ +# Imported Python Transfer Function +# +import sensor_msgs.msg +@nrp.MapRobotSubscriber("camera", Topic('/husky/husky/camera', sensor_msgs.msg.Image)) +@nrp.MapSpikeSource("input_neuron", nrp.brain.neurons[0], nrp.poisson) +@nrp.Robot2Neuron() +# Example TF: get image and fire at constant rate. You could do something with the image here and fire accordingly. +def grab_image(t, camera, input_neuron): + image = camera.value + input_neuron.rate = 10 +# + diff --git a/examples/integration_test/test_experiment_folder/template_husky.bibi b/examples/integration_test/test_experiment_folder/template_husky.bibi new file mode 100644 index 0000000000000000000000000000000000000000..a7452cabdbd23918cca265c6cf29008930fbe4bc --- /dev/null +++ b/examples/integration_test/test_experiment_folder/template_husky.bibi @@ -0,0 +1,12 @@ +<?xml version="1.0" ?> +<ns1:bibi xmlns:ns1="http://schemas.humanbrainproject.eu/SP10/2014/BIBI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <ns1:brainModel> + <ns1:file>brain_model/idle_brain.py</ns1:file> + <ns1:populations from="0" population="neurons" to="2" xsi:type="ns1:Range"/> + <ns1:populations from="0" population="record" to="2" xsi:type="ns1:Range"/> + </ns1:brainModel> + <ns1:bodyModel robotId="husky">husky_model/model.sdf</ns1:bodyModel> + <ns1:transferFunction src="all_neurons_spike_monitor.py" xsi:type="ns1:PythonTransferFunction"/> + <ns1:transferFunction src="turn_around.py" xsi:type="ns1:PythonTransferFunction"/> + <ns1:transferFunction src="grab_image.py" xsi:type="ns1:PythonTransferFunction"/> +</ns1:bibi> diff --git a/examples/integration_test/test_experiment_folder/turn_around.py b/examples/integration_test/test_experiment_folder/turn_around.py new file mode 100644 index 0000000000000000000000000000000000000000..ba5da46d3e80a194502245fc26b1df7b310b657f --- /dev/null +++ b/examples/integration_test/test_experiment_folder/turn_around.py @@ -0,0 +1,14 @@ +# Imported Python Transfer Function +# +import hbp_nrp_cle.tf_framework as nrp +from hbp_nrp_cle.robotsim.RobotInterface import Topic +import geometry_msgs.msg +@nrp.MapSpikeSink("output_neuron", nrp.brain.neurons[1], nrp.leaky_integrator_alpha) +@nrp.Neuron2Robot(Topic('/husky/husky/cmd_vel', geometry_msgs.msg.Twist)) +# Example TF: get output neuron voltage and output constant on robot actuator. You could do something with the voltage here and command the robot accordingly. +def turn_around(t, output_neuron): + voltage=output_neuron.voltage + return geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(0,0,0), + angular=geometry_msgs.msg.Vector3(0,0,5)) +# + diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py index 53d842591cec9a932bdeba47510fc5c42e10fe65..118d9a7ed40e9438494a80e640f244f5a9726189 100644 --- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py +++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/config.py @@ -86,7 +86,7 @@ class Config(dict): self.__validate('proxy-services', ['experiment-list', 'available-servers', 'server-info', 'experiment-clone', 'experiment-delete', 'storage-authentication', 'storage-experiment-list', - 'csv-files', 'experiment-file']) + 'csv-files', 'experiment-file', 'experiment-import']) self.__validate('simulation-services', ['create', 'state', 'reset']) self.__validate('simulation-scripts', ['state-machine', 'transfer-function', 'brain', 'sdf-world']) diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test.zip b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test.zip new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_experiment_folder/empty_file.txt b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_experiment_folder/empty_file.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py index 77741b6b5ea70c0dc6020b07672f50b2ae9a66dd..5a98e3a1f98fa89440af298d1245c78cd12c2c6e 100644 --- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py +++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/tests/test_virtual_coach.py @@ -39,6 +39,7 @@ import copy from dateutil import parser import json from StringIO import StringIO +import os class TestVirtualCoach(unittest.TestCase): @@ -58,6 +59,7 @@ class TestVirtualCoach(unittest.TestCase): return realimport(name, globals, locals, fromlist, level) builtins.__import__ = rospy_import_fail + self._tests_path = os.path.dirname(os.path.realpath(__file__)) self._vc = VirtualCoach(environment='local', storage_username='nrpuser') @@ -626,3 +628,18 @@ mock-server-5 request.return_value = Request() self._vc.set_experiment_file('exp_id', 'file_name', 'file_content') + @patch('requests.post') + def test_import_experiment_zip(self, mock_request): + mock_request.side_effect = [MagicMock(status_code=requests.codes.ok)] + self.assertRaises(Exception, self._vc.import_experiment, 'imaginary_file.zip') + path = os.path.join(self._tests_path, 'test.zip') + response = self._vc.import_experiment(path) + self.assertEqual(response.status_code, requests.codes.ok) + + @patch('requests.post') + def test_import_experiment_folder(self, mock_request): + mock_request.side_effect = [MagicMock(status_code=requests.codes.ok)] + self.assertRaises(Exception, self._vc.import_experiment, 'imaginary_experiment_folder') + path = os.path.join(self._tests_path, 'test_experiment_folder') + response = self._vc.import_experiment(path) + self.assertEqual(response.status_code, requests.codes.ok) diff --git a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py index 3edcd0286d732a8f4b7f689cd5ab89a16e6e8a5e..b5a5e896e258fd9485c85fc58409ae2c20644823 100644 --- a/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py +++ b/hbp_nrp_virtual_coach/hbp_nrp_virtual_coach/virtual_coach.py @@ -38,6 +38,9 @@ import requests import logging import getpass import httplib +import os +import zipfile +import tempfile from texttable import Texttable from copy import copy @@ -603,3 +606,74 @@ class VirtualCoach(object): % (response.status_code, response)) return response.content + + @staticmethod + def __zip_directory(dirpath, zip_filehandle): + """ + Internal helper function + It zips the target directory + + :param dirpath: Path to the experiment folder to be zipped + :param zip_filehandle: Handle of the zip file to be populated + """ + dirpath = os.path.abspath(dirpath) + dirname = os.path.dirname(dirpath) + basename = os.path.basename(dirpath) + os.chdir(dirname) + for root, _, files in os.walk(basename): + for f in files: + zip_filehandle.write(os.path.join(root, f)) + + @staticmethod + def __get_directory_content(dirpath): + """ + Internal helper function + It zips the target folder and returns its content + + :param dirpath: path to the experiment folder to be zipped + """ + temp = tempfile.mktemp() + zip_file = zipfile.ZipFile(temp, 'w', zipfile.ZIP_DEFLATED) + try: + VirtualCoach.__zip_directory(dirpath, zip_file) + except Exception as e: + logger.error('The folder %s could not be zipped', dirpath) + raise e + zip_file.close() + content = open(temp, 'r').read() + os.remove(temp) + return content + + def import_experiment(self, path): + """ + Imports an experiment folder, possibly a zipped folder, into user storage + + :param path: path to the experiment folder or to the zip file to be imported + :type path: str + """ + if not isinstance(path, str): + raise TypeError('The provided argument is not a string.') + if not os.path.isfile(path) and not os.path.isdir(path): + raise ValueError('The file or folder named %(path)s does not exist.' % {'path': path}) + + url = self.__config['proxy-services']['experiment-import'] + file_headers = copy(self.__http_headers) + file_headers['Content-Type'] = 'application/octet-stream' + content = None + if os.path.isdir(path): + # Handles an experiment folder + content = VirtualCoach.__get_directory_content(path) + else: + # Handles a zip file + try: + content = open(path, 'r').read() + except Exception as e: + logger.error('The file %s could not be open', path) + raise e + + response = requests.post(url, data=content, headers=file_headers) + # pylint: disable=no-member + if response.status_code != requests.codes.ok: + raise Exception('Error when importing experiment: %d. Error: %s' + % (response.status_code, response)) + return response diff --git a/verify.sh b/verify.sh index 58373b3b02445a7c37986dbc7e8adf0b119468f2..d09f05fbf3a3dffbca2d1575d60a765cb56be088 100755 --- a/verify.sh +++ b/verify.sh @@ -1,8 +1,8 @@ #!/bin/bash # This script is designed for local usage. -# Ignore generated files. -export IGNORE_LINT="platform_venv" +# Ignore generated files and files provided for testing. +export IGNORE_LINT="platform_venv|config_files|examples/integration_test/test_experiment_folder" export VIRTUAL_ENV=$NRP_VIRTUAL_ENV # This script only runs static code analysis, the tests can be run separately using run_tests.sh