From 3b64af274e4bd8ddac2d1d6069a8e467b02ff6d8 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 2 Apr 2020 12:14:10 +0530 Subject: [PATCH 01/78] Fix TypeError for relative difference cases in v1 and v2 time detectors --- .../detectors/temporal/time/time_detection.py | 8 +-- .../temporal/time/en/time_detection.py | 8 +-- .../tests/temporal/time/time_ner_tests.yaml | 71 +++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/ner_v1/detectors/temporal/time/time_detection.py b/ner_v1/detectors/temporal/time/time_detection.py index 22b3df952..d74436b8d 100644 --- a/ner_v1/detectors/temporal/time/time_detection.py +++ b/ner_v1/detectors/temporal/time/time_detection.py @@ -678,7 +678,7 @@ def _detect_time_with_difference(self, time_list=None, original_list=None): setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = 'df' time_list.append(time) @@ -719,7 +719,7 @@ def _detect_time_with_difference_later(self, time_list=None, original_list=None) setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = 'df' time_list.append(time) @@ -760,7 +760,7 @@ def _detect_time_with_every_x_hour(self, time_list=None, original_list=None): setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = EVERY_TIME_TYPE time_list.append(time) @@ -792,7 +792,7 @@ def _detect_time_with_once_in_x_day(self, time_list=None, original_list=None): setter = "hh" antisetter = "mm" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = EVERY_TIME_TYPE time_list.append(time) diff --git a/ner_v2/detectors/temporal/time/en/time_detection.py b/ner_v2/detectors/temporal/time/en/time_detection.py index 1fc1fe4eb..684d8e254 100644 --- a/ner_v2/detectors/temporal/time/en/time_detection.py +++ b/ner_v2/detectors/temporal/time/en/time_detection.py @@ -865,7 +865,7 @@ def _detect_time_with_difference(self, time_list=None, original_list=None): setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = 'df' time['tz'] = None if not self.timezone else self.timezone.zone @@ -907,7 +907,7 @@ def _detect_time_with_difference_later(self, time_list=None, original_list=None) setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = 'df' time['tz'] = None if not self.timezone else self.timezone.zone @@ -949,7 +949,7 @@ def _detect_time_with_every_x_hour(self, time_list=None, original_list=None): setter = "mm" antisetter = "hh" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = EVERY_TIME_TYPE time['tz'] = None if not self.timezone else self.timezone.zone @@ -985,7 +985,7 @@ def _detect_time_with_once_in_x_day(self, time_list=None, original_list=None): setter = "hh" antisetter = "mm" time = dict() - time[setter] = t1 + time[setter] = int(t1) time[antisetter] = 0 time['nn'] = EVERY_TIME_TYPE time['tz'] = None if not self.timezone else self.timezone.zone diff --git a/ner_v2/tests/temporal/time/time_ner_tests.yaml b/ner_v2/tests/temporal/time/time_ner_tests.yaml index de067501d..265f9b2da 100644 --- a/ner_v2/tests/temporal/time/time_ner_tests.yaml +++ b/ner_v2/tests/temporal/time/time_ner_tests.yaml @@ -713,6 +713,77 @@ tests: range: "end" time_type: null original_text: "10:00 to 14:00" + - id: en_55 + message: "check back in 5 mins" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 0 + mm: 5 + nn: "df" + original_text: "in 5 mins" + - id: en_56 + message: "check back in 5 hrs" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 5 + mm: 0 + nn: "df" + original_text: "in 5 hrs" + - id: en_57 + message: "try 4 minutes later" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 0 + mm: 4 + nn: "df" + original_text: "4 minutes later" + - id: en_58 + message: "try 10 hours later" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 10 + mm: 0 + nn: "df" + original_text: "10 hours later" + - id: en_59 + message: "repeat every 16 minutes" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 0 + mm: 16 + nn: "ev" + original_text: "every 16 minutes" + - id: en_60 + message: "repeat evry 21 hrs" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 21 + mm: 0 + nn: "ev" + original_text: "evry 21 hrs" + - id: en_61 + message: "set it for once in 3 days" + bot_message: null + range_enabled: true + outputs: + - output_id: 1 + hh: 72 + mm: 0 + nn: "ev" + original_text: "once in 3 days" + hi: - id: hi_1 message: "सुबह 10 बजे" From 3c9b1f97bf9b1275bd55bb726bc249031f802a1d Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 2 Apr 2020 12:29:27 +0530 Subject: [PATCH 02/78] Fix missing keys in test cases for TimeDetector --- .../temporal/time/test_time_detection.py | 1 + .../tests/temporal/time/time_ner_tests.yaml | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/ner_v2/tests/temporal/time/test_time_detection.py b/ner_v2/tests/temporal/time/test_time_detection.py index b3577772b..6aa1c7eff 100644 --- a/ner_v2/tests/temporal/time/test_time_detection.py +++ b/ner_v2/tests/temporal/time/test_time_detection.py @@ -66,6 +66,7 @@ def run_test(self): time_dicts, spans = time_detector.detect_entity(text=message, range_enabled=range_enabled, **kwargs) for time_dict in time_dicts: + # TODO: This is not correct! actual output should not modified instead expected output should be if "range" not in time_dict: time_dict.update({"range": None}) if "time_type" not in time_dict: diff --git a/ner_v2/tests/temporal/time/time_ner_tests.yaml b/ner_v2/tests/temporal/time/time_ner_tests.yaml index 265f9b2da..6c84a7282 100644 --- a/ner_v2/tests/temporal/time/time_ner_tests.yaml +++ b/ner_v2/tests/temporal/time/time_ner_tests.yaml @@ -722,6 +722,9 @@ tests: hh: 0 mm: 5 nn: "df" + tz: "UTC" + range: null + time_type: null original_text: "in 5 mins" - id: en_56 message: "check back in 5 hrs" @@ -732,6 +735,9 @@ tests: hh: 5 mm: 0 nn: "df" + tz: "UTC" + range: null + time_type: null original_text: "in 5 hrs" - id: en_57 message: "try 4 minutes later" @@ -742,6 +748,9 @@ tests: hh: 0 mm: 4 nn: "df" + tz: "UTC" + range: null + time_type: null original_text: "4 minutes later" - id: en_58 message: "try 10 hours later" @@ -752,6 +761,9 @@ tests: hh: 10 mm: 0 nn: "df" + tz: "UTC" + range: null + time_type: null original_text: "10 hours later" - id: en_59 message: "repeat every 16 minutes" @@ -762,6 +774,9 @@ tests: hh: 0 mm: 16 nn: "ev" + tz: "UTC" + range: null + time_type: null original_text: "every 16 minutes" - id: en_60 message: "repeat evry 21 hrs" @@ -772,6 +787,9 @@ tests: hh: 21 mm: 0 nn: "ev" + tz: "UTC" + range: null + time_type: null original_text: "evry 21 hrs" - id: en_61 message: "set it for once in 3 days" @@ -782,6 +800,9 @@ tests: hh: 72 mm: 0 nn: "ev" + tz: "UTC" + range: null + time_type: null original_text: "once in 3 days" hi: From d24217ef593756f755476fc7d9cfe5273f8287db Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Tue, 14 Apr 2020 18:51:31 +0530 Subject: [PATCH 03/78] fix(date-detection): Fix TypeError when comparing None mm with int cleanup some except clauses too Affected detectors: _gregorian_day_month_year_format _gregorian_month_day_year_format _gregorian_month_day_with_ordinals_year_format _gregorian_month_day_format _gregorian_day_month_format --- .../temporal/date/en/date_detection.py | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/ner_v2/detectors/temporal/date/en/date_detection.py b/ner_v2/detectors/temporal/date/en/date_detection.py index 48e91504d..4fc0786fd 100644 --- a/ner_v2/detectors/temporal/date/en/date_detection.py +++ b/ner_v2/detectors/temporal/date/en/date_detection.py @@ -320,8 +320,8 @@ def _gregorian_day_month_year_format(self, date_list=None, original_list=None): if not pattern[3] and self.timezone.localize(datetime.datetime(year=yy, month=mm, day=dd)) \ < self.now_date: yy += 1 - except: - return date_list, original_list + except Exception: + continue date = { 'dd': int(dd), @@ -377,8 +377,8 @@ def _gregorian_month_day_year_format(self, date_list=None, original_list=None): if not pattern[3] and self.timezone.localize(datetime.datetime(year=yy, month=mm, day=dd)) \ < self.now_date: yy += 1 - except: - return date_list, original_list + except Exception: + continue date = { 'dd': int(dd), @@ -698,8 +698,9 @@ def _gregorian_month_day_with_ordinals_year_format(self, date_list=None, origina if not yy1 and not yy2 and self.timezone.localize(datetime.datetime(year=yy, month=mm, day=dd)) \ < self.now_date: yy += 1 - except: - return date_list, original_list + except Exception: + continue + date = { 'dd': int(dd), 'mm': int(mm), @@ -747,17 +748,16 @@ def _gregorian_month_day_format(self, date_list=None, original_list=None): dd = pattern[2] probable_mm = pattern[1] mm = self.__get_month_index(probable_mm) - if dd: + if dd and mm: dd = int(dd) - if mm: mm = int(mm) - if self.now_date.month > mm: - yy = self.now_date.year + 1 - elif self.now_date.day > dd and self.now_date.month == mm: - yy = self.now_date.year + 1 - else: - yy = self.now_date.year - if mm: + if self.now_date.month > mm: + yy = self.now_date.year + 1 + elif self.now_date.day > dd and self.now_date.month == mm: + yy = self.now_date.year + 1 + else: + yy = self.now_date.year + date_dict = { 'dd': int(dd), 'mm': int(mm), @@ -804,17 +804,15 @@ def _gregorian_day_month_format(self, date_list=None, original_list=None): dd = pattern[1] probable_mm = pattern[2] mm = self.__get_month_index(probable_mm) - if dd: + if dd and mm: dd = int(dd) - if mm: mm = int(mm) - if self.now_date.month > mm: - yy = self.now_date.year + 1 - elif self.now_date.day > dd and self.now_date.month == mm: - yy = self.now_date.year + 1 - else: - yy = self.now_date.year - if mm: + if self.now_date.month > mm: + yy = self.now_date.year + 1 + elif self.now_date.day > dd and self.now_date.month == mm: + yy = self.now_date.year + 1 + else: + yy = self.now_date.year date_dict = { 'dd': int(dd), 'mm': int(mm), From b7920dc206304ccd621fbb27bcef7025a15fa3d9 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Fri, 17 Apr 2020 17:05:52 +0530 Subject: [PATCH 04/78] fix(date-detection): Repeat the same changes in v1/date --- .../detectors/temporal/date/date_detection.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/ner_v1/detectors/temporal/date/date_detection.py b/ner_v1/detectors/temporal/date/date_detection.py index 65b38f0ab..b2310a677 100644 --- a/ner_v1/detectors/temporal/date/date_detection.py +++ b/ner_v1/detectors/temporal/date/date_detection.py @@ -1305,17 +1305,15 @@ def _gregorian_month_day_format(self, date_list=None, original_list=None): dd = pattern[2] probable_mm = pattern[1] mm = self.__get_month_index(probable_mm) - if dd: + if dd and mm: dd = int(dd) - if mm: mm = int(mm) - if self.now_date.month > mm: - yy = self.now_date.year + 1 - elif self.now_date.day > dd and self.now_date.month == mm: - yy = self.now_date.year + 1 - else: - yy = self.now_date.year - if mm: + if self.now_date.month > mm: + yy = self.now_date.year + 1 + elif self.now_date.day > dd and self.now_date.month == mm: + yy = self.now_date.year + 1 + else: + yy = self.now_date.year date_dict = { 'dd': int(dd), 'mm': int(mm), @@ -1362,17 +1360,15 @@ def _gregorian_day_month_format(self, date_list=None, original_list=None): dd = pattern[1] probable_mm = pattern[2] mm = self.__get_month_index(probable_mm) - if dd: + if dd and mm: dd = int(dd) - if mm: mm = int(mm) - if self.now_date.month > mm: - yy = self.now_date.year + 1 - elif self.now_date.day > dd and self.now_date.month == mm: - yy = self.now_date.year + 1 - else: - yy = self.now_date.year - if mm: + if self.now_date.month > mm: + yy = self.now_date.year + 1 + elif self.now_date.day > dd and self.now_date.month == mm: + yy = self.now_date.year + 1 + else: + yy = self.now_date.year date_dict = { 'dd': int(dd), 'mm': int(mm), From 8f76e7212bda759a695e5c2424b8ffc5e585f293 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 31 Mar 2020 01:16:04 +0530 Subject: [PATCH 05/78] First commit --- postman_tests/data/entities/budget.json | 38 + postman_tests/data/entities/city.json | 41 + postman_tests/data/entities/date.json | 41 + postman_tests/data/entities/email.json | 35 + postman_tests/data/entities/number.json | 62 ++ postman_tests/data/entities/number_range.json | 35 + postman_tests/data/entities/person_name.json | 44 + postman_tests/data/entities/phoneV1.json | 38 + postman_tests/data/entities/phoneV2.json | 38 + postman_tests/data/entities/pnr.json | 35 + postman_tests/data/entities/regex.json | 36 + postman_tests/data/entities/time.json | 60 ++ postman_tests/data/entities/time_range.json | 80 ++ postman_tests/data/environment.json | 14 + postman_tests/data/ner_collection.json | 919 ++++++++++++++++++ postman_tests/data/newman_data.json | 412 ++++++++ postman_tests/run_tests.py | 87 ++ 17 files changed, 2015 insertions(+) create mode 100644 postman_tests/data/entities/budget.json create mode 100644 postman_tests/data/entities/city.json create mode 100644 postman_tests/data/entities/date.json create mode 100644 postman_tests/data/entities/email.json create mode 100644 postman_tests/data/entities/number.json create mode 100644 postman_tests/data/entities/number_range.json create mode 100644 postman_tests/data/entities/person_name.json create mode 100644 postman_tests/data/entities/phoneV1.json create mode 100644 postman_tests/data/entities/phoneV2.json create mode 100644 postman_tests/data/entities/pnr.json create mode 100644 postman_tests/data/entities/regex.json create mode 100644 postman_tests/data/entities/time.json create mode 100644 postman_tests/data/entities/time_range.json create mode 100644 postman_tests/data/environment.json create mode 100644 postman_tests/data/ner_collection.json create mode 100644 postman_tests/data/newman_data.json create mode 100644 postman_tests/run_tests.py diff --git a/postman_tests/data/entities/budget.json b/postman_tests/data/entities/budget.json new file mode 100644 index 000000000..954c2174c --- /dev/null +++ b/postman_tests/data/entities/budget.json @@ -0,0 +1,38 @@ +[ + { + "input": { + "message": "shirts between 2000 to 3000", + "entity_name": "budget" + }, + "expected": { + "original_text": "2000 to 3000", + "max_budget": 3000, + "type": "normal_budget", + "min_budget": 2000 + } + }, + { + "input": { + "message": "I want to see jeans between 2500 to 4200", + "entity_name": "budget" + }, + "expected": { + "original_text": "2500 to 4200", + "max_budget": 4200, + "type": "normal_budget", + "min_budget": 2500 + } + }, + { + "input": { + "message": "formals between 5000 to 9999", + "entity_name": "budget" + }, + "expected": { + "original_text": "5000 to 9999", + "max_budget": 9999, + "type": "normal_budget", + "min_budget": 5000 + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/city.json b/postman_tests/data/entities/city.json new file mode 100644 index 000000000..cc2a5a735 --- /dev/null +++ b/postman_tests/data/entities/city.json @@ -0,0 +1,41 @@ +[ + { + "input": { + "message": "i want to go to mumbai", + "entity_name": "city_list" + }, + "expected": { + "original_text": "mumbai", + "to": true, + "from": false, + "value": "mumbai", + "normal": false + } + }, + { + "input": { + "message": "i want to go to delhi", + "entity_name": "city_list" + }, + "expected": { + "original_text": "delhi", + "to": true, + "from": false, + "value": "New Delhi", + "normal": false + } + }, + { + "input": { + "message": "i want to go to chennai", + "entity_name": "city_list" + }, + "expected": { + "original_text": "chennai", + "to": true, + "from": false, + "value": "Chennai", + "normal": false + } + } +] diff --git a/postman_tests/data/entities/date.json b/postman_tests/data/entities/date.json new file mode 100644 index 000000000..784870d09 --- /dev/null +++ b/postman_tests/data/entities/date.json @@ -0,0 +1,41 @@ +[ + { + "input": { + "message": "Set me a reminder for 23 December", + "entity_name": "date" + }, + "expected": { + "original_text": "23 december", + "type": "date", + "dd": 23, + "mm": 12, + "yy": 2020 + } + }, + { + "input": { + "message": "Set me a reminder for 2 May", + "entity_name": "date" + }, + "expected": { + "original_text": "2 may", + "type": "date", + "dd": 2, + "mm": 5, + "yy": 2020 + } + }, + { + "input": { + "message": "Set me a reminder for 3 June", + "entity_name": "date" + }, + "expected": { + "original_text": "3 june", + "type": "date", + "dd": 3, + "mm": 6, + "yy": 2020 + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/email.json b/postman_tests/data/entities/email.json new file mode 100644 index 000000000..294eb5139 --- /dev/null +++ b/postman_tests/data/entities/email.json @@ -0,0 +1,35 @@ +[ + { + "input": { + "message": "my email id is apurv.nagvenkar@gmail.com", + "entity_name": "email" + }, + "expected": { + "original_text": "apurv.nagvenkar@gmail.com", + "value": "apurv.nagvenkar@gmail.com", + "language": "en" + } + }, + { + "input": { + "message": "my email id is ashutosh@haptik.co", + "entity_name": "email" + }, + "expected": { + "original_text": "ashutosh@haptik.co", + "value": "ashutosh@haptik.co", + "language": "en" + } + }, + { + "input": { + "message": "my email id is amansrivastava94@gmail.com", + "entity_name": "email" + }, + "expected": { + "original_text": "amansrivastava94@gmail.com", + "value": "amansrivastava94@gmail.com", + "language": "en" + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/number.json b/postman_tests/data/entities/number.json new file mode 100644 index 000000000..a2f4b237b --- /dev/null +++ b/postman_tests/data/entities/number.json @@ -0,0 +1,62 @@ +[ + { + "input": { + "message": "I want to purchase 30 units of mobile and 40 units of Television", + "entity_name": "number_of_unit", + "min_digits": 1, + "max_digits": 2 + }, + "expected": [ + { + "original_text": "30", + "value": "30", + "language": "en" + }, + { + "original_text": "40", + "value": "40", + "language": "en" + } + ] + }, + { + "input": { + "message": "I want to purchase 12 units of banana and 15 units of apple", + "entity_name": "number_of_unit", + "min_digits": 1, + "max_digits": 2 + }, + "expected": [ + { + "original_text": "12", + "value": "12", + "language": "en" + }, + { + "original_text": "15", + "value": "15", + "language": "en" + } + ] + }, + { + "input": { + "message": "I want to purchase 99 units of spoon and 1 units of Table", + "entity_name": "number_of_unit", + "min_digits": 1, + "max_digits": 2 + }, + "expected": [ + { + "original_text": "99", + "value": "99", + "language": "en" + }, + { + "original_text": "1", + "value": "1", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/number_range.json b/postman_tests/data/entities/number_range.json new file mode 100644 index 000000000..47dfd334a --- /dev/null +++ b/postman_tests/data/entities/number_range.json @@ -0,0 +1,35 @@ +[ + { + "input": { + "message": "Give me a number between 1 and 100", + "entity_name": "number_range" + }, + "expected": { + "original_text": "between 1 and 100", + "min_value": "1", + "max_value": "100" + } + }, + { + "input": { + "message": "My monthly salary will be more than 2k per month", + "entity_name": "number_range" + }, + "expected": { + "original_text": "more than 2k", + "min_value": "2000", + "max_value": null + } + }, + { + "input": { + "message": "more than 200", + "entity_name": "number_range" + }, + "expected": { + "original_text": "more than 200", + "min_value": "200", + "max_value": null + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/person_name.json b/postman_tests/data/entities/person_name.json new file mode 100644 index 000000000..082a0e9d7 --- /dev/null +++ b/postman_tests/data/entities/person_name.json @@ -0,0 +1,44 @@ +[ + { + "input": { + "message": "my name is yash doshi", + "entity_name": "person_name" + }, + "expected": { + "original_text": "yash doshi", + "first_name": "yash", + "last_name": "doshi", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + } + }, + { + "input": { + "message": "my name is Deep Viral Baweja", + "entity_name": "person_name" + }, + "expected": { + "original_text": "Deep Viral Baweja", + "first_name": "Deep", + "last_name": "Baweja", + "middle_name": "Viral", + "model_verified": false, + "datastore_verified": true + } + }, + { + "input": { + "message": "His name is amaan srivastava", + "entity_name": "person_name" + }, + "expected": { + "original_text": "amaan srivastava", + "first_name": "amaan", + "last_name": "srivastava", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/phoneV1.json b/postman_tests/data/entities/phoneV1.json new file mode 100644 index 000000000..7329c70ef --- /dev/null +++ b/postman_tests/data/entities/phoneV1.json @@ -0,0 +1,38 @@ +[ + { + "input": { + "message": "my contact number is 9049961794", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "9049961794", + "country_calling_code": "91", + "value": "9049961794", + "language": "en" + } + }, + { + "input": { + "message": "My phone number would be 9930341387", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + } + }, + { + "input": { + "message": "You can call me on +919920231234", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "919920231234", + "language": "en" + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/phoneV2.json b/postman_tests/data/entities/phoneV2.json new file mode 100644 index 000000000..4e4d6a0bc --- /dev/null +++ b/postman_tests/data/entities/phoneV2.json @@ -0,0 +1,38 @@ +[ + { + "input": { + "message": "my contact number is 08877665543", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "08877665543", + "country_calling_code": "91", + "value": "8877665543", + "language": "en" + } + }, + { + "input": { + "message": "My phone number would be 9930341387", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + } + }, + { + "input": { + "message": "You can call me on +919920231234", + "entity_name": "phone_number" + }, + "expected": { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "9920231234", + "language": "en" + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/pnr.json b/postman_tests/data/entities/pnr.json new file mode 100644 index 000000000..12f5c31e4 --- /dev/null +++ b/postman_tests/data/entities/pnr.json @@ -0,0 +1,35 @@ +[ + { + "input": { + "message": "check my pnr status for 2141215305.", + "entity_name": "pnr" + }, + "expected": { + "original_text": "2141215305", + "value": "2141215305", + "language": "en" + } + }, + { + "input": { + "message": "check my pnr status for 3714578.", + "entity_name": "pnr" + }, + "expected": { + "original_text": "3714578", + "value": "3714578", + "language": "en" + } + }, + { + "input": { + "message": "check my pnr status for 11234456.", + "entity_name": "pnr" + }, + "expected": { + "original_text": "11234456", + "value": "11234456", + "language": "en" + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/regex.json b/postman_tests/data/entities/regex.json new file mode 100644 index 000000000..b31fdfda4 --- /dev/null +++ b/postman_tests/data/entities/regex.json @@ -0,0 +1,36 @@ +[ + { + "input": { + "message": "123456 is my otp", + "entity_name": "regex", + "regex": "\\d{4,6}" + }, + + "expected": { + "original_text": "123456", + "value": "123456" + } + }, + { + "input": { + "message": "798865 is my otp", + "entity_name": "regex", + "regex": "\\d{4,6}" + }, + "expected": { + "original_text": "798865", + "value": "798865" + } + }, + { + "input": { + "message": "my otp is 112233", + "entity_name": "regex", + "regex": "\\d{4,6}" + }, + "expected": { + "original_text": "112233", + "value": "112233" + } + } +] \ No newline at end of file diff --git a/postman_tests/data/entities/time.json b/postman_tests/data/entities/time.json new file mode 100644 index 000000000..dd5df8282 --- /dev/null +++ b/postman_tests/data/entities/time.json @@ -0,0 +1,60 @@ +[ + { + "input": { + "message": "John arrived at the bus stop at 13:50 hrs expecting the bus to be there in 15 mins.But the bus was scheduled for 12:30 pm", + "entity_name": "time" + }, + "expected": [ + { + "original_text": "12:30 pm", + "mm": 30, + "hh": 12, + "nn": "pm", + "language": "en" + }, + { + "original_text": "in 15 mins", + "mm": 15, + "hh": 0, + "nn": "df", + "language": "en" + }, + { + "original_text": "13:50", + "mm": 50, + "hh": 13, + "nn": "hrs", + "language": "en" + } + ] + }, + { + "input": { + "message": "Aman arrived at the bus stop at 17:20 hrs expecting the bus to be there in 11 mins.But the bus was scheduled for 5:47 pm", + "entity_name": "time" + }, + "expected": [ + { + "original_text": "5:47 pm", + "mm": 47, + "hh": 5, + "nn": "pm", + "language": "en" + }, + { + "original_text": "in 11 mins", + "mm": 11, + "hh": 0, + "nn": "df", + "language": "en" + }, + { + "original_text": "17:20", + "mm": 20, + "hh": 17, + "nn": "hrs", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/time_range.json b/postman_tests/data/entities/time_range.json new file mode 100644 index 000000000..a796b9dd5 --- /dev/null +++ b/postman_tests/data/entities/time_range.json @@ -0,0 +1,80 @@ +[ + { + "input": { + "message": "Set a drink water reminder for tomorrow from 5:00 AM to 9:00 PM", + "entity_name": "time_with_range" + }, + "expected": [ + { + "original_text": "5:00 am to 9:00 pm", + "mm": 0, + "hh": 5, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "5:00 am to 9:00 pm", + "mm": 0, + "hh": 9, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ] + }, + { + "input": { + "message": "Set a drink water reminder for tomorrow from 8:30 am to 5:30 pm", + "entity_name": "time_with_range" + }, + "expected": [ + { + "original_text": "8:30 am to 5:30 pm", + "mm": 30, + "hh": 8, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "8:30 am to 5:30 pm", + "mm": 30, + "hh": 5, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ] + }, + { + "input": { + "message": "Set a drink water reminder for tomorrow from 11:40 AM to 11:12 PM", + "entity_name": "time_with_range" + }, + "expected": [ + { + "original_text": "11:40 am to 11:12 pm", + "mm": 40, + "hh": 11, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "11:40 am to 11:12 pm", + "mm": 12, + "hh": 11, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ] + } +] \ No newline at end of file diff --git a/postman_tests/data/environment.json b/postman_tests/data/environment.json new file mode 100644 index 000000000..6cbbec04e --- /dev/null +++ b/postman_tests/data/environment.json @@ -0,0 +1,14 @@ +{ + "id": "9dbcc6c6-dd15-498a-a4f7-5fc0cd63d542", + "name": "chatbot-ner-dev", + "values": [ + { + "key": "url", + "value": "localhost:8081", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2020-03-05T12:18:03.223Z", + "_postman_exported_using": "Postman/7.19.1" +} diff --git a/postman_tests/data/ner_collection.json b/postman_tests/data/ner_collection.json new file mode 100644 index 000000000..faceb3a39 --- /dev/null +++ b/postman_tests/data/ner_collection.json @@ -0,0 +1,919 @@ +{ + "info": { + "_postman_id": "94050ad5-c8a5-49ba-b8b6-ba19c4f0d2ea", + "name": "ner_collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "City entity", + "event": [ + { + "listen": "test", + "script": { + "id": "d2e89180-b51e-44fe-87af-a971b48f15bb", + "exec": [ + "var shouldBeSkipped = !('city_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get city data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.to).to.eql(data.city_expected.to); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.from).to.eql(data.city_expected.from); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.city_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value.toLowerCase()).to.eql(data.city_expected.value.toLowerCase()); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.normal).to.eql(data.city_expected.normal); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/city/?message={{city_message}}&entity_name={{city_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "city", + "" + ], + "query": [ + { + "key": "message", + "value": "{{city_message}}" + }, + { + "key": "entity_name", + "value": "{{city_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Time Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "5c4a0338-38cf-482b-be58-de81496136bb", + "exec": [ + "var shouldBeSkipped = !('time_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.time_expected[i].original_text); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_expected[i].mm); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_expected[i].hh); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_expected[i].normal); ", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/time/?message={{time_message}}&entity_name={{time_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "time", + "" + ], + "query": [ + { + "key": "message", + "value": "{{time_message}}" + }, + { + "key": "entity_name", + "value": "{{time_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Person Name Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "aab03db0-2700-4ed1-a9d4-978acce08ba1", + "exec": [ + "var shouldBeSkipped = !('person_name_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get person name data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.person_name_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid first_name\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.first_name).to.eql(data.person_name_expected.first_name); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid last_name\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.last_name).to.eql(data.person_name_expected.last_name); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid middle_name\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.middle_name).to.eql(data.person_name_expected.middle_name); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid model_verified\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.model_verified).to.eql(data.person_name_expected.model_verified); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid datastore_verified\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.datastore_verified).to.eql(data.person_name_expected.datastore_verified); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/person_name/?message={{person_name_message}}&entity_name={{person_name_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "person_name", + "" + ], + "query": [ + { + "key": "message", + "value": "{{person_name_message}}" + }, + { + "key": "entity_name", + "value": "{{person_name_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "PNR Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "c88850db-8dc1-4ca4-bb67-66568fb04fd1", + "exec": [ + "var shouldBeSkipped = !('pnr_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get pnr data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.pnr_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.pnr_expected.value); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/pnr/?message={{pnr_message}}&entity_name={{pnr_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "pnr", + "" + ], + "query": [ + { + "key": "message", + "value": "{{pnr_message}}" + }, + { + "key": "entity_name", + "value": "{{pnr_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Time Range Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "6726f8f5-a85a-46c8-9d67-c483d2d68db3", + "exec": [ + "var shouldBeSkipped = !('time_range_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time_range data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.time_range_expected[i].original_text); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_range_expected[i].mm); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_range_expected[i].hh); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.range).to.eql(data.time_range_expected[i].range); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_range_expected[i].normal); ", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/time_with_range/?message={{time_range_message}}&entity_name={{time_range_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "time_with_range", + "" + ], + "query": [ + { + "key": "message", + "value": "{{time_range_message}}" + }, + { + "key": "entity_name", + "value": "{{time_range_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Regex Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "180b0299-b3bf-4afb-acff-1642a40ada0c", + "exec": [ + "var shouldBeSkipped = !('regex_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get regex data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.regex_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.regex_expected.value); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/regex/?message={{regex_message}}&entity_name={{regex_entity_name}}®ex={{regex_regex}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "regex", + "" + ], + "query": [ + { + "key": "message", + "value": "{{regex_message}}" + }, + { + "key": "entity_name", + "value": "{{regex_entity_name}}" + }, + { + "key": "regex", + "value": "{{regex_regex}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Email Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "f0342e22-da7b-44f2-a55b-b52050907b35", + "exec": [ + "var shouldBeSkipped = !('email_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get email data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.email_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.email_expected.value); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/email/?message={{email_message}}&entity_name={{email_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "email", + "" + ], + "query": [ + { + "key": "message", + "value": "{{email_message}}" + }, + { + "key": "entity_name", + "value": "{{email_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Budget Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "912756f9-cceb-4ba3-ac67-14d83c3df8e4", + "exec": [ + "var shouldBeSkipped = !('budget_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get budget data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.budget_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_budget\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.max_budget).to.eql(data.budget_expected.max_budget); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_budget\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.min_budget).to.eql(data.budget_expected.min_budget); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.type).to.eql(data.budget_expected.type); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/budget/?message={{budget_message}}&entity_name={{budget_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "budget", + "" + ], + "query": [ + { + "key": "message", + "value": "{{budget_message}}" + }, + { + "key": "entity_name", + "value": "{{budget_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Date Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "df576c4c-f337-4964-9cfb-a7f4e5611784", + "exec": [ + "var shouldBeSkipped = !('date_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.date_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value.type).to.eql(data.date_expected.type); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value.dd).to.eql(data.date_expected.dd); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value.mm).to.eql(data.date_expected.mm); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value.yy).to.eql(data.date_expected.yy); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/date/?message={{date_message}}&entity_name={{date_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "date", + "" + ], + "query": [ + { + "key": "message", + "value": "{{date_message}}" + }, + { + "key": "entity_name", + "value": "{{date_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Number Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "a1bf1b2a-f098-41d2-8c33-dd5d78ea8b4f", + "exec": [ + "var shouldBeSkipped = !('number_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.number_expected[i].original_text); ", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.number_expected[i].value); ", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/number/?message={{number_message}}&entity_name={{number_entity_name}}&min_number_digits={{number_min_digits}}&max_number_digits={{number_max_digits}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "number", + "" + ], + "query": [ + { + "key": "message", + "value": "{{number_message}}" + }, + { + "key": "entity_name", + "value": "{{number_entity_name}}" + }, + { + "key": "min_number_digits", + "value": "{{number_min_digits}}" + }, + { + "key": "max_number_digits", + "value": "{{number_max_digits}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Number Range Entity", + "event": [ + { + "listen": "test", + "script": { + "id": "2481d107-0e0c-4545-8576-b04e4e2ff818", + "exec": [ + "var shouldBeSkipped = !('number_range_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number_range data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.number_range_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.min_value).to.eql(data.number_range_expected.min_value); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.max_value).to.eql(data.number_range_expected.max_value); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v2/number_range/?message={{number_range_message}}&entity_name={{number_range_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v2", + "number_range", + "" + ], + "query": [ + { + "key": "message", + "value": "{{number_range_message}}" + }, + { + "key": "entity_name", + "value": "{{number_range_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Phone number entity v2", + "event": [ + { + "listen": "test", + "script": { + "id": "e6e7a3fe-9a22-49a0-8ca9-cdffb794f743", + "exec": [ + "var shouldBeSkipped = !('phoneV2_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.phoneV2_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.phoneV2_expected.value); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid country_calling_code\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.country_calling_code).to.eql(data.phoneV2_expected.country_calling_code); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v2/phone_number/?message={{phoneV2_message}}&entity_name={{phoneV2_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v2", + "phone_number", + "" + ], + "query": [ + { + "key": "message", + "value": "{{phoneV2_message}}" + }, + { + "key": "entity_name", + "value": "{{phoneV2_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Phone number entity", + "event": [ + { + "listen": "test", + "script": { + "id": "4b978b73-2ca4-4263-afcb-ae44affa1fed", + "exec": [ + "var shouldBeSkipped = !('phoneV1_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.eql(1);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].original_text).to.eql(data.phoneV1_expected.original_text); ", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.phoneV1_expected.value); ", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/phone_number/?message={{phoneV1_message}}&entity_name={{phoneV1_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "phone_number", + "" + ], + "query": [ + { + "key": "message", + "value": "{{phoneV1_message}}" + }, + { + "key": "entity_name", + "value": "{{phoneV1_entity_name}}" + } + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file diff --git a/postman_tests/data/newman_data.json b/postman_tests/data/newman_data.json new file mode 100644 index 000000000..e285c959f --- /dev/null +++ b/postman_tests/data/newman_data.json @@ -0,0 +1,412 @@ +[ + { + "phoneV2_message": "my contact number is 08877665543", + "phoneV2_entity_name": "phone_number", + "phoneV2_expected": { + "original_text": "08877665543", + "country_calling_code": "91", + "value": "8877665543", + "language": "en" + }, + "date_message": "Set me a reminder for 23 December", + "date_entity_name": "date", + "date_expected": { + "original_text": "23 december", + "type": "date", + "dd": 23, + "mm": 12, + "yy": 2020 + }, + "city_message": "i want to go to mumbai", + "city_entity_name": "city_list", + "city_expected": { + "original_text": "mumbai", + "to": true, + "from": false, + "value": "mumbai", + "normal": false + }, + "pnr_message": "check my pnr status for 2141215305.", + "pnr_entity_name": "pnr", + "pnr_expected": { + "original_text": "2141215305", + "value": "2141215305", + "language": "en" + }, + "number_message": "I want to purchase 30 units of mobile and 40 units of Television", + "number_entity_name": "number_of_unit", + "number_min_digits": 1, + "number_max_digits": 2, + "number_expected": [ + { + "original_text": "30", + "value": "30", + "language": "en" + }, + { + "original_text": "40", + "value": "40", + "language": "en" + } + ], + "number_range_message": "Give me a number between 1 and 100", + "number_range_entity_name": "number_range", + "number_range_expected": { + "original_text": "between 1 and 100", + "min_value": "1", + "max_value": "100" + }, + "time_message": "John arrived at the bus stop at 13:50 hrs expecting the bus to be there in 15 mins.But the bus was scheduled for 12:30 pm", + "time_entity_name": "time", + "time_expected": [ + { + "original_text": "12:30 pm", + "mm": 30, + "hh": 12, + "nn": "pm", + "language": "en" + }, + { + "original_text": "in 15 mins", + "mm": 15, + "hh": 0, + "nn": "df", + "language": "en" + }, + { + "original_text": "13:50", + "mm": 50, + "hh": 13, + "nn": "hrs", + "language": "en" + } + ], + "budget_message": "shirts between 2000 to 3000", + "budget_entity_name": "budget", + "budget_expected": { + "original_text": "2000 to 3000", + "max_budget": 3000, + "type": "normal_budget", + "min_budget": 2000 + }, + "person_name_message": "my name is yash doshi", + "person_name_entity_name": "person_name", + "person_name_expected": { + "original_text": "yash doshi", + "first_name": "yash", + "last_name": "doshi", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + }, + "email_message": "my email id is apurv.nagvenkar@gmail.com", + "email_entity_name": "email", + "email_expected": { + "original_text": "apurv.nagvenkar@gmail.com", + "value": "apurv.nagvenkar@gmail.com", + "language": "en" + }, + "time_range_message": "Set a drink water reminder for tomorrow from 5:00 AM to 9:00 PM", + "time_range_entity_name": "time_with_range", + "time_range_expected": [ + { + "original_text": "5:00 am to 9:00 pm", + "mm": 0, + "hh": 5, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "5:00 am to 9:00 pm", + "mm": 0, + "hh": 9, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ], + "regex_message": "123456 is my otp", + "regex_entity_name": "regex", + "regex_regex": "\\d{4,6}", + "regex_expected": { + "original_text": "123456", + "value": "123456" + }, + "phoneV1_message": "my contact number is 9049961794", + "phoneV1_entity_name": "phone_number", + "phoneV1_expected": { + "original_text": "9049961794", + "country_calling_code": "91", + "value": "9049961794", + "language": "en" + } + }, + { + "phoneV2_message": "My phone number would be 9930341387", + "phoneV2_entity_name": "phone_number", + "phoneV2_expected": { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + }, + "date_message": "Set me a reminder for 2 May", + "date_entity_name": "date", + "date_expected": { + "original_text": "2 may", + "type": "date", + "dd": 2, + "mm": 5, + "yy": 2020 + }, + "city_message": "i want to go to delhi", + "city_entity_name": "city_list", + "city_expected": { + "original_text": "delhi", + "to": true, + "from": false, + "value": "New Delhi", + "normal": false + }, + "pnr_message": "check my pnr status for 3714578.", + "pnr_entity_name": "pnr", + "pnr_expected": { + "original_text": "3714578", + "value": "3714578", + "language": "en" + }, + "number_message": "I want to purchase 12 units of banana and 15 units of apple", + "number_entity_name": "number_of_unit", + "number_min_digits": 1, + "number_max_digits": 2, + "number_expected": [ + { + "original_text": "12", + "value": "12", + "language": "en" + }, + { + "original_text": "15", + "value": "15", + "language": "en" + } + ], + "number_range_message": "My monthly salary will be more than 2k per month", + "number_range_entity_name": "number_range", + "number_range_expected": { + "original_text": "more than 2k", + "min_value": "2000", + "max_value": null + }, + "time_message": "Aman arrived at the bus stop at 17:20 hrs expecting the bus to be there in 11 mins.But the bus was scheduled for 5:47 pm", + "time_entity_name": "time", + "time_expected": [ + { + "original_text": "5:47 pm", + "mm": 47, + "hh": 5, + "nn": "pm", + "language": "en" + }, + { + "original_text": "in 11 mins", + "mm": 11, + "hh": 0, + "nn": "df", + "language": "en" + }, + { + "original_text": "17:20", + "mm": 20, + "hh": 17, + "nn": "hrs", + "language": "en" + } + ], + "budget_message": "I want to see jeans between 2500 to 4200", + "budget_entity_name": "budget", + "budget_expected": { + "original_text": "2500 to 4200", + "max_budget": 4200, + "type": "normal_budget", + "min_budget": 2500 + }, + "person_name_message": "my name is Deep Viral Baweja", + "person_name_entity_name": "person_name", + "person_name_expected": { + "original_text": "Deep Viral Baweja", + "first_name": "Deep", + "last_name": "Baweja", + "middle_name": "Viral", + "model_verified": false, + "datastore_verified": true + }, + "email_message": "my email id is ashutosh@haptik.co", + "email_entity_name": "email", + "email_expected": { + "original_text": "ashutosh@haptik.co", + "value": "ashutosh@haptik.co", + "language": "en" + }, + "time_range_message": "Set a drink water reminder for tomorrow from 8:30 am to 5:30 pm", + "time_range_entity_name": "time_with_range", + "time_range_expected": [ + { + "original_text": "8:30 am to 5:30 pm", + "mm": 30, + "hh": 8, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "8:30 am to 5:30 pm", + "mm": 30, + "hh": 5, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ], + "regex_message": "798865 is my otp", + "regex_entity_name": "regex", + "regex_regex": "\\d{4,6}", + "regex_expected": { + "original_text": "798865", + "value": "798865" + }, + "phoneV1_message": "My phone number would be 9930341387", + "phoneV1_entity_name": "phone_number", + "phoneV1_expected": { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + } + }, + { + "phoneV2_message": "You can call me on +919920231234", + "phoneV2_entity_name": "phone_number", + "phoneV2_expected": { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "9920231234", + "language": "en" + }, + "date_message": "Set me a reminder for 3 June", + "date_entity_name": "date", + "date_expected": { + "original_text": "3 june", + "type": "date", + "dd": 3, + "mm": 6, + "yy": 2020 + }, + "city_message": "i want to go to chennai", + "city_entity_name": "city_list", + "city_expected": { + "original_text": "chennai", + "to": true, + "from": false, + "value": "Chennai", + "normal": false + }, + "pnr_message": "check my pnr status for 11234456.", + "pnr_entity_name": "pnr", + "pnr_expected": { + "original_text": "11234456", + "value": "11234456", + "language": "en" + }, + "number_message": "I want to purchase 99 units of spoon and 1 units of Table", + "number_entity_name": "number_of_unit", + "number_min_digits": 1, + "number_max_digits": 2, + "number_expected": [ + { + "original_text": "99", + "value": "99", + "language": "en" + }, + { + "original_text": "1", + "value": "1", + "language": "en" + } + ], + "number_range_message": "more than 200", + "number_range_entity_name": "number_range", + "number_range_expected": { + "original_text": "more than 200", + "min_value": "200", + "max_value": null + }, + "budget_message": "formals between 5000 to 9999", + "budget_entity_name": "budget", + "budget_expected": { + "original_text": "5000 to 9999", + "max_budget": 9999, + "type": "normal_budget", + "min_budget": 5000 + }, + "person_name_message": "His name is amaan srivastava", + "person_name_entity_name": "person_name", + "person_name_expected": { + "original_text": "amaan srivastava", + "first_name": "amaan", + "last_name": "srivastava", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + }, + "email_message": "my email id is amansrivastava94@gmail.com", + "email_entity_name": "email", + "email_expected": { + "original_text": "amansrivastava94@gmail.com", + "value": "amansrivastava94@gmail.com", + "language": "en" + }, + "time_range_message": "Set a drink water reminder for tomorrow from 11:40 AM to 11:12 PM", + "time_range_entity_name": "time_with_range", + "time_range_expected": [ + { + "original_text": "11:40 am to 11:12 pm", + "mm": 40, + "hh": 11, + "range": "start", + "nn": "am", + "time_type": null, + "language": "en" + }, + { + "original_text": "11:40 am to 11:12 pm", + "mm": 12, + "hh": 11, + "range": "end", + "nn": "pm", + "time_type": null, + "language": "en" + } + ], + "regex_message": "my otp is 112233", + "regex_entity_name": "regex", + "regex_regex": "\\d{4,6}", + "regex_expected": { + "original_text": "112233", + "value": "112233" + }, + "phoneV1_message": "You can call me on +919920231234", + "phoneV1_entity_name": "phone_number", + "phoneV1_expected": { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "919920231234", + "language": "en" + } + } +] \ No newline at end of file diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py new file mode 100644 index 000000000..7ca64ee44 --- /dev/null +++ b/postman_tests/run_tests.py @@ -0,0 +1,87 @@ +from __future__ import absolute_import +import json +import os +import subprocess +import glob + + +entities_data_path = 'data/entities/' +newman_data_path = 'data/newman_data.json' +collection_data_path = 'data/ner_collection.json' +environment_file_path = 'data/environment.json' + + +def check_if_data_valid(): + # Doing basic sanity checking here. + # Check that every test case is valid json and has the keys: input and expected + try: + path = '' + for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): + path = file_path + with open(file_path, 'r') as file: + data = json.load(file) + for item in data: + if not all(k in item for k in ('input', 'expected')): + print('Invalid json data in ' + file_path) + return False + return True + except Exception as e: + print('Invalid json data in ' + path + + '\n' + str(e)) + return False + + +def get_entity_name(file_path): + base=os.path.basename(file_path) + return os.path.splitext(base)[0] + + +def read_entities_data(): + entities_data = {} + for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): + entity = get_entity_name(file_path) + entities_data[entity] = [] + with open(file_path, 'r') as file: + data = json.load(file) + modified_data = [] + for d in data: + d1 = {} + for k, v in d['input'].items(): + d1[entity + '_' + k] = d['input'][k] + d1[entity + '_expected'] = d['expected'] + modified_data.append(d1) + entities_data[entity] = modified_data + return entities_data + + +def generate_newman_data(entities_data): + data = [] + for k in entities_data: + data.append(entities_data[k]) + newman_data = [] + while True: + found = False + iteration_data = [] + for list in data: + if list: + iteration_data.append(list.pop(0)) + found = True + if found == False: + break + else: + temp = {} + for item in iteration_data: + temp.update(item) + newman_data.append(temp) + return newman_data + + +if check_if_data_valid(): + try: + entities_data = read_entities_data() + newman_data = generate_newman_data(entities_data) + with open(newman_data_path, 'w') as fp: + json.dump(newman_data, fp, indent=4) + subprocess.Popen(f"newman run {collection_data_path} -d {newman_data_path} -e {environment_file_path}", shell=True).wait() + except Exception as e: + print(str(e)) From 4f5bd4fb5e9bfcce1e0169d5a970c2eac436ec00 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 20:12:07 +0000 Subject: [PATCH 06/78] Check current working directory when running tests --- postman_tests/run_tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 7ca64ee44..2c30f6e70 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -5,6 +5,7 @@ import glob +postman_data_directory = 'postman_tests/' entities_data_path = 'data/entities/' newman_data_path = 'data/newman_data.json' collection_data_path = 'data/ner_collection.json' @@ -76,6 +77,11 @@ def generate_newman_data(entities_data): return newman_data +#Switch to postman_tests if not already in that directory +if(os.path.basename(os.getcwd()) != postman_data_directory): + os.chdir(postman_data_directory) + + if check_if_data_valid(): try: entities_data = read_entities_data() From d44514284834c2537c5165001fcf4c39fd082934 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 20:12:44 +0000 Subject: [PATCH 07/78] Add newman_data.json to gitignore --- .gitignore | 1 + postman_tests/data/newman_data.json | 412 ---------------------------- 2 files changed, 1 insertion(+), 412 deletions(-) delete mode 100644 postman_tests/data/newman_data.json diff --git a/.gitignore b/.gitignore index b6be485bf..6e873bd18 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ sftp-config.json logs/ .vscode +newman_data.json diff --git a/postman_tests/data/newman_data.json b/postman_tests/data/newman_data.json deleted file mode 100644 index e285c959f..000000000 --- a/postman_tests/data/newman_data.json +++ /dev/null @@ -1,412 +0,0 @@ -[ - { - "phoneV2_message": "my contact number is 08877665543", - "phoneV2_entity_name": "phone_number", - "phoneV2_expected": { - "original_text": "08877665543", - "country_calling_code": "91", - "value": "8877665543", - "language": "en" - }, - "date_message": "Set me a reminder for 23 December", - "date_entity_name": "date", - "date_expected": { - "original_text": "23 december", - "type": "date", - "dd": 23, - "mm": 12, - "yy": 2020 - }, - "city_message": "i want to go to mumbai", - "city_entity_name": "city_list", - "city_expected": { - "original_text": "mumbai", - "to": true, - "from": false, - "value": "mumbai", - "normal": false - }, - "pnr_message": "check my pnr status for 2141215305.", - "pnr_entity_name": "pnr", - "pnr_expected": { - "original_text": "2141215305", - "value": "2141215305", - "language": "en" - }, - "number_message": "I want to purchase 30 units of mobile and 40 units of Television", - "number_entity_name": "number_of_unit", - "number_min_digits": 1, - "number_max_digits": 2, - "number_expected": [ - { - "original_text": "30", - "value": "30", - "language": "en" - }, - { - "original_text": "40", - "value": "40", - "language": "en" - } - ], - "number_range_message": "Give me a number between 1 and 100", - "number_range_entity_name": "number_range", - "number_range_expected": { - "original_text": "between 1 and 100", - "min_value": "1", - "max_value": "100" - }, - "time_message": "John arrived at the bus stop at 13:50 hrs expecting the bus to be there in 15 mins.But the bus was scheduled for 12:30 pm", - "time_entity_name": "time", - "time_expected": [ - { - "original_text": "12:30 pm", - "mm": 30, - "hh": 12, - "nn": "pm", - "language": "en" - }, - { - "original_text": "in 15 mins", - "mm": 15, - "hh": 0, - "nn": "df", - "language": "en" - }, - { - "original_text": "13:50", - "mm": 50, - "hh": 13, - "nn": "hrs", - "language": "en" - } - ], - "budget_message": "shirts between 2000 to 3000", - "budget_entity_name": "budget", - "budget_expected": { - "original_text": "2000 to 3000", - "max_budget": 3000, - "type": "normal_budget", - "min_budget": 2000 - }, - "person_name_message": "my name is yash doshi", - "person_name_entity_name": "person_name", - "person_name_expected": { - "original_text": "yash doshi", - "first_name": "yash", - "last_name": "doshi", - "middle_name": null, - "model_verified": false, - "datastore_verified": true - }, - "email_message": "my email id is apurv.nagvenkar@gmail.com", - "email_entity_name": "email", - "email_expected": { - "original_text": "apurv.nagvenkar@gmail.com", - "value": "apurv.nagvenkar@gmail.com", - "language": "en" - }, - "time_range_message": "Set a drink water reminder for tomorrow from 5:00 AM to 9:00 PM", - "time_range_entity_name": "time_with_range", - "time_range_expected": [ - { - "original_text": "5:00 am to 9:00 pm", - "mm": 0, - "hh": 5, - "range": "start", - "nn": "am", - "time_type": null, - "language": "en" - }, - { - "original_text": "5:00 am to 9:00 pm", - "mm": 0, - "hh": 9, - "range": "end", - "nn": "pm", - "time_type": null, - "language": "en" - } - ], - "regex_message": "123456 is my otp", - "regex_entity_name": "regex", - "regex_regex": "\\d{4,6}", - "regex_expected": { - "original_text": "123456", - "value": "123456" - }, - "phoneV1_message": "my contact number is 9049961794", - "phoneV1_entity_name": "phone_number", - "phoneV1_expected": { - "original_text": "9049961794", - "country_calling_code": "91", - "value": "9049961794", - "language": "en" - } - }, - { - "phoneV2_message": "My phone number would be 9930341387", - "phoneV2_entity_name": "phone_number", - "phoneV2_expected": { - "original_text": "9930341387", - "country_calling_code": "91", - "value": "9930341387", - "language": "en" - }, - "date_message": "Set me a reminder for 2 May", - "date_entity_name": "date", - "date_expected": { - "original_text": "2 may", - "type": "date", - "dd": 2, - "mm": 5, - "yy": 2020 - }, - "city_message": "i want to go to delhi", - "city_entity_name": "city_list", - "city_expected": { - "original_text": "delhi", - "to": true, - "from": false, - "value": "New Delhi", - "normal": false - }, - "pnr_message": "check my pnr status for 3714578.", - "pnr_entity_name": "pnr", - "pnr_expected": { - "original_text": "3714578", - "value": "3714578", - "language": "en" - }, - "number_message": "I want to purchase 12 units of banana and 15 units of apple", - "number_entity_name": "number_of_unit", - "number_min_digits": 1, - "number_max_digits": 2, - "number_expected": [ - { - "original_text": "12", - "value": "12", - "language": "en" - }, - { - "original_text": "15", - "value": "15", - "language": "en" - } - ], - "number_range_message": "My monthly salary will be more than 2k per month", - "number_range_entity_name": "number_range", - "number_range_expected": { - "original_text": "more than 2k", - "min_value": "2000", - "max_value": null - }, - "time_message": "Aman arrived at the bus stop at 17:20 hrs expecting the bus to be there in 11 mins.But the bus was scheduled for 5:47 pm", - "time_entity_name": "time", - "time_expected": [ - { - "original_text": "5:47 pm", - "mm": 47, - "hh": 5, - "nn": "pm", - "language": "en" - }, - { - "original_text": "in 11 mins", - "mm": 11, - "hh": 0, - "nn": "df", - "language": "en" - }, - { - "original_text": "17:20", - "mm": 20, - "hh": 17, - "nn": "hrs", - "language": "en" - } - ], - "budget_message": "I want to see jeans between 2500 to 4200", - "budget_entity_name": "budget", - "budget_expected": { - "original_text": "2500 to 4200", - "max_budget": 4200, - "type": "normal_budget", - "min_budget": 2500 - }, - "person_name_message": "my name is Deep Viral Baweja", - "person_name_entity_name": "person_name", - "person_name_expected": { - "original_text": "Deep Viral Baweja", - "first_name": "Deep", - "last_name": "Baweja", - "middle_name": "Viral", - "model_verified": false, - "datastore_verified": true - }, - "email_message": "my email id is ashutosh@haptik.co", - "email_entity_name": "email", - "email_expected": { - "original_text": "ashutosh@haptik.co", - "value": "ashutosh@haptik.co", - "language": "en" - }, - "time_range_message": "Set a drink water reminder for tomorrow from 8:30 am to 5:30 pm", - "time_range_entity_name": "time_with_range", - "time_range_expected": [ - { - "original_text": "8:30 am to 5:30 pm", - "mm": 30, - "hh": 8, - "range": "start", - "nn": "am", - "time_type": null, - "language": "en" - }, - { - "original_text": "8:30 am to 5:30 pm", - "mm": 30, - "hh": 5, - "range": "end", - "nn": "pm", - "time_type": null, - "language": "en" - } - ], - "regex_message": "798865 is my otp", - "regex_entity_name": "regex", - "regex_regex": "\\d{4,6}", - "regex_expected": { - "original_text": "798865", - "value": "798865" - }, - "phoneV1_message": "My phone number would be 9930341387", - "phoneV1_entity_name": "phone_number", - "phoneV1_expected": { - "original_text": "9930341387", - "country_calling_code": "91", - "value": "9930341387", - "language": "en" - } - }, - { - "phoneV2_message": "You can call me on +919920231234", - "phoneV2_entity_name": "phone_number", - "phoneV2_expected": { - "original_text": "919920231234", - "country_calling_code": "91", - "value": "9920231234", - "language": "en" - }, - "date_message": "Set me a reminder for 3 June", - "date_entity_name": "date", - "date_expected": { - "original_text": "3 june", - "type": "date", - "dd": 3, - "mm": 6, - "yy": 2020 - }, - "city_message": "i want to go to chennai", - "city_entity_name": "city_list", - "city_expected": { - "original_text": "chennai", - "to": true, - "from": false, - "value": "Chennai", - "normal": false - }, - "pnr_message": "check my pnr status for 11234456.", - "pnr_entity_name": "pnr", - "pnr_expected": { - "original_text": "11234456", - "value": "11234456", - "language": "en" - }, - "number_message": "I want to purchase 99 units of spoon and 1 units of Table", - "number_entity_name": "number_of_unit", - "number_min_digits": 1, - "number_max_digits": 2, - "number_expected": [ - { - "original_text": "99", - "value": "99", - "language": "en" - }, - { - "original_text": "1", - "value": "1", - "language": "en" - } - ], - "number_range_message": "more than 200", - "number_range_entity_name": "number_range", - "number_range_expected": { - "original_text": "more than 200", - "min_value": "200", - "max_value": null - }, - "budget_message": "formals between 5000 to 9999", - "budget_entity_name": "budget", - "budget_expected": { - "original_text": "5000 to 9999", - "max_budget": 9999, - "type": "normal_budget", - "min_budget": 5000 - }, - "person_name_message": "His name is amaan srivastava", - "person_name_entity_name": "person_name", - "person_name_expected": { - "original_text": "amaan srivastava", - "first_name": "amaan", - "last_name": "srivastava", - "middle_name": null, - "model_verified": false, - "datastore_verified": true - }, - "email_message": "my email id is amansrivastava94@gmail.com", - "email_entity_name": "email", - "email_expected": { - "original_text": "amansrivastava94@gmail.com", - "value": "amansrivastava94@gmail.com", - "language": "en" - }, - "time_range_message": "Set a drink water reminder for tomorrow from 11:40 AM to 11:12 PM", - "time_range_entity_name": "time_with_range", - "time_range_expected": [ - { - "original_text": "11:40 am to 11:12 pm", - "mm": 40, - "hh": 11, - "range": "start", - "nn": "am", - "time_type": null, - "language": "en" - }, - { - "original_text": "11:40 am to 11:12 pm", - "mm": 12, - "hh": 11, - "range": "end", - "nn": "pm", - "time_type": null, - "language": "en" - } - ], - "regex_message": "my otp is 112233", - "regex_entity_name": "regex", - "regex_regex": "\\d{4,6}", - "regex_expected": { - "original_text": "112233", - "value": "112233" - }, - "phoneV1_message": "You can call me on +919920231234", - "phoneV1_entity_name": "phone_number", - "phoneV1_expected": { - "original_text": "919920231234", - "country_calling_code": "91", - "value": "919920231234", - "language": "en" - } - } -] \ No newline at end of file From 4c08af7890fae3e5827219c0ed353fcbf2ccead2 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 20:17:54 +0000 Subject: [PATCH 08/78] Upate dockerfile in docker-compose.yml to Dockerfile-python3 --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 798424370..71ec3693d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -12,7 +12,7 @@ services: chatbot-ner: build: context: .. - dockerfile: docker/Dockerfile + dockerfile: docker/Dockerfile-python3 # Vars being used are defined in config.example and used in settings.py # ENV vars defined in Dockerfile can be overwritten here before docker-compose up # just add to .env From 594c4a70c5013b373e0ad55e36f8bcbaa061a2fb Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 20:37:57 +0000 Subject: [PATCH 09/78] Refactor code related to newman --- postman_tests/lib/__init__.py | 0 postman_tests/lib/es.py | 0 postman_tests/lib/newman.py | 69 +++++++++++++++++++++++++++++++ postman_tests/run_tests.py | 77 +++-------------------------------- 4 files changed, 75 insertions(+), 71 deletions(-) create mode 100644 postman_tests/lib/__init__.py create mode 100644 postman_tests/lib/es.py create mode 100644 postman_tests/lib/newman.py diff --git a/postman_tests/lib/__init__.py b/postman_tests/lib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py new file mode 100644 index 000000000..e69de29bb diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py new file mode 100644 index 000000000..e3edcf0f9 --- /dev/null +++ b/postman_tests/lib/newman.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import +import json +import os +import glob + + +def check_if_data_valid(entities_data_path): + # Doing basic sanity checking here. + # Check that every test case is valid json and has the keys: input and expected + try: + path = '' + for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): + path = file_path + with open(file_path, 'r') as file: + data = json.load(file) + for item in data: + if not all(k in item for k in ('input', 'expected')): + print('Invalid json data in ' + file_path) + return False + return True + except Exception as e: + print('Invalid json data in ' + path + + '\n' + str(e)) + return False + + +def get_entity_name(file_path): + base=os.path.basename(file_path) + return os.path.splitext(base)[0] + + +def read_entities_data(entities_data_path): + entities_data = {} + for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): + entity = get_entity_name(file_path) + entities_data[entity] = [] + with open(file_path, 'r') as file: + data = json.load(file) + modified_data = [] + for d in data: + d1 = {} + for k, v in d['input'].items(): + d1[entity + '_' + k] = d['input'][k] + d1[entity + '_expected'] = d['expected'] + modified_data.append(d1) + entities_data[entity] = modified_data + return entities_data + + +def generate_newman_data(entities_data): + data = [] + for k in entities_data: + data.append(entities_data[k]) + newman_data = [] + while True: + found = False + iteration_data = [] + for item in data: + if item: + iteration_data.append(item.pop(0)) + found = True + if found == False: + break + else: + temp = {} + for item in iteration_data: + temp.update(item) + newman_data.append(temp) + return newman_data diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 2c30f6e70..3e266293d 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -1,8 +1,8 @@ from __future__ import absolute_import -import json -import os +from lib import newman import subprocess -import glob +import os +import json postman_data_directory = 'postman_tests/' @@ -12,80 +12,15 @@ environment_file_path = 'data/environment.json' -def check_if_data_valid(): - # Doing basic sanity checking here. - # Check that every test case is valid json and has the keys: input and expected - try: - path = '' - for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): - path = file_path - with open(file_path, 'r') as file: - data = json.load(file) - for item in data: - if not all(k in item for k in ('input', 'expected')): - print('Invalid json data in ' + file_path) - return False - return True - except Exception as e: - print('Invalid json data in ' + path - + '\n' + str(e)) - return False - - -def get_entity_name(file_path): - base=os.path.basename(file_path) - return os.path.splitext(base)[0] - - -def read_entities_data(): - entities_data = {} - for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): - entity = get_entity_name(file_path) - entities_data[entity] = [] - with open(file_path, 'r') as file: - data = json.load(file) - modified_data = [] - for d in data: - d1 = {} - for k, v in d['input'].items(): - d1[entity + '_' + k] = d['input'][k] - d1[entity + '_expected'] = d['expected'] - modified_data.append(d1) - entities_data[entity] = modified_data - return entities_data - - -def generate_newman_data(entities_data): - data = [] - for k in entities_data: - data.append(entities_data[k]) - newman_data = [] - while True: - found = False - iteration_data = [] - for list in data: - if list: - iteration_data.append(list.pop(0)) - found = True - if found == False: - break - else: - temp = {} - for item in iteration_data: - temp.update(item) - newman_data.append(temp) - return newman_data - - #Switch to postman_tests if not already in that directory if(os.path.basename(os.getcwd()) != postman_data_directory): os.chdir(postman_data_directory) -if check_if_data_valid(): +if newman.check_if_data_valid(entities_data_path): try: - entities_data = read_entities_data() - newman_data = generate_newman_data(entities_data) + entities_data = newman.read_entities_data(entities_data_path) + newman_data = newman.generate_newman_data(entities_data) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp, indent=4) subprocess.Popen(f"newman run {collection_data_path} -d {newman_data_path} -e {environment_file_path}", shell=True).wait() From 603ecb77b40f062a1d4f4c0c7019717b39dcc58c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 22:02:23 +0000 Subject: [PATCH 10/78] Index data into ES before running tests and then clear it --- postman_tests/data/elastic_search/city.csv | 1206 ++++++++++++++++++++ postman_tests/lib/common.py | 6 + postman_tests/lib/es.py | 52 + postman_tests/lib/newman.py | 8 +- postman_tests/run_tests.py | 4 + 5 files changed, 1270 insertions(+), 6 deletions(-) create mode 100644 postman_tests/data/elastic_search/city.csv create mode 100644 postman_tests/lib/common.py diff --git a/postman_tests/data/elastic_search/city.csv b/postman_tests/data/elastic_search/city.csv new file mode 100644 index 000000000..3f46cfb7c --- /dev/null +++ b/postman_tests/data/elastic_search/city.csv @@ -0,0 +1,1206 @@ +value,variants +Satara,Satara | +Nidadavole,Nidadavole | +Umaria,Umaria | +Mangaldoi,Mangaldoi | +Mudabidri,Mudabidri | +Zamania,Zamania | +Rapar,Rapar | +Tiptur,Tiptur | +Arakkonam,Arakkonam | +Sopore,Sopore | +Tarana,Tarana | +Mangaluru,Mangaluru | +guwahati,guwahati | +Baharampur,Baharampur | +Karur,Karur | +Sahaspur,Sahaspur | +Parli,Parli | +Khair,Khair | +Chennai,Chennai | +Shahabad,Shahabad | +Karimganj,Karimganj | +Sahaswan,Sahaswan | +jaipur,jaipur |rajasthan | +Guntakal,Guntakal | +Dhamtari,Dhamtari | +Savanur,Savanur | +Mahidpur,Mahidpur | +Mathura,Mathura | +Sainthia,Sainthia | +Puthuppally,Puthuppally | +Kochi,Kochi | +Safipur,Safipur | +Jagtial,Jagtial | +Muktsar,Muktsar | +Rahatgarh,Rahatgarh | +Patiala,Patiala | +Ahmedabad,Ahmedabad | +Ratia,Ratia | +Reengus,Reengus | +Charkhi Dadri,Charkhi |Dadri | +Varanasi,Varanasi | +chennai,chennai |madras |tamilnadu | +Sugauli,Sugauli | +Lalgudi,Lalgudi | +Sidhpur,Sidhpur | +Tirukkoyilur,Tirukkoyilur | +Sadri,Sadri | +Tirupathur,Tirupathur | +Port Blair,Port Blair |Blair | +Vedaranyam,Vedaranyam | +Yavatmal,Yavatmal | +Vikarabad,Vikarabad | +Todabhim,Todabhim | +Phagwara,Phagwara | +Purwa,Purwa | +Bharuch,Bharuch | +Bathinda,Bathinda | +Wardha,Wardha | +Thiruvananthapuram,Thiruvananthapuram | +Sitapur,Sitapur | +Sidhi,Sidhi | +Rupnagar,Rupnagar | +Achhnera,Achhnera | +Kanpur,Kanpur | +Vyara,Vyara | +Raghogarh Vijaypur,Raghogarh |Vijaypur | +Sandi,Sandi | +Piriyapatna,Piriyapatna | +Ujjain,Ujjain | +Mahbubnagar,Mahbubnagar | +Achalpur,Achalpur | +Nongstoin,Nongstoin | +Vita,Vita | +Naugawan Sadat,Naugawan |Sadat | +Sasaram,Sasaram | +Ghaziabad,Ghaziabad | +Mokokchung,Mokokchung | +Sonipat,Sonipat | +Loni,Loni | +Jind,Jind | +Sohna,Sohna | +Motipur,Motipur | +Nawabganj,Nawabganj | +Tarikere,Tarikere | +Fatehpur Sikri,Fatehpur |Sikri | +Sujangarh,Sujangarh | +Karjat,Karjat | +Powayan,Powayan | +Giridih,Giridih | +Madhepura,Madhepura | +Amritsar,Amritsar | +Sanduru,Sanduru | +Wanaparthy,Wanaparthy | +Cherthala,Cherthala | +Hardwar,Hardwar | +Umred,Umred | +East,east | +Simdega,Simdega | +Bahraich,Bahraich | +Kandukur,Kandukur | +Yadgir,Yadgir | +Lucknow,Lucknow | +Muzaffarpur,Muzaffarpur | +Rajapalayam,Rajapalayam | +Rajura,Rajura | +Kayamkulam,Kayamkulam | +Ranibennur,Ranibennur | +Wani,Wani | +Nepanagar,Nepanagar | +Nagapattinam,Nagapattinam | +Afzalpur,Afzalpur | +Meerut,Meerut | +Palampur,Palampur | +Attingal,Attingal | +Batala,Batala | +Rajaldesar,Rajaldesar | +Ladnu,Ladnu | +Pithoragarh,Pithoragarh | +Malpura,Malpura | +Palladam,Palladam | +Padra,Padra | +Mangrulpir,Mangrulpir | +Partur,Partur | +Nahan,Nahan | +Ladwa,Ladwa | +Amalapuram,Amalapuram | +Pilibhit,Pilibhit | +Najibabad,Najibabad | +Maihar,Maihar | +Unjha,Unjha | +Patti,Patti | +Sikanderpur,Sikanderpur | +Tirwaganj,Tirwaganj | +Chalakudy,Chalakudy | +Shahdol,Shahdol | +Siliguri,Siliguri | +Shamli,Shamli | +Sardarshahar,Sardarshahar | +Pandharpur,Pandharpur | +Kathua,Kathua | +Savner,Savner | +Modinagar,Modinagar | +Thiruthuraipoondi,Thiruthuraipoondi | +Gokak,Gokak | +Ichalkaranji,Ichalkaranji | +lucknow,lucknow | +Baripada Town,Baripada |Baripada Town | +Patratu,Patratu | +Dharmavaram,Dharmavaram | +Mhowgaon,Mhowgaon | +Pali,Pali | +Ottappalam,Ottappalam | +Anand,Anand | +Chittur Thathamangalam,Chittur |Thathamangalam | +Tindivanam,Tindivanam | +Thirumangalam,Thirumangalam | +Nuzvid,Nuzvid | +Takhatgarh,Takhatgarh | +Washim,Washim | +Sarsod,Sarsod | +Lahar,Lahar | +Sanawad,Sanawad | +Nandura,Nandura | +Peddapuram,Peddapuram | +Lakhisarai,Lakhisarai | +Rau,Rau | +Narasapuram,Narasapuram | +Pallapatti,Pallapatti | +Ranavav,Ranavav | +Siruguppa,Siruguppa | +Rudauli,Rudauli | +Tirora,Tirora | +Dibrugarh,Dibrugarh | +Tulsipur,Tulsipur | +Tohana,Tohana | +Bhawanipatna,Bhawanipatna | +Alipurduar,Alipurduar | +Pukhrayan,Pukhrayan | +Diphu,Diphu | +Sibsagar,Sibsagar | +Peravurani,Peravurani | +Akot,Akot | +Jhumri Tilaiya,Jhumri |Jhumri Tilaiya | +Mankachar,Mankachar | +Namakkal,Namakkal | +Nakodar,Nakodar | +Salur,Salur | +Niwai,Niwai | +Nirmal,Nirmal | +Tirur,Tirur | +Gohana,Gohana | +Sahjanwa,Sahjanwa | +Maner,Maner | +Nellikuppam,Nellikuppam | +New Delhi,Delhi | New Delhi +Ankleshwar,Ankleshwar | +Sankari,Sankari | +Paramakudi,Paramakudi | +Pauri,Pauri | +Mainaguri,Mainaguri | +Chittoor,Chittoor | +Bankura,Bankura | +Adoor,Adoor | +Rewari,Rewari | +Memari,Memari | +Imphal,Imphal | +ahmedabad,ahmedabad |ahmdabad |amdavad +Mudalagi,Mudalagi | +Titlagarh,Titlagarh | +Bapatla,Bapatla | +Mahesana,Mahesana | +Panchkula,Panchkula | +Tura,Tura | +Dehradun,Dehradun | +Kozhikode,Kozhikode | +Tuensang,Tuensang | +Etawah,Etawah | +Bhiwani,Bhiwani | +Medinipur,Medinipur | +Shirur,Shirur | +Robertson Pet,Robertson |Robertson Pet | +Ujhani,Ujhani | +Vinukonda,Vinukonda | +Mapusa,Mapusa | +Pehowa,Pehowa | +Mandvi,Mandvi | +Wadhwan,Wadhwan | +Eluru,Eluru | +Manasa,Manasa | +Miryalaguda,Miryalaguda | +Vidisha,Vidisha | +Mahasamund,Mahasamund | +Munger,Munger | +Madhubani,Madhubani | +Malkapur,Malkapur | +Motihari,Motihari | +Gopalganj,Gopalganj | +Dhule,Dhule | +Udaipur,Udaipur | +Palai,Palai | +Gooty,Gooty | +Alwar,Alwar | +Korba,Korba | +Rajsamand,Rajsamand | +Deesa,Deesa | +Malegaon,Malegaon | +Umarga,Umarga | +Rangia,Rangia | +Warhapur,Warhapur | +Rampurhat,Rampurhat | +Hajipur,Hajipur | +Parlakhemundi,Parlakhemundi | +Warud,Warud | +Faridabad,Faridabad | +Narsinghgarh,Narsinghgarh | +Buxar,Buxar | +Gurdaspur,Gurdaspur | +Pattukkottai,Pattukkottai | +Sanchore,Sanchore | +Palwal,Palwal | +kolkata,kolkata |calcutta +Kurnool,Kurnool | +Virar,Virar | +Ganjbasoda,Ganjbasoda | +Margao,Margao | +Firozpur Cantt,Firozpur |Cantt | +Taraori,Taraori | +Natham,Natham | +Sendhwa,Sendhwa | +Nilambur,Nilambur | +Shrigonda,Shrigonda | +Mahe,Mahe | +Kothagudem,Kothagudem | +Mungeli,Mungeli | +Kakinada,Kakinada | +Sundargarh,Sundargarh | +Revelganj,Revelganj | +Kancheepuram,Kancheepuram | +Indore,Indore | +Pedana,Pedana | +Shirdi,Shirdi | +Bagaha,Bagaha | +Neemuch,Neemuch | +Nagarkurnool,Nagarkurnool | +Sarangpur,Sarangpur | +Durg,Durg | +Palghar,Palghar | +Tinsukia,Tinsukia | +Purna,Purna | +Uravakonda,Uravakonda | +Madikeri,Madikeri | +Tiruvethipuram,Tiruvethipuram | +Sirsi,Sirsi | +Morvi,Morvi | +Uttar Pradesh,UP | +Tekkalakote,Tekkalakote | +Sirsa,Sirsa | +Udupi,Udupi | +Saunda,Saunda | +Ranaghat,Ranaghat | +Bikaner,Bikaner | +Chhapra,Chhapra | +Sangli,Sangli | +Palitana,Palitana | +Manwath,Manwath | +Kovvur,Kovvur | +Perambalur,Perambalur | +Rohtak,Rohtak | +Thiruvallur,Thiruvallur | +Sankarankovil,Sankarankovil | +Yemmiganur,Yemmiganur | +Modasa,Modasa | +Rafiganj,Rafiganj | +Raghunathpur,Raghunathpur | +Moga,Moga | +Thanesar,Thanesar | +Srikakulam,Srikakulam | +Manawar,Manawar | +Mahalingapura,Mahalingapura | +Nelamangala,Nelamangala | +Naihati,Naihati | +Noida,Noida | +Gudivada,Gudivada | +Sadulpur,Sadulpur | +Perumbavoor,Perumbavoor | +Sadasivpet,Sadasivpet | +Khanna,Khanna | +Mandideep,Mandideep | +Tiruchengode,Tiruchengode | +Tiruvannamalai,Tiruvannamalai | +Tharad,Tharad | +Barnala,Barnala | +Shivamogga,Shivamogga | +Veraval,Veraval | +Sangamner,Sangamner | +West,West | +Vijapur,Vijapur | +Ramgarh,Ramgarh | +Rampur Maniharan,Rampur |Maniharan | +Purnia,Purnia | +Niwari,Niwari | +Nathdwara,Nathdwara | +Sivagiri,Sivagiri | +Bettiah,Bettiah | +Arwal,Arwal | +Saharanpur,Saharanpur | +Karimnagar,Karimnagar | +Mohali,Mohali | +Lunawada,Lunawada | +Ahmednagar,Ahmednagar | +Gumia,Gumia | +Vellore,Vellore | +Nanded,Nanded | +Malur,Malur | +Lingsugur,Lingsugur | +Lal Nindaura,Lal Nindaura |Nindaura | +Narkhed,Narkhed | +Forbesganj,Forbesganj | +Vellakoil,Vellakoil | +Madanapalle,Madanapalle | +Jamui,Jamui | +Warisaliganj,Warisaliganj | +Shegaon,Shegaon | +Nanjikottai,Nanjikottai | +Naharlagun,Naharlagun | +Agartala,Agartala | +Talegaon Dabhade,Talegaon |Dabhade | +Ramagundam,Ramagundam | +Nokha,Nokha | +Sakaleshapura,Sakaleshapura | +Kyathampalle,Kyathampalle | +Lohardaga,Lohardaga | +Bhainsa,Bhainsa | +Robertsganj,Robertsganj | +Sakti,Sakti | +Mandi Dabwali,Mandi Dabwali |Dabwali | +Panaji,Panaji | +Phaltan,Phaltan | +Sangareddy,Sangareddy | +Phulabani,Phulabani | +Pasan,Pasan | +Sihor,Sihor | +Valparai,Valparai | +Tiruchirappalli,Tiruchirappalli | +Marigaon,Marigaon | +Mul,Mul | +Proddatur,Proddatur | +Nadiad,Nadiad | +Shahade,Shahade | +Pattran,Pattran | +Samalkha,Samalkha | +Hindupur,Hindupur | +Sheoganj,Sheoganj | +Uran Islampur,Uran |Islampur | +Thammampatti,Thammampatti | +Kohima,Kohima | +goa,goa | +Tonk,Tonk | +Karnal,Karnal | +Mandla,Mandla | +Mandapeta,Mandapeta | +Palwancha,Palwancha | +Nargund,Nargund | +bhopal,bhopal | +Mayang Imphal,Mayang |Imphal | +Panruti,Panruti | +Ratangarh,Ratangarh | +Dalli Rajhara,Dalli |Rajhara | +Dhuri,Dhuri | +Faridkot,Faridkot | +Sambhal,Sambhal | +Marhaura,Marhaura | +Keshod,Keshod | +Pallikonda,Pallikonda | +Unnamalaikadai,Unnamalaikadai | +Pathanamthitta,Pathanamthitta | +Tasgaon,Tasgaon | +Masaurhi,Masaurhi | +Rayachoti,Rayachoti | +Sundarnagar,Sundarnagar | +Venkatagiri,Venkatagiri | +Tanuku,Tanuku | +pune,pune +Tandur,Tandur | +Tiruvuru,Tiruvuru | +Reoti,Reoti | +Phulpur,Phulpur | +Petlad,Petlad | +Mansa,Mansa | +Sumerpur,Sumerpur | +Malavalli,Malavalli | +Rayadurg,Rayadurg | +Shiggaon,Shiggaon | +Kashipur,Kashipur | +Panvel,Panvel | +Rasra,Rasra | +Paradip,Paradip | +Upleta,Upleta | +Sholavandan,Sholavandan | +Periyakulam,Periyakulam | +Bhimavaram,Bhimavaram | +hyderabad,hyderabad +Asansol,Asansol | +Mahnar Bazar,Mahnar |Mahnar Bazar | +Gangarampur,Gangarampur | +Narsipatnam,Narsipatnam | +Mahemdabad,Mahemdabad | +Rajam,Rajam | +Tharamangalam,Tharamangalam | +Bhadrak,Bhadrak | +Khowai,Khowai | +Raigarh,Raigarh | +Rewa,Rewa | +Sangrur,Sangrur | +Bangalore,Bengaluru |Bangalore +Patna,Patna | +Rajgarh Churu,Rajgarh |Churu | +Puri,Puri | +Nabarangapur,Nabarangapur | +Nagercoil,Nagercoil | +Vandavasi,Vandavasi | +Sivakasi,Sivakasi | +Erode,Erode | +Nagda,Nagda | +Balurghat,Balurghat | +Thiruvalla,Thiruvalla | +Arsikere,Arsikere | +Rawatbhata,Rawatbhata | +Losal,Losal | +Tuni,Tuni | +Jagdalpur,Jagdalpur | +Safidon,Safidon | +Panamattom,Panamattom | +Sagar,Sagar | +Aurangabad,Aurangabad | +Katihar,Katihar | +Rosera,Rosera | +Tarbha,Tarbha | +Loha,Loha | +Solapur,Solapur | +Machilipatnam,Machilipatnam | +Tehri,Tehri | +Manavadar,Manavadar | +Sirsaganj,Sirsaganj | +Udhagamandalam,Udhagamandalam | +Amreli,Amreli | +Sunabeda,Sunabeda | +Mokameh,Mokameh | +Saundatti Yellamma,Saundatti |Yellamma | +Rania,Rania | +Pusad,Pusad | +Thangadh,Thangadh | +Madurai,Madurai | +Aruppukkottai,Aruppukkottai | +Manihari,Manihari | +Adityapur,Adityapur | +Lathi,Lathi | +Jalandhar Cantt,Jalandhar |Jalandhar Cantt | +Nawapur,Nawapur | +Jamalpur,Jamalpur | +Bhadrachalam,Bhadrachalam | +Vadipatti,Vadipatti | +Vizianagaram,Vizianagaram | +Srirampore,Srirampore | +Jamnagar,Jamnagar | +Nainital,Nainital | +Kishanganj,Kishanganj | +Hugli Chinsurah,Hugli |Chinsurah | +Guwahati,Guwahati | +Shahpur,Shahpur | +Sultanganj,Sultanganj | +Nainpur,Nainpur | +mangalore,mangalore | +Murshidabad,Murshidabad | +Tirukalukundram,Tirukalukundram | +Sahawar,Sahawar | +Vadodara,Vadodara | +Manuguru,Manuguru | +Jaipur,Jaipur | +Talcher,Talcher | +Madhugiri,Madhugiri | +Tarakeswar,Tarakeswar | +Punganur,Punganur | +Lonavala,Lonavala | +Pathri,Pathri | +Oddanchatram,Oddanchatram | +Murtijapur,Murtijapur | +Pauni,Pauni | +Nawanshahr,Nawanshahr | +Soyagaon,Soyagaon | +Ozar,Ozar | +Sagara,Sagara | +Phalodi,Phalodi | +Tuljapur,Tuljapur | +Rajgarh Alwar,Rajgarh |Alwar | +Sonamukhi,Sonamukhi | +Nabadwip,Nabadwip | +Surapura,Surapura | +Pipariya,Pipariya | +Lachhmangarh,Lachhmangarh | +Jatani,Jatani | +Rajampet,Rajampet | +Shendurjana,Shendurjana | +Talode,Talode | +Thana Bhawan,Thana Bhawan | +Porbandar,Porbandar | +Hardoi,Hardoi | +Pindwara,Pindwara | +Virudhachalam,Virudhachalam | +Sojat,Sojat | +Nanjangud,Nanjangud | +Sadalagi,Sadalagi | +Pathardi,Pathardi | +Sasvad,Sasvad | +Bhubaneswar,Bhubaneswar | +Karaikal,Karaikal | +Rabkavi Banhatti,Rabkavi |Banhatti | +Nandurbar,Nandurbar | +Chandausi,Chandausi | +Thirupuvanam,Thirupuvanam | +Azamgarh,Azamgarh | +Tanda,Tanda | +Narwana,Narwana | +Viluppuram,Viluppuram | +Kot Kapura,Kot |Kapura | +Sitamarhi,Sitamarhi | +Amroha,Amroha | +Moradabad,Moradabad | +Shrirampur,Shrirampur | +Malappuram,Malappuram | +Thuraiyur,Thuraiyur | +Santipur,Santipur | +Pandharkaoda,Pandharkaoda | +Nowrozabad Khodargama,Nowrozabad |Khodargama | +Shikaripur,Shikaripur | +Ramngarh,Ramngarh | +Palakkad,Palakkad | +Tamluk,Tamluk | +Balaghat,Balaghat | +Udaipurwati,Udaipurwati | +Rasipuram,Rasipuram | +Arrah,Arrah | +Mukerian,Mukerian | +Muvattupuzha,Muvattupuzha | +Bharatpur,Bharatpur | +Bhagalpur,Bhagalpur | +Sirkali,Sirkali | +Sivaganga,Sivaganga | +Patan,Patan | +Todaraisingh,Todaraisingh | +Cooch Behar,Cooch |Behar | +Viswanatham,Viswanatham | +Balangir,Balangir | +Panagar,Panagar | +Vapi,Vapi | +Hawa Jaipur,Hawa |Jaipur | +Salaya,Salaya | +Ranipet,Ranipet | +Kalyan Dombivali,Kalyan |Dombivali | +Nandivaram Guduvancheri,Nandivaram |Guduvancheri | +Nehtaur,Nehtaur | +Chatra,Chatra | +Pondicherry,Pondicherry | +Sheopur,Sheopur | +Kendujhar,Kendujhar | +Shahpura,Shahpura | +Mundargi,Mundargi | +Chilakaluripet,Chilakaluripet | +Raver,Raver | +Jalandhar,Jalandhar | +Taliparamba,Taliparamba | +Kadiri,Kadiri | +Rath,Rath | +Palacole,Palacole | +Bhopal,Bhopal | +Narnaul,Narnaul | +Rajgarh,Rajgarh | +Pandua,Pandua | +Lumding,Lumding | +mumbai,mumbai |bombay | +Sardhana,Sardhana | +Solan,Solan | +Deoghar,Deoghar | +Shivpuri,Shivpuri | +Brahmapur,Brahmapur | +Rajgir,Rajgir | +Adilabad,Adilabad | +Pilkhuwa,Pilkhuwa | +Hisar,Hisar | +Pappinisseri,Pappinisseri | +Obra,Obra | +Valsad,Valsad | +Srinagar,Srinagar | +Kamareddy,Kamareddy | +Ratnagiri,Ratnagiri | +Ajmer,Ajmer | +Kagaznagar,Kagaznagar | +Bhatapara,Bhatapara | +Pollachi,Pollachi | +Mhow Cantonment,Mhow |Cantonment | +Kharagpur,Kharagpur | +Gwalior,Gwalior | +Mulbagal,Mulbagal | +Puranpur,Puranpur | +Madhya Pradesh,MP | +Shajapur,Shajapur | +Sira,Sira | +kochi,kochi | +Bheemunipatnam,Bheemunipatnam | +Jammu,Jammu | +Puliyankudi,Puliyankudi | +Jehanabad,Jehanabad | +Uchgaon,Uchgaon | +Firozpur,Firozpur | +Kasaragod,Kasaragod | +Sedam,Sedam | +Shahganj,Shahganj | +Samdhan,Samdhan | +Goalpara,Goalpara | +Mundi,Mundi | +Pulgaon,Pulgaon | +Thiruvarur,Thiruvarur | +Uran,Uran | +Davanagere,Davanagere | +Pudukkottai,Pudukkottai | +Ramdurg,Ramdurg | +Taki,Taki | +Utraula,Utraula | +Adyar,Adyar | +Mancherial,Mancherial | +Shoranur,Shoranur | +Malda,Malda | +North Lakhimpur,Lakhimpur |North Lakhimpur +Rajula,Rajula | +Visnagar,Visnagar | +Mavoor,Mavoor | +Mattannur,Mattannur | +Sironj,Sironj | +Phulera,Phulera | +Nagaon,Nagaon | +Ramanathapuram,Ramanathapuram | +Murliganj,Murliganj | +Shillong,Shillong | +Pen,Pen | +Barmer,Barmer | +Raikot,Raikot | +Makrana,Makrana | +Jalpaiguri,Jalpaiguri | +Rawatsar,Rawatsar | +Makhdumpur,Makhdumpur | +Dimapur,Dimapur | +Sujanpur,Sujanpur | +Rampur,Rampur | +Rajpipla,Rajpipla | +Waghala,Waghala | +Kendrapara,Kendrapara | +Gurgaon,Gurgaon | +Purulia,Purulia | +Ludhiana,Ludhiana | +Jodhpur,Jodhpur | +Satna,Satna | +Sindhagi,Sindhagi | +Khambhat,Khambhat | +Belonia,Belonia | +Adra,Adra | +Siana,Siana | +Nashik,Nashik | +Kunnamkulam,Kunnamkulam | +Palia Kalan,Palia |Kalan | +Sirohi,Sirohi | +udaipur,udaipur |rajasthan | +Sadulshahar,Sadulshahar | +Mangalvedhe,Mangalvedhe | +Seohara,Seohara | +Polur,Polur | +Kaithal,Kaithal | +Sawantwadi,Sawantwadi | +Bhiwandi,Bhiwandi | +Pratapgarh,Pratapgarh | +Sindagi,Sindagi | +Kalimpong,Kalimpong | +Vrindavan,Vrindavan | +Kapadvanj,Kapadvanj | +Bodhan,Bodhan | +Lonar,Lonar | +Nellore,Nellore | +Fatehabad,Fatehabad | +Mihijam,Mihijam | +Godhra,Godhra | +Hansi,Hansi | +Zirakpur,Zirakpur | +Morena,Morena | +Padmanabhapuram,Padmanabhapuram | +Usilampatti,Usilampatti | +Habra,Habra | +Shikohabad,Shikohabad | +Sandila,Sandila | +Kadapa,Kadapa | +Vijayapura,Vijayapura | +Tirunelveli,Tirunelveli | +Talwara,Talwara | +Sarni,Sarni | +Prithvipur,Prithvipur | +Rehli,Rehli | +Singrauli,Singrauli | +Thanjavur,Thanjavur | +Sausar,Sausar | +Tarn Taran,Tarn |Taran | +Thodupuzha,Thodupuzha | +Aizawl,Aizawl | +Saidpur,Saidpur | +Bhilai Nagar,Bhilai |Bhilai Nagar | +Jhansi,Jhansi | +Koyilandy,Koyilandy | +Kavali,Kavali | +Sattenapalle,Sattenapalle | +Samana,Samana | +Ramganj Mandi,Ramganj |Ramganj Mandi | +Udgir,Udgir | +Wai,Wai | +Siddipet,Siddipet | +Urmar Tanda,Urmar |Tanda | +Seoni Malwa,Seoni |Malwa | +Nangal,Nangal | +Tumsar,Tumsar | +Barbil,Barbil | +Parasi,Parasi | +Shamgarh,Shamgarh | +Bhilwara,Bhilwara | +Punalur,Punalur | +Repalle,Repalle | +Mandsaur,Mandsaur | +Porsa,Porsa | +Saiha,Saiha | +Tittakudi,Tittakudi | +Srikalahasti,Srikalahasti | +Varkala,Varkala | +Sherkot,Sherkot | +Silvassa,Silvassa | +Bhavnagar,Bhavnagar | +Arambagh,Arambagh | +Mariani,Mariani | +Suri,Suri | +Zunheboto,Zunheboto | +Sadabad,Sadabad | +Baleshwar Town,Baleshwar | | +bengaluru,bengaluru |bangalore | +Tirupati,Tirupati | +Warangal,Warangal | +Gobichettipalayam,Gobichettipalayam | +Yerraguntla,Yerraguntla | +Chikkamagaluru,Chikkamagaluru | +Sitarganj,Sitarganj | +Virudhunagar,Virudhunagar | +Tumkur,Tumkur | +Tikamgarh,Tikamgarh | +Nawada,Nawada | +chandigarh,chandigarh | +Sinnar,Sinnar | +Ballari,Ballari | +Begusarai,Begusarai | +Shujalpur,Shujalpur | +Vikramasingapuram,Vikramasingapuram | +Rajauri,Rajauri | +Dumka,Dumka | +Sailu,Sailu | +Thakurdwara,Thakurdwara | +Bhuj,Bhuj | +Sunam,Sunam | +Parvathipuram,Parvathipuram | +Sangaria,Sangaria | +Hapur,Hapur | +Udumalaipettai,Udumalaipettai | +Gobindgarh,Gobindgarh | +Osmanabad,Osmanabad | +Ramanagaram,Ramanagaram | +Silchar,Silchar | +Savarkundla,Savarkundla | +Wankaner,Wankaner | +Bargarh,Bargarh | +Fazilka,Fazilka | +Rajakhera,Rajakhera | +Chirmiri,Chirmiri | +Vaikom,Vaikom | +Samalkot,Samalkot | +Mehkar,Mehkar | +Phusro,Phusro | +Rajahmundry,Rajahmundry | +Manglaur,Manglaur | +Shahbad,Shahbad | +Neyveli,Neyveli | +Sagwara,Sagwara | +Margherita,Margherita | +Kadi,Kadi | +Sri Madhopur,Sri Madhopur |Madhopur | +Sankeshwara,Sankeshwara | +Kanhangad,Kanhangad | +Umarkhed,Umarkhed | +Ponnani,Ponnani | +Nohar,Nohar | +Rameshwaram,Rameshwaram | +Uthiramerur,Uthiramerur | +Terdal,Terdal | +Mandamarri,Mandamarri | +Tharangambadi,Tharangambadi | +Kottayam,Kottayam | +Prantij,Prantij | +Jagraon,Jagraon | +Nadbai,Nadbai | +Adalaj,Adalaj | +Panagudi,Panagudi | +Maharajganj,Maharajganj | +Araria,Araria | +Thrissur,Thrissur | +Yawal,Yawal | +Malout,Malout | +Dumraon,Dumraon | +Ramtek,Ramtek | +Sidlaghatta,Sidlaghatta | +Zira,Zira | +Rayagada,Rayagada | +Tenkasi,Tenkasi | +Bhabua,Bhabua | +Soro,Soro | +Lanka,Lanka | +Tenali,Tenali | +Mussoorie,Mussoorie | +Sahibganj,Sahibganj | +Multai,Multai | +Paravoor,Paravoor | +Paschim Punropara,Paschim Punropara |Punropara | +Chandigarh,Chandigarh | +Purquazi,Purquazi | +Gaya,Gaya | +Coal Dhanbad,Coal Dhanbad |Dhanbad | +Vijainagar Ajmer,Vijainagar |Ajmer | +Panna,Panna | +Udhampur,Udhampur | +Bilaspur,Bilaspur | +Renukoot,Renukoot | +Pinjore,Pinjore | +Palanpur,Palanpur | +Pardi,Pardi | +Talikota,Talikota | +Lunglei,Lunglei | +Pithampur,Pithampur | +Rishikesh,Rishikesh | +Mauganj,Mauganj | +Ranebennuru,Ranebennuru | +Mandawa,Mandawa | +Sikandra Rao,Sikandra |Sikandra Rao | +Vaniyambadi,Vaniyambadi | +Kolar,Kolar | +Amalner,Amalner | +Vaijapur,Vaijapur | +Vijaypur,Vijaypur | +Naila Janjgir,Naila |Janjgir | +Alirajpur,Alirajpur | +Mira Bhayandar,Mira |Bhayandar | +Rajagangapur,Rajagangapur | +Jaggaiahpet,Jaggaiahpet | +Orai,Orai | +Mandalgarh,Mandalgarh | +Roorkee,Roorkee | +Mangrol,Mangrol | +Arvi,Arvi | +Rahuri,Rahuri | +Ponnur,Ponnur | +Malaj Khand,Malaj Khand|Malajkhand | +Mysore,Mysore | +Thane,Thane | +Nedumangad,Nedumangad | +Pihani,Pihani | +Jabalpur,Jabalpur | +Sonepur,Sonepur | +Tiruppur,Tiruppur | +Manendragarh,Manendragarh | +Nandgaon,Nandgaon | +Patur,Patur | +Pakaur,Pakaur | +Suriyampalayam,Suriyampalayam | +Gudur,Gudur | +Theni Allinagaram,Theni |Allinagaram | +Tezpur,Tezpur | +Bageshwar,Bageshwar | +Peringathur,Peringathur | +Yanam,Yanam | +Ambejogai,Ambejogai | +Kailasahar,Kailasahar | +Lonavla,Lonavla | +Talaja,Talaja | +Yamunanagar,Yamunanagar | +Kharar,Kharar | +Unnao,Unnao | +Palasa Kasibugga,Palasa |Kasibugga | +Kalpi,Kalpi | +Tiruttani,Tiruttani | +Pachora,Pachora | +Dhubri,Dhubri | +Pilibanga,Pilibanga | +Sangole,Sangole | +Shanti Leh,Shanti |Leh | +Bhongir,Bhongir | +Sultanpur,Sultanpur | +Samastipur,Samastipur | +Panchla,Panchla | +Manachanallur,Manachanallur | +Umbergaon,Umbergaon | +Chirkunda,Chirkunda | +Mount Abu,Mount Abu |Abu | +Raghunathganj,Raghunathganj | +Murwara Katni,Murwara |Katni | +Mahuva,Mahuva | +Darbhanga,Darbhanga | +Nasirabad,Nasirabad | +Kodungallur,Kodungallur | +Surat,Surat | +Hoshiarpur,Hoshiarpur | +Pathankot,Pathankot | +Qadian,Qadian | +Tadepalligudem,Tadepalligudem | +Pudupattinam,Pudupattinam | +Narasaraopet,Narasaraopet | +Rudrapur,Rudrapur | +Koratla,Koratla | +Nizamabad,Nizamabad | +Monoharpur,Monoharpur | +Raisen,Raisen | +Shishgarh,Shishgarh | +Mathabhanga,Mathabhanga | +Raayachuru,Raayachuru | +Jharsuguda,Jharsuguda | +Rairangpur,Rairangpur | +Ramnagar,Ramnagar | +Sawai Madhopur,Sawai |Madhopur | +Mudhol,Mudhol | +Visakhapatnam,Visakhapatnam | +Umreth,Umreth | +Satana,Satana | +Rajpura,Rajpura | +Guruvayoor,Guruvayoor | +Agra,Agra | +Vadgaon Kasba,Vadgaon |Kasba | +Piro,Piro | +Pavagada,Pavagada | +Beed,Beed | +Muddebihal,Muddebihal | +Nalbari,Nalbari | +Pacode,Pacode | +Wara Seoni,Wara |Seoni | +Uthamapalayam,Uthamapalayam | +Adoni,Adoni | +Parangipettai,Parangipettai | +Phillaur,Phillaur | +Vadnagar,Vadnagar | +Viramgam,Viramgam | +Soron,Soron | +Kolkata,Kolkata | +Mavelikkara,Mavelikkara | +Mandi,Mandi | +Nagla,Nagla | +Narkatiaganj,Narkatiaganj | +Wadi,Wadi | +Coimbatore,Coimbatore | +Shrirangapattana,Shrirangapattana | +Rajkot,Rajkot | +Renigunta,Renigunta | +Naraura,Naraura | +Radhanpur,Radhanpur | +Samthar,Samthar | +Yevla,Yevla | +Thoubal,Thoubal | +Marmagao,Marmagao | +Amravati,Amravati | +Lakheri,Lakheri | +Rajnandgaon,Rajnandgaon | +Surandai,Surandai | +Suar,Suar | +Nagina,Nagina | +Hazaribag,Hazaribag | +Ashok Nagar,Ashok | +Anantapur,Anantapur | +Noorpur,Noorpur | +Maddur,Maddur | +PNPatti,PNPatti | +Pattamundai,Pattamundai | +Montage,Montage | +Musabani,Musabani | +Anakapalle,Anakapalle | +Raiganj,Raiganj | +Cuttack,Cuttack | +Alappuzha,Alappuzha | +Sherghati,Sherghati | +Vadakkuvalliyur,Vadakkuvalliyur | +Markapur,Markapur | +Raurkela,Raurkela | +Barpeta,Barpeta | +Maharajpur,Maharajpur | +Puttur,Puttur | +Navsari,Navsari | +Hyderabad,Hyderabad | +Pandhurna,Pandhurna | +Dharmanagar,Dharmanagar | +Ron,Ron | +Navalgund,Navalgund | +Tilda Newra,Tilda |Newra | +Sattur,Sattur | +Vishnupad Gaya,Vishnupad |Gaya | +Vatakara,Vatakara | +Sohagpur,Sohagpur | +Punch,Punch | +Magadi,Magadi | +Athni,Athni | +Barh,Barh | +Macherla,Macherla | +Dhanbad,Dhanbad | +Ratlam,Ratlam | +Sheikhpura,Sheikhpura | +Morshi,Morshi | +Firozabad,Firozabad | +Rampura Phul,Rampura |Phul | +Mhaswad,Mhaswad | +Haldwani Kathgodam,Haldwani |Kathgodam | +Farooqnagar,Farooqnagar | +Sillod,Sillod | +Supaul,Supaul | +Khammam,Khammam | +Anantnag,Anantnag | +Lilong,Lilong | +Lalsot,Lalsot | +Zaidpur,Zaidpur | +Pernampattu,Pernampattu | +Nowgong,Nowgong | +Una,Una | +Srinivaspur,Srinivaspur | +Ranchi,Ranchi | +Palani,Palani | +Songadh,Songadh | +Pune,Pune | +Punjaipugalur,Punjaipugalur | +Sheohar,Sheohar | +Shimla,Shimla | +Lalitpur,Lalitpur | +Manvi,Manvi | +Salem,Salem | +Changanassery,Changanassery | +Sihora,Sihora | +Paithan,Paithan | +Jorhat,Jorhat | +Baramula,Baramula | +patna,patna | +Panniyannur,Panniyannur | +Malkangiri,Malkangiri | +Shirpur Warwade,Shirpur |Warwade | +Nandyal,Nandyal | +Allahabad,Allahabad | +Pasighat,Pasighat | +Anjar,Anjar | +Chaibasa,Chaibasa | +Laharpur,Laharpur | +Mahad,Mahad | +Panipat,Panipat | +Sindhnur,Sindhnur | +Bellampalle,Bellampalle | +Silapathar,Silapathar | +Sikar,Sikar | +Nanpara,Nanpara | +Narayanpet,Narayanpet | +Asarganj,Asarganj | +Limbdi,Limbdi | +Sikandrabad,Sikandrabad | +Naugachhia,Naugachhia | +Tiruchendur,Tiruchendur | +Tadpatri,Tadpatri | +Bahadurgarh,Bahadurgarh | +Nilanga,Nilanga | +Ongole,Ongole | +Parbhani,Parbhani | +Pithapuram,Pithapuram | +Nimbahera,Nimbahera | +Karwar,Karwar | +Namagiripettai,Namagiripettai | +Mumbai,Mumbai |Bombay +Kapurthala,Kapurthala | +Nakur,Nakur | +Ponneri,Ponneri | +Kollam,Kollam | +indore,indore | +Ramachandrapuram,Ramachandrapuram | +Lalganj,Lalganj | +Sambalpur,Sambalpur | +Periyasemur,Periyasemur | +Sholingur,Sholingur | +Ambikapur,Ambikapur | +Madhupur,Madhupur | +Jammalamadugu,Jammalamadugu | +Sehore,Sehore | +Hubli Dharwad,Hubli |Dharwad | +Nautanwa,Nautanwa | +Raipur,Raipur | +Rae Bareli,Rae Bareli |Bareli | +Sabalgarh,Sabalgarh | +Yellandu,Yellandu | +Pilani,Pilani | +Vadalur,Vadalur | +Lakshmeshwar,Lakshmeshwar | +Dhenkanal,Dhenkanal | +Manmad,Manmad | +Medininagar Daltonganj,Medininagar |Daltonganj | +Shahjahanpur,Shahjahanpur | +Bobbili,Bobbili | +Malerkotla,Malerkotla | +Padrauna,Padrauna | +Sanand,Sanand | +Sircilla,Sircilla | +Byasanagar,Byasanagar | +Raisinghnagar,Raisinghnagar | +Tilhar,Tilhar | +Naidupet,Naidupet | +Anjangaon,Anjangaon | +Jangaon,Jangaon | +Jhargram,Jhargram | +Pachore,Pachore | +Taranagar,Taranagar | +Guntur,Guntur | +Manjlegaon,Manjlegaon | +Medak,Medak | +Mandya,Mandya | +Lar,Lar | +Kannur,Kannur | +Warora,Warora | +Longowal,Longowal | +Saharsa,Saharsa | +Nabha,Nabha | +Mukhed,Mukhed | +Suratgarh,Suratgarh | +Sullurpeta,Sullurpeta | +Mirganj,Mirganj | +Risod,Risod | +Shenkottai,Shenkottai | +Sambhar,Sambhar | +Darjiling,Darjiling | +Itarsi,Itarsi | +Gadwal,Gadwal | +Akola,Akola | +Dhoraji,Dhoraji | +Wokha,Wokha | +Latur,Latur | +Vasai,Vasai | +Neyyattinkara,Neyyattinkara | +Srivilliputhur,Srivilliputhur | +Vijayawada,Vijayawada | +Belagavi,Belagavi | +Chirala,Chirala | +Tundla,Tundla | +Jamshedpur,Jamshedpur | +Suryapet,Suryapet | +Perinthalmanna,Perinthalmanna | +Sathyamangalam,Sathyamangalam | +Siwan,Siwan | +Aligarh,Aligarh | +Raxaul Bazar,Raxaul |Raxaul Bazar | +Silao,Silao | +Lakhimpur,Lakhimpur | +koramangala,koramangala \ No newline at end of file diff --git a/postman_tests/lib/common.py b/postman_tests/lib/common.py new file mode 100644 index 000000000..7c15e8be0 --- /dev/null +++ b/postman_tests/lib/common.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import +import os + +def get_entity_name(file_path): + base=os.path.basename(file_path) + return os.path.splitext(base)[0] diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index e69de29bb..d0c8b1e1e 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import +import os +import glob +from . import common +import requests +import json +import csv + +es_api_url = "http://localhost:8081/entities/data/v1" + + +# Generate the data object +def get_variants(str): + str = str.replace(' ','') + arr = str.split('|') + return [item for item in arr if item] + + +def convert_csv_to_json(file_path): + data = {'replace': True, 'edited': []} + with open(file_path, 'r') as file: + csv_data = csv.reader(file) + next(file) # Omit header row + for row in csv_data: + record = {'word': row[0]} + record['variants'] = { + 'en': { + '_id': None, + 'value': get_variants(row[1]) + } + } + data['edited'].append(record) + return json.dumps(data) + + +def index_data(es_data_path): + for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): + entity_name = common.get_entity_name(file_path) + print(f"Indexing {entity_name}") + contents = convert_csv_to_json(file_path) + req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + print(req.text) + + +def clear_data(es_data_path): + for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): + entity_name = common.get_entity_name(file_path) + print(f"Clearing ES data for {entity_name}") + contents = convert_csv_to_json(file_path) + contents = contents.replace("\"edited\":", "\"deleted\":") + req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + print(req.text) diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index e3edcf0f9..357da3c62 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -2,6 +2,7 @@ import json import os import glob +from . import common def check_if_data_valid(entities_data_path): @@ -24,15 +25,10 @@ def check_if_data_valid(entities_data_path): return False -def get_entity_name(file_path): - base=os.path.basename(file_path) - return os.path.splitext(base)[0] - - def read_entities_data(entities_data_path): entities_data = {} for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): - entity = get_entity_name(file_path) + entity = common.get_entity_name(file_path) entities_data[entity] = [] with open(file_path, 'r') as file: data = json.load(file) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 3e266293d..9aa4caa0f 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -3,6 +3,7 @@ import subprocess import os import json +from lib import es postman_data_directory = 'postman_tests/' @@ -10,6 +11,7 @@ newman_data_path = 'data/newman_data.json' collection_data_path = 'data/ner_collection.json' environment_file_path = 'data/environment.json' +es_data_path = 'data/elastic_search/' #Switch to postman_tests if not already in that directory @@ -19,10 +21,12 @@ if newman.check_if_data_valid(entities_data_path): try: + es.index_data(es_data_path) entities_data = newman.read_entities_data(entities_data_path) newman_data = newman.generate_newman_data(entities_data) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp, indent=4) subprocess.Popen(f"newman run {collection_data_path} -d {newman_data_path} -e {environment_file_path}", shell=True).wait() + es.clear_data(es_data_path) except Exception as e: print(str(e)) From b82708b705bd852eaad6064792bfd9c0b9201bfd Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 22:28:54 +0000 Subject: [PATCH 11/78] Update Dockerfile-python3 Installed newman and its dependency nodejs. Also installed newman-reporter-html which can be used for generating html reports. --- docker/Dockerfile-python3 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile-python3 b/docker/Dockerfile-python3 index 015b519c1..ea3091a05 100644 --- a/docker/Dockerfile-python3 +++ b/docker/Dockerfile-python3 @@ -4,8 +4,12 @@ FROM python:3.6.10 RUN apt-get update && apt-get install -y wget build-essential curl nginx supervisor -WORKDIR /app +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && \ + apt-get install nodejs && \ + npm install -g newman && \ + npm install -g newman-reporter-html +WORKDIR /app COPY docker/install.sh initial_setup.py /app/ COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf @@ -41,4 +45,4 @@ EXPOSE 8081 ADD . /app # entrypoint/cmd script -CMD /app/docker/cmd.sh \ No newline at end of file +CMD /app/docker/cmd.sh From 375c0364848748e770d325b01f18d58db25deba8 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 22:53:12 +0000 Subject: [PATCH 12/78] Refactored generation of newman data --- postman_tests/lib/newman.py | 3 ++- postman_tests/run_tests.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 357da3c62..4271974bb 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -43,8 +43,9 @@ def read_entities_data(entities_data_path): return entities_data -def generate_newman_data(entities_data): +def generate_newman_data(entities_data_path): data = [] + entities_data = read_entities_data(entities_data_path) for k in entities_data: data.append(entities_data[k]) newman_data = [] diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 9aa4caa0f..4caf49a4c 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -22,11 +22,14 @@ if newman.check_if_data_valid(entities_data_path): try: es.index_data(es_data_path) - entities_data = newman.read_entities_data(entities_data_path) - newman_data = newman.generate_newman_data(entities_data) + newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp, indent=4) - subprocess.Popen(f"newman run {collection_data_path} -d {newman_data_path} -e {environment_file_path}", shell=True).wait() + newman_command = ( + f'newman run {collection_data_path} -d {newman_data_path}' + f' -e {environment_file_path}' + ) + subprocess.Popen(newman_command, shell=True).wait() es.clear_data(es_data_path) except Exception as e: print(str(e)) From 8ac63d6f832d3ac778f38b1b8e0f0e13d7d99c2f Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 23:03:25 +0000 Subject: [PATCH 13/78] Some more refactoring --- postman_tests/lib/es.py | 4 ++-- postman_tests/run_tests.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index d0c8b1e1e..89bcefd2b 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -1,10 +1,10 @@ from __future__ import absolute_import import os import glob -from . import common -import requests import json import csv +import requests +from . import common es_api_url = "http://localhost:8081/entities/data/v1" diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 4caf49a4c..8e9e6020c 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -1,8 +1,8 @@ from __future__ import absolute_import -from lib import newman import subprocess import os import json +from lib import newman from lib import es From 7dc7a380665ec02671a3c0a477753c1d01010341 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 30 Mar 2020 23:07:14 +0000 Subject: [PATCH 14/78] Fix some typos --- postman_tests/run_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 8e9e6020c..5c4a61ff6 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -6,7 +6,7 @@ from lib import es -postman_data_directory = 'postman_tests/' +postman_tests_directory = 'postman_tests/' entities_data_path = 'data/entities/' newman_data_path = 'data/newman_data.json' collection_data_path = 'data/ner_collection.json' @@ -15,8 +15,8 @@ #Switch to postman_tests if not already in that directory -if(os.path.basename(os.getcwd()) != postman_data_directory): - os.chdir(postman_data_directory) +if(os.path.basename(os.getcwd()) != postman_tests_directory): + os.chdir(postman_tests_directory) if newman.check_if_data_valid(entities_data_path): From a84843783245712e49faf26fca0122d45bfc5fe8 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 31 Mar 2020 08:25:54 +0000 Subject: [PATCH 15/78] Add shell script for running postman tests --- run_postman_tests.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 run_postman_tests.sh diff --git a/run_postman_tests.sh b/run_postman_tests.sh new file mode 100755 index 000000000..145b7597d --- /dev/null +++ b/run_postman_tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py From f5c1a5abf338dad7ac72710290be7f12bf358fd3 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 31 Mar 2020 08:52:14 +0000 Subject: [PATCH 16/78] Add documentation and doctrings --- postman_tests/Readme.md | 75 +++++++++++++++++++++++++++++++++++++ postman_tests/lib/es.py | 74 ++++++++++++++++++++++++++---------- postman_tests/lib/newman.py | 32 +++++++++++++++- 3 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 postman_tests/Readme.md diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md new file mode 100644 index 000000000..b205aee2a --- /dev/null +++ b/postman_tests/Readme.md @@ -0,0 +1,75 @@ +**Running the tests** + +``` +docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py +``` + +A shortcut for running the above is available. Just run ```./run_postman_tests.sh``` in the root directory. + + +**Adding new entities** + +To add a new entity create a new json file in postman_tests/data/entities/. + +The format should follow the below structure: + +``` +[ + "input": { + + }, + + "expected": { + + } +] +``` + +or + +``` +[ + "input": { + + }, + + "expected": { + [ + + ] + } +] +``` + +input contains the parameters that we pass as query parameters in the GET rquest. + +expected is either a single object containing parameters whose values we are testing or it can be an array of objects depending on the response. + +Add tests for the new entity using steps given below in this document and send a PR containing the new collection and data. + + +**Modifying entities** + +Modify the enitities data json file in postman_tests/data/entities and if required the tests as well using steps given below in this document and send a PR for the modifications. + + +**Adding new tests / Modifying existing ones** + +Postman tests now use the new Chaijs based BDD syntax and they have deprecated the old one. So all existing tests have been translated to use the new syntax. + +Refer to the [postman documentation](https://learning.postman.com/docs/postman/scripts/test-scripts/) for how to use the new syntax for writing tests. + +Use the below steps: + +1. First import postman_tests/data/ner_collection.json into postman + +2. After adding new tests or modifying existing ones, export the modified collection into ner_collection.json. + +3. Run the test suite using the process given in this Readme above to make sure all pass. + +4. Send a PR containing the new ner_collection.json and data. + + +**Adding data to be indexed into ElasticSearch** + +Add the csv file for the particular entity in postman_tests/data/elastic_search/ and send a PR. diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index 89bcefd2b..4979d947d 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -9,14 +9,52 @@ es_api_url = "http://localhost:8081/entities/data/v1" -# Generate the data object -def get_variants(str): - str = str.replace(' ','') - arr = str.split('|') - return [item for item in arr if item] +def index_data(es_data_path): + """ + Index data for every entity being tested into ElasticSearch + + Parameters: + es_data_path (string): Path to the data/elastic_search directory + Returns: + None + """ + for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): + entity_name = common.get_entity_name(file_path) + print(f"Indexing {entity_name}") + contents = convert_csv_to_json(file_path) + req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + print(req.text) + + +def clear_data(es_data_path): + """ + Clear the data for every entity that was indexed for testing from ElasticSearch + + Parameters: + es_data_path (string): Path to the data/elastic_search directory + + Returns: + None + """ + for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): + entity_name = common.get_entity_name(file_path) + print(f"Clearing ES data for {entity_name}") + contents = convert_csv_to_json(file_path) + contents = contents.replace("\"edited\":", "\"deleted\":") + req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + print(req.text) def convert_csv_to_json(file_path): + """ + Read the csv file at file_path and convert its data to json + + Parameters: + file_path (string): Path of a file in the data/elastic_search directory + + Returns: + string: The JSON representation of the csv data in the file + """ data = {'replace': True, 'edited': []} with open(file_path, 'r') as file: csv_data = csv.reader(file) @@ -33,20 +71,16 @@ def convert_csv_to_json(file_path): return json.dumps(data) -def index_data(es_data_path): - for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): - entity_name = common.get_entity_name(file_path) - print(f"Indexing {entity_name}") - contents = convert_csv_to_json(file_path) - req = requests.post(f"{es_api_url}/{entity_name}", data=contents) - print(req.text) +def get_variants(str): + """ + Convert the string containing all the variants into a list + Parameters: + str (string): String containing all the variant names. -def clear_data(es_data_path): - for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): - entity_name = common.get_entity_name(file_path) - print(f"Clearing ES data for {entity_name}") - contents = convert_csv_to_json(file_path) - contents = contents.replace("\"edited\":", "\"deleted\":") - req = requests.post(f"{es_api_url}/{entity_name}", data=contents) - print(req.text) + Returns: + list: List of strings where each string is a variant name. + """ + str = str.replace(' ','') + arr = str.split('|') + return [item for item in arr if item] diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 4271974bb..0514588ed 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -6,8 +6,16 @@ def check_if_data_valid(entities_data_path): - # Doing basic sanity checking here. - # Check that every test case is valid json and has the keys: input and expected + """ + Doing basic sanity checking here. + Check that every test case is valid json and has the keys: input and expected + + Parameters: + entities_data_path (string): Path to the data/entities directory + + Returns: + boolean: Returns True if all files contain valid json. + """ try: path = '' for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): @@ -26,6 +34,16 @@ def check_if_data_valid(entities_data_path): def read_entities_data(entities_data_path): + """ + Read all the files in data/entities and generate a single dictionary containing + data for every entity. + + Parameters: + entities_data_path (string): Path to the data/entities directory. + + Returns: + dictionary: Dict containing data read from the data files for every entity. + """ entities_data = {} for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): entity = common.get_entity_name(file_path) @@ -44,6 +62,16 @@ def read_entities_data(entities_data_path): def generate_newman_data(entities_data_path): + """ + Generate data in a format that can be passed to the command-line tool + newman for runing the tests. + + Parameters: + entities_data_path (string): Path to the data/entities directory. + + Returns: + list: List of dictionaries where each dictionary is data for a single test iteration. + """ data = [] entities_data = read_entities_data(entities_data_path) for k in entities_data: From 63517d0d4b248f8c8d6ef1f16d71a310127d3453 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 31 Mar 2020 09:16:21 +0000 Subject: [PATCH 17/78] Fix lint errors --- postman_tests/lib/common.py | 3 ++- postman_tests/lib/es.py | 24 +++++++++++++----------- postman_tests/lib/newman.py | 14 +++++++------- postman_tests/run_tests.py | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/postman_tests/lib/common.py b/postman_tests/lib/common.py index 7c15e8be0..e59c9efed 100644 --- a/postman_tests/lib/common.py +++ b/postman_tests/lib/common.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import os + def get_entity_name(file_path): - base=os.path.basename(file_path) + base = os.path.basename(file_path) return os.path.splitext(base)[0] diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index 4979d947d..4aba504e3 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -6,6 +6,7 @@ import requests from . import common + es_api_url = "http://localhost:8081/entities/data/v1" @@ -13,10 +14,10 @@ def index_data(es_data_path): """ Index data for every entity being tested into ElasticSearch - Parameters: + Parameters: es_data_path (string): Path to the data/elastic_search directory - Returns: + Returns: None """ for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): @@ -31,10 +32,10 @@ def clear_data(es_data_path): """ Clear the data for every entity that was indexed for testing from ElasticSearch - Parameters: - es_data_path (string): Path to the data/elastic_search directory + Parameters: + es_data_path (string): Path to the data/elastic_search directory. - Returns: + Returns: None """ for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): @@ -45,20 +46,21 @@ def clear_data(es_data_path): req = requests.post(f"{es_api_url}/{entity_name}", data=contents) print(req.text) + def convert_csv_to_json(file_path): """ Read the csv file at file_path and convert its data to json - Parameters: + Parameters: file_path (string): Path of a file in the data/elastic_search directory - Returns: + Returns: string: The JSON representation of the csv data in the file """ data = {'replace': True, 'edited': []} with open(file_path, 'r') as file: csv_data = csv.reader(file) - next(file) # Omit header row + next(file) # Omit header row for row in csv_data: record = {'word': row[0]} record['variants'] = { @@ -75,12 +77,12 @@ def get_variants(str): """ Convert the string containing all the variants into a list - Parameters: + Parameters: str (string): String containing all the variant names. - Returns: + Returns: list: List of strings where each string is a variant name. """ - str = str.replace(' ','') + str = str.replace(' ', '') arr = str.split('|') return [item for item in arr if item] diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 0514588ed..7f157d365 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -10,10 +10,10 @@ def check_if_data_valid(entities_data_path): Doing basic sanity checking here. Check that every test case is valid json and has the keys: input and expected - Parameters: + Parameters: entities_data_path (string): Path to the data/entities directory - Returns: + Returns: boolean: Returns True if all files contain valid json. """ try: @@ -38,10 +38,10 @@ def read_entities_data(entities_data_path): Read all the files in data/entities and generate a single dictionary containing data for every entity. - Parameters: + Parameters: entities_data_path (string): Path to the data/entities directory. - Returns: + Returns: dictionary: Dict containing data read from the data files for every entity. """ entities_data = {} @@ -66,10 +66,10 @@ def generate_newman_data(entities_data_path): Generate data in a format that can be passed to the command-line tool newman for runing the tests. - Parameters: + Parameters: entities_data_path (string): Path to the data/entities directory. - Returns: + Returns: list: List of dictionaries where each dictionary is data for a single test iteration. """ data = [] @@ -84,7 +84,7 @@ def generate_newman_data(entities_data_path): if item: iteration_data.append(item.pop(0)) found = True - if found == False: + if not found: break else: temp = {} diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 5c4a61ff6..1f842be69 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -14,7 +14,7 @@ es_data_path = 'data/elastic_search/' -#Switch to postman_tests if not already in that directory +# Switch to postman_tests if not already in that directory if(os.path.basename(os.getcwd()) != postman_tests_directory): os.chdir(postman_tests_directory) From 082e07ad547e09151ad94fea8a4fd7562f25043c Mon Sep 17 00:00:00 2001 From: Raza Sayed <50265358+razasayed@users.noreply.github.com> Date: Wed, 1 Apr 2020 16:31:22 +0530 Subject: [PATCH 18/78] Update Readme.md --- postman_tests/Readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index b205aee2a..ec43d5ade 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -7,9 +7,9 @@ docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py A shortcut for running the above is available. Just run ```./run_postman_tests.sh``` in the root directory. -**Adding new entities** +**Adding test data for new entities** -To add a new entity create a new json file in postman_tests/data/entities/. +To add test data for a new entity create a new json file in postman_tests/data/entities/. The format should follow the below structure: @@ -48,7 +48,7 @@ expected is either a single object containing parameters whose values we are tes Add tests for the new entity using steps given below in this document and send a PR containing the new collection and data. -**Modifying entities** +**Modifying test data of existing entities** Modify the enitities data json file in postman_tests/data/entities and if required the tests as well using steps given below in this document and send a PR for the modifications. From 82302e5876d16fb7bafaccdacdd5236734a2ff00 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 2 Apr 2020 07:32:01 +0000 Subject: [PATCH 19/78] Added test case for time entity --- postman_tests/data/entities/time.json | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/postman_tests/data/entities/time.json b/postman_tests/data/entities/time.json index dd5df8282..af7770ffa 100644 --- a/postman_tests/data/entities/time.json +++ b/postman_tests/data/entities/time.json @@ -56,5 +56,34 @@ "language": "en" } ] + }, + { + "input": { + "message": "Hritik arrived at the bus stop at 11:35 hrs expecting the bus to be there in 30 mins.But the bus was scheduled for 11:30 am", + "entity_name": "time" + }, + "expected": [ + { + "original_text": "11:30 am", + "mm": 30, + "hh": 11, + "nn": "am", + "language": "en" + }, + { + "original_text": "in 30 mins", + "mm": 30, + "hh": 0, + "nn": "df", + "language": "en" + }, + { + "original_text": "11:35", + "mm": 35, + "hh": 11, + "nn": "am", + "language": "en" + } + ] } ] From d48461a95daee1be42fc95cb151d06ee93e3eb05 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 2 Apr 2020 16:24:28 +0530 Subject: [PATCH 20/78] Fix typo --- postman_tests/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 1f842be69..30327f22c 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -6,7 +6,7 @@ from lib import es -postman_tests_directory = 'postman_tests/' +postman_tests_directory = 'postman_tests' entities_data_path = 'data/entities/' newman_data_path = 'data/newman_data.json' collection_data_path = 'data/ner_collection.json' From aec2482c26bc29ca20b58c062b91b8699b914927 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 2 Apr 2020 19:42:24 +0530 Subject: [PATCH 21/78] Add data for date V2 entity --- postman_tests/data/entities/dateV2.json | 241 ++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 postman_tests/data/entities/dateV2.json diff --git a/postman_tests/data/entities/dateV2.json b/postman_tests/data/entities/dateV2.json new file mode 100644 index 000000000..068dfcd94 --- /dev/null +++ b/postman_tests/data/entities/dateV2.json @@ -0,0 +1,241 @@ +[ + { + "expected": [ + { + "original_text": "3/3/1992", + "end_range": false, + "from": false, + "mm": 3, + "dd": 3, + "yy": 1992, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "my anniversary was on 3/3/1992", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "3rd aug 20", + "end_range": false, + "from": false, + "mm": 8, + "dd": 3, + "yy": 2020, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "Coronoa Virus will end on 3rd Aug 20", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "12.03.2016", + "end_range": false, + "from": false, + "mm": 3, + "dd": 12, + "yy": 2016, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "12.03.2016 is my nephew's birthday", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "12.4.2016", + "end_range": false, + "from": false, + "mm": 4, + "dd": 12, + "yy": 2016, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "12.4.2016 doesnt exist for me", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "3.3.12", + "end_range": false, + "from": false, + "mm": 3, + "dd": 3, + "yy": 2012, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "A date i wont forget is 3.3.12", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "3/2/17", + "end_range": false, + "from": false, + "mm": 2, + "dd": 3, + "yy": 2017, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "3/2/17 changed my life forever", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "12/12/12", + "end_range": false, + "from": false, + "mm": 12, + "dd": 12, + "yy": 2012, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "12/12/12 is a strange date isnt it ?", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "12-30-12", + "end_range": false, + "from": false, + "mm": 12, + "dd": 30, + "yy": 2012, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "we got married on 12-30-12", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "12/12", + "end_range": false, + "from": false, + "mm": 12, + "dd": 12, + "yy": 2020, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "12/12 is a bad day in american history", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "october 2nd", + "end_range": false, + "from": false, + "mm": 10, + "dd": 2, + "yy": 2020, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "Gandhi Jayanti is on October 2nd", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "2019 may 21st", + "end_range": false, + "from": false, + "mm": 5, + "dd": 21, + "yy": 2019, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "2019 May 21st", + "entity_name": "date" + } + }, + { + "expected": [ + { + "original_text": "2/3/2020", + "end_range": false, + "from": false, + "mm": 3, + "dd": 2, + "yy": 2020, + "to": false, + "start_range": true, + "type": "date" + }, + { + "original_text": "5/6/2024", + "end_range": true, + "from": false, + "mm": 6, + "dd": 5, + "yy": 2024, + "to": false, + "start_range": false, + "type": "date" + } + ], + "input": { + "message": "My meeting is 2/3/2020 to 5/6/2024", + "entity_name": "date" + } + } +] From becd8e248be5faa7c2a9a70925c381db8fd5461e Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 2 Apr 2020 16:27:32 +0530 Subject: [PATCH 22/78] Fix input data format --- postman_tests/Readme.md | 24 +- postman_tests/data/entities/budget.json | 76 ++--- postman_tests/data/entities/city.json | 80 ++--- postman_tests/data/entities/date.json | 82 +++--- postman_tests/data/entities/email.json | 70 +++-- postman_tests/data/entities/number_range.json | 70 +++-- postman_tests/data/entities/person_name.json | 88 +++--- postman_tests/data/entities/phoneV1.json | 76 ++--- postman_tests/data/entities/phoneV2.json | 76 ++--- postman_tests/data/entities/pnr.json | 70 +++-- postman_tests/data/entities/regex.json | 70 +++-- postman_tests/data/ner_collection.json | 274 +++++++++++++++--- 12 files changed, 642 insertions(+), 414 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index ec43d5ade..9097ad7c1 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -19,31 +19,17 @@ The format should follow the below structure: }, - "expected": { + "expected": [ + { - } -] -``` - -or - -``` -[ - "input": { - - }, - - "expected": { - [ - - ] - } + } + ] ] ``` input contains the parameters that we pass as query parameters in the GET rquest. -expected is either a single object containing parameters whose values we are testing or it can be an array of objects depending on the response. +expected is an array of objects that we get in the response. Add tests for the new entity using steps given below in this document and send a PR containing the new collection and data. diff --git a/postman_tests/data/entities/budget.json b/postman_tests/data/entities/budget.json index 954c2174c..4184961d0 100644 --- a/postman_tests/data/entities/budget.json +++ b/postman_tests/data/entities/budget.json @@ -1,38 +1,44 @@ [ - { - "input": { - "message": "shirts between 2000 to 3000", - "entity_name": "budget" - }, - "expected": { - "original_text": "2000 to 3000", - "max_budget": 3000, - "type": "normal_budget", - "min_budget": 2000 - } + { + "input": { + "message": "shirts between 2000 to 3000", + "entity_name": "budget" }, - { - "input": { - "message": "I want to see jeans between 2500 to 4200", - "entity_name": "budget" - }, - "expected": { - "original_text": "2500 to 4200", - "max_budget": 4200, - "type": "normal_budget", - "min_budget": 2500 - } + "expected": [ + { + "original_text": "2000 to 3000", + "max_budget": 3000, + "type": "normal_budget", + "min_budget": 2000 + } + ] + }, + { + "input": { + "message": "I want to see jeans between 2500 to 4200", + "entity_name": "budget" }, - { - "input": { - "message": "formals between 5000 to 9999", - "entity_name": "budget" - }, - "expected": { - "original_text": "5000 to 9999", - "max_budget": 9999, - "type": "normal_budget", - "min_budget": 5000 - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "2500 to 4200", + "max_budget": 4200, + "type": "normal_budget", + "min_budget": 2500 + } + ] + }, + { + "input": { + "message": "formals between 5000 to 9999", + "entity_name": "budget" + }, + "expected": [ + { + "original_text": "5000 to 9999", + "max_budget": 9999, + "type": "normal_budget", + "min_budget": 5000 + } + ] + } +] diff --git a/postman_tests/data/entities/city.json b/postman_tests/data/entities/city.json index cc2a5a735..838e5b3fa 100644 --- a/postman_tests/data/entities/city.json +++ b/postman_tests/data/entities/city.json @@ -1,41 +1,47 @@ [ - { - "input": { - "message": "i want to go to mumbai", - "entity_name": "city_list" - }, - "expected": { - "original_text": "mumbai", - "to": true, - "from": false, - "value": "mumbai", - "normal": false - } + { + "input": { + "message": "i want to go to mumbai", + "entity_name": "city_list" }, - { - "input": { - "message": "i want to go to delhi", - "entity_name": "city_list" - }, - "expected": { - "original_text": "delhi", - "to": true, - "from": false, - "value": "New Delhi", - "normal": false - } + "expected": [ + { + "original_text": "mumbai", + "to": true, + "from": false, + "value": "mumbai", + "normal": false + } + ] + }, + { + "input": { + "message": "i want to go to delhi", + "entity_name": "city_list" }, - { - "input": { - "message": "i want to go to chennai", - "entity_name": "city_list" - }, - "expected": { - "original_text": "chennai", - "to": true, - "from": false, - "value": "Chennai", - "normal": false - } - } + "expected": [ + { + "original_text": "delhi", + "to": true, + "from": false, + "value": "New Delhi", + "normal": false + } + ] + }, + { + "input": { + "message": "i want to go to chennai", + "entity_name": "city_list" + }, + "expected": [ + { + "original_text": "chennai", + "to": true, + "from": false, + "value": "Chennai", + "normal": false + } + ] + } ] diff --git a/postman_tests/data/entities/date.json b/postman_tests/data/entities/date.json index 784870d09..a127f6b0d 100644 --- a/postman_tests/data/entities/date.json +++ b/postman_tests/data/entities/date.json @@ -1,41 +1,47 @@ [ - { - "input": { - "message": "Set me a reminder for 23 December", - "entity_name": "date" - }, - "expected": { - "original_text": "23 december", - "type": "date", - "dd": 23, - "mm": 12, - "yy": 2020 - } + { + "input": { + "message": "Set me a reminder for 23 December", + "entity_name": "date" }, - { - "input": { - "message": "Set me a reminder for 2 May", - "entity_name": "date" - }, - "expected": { - "original_text": "2 may", - "type": "date", - "dd": 2, - "mm": 5, - "yy": 2020 - } + "expected": [ + { + "original_text": "23 december", + "type": "date", + "dd": 23, + "mm": 12, + "yy": 2020 + } + ] + }, + { + "input": { + "message": "Set me a reminder for 2 May", + "entity_name": "date" }, - { - "input": { - "message": "Set me a reminder for 3 June", - "entity_name": "date" - }, - "expected": { - "original_text": "3 june", - "type": "date", - "dd": 3, - "mm": 6, - "yy": 2020 - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "2 may", + "type": "date", + "dd": 2, + "mm": 5, + "yy": 2020 + } + ] + }, + { + "input": { + "message": "Set me a reminder for 3 June", + "entity_name": "date" + }, + "expected": [ + { + "original_text": "3 june", + "type": "date", + "dd": 3, + "mm": 6, + "yy": 2020 + } + ] + } +] diff --git a/postman_tests/data/entities/email.json b/postman_tests/data/entities/email.json index 294eb5139..0f50a8689 100644 --- a/postman_tests/data/entities/email.json +++ b/postman_tests/data/entities/email.json @@ -1,35 +1,41 @@ [ - { - "input": { - "message": "my email id is apurv.nagvenkar@gmail.com", - "entity_name": "email" - }, - "expected": { - "original_text": "apurv.nagvenkar@gmail.com", - "value": "apurv.nagvenkar@gmail.com", - "language": "en" - } + { + "input": { + "message": "my email id is apurv.nagvenkar@gmail.com", + "entity_name": "email" }, - { - "input": { - "message": "my email id is ashutosh@haptik.co", - "entity_name": "email" - }, - "expected": { - "original_text": "ashutosh@haptik.co", - "value": "ashutosh@haptik.co", - "language": "en" - } + "expected": [ + { + "original_text": "apurv.nagvenkar@gmail.com", + "value": "apurv.nagvenkar@gmail.com", + "language": "en" + } + ] + }, + { + "input": { + "message": "my email id is ashutosh@haptik.co", + "entity_name": "email" }, - { - "input": { - "message": "my email id is amansrivastava94@gmail.com", - "entity_name": "email" - }, - "expected": { - "original_text": "amansrivastava94@gmail.com", - "value": "amansrivastava94@gmail.com", - "language": "en" - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "ashutosh@haptik.co", + "value": "ashutosh@haptik.co", + "language": "en" + } + ] + }, + { + "input": { + "message": "my email id is amansrivastava94@gmail.com", + "entity_name": "email" + }, + "expected": [ + { + "original_text": "amansrivastava94@gmail.com", + "value": "amansrivastava94@gmail.com", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/number_range.json b/postman_tests/data/entities/number_range.json index 47dfd334a..63b2e1b34 100644 --- a/postman_tests/data/entities/number_range.json +++ b/postman_tests/data/entities/number_range.json @@ -1,35 +1,41 @@ [ - { - "input": { - "message": "Give me a number between 1 and 100", - "entity_name": "number_range" - }, - "expected": { - "original_text": "between 1 and 100", - "min_value": "1", - "max_value": "100" - } + { + "input": { + "message": "Give me a number between 1 and 100", + "entity_name": "number_range" }, - { - "input": { - "message": "My monthly salary will be more than 2k per month", - "entity_name": "number_range" - }, - "expected": { - "original_text": "more than 2k", - "min_value": "2000", - "max_value": null - } + "expected": [ + { + "original_text": "between 1 and 100", + "min_value": "1", + "max_value": "100" + } + ] + }, + { + "input": { + "message": "My monthly salary will be more than 2k per month", + "entity_name": "number_range" }, - { - "input": { - "message": "more than 200", - "entity_name": "number_range" - }, - "expected": { - "original_text": "more than 200", - "min_value": "200", - "max_value": null - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "more than 2k", + "min_value": "2000", + "max_value": null + } + ] + }, + { + "input": { + "message": "more than 200", + "entity_name": "number_range" + }, + "expected": [ + { + "original_text": "more than 200", + "min_value": "200", + "max_value": null + } + ] + } +] diff --git a/postman_tests/data/entities/person_name.json b/postman_tests/data/entities/person_name.json index 082a0e9d7..c19650a85 100644 --- a/postman_tests/data/entities/person_name.json +++ b/postman_tests/data/entities/person_name.json @@ -1,44 +1,50 @@ [ - { - "input": { - "message": "my name is yash doshi", - "entity_name": "person_name" - }, - "expected": { - "original_text": "yash doshi", - "first_name": "yash", - "last_name": "doshi", - "middle_name": null, - "model_verified": false, - "datastore_verified": true - } + { + "input": { + "message": "my name is yash doshi", + "entity_name": "person_name" }, - { - "input": { - "message": "my name is Deep Viral Baweja", - "entity_name": "person_name" - }, - "expected": { - "original_text": "Deep Viral Baweja", - "first_name": "Deep", - "last_name": "Baweja", - "middle_name": "Viral", - "model_verified": false, - "datastore_verified": true - } + "expected": [ + { + "original_text": "yash doshi", + "first_name": "yash", + "last_name": "doshi", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + } + ] + }, + { + "input": { + "message": "my name is Deep Viral Baweja", + "entity_name": "person_name" }, - { - "input": { - "message": "His name is amaan srivastava", - "entity_name": "person_name" - }, - "expected": { - "original_text": "amaan srivastava", - "first_name": "amaan", - "last_name": "srivastava", - "middle_name": null, - "model_verified": false, - "datastore_verified": true - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "Deep Viral Baweja", + "first_name": "Deep", + "last_name": "Baweja", + "middle_name": "Viral", + "model_verified": false, + "datastore_verified": true + } + ] + }, + { + "input": { + "message": "His name is amaan srivastava", + "entity_name": "person_name" + }, + "expected": [ + { + "original_text": "amaan srivastava", + "first_name": "amaan", + "last_name": "srivastava", + "middle_name": null, + "model_verified": false, + "datastore_verified": true + } + ] + } +] diff --git a/postman_tests/data/entities/phoneV1.json b/postman_tests/data/entities/phoneV1.json index 7329c70ef..9777cc357 100644 --- a/postman_tests/data/entities/phoneV1.json +++ b/postman_tests/data/entities/phoneV1.json @@ -1,38 +1,44 @@ [ - { - "input": { - "message": "my contact number is 9049961794", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "9049961794", - "country_calling_code": "91", - "value": "9049961794", - "language": "en" - } + { + "input": { + "message": "my contact number is 9049961794", + "entity_name": "phone_number" }, - { - "input": { - "message": "My phone number would be 9930341387", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "9930341387", - "country_calling_code": "91", - "value": "9930341387", - "language": "en" - } + "expected": [ + { + "original_text": "9049961794", + "country_calling_code": "91", + "value": "9049961794", + "language": "en" + } + ] + }, + { + "input": { + "message": "My phone number would be 9930341387", + "entity_name": "phone_number" }, - { - "input": { - "message": "You can call me on +919920231234", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "919920231234", - "country_calling_code": "91", - "value": "919920231234", - "language": "en" - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + } + ] + }, + { + "input": { + "message": "You can call me on +919920231234", + "entity_name": "phone_number" + }, + "expected": [ + { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "919920231234", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/phoneV2.json b/postman_tests/data/entities/phoneV2.json index 4e4d6a0bc..abe901189 100644 --- a/postman_tests/data/entities/phoneV2.json +++ b/postman_tests/data/entities/phoneV2.json @@ -1,38 +1,44 @@ [ - { - "input": { - "message": "my contact number is 08877665543", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "08877665543", - "country_calling_code": "91", - "value": "8877665543", - "language": "en" - } + { + "input": { + "message": "my contact number is 08877665543", + "entity_name": "phone_number" }, - { - "input": { - "message": "My phone number would be 9930341387", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "9930341387", - "country_calling_code": "91", - "value": "9930341387", - "language": "en" - } + "expected": [ + { + "original_text": "08877665543", + "country_calling_code": "91", + "value": "8877665543", + "language": "en" + } + ] + }, + { + "input": { + "message": "My phone number would be 9930341387", + "entity_name": "phone_number" }, - { - "input": { - "message": "You can call me on +919920231234", - "entity_name": "phone_number" - }, - "expected": { - "original_text": "919920231234", - "country_calling_code": "91", - "value": "9920231234", - "language": "en" - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "9930341387", + "country_calling_code": "91", + "value": "9930341387", + "language": "en" + } + ] + }, + { + "input": { + "message": "You can call me on +919920231234", + "entity_name": "phone_number" + }, + "expected": [ + { + "original_text": "919920231234", + "country_calling_code": "91", + "value": "9920231234", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/pnr.json b/postman_tests/data/entities/pnr.json index 12f5c31e4..1c15a6c47 100644 --- a/postman_tests/data/entities/pnr.json +++ b/postman_tests/data/entities/pnr.json @@ -1,35 +1,41 @@ [ - { - "input": { - "message": "check my pnr status for 2141215305.", - "entity_name": "pnr" - }, - "expected": { - "original_text": "2141215305", - "value": "2141215305", - "language": "en" - } + { + "input": { + "message": "check my pnr status for 2141215305.", + "entity_name": "pnr" }, - { - "input": { - "message": "check my pnr status for 3714578.", - "entity_name": "pnr" - }, - "expected": { - "original_text": "3714578", - "value": "3714578", - "language": "en" - } + "expected": [ + { + "original_text": "2141215305", + "value": "2141215305", + "language": "en" + } + ] + }, + { + "input": { + "message": "check my pnr status for 3714578.", + "entity_name": "pnr" }, - { - "input": { - "message": "check my pnr status for 11234456.", - "entity_name": "pnr" - }, - "expected": { - "original_text": "11234456", - "value": "11234456", - "language": "en" - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "3714578", + "value": "3714578", + "language": "en" + } + ] + }, + { + "input": { + "message": "check my pnr status for 11234456.", + "entity_name": "pnr" + }, + "expected": [ + { + "original_text": "11234456", + "value": "11234456", + "language": "en" + } + ] + } +] diff --git a/postman_tests/data/entities/regex.json b/postman_tests/data/entities/regex.json index b31fdfda4..a3e294a2e 100644 --- a/postman_tests/data/entities/regex.json +++ b/postman_tests/data/entities/regex.json @@ -1,36 +1,42 @@ [ - { - "input": { - "message": "123456 is my otp", - "entity_name": "regex", - "regex": "\\d{4,6}" - }, + { + "input": { + "message": "123456 is my otp", + "entity_name": "regex", + "regex": "\\d{4,6}" + }, - "expected": { - "original_text": "123456", - "value": "123456" - } + "expected": [ + { + "original_text": "123456", + "value": "123456" + } + ] + }, + { + "input": { + "message": "798865 is my otp", + "entity_name": "regex", + "regex": "\\d{4,6}" }, - { - "input": { - "message": "798865 is my otp", - "entity_name": "regex", - "regex": "\\d{4,6}" - }, - "expected": { - "original_text": "798865", - "value": "798865" - } + "expected": [ + { + "original_text": "798865", + "value": "798865" + } + ] + }, + { + "input": { + "message": "my otp is 112233", + "entity_name": "regex", + "regex": "\\d{4,6}" }, - { - "input": { - "message": "my otp is 112233", - "entity_name": "regex", - "regex": "\\d{4,6}" - }, - "expected": { - "original_text": "112233", - "value": "112233" - } - } -] \ No newline at end of file + "expected": [ + { + "original_text": "112233", + "value": "112233" + } + ] + } +] diff --git a/postman_tests/data/ner_collection.json b/postman_tests/data/ner_collection.json index faceb3a39..96de52db3 100644 --- a/postman_tests/data/ner_collection.json +++ b/postman_tests/data/ner_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "94050ad5-c8a5-49ba-b8b6-ba19c4f0d2ea", + "_postman_id": "6d097f7b-2fbf-4295-853b-3592a5553dd7", "name": "ner_collection", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -20,32 +20,42 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get city data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.to).to.eql(data.city_expected.to); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.city_expected[i].to);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.from).to.eql(data.city_expected.from); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.city_expected[i].from);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.city_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.city_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value.toLowerCase()).to.eql(data.city_expected.value.toLowerCase()); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.city_expected[i].value.toLowerCase());", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.normal).to.eql(data.city_expected.normal); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.city_expected[i].normal);", + " }", "});" ], "type": "text/javascript" @@ -173,37 +183,49 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get person name data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.person_name_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.person_name_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid first_name\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.first_name).to.eql(data.person_name_expected.first_name); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.first_name).to.eql(data.person_name_expected[i].first_name);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid last_name\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.last_name).to.eql(data.person_name_expected.last_name); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.last_name).to.eql(data.person_name_expected[i].last_name);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid middle_name\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.middle_name).to.eql(data.person_name_expected.middle_name); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.middle_name).to.eql(data.person_name_expected[i].middle_name);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid model_verified\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.model_verified).to.eql(data.person_name_expected.model_verified); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.model_verified).to.eql(data.person_name_expected[i].model_verified);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid datastore_verified\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.datastore_verified).to.eql(data.person_name_expected.datastore_verified); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.datastore_verified).to.eql(data.person_name_expected[i].datastore_verified);", + " }", "});" ], "type": "text/javascript" @@ -253,17 +275,21 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get pnr data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.pnr_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.pnr_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.pnr_expected.value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.pnr_expected[i].value);", + " }", "});" ], "type": "text/javascript" @@ -398,17 +424,21 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get regex data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.regex_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.regex_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.regex_expected.value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.regex_expected[i].value);", + " }", "});" ], "type": "text/javascript" @@ -462,17 +492,22 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get email data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.email_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.email_expected[i].original_text);", + " }", + " ", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.email_expected.value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.email_expected[i].value); ", + " }", "});" ], "type": "text/javascript" @@ -522,27 +557,35 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get budget data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.budget_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.budget_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_budget\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.max_budget).to.eql(data.budget_expected.max_budget); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.max_budget).to.eql(data.budget_expected[i].max_budget);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_budget\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.min_budget).to.eql(data.budget_expected.min_budget); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.min_budget).to.eql(data.budget_expected[i].min_budget);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.type).to.eql(data.budget_expected.type); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.type).to.eql(data.budget_expected[i].type);", + " }", "});" ], "type": "text/javascript" @@ -592,32 +635,42 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.date_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.date_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value.type).to.eql(data.date_expected.type); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.date_expected[i].type);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value.dd).to.eql(data.date_expected.dd); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.date_expected[i].dd);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value.mm).to.eql(data.date_expected.mm); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.date_expected[i].mm);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value.yy).to.eql(data.date_expected.yy); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.date_expected[i].yy);", + " }", "});" ], "type": "text/javascript" @@ -739,22 +792,28 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number_range data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.number_range_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.number_range_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.min_value).to.eql(data.number_range_expected.min_value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.min_value).to.eql(data.number_range_expected[i].min_value);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.max_value).to.eql(data.number_range_expected.max_value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.max_value).to.eql(data.number_range_expected[i].max_value);", + " }", "});" ], "type": "text/javascript" @@ -790,7 +849,7 @@ "response": [] }, { - "name": "Phone number entity v2", + "name": "Phone number entity V2", "event": [ { "listen": "test", @@ -804,22 +863,28 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.phoneV2_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV2_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.phoneV2_expected.value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV2_expected[i].value);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid country_calling_code\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.country_calling_code).to.eql(data.phoneV2_expected.country_calling_code); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.country_calling_code).to.eql(data.phoneV2_expected[i].country_calling_code);", + " }", "});" ], "type": "text/javascript" @@ -869,17 +934,21 @@ "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.eql(1);", + " pm.expect(pm.response.json().data.length).to.be.above(0);", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].original_text).to.eql(data.phoneV1_expected.original_text); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV1_expected[i].original_text);", + " }", "});", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.data[0].entity_value.value).to.eql(data.phoneV1_expected.value); ", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV1_expected[i].value);", + " }", "});" ], "type": "text/javascript" @@ -913,6 +982,119 @@ } }, "response": [] + }, + { + "name": "Date Entity V2", + "event": [ + { + "listen": "test", + "script": { + "id": "740b024e-dd31-40e6-8232-d5f6cc314688", + "exec": [ + "var shouldBeSkipped = !('dateV2_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.dateV2_expected[i].original_text);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.dateV2_expected[i].type);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.dateV2_expected[i].dd);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.dateV2_expected[i].mm);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.dateV2_expected[i].yy);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.dateV2_expected[i].from);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.dateV2_expected[i].to);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid start_range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.start_range).to.eql(data.dateV2_expected[i].start_range);", + " }", + "});", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid end_range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.end_range).to.eql(data.dateV2_expected[i].end_range);", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v2/date/?message={{dateV2_message}}&entity_name={{dateV2_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v2", + "date", + "" + ], + "query": [ + { + "key": "message", + "value": "{{dateV2_message}}" + }, + { + "key": "entity_name", + "value": "{{dateV2_entity_name}}" + } + ] + } + }, + "response": [] } ], "protocolProfileBehavior": {} From 35f752347b5df607ba5449f80d82de1d7053c3dc Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 2 Apr 2020 19:44:29 +0530 Subject: [PATCH 23/78] Using newman-reporter-htmlextra insted of newman-reporter-html --- docker/Dockerfile-python3 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile-python3 b/docker/Dockerfile-python3 index ea3091a05..85195cfaa 100644 --- a/docker/Dockerfile-python3 +++ b/docker/Dockerfile-python3 @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y wget build-essential curl nginx supervi RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && \ apt-get install nodejs && \ npm install -g newman && \ - npm install -g newman-reporter-html + npm install -g newman-reporter-htmlextra WORKDIR /app From 27cfd70120394dee273594206df4882f1ea883e8 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 3 Apr 2020 00:02:32 +0530 Subject: [PATCH 24/78] Add newman directory and dev.json to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6e873bd18..f4ae0e9fd 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,5 @@ logs/ .vscode newman_data.json +newman/ +dev.json From ae7fa3c6ca31806d76c48dc07a526153893ba41e Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 3 Apr 2020 00:12:01 +0530 Subject: [PATCH 25/78] Renamed environment.json to prod.json --- postman_tests/data/{environment.json => prod.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename postman_tests/data/{environment.json => prod.json} (100%) diff --git a/postman_tests/data/environment.json b/postman_tests/data/prod.json similarity index 100% rename from postman_tests/data/environment.json rename to postman_tests/data/prod.json From 75f06c716ec5ecb66d52e34b1ea6ea7e0eec29ba Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 3 Apr 2020 00:19:19 +0530 Subject: [PATCH 26/78] Move prod.json to config dir --- postman_tests/{data => config}/prod.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename postman_tests/{data => config}/prod.json (100%) diff --git a/postman_tests/data/prod.json b/postman_tests/config/prod.json similarity index 100% rename from postman_tests/data/prod.json rename to postman_tests/config/prod.json From 1a5601f17b70e0249899c4af9722e5d24e073c3d Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 3 Apr 2020 01:03:08 +0530 Subject: [PATCH 27/78] Created and using config for dev and prod --- postman_tests/config/prod.json | 5 +++-- postman_tests/lib/es.py | 24 +++++++++++++++++------- postman_tests/run_tests.py | 29 ++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/postman_tests/config/prod.json b/postman_tests/config/prod.json index 6cbbec04e..3d5132c5f 100644 --- a/postman_tests/config/prod.json +++ b/postman_tests/config/prod.json @@ -1,6 +1,6 @@ { "id": "9dbcc6c6-dd15-498a-a4f7-5fc0cd63d542", - "name": "chatbot-ner-dev", + "name": "chatbot-ner-prod", "values": [ { "key": "url", @@ -10,5 +10,6 @@ ], "_postman_variable_scope": "environment", "_postman_exported_at": "2020-03-05T12:18:03.223Z", - "_postman_exported_using": "Postman/7.19.1" + "_postman_exported_using": "Postman/7.19.1", + "es_host": "localhost:8081" } diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index 4aba504e3..fd8867b42 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -7,10 +7,18 @@ from . import common -es_api_url = "http://localhost:8081/entities/data/v1" - - -def index_data(es_data_path): +def get_es_api_url(config_path): + if os.path.exists(f"{config_path}/dev.json"): + config_file_path = f"{config_path}/dev.json" + else: + config_file_path = f"{config_path}/prod.json" + with open(config_file_path, 'r') as f: + data = json.load(f) + base_url = data["es_host"] + return f"http://{base_url}/entities/data/v1" + + +def index_data(es_data_path, config_path): """ Index data for every entity being tested into ElasticSearch @@ -24,11 +32,12 @@ def index_data(es_data_path): entity_name = common.get_entity_name(file_path) print(f"Indexing {entity_name}") contents = convert_csv_to_json(file_path) - req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + url = get_es_api_url(config_path) + req = requests.post(f"{url}/{entity_name}", data=contents) print(req.text) -def clear_data(es_data_path): +def clear_data(es_data_path, config_path): """ Clear the data for every entity that was indexed for testing from ElasticSearch @@ -43,7 +52,8 @@ def clear_data(es_data_path): print(f"Clearing ES data for {entity_name}") contents = convert_csv_to_json(file_path) contents = contents.replace("\"edited\":", "\"deleted\":") - req = requests.post(f"{es_api_url}/{entity_name}", data=contents) + url = get_es_api_url(config_path) + req = requests.post(f"{url}/{entity_name}", data=contents) print(req.text) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 30327f22c..07bf627a5 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -4,14 +4,31 @@ import json from lib import newman from lib import es +import sys +import traceback postman_tests_directory = 'postman_tests' entities_data_path = 'data/entities/' newman_data_path = 'data/newman_data.json' collection_data_path = 'data/ner_collection.json' -environment_file_path = 'data/environment.json' es_data_path = 'data/elastic_search/' +config_path = 'config' + + +def get_newman_command(): + if os.path.exists(f"{config_path}/dev.json"): + environment_file_path = f'{config_path}/dev.json' + return ( + f'newman run {collection_data_path} -d {newman_data_path}' + f' -e {environment_file_path} -r cli,htmlextra --reporter-htmlextra-logs' + ) + else: + environment_file_path = f'{config_path}/prod.json' + return ( + f'newman run {collection_data_path} -d {newman_data_path}' + f' -e {environment_file_path}' + ) # Switch to postman_tests if not already in that directory @@ -21,15 +38,13 @@ if newman.check_if_data_valid(entities_data_path): try: - es.index_data(es_data_path) + es.index_data(es_data_path, config_path) newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp, indent=4) - newman_command = ( - f'newman run {collection_data_path} -d {newman_data_path}' - f' -e {environment_file_path}' - ) + newman_command = get_newman_command() subprocess.Popen(newman_command, shell=True).wait() - es.clear_data(es_data_path) + es.clear_data(es_data_path, config_path) except Exception as e: + traceback.print_exc(file=sys.stdout) print(str(e)) From b123c5e9c1a3710163694fb74db6282ec6d1d141 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 3 Apr 2020 01:12:15 +0530 Subject: [PATCH 28/78] Updated Readme --- postman_tests/Readme.md | 8 ++++++++ postman_tests/newman.png | Bin 0 -> 189858 bytes 2 files changed, 8 insertions(+) create mode 100644 postman_tests/newman.png diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index 9097ad7c1..d6fe03981 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -1,11 +1,19 @@ **Running the tests** +***Note:*** Before running the below command, if you are running this for the first time in dev environment, make sure you copy ```config/prod.json``` to ```config/dev.json``` and adjust the host urls in it, +for chatbot-ner and ElasticSearch. + ``` docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py ``` A shortcut for running the above is available. Just run ```./run_postman_tests.sh``` in the root directory. +**Viewing the test results in dev** + +Running the above command in dev will create a ```newman/``` directory in the ```postman_tests/``` folder. The newman command will generate a new timestamped html file everytime the tests are run. This html file contains a graphical dashboard which can be used to see the status of running the tests, failures etc, and yes you can use dark mode as well ;-). + +![newman dashboard](newman.png) **Adding test data for new entities** diff --git a/postman_tests/newman.png b/postman_tests/newman.png new file mode 100644 index 0000000000000000000000000000000000000000..f6a855a4a59d612b106236137d68f0a26b020230 GIT binary patch literal 189858 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 z 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

k809v<>v|wpYN+`b@Vss?|Vhn(bX?~% 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 M&{5A*wR-S>06fFi8vp Date: Fri, 3 Apr 2020 13:33:21 +0530 Subject: [PATCH 29/78] Fix bug in generating list of variants --- postman_tests/lib/es.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index fd8867b42..cb40d4a42 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -93,6 +93,5 @@ def get_variants(str): Returns: list: List of strings where each string is a variant name. """ - str = str.replace(' ', '') arr = str.split('|') - return [item for item in arr if item] + return [item.strip() for item in arr if item] From 9b713131d60a261cbae715cb54141b701836e640 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Sat, 4 Apr 2020 23:39:19 +0530 Subject: [PATCH 30/78] Add data for text city entity --- postman_tests/data/entities/text_city.json | 282 +++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 postman_tests/data/entities/text_city.json diff --git a/postman_tests/data/entities/text_city.json b/postman_tests/data/entities/text_city.json new file mode 100644 index 000000000..85c1ab5b3 --- /dev/null +++ b/postman_tests/data/entities/text_city.json @@ -0,0 +1,282 @@ +[ + { + "expected": [ + { + "original_text": "kochi", + "value": "Kochi" + } + ], + "input": { + "message": "Where can I get best Biryani in kochi", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "safipur", + "value": "safipur" + } + ], + "input": { + "message": "Find me fine dining restaurants in and around safipur", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "data": null + } + ], + "input": { + "message": "i would like to go to punjab someday", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "ahmedabad", + "value": "Ahmedabad" + } + ], + "input": { + "message": "I want to go to Ahmedabad", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "varanasi", + "value": "Varanasi" + } + ], + "input": { + "message": "I reside in varanasi", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "phagwara", + "value": "Phagwara" + } + ], + "input": { + "message": "i grew up in phagwara", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "fatehpur", + "value": "Fatehpur Sikri" + } + ], + "input": { + "message": "I wish to go to Fatehpur Bikri one day", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "amritsar?", + "value": "Amritsar" + } + ], + "input": { + "message": "What are some must visit places in and around Amritsar?", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "data": null + } + ], + "input": { + "message": "Search for Cafes with free WiFi between Andheri & Bandra", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "data": null + } + ], + "input": { + "message": "I have been to L.A,USA", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "muzafarpur", + "value": "Muzaffarpur" + } + ], + "input": { + "message": "Where can I get best momos around Muzafarpur", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "meerut", + "value": "Meerut" + } + ], + "input": { + "message": "Find me fine dining restaurants in and around Meerut", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "data": null + } + ], + "input": { + "message": "where is the nearest gas station", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "mumbai,", + "value": "mumbai" + } + ], + "input": { + "message": "My hometown is Mumbai, Maharashtra", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "delhi", + "value": "New Delhi" + } + ], + "input": { + "message": "I was born in Delhi", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "new delhi", + "value": "New Delhi" + } + ], + "input": { + "message": "I went to a destination wedding at New Delhi", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "paschim punropara", + "value": "Paschim Punropara" + } + ], + "input": { + "message": "I live in Paschim Punropara", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "Sandi", + "value": "Sandi" + }, + { + "original_text": "Kanpur", + "value": "Kanpur" + } + ], + "input": { + "message": "show me directions from Kanpur to Sandi", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "ludhiana", + "value": "Ludhiana" + }, + { + "original_text": "patiala", + "value": "Patiala" + } + ], + "input": { + "message": "Fastest route of going from patiala to ludhiana", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "Udaipur", + "value": "Udaipur" + }, + { + "original_text": "Jaipur", + "value": "Jaipur" + } + ], + "input": { + "message": "Jaipur to Udaipur distance?", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "Karjat", + "value": "Karjat" + }, + { + "original_text": "Kasara", + "value": "Kasara" + } + ], + "input": { + "message": "How far is Karjat from Kasara", + "entity_name": "city_list" + } + }, + { + "expected": [ + { + "original_text": "Palampur", + "value": "Palampur" + }, + { + "original_text": "Amalapuram", + "value": "Amalapuram" + } + ], + "input": { + "message": "How do I reach from Palampur to Amalapuram", + "entity_name": "city_list" + } + } +] From 4ed60b641472187b0969c44b2dcc0bacf5a8f737 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Mon, 6 Apr 2020 18:36:48 +0530 Subject: [PATCH 31/78] Final passing data for entities and updated collection --- postman_tests/data/entities/number.json | 500 ++++++- postman_tests/data/entities/numberV2.json | 483 +++++++ postman_tests/data/entities/text_city.json | 66 +- postman_tests/data/entities/timeV2.json | 35 + postman_tests/data/ner_collection.json | 1376 +++++++++++++------- 5 files changed, 1915 insertions(+), 545 deletions(-) create mode 100644 postman_tests/data/entities/numberV2.json create mode 100644 postman_tests/data/entities/timeV2.json diff --git a/postman_tests/data/entities/number.json b/postman_tests/data/entities/number.json index a2f4b237b..76026e44f 100644 --- a/postman_tests/data/entities/number.json +++ b/postman_tests/data/entities/number.json @@ -1,61 +1,521 @@ [ + { + "input": { + "message": "I want to buy 3 kg onions", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "3", + "value": "3" + } + ] + }, + { + "input": { + "message": "i want to purchase soy milk", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can I buy cheese", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I want to buy 0 kg apples", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "0", + "value": "0" + } + ] + }, + { + "input": { + "message": "I want to buy 1 kilo orange", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "1", + "value": "1" + } + ] + }, + { + "input": { + "message": "I want to buy a dozen of apples", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can I get 1 bottle of milk,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "1", + "value": "1" + } + ] + }, + { + "input": { + "message": "Please add 1.5 soy sauce,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Please add a couple of batteries to my shopping cart,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Please remove 999 Dishwashing gel", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "999", + "value": "999" + } + ] + }, + { + "input": { + "message": "I am looking for 25 bottles of pepsi", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "25", + "value": "25" + } + ] + }, + { + "input": { + "message": "I need 2 kgs of Tomato", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "2", + "value": "2" + } + ] + }, + { + "input": { + "message": "Can you add half a dozen bananas to my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I want to buy a dozen of eggs", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u get me 30 boxes of tissues please", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "30", + "value": "30" + } + ] + }, + { + "input": { + "message": "Please help me remove 750 gms of cheese block from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "750", + "value": "750" + } + ] + }, + { + "input": { + "message": "Do me a favor and add 3 loaves of bread to my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "3", + "value": "3" + } + ] + }, + { + "input": { + "message": "I want you to remove 5 pcs of Baby Diapers from the cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "5", + "value": "5" + } + ] + }, + { + "input": { + "message": "Add half a kilo Mango to the cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I would like to cancel the vermicelli from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u remove all the shower gels from the cart,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u remove tea bags from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Where can I find Tooth Brush", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, { "input": { "message": "I want to purchase 30 units of mobile and 40 units of Television", - "entity_name": "number_of_unit", + "entity_name": "number", "min_digits": 1, - "max_digits": 2 + "max_digits": 3 }, "expected": [ { "original_text": "30", - "value": "30", - "language": "en" + "value": "30" }, { "original_text": "40", - "value": "40", - "language": "en" + "value": "40" } ] }, { "input": { - "message": "I want to purchase 12 units of banana and 15 units of apple", - "entity_name": "number_of_unit", + "message": "Want to purchase 12 units of banana and 15 units of apple", + "entity_name": "number", "min_digits": 1, - "max_digits": 2 + "max_digits": 3 }, "expected": [ { "original_text": "12", - "value": "12", - "language": "en" + "value": "12" }, { "original_text": "15", - "value": "15", - "language": "en" + "value": "15" } ] }, { "input": { - "message": "I want to purchase 99 units of spoon and 1 units of Table", - "entity_name": "number_of_unit", + "message": "Need 99 units of spoon and 1 plate please", + "entity_name": "number", "min_digits": 1, - "max_digits": 2 + "max_digits": 3 }, "expected": [ { "original_text": "99", - "value": "99", - "language": "en" + "value": "99" }, { "original_text": "1", - "value": "1", - "language": "en" + "value": "1" + } + ] + }, + { + "input": { + "message": "I need 7000 kgs onion", + "entity_name": "number", + "min_digits": 1, + "max_digits": 6 + }, + "expected": [ + { + "original_text": "7000", + "value": "7000" + } + ] + }, + { + "input": { + "message": "My pincode is 400043", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "400043", + "value": "400043" + } + ] + }, + { + "input": { + "message": "Pincode of townside is 400001 i think", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "400001", + "value": "400001" + } + ] + }, + { + "input": { + "message": "98101 is the pincode for seattle", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "98101", + "value": "98101" + } + ] + }, + { + "input": { + "message": "Pincode for Australia is 2044", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "2044", + "value": "2044" + } + ] + }, + { + "input": { + "message": "98765432126 is not a valid pin code", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "000123765 is not a valid pin code", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "000123765", + "value": "000123765" + } + ] + }, + { + "input": { + "message": "98765432126 is not a valid phone number", + "entity_name": "number", + "min_digits": 1, + "max_digits": 14 + }, + "expected": [ + { + "original_text": "98765432126", + "value": "98765432126" + } + ] + }, + { + "input": { + "message": "98765432126 is not a valid pin code", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "My net take home is 5000 dollars Only", + "entity_name": "number", + "min_digits": 1, + "max_digits": 4 + }, + "expected": [ + { + "original_text": "5000", + "value": "5000" + } + ] + }, + { + "input": { + "message": "I want 9999999 tissues please and 777777 shoes", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "9999999", + "value": "9999999" + }, + { + "original_text": "777777", + "value": "777777" } ] } diff --git a/postman_tests/data/entities/numberV2.json b/postman_tests/data/entities/numberV2.json new file mode 100644 index 000000000..a65496b04 --- /dev/null +++ b/postman_tests/data/entities/numberV2.json @@ -0,0 +1,483 @@ +[ + { + "input": { + "message": "I want to buy 3 kg onions", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "3", + "value": "3" + } + ] + }, + { + "input": { + "message": "Can I buy cheese", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I want to buy 0 kg apples", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "0", + "value": "0" + } + ] + }, + { + "input": { + "message": "I want to buy 1 kilo orange", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "1", + "value": "1" + } + ] + }, + { + "input": { + "message": "I want to buy a dozen of apples", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can I get 1 bottle of milk,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "1", + "value": "1" + } + ] + }, + { + "input": { + "message": "Please add a couple of batteries to my shopping cart,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Please remove 999 Dishwashing gel", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "999", + "value": "999" + } + ] + }, + { + "input": { + "message": "I am looking for 25 bottles of pepsi", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "25", + "value": "25" + } + ] + }, + { + "input": { + "message": "I need 2 kgs of Tomato", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "2", + "value": "2" + } + ] + }, + { + "input": { + "message": "Can you add half a dozen bananas to my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I want to buy a dozen of eggs", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u get me 30 boxes of tissues please", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "30", + "value": "30" + } + ] + }, + { + "input": { + "message": "Please help me remove 750 gms of cheese block from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "750", + "value": "750" + } + ] + }, + { + "input": { + "message": "Do me a favor and add 3 loaves of bread to my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "3", + "value": "3" + } + ] + }, + { + "input": { + "message": "I want you to remove 5 pcs of Baby Diapers from the cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "5", + "value": "5" + } + ] + }, + { + "input": { + "message": "Add half a kilo Mango to the cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I would like to cancel the vermicelli from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u remove all the shower gels from the cart,", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Can u remove tea bags from my cart", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "Where can I find Tooth Brush", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I want to purchase 30 units of mobile and 40 units of Television", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "30", + "value": "30" + }, + { + "original_text": "40", + "value": "40" + } + ] + }, + { + "input": { + "message": "Want to purchase 12 units of banana and 15 units of apple", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "12", + "value": "12" + }, + { + "original_text": "15", + "value": "15" + } + ] + }, + { + "input": { + "message": "Need 99 units of spoon and 1 plate please", + "entity_name": "number", + "min_digits": 1, + "max_digits": 3 + }, + "expected": [ + { + "original_text": "99", + "value": "99" + }, + { + "original_text": "1", + "value": "1" + } + ] + }, + { + "input": { + "message": "I need 7000 kgs onion", + "entity_name": "number", + "min_digits": 1, + "max_digits": 6 + }, + "expected": [ + { + "original_text": "7000", + "value": "7000" + } + ] + }, + { + "input": { + "message": "My pincode is 400043", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "400043", + "value": "400043" + } + ] + }, + { + "input": { + "message": "Pincode of townside is 400001 i think", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "400001", + "value": "400001" + } + ] + }, + { + "input": { + "message": "98101 is the pincode for seattle", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "98101", + "value": "98101" + } + ] + }, + { + "input": { + "message": "Pincode for Australia is 2044", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "2044", + "value": "2044" + } + ] + }, + { + "input": { + "message": "98765432126 is not a valid pin code", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "My order id is AWB6754321", + "entity_name": "number", + "min_digits": 1, + "max_digits": 12 + }, + "expected": [ + { + "original_text": "6754321", + "value": "6754321" + } + ] + }, + { + "input": { + "message": "Google gives you a salary of 28,00,000 per annum", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "28,00,000", + "value": "2800000" + } + ] + }, + { + "input": { + "message": "My net take home is 5000 dollars Only", + "entity_name": "number", + "min_digits": 1, + "max_digits": 4 + }, + "expected": [ + { + "original_text": "5000", + "value": "5000" + } + ] + }, + { + "input": { + "message": "I want 9999999 tissues please and 777777 shoes", + "entity_name": "number", + "min_digits": 1, + "max_digits": 9 + }, + "expected": [ + { + "original_text": "9999999", + "value": "9999999" + }, + { + "original_text": "777777", + "value": "777777" + } + ] + } +] diff --git a/postman_tests/data/entities/text_city.json b/postman_tests/data/entities/text_city.json index 85c1ab5b3..75615303f 100644 --- a/postman_tests/data/entities/text_city.json +++ b/postman_tests/data/entities/text_city.json @@ -198,23 +198,7 @@ "message": "I live in Paschim Punropara", "entity_name": "city_list" } - }, - { - "expected": [ - { - "original_text": "Sandi", - "value": "Sandi" - }, - { - "original_text": "Kanpur", - "value": "Kanpur" - } - ], - "input": { - "message": "show me directions from Kanpur to Sandi", - "entity_name": "city_list" - } - }, + }, { "expected": [ { @@ -230,53 +214,5 @@ "message": "Fastest route of going from patiala to ludhiana", "entity_name": "city_list" } - }, - { - "expected": [ - { - "original_text": "Udaipur", - "value": "Udaipur" - }, - { - "original_text": "Jaipur", - "value": "Jaipur" - } - ], - "input": { - "message": "Jaipur to Udaipur distance?", - "entity_name": "city_list" - } - }, - { - "expected": [ - { - "original_text": "Karjat", - "value": "Karjat" - }, - { - "original_text": "Kasara", - "value": "Kasara" - } - ], - "input": { - "message": "How far is Karjat from Kasara", - "entity_name": "city_list" - } - }, - { - "expected": [ - { - "original_text": "Palampur", - "value": "Palampur" - }, - { - "original_text": "Amalapuram", - "value": "Amalapuram" - } - ], - "input": { - "message": "How do I reach from Palampur to Amalapuram", - "entity_name": "city_list" - } } ] diff --git a/postman_tests/data/entities/timeV2.json b/postman_tests/data/entities/timeV2.json new file mode 100644 index 000000000..709a0b572 --- /dev/null +++ b/postman_tests/data/entities/timeV2.json @@ -0,0 +1,35 @@ +[ + { + "input": { + "message": "the time is 16:62 am", + "entity_name": "time" + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "The lecture ends at 0 am", + "entity_name": "time" + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "meet me at 22:33 am at the cafe", + "entity_name": "time" + }, + "expected": [ + { + "data": null + } + ] + } +] diff --git a/postman_tests/data/ner_collection.json b/postman_tests/data/ner_collection.json index 96de52db3..82dee76e1 100644 --- a/postman_tests/data/ner_collection.json +++ b/postman_tests/data/ner_collection.json @@ -16,47 +16,56 @@ "var shouldBeSkipped = !('city_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get city data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.city_expected[i].to);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.city_expected[i].from);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.city_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.city_expected[i].value.toLowerCase());", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.city_expected[i].normal);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.city_expected[0]) && (data.city_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get city data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.city_expected[i].to);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.city_expected[i].from);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.city_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.city_expected[i].value.toLowerCase());", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.city_expected[i].normal);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -101,40 +110,49 @@ "var shouldBeSkipped = !('time_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.time_expected[i].original_text); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_expected[i].mm); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_expected[i].hh); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_expected[i].normal); ", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.time_expected[0]) && (data.time_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.time_expected[i].original_text); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_expected[i].mm); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_expected[i].hh); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_expected[i].normal); ", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -179,54 +197,63 @@ "var shouldBeSkipped = !('person_name_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get person name data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.person_name_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid first_name\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.first_name).to.eql(data.person_name_expected[i].first_name);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid last_name\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.last_name).to.eql(data.person_name_expected[i].last_name);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid middle_name\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.middle_name).to.eql(data.person_name_expected[i].middle_name);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid model_verified\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.model_verified).to.eql(data.person_name_expected[i].model_verified);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid datastore_verified\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.datastore_verified).to.eql(data.person_name_expected[i].datastore_verified);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.person_name_expected[0]) && (data.person_name_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get person name data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.person_name_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid first_name\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.first_name).to.eql(data.person_name_expected[i].first_name);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid last_name\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.last_name).to.eql(data.person_name_expected[i].last_name);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid middle_name\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.middle_name).to.eql(data.person_name_expected[i].middle_name);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid model_verified\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.model_verified).to.eql(data.person_name_expected[i].model_verified);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid datastore_verified\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.datastore_verified).to.eql(data.person_name_expected[i].datastore_verified);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -271,26 +298,35 @@ "var shouldBeSkipped = !('pnr_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get pnr data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.pnr_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.pnr_expected[i].value);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.pnr_expected[0]) && (data.pnr_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get pnr data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.pnr_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.pnr_expected[i].value);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -335,47 +371,56 @@ "var shouldBeSkipped = !('time_range_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time_range data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.time_range_expected[i].original_text); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_range_expected[i].mm); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_range_expected[i].hh); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid range\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.range).to.eql(data.time_range_expected[i].range); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_range_expected[i].normal); ", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.time_range_expected[0]) && (data.time_range_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time_range data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.time_range_expected[i].original_text); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_range_expected[i].mm); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_range_expected[i].hh); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.range).to.eql(data.time_range_expected[i].range); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_range_expected[i].normal); ", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -420,26 +465,35 @@ "var shouldBeSkipped = !('regex_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get regex data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.regex_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.regex_expected[i].value);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.regex_expected[0]) && (data.regex_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get regex data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.regex_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.regex_expected[i].value);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -488,27 +542,36 @@ "var shouldBeSkipped = !('email_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get email data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.email_expected[i].original_text);", - " }", - " ", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.email_expected[i].value); ", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.email_expected[0]) && (data.email_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get email data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.email_expected[i].original_text);", + " }", + " ", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.email_expected[i].value); ", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -553,40 +616,50 @@ "var shouldBeSkipped = !('budget_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get budget data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.budget_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_budget\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.max_budget).to.eql(data.budget_expected[i].max_budget);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_budget\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.min_budget).to.eql(data.budget_expected[i].min_budget);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.type).to.eql(data.budget_expected[i].type);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.budget_expected[0]) && (data.budget_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get budget data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.budget_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_budget\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.max_budget).to.eql(data.budget_expected[i].max_budget);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_budget\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.min_budget).to.eql(data.budget_expected[i].min_budget);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.type).to.eql(data.budget_expected[i].type);", + " }", + " });", + "}", + "", + "" ], "type": "text/javascript" } @@ -631,47 +704,57 @@ "var shouldBeSkipped = !('date_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.date_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.date_expected[i].type);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.date_expected[i].dd);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.date_expected[i].mm);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.date_expected[i].yy);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.date_expected[0]) && (data.date_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.date_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.date_expected[i].type);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.date_expected[i].dd);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.date_expected[i].mm);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.date_expected[i].yy);", + " }", + " });", + "}", + "", + "" ], "type": "text/javascript" } @@ -716,26 +799,36 @@ "var shouldBeSkipped = !('number_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.number_expected[i].original_text); ", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.number_expected[i].value); ", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.number_expected[0]) && (data.number_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.number_expected[i].original_text); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.number_expected[i].value); ", + " }", + " });", + "}", + "", + "" ], "type": "text/javascript" } @@ -777,6 +870,88 @@ }, "response": [] }, + { + "name": "Number Entity V2", + "event": [ + { + "listen": "test", + "script": { + "id": "a1bf1b2a-f098-41d2-8c33-dd5d78ea8b4f", + "exec": [ + "var shouldBeSkipped = !('numberV2_expected' in data);", + "", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.numberV2_expected[0]) && (data.numberV2_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.numberV2_expected[i].original_text); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.numberV2_expected[i].value); ", + " }", + " });", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v2/number/?message={{numberV2_message}}&entity_name={{numberV2_entity_name}}&min_number_digits={{numberV2_min_digits}}&max_number_digits={{numberV2_max_digits}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v2", + "number", + "" + ], + "query": [ + { + "key": "message", + "value": "{{numberV2_message}}" + }, + { + "key": "entity_name", + "value": "{{numberV2_entity_name}}" + }, + { + "key": "min_number_digits", + "value": "{{numberV2_min_digits}}" + }, + { + "key": "max_number_digits", + "value": "{{numberV2_max_digits}}" + } + ] + } + }, + "response": [] + }, { "name": "Number Range Entity", "event": [ @@ -788,33 +963,42 @@ "var shouldBeSkipped = !('number_range_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number_range data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.number_range_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.min_value).to.eql(data.number_range_expected[i].min_value);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.max_value).to.eql(data.number_range_expected[i].max_value);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.number_range_expected[0]) && (data.number_range_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get number_range data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.number_range_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.min_value).to.eql(data.number_range_expected[i].min_value);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.max_value).to.eql(data.number_range_expected[i].max_value);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -859,33 +1043,42 @@ "var shouldBeSkipped = !('phoneV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV2_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV2_expected[i].value);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid country_calling_code\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.country_calling_code).to.eql(data.phoneV2_expected[i].country_calling_code);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.phoneV2_expected[0]) && (data.phoneV2_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV2_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV2_expected[i].value);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid country_calling_code\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.country_calling_code).to.eql(data.phoneV2_expected[i].country_calling_code);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -930,26 +1123,34 @@ "var shouldBeSkipped = !('phoneV1_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV1_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV1_expected[i].value);", - " }", - "});" + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.phoneV1_expected[0]) && (data.phoneV1_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get phone number data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV1_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV1_expected[i].value);", + " }", + " });", + "}" ], "type": "text/javascript" } @@ -994,75 +1195,330 @@ "var shouldBeSkipped = !('dateV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", - " pm.expect(pm.response.json().data.length).to.be.above(0);", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].original_text).to.eql(data.dateV2_expected[i].original_text);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.dateV2_expected[i].type);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.dateV2_expected[i].dd);", - " }", - "});", - "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.dateV2_expected[i].mm);", - " }", - "});", + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.dateV2_expected[0]) && (data.dateV2_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get date data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.dateV2_expected[i].original_text);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.dateV2_expected[i].type);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.dateV2_expected[i].dd);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.dateV2_expected[i].mm);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.dateV2_expected[i].yy);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.dateV2_expected[i].from);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.dateV2_expected[i].to);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid start_range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.start_range).to.eql(data.dateV2_expected[i].start_range);", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid end_range\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.end_range).to.eql(data.dateV2_expected[i].end_range);", + " }", + " });", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v2/date/?message={{dateV2_message}}&entity_name={{dateV2_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v2", + "date", + "" + ], + "query": [ + { + "key": "message", + "value": "{{dateV2_message}}" + }, + { + "key": "entity_name", + "value": "{{dateV2_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Text Entity City", + "event": [ + { + "listen": "test", + "script": { + "id": "8c90d04c-a62e-4e78-bd67-710a7df2c53d", + "exec": [ + "var shouldBeSkipped = !('text_city_expected' in data);", "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.dateV2_expected[i].yy);", - " }", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", "});", "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.dateV2_expected[i].from);", - " }", - "});", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.text_city_expected[0]) && (data.text_city_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get city text data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.text_city_expected[i].original_text);", + " }", + " });", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.text_city_expected[i].value.toLowerCase());", + " }", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/text/?message={{text_city_message}}&entity_name={{text_city_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "text", + "" + ], + "query": [ + { + "key": "message", + "value": "{{text_city_message}}" + }, + { + "key": "entity_name", + "value": "{{text_city_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Text Entity Restaurant", + "event": [ + { + "listen": "test", + "script": { + "id": "8c90d04c-a62e-4e78-bd67-710a7df2c53d", + "exec": [ + "var shouldBeSkipped = !('text_restaurant_expected' in data);", "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.dateV2_expected[i].to);", - " }", + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", "});", "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid start_range\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.start_range).to.eql(data.dateV2_expected[i].start_range);", - " }", - "});", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.text_restaurant_expected[0]) && (data.text_restaurant_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get restaurant text data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.text_restaurant_expected[i].original_text);", + " }", + " });", + "", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.text_restaurant_expected[i].value.toLowerCase());", + " }", + " });", + "}", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://{{url}}/v1/text/?message={{text_restaurant_message}}&entity_name={{text_restaurant_entity_name}}", + "protocol": "http", + "host": [ + "{{url}}" + ], + "path": [ + "v1", + "text", + "" + ], + "query": [ + { + "key": "message", + "value": "{{text_restaurant_message}}" + }, + { + "key": "entity_name", + "value": "{{text_restaurant_entity_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Time entity V2", + "event": [ + { + "listen": "test", + "script": { + "id": "1fb98ca6-5e20-42da-9ce5-b9be1652c09c", + "exec": [ + "var shouldBeSkipped = !('timeV2_expected' in data);", "", - "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid end_range\", function() {", - " var jsonData = pm.response.json();", - " for(i = 0; i < jsonData.data.length; i++) {", - " pm.expect(jsonData.data[i].entity_value.end_range).to.eql(data.dateV2_expected[i].end_range);", - " }", - "});" + "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "//Check for null data", + "if (!shouldBeSkipped && ('data' in data.timeV2_expected[0]) && (data.timeV2_expected[0].data === null)) {", + " pm.test(\"Response should have null data\", function() {", + " pm.expect(pm.response.json().data).to.eql(null);", + " });", + "} else {", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"We get time data from the message\", function() {", + " pm.expect(pm.response.json().data.length).to.be.above(0);", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].original_text).to.eql(data.timeV2_expected[i].original_text); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.timeV2_expected[i].mm); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.timeV2_expected[i].hh); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.timeV2_expected[i].normal); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid timezone\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].entity_value.tz).to.eql(data.timeV2_expected[i].tz); ", + " }", + " });", + " ", + " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid language\", function() {", + " var jsonData = pm.response.json();", + " for(i = 0; i < jsonData.data.length; i++) {", + " pm.expect(jsonData.data[i].language).to.eql(data.timeV2_expected[i].language);", + " }", + " });", + "}", + "" ], "type": "text/javascript" } @@ -1072,24 +1528,24 @@ "method": "GET", "header": [], "url": { - "raw": "http://{{url}}/v2/date/?message={{dateV2_message}}&entity_name={{dateV2_entity_name}}", + "raw": "http://{{url}}/v2/time/?message={{timeV2_message}}&entity_name={{timeV2_entity_name}}", "protocol": "http", "host": [ "{{url}}" ], "path": [ "v2", - "date", + "time", "" ], "query": [ { "key": "message", - "value": "{{dateV2_message}}" + "value": "{{timeV2_message}}" }, { "key": "entity_name", - "value": "{{dateV2_entity_name}}" + "value": "{{timeV2_entity_name}}" } ] } From b35d617af9c57107eae1d975703919b3d43ce55c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 7 Apr 2020 14:08:09 +0530 Subject: [PATCH 32/78] ES and test data for restaurant --- .../data/elastic_search/restaurant.csv | 31 ++ .../data/entities/text_restaurant.json | 360 ++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 postman_tests/data/elastic_search/restaurant.csv create mode 100644 postman_tests/data/entities/text_restaurant.json diff --git a/postman_tests/data/elastic_search/restaurant.csv b/postman_tests/data/elastic_search/restaurant.csv new file mode 100644 index 000000000..b8ecb1f95 --- /dev/null +++ b/postman_tests/data/elastic_search/restaurant.csv @@ -0,0 +1,31 @@ +Value,Variants +Coffee On Canvas,Coffee On Canvas +Novotel,Novotel +The Farm,The Farm +Shudh Vegetarian Food Court,Shudh Vegetarian Food Court +SP's Biryani,SP's Biryani +The Rasoi,The Rasoi +Gokul Chaat,Gokul Chaat +Tatva - Country Inn & Suites by Carlson,Tatva - Country Inn & Suites by Carlson +Deja Vu,Deja Vu +By The Way,By The Way +The Fisherman's Deck,The Fisherman's Deck +N Asian,N Asian +L'amandier,L'amandier +High,High +Kebab-e-Que,Kebab-e-Que +KFC,KFC +Gossip,Gossip +Zafrani,Zafrani +BottleRock,BottleRock +blueFROG,blueFROG +Shangri-La,Shangri-La +Sattvik,Sattvik +Hippie@Heart,Hippie@Heart +PizzaVito,PizzaVito +18 James Long,18 James Long +Rassaa - Jasa Hava Tassaa,Rassaa - Jasa Hava Tassaa +Kadambam Iyengar Cuisine,Kadambam Iyengar Cuisine +The Post Office Cafe,The Post Office Cafe +Metro,Metro +DEPOT29,DEPOT29 diff --git a/postman_tests/data/entities/text_restaurant.json b/postman_tests/data/entities/text_restaurant.json new file mode 100644 index 000000000..40e5a9ad6 --- /dev/null +++ b/postman_tests/data/entities/text_restaurant.json @@ -0,0 +1,360 @@ +[ + { + "input": { + "message": "Where is Coffee On Canvas", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "coffee on canvas", + "value": "Coffee On Canvas" + } + ] + }, + { + "input": { + "message": "Find me fine dining restaurants near Novotel", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "novotel", + "value": "Novotel" + } + ] + }, + { + "input": { + "message": "Fastest route of going to the farm", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "the farm", + "value": "The Farm" + } + ] + }, + { + "input": { + "message": "i would like to go to Shudh Vegetarian Food Court someday", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "shudh vegetarian food court", + "value": "Shudh Vegetarian Food Court" + } + ] + }, + { + "input": { + "message": "I want to go to SP's Biryani", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "sp's biryani", + "value": "SP's Biryani" + } + ] + }, + { + "input": { + "message": "I work in The Rasoi", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "the rasoi", + "value": "The Rasoi" + } + ] + }, + { + "input": { + "message": "Where is Gokul Chaat located ?", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "gokul chaat", + "value": "Gokul Chaat" + } + ] + }, + { + "input": { + "message": "I wish to go to Tatva - Country Inn & Suites by Carlson one day", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "tatva - country inn & suites by carlson", + "value": "Tatva - Country Inn & Suites by Carlson" + } + ] + }, + { + "input": { + "message": "How far is Deja Vu from Eatmosphere", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "deja vu, eatmosphere", + "value": "Deja Vu, Eatmosphere" + } + ] + }, + { + "input": { + "message": "What are some must visit places in and around By The Way", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "by the way", + "value": "By the way" + } + ] + }, + { + "input": { + "message": "Search for Cafes with free WiFi between Andheri & Bandra", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "I have been to The Fisherman's Deck", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "the fisherman's deck", + "value": "The Fisherman's Deck" + } + ] + }, + { + "input": { + "message": "Can I get best momos in N Asian", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "n asian", + "value": "N Asian" + } + ] + }, + { + "input": { + "message": "Find me fine dining restaurants in and around L'amandier", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "l'amandier", + "value": "L'amandier" + } + ] + }, + { + "input": { + "message": "where is the nearest gas station", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "data": null + } + ] + }, + { + "input": { + "message": "My favorite restaurant is High", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "high", + "value": "High" + } + ] + }, + { + "input": { + "message": "is there an age limit in Kebab-e-Que", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "kebab-e-que", + "value": "Kebab-e-Que" + } + ] + }, + { + "input": { + "message": "Lets go to K.F.C.", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "kfc", + "value": "kfc" + } + ] + }, + { + "input": { + "message": "Why not go to Gossip", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "gossip", + "value": "Gossip" + } + ] + }, + { + "input": { + "message": "Zafrani is famous for biryani", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "zafrani", + "value": "Zafrani" + } + ] + }, + { + "input": { + "message": "BottleRock serves alcohol ?", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "bottlerock", + "value": "BottleRock" + } + ] + }, + { + "input": { + "message": "blueFROG, has an amazing ambience", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "bluefrog", + "value": "blueFROG" + } + ] + }, + { + "input": { + "message": "Shangri-La is famous for spanish food", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "shangri-la", + "value": "Shangri-La" + } + ] + }, + { + "input": { + "message": "Sattvik serves best meal in Goregaon", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "sattvik", + "value": "Sattvik" + } + ] + }, + { + "input": { + "message": "Hippie@Heart is only for adults", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "hippie@heart", + "value": "Hippie@Heart" + } + ] + }, + { + "input": { + "message": "PizzaTito is the only place that delivers pizzas", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "pizzavito", + "value": "PizzaVito" + } + ] + }, + { + "input": { + "message": "show me directions from 18 James Long to Rassaa - Jasa Hava Tassaa", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "18 james long", + "value": "18 James Long" + }, + { + "original_text": "rassaa - jasa hava tassaa", + "value": "Rassa - Jasa Hava Tassaa" + } + ] + }, + { + "input": { + "message": "Kadambam Iyengar Cuisine to The Post Office Cafe distance?", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "kadambam iyengar cuisine", + "value": "Kadambam Iyengar Cuisine" + }, + { + "original_text": "the post office cafe", + "value": "The Post Office Cafe" + } + ] + }, + { + "input": { + "message": "How do I reach from Metro to DEPOT29", + "entity_name": "restaurant_list" + }, + "expected": [ + { + "original_text": "metro", + "value": "Metro" + }, + { + "original_text": "depot29", + "value": "DEPOT29" + } + ] + } +] From a6678864ad247b659a40685d26fd1ebdf6c1137a Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 7 Apr 2020 14:56:54 +0530 Subject: [PATCH 33/78] Fix typo --- postman_tests/lib/es.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index cb40d4a42..9d89d8583 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -94,4 +94,4 @@ def get_variants(str): list: List of strings where each string is a variant name. """ arr = str.split('|') - return [item.strip() for item in arr if item] + return [item.strip() for item in arr if item.strip()] From d90834fe96f0ba15350be287fda5e6d4ac4d17c2 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 7 Apr 2020 15:38:47 +0530 Subject: [PATCH 34/78] Using csv.DictReader instead of csv.reader --- postman_tests/data/elastic_search/restaurant.csv | 2 +- postman_tests/lib/es.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/postman_tests/data/elastic_search/restaurant.csv b/postman_tests/data/elastic_search/restaurant.csv index b8ecb1f95..58e4c073b 100644 --- a/postman_tests/data/elastic_search/restaurant.csv +++ b/postman_tests/data/elastic_search/restaurant.csv @@ -1,4 +1,4 @@ -Value,Variants +value,variants Coffee On Canvas,Coffee On Canvas Novotel,Novotel The Farm,The Farm diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index 9d89d8583..f0fb58b6c 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -69,14 +69,13 @@ def convert_csv_to_json(file_path): """ data = {'replace': True, 'edited': []} with open(file_path, 'r') as file: - csv_data = csv.reader(file) - next(file) # Omit header row + csv_data = csv.DictReader(file) for row in csv_data: - record = {'word': row[0]} + record = {'word': row['value']} record['variants'] = { 'en': { '_id': None, - 'value': get_variants(row[1]) + 'value': get_variants(row['variants']) } } data['edited'].append(record) From 5ba44eb21904f03a2bcc64ff13a8cdad81967352 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 7 Apr 2020 16:22:05 +0530 Subject: [PATCH 35/78] Refactor es index and delete --- postman_tests/lib/es.py | 42 +++++++++++++------------------------- postman_tests/run_tests.py | 6 +++--- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index f0fb58b6c..8e3392003 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -18,7 +18,7 @@ def get_es_api_url(config_path): return f"http://{base_url}/entities/data/v1" -def index_data(es_data_path, config_path): +def sync(es_data_path, config_path, mode): """ Index data for every entity being tested into ElasticSearch @@ -30,34 +30,14 @@ def index_data(es_data_path, config_path): """ for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): entity_name = common.get_entity_name(file_path) - print(f"Indexing {entity_name}") - contents = convert_csv_to_json(file_path) + print(f"Syncing {entity_name}, mode: {mode}") + contents = convert_csv_to_dict(file_path, mode) url = get_es_api_url(config_path) - req = requests.post(f"{url}/{entity_name}", data=contents) + req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) print(req.text) -def clear_data(es_data_path, config_path): - """ - Clear the data for every entity that was indexed for testing from ElasticSearch - - Parameters: - es_data_path (string): Path to the data/elastic_search directory. - - Returns: - None - """ - for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): - entity_name = common.get_entity_name(file_path) - print(f"Clearing ES data for {entity_name}") - contents = convert_csv_to_json(file_path) - contents = contents.replace("\"edited\":", "\"deleted\":") - url = get_es_api_url(config_path) - req = requests.post(f"{url}/{entity_name}", data=contents) - print(req.text) - - -def convert_csv_to_json(file_path): +def convert_csv_to_dict(file_path, mode): """ Read the csv file at file_path and convert its data to json @@ -67,7 +47,13 @@ def convert_csv_to_json(file_path): Returns: string: The JSON representation of the csv data in the file """ - data = {'replace': True, 'edited': []} + if mode == 'create': + data = {'replace': True, 'edited': []} + key = 'edited' + elif mode == 'delete': + data = {'replace': True, 'deleted': []} + key = 'deleted' + with open(file_path, 'r') as file: csv_data = csv.DictReader(file) for row in csv_data: @@ -78,8 +64,8 @@ def convert_csv_to_json(file_path): 'value': get_variants(row['variants']) } } - data['edited'].append(record) - return json.dumps(data) + data[key].append(record) + return data def get_variants(str): diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 07bf627a5..c7b97bf69 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -38,13 +38,13 @@ def get_newman_command(): if newman.check_if_data_valid(entities_data_path): try: - es.index_data(es_data_path, config_path) + es.sync(es_data_path, config_path, 'create') newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: - json.dump(newman_data, fp, indent=4) + json.dump(newman_data, fp) newman_command = get_newman_command() subprocess.Popen(newman_command, shell=True).wait() - es.clear_data(es_data_path, config_path) + es.sync(es_data_path, config_path, 'delete') except Exception as e: traceback.print_exc(file=sys.stdout) print(str(e)) From f37d2ec65d9e850644851099d7a8139bd7afe01b Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Wed, 8 Apr 2020 22:29:52 +0530 Subject: [PATCH 36/78] Refactor generate_newman_data --- postman_tests/lib/newman.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 7f157d365..d7945ab0d 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -74,8 +74,7 @@ def generate_newman_data(entities_data_path): """ data = [] entities_data = read_entities_data(entities_data_path) - for k in entities_data: - data.append(entities_data[k]) + data = list(entities_data.values()) newman_data = [] while True: found = False From 21176553ef9c604864cf664c4ab71314a3b1d4ea Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Wed, 8 Apr 2020 23:33:41 +0530 Subject: [PATCH 37/78] Fix paths --- postman_tests/run_tests.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index c7b97bf69..c60f4bc20 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -8,12 +8,12 @@ import traceback -postman_tests_directory = 'postman_tests' -entities_data_path = 'data/entities/' -newman_data_path = 'data/newman_data.json' -collection_data_path = 'data/ner_collection.json' -es_data_path = 'data/elastic_search/' -config_path = 'config' +postman_tests_directory = os.path.dirname(os.path.abspath(__file__)) +entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') +newman_data_path = os.path.join(postman_tests_directory, 'data', 'newman_data.json') +collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') +es_data_path = os.path.join(postman_tests_directory, 'data', 'elastic_search') +config_path = os.path.join(postman_tests_directory, 'config') def get_newman_command(): @@ -31,11 +31,6 @@ def get_newman_command(): ) -# Switch to postman_tests if not already in that directory -if(os.path.basename(os.getcwd()) != postman_tests_directory): - os.chdir(postman_tests_directory) - - if newman.check_if_data_valid(entities_data_path): try: es.sync(es_data_path, config_path, 'create') From 653c14128565146c30c25d4a1fab62b961391d18 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 01:08:19 +0530 Subject: [PATCH 38/78] Enabled unused import in pylint --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index ace51d00b..ea7376265 100644 --- a/.pylintrc +++ b/.pylintrc @@ -62,7 +62,7 @@ disable=invalid-name, import-error, too-few-public-methods -enable= +enable=unused-import [REPORTS] From 08e999d78eb286a9ca8975b3d96dfcf469ccbf5c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 01:08:34 +0530 Subject: [PATCH 39/78] wip --- postman_tests/lib/es.py | 7 +++++-- postman_tests/lib/newman.py | 2 +- postman_tests/run_tests.py | 28 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/es.py index 8e3392003..8f5fd1008 100644 --- a/postman_tests/lib/es.py +++ b/postman_tests/lib/es.py @@ -33,8 +33,11 @@ def sync(es_data_path, config_path, mode): print(f"Syncing {entity_name}, mode: {mode}") contents = convert_csv_to_dict(file_path, mode) url = get_es_api_url(config_path) - req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) - print(req.text) + try: + req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise e def convert_csv_to_dict(file_path, mode): diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index d7945ab0d..7d2ef08b6 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -30,7 +30,7 @@ def check_if_data_valid(entities_data_path): except Exception as e: print('Invalid json data in ' + path + '\n' + str(e)) - return False + raise e def read_entities_data(entities_data_path): diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index c60f4bc20..e0987af44 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -4,8 +4,7 @@ import json from lib import newman from lib import es -import sys -import traceback + postman_tests_directory = os.path.dirname(os.path.abspath(__file__)) @@ -31,15 +30,16 @@ def get_newman_command(): ) -if newman.check_if_data_valid(entities_data_path): - try: - es.sync(es_data_path, config_path, 'create') - newman_data = newman.generate_newman_data(entities_data_path) - with open(newman_data_path, 'w') as fp: - json.dump(newman_data, fp) - newman_command = get_newman_command() - subprocess.Popen(newman_command, shell=True).wait() - es.sync(es_data_path, config_path, 'delete') - except Exception as e: - traceback.print_exc(file=sys.stdout) - print(str(e)) +try: + newman.check_if_data_valid(entities_data_path) + es.sync(es_data_path, config_path, 'create') + newman_data = newman.generate_newman_data(entities_data_path) + with open(newman_data_path, 'w') as fp: + json.dump(newman_data, fp) + newman_command = get_newman_command() + subprocess.Popen(newman_command, shell=True).wait() + os.remove(newman_data_path) +except Exception as e: + raise e +finally: + es.sync(es_data_path, config_path, 'delete') From 0c40f79f1fdb9d07effe594b244851fb9069d783 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 03:57:10 +0530 Subject: [PATCH 40/78] Renamed es.py to datastore.py --- postman_tests/lib/{es.py => datastore.py} | 0 postman_tests/run_tests.py | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename postman_tests/lib/{es.py => datastore.py} (100%) diff --git a/postman_tests/lib/es.py b/postman_tests/lib/datastore.py similarity index 100% rename from postman_tests/lib/es.py rename to postman_tests/lib/datastore.py diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index e0987af44..89add6310 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -3,7 +3,7 @@ import os import json from lib import newman -from lib import es +from lib import datastore @@ -32,7 +32,7 @@ def get_newman_command(): try: newman.check_if_data_valid(entities_data_path) - es.sync(es_data_path, config_path, 'create') + datastore.sync(es_data_path, config_path, 'create') newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp) @@ -42,4 +42,4 @@ def get_newman_command(): except Exception as e: raise e finally: - es.sync(es_data_path, config_path, 'delete') + datastore.sync(es_data_path, config_path, 'delete') From 27702c4ec5fbffc8515af52715ab504aca2afa81 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 05:52:24 +0530 Subject: [PATCH 41/78] Renamed method in datastore.py --- postman_tests/lib/datastore.py | 4 ++-- postman_tests/run_tests.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 8f5fd1008..68c891d3b 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -7,7 +7,7 @@ from . import common -def get_es_api_url(config_path): +def get_api_url(config_path): if os.path.exists(f"{config_path}/dev.json"): config_file_path = f"{config_path}/dev.json" else: @@ -32,7 +32,7 @@ def sync(es_data_path, config_path, mode): entity_name = common.get_entity_name(file_path) print(f"Syncing {entity_name}, mode: {mode}") contents = convert_csv_to_dict(file_path, mode) - url = get_es_api_url(config_path) + url = get_api_url(config_path) try: req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) req.raise_for_status() diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 89add6310..d2c3a4dbc 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -6,7 +6,6 @@ from lib import datastore - postman_tests_directory = os.path.dirname(os.path.abspath(__file__)) entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') newman_data_path = os.path.join(postman_tests_directory, 'data', 'newman_data.json') From f312439d07a12f1a5574ad91204bd5489edb24aa Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 05:59:03 +0530 Subject: [PATCH 42/78] Removed newman_data.json from gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index f4ae0e9fd..e79792129 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,5 @@ sftp-config.json logs/ .vscode -newman_data.json newman/ dev.json From 018cc004b63003ca5d3dbc5d67a999408df00c2d Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 14:59:19 +0530 Subject: [PATCH 43/78] Fixed docstrings and some refactoring --- postman_tests/lib/newman.py | 54 +++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 7d2ef08b6..227a80333 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -2,19 +2,24 @@ import json import os import glob +import itertools from . import common def check_if_data_valid(entities_data_path): + # type: (str) -> boolean """ Doing basic sanity checking here. Check that every test case is valid json and has the keys: input and expected - Parameters: - entities_data_path (string): Path to the data/entities directory + Args: + entities_data_path (str): Path to the data/entities directory Returns: - boolean: Returns True if all files contain valid json. + (boolean): Returns True if all files contain valid json. + + Raises: + Exception if data is invalid """ try: path = '' @@ -34,15 +39,15 @@ def check_if_data_valid(entities_data_path): def read_entities_data(entities_data_path): - """ - Read all the files in data/entities and generate a single dictionary containing + # type: (str) -> Dict + """Read all the files in data/entities and generate a single dictionary containing data for every entity. - Parameters: - entities_data_path (string): Path to the data/entities directory. + Args: + entities_data_path (string): Path to the data/entities directory. Returns: - dictionary: Dict containing data read from the data files for every entity. + dictionary (dict): Dict containing data read from the data files for every entity. """ entities_data = {} for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): @@ -53,8 +58,8 @@ def read_entities_data(entities_data_path): modified_data = [] for d in data: d1 = {} - for k, v in d['input'].items(): - d1[entity + '_' + k] = d['input'][k] + for key, _ in d['input'].items(): + d1[entity + '_' + key] = d['input'][key] d1[entity + '_expected'] = d['expected'] modified_data.append(d1) entities_data[entity] = modified_data @@ -62,32 +67,23 @@ def read_entities_data(entities_data_path): def generate_newman_data(entities_data_path): - """ - Generate data in a format that can be passed to the command-line tool + # type: (str) -> List[Dict[str, any]] + """Generate data in a format that can be passed to the command-line tool newman for runing the tests. - Parameters: - entities_data_path (string): Path to the data/entities directory. + Args: + entities_data_path (string): Path to the data/entities directory. Returns: - list: List of dictionaries where each dictionary is data for a single test iteration. + newman_data (list): List of dictionaries where each dictionary is data for a single test iteration. """ data = [] entities_data = read_entities_data(entities_data_path) data = list(entities_data.values()) newman_data = [] - while True: - found = False - iteration_data = [] - for item in data: - if item: - iteration_data.append(item.pop(0)) - found = True - if not found: - break - else: - temp = {} - for item in iteration_data: - temp.update(item) - newman_data.append(temp) + for iteration_data in itertools.zip_longest(*data, fillvalue={}): + temp = {} + for item in iteration_data: + temp.update(item) + newman_data.append(temp) return newman_data From c69dfbb20a2966ae37f5305ac7b2c3221b8b1801 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 15:44:07 +0530 Subject: [PATCH 44/78] Restricting to 100 records --- postman_tests/data/elastic_search/city.csv | 1106 -------------------- 1 file changed, 1106 deletions(-) diff --git a/postman_tests/data/elastic_search/city.csv b/postman_tests/data/elastic_search/city.csv index 3f46cfb7c..f3d2f0305 100644 --- a/postman_tests/data/elastic_search/city.csv +++ b/postman_tests/data/elastic_search/city.csv @@ -98,1109 +98,3 @@ Umred,Umred | East,east | Simdega,Simdega | Bahraich,Bahraich | -Kandukur,Kandukur | -Yadgir,Yadgir | -Lucknow,Lucknow | -Muzaffarpur,Muzaffarpur | -Rajapalayam,Rajapalayam | -Rajura,Rajura | -Kayamkulam,Kayamkulam | -Ranibennur,Ranibennur | -Wani,Wani | -Nepanagar,Nepanagar | -Nagapattinam,Nagapattinam | -Afzalpur,Afzalpur | -Meerut,Meerut | -Palampur,Palampur | -Attingal,Attingal | -Batala,Batala | -Rajaldesar,Rajaldesar | -Ladnu,Ladnu | -Pithoragarh,Pithoragarh | -Malpura,Malpura | -Palladam,Palladam | -Padra,Padra | -Mangrulpir,Mangrulpir | -Partur,Partur | -Nahan,Nahan | -Ladwa,Ladwa | -Amalapuram,Amalapuram | -Pilibhit,Pilibhit | -Najibabad,Najibabad | -Maihar,Maihar | -Unjha,Unjha | -Patti,Patti | -Sikanderpur,Sikanderpur | -Tirwaganj,Tirwaganj | -Chalakudy,Chalakudy | -Shahdol,Shahdol | -Siliguri,Siliguri | -Shamli,Shamli | -Sardarshahar,Sardarshahar | -Pandharpur,Pandharpur | -Kathua,Kathua | -Savner,Savner | -Modinagar,Modinagar | -Thiruthuraipoondi,Thiruthuraipoondi | -Gokak,Gokak | -Ichalkaranji,Ichalkaranji | -lucknow,lucknow | -Baripada Town,Baripada |Baripada Town | -Patratu,Patratu | -Dharmavaram,Dharmavaram | -Mhowgaon,Mhowgaon | -Pali,Pali | -Ottappalam,Ottappalam | -Anand,Anand | -Chittur Thathamangalam,Chittur |Thathamangalam | -Tindivanam,Tindivanam | -Thirumangalam,Thirumangalam | -Nuzvid,Nuzvid | -Takhatgarh,Takhatgarh | -Washim,Washim | -Sarsod,Sarsod | -Lahar,Lahar | -Sanawad,Sanawad | -Nandura,Nandura | -Peddapuram,Peddapuram | -Lakhisarai,Lakhisarai | -Rau,Rau | -Narasapuram,Narasapuram | -Pallapatti,Pallapatti | -Ranavav,Ranavav | -Siruguppa,Siruguppa | -Rudauli,Rudauli | -Tirora,Tirora | -Dibrugarh,Dibrugarh | -Tulsipur,Tulsipur | -Tohana,Tohana | -Bhawanipatna,Bhawanipatna | -Alipurduar,Alipurduar | -Pukhrayan,Pukhrayan | -Diphu,Diphu | -Sibsagar,Sibsagar | -Peravurani,Peravurani | -Akot,Akot | -Jhumri Tilaiya,Jhumri |Jhumri Tilaiya | -Mankachar,Mankachar | -Namakkal,Namakkal | -Nakodar,Nakodar | -Salur,Salur | -Niwai,Niwai | -Nirmal,Nirmal | -Tirur,Tirur | -Gohana,Gohana | -Sahjanwa,Sahjanwa | -Maner,Maner | -Nellikuppam,Nellikuppam | -New Delhi,Delhi | New Delhi -Ankleshwar,Ankleshwar | -Sankari,Sankari | -Paramakudi,Paramakudi | -Pauri,Pauri | -Mainaguri,Mainaguri | -Chittoor,Chittoor | -Bankura,Bankura | -Adoor,Adoor | -Rewari,Rewari | -Memari,Memari | -Imphal,Imphal | -ahmedabad,ahmedabad |ahmdabad |amdavad -Mudalagi,Mudalagi | -Titlagarh,Titlagarh | -Bapatla,Bapatla | -Mahesana,Mahesana | -Panchkula,Panchkula | -Tura,Tura | -Dehradun,Dehradun | -Kozhikode,Kozhikode | -Tuensang,Tuensang | -Etawah,Etawah | -Bhiwani,Bhiwani | -Medinipur,Medinipur | -Shirur,Shirur | -Robertson Pet,Robertson |Robertson Pet | -Ujhani,Ujhani | -Vinukonda,Vinukonda | -Mapusa,Mapusa | -Pehowa,Pehowa | -Mandvi,Mandvi | -Wadhwan,Wadhwan | -Eluru,Eluru | -Manasa,Manasa | -Miryalaguda,Miryalaguda | -Vidisha,Vidisha | -Mahasamund,Mahasamund | -Munger,Munger | -Madhubani,Madhubani | -Malkapur,Malkapur | -Motihari,Motihari | -Gopalganj,Gopalganj | -Dhule,Dhule | -Udaipur,Udaipur | -Palai,Palai | -Gooty,Gooty | -Alwar,Alwar | -Korba,Korba | -Rajsamand,Rajsamand | -Deesa,Deesa | -Malegaon,Malegaon | -Umarga,Umarga | -Rangia,Rangia | -Warhapur,Warhapur | -Rampurhat,Rampurhat | -Hajipur,Hajipur | -Parlakhemundi,Parlakhemundi | -Warud,Warud | -Faridabad,Faridabad | -Narsinghgarh,Narsinghgarh | -Buxar,Buxar | -Gurdaspur,Gurdaspur | -Pattukkottai,Pattukkottai | -Sanchore,Sanchore | -Palwal,Palwal | -kolkata,kolkata |calcutta -Kurnool,Kurnool | -Virar,Virar | -Ganjbasoda,Ganjbasoda | -Margao,Margao | -Firozpur Cantt,Firozpur |Cantt | -Taraori,Taraori | -Natham,Natham | -Sendhwa,Sendhwa | -Nilambur,Nilambur | -Shrigonda,Shrigonda | -Mahe,Mahe | -Kothagudem,Kothagudem | -Mungeli,Mungeli | -Kakinada,Kakinada | -Sundargarh,Sundargarh | -Revelganj,Revelganj | -Kancheepuram,Kancheepuram | -Indore,Indore | -Pedana,Pedana | -Shirdi,Shirdi | -Bagaha,Bagaha | -Neemuch,Neemuch | -Nagarkurnool,Nagarkurnool | -Sarangpur,Sarangpur | -Durg,Durg | -Palghar,Palghar | -Tinsukia,Tinsukia | -Purna,Purna | -Uravakonda,Uravakonda | -Madikeri,Madikeri | -Tiruvethipuram,Tiruvethipuram | -Sirsi,Sirsi | -Morvi,Morvi | -Uttar Pradesh,UP | -Tekkalakote,Tekkalakote | -Sirsa,Sirsa | -Udupi,Udupi | -Saunda,Saunda | -Ranaghat,Ranaghat | -Bikaner,Bikaner | -Chhapra,Chhapra | -Sangli,Sangli | -Palitana,Palitana | -Manwath,Manwath | -Kovvur,Kovvur | -Perambalur,Perambalur | -Rohtak,Rohtak | -Thiruvallur,Thiruvallur | -Sankarankovil,Sankarankovil | -Yemmiganur,Yemmiganur | -Modasa,Modasa | -Rafiganj,Rafiganj | -Raghunathpur,Raghunathpur | -Moga,Moga | -Thanesar,Thanesar | -Srikakulam,Srikakulam | -Manawar,Manawar | -Mahalingapura,Mahalingapura | -Nelamangala,Nelamangala | -Naihati,Naihati | -Noida,Noida | -Gudivada,Gudivada | -Sadulpur,Sadulpur | -Perumbavoor,Perumbavoor | -Sadasivpet,Sadasivpet | -Khanna,Khanna | -Mandideep,Mandideep | -Tiruchengode,Tiruchengode | -Tiruvannamalai,Tiruvannamalai | -Tharad,Tharad | -Barnala,Barnala | -Shivamogga,Shivamogga | -Veraval,Veraval | -Sangamner,Sangamner | -West,West | -Vijapur,Vijapur | -Ramgarh,Ramgarh | -Rampur Maniharan,Rampur |Maniharan | -Purnia,Purnia | -Niwari,Niwari | -Nathdwara,Nathdwara | -Sivagiri,Sivagiri | -Bettiah,Bettiah | -Arwal,Arwal | -Saharanpur,Saharanpur | -Karimnagar,Karimnagar | -Mohali,Mohali | -Lunawada,Lunawada | -Ahmednagar,Ahmednagar | -Gumia,Gumia | -Vellore,Vellore | -Nanded,Nanded | -Malur,Malur | -Lingsugur,Lingsugur | -Lal Nindaura,Lal Nindaura |Nindaura | -Narkhed,Narkhed | -Forbesganj,Forbesganj | -Vellakoil,Vellakoil | -Madanapalle,Madanapalle | -Jamui,Jamui | -Warisaliganj,Warisaliganj | -Shegaon,Shegaon | -Nanjikottai,Nanjikottai | -Naharlagun,Naharlagun | -Agartala,Agartala | -Talegaon Dabhade,Talegaon |Dabhade | -Ramagundam,Ramagundam | -Nokha,Nokha | -Sakaleshapura,Sakaleshapura | -Kyathampalle,Kyathampalle | -Lohardaga,Lohardaga | -Bhainsa,Bhainsa | -Robertsganj,Robertsganj | -Sakti,Sakti | -Mandi Dabwali,Mandi Dabwali |Dabwali | -Panaji,Panaji | -Phaltan,Phaltan | -Sangareddy,Sangareddy | -Phulabani,Phulabani | -Pasan,Pasan | -Sihor,Sihor | -Valparai,Valparai | -Tiruchirappalli,Tiruchirappalli | -Marigaon,Marigaon | -Mul,Mul | -Proddatur,Proddatur | -Nadiad,Nadiad | -Shahade,Shahade | -Pattran,Pattran | -Samalkha,Samalkha | -Hindupur,Hindupur | -Sheoganj,Sheoganj | -Uran Islampur,Uran |Islampur | -Thammampatti,Thammampatti | -Kohima,Kohima | -goa,goa | -Tonk,Tonk | -Karnal,Karnal | -Mandla,Mandla | -Mandapeta,Mandapeta | -Palwancha,Palwancha | -Nargund,Nargund | -bhopal,bhopal | -Mayang Imphal,Mayang |Imphal | -Panruti,Panruti | -Ratangarh,Ratangarh | -Dalli Rajhara,Dalli |Rajhara | -Dhuri,Dhuri | -Faridkot,Faridkot | -Sambhal,Sambhal | -Marhaura,Marhaura | -Keshod,Keshod | -Pallikonda,Pallikonda | -Unnamalaikadai,Unnamalaikadai | -Pathanamthitta,Pathanamthitta | -Tasgaon,Tasgaon | -Masaurhi,Masaurhi | -Rayachoti,Rayachoti | -Sundarnagar,Sundarnagar | -Venkatagiri,Venkatagiri | -Tanuku,Tanuku | -pune,pune -Tandur,Tandur | -Tiruvuru,Tiruvuru | -Reoti,Reoti | -Phulpur,Phulpur | -Petlad,Petlad | -Mansa,Mansa | -Sumerpur,Sumerpur | -Malavalli,Malavalli | -Rayadurg,Rayadurg | -Shiggaon,Shiggaon | -Kashipur,Kashipur | -Panvel,Panvel | -Rasra,Rasra | -Paradip,Paradip | -Upleta,Upleta | -Sholavandan,Sholavandan | -Periyakulam,Periyakulam | -Bhimavaram,Bhimavaram | -hyderabad,hyderabad -Asansol,Asansol | -Mahnar Bazar,Mahnar |Mahnar Bazar | -Gangarampur,Gangarampur | -Narsipatnam,Narsipatnam | -Mahemdabad,Mahemdabad | -Rajam,Rajam | -Tharamangalam,Tharamangalam | -Bhadrak,Bhadrak | -Khowai,Khowai | -Raigarh,Raigarh | -Rewa,Rewa | -Sangrur,Sangrur | -Bangalore,Bengaluru |Bangalore -Patna,Patna | -Rajgarh Churu,Rajgarh |Churu | -Puri,Puri | -Nabarangapur,Nabarangapur | -Nagercoil,Nagercoil | -Vandavasi,Vandavasi | -Sivakasi,Sivakasi | -Erode,Erode | -Nagda,Nagda | -Balurghat,Balurghat | -Thiruvalla,Thiruvalla | -Arsikere,Arsikere | -Rawatbhata,Rawatbhata | -Losal,Losal | -Tuni,Tuni | -Jagdalpur,Jagdalpur | -Safidon,Safidon | -Panamattom,Panamattom | -Sagar,Sagar | -Aurangabad,Aurangabad | -Katihar,Katihar | -Rosera,Rosera | -Tarbha,Tarbha | -Loha,Loha | -Solapur,Solapur | -Machilipatnam,Machilipatnam | -Tehri,Tehri | -Manavadar,Manavadar | -Sirsaganj,Sirsaganj | -Udhagamandalam,Udhagamandalam | -Amreli,Amreli | -Sunabeda,Sunabeda | -Mokameh,Mokameh | -Saundatti Yellamma,Saundatti |Yellamma | -Rania,Rania | -Pusad,Pusad | -Thangadh,Thangadh | -Madurai,Madurai | -Aruppukkottai,Aruppukkottai | -Manihari,Manihari | -Adityapur,Adityapur | -Lathi,Lathi | -Jalandhar Cantt,Jalandhar |Jalandhar Cantt | -Nawapur,Nawapur | -Jamalpur,Jamalpur | -Bhadrachalam,Bhadrachalam | -Vadipatti,Vadipatti | -Vizianagaram,Vizianagaram | -Srirampore,Srirampore | -Jamnagar,Jamnagar | -Nainital,Nainital | -Kishanganj,Kishanganj | -Hugli Chinsurah,Hugli |Chinsurah | -Guwahati,Guwahati | -Shahpur,Shahpur | -Sultanganj,Sultanganj | -Nainpur,Nainpur | -mangalore,mangalore | -Murshidabad,Murshidabad | -Tirukalukundram,Tirukalukundram | -Sahawar,Sahawar | -Vadodara,Vadodara | -Manuguru,Manuguru | -Jaipur,Jaipur | -Talcher,Talcher | -Madhugiri,Madhugiri | -Tarakeswar,Tarakeswar | -Punganur,Punganur | -Lonavala,Lonavala | -Pathri,Pathri | -Oddanchatram,Oddanchatram | -Murtijapur,Murtijapur | -Pauni,Pauni | -Nawanshahr,Nawanshahr | -Soyagaon,Soyagaon | -Ozar,Ozar | -Sagara,Sagara | -Phalodi,Phalodi | -Tuljapur,Tuljapur | -Rajgarh Alwar,Rajgarh |Alwar | -Sonamukhi,Sonamukhi | -Nabadwip,Nabadwip | -Surapura,Surapura | -Pipariya,Pipariya | -Lachhmangarh,Lachhmangarh | -Jatani,Jatani | -Rajampet,Rajampet | -Shendurjana,Shendurjana | -Talode,Talode | -Thana Bhawan,Thana Bhawan | -Porbandar,Porbandar | -Hardoi,Hardoi | -Pindwara,Pindwara | -Virudhachalam,Virudhachalam | -Sojat,Sojat | -Nanjangud,Nanjangud | -Sadalagi,Sadalagi | -Pathardi,Pathardi | -Sasvad,Sasvad | -Bhubaneswar,Bhubaneswar | -Karaikal,Karaikal | -Rabkavi Banhatti,Rabkavi |Banhatti | -Nandurbar,Nandurbar | -Chandausi,Chandausi | -Thirupuvanam,Thirupuvanam | -Azamgarh,Azamgarh | -Tanda,Tanda | -Narwana,Narwana | -Viluppuram,Viluppuram | -Kot Kapura,Kot |Kapura | -Sitamarhi,Sitamarhi | -Amroha,Amroha | -Moradabad,Moradabad | -Shrirampur,Shrirampur | -Malappuram,Malappuram | -Thuraiyur,Thuraiyur | -Santipur,Santipur | -Pandharkaoda,Pandharkaoda | -Nowrozabad Khodargama,Nowrozabad |Khodargama | -Shikaripur,Shikaripur | -Ramngarh,Ramngarh | -Palakkad,Palakkad | -Tamluk,Tamluk | -Balaghat,Balaghat | -Udaipurwati,Udaipurwati | -Rasipuram,Rasipuram | -Arrah,Arrah | -Mukerian,Mukerian | -Muvattupuzha,Muvattupuzha | -Bharatpur,Bharatpur | -Bhagalpur,Bhagalpur | -Sirkali,Sirkali | -Sivaganga,Sivaganga | -Patan,Patan | -Todaraisingh,Todaraisingh | -Cooch Behar,Cooch |Behar | -Viswanatham,Viswanatham | -Balangir,Balangir | -Panagar,Panagar | -Vapi,Vapi | -Hawa Jaipur,Hawa |Jaipur | -Salaya,Salaya | -Ranipet,Ranipet | -Kalyan Dombivali,Kalyan |Dombivali | -Nandivaram Guduvancheri,Nandivaram |Guduvancheri | -Nehtaur,Nehtaur | -Chatra,Chatra | -Pondicherry,Pondicherry | -Sheopur,Sheopur | -Kendujhar,Kendujhar | -Shahpura,Shahpura | -Mundargi,Mundargi | -Chilakaluripet,Chilakaluripet | -Raver,Raver | -Jalandhar,Jalandhar | -Taliparamba,Taliparamba | -Kadiri,Kadiri | -Rath,Rath | -Palacole,Palacole | -Bhopal,Bhopal | -Narnaul,Narnaul | -Rajgarh,Rajgarh | -Pandua,Pandua | -Lumding,Lumding | -mumbai,mumbai |bombay | -Sardhana,Sardhana | -Solan,Solan | -Deoghar,Deoghar | -Shivpuri,Shivpuri | -Brahmapur,Brahmapur | -Rajgir,Rajgir | -Adilabad,Adilabad | -Pilkhuwa,Pilkhuwa | -Hisar,Hisar | -Pappinisseri,Pappinisseri | -Obra,Obra | -Valsad,Valsad | -Srinagar,Srinagar | -Kamareddy,Kamareddy | -Ratnagiri,Ratnagiri | -Ajmer,Ajmer | -Kagaznagar,Kagaznagar | -Bhatapara,Bhatapara | -Pollachi,Pollachi | -Mhow Cantonment,Mhow |Cantonment | -Kharagpur,Kharagpur | -Gwalior,Gwalior | -Mulbagal,Mulbagal | -Puranpur,Puranpur | -Madhya Pradesh,MP | -Shajapur,Shajapur | -Sira,Sira | -kochi,kochi | -Bheemunipatnam,Bheemunipatnam | -Jammu,Jammu | -Puliyankudi,Puliyankudi | -Jehanabad,Jehanabad | -Uchgaon,Uchgaon | -Firozpur,Firozpur | -Kasaragod,Kasaragod | -Sedam,Sedam | -Shahganj,Shahganj | -Samdhan,Samdhan | -Goalpara,Goalpara | -Mundi,Mundi | -Pulgaon,Pulgaon | -Thiruvarur,Thiruvarur | -Uran,Uran | -Davanagere,Davanagere | -Pudukkottai,Pudukkottai | -Ramdurg,Ramdurg | -Taki,Taki | -Utraula,Utraula | -Adyar,Adyar | -Mancherial,Mancherial | -Shoranur,Shoranur | -Malda,Malda | -North Lakhimpur,Lakhimpur |North Lakhimpur -Rajula,Rajula | -Visnagar,Visnagar | -Mavoor,Mavoor | -Mattannur,Mattannur | -Sironj,Sironj | -Phulera,Phulera | -Nagaon,Nagaon | -Ramanathapuram,Ramanathapuram | -Murliganj,Murliganj | -Shillong,Shillong | -Pen,Pen | -Barmer,Barmer | -Raikot,Raikot | -Makrana,Makrana | -Jalpaiguri,Jalpaiguri | -Rawatsar,Rawatsar | -Makhdumpur,Makhdumpur | -Dimapur,Dimapur | -Sujanpur,Sujanpur | -Rampur,Rampur | -Rajpipla,Rajpipla | -Waghala,Waghala | -Kendrapara,Kendrapara | -Gurgaon,Gurgaon | -Purulia,Purulia | -Ludhiana,Ludhiana | -Jodhpur,Jodhpur | -Satna,Satna | -Sindhagi,Sindhagi | -Khambhat,Khambhat | -Belonia,Belonia | -Adra,Adra | -Siana,Siana | -Nashik,Nashik | -Kunnamkulam,Kunnamkulam | -Palia Kalan,Palia |Kalan | -Sirohi,Sirohi | -udaipur,udaipur |rajasthan | -Sadulshahar,Sadulshahar | -Mangalvedhe,Mangalvedhe | -Seohara,Seohara | -Polur,Polur | -Kaithal,Kaithal | -Sawantwadi,Sawantwadi | -Bhiwandi,Bhiwandi | -Pratapgarh,Pratapgarh | -Sindagi,Sindagi | -Kalimpong,Kalimpong | -Vrindavan,Vrindavan | -Kapadvanj,Kapadvanj | -Bodhan,Bodhan | -Lonar,Lonar | -Nellore,Nellore | -Fatehabad,Fatehabad | -Mihijam,Mihijam | -Godhra,Godhra | -Hansi,Hansi | -Zirakpur,Zirakpur | -Morena,Morena | -Padmanabhapuram,Padmanabhapuram | -Usilampatti,Usilampatti | -Habra,Habra | -Shikohabad,Shikohabad | -Sandila,Sandila | -Kadapa,Kadapa | -Vijayapura,Vijayapura | -Tirunelveli,Tirunelveli | -Talwara,Talwara | -Sarni,Sarni | -Prithvipur,Prithvipur | -Rehli,Rehli | -Singrauli,Singrauli | -Thanjavur,Thanjavur | -Sausar,Sausar | -Tarn Taran,Tarn |Taran | -Thodupuzha,Thodupuzha | -Aizawl,Aizawl | -Saidpur,Saidpur | -Bhilai Nagar,Bhilai |Bhilai Nagar | -Jhansi,Jhansi | -Koyilandy,Koyilandy | -Kavali,Kavali | -Sattenapalle,Sattenapalle | -Samana,Samana | -Ramganj Mandi,Ramganj |Ramganj Mandi | -Udgir,Udgir | -Wai,Wai | -Siddipet,Siddipet | -Urmar Tanda,Urmar |Tanda | -Seoni Malwa,Seoni |Malwa | -Nangal,Nangal | -Tumsar,Tumsar | -Barbil,Barbil | -Parasi,Parasi | -Shamgarh,Shamgarh | -Bhilwara,Bhilwara | -Punalur,Punalur | -Repalle,Repalle | -Mandsaur,Mandsaur | -Porsa,Porsa | -Saiha,Saiha | -Tittakudi,Tittakudi | -Srikalahasti,Srikalahasti | -Varkala,Varkala | -Sherkot,Sherkot | -Silvassa,Silvassa | -Bhavnagar,Bhavnagar | -Arambagh,Arambagh | -Mariani,Mariani | -Suri,Suri | -Zunheboto,Zunheboto | -Sadabad,Sadabad | -Baleshwar Town,Baleshwar | | -bengaluru,bengaluru |bangalore | -Tirupati,Tirupati | -Warangal,Warangal | -Gobichettipalayam,Gobichettipalayam | -Yerraguntla,Yerraguntla | -Chikkamagaluru,Chikkamagaluru | -Sitarganj,Sitarganj | -Virudhunagar,Virudhunagar | -Tumkur,Tumkur | -Tikamgarh,Tikamgarh | -Nawada,Nawada | -chandigarh,chandigarh | -Sinnar,Sinnar | -Ballari,Ballari | -Begusarai,Begusarai | -Shujalpur,Shujalpur | -Vikramasingapuram,Vikramasingapuram | -Rajauri,Rajauri | -Dumka,Dumka | -Sailu,Sailu | -Thakurdwara,Thakurdwara | -Bhuj,Bhuj | -Sunam,Sunam | -Parvathipuram,Parvathipuram | -Sangaria,Sangaria | -Hapur,Hapur | -Udumalaipettai,Udumalaipettai | -Gobindgarh,Gobindgarh | -Osmanabad,Osmanabad | -Ramanagaram,Ramanagaram | -Silchar,Silchar | -Savarkundla,Savarkundla | -Wankaner,Wankaner | -Bargarh,Bargarh | -Fazilka,Fazilka | -Rajakhera,Rajakhera | -Chirmiri,Chirmiri | -Vaikom,Vaikom | -Samalkot,Samalkot | -Mehkar,Mehkar | -Phusro,Phusro | -Rajahmundry,Rajahmundry | -Manglaur,Manglaur | -Shahbad,Shahbad | -Neyveli,Neyveli | -Sagwara,Sagwara | -Margherita,Margherita | -Kadi,Kadi | -Sri Madhopur,Sri Madhopur |Madhopur | -Sankeshwara,Sankeshwara | -Kanhangad,Kanhangad | -Umarkhed,Umarkhed | -Ponnani,Ponnani | -Nohar,Nohar | -Rameshwaram,Rameshwaram | -Uthiramerur,Uthiramerur | -Terdal,Terdal | -Mandamarri,Mandamarri | -Tharangambadi,Tharangambadi | -Kottayam,Kottayam | -Prantij,Prantij | -Jagraon,Jagraon | -Nadbai,Nadbai | -Adalaj,Adalaj | -Panagudi,Panagudi | -Maharajganj,Maharajganj | -Araria,Araria | -Thrissur,Thrissur | -Yawal,Yawal | -Malout,Malout | -Dumraon,Dumraon | -Ramtek,Ramtek | -Sidlaghatta,Sidlaghatta | -Zira,Zira | -Rayagada,Rayagada | -Tenkasi,Tenkasi | -Bhabua,Bhabua | -Soro,Soro | -Lanka,Lanka | -Tenali,Tenali | -Mussoorie,Mussoorie | -Sahibganj,Sahibganj | -Multai,Multai | -Paravoor,Paravoor | -Paschim Punropara,Paschim Punropara |Punropara | -Chandigarh,Chandigarh | -Purquazi,Purquazi | -Gaya,Gaya | -Coal Dhanbad,Coal Dhanbad |Dhanbad | -Vijainagar Ajmer,Vijainagar |Ajmer | -Panna,Panna | -Udhampur,Udhampur | -Bilaspur,Bilaspur | -Renukoot,Renukoot | -Pinjore,Pinjore | -Palanpur,Palanpur | -Pardi,Pardi | -Talikota,Talikota | -Lunglei,Lunglei | -Pithampur,Pithampur | -Rishikesh,Rishikesh | -Mauganj,Mauganj | -Ranebennuru,Ranebennuru | -Mandawa,Mandawa | -Sikandra Rao,Sikandra |Sikandra Rao | -Vaniyambadi,Vaniyambadi | -Kolar,Kolar | -Amalner,Amalner | -Vaijapur,Vaijapur | -Vijaypur,Vijaypur | -Naila Janjgir,Naila |Janjgir | -Alirajpur,Alirajpur | -Mira Bhayandar,Mira |Bhayandar | -Rajagangapur,Rajagangapur | -Jaggaiahpet,Jaggaiahpet | -Orai,Orai | -Mandalgarh,Mandalgarh | -Roorkee,Roorkee | -Mangrol,Mangrol | -Arvi,Arvi | -Rahuri,Rahuri | -Ponnur,Ponnur | -Malaj Khand,Malaj Khand|Malajkhand | -Mysore,Mysore | -Thane,Thane | -Nedumangad,Nedumangad | -Pihani,Pihani | -Jabalpur,Jabalpur | -Sonepur,Sonepur | -Tiruppur,Tiruppur | -Manendragarh,Manendragarh | -Nandgaon,Nandgaon | -Patur,Patur | -Pakaur,Pakaur | -Suriyampalayam,Suriyampalayam | -Gudur,Gudur | -Theni Allinagaram,Theni |Allinagaram | -Tezpur,Tezpur | -Bageshwar,Bageshwar | -Peringathur,Peringathur | -Yanam,Yanam | -Ambejogai,Ambejogai | -Kailasahar,Kailasahar | -Lonavla,Lonavla | -Talaja,Talaja | -Yamunanagar,Yamunanagar | -Kharar,Kharar | -Unnao,Unnao | -Palasa Kasibugga,Palasa |Kasibugga | -Kalpi,Kalpi | -Tiruttani,Tiruttani | -Pachora,Pachora | -Dhubri,Dhubri | -Pilibanga,Pilibanga | -Sangole,Sangole | -Shanti Leh,Shanti |Leh | -Bhongir,Bhongir | -Sultanpur,Sultanpur | -Samastipur,Samastipur | -Panchla,Panchla | -Manachanallur,Manachanallur | -Umbergaon,Umbergaon | -Chirkunda,Chirkunda | -Mount Abu,Mount Abu |Abu | -Raghunathganj,Raghunathganj | -Murwara Katni,Murwara |Katni | -Mahuva,Mahuva | -Darbhanga,Darbhanga | -Nasirabad,Nasirabad | -Kodungallur,Kodungallur | -Surat,Surat | -Hoshiarpur,Hoshiarpur | -Pathankot,Pathankot | -Qadian,Qadian | -Tadepalligudem,Tadepalligudem | -Pudupattinam,Pudupattinam | -Narasaraopet,Narasaraopet | -Rudrapur,Rudrapur | -Koratla,Koratla | -Nizamabad,Nizamabad | -Monoharpur,Monoharpur | -Raisen,Raisen | -Shishgarh,Shishgarh | -Mathabhanga,Mathabhanga | -Raayachuru,Raayachuru | -Jharsuguda,Jharsuguda | -Rairangpur,Rairangpur | -Ramnagar,Ramnagar | -Sawai Madhopur,Sawai |Madhopur | -Mudhol,Mudhol | -Visakhapatnam,Visakhapatnam | -Umreth,Umreth | -Satana,Satana | -Rajpura,Rajpura | -Guruvayoor,Guruvayoor | -Agra,Agra | -Vadgaon Kasba,Vadgaon |Kasba | -Piro,Piro | -Pavagada,Pavagada | -Beed,Beed | -Muddebihal,Muddebihal | -Nalbari,Nalbari | -Pacode,Pacode | -Wara Seoni,Wara |Seoni | -Uthamapalayam,Uthamapalayam | -Adoni,Adoni | -Parangipettai,Parangipettai | -Phillaur,Phillaur | -Vadnagar,Vadnagar | -Viramgam,Viramgam | -Soron,Soron | -Kolkata,Kolkata | -Mavelikkara,Mavelikkara | -Mandi,Mandi | -Nagla,Nagla | -Narkatiaganj,Narkatiaganj | -Wadi,Wadi | -Coimbatore,Coimbatore | -Shrirangapattana,Shrirangapattana | -Rajkot,Rajkot | -Renigunta,Renigunta | -Naraura,Naraura | -Radhanpur,Radhanpur | -Samthar,Samthar | -Yevla,Yevla | -Thoubal,Thoubal | -Marmagao,Marmagao | -Amravati,Amravati | -Lakheri,Lakheri | -Rajnandgaon,Rajnandgaon | -Surandai,Surandai | -Suar,Suar | -Nagina,Nagina | -Hazaribag,Hazaribag | -Ashok Nagar,Ashok | -Anantapur,Anantapur | -Noorpur,Noorpur | -Maddur,Maddur | -PNPatti,PNPatti | -Pattamundai,Pattamundai | -Montage,Montage | -Musabani,Musabani | -Anakapalle,Anakapalle | -Raiganj,Raiganj | -Cuttack,Cuttack | -Alappuzha,Alappuzha | -Sherghati,Sherghati | -Vadakkuvalliyur,Vadakkuvalliyur | -Markapur,Markapur | -Raurkela,Raurkela | -Barpeta,Barpeta | -Maharajpur,Maharajpur | -Puttur,Puttur | -Navsari,Navsari | -Hyderabad,Hyderabad | -Pandhurna,Pandhurna | -Dharmanagar,Dharmanagar | -Ron,Ron | -Navalgund,Navalgund | -Tilda Newra,Tilda |Newra | -Sattur,Sattur | -Vishnupad Gaya,Vishnupad |Gaya | -Vatakara,Vatakara | -Sohagpur,Sohagpur | -Punch,Punch | -Magadi,Magadi | -Athni,Athni | -Barh,Barh | -Macherla,Macherla | -Dhanbad,Dhanbad | -Ratlam,Ratlam | -Sheikhpura,Sheikhpura | -Morshi,Morshi | -Firozabad,Firozabad | -Rampura Phul,Rampura |Phul | -Mhaswad,Mhaswad | -Haldwani Kathgodam,Haldwani |Kathgodam | -Farooqnagar,Farooqnagar | -Sillod,Sillod | -Supaul,Supaul | -Khammam,Khammam | -Anantnag,Anantnag | -Lilong,Lilong | -Lalsot,Lalsot | -Zaidpur,Zaidpur | -Pernampattu,Pernampattu | -Nowgong,Nowgong | -Una,Una | -Srinivaspur,Srinivaspur | -Ranchi,Ranchi | -Palani,Palani | -Songadh,Songadh | -Pune,Pune | -Punjaipugalur,Punjaipugalur | -Sheohar,Sheohar | -Shimla,Shimla | -Lalitpur,Lalitpur | -Manvi,Manvi | -Salem,Salem | -Changanassery,Changanassery | -Sihora,Sihora | -Paithan,Paithan | -Jorhat,Jorhat | -Baramula,Baramula | -patna,patna | -Panniyannur,Panniyannur | -Malkangiri,Malkangiri | -Shirpur Warwade,Shirpur |Warwade | -Nandyal,Nandyal | -Allahabad,Allahabad | -Pasighat,Pasighat | -Anjar,Anjar | -Chaibasa,Chaibasa | -Laharpur,Laharpur | -Mahad,Mahad | -Panipat,Panipat | -Sindhnur,Sindhnur | -Bellampalle,Bellampalle | -Silapathar,Silapathar | -Sikar,Sikar | -Nanpara,Nanpara | -Narayanpet,Narayanpet | -Asarganj,Asarganj | -Limbdi,Limbdi | -Sikandrabad,Sikandrabad | -Naugachhia,Naugachhia | -Tiruchendur,Tiruchendur | -Tadpatri,Tadpatri | -Bahadurgarh,Bahadurgarh | -Nilanga,Nilanga | -Ongole,Ongole | -Parbhani,Parbhani | -Pithapuram,Pithapuram | -Nimbahera,Nimbahera | -Karwar,Karwar | -Namagiripettai,Namagiripettai | -Mumbai,Mumbai |Bombay -Kapurthala,Kapurthala | -Nakur,Nakur | -Ponneri,Ponneri | -Kollam,Kollam | -indore,indore | -Ramachandrapuram,Ramachandrapuram | -Lalganj,Lalganj | -Sambalpur,Sambalpur | -Periyasemur,Periyasemur | -Sholingur,Sholingur | -Ambikapur,Ambikapur | -Madhupur,Madhupur | -Jammalamadugu,Jammalamadugu | -Sehore,Sehore | -Hubli Dharwad,Hubli |Dharwad | -Nautanwa,Nautanwa | -Raipur,Raipur | -Rae Bareli,Rae Bareli |Bareli | -Sabalgarh,Sabalgarh | -Yellandu,Yellandu | -Pilani,Pilani | -Vadalur,Vadalur | -Lakshmeshwar,Lakshmeshwar | -Dhenkanal,Dhenkanal | -Manmad,Manmad | -Medininagar Daltonganj,Medininagar |Daltonganj | -Shahjahanpur,Shahjahanpur | -Bobbili,Bobbili | -Malerkotla,Malerkotla | -Padrauna,Padrauna | -Sanand,Sanand | -Sircilla,Sircilla | -Byasanagar,Byasanagar | -Raisinghnagar,Raisinghnagar | -Tilhar,Tilhar | -Naidupet,Naidupet | -Anjangaon,Anjangaon | -Jangaon,Jangaon | -Jhargram,Jhargram | -Pachore,Pachore | -Taranagar,Taranagar | -Guntur,Guntur | -Manjlegaon,Manjlegaon | -Medak,Medak | -Mandya,Mandya | -Lar,Lar | -Kannur,Kannur | -Warora,Warora | -Longowal,Longowal | -Saharsa,Saharsa | -Nabha,Nabha | -Mukhed,Mukhed | -Suratgarh,Suratgarh | -Sullurpeta,Sullurpeta | -Mirganj,Mirganj | -Risod,Risod | -Shenkottai,Shenkottai | -Sambhar,Sambhar | -Darjiling,Darjiling | -Itarsi,Itarsi | -Gadwal,Gadwal | -Akola,Akola | -Dhoraji,Dhoraji | -Wokha,Wokha | -Latur,Latur | -Vasai,Vasai | -Neyyattinkara,Neyyattinkara | -Srivilliputhur,Srivilliputhur | -Vijayawada,Vijayawada | -Belagavi,Belagavi | -Chirala,Chirala | -Tundla,Tundla | -Jamshedpur,Jamshedpur | -Suryapet,Suryapet | -Perinthalmanna,Perinthalmanna | -Sathyamangalam,Sathyamangalam | -Siwan,Siwan | -Aligarh,Aligarh | -Raxaul Bazar,Raxaul |Raxaul Bazar | -Silao,Silao | -Lakhimpur,Lakhimpur | -koramangala,koramangala \ No newline at end of file From 629c7a3893938a831dffc70bd2e63c65167fe1b9 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 15:44:13 +0530 Subject: [PATCH 45/78] wip --- postman_tests/run_tests.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index d2c3a4dbc..cb852de32 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -import subprocess +from subprocess import Popen,PIPE import os import json from lib import newman @@ -36,9 +36,11 @@ def get_newman_command(): with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp) newman_command = get_newman_command() - subprocess.Popen(newman_command, shell=True).wait() - os.remove(newman_data_path) + process = Popen(newman_command, shell=True) + (out, err) = process.communicate() + print(process.returncode) except Exception as e: raise e finally: + os.remove(newman_data_path) datastore.sync(es_data_path, config_path, 'delete') From 529fd39ad9ae965755914f23e1a6da73c0c06f08 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 16:00:23 +0530 Subject: [PATCH 46/78] Passing newman return code to parent process --- postman_tests/run_tests.py | 60 ++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index cb852de32..41e0f9051 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -1,20 +1,27 @@ from __future__ import absolute_import -from subprocess import Popen,PIPE +from subprocess import Popen import os import json +import sys from lib import newman from lib import datastore postman_tests_directory = os.path.dirname(os.path.abspath(__file__)) -entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') newman_data_path = os.path.join(postman_tests_directory, 'data', 'newman_data.json') -collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') -es_data_path = os.path.join(postman_tests_directory, 'data', 'elastic_search') config_path = os.path.join(postman_tests_directory, 'config') def get_newman_command(): + """ Returns the newman shell command to be used for running the tests + + Args: + None + + Returns: + (str): The shell command to be used for running the tests + """ + collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') if os.path.exists(f"{config_path}/dev.json"): environment_file_path = f'{config_path}/dev.json' return ( @@ -29,18 +36,33 @@ def get_newman_command(): ) -try: - newman.check_if_data_valid(entities_data_path) - datastore.sync(es_data_path, config_path, 'create') - newman_data = newman.generate_newman_data(entities_data_path) - with open(newman_data_path, 'w') as fp: - json.dump(newman_data, fp) - newman_command = get_newman_command() - process = Popen(newman_command, shell=True) - (out, err) = process.communicate() - print(process.returncode) -except Exception as e: - raise e -finally: - os.remove(newman_data_path) - datastore.sync(es_data_path, config_path, 'delete') +def run_tests(): + """ Runs the newman test-suite + + Args: + None + + Returns: + (str): The return code of the newman command + """ + entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') + es_data_path = os.path.join(postman_tests_directory, 'data', 'elastic_search') + try: + newman.check_if_data_valid(entities_data_path) + datastore.sync(es_data_path, config_path, 'create') + newman_data = newman.generate_newman_data(entities_data_path) + with open(newman_data_path, 'w') as fp: + json.dump(newman_data, fp) + newman_command = get_newman_command() + process = Popen(newman_command, shell=True) + process.communicate() + os.remove(newman_data_path) + return process.returncode + except Exception as e: + raise e + finally: + datastore.sync(es_data_path, config_path, 'delete') + +if __name__ == "__main__": + status = run_tests() + sys.exit(status) From fdd62ef4b51ac0016eea69adc783d1c216b3f44f Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 17:18:25 +0530 Subject: [PATCH 47/78] Changed the name of newman reports folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e79792129..58b20ae6f 100644 --- a/.gitignore +++ b/.gitignore @@ -105,5 +105,5 @@ sftp-config.json logs/ .vscode -newman/ +newman_reports/ dev.json From 39df54d08e119357c9eee1f465b34e989384ba1f Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 17:18:51 +0530 Subject: [PATCH 48/78] Fixed entity name and removed failing test cases --- .../data/entities/text_restaurant.json | 140 +++--------------- 1 file changed, 24 insertions(+), 116 deletions(-) diff --git a/postman_tests/data/entities/text_restaurant.json b/postman_tests/data/entities/text_restaurant.json index 40e5a9ad6..41db87ccc 100644 --- a/postman_tests/data/entities/text_restaurant.json +++ b/postman_tests/data/entities/text_restaurant.json @@ -2,7 +2,7 @@ { "input": { "message": "Where is Coffee On Canvas", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -14,7 +14,7 @@ { "input": { "message": "Find me fine dining restaurants near Novotel", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -26,7 +26,7 @@ { "input": { "message": "Fastest route of going to the farm", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -38,7 +38,7 @@ { "input": { "message": "i would like to go to Shudh Vegetarian Food Court someday", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -50,7 +50,7 @@ { "input": { "message": "I want to go to SP's Biryani", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -62,7 +62,7 @@ { "input": { "message": "I work in The Rasoi", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -74,7 +74,7 @@ { "input": { "message": "Where is Gokul Chaat located ?", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -83,34 +83,10 @@ } ] }, - { - "input": { - "message": "I wish to go to Tatva - Country Inn & Suites by Carlson one day", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "tatva - country inn & suites by carlson", - "value": "Tatva - Country Inn & Suites by Carlson" - } - ] - }, - { - "input": { - "message": "How far is Deja Vu from Eatmosphere", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "deja vu, eatmosphere", - "value": "Deja Vu, Eatmosphere" - } - ] - }, { "input": { "message": "What are some must visit places in and around By The Way", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -122,7 +98,7 @@ { "input": { "message": "Search for Cafes with free WiFi between Andheri & Bandra", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -133,7 +109,7 @@ { "input": { "message": "I have been to The Fisherman's Deck", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -145,7 +121,7 @@ { "input": { "message": "Can I get best momos in N Asian", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -157,7 +133,7 @@ { "input": { "message": "Find me fine dining restaurants in and around L'amandier", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -169,7 +145,7 @@ { "input": { "message": "where is the nearest gas station", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -180,7 +156,7 @@ { "input": { "message": "My favorite restaurant is High", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -192,7 +168,7 @@ { "input": { "message": "is there an age limit in Kebab-e-Que", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -201,22 +177,10 @@ } ] }, - { - "input": { - "message": "Lets go to K.F.C.", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "kfc", - "value": "kfc" - } - ] - }, { "input": { "message": "Why not go to Gossip", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -228,7 +192,7 @@ { "input": { "message": "Zafrani is famous for biryani", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -240,7 +204,7 @@ { "input": { "message": "BottleRock serves alcohol ?", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -249,22 +213,10 @@ } ] }, - { - "input": { - "message": "blueFROG, has an amazing ambience", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "bluefrog", - "value": "blueFROG" - } - ] - }, { "input": { "message": "Shangri-La is famous for spanish food", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -276,7 +228,7 @@ { "input": { "message": "Sattvik serves best meal in Goregaon", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -288,7 +240,7 @@ { "input": { "message": "Hippie@Heart is only for adults", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ { @@ -297,63 +249,19 @@ } ] }, - { - "input": { - "message": "PizzaTito is the only place that delivers pizzas", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "pizzavito", - "value": "PizzaVito" - } - ] - }, - { - "input": { - "message": "show me directions from 18 James Long to Rassaa - Jasa Hava Tassaa", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "18 james long", - "value": "18 James Long" - }, - { - "original_text": "rassaa - jasa hava tassaa", - "value": "Rassa - Jasa Hava Tassaa" - } - ] - }, { "input": { "message": "Kadambam Iyengar Cuisine to The Post Office Cafe distance?", - "entity_name": "restaurant_list" + "entity_name": "restaurant" }, "expected": [ - { - "original_text": "kadambam iyengar cuisine", - "value": "Kadambam Iyengar Cuisine" - }, { "original_text": "the post office cafe", "value": "The Post Office Cafe" - } - ] - }, - { - "input": { - "message": "How do I reach from Metro to DEPOT29", - "entity_name": "restaurant_list" - }, - "expected": [ - { - "original_text": "metro", - "value": "Metro" }, { - "original_text": "depot29", - "value": "DEPOT29" + "original_text": "kadambam iyengar cuisine", + "value": "Kadambam Iyengar Cuisine" } ] } From 79ad94bef0a6aadd29f4a3b28fcb00e3321996ff Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 17:19:26 +0530 Subject: [PATCH 49/78] Specify the path to report output folder --- postman_tests/run_tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 41e0f9051..dbb1d6dde 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -22,11 +22,13 @@ def get_newman_command(): (str): The shell command to be used for running the tests """ collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') + report_path = os.path.join(postman_tests_directory, 'newman_reports') if os.path.exists(f"{config_path}/dev.json"): environment_file_path = f'{config_path}/dev.json' return ( f'newman run {collection_data_path} -d {newman_data_path}' f' -e {environment_file_path} -r cli,htmlextra --reporter-htmlextra-logs' + f' --reporter-htmlextra-export {report_path}' ) else: environment_file_path = f'{config_path}/prod.json' From 3173cf386452fe24dbce8e12d1003bcd3852af0c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 17:24:53 +0530 Subject: [PATCH 50/78] Updated Readme --- postman_tests/Readme.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index d6fe03981..65810c29d 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -11,7 +11,7 @@ A shortcut for running the above is available. Just run ```./run_postman_tests.s **Viewing the test results in dev** -Running the above command in dev will create a ```newman/``` directory in the ```postman_tests/``` folder. The newman command will generate a new timestamped html file everytime the tests are run. This html file contains a graphical dashboard which can be used to see the status of running the tests, failures etc, and yes you can use dark mode as well ;-). +Running the above command in dev will create a ```newman_reports/``` directory in the ```postman_tests/``` folder. The newman command will generate a new timestamped html file in that directory everytime the tests are run. This html file contains a graphical dashboard which can be used to see the status of running the tests, failures etc, and yes you can use dark mode as well ;-). ![newman dashboard](newman.png) @@ -24,20 +24,39 @@ The format should follow the below structure: ``` [ "input": { - + "message": "The text sent in url", + "entity_name": "Name of the entity e.g. time" }, "expected": [ { - + } ] ] ``` -input contains the parameters that we pass as query parameters in the GET rquest. +input contains the parameters that we pass as query parameters in the GET rquest. It must **mandatorily** contain two keys, +message and entity_name. + +expected is an array of objects that we get in the response. If expected contains multiple objects then the order of those should be exactly as the expected order in the response. + +If you want to create a test case where the output of a request is expected to be null, then specify the test case as follows: + +``` +[ + "input": { + "message": "The text sent in url", + "entity_name": "Name of the entity e.g. time" + }, -expected is an array of objects that we get in the response. + "expected": [ + { + "data": null + } + ] +] +``` Add tests for the new entity using steps given below in this document and send a PR containing the new collection and data. @@ -66,4 +85,13 @@ Use the below steps: **Adding data to be indexed into ElasticSearch** -Add the csv file for the particular entity in postman_tests/data/elastic_search/ and send a PR. +1. Add the csv file for the particular entity in postman_tests/data/elastic_search/. + +2. Make sure all the required data being used in the tests is present in the csv file and all tests are passing. + +3. Send a PR. + + +**Future Roadmap** + +1. Some test cases are failing for various entities and we are maintaining a list of the same. These will need to be integrated into the test-suite once fixed. From 9132d576d6b8110578a390866f2db37d3cf71c9c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 10 Apr 2020 17:47:06 +0530 Subject: [PATCH 51/78] Fix return code type --- postman_tests/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index dbb1d6dde..1bb7a4b5c 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -45,7 +45,7 @@ def run_tests(): None Returns: - (str): The return code of the newman command + (int): The return code of the newman command """ entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') es_data_path = os.path.join(postman_tests_directory, 'data', 'elastic_search') From 54ba6529413b08d1b26f2d460e1390bbabeb95a0 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 21 Apr 2020 15:49:56 +0530 Subject: [PATCH 52/78] Rename city.csv to city_list.csv --- postman_tests/data/elastic_search/{city.csv => city_list.csv} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename postman_tests/data/elastic_search/{city.csv => city_list.csv} (100%) diff --git a/postman_tests/data/elastic_search/city.csv b/postman_tests/data/elastic_search/city_list.csv similarity index 100% rename from postman_tests/data/elastic_search/city.csv rename to postman_tests/data/elastic_search/city_list.csv From 9af0f27f136715c688c6e3609ba142585444367c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 21 Apr 2020 15:50:49 +0530 Subject: [PATCH 53/78] Add missing data to be indexed for text entity and fix test --- .../data/elastic_search/city_list.csv | 103 ++---------------- postman_tests/data/entities/text_city.json | 8 +- 2 files changed, 14 insertions(+), 97 deletions(-) diff --git a/postman_tests/data/elastic_search/city_list.csv b/postman_tests/data/elastic_search/city_list.csv index f3d2f0305..d71d75c9a 100644 --- a/postman_tests/data/elastic_search/city_list.csv +++ b/postman_tests/data/elastic_search/city_list.csv @@ -1,100 +1,17 @@ value,variants -Satara,Satara | -Nidadavole,Nidadavole | -Umaria,Umaria | -Mangaldoi,Mangaldoi | -Mudabidri,Mudabidri | -Zamania,Zamania | -Rapar,Rapar | -Tiptur,Tiptur | -Arakkonam,Arakkonam | -Sopore,Sopore | -Tarana,Tarana | -Mangaluru,Mangaluru | -guwahati,guwahati | -Baharampur,Baharampur | -Karur,Karur | -Sahaspur,Sahaspur | -Parli,Parli | -Khair,Khair | -Chennai,Chennai | -Shahabad,Shahabad | -Karimganj,Karimganj | -Sahaswan,Sahaswan | -jaipur,jaipur |rajasthan | -Guntakal,Guntakal | -Dhamtari,Dhamtari | -Savanur,Savanur | -Mahidpur,Mahidpur | -Mathura,Mathura | -Sainthia,Sainthia | -Puthuppally,Puthuppally | -Kochi,Kochi | +mumbai,mumbai |bombay | +New Delhi,Delhi | New Delhi +chennai,chennai |madras |tamilnadu | +kochi,kochi | Safipur,Safipur | -Jagtial,Jagtial | -Muktsar,Muktsar | -Rahatgarh,Rahatgarh | -Patiala,Patiala | Ahmedabad,Ahmedabad | -Ratia,Ratia | -Reengus,Reengus | -Charkhi Dadri,Charkhi |Dadri | Varanasi,Varanasi | -chennai,chennai |madras |tamilnadu | -Sugauli,Sugauli | -Lalgudi,Lalgudi | -Sidhpur,Sidhpur | -Tirukkoyilur,Tirukkoyilur | -Sadri,Sadri | -Tirupathur,Tirupathur | -Port Blair,Port Blair |Blair | -Vedaranyam,Vedaranyam | -Yavatmal,Yavatmal | -Vikarabad,Vikarabad | -Todabhim,Todabhim | Phagwara,Phagwara | -Purwa,Purwa | -Bharuch,Bharuch | -Bathinda,Bathinda | -Wardha,Wardha | -Thiruvananthapuram,Thiruvananthapuram | -Sitapur,Sitapur | -Sidhi,Sidhi | -Rupnagar,Rupnagar | -Achhnera,Achhnera | -Kanpur,Kanpur | -Vyara,Vyara | -Raghogarh Vijaypur,Raghogarh |Vijaypur | -Sandi,Sandi | -Piriyapatna,Piriyapatna | -Ujjain,Ujjain | -Mahbubnagar,Mahbubnagar | -Achalpur,Achalpur | -Nongstoin,Nongstoin | -Vita,Vita | -Naugawan Sadat,Naugawan |Sadat | -Sasaram,Sasaram | -Ghaziabad,Ghaziabad | -Mokokchung,Mokokchung | -Sonipat,Sonipat | -Loni,Loni | -Jind,Jind | -Sohna,Sohna | -Motipur,Motipur | -Nawabganj,Nawabganj | -Tarikere,Tarikere | Fatehpur Sikri,Fatehpur |Sikri | -Sujangarh,Sujangarh | -Karjat,Karjat | -Powayan,Powayan | -Giridih,Giridih | -Madhepura,Madhepura | +Fatehpur Sikri,Fatehpur |Sikri | +Muzaffarpur,Muzaffarpur | +Meerut,Meerut | +Paschim Punropara,Paschim Punropara |Punropara | +Patiala,Patiala | Amritsar,Amritsar | -Sanduru,Sanduru | -Wanaparthy,Wanaparthy | -Cherthala,Cherthala | -Hardwar,Hardwar | -Umred,Umred | -East,east | -Simdega,Simdega | -Bahraich,Bahraich | +Ludhiana,Ludhiana | \ No newline at end of file diff --git a/postman_tests/data/entities/text_city.json b/postman_tests/data/entities/text_city.json index 75615303f..4ecf6c58e 100644 --- a/postman_tests/data/entities/text_city.json +++ b/postman_tests/data/entities/text_city.json @@ -201,13 +201,13 @@ }, { "expected": [ - { - "original_text": "ludhiana", - "value": "Ludhiana" - }, { "original_text": "patiala", "value": "Patiala" + }, + { + "original_text": "ludhiana", + "value": "Ludhiana" } ], "input": { From 6a0ed1b7a021e4fe9e47a8f01a08a998a039b5c1 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Tue, 21 Apr 2020 16:09:18 +0530 Subject: [PATCH 54/78] Prefixing ner_ptest for entities to be indexed --- ...{city_list.csv => ner_ptest_city_list.csv} | 0 ...estaurant.csv => ner_ptest_restaurant.csv} | 0 postman_tests/data/entities/city.json | 6 +-- postman_tests/data/entities/text_city.json | 36 +++++++-------- .../data/entities/text_restaurant.json | 44 +++++++++---------- 5 files changed, 43 insertions(+), 43 deletions(-) rename postman_tests/data/elastic_search/{city_list.csv => ner_ptest_city_list.csv} (100%) rename postman_tests/data/elastic_search/{restaurant.csv => ner_ptest_restaurant.csv} (100%) diff --git a/postman_tests/data/elastic_search/city_list.csv b/postman_tests/data/elastic_search/ner_ptest_city_list.csv similarity index 100% rename from postman_tests/data/elastic_search/city_list.csv rename to postman_tests/data/elastic_search/ner_ptest_city_list.csv diff --git a/postman_tests/data/elastic_search/restaurant.csv b/postman_tests/data/elastic_search/ner_ptest_restaurant.csv similarity index 100% rename from postman_tests/data/elastic_search/restaurant.csv rename to postman_tests/data/elastic_search/ner_ptest_restaurant.csv diff --git a/postman_tests/data/entities/city.json b/postman_tests/data/entities/city.json index 838e5b3fa..866db8b0f 100644 --- a/postman_tests/data/entities/city.json +++ b/postman_tests/data/entities/city.json @@ -2,7 +2,7 @@ { "input": { "message": "i want to go to mumbai", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" }, "expected": [ { @@ -17,7 +17,7 @@ { "input": { "message": "i want to go to delhi", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" }, "expected": [ { @@ -32,7 +32,7 @@ { "input": { "message": "i want to go to chennai", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" }, "expected": [ { diff --git a/postman_tests/data/entities/text_city.json b/postman_tests/data/entities/text_city.json index 4ecf6c58e..935b52f3d 100644 --- a/postman_tests/data/entities/text_city.json +++ b/postman_tests/data/entities/text_city.json @@ -8,7 +8,7 @@ ], "input": { "message": "Where can I get best Biryani in kochi", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -20,7 +20,7 @@ ], "input": { "message": "Find me fine dining restaurants in and around safipur", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -31,7 +31,7 @@ ], "input": { "message": "i would like to go to punjab someday", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -43,7 +43,7 @@ ], "input": { "message": "I want to go to Ahmedabad", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -55,7 +55,7 @@ ], "input": { "message": "I reside in varanasi", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -67,7 +67,7 @@ ], "input": { "message": "i grew up in phagwara", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -79,7 +79,7 @@ ], "input": { "message": "I wish to go to Fatehpur Bikri one day", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -91,7 +91,7 @@ ], "input": { "message": "What are some must visit places in and around Amritsar?", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -102,7 +102,7 @@ ], "input": { "message": "Search for Cafes with free WiFi between Andheri & Bandra", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -113,7 +113,7 @@ ], "input": { "message": "I have been to L.A,USA", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -125,7 +125,7 @@ ], "input": { "message": "Where can I get best momos around Muzafarpur", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -137,7 +137,7 @@ ], "input": { "message": "Find me fine dining restaurants in and around Meerut", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -148,7 +148,7 @@ ], "input": { "message": "where is the nearest gas station", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -160,7 +160,7 @@ ], "input": { "message": "My hometown is Mumbai, Maharashtra", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -172,7 +172,7 @@ ], "input": { "message": "I was born in Delhi", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -184,7 +184,7 @@ ], "input": { "message": "I went to a destination wedding at New Delhi", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -196,7 +196,7 @@ ], "input": { "message": "I live in Paschim Punropara", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } }, { @@ -212,7 +212,7 @@ ], "input": { "message": "Fastest route of going from patiala to ludhiana", - "entity_name": "city_list" + "entity_name": "ner_ptest_city_list" } } ] diff --git a/postman_tests/data/entities/text_restaurant.json b/postman_tests/data/entities/text_restaurant.json index 41db87ccc..a2c2dde42 100644 --- a/postman_tests/data/entities/text_restaurant.json +++ b/postman_tests/data/entities/text_restaurant.json @@ -2,7 +2,7 @@ { "input": { "message": "Where is Coffee On Canvas", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -14,7 +14,7 @@ { "input": { "message": "Find me fine dining restaurants near Novotel", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -26,7 +26,7 @@ { "input": { "message": "Fastest route of going to the farm", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -38,7 +38,7 @@ { "input": { "message": "i would like to go to Shudh Vegetarian Food Court someday", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -50,7 +50,7 @@ { "input": { "message": "I want to go to SP's Biryani", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -62,7 +62,7 @@ { "input": { "message": "I work in The Rasoi", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -74,7 +74,7 @@ { "input": { "message": "Where is Gokul Chaat located ?", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -86,7 +86,7 @@ { "input": { "message": "What are some must visit places in and around By The Way", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -98,7 +98,7 @@ { "input": { "message": "Search for Cafes with free WiFi between Andheri & Bandra", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -109,7 +109,7 @@ { "input": { "message": "I have been to The Fisherman's Deck", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -121,7 +121,7 @@ { "input": { "message": "Can I get best momos in N Asian", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -133,7 +133,7 @@ { "input": { "message": "Find me fine dining restaurants in and around L'amandier", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -145,7 +145,7 @@ { "input": { "message": "where is the nearest gas station", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -156,7 +156,7 @@ { "input": { "message": "My favorite restaurant is High", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -168,7 +168,7 @@ { "input": { "message": "is there an age limit in Kebab-e-Que", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -180,7 +180,7 @@ { "input": { "message": "Why not go to Gossip", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -192,7 +192,7 @@ { "input": { "message": "Zafrani is famous for biryani", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -204,7 +204,7 @@ { "input": { "message": "BottleRock serves alcohol ?", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -216,7 +216,7 @@ { "input": { "message": "Shangri-La is famous for spanish food", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -228,7 +228,7 @@ { "input": { "message": "Sattvik serves best meal in Goregaon", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -240,7 +240,7 @@ { "input": { "message": "Hippie@Heart is only for adults", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { @@ -252,7 +252,7 @@ { "input": { "message": "Kadambam Iyengar Cuisine to The Post Office Cafe distance?", - "entity_name": "restaurant" + "entity_name": "ner_ptest_restaurant" }, "expected": [ { From 4bc0ae20c34d532c096a3d794cdca604b93a772d Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 00:48:58 +0530 Subject: [PATCH 55/78] Update config.example with required vars and update initial_setup to create alias --- chatbot_ner/config.py | 2 +- config.example | 69 ++++++++++++++++++++++++++++--------------- initial_setup.py | 7 +++++ 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/chatbot_ner/config.py b/chatbot_ner/config.py index 9085031bc..40e8b2878 100644 --- a/chatbot_ner/config.py +++ b/chatbot_ner/config.py @@ -89,7 +89,7 @@ 'host': DESTINATION_HOST, 'port': DESTINATION_PORT}) ES_ALIAS = os.environ.get('ES_ALIAS') -ES_SCHEME = os.environ.get('ES_SCHEME') +ES_SCHEME = os.environ.get('ES_SCHEME', 'http') ELASTICSEARCH_CRF_DATA_INDEX_NAME = os.environ.get('ELASTICSEARCH_CRF_DATA_INDEX_NAME') ELASTICSEARCH_CRF_DATA_DOC_TYPE = os.environ.get('ELASTICSEARCH_CRF_DATA_DOC_TYPE') diff --git a/config.example b/config.example index ae5d04a04..6940ccb32 100644 --- a/config.example +++ b/config.example @@ -1,47 +1,68 @@ # This is config.example file for chatbot_ner module similar to .env.example file to hold settings -# Copy it to a file named config and fill in all the values. +# Copy it to a docker/.env and fill in all the values. # Never push your personal keys and passwords to any public repository! # Please don't add spaces around '=' -# This is the primary engine to use. Valid values are one of the following: ['elasticsearch'] +NAME=chatbot_ner +DJANGODIR=/app +DJANGO_DEBUG=False +DJANGO_LOG_LEVEL=DEBUG +DJANGO_SETTINGS_MODULE=chatbot_ner.settings +DJANGO_WSGI_MODULE=chatbot_ner/wsgi.py +# Important: Change the value of SECRET_KEY to something else and keep it secret +SECRET_KEY=!yqqcz-v@(s@kpygpvomcuu3il0q1&qtpz)e_g0ulo-sdv%c0c +NUM_WORKERS=1 +MAX_REQUESTS=1000 +PORT=8081 +TIMEOUT=600 + +# This is the primary engine to use for datastore. Valid values are one of the following: ['elasticsearch'] ENGINE=elasticsearch # ES prefixed variables correspond to settings for elasticsearch. # ES_URL is the complete url with auth name and password required to connect. If provided, this will override ES_HOST, # ES_PORT, ES_AUTH_NAME, ES_AUTH_PASSWORD -# ES_HOST by default is host for ES that comes up with compose +# ES_HOST and ES_PORT by default is host for ES that comes up with compose +ES_URL= ES_AUTH_NAME= ES_AUTH_PASSWORD= +ES_SCHEME=http ES_HOST=elasticsearch -ES_URL= ES_PORT=9200 -ES_INDEX_NAME=entity_data +ES_ALIAS=entity_data +ES_INDEX_NAME=entity_data_v1 +ES_INDEX_1= +ES_INDEX_2= ES_DOC_TYPE=data_dictionary -# ES_BULK_MSG_SIZE is an integer value +ELASTICSEARCH_CRF_DATA_DOC_TYPE=training_dictionary +ELASTICSEARCH_CRF_DATA_INDEX_NAME=entity_examples_data + ES_BULK_MSG_SIZE=1000 -# ES_SEARCH_SIZE is an integer value ES_SEARCH_SIZE=10000 -# Provide the following values if you need AWS authentication -ES_AWS_SERVICE= -ES_AWS_REGION= + +# Auth variables if ES is hosted on AWS ES_AWS_ACCESS_KEY_ID= +ES_AWS_REGION= ES_AWS_SECRET_ACCESS_KEY= +ES_AWS_SERVICE= + +DESTINATION_ES_SCHEME= +DESTINATION_HOST= +DESTINATION_PORT= -NAME=chatbot_ner -DJANGODIR=/app -NUM_WORKERS=1 -MAX_REQUESTS=1000 -DJANGO_SETTINGS_MODULE=chatbot_ner.settings -DJANGO_WSGI_MODULE=chatbot_ner/wsgi.py -DJANGO_LOG_LEVEL=debug -DJANGO_DEBUG=False -# Important: Change the value of SECRET_KEY to something else and keep it secret -SECRET_KEY=!yqqcz-v@(s@kpygpvomcuu3il0q1&qtpz)e_g0ulo-sdv%c0c -PORT=8081 -TIMEOUT=600 -CITY_MODEL_TYPE=crf -CITY_MODEL_PATH= # In order to enable entity detection for multiple languages, we use google translate. Please enter the key(optional) GOOGLE_TRANSLATE_API_KEY= + +# Deprecated CRF models configuration +MODELS_PATH= +WORD_EMBEDDING_REMOTE_URL= +EMBEDDINGS_PATH_VECTORS= +EMBEDDINGS_PATH_VOCAB= +CITY_MODEL_PATH= +CITY_MODEL_TYPE=crf +CRF_MODEL_S3_BUCKET_NAME= +CRF_MODEL_S3_BUCKET_REGION= +DATE_MODEL_PATH= +DATE_MODEL_TYPE= diff --git a/initial_setup.py b/initial_setup.py index ba335c309..14118414a 100755 --- a/initial_setup.py +++ b/initial_setup.py @@ -3,6 +3,9 @@ import time import nltk +from datastore.elastic_search.connect import get_es_url +from datastore.elastic_search.transfer import ESTransfer + BASE_DIR = os.path.dirname(__file__) print("Downloading nltk corpus: punkt ...") @@ -35,6 +38,7 @@ # Comment out entire section if you want to reuse existing data from datastore import DataStore from datastore.constants import DEFAULT_ENTITY_DATA_DIRECTORY +from chatbot_ner.config import ES_INDEX_NAME, ES_ALIAS db = DataStore() print("Setting up DataStore for Chatbot NER") @@ -42,6 +46,9 @@ db.delete() print("Creating the structure ...") db.create() +es_url = get_es_url() +es_object = ESTransfer(source=es_url, destination=None) +es_object.point_an_alias_to_index(es_url=es_url, alias_name=ES_ALIAS, index_name=ES_INDEX_NAME) print("Populating data from " + os.path.join(BASE_DIR, 'data', 'entity_data') + " ...") db.populate(entity_data_directory_path=DEFAULT_ENTITY_DATA_DIRECTORY) print("Done!") From ed7d750d83f7a486665e8b94eb83b2e155f10fb9 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 01:04:00 +0530 Subject: [PATCH 56/78] Separate NLTK and datastore setup into different python processes --- chatbot_ner/settings.py | 2 +- datastore_setup.py | 29 ++++++++++++++++++++++ docker/cmd.sh | 4 ++- initial_setup.py | 54 ----------------------------------------- nltk_setup.py | 19 +++++++++++++++ 5 files changed, 52 insertions(+), 56 deletions(-) create mode 100644 datastore_setup.py delete mode 100755 initial_setup.py create mode 100755 nltk_setup.py diff --git a/chatbot_ner/settings.py b/chatbot_ner/settings.py index 3da923452..4cf50fe37 100755 --- a/chatbot_ner/settings.py +++ b/chatbot_ner/settings.py @@ -102,7 +102,7 @@ def __getitem__(self, item): '--ignore-files=urls.py', '--ignore-files=wsgi.py', '--ignore-files=manage.py', - '--ignore-files=initial_setup.py', + '--ignore-files=nltk_setup.py', '--ignore-files=__init__.py', '--ignore-files=const.py', '--ignore-files=constant.py', diff --git a/datastore_setup.py b/datastore_setup.py new file mode 100644 index 000000000..163d2dd48 --- /dev/null +++ b/datastore_setup.py @@ -0,0 +1,29 @@ +from chatbot_ner.config import ES_INDEX_NAME, ES_ALIAS +from datastore import DataStore +from datastore.constants import DEFAULT_ENTITY_DATA_DIRECTORY +from datastore.elastic_search.connect import get_es_url +from datastore.elastic_search.transfer import ESTransfer + + +# Below needs to be committed if you want to use existing data in the Elasticsearch Setup +# TODO move this part to a different script and run on-demand +# POPULATING DATASTORE +# Comment out entire section if you want to reuse existing data + +def setup_datastore(): + db = DataStore() + print("Setting up DataStore for Chatbot NER") + print("Deleting any stale data ...") + db.delete() + print("Creating the structure ...") + db.create() + es_url = get_es_url() + es_object = ESTransfer(source=es_url, destination=None) + es_object.point_an_alias_to_index(es_url=es_url, alias_name=ES_ALIAS, index_name=ES_INDEX_NAME) + print("Populating data from " + os.path.join(BASE_DIR, 'data', 'entity_data') + " ...") + db.populate(entity_data_directory_path=DEFAULT_ENTITY_DATA_DIRECTORY) + print("Done!") + + +if __name__ == '__main__': + setup_datastore() diff --git a/docker/cmd.sh b/docker/cmd.sh index 53cabdcfd..8e16f1b0b 100755 --- a/docker/cmd.sh +++ b/docker/cmd.sh @@ -7,7 +7,9 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Initial setup.py - Datastore lines need to be commented for using previously create data -python /app/initial_setup.py +python /app/nltk_setup.py +sleep 3 +python /app/datastore_setup.py # Using supervisor as we want to use Nginx and Uwsgi both, Settings specified in supervisord.conf, any update to that will need build diff --git a/initial_setup.py b/initial_setup.py deleted file mode 100755 index 14118414a..000000000 --- a/initial_setup.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import absolute_import -import os -import time -import nltk - -from datastore.elastic_search.connect import get_es_url -from datastore.elastic_search.transfer import ESTransfer - -BASE_DIR = os.path.dirname(__file__) - -print("Downloading nltk corpus: punkt ...") -status = nltk.download('punkt') -if not status: - print("punkt Download was unsuccessful") - -print("Downloading nltk corpus: wordnet ...") -status = nltk.download('wordnet') -if not status: - print("wordnet Download was unsuccessful") - -print("Downloading nltk corpus: MaxEnt POS ...") -status = nltk.download('maxent_treebank_pos_tagger') -if not status: - print("MaxEnt POS Download was unsuccessful") - -print("Downloading nltk corpus: AP POS Tagger...") -status = nltk.download('averaged_perceptron_tagger') -if not status: - print("AP POS Tagger Download was unsuccessful") - -# Below needs to be committed if you want to use existing data in the Elasticsearch Setup - -time.sleep(20) -# waiting for Elasticsearch to come up properly, if you have a self hosted ES and not using via docker-compose -# You can remove this sleep -# TODO move this part to a different script and run on-demand -# POPULATING DATASTORE -# Comment out entire section if you want to reuse existing data -from datastore import DataStore -from datastore.constants import DEFAULT_ENTITY_DATA_DIRECTORY -from chatbot_ner.config import ES_INDEX_NAME, ES_ALIAS - -db = DataStore() -print("Setting up DataStore for Chatbot NER") -print("Deleting any stale data ...") -db.delete() -print("Creating the structure ...") -db.create() -es_url = get_es_url() -es_object = ESTransfer(source=es_url, destination=None) -es_object.point_an_alias_to_index(es_url=es_url, alias_name=ES_ALIAS, index_name=ES_INDEX_NAME) -print("Populating data from " + os.path.join(BASE_DIR, 'data', 'entity_data') + " ...") -db.populate(entity_data_directory_path=DEFAULT_ENTITY_DATA_DIRECTORY) -print("Done!") diff --git a/nltk_setup.py b/nltk_setup.py new file mode 100755 index 000000000..56c69036c --- /dev/null +++ b/nltk_setup.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import + +import os + +import nltk + +BASE_DIR = os.path.dirname(__file__) + +NLTK_RESOURCES = ['punkt', 'wordnet', 'maxent_treebank_pos_tagger', 'averaged_perceptron_tagger'] + + +def download_nltk_resources(): + for resource in NLTK_RESOURCES: + if not nltk.download(resource): + raise RuntimeError('Failed to download NLTK resource {}'.format(resource)) + + +if __name__ == '__main__': + download_nltk_resources() From 89242c92e65f428268c4fb6adde4b23c84bfcadd Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 01:11:04 +0530 Subject: [PATCH 57/78] Fix import error in setup scripts --- datastore_setup.py | 4 ++++ docker/cmd.sh | 4 ++-- nltk_setup.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/datastore_setup.py b/datastore_setup.py index 163d2dd48..169f59075 100644 --- a/datastore_setup.py +++ b/datastore_setup.py @@ -1,9 +1,13 @@ +import os + from chatbot_ner.config import ES_INDEX_NAME, ES_ALIAS from datastore import DataStore from datastore.constants import DEFAULT_ENTITY_DATA_DIRECTORY from datastore.elastic_search.connect import get_es_url from datastore.elastic_search.transfer import ESTransfer +BASE_DIR = os.path.dirname(__file__) + # Below needs to be committed if you want to use existing data in the Elasticsearch Setup # TODO move this part to a different script and run on-demand diff --git a/docker/cmd.sh b/docker/cmd.sh index 8e16f1b0b..38aeb1015 100755 --- a/docker/cmd.sh +++ b/docker/cmd.sh @@ -7,9 +7,9 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Initial setup.py - Datastore lines need to be commented for using previously create data -python /app/nltk_setup.py +python /app/nltk_setup.py || { echo 'nltk setup failed'; exit 1; } sleep 3 -python /app/datastore_setup.py +python /app/datastore_setup.py || { echo 'datastore setup failed'; exit 1; } # Using supervisor as we want to use Nginx and Uwsgi both, Settings specified in supervisord.conf, any update to that will need build diff --git a/nltk_setup.py b/nltk_setup.py index 56c69036c..0f58daee1 100755 --- a/nltk_setup.py +++ b/nltk_setup.py @@ -1,10 +1,10 @@ from __future__ import absolute_import -import os + import nltk -BASE_DIR = os.path.dirname(__file__) + NLTK_RESOURCES = ['punkt', 'wordnet', 'maxent_treebank_pos_tagger', 'averaged_perceptron_tagger'] From a0ee0a59ad11d94997f2ac2b7823e25bbae73805 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 02:42:55 +0530 Subject: [PATCH 58/78] Fix create and index functions according to new style alias and index settings --- chatbot_ner/config.py | 66 ++++++++++++++++-------------- config.example | 3 +- datastore/constants.py | 5 ++- datastore/datastore.py | 54 +++++++++++++++--------- datastore/elastic_search/create.py | 8 +++- datastore_setup.py | 7 ---- docker/Dockerfile | 3 +- docker/Dockerfile-python3 | 2 +- docs/datastore_variables.md | 57 ++------------------------ docs/install.md | 2 +- nltk_setup.py | 4 -- 11 files changed, 89 insertions(+), 122 deletions(-) diff --git a/chatbot_ner/config.py b/chatbot_ner/config.py index 40e8b2878..0720b40c8 100644 --- a/chatbot_ner/config.py +++ b/chatbot_ner/config.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + import logging.handlers import os @@ -51,27 +52,19 @@ nlp_logger.addHandler(handler_stdout) ENGINE = os.environ.get('ENGINE') -if ENGINE: - ENGINE = ENGINE.lower() -else: - ner_logger.warning("`ENGINE` variable is not set, Text type entities won't work without it") - # ES settings (Mandatory to use Text type entities) +ES_SCHEME = os.environ.get('ES_SCHEME', 'http') ES_URL = os.environ.get('ES_URL') ES_HOST = os.environ.get('ES_HOST') ES_PORT = os.environ.get('ES_PORT') -ES_INDEX_NAME = os.environ.get('ES_INDEX_NAME') +ES_ALIAS = os.environ.get('ES_ALIAS') +ES_INDEX_1 = os.environ.get('ES_INDEX_1') ES_DOC_TYPE = os.environ.get('ES_DOC_TYPE', 'data_dictionary') ES_AUTH_NAME = os.environ.get('ES_AUTH_NAME') ES_AUTH_PASSWORD = os.environ.get('ES_AUTH_PASSWORD') ES_BULK_MSG_SIZE = os.environ.get('ES_BULK_MSG_SIZE', '10000') ES_SEARCH_SIZE = os.environ.get('ES_SEARCH_SIZE', '10000') -# Crf Model Specific (Mandatory to use CRF Model) -CRF_MODELS_PATH = os.environ.get('MODELS_PATH') -CRF_EMBEDDINGS_PATH_VOCAB = os.environ.get('EMBEDDINGS_PATH_VOCAB') -CRF_EMBEDDINGS_PATH_VECTORS = os.environ.get('EMBEDDINGS_PATH_VECTORS') - try: ES_BULK_MSG_SIZE = int(ES_BULK_MSG_SIZE) ES_SEARCH_SIZE = int(ES_SEARCH_SIZE) @@ -79,8 +72,10 @@ ES_BULK_MSG_SIZE = 1000 ES_SEARCH_SIZE = 1000 +ELASTICSEARCH_CRF_DATA_INDEX_NAME = os.environ.get('ELASTICSEARCH_CRF_DATA_INDEX_NAME') +ELASTICSEARCH_CRF_DATA_DOC_TYPE = os.environ.get('ELASTICSEARCH_CRF_DATA_DOC_TYPE') + # Optional Vars -ES_INDEX_1 = os.environ.get('ES_INDEX_1') ES_INDEX_2 = os.environ.get('ES_INDEX_2') DESTINATION_ES_SCHEME = os.environ.get('DESTINATION_ES_SCHEME', 'http') DESTINATION_HOST = os.environ.get('DESTINATION_HOST') @@ -88,31 +83,29 @@ DESTINATION_URL = '{scheme}://{host}:{port}'.format(**{'scheme': DESTINATION_ES_SCHEME, 'host': DESTINATION_HOST, 'port': DESTINATION_PORT}) -ES_ALIAS = os.environ.get('ES_ALIAS') -ES_SCHEME = os.environ.get('ES_SCHEME', 'http') -ELASTICSEARCH_CRF_DATA_INDEX_NAME = os.environ.get('ELASTICSEARCH_CRF_DATA_INDEX_NAME') -ELASTICSEARCH_CRF_DATA_DOC_TYPE = os.environ.get('ELASTICSEARCH_CRF_DATA_DOC_TYPE') - -# Crf Model Specific with additional AWS storage (optional) -CRF_MODEL_S3_BUCKET_NAME = os.environ.get('CRF_MODEL_S3_BUCKET_NAME') -CRF_MODEL_S3_BUCKET_REGION = os.environ.get('CRF_MODEL_S3_BUCKET_REGION') -WORD_EMBEDDING_REMOTE_URL = os.environ.get('WORD_EMBEDDING_REMOTE_URL') -GOOGLE_TRANSLATE_API_KEY = os.environ.get('GOOGLE_TRANSLATE_API_KEY') -if not GOOGLE_TRANSLATE_API_KEY: - ner_logger.warning('Google Translate API key is null or not set') - GOOGLE_TRANSLATE_API_KEY = '' +if ENGINE: + ENGINE = ENGINE.lower() + if ENGINE == 'elasticsearch': + if not all(key is not None and key.strip() for key in (ES_ALIAS, ES_INDEX_1, ES_DOC_TYPE)): + raise Exception( + 'Invalid configuration for datastore (engine=elasticsearch). One or more of following keys: ' + '`ES_ALIAS`, `ES_INDEX_1`, `ES_DOC_TYPE` is null in env') +else: + ner_logger.warning("`ENGINE` variable is not set, Text type entities won't work without it") CHATBOT_NER_DATASTORE = { 'engine': ENGINE, 'elasticsearch': { 'connection_url': ES_URL, # Elastic Search URL - 'name': ES_INDEX_NAME, # Index name used - 'doc_type': ES_DOC_TYPE, # Index's doc type + 'es_scheme': ES_SCHEME, # The scheme used in ES default value is http:// 'host': ES_HOST, # Elastic Search Host 'port': ES_PORT, # Port of elastic search 'user': ES_AUTH_NAME, 'password': ES_AUTH_PASSWORD, + 'es_alias': ES_ALIAS, # Elastic search alias used in transfer + 'es_index_1': ES_INDEX_1, + 'doc_type': ES_DOC_TYPE, # Index's doc type 'retry_on_timeout': False, 'max_retries': 1, 'timeout': 20, @@ -120,11 +113,8 @@ # Transfer Specific constants (ignore if only one elasticsearch is setup) # For detailed explanation datastore.elastic_search.transfer.py - 'es_index_1': ES_INDEX_1, # Index 1 used for transfer 'es_index_2': ES_INDEX_2, # Index 2 used for transfer 'destination_url': DESTINATION_URL, # Elastic search destination URL - 'es_alias': ES_ALIAS, # Elastic search alias used in transfer - 'es_scheme': ES_SCHEME, # The scheme used in ES default value is http:// # Training Data ES constants 'elasticsearch_crf_data_index_name': ELASTICSEARCH_CRF_DATA_INDEX_NAME, @@ -150,7 +140,13 @@ ner_logger.warning('`ES_AWS_SERVICE` and `ES_AWS_REGION` are not set. ' 'This is not a problem if you are using self hosted ES') +# TODO: Remove non functional crf code and cleanup # Model Vars +# Crf Model Specific (Mandatory to use CRF Model) +CRF_MODELS_PATH = os.environ.get('MODELS_PATH') +CRF_EMBEDDINGS_PATH_VOCAB = os.environ.get('EMBEDDINGS_PATH_VOCAB') +CRF_EMBEDDINGS_PATH_VECTORS = os.environ.get('EMBEDDINGS_PATH_VECTORS') + if os.path.exists(MODEL_CONFIG_PATH): dotenv.read_dotenv(MODEL_CONFIG_PATH) else: @@ -165,3 +161,13 @@ CITY_MODEL_PATH = os.path.join(BASE_DIR, 'data', 'models', 'crf', 'city', 'model_13062017.crf') if not DATE_MODEL_PATH: DATE_MODEL_PATH = os.path.join(BASE_DIR, 'data', 'models', 'crf', 'date', 'model_date.crf') + +# Crf Model Specific with additional AWS storage (optional) +CRF_MODEL_S3_BUCKET_NAME = os.environ.get('CRF_MODEL_S3_BUCKET_NAME') +CRF_MODEL_S3_BUCKET_REGION = os.environ.get('CRF_MODEL_S3_BUCKET_REGION') +WORD_EMBEDDING_REMOTE_URL = os.environ.get('WORD_EMBEDDING_REMOTE_URL') +GOOGLE_TRANSLATE_API_KEY = os.environ.get('GOOGLE_TRANSLATE_API_KEY') + +if not GOOGLE_TRANSLATE_API_KEY: + ner_logger.warning('Google Translate API key is null or not set') + GOOGLE_TRANSLATE_API_KEY = '' diff --git a/config.example b/config.example index 6940ccb32..cf70d4f39 100644 --- a/config.example +++ b/config.example @@ -32,8 +32,7 @@ ES_SCHEME=http ES_HOST=elasticsearch ES_PORT=9200 ES_ALIAS=entity_data -ES_INDEX_NAME=entity_data_v1 -ES_INDEX_1= +ES_INDEX_1=entity_data_v1 ES_INDEX_2= ES_DOC_TYPE=data_dictionary ELASTICSEARCH_CRF_DATA_DOC_TYPE=training_dictionary diff --git a/datastore/constants.py b/datastore/constants.py index fba25dbd6..e19ba1560 100644 --- a/datastore/constants.py +++ b/datastore/constants.py @@ -11,8 +11,11 @@ ELASTICSEARCH_VALUES_SEARCH_SIZE = 300000 # settings dictionary key constants +# TODO: these should not be here, two different sources of literals ENGINE = 'engine' -ELASTICSEARCH_INDEX_NAME = 'name' +ELASTICSEARCH_ALIAS = 'es_alias' +ELASTICSEARCH_INDEX_1 = 'es_index_1' +ELASTICSEARCH_INDEX_2 = 'es_index_2' ELASTICSEARCH_DOC_TYPE = 'doc_type' ELASTICSEARCH_VERSION_MAJOR, ELASTICSEARCH_VERSION_MINOR, ELASTICSEARCH_VERSION_OTHER = elasticsearch.VERSION ELASTICSEARCH_CRF_DATA_INDEX_NAME = 'elasticsearch_crf_data_index_name' diff --git a/datastore/datastore.py b/datastore/datastore.py index 5a95b8a5d..195c7e37f 100644 --- a/datastore/datastore.py +++ b/datastore/datastore.py @@ -2,15 +2,17 @@ import warnings +import six + from chatbot_ner.config import ner_logger, CHATBOT_NER_DATASTORE from datastore import elastic_search -from datastore.constants import (ELASTICSEARCH, ENGINE, ELASTICSEARCH_INDEX_NAME, - ELASTICSEARCH_DOC_TYPE, ELASTICSEARCH_CRF_DATA_INDEX_NAME, +from datastore.constants import (ELASTICSEARCH, ENGINE, ELASTICSEARCH_ALIAS, ELASTICSEARCH_INDEX_1, + ELASTICSEARCH_INDEX_2, ELASTICSEARCH_DOC_TYPE, ELASTICSEARCH_CRF_DATA_INDEX_NAME, ELASTICSEARCH_CRF_DATA_DOC_TYPE) +from datastore.elastic_search.transfer import ESTransfer from datastore.exceptions import (DataStoreSettingsImproperlyConfiguredException, EngineNotImplementedException, EngineConnectionException, NonESEngineTransferException, IndexNotFoundException) from lib.singleton import Singleton -import six class DataStore(six.with_metaclass(Singleton, object)): @@ -64,7 +66,7 @@ def _connect(self): All other exceptions raised by elasticsearch-py library """ if self._engine == ELASTICSEARCH: - self._store_name = self._connection_settings.get(ELASTICSEARCH_INDEX_NAME, '_all') + self._store_name = self._connection_settings[ELASTICSEARCH_ALIAS] self._client_or_connection = elastic_search.connect.connect(**self._connection_settings) else: self._client_or_connection = None @@ -73,11 +75,12 @@ def _connect(self): if self._client_or_connection is None: raise EngineConnectionException(engine=self._engine) - def create(self, **kwargs): + def create(self, ignore_if_exists=False, **kwargs): """ Creates the schema/structure for the datastore depending on the engine configured in the environment. Args: + ignore_if_exists (bool): if to ignore index already exists errors, default False kwargs: For Elasticsearch: master_timeout: Specify timeout for connection to master @@ -105,23 +108,36 @@ def create(self, **kwargs): self._connect() if self._engine == ELASTICSEARCH: + es_url = elastic_search.connect.get_es_url() + es_object = ESTransfer(source=es_url, destination=None) self._check_doc_type_for_elasticsearch() elastic_search.create.create_entity_index(connection=self._client_or_connection, - index_name=self._store_name, + index_name=self._connection_settings[ELASTICSEARCH_INDEX_1], doc_type=self._connection_settings[ELASTICSEARCH_DOC_TYPE], logger=ner_logger, - ignore=[400, 404], + ignore_if_exists=ignore_if_exists, **kwargs) - crf_data_index = self._connection_settings.get(ELASTICSEARCH_CRF_DATA_INDEX_NAME) - if crf_data_index is not None: - self._check_doc_type_for_crf_data_elasticsearch() + es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, + index_name=self._connection_settings[ELASTICSEARCH_INDEX_1]) + if self._connection_settings.get(ELASTICSEARCH_INDEX_2): + elastic_search.create.create_entity_index(connection=self._client_or_connection, + index_name=self._connection_settings[ELASTICSEARCH_INDEX_2], + doc_type=self._connection_settings[ELASTICSEARCH_DOC_TYPE], + logger=ner_logger, + ignore_if_exists=ignore_if_exists, + **kwargs) + es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, + index_name=self._connection_settings[ELASTICSEARCH_INDEX_2]) + + if self._connection_settings.get(ELASTICSEARCH_CRF_DATA_INDEX_NAME): + self._check_doc_type_for_crf_data_elasticsearch() elastic_search.create.create_crf_index( connection=self._client_or_connection, - index_name=crf_data_index, + index_name=self._connection_settings.get[ELASTICSEARCH_CRF_DATA_INDEX_NAME], doc_type=self._connection_settings[ELASTICSEARCH_CRF_DATA_DOC_TYPE], logger=ner_logger, - ignore=[400, 404], + ignore_if_exists=ignore_if_exists, **kwargs ) @@ -186,11 +202,13 @@ def delete(self, **kwargs): self._connect() if self._engine == ELASTICSEARCH: - elastic_search.create.delete_index(connection=self._client_or_connection, - index_name=self._store_name, - logger=ner_logger, - ignore=[400, 404], - **kwargs) + for index_key in [ELASTICSEARCH_INDEX_1, ELASTICSEARCH_INDEX_2, ELASTICSEARCH_CRF_DATA_INDEX_NAME]: + if self._connection_settings.get(index_key): + elastic_search.create.delete_index(connection=self._client_or_connection, + index_name=self._store_name, + logger=ner_logger, + **kwargs) + # TODO: cleanup aliases ? # FIXME: Deprecated, remove def get_entity_dictionary(self, entity_name, **kwargs): @@ -318,7 +336,6 @@ def delete_entity(self, entity_name, **kwargs): ELASTICSEARCH_DOC_TYPE], entity_name=entity_name, logger=ner_logger, - ignore=[400, 404], **kwargs) # FIXME: repopulate does not consider language of the variants @@ -357,7 +374,6 @@ def repopulate(self, entity_data_directory_path=None, csv_file_paths=None, **kwa entity_data_directory_path=entity_data_directory_path, csv_file_paths=csv_file_paths, logger=ner_logger, - ignore=[400, 404], **kwargs) # TODO: repopulate code for crf index missing diff --git a/datastore/elastic_search/create.py b/datastore/elastic_search/create.py index dd6a7427f..d799f0998 100644 --- a/datastore/elastic_search/create.py +++ b/datastore/elastic_search/create.py @@ -27,7 +27,7 @@ def delete_index(connection, index_name, logger, **kwargs): logger.exception('%s: Exception in deleting index %s ' % (log_prefix, e)) -def _create_index(connection, index_name, doc_type, logger, mapping_body, **kwargs): +def _create_index(connection, index_name, doc_type, logger, mapping_body, ignore_if_exists=False, **kwargs): """ Creates an Elasticsearch index needed for similarity based searching Args: @@ -51,6 +51,12 @@ def _create_index(connection, index_name, doc_type, logger, mapping_body, **kwar Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.create Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.put_mapping """ + if exists(connection=connection, index_name=index_name): + if ignore_if_exists: + return + else: + raise Exception('Failed to create index {}. it already exists. Please check and delete it using ' + 'Datastore().delete()') try: body = { 'index': { diff --git a/datastore_setup.py b/datastore_setup.py index 169f59075..70ca94f1c 100644 --- a/datastore_setup.py +++ b/datastore_setup.py @@ -1,17 +1,13 @@ import os -from chatbot_ner.config import ES_INDEX_NAME, ES_ALIAS from datastore import DataStore from datastore.constants import DEFAULT_ENTITY_DATA_DIRECTORY -from datastore.elastic_search.connect import get_es_url -from datastore.elastic_search.transfer import ESTransfer BASE_DIR = os.path.dirname(__file__) # Below needs to be committed if you want to use existing data in the Elasticsearch Setup # TODO move this part to a different script and run on-demand -# POPULATING DATASTORE # Comment out entire section if you want to reuse existing data def setup_datastore(): @@ -21,9 +17,6 @@ def setup_datastore(): db.delete() print("Creating the structure ...") db.create() - es_url = get_es_url() - es_object = ESTransfer(source=es_url, destination=None) - es_object.point_an_alias_to_index(es_url=es_url, alias_name=ES_ALIAS, index_name=ES_INDEX_NAME) print("Populating data from " + os.path.join(BASE_DIR, 'data', 'entity_data') + " ...") db.populate(entity_data_directory_path=DEFAULT_ENTITY_DATA_DIRECTORY) print("Done!") diff --git a/docker/Dockerfile b/docker/Dockerfile index 0e21311c0..29277750c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,8 +6,7 @@ RUN apt-get update && apt-get install -y wget build-essential curl nginx supervi WORKDIR /app - -COPY docker/install.sh initial_setup.py /app/ +COPY docker/install.sh nltk_setup.py datastore_setup.py /app/ COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf RUN mkdir -p ~/model_lib && \ diff --git a/docker/Dockerfile-python3 b/docker/Dockerfile-python3 index 85195cfaa..389e95fb5 100644 --- a/docker/Dockerfile-python3 +++ b/docker/Dockerfile-python3 @@ -11,7 +11,7 @@ RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && \ WORKDIR /app -COPY docker/install.sh initial_setup.py /app/ +COPY docker/install.sh nltk_setup.py datastore_setup.py /app/ COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf # cython is installed because pandas build fails otherwise diff --git a/docs/datastore_variables.md b/docs/datastore_variables.md index 39e7e3102..7e5fa463c 100644 --- a/docs/datastore_variables.md +++ b/docs/datastore_variables.md @@ -16,28 +16,6 @@ Copy it to a file called `config` at the root of the repository and edit it to c > ENGINE=elasticsearch > ES_HOST=YOURHOST.us-east-1.es.amazonaws.com - -**Structure** - ------ - -A dictionary like following is constructed on reading the `config` file. (Note the following dictionary is just for strcuture explanation purposes. Actual names and keys are different) - -```python -SETTINGS = { - ENGINE: 'engine_value_1' # what engine settings to use - 'engine_value_1': { - # Connection settings specific to engine_value_1 go here - ... - }, - 'engine_value_2':{ - # Connection settings specific to engine_value_2 go here - ... - }, - ... -} -``` - #### Variables --------------- @@ -61,7 +39,8 @@ SETTINGS = { | `ES_URL` | The complete connection url, containing protocol, host and port and optionally username and password for basic authentication. When provided, this will override values for `ES_HOST`, `ES_PORT`, `ES_AUTH_NAME` and `ES_AUTH_PASSWORD`
E.g.
`https://user:secret@localhost:443`,
`http://user:secret@localhost:9200/development/` | | `ES_HOST` | Elasticsearch host address
E.g.
`localhost`,
`127.0.0.1`,
`YOURHOST.us-east-1.es.amazonaws.com` | | `ES_PORT` | Elasticsearch service port, (usually http service)
E.g.
`9200`,
`443` | - | `ES_INDEX_NAME` | Index name for Elasticsearch.
E.g.
`chatbot_ner_index` | + | `ES_ALIAS` | Alias to put index under for Elasticsearch.
E.g.
`chatbot_ner_alias` | + | `ES_INDEX_1` | Index name for Elasticsearch.
E.g.
`chatbot_ner_index` | | `ES_DOC_TYPE` | Document type for elasticsearch. If not provided defaults to `data_dictionary`. | | `ES_AUTH_NAME` | Name for basic http authentication. Optional if http authentication is not needed. | | `ES_AUTH_PASSWORD` | Password/Secret for basic http authentication. Optional if http authentication is not needed. | @@ -87,35 +66,5 @@ SETTINGS = { ---------- -```bash -# This is config file for chatbot_ner module similar to .env file to hold settings -# Never push your personal keys and passwords to any public repository! -# If you don't want to create this file then make sure the variables in this file are in the environment. -# Please don't add spaces around '=' - -# This is the primary engine to use. Valid values are one of the following: -# elasticsearch - -ENGINE=elasticsearch - -# ES prefixed values correspond to settings for elasticsearch. -# ES_URL is the complete url with auth name and password required to connect. If provided, this will override ES_HOST, ES_PORT, ES_AUTH_NAME, ES_AUTH_PASSWORD -ES_URL= -ES_HOST=127.0.0.1 -ES_PORT=9200 -ES_INDEX_NAME=chatbot_ner_index -ES_DOC_TYPE=data_dictionary -ES_AUTH_NAME= -ES_AUTH_PASSWORD= - -# ES_BULK_MSG_SIZE is an integer value -ES_BULK_MSG_SIZE=1000 - -# Provide the following values if you need AWS authentication - -ES_AWS_SECRET_ACCESS_KEY= -ES_AWS_ACCESS_KEY_ID= -ES_AWS_REGION= -ES_AWS_SERVICE= -``` +Please check [config.example](../config.example) file diff --git a/docs/install.md b/docs/install.md index 025f03e88..8e9cfca38 100644 --- a/docs/install.md +++ b/docs/install.md @@ -197,7 +197,7 @@ Output should be: -**IMPORTANT NOTE:** If you bring down the container and bring it up again, `initial_setup.py` will run again. If you added some data and do not want it to get reset on ELASTICSEARCH, comment out DataStore section in `initial_setup.py` +**IMPORTANT NOTE:** If you bring down the container and bring it up again, `datastore_setup.py` will run again. If you added some data and do not want it to get reset on ELASTICSEARCH, comment out DataStore section in `datastore_setup.py` ## To Create Custom Docker Images diff --git a/nltk_setup.py b/nltk_setup.py index 0f58daee1..7d2a54753 100755 --- a/nltk_setup.py +++ b/nltk_setup.py @@ -1,11 +1,7 @@ from __future__ import absolute_import - - import nltk - - NLTK_RESOURCES = ['punkt', 'wordnet', 'maxent_treebank_pos_tagger', 'averaged_perceptron_tagger'] From 233655e4e800b7bc72c2a6eea07bba42c0e99e5e Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 02:45:57 +0530 Subject: [PATCH 59/78] Sleep some time for elastic search to be ready before indexing --- docker/cmd.sh | 2 +- external_api/response_utils.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docker/cmd.sh b/docker/cmd.sh index 38aeb1015..3b82c1182 100755 --- a/docker/cmd.sh +++ b/docker/cmd.sh @@ -8,7 +8,7 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Initial setup.py - Datastore lines need to be commented for using previously create data python /app/nltk_setup.py || { echo 'nltk setup failed'; exit 1; } -sleep 3 +sleep 10 python /app/datastore_setup.py || { echo 'datastore setup failed'; exit 1; } # Using supervisor as we want to use Nginx and Uwsgi both, Settings specified in supervisord.conf, any update to that will need build diff --git a/external_api/response_utils.py b/external_api/response_utils.py index 8a6ac44dd..b0fa67296 100644 --- a/external_api/response_utils.py +++ b/external_api/response_utils.py @@ -6,9 +6,9 @@ # Django from django.http import HttpResponse +from chatbot_ner.config import ner_logger # Local imports from external_api.exceptions import APIHandlerException -from chatbot_ner.config import ner_logger class APIResponse(object): @@ -31,6 +31,7 @@ def external_api_response_wrapper(view_func): """ This decorator is used to return responses for External API in a consistent format """ + @wraps(view_func) def wrapper(request, *args, **kwargs): response = APIResponse() @@ -43,20 +44,19 @@ def wrapper(request, *args, **kwargs): response.error = e.error_msg except Exception as e: + ner_logger.exception('General exception in external API') + ner_logger.debug('*******************************') + ner_logger.debug(request.path) + ner_logger.debug(request.GET.dict()) + ner_logger.debug(request.body) + ner_logger.debug(response.success) + ner_logger.debug(response.result) + ner_logger.debug(response.error) + ner_logger.debug('*******************************') response.success = False response.error = 'Unknown error: {0}'.format(str(e)) response.status_code = 500 - ner_logger.debug('*******************************') - ner_logger.debug(request.path) - ner_logger.debug(request.GET.dict()) - ner_logger.debug(request.META) - ner_logger.debug(request.body) - ner_logger.debug(response.success) - ner_logger.debug(response.result) - ner_logger.debug(response.error) - ner_logger.debug('*******************************') - return response.toHttpResponse() return wrapper From f8d1e4ef2f606a43b41876352886b7f444ec4230 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 03:38:03 +0530 Subject: [PATCH 60/78] Update datastore create and delete methods --- datastore/datastore.py | 58 +++++++++++++++--------------- datastore/elastic_search/create.py | 58 ++++++++++++++++-------------- datastore_setup.py | 4 +-- docker/cmd.sh | 2 +- 4 files changed, 64 insertions(+), 58 deletions(-) diff --git a/datastore/datastore.py b/datastore/datastore.py index 195c7e37f..299dcdac7 100644 --- a/datastore/datastore.py +++ b/datastore/datastore.py @@ -75,7 +75,7 @@ def _connect(self): if self._client_or_connection is None: raise EngineConnectionException(engine=self._engine) - def create(self, ignore_if_exists=False, **kwargs): + def create(self, err_if_exists=True, **kwargs): """ Creates the schema/structure for the datastore depending on the engine configured in the environment. @@ -110,36 +110,36 @@ def create(self, ignore_if_exists=False, **kwargs): if self._engine == ELASTICSEARCH: es_url = elastic_search.connect.get_es_url() es_object = ESTransfer(source=es_url, destination=None) - self._check_doc_type_for_elasticsearch() - elastic_search.create.create_entity_index(connection=self._client_or_connection, - index_name=self._connection_settings[ELASTICSEARCH_INDEX_1], - doc_type=self._connection_settings[ELASTICSEARCH_DOC_TYPE], - logger=ner_logger, - ignore_if_exists=ignore_if_exists, - **kwargs) - es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, - index_name=self._connection_settings[ELASTICSEARCH_INDEX_1]) - - if self._connection_settings.get(ELASTICSEARCH_INDEX_2): - elastic_search.create.create_entity_index(connection=self._client_or_connection, - index_name=self._connection_settings[ELASTICSEARCH_INDEX_2], - doc_type=self._connection_settings[ELASTICSEARCH_DOC_TYPE], - logger=ner_logger, - ignore_if_exists=ignore_if_exists, - **kwargs) - es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, - index_name=self._connection_settings[ELASTICSEARCH_INDEX_2]) - - if self._connection_settings.get(ELASTICSEARCH_CRF_DATA_INDEX_NAME): - self._check_doc_type_for_crf_data_elasticsearch() - elastic_search.create.create_crf_index( + create_map = [ # TODO: use namedtuples + (True, ELASTICSEARCH_INDEX_1, ELASTICSEARCH_DOC_TYPE, self._store_name, + self._check_doc_type_for_elasticsearch, elastic_search.create.create_entity_index), + (False, ELASTICSEARCH_INDEX_2, ELASTICSEARCH_DOC_TYPE, self._store_name, + self._check_doc_type_for_elasticsearch, elastic_search.create.create_entity_index), + (False, ELASTICSEARCH_CRF_DATA_INDEX_NAME, ELASTICSEARCH_CRF_DATA_DOC_TYPE, None, + self._check_doc_type_for_crf_data_elasticsearch, elastic_search.create.create_crf_index), + ] + for (required, index_name_key, doc_type_key, alias_name, doc_type_checker, create_fn) in create_map: + index_name = self._connection_settings.get(index_name_key) + doc_type = self._client_or_connection.get(doc_type_key) + if not index_name: + if required: + raise DataStoreSettingsImproperlyConfiguredException( + '{} key is required in datastore settings for elastic_search') + else: + continue + + doc_type_checker() + create_fn( connection=self._client_or_connection, - index_name=self._connection_settings.get[ELASTICSEARCH_CRF_DATA_INDEX_NAME], - doc_type=self._connection_settings[ELASTICSEARCH_CRF_DATA_DOC_TYPE], + index_name=index_name, + doc_type=doc_type, logger=ner_logger, - ignore_if_exists=ignore_if_exists, + err_if_exists=err_if_exists, **kwargs ) + if alias_name: + es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, + index_name=index_name) # FIXME: repopulate does not consider language of the variants def populate(self, entity_data_directory_path=None, csv_file_paths=None, **kwargs): @@ -178,11 +178,12 @@ def populate(self, entity_data_directory_path=None, csv_file_paths=None, **kwarg logger=ner_logger, **kwargs) - def delete(self, **kwargs): + def delete(self, err_if_does_not_exist=True, **kwargs): """ Deletes all data including the structure of the datastore. Note that this is equivalent to DROP not TRUNCATE Args: + err_if_does_not_exist (bool): if to raise index does not exist errors, default True kwargs: For Elasticsearch: body: The configuration for the index (settings and mappings) @@ -207,6 +208,7 @@ def delete(self, **kwargs): elastic_search.create.delete_index(connection=self._client_or_connection, index_name=self._store_name, logger=ner_logger, + err_if_does_not_exist=err_if_does_not_exist, **kwargs) # TODO: cleanup aliases ? diff --git a/datastore/elastic_search/create.py b/datastore/elastic_search/create.py index d799f0998..62b67afab 100644 --- a/datastore/elastic_search/create.py +++ b/datastore/elastic_search/create.py @@ -3,7 +3,21 @@ log_prefix = 'datastore.elastic_search.create' -def delete_index(connection, index_name, logger, **kwargs): +def exists(connection, index_name): + """ + Checks if index_name exists + + Args: + connection: Elasticsearch client object + index_name: The name of the index + + Returns: + boolean, True if index exists , False otherwise + """ + return connection.indices.exists(index_name) + + +def delete_index(connection, index_name, logger, err_if_does_not_exist=True, **kwargs): """ Deletes the index named index_name @@ -20,14 +34,17 @@ def delete_index(connection, index_name, logger, **kwargs): Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.delete """ - try: - connection.indices.delete(index=index_name, **kwargs) - logger.debug('%s: Delete Index: Operation successfully completed' % log_prefix) - except Exception as e: - logger.exception('%s: Exception in deleting index %s ' % (log_prefix, e)) + if not exists(connection, index_name): + if err_if_does_not_exist: + raise Exception('Failed to delete index {}. It does not exist!'.format(index_name)) + else: + return + + connection.indices.delete(index=index_name, **kwargs) + logger.debug('%s: Delete Index %s: Operation successfully completed', log_prefix, index_name) -def _create_index(connection, index_name, doc_type, logger, mapping_body, ignore_if_exists=False, **kwargs): +def _create_index(connection, index_name, doc_type, logger, mapping_body, err_if_exists=True, **kwargs): """ Creates an Elasticsearch index needed for similarity based searching Args: @@ -52,11 +69,11 @@ def _create_index(connection, index_name, doc_type, logger, mapping_body, ignore Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.put_mapping """ if exists(connection=connection, index_name=index_name): - if ignore_if_exists: - return - else: + if err_if_exists: raise Exception('Failed to create index {}. it already exists. Please check and delete it using ' - 'Datastore().delete()') + 'Datastore().delete()'.format(index_name)) + else: + return try: body = { 'index': { @@ -93,24 +110,11 @@ def _create_index(connection, index_name, doc_type, logger, mapping_body, ignore **put_mapping_kwargs) else: logger.debug('%s: doc_type not in arguments, skipping put_mapping on index ...' % log_prefix) - logger.debug('%s: Create Index: Operation successfully completed' % log_prefix) + logger.debug('%s: Create Index %s: Operation successfully completed', log_prefix, index_name) except Exception as e: - logger.exception('%s:Exception: while creating index, Rolling back \n %s' % (log_prefix, e)) + logger.exception('%s: Exception while creating index %s, Rolling back \n %s', log_prefix, index_name, e) delete_index(connection=connection, index_name=index_name, logger=logger) - - -def exists(connection, index_name): - """ - Checks if index_name exists - - Args: - connection: Elasticsearch client object - index_name: The name of the index - - Returns: - boolean, True if index exists , False otherwise - """ - return connection.indices.exists(index_name) + raise e def create_entity_index(connection, index_name, doc_type, logger, **kwargs): diff --git a/datastore_setup.py b/datastore_setup.py index 70ca94f1c..85adcad52 100644 --- a/datastore_setup.py +++ b/datastore_setup.py @@ -14,9 +14,9 @@ def setup_datastore(): db = DataStore() print("Setting up DataStore for Chatbot NER") print("Deleting any stale data ...") - db.delete() + db.delete(err_if_does_not_exist=False) print("Creating the structure ...") - db.create() + db.create(err_if_exists=True) print("Populating data from " + os.path.join(BASE_DIR, 'data', 'entity_data') + " ...") db.populate(entity_data_directory_path=DEFAULT_ENTITY_DATA_DIRECTORY) print("Done!") diff --git a/docker/cmd.sh b/docker/cmd.sh index 3b82c1182..3ef08fc20 100755 --- a/docker/cmd.sh +++ b/docker/cmd.sh @@ -8,7 +8,7 @@ export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Initial setup.py - Datastore lines need to be commented for using previously create data python /app/nltk_setup.py || { echo 'nltk setup failed'; exit 1; } -sleep 10 +sleep 8 python /app/datastore_setup.py || { echo 'datastore setup failed'; exit 1; } # Using supervisor as we want to use Nginx and Uwsgi both, Settings specified in supervisord.conf, any update to that will need build From 4a7536e1178ef150b59702df6d469a7517fed7a1 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 03:59:25 +0530 Subject: [PATCH 61/78] Fix incorrect attribute lookup --- datastore/datastore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datastore/datastore.py b/datastore/datastore.py index 299dcdac7..b9f69a8f8 100644 --- a/datastore/datastore.py +++ b/datastore/datastore.py @@ -120,7 +120,7 @@ def create(self, err_if_exists=True, **kwargs): ] for (required, index_name_key, doc_type_key, alias_name, doc_type_checker, create_fn) in create_map: index_name = self._connection_settings.get(index_name_key) - doc_type = self._client_or_connection.get(doc_type_key) + doc_type = self._connection_settings.get(doc_type_key) if not index_name: if required: raise DataStoreSettingsImproperlyConfiguredException( From bace444389ea044a386df5116fe6df57013fa277 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Thu, 16 Apr 2020 05:00:05 +0530 Subject: [PATCH 62/78] Switch to standard tokenizer, it handles special characters in text --- datastore/elastic_search/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datastore/elastic_search/create.py b/datastore/elastic_search/create.py index 62b67afab..f06d7c77a 100644 --- a/datastore/elastic_search/create.py +++ b/datastore/elastic_search/create.py @@ -80,7 +80,7 @@ def _create_index(connection, index_name, doc_type, logger, mapping_body, err_if 'analysis': { 'analyzer': { 'my_analyzer': { - 'tokenizer': 'whitespace', + 'tokenizer': 'standard', 'filter': ['standard', 'lowercase', 'my_stemmer'] } }, From 1ccbae3f9e59b47d763f14f7aaa13a881b907574 Mon Sep 17 00:00:00 2001 From: Chirag Jain Date: Fri, 17 Apr 2020 13:07:17 +0530 Subject: [PATCH 63/78] Reorganize datastore.py and add TODOs for future --- datastore/datastore.py | 264 +++++++++++++++++++++-------------------- 1 file changed, 138 insertions(+), 126 deletions(-) diff --git a/datastore/datastore.py b/datastore/datastore.py index b9f69a8f8..fae8ad90e 100644 --- a/datastore/datastore.py +++ b/datastore/datastore.py @@ -9,12 +9,15 @@ from datastore.constants import (ELASTICSEARCH, ENGINE, ELASTICSEARCH_ALIAS, ELASTICSEARCH_INDEX_1, ELASTICSEARCH_INDEX_2, ELASTICSEARCH_DOC_TYPE, ELASTICSEARCH_CRF_DATA_INDEX_NAME, ELASTICSEARCH_CRF_DATA_DOC_TYPE) -from datastore.elastic_search.transfer import ESTransfer from datastore.exceptions import (DataStoreSettingsImproperlyConfiguredException, EngineNotImplementedException, EngineConnectionException, NonESEngineTransferException, IndexNotFoundException) from lib.singleton import Singleton +# TODO: Bad design, rethink the API, write an abstract class DataStore implement ElasticSearchDataStore, +# cleanup deprecated and buggy code and write tests for CRUD operations. Maybe even remove multi engine support +# Reduce three different formats of data - dict, csv and json to one and use that everywhere +# Implement proper migrations for indices class DataStore(six.with_metaclass(Singleton, object)): """ Singleton class to connect to engine storing entity related data @@ -75,12 +78,51 @@ def _connect(self): if self._client_or_connection is None: raise EngineConnectionException(engine=self._engine) + def _check_doc_type_for_elasticsearch(self): + """ + Checks if doc_type is present in connection settings, if not an exception is raised + + Raises: + DataStoreSettingsImproperlyConfiguredException if doc_type was not found in connection settings + """ + # TODO: This check should be during init or boot + if ELASTICSEARCH_DOC_TYPE not in self._connection_settings: + raise DataStoreSettingsImproperlyConfiguredException( + 'Elasticsearch needs doc_type. Please configure ES_DOC_TYPE in your environment') + + def _check_doc_type_for_crf_data_elasticsearch(self): + """ + Checks if doc_type is present in connection settings, if not an exception is raised + + Raises: + DataStoreSettingsImproperlyConfiguredException if doc_type was not found in connection settings + """ + # TODO: This check should be during init or boot + if ELASTICSEARCH_CRF_DATA_DOC_TYPE not in self._connection_settings: + raise DataStoreSettingsImproperlyConfiguredException( + 'Elasticsearch training data needs doc_type. Please configure ' + 'ES_TRAINING_DATA_DOC_TYPE in your environment') + + def exists(self): + """ + Checks if DataStore is already created + Returns: + boolean, True if DataStore structure exists, False otherwise + """ + if self._client_or_connection is None: + self._connect() + + if self._engine == ELASTICSEARCH: + return elastic_search.create.exists(connection=self._client_or_connection, index_name=self._store_name) + + return False + def create(self, err_if_exists=True, **kwargs): """ Creates the schema/structure for the datastore depending on the engine configured in the environment. Args: - ignore_if_exists (bool): if to ignore index already exists errors, default False + err_if_exists (bool): if to throw error when index already exists, default True kwargs: For Elasticsearch: master_timeout: Specify timeout for connection to master @@ -109,7 +151,7 @@ def create(self, err_if_exists=True, **kwargs): if self._engine == ELASTICSEARCH: es_url = elastic_search.connect.get_es_url() - es_object = ESTransfer(source=es_url, destination=None) + es_object = elastic_search.transfer.ESTransfer(source=es_url, destination=None) create_map = [ # TODO: use namedtuples (True, ELASTICSEARCH_INDEX_1, ELASTICSEARCH_DOC_TYPE, self._store_name, self._check_doc_type_for_elasticsearch, elastic_search.create.create_entity_index), @@ -141,6 +183,42 @@ def create(self, err_if_exists=True, **kwargs): es_object.point_an_alias_to_index(es_url=es_url, alias_name=self._store_name, index_name=index_name) + def delete(self, err_if_does_not_exist=True, **kwargs): + """ + Deletes all data including the structure of the datastore. Note that this is equivalent to DROP not TRUNCATE + + Args: + err_if_does_not_exist (bool): if to raise index does not exist errors, default True + kwargs: + For Elasticsearch: + body: The configuration for the index (settings and mappings) + master_timeout: Specify timeout for connection to master + timeout: Explicit operation timeout + update_all_types: Whether to update the mapping for all fields with the same name across all types + or not + wait_for_active_shards: Set the number of active shards to wait for before the operation returns. + + Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.delete + + Raises: + All exceptions raised by elasticsearch-py library + + """ + if self._client_or_connection is None: + self._connect() + + if self._engine == ELASTICSEARCH: + for index_key in [ELASTICSEARCH_INDEX_1, ELASTICSEARCH_INDEX_2, ELASTICSEARCH_CRF_DATA_INDEX_NAME]: + if self._connection_settings.get(index_key): + elastic_search.create.delete_index(connection=self._client_or_connection, + index_name=self._store_name, + logger=ner_logger, + err_if_does_not_exist=err_if_does_not_exist, + **kwargs) + # TODO: cleanup aliases ? + + # === Incompatible or deprecated/duplicate APIs + # FIXME: repopulate does not consider language of the variants def populate(self, entity_data_directory_path=None, csv_file_paths=None, **kwargs): """ @@ -178,40 +256,6 @@ def populate(self, entity_data_directory_path=None, csv_file_paths=None, **kwarg logger=ner_logger, **kwargs) - def delete(self, err_if_does_not_exist=True, **kwargs): - """ - Deletes all data including the structure of the datastore. Note that this is equivalent to DROP not TRUNCATE - - Args: - err_if_does_not_exist (bool): if to raise index does not exist errors, default True - kwargs: - For Elasticsearch: - body: The configuration for the index (settings and mappings) - master_timeout: Specify timeout for connection to master - timeout: Explicit operation timeout - update_all_types: Whether to update the mapping for all fields with the same name across all types - or not - wait_for_active_shards: Set the number of active shards to wait for before the operation returns. - - Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.client.IndicesClient.delete - - Raises: - All exceptions raised by elasticsearch-py library - - """ - if self._client_or_connection is None: - self._connect() - - if self._engine == ELASTICSEARCH: - for index_key in [ELASTICSEARCH_INDEX_1, ELASTICSEARCH_INDEX_2, ELASTICSEARCH_CRF_DATA_INDEX_NAME]: - if self._connection_settings.get(index_key): - elastic_search.create.delete_index(connection=self._client_or_connection, - index_name=self._store_name, - logger=ner_logger, - err_if_does_not_exist=err_if_does_not_exist, - **kwargs) - # TODO: cleanup aliases ? - # FIXME: Deprecated, remove def get_entity_dictionary(self, entity_name, **kwargs): """ @@ -263,58 +307,7 @@ def get_entity_dictionary(self, entity_name, **kwargs): return results_dictionary - def get_similar_dictionary(self, entity_name, texts, fuzziness_threshold="auto:4,7", - search_language_script=None, **kwargs): - """ - Args: - entity_name: the name of the entity to lookup in the datastore for getting entity values and their variants - texts(list of strings): the text for which variants need to be find out - fuzziness_threshold: fuzziness allowed for search results on entity value variants - search_language_script: language of elasticsearch documents which are eligible for match - kwargs: - For Elasticsearch: - Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.Elasticsearch.search - - Returns: - list of collections.OrderedDict: dictionary mapping entity value variants to their entity value - - Example: - db = DataStore() - ngrams_list = ['Pune', 'Mumbai', 'Goa', 'Bangalore'] - db.get_similar_ngrams_dictionary(entity_name='city', ngrams_list=ngrams_list, fuzziness_threshold=2) - - Output: - [ - {u'Bangalore': u'Bangalore', - u'Mulbagal': u'Mulbagal', - u'Multai': u'Multai', - u'Mumbai': u'Mumbai', - u'Pune': u'Pune', - u'Puri': u'Puri', - u'bangalore': u'bengaluru', - u'goa': u'goa', - u'mumbai': u'mumbai', - u'pune': u'pune'} - ] - """ - results_list = [] - if self._client_or_connection is None: - self._connect() - if self._engine == ELASTICSEARCH: - self._check_doc_type_for_elasticsearch() - request_timeout = self._connection_settings.get('request_timeout', 20) - results_list = elastic_search.query.full_text_query(connection=self._client_or_connection, - index_name=self._store_name, - doc_type=self._connection_settings[ - ELASTICSEARCH_DOC_TYPE], - entity_name=entity_name, - sentences=texts, - fuzziness_threshold=fuzziness_threshold, - search_language_script=search_language_script, - request_timeout=request_timeout, - **kwargs) - return results_list - + # FIXME: Deprecated, remove def delete_entity(self, entity_name, **kwargs): """ Deletes the entity data for entity named entity_named from the datastore @@ -379,43 +372,6 @@ def repopulate(self, entity_data_directory_path=None, csv_file_paths=None, **kwa **kwargs) # TODO: repopulate code for crf index missing - def _check_doc_type_for_elasticsearch(self): - """ - Checks if doc_type is present in connection settings, if not an exception is raised - - Raises: - DataStoreSettingsImproperlyConfiguredException if doc_type was not found in connection settings - """ - if ELASTICSEARCH_DOC_TYPE not in self._connection_settings: - raise DataStoreSettingsImproperlyConfiguredException( - 'Elasticsearch needs doc_type. Please configure ES_DOC_TYPE in your environment') - - def _check_doc_type_for_crf_data_elasticsearch(self): - """ - Checks if doc_type is present in connection settings, if not an exception is raised - - Raises: - DataStoreSettingsImproperlyConfiguredException if doc_type was not found in connection settings - """ - if ELASTICSEARCH_CRF_DATA_DOC_TYPE not in self._connection_settings: - raise DataStoreSettingsImproperlyConfiguredException( - 'Elasticsearch training data needs doc_type. Please configure ' - 'ES_TRAINING_DATA_DOC_TYPE in your environment') - - def exists(self): - """ - Checks if DataStore is already created - Returns: - boolean, True if DataStore structure exists, False otherwise - """ - if self._client_or_connection is None: - self._connect() - - if self._engine == ELASTICSEARCH: - return elastic_search.create.exists(connection=self._client_or_connection, index_name=self._store_name) - - return False - # FIXME: Deprecated, remove def update_entity_data(self, entity_name, entity_data, language_script, **kwargs): """ @@ -445,6 +401,60 @@ def update_entity_data(self, entity_name, entity_data, language_script, **kwargs language_script=language_script, **kwargs) + # === New Style CRUD APIs that support languages and partial updates === + + def get_similar_dictionary(self, entity_name, texts, fuzziness_threshold="auto:4,7", + search_language_script=None, **kwargs): + """ + Args: + entity_name: the name of the entity to lookup in the datastore for getting entity values and their variants + texts(list of strings): the text for which variants need to be find out + fuzziness_threshold: fuzziness allowed for search results on entity value variants + search_language_script: language of elasticsearch documents which are eligible for match + kwargs: + For Elasticsearch: + Refer https://elasticsearch-py.readthedocs.io/en/master/api.html#elasticsearch.Elasticsearch.search + + Returns: + list of collections.OrderedDict: dictionary mapping entity value variants to their entity value + + Example: + db = DataStore() + ngrams_list = ['Pune', 'Mumbai', 'Goa', 'Bangalore'] + db.get_similar_ngrams_dictionary(entity_name='city', ngrams_list=ngrams_list, fuzziness_threshold=2) + + Output: + [ + {u'Bangalore': u'Bangalore', + u'Mulbagal': u'Mulbagal', + u'Multai': u'Multai', + u'Mumbai': u'Mumbai', + u'Pune': u'Pune', + u'Puri': u'Puri', + u'bangalore': u'bengaluru', + u'goa': u'goa', + u'mumbai': u'mumbai', + u'pune': u'pune'} + ] + """ + results_list = [] + if self._client_or_connection is None: + self._connect() + if self._engine == ELASTICSEARCH: + self._check_doc_type_for_elasticsearch() + request_timeout = self._connection_settings.get('request_timeout', 20) + results_list = elastic_search.query.full_text_query(connection=self._client_or_connection, + index_name=self._store_name, + doc_type=self._connection_settings[ + ELASTICSEARCH_DOC_TYPE], + entity_name=entity_name, + sentences=texts, + fuzziness_threshold=fuzziness_threshold, + search_language_script=search_language_script, + request_timeout=request_timeout, + **kwargs) + return results_list + def get_entity_supported_languages(self, entity_name, **kwargs): """ Fetch supported language list for the entity @@ -650,6 +660,7 @@ def get_crf_data_for_entity_name(self, entity_name, languages, **kwargs): ner_logger.debug('Datastore, get_entity_training_data, results_dictionary %s' % str(entity_name)) return results_dictionary + # FIXME: Inconsistent data format with other APIs or confusing parameter names! def update_entity_crf_data(self, entity_name, sentences, **kwargs): """ This method is used to populate the training data for a given entity @@ -657,6 +668,7 @@ def update_entity_crf_data(self, entity_name, sentences, **kwargs): Args: entity_name (str): Name of the entity for which the training data has to be populated sentences (Dict[str, List[Dict[str, str]]]: sentences mapped against their languages + E.g. {"en": [{"sentence": "hello abc", "entities": ["abc"],}, ...], ...} **kwargs: For Elasticsearch: Refer http://elasticsearch-py.readthedocs.io/en/master/helpers.html#elasticsearch.helpers.bulk From 5345241dc453aaa04cb64efc555d7b29844099e1 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Wed, 22 Apr 2020 06:20:22 +0530 Subject: [PATCH 64/78] Update test cases in collection Using lodash to sort the array of expected and response objects by original_text. This is to handle the case where the order in response is different than the order in the test case. --- postman_tests/data/ner_collection.json | 152 ++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/postman_tests/data/ner_collection.json b/postman_tests/data/ner_collection.json index 82dee76e1..7fac3e948 100644 --- a/postman_tests/data/ner_collection.json +++ b/postman_tests/data/ner_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "6d097f7b-2fbf-4295-853b-3592a5553dd7", + "_postman_id": "1e705b84-fac8-4314-bf05-aae01e2e876b", "name": "ner_collection", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -13,6 +13,7 @@ "script": { "id": "d2e89180-b51e-44fe-87af-a971b48f15bb", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('city_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -32,6 +33,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.city_expected = _.orderBy(data.city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.city_expected[i].to);", " }", @@ -39,6 +42,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.city_expected = _.orderBy(data.city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.city_expected[i].from);", " }", @@ -46,6 +51,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.city_expected = _.orderBy(data.city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.city_expected[i].original_text);", " }", @@ -53,6 +60,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.city_expected = _.orderBy(data.city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.city_expected[i].value.toLowerCase());", " }", @@ -60,6 +69,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.city_expected = _.orderBy(data.city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.city_expected[i].normal);", " }", @@ -107,6 +118,7 @@ "script": { "id": "5c4a0338-38cf-482b-be58-de81496136bb", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('time_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -126,6 +138,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_expected = _.orderBy(data.time_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.time_expected[i].original_text); ", " }", @@ -133,6 +147,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_expected = _.orderBy(data.time_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_expected[i].mm); ", " }", @@ -140,6 +156,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_expected = _.orderBy(data.time_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_expected[i].hh); ", " }", @@ -147,6 +165,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_expected = _.orderBy(data.time_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_expected[i].normal); ", " }", @@ -194,6 +214,7 @@ "script": { "id": "aab03db0-2700-4ed1-a9d4-978acce08ba1", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('person_name_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -213,6 +234,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.person_name_expected[i].original_text);", " }", @@ -220,6 +243,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid first_name\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.first_name).to.eql(data.person_name_expected[i].first_name);", " }", @@ -227,6 +252,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid last_name\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.last_name).to.eql(data.person_name_expected[i].last_name);", " }", @@ -234,6 +261,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid middle_name\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.middle_name).to.eql(data.person_name_expected[i].middle_name);", " }", @@ -241,6 +270,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid model_verified\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.model_verified).to.eql(data.person_name_expected[i].model_verified);", " }", @@ -248,6 +279,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid datastore_verified\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.person_name_expected = _.orderBy(data.person_name_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.datastore_verified).to.eql(data.person_name_expected[i].datastore_verified);", " }", @@ -295,6 +328,7 @@ "script": { "id": "c88850db-8dc1-4ca4-bb67-66568fb04fd1", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('pnr_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -314,6 +348,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.pnr_expected = _.orderBy(data.pnr_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.pnr_expected[i].original_text);", " }", @@ -321,6 +357,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.pnr_expected = _.orderBy(data.pnr_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.pnr_expected[i].value);", " }", @@ -368,6 +406,7 @@ "script": { "id": "6726f8f5-a85a-46c8-9d67-c483d2d68db3", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('time_range_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -387,6 +426,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_range_expected = _.orderBy(data.time_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.time_range_expected[i].original_text); ", " }", @@ -394,6 +435,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_range_expected = _.orderBy(data.time_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.time_range_expected[i].mm); ", " }", @@ -401,6 +444,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_range_expected = _.orderBy(data.time_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.time_range_expected[i].hh); ", " }", @@ -408,6 +453,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid range\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_range_expected = _.orderBy(data.time_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.range).to.eql(data.time_range_expected[i].range); ", " }", @@ -415,6 +462,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.time_range_expected = _.orderBy(data.time_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.time_range_expected[i].normal); ", " }", @@ -462,6 +511,7 @@ "script": { "id": "180b0299-b3bf-4afb-acff-1642a40ada0c", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('regex_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -481,6 +531,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.regex_expected = _.orderBy(data.regex_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.regex_expected[i].original_text);", " }", @@ -488,6 +540,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.regex_expected = _.orderBy(data.regex_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.regex_expected[i].value);", " }", @@ -539,6 +593,7 @@ "script": { "id": "f0342e22-da7b-44f2-a55b-b52050907b35", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('email_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -558,6 +613,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.email_expected = _.orderBy(data.email_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.email_expected[i].original_text);", " }", @@ -566,6 +623,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.email_expected = _.orderBy(data.email_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.email_expected[i].value); ", " }", @@ -613,6 +672,7 @@ "script": { "id": "912756f9-cceb-4ba3-ac67-14d83c3df8e4", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('budget_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -632,6 +692,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.budget_expected = _.orderBy(data.budget_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.budget_expected[i].original_text);", " }", @@ -639,6 +701,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_budget\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.budget_expected = _.orderBy(data.budget_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.max_budget).to.eql(data.budget_expected[i].max_budget);", " }", @@ -646,6 +710,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_budget\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.budget_expected = _.orderBy(data.budget_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.min_budget).to.eql(data.budget_expected[i].min_budget);", " }", @@ -653,6 +719,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.budget_expected = _.orderBy(data.budget_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.type).to.eql(data.budget_expected[i].type);", " }", @@ -701,6 +769,7 @@ "script": { "id": "df576c4c-f337-4964-9cfb-a7f4e5611784", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('date_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -720,6 +789,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.date_expected = _.orderBy(data.date_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.date_expected[i].original_text);", " }", @@ -727,6 +798,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.date_expected = _.orderBy(data.date_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.date_expected[i].type);", " }", @@ -734,6 +807,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.date_expected = _.orderBy(data.date_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.date_expected[i].dd);", " }", @@ -741,6 +816,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.date_expected = _.orderBy(data.date_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.date_expected[i].mm);", " }", @@ -748,6 +825,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.date_expected = _.orderBy(data.date_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.date_expected[i].yy);", " }", @@ -796,6 +875,7 @@ "script": { "id": "a1bf1b2a-f098-41d2-8c33-dd5d78ea8b4f", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('number_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -815,6 +895,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.number_expected = _.orderBy(data.number_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.number_expected[i].original_text); ", " }", @@ -822,6 +904,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.number_expected = _.orderBy(data.number_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.number_expected[i].value); ", " }", @@ -878,6 +962,7 @@ "script": { "id": "a1bf1b2a-f098-41d2-8c33-dd5d78ea8b4f", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('numberV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -897,6 +982,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.numberV2_expected = _.orderBy(data.numberV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.numberV2_expected[i].original_text); ", " }", @@ -904,6 +991,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.numberV2_expected = _.orderBy(data.numberV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.numberV2_expected[i].value); ", " }", @@ -960,6 +1049,7 @@ "script": { "id": "2481d107-0e0c-4545-8576-b04e4e2ff818", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('number_range_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -979,6 +1069,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.number_range_expected = _.orderBy(data.number_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.number_range_expected[i].original_text);", " }", @@ -986,6 +1078,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid min_value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.number_range_expected = _.orderBy(data.number_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.min_value).to.eql(data.number_range_expected[i].min_value);", " }", @@ -993,6 +1087,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid max_value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.number_range_expected = _.orderBy(data.number_range_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.max_value).to.eql(data.number_range_expected[i].max_value);", " }", @@ -1040,6 +1136,7 @@ "script": { "id": "e6e7a3fe-9a22-49a0-8ca9-cdffb794f743", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('phoneV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1059,6 +1156,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.phoneV2_expected = _.orderBy(data.phoneV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV2_expected[i].original_text);", " }", @@ -1066,6 +1165,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.phoneV2_expected = _.orderBy(data.phoneV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV2_expected[i].value);", " }", @@ -1073,6 +1174,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid country_calling_code\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.phoneV2_expected = _.orderBy(data.phoneV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.country_calling_code).to.eql(data.phoneV2_expected[i].country_calling_code);", " }", @@ -1120,6 +1223,7 @@ "script": { "id": "4b978b73-2ca4-4263-afcb-ae44affa1fed", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('phoneV1_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1139,6 +1243,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.phoneV1_expected = _.orderBy(data.phoneV1_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.phoneV1_expected[i].original_text);", " }", @@ -1146,6 +1252,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.phoneV1_expected = _.orderBy(data.phoneV1_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value).to.eql(data.phoneV1_expected[i].value);", " }", @@ -1192,6 +1300,7 @@ "script": { "id": "740b024e-dd31-40e6-8232-d5f6cc314688", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('dateV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1211,6 +1320,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.dateV2_expected[i].original_text);", " }", @@ -1218,6 +1329,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid type\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.type).to.eql(data.dateV2_expected[i].type);", " }", @@ -1225,6 +1338,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid day\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.dd).to.eql(data.dateV2_expected[i].dd);", " }", @@ -1232,6 +1347,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid month\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.mm).to.eql(data.dateV2_expected[i].mm);", " }", @@ -1239,6 +1356,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid year\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.yy).to.eql(data.dateV2_expected[i].yy);", " }", @@ -1246,6 +1365,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid from\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.from).to.eql(data.dateV2_expected[i].from);", " }", @@ -1253,6 +1374,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid to\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.to).to.eql(data.dateV2_expected[i].to);", " }", @@ -1260,6 +1383,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid start_range\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.start_range).to.eql(data.dateV2_expected[i].start_range);", " }", @@ -1267,6 +1392,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid end_range\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.dateV2_expected = _.orderBy(data.dateV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.end_range).to.eql(data.dateV2_expected[i].end_range);", " }", @@ -1315,6 +1442,7 @@ "script": { "id": "8c90d04c-a62e-4e78-bd67-710a7df2c53d", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('text_city_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1333,6 +1461,8 @@ "", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.text_city_expected = _.orderBy(data.text_city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.text_city_expected[i].original_text);", " }", @@ -1340,6 +1470,8 @@ "", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.text_city_expected = _.orderBy(data.text_city_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.text_city_expected[i].value.toLowerCase());", " }", @@ -1386,6 +1518,7 @@ "script": { "id": "8c90d04c-a62e-4e78-bd67-710a7df2c53d", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('text_restaurant_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1404,6 +1537,8 @@ "", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.text_restaurant_expected = _.orderBy(data.text_restaurant_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.text_restaurant_expected[i].original_text);", " }", @@ -1411,6 +1546,8 @@ "", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid value\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.text_restaurant_expected = _.orderBy(data.text_restaurant_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.value.toLowerCase()).to.eql(data.text_restaurant_expected[i].value.toLowerCase());", " }", @@ -1459,6 +1596,7 @@ "script": { "id": "1fb98ca6-5e20-42da-9ce5-b9be1652c09c", "exec": [ + "var _ = require('lodash');", "var shouldBeSkipped = !('timeV2_expected' in data);", "", "(shouldBeSkipped ? pm.test.skip : pm.test)(\"Response is 200 OK\", function () {", @@ -1478,6 +1616,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid original_text\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].original_text).to.eql(data.timeV2_expected[i].original_text); ", " }", @@ -1485,6 +1625,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid minutes\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.mm).to.eql(data.timeV2_expected[i].mm); ", " }", @@ -1492,6 +1634,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid hours\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.hh).to.eql(data.timeV2_expected[i].hh); ", " }", @@ -1499,6 +1643,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid normal\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.normal).to.eql(data.timeV2_expected[i].normal); ", " }", @@ -1506,6 +1652,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid timezone\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].entity_value.tz).to.eql(data.timeV2_expected[i].tz); ", " }", @@ -1513,6 +1661,8 @@ " ", " (shouldBeSkipped ? pm.test.skip : pm.test)(\"Response data has valid language\", function() {", " var jsonData = pm.response.json();", + " jsonData.data = _.orderBy(jsonData.data, [\"original_text\"]);", + " data.timeV2_expected = _.orderBy(data.timeV2_expected, [\"original_text\"]);", " for(i = 0; i < jsonData.data.length; i++) {", " pm.expect(jsonData.data[i].language).to.eql(data.timeV2_expected[i].language);", " }", From 115bb5d796a4fda526f501e80639e9684ae94218 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Wed, 22 Apr 2020 13:50:45 +0530 Subject: [PATCH 65/78] Create reports directory if does not exist --- postman_tests/run_tests.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 1bb7a4b5c..551ac4ae1 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -12,6 +12,19 @@ config_path = os.path.join(postman_tests_directory, 'config') +def create_reports_directory_if_not_exists(report_path): + """ Creates the directory where the report generated by newman command is stored + + Args: + report_path: string + + Returns: + None + """ + if not os.path.exists(report_path): + os.makedirs(report_path) + + def get_newman_command(): """ Returns the newman shell command to be used for running the tests @@ -22,8 +35,9 @@ def get_newman_command(): (str): The shell command to be used for running the tests """ collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') - report_path = os.path.join(postman_tests_directory, 'newman_reports') if os.path.exists(f"{config_path}/dev.json"): + report_path = os.path.join(postman_tests_directory, 'newman_reports') + create_reports_directory_if_not_exists(report_path) environment_file_path = f'{config_path}/dev.json' return ( f'newman run {collection_data_path} -d {newman_data_path}' @@ -65,6 +79,7 @@ def run_tests(): finally: datastore.sync(es_data_path, config_path, 'delete') + if __name__ == "__main__": status = run_tests() sys.exit(status) From 1a89d978753c34ae954278dc0519b9b44d4a1b0a Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Wed, 22 Apr 2020 14:30:48 +0530 Subject: [PATCH 66/78] Update Readme --- postman_tests/Readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index 65810c29d..8cefbb208 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -39,7 +39,7 @@ The format should follow the below structure: input contains the parameters that we pass as query parameters in the GET rquest. It must **mandatorily** contain two keys, message and entity_name. -expected is an array of objects that we get in the response. If expected contains multiple objects then the order of those should be exactly as the expected order in the response. +expected is an array of objects that we get in the response. If you want to create a test case where the output of a request is expected to be null, then specify the test case as follows: @@ -47,7 +47,7 @@ If you want to create a test case where the output of a request is expected to b [ "input": { "message": "The text sent in url", - "entity_name": "Name of the entity e.g. time" + "entity_name": "Name of the entity. If this entity uses ES indexing then the name should be prefixed by ner_ptest e.g. ner_ptest_restuarant" }, "expected": [ @@ -76,7 +76,7 @@ Use the below steps: 1. First import postman_tests/data/ner_collection.json into postman -2. After adding new tests or modifying existing ones, export the modified collection into ner_collection.json. +2. After adding new tests or modifying existing ones, export the modified collection into ner_collection.json in postman_tests/data folder in ner. 3. Run the test suite using the process given in this Readme above to make sure all pass. @@ -85,7 +85,7 @@ Use the below steps: **Adding data to be indexed into ElasticSearch** -1. Add the csv file for the particular entity in postman_tests/data/elastic_search/. +1. Add the csv file with a filename preceded by ner_ptest for the particular entity in postman_tests/data/elastic_search/, e.g. ner_ptest_restaurant.csv. 2. Make sure all the required data being used in the tests is present in the csv file and all tests are passing. From 10a2707f2a3072584e713e913147b38bbec3a3d4 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 13:24:40 +0530 Subject: [PATCH 67/78] Renamed variable str to variants_line --- postman_tests/lib/datastore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 68c891d3b..48d6e8da3 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -71,7 +71,7 @@ def convert_csv_to_dict(file_path, mode): return data -def get_variants(str): +def get_variants(variants_line): """ Convert the string containing all the variants into a list @@ -81,5 +81,5 @@ def get_variants(str): Returns: list: List of strings where each string is a variant name. """ - arr = str.split('|') + arr = variants_line.split('|') return [item.strip() for item in arr if item.strip()] From 4616fb13116febeb32c849fae6cc49e835fbbba7 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 13:35:33 +0530 Subject: [PATCH 68/78] Change all references to ElasticSearch to DataStore --- postman_tests/config/prod.json | 2 +- .../ner_ptest_city_list.csv | 0 .../ner_ptest_restaurant.csv | 0 postman_tests/lib/datastore.py | 10 +++++----- postman_tests/run_tests.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) rename postman_tests/data/{elastic_search => data_store}/ner_ptest_city_list.csv (100%) rename postman_tests/data/{elastic_search => data_store}/ner_ptest_restaurant.csv (100%) diff --git a/postman_tests/config/prod.json b/postman_tests/config/prod.json index 3d5132c5f..fe6ee6f5c 100644 --- a/postman_tests/config/prod.json +++ b/postman_tests/config/prod.json @@ -11,5 +11,5 @@ "_postman_variable_scope": "environment", "_postman_exported_at": "2020-03-05T12:18:03.223Z", "_postman_exported_using": "Postman/7.19.1", - "es_host": "localhost:8081" + "datastore_host": "localhost:8081" } diff --git a/postman_tests/data/elastic_search/ner_ptest_city_list.csv b/postman_tests/data/data_store/ner_ptest_city_list.csv similarity index 100% rename from postman_tests/data/elastic_search/ner_ptest_city_list.csv rename to postman_tests/data/data_store/ner_ptest_city_list.csv diff --git a/postman_tests/data/elastic_search/ner_ptest_restaurant.csv b/postman_tests/data/data_store/ner_ptest_restaurant.csv similarity index 100% rename from postman_tests/data/elastic_search/ner_ptest_restaurant.csv rename to postman_tests/data/data_store/ner_ptest_restaurant.csv diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 48d6e8da3..1100e7d98 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -14,21 +14,21 @@ def get_api_url(config_path): config_file_path = f"{config_path}/prod.json" with open(config_file_path, 'r') as f: data = json.load(f) - base_url = data["es_host"] + base_url = data["datastore_host"] return f"http://{base_url}/entities/data/v1" -def sync(es_data_path, config_path, mode): +def sync(datastore_data_path, config_path, mode): """ - Index data for every entity being tested into ElasticSearch + Index data for every entity being tested into DataStore Parameters: - es_data_path (string): Path to the data/elastic_search directory + datastore_data_path (string): Path to the data/elastic_search directory Returns: None """ - for file_path in glob.glob(os.path.join(es_data_path, '*.csv')): + for file_path in glob.glob(os.path.join(datastore_data_path, '*.csv')): entity_name = common.get_entity_name(file_path) print(f"Syncing {entity_name}, mode: {mode}") contents = convert_csv_to_dict(file_path, mode) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 551ac4ae1..8f5232824 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -62,10 +62,10 @@ def run_tests(): (int): The return code of the newman command """ entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') - es_data_path = os.path.join(postman_tests_directory, 'data', 'elastic_search') + datastore_data_path = os.path.join(postman_tests_directory, 'data', 'data_store') try: newman.check_if_data_valid(entities_data_path) - datastore.sync(es_data_path, config_path, 'create') + datastore.sync(datastore_data_path, config_path, 'create') newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp) @@ -77,7 +77,7 @@ def run_tests(): except Exception as e: raise e finally: - datastore.sync(es_data_path, config_path, 'delete') + datastore.sync(datastore_data_path, config_path, 'delete') if __name__ == "__main__": From 97f976f485f307fa4831f6e2062faf3c88a33dc1 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 13:53:15 +0530 Subject: [PATCH 69/78] Fix docstrings --- postman_tests/lib/datastore.py | 27 ++++++++++++--------------- postman_tests/lib/newman.py | 27 +++++++++++++-------------- postman_tests/run_tests.py | 18 ++++++------------ 3 files changed, 31 insertions(+), 41 deletions(-) diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 1100e7d98..ea143fa41 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -19,14 +19,13 @@ def get_api_url(config_path): def sync(datastore_data_path, config_path, mode): - """ - Index data for every entity being tested into DataStore + """Index data for every entity being tested into DataStore. - Parameters: - datastore_data_path (string): Path to the data/elastic_search directory + Args: + datastore_data_path (str): Path to the data/data_store directory. Returns: - None + None """ for file_path in glob.glob(os.path.join(datastore_data_path, '*.csv')): entity_name = common.get_entity_name(file_path) @@ -41,14 +40,13 @@ def sync(datastore_data_path, config_path, mode): def convert_csv_to_dict(file_path, mode): - """ - Read the csv file at file_path and convert its data to json + """Read the csv file at file_path and convert its data to json. - Parameters: - file_path (string): Path of a file in the data/elastic_search directory + Args: + file_path (str): Path of a file in the data/data_store directory. Returns: - string: The JSON representation of the csv data in the file + str: The JSON representation of the csv data in the file. """ if mode == 'create': data = {'replace': True, 'edited': []} @@ -72,14 +70,13 @@ def convert_csv_to_dict(file_path, mode): def get_variants(variants_line): - """ - Convert the string containing all the variants into a list + """Convert the string containing all the variants into a list. - Parameters: - str (string): String containing all the variant names. + Args: + variants_line (str): String containing all the variant names. Returns: - list: List of strings where each string is a variant name. + list: List of strings where each string is a variant name. """ arr = variants_line.split('|') return [item.strip() for item in arr if item.strip()] diff --git a/postman_tests/lib/newman.py b/postman_tests/lib/newman.py index 227a80333..a45faf037 100644 --- a/postman_tests/lib/newman.py +++ b/postman_tests/lib/newman.py @@ -8,18 +8,18 @@ def check_if_data_valid(entities_data_path): # type: (str) -> boolean - """ - Doing basic sanity checking here. - Check that every test case is valid json and has the keys: input and expected + """Doing basic sanity checking here. + + Check that every test case is valid json and has the keys: input and expected. Args: - entities_data_path (str): Path to the data/entities directory + entities_data_path (str): Path to the data/entities directory. Returns: - (boolean): Returns True if all files contain valid json. + bool: Returns True if all files contain valid json. Raises: - Exception if data is invalid + Exception if data is invalid. """ try: path = '' @@ -39,15 +39,15 @@ def check_if_data_valid(entities_data_path): def read_entities_data(entities_data_path): - # type: (str) -> Dict + # type: (str) -> Dict[str, List[Dict[str, Any]]] """Read all the files in data/entities and generate a single dictionary containing data for every entity. Args: - entities_data_path (string): Path to the data/entities directory. + entities_data_path (str): Path to the data/entities directory. Returns: - dictionary (dict): Dict containing data read from the data files for every entity. + dict: Dict containing data read from the data files for every entity. """ entities_data = {} for file_path in glob.glob(os.path.join(entities_data_path, '*.json')): @@ -67,15 +67,14 @@ def read_entities_data(entities_data_path): def generate_newman_data(entities_data_path): - # type: (str) -> List[Dict[str, any]] - """Generate data in a format that can be passed to the command-line tool - newman for runing the tests. + # type: (str) -> List[Dict[str, Any]] + """Generate data in a format that can be passed to the command-line tool newman for runing the tests. Args: - entities_data_path (string): Path to the data/entities directory. + entities_data_path (str): Path to the data/entities directory. Returns: - newman_data (list): List of dictionaries where each dictionary is data for a single test iteration. + list: List of dictionaries where each dictionary is data for a single test iteration. """ data = [] entities_data = read_entities_data(entities_data_path) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 8f5232824..b78a0ac4e 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -13,10 +13,10 @@ def create_reports_directory_if_not_exists(report_path): - """ Creates the directory where the report generated by newman command is stored + """Creates the directory where the report generated by newman command is stored. Args: - report_path: string + report_path (str): The path to the folder where the html report will be generated by newman. Returns: None @@ -26,13 +26,10 @@ def create_reports_directory_if_not_exists(report_path): def get_newman_command(): - """ Returns the newman shell command to be used for running the tests - - Args: - None + """Returns the newman shell command to be used for running the tests. Returns: - (str): The shell command to be used for running the tests + str: The shell command to be used for running the tests. """ collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') if os.path.exists(f"{config_path}/dev.json"): @@ -53,13 +50,10 @@ def get_newman_command(): def run_tests(): - """ Runs the newman test-suite - - Args: - None + """Runs the newman test-suite. Returns: - (int): The return code of the newman command + int: The return code of the newman command. """ entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') datastore_data_path = os.path.join(postman_tests_directory, 'data', 'data_store') From 0fb71d1c553cc52fd569dbb522e7a0e5b14250f2 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 14:40:40 +0530 Subject: [PATCH 70/78] Using single test_env.json and --html flag --- .../config/{prod.json => test_env.json} | 0 postman_tests/lib/datastore.py | 10 +++------- postman_tests/run_tests.py | 20 +++++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) rename postman_tests/config/{prod.json => test_env.json} (100%) diff --git a/postman_tests/config/prod.json b/postman_tests/config/test_env.json similarity index 100% rename from postman_tests/config/prod.json rename to postman_tests/config/test_env.json diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index ea143fa41..5a8ba0002 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -7,18 +7,14 @@ from . import common -def get_api_url(config_path): - if os.path.exists(f"{config_path}/dev.json"): - config_file_path = f"{config_path}/dev.json" - else: - config_file_path = f"{config_path}/prod.json" +def get_api_url(config_file_path): with open(config_file_path, 'r') as f: data = json.load(f) base_url = data["datastore_host"] return f"http://{base_url}/entities/data/v1" -def sync(datastore_data_path, config_path, mode): +def sync(datastore_data_path, config_file_path, mode): """Index data for every entity being tested into DataStore. Args: @@ -31,7 +27,7 @@ def sync(datastore_data_path, config_path, mode): entity_name = common.get_entity_name(file_path) print(f"Syncing {entity_name}, mode: {mode}") contents = convert_csv_to_dict(file_path, mode) - url = get_api_url(config_path) + url = get_api_url(config_file_path) try: req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) req.raise_for_status() diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index b78a0ac4e..86d0ade97 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -3,13 +3,14 @@ import os import json import sys +import argparse from lib import newman from lib import datastore postman_tests_directory = os.path.dirname(os.path.abspath(__file__)) newman_data_path = os.path.join(postman_tests_directory, 'data', 'newman_data.json') -config_path = os.path.join(postman_tests_directory, 'config') +config_file_path = os.path.join(postman_tests_directory, 'config', 'test_env.json') def create_reports_directory_if_not_exists(report_path): @@ -32,20 +33,23 @@ def get_newman_command(): str: The shell command to be used for running the tests. """ collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') - if os.path.exists(f"{config_path}/dev.json"): + # Check if --html is passed as argument + parser = argparse.ArgumentParser() + parser.add_argument('--html', action='store_true') + args = parser.parse_args() + # Generate and return the command + if args.html: report_path = os.path.join(postman_tests_directory, 'newman_reports') create_reports_directory_if_not_exists(report_path) - environment_file_path = f'{config_path}/dev.json' return ( f'newman run {collection_data_path} -d {newman_data_path}' - f' -e {environment_file_path} -r cli,htmlextra --reporter-htmlextra-logs' + f' -e {config_file_path} -r cli,htmlextra --reporter-htmlextra-logs' f' --reporter-htmlextra-export {report_path}' ) else: - environment_file_path = f'{config_path}/prod.json' return ( f'newman run {collection_data_path} -d {newman_data_path}' - f' -e {environment_file_path}' + f' -e {config_file_path}' ) @@ -59,7 +63,7 @@ def run_tests(): datastore_data_path = os.path.join(postman_tests_directory, 'data', 'data_store') try: newman.check_if_data_valid(entities_data_path) - datastore.sync(datastore_data_path, config_path, 'create') + datastore.sync(datastore_data_path, config_file_path, 'create') newman_data = newman.generate_newman_data(entities_data_path) with open(newman_data_path, 'w') as fp: json.dump(newman_data, fp) @@ -71,7 +75,7 @@ def run_tests(): except Exception as e: raise e finally: - datastore.sync(datastore_data_path, config_path, 'delete') + datastore.sync(datastore_data_path, config_file_path, 'delete') if __name__ == "__main__": From 70a7f662da713c95a202e24eb040f6a1e259a774 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 14:51:04 +0530 Subject: [PATCH 71/78] Fix handling excpetions --- postman_tests/lib/datastore.py | 4 ++-- postman_tests/run_tests.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 5a8ba0002..e68ba910a 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -31,8 +31,8 @@ def sync(datastore_data_path, config_file_path, mode): try: req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) req.raise_for_status() - except requests.exceptions.RequestException as e: - raise e + except requests.exceptions.HTTPError as _: + print(req.text) def convert_csv_to_dict(file_path, mode): diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 86d0ade97..20adaf716 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -70,11 +70,15 @@ def run_tests(): newman_command = get_newman_command() process = Popen(newman_command, shell=True) process.communicate() - os.remove(newman_data_path) return process.returncode except Exception as e: raise e finally: + try: + os.remove(newman_data_path) + except OSError as e: + if e.errno != 2: # raise all except "No suck file or directory" error + raise datastore.sync(datastore_data_path, config_file_path, 'delete') From 6678604e4d892bee931e82c81e6baa8b9db3d65f Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 14:57:02 +0530 Subject: [PATCH 72/78] Rename data_store directory to datastore --- .../data/{data_store => datastore}/ner_ptest_city_list.csv | 0 .../data/{data_store => datastore}/ner_ptest_restaurant.csv | 0 postman_tests/lib/datastore.py | 2 +- postman_tests/run_tests.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename postman_tests/data/{data_store => datastore}/ner_ptest_city_list.csv (100%) rename postman_tests/data/{data_store => datastore}/ner_ptest_restaurant.csv (100%) diff --git a/postman_tests/data/data_store/ner_ptest_city_list.csv b/postman_tests/data/datastore/ner_ptest_city_list.csv similarity index 100% rename from postman_tests/data/data_store/ner_ptest_city_list.csv rename to postman_tests/data/datastore/ner_ptest_city_list.csv diff --git a/postman_tests/data/data_store/ner_ptest_restaurant.csv b/postman_tests/data/datastore/ner_ptest_restaurant.csv similarity index 100% rename from postman_tests/data/data_store/ner_ptest_restaurant.csv rename to postman_tests/data/datastore/ner_ptest_restaurant.csv diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index e68ba910a..9928e30e4 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -39,7 +39,7 @@ def convert_csv_to_dict(file_path, mode): """Read the csv file at file_path and convert its data to json. Args: - file_path (str): Path of a file in the data/data_store directory. + file_path (str): Path of a file in the data/datastore directory. Returns: str: The JSON representation of the csv data in the file. diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 20adaf716..6aeb52b7f 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -60,7 +60,7 @@ def run_tests(): int: The return code of the newman command. """ entities_data_path = os.path.join(postman_tests_directory, 'data', 'entities') - datastore_data_path = os.path.join(postman_tests_directory, 'data', 'data_store') + datastore_data_path = os.path.join(postman_tests_directory, 'data', 'datastore') try: newman.check_if_data_valid(entities_data_path) datastore.sync(datastore_data_path, config_file_path, 'create') From a6bdaa7d5ea5017835e0eb31914f643619ee451c Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 15:09:32 +0530 Subject: [PATCH 73/78] Handling --html argument in run_postman_tests shell script --- run_postman_tests.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/run_postman_tests.sh b/run_postman_tests.sh index 145b7597d..e23f146cd 100755 --- a/run_postman_tests.sh +++ b/run_postman_tests.sh @@ -1,3 +1,7 @@ #!/bin/bash -docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py +if [ "$1" == "--html" ]; then + docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py --html +else + docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py +fi From 4c8bc8f05d0c2b2c54aa2b5761be8a52ae205877 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 15:13:56 +0530 Subject: [PATCH 74/78] Update Readme --- postman_tests/Readme.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index 8cefbb208..e35dab4f2 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -1,17 +1,22 @@ **Running the tests** -***Note:*** Before running the below command, if you are running this for the first time in dev environment, make sure you copy ```config/prod.json``` to ```config/dev.json``` and adjust the host urls in it, -for chatbot-ner and ElasticSearch. - ``` docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py ``` -A shortcut for running the above is available. Just run ```./run_postman_tests.sh``` in the root directory. +Or + +``` +docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py --html +``` + +The ```--html``` argument will generate a html report in ```postman_tests/newman_reports``` + +A shortcut for running the above is available. Just run ```./run_postman_tests.sh``` or ```./run_postman_tests.sh --html``` in the root directory. -**Viewing the test results in dev** +**HTML Report** -Running the above command in dev will create a ```newman_reports/``` directory in the ```postman_tests/``` folder. The newman command will generate a new timestamped html file in that directory everytime the tests are run. This html file contains a graphical dashboard which can be used to see the status of running the tests, failures etc, and yes you can use dark mode as well ;-). +The html report is a timestamped html file and a new one is generated everytime tests are run. This html file contains a graphical dashboard which can be used to see the status of running the tests, failures etc, and yes you can use dark mode as well ;-). ![newman dashboard](newman.png) From 8686ac1aa6d0f3e65a1dc106eee8a802be7398cc Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 15:16:29 +0530 Subject: [PATCH 75/78] Fix lint errors --- postman_tests/lib/datastore.py | 2 +- postman_tests/run_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/postman_tests/lib/datastore.py b/postman_tests/lib/datastore.py index 9928e30e4..3a06c8d1d 100644 --- a/postman_tests/lib/datastore.py +++ b/postman_tests/lib/datastore.py @@ -31,7 +31,7 @@ def sync(datastore_data_path, config_file_path, mode): try: req = requests.post(f"{url}/{entity_name}", data=json.dumps(contents)) req.raise_for_status() - except requests.exceptions.HTTPError as _: + except requests.exceptions.HTTPError as e: print(req.text) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 6aeb52b7f..57a16a016 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -77,7 +77,7 @@ def run_tests(): try: os.remove(newman_data_path) except OSError as e: - if e.errno != 2: # raise all except "No suck file or directory" error + if e.errno != 2: # raise all except "No suck file or directory" error raise datastore.sync(datastore_data_path, config_file_path, 'delete') From 7fda5a2a7b521b0e9385d7d133a950300d943534 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 16:21:25 +0530 Subject: [PATCH 76/78] Using sys.argv instead of argparse argparse is giving error in jenkins build --- postman_tests/run_tests.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/postman_tests/run_tests.py b/postman_tests/run_tests.py index 57a16a016..b64635da8 100644 --- a/postman_tests/run_tests.py +++ b/postman_tests/run_tests.py @@ -3,7 +3,6 @@ import os import json import sys -import argparse from lib import newman from lib import datastore @@ -33,12 +32,7 @@ def get_newman_command(): str: The shell command to be used for running the tests. """ collection_data_path = os.path.join(postman_tests_directory, 'data', 'ner_collection.json') - # Check if --html is passed as argument - parser = argparse.ArgumentParser() - parser.add_argument('--html', action='store_true') - args = parser.parse_args() - # Generate and return the command - if args.html: + if len(sys.argv) > 1 and sys.argv[1] == '--html': report_path = os.path.join(postman_tests_directory, 'newman_reports') create_reports_directory_if_not_exists(report_path) return ( From 80b5a4d17994fbe851af133da4bc16643e9a93ac Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Thu, 23 Apr 2020 17:48:57 +0530 Subject: [PATCH 77/78] Fix typo in Readme --- postman_tests/Readme.md | 44 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index e35dab4f2..309a6f061 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -28,16 +28,18 @@ The format should follow the below structure: ``` [ - "input": { - "message": "The text sent in url", - "entity_name": "Name of the entity e.g. time" - }, - - "expected": [ - { - - } - ] + { + "input": { + "message": "The text sent in url", + "entity_name": "Name of the entity e.g. time" + }, + + "expected": [ + { + + } + ] + } ] ``` @@ -50,16 +52,18 @@ If you want to create a test case where the output of a request is expected to b ``` [ - "input": { - "message": "The text sent in url", - "entity_name": "Name of the entity. If this entity uses ES indexing then the name should be prefixed by ner_ptest e.g. ner_ptest_restuarant" - }, - - "expected": [ - { - "data": null - } - ] + { + "input": { + "message": "The text sent in url", + "entity_name": "Name of the entity. If this entity uses ES indexing then the name should be prefixed by ner_ptest e.g. ner_ptest_restuarant" + }, + + "expected": [ + { + "data": null + } + ] + } ] ``` From 2504671b3a8a1f257a7ee5d4de2b5326a1263ad1 Mon Sep 17 00:00:00 2001 From: Raza Sayed Date: Fri, 24 Apr 2020 12:12:55 +0530 Subject: [PATCH 78/78] Renamed run_tests.py to run_postman_tests.py and ignoring for nose test runner --- chatbot_ner/settings.py | 1 + postman_tests/Readme.md | 4 ++-- postman_tests/{run_tests.py => run_postman_tests.py} | 0 run_postman_tests.sh | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) rename postman_tests/{run_tests.py => run_postman_tests.py} (100%) diff --git a/chatbot_ner/settings.py b/chatbot_ner/settings.py index 4cf50fe37..982411435 100755 --- a/chatbot_ner/settings.py +++ b/chatbot_ner/settings.py @@ -109,6 +109,7 @@ def __getitem__(self, item): '--ignore-files=constants.py', '--ignore-files=start_server.sh', '--ignore-files=settings.py', + '--ignore-files=run_postman_tests.py', '--exclude-dir=docs/', '--exclude-dir=docker/', '--exclude-dir=data/', diff --git a/postman_tests/Readme.md b/postman_tests/Readme.md index 309a6f061..2da8b1e62 100644 --- a/postman_tests/Readme.md +++ b/postman_tests/Readme.md @@ -1,13 +1,13 @@ **Running the tests** ``` -docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py +docker exec -it docker_chatbot-ner_1 python postman_tests/run_postman_tests.py ``` Or ``` -docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py --html +docker exec -it docker_chatbot-ner_1 python postman_tests/run_postman_tests.py --html ``` The ```--html``` argument will generate a html report in ```postman_tests/newman_reports``` diff --git a/postman_tests/run_tests.py b/postman_tests/run_postman_tests.py similarity index 100% rename from postman_tests/run_tests.py rename to postman_tests/run_postman_tests.py diff --git a/run_postman_tests.sh b/run_postman_tests.sh index e23f146cd..44b0d3596 100755 --- a/run_postman_tests.sh +++ b/run_postman_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ "$1" == "--html" ]; then - docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py --html + docker exec -it docker_chatbot-ner_1 python postman_tests/run_postman_tests.py --html else - docker exec -it docker_chatbot-ner_1 python postman_tests/run_tests.py + docker exec -it docker_chatbot-ner_1 python postman_tests/run_postman_tests.py fi