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 + + + + + + Say your first name. + + + + @name + + @name + + + + + Thank you, @name. + + + + + Please say your first name. + + + + + + + + + Say your age in years. + + + + @sys-number + + @sys-number + + + + + You are @sys-number years old. + + + + + Please say your age in years. + + + + + + + + + Tell me your first name and age in years and I will find a friend for you. + + + + @sys-date + + Today is nice weather, isn't it? + + + + #ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_ + + + sequential + I enjoy hiking + I love swimming? + + + + + + I will find some friend for you, $name. + + 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 + + + + + + Say your first name. + + + + @name + + @name + + + + + Thank you, @name. + + + + + Please say your first name. + + + + + + + + + Say your age in years. + + + + @sys-number + + @sys-number + + + + + You are @sys-number years old. + + + + + Please say your age in years. + + + + + + + + + Tell me your first name and age in years and I will find a friend for you. + + + + @sys-date + + Today is nice weather, isn't it? + + + + #ALL_ABOUT_ME_WHAT_DO_YOU_LIKE_TO_DO_FOR_FUN_ + + + I enjoy hiking + I love swimming? + sequential + + + + + + I will find some friend for you, $name. + + 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: