Skip to content

Commit 3fad1a2

Browse files
artemrysMateuszMa
andauthored
feat: build commands produces detailed output of what happened (#927)
This is a copy of the PR @MateuszMa created but due to how the repository was previously organized I needed to create another one myself. This PR introduces a new option `-v` to the `ucc-gen build` command which shows detailed information about created/copied/modified/conflict files after build is complete. --------- Co-authored-by: Mateusz Macalik <[email protected]>
1 parent 19998ad commit 3fad1a2

File tree

19 files changed

+758
-2
lines changed

19 files changed

+758
-2
lines changed

docs/quickstart.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ It takes the following parameters:
118118
Accepts absolute paths as well.
119119
* `--python-binary-name` - [optional] Python binary name to use when
120120
installing Python libraries. Default: `python3`.
121+
* `-v` / `--verbose` - [optional] show detailed information about
122+
created/copied/modified/conflict files after build is complete.
123+
This option is in experimental mode. Default: `False`.
124+
125+
#### Verbose mode
126+
127+
Available from `v5.33.0`.
128+
129+
Running `ucc-gen build -v` or `ucc-gen build --verbose` prints additional information about
130+
what was exactly created / copied / modified / conflict after the build is complete. It does
131+
not scan `lib` folder due to the nature of the folder.
132+
133+
Below is the explanation on what exactly each state means:
134+
135+
* `created` - file is not in the original package and was created during the build process
136+
* `copied` - file is in the original package and was copied during the build process
137+
* `modified` - file is in the original package and was modified during the build process
138+
* `conflict` - file is in the original package and was copied during the build process but may be generated by UCC itself so incorrect usage can lead to not working add-on
121139

122140
### `ucc-gen init`
123141

splunk_add_on_ucc_framework/commands/build.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import sys
2222
from typing import Optional, List
2323
import subprocess
24+
import colorama as c
25+
import fnmatch
26+
import filecmp
2427

2528
from openapi3 import OpenAPI
2629

@@ -327,12 +330,126 @@ def _get_python_version_from_executable(python_binary_name: str) -> str:
327330
)
328331

329332

333+
def summary_report(
334+
source: str,
335+
ta_name: str,
336+
output_directory: str,
337+
verbose_file_summary_report: bool,
338+
) -> None:
339+
# initialising colorama to handle ASCII color in windows cmd
340+
c.init()
341+
color_palette = {
342+
"copied": c.Fore.GREEN,
343+
"conflict": c.Fore.RED,
344+
"modified": c.Fore.YELLOW,
345+
}
346+
347+
# conflicting files from ucc-gen package folder
348+
conflict_path = os.path.join(internal_root_dir, "package")
349+
# conflict files generated through-out the process
350+
conflict_static_list = frozenset(
351+
[
352+
"import_declare_test.py",
353+
f"{ta_name}_rh_*.py",
354+
"app.conf",
355+
"inputs.conf*",
356+
"restmap.conf",
357+
"server.conf",
358+
f"{ta_name}_*.conf*",
359+
"web.conf",
360+
"default.xml",
361+
"configuration.xml",
362+
"dashboard.xml",
363+
"inputs.xml",
364+
"openapi.json",
365+
]
366+
)
367+
368+
def line_print(print_path: str, mod_type: str) -> None:
369+
if verbose_file_summary_report:
370+
logger.info(
371+
color_palette.get(mod_type, "")
372+
+ str(print_path).ljust(80)
373+
+ mod_type
374+
+ c.Style.RESET_ALL,
375+
)
376+
summary[mod_type] += 1
377+
378+
def check_for_conflict(file: str, relative_file_path: str) -> bool:
379+
conflict_path_file = os.path.join(conflict_path, relative_file_path)
380+
if os.path.isfile(conflict_path_file):
381+
return True
382+
for pattern in conflict_static_list:
383+
if fnmatch.fnmatch(file, pattern):
384+
return True
385+
return False
386+
387+
def file_check(
388+
file: str, output_directory: str, relative_file_path: str, source: str
389+
) -> None:
390+
source_path = os.path.join(source, relative_file_path)
391+
392+
if os.path.isfile(source_path):
393+
# file is present in package
394+
output_path = os.path.join(output_directory, relative_file_path)
395+
396+
is_conflict = check_for_conflict(file, relative_file_path)
397+
398+
if not is_conflict:
399+
files_are_same = filecmp.cmp(source_path, output_path)
400+
if not files_are_same:
401+
# output file was modified
402+
line_print(relative_file_path, "modified")
403+
else:
404+
# files are the same
405+
line_print(relative_file_path, "copied")
406+
else:
407+
line_print(relative_file_path, "conflict")
408+
else:
409+
# file does not exist in package
410+
line_print(relative_file_path, "created")
411+
412+
summary = {"created": 0, "copied": 0, "modified": 0, "conflict": 0}
413+
414+
path_len = len(output_directory) + 1
415+
416+
if verbose_file_summary_report:
417+
logger.info("Detailed information about created/copied/modified/conflict files")
418+
logger.info(
419+
"Read more about it here: "
420+
"https://splunk.github.io/addonfactory-ucc-generator/quickstart/#verbose-mode"
421+
)
422+
423+
for path, dir, files in os.walk(output_directory):
424+
relative_path = path[path_len:]
425+
# skipping lib directory
426+
if relative_path[:3] == "lib":
427+
if relative_path == "lib":
428+
line_print("lib", "created")
429+
continue
430+
431+
files = sorted(files, key=str.casefold)
432+
433+
for file in files:
434+
relative_file_path = os.path.join(relative_path, file)
435+
file_check(file, output_directory, relative_file_path, source)
436+
437+
summary_combined = ", ".join(
438+
[
439+
f"{file_type}: {amount_of_files}"
440+
for file_type, amount_of_files in summary.items()
441+
]
442+
)
443+
logger.info(f"File creation summary: {summary_combined}")
444+
445+
330446
def generate(
331447
source: str,
332448
config_path: Optional[str] = None,
333449
addon_version: Optional[str] = None,
334450
output_directory: Optional[str] = None,
335451
python_binary_name: str = "python3",
452+
verbose_file_summary_report: bool = False,
336453
) -> None:
337454
logger.info(f"ucc-gen version {__version__} is used")
338455
logger.info(f"Python binary name to use: {python_binary_name}")
@@ -583,3 +700,10 @@ def generate(
583700
logger.info(f"Creating {output_openapi_folder} folder")
584701
with open(output_openapi_path, "w") as openapi_file:
585702
json.dump(open_api.raw_element, openapi_file, indent=4)
703+
704+
summary_report(
705+
source,
706+
ta_name,
707+
os.path.join(output_directory, ta_name),
708+
verbose_file_summary_report,
709+
)

splunk_add_on_ucc_framework/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
103103
help="Python binary name to use to install requirements",
104104
default="python3",
105105
)
106+
build_parser.add_argument(
107+
"-v",
108+
"--verbose",
109+
action="store_true",
110+
default=False,
111+
help=(
112+
"[experimental] show detailed information about "
113+
"created/copied/modified/conflict files after build is complete"
114+
),
115+
)
106116

107117
package_parser = subparsers.add_parser("package", description="Package an add-on")
108118
package_parser.add_argument(
@@ -178,6 +188,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
178188
addon_version=args.ta_version,
179189
output_directory=args.output,
180190
python_binary_name=args.python_binary_name,
191+
verbose_file_summary_report=args.verbose,
181192
)
182193
if args.command == "package":
183194
package.package(path_to_built_addon=args.path, output_directory=args.output)

tests/smoke/test_ucc_build.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import tempfile
33
import sys
44
import pytest
5+
import logging
6+
import json
57
from os import path
68

79
from tests.smoke import helpers
@@ -279,3 +281,114 @@ def test_ucc_generate_openapi_with_configuration_files_only():
279281
temp_dir, "Splunk_TA_UCCExample", "appserver", "static", "openapi.json"
280282
)
281283
assert not path.exists(expected_file_path)
284+
285+
286+
def test_ucc_build_verbose_mode(caplog):
287+
"""
288+
Tests results will test both no option and --verbose mode of build command.
289+
No option provides a short summary of file created in manner: File creation summary: <result>
290+
--verbose shows each file specific case and short summary
291+
"""
292+
293+
caplog.set_level(logging.INFO, logger="ucc-gen")
294+
295+
def extract_summary_logs():
296+
return_logs = []
297+
copy_logs = False
298+
299+
message_to_start = (
300+
"Detailed information about created/copied/modified/conflict files"
301+
)
302+
message_to_end = "File creation summary:"
303+
304+
for record in caplog.records:
305+
if record.message == message_to_start:
306+
copy_logs = True
307+
308+
if copy_logs:
309+
return_logs.append(record)
310+
311+
if record.message[:22] == message_to_end:
312+
copy_logs = False
313+
314+
return return_logs
315+
316+
def generate_expected_log():
317+
def append_appserver_content(raw_expected_logs):
318+
path_len = len(app_server_lib_path) + 1
319+
excluded_files = ["redirect_page.js", "redirect.html"]
320+
321+
for full_path, dir, files in os.walk(app_server_lib_path):
322+
if files:
323+
relative_path = full_path[path_len:]
324+
for file in files:
325+
if file not in excluded_files:
326+
relative_file_path = os.path.join(relative_path, file)
327+
key_to_insert = (
328+
str(relative_file_path).ljust(80) + "created\u001b[0m"
329+
)
330+
raw_expected_logs[key_to_insert] = "INFO"
331+
332+
def summarize_types(raw_expected_logs):
333+
summary_counter = {"created": 0, "copied": 0, "modified": 0, "conflict": 0}
334+
335+
for log in raw_expected_logs:
336+
end = log.find("\u001b[0m")
337+
if end > 1:
338+
string_end = end - 10
339+
operation_type = log[string_end:end].strip()
340+
summary_counter[operation_type] += 1
341+
342+
summary_message = (
343+
f'File creation summary: created: {summary_counter.get("created")}, '
344+
f'copied: {summary_counter.get("copied")}, '
345+
f'modified: {summary_counter.get("modified")}, '
346+
f'conflict: {summary_counter.get("conflict")}'
347+
)
348+
raw_expected_logs[summary_message] = "INFO"
349+
350+
with open(expected_logs_path) as f:
351+
raw_expected_logs = json.load(f)
352+
353+
append_appserver_content(raw_expected_logs)
354+
summarize_types(raw_expected_logs)
355+
356+
return raw_expected_logs
357+
358+
with tempfile.TemporaryDirectory() as temp_dir:
359+
package_folder = path.join(
360+
path.dirname(path.realpath(__file__)),
361+
"..",
362+
"testdata",
363+
"test_addons",
364+
"package_files_conflict_test",
365+
"package",
366+
)
367+
368+
expected_logs_path = path.join(
369+
path.dirname(path.realpath(__file__)),
370+
"..",
371+
"testdata",
372+
"expected_addons",
373+
"expected_files_conflict_test",
374+
"expected_log.json",
375+
)
376+
377+
build.generate(
378+
source=package_folder,
379+
output_directory=temp_dir,
380+
verbose_file_summary_report=True,
381+
)
382+
383+
app_server_lib_path = os.path.join(build.internal_root_dir, "package")
384+
385+
summary_logs = extract_summary_logs()
386+
387+
expected_logs = generate_expected_log()
388+
389+
assert len(summary_logs) == len(expected_logs)
390+
391+
for log_line in summary_logs:
392+
# summary messages must be the same but might come in different order
393+
assert log_line.message in expected_logs.keys()
394+
assert log_line.levelname == expected_logs[log_line.message]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Detailed information about created/copied/modified/conflict files": "INFO",
3+
"Read more about it here: https://splunk.github.io/addonfactory-ucc-generator/quickstart/#verbose-mode": "INFO",
4+
"\u001b[33mapp.manifest modified\u001b[0m": "INFO",
5+
"\u001b[32mLICENSE.txt copied\u001b[0m": "INFO",
6+
"\u001b[32mREADME.txt copied\u001b[0m": "INFO",
7+
"VERSION created\u001b[0m": "INFO",
8+
"\u001b[31mbin/import_declare_test.py conflict\u001b[0m": "INFO",
9+
"\u001b[32mbin/my_first_input.py copied\u001b[0m": "INFO",
10+
"\u001b[31mbin/test_addon_rh_account.py conflict\u001b[0m": "INFO",
11+
"bin/test_addon_rh_my_first_input.py created\u001b[0m": "INFO",
12+
"bin/test_addon_rh_settings.py created\u001b[0m": "INFO",
13+
"default/app.conf created\u001b[0m": "INFO",
14+
"default/inputs.conf created\u001b[0m": "INFO",
15+
"default/restmap.conf created\u001b[0m": "INFO",
16+
"default/server.conf created\u001b[0m": "INFO",
17+
"default/test_addon_settings.conf created\u001b[0m": "INFO",
18+
"default/web.conf created\u001b[0m": "INFO",
19+
"default/data/ui/nav/default.xml created\u001b[0m": "INFO",
20+
"default/data/ui/views/configuration.xml created\u001b[0m": "INFO",
21+
"default/data/ui/views/dashboard.xml created\u001b[0m": "INFO",
22+
"default/data/ui/views/inputs.xml created\u001b[0m": "INFO",
23+
"\u001b[31mREADME/inputs.conf.spec conflict\u001b[0m": "INFO",
24+
"\u001b[31mREADME/test_addon_account.conf.spec conflict\u001b[0m": "INFO",
25+
"\u001b[31mREADME/test_addon_settings.conf.spec conflict\u001b[0m": "INFO",
26+
"lib created\u001b[0m": "INFO",
27+
"appserver/static/js/build/globalConfig.json created\u001b[0m": "INFO",
28+
"appserver/static/openapi.json created\u001b[0m": "INFO",
29+
"metadata/default.meta created\u001b[0m": "INFO"
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# test_addon

0 commit comments

Comments
 (0)