1717from robot .output import LOGGER , Message
1818from robot .running .builder import TestSuiteBuilder
1919from robot .running .builder .builders import SuiteStructureParser
20+ from robot .utils import NormalizedDict
2021from robot .utils .filereader import FileReader
2122from robotcode .core .dataclasses import from_json
22- from robotcode .core .lsp .types import Diagnostic , DiagnosticSeverity , DocumentUri , Position , Range
23+ from robotcode .core .lsp .types import (
24+ Diagnostic ,
25+ DiagnosticSeverity ,
26+ DocumentUri ,
27+ Position ,
28+ Range ,
29+ )
2330from robotcode .core .uri import Uri
2431from robotcode .plugin import Application , OutputFormat , UnknownError , pass_application
2532from robotcode .plugin .click_helper .types import add_options
@@ -47,9 +54,13 @@ def _patch() -> None:
4754
4855 if get_robot_version () <= (6 , 1 ):
4956 if get_robot_version () > (5 , 0 ) and get_robot_version () < (6 , 0 ) or get_robot_version () < (5 , 0 ):
50- from robot .running .builder .testsettings import TestDefaults # pyright: ignore[reportMissingImports]
57+ from robot .running .builder .testsettings import (
58+ TestDefaults ,
59+ )
5160 else :
52- from robot .running .builder .settings import Defaults as TestDefaults # pyright: ignore[reportMissingImports]
61+ from robot .running .builder .settings import (
62+ Defaults as TestDefaults ,
63+ )
5364
5465 old_validate_test_counts = TestSuiteBuilder ._validate_test_counts
5566
@@ -74,11 +85,15 @@ def build_suite(self: SuiteStructureParser, structure: Any) -> Tuple[TestSuite,
7485 from robot .running .builder .parsers import format_name
7586
7687 return ErroneousTestSuite (
77- error_message = str (e ), name = format_name (structure .source ), source = structure .source
88+ error_message = str (e ),
89+ name = format_name (structure .source ),
90+ source = structure .source ,
7891 ), TestDefaults (parent_defaults )
7992
8093 return ErroneousTestSuite (
81- error_message = str (e ), name = TestSuite .name_from_source (structure .source ), source = structure .source
94+ error_message = str (e ),
95+ name = TestSuite .name_from_source (structure .source ),
96+ source = structure .source ,
8297 ), TestDefaults (parent_defaults )
8398
8499 SuiteStructureParser ._build_suite = build_suite
@@ -95,7 +110,9 @@ def _validate_execution_mode(self: SuiteStructureParser, suite: TestSuite) -> No
95110
96111 elif get_robot_version () >= (6 , 1 ):
97112 from robot .parsing .suitestructure import SuiteDirectory , SuiteFile
98- from robot .running .builder .settings import TestDefaults # pyright: ignore[reportMissingImports]
113+ from robot .running .builder .settings import (
114+ TestDefaults ,
115+ )
99116
100117 old_validate_not_empty = TestSuiteBuilder ._validate_not_empty
101118
@@ -115,7 +132,9 @@ def build_suite_file(self: SuiteStructureParser, structure: SuiteFile) -> TestSu
115132 except DataError as e :
116133 LOGGER .error (str (e ))
117134 return ErroneousTestSuite (
118- error_message = str (e ), name = TestSuite .name_from_source (structure .source ), source = structure .source
135+ error_message = str (e ),
136+ name = TestSuite .name_from_source (structure .source ),
137+ source = structure .source ,
119138 )
120139
121140 SuiteStructureParser ._build_suite_file = build_suite_file
@@ -130,7 +149,9 @@ def build_suite_directory(
130149 except DataError as e :
131150 LOGGER .error (str (e ))
132151 return ErroneousTestSuite (
133- error_message = str (e ), name = TestSuite .name_from_source (structure .source ), source = structure .source
152+ error_message = str (e ),
153+ name = TestSuite .name_from_source (structure .source ),
154+ source = structure .source ,
134155 ), TestDefaults (self .parent_defaults )
135156
136157 SuiteStructureParser ._build_suite_directory = build_suite_directory
@@ -214,8 +235,22 @@ def __init__(self) -> None:
214235 self .tests : List [TestItem ] = []
215236 self .tags : Dict [str , List [TestItem ]] = defaultdict (list )
216237 self .statistics = Statistics ()
238+ self ._collected = [NormalizedDict (ignore = "_" )]
217239
218240 def visit_suite (self , suite : TestSuite ) -> None :
241+ if suite .name in self ._collected [- 1 ] and suite .parent .source :
242+ LOGGER .warn (
243+ (
244+ f"Warning in { 'file' if Path (suite .parent .source ).is_file () else 'folder' } "
245+ f"'{ suite .parent .source } ': "
246+ if suite .source and Path (suite .parent .source ).exists ()
247+ else ""
248+ )
249+ + f"Multiple suites with name '{ suite .name } ' in suite '{ suite .parent .longname } '."
250+ )
251+
252+ self ._collected [- 1 ][suite .name ] = True
253+ self ._collected .append (NormalizedDict (ignore = "_" ))
219254 try :
220255 item = TestItem (
221256 type = "suite" ,
@@ -253,7 +288,18 @@ def visit_suite(self, suite: TestSuite) -> None:
253288 if suite .tests :
254289 self .statistics .suites_with_tests += 1
255290
291+ def end_suite (self , _suite : TestSuite ) -> None :
292+ self ._collected .pop ()
293+
256294 def visit_test (self , test : TestCase ) -> None :
295+ if test .name in self ._collected [- 1 ]:
296+ LOGGER .warn (
297+ f"Warning in file '{ test .source } ' on line { test .lineno } : "
298+ f"Multiple { 'task' if test .parent .rpa else 'test' } s with name '{ test .name } ' in suite "
299+ f"'{ test .parent .longname } '."
300+ )
301+ self ._collected [- 1 ][test .name ] = True
302+
257303 if self ._current .children is None :
258304 self ._current .children = []
259305 try :
@@ -284,7 +330,10 @@ def visit_test(self, test: TestCase) -> None:
284330
285331@click .group (invoke_without_command = False )
286332@click .option (
287- "--read-from-stdin" , is_flag = True , help = "Read file contents from stdin. This is an internal option." , hidden = True
333+ "--read-from-stdin" ,
334+ is_flag = True ,
335+ help = "Read file contents from stdin. This is an internal option." ,
336+ hidden = True ,
288337)
289338@pass_application
290339def discover (app : Application , read_from_stdin : bool ) -> None :
@@ -304,7 +353,9 @@ def discover(app: Application, read_from_stdin: bool) -> None:
304353 app .verbose (f"Read data from stdin: { _stdin_data !r} " )
305354
306355
307- RE_IN_FILE_LINE_MATCHER = re .compile (r".+\sin\sfile\s'(?P<file>.*)'\son\sline\s(?P<line>\d+):(?P<message>.*)" )
356+ RE_IN_FILE_LINE_MATCHER = re .compile (
357+ r".+\sin\s(file|folder)\s'(?P<file>.*)'(\son\sline\s(?P<line>\d+))?:(?P<message>.*)"
358+ )
308359RE_PARSING_FAILED_MATCHER = re .compile (r"Parsing\s'(?P<file>.*)'\sfailed:(?P<message>.*)" )
309360
310361
@@ -321,7 +372,10 @@ def build_diagnostics(messages: List[Message]) -> Dict[str, List[Diagnostic]]:
321372 result : Dict [str , List [Diagnostic ]] = {}
322373
323374 def add_diagnostic (
324- message : Message , source_uri : Optional [str ] = None , line : Optional [int ] = None , text : Optional [str ] = None
375+ message : Message ,
376+ source_uri : Optional [str ] = None ,
377+ line : Optional [int ] = None ,
378+ text : Optional [str ] = None ,
325379 ) -> None :
326380 source_uri = str (Uri .from_path (Path (source_uri ).resolve () if source_uri else Path .cwd ()))
327381
@@ -343,7 +397,12 @@ def add_diagnostic(
343397
344398 for message in messages :
345399 if match := RE_IN_FILE_LINE_MATCHER .match (message .message ):
346- add_diagnostic (message , match .group ("file" ), int (match .group ("line" )), text = match .group ("message" ).strip ())
400+ add_diagnostic (
401+ message ,
402+ match .group ("file" ),
403+ int (match .group ("line" )) if match .group ("line" ) is not None else None ,
404+ text = match .group ("message" ).strip (),
405+ )
347406 elif match := RE_PARSING_FAILED_MATCHER .match (message .message ):
348407 add_diagnostic (message , match .group ("file" ), text = match .group ("message" ).strip ())
349408 else :
@@ -357,7 +416,7 @@ def handle_options(
357416 by_longname : Tuple [str , ...],
358417 exclude_by_longname : Tuple [str , ...],
359418 robot_options_and_args : Tuple [str , ...],
360- ) -> Tuple [TestSuite , Optional [Dict [str , List [Diagnostic ]]]]:
419+ ) -> Tuple [TestSuite , Collector , Optional [Dict [str , List [Diagnostic ]]]]:
361420 root_folder , profile , cmd_options = handle_robot_options (
362421 app , by_longname , exclude_by_longname , robot_options_and_args
363422 )
@@ -418,7 +477,10 @@ def handle_options(
418477 suite .visit (ModelModifier (settings .pre_run_modifiers , settings .run_empty_suite , LOGGER ))
419478 suite .configure (** settings .suite_config )
420479
421- return suite , build_diagnostics (diagnostics_logger .messages )
480+ collector = Collector ()
481+ suite .visit (collector )
482+
483+ return suite , collector , build_diagnostics (diagnostics_logger .messages )
422484
423485 except Information as err :
424486 app .echo (str (err ))
@@ -458,18 +520,16 @@ def all(
458520 ```
459521 """
460522
461- suite , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
462-
463- collector = Collector ()
464- suite .visit (collector )
523+ suite , collector , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
465524
466525 if collector .all .children :
467526 if app .config .output_format is None or app .config .output_format == OutputFormat .TEXT :
468527 tests_or_tasks = "Task" if suite .rpa else "Test"
469528
470529 def print (item : TestItem , indent : int = 0 ) -> Iterable [str ]:
471530 type = click .style (
472- item .type .capitalize () if item .type == "suite" else tests_or_tasks .capitalize (), fg = "green"
531+ item .type .capitalize () if item .type == "suite" else tests_or_tasks .capitalize (),
532+ fg = "green" ,
473533 )
474534
475535 if item .type == "test" :
@@ -529,10 +589,7 @@ def tests(
529589 ```
530590 """
531591
532- suite , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
533-
534- collector = Collector ()
535- suite .visit (collector )
592+ suite , collector , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
536593
537594 if collector .all .children :
538595 if app .config .output_format is None or app .config .output_format == OutputFormat .TEXT :
@@ -576,10 +633,7 @@ def suites(
576633 ```
577634 """
578635
579- suite , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
580-
581- collector = Collector ()
582- suite .visit (collector )
636+ suite , collector , diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
583637
584638 if collector .all .children :
585639 if app .config .output_format is None or app .config .output_format == OutputFormat .TEXT :
@@ -630,10 +684,7 @@ def tags(
630684 ```
631685 """
632686
633- suite , _diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
634-
635- collector = Collector ()
636- suite .visit (collector )
687+ _suite , collector , _diagnostics = handle_options (app , by_longname , exclude_by_longname , robot_options_and_args )
637688
638689 if collector .all .children :
639690 if app .config .output_format is None or app .config .output_format == OutputFormat .TEXT :
0 commit comments