Skip to content

Commit

Permalink
feat(cli): new command discover files
Browse files Browse the repository at this point in the history
find all relevant robot framework files in a project.
  • Loading branch information
d-biehl committed Aug 29, 2024
1 parent 9e2d6f7 commit 505f473
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .robotignore
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ report.html
# ruff
.ruff_cache/

bundled/libs
bundled/

# robotframework
results/
Expand Down
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
// "suites",
// // "discover", "tests", "--tags"
// "."
"language-server"
"discover",
"files",
".."
]
},
{
Expand Down
41 changes: 41 additions & 0 deletions docs/04_contribute/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Contribute to RobotCode

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!

## How You Can Contribute

### 1. Code Contributions
- **Feature Development:** Help us implement new features and improve existing ones.
- **Bug Fixes:** Identify and fix bugs to make RobotCode more robust.
- **Code Reviews:** Review pull requests and provide feedback to ensure code quality.

### 2. Documentation
- **Writing Guides:** Contribute to the documentation by writing or improving guides, tutorials, and examples.
- **Translating:** Help us translate the documentation to make RobotCode accessible to a wider audience.
- **Improving Existing Docs:** Enhance the clarity, accuracy, and completeness of our current documentation.

### 3. Testing
- **Manual Testing:** Test new features and report issues.
- **Automated Testing:** Write and maintain test scripts to ensure continuous quality.
- **User Feedback:** Provide feedback on your experience using RobotCode.

### 4. Spread the Word
- **Social Media:** Share RobotCode on social media platforms to increase its visibility.
- **Blog Posts:** Write articles or tutorials about how you use RobotCode.
- **Talks and Meetups:** Present RobotCode at local meetups or conferences.

### 5. Donations
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.

- **GitHub Sponsors:** [Sponsor us on GitHub](https://github.com/sponsors/robotcodedev) to support ongoing development.
- **PayPal:** Make a one-time donation via PayPal.
- **Patreon:** Become a monthly supporter on Patreon and gain exclusive benefits.

## Get Involved

Join our community and start contributing today! Whether you’re coding, writing, testing, or donating, every bit helps RobotCode grow and improve.

- **GitHub:** [Check out our repository](https://github.com/robotcodedev/robotcode) and get started with your first contribution.
- **Discord:** Join our [Discord community](https://discord.gg/robotcode) to connect with other contributors and stay updated on the latest news.

Thank you for your support and for being a part of the RobotCode community!
88 changes: 75 additions & 13 deletions packages/core/src/robotcode/core/ignore_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import re
import sys
from pathlib import Path, PurePath
from typing import Dict, Iterable, Iterator, NamedTuple, Optional, Reversible, Tuple
from typing import Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Reversible, Tuple, Union

from robotcode.core.utils.path import path_is_relative_to

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


def iter_files(
root: Path,
paths: Union[Path, Iterable[Path]],
root: Optional[Path] = None,
ignore_files: Iterable[str] = [GIT_IGNORE_FILE],
include_hidden: bool = True,
parent_spec: Optional[IgnoreSpec] = None,
verbose_callback: Optional[Callable[[str], None]] = None,
) -> Iterator[Path]:
if isinstance(paths, Path):
paths = [paths]

for path in paths:
yield from _iter_files(
Path(os.path.abspath(path)),
root=Path(os.path.abspath(root)) if root is not None else root,
ignore_files=ignore_files,
include_hidden=include_hidden,
parent_spec=parent_spec,
verbose_callback=verbose_callback,
)


def _iter_files(
path: Path,
root: Optional[Path] = None,
ignore_files: Iterable[str] = [GIT_IGNORE_FILE],
include_hidden: bool = True,
parent_spec: Optional[IgnoreSpec] = None,
verbose_callback: Optional[Callable[[str], None]] = None,
) -> Iterator[Path]:

if root is None:
root = path if path.is_dir() else path.parent

if parent_spec is None:
parent_spec = IgnoreSpec.from_list(DEFAULT_SPEC_RULES, root)
parent_spec = IgnoreSpec.from_list(DEFAULT_SPEC_RULES, path)

if path_is_relative_to(path, root):
parents: List[Path] = []
p = path if path.is_dir() else path.parent
while True:
p = p.parent

if p < root:
break

parents.insert(0, p)

for p in parents:
ignore_file = next((p / f for f in ignore_files if (p / f).is_file()), None)

if ignore_file is not None:
if verbose_callback is not None:
verbose_callback(f"using ignore file: '{ignore_file}'")
parent_spec = parent_spec + IgnoreSpec.from_gitignore(ignore_file)
ignore_files = [ignore_file.name]

ignore_file = next((root / f for f in ignore_files if (root / f).is_file()), None)
ignore_file = next((path / f for f in ignore_files if (path / f).is_file()), None)

if ignore_file is not None:
gitignore = parent_spec + IgnoreSpec.from_gitignore(root / ignore_file)
if verbose_callback is not None:
verbose_callback(f"using ignore file: '{ignore_file}'")
spec = parent_spec + IgnoreSpec.from_gitignore(ignore_file)
ignore_files = [ignore_file.name]
else:
gitignore = parent_spec
spec = parent_spec

for path in root.iterdir():
if not path.is_dir():
if spec is not None and spec.matches(path):
return
yield path
return

if not include_hidden and _is_hidden(path):
for p in path.iterdir():
if not include_hidden and _is_hidden(p):
continue

if gitignore is not None and gitignore.matches(path):
if spec is not None and spec.matches(p):
continue

if path.is_dir():
yield from iter_files(path, ignore_files, include_hidden, gitignore)
elif path.is_file():
yield path
if p.is_dir():
yield from _iter_files(
p,
ignore_files=ignore_files,
include_hidden=include_hidden,
parent_spec=spec,
verbose_callback=verbose_callback,
)
elif p.is_file():
yield p
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def load_workspace_documents(self, sender: Any) -> List[WorkspaceDocumentsResult
lambda f: f.suffix in extensions,
iter_files(
folder.uri.to_path(),
[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
include_hidden=False,
parent_spec=IgnoreSpec.from_list(
[*DEFAULT_SPEC_RULES, *(config.workspace.exclude_patterns or [])],
Expand Down
27 changes: 15 additions & 12 deletions packages/plugin/src/robotcode/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,21 @@ def echo_via_pager(
text_or_generator: Union[Iterable[str], Callable[[], Iterable[str]], str],
color: Optional[bool] = None,
) -> None:
if not self.config.pager:
text = (
text_or_generator
if isinstance(text_or_generator, str)
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
)
click.echo(text, color=color if color is not None else self.colored)
else:
click.echo_via_pager(
text_or_generator,
color=color if color is not None else self.colored,
)
try:
if not self.config.pager:
text = (
text_or_generator
if isinstance(text_or_generator, str)
else "".join(text_or_generator() if callable(text_or_generator) else text_or_generator)
)
click.echo(text, color=color if color is not None else self.colored)
else:
click.echo_via_pager(
text_or_generator,
color=color if color is not None else self.colored,
)
except OSError:
pass

def keyboard_interrupt(self) -> None:
self.verbose("Aborted!", file=sys.stderr)
Expand Down
89 changes: 84 additions & 5 deletions packages/runner/src/robotcode/runner/cli/discover/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from robot.utils import NormalizedDict, normalize
from robot.utils.filereader import FileReader

from robotcode.core.ignore_spec import GIT_IGNORE_FILE, ROBOT_IGNORE_FILE, iter_files
from robotcode.core.lsp.types import (
Diagnostic,
DiagnosticSeverity,
Expand Down Expand Up @@ -594,7 +595,7 @@ def all(
def print(item: TestItem, indent: int = 0) -> Iterable[str]:
type = click.style(
item.type.capitalize() if item.type == "suite" else tests_or_tasks.capitalize(),
fg="blue",
fg="green",
)

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

if indent == 0:
yield os.linesep
yield f"Summary:{os.linesep}"
yield f" Suites: {collector.statistics.suites}{os.linesep}"
yield f" Suites with {tests_or_tasks}: {collector.statistics.suites_with_tests}{os.linesep}"
yield f" {tests_or_tasks}: {collector.statistics.tests}{os.linesep}"

yield click.style("Suites: ", underline=True, bold=True, fg="blue")
yield f"{collector.statistics.suites}{os.linesep}"
yield click.style(f"Suites with {tests_or_tasks}: ", underline=True, bold=True, fg="blue")
yield f"{collector.statistics.suites_with_tests}{os.linesep}"
yield click.style(f"{tests_or_tasks}: ", underline=True, bold=True, fg="blue")
yield f"{collector.statistics.tests}{os.linesep}"

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

Expand Down Expand Up @@ -912,3 +916,78 @@ def info(app: Application) -> None:
# app.print_data(info, remove_defaults=True)
else:
app.print_data(info, remove_defaults=True)


@discover.command(add_help_option=True)
@click.option(
"--full-paths / --no-full-paths",
"full_paths",
default=False,
show_default=True,
help="Show full paths instead of releative.",
)
@click.argument(
"paths",
nargs=-1,
type=click.Path(exists=True, file_okay=True, dir_okay=True),
)
@pass_application
def files(app: Application, full_paths: bool, paths: Iterable[Path]) -> None:
"""\
Shows all files that are used to discover the tests.
Note: At the moment only `.robot` and `.resource` files are shown.
\b
Examples:
```
robotcode discover files .
```
"""

root_folder, profile, cmd_options = handle_robot_options(app, ())

search_paths = set(
(
(
[*(app.config.default_paths if app.config.default_paths else ())]
if profile.paths is None
else profile.paths if isinstance(profile.paths, list) else [profile.paths]
)
if not paths
else [str(p) for p in paths]
)
)
if not search_paths:
raise click.UsageError("Expected at least 1 argument.")

def filter_extensions(p: Path) -> bool:
return p.suffix in [".robot", ".resource"]

result: List[str] = list(
map(
lambda p: os.path.abspath(p) if full_paths else (get_rel_source(str(p)) or str(p)),
filter(
filter_extensions,
iter_files(
(Path(s) for s in search_paths),
root=root_folder,
ignore_files=[ROBOT_IGNORE_FILE, GIT_IGNORE_FILE],
include_hidden=False,
verbose_callback=app.verbose,
),
),
)
)
if app.config.output_format is None or app.config.output_format == OutputFormat.TEXT:

def print() -> Iterable[str]:
for p in result:
yield f"{p}{os.linesep}"

yield os.linesep
yield click.style("Total: ", underline=True, bold=True, fg="blue")
yield click.style(f"{len(result)} file(s){os.linesep}")

app.echo_via_pager(print())
else:
app.print_data(result, remove_defaults=True)
11 changes: 8 additions & 3 deletions packages/runner/src/robotcode/runner/cli/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def _is_ignored(builder: SuiteStructureBuilder, path: Path) -> bool:
if not path_is_relative_to(dir, cache_data.base_path):
break
else:
# TODO: we are in a different folder
if curr_dir.parent in cache_data.data:
parent_data = cache_data.data[curr_dir.parent]
parent_spec_dir = curr_dir.parent
Expand All @@ -121,7 +120,10 @@ def _is_ignored(builder: SuiteStructureBuilder, path: Path) -> bool:
if ignore_file is not None:
parent_data.ignore_files = [ignore_file.name]

parent_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(parent_spec_dir / ignore_file)
if _app is not None:
_app.verbose(f"using ignore file: '{ignore_file}'")

parent_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(ignore_file)
cache_data.data[parent_spec_dir] = parent_data

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

curr_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(curr_dir / ignore_file)
if _app is not None:
_app.verbose(f"using ignore file: '{ignore_file}'")

curr_data.spec = parent_data.spec + IgnoreSpec.from_gitignore(ignore_file)
curr_data.ignore_files = [ignore_file.name]

cache_data.data[curr_dir] = curr_data
Expand Down

0 comments on commit 505f473

Please sign in to comment.