From d1f90619a3dec7e140d7a15d233de8f51a7da921 Mon Sep 17 00:00:00 2001
From: Philipp Schillinger <philsplus@gmail.com>
Date: Sat, 24 Nov 2018 16:41:24 +0100
Subject: [PATCH] Merge remote-tracking branch 'origin/develop'

---
 .gitignore                                 |  13 +-
 .travis.yml                                |  17 ++
 CMakeLists.txt                             |  45 +++-
 README.md                                  |  40 ++--
 bin/nwjs_install                           |  23 +-
 bin/run_app                                |  11 +-
 bin/shortcut                               |   6 +-
 bin/test_report                            |  20 ++
 launch/test_report.test                    |   7 +
 package.json                               |   2 +-
 package.xml                                |   7 +-
 src/_model/behavior.js                     |  11 +
 src/_model/state.js                        | 143 +++++++++++-
 src/_model/statemachine.js                 |  15 ++
 src/{_testing => _test}/scripts.js         |   0
 src/_test/statelib.test.js                 |  11 +
 src/_test/testreport.js                    |  21 ++
 src/_testing/test.js                       | 248 ---------------------
 src/events.js                              |   1 +
 src/io/io_behaviorloader.js                |  83 +++----
 src/io/io_behaviorpacker.js                |   5 +-
 src/io/io_behaviorsaver.js                 |   5 +-
 src/io/io_manifestparser.js                |   4 +-
 src/io/io_packageparser.js                 | 105 ++++++---
 src/prototype.js                           |   5 +
 src/ros/ros.js                             |  43 +++-
 src/ui/panels/ui_panels_stateproperties.js |  16 ++
 src/ui/ui_dashboard.js                     |   1 +
 src/ui/ui_menu.js                          |   7 +-
 src/ui/ui_settings.js                      |   8 +-
 src/ui/ui_statemachine.js                  |   6 +-
 src/window.html                            |   5 +-
 src/window.js                              |   6 +
 33 files changed, 531 insertions(+), 409 deletions(-)
 create mode 100644 .travis.yml
 create mode 100755 bin/test_report
 create mode 100644 launch/test_report.test
 rename src/{_testing => _test}/scripts.js (100%)
 create mode 100644 src/_test/statelib.test.js
 create mode 100644 src/_test/testreport.js
 delete mode 100644 src/_testing/test.js

