Skip to content

Commit 505f473

Browse files
committed
feat(cli): new command discover files
find all relevant robot framework files in a project.
1 parent 9e2d6f7 commit 505f473

File tree

8 files changed

+228
-36
lines changed

8 files changed

+228
-36
lines changed

.robotignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ report.html
292292
# ruff
293293
.ruff_cache/
294294

295-
bundled/libs
295+
bundled/
296296

297297
# robotframework
298298
results/

.vscode/launch.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
// "suites",
5151
// // "discover", "tests", "--tags"
5252
// "."
53-
"language-server"
53+
"discover",
54+
"files",
55+
".."
5456
]
5557
},
5658
{

docs/04_contribute/index.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Contribute to RobotCode
2+
3+
RobotCode is an open-source project driven by the passion and dedication of its community. We welcome contributions in any form—whether you're a developer, tester, writer, or simply a fan of the project, your help makes a difference!
4+
5+
## How You Can Contribute
6+
7+
### 1. Code Contributions
8+
- **Feature Development:** Help us implement new features and improve existing ones.
9+
- **Bug Fixes:** Identify and fix bugs to make RobotCode more robust.
10+
- **Code Reviews:** Review pull requests and provide feedback to ensure code quality.
11+
12+
### 2. Documentation
13+
- **Writing Guides:** Contribute to the documentation by writing or improving guides, tutorials, and examples.
14+
- **Translating:** Help us translate the documentation to make RobotCode accessible to a wider audience.
15+
- **Improving Existing Docs:** Enhance the clarity, accuracy, and completeness of our current documentation.
16+
17+
### 3. Testing
18+
- **Manual Testing:** Test new features and report issues.
19+
- **Automated Testing:** Write and maintain test scripts to ensure continuous quality.
20+
- **User Feedback:** Provide feedback on your experience using RobotCode.
21+
22+
### 4. Spread the Word
23+
- **Social Media:** Share RobotCode on social media platforms to increase its visibility.
24+
- **Blog Posts:** Write articles or tutorials about how you use RobotCode.
25+
- **Talks and Meetups:** Present RobotCode at local meetups or conferences.
26+
27+
### 5. Donations
28+
If you enjoy using RobotCode and would like to support its development, consider making a donation. Your contributions help us cover costs like hosting, development tools, and more.
29+
30+
- **GitHub Sponsors:** [Sponsor us on GitHub](https://github.com/sponsors/robotcodedev) to support ongoing development.
31+
- **PayPal:** Make a one-time donation via PayPal.
32+
- **Patreon:** Become a monthly supporter on Patreon and gain exclusive benefits.
33+
34+
## Get Involved
35+
36+
Join our community and start contributing today! Whether you’re coding, writing, testing, or donating, every bit helps RobotCode grow and improve.
37+
38+
- **GitHub:** [Check out our repository](https://github.com/robotcodedev/robotcode) and get started with your first contribution.
39+
- **Discord:** Join our [Discord community](https://discord.gg/robotcode) to connect with other contributors and stay updated on the latest news.
40+
41+
Thank you for your support and for being a part of the RobotCode community!

packages/core/src/robotcode/core/ignore_spec.py

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import re
33
import sys
44
from pathlib import Path, PurePath
5-
from typing import Dict, Iterable, Iterator, NamedTuple, Optional, Reversible, Tuple
5+
from typing import Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Reversible, Tuple, Union
6+
7+
from robotcode.core.utils.path import path_is_relative_to
68

79
_SEPARATORS = ["/"]
810
_SEPARATORS_GROUP = f"[{'|'.join(_SEPARATORS)}]"
@@ -280,32 +282,92 @@ def _is_hidden(entry: Path) -> bool:
280282

281283

282284
def iter_files(
283-
root: Path,
285+
paths: Union[Path, Iterable[Path]],
286+
root: Optional[Path] = None,
287+
ignore_files: Iterable[str] = [GIT_IGNORE_FILE],
288+
include_hidden: bool = True,
289+
parent_spec: Optional[IgnoreSpec] = None,
290+
verbose_callback: Optional[Callable[[str], None]] = None,
291+
) -> Iterator[Path]:
292+
if isinstance(paths, Path):
293+
paths = [paths]
294+
295+
for path in paths:
296+
yield from _iter_files(
297+
Path(os.path.abspath(path)),
298+
root=Path(os.path.abspath(root)) if root is not None else root,
299+
ignore_files=ignore_files,
300+
include_hidden=include_hidden,
301+
parent_spec=parent_spec,
302+
verbose_callback=verbose_callback,
303+
)
304+
305+
306+
def _iter_files(
307+
path: Path,
308+
root: Optional[Path] = None,
284309
ignore_files: Iterable[str] = [GIT_IGNORE_FILE],
285310
include_hidden: bool = True,
286311
parent_spec: Optional[IgnoreSpec] = None,
312+
verbose_callback: Optional[Callable[[str], None]] = None,
287313
) -> Iterator[Path]:
288314

315+
if root is None:
316+
root = path if path.is_dir() else path.parent
317+
289318
if parent_spec is None:
290-
parent_spec = IgnoreSpec.from_list(DEFAULT_SPEC_RULES, root)
319+
parent_spec = IgnoreSpec.from_list(DEFAULT_SPEC_RULES, path)
320+
321+
if path_is_relative_to(path, root):
322+
parents: List[Path] = []
323+
p = path if path.is_dir() else path.parent
324+
while True:
325+
p = p.parent
326+
327+
if p < root:
328+
break
329+
330+
parents.insert(0, p)
331+
332+
for p in parents:
333+
ignore_file = next((p / f for f in ignore_files if (p / f).is_file()), None)
334+
335+
if ignore_file is not None:
336+
if verbose_callback is not None:
337+
verbose_callback(f"using ignore file: '{ignore_file}'")
338+
parent_spec = parent_spec + IgnoreSpec.from_gitignore(ignore_file)
339+
ignore_files = [ignore_file.name]
291340

292-
ignore_file = next((root / f for f in ignore_files if (root / f).is_file()), None)
341+
ignore_file = next((path / f for f in ignore_files if (path / f).is_file()), None)
293342

294343
if ignore_file is not None:
295-
gitignore = parent_spec + IgnoreSpec.from_gitignore(root / ignore_file)
344+
if verbose_callback is not None:
345+
verbose_callback(f"using ignore file: '{ignore_file}'")
346+
spec = parent_spec + IgnoreSpec.from_gitignore(ignore_file)
296347
ignore_files = [ignore_file.name]
297348
else:
298-
gitignore = parent_spec
349+
spec = parent_spec
299350

300-
for path in root.iterdir():
351+
if not path.is_dir():
352+
if spec is not None and spec.matches(path):
353+
return
354+
yield path
355+
return
301356

302-
if not include_hidden and _is_hidden(path):
357+
for p in path.iterdir():
358+
if not include_hidden and _is_hidden(p):
303359
continue
304360

305-
if gitignore is not None and gitignore.matches(path):
361+
if spec is not None and spec.matches(p):
306362
continue
307363

308-
if path.is_dir():
309-
yield from iter_files(path, ignore_files, include_hidden, gitignore)
310-
elif path.is_file():
311-
yield path
364+
if p.is_dir():
365+
yield from _iter_files(
366+
p,
367+
ignore_files=ignore_files,
368+
include_hidden=include_hidden,
369+
parent_spec=spec,
370+
verbose_callback=verbose_callback,
371+
)
372+
elif p.is_file():
373+
yield p

packages/language_server/src/robotcode/language_server/robotframework/parts/robot_workspace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def load_workspace_documents(self, sender: Any) -> List[WorkspaceDocumentsResult
7373
lambda f: f.suffix in extensions,
7474
iter_files(
7575
folder.uri.to_path(),
76-
[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
76+
ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
7777
include_hidden=False,
7878
parent_spec=IgnoreSpec.from_list(
7979
[*DEFAULT_SPEC_RULES, *(config.workspace.exclude_patterns or [])],

packages/plugin/src/robotcode/plugin/__init__.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,18 +266,21 @@ def echo_via_pager(
266266
text_or_generator: Union[Iterable[str], Callable[[], Iterable[str]], str],
267267
color: Optional[bool] = None,
268268
) -> None:
269-
if not self.config.pager:
270-
text = (
271-
text_or_generator
272-
if isinstance(text_or_generator, str)
273-
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
274-
)
275-
click.echo(text, color=color if color is not None else self.colored)
276-
else:
277-
click.echo_via_pager(
278-
text_or_generator,
279-
color=color if color is not None else self.colored,
280-
)
269+
try:
270+
if not self.config.pager:
271+
text = (
272+
text_or_generator
273+
if isinstance(text_or_generator, str)
274+
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
275+
)
276+
click.echo(text, color=color if color is not None else self.colored)
277+
else:
278+
click.echo_via_pager(
279+
text_or_generator,
280+
color=color if color is not None else self.colored,
281+
)
282+
except OSError:
283+
pass
281284

282285
def keyboard_interrupt(self) -> None:
283286
self.verbose("Aborted!", file=sys.stderr)

packages/runner/src/robotcode/runner/cli/discover/discover.py

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from robot.utils import NormalizedDict, normalize
3030
from robot.utils.filereader import FileReader
3131

32+
from robotcode.core.ignore_spec import GIT_IGNORE_FILE, ROBOT_IGNORE_FILE, iter_files
3233
from robotcode.core.lsp.types import (
3334
Diagnostic,
3435
DiagnosticSeverity,
@@ -594,7 +595,7 @@ def all(
594595
def print(item: TestItem, indent: int = 0) -> Iterable[str]:
595596
type = click.style(
596597
item.type.capitalize() if item.type == "suite" else tests_or_tasks.capitalize(),
597-
fg="blue",
598+
fg="green",
598599
)
599600

600601
if item.type == "test":
@@ -617,10 +618,13 @@ def print(item: TestItem, indent: int = 0) -> Iterable[str]:
617618

618619
if indent == 0:
619620
yield os.linesep
620-
yield f"Summary:{os.linesep}"
621-
yield f" Suites: {collector.statistics.suites}{os.linesep}"
622-
yield f" Suites with {tests_or_tasks}: {collector.statistics.suites_with_tests}{os.linesep}"
623-
yield f" {tests_or_tasks}: {collector.statistics.tests}{os.linesep}"
621+
622+
yield click.style("Suites: ", underline=True, bold=True, fg="blue")
623+
yield f"{collector.statistics.suites}{os.linesep}"
624+
yield click.style(f"Suites with {tests_or_tasks}: ", underline=True, bold=True, fg="blue")
625+
yield f"{collector.statistics.suites_with_tests}{os.linesep}"
626+
yield click.style(f"{tests_or_tasks}: ", underline=True, bold=True, fg="blue")
627+
yield f"{collector.statistics.tests}{os.linesep}"
624628

625629
app.echo_via_pager(print(collector.all.children[0]))
626630

@@ -912,3 +916,78 @@ def info(app: Application) -> None:
912916
# app.print_data(info, remove_defaults=True)
913917
else:
914918
app.print_data(info, remove_defaults=True)
919+
920+
921+
@discover.command(add_help_option=True)
922+
@click.option(
923+
"--full-paths / --no-full-paths",
924+
"full_paths",
925+
default=False,
926+
show_default=True,
927+
help="Show full paths instead of releative.",
928+
)
929+
@click.argument(
930+
"paths",
931+
nargs=-1,
932+
type=click.Path(exists=True, file_okay=True, dir_okay=True),
933+
)
934+
@pass_application
935+
def files(app: Application, full_paths: bool, paths: Iterable[Path]) -> None:
936+
"""\
937+
Shows all files that are used to discover the tests.
938+
939+
Note: At the moment only `.robot` and `.resource` files are shown.
940+
\b
941+
Examples:
942+
```
943+
robotcode discover files .
944+
```
945+
"""
946+
947+
root_folder, profile, cmd_options = handle_robot_options(app, ())
948+
949+
search_paths = set(
950+
(
951+
(
952+
[*(app.config.default_paths if app.config.default_paths else ())]
953+
if profile.paths is None
954+
else profile.paths if isinstance(profile.paths, list) else [profile.paths]
955+
)
956+
if not paths
957+
else [str(p) for p in paths]
958+
)
959+
)
960+
if not search_paths:
961+
raise click.UsageError("Expected at least 1 argument.")
962+
963+
def filter_extensions(p: Path) -> bool:
964+
return p.suffix in [".robot", ".resource"]
965+
966+
result: List[str] = list(
967+
map(
968+
lambda p: os.path.abspath(p) if full_paths else (get_rel_source(str(p)) or str(p)),
969+
filter(
970+
filter_extensions,
971+
iter_files(
972+
(Path(s) for s in search_paths),
973+
root=root_folder,
974+
ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
975+
include_hidden=False,
976+
verbose_callback=app.verbose,
977+
),
978+
),
979+
)
980+
)
981+
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:
982+
983+
def print() -> Iterable[str]:
984+
for p in result:
985+
yield f"{p}{os.linesep}"
986+
987+
yield os.linesep
988+
yield click.style("Total: ", underline=True, bold=True, fg="blue")
989+
yield click.style(f"{len(result)} file(s){os.linesep}")
990+
991+
app.echo_via_pager(print())
992+
else:
993+
app.print_data(result, remove_defaults=True)

packages/runner/src/robotcode/runner/cli/robot.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ def _is_ignored(builder: SuiteStructureBuilder, path: Path) -> bool:
9898
if not path_is_relative_to(dir, cache_data.base_path):
9999
break
100100
else:
101-
# TODO: we are in a different folder
102101
if curr_dir.parent in cache_data.data:
103102
parent_data = cache_data.data[curr_dir.parent]
104103
parent_spec_dir = curr_dir.parent
@@ -121,7 +120,10 @@ def _is_ignored(builder: SuiteStructureBuilder, path: Path) -> bool:
121120
if ignore_file is not None:
122121
parent_data.ignore_files = [ignore_file.name]
123122

124-
parent_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(parent_spec_dir / ignore_file)
123+
if _app is not None:
124+
_app.verbose(f"using ignore file: '{ignore_file}'")
125+
126+
parent_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(ignore_file)
125127
cache_data.data[parent_spec_dir] = parent_data
126128

127129
if parent_data is not None and parent_data.spec is not None and parent_spec_dir != curr_dir:
@@ -130,7 +132,10 @@ def _is_ignored(builder: SuiteStructureBuilder, path: Path) -> bool:
130132
if ignore_file is not None:
131133
curr_data = BuilderCacheData()
132134

133-
curr_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(curr_dir / ignore_file)
135+
if _app is not None:
136+
_app.verbose(f"using ignore file: '{ignore_file}'")
137+
138+
curr_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(ignore_file)
134139
curr_data.ignore_files = [ignore_file.name]
135140

136141
cache_data.data[curr_dir] = curr_data

0 commit comments

Comments
 (0)