diff --git a/tests/smoke/test_ucc_build.py b/tests/smoke/test_ucc_build.py index 14f2b3c203..4d821c8974 100644 --- a/tests/smoke/test_ucc_build.py +++ b/tests/smoke/test_ucc_build.py @@ -153,6 +153,8 @@ def test_ucc_generate_with_everything(caplog): ("default", "splunk_ta_uccexample_settings.conf"), ("default", "web.conf"), ("default", "server.conf"), + ("default", "commands.conf"), + ("default", "searchbnf.conf"), ("default", "data", "ui", "alerts", "test_alert.html"), ("default", "data", "ui", "nav", "default.xml"), ("default", "data", "ui", "views", "configuration.xml"), @@ -166,6 +168,16 @@ def test_ucc_generate_with_everything(caplog): ("bin", "example_input_three.py"), ("bin", "example_input_four.py"), ("bin", "import_declare_test.py"), + ("bin", "countmatchescommand.py"), + ("bin", "countmatches.py"), + ("bin", "filter.py"), + ("bin", "filtercommand.py"), + ("bin", "generatetext.py"), + ("bin", "generatetextcommand.py"), + ("bin", "sumcommand.py"), + ("bin", "sum.py"), + ("bin", "sum_without_map.py"), + ("bin", "sumtwocommand.py"), ("bin", "splunk_ta_uccexample_rh_account.py"), ("bin", "splunk_ta_uccexample_rh_example_input_one.py"), ("bin", "splunk_ta_uccexample_rh_example_input_two.py"), diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/.appinspect.manualcheck.yaml b/tests/testdata/expected_addons/expected_output_global_config_everything/.appinspect.manualcheck.yaml index 198439926d..6b25892c71 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_everything/.appinspect.manualcheck.yaml +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/.appinspect.manualcheck.yaml @@ -34,3 +34,5 @@ check_requires_access_to_files_outside_apps_dir: comment: 'Y' check_for_secret_disclosure: comment: 'Y' +check_custom_commands: + comment: "The custom command is only for testing purpose, the add-on won't be released." diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/appserver/static/js/build/globalConfig.json b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/appserver/static/js/build/globalConfig.json index 9971c405e0..6c9a3e585a 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/appserver/static/js/build/globalConfig.json +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/appserver/static/js/build/globalConfig.json @@ -1034,5 +1034,106 @@ "version": "5.5.8+5fd76615", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.2" - } + }, + "customSearchCommand": [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": true, + "description": " This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": true, + "validate": { + "type": "Integer", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "text", + "required": true + } + ] + }, + { + "commandName": "filtercommand", + "fileName": "filter.py", + "commandType": "dataset processing", + "requiredSearchAssistant": true, + "description": "It filters records from the events stream returning only those which has :code:`contains` in them and replaces :code:`replace_array[0]` with :code:`replace_array[1]`.", + "syntax": "| filtercommand contains='value1' replace='value to be replaced,value to replace with'", + "usage": "public", + "arguments": [ + { + "name": "contains" + }, + { + "name": "replace_array" + } + ] + }, + { + "commandName": "sumcommand", + "fileName": "sum.py", + "commandType": "transforming", + "requiredSearchAssistant": true, + "description": "The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.", + "syntax": "| sumcommand total=lines linecount", + "usage": "public", + "arguments": [ + { + "name": "total", + "validate": { + "type": "Fieldname" + }, + "required": true + } + ] + }, + { + "commandName": "sumtwocommand", + "fileName": "sum_without_map.py", + "commandType": "transforming", + "requiredSearchAssistant": true, + "description": "The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.", + "syntax": "| sumtwocommand total=lines linecount", + "usage": "public", + "arguments": [ + { + "name": "total", + "validate": { + "type": "Fieldname" + }, + "required": true + } + ] + }, + { + "commandName": "countmatchescommand", + "fileName": "countmatches.py", + "commandType": "streaming", + "requiredSearchAssistant": false, + "arguments": [ + { + "name": "fieldname", + "validate": { + "type": "Fieldname" + }, + "required": true + }, + { + "name": "pattern", + "validate": { + "type": "RegularExpression" + }, + "required": true + } + ] + } + ] } \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatches.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatches.py new file mode 100644 index 0000000000..da79b16420 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatches.py @@ -0,0 +1,4 @@ +def stream(self, records): + for record in records: + # write custom logic for the search command + yield record \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatchescommand.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatchescommand.py new file mode 100644 index 0000000000..e2faa5da18 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/countmatchescommand.py @@ -0,0 +1,17 @@ +import sys +import import_declare_test + +from splunklib.searchcommands import \ + dispatch, StreamingCommand, Configuration, Option, validators +from countmatches import stream + +@Configuration() +class CountmatchescommandCommand(StreamingCommand): + + fieldname = Option(name='fieldname', require=True, validate=validators.Fieldname()) + pattern = Option(name='pattern', require=True, validate=validators.RegularExpression()) + + def stream(self, events): + return stream(self, events) + +dispatch(CountmatchescommandCommand, sys.argv, sys.stdin, sys.stdout, __name__) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filter.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filter.py new file mode 100644 index 0000000000..51d476833f --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filter.py @@ -0,0 +1,36 @@ +def transform(self, records): + contains = self.contains + replace_array = self.replace_array + + if contains and replace_array: + arr = replace_array.split(",") + if len(arr) != 2: + raise ValueError("Please provide only two arguments, separated by comma for 'replace'") + + for record in records: + _raw = record.get("_raw") + if contains in _raw: + record["_raw"] = _raw.replace(arr[0], arr[1]) + yield record + return + + if contains: + for record in records: + _raw = record.get("_raw") + if contains in _raw: + yield record + return + + if replace_array: + arr = replace_array.split(",") + if len(arr) != 2: + raise ValueError("Please provide only two arguments, separated by comma for 'replace'") + + for record in records: + _raw = record.get("_raw") + record["_raw"] = _raw.replace(arr[0], arr[1]) + yield record + return + + for record in records: + yield record \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filtercommand.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filtercommand.py new file mode 100644 index 0000000000..8ab7cb354d --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/filtercommand.py @@ -0,0 +1,26 @@ +import sys +import import_declare_test + +from splunklib.searchcommands import \ + dispatch, EventingCommand, Configuration, Option, validators +from filter import transform + +@Configuration() +class FiltercommandCommand(EventingCommand): + """ + + ##Syntax + | filtercommand contains='value1' replace='value to be replaced,value to replace with' + + ##Description + It filters records from the events stream returning only those which has :code:`contains` in them and replaces :code:`replace_array[0]` with :code:`replace_array[1]`. + + """ + + contains = Option(name='contains', require=False) + replace_array = Option(name='replace_array', require=False) + + def transform(self, events): + return transform(self, events) + +dispatch(FiltercommandCommand, sys.argv, sys.stdin, sys.stdout, __name__) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetext.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetext.py new file mode 100644 index 0000000000..897779284c --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetext.py @@ -0,0 +1,8 @@ +import time +import logging + + +def generate(self): + logging.debug("Generating %d events with text %s" % (self.count, self.text)) + for i in range(1, self.count + 1): + yield {'_serial': i, '_time': time.time(), '_raw': str(i) + '. ' + self.text} diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetextcommand.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetextcommand.py new file mode 100644 index 0000000000..89459faa90 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/generatetextcommand.py @@ -0,0 +1,25 @@ +import sys +import import_declare_test + +from splunklib.searchcommands import \ + dispatch, GeneratingCommand, Configuration, Option, validators +from generatetext import generate + +@Configuration() +class GeneratetextcommandCommand(GeneratingCommand): + """ + + ##Syntax + generatetextcommand count= text= + + ##Description + This command generates COUNT occurrences of a TEXT string. + + """ + count = Option(name='count', require=True, validate=validators.Integer(minimum=5, maximum=10)) + text = Option(name='text', require=True) + + def generate(self): + return generate(self) + +dispatch(GeneratetextcommandCommand, sys.argv, sys.stdin, sys.stdout, __name__) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum.py new file mode 100644 index 0000000000..1f4a4b88fc --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum.py @@ -0,0 +1,22 @@ +import logging + +def map(self, records): + """ Computes sum(fieldname, 1, n) and stores the result in 'total' """ + fieldnames = self.fieldnames + total = 0.0 + for record in records: + for fieldname in fieldnames: + total += float(record[fieldname]) + yield {self.total: total} + +def reduce(self, records): + """ Computes sum(total, 1, N) and stores the result in 'total' """ + fieldname = self.total + total = 0.0 + for record in records: + value = record[fieldname] + try: + total += float(value) + except ValueError: + logging.debug(' could not convert %s value to float: %s', fieldname, repr(value)) + yield {self.total: total} \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum_without_map.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum_without_map.py new file mode 100644 index 0000000000..1bf17a8ae3 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sum_without_map.py @@ -0,0 +1,13 @@ +import logging + +def reduce(self, records): + """ Computes sum(total, 1, N) and stores the result in 'total' """ + fieldname = self.total + total = 0.0 + for record in records: + value = record[fieldname] + try: + total += float(value) + except ValueError: + logging.debug(' could not convert %s value to float: %s', fieldname, repr(value)) + yield {self.total: total} \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumcommand.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumcommand.py new file mode 100644 index 0000000000..6e71fa7832 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumcommand.py @@ -0,0 +1,36 @@ +import sys +import import_declare_test + +from splunklib.searchcommands import \ + dispatch, ReportingCommand, Configuration, Option, validators + +from sum import reduce + +try: + from sum import map as module_map +except ImportError: + module_map = None + +@Configuration() +class SumcommandCommand(ReportingCommand): + """ + + ##Syntax + | sumcommand total=lines linecount + + ##Description + The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records. + + """ + + total = Option(name='total', require=True, validate=validators.Fieldname()) + + if module_map is not None: + @Configuration() + def map(self, events): + return module_map(self, events) + + def reduce(self, events): + return reduce(self, events) + +dispatch(SumcommandCommand, sys.argv, sys.stdin, sys.stdout, __name__) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumtwocommand.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumtwocommand.py new file mode 100644 index 0000000000..2486a1659f --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/sumtwocommand.py @@ -0,0 +1,36 @@ +import sys +import import_declare_test + +from splunklib.searchcommands import \ + dispatch, ReportingCommand, Configuration, Option, validators + +from sum_without_map import reduce + +try: + from sum_without_map import map as module_map +except ImportError: + module_map = None + +@Configuration() +class SumtwocommandCommand(ReportingCommand): + """ + + ##Syntax + | sumtwocommand total=lines linecount + + ##Description + The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records. + + """ + + total = Option(name='total', require=True, validate=validators.Fieldname()) + + if module_map is not None: + @Configuration() + def map(self, events): + return module_map(self, events) + + def reduce(self, events): + return reduce(self, events) + +dispatch(SumtwocommandCommand, sys.argv, sys.stdin, sys.stdout, __name__) \ No newline at end of file diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/commands.conf b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/commands.conf new file mode 100644 index 0000000000..d2ee56fd17 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/commands.conf @@ -0,0 +1,24 @@ +[generatetextcommand] +filename = generatetextcommand.py +chunked = true +python.version = python3 + +[filtercommand] +filename = filtercommand.py +chunked = true +python.version = python3 + +[sumcommand] +filename = sumcommand.py +chunked = true +python.version = python3 + +[sumtwocommand] +filename = sumtwocommand.py +chunked = true +python.version = python3 + +[countmatchescommand] +filename = countmatchescommand.py +chunked = true +python.version = python3 diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/searchbnf.conf b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/searchbnf.conf new file mode 100644 index 0000000000..05ee83ea11 --- /dev/null +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/default/searchbnf.conf @@ -0,0 +1,19 @@ +[generatetextcommand] +syntax = generatetextcommand count= text= +description = This command generates COUNT occurrences of a TEXT string. +usage = public + +[filtercommand] +syntax = | filtercommand contains='value1' replace='value to be replaced,value to replace with' +description = It filters records from the events stream returning only those which has :code:`contains` in them and replaces :code:`replace_array[0]` with :code:`replace_array[1]`. +usage = public + +[sumcommand] +syntax = | sumcommand total=lines linecount +description = The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records. +usage = public + +[sumtwocommand] +syntax = | sumtwocommand total=lines linecount +description = The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records. +usage = public diff --git a/tests/testdata/test_addons/package_global_config_everything/additional_packaging.py b/tests/testdata/test_addons/package_global_config_everything/additional_packaging.py index 208d9e5ef0..523c766292 100644 --- a/tests/testdata/test_addons/package_global_config_everything/additional_packaging.py +++ b/tests/testdata/test_addons/package_global_config_everything/additional_packaging.py @@ -1,5 +1,6 @@ from os.path import sep, exists, dirname, realpath, join from os import remove, system, _exit, WEXITSTATUS +from shutil import rmtree def additional_packaging(ta_name=None): """ @@ -21,6 +22,8 @@ def cleanup_output_files(output_path: str, ta_name: str) -> None: or present in app.manifest file of add-on. """ files_to_delete = [] + directories_to_delete = [] + files_to_delete.append(sep.join([output_path, ta_name, "default", "redundant.conf"])) files_to_delete.append(sep.join([output_path, ta_name, "bin", "template_modinput_layout.py"])) files_to_delete.append(sep.join([output_path, ta_name, "bin", "example_one_input_one.py"])) @@ -28,6 +31,15 @@ def cleanup_output_files(output_path: str, ta_name: str) -> None: files_to_delete.append(sep.join([output_path, ta_name, "bin", "file_does_not_exist.py"])) files_to_delete.append(sep.join([output_path, ta_name, "default", "nav", "views", "file_copied_from_source_code.xml"])) + directories_to_delete.append(sep.join([output_path, ta_name, "bin", "__pycache__"])) + + for directories in directories_to_delete: + try: + rmtree(directories) + except (NotADirectoryError): + # simply pass if the directory doesn't exist + pass + for delete_file in files_to_delete: try: remove(delete_file) diff --git a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json index 8e8c5c378b..1ea356a0ca 100644 --- a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json +++ b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json @@ -2090,7 +2090,7 @@ "meta": { "name": "Splunk_TA_UCCExample", "restRoot": "splunk_ta_uccexample", - "version": "5.58.1+10197a346", + "version": "5.60.0+d9c822917", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.9", "supportedThemes": [ @@ -2098,5 +2098,106 @@ "dark" ], "isVisible": true - } + }, + "customSearchCommand": [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": true, + "description": " This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": true, + "validate": { + "type": "Integer", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "text", + "required": true + } + ] + }, + { + "commandName": "filtercommand", + "fileName": "filter.py", + "commandType": "dataset processing", + "requiredSearchAssistant": true, + "description": "It filters records from the events stream returning only those which has :code:`contains` in them and replaces :code:`replace_array[0]` with :code:`replace_array[1]`.", + "syntax": "| filtercommand contains='value1' replace='value to be replaced,value to replace with'", + "usage": "public", + "arguments": [ + { + "name": "contains" + }, + { + "name": "replace_array" + } + ] + }, + { + "commandName": "sumcommand", + "fileName": "sum.py", + "commandType": "transforming", + "requiredSearchAssistant": true, + "description": "The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.", + "syntax": "| sumcommand total=lines linecount", + "usage": "public", + "arguments": [ + { + "name": "total", + "validate": { + "type": "Fieldname" + }, + "required": true + } + ] + }, + { + "commandName": "sumtwocommand", + "fileName": "sum_without_map.py", + "commandType": "transforming", + "requiredSearchAssistant": true, + "description": "The total produced is sum(sum(fieldname, 1, n), 1, N) where n = number of fields, N = number of records.", + "syntax": "| sumtwocommand total=lines linecount", + "usage": "public", + "arguments": [ + { + "name": "total", + "validate": { + "type": "Fieldname" + }, + "required": true + } + ] + }, + { + "commandName": "countmatchescommand", + "fileName": "countmatches.py", + "commandType": "streaming", + "requiredSearchAssistant": false, + "arguments": [ + { + "name": "fieldname", + "validate": { + "type": "Fieldname" + }, + "required": true + }, + { + "name": "pattern", + "validate": { + "type": "RegularExpression" + }, + "required": true + } + ] + } + ] } diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/countmatches.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/countmatches.py new file mode 100644 index 0000000000..da79b16420 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/countmatches.py @@ -0,0 +1,4 @@ +def stream(self, records): + for record in records: + # write custom logic for the search command + yield record \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/filter.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/filter.py new file mode 100644 index 0000000000..51d476833f --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/filter.py @@ -0,0 +1,36 @@ +def transform(self, records): + contains = self.contains + replace_array = self.replace_array + + if contains and replace_array: + arr = replace_array.split(",") + if len(arr) != 2: + raise ValueError("Please provide only two arguments, separated by comma for 'replace'") + + for record in records: + _raw = record.get("_raw") + if contains in _raw: + record["_raw"] = _raw.replace(arr[0], arr[1]) + yield record + return + + if contains: + for record in records: + _raw = record.get("_raw") + if contains in _raw: + yield record + return + + if replace_array: + arr = replace_array.split(",") + if len(arr) != 2: + raise ValueError("Please provide only two arguments, separated by comma for 'replace'") + + for record in records: + _raw = record.get("_raw") + record["_raw"] = _raw.replace(arr[0], arr[1]) + yield record + return + + for record in records: + yield record \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/generatetext.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/generatetext.py new file mode 100644 index 0000000000..897779284c --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/generatetext.py @@ -0,0 +1,8 @@ +import time +import logging + + +def generate(self): + logging.debug("Generating %d events with text %s" % (self.count, self.text)) + for i in range(1, self.count + 1): + yield {'_serial': i, '_time': time.time(), '_raw': str(i) + '. ' + self.text} diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/sum.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/sum.py new file mode 100644 index 0000000000..1f4a4b88fc --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/sum.py @@ -0,0 +1,22 @@ +import logging + +def map(self, records): + """ Computes sum(fieldname, 1, n) and stores the result in 'total' """ + fieldnames = self.fieldnames + total = 0.0 + for record in records: + for fieldname in fieldnames: + total += float(record[fieldname]) + yield {self.total: total} + +def reduce(self, records): + """ Computes sum(total, 1, N) and stores the result in 'total' """ + fieldname = self.total + total = 0.0 + for record in records: + value = record[fieldname] + try: + total += float(value) + except ValueError: + logging.debug(' could not convert %s value to float: %s', fieldname, repr(value)) + yield {self.total: total} \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/sum_without_map.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/sum_without_map.py new file mode 100644 index 0000000000..1bf17a8ae3 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/sum_without_map.py @@ -0,0 +1,13 @@ +import logging + +def reduce(self, records): + """ Computes sum(total, 1, N) and stores the result in 'total' """ + fieldname = self.total + total = 0.0 + for record in records: + value = record[fieldname] + try: + total += float(value) + except ValueError: + logging.debug(' could not convert %s value to float: %s', fieldname, repr(value)) + yield {self.total: total} \ No newline at end of file diff --git a/tests/unit/generators/conf_files/test_conf_files_init.py b/tests/unit/generators/conf_files/test_conf_files_init.py index 018b5aec04..817b4f4614 100644 --- a/tests/unit/generators/conf_files/test_conf_files_init.py +++ b/tests/unit/generators/conf_files/test_conf_files_init.py @@ -11,6 +11,8 @@ def test___init__conf(): "InputsConf", "AccountConf", "SettingsConf", + "SearchbnfConf", + "CommandsConf", ] expected_modules = [ "conf_generator", @@ -24,6 +26,8 @@ def test___init__conf(): "create_web_conf", "create_account_conf", "create_settings_conf", + "create_searchbnf_conf", + "create_commands_conf", ] import splunk_add_on_ucc_framework.generators.conf_files as conf diff --git a/tests/unit/generators/conf_files/test_create_commands_conf.py b/tests/unit/generators/conf_files/test_create_commands_conf.py new file mode 100644 index 0000000000..7853e54d9a --- /dev/null +++ b/tests/unit/generators/conf_files/test_create_commands_conf.py @@ -0,0 +1,70 @@ +from unittest.mock import patch, MagicMock +from splunk_add_on_ucc_framework.generators.conf_files import CommandsConf # type: ignore[attr-defined] + + +def test_set_attributes_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + commands_conf = CommandsConf( + global_config_only_configuration, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert not hasattr(commands_conf, "command_names") + + +def test_set_attributes( + global_config_all_json, input_dir, output_dir, ucc_dir, ta_name +): + commands_conf = CommandsConf( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert commands_conf.conf_file == "commands.conf" + assert commands_conf.command_names == ["generatetextcommand"] + + +def test_generate_conf_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + commands_conf = CommandsConf( + global_config_only_configuration, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + file_paths = commands_conf.generate_conf() + + # Assert that no files are returned since no custom command is configured + assert file_paths is None + + +@patch( + "splunk_add_on_ucc_framework.generators.conf_files.CommandsConf.set_template_and_render" +) +def test_generate_conf( + mock_template, global_config_all_json, input_dir, output_dir, ucc_dir, ta_name +): + content = "content" + exp_fname = "commands.conf" + template_render = MagicMock() + template_render.render.return_value = content + + commands_conf = CommandsConf( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + commands_conf._template = template_render + file_paths = commands_conf.generate_conf() + + assert mock_template.call_count == 1 + assert file_paths == {exp_fname: f"{output_dir}/{ta_name}/default/{exp_fname}"} diff --git a/tests/unit/generators/conf_files/test_create_searchbnf_conf.py b/tests/unit/generators/conf_files/test_create_searchbnf_conf.py new file mode 100644 index 0000000000..6fb7fac775 --- /dev/null +++ b/tests/unit/generators/conf_files/test_create_searchbnf_conf.py @@ -0,0 +1,110 @@ +from pytest import fixture +from unittest.mock import patch, MagicMock +from splunk_add_on_ucc_framework.generators.conf_files import SearchbnfConf # type: ignore[attr-defined] + + +@fixture +def custom_search_command_without_search_assistance(): + return [ + { + "commandName": "testcommand2", + "commandType": "streaming", + "fileName": "test2.py", + } + ] + + +def test_set_attributes_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + searchbnf_conf = SearchbnfConf( + global_config_only_configuration, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert searchbnf_conf.searchbnf_info == [] + + +def test_set_attributes( + global_config_all_json, input_dir, output_dir, ucc_dir, ta_name +): + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert searchbnf_conf.conf_file == "searchbnf.conf" + assert searchbnf_conf.searchbnf_info == [ + { + "command_name": "generatetextcommand", + "description": "This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + } + ] + + +def test_set_attributes_without_search_assistance( + global_config_all_json, + input_dir, + output_dir, + ucc_dir, + ta_name, + custom_search_command_without_search_assistance, +): + global_config_all_json._content[ + "customSearchCommand" + ] = custom_search_command_without_search_assistance + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert searchbnf_conf.searchbnf_info == [] + + +def test_generate_conf_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + searchbnf_conf = SearchbnfConf( + global_config_only_configuration, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + file_paths = searchbnf_conf.generate_conf() + + # Assert that no files are returned since no custom command is configured + assert file_paths is None + + +@patch( + "splunk_add_on_ucc_framework.generators.conf_files.SearchbnfConf.set_template_and_render" +) +def test_generate_conf( + mock_template, global_config_all_json, input_dir, output_dir, ucc_dir, ta_name +): + content = "content" + exp_fname = "searchbnf.conf" + template_render = MagicMock() + template_render.render.return_value = content + + searchbnf_conf = SearchbnfConf( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + searchbnf_conf._template = template_render + file_paths = searchbnf_conf.generate_conf() + + assert mock_template.call_count == 1 + assert file_paths == {exp_fname: f"{output_dir}/{ta_name}/default/{exp_fname}"} diff --git a/tests/unit/generators/python_files/test_create_custom_command_python.py b/tests/unit/generators/python_files/test_create_custom_command_python.py new file mode 100644 index 0000000000..50618e4c88 --- /dev/null +++ b/tests/unit/generators/python_files/test_create_custom_command_python.py @@ -0,0 +1,218 @@ +from pytest import fixture +from unittest.mock import patch, MagicMock +from splunk_add_on_ucc_framework.generators.python_files import CustomCommandPy # type: ignore[attr-defined] +import sys + + +@fixture +def custom_search_commands(): + return [ + { + "commandName": "testcommand", + "commandType": "generating", + "fileName": "test.py", + "requiredSearchAssistant": True, + "description": "This is test command", + "syntax": "testcommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": True, + "validate": {"type": "Integer", "minimum": 5, "maximum": 10}, + }, + { + "name": "age", + "validate": {"type": "Integer", "minimum": 18}, + }, + {"name": "text", "required": True, "defaultValue": "test_text"}, + ], + } + ] + + +@fixture +def transforming_custom_search_command(): + return [ + { + "commandName": "transformingcommand", + "commandType": "transforming", + "fileName": "transforming_test.py", + "description": "This is a transforming command", + "syntax": "transformingcommand action=", + "arguments": [ + { + "name": "action", + "required": True, + "validate": {"type": "Fieldname"}, + }, + { + "name": "test", + }, + ], + } + ] + + +@patch.dict(sys.modules, {"transforming_test": MagicMock(map=True)}) +def test_set_attributes_for_transforming_command( + global_config_all_json, + input_dir, + output_dir, + ucc_dir, + ta_name, + transforming_custom_search_command, +): + global_config_all_json._content[ + "customSearchCommand" + ] = transforming_custom_search_command + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + + assert custom_command_py.commands_info == [ + { + "imported_file_name": "transforming_test", + "file_name": "transformingcommand", + "class_name": "Transformingcommand", + "description": "This is a transforming command", + "syntax": "transformingcommand action=", + "template": "transforming.template", + "list_arg": [ + "action = Option(name='action', require=True, validate=validators.Fieldname())", + "test = Option(name='test', require=False)", + ], + "import_map": True, + } + ] + + +@patch.dict(sys.modules, {"transforming_test": MagicMock(spec=[])}) +def test_set_attributes_for_transforming_command_without_map( + global_config_all_json, + input_dir, + output_dir, + ucc_dir, + ta_name, + transforming_custom_search_command, +): + global_config_all_json._content[ + "customSearchCommand" + ] = transforming_custom_search_command + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + + assert custom_command_py.commands_info == [ + { + "imported_file_name": "transforming_test", + "file_name": "transformingcommand", + "class_name": "Transformingcommand", + "description": "This is a transforming command", + "syntax": "transformingcommand action=", + "template": "transforming.template", + "list_arg": [ + "action = Option(name='action', require=True, validate=validators.Fieldname())", + "test = Option(name='test', require=False)", + ], + "import_map": False, + } + ] + + +def test_set_attributes_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + custom_command = CustomCommandPy( + global_config_only_configuration, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + assert custom_command.commands_info == [] + + +def test_set_attributes( + global_config_all_json, + input_dir, + output_dir, + ucc_dir, + ta_name, + custom_search_commands, +): + global_config_all_json._content["customSearchCommand"] = custom_search_commands + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + custom_search_commands=custom_search_commands, + ) + assert custom_command_py.commands_info == [ + { + "imported_file_name": "test", + "file_name": "testcommand", + "class_name": "Testcommand", + "description": "This is test command", + "syntax": "testcommand count= text=", + "template": "generating.template", + "list_arg": [ + "count = Option(name='count', require=True, " + "validate=validators.Integer(minimum=5, maximum=10))", + "age = Option(name='age', require=False, validate=validators.Integer(minimum=18))", + "text = Option(name='text', require=True, default='test_text')", + ], + "import_map": False, + } + ] + + +def test_generate_python_without_custom_command( + global_config_only_configuration, input_dir, output_dir, ucc_dir, ta_name +): + custom_command = CustomCommandPy( + global_config_all_json=global_config_only_configuration, + input_dir=input_dir, + output_dir=output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + file_paths = custom_command.generate() + + # Assert that no files are returned since no custom command is configured + assert file_paths == {"": ""} + + +@patch( + "splunk_add_on_ucc_framework.generators.python_files.CustomCommandPy.set_template_and_render" +) +def test_generate_python( + mock_template, global_config_all_json, input_dir, output_dir, ucc_dir, ta_name +): + content = "content" + exp_fname = "generatetextcommand.py" + template_render = MagicMock() + template_render.render.return_value = content + + custom_command_py = CustomCommandPy( + global_config_all_json, + input_dir, + output_dir, + ucc_dir=ucc_dir, + addon_name=ta_name, + ) + custom_command_py._template = template_render + file_paths = custom_command_py.generate() + + assert mock_template.call_count == 1 + assert file_paths == {exp_fname: f"{output_dir}/{ta_name}/bin/{exp_fname}"} diff --git a/tests/unit/generators/python_files/test_python_files_init.py b/tests/unit/generators/python_files/test_python_files_init.py new file mode 100644 index 0000000000..97eaedf4bc --- /dev/null +++ b/tests/unit/generators/python_files/test_python_files_init.py @@ -0,0 +1,12 @@ +def test___init__Python(): + expected_classes = ["FileGenerator", "CustomCommandPy"] + expected_modules = ["file_generator", "create_custom_command_python"] + import splunk_add_on_ucc_framework.generators.python_files as py + + assert py.__all__ == expected_classes # type: ignore[attr-defined] + + for attrib in dir(py): + if attrib.startswith("__") and attrib.endswith("__"): + # ignore the builtin modules + continue + assert attrib in expected_classes or attrib in expected_modules diff --git a/tests/unit/generators/test_init_.py b/tests/unit/generators/test_init_.py index 01b48a6931..69576a7cdd 100644 --- a/tests/unit/generators/test_init_.py +++ b/tests/unit/generators/test_init_.py @@ -12,7 +12,13 @@ def test___init__gen(): assert gen.__all__ == expected_classes - not_allowed = ["conf_files", "xml_files", "html_files", "doc_generator"] + not_allowed = [ + "conf_files", + "xml_files", + "html_files", + "python_files", + "doc_generator", + ] for attrib in dir(gen): if attrib.startswith("__") and attrib.endswith("__") or attrib in not_allowed: # ignore the builtin modules diff --git a/tests/unit/test_const.py b/tests/unit/test_const.py new file mode 100644 index 0000000000..0acee21032 --- /dev/null +++ b/tests/unit/test_const.py @@ -0,0 +1,33 @@ +import re +import urllib.request + +from splunk_add_on_ucc_framework.const import SPLUNK_COMMANDS + + +def test_command_list_up_to_date(): + with urllib.request.urlopen( + "https://docs.splunk.com/Documentation/Splunk/latest/SearchReference" + ) as resp: + content = resp.read().decode() + + match = re.search(r"Search\s+Commands.+?(.+?)", content, re.S) + if match: + search_commands_ul = match.group(1) + search_commands = re.findall( + r"]*>.*?]*>\s*([^\s<]+)\s+?", search_commands_ul, re.S + ) + else: + search_commands_ul = None + search_commands = [] + + # These are the search commands for the serviceNow add-on. They are not valid for all add-on + not_global_commands = [ + "snowincidentstream", + "snoweventstream", + "snowincident", + "snowevent", + ] + for command in not_global_commands: + search_commands.remove(command) + + assert set(search_commands) == set(SPLUNK_COMMANDS) diff --git a/tests/unit/test_global_config.py b/tests/unit/test_global_config.py index a924177ffb..48bca68bf8 100644 --- a/tests/unit/test_global_config.py +++ b/tests/unit/test_global_config.py @@ -61,6 +61,31 @@ def test_global_config_settings(global_config_only_configuration): assert expected_settings_names == settings_names +def test_global_config_custom_search_commands(global_config_all_json): + custom_search_commands = global_config_all_json.custom_search_commands + expected_result = [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": True, + "description": " This command generates COUNT occurrences of a TEXT string.", + "syntax": "mycommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": True, + "validate": {"type": "Integer", "minimum": 5, "maximum": 10}, + }, + {"name": "text", "required": True}, + ], + } + ] + assert expected_result == custom_search_commands + assert global_config_all_json.has_custom_search_commands() is True + + def test_global_config_configs(global_config_only_configuration): configs = global_config_only_configuration.configs expected_configs_names = ["account"] diff --git a/tests/unit/test_global_config_validator.py b/tests/unit/test_global_config_validator.py index 929c4fae96..49dc2c7de7 100644 --- a/tests/unit/test_global_config_validator.py +++ b/tests/unit/test_global_config_validator.py @@ -3,6 +3,7 @@ from contextlib import nullcontext as does_not_raise from copy import deepcopy from typing import Dict, Any +from unittest.mock import patch import pytest @@ -20,7 +21,6 @@ @pytest.mark.parametrize( "filename", [ - "valid_config.json", "valid_config.yaml", "valid_config_only_logging.json", ], @@ -64,6 +64,10 @@ def test_autocompletefields_children_support_integer_values(): @pytest.mark.parametrize( "filename,exception_message", [ + ( + "invalid_config_for_custom_search_command.json", + "generatetext.py is not present in `bin` directory. Please ensure the file exists.", + ), ( "invalid_config_no_configuration_tabs.json", "[] is too short", @@ -344,6 +348,80 @@ def test_config_validation_when_error(filename, exception_message): assert msg == exception_message +@pytest.mark.parametrize( + "filename,invalid_custom_search_command,exception_message", + [ + ( + "invalid_config_for_custom_search_command.json", + [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": True, + "description": " This command generates COUNT occurrences of a TEXT string.", + "arguments": [ + { + "name": "text", + } + ], + } + ], + "One of the attributes among `description`, `usage`, `syntax` " + "is not been defined in globalConfig. Define them as requiredSearchAssistant is set to True.", + ), + ( + "invalid_config_for_custom_search_command.json", + [ + { + "commandName": "generatetext", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": False, + "arguments": [ + { + "name": "text", + } + ], + } + ], + "Filename: generatetext and CommandName: generatetext should not be same for custom search command.", + ), + ( + "invalid_config_for_custom_search_command.json", + [ + { + "commandName": "abstract", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": False, + "arguments": [ + { + "name": "text", + } + ], + } + ], + "CommandName: abstract cannot have the same name as Splunk built-in command.", + ), + ], +) +@patch("os.path.isfile", return_value=True) +def test_validate_custom_search_command( + mock_isFile, filename, invalid_custom_search_command, exception_message +): + global_config_path = helpers.get_testdata_file_path(filename) + global_config = global_config_lib.GlobalConfig(global_config_path) + + validator = GlobalConfigValidator(helpers.get_path_to_source_dir(), global_config) + global_config._content["customSearchCommand"] = invalid_custom_search_command + with pytest.raises(GlobalConfigValidatorException) as exc_info: + validator.validate() + + (msg,) = exc_info.value.args + assert msg == exception_message + + def test_config_validation_modifications_on_change(): global_config_path = helpers.get_testdata_file_path( "valid_config_with_modification_on_value_change.json" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index f0e97c0087..b3e92dfb21 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -33,6 +33,12 @@ def test_get_j2_env(): "conf_files/settings_conf.template", "conf_files/tags_conf.template", "conf_files/web_conf.template", + "conf_files/commands.conf.template", + "conf_files/searchbnf.conf.template", + "custom_command/dataset processing.template", + "custom_command/generating.template", + "custom_command/transforming.template", + "custom_command/streaming.template", ] assert sorted(expected_list_of_templates) == sorted(list_of_templates) diff --git a/tests/unit/testdata/invalid_config_for_custom_search_command.json b/tests/unit/testdata/invalid_config_for_custom_search_command.json new file mode 100644 index 0000000000..e3f1f7a301 --- /dev/null +++ b/tests/unit/testdata/invalid_config_for_custom_search_command.json @@ -0,0 +1,79 @@ +{ + "pages": { + "configuration": { + "tabs": [ + { + "name": "logging", + "entity": [ + { + "type": "singleSelect", + "label": "Log level", + "options": { + "disableSearch": true, + "autoCompleteFields": [ + { + "value": "DEBUG", + "label": "DEBUG" + }, + { + "value": "INFO", + "label": "INFO" + }, + { + "value": "WARNING", + "label": "WARNING" + }, + { + "value": "ERROR", + "label": "ERROR" + }, + { + "value": "CRITICAL", + "label": "CRITICAL" + } + ] + }, + "defaultValue": "INFO", + "field": "loglevel", + "required": true + } + ], + "title": "Logging" + } + ], + "title": "Configuration", + "description": "Set up your add-on" + } + }, + "meta": { + "name": "Splunk_TA_UCCExample", + "restRoot": "splunk_ta_uccexample", + "version": "1.0.0", + "displayName": "Splunk UCC test Add-on", + "schemaVersion": "0.0.3" + }, + "customSearchCommand": [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": true, + "description": " This command generates COUNT occurrences of a TEXT string.", + "arguments": [ + { + "name": "count", + "required": true, + "validate": { + "type": "Integer", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "text", + "required": true + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/unit/testdata/valid_config.json b/tests/unit/testdata/valid_config.json index 1ebb30b219..7019c6d5b4 100644 --- a/tests/unit/testdata/valid_config.json +++ b/tests/unit/testdata/valid_config.json @@ -1356,5 +1356,31 @@ "target": "3rdparty/linux" } ] - } + }, + "customSearchCommand": [ + { + "commandName": "generatetextcommand", + "fileName": "generatetext.py", + "commandType": "generating", + "requiredSearchAssistant": true, + "description": "This command generates COUNT occurrences of a TEXT string.", + "syntax": "generatetextcommand count= text=", + "usage": "public", + "arguments": [ + { + "name": "count", + "required": true, + "validate": { + "type": "Integer", + "minimum": 5, + "maximum": 10 + } + }, + { + "name": "text", + "required": true + } + ] + } + ] } \ No newline at end of file