ara-cli 0.1.9.95__py3-none-any.whl → 0.1.9.96__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ara_cli/__init__.py +4 -1
- ara_cli/__main__.py +57 -11
- ara_cli/ara_command_action.py +31 -19
- ara_cli/artefact_autofix.py +131 -2
- ara_cli/artefact_creator.py +2 -7
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_models/artefact_templates.py +3 -3
- ara_cli/artefact_models/feature_artefact_model.py +25 -0
- ara_cli/artefact_reader.py +4 -5
- ara_cli/chat.py +12 -13
- ara_cli/commands/extract_command.py +4 -11
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +3 -2
- ara_cli/prompt_extractor.py +1 -1
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.9.96.dist-info}/METADATA +1 -1
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.9.96.dist-info}/RECORD +26 -25
- tests/test_ara_command_action.py +66 -52
- tests/test_artefact_autofix.py +361 -5
- tests/test_chat.py +88 -6
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.9.96.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.9.96.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.95.dist-info → ara_cli-0.1.9.96.dist-info}/top_level.txt +0 -0
ara_cli/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from .
|
|
2
|
+
from .error_handler import ErrorHandler
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
whitelisted_commands = ["RERUN", "SEND", "EXTRACT", "LOAD_IMAGE", "CHOOSE_MODEL", "CHOOSE_EXTRACTION_MODEL", "CURRENT_MODEL", "CURRENT_EXTRACTION_MODEL", "LIST_MODELS"]
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
error_handler = ErrorHandler()
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
# ANSI escape codes for coloring
|
|
9
12
|
YELLOW = '\033[93m'
|
|
10
13
|
RESET = '\033[0m'
|
ara_cli/__main__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# PYTHON_ARGCOMPLETE_OK
|
|
2
2
|
from ara_cli.ara_command_parser import action_parser
|
|
3
|
+
from ara_cli.error_handler import AraError
|
|
3
4
|
from ara_cli.version import __version__
|
|
4
5
|
from ara_cli.ara_command_action import (
|
|
5
6
|
create_action,
|
|
@@ -22,8 +23,10 @@ from ara_cli.ara_command_action import (
|
|
|
22
23
|
autofix_action,
|
|
23
24
|
extract_action
|
|
24
25
|
)
|
|
26
|
+
from . import error_handler
|
|
25
27
|
import argcomplete
|
|
26
28
|
import sys
|
|
29
|
+
from os import getenv
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def define_action_mapping():
|
|
@@ -51,31 +54,74 @@ def define_action_mapping():
|
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def handle_invalid_action(args):
|
|
54
|
-
|
|
57
|
+
raise AraError("Invalid action provided. Type ara -h for help", error_code=1)
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def
|
|
58
|
-
|
|
60
|
+
def is_debug_mode_enabled():
|
|
61
|
+
"""Check if debug mode is enabled via environment variable."""
|
|
62
|
+
return getenv('ARA_DEBUG', '').lower() in ('1', 'true', 'yes')
|
|
59
63
|
|
|
64
|
+
|
|
65
|
+
def setup_parser():
|
|
66
|
+
"""Create and configure the argument parser."""
|
|
67
|
+
parser = action_parser()
|
|
68
|
+
|
|
60
69
|
# Show examples when help is called
|
|
61
70
|
if any(arg in sys.argv for arg in ["-h", "--help"]):
|
|
62
71
|
parser.add_examples = True
|
|
63
|
-
|
|
72
|
+
|
|
64
73
|
parser.add_argument(
|
|
65
74
|
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
66
75
|
)
|
|
76
|
+
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--debug", action="store_true", help="Enable debug mode for detailed error output"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return parser
|
|
67
82
|
|
|
68
|
-
action_mapping = define_action_mapping()
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
def configure_debug_mode(args, env_debug_mode):
|
|
85
|
+
"""Configure debug mode based on arguments and environment."""
|
|
86
|
+
if (hasattr(args, 'debug') and args.debug) or env_debug_mode:
|
|
87
|
+
error_handler.debug_mode = True
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
|
|
90
|
+
def should_show_help(args):
|
|
91
|
+
"""Check if help should be displayed."""
|
|
92
|
+
return not hasattr(args, "action") or not args.action
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def execute_action(args, action_mapping):
|
|
96
|
+
"""Execute the specified action."""
|
|
76
97
|
action = action_mapping.get(args.action, handle_invalid_action)
|
|
77
98
|
action(args)
|
|
78
99
|
|
|
79
100
|
|
|
101
|
+
def cli():
|
|
102
|
+
debug_mode = is_debug_mode_enabled()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
parser = setup_parser()
|
|
106
|
+
action_mapping = define_action_mapping()
|
|
107
|
+
|
|
108
|
+
argcomplete.autocomplete(parser)
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
|
|
111
|
+
configure_debug_mode(args, debug_mode)
|
|
112
|
+
|
|
113
|
+
if should_show_help(args):
|
|
114
|
+
parser.print_help()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
execute_action(args, action_mapping)
|
|
118
|
+
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
print("\n[INFO] Operation cancelled by user", file=sys.stderr)
|
|
121
|
+
sys.exit(130) # Standard exit code for Ctrl+C
|
|
122
|
+
except Exception as e:
|
|
123
|
+
error_handler.handle_error(e, context="cli")
|
|
124
|
+
|
|
125
|
+
|
|
80
126
|
if __name__ == "__main__":
|
|
81
|
-
cli()
|
|
127
|
+
cli()
|
ara_cli/ara_command_action.py
CHANGED
|
@@ -2,17 +2,19 @@ from os.path import join
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import json
|
|
5
|
+
from ara_cli.error_handler import AraError
|
|
6
|
+
from ara_cli.error_handler import handle_errors, AraValidationError
|
|
5
7
|
from ara_cli.output_suppressor import suppress_stdout
|
|
6
8
|
from ara_cli.artefact_fuzzy_search import suggest_close_name_matches
|
|
7
|
-
from . import whitelisted_commands
|
|
9
|
+
from . import whitelisted_commands, error_handler
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def check_validity(condition, error_message):
|
|
11
13
|
if not condition:
|
|
12
|
-
|
|
13
|
-
sys.exit(1)
|
|
14
|
+
raise AraValidationError(error_message)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
@handle_errors(context="create action", error_handler=error_handler)
|
|
16
18
|
def create_action(args):
|
|
17
19
|
from ara_cli.artefact_creator import ArtefactCreator
|
|
18
20
|
from ara_cli.classifier import Classifier
|
|
@@ -62,6 +64,7 @@ def create_action(args):
|
|
|
62
64
|
artefact_creator.run(args.parameter, args.classifier, parent_classifier, parent_name, rule)
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
@handle_errors(context="delete action", error_handler=error_handler)
|
|
65
68
|
def delete_action(args):
|
|
66
69
|
from ara_cli.artefact_deleter import ArtefactDeleter
|
|
67
70
|
|
|
@@ -69,6 +72,7 @@ def delete_action(args):
|
|
|
69
72
|
artefact_deleter.delete(args.parameter, args.classifier, args.force)
|
|
70
73
|
|
|
71
74
|
|
|
75
|
+
@handle_errors(context="rename action", error_handler=error_handler)
|
|
72
76
|
def rename_action(args):
|
|
73
77
|
from ara_cli.artefact_renamer import ArtefactRenamer
|
|
74
78
|
from ara_cli.classifier import Classifier
|
|
@@ -82,6 +86,7 @@ def rename_action(args):
|
|
|
82
86
|
artefact_renamer.rename(args.parameter, args.aspect, args.classifier)
|
|
83
87
|
|
|
84
88
|
|
|
89
|
+
@handle_errors(context="rename action", error_handler=error_handler)
|
|
85
90
|
def list_action(args):
|
|
86
91
|
from ara_cli.artefact_lister import ArtefactLister
|
|
87
92
|
from ara_cli.list_filter import ListFilter
|
|
@@ -131,6 +136,7 @@ def list_action(args):
|
|
|
131
136
|
artefact_lister.list_files(list_filter=list_filter)
|
|
132
137
|
|
|
133
138
|
|
|
139
|
+
@handle_errors(context="list-tags action", error_handler=error_handler)
|
|
134
140
|
def list_tags_action(args):
|
|
135
141
|
from ara_cli.tag_extractor import TagExtractor
|
|
136
142
|
from ara_cli.list_filter import ListFilter
|
|
@@ -155,6 +161,7 @@ def list_tags_action(args):
|
|
|
155
161
|
print(output)
|
|
156
162
|
|
|
157
163
|
|
|
164
|
+
@handle_errors(context="prompt action", error_handler=error_handler)
|
|
158
165
|
def prompt_action(args):
|
|
159
166
|
from ara_cli.classifier import Classifier
|
|
160
167
|
from ara_cli.filename_validator import is_valid_filename
|
|
@@ -227,6 +234,7 @@ def prompt_action(args):
|
|
|
227
234
|
raise ValueError(f"Unknown command '{init}' provided.")
|
|
228
235
|
|
|
229
236
|
|
|
237
|
+
@handle_errors(context="chat action", error_handler=error_handler)
|
|
230
238
|
def chat_action(args):
|
|
231
239
|
from ara_cli.chat import Chat
|
|
232
240
|
|
|
@@ -253,6 +261,7 @@ def chat_action(args):
|
|
|
253
261
|
chat.start()
|
|
254
262
|
|
|
255
263
|
|
|
264
|
+
@handle_errors(context="template action", error_handler=error_handler)
|
|
256
265
|
def template_action(args):
|
|
257
266
|
from ara_cli.classifier import Classifier
|
|
258
267
|
from ara_cli.template_manager import TemplatePathManager
|
|
@@ -265,6 +274,7 @@ def template_action(args):
|
|
|
265
274
|
print(content)
|
|
266
275
|
|
|
267
276
|
|
|
277
|
+
@handle_errors(context="fetch-templates action", error_handler=error_handler)
|
|
268
278
|
def fetch_templates_action(args):
|
|
269
279
|
import shutil
|
|
270
280
|
from ara_cli.ara_config import ConfigManager
|
|
@@ -294,6 +304,7 @@ def fetch_templates_action(args):
|
|
|
294
304
|
os.makedirs(join(local_prompt_modules_dir, subdir), exist_ok=True)
|
|
295
305
|
|
|
296
306
|
|
|
307
|
+
@handle_errors(context="read action", error_handler=error_handler)
|
|
297
308
|
def read_action(args):
|
|
298
309
|
from ara_cli.commands.read_command import ReadCommand
|
|
299
310
|
from ara_cli.list_filter import ListFilter
|
|
@@ -321,6 +332,7 @@ def read_action(args):
|
|
|
321
332
|
command.execute()
|
|
322
333
|
|
|
323
334
|
|
|
335
|
+
@handle_errors(context="reconnect action", error_handler=error_handler)
|
|
324
336
|
def reconnect_action(args):
|
|
325
337
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
326
338
|
from ara_cli.artefact_models.artefact_model import Contribution
|
|
@@ -348,8 +360,7 @@ def reconnect_action(args):
|
|
|
348
360
|
)
|
|
349
361
|
|
|
350
362
|
if not artefact:
|
|
351
|
-
|
|
352
|
-
return
|
|
363
|
+
raise AraError(read_error_message)
|
|
353
364
|
|
|
354
365
|
parent = ArtefactReader.read_artefact(
|
|
355
366
|
artefact_name=parent_name,
|
|
@@ -358,8 +369,7 @@ def reconnect_action(args):
|
|
|
358
369
|
)
|
|
359
370
|
|
|
360
371
|
if not parent:
|
|
361
|
-
|
|
362
|
-
return
|
|
372
|
+
raise AraError(read_error_message)
|
|
363
373
|
|
|
364
374
|
contribution = Contribution(
|
|
365
375
|
artefact_name=parent.title,
|
|
@@ -367,13 +377,9 @@ def reconnect_action(args):
|
|
|
367
377
|
)
|
|
368
378
|
|
|
369
379
|
if rule:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
feedback_message += f" using rule '{closest_rule}'"
|
|
374
|
-
except TypeError as e:
|
|
375
|
-
print(f"{type(e).__name__}:", e)
|
|
376
|
-
exit(1)
|
|
380
|
+
closest_rule = find_closest_rule(parent, rule)
|
|
381
|
+
contribution.rule = closest_rule
|
|
382
|
+
feedback_message += f" using rule '{closest_rule}'"
|
|
377
383
|
|
|
378
384
|
artefact.contribution = contribution
|
|
379
385
|
with open(artefact.file_path, 'w', encoding='utf-8') as file:
|
|
@@ -383,6 +389,7 @@ def reconnect_action(args):
|
|
|
383
389
|
print(feedback_message + ".")
|
|
384
390
|
|
|
385
391
|
|
|
392
|
+
@handle_errors(context="read-status action", error_handler=error_handler)
|
|
386
393
|
def read_status_action(args):
|
|
387
394
|
from ara_cli.file_classifier import FileClassifier
|
|
388
395
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -396,7 +403,7 @@ def read_status_action(args):
|
|
|
396
403
|
|
|
397
404
|
all_artefact_names = [artefact_info["title"] for artefact_info in artefact_info_dicts]
|
|
398
405
|
if artefact_name not in all_artefact_names:
|
|
399
|
-
suggest_close_name_matches(artefact_name, all_artefact_names)
|
|
406
|
+
suggest_close_name_matches(artefact_name, all_artefact_names, report_as_error=True)
|
|
400
407
|
return
|
|
401
408
|
|
|
402
409
|
artefact_info = next(filter(
|
|
@@ -415,6 +422,7 @@ def read_status_action(args):
|
|
|
415
422
|
print(status)
|
|
416
423
|
|
|
417
424
|
|
|
425
|
+
@handle_errors(context="read-user action", error_handler=error_handler)
|
|
418
426
|
def read_user_action(args):
|
|
419
427
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
420
428
|
from ara_cli.file_classifier import FileClassifier
|
|
@@ -428,7 +436,7 @@ def read_user_action(args):
|
|
|
428
436
|
|
|
429
437
|
all_artefact_names = [artefact_info["title"] for artefact_info in artefact_info_dicts]
|
|
430
438
|
if artefact_name not in all_artefact_names:
|
|
431
|
-
suggest_close_name_matches(artefact_name, all_artefact_names)
|
|
439
|
+
suggest_close_name_matches(artefact_name, all_artefact_names, report_as_error=True)
|
|
432
440
|
return
|
|
433
441
|
|
|
434
442
|
artefact_info = next(filter(
|
|
@@ -448,6 +456,7 @@ def read_user_action(args):
|
|
|
448
456
|
print(f" - {tag}")
|
|
449
457
|
|
|
450
458
|
|
|
459
|
+
@handle_errors(context="set-status action", error_handler=error_handler)
|
|
451
460
|
def set_status_action(args):
|
|
452
461
|
from ara_cli.artefact_models.artefact_model import ALLOWED_STATUS_VALUES
|
|
453
462
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -490,6 +499,7 @@ def set_status_action(args):
|
|
|
490
499
|
print(f"Status of task '{artefact_name}' has been updated to '{new_status}'.")
|
|
491
500
|
|
|
492
501
|
|
|
502
|
+
@handle_errors(context="set-user action", error_handler=error_handler)
|
|
493
503
|
def set_user_action(args):
|
|
494
504
|
from ara_cli.file_classifier import FileClassifier
|
|
495
505
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -528,6 +538,7 @@ def set_user_action(args):
|
|
|
528
538
|
print(f"User of task '{artefact_name}' has been updated to '{new_user}'.")
|
|
529
539
|
|
|
530
540
|
|
|
541
|
+
@handle_errors(context="classifier-directory action", error_handler=error_handler)
|
|
531
542
|
def classifier_directory_action(args):
|
|
532
543
|
from ara_cli.classifier import Classifier
|
|
533
544
|
|
|
@@ -536,6 +547,7 @@ def classifier_directory_action(args):
|
|
|
536
547
|
print(subdirectory)
|
|
537
548
|
|
|
538
549
|
|
|
550
|
+
@handle_errors(context="scan action", error_handler=error_handler)
|
|
539
551
|
def scan_action(args):
|
|
540
552
|
from ara_cli.file_classifier import FileClassifier
|
|
541
553
|
from ara_cli.artefact_scan import find_invalid_files, show_results
|
|
@@ -550,6 +562,7 @@ def scan_action(args):
|
|
|
550
562
|
show_results(invalid_artefacts)
|
|
551
563
|
|
|
552
564
|
|
|
565
|
+
@handle_errors(context="autofix_action", error_handler=error_handler)
|
|
553
566
|
def autofix_action(args):
|
|
554
567
|
from ara_cli.artefact_autofix import parse_report, apply_autofix, read_report_file
|
|
555
568
|
from ara_cli.file_classifier import FileClassifier
|
|
@@ -589,18 +602,17 @@ def autofix_action(args):
|
|
|
589
602
|
print("\nAutofix process completed. Please review the changes.")
|
|
590
603
|
|
|
591
604
|
|
|
605
|
+
@handle_errors(context="extract action", error_handler=error_handler)
|
|
592
606
|
def extract_action(args):
|
|
593
607
|
from ara_cli.commands.extract_command import ExtractCommand
|
|
594
608
|
|
|
595
609
|
filename = args.filename
|
|
596
610
|
force = args.force
|
|
597
611
|
write = getattr(args, 'write', False)
|
|
598
|
-
print(filename)
|
|
599
612
|
command = ExtractCommand(
|
|
600
613
|
file_name=filename,
|
|
601
614
|
force=force,
|
|
602
615
|
write=write,
|
|
603
|
-
output=lambda msg: print(msg, file=sys.stdout)
|
|
604
|
-
error_output=lambda msg: print(msg, file=sys.stderr)
|
|
616
|
+
output=lambda msg: print(msg, file=sys.stdout)
|
|
605
617
|
)
|
|
606
618
|
command.execute()
|
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ara_cli.error_handler import AraError
|
|
1
2
|
from ara_cli.artefact_scan import check_file
|
|
2
3
|
from ara_cli.artefact_fuzzy_search import (
|
|
3
4
|
find_closest_name_matches,
|
|
@@ -10,6 +11,7 @@ from ara_cli.artefact_models.artefact_model import Artefact
|
|
|
10
11
|
from typing import Optional, Dict, List, Tuple
|
|
11
12
|
import difflib
|
|
12
13
|
import os
|
|
14
|
+
import re
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def populate_classified_artefact_info(
|
|
@@ -104,8 +106,9 @@ def determine_artefact_type_and_class(classifier):
|
|
|
104
106
|
|
|
105
107
|
artefact_class = artefact_type_mapping.get(artefact_type)
|
|
106
108
|
if not artefact_class:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
raise AraError(f"No artefact class found for {artefact_type}")
|
|
110
|
+
# print(f"No artefact class found for {artefact_type}")
|
|
111
|
+
# return None, None
|
|
109
112
|
|
|
110
113
|
return artefact_type, artefact_class
|
|
111
114
|
|
|
@@ -372,6 +375,131 @@ def set_closest_contribution(
|
|
|
372
375
|
return artefact, True
|
|
373
376
|
|
|
374
377
|
|
|
378
|
+
def fix_scenario_placeholder_mismatch(
|
|
379
|
+
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
380
|
+
) -> str:
|
|
381
|
+
"""
|
|
382
|
+
Converts a regular Scenario with placeholders to a Scenario Outline.
|
|
383
|
+
This is a deterministic fix that detects placeholders and converts the format.
|
|
384
|
+
"""
|
|
385
|
+
lines = artefact_text.splitlines()
|
|
386
|
+
new_lines = []
|
|
387
|
+
i = 0
|
|
388
|
+
|
|
389
|
+
while i < len(lines):
|
|
390
|
+
line = lines[i]
|
|
391
|
+
stripped_line = line.strip()
|
|
392
|
+
|
|
393
|
+
if stripped_line.startswith('Scenario:'):
|
|
394
|
+
scenario_lines, next_index = _extract_scenario_block(lines, i)
|
|
395
|
+
processed_lines = _process_scenario_block(scenario_lines)
|
|
396
|
+
new_lines.extend(processed_lines)
|
|
397
|
+
i = next_index
|
|
398
|
+
else:
|
|
399
|
+
new_lines.append(line)
|
|
400
|
+
i += 1
|
|
401
|
+
|
|
402
|
+
return "\n".join(new_lines)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _extract_scenario_block(lines: list, start_index: int) -> tuple[list, int]:
|
|
406
|
+
"""Extract all lines belonging to a scenario block."""
|
|
407
|
+
scenario_lines = [lines[start_index]]
|
|
408
|
+
j = start_index + 1
|
|
409
|
+
|
|
410
|
+
while j < len(lines):
|
|
411
|
+
next_line = lines[j].strip()
|
|
412
|
+
if _is_scenario_boundary(next_line):
|
|
413
|
+
break
|
|
414
|
+
scenario_lines.append(lines[j])
|
|
415
|
+
j += 1
|
|
416
|
+
|
|
417
|
+
return scenario_lines, j
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _is_scenario_boundary(line: str) -> bool:
|
|
421
|
+
"""Check if a line marks the boundary of a scenario block."""
|
|
422
|
+
boundaries = ['Scenario:', 'Scenario Outline:', 'Background:', 'Feature:']
|
|
423
|
+
return any(line.startswith(boundary) for boundary in boundaries)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _process_scenario_block(scenario_lines: list) -> list:
|
|
427
|
+
"""Process a scenario block and convert to outline if placeholders are found."""
|
|
428
|
+
if not scenario_lines:
|
|
429
|
+
return scenario_lines
|
|
430
|
+
|
|
431
|
+
first_line = scenario_lines[0]
|
|
432
|
+
indentation = _get_line_indentation(first_line)
|
|
433
|
+
placeholders = _extract_placeholders_from_scenario(scenario_lines[1:])
|
|
434
|
+
|
|
435
|
+
if not placeholders:
|
|
436
|
+
return scenario_lines
|
|
437
|
+
|
|
438
|
+
return _convert_to_scenario_outline(scenario_lines, placeholders, indentation)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _get_line_indentation(line: str) -> str:
|
|
442
|
+
"""Get the indentation of a line."""
|
|
443
|
+
return line[:len(line) - len(line.lstrip())]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _extract_placeholders_from_scenario(step_lines: list) -> set:
|
|
447
|
+
"""Extract placeholders from scenario step lines, ignoring docstrings."""
|
|
448
|
+
placeholders = set()
|
|
449
|
+
in_docstring = False
|
|
450
|
+
|
|
451
|
+
for line in step_lines:
|
|
452
|
+
step_line = line.strip()
|
|
453
|
+
if not step_line:
|
|
454
|
+
continue
|
|
455
|
+
|
|
456
|
+
in_docstring = _update_docstring_state(step_line, in_docstring)
|
|
457
|
+
|
|
458
|
+
if not in_docstring and '"""' not in step_line:
|
|
459
|
+
found = re.findall(r'<([^>]+)>', step_line)
|
|
460
|
+
placeholders.update(found)
|
|
461
|
+
|
|
462
|
+
return placeholders
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _update_docstring_state(line: str, current_state: bool) -> bool:
|
|
466
|
+
"""Update the docstring state based on the current line."""
|
|
467
|
+
if '"""' in line:
|
|
468
|
+
return not current_state
|
|
469
|
+
return current_state
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _convert_to_scenario_outline(scenario_lines: list, placeholders: set, indentation: str) -> list:
|
|
473
|
+
"""Convert scenario lines to scenario outline format with examples table."""
|
|
474
|
+
first_line = scenario_lines[0]
|
|
475
|
+
title = first_line.strip()[len('Scenario:'):].strip()
|
|
476
|
+
|
|
477
|
+
new_lines = [f"{indentation}Scenario Outline: {title}"]
|
|
478
|
+
new_lines.extend(scenario_lines[1:])
|
|
479
|
+
new_lines.append("")
|
|
480
|
+
|
|
481
|
+
examples_lines = _create_examples_table(placeholders, indentation)
|
|
482
|
+
new_lines.extend(examples_lines)
|
|
483
|
+
|
|
484
|
+
return new_lines
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _create_examples_table(placeholders: set, base_indentation: str) -> list:
|
|
488
|
+
"""Create the Examples table for the scenario outline."""
|
|
489
|
+
examples_indentation = base_indentation + " "
|
|
490
|
+
table_indentation = examples_indentation + " "
|
|
491
|
+
|
|
492
|
+
sorted_placeholders = sorted(placeholders)
|
|
493
|
+
header = "| " + " | ".join(sorted_placeholders) + " |"
|
|
494
|
+
sample_row = "| " + " | ".join(f"<{p}_value>" for p in sorted_placeholders) + " |"
|
|
495
|
+
|
|
496
|
+
return [
|
|
497
|
+
f"{examples_indentation}Examples:",
|
|
498
|
+
f"{table_indentation}{header}",
|
|
499
|
+
f"{table_indentation}{sample_row}"
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
|
|
375
503
|
def fix_title_mismatch(
|
|
376
504
|
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
377
505
|
) -> str:
|
|
@@ -584,6 +712,7 @@ def apply_autofix(
|
|
|
584
712
|
"Filename-Title Mismatch": fix_title_mismatch,
|
|
585
713
|
"Invalid Contribution Reference": fix_contribution,
|
|
586
714
|
"Rule Mismatch": fix_rule,
|
|
715
|
+
"Scenario Contains Placeholders": fix_scenario_placeholder_mismatch,
|
|
587
716
|
}
|
|
588
717
|
|
|
589
718
|
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|
ara_cli/artefact_creator.py
CHANGED
|
@@ -38,11 +38,9 @@ class ArtefactCreator:
|
|
|
38
38
|
destination = Path(dir_path) / dest_name
|
|
39
39
|
|
|
40
40
|
if not source.exists():
|
|
41
|
-
print("[ERROR] Source file does not exist!")
|
|
42
41
|
raise FileNotFoundError(f"Source file {source} not found!")
|
|
43
42
|
|
|
44
43
|
if not destination.parent.exists():
|
|
45
|
-
print("[ERROR] Destination directory does not exist!")
|
|
46
44
|
raise NotADirectoryError(f"Destination directory {destination.parent} does not exist!")
|
|
47
45
|
|
|
48
46
|
copyfile(source, destination)
|
|
@@ -70,9 +68,7 @@ class ArtefactCreator:
|
|
|
70
68
|
def validate_template(self, template_path, classifier):
|
|
71
69
|
template_name = f"template.{classifier}"
|
|
72
70
|
if not self.template_exists(template_path, template_name):
|
|
73
|
-
|
|
74
|
-
return False
|
|
75
|
-
return True
|
|
71
|
+
raise FileNotFoundError(f"Template file '{template_name}' not found in the specified template path.")
|
|
76
72
|
|
|
77
73
|
def set_artefact_parent(self, artefact, parent_classifier, parent_file_name) -> Artefact:
|
|
78
74
|
classified_artefacts = ArtefactReader.read_artefacts()
|
|
@@ -94,8 +90,7 @@ class ArtefactCreator:
|
|
|
94
90
|
navigator.navigate_to_target()
|
|
95
91
|
|
|
96
92
|
if not Classifier.is_valid_classifier(classifier):
|
|
97
|
-
|
|
98
|
-
return
|
|
93
|
+
raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
|
|
99
94
|
|
|
100
95
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
101
96
|
file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
|
ara_cli/artefact_deleter.py
CHANGED
|
@@ -20,16 +20,14 @@ class ArtefactDeleter:
|
|
|
20
20
|
self.navigate_to_target()
|
|
21
21
|
|
|
22
22
|
if not Classifier.is_valid_classifier(classifier):
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
|
|
25
24
|
|
|
26
25
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
27
26
|
file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
|
|
28
27
|
dir_path = self.file_system.path.join(sub_directory, f"{filename}.data")
|
|
29
28
|
|
|
30
29
|
if not self.file_system.path.exists(file_path):
|
|
31
|
-
|
|
32
|
-
return
|
|
30
|
+
raise FileNotFoundError(f"Artefact {file_path} not found.")
|
|
33
31
|
if not force:
|
|
34
32
|
user_choice = input(f"Are you sure you want to delete the file {filename} and data directory if existing? (y/N): ")
|
|
35
33
|
|
ara_cli/artefact_fuzzy_search.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import difflib
|
|
2
2
|
from textwrap import indent
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from . import error_handler
|
|
5
|
+
from ara_cli.error_handler import AraError
|
|
4
6
|
|
|
5
7
|
|
|
6
|
-
def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5):
|
|
8
|
+
def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5, report_as_error: bool = False):
|
|
7
9
|
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=cutoff)
|
|
8
|
-
|
|
10
|
+
if report_as_error:
|
|
11
|
+
error_handler.report_error(AraError(message))
|
|
12
|
+
else:
|
|
13
|
+
print(message)
|
|
9
14
|
if not closest_matches:
|
|
10
15
|
return
|
|
11
16
|
print("Closest matches:")
|
|
@@ -13,23 +18,25 @@ def suggest_close_names(artefact_name: str, all_artefact_names: list[str], messa
|
|
|
13
18
|
print(f" - {match}")
|
|
14
19
|
|
|
15
20
|
|
|
16
|
-
def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str]):
|
|
21
|
+
def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str], report_as_error: bool = False):
|
|
17
22
|
message = f"No match found for artefact with name '{artefact_name}'"
|
|
18
23
|
|
|
19
24
|
suggest_close_names(
|
|
20
25
|
artefact_name=artefact_name,
|
|
21
26
|
all_artefact_names=all_artefact_names,
|
|
22
|
-
message=message
|
|
27
|
+
message=message,
|
|
28
|
+
report_as_error=report_as_error
|
|
23
29
|
)
|
|
24
30
|
|
|
25
31
|
|
|
26
|
-
def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str):
|
|
32
|
+
def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str, report_as_error: bool = False):
|
|
27
33
|
message = f"No match found for parent of '{artefact_name}' with name '{parent_name}'"
|
|
28
34
|
|
|
29
35
|
suggest_close_names(
|
|
30
36
|
artefact_name=parent_name,
|
|
31
37
|
all_artefact_names=all_artefact_names,
|
|
32
|
-
message=message
|
|
38
|
+
message=message,
|
|
39
|
+
report_as_error=report_as_error
|
|
33
40
|
)
|
|
34
41
|
|
|
35
42
|
|
|
@@ -148,9 +148,9 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
|
|
|
148
148
|
Scenario(
|
|
149
149
|
title="<descriptive_scenario_title>",
|
|
150
150
|
steps=[
|
|
151
|
-
"Given
|
|
152
|
-
"When
|
|
153
|
-
"Then
|
|
151
|
+
"Given [precondition]",
|
|
152
|
+
"When [action]",
|
|
153
|
+
"Then [expected result]"
|
|
154
154
|
],
|
|
155
155
|
),
|
|
156
156
|
ScenarioOutline(
|
|
@@ -148,6 +148,30 @@ class Scenario(BaseModel):
|
|
|
148
148
|
raise ValueError("steps list must not be empty")
|
|
149
149
|
return steps
|
|
150
150
|
|
|
151
|
+
@model_validator(mode='after')
|
|
152
|
+
def check_no_placeholders(cls, values: 'Scenario') -> 'Scenario':
|
|
153
|
+
"""Ensure regular scenarios don't contain placeholders that should be in scenario outlines."""
|
|
154
|
+
placeholders = set()
|
|
155
|
+
for step in values.steps:
|
|
156
|
+
# Skip validation if step contains docstring placeholders (during parsing)
|
|
157
|
+
if '__DOCSTRING_PLACEHOLDER_' in step:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Skip validation if step contains docstring markers (after reinjection)
|
|
161
|
+
if '"""' in step:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
found = re.findall(r'<([^>]+)>', step)
|
|
165
|
+
placeholders.update(found)
|
|
166
|
+
|
|
167
|
+
if placeholders:
|
|
168
|
+
placeholder_list = ', '.join(f"<{p}>" for p in sorted(placeholders))
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Scenario Contains Placeholders ({placeholder_list}) but is not a Scenario Outline. "
|
|
171
|
+
f"Use 'Scenario Outline:' instead of 'Scenario:' and provide an Examples table."
|
|
172
|
+
)
|
|
173
|
+
return values
|
|
174
|
+
|
|
151
175
|
@classmethod
|
|
152
176
|
def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['Scenario', int]:
|
|
153
177
|
"""Parse a Scenario from a list of lines starting at start_idx."""
|
|
@@ -277,6 +301,7 @@ class FeatureArtefact(Artefact):
|
|
|
277
301
|
f"FeatureArtefact must have artefact_type of '{ArtefactType.feature}', not '{v}'")
|
|
278
302
|
return v
|
|
279
303
|
|
|
304
|
+
|
|
280
305
|
@classmethod
|
|
281
306
|
def _title_prefix(cls) -> str:
|
|
282
307
|
return "Feature:"
|