diff --git a/.gitignore b/.gitignore
index c8524f2..2c0b2c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1 @@
-lib/*
-locales/*
-swiftshader/*
-credits.html
-icudtl.dat
-natives_blob.bin
-nw
-nw_100_percent.pak
-nw_200_percent.pak
-resources.pak
-snapshot_blob.bin
-
+nwjs/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a41323f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+sudo: required
+dist: trusty
+language: generic
+
+before_install:
+  - git clone https://github.com/FlexBE/flexbe_ci.git ~/flexbe_ci
+  - source ~/flexbe_ci/setup.bash
+  - ~/flexbe_ci/ci_scripts/before_install.bash
+  
+install:
+  - ~/flexbe_ci/ci_scripts/install.bash
+  
+before_script:
+  - ~/flexbe_ci/ci_scripts/before_script.bash
+  
+script:
+  - ~/flexbe_ci/ci_scripts/script.bash
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7b44927..575d37d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,23 +21,50 @@ catkin_package(
 # use add_library() or add_executable() as required
 #add_library(${PROJECT_NAME} ${${PROJECT_NAME}_SRCS})
 
-#FILE(GLOB BIN_FILES "bin/*")
-
-# install executables
-#install(PROGRAMS ${BIN_FILES} DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})
-#install(DIRECTORY launch DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION})
-
 #add_custom_target(nwjs_inst)
 #add_custom_command(TARGET nwjs_inst POST_BUILD COMMAND bin/nwjs_install)
 
-add_custom_command(OUTPUT nw
+add_custom_command(OUTPUT nwjs
   COMMAND bin/nwjs_install
   WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
   VERBATIM
 )
 
-add_custom_target(nw_install DEPENDS nw)
+add_custom_target(nw_install DEPENDS nwjs)
 
-if(NOT ${CMAKE_CURRENT_SOURCE_DIR}/nw)
+if(NOT ${CMAKE_CURRENT_SOURCE_DIR}/nwjs)
   safe_execute_process(COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/bin/nwjs_install)
 endif()
+
+#############
+## Install ##
+#############
+
+INSTALL(PROGRAMS
+    bin/run_app
+    bin/shortcut
+    DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
+)
+INSTALL(FILES
+    package.json
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+INSTALL(DIRECTORY
+    src
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+INSTALL(DIRECTORY
+    nwjs
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+    USE_SOURCE_PERMISSIONS
+)
+install(DIRECTORY
+    launch
+    DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
+)
+
+# run tests
+if(CATKIN_ENABLE_TESTING)
+	find_package(rostest REQUIRED)
+	add_rostest(launch/test_report.test)
+endif()
diff --git a/README.md b/README.md
index 66e2e7a..6bf0208 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,6 @@
 
 User interface (editor + runtime control) for the FlexBE behavior engine.
 
-The FlexBE App in this repository replaces the previous *flexbe_chrome_app*. Please refer to the following announcement for an overview of the most important changes: [Future of the FlexBE Chrome App](https://github.com/pschillinger/flexbe_chrome_app/issues/11)
-
----
-
-FlexBE App branch: **feature/flexbe_app**
-
-*Changes to FlexBE required for running this version of the FlexBE App are not yet merged into master. Please checkout the above branch on all repos if available.*
-
----
-
 ## Installation
 
 Clone the following repos into your ROS workspace:
@@ -19,19 +9,12 @@ Clone the following repos into your ROS workspace:
     git clone https://github.com/team-vigir/flexbe_behavior_engine.git  # if not already present
     git clone https://github.com/FlexBE/flexbe_app.git
 
-As mentioned above, switch to the correct branch:
-
-    cd flexbe_behavior_engine
-    git fetch
-    git checkout feature/flexbe_app
-    cd ..
-
 Build you workspace:
 
-    cd ..
     catkin_make # or catkin build
 
 During the build process, the required nwjs binaries are automatically downloaded and extracted.
+To download the binaries manually instead, run the script `bin/nwjs_install`.
 
 ## Workspace
 
@@ -41,8 +24,6 @@ In order to create and prepare a new repository for behavior development, run th
 
 This will initialize a new local git repository with the correct workspace structure which you can then push to a desired remote location. Make sure that you build the workspace afterwards.
 
-If you have been using FlexBE already, you can convert the content of your repository according to the structure defined below. Besides adding the export statement to your state packages, you can automate this conversion by running the FlexBE App. If no behavior package is detected, it will suggest you to initialize one.
-
 ## Usage
 
 If desired, run the following command to create a shortcut in the application menu:
@@ -65,7 +46,24 @@ Use the following launch file to run both of the above for local behavior execut
 
     roslaunch flexbe_app flexbe_full.launch
 
-## Packages
+
+## Backwards Compatibility
+
+The FlexBE App in this repository replaces the previous *flexbe_chrome_app*. Please refer to the following announcement for an overview of the most important changes: [Future of the FlexBE Chrome App](https://github.com/pschillinger/flexbe_chrome_app/issues/11)
+
+If you have been using FlexBE already with the old Chrome app, you can convert the content of your repository according to the structure defined below. Besides adding the export statement to your state packages, you can automate this conversion by running the FlexBE App. If no behavior package is detected, it will suggest you to initialize one.
+
+---
+
+Deprecated Chrome App branch: **deprecated/chrome_app**
+
+*Please checkout the above branch on all repos if available for a best-effort support of the deprecated Chrome app. However, please consider to update as soon as possible according to the instructions below to ensure that the system will remain working in the future and to receive all updates.*
+
+---
+
+Please note that the way how state and behavior packages are detected has changed and breaks direct compatibility.
+Follow the instructions below to make the required changes.
+Behavior packages can also be converted automatically by the new FlexBE App.
 
 ### State packages
 
diff --git a/bin/nwjs_install b/bin/nwjs_install
index f72c17f..9dfc7f4 100755
--- a/bin/nwjs_install
+++ b/bin/nwjs_install
@@ -1,15 +1,24 @@
 #!/bin/bash -e
 cd "$( dirname "${BASH_SOURCE[0]}" )"
 TEMPFILE="nwjs.tar.gz"
-cd ..
+mkdir -p ../nwjs
+cd ../nwjs
+
+VERSION="v0.34.1" # verified working, to be incremented explicitly
 
-# check if nw is already existing and abort if so
+# check if nw is already existing and up-to-date, abort if so
 if [ -f nw ]; then
-  exit 0
+	if [ -f version ] && [ "$(cat version)" == "$VERSION" ]; then
+		exit 0
+	else
+		cd ..
+		mv nwjs nwjs_old
+		mkdir nwjs
+		cd nwjs
+	fi
 fi
 
 # determine correct executable version
-VERSION="v0.27.2" # verified working, to be incremented explicitly
 OS="linux"
 if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
 	OS="win"
@@ -32,5 +41,11 @@ wget -q -O $TEMPFILE https://dl.nwjs.io/$VERSION/nwjs-$VERSION-$OS-$ARCH.tar.gz
 echo "Unpacking..."
 tar -xzf $TEMPFILE --strip-components 1
 rm $TEMPFILE
+echo "$VERSION" > version
+
+cd ..
+if [ -f nwjs_old/nw ]; then
+	rm -rf nwjs_old
+fi
 
 echo -e "Successfully downloaded nwjs!"
diff --git a/bin/run_app b/bin/run_app
index 32a25a5..3164ef1 100755
--- a/bin/run_app
+++ b/bin/run_app
@@ -1,11 +1,16 @@
 #!/bin/bash
-cd "$( dirname "${BASH_SOURCE[0]}" )"
 
-if [ ! -f ../nw ]; then
+ROOT_PATH="$(rospack find flexbe_app)"
+NW=$ROOT_PATH/nwjs/nw
+if [ ! -f $NW ]; then
+    ROOT_PATH=.
+    NW=$ROOT_PATH/nwjs/nw
+fi
+if [ ! -x $NW ]; then
   echo "Cannot run flexbe_app, need to download nwjs first."
   echo "Please build flexbe_app via catkin before using it or run the following command now:"
   echo "  rosrun flexbe_app nwjs_install"
   exit -1
 fi
 
-../nw --password-store=basic $*
+"$NW" --password-store=basic $ROOT_PATH "$@"
diff --git a/bin/shortcut b/bin/shortcut
index c034e04..e4cf77c 100755
--- a/bin/shortcut
+++ b/bin/shortcut
@@ -1,10 +1,10 @@
 #!/bin/bash
-if [ $1 == "create" ] ; then
-	cd "$( dirname "${BASH_SOURCE[0]}" )"/..
+if [ "$1" == "create" ] ; then
+	cd $(rospack find flexbe_app)
 	cp flexbe.desktop ~/.local/share/applications
 	cp src/img/icon-128.png ~/.local/share/icons/flexbe_app.png
 	echo "Launcher shortcut created"
-elif [ $1 == "remove" ] ; then
+elif [ "$1" == "remove" ] ; then
 	rm ~/.local/share/applications/flexbe.desktop
 	rm ~/.local/share/icons/flexbe_app.png
 	echo "Launch shortcut removed"
diff --git a/bin/test_report b/bin/test_report
new file mode 100755
index 0000000..732468f
--- /dev/null
+++ b/bin/test_report
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+import yaml
+import unittest
+
+class TestReport(unittest.TestCase):
+
+	def test_report(self):
+		try:
+			with open("/tmp/flexbe_app_report.log", 'r') as f:
+				report = yaml.load(f)
+		except IOError:
+			return # skip test since there is no report to evaluate
+		for test_type, tests in report.items():
+			for test_name, test_data in tests.items():
+				if test_type == "assertTrue":
+					self.assertTrue(test_data, test_name)
+
+if __name__ == '__main__':
+    import rosunit
+    rosunit.unitrun("flexbe_app", "test_report", TestReport)
diff --git a/launch/test_report.test b/launch/test_report.test
new file mode 100644
index 0000000..9c7189d
--- /dev/null
+++ b/launch/test_report.test
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+
+<launch>
+
+<test test-name="flexbe_app" name="flexbe_app" pkg="flexbe_app" type="test_report" />
+
+</launch>
diff --git a/package.json b/package.json
index 2d496a3..a017589 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "FlexBE App",
-  "version": "2.0.6",
+  "version": "2.0.10",
   "main": "src/main.js",
   "window": {
     "icon": "src/img/icon-128.png",
diff --git a/package.xml b/package.xml
index ce310e3..2eb2c99 100644
--- a/package.xml
+++ b/package.xml
@@ -1,10 +1,10 @@
 <package>
   <name>flexbe_app</name>
-  <version>2.0.6</version>
+  <version>2.0.10</version>
   <description>
      flexbe_app provides a user interface (editor + runtime control) for the FlexBE behavior engine.
   </description>
-  <maintainer email="schillinger@sim.tu-darmstadt.de">Philipp Schillinger</maintainer>
+  <maintainer email="schillin@kth.se">Philipp Schillinger</maintainer>
   <license>BSD</license>
 
   <url>http://ros.org/wiki/flexbe_app</url>
@@ -15,5 +15,8 @@
   <run_depend>rospy</run_depend>
   <run_depend>genpy</run_depend>
   <run_depend>actionlib</run_depend>
+  <run_depend>rospack</run_depend>
+
+  <test_depend>rosunit</test_depend>
 
 </package>
diff --git a/src/_model/behavior.js b/src/_model/behavior.js
index bf1c4e6..df71c7f 100644
--- a/src/_model/behavior.js
+++ b/src/_model/behavior.js
@@ -31,6 +31,8 @@ Behavior = new (function() {
 	var file_name = undefined;
 	var manifest_path = undefined;
 
+	var readonly = false;
+
 
 	this.getBehaviorName = function() {
 		return behavior_name;
@@ -274,6 +276,14 @@ Behavior = new (function() {
 		root_sm = _root_sm;
 	}
 
+	this.setReadonly = function(_readonly) {
+		readonly = _readonly;
+	}
+
+	this.isReadonly = function() {
+		return readonly;
+	}
+
 	this.resetBehavior = function() {
 		behavior_name = "";
 		behavior_package = "";
@@ -299,6 +309,7 @@ Behavior = new (function() {
 		comment_notes = [];
 
 		root_sm = new Statemachine("", new WS.StateMachineDefinition([], [], []));
+		readonly = false;
 	}
 
 	this.setFiles = function(_file_name, _manifest_path) {
diff --git a/src/_model/state.js b/src/_model/state.js
index 75a612e..560a521 100644
--- a/src/_model/state.js
+++ b/src/_model/state.js
@@ -130,6 +130,9 @@ State = function(state_name, state_def) {
 		resolved_parameter_output_values_old = resolved_parameter_output_values;
 	}
 
+	var noMetaFilter = function(element) { return element[0] != '$'; };
+	var metaFilter = function(element) { return element[0] == '$'; };
+
 	var state_name = state_name;
 	var state_class = state_def.getStateClass();
 	var state_import = state_def.getStatePath();
@@ -139,22 +142,22 @@ State = function(state_name, state_def) {
 	var parameters = state_def.getParameters().clone();
 	var parameter_values = state_def.getDefaultParameterValues().clone();
 
-	var outcomes = state_def.getOutcomes().filter(function(element) { return element[0] != '$'; });
+	var outcomes = state_def.getOutcomes().filter(noMetaFilter);
 	var autonomy = state_def.getDefaultAutonomy().clone();
 
-	var meta_outcomes = state_def.getOutcomes().filter(function(element) { return element[0] == '$'; });
+	var meta_outcomes = state_def.getOutcomes().filter(metaFilter);
 	for (var i=0; i<meta_outcomes.length; ++i) meta_outcomes[i] = meta_outcomes[i].slice(1, meta_outcomes[i].length);
 
 	var outcomes_unc = outcomes.clone();
 	var outcomes_con = [];
 
-	var input_keys = state_def.getInputKeys().filter(function(element) { return element[0] != '$'; });
-	var output_keys = state_def.getOutputKeys().filter(function(element) { return element[0] != '$'; });
+	var input_keys = state_def.getInputKeys().filter(noMetaFilter);
+	var output_keys = state_def.getOutputKeys().filter(noMetaFilter);
 
-	var meta_input = state_def.getInputKeys().filter(function(element) { return element[0] == '$'; });
+	var meta_input = state_def.getInputKeys().filter(metaFilter);
 	for (var i=0; i<meta_input.length; ++i) meta_input[i] = meta_input[i].slice(1, meta_input[i].length);
 
-	var meta_output = state_def.getOutputKeys().filter(function(element) { return element[0] == '$'; });
+	var meta_output = state_def.getOutputKeys().filter(metaFilter);
 	for (var i=0; i<meta_output.length; ++i) meta_output[i] = meta_output[i].slice(1, meta_output[i].length);
 
 	var input_mapping = [];
@@ -357,4 +360,132 @@ State = function(state_name, state_def) {
 		return behavior;
 	}
 
+	this.updateStateDefinition = function(new_def) {
+		state_class = new_def.getStateClass();
+		state_import = new_def.getStatePath();
+		state_pkg = new_def.getStatePackage();
+
+		var updateKeys = function(old_keys, new_keys, old_values, new_values) {
+			var result = {keys: [], values: []};
+			for (var i=0; i<new_keys.length; i++) {
+				var j = old_keys.indexOf(new_keys[i]);
+				if (j != -1) {
+					result.keys.push(old_keys[j]);
+					result.values.push(old_values[j]);
+				} else {
+					result.keys.push(new_keys[i]);
+					result.values.push(new_values[i]);
+				}
+			}
+			return result;
+		}
+
+		// required for meta
+		var old_params = parameters;
+
+		if (!parameters.hasSameElements(new_def.getParameters())) {
+			var result = updateKeys(parameters, new_def.getParameters(), parameter_values, new_def.getDefaultParameterValues());
+			parameters = result.keys;
+			parameter_values = result.values;
+		}
+
+		if (!outcomes.hasSameElements(new_def.getOutcomes().filter(noMetaFilter))) {
+			var old_outcomes = outcomes;
+			var result = updateKeys(outcomes, new_def.getOutcomes().filter(noMetaFilter), autonomy, new_def.getDefaultAutonomy());
+			outcomes = result.keys;
+			autonomy = result.values;
+			for (var i=0; i<outcomes.length; i++) {
+				if (!old_outcomes.contains(outcomes[i])) {
+					outcomes_unc.push(outcomes[i]);
+				}
+			}
+			for (var i=0; i<old_outcomes.length; i++) {
+				if (!outcomes.contains(old_outcomes[i])) {
+					if (outcomes_con.contains(old_outcomes[i])) {
+						if (container != undefined) {
+							container.removeTransitionFrom(that, old_outcomes[i]);
+						}
+						outcomes_con.remove(old_outcomes[i]);
+					} else {
+						outcomes_unc.remove(old_outcomes[i]);
+					}
+				}
+			}
+		}
+
+		var new_meta_outcomes = new_def.getOutcomes().filter(metaFilter);
+		for (var i=0; i<new_meta_outcomes.length; ++i) new_meta_outcomes[i] = new_meta_outcomes[i].slice(1, new_meta_outcomes[i].length);
+		if (!meta_outcomes.hasSameElements(new_meta_outcomes)) {
+			for (var i=0; i<meta_outcomes.length; i++) {
+				if (!new_meta_outcomes.contains(meta_outcomes[i])) {
+					var pidx = old_params.indexOf(meta_outcomes[i]);
+					var remove_list = resolved_parameter_outcome_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = outcomes.indexOf(element);
+						outcomes.splice(idx, 1);
+						autonomy.splice(idx, 1);
+						if (outcomes_unc.contains(element)) {
+							outcomes_unc.remove(element);
+						} else if (outcomes_con.contains(element)) {
+							outcomes_con.remove(element);
+							if (container != undefined) {
+								container.removeTransitionFrom(that, element);
+							}
+						}
+					});
+				}
+			}
+			meta_outcomes = new_meta_outcomes;
+			updateGeneratedOutcomes(parameter_values);
+		}
+
+		if (!input_keys.hasSameElements(new_def.getInputKeys().filter(noMetaFilter))) {
+			var result = updateKeys(input_keys, new_def.getInputKeys().filter(noMetaFilter), input_mapping, new_def.getInputKeys().filter(noMetaFilter));
+			input_keys = result.keys;
+			input_mapping = result.values;
+		}
+
+		var new_meta_input = new_def.getInputKeys().filter(metaFilter);
+		for (var i=0; i<new_meta_input.length; ++i) new_meta_input[i] = new_meta_input[i].slice(1, new_meta_input[i].length);
+		if (!meta_input.hasSameElements(new_meta_input)) {
+			for (var i=0; i<meta_input.length; i++) {
+				if (!new_meta_input.contains(meta_input[i])) {
+					var pidx = old_params.indexOf(meta_input[i]);
+					var remove_list = resolved_parameter_input_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = input_keys.indexOf(element);
+						input_keys.splice(idx, 1);
+						input_mapping.splice(idx, 1);
+					});
+				}
+			}
+			meta_input = new_meta_input;
+			updateGeneratedInput(parameter_values);
+		}
+
+		if (!output_keys.hasSameElements(new_def.getOutputKeys().filter(noMetaFilter))) {
+			var result = updateKeys(output_keys, new_def.getOutputKeys().filter(noMetaFilter), output_mapping, new_def.getOutputKeys().filter(noMetaFilter));
+			output_keys = result.keys;
+			output_mapping = result.values;
+		}
+
+		var new_meta_output = new_def.getOutputKeys().filter(metaFilter);
+		for (var i=0; i<new_meta_output.length; ++i) new_meta_output[i] = new_meta_output[i].slice(1, new_meta_output[i].length);
+		if (!meta_output.hasSameElements(new_meta_output)) {
+			for (var i=0; i<meta_output.length; i++) {
+				if (!new_meta_output.contains(meta_output[i])) {
+					var pidx = old_params.indexOf(meta_output[i]);
+					var remove_list = resolved_parameter_output_values_old[i];
+					remove_list.forEach(function(element) {
+						var idx = output_keys.indexOf(element);
+						output_keys.splice(idx, 1);
+						output_mapping.splice(idx, 1);
+					});
+				}
+			}
+			meta_output = new_meta_output;
+			updateGeneratedOutput(parameter_values);
+		}
+	}
+
 };
\ No newline at end of file
diff --git a/src/_model/statemachine.js b/src/_model/statemachine.js
index ceaf522..c29591c 100644
--- a/src/_model/statemachine.js
+++ b/src/_model/statemachine.js
@@ -74,6 +74,21 @@ Statemachine = function(sm_name, sm_definition) {
 		}
 		return child.getStateByPath(path.slice(that.getStateName().length + 1));
 	}
+	this.traverseStates = function(_filter) {
+		var result = [];
+		for(var i=0; i<states.length; ++i) {
+			if (states[i] instanceof Statemachine) {
+				result = result.concat(states[i].traverseStates(_filter));
+				continue;
+			} else if (states[i] instanceof BehaviorState) {
+				continue;
+			}
+			if (_filter(states[i])) {
+				result.push(states[i]);
+			}
+		}
+		return result;
+	}
 
 	this.addState = function(state) {
 		states.push(state);
diff --git a/src/_testing/scripts.js b/src/_test/scripts.js
similarity index 100%
rename from src/_testing/scripts.js
rename to src/_test/scripts.js
diff --git a/src/_test/statelib.test.js b/src/_test/statelib.test.js
new file mode 100644
index 0000000..c3d62ec
--- /dev/null
+++ b/src/_test/statelib.test.js
@@ -0,0 +1,11 @@
+StatelibTest = new (function() {
+    var that = this;
+
+    this.testWaitStateAvailable = function(report, done) {
+        // flexbe_states/WaitState is found and properly imported
+        var result = WS.Statelib.getClassList().contains("WaitState");
+        report.assertTrue.test_statelib = result;
+        done();
+    }
+
+})();
\ No newline at end of file
diff --git a/src/_test/testreport.js b/src/_test/testreport.js
new file mode 100644
index 0000000..d24286a
--- /dev/null
+++ b/src/_test/testreport.js
@@ -0,0 +1,21 @@
+TestReport = new (function() {
+    var that = this;
+
+    var report = {'assertTrue': {}};
+
+    this.runAllTests = function(callback) {
+        var testCases = [
+            StatelibTest.testWaitStateAvailable
+        ];
+        var done = function(code) {
+            if (testCases.length == 0) {
+                IO.Filesystem.createFile('/tmp', "flexbe_app_report.log", JSON.stringify(report), callback);
+            } else {
+                var test = testCases.shift();
+                test(report, done);
+            }
+        }
+        done();
+    }
+
+})();
\ No newline at end of file
diff --git a/src/_testing/test.js b/src/_testing/test.js
deleted file mode 100644
index 7406703..0000000
--- a/src/_testing/test.js
+++ /dev/null
@@ -1,248 +0,0 @@
-console.log("### Test Cases Loaded ###");
-
-test_simpleSM = function() {
-	UI.Menu.toStatemachineClicked();
-
-	state = new State("Log_Anything", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 100;
-	state.getPosition().y += 20;
-	UI.Statemachine.getRoot().addState(state);
-
-	UI.Statemachine.refreshView();
-}
-
-test_simpleCP = function() {
-	UI.Menu.toStatemachineClicked();
-
-	var state1 = new State("Log_Anything", Statelib.getFromLib("LogState"));
-	state1.getPosition().x += 100;
-	state1.getPosition().y += 20;
-	Behavior.getStatemachine().addState(state1);
-
-	var state2 = new State("Bla", Statelib.getFromLib("CalculationState"));
-	state2.getPosition().x += 400;
-	state2.getPosition().y += 30;
-	Behavior.getStatemachine().addState(state2);
-
-	UI.Statemachine.beginTransition(state1, "done");
-	UI.Statemachine.connectTransition(state2);
-}
-
-test_runtimeControlDisplay = function() {
-	UI.Menu.toStatemachineClicked();
-
-	state = new State("Calc_Pose", Statelib.getFromLib("CalculationState"));
-	state.getPosition().x += 100;
-	state.getPosition().y += 40;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Grasp_Object", Statelib.getFromLib("GraspTemplateState"));
-	state.getPosition().x += 250;
-	state.getPosition().y += 40;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Log_Success", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 400;
-	state.getPosition().y += 10;
-	UI.Statemachine.getRoot().addState(state);
-
-	state = new State("Log_Failure", Statelib.getFromLib("LogState"));
-	state.getPosition().x += 400;
-	state.getPosition().y += 80;
-	UI.Statemachine.getRoot().addState(state);
-
-	UI.Menu.toStatemachineClicked();
-}
-
-
-test_executeAll = function() {
-
-}
-
-test_normalRun = function() {
-	var manifest = Behaviorlib.getByName("FlexBE Test Behavior").getBehaviorManifest();
-	BehaviorLoader.loadBehavior(manifest);
-	UI.Settings.connectRosbridgeClicked();
-	UI.Menu.toControlClicked();
-	setTimeout(function () {
-		UI.RuntimeControl.startBehaviorClicked();
-	}, 2000);
-}
-
-test_synthesisResult = function() {
-	RC.PubSub.DEBUG_synthesis_action_result_callback({
-		error_code: {
-			value: 1
-		},
-		states: [
-			{
-				state_path: "/",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Entry_Msg",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished', 'failed'],
-				transitions: [],
-				autonomy: [],
-				cond_outcome: [],
-				cond_transition: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Wait_A_Bit",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['Con1'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Entry_Msg",
-				state_class: "LogState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['text'],
-				parameter_values: ['"Now at inner behavior..."'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['Testprint_Behavior'],
-				autonomy: [0],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Con",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [],
-				outcomes: ['finished'],
-				transitions: ['Con2'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1/Con",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Test_1",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['finished'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Con",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['Con2'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2/Con",
-				state_class: ":STATEMACHINE",
-				initial_state_name: "Test_2",
-				behavior_class: "",
-				parameter_names: [],
-				parameter_values: [],
-				input_keys:[],
-				output_keys: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['finished'],
-				autonomy: [],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con1/Con/Test_1",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Con2/Con/Test_2",
-				state_class: "WaitState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['wait_time'],
-				parameter_values: ['2'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [2],
-				userdata_keys: [],
-				userdata_remapping: []
-			},
-			{
-				state_path: "/Print_State",
-				state_class: "CalculationState",
-				initial_state_name: "",
-				behavior_class: "",
-				parameter_names: ['calculation'],
-				parameter_values: ['lambda x: 0'],
-				position: [0,0],
-				outcomes: ['done'],
-				transitions: ['finished'],
-				autonomy: [0],
-				userdata_keys: ['input_value', 'output_value'],
-				userdata_remapping: ['my_input', 'output_value']
-			},
-			{
-				state_path: "/Testprint_Behavior",
-				state_class: ":BEHAVIOR",
-				initial_state_name: "",
-				behavior_class: "FlexBETestprintBehaviorSM",
-				parameter_names: [],
-				parameter_values: [],
-				position: [0,0],
-				outcomes: ['finished'],
-				transitions: ['Wait_A_Bit'],
-				autonomy: [-1],
-				userdata_keys: [],
-				userdata_remapping: []
-			}
-		]
-	},
-	"/Synthesis_Test");
-	UI.Menu.toStatemachineClicked();
-}
\ No newline at end of file
diff --git a/src/events.js b/src/events.js
index 6ecd912..30b4116 100644
--- a/src/events.js
+++ b/src/events.js
@@ -172,6 +172,7 @@ document.addEventListener('DOMContentLoaded', function() {
     process.on('exit', (code) => {
         RC.PubSub.shutdown();
         ROS.shutdown();
+        IO.PackageParser.stopWatching();
     });
 
     nw.App.on('open', () => {
diff --git a/src/io/io_behaviorloader.js b/src/io/io_behaviorloader.js
index f5ca06f..b148774 100644
--- a/src/io/io_behaviorloader.js
+++ b/src/io/io_behaviorloader.js
@@ -34,6 +34,15 @@ IO.BehaviorLoader = new (function() {
 		T.logInfo("Behavior state machine built.");
 		
 		ActivityTracer.resetActivities();
+
+		ROS.getPackagePath(manifest.rosnode_name, (package_path) => {
+			ROS.getPackagePythonPath(manifest.rosnode_name, (python_path) => {
+				if (!python_path.startsWith(package_path)) {
+					Behavior.setReadonly(true);
+				}
+				UI.Statemachine.refreshView();
+			});
+		});
 	}
 
 	var resetEditor = function() {
@@ -52,48 +61,33 @@ IO.BehaviorLoader = new (function() {
 
 		resetEditor();
 
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
-				return;
-			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				T.logInfo("Parsing sourcecode...");
-				parseCode(content, manifest);
-			});
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			T.logInfo("Parsing sourcecode...");
+			parseCode(content, manifest);
 		});
 	}
 
 	this.loadBehaviorInterface = function(manifest, callback) {
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			try {
+				var parsingResult = IO.CodeParser.parseSMInterface(content);
+				callback(parsingResult);
+			} catch (err) {
+				T.logError("Failed to parse behavior interface of " + manifest.name + ": " + err);
 				return;
 			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				try {
-					var parsingResult = IO.CodeParser.parseSMInterface(content);
-					callback(parsingResult);
-				} catch (err) {
-					T.logError("Failed to parse behavior interface of " + manifest.name + ": " + err);
-					return;
-				}
-			});
 		});
 	}
 
 	this.updateManualSections = function(callback) {
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
+			if (folder_path == undefined) {
 				return;
 			}
-			var folder_path = path.join(package_path, 'src', package_name);
 			var file_path = path.join(folder_path, names.file_name);
 			IO.Filesystem.checkFileExists(folder_path, names.file_name, (exists) => {
 				if (exists) {
@@ -115,28 +109,21 @@ IO.BehaviorLoader = new (function() {
 	}
 
 	this.parseBehaviorSM = function(manifest, callback) {
-		var package_name = manifest.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
-				T.logError("Failed to load behavior: ROS package "+package_name+" not found, please check behavior manifest.");
+		var file_path = path.join(manifest.codefile_path, manifest.codefile_name);
+		IO.Filesystem.readFile(file_path, (content) => {
+			console.log("Preparing sourcecode of behavior " + manifest.name + "...");
+			try {
+				parsingResult = IO.CodeParser.parseCode(content);
+			} catch (err) {
+				console.log("Code parsing failed: " + err);
 				return;
 			}
-			var file_path = path.join(package_path, 'src', package_name, manifest.codefile_name);
-			IO.Filesystem.readFile(file_path, (content) => {
-				console.log("Preparing sourcecode of behavior " + manifest.name + "...");
-				try {
-					parsingResult = IO.CodeParser.parseCode(content);
-				} catch (err) {
-					console.log("Code parsing failed: " + err);
-					return;
-				}
-				callback({
-					container_name: "",
-					container_sm_var_name: parsingResult.root_sm_name,
-					sm_defs: parsingResult.sm_defs,
-					sm_states: parsingResult.sm_states,
-					default_userdata: parsingResult.default_userdata
-				});
+			callback({
+				container_name: "",
+				container_sm_var_name: parsingResult.root_sm_name,
+				sm_defs: parsingResult.sm_defs,
+				sm_states: parsingResult.sm_states,
+				default_userdata: parsingResult.default_userdata
 			});
 		});
 	}
diff --git a/src/io/io_behaviorpacker.js b/src/io/io_behaviorpacker.js
index a468b4f..2a5b586 100644
--- a/src/io/io_behaviorpacker.js
+++ b/src/io/io_behaviorpacker.js
@@ -6,11 +6,10 @@ IO.BehaviorPacker = new (function() {
 	this.loadBehaviorCode = function(callback) {
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			if (package_path == undefined) {
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
+			if (folder_path == undefined) {
 				return;
 			}
-			var folder_path = path.join(package_path, 'src', package_name);
 			var file_path = path.join(folder_path, names.file_name);
 			IO.Filesystem.checkFileExists(folder_path, names.file_name, (exists) => {
 				if (exists) {
diff --git a/src/io/io_behaviorsaver.js b/src/io/io_behaviorsaver.js
index 9e1e9bb..bca6ccf 100644
--- a/src/io/io_behaviorsaver.js
+++ b/src/io/io_behaviorsaver.js
@@ -11,8 +11,7 @@ IO.BehaviorSaver = new (function() {
 		};
 		var names = Behavior.createNames();
 		var package_name = names.rosnode_name;
-		ROS.getPackagePath(package_name, (package_path) => {
-			var folder_path = path.join(package_path, 'src', package_name);
+		ROS.getPackagePythonPath(package_name, (folder_path) => {
 			var file_path = path.join(folder_path, names.file_name);
 			var file_tmp_path = path.join(folder_path, names.file_name_tmp);
 			if (RC.Controller.isConnected()) {
@@ -21,7 +20,7 @@ IO.BehaviorSaver = new (function() {
 						IO.Filesystem.checkFileExists(folder_path, names.file_name_tmp, function(src_exists) {
 							if (src_exists) {
 								IO.Filesystem.getFileContent(folder_path, names.file_name, function(content_onboard) {
-									IO.Filesystem.createFile(folder_path, names.file_name_tmp, content_onboard, function() { 
+									IO.Filesystem.createFile(folder_path, names.file_name_tmp, content_onboard, function() {
 										create_callback(folder_path);
 									});
 								});
diff --git a/src/io/io_manifestparser.js b/src/io/io_manifestparser.js
index b40ec63..efff78c 100644
--- a/src/io/io_manifestparser.js
+++ b/src/io/io_manifestparser.js
@@ -1,7 +1,7 @@
 IO.ManifestParser = new (function() {
 	var that = this;
 
-	this.parseManifest = function(content, file_path) {
+	this.parseManifest = function(content, file_path, python_path) {
 		parser = new DOMParser();
 		xml = parser.parseFromString(content,"text/xml");
 
@@ -26,6 +26,7 @@ IO.ManifestParser = new (function() {
 		var path = xml.getElementsByTagName("executable")[0].getAttribute("package_path").split(".");
 		var rosnode_name = path[0];
 		var codefile_name = path[1] + ".py";
+		var codefile_path = python_path;
 		var class_name = xml.getElementsByTagName("executable")[0].getAttribute("class");
 
 		var params_element = xml.getElementsByTagName("params");
@@ -74,6 +75,7 @@ IO.ManifestParser = new (function() {
 			date: 			date,
 			rosnode_name: 	rosnode_name,
 			codefile_name: 	codefile_name,
+			codefile_path: 	codefile_path,
 			class_name: 	class_name,
 			params: 		param_list,
 			contains: 		contains_list,
diff --git a/src/io/io_packageparser.js b/src/io/io_packageparser.js
index 2f1b125..b21016c 100644
--- a/src/io/io_packageparser.js
+++ b/src/io/io_packageparser.js
@@ -4,7 +4,6 @@ IO.PackageParser = new (function() {
 	var fs = require('fs');
 	var path = require('path');
 
-
 	var dom_parser = new DOMParser();
 	var watched_states = {};
 
@@ -27,17 +26,37 @@ IO.PackageParser = new (function() {
 					callback(pkg_list, add_states, add_behaviors);
 				} else {
 					checkForRelevance(entry['path'], (has_states, has_behaviors) => {
-							if (has_states) add_states.push(entry);
-							if (has_behaviors) add_behaviors.push(entry);
-						});
-
-					processEntry(idx+1);
+						if (has_states || has_behaviors) {
+							var add_package = function(python_path) {
+								if (python_path != undefined) {
+									entry['python_path'] = python_path;
+									if (has_states) add_states.push(entry);
+									if (has_behaviors) add_behaviors.push(entry);
+								}
+								processEntry(idx+1);
+							}
+							python_path = entry['python_path'];
+							if (python_path == undefined) {
+								ROS.getPackagePythonPath(entry['name'], add_package);
+							} else {
+								add_package(python_path);
+							}
+						} else {
+							processEntry(idx+1);
+						}
+					});
 				}
 			};
 			processEntry(0);
 		});
 	}
 
+	this.stopWatching = function() {
+		for (var state in watched_states) {
+			watched_states[state].close();
+		}
+	}
+
 	var checkForRelevance = function(pkg_path, callback) {
 		var data = fs.readFileSync(path.join(pkg_path, 'package.xml'));
 		var xml = dom_parser.parseFromString(String(data), "text/xml");
@@ -51,38 +70,54 @@ IO.PackageParser = new (function() {
 	var watchStateFolder = function(folder_path, import_path) {
 		if (watched_states[folder_path] != undefined) return;
 
-		// watched_states[folder_path] = fs.watch(folder_path,
-		// 	{persistent: false},
-		// 	(eventType, filename) => {
-		// 		if (eventType == 'change') {
-		// 			var entry = path.join(folder_path, filename);
-		// 			IO.Filesystem.readFile(entry, (content) => {
-		// 				var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
-		// 				var state_def = IO.StateParser.parseState(content, imports);
-		// 				if (state_def != undefined) {
-		// 					state_def.setFilePath(entry);
-		// 					WS.Statelib.updateDef(state_def);
-		// 					T.logInfo("Updating changed definition for state: " + state_def.getStateClass());
-		// 					// TODO update defs for existing states and re-draw
-		// 				}
-		// 			});
-		// 		}
-		// 	}
-		// );
+		watched_states[folder_path] = fs.watch(folder_path,
+			{persistent: false},
+			(eventType, filename) => {
+				if(RC.Controller.isReadonly()) {
+					T.logWarn("A state definition source file changed while in read-only mode, ignoring the change for now!");
+					return;
+				}
+				if (filename.endsWith(".py")) {
+					var entry = path.join(folder_path, filename);
+					IO.Filesystem.readFile(entry, (content) => {
+						var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
+						var state_def = IO.StateParser.parseState(content, imports);
+						if (state_def != undefined) {
+							state_def.setFilePath(entry);
+							WS.Statelib.updateDef(state_def);
+							T.logInfo("Updating changed definition for state: " + state_def.getStateClass());
+							var update_states = Behavior.getStatemachine().traverseStates(function(state) {
+								return state.getStateClass() == state_def.getStateClass();
+							});
+							update_states.forEach(function (state) {
+								state.updateStateDefinition(state_def);
+								if (state.getContainer() == UI.Statemachine.getDisplayedSM() 
+									&& UI.Panels.StateProperties.isCurrentState(state)) {
+									UI.Panels.StateProperties.hide();
+									UI.Panels.StateProperties.displayStateProperties(state);
+								}
+							});
+							if (UI.Menu.isPageStatemachine()) {
+								UI.Statemachine.refreshView();
+							}
+							RC.Controller.signalChanged();
+						}
+					});
+				}
+			}
+		);
 	}
 
-	this.parseStateFolder = function(folder, import_path, has_init) {
+	this.parseStateFolder = function(folder, import_path) {
 		IO.Filesystem.checkFileExists(folder, "__init__.py", function(exists) {
-			has_init = has_init || exists;
+			if (exists) {
+				import_path = import_path || path.dirname(folder);
+			}
 			IO.Filesystem.getFolderContent(folder, function(files) {
 				files.sort().forEach(function(entry, i) {
 					if(IO.Filesystem.isFolder(entry)) {
-						if (!has_init) {
-							that.parseStateFolder(entry, path.dirname(entry), has_init);
-						} else {
-							that.parseStateFolder(entry, import_path, has_init);
-						}
-					} else if (has_init) {
+						that.parseStateFolder(entry, import_path);
+					} else if (import_path != undefined) {
 						if (path.extname(entry) != ".py") return;
 						IO.Filesystem.readFile(entry, (content) => {
 							var imports = entry.replace(import_path+"/", "").replace(/.py$/i, "").replace(/[\/]/g, ".");
@@ -99,15 +134,15 @@ IO.PackageParser = new (function() {
 		});
 	}
 
-	this.parseBehaviorFolder = function(folder, pkg_name) {
+	this.parseBehaviorFolder = function(folder, pkg_name, python_path) {
 		IO.Filesystem.getFolderContent(folder, function(files) {
 			files.sort().forEach(function(entry, i) {
 				if(IO.Filesystem.isFolder(entry)) {
-					that.parseBehaviorFolder(entry, pkg_name);
+					that.parseBehaviorFolder(entry, pkg_name, python_path);
 				} else {
 					if (path.extname(entry) != ".xml" || path.basename(entry)[0] == '#') return;
 					IO.Filesystem.readFile(entry, (content) => {
-						var manifest = IO.ManifestParser.parseManifest(content, entry);
+						var manifest = IO.ManifestParser.parseManifest(content, entry, python_path);
 						if (manifest != undefined) {
 							if (manifest.rosnode_name != pkg_name) {
 								T.logWarn("Ignoring behavior " + manifest.name + ": Manifest and code need to be in the same ROS package.");
diff --git a/src/prototype.js b/src/prototype.js
index 66ff5dd..b88a91f 100644
--- a/src/prototype.js
+++ b/src/prototype.js
@@ -33,6 +33,11 @@ Array.prototype.contains = function(element) {
 	return this.indexOf(element) != -1;
 }
 
+Array.prototype.hasSameElements = function(other) {
+	var diff = this.filter(el => !other.contains(el));
+	return 0 == diff.concat(other.filter(el => !this.contains(el))).length;
+}
+
 
 //=====================
 //	String
diff --git a/src/ros/ros.js b/src/ros/ros.js
index 34f0935..02a6de7 100644
--- a/src/ros/ros.js
+++ b/src/ros/ros.js
@@ -64,7 +64,8 @@ rospy.spin()
 					package_cache[i] = package_cache[i].split(" ");
 					package_cache[i] = {
 						'name': package_cache[i][0],
-						'path': package_cache[i][1]
+						'path': package_cache[i][1],
+						'python_path': undefined
 					}
 				}
 				callback(package_cache.clone());
@@ -89,6 +90,46 @@ rospy.spin()
 		});
 	}
 
+	that.getPackagePythonPath = function(package_name, callback) {
+		var python_path = undefined;
+		that.getPackageList((package_cache) => {
+			for (var i=0; i<package_cache.length; i++) {
+				if (package_cache[i]['name'] == package_name) {
+					python_path = package_cache[i]['python_path'];
+					break;
+				}
+			}
+		});
+		if (python_path !== undefined) {
+            process.nextTick(() => {
+                callback(python_path);
+            });
+    	} else {
+			var proc = spawn('python', ['-c', `import importlib; print(importlib.import_module('` + package_name + `').__path__[-1])`]);
+			var path_data = '';
+			proc.stdout.on('data', data => {
+				path_data += data;
+			});
+			proc.stderr.on('data', data => {
+				console.log(package_name+" failed to import: "+data);
+			});
+			proc.on('close', (code) => {
+				if (path_data != "") {
+					python_path = path_data.replace(/\n/g, '');
+					for (var i=0; i<package_cache.length; i++) {
+						if (package_cache[i]['name'] == package_name) {
+							package_cache[i]['python_path'] = python_path;
+							break;
+						}
+					}
+					callback(python_path);
+				} else {
+					callback(undefined);
+				}
+			});
+		}
+	}
+
 	// that.getParam = function(name, callback) {
 	// 	var proc = spawn('rosparam', ['get', name]);
 	// 	proc.stdout.on('data', data => {
diff --git a/src/ui/panels/ui_panels_stateproperties.js b/src/ui/panels/ui_panels_stateproperties.js
index a17dd54..988b178 100644
--- a/src/ui/panels/ui_panels_stateproperties.js
+++ b/src/ui/panels/ui_panels_stateproperties.js
@@ -295,6 +295,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -328,6 +329,7 @@ UI.Panels.StateProperties = new (function() {
 			input_field.addEventListener("blur", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -348,6 +350,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -383,6 +386,7 @@ UI.Panels.StateProperties = new (function() {
 			input_field.addEventListener("blur", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -403,6 +407,7 @@ UI.Panels.StateProperties = new (function() {
 			remove_button.addEventListener("click", function() {
 				if (RC.Controller.isReadonly()
 					|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+					|| Behavior.isReadonly()
 					|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 					|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 					) return;
@@ -473,6 +478,7 @@ UI.Panels.StateProperties = new (function() {
 				input_field.addEventListener("blur", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -493,6 +499,7 @@ UI.Panels.StateProperties = new (function() {
 				default_button.addEventListener("change", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -546,6 +553,7 @@ UI.Panels.StateProperties = new (function() {
 				input_field.addEventListener("blur", function() {
 					if (RC.Controller.isReadonly()
 						|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+						|| Behavior.isReadonly()
 						|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 						|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 						) return;
@@ -596,6 +604,7 @@ UI.Panels.StateProperties = new (function() {
 	this.deleteStateClicked = function() {
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -692,6 +701,7 @@ UI.Panels.StateProperties = new (function() {
 	this.applyPropertiesClicked = function() {
 		if (RC.Controller.isReadonly() 
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -836,6 +846,7 @@ UI.Panels.StateProperties = new (function() {
 
 		if (!RC.Controller.isReadonly()
 			&& !UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			&& !Behavior.isReadonly()
 			&& (!RC.Controller.isLocked() || !RC.Controller.isStateLocked(current_prop_state.getStatePath()))
 			&& !RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) {
@@ -875,6 +886,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_outcome_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -910,6 +922,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_input_key_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -952,6 +965,7 @@ UI.Panels.StateProperties = new (function() {
 		if (document.getElementById("input_prop_output_key_add").value == "") return;
 		if (RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -993,6 +1007,7 @@ UI.Panels.StateProperties = new (function() {
 	this.containerTypeChanged = function(evt) {
 		if(RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
@@ -1094,6 +1109,7 @@ UI.Panels.StateProperties = new (function() {
 	this.synthesizeClicked = function() {
 		if(RC.Controller.isReadonly()
 			|| UI.Statemachine.getDisplayedSM().isInsideDifferentBehavior()
+			|| Behavior.isReadonly()
 			|| RC.Controller.isLocked() && RC.Controller.isStateLocked(current_prop_state.getStatePath())
 			|| RC.Controller.isOnLockedPath(current_prop_state.getStatePath())
 			) return;
diff --git a/src/ui/ui_dashboard.js b/src/ui/ui_dashboard.js
index a3a112a..edd3b4c 100644
--- a/src/ui/ui_dashboard.js
+++ b/src/ui/ui_dashboard.js
@@ -1225,6 +1225,7 @@ UI.Dashboard = new (function() {
 
 	this.resetAllFields = function() {
 		UI.Settings.createBehaviorPackageSelect(document.getElementById('select_behavior_package'));
+		Behavior.setBehaviorPackage(UI.Settings.getDefaultPackage());
 		document.getElementById('input_behavior_name').value = "";
 		document.getElementById('input_behavior_description').value = "";
 		document.getElementById('input_behavior_tags').value = "";
diff --git a/src/ui/ui_menu.js b/src/ui/ui_menu.js
index 4fae0c9..f7fbc08 100644
--- a/src/ui/ui_menu.js
+++ b/src/ui/ui_menu.js
@@ -284,7 +284,12 @@ UI.Menu = new (function() {
 	}
 
 	this.saveBehaviorClicked = function() {
-		var check_error_string = Checking.checkBehavior();
+		var check_error_string = undefined;
+		if (Behavior.isReadonly()) {
+			check_error_string = "behavior has been loaded from a read-only file";
+		} else {
+			check_error_string = Checking.checkBehavior();
+		}
 		if (check_error_string != undefined) {
 			T.clearLog();
 			T.show();
diff --git a/src/ui/ui_settings.js b/src/ui/ui_settings.js
index 1b6f566..70e5436 100644
--- a/src/ui/ui_settings.js
+++ b/src/ui/ui_settings.js
@@ -144,14 +144,14 @@ UI.Settings = new (function() {
 	this.updateStatelib = function() {
 		WS.Statelib.resetLib();
 		state_pkg_cache.forEach(state_pkg => {
-			IO.PackageParser.parseStateFolder(state_pkg['path'], state_pkg['path'], false);
+			IO.PackageParser.parseStateFolder(state_pkg['python_path']);
 		});
 	}
 
 	this.updateBehaviorlib = function() {
 		WS.Behaviorlib.resetLib();
 		behavior_pkg_cache.forEach(behavior_pkg => {
-			IO.PackageParser.parseBehaviorFolder(behavior_pkg['path'], behavior_pkg['name']);
+			IO.PackageParser.parseBehaviorFolder(behavior_pkg['path'], behavior_pkg['name'], behavior_pkg['python_path']);
 		});
 	}
 
@@ -208,7 +208,9 @@ UI.Settings = new (function() {
 			var pkg_select = document.createElement("select");
 			pkg_select.setAttribute("style", "width:100%; margin: 5px 0;");
 			var pkg_select_update_title = function() {
-				pkg_select.setAttribute("title", pkg_select.options[pkg_select.selectedIndex].getAttribute("title"));
+				if (pkg_select.options.length > 0) {
+					pkg_select.setAttribute("title", pkg_select.options[pkg_select.selectedIndex].getAttribute("title"));
+				}
 			};
 			pkg_select.addEventListener('change', pkg_select_update_title);
 			var packages = ros_pkg_cache.filter((pkg) => { return !pkg['path'].startsWith("/opt/ros"); });
diff --git a/src/ui/ui_statemachine.js b/src/ui/ui_statemachine.js
index 44646f3..3f6b786 100644
--- a/src/ui/ui_statemachine.js
+++ b/src/ui/ui_statemachine.js
@@ -290,7 +290,7 @@ UI.Statemachine = new (function() {
 	}
 
 	this.isReadonly = function() {
-		return RC.Controller.isReadonly() || displayed_sm.isInsideDifferentBehavior();
+		return RC.Controller.isReadonly() || displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly();
 	}
 
 	this.applyGraphLayout = function() {
@@ -416,7 +416,7 @@ UI.Statemachine = new (function() {
 		}
 
 		// draw transitions at last
-		var transitions_readonly = RC.Controller.isReadonly() || dataflow_displayed || displayed_sm.isInsideDifferentBehavior();
+		var transitions_readonly = RC.Controller.isReadonly() || dataflow_displayed || displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly();
 		var new_transitions = [];
 		for (var i=0; i<transitions.length; ++i) {
 			var t = transitions[i];
@@ -472,7 +472,7 @@ UI.Statemachine = new (function() {
 
 		if (RC.Controller.isReadonly()) {
 			background.attr({fill: '#f3f6ff', stroke: '#c5d2ee'});
-		} else if (displayed_sm.isInsideDifferentBehavior()) {
+		} else if (displayed_sm.isInsideDifferentBehavior() || Behavior.isReadonly()) {
 			background.attr({fill: '#fff3f6'});
 		} else {
 			background.attr({fill: '#FFF'});
diff --git a/src/window.html b/src/window.html
index 3e5d9d8..54f9a1c 100644
--- a/src/window.html
+++ b/src/window.html
@@ -75,8 +75,9 @@
 	<script src="ws/ws_statemachinedefinition.js"></script>
 	<script src="ws/ws_statelib.js"></script>
 
-	<script src="_testing/scripts.js"></script>
-	<script src="_testing/test.js"></script>
+	<script src="_test/scripts.js"></script>
+	<script src="_test/testreport.js"></script>
+	<script src="_test/statelib.test.js"></script>
 
 	<link rel="stylesheet" href="style/main.css" type="text/css" />
 	<link rel="stylesheet" href="style/dashboard.css" type="text/css" />
diff --git a/src/window.js b/src/window.js
index 98c5950..38027bf 100644
--- a/src/window.js
+++ b/src/window.js
@@ -25,4 +25,10 @@ window.onload = function() {
 	UI.Settings.restoreSettings();
 
 	UI.Feed.initialize();
+
+	if (gui.App.argv.contains('--run-tests')) {
+		setTimeout(() => {
+			TestReport.runAllTests(status =>  gui.App.quit());
+		}, 5 * 1000);
+	}
 }
-- 
GitLab