diff --git a/.gitignore b/.gitignore
index 3f35858..f8f381a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@ tests/data/dialog/g_dialogs
**/.pytest_cache/
ci/outputs.tar.gz
ci/unit_tests/workspace_addjson/main_data/workspace_forAddJsonTest.json
+.env
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index 14e6754..4565131 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ Then set following environment variables
- `WA_USERNAME` (Watson Assistant username - ALWAYS USE INSTANCE DEDICATED FOR TESTING ONLY! ALL CONTENT WILL BE DELETED DURING TESTING PROCESS!)
- `WA_PASSWORD` (Watson Assistant password)
+- `WA_WORKSPACES_API_URL` (`https://.../assistant/api/v1/workspaces`)
- `CLOUD_FUNCTIONS_USERNAME` (Cloud Functions username)
- `CLOUD_FUNCTIONS_PASSWORD` (Cloud Functions password)
- `CLOUD_FUNCTIONS_URL` (Cloud Functions namespace - it should contain `https://` at the beginning and `/api/v1/namespaces` at the end)
diff --git a/ci/app_tests/convertWorkspaceBackAndForth_test.py b/ci/app_tests/convertWorkspaceBackAndForth_test.py
index 1dd7241..772e210 100644
--- a/ci/app_tests/convertWorkspaceBackAndForth_test.py
+++ b/ci/app_tests/convertWorkspaceBackAndForth_test.py
@@ -26,7 +26,7 @@
class TestConvertWorkspaceBackAndForth(BaseTestCaseCapture):
- dataBasePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'convertWorkspaceBackAndForth_data' + os.sep)
+ dataBasePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'convertWorkspaceBackAndForth_data')
testOutputPath = os.path.join(dataBasePath, 'outputs')
def setup_class(cls):
@@ -48,7 +48,11 @@ def test_basic(self, dialogFilename):
f.write(self.captured.out)
# convert xml back to json
- self.t_fun_noException(dialog_xml2json.main, [['-dm', dialogXmlPath, '-of', self.testOutputPath, '-od', dialogFilename, '-c', configPath, '-v']])
+ self.t_fun_noException(dialog_xml2json.main, [['-dm', dialogXmlPath,
+ '-of', self.testOutputPath,
+ '-od', dialogFilename,
+ '-c', configPath,
+ '-v']])
# compare dialogs if they are same
- self.t_fun_exitCode(compare_dialogs.main, 0, [[dialogJsonRefPath, dialogJsonHypPath]])
+ self.t_fun_exitCode(compare_dialogs.main, 0, [[dialogJsonRefPath, dialogJsonHypPath, '--verbose']])
diff --git a/ci/app_tests/generateAndTestWorkspace_data/dialogs/music.xml b/ci/app_tests/generateAndTestWorkspace_data/dialogs/music.xml
index 617d824..112d157 100644
--- a/ci/app_tests/generateAndTestWorkspace_data/dialogs/music.xml
+++ b/ci/app_tests/generateAndTestWorkspace_data/dialogs/music.xml
@@ -41,6 +41,7 @@
#ui-musicPlay
+ skip_user_input
generalContext_musicPlay_1stChild
condition
@@ -68,6 +69,7 @@
+ skip_user_input
musicPlayingSong
user_input
@@ -78,6 +80,7 @@
+ skip_user_input
musicList
user_input
@@ -87,6 +90,7 @@
+ skip_user_input
musicPlayingSong
user_input
@@ -130,6 +134,7 @@
+ skip_user_input
musicPlayingSong
user_input
@@ -140,6 +145,7 @@
+ skip_user_input
musicPlayingSong
user_input
diff --git a/ci/unit_tests/dialog_json2xml/main_data/expectedNodeTypesValid.xml b/ci/unit_tests/dialog_json2xml/main_data/expectedNodeTypesValid.xml
index e39b485..ddc20e4 100644
--- a/ci/unit_tests/dialog_json2xml/main_data/expectedNodeTypesValid.xml
+++ b/ci/unit_tests/dialog_json2xml/main_data/expectedNodeTypesValid.xml
@@ -6,11 +6,10 @@
response_condition
- frame
+
+
+
-
- slot
-
anything_else
@@ -19,10 +18,9 @@
-
- event_handler
- filled
-
+
+
+
\ No newline at end of file
diff --git a/ci/unit_tests/dialog_json2xml/main_data/expectedSlotsValid.xml b/ci/unit_tests/dialog_json2xml/main_data/expectedSlotsValid.xml
new file mode 100644
index 0000000..7364038
--- /dev/null
+++ b/ci/unit_tests/dialog_json2xml/main_data/expectedSlotsValid.xml
@@ -0,0 +1,85 @@
+
+
+ #DESIRES_FRIEND
+
+
+
+
+
+
+
+ @name
+
+ @name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @sys-number
+
+ @sys-number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @sys-date
+
+
+
+ #ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_
+
+
+
+
+ allow_returning
+
+
+
diff --git a/ci/unit_tests/dialog_json2xml/main_data/inputSlotsValid.json b/ci/unit_tests/dialog_json2xml/main_data/inputSlotsValid.json
new file mode 100644
index 0000000..a39543f
--- /dev/null
+++ b/ci/unit_tests/dialog_json2xml/main_data/inputSlotsValid.json
@@ -0,0 +1,142 @@
+[
+ {
+ "conditions": "#DESIRES_FRIEND",
+ "dialog_node": "frame_test",
+ "digress_out_slots": "allow_returning",
+ "output": {
+ "text": "I will find some friend for you, $name."
+ },
+ "type": "frame"
+ },
+ {
+ "dialog_node": "node_0",
+ "parent": "frame_test",
+ "type": "slot",
+ "variable": "name"
+ },
+ {
+ "dialog_node": "node_1",
+ "event_name": "focus",
+ "output": {
+ "text": "Say your first name."
+ },
+ "parent": "node_0",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@name",
+ "context": {
+ "name": "@name"
+ },
+ "dialog_node": "node_2",
+ "event_name": "input",
+ "parent": "node_0",
+ "previous_sibling": "node_1",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_3",
+ "event_name": "filled",
+ "output": {
+ "text": "Thank you, @name."
+ },
+ "parent": "node_0",
+ "previous_sibling": "node_2",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_4",
+ "event_name": "nomatch",
+ "output": {
+ "text": "Please say your first name."
+ },
+ "parent": "node_0",
+ "previous_sibling": "node_3",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_5",
+ "parent": "frame_test",
+ "previous_sibling": "node_0",
+ "type": "slot",
+ "variable": "age"
+ },
+ {
+ "dialog_node": "node_6",
+ "event_name": "focus",
+ "output": {
+ "text": "Say your age in years."
+ },
+ "parent": "node_5",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@sys-number",
+ "context": {
+ "age": "@sys-number"
+ },
+ "dialog_node": "node_7",
+ "event_name": "input",
+ "parent": "node_5",
+ "previous_sibling": "node_6",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_8",
+ "event_name": "filled",
+ "output": {
+ "text": "You are @sys-number years old."
+ },
+ "parent": "node_5",
+ "previous_sibling": "node_7",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_9",
+ "event_name": "nomatch",
+ "output": {
+ "text": "Please say your age in years."
+ },
+ "parent": "node_5",
+ "previous_sibling": "node_8",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_10",
+ "event_name": "focus",
+ "output": {
+ "text": "Tell me your first name and age in years and I will find a friend for you."
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_5",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@sys-date",
+ "dialog_node": "node_11",
+ "event_name": "generic",
+ "output": {
+ "text": "Today is nice weather, isn't it?"
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_10",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "#ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_",
+ "dialog_node": "node_12",
+ "event_name": "generic",
+ "output": {
+ "text": {
+ "selection_policy": "sequential",
+ "values": [
+ "I enjoy hiking",
+ "I love swimming?"
+ ]
+ }
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_11",
+ "type": "event_handler"
+ }
+]
\ No newline at end of file
diff --git a/ci/unit_tests/dialog_json2xml/main_test.py b/ci/unit_tests/dialog_json2xml/main_test.py
index 5ed8921..8097c17 100644
--- a/ci/unit_tests/dialog_json2xml/main_test.py
+++ b/ci/unit_tests/dialog_json2xml/main_test.py
@@ -17,17 +17,38 @@
from lxml import etree
+
import dialog_json2xml
from ...test_utils import BaseTestCaseCapture
+# From # http://doc.pytest.org/en/latest/example/parametrize.html#parametrizing-test-methods-through-per-class-configuration
+def pytest_generate_tests(metafunc):
+ # called once per each test function
+ funcname = metafunc.function.__name__
+ if funcname in metafunc.cls.params:
+ funcarglist = metafunc.cls.params[metafunc.function.__name__]
+ argnames = sorted(funcarglist[0])
+ metafunc.parametrize(
+ argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist]
+ )
+
+
class TestMain(BaseTestCaseCapture):
dataBasePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'main_data')
testOutputPath = os.path.join(dataBasePath, 'outputs')
+ # a map specifying multiple argument sets for a test method
+ params = {
+ "test_validToXmlTransformation": [{'category': 'Actions'},
+ {'category': 'Bool'},
+ {'category': 'NodeTypes'},
+ {'category': 'Slots'}]
+ }
+ @classmethod
def setup_class(cls):
''' Setup any state specific to the execution of the given class (which usually contains tests). '''
# create output folder
@@ -40,19 +61,24 @@ def _assertXmlEqual(self, xml1path, xml2path):
"""Tests if two xml files are equal."""
with open(xml1path, 'r') as xml1File:
xml1 = etree.XML(xml1File.read(), etree.XMLParser(remove_blank_text=True))
- for parent in xml1.xpath('//*[./*]'): # Search for parent elements
+ for parent in xml1.xpath('//*[./*]'): # Search for parent elements
parent[:] = sorted(parent, key=lambda x: x.tag)
+ # with open(xml2path, 'r') as xml2File:
+ # print(f'{xml2File.read()}')
with open(xml2path, 'r') as xml2File:
xml2 = etree.XML(xml2File.read(), etree.XMLParser(remove_blank_text=True))
- for parent in xml2.xpath('//*[./*]'): # Search for parent elements
+ for parent in xml2.xpath('//*[./*]'): # Search for parent elements
parent[:] = sorted(parent, key=lambda x: x.tag)
- assert etree.tostring(xml1) == etree.tostring(xml2)
+ assert etree.tostring(xml1, encoding=str, pretty_print=True) == \
+ etree.tostring(xml2, encoding=str, pretty_print=True)
- def test_mainValidActions(self):
- """Tests if the script successfully completes with valid input file with actions."""
- inputJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputActionsValid.json'))
- expectedXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedActionsValid.xml'))
+ def test_validToXmlTransformation(self, category):
+ """Tests if the script successfully completes with valid input file"""
+ inputFileName = 'input' + category + 'Valid.json'
+ expectedFileName = 'expected' + category + 'Valid.xml'
+ inputJsonPath = os.path.abspath(os.path.join(self.dataBasePath, inputFileName))
+ expectedXmlPath = os.path.abspath(os.path.join(self.dataBasePath, expectedFileName))
outputXmlDirPath = os.path.join(self.testOutputPath, 'outputActionsValidResult')
outputXmlPath = os.path.join(outputXmlDirPath, 'dialog.xml')
@@ -61,30 +87,3 @@ def test_mainValidActions(self):
self.t_noException([[inputJsonPath, '-d', outputXmlDirPath]])
self._assertXmlEqual(expectedXmlPath, outputXmlPath)
-
- def test_mainValidNodeTypes(self):
- """Tests if the script successfully completes with valid input file with node types."""
- inputJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputNodeTypesValid.json'))
- expectedXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedNodeTypesValid.xml'))
-
- outputXmlDirPath = os.path.join(self.testOutputPath, 'outputNodeTypesValidResult')
- outputXmlPath = os.path.join(outputXmlDirPath, 'dialog.xml')
-
- BaseTestCaseCapture.createFolder(outputXmlDirPath)
-
- self.t_noException([[inputJsonPath, '-d', outputXmlDirPath]])
- self._assertXmlEqual(expectedXmlPath, outputXmlPath)
-
-
- def test_mainValidBool(self):
- """Tests if the script successfully completes with valid input file with bool."""
- inputJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputBoolValid.json'))
- expectedXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedBoolValid.xml'))
-
- outputXmlDirPath = os.path.join(self.testOutputPath, 'outputBoolValidResult')
- outputXmlPath = os.path.join(outputXmlDirPath, 'dialog.xml')
-
- BaseTestCaseCapture.createFolder(outputXmlDirPath)
-
- self.t_noException([[inputJsonPath, '-d', outputXmlDirPath]])
- self._assertXmlEqual(expectedXmlPath, outputXmlPath)
diff --git a/ci/unit_tests/dialog_xml2json/main_data/expectedSlotsValid.json b/ci/unit_tests/dialog_xml2json/main_data/expectedSlotsValid.json
new file mode 100644
index 0000000..a39543f
--- /dev/null
+++ b/ci/unit_tests/dialog_xml2json/main_data/expectedSlotsValid.json
@@ -0,0 +1,142 @@
+[
+ {
+ "conditions": "#DESIRES_FRIEND",
+ "dialog_node": "frame_test",
+ "digress_out_slots": "allow_returning",
+ "output": {
+ "text": "I will find some friend for you, $name."
+ },
+ "type": "frame"
+ },
+ {
+ "dialog_node": "node_0",
+ "parent": "frame_test",
+ "type": "slot",
+ "variable": "name"
+ },
+ {
+ "dialog_node": "node_1",
+ "event_name": "focus",
+ "output": {
+ "text": "Say your first name."
+ },
+ "parent": "node_0",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@name",
+ "context": {
+ "name": "@name"
+ },
+ "dialog_node": "node_2",
+ "event_name": "input",
+ "parent": "node_0",
+ "previous_sibling": "node_1",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_3",
+ "event_name": "filled",
+ "output": {
+ "text": "Thank you, @name."
+ },
+ "parent": "node_0",
+ "previous_sibling": "node_2",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_4",
+ "event_name": "nomatch",
+ "output": {
+ "text": "Please say your first name."
+ },
+ "parent": "node_0",
+ "previous_sibling": "node_3",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_5",
+ "parent": "frame_test",
+ "previous_sibling": "node_0",
+ "type": "slot",
+ "variable": "age"
+ },
+ {
+ "dialog_node": "node_6",
+ "event_name": "focus",
+ "output": {
+ "text": "Say your age in years."
+ },
+ "parent": "node_5",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@sys-number",
+ "context": {
+ "age": "@sys-number"
+ },
+ "dialog_node": "node_7",
+ "event_name": "input",
+ "parent": "node_5",
+ "previous_sibling": "node_6",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_8",
+ "event_name": "filled",
+ "output": {
+ "text": "You are @sys-number years old."
+ },
+ "parent": "node_5",
+ "previous_sibling": "node_7",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_9",
+ "event_name": "nomatch",
+ "output": {
+ "text": "Please say your age in years."
+ },
+ "parent": "node_5",
+ "previous_sibling": "node_8",
+ "type": "event_handler"
+ },
+ {
+ "dialog_node": "node_10",
+ "event_name": "focus",
+ "output": {
+ "text": "Tell me your first name and age in years and I will find a friend for you."
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_5",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "@sys-date",
+ "dialog_node": "node_11",
+ "event_name": "generic",
+ "output": {
+ "text": "Today is nice weather, isn't it?"
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_10",
+ "type": "event_handler"
+ },
+ {
+ "conditions": "#ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_",
+ "dialog_node": "node_12",
+ "event_name": "generic",
+ "output": {
+ "text": {
+ "selection_policy": "sequential",
+ "values": [
+ "I enjoy hiking",
+ "I love swimming?"
+ ]
+ }
+ },
+ "parent": "frame_test",
+ "previous_sibling": "node_11",
+ "type": "event_handler"
+ }
+]
\ No newline at end of file
diff --git a/ci/unit_tests/dialog_xml2json/main_data/inputSlotsValid.xml b/ci/unit_tests/dialog_xml2json/main_data/inputSlotsValid.xml
new file mode 100644
index 0000000..456b77c
--- /dev/null
+++ b/ci/unit_tests/dialog_xml2json/main_data/inputSlotsValid.xml
@@ -0,0 +1,85 @@
+
+
+
+ #DESIRES_FRIEND
+
+
+
+
+
+
+
+ @name
+
+ @name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @sys-number
+
+ @sys-number
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @sys-date
+
+
+
+ #ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_
+
+
+
+
+ allow_returning
+
+
diff --git a/ci/unit_tests/dialog_xml2json/main_data/validInputsAccordingToSchema/goto_targetValid.xml b/ci/unit_tests/dialog_xml2json/main_data/validInputsAccordingToSchema/goto_targetValid.xml
index 67e75fb..2f1c47b 100644
--- a/ci/unit_tests/dialog_xml2json/main_data/validInputsAccordingToSchema/goto_targetValid.xml
+++ b/ci/unit_tests/dialog_xml2json/main_data/validInputsAccordingToSchema/goto_targetValid.xml
@@ -1,7 +1,7 @@
- ::FIRST_SIBLING
+ skip_user_input
diff --git a/ci/unit_tests/dialog_xml2json/main_test.py b/ci/unit_tests/dialog_xml2json/main_test.py
index 9b42702..3a3bcbe 100644
--- a/ci/unit_tests/dialog_xml2json/main_test.py
+++ b/ci/unit_tests/dialog_xml2json/main_test.py
@@ -18,14 +18,37 @@
import lxml
import dialog_xml2json
+import pytest
+
from ...test_utils import BaseTestCaseCapture
+# From http://doc.pytest.org/en/latest/example/parametrize.html#parametrizing-test-methods-through-per-class-configuration
+
+def pytest_generate_tests(metafunc):
+ # called once per each test function
+ funcname = metafunc.function.__name__
+ if funcname in metafunc.cls.params:
+ funcarglist = metafunc.cls.params[metafunc.function.__name__]
+ argnames = sorted(funcarglist[0])
+ metafunc.parametrize(
+ argnames, [[funcargs[name] for name in argnames] for funcargs in funcarglist]
+ )
+
class TestMain(BaseTestCaseCapture):
dataBasePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'main_data')
testOutputPath = os.path.join(dataBasePath, 'outputs')
-
+
+ # a map specifying multiple argument sets for a test method
+ params = {
+ "test_validToJsonTransformation": [{'category': 'Actions'},
+ {'category': 'Bool'},
+ {'category': 'NodeTypes'},
+ {'category': 'Slots'}],
+ }
+
+ @classmethod
def setup_class(cls):
''' Setup any state specific to the execution of the given class (which usually contains tests). '''
# create output folder
@@ -35,11 +58,12 @@ def setup_class(cls):
def callfunc(self, *args, **kwargs):
dialog_xml2json.main(*args, **kwargs)
-
- def test_mainValidActions(self):
- """Tests if the script successfully completes with valid input file with actions."""
- inputXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputActionsValid.xml'))
- expectedJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedActionsValid.json'))
+ def test_validToJsonTransformation(self, category):
+ """Tests if the script successfully completes with valid input file"""
+ inputFileName = 'input' + category + 'Valid.xml'
+ expectedFileName = 'expected' + category + 'Valid.json'
+ inputXmlPath = os.path.abspath(os.path.join(self.dataBasePath, inputFileName))
+ expectedJsonPath = os.path.abspath(os.path.join(self.dataBasePath, expectedFileName))
outputJsonDirPath = os.path.join(self.testOutputPath, 'outputActionsValidResult')
outputJsonPath = os.path.join(outputJsonDirPath, 'dialog.json')
@@ -55,41 +79,6 @@ def test_mainValidActions(self):
with open(expectedJsonPath, 'r') as expectedJsonFile, open(outputJsonPath, 'r') as outputJsonFile:
assert json.load(expectedJsonFile) == json.load(outputJsonFile)
- def test_mainValidBool(self):
- """Tests if the script successfully completes with valid input file with bools."""
- inputXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputBoolValid.xml'))
- expectedJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedBoolValid.json'))
-
- outputJsonDirPath = os.path.join(self.testOutputPath, 'outputBoolValidResult')
- outputJsonPath = os.path.join(outputJsonDirPath, 'dialog.json')
-
- BaseTestCaseCapture.createFolder(outputJsonDirPath)
-
- self.t_noException([['--common_dialog_main', inputXmlPath,
- '--common_outputs_dialogs', 'dialog.json',
- '--common_outputs_directory', outputJsonDirPath]])
-
- with open(expectedJsonPath, 'r') as expectedJsonFile, open(outputJsonPath, 'r') as outputJsonFile:
- assert json.load(expectedJsonFile) == json.load(outputJsonFile)
-
- def test_mainValidNodeTypes(self):
- """Tests if the script successfully completes with valid input file with node types."""
- inputXmlPath = os.path.abspath(os.path.join(self.dataBasePath, 'inputNodeTypesValid.xml'))
- expectedJsonPath = os.path.abspath(os.path.join(self.dataBasePath, 'expectedNodeTypesValid.json'))
-
- outputJsonDirPath = os.path.join(self.testOutputPath, 'outputNodeTypesValidResult')
- outputJsonPath = os.path.join(outputJsonDirPath, 'dialog.json')
-
- BaseTestCaseCapture.createFolder(outputJsonDirPath)
-
- self.t_noException([['--common_dialog_main', inputXmlPath,
- '--common_outputs_dialogs', 'dialog.json',
- '--common_outputs_directory', outputJsonDirPath,
- '--common_schema', self.dialogSchemaPath]])
-
- with open(expectedJsonPath, 'r') as expectedJsonFile, open(outputJsonPath, 'r') as outputJsonFile:
- assert json.load(expectedJsonFile) == json.load(outputJsonFile)
-
def test_mainMissingImport(self):
"""Tests if the script fails with file with missing imported dialog file."""
@@ -114,7 +103,7 @@ def test_validation(self):
("autogenerate_typeValid.xml", "autogenerate_typeInvalid.xml",
"Element 'autogenerate': The attribute 'type' is required but missing."),
("goto_targetValid.xml", "goto_targetInvalid.xml",
- "Element 'goto': Missing child element(s). Expected is one of ( behavior, target )."),
+ "Element 'goto': Missing child element(s). Expected is ( behavior )."),
("node_attributeValid.xml", "node_attributeInvalid.xml",
"Element 'node', attribute 'nonexistentAttribute': The attribute 'nonexistentAttribute' is not allowed."),
("node_singleElementValid.xml", "node_singleElementInvalid.xml",
diff --git a/ci/unit_tests/workspace_delete/main_test.py b/ci/unit_tests/workspace_delete/main_test.py
index 40e092b..b43aa32 100644
--- a/ci/unit_tests/workspace_delete/main_test.py
+++ b/ci/unit_tests/workspace_delete/main_test.py
@@ -34,7 +34,8 @@ class TestMain(BaseTestCaseCapture):
outputPath = os.path.join(dataBasePath, 'outputs')
jsonWorkspaceFilename = 'sample-skill.json'
jsonWorkspacePath = os.path.abspath(os.path.join(dataBasePath, jsonWorkspaceFilename))
- workspacesUrl = 'https://gateway.watsonplatform.net/conversation/api/v1/workspaces'
+ workspacesUrl = os.environ.get('WA_WORKSPACES_API_URL',
+ 'https://gateway.watsonplatform.net/conversation/api/v1/workspaces')
version = '2017-02-03'
def setup_class(cls):
diff --git a/ci/unit_tests/workspace_deploy/main_test.py b/ci/unit_tests/workspace_deploy/main_test.py
index d85338c..f8b22a2 100644
--- a/ci/unit_tests/workspace_deploy/main_test.py
+++ b/ci/unit_tests/workspace_deploy/main_test.py
@@ -28,7 +28,8 @@ class TestMain(BaseTestCaseCapture):
dataBasePath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'main_data')
outputPath = os.path.join(dataBasePath, 'outputs')
- workspacesUrl = 'https://gateway.watsonplatform.net/conversation/api/v1/workspaces'
+ workspacesUrl = os.environ.get('WA_WORKSPACES_API_URL',
+ 'https://gateway.watsonplatform.net/conversation/api/v1/workspaces')
version = '2017-02-03'
def setup_class(cls):
diff --git a/data_spec/dialog_schema.xml b/data_spec/dialog_schema.xml
index c6c99a9..6744cb6 100644
--- a/data_spec/dialog_schema.xml
+++ b/data_spec/dialog_schema.xml
@@ -197,8 +197,8 @@
-
-
+
+
@@ -281,6 +281,7 @@
+
diff --git a/doc/WAW_dialog_structure.md b/doc/WAW_dialog_structure.md
index 0c65759..f32895b 100644
--- a/doc/WAW_dialog_structure.md
+++ b/doc/WAW_dialog_structure.md
@@ -323,8 +323,8 @@ Represents a jump to another node or to another type of next step.
### Elements (in this order)
-- [``](#behavior) (only once)
-- [``](#target) **REQUIRED** (only once)
+- [``](#behavior) **REQUIRED** (only once)
+- [``](#target) (only once)
- [``](#selector) (only once)
### Attributes
diff --git a/scripts/XMLHandler.py b/scripts/XMLHandler.py
index 9e098aa..bae693e 100644
--- a/scripts/XMLHandler.py
+++ b/scripts/XMLHandler.py
@@ -159,6 +159,7 @@ def _createContextElement(self, variables):
def _createGotoElement(self, target, selector):
gotoXml = XML.Element('goto')
+ gotoXml.append(self._createXmlElement('behavior', 'skip_user_input'))
gotoXml.append(self._createXmlElement('target', target))
gotoXml.append(self._createXmlElement('selector', selector))
return gotoXml
diff --git a/scripts/compare_dialogs.py b/scripts/compare_dialogs.py
index 0442a95..7334a3d 100644
--- a/scripts/compare_dialogs.py
+++ b/scripts/compare_dialogs.py
@@ -18,6 +18,7 @@
import logging
import os
import os.path
+import pprint
import sys
from deepdiff import DeepDiff
@@ -61,10 +62,40 @@ def main(argv):
with open(outputpath) as g:
dialogOutputUnsorted = json.load(g)
- result = DeepDiff(dialogInputUnsorted, dialogOutputUnsorted, ignore_order=True).json
- logger.debug("result: %s", json.dumps(result, indent=4))
-
- if result == '{}':
+ try:
+ if not ((isinstance(dialogInputUnsorted, list) and isinstance(dialogOutputUnsorted, list)) or \
+ (isinstance(dialogInputUnsorted, dict) and isinstance(dialogOutputUnsorted, dict))):
+ # go for DeepDiff
+ raise TypeError
+ equal = True
+ for nodeIn in dialogInputUnsorted:
+ matchingOut = [node for node in dialogOutputUnsorted if node['dialog_node'] == nodeIn['dialog_node']]
+ if not len(matchingOut):
+ equal = False
+ logger.debug('missing output node ' + nodeIn['dialog_node'])
+ elif len(matchingOut) != 1:
+ equal = False
+ logger.debug('extra output nodes for ' + nodeIn['dialog_node'])
+ else:
+ result = DeepDiff(nodeIn, matchingOut[0], ignore_order=True)
+ if result:
+ equal = False
+ logger.debug(nodeIn['dialog_node'] + ' ' + pprint.pformat(result))
+
+ for node in matchingOut:
+ dialogOutputUnsorted.remove(node)
+
+ if len(dialogOutputUnsorted):
+ for node in dialogOutputUnsorted:
+ equal = False
+ logger.debug('unmatched output node ' + node['dialog_node'])
+ except:
+ # For non-lists/dictionaries or stuff without 'dialog_node' key
+ result = DeepDiff(dialogInputUnsorted, dialogOutputUnsorted, ignore_order=True).json
+ logger.debug("result: %s", json.dumps(result, indent=4))
+ equal = result == '{}'
+
+ if equal:
logger.info("Dialog JSONs are same.")
exit(0)
else:
diff --git a/scripts/dialog_json2xml.py b/scripts/dialog_json2xml.py
index 9c2e30e..94c0dd7 100644
--- a/scripts/dialog_json2xml.py
+++ b/scripts/dialog_json2xml.py
@@ -35,36 +35,60 @@ def convertDialog(dialogNodesJSON):
dialogXML = LET.Element("nodes", nsmap=NSMAP)
- #print dialogNodesJSON
- # find root
- rootJSON = findNode(dialogNodesJSON, None, None)
- expandNode(dialogNodesJSON, dialogXML, rootJSON)
+ topNodes = popAllChildren(dialogNodesJSON, None)
+ for topNode in topNodes:
+ dialogXML.append(expandNode(dialogNodesJSON, topNode))
+
if (len(dialogNodesJSON) > 0):
logger.error("There are " + str(len(dialogNodesJSON)) + " unprocessed nodes: " + str(dialogNodesJSON))
return dialogXML
# dialogNodesJSON: rest of nodes to process
-# upperNodeXML: where to append siblings
# nodeJSON: node to expand
-# Converts this node and recursively all its children and siblings
-def expandNode(dialogNodesJSON, upperNodeXML, nodeJSON):
+# Converts this node and recursively all its children
+def expandNode(dialogNodesJSON, nodeJSON):
nodeXML = convertNode(nodeJSON)
- upperNodeXML.append(nodeXML)
- # find first child
- childJSON = findNode(dialogNodesJSON, nodeJSON['dialog_node'], None) # expanded node as parent, None as sibling
- if childJSON is not None:
- childrenXML = LET.Element('nodes') # create 'nodes' tag
- nodeXML.append(childrenXML)
- expandNode(dialogNodesJSON, childrenXML, childJSON) # expand first child (process its siblings and children)
+ childrenXML = []
+ for childJSON in popAllChildren(dialogNodesJSON, nodeJSON):
+ childrenXML.append(expandNode(dialogNodesJSON, childJSON))
+
+ lastChildContainerTag = ''
+ childContainer = None
+ for childXML in childrenXML:
+ containerTag = getContainerTag(childXML)
+ if containerTag != lastChildContainerTag:
+ childContainer = LET.Element(containerTag)
+ nodeXML.append(childContainer)
+ lastChildContainerTag = containerTag
+ childContainer.append(childXML)
+
+ return nodeXML
+
+
+def getContainerTag(nodeXML):
+ containerTag = 'nodes'
+ if nodeXML.tag in ['slot', 'handler']:
+ containerTag = nodeXML.tag + 's'
+ return containerTag
- # find next sibling
- siblingJSON = findNode(dialogNodesJSON, nodeJSON['parent'] if 'parent' in nodeJSON else None, nodeJSON['dialog_node'])
- if siblingJSON is not None:
- expandNode(dialogNodesJSON, upperNodeXML, siblingJSON) # expand next sibling (process its siblings and children)
def convertNode(nodeJSON):
- nodeXML = LET.Element('node')
+
+ def node_tag(nodeJSON):
+ if 'type' in nodeJSON:
+ if nodeJSON['type'] == 'slot':
+ return 'slot'
+ if nodeJSON['type'] == 'event_handler':
+ return 'handler'
+ return 'node'
+
+ def removeChildrenWithTags(nodeXML, tags):
+ for child in list(nodeXML):
+ if child.tag in tags:
+ nodeXML.remove(child)
+
+ nodeXML = LET.Element(node_tag(nodeJSON))
nodeXML.attrib['name'] = nodeJSON['dialog_node']
logger.verbose("node '%s'", nodeXML.attrib['name'])
@@ -74,9 +98,10 @@ def convertNode(nodeJSON):
nodeXML.attrib['title'] = nodeJSON['title']
#type
if 'type' in nodeJSON:
- typeNodeXML = LET.Element('type')
- typeNodeXML.text = nodeJSON['type']
- nodeXML.append(typeNodeXML)
+ if nodeJSON['type'] != 'frame':
+ typeNodeXML = LET.Element('type')
+ typeNodeXML.text = nodeJSON['type']
+ nodeXML.append(typeNodeXML)
#disabled
if 'disabled' in nodeJSON:
disabledNodeXML = LET.Element('disabled')
@@ -107,6 +132,8 @@ def convertNode(nodeJSON):
else:
contextXML.text = ""
else:
+ if isinstance(nodeJSON['context'], dict):
+ nodeJSON['context'].pop('', 'unused value to remove occasional "":"" from context')
convertAll(nodeXML, nodeJSON, 'context')
#output
if 'output' in nodeJSON:
@@ -126,7 +153,12 @@ def convertNode(nodeJSON):
else:
convertAll(nodeXML, nodeJSON, 'output')
if 'text' in nodeJSON['output'] and not isinstance(nodeJSON['output']['text'], basestring):
- outputXML = nodeXML.find('output').find('text').tag = 'textValues'
+ textValuesXML = nodeXML.find('output').find('text')
+ textValuesXML.tag = 'textValues'
+ if len(textValuesXML.findall('values')) == 1:
+ textValuesXML = textValuesXML.find('values')
+ if 'structure' not in textValuesXML.attrib:
+ textValuesXML.attrib['structure'] = 'listItem'
if 'generic' in nodeJSON['output']:
if nodeJSON['output']['generic'] is None or len(nodeXML.find('output').findall('generic')) == 0:
return
@@ -230,7 +262,6 @@ def convertNode(nodeJSON):
actionsXML.append(actionXML)
nodeXML.append(actionsXML)
- #TODO handlers
#events
if 'event_name' in nodeJSON:
eventXML = LET.Element('event_name')
@@ -239,7 +270,18 @@ def convertNode(nodeJSON):
eventXML.attrib[XSI+'nil'] = "true"
else:
eventXML.text = nodeJSON['event_name']
- #TODO slots
+
+ if nodeXML.tag == 'slot':
+ if 'variable' in nodeJSON:
+ nodeXML.attrib['variable'] = nodeJSON['variable']
+ removeChildrenWithTags(nodeXML, ['type'])
+ # nodeXML.attrib.pop('name', None)
+
+ if nodeXML.tag == 'handler':
+ nodeXML.attrib['eventName'] = nodeJSON['event_name']
+ removeChildrenWithTags(nodeXML, ['event_name', 'type'])
+ # nodeXML.attrib.pop('name', None)
+
#TODO responses
return nodeXML
@@ -257,14 +299,18 @@ def convertAll(upperNodeXML, nodeJSON, keyJSON, nameXML = None):
logger.verbose("structure 'listItem'")
logger.verbose("name '%s'", nameXML)
+ def checkIfStructureIsListItem(structure, nameXML, nodeXML):
+ namesNotToSetListItem = ['action', 'actions', 'values']
+ if structure is not None and nameXML not in namesNotToSetListItem:
+ nodeXML.attrib['structure'] = "listItem"
+ logger.verbose("adding structure 'listItem' to node")
+
# None
if nodeJSON[keyJSON] is None:
logger.verbose("node is None")
nodeXML = LET.Element(str(nameXML))
upperNodeXML.append(nodeXML)
- if structure is not None and nameXML != "action" and nameXML != "actions":
- nodeXML.attrib['structure'] = "listItem"
- logger.verbose("adding structure 'listItem' to node")
+ checkIfStructureIsListItem(structure, nameXML, nodeXML)
nodeXML.attrib[XSI+'nil'] = "true"
# list
elif isinstance(nodeJSON[keyJSON], list):
@@ -294,9 +340,7 @@ def convertAll(upperNodeXML, nodeJSON, keyJSON, nameXML = None):
else:
nodeXML = LET.Element(str(nameXML))
upperNodeXML.append(nodeXML)
- if structure is not None and nameXML != "action" and nameXML != "actions":
- nodeXML.attrib['structure'] = "listItem"
- logger.verbose("add structure 'listItem' for '%s'", nameXML)
+ checkIfStructureIsListItem(structure, nameXML, nodeXML)
for subKeyJSON in nodeJSON[keyJSON]:
convertAll(nodeXML, nodeJSON[keyJSON], subKeyJSON)
# string
@@ -304,18 +348,14 @@ def convertAll(upperNodeXML, nodeJSON, keyJSON, nameXML = None):
logger.verbose("node is string")
nodeXML = LET.Element(str(nameXML))
upperNodeXML.append(nodeXML)
- if structure is not None and nameXML != "action" and nameXML != "actions":
- nodeXML.attrib['structure'] = "listItem"
- logger.verbose("add structure 'listItem' for '%s'", nameXML)
+ checkIfStructureIsListItem(structure, nameXML, nodeXML)
nodeXML.text = nodeJSON[keyJSON]
# bool
elif isinstance(nodeJSON[keyJSON], bool):
logger.verbose("node is boolean")
nodeXML = LET.Element(str(nameXML))
upperNodeXML.append(nodeXML)
- if structure is not None and nameXML != "action" and nameXML != "actions":
- nodeXML.attrib['structure'] = "listItem"
- logger.verbose("add structure 'listItem' for '%s'", nameXML)
+ checkIfStructureIsListItem(structure, nameXML, nodeXML)
nodeXML.text = str(nodeJSON[keyJSON])
nodeXML.attrib['type'] = "boolean"
# int, long, float, complex
@@ -323,9 +363,7 @@ def convertAll(upperNodeXML, nodeJSON, keyJSON, nameXML = None):
logger.verbose("node is number")
nodeXML = LET.Element(str(nameXML))
upperNodeXML.append(nodeXML)
- if structure is not None and nameXML != "action" and nameXML != "actions":
- nodeXML.attrib['structure'] = "listItem"
- logger.verbose("add structure 'listItem' for '%s'", nameXML)
+ checkIfStructureIsListItem(structure, nameXML, nodeXML)
nodeXML.text = str(nodeJSON[keyJSON])
nodeXML.attrib['type'] = "number"
else:
@@ -340,6 +378,27 @@ def findNode(dialogNodesJSON, parentName, siblingName):
return nodeJSON
return None
+def popFirstChild(dialogNodesJSON, parentNode):
+ return findNode(dialogNodesJSON, getValue(parentNode, 'dialog_node'), None)
+
+def popNextSibling(dialogNodesJSON, parentNode, siblingNode):
+ return findNode(dialogNodesJSON, getValue(parentNode, 'dialog_node'), getValue(siblingNode, 'dialog_node'))
+
+def popAllChildren(dialogNodesJSON, node):
+ if not node:
+ node = {'dialog_node': None}
+ children = []
+ child = popFirstChild(dialogNodesJSON, node)
+ if child:
+ children.append(child)
+ while True:
+ child = popNextSibling(dialogNodesJSON, node, child)
+ if child:
+ children.append(child)
+ else:
+ break
+ return children
+
def getValue(dict, key):
if key in dict:
# check another null like values
diff --git a/scripts/dialog_xml2json.py b/scripts/dialog_xml2json.py
index f6a5d36..48c46d8 100644
--- a/scripts/dialog_xml2json.py
+++ b/scripts/dialog_xml2json.py
@@ -616,6 +616,8 @@ def printNodes(root, parent, dialogJSON):
nodeJSON['type'] = nodeXML.find('type').text
elif nodeXML.find('slots') is not None:
nodeJSON['type'] = "frame"
+ elif nodeXML.tag == 'slot':
+ nodeJSON['type'] = "slot"
# disabled
if nodeXML.find('disabled') is not None:
if nodeXML.find('disabled').text in ["True", "true"]:
@@ -701,9 +703,12 @@ def printNodes(root, parent, dialogJSON):
logger.warning('missing goto target in node: %s', nodeXML.find('name').text)
elif nodeXML.find('goto').find('target').text == '::FIRST_SIBLING':
nodeXML.find('goto').find('target').text = next(x for x in root if x.tag == 'node').find('name').text
- gotoJson = {'dialog_node':nodeXML.find('goto').find('target').text}
+ gotoJson = {}
gotoJson['behavior'] = nodeXML.find('goto').find('behavior').text if nodeXML.find('goto').find('behavior') is not None else DEFAULT_BEHAVIOR
- gotoJson['selector'] = nodeXML.find('goto').find('selector').text if nodeXML.find('goto').find('selector') is not None else DEFAULT_SELECTOR
+ if nodeXML.find('goto').find('target') is not None:
+ gotoJson['dialog_node'] = nodeXML.find('goto').find('target').text
+ if nodeXML.find('goto').find('selector') is not None:
+ gotoJson['selector'] = nodeXML.find('goto').find('selector').text
nodeJSON['next_step'] = gotoJson
# PARENT
if parent is not None:
@@ -726,20 +731,11 @@ def printNodes(root, parent, dialogJSON):
# CLOSE NODE
previousSibling = nodeXML
- # ADD ALL CHILDREN NODES
- nodes = nodeXML.find('nodes')
- if nodes is not None:
- children.extend(nodes)
-
- # ADD ALL SLOTS (FRAME FUNCTIONALITY)
- slots = nodeXML.find('slots')
- if slots is not None:
- children.extend(slots)
-
- # ADD ALL HANDLERS (FRAME FUNCTIONALITY)
- handlers = nodeXML.find('handlers')
- if handlers is not None:
- children.extend(handlers)
+ for node in list(nodeXML):
+ if node.tag in ['node', 'slot', 'handler']:
+ children.append(node)
+ if node.tag in ['nodes', 'slots', 'handlers']:
+ children.extend(node)
# PROCESS ALL CHILDREN
if children: