Skip to content

Commit 329016c

Browse files
authored
Merge pull request #15 from tomasohara/tom-dev
integrate tom-dev
2 parents cf2b42e + 84f9cd6 commit 329016c

13 files changed

Lines changed: 163 additions & 71 deletions

.github/workflows/tests.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,18 @@ jobs:
2727
## TODO?: runs-on: ubuntu:act-20.04
2828
runs-on: ubuntu-20.04
2929
## OLD: runs-on: ubuntu-latest
30-
30+
31+
container:
32+
image: my-shell-scripts
33+
options: --user=root
34+
3135
steps:
3236
- uses: actions/checkout@v3
3337

3438
- name: Build docker image
3539
run: |
36-
docker build -t name:shell-scripts-dev -f- . <Dockerfile
40+
## OLD: docker build -t name:shell-scripts-dev -f- . <Dockerfile
41+
docker build --tag my-shell-scripts -f- . <Dockerfile
3742
3843
## TEST
3944
- name: Configure Environment
@@ -46,4 +51,6 @@ jobs:
4651
## OLD: docker run --env "DEBUG_LEVEL=5" --rm --mount type=bind,source="$(pwd)",target=/home/shell-scripts shell-scripts-dev
4752
## TODO???: put repo under /mnt/local-shell-scripts and installed version under /home/shell-scripts
4853
## OLD: docker run --env "DEBUG_LEVEL=5" --rm --mount type=bind,source="$(pwd)",target=/home/shell-scripts shell-scripts-dev
49-
docker run --env "DEBUG_LEVEL=5" --rm --mount type=bind,source="$(pwd)",target=/home/shell-scripts ubuntu:act-20.04
54+
## OLD: docker run --env "DEBUG_LEVEL=5" --rm --mount type=bind,source="$(pwd)",target=/home/shell-scripts ubuntu:act-20.04
55+
## TEST:
56+
docker run --env "DEBUG_LEVEL=5" --rm --mount type=bind,source="$(pwd)",target=/home/shell-scripts my-shell-scripts

check_errors.perl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ BEGIN
9696
# TODO: rework error in line test to omit files
9797
# NOTE: It can be easier to add special-case rules rather than devise a general regex;
9898
# ex: 'error' occuring within a line even at word boundaries can be too broad.
99-
elsif (## &debug_print(7, "here\n", 7) &&
100-
/^(ERROR|Error)\b/
99+
elsif (## DEBUG: &debug_print(7, "here\n", 7) &&
100+
## OLD: /^(ERROR|Error)\b/
101+
/^(Error)\b/i
101102
## OLD: || /command not found/i
102103
## NOTE: maldito modules package polutes environment and man page not clear about disabling
103104
|| (/command not found/i && (! /Cannot switch to Modules/))

examples/extract-all-git-versions.bash

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ while read -r LINE; do
7777
COMMIT_DATE=$(echo "$LINE" | cut -d ' ' -f 1)
7878
COMMIT_SHA=$(echo "$LINE" | cut -d ' ' -f 2)
7979
## DEBUG: echo "COUNT=$COUNT LINE=$LINE COMMIT_DATE=$COMMIT_DATE COMMIT_SHA=$COMMIT_SHA"
80-
$verbose && printf '.'
81-
git cat-file -p "$COMMIT_SHA:$REL_GIT_PATH_TO_FILE" > "$EXPORT_TO/$GIT_SHORT_FILENAME.$COUNT.$COMMIT_DATE"
80+
## OLD: $verbose && printf '.'
81+
output_file="$EXPORT_TO/$GIT_SHORT_FILENAME.$COUNT.$COMMIT_DATE"
82+
git cat-file -p "$COMMIT_SHA:$REL_GIT_PATH_TO_FILE" > "$output_file"
83+
$verbose && echo "$output_file"
8284
done <"$info"
8385

8486
# return success code

git-aliases.bash

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -772,8 +772,8 @@ function git-misc-alias-usage() {
772772
## OLD:
773773
## echo " git mv --verbose old-file new-file"
774774
## echo " GIT_MESSAGE='renamed' GIT_SKIP_ADD=1 git-update-commit-push old-file new-file"
775-
echo " GIT_MESSAGE='renamed' git-rename-file old-file new-file"
776-
echo " GIT_MESSAGE='moved' git-move-to-dir dir file1 file2"
775+
echo " GIT_MESSAGE='renamed' git-rename-file OLD NEW-file"
776+
echo " GIT_MESSAGE='moved' git-move-to-dir DIR file1 file2"
777777
echo ""
778778
echo "To delete files (mucho cuidado):"
779779
echo " git rm old-file"

local-workflows.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,25 @@ if [ "${RUN_BUILD:-1}" = "1" ]; then
7070
# https://stackoverflow.com/questions/42297387/docker-build-with-build-arg-with-multiple-arguments
7171
## TODO: docker build --build-arg "DEBUG_LEVEL=$DEBUG_LEVEL" --build-arg "GIT_BRANCH=$GIT_BRANCH" --platform linux/x86_64 --tag "$IMAGE_NAME" .
7272
# shellcheck disable=SC2086
73-
docker build --build-arg "GIT_BRANCH=$GIT_BRANCH" --platform linux/x86_64 $BUILD_OPTS --tag "name:$IMAGE_NAME" .
73+
## OLD: docker build --build-arg "GIT_BRANCH=$GIT_BRANCH" --platform linux/x86_64 $BUILD_OPTS --tag "name:$IMAGE_NAME" .
74+
## TEST
75+
docker build --build-arg "GIT_BRANCH=$GIT_BRANCH" --platform linux/x86_64 $BUILD_OPTS --tag my-shell-scripts .
7476
fi
7577

7678
# Run the Github Actions workflow locally
7779
# TODO: get this to effing work [maldito act]
7880
if [ "${RUN_WORKFLOW:-1}" = "1" ]; then
7981
file="${WORKFLOW_FILE:-act.yml}"
80-
## TEST (Pass along debug level in evvironment)
81-
echo "Running Github Actions locally"
82+
## TEST (Pass along debug level in environment)
83+
echo "Running Github Actions locally w/ $file"
8284
# shellcheck disable=SC2086
8385
## TODO: act --env DEBUG_LEVEL="$DEBUG_LEVEL" --container-architecture linux/amd64 --pull="$ACT_PULL" -platform "$ACT_WORKFLOW" --workflows ./.github/workflows/"$file" $RUN_OPTS
84-
act --verbose --env DEBUG_LEVEL="$DEBUG_LEVEL" --container-architecture linux/amd64 --pull="$ACT_PULL" -platform "$ACT_WORKFLOW" --workflows ./.github/workflows/"$file" $RUN_OPTS
86+
act --verbose --env DEBUG_LEVEL="$DEBUG_LEVEL" --container-architecture linux/amd64 --pull="$ACT_PULL" -platform my-shell-scripts --workflows ./.github/workflows/"$file" $RUN_OPTS
8587
fi
8688

89+
# Run via docker directly
8790
if [ "${RUN_DOCKER:-1}" = "1" ]; then
8891
echo "Running Tests via Docker"
89-
docker run -it --env DEBUG_LEVEL="$DEBUG_LEVEL" --mount type=bind,source="$(pwd)",target=/home/shell-scripts shell-scripts-dev
92+
## OLD: docker run -it --env DEBUG_LEVEL="$DEBUG_LEVEL" --mount type=bind,source="$(pwd)",target=/home/shell-scripts shell-scripts-dev
93+
docker run -it --env DEBUG_LEVEL="$DEBUG_LEVEL" --mount type=bind,source="$(pwd)",target=/home/shell-scripts my-shell-scripts
9094
fi

simple_batspp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ def _convert_to_bats(self, test):
665665
## TEST: ("" if not GLOBAL_SETUP.strip() else ("\t" + GLOBAL_SETUP + ";\n")) +
666666
f'\ttestfolder="{TEMP_DIR}/{unspaced_title}"\n' +
667667
f'\tmkdir --parents "$testfolder"\n' +
668-
(f'\tcommand cp ./*.* "$testfolder"\n' if COPY_DIR else '') +
668+
(f'\tcommand cp -R ./. "$testfolder"\n' if COPY_DIR else '') +
669669
# note: warning added for sake of shellcheck
670670
f'\tbuiltin cd "$testfolder" || echo Warning: Unable to "cd $testfolder"\n')
671671
setup_sans_prompt = my_re.sub(r'^\s*\$', '\t', setup, flags=my_re.MULTILINE)

tests/batspp_report.py

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
# DESCRIPTION: Test automation & report generation for ipynb test files (for BatsPP 1.5.X).
55
# BatsPP files are generated by default (stored at ./batspp-only).
66
#
7+
# TODO2:
8+
# - use gh.form_path instead of f"{dir}/{file}" (for sake of Windows users).
9+
#
710

811
"""
912
Test automation & report generation for BatsPP test files for Bash.
1013
The files can be generated from Jupyter .ipynb files (see ./batspp-only).
1114
"""
1215

1316
# Standard modules
14-
## TODO: import json
17+
import math
1518

1619
# Installed modules
1720
import yaml
@@ -71,6 +74,8 @@
7174
"Regex for tests to include; ex: 'c.*' for debugging")
7275
SINGLE_STORE = system.getenv_bool("SINGLE_STORE", False,
7376
f"Whether to just use {BATSPP_OUTPUT_STORE} for all store dirs except kcov")
77+
CLEAN_DEFAULT = system.getenv_bool("CLEAN_OUTPUT", not debug.detailed_debugging(),
78+
f"Whether to clean existing output by remove entire directories")
7479
## NOTE: the code needs to be thoroughly revamped (e.g., currently puts .batspp in same place as .bats)
7580
if SINGLE_STORE:
7681
BATSPP_STORE = BATS_STORE = TXT_STORE = BATSPP_OUTPUT_STORE
@@ -112,17 +117,21 @@ def main():
112117
BATSPP_SWITCH_ARG = "switch"
113118
FORCE_ARG = "force"
114119
CLEAN_ARG = "clean"
120+
DEFINITIONS_ARG = "definitions"
115121
main_app = Main(
116122
description=__doc__.format(script=gh.basename(__file__)).strip("\n"),
117123
boolean_options=[
118124
(NO_REPORTS_ARG, "No reports are generated, testfiles are shown"),
119125
(KCOV_REPORTS_ARG, f"KCOV (HTML based) reports generated, stored at {KCOV_STORE}"),
120126
(TEXT_REPORTS_ARG, f"Textfile based reports generated, stored at {TXT_STORE}"),
121-
(ALL_REPORTS_ARG, "Generates report for all available testfiles (NOBATSPP testfiles were ignored by default)"),
127+
(ALL_REPORTS_ARG, "Generates report for all available testfiles (n.b., NOBATSPP testfiles ignored by default)"),
122128
(FORCE_ARG, "Force running under admin-like account"),
123129
(CLEAN_ARG, "Remove output from previous runs; *** warning: this removes entire subdirectories"),
124130
(BATSPP_SWITCH_ARG, "Uses batspp library instead of ../simple_batspp.py script"),
125131
],
132+
text_options=[
133+
(DEFINITIONS_ARG, "Script with alias definitions to be sourced"),
134+
],
126135
skip_input=False,
127136
manual_input=False,
128137
short_options=True,
@@ -134,10 +143,12 @@ def main():
134143
KCOV_OPTION = main_app.get_parsed_option(KCOV_REPORTS_ARG)
135144
ALL_OPTION = main_app.get_parsed_option(ALL_REPORTS_ARG)
136145
FORCE_OPTION = main_app.get_parsed_option(FORCE_ARG, UNDER_DOCKER)
137-
CLEAN_OPTION = main_app.get_parsed_option(CLEAN_ARG)
146+
CLEAN_OPTION = main_app.get_parsed_option(CLEAN_ARG, CLEAN_DEFAULT)
138147
BATSPP_SWITCH_OPTION = main_app.get_parsed_option(BATSPP_SWITCH_ARG)
139148
USE_SIMPLE_BATSPP = (not BATSPP_SWITCH_OPTION)
140-
debug.trace_expr(4, NO_OPTION, TXT_OPTION, KCOV_OPTION, FORCE_OPTION, CLEAN_OPTION, BATSPP_SWITCH_OPTION, USE_SIMPLE_BATSPP)
149+
DEFINITIONS_SCRIPT = main_app.get_parsed_option(DEFINITIONS_ARG)
150+
debug.trace_expr(4, NO_OPTION, TXT_OPTION, KCOV_OPTION, FORCE_OPTION, CLEAN_OPTION, BATSPP_SWITCH_OPTION, USE_SIMPLE_BATSPP, DEFINITIONS_SCRIPT)
151+
RUN_BATS = (TXT_OPTION or not NO_OPTION)
141152

142153
# Do check for adminstrative user and exit unless --force
143154
is_admin = my_re.search(r"root|admin|adm", gh.run("groups"))
@@ -148,9 +159,9 @@ def main():
148159

149160
# Cleanup up previous rusn
150161
# Warning: 'rm -rf' is a very dangerous command:
151-
# it should only be done in temporary directories (i.e., not under
162+
# it should only be done in temporary directories (e.g., not under repo)
152163
if CLEAN_OPTION:
153-
if not NO_OPTION:
164+
if RUN_BATS:
154165
gh.run(f"rm -rf {BATSPP_STORE}/*")
155166
gh.run(f"rm -rf {BATS_STORE}/*")
156167

@@ -167,12 +178,14 @@ def run_batspp(input_file, output_file):
167178
real_output_file = output_file + ".out"
168179
log_file = output_file + ".log"
169180
debug.trace(5, f"run_batspp{(input_file, output_file)}")
181+
source_spec = (f"--source '{DEFINITIONS_SCRIPT}'" if DEFINITIONS_SCRIPT else "")
170182
if USE_SIMPLE_BATSPP:
171183
# note: adds sentinels around paragraph segments for simpler parsing;
172-
# also uses Bash instead of Bats and copies ./tests files into bats test dir (under temp)
173-
run_output = gh.run(f"MATCH_SENTINELS=1 PARA_BLOCKS=1 BASH_EVAL=1 COPY_DIR=1 FORCE_RUN={FORCE_OPTION} python3 ../simple_batspp.py {input_file} --output {output_file} > {real_output_file} 2> {log_file}")
184+
# uses Bash instead of Bats (to bypass need for global setup sections)
185+
# and copies ./tests files into bats test dir (under temp).
186+
run_output = gh.run(f"MATCH_SENTINELS=1 PARA_BLOCKS=1 BASH_EVAL=1 COPY_DIR=1 FORCE_RUN={FORCE_OPTION} python3 ../simple_batspp.py {input_file} --output {output_file} {source_spec} > {real_output_file} 2> {log_file}")
174187
else:
175-
run_output = gh.run(f"batspp {input_file} --save {output_file} 2> {log_file}")
188+
run_output = gh.run(f"batspp {input_file} --save {output_file} {source_spec} 2> {log_file}")
176189
debug.code(4, lambda: gh.run(f"check_errors.perl {log_file}"))
177190
debug.assertion(not run_output.strip())
178191
real_output = system.read_file(real_output_file)
@@ -207,15 +220,16 @@ def run_batspp(input_file, output_file):
207220
is_ipynb = file.endswith(IPYNB)
208221
if is_ipynb:
209222
if TEST_REGEX and not my_re.match(fr"{TEST_REGEX}", file):
210-
debug.trace(3, f"Ignoring {file}")
223+
debug.trace(3, f"FYI: Ignoring {file} not mathing TEST_REGEX ({TEST_REGEX})")
211224
continue
212225
if not ALL_OPTION and NOBATSPP in file:
226+
debug.trace(4, f"FYI: Ignoring NOBATSPP file: {file}")
213227
print(f"NOBATSPP File Found [{i}]: {file}")
214228
avoid_array.append(file)
215229
avoid_count += 1
216-
else:
217-
print(f"JUPYTER Testfile Found [{i}]: {file}")
218-
ipynb_array.append(file)
230+
continue
231+
print(f"JUPYTER Testfile Found [{i}]: {file}")
232+
ipynb_array.append(file)
219233
i += 1
220234

221235
print(f"\nIPYNB Files Found (Total - NOBATSPP): {i-1} - {avoid_count} = {i-avoid_count-1}")
@@ -233,7 +247,7 @@ def run_batspp(input_file, output_file):
233247
gh.basename(testfile.replace(IPYNB, BATSPP)))
234248
print(f"IPYNB TESTFILE [{i}]: {testfile} => {batspp_from_ipynb}")
235249
log_file = f"{batspp_from_ipynb}.log"
236-
gh.run(f"python3 ../jupyter_to_batspp.py {testfile} --output {batspp_from_ipynb}")
250+
gh.run(f"python3 ../jupyter_to_batspp.py {testfile} --output {batspp_from_ipynb} 2> {log_file}")
237251
# note: uses call to avoid issue with lambda function argument binding
238252
debug.call(4, gh.run, f"check_errors.perl {log_file}", **{"output": True})
239253
batspp_array.append(batspp_from_ipynb)
@@ -244,9 +258,14 @@ def run_batspp(input_file, output_file):
244258

245259
# 3) Executing batspp files & storing them as bats
246260
print(f"\n\n==========BATS GENERATED==========\n")
261+
# TODO3: rename i => num_test_files, total_count_ok => total_ok_tests, and total_count_total to total_num_tests
247262
i = 1
263+
total_count_ok = 0
264+
total_count_total = 0
265+
total_success_rate = 0
266+
total_num_successful = 0
248267

249-
if NO_OPTION:
268+
if not RUN_BATS:
250269
print(f">> SKIPPING BATSPP CHECK (-n ARGUMENT PROVIDED)\n")
251270
else:
252271
for batsppfile_path in batspp_array:
@@ -266,34 +285,41 @@ def run_batspp(input_file, output_file):
266285
i += 1
267286

268287
if TXT_OPTION:
269-
bats_output = run_batspp(batsppfile_path, f"{BATSPP_OUTPUT_STORE}/{output_from_batspp}")
288+
output_from_batspp_path = gh.form_path(BATSPP_OUTPUT_STORE, output_from_batspp)
289+
bats_output = run_batspp(batsppfile_path, output_from_batspp_path)
270290

271291
output_lines = bats_output.splitlines()
272292
output_lines_filtered = [item for item in output_lines if not item.startswith("#")]
273293
debug.trace_expr(5, output_lines_filtered)
274294
if output_lines_filtered:
275295
# Ignore the line given the number of tests (e.g., "1..5")
276296
header_line = output_lines_filtered.pop(0)
277-
debug.assertion(my_re.search(r"^1\.\.\d+", header_line))
297+
debug.trace_expr(5, header_line)
298+
debug.assertion(my_re.search(r"^1\.\.\d+", header_line) or (header_line == "0..0"),
299+
f"Unexpected header line for {output_from_batspp_path}")
278300
debug.assertion(len(output_lines_filtered))
279301

280302
count_ok = len([item for item in output_lines_filtered if item.startswith("ok")])
281303
count_bad = len([item for item in output_lines_filtered if item.startswith("not ok")])
282304
count_total = (count_ok + count_bad)
283-
count_success_rate = (round((count_ok / count_total)*100, 2) if count_total else 0)
305+
success_rate = (round((count_ok / count_total)*100, 2) if count_total else 0)
284306
min_score = system.to_float(thresholds.get(ipynb_from_batspp, DEFAULT_MIN_SCORE))
285-
count_success_rate_bool = (count_success_rate >= min_score)
286-
debug.trace_expr(4, min_score, count_ok, count_bad, count_total, count_success_rate, count_success_rate_bool)
287-
SUMMARY_TEXT = f"{count_ok} out of {count_total} successful ({count_success_rate}%)\nSuccess: {count_success_rate_bool}"
307+
successful = (success_rate >= min_score)
308+
debug.trace_expr(4, min_score, count_ok, count_bad, count_total, success_rate, successful)
309+
SUMMARY_TEXT = f"{count_ok} out of {count_total} successful ({success_rate}%)\nSuccess: {successful}"
288310
gh.write_file(f"{TXT_STORE}/{txt_from_batspp}", SUMMARY_TEXT)
289311
print(f"{test_extensionless}: {SUMMARY_TEXT}")
312+
total_count_ok += count_ok
313+
total_count_total += count_total
314+
total_success_rate += success_rate
315+
total_num_successful += int(successful)
290316

291317
# Categorizing Tests if they are successful or not
292318
txt_option_JSON = {}
293319
txt_option_JSON["test_name"] = test_extensionless
294320
txt_option_JSON["test_min_score"] = min_score
295-
txt_option_JSON["test_success_rate"] = count_success_rate
296-
if count_success_rate_bool:
321+
txt_option_JSON["test_success_rate"] = success_rate
322+
if successful:
297323
success_test_array.append(txt_option_JSON)
298324
else:
299325
failure_test_array.append(txt_option_JSON)
@@ -326,24 +352,40 @@ def run_batspp(input_file, output_file):
326352
error_testfiles = [tf for tf in ipynb_array if tf not in set_wt]
327353
faulty_count = ipynb_count - batspp_count
328354

355+
NaN = math.nan
329356
print(f"\n======================================================")
330357
print(f"SUMMARY STATISTICS:\n")
331358
print(f"simple_batspp.py used: {bool(BATSPP_SWITCH_OPTION)}")
332359
print(f"No. of IPYNB testfiles: {ipynb_count + avoid_count}")
333-
print(f"No. of BATSPP files (generated): {batspp_count if TXT_OPTION or NO_OPTION != 1 else 'NaN'}")
334-
print(f"No. of FAULTY testfiles: {faulty_count if TXT_OPTION or NO_OPTION != 1 else 'NaN'}")
360+
print(f"No. of BATSPP files (generated): {batspp_count if RUN_BATS else NaN}")
361+
print(f"No. of FAULTY testfiles: {faulty_count if RUN_BATS else NaN}")
335362
print(f"No. of AVOIDED testfiles: {avoid_count}")
363+
print(f"Total no. of good tests: {total_count_ok}")
364+
print(f"Total no. of individual tests: {total_count_total}")
365+
# Note: macro-average is mean of success score, whereas micro-average is based on global counts.
366+
# See https://datascience.stackexchange.com/questions/15989/micro-average-vs-macro-average-performance-in-a-multiclass-classification-settin
367+
avg_successful = macro_success_rate = micro_success_rate = NaN
368+
if batspp_count:
369+
avg_successful = total_num_successful / batspp_count * 100
370+
macro_success_rate = total_success_rate / batspp_count
371+
micro_success_rate = total_count_ok / total_count_total * 100
372+
print(f"Total no. files OK w/ threshold: {total_num_successful}")
373+
print(f"Average no. files OK / threshold: {system.round_num(avg_successful)}%")
374+
print(f"Macro success score: {system.round_num(macro_success_rate)}%")
375+
print(f"Micro success score: {system.round_num(micro_success_rate)}%")
376+
print(" where successful macro is mean of individual scores and micro is global metric")
336377

337378
print(f"\nFAULTY TESTFILES:")
338379
if faulty_count == 0:
339-
print("NaN")
380+
print("n/a")
340381
else:
382+
## TODO3: what is the intention here (e.g., the '>>')?
341383
for tf in error_testfiles:
342384
print(f">> {tf}")
343385

344386
print("\nAVOIDED TESTFILES:")
345387
if avoid_count == 0:
346-
print("NaN")
388+
print("n/a")
347389
else:
348390
for tf in avoid_array:
349391
print(f">> {tf}")
@@ -352,6 +394,7 @@ def run_batspp(input_file, output_file):
352394
print("\nTEST SUCCESS (--txt ENABLED):")
353395
print(f"No. of Successful Tests:", len(success_test_array))
354396
print(f"No. of Failure Tests:", len(failure_test_array))
397+
debug.assertion(len(success_test_array) == total_num_successful)
355398

356399
def print_test_array(arr):
357400
"""Print summary of test results in ARR"""
@@ -371,7 +414,7 @@ def print_test_array(arr):
371414
print(f"======================================================")
372415

373416
# Return number of failed tests as statues (i.e., OK if 0 failed)
374-
code = len(failure_test_array)
417+
code = (len(failure_test_array) if success_test_array else -1)
375418
system.exit(status_code=code)
376419

377420
# -------------------------------------------------------------------------------

0 commit comments

Comments
 (0)