behave-runner 0.1.0__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.
- behave_runner/__init__.py +5 -0
- behave_runner/__main__.py +14 -0
- behave_runner/cli/__init__.py +0 -0
- behave_runner/cli/app.py +69 -0
- behave_runner/commands/__init__.py +0 -0
- behave_runner/commands/config_cmd.py +173 -0
- behave_runner/commands/doctor.py +30 -0
- behave_runner/commands/format_cmd.py +41 -0
- behave_runner/commands/generate.py +53 -0
- behave_runner/commands/impact.py +50 -0
- behave_runner/commands/init.py +37 -0
- behave_runner/commands/lint.py +30 -0
- behave_runner/commands/list_cmd.py +76 -0
- behave_runner/commands/open_cmd.py +65 -0
- behave_runner/commands/record.py +83 -0
- behave_runner/commands/report.py +90 -0
- behave_runner/commands/run.py +131 -0
- behave_runner/commands/select.py +110 -0
- behave_runner/commands/steps.py +59 -0
- behave_runner/commands/trace.py +50 -0
- behave_runner/commands/watch.py +82 -0
- behave_runner/core/__init__.py +0 -0
- behave_runner/core/config.py +39 -0
- behave_runner/core/deps.py +29 -0
- behave_runner/core/orchestrator.py +260 -0
- behave_runner/core/output.py +34 -0
- behave_runner/core/watcher.py +64 -0
- behave_runner/exceptions.py +23 -0
- behave_runner/py.typed +0 -0
- behave_runner/utils.py +26 -0
- behave_runner-0.1.0.dist-info/METADATA +178 -0
- behave_runner-0.1.0.dist-info/RECORD +35 -0
- behave_runner-0.1.0.dist-info/WHEEL +4 -0
- behave_runner-0.1.0.dist-info/entry_points.txt +2 -0
- behave_runner-0.1.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
behave_runner/cli/app.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CLI app for behave-runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from behave_runner import __version__
|
|
8
|
+
from behave_runner.commands.config_cmd import config_app
|
|
9
|
+
from behave_runner.commands.doctor import doctor_command
|
|
10
|
+
from behave_runner.commands.format_cmd import format_command
|
|
11
|
+
from behave_runner.commands.generate import generate_app
|
|
12
|
+
from behave_runner.commands.impact import impact_command
|
|
13
|
+
from behave_runner.commands.init import init_command
|
|
14
|
+
from behave_runner.commands.lint import lint_command
|
|
15
|
+
from behave_runner.commands.list_cmd import list_command
|
|
16
|
+
from behave_runner.commands.open_cmd import open_command
|
|
17
|
+
from behave_runner.commands.record import record_command
|
|
18
|
+
from behave_runner.commands.report import report_app
|
|
19
|
+
from behave_runner.commands.run import run_command
|
|
20
|
+
from behave_runner.commands.select import select_command
|
|
21
|
+
from behave_runner.commands.steps import steps_app
|
|
22
|
+
from behave_runner.commands.trace import trace_app
|
|
23
|
+
from behave_runner.commands.watch import watch_command
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="behave-runner",
|
|
27
|
+
help="CLI completa para Behave con subcomandos.",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
rich_markup_mode="rich",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def version_callback(value: bool) -> None:
|
|
34
|
+
if value:
|
|
35
|
+
print(__version__)
|
|
36
|
+
raise typer.Exit
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.callback()
|
|
40
|
+
def main(
|
|
41
|
+
version: bool = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--version",
|
|
44
|
+
callback=version_callback,
|
|
45
|
+
is_eager=True,
|
|
46
|
+
help="Show version and exit.",
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""behave-runner — CLI completa para Behave con subcomandos."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
app.command(name="run")(run_command)
|
|
54
|
+
app.command(name="select")(select_command)
|
|
55
|
+
app.command(name="list")(list_command)
|
|
56
|
+
app.command(name="format")(format_command)
|
|
57
|
+
app.command(name="lint")(lint_command)
|
|
58
|
+
app.command(name="doctor")(doctor_command)
|
|
59
|
+
app.command(name="init")(init_command)
|
|
60
|
+
app.add_typer(report_app, name="report")
|
|
61
|
+
app.add_typer(generate_app, name="generate")
|
|
62
|
+
app.add_typer(trace_app, name="trace")
|
|
63
|
+
app.command(name="watch")(watch_command)
|
|
64
|
+
app.command(name="impact")(impact_command)
|
|
65
|
+
app.add_typer(steps_app, name="steps")
|
|
66
|
+
app.command(name="impact")(impact_command)
|
|
67
|
+
app.command(name="record")(record_command)
|
|
68
|
+
app.command(name="open")(open_command)
|
|
69
|
+
app.add_typer(config_app, name="config")
|
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Config command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from behave_runner.core.config import load_config
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
config_app = typer.Typer(
|
|
18
|
+
name="config",
|
|
19
|
+
help="Manage behave-runner configuration.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _find_pyproject() -> Path:
|
|
25
|
+
"""Find pyproject.toml in the current directory."""
|
|
26
|
+
pyproject = Path.cwd() / "pyproject.toml"
|
|
27
|
+
if not pyproject.exists():
|
|
28
|
+
console.print("[red]Error: pyproject.toml not found in current directory.[/red]")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
return pyproject
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_toml(path: Path) -> dict[str, Any]:
|
|
34
|
+
"""Read a TOML file and return its contents."""
|
|
35
|
+
with path.open("rb") as f:
|
|
36
|
+
return tomllib.load(f)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_toml_section(path: Path, data: dict[str, Any]) -> None:
|
|
40
|
+
"""Append or update [tool.behave-runner] section preserving the rest."""
|
|
41
|
+
content = path.read_text(encoding="utf-8")
|
|
42
|
+
section_header = "[tool.behave-runner]"
|
|
43
|
+
|
|
44
|
+
if section_header in content:
|
|
45
|
+
console.print("[yellow]Section [tool.behave-runner] already exists.[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
lines = content.splitlines(keepends=True)
|
|
49
|
+
new_lines: list[str] = []
|
|
50
|
+
inserted = False
|
|
51
|
+
|
|
52
|
+
for line in lines:
|
|
53
|
+
new_lines.append(line)
|
|
54
|
+
if not inserted and line.strip().startswith("[tool."):
|
|
55
|
+
new_lines.append(f"\n{section_header}\n")
|
|
56
|
+
for key, value in data.items():
|
|
57
|
+
new_lines.append(f"{key} = {_format_value(value)}\n")
|
|
58
|
+
inserted = True
|
|
59
|
+
|
|
60
|
+
if not inserted:
|
|
61
|
+
if new_lines and not new_lines[-1].endswith("\n"):
|
|
62
|
+
new_lines[-1] += "\n"
|
|
63
|
+
new_lines.append(f"\n{section_header}\n")
|
|
64
|
+
for key, value in data.items():
|
|
65
|
+
new_lines.append(f"{key} = {_format_value(value)}\n")
|
|
66
|
+
|
|
67
|
+
path.write_text("".join(new_lines), encoding="utf-8")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _set_config_value(path: Path, key: str, value: str) -> None:
|
|
71
|
+
"""Set a key in the [tool.behave-runner] section."""
|
|
72
|
+
content = path.read_text(encoding="utf-8")
|
|
73
|
+
section_header = "[tool.behave-runner]"
|
|
74
|
+
|
|
75
|
+
if section_header not in content:
|
|
76
|
+
_write_toml_section(path, {key: _parse_value(value)})
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
lines = content.splitlines(keepends=True)
|
|
80
|
+
new_lines: list[str] = []
|
|
81
|
+
in_section = False
|
|
82
|
+
key_found = False
|
|
83
|
+
|
|
84
|
+
for line in lines:
|
|
85
|
+
stripped = line.strip()
|
|
86
|
+
|
|
87
|
+
if stripped == section_header:
|
|
88
|
+
in_section = True
|
|
89
|
+
new_lines.append(line)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if in_section and stripped.startswith("[") and stripped != section_header:
|
|
93
|
+
if not key_found:
|
|
94
|
+
new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
|
|
95
|
+
key_found = True
|
|
96
|
+
in_section = False
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
in_section
|
|
100
|
+
and stripped.startswith(f"{key} ")
|
|
101
|
+
or (in_section and stripped.startswith(f"{key}="))
|
|
102
|
+
):
|
|
103
|
+
new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
|
|
104
|
+
key_found = True
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
new_lines.append(line)
|
|
108
|
+
|
|
109
|
+
if in_section and not key_found:
|
|
110
|
+
new_lines.append(f"{key} = {_format_value(_parse_value(value))}\n")
|
|
111
|
+
|
|
112
|
+
path.write_text("".join(new_lines), encoding="utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _format_value(value: Any) -> str:
|
|
116
|
+
"""Format a Python value as a TOML literal."""
|
|
117
|
+
if isinstance(value, bool):
|
|
118
|
+
return "true" if value else "false"
|
|
119
|
+
if isinstance(value, int):
|
|
120
|
+
return str(value)
|
|
121
|
+
if isinstance(value, float):
|
|
122
|
+
return str(value)
|
|
123
|
+
return f'"{value}"'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_value(value: str) -> Any:
|
|
127
|
+
"""Parse a string value into a Python type for TOML."""
|
|
128
|
+
if value.lower() in ("true", "false"):
|
|
129
|
+
return value.lower() == "true"
|
|
130
|
+
try:
|
|
131
|
+
return int(value)
|
|
132
|
+
except ValueError:
|
|
133
|
+
pass
|
|
134
|
+
try:
|
|
135
|
+
return float(value)
|
|
136
|
+
except ValueError:
|
|
137
|
+
pass
|
|
138
|
+
return value.strip("\"'")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@config_app.command(name="show")
|
|
142
|
+
def config_show() -> None:
|
|
143
|
+
"""Show current behave-runner configuration."""
|
|
144
|
+
config = load_config()
|
|
145
|
+
if not config:
|
|
146
|
+
console.print("[yellow]No [tool.behave-runner] configuration found.[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
table = Table(title="behave-runner configuration")
|
|
150
|
+
table.add_column("Key", style="cyan")
|
|
151
|
+
table.add_column("Value", style="green")
|
|
152
|
+
for key, value in sorted(config.items()):
|
|
153
|
+
table.add_row(key, str(value))
|
|
154
|
+
console.print(table)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@config_app.command(name="init")
|
|
158
|
+
def config_init() -> None:
|
|
159
|
+
"""Create [tool.behave-runner] section in pyproject.toml."""
|
|
160
|
+
pyproject = _find_pyproject()
|
|
161
|
+
_write_toml_section(pyproject, {})
|
|
162
|
+
console.print("[green]Created [tool.behave-runner] section in pyproject.toml[/green]")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@config_app.command(name="set")
|
|
166
|
+
def config_set(
|
|
167
|
+
key: str = typer.Argument(..., help="Configuration key to set."),
|
|
168
|
+
value: str = typer.Argument(..., help="Value to assign."),
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Set a configuration value in [tool.behave-runner]."""
|
|
171
|
+
pyproject = _find_pyproject()
|
|
172
|
+
_set_config_value(pyproject, key, value)
|
|
173
|
+
console.print(f"[green]Set {key} = {value}[/green]")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Doctor command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def doctor_command(
|
|
16
|
+
args: list[str] = typer.Argument(None, help="Arguments to pass to behave-doctor."),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Run behave-doctor to diagnose and fix common issues."""
|
|
19
|
+
if not check_optional("doctor", "behave_doctor", "doctor"):
|
|
20
|
+
raise typer.Exit(2)
|
|
21
|
+
|
|
22
|
+
cmd = ["behave-doctor", *(args or [])]
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
25
|
+
raise typer.Exit(result.returncode)
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
console.print(
|
|
28
|
+
"[red]Error: behave-doctor not found. Install with: pip install behave-doctor[/red]"
|
|
29
|
+
)
|
|
30
|
+
raise typer.Exit(2) from None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Format command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_command(
|
|
16
|
+
args: list[str] = typer.Argument(None, help="Arguments to pass to behave-format."),
|
|
17
|
+
check: bool = typer.Option(False, "--check", help="Check only, don't modify files."),
|
|
18
|
+
diff: bool = typer.Option(False, "--diff", help="Show diff of changes."),
|
|
19
|
+
in_place: bool = typer.Option(False, "--in-place", help="Modify files in place."),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Run behave-format to format feature files."""
|
|
22
|
+
if not check_optional("format", "behave_format", "format"):
|
|
23
|
+
raise typer.Exit(2)
|
|
24
|
+
|
|
25
|
+
cmd = ["behave-format"]
|
|
26
|
+
if check:
|
|
27
|
+
cmd.append("--check")
|
|
28
|
+
if diff:
|
|
29
|
+
cmd.append("--diff")
|
|
30
|
+
if in_place:
|
|
31
|
+
cmd.append("--in-place")
|
|
32
|
+
cmd.extend(args or [])
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
36
|
+
raise typer.Exit(result.returncode)
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
console.print(
|
|
39
|
+
"[red]Error: behave-format not found. Install with: pip install behave-format[/red]"
|
|
40
|
+
)
|
|
41
|
+
raise typer.Exit(2) from None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Generate command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
generate_app = typer.Typer(
|
|
15
|
+
name="generate",
|
|
16
|
+
help="Generate step definitions and feature files using behave-gen.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _run_behave_gen(cmd: list[str]) -> None:
|
|
22
|
+
"""Delegate to behave-gen via subprocess."""
|
|
23
|
+
if not check_optional("gen", "behave_gen", "generate"):
|
|
24
|
+
raise typer.Exit(2)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
28
|
+
raise typer.Exit(result.returncode)
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
console.print(
|
|
31
|
+
"[red]Error: behave-gen not found. Install with: pip install behave-gen[/red]"
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(2) from None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@generate_app.command(name="step")
|
|
37
|
+
def generate_step(
|
|
38
|
+
lib: str = typer.Option(..., "--lib", help="Step library name (e.g. http, auth)."),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Generate step definitions from a step library."""
|
|
41
|
+
_run_behave_gen(["behave-gen", "add", "steps", "--lib", lib])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@generate_app.command(name="feature")
|
|
45
|
+
def generate_feature(
|
|
46
|
+
name: str = typer.Argument(..., help="Feature name (without extension)."),
|
|
47
|
+
tags: str | None = typer.Option(None, "--tags", help="Comma or space separated tags."),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Generate a feature file skeleton."""
|
|
50
|
+
cmd = ["behave-gen", "add", "feature", name]
|
|
51
|
+
if tags:
|
|
52
|
+
cmd.extend(["--tags", tags])
|
|
53
|
+
_run_behave_gen(cmd)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Impact command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
from behave_runner.core.orchestrator import RunConfig, run
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def impact_command(
|
|
17
|
+
path: str = typer.Argument(".", help="Project root directory to analyze."),
|
|
18
|
+
fmt: str = typer.Option("text", "--format", help="Output format: text, json, sarif."),
|
|
19
|
+
run_affected: bool = typer.Option(
|
|
20
|
+
False, "--run", help="Run affected scenarios after detecting them."
|
|
21
|
+
),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Detect scenarios affected by code changes using behave-doctor."""
|
|
24
|
+
if not check_optional("doctor", "behave_doctor", "impact"):
|
|
25
|
+
raise typer.Exit(2)
|
|
26
|
+
|
|
27
|
+
cmd = ["behave-doctor", "scan", "--format", fmt, path]
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run( # noqa: S603
|
|
31
|
+
cmd, check=False, capture_output=run_affected, text=True
|
|
32
|
+
)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
console.print(
|
|
35
|
+
"[red]Error: behave-doctor not found. Install with: pip install behave-doctor[/red]"
|
|
36
|
+
)
|
|
37
|
+
raise typer.Exit(2) from None
|
|
38
|
+
|
|
39
|
+
if run_affected and result.stdout:
|
|
40
|
+
names = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
|
|
41
|
+
if names:
|
|
42
|
+
console.print(f"[cyan]Running {len(names)} affected scenarios...[/cyan]")
|
|
43
|
+
for name in names:
|
|
44
|
+
config = RunConfig(features=[path], name=[name])
|
|
45
|
+
run(config)
|
|
46
|
+
else:
|
|
47
|
+
console.print("[yellow]No affected scenarios to run.[/yellow]")
|
|
48
|
+
raise typer.Exit(result.returncode)
|
|
49
|
+
|
|
50
|
+
raise typer.Exit(result.returncode)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Init command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def init_command(
|
|
16
|
+
name: str | None = typer.Option(
|
|
17
|
+
None, "--name", help="Project name for the generated structure."
|
|
18
|
+
),
|
|
19
|
+
args: list[str] = typer.Argument(None, help="Additional arguments for behave-gen."),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize a new behave project structure using behave-gen."""
|
|
22
|
+
if not check_optional("gen", "behave_gen", "init"):
|
|
23
|
+
raise typer.Exit(2)
|
|
24
|
+
|
|
25
|
+
cmd = ["behave-gen", "init"]
|
|
26
|
+
if name:
|
|
27
|
+
cmd.extend(["--name", name])
|
|
28
|
+
cmd.extend(args or [])
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(cmd, check=False) # noqa: S603
|
|
32
|
+
raise typer.Exit(result.returncode)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
console.print(
|
|
35
|
+
"[red]Error: behave-gen not found. Install with: pip install behave-gen[/red]"
|
|
36
|
+
)
|
|
37
|
+
raise typer.Exit(2) from None
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Lint command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from behave_runner.core.deps import check_optional
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def lint_command(
|
|
16
|
+
args: list[str] = typer.Argument(None, help="Arguments to pass to behave-lint."),
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Run behave-lint static analysis on features."""
|
|
19
|
+
if not check_optional("lint", "behave_lint", "lint"):
|
|
20
|
+
raise typer.Exit(2)
|
|
21
|
+
|
|
22
|
+
cmd = [*args] if args else []
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(["behave-lint", *cmd], check=False) # noqa: S603
|
|
25
|
+
raise typer.Exit(result.returncode)
|
|
26
|
+
except FileNotFoundError:
|
|
27
|
+
console.print(
|
|
28
|
+
"[red]Error: behave-lint not found. Install with: pip install behave-lint[/red]"
|
|
29
|
+
)
|
|
30
|
+
raise typer.Exit(2) from None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""List command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from behave_model import load_feature
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _collect_scenarios(feature_paths: list[Path], tags: list[str]) -> list[dict[str, Any]]:
|
|
18
|
+
"""Parse feature files and collect matching scenarios."""
|
|
19
|
+
scenarios: list[dict[str, Any]] = []
|
|
20
|
+
for fp in feature_paths:
|
|
21
|
+
if fp.is_dir():
|
|
22
|
+
feature_files = sorted(fp.rglob("*.feature"))
|
|
23
|
+
elif fp.is_file() and fp.suffix == ".feature":
|
|
24
|
+
feature_files = [fp]
|
|
25
|
+
else:
|
|
26
|
+
continue
|
|
27
|
+
for ff in feature_files:
|
|
28
|
+
feature = load_feature(str(ff))
|
|
29
|
+
for scenario in feature.scenarios:
|
|
30
|
+
if _matches_tags(scenario.tag_names, tags):
|
|
31
|
+
scenarios.append(
|
|
32
|
+
{
|
|
33
|
+
"feature": feature.name,
|
|
34
|
+
"scenario": scenario.name,
|
|
35
|
+
"location": str(scenario.location),
|
|
36
|
+
"tags": list(scenario.tag_names),
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
return scenarios
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _matches_tags(scenario_tags: list[str], filter_tags: list[str]) -> bool:
|
|
43
|
+
"""Check if scenario matches all filter tags."""
|
|
44
|
+
if not filter_tags:
|
|
45
|
+
return True
|
|
46
|
+
return all(tag in scenario_tags for tag in filter_tags)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_command(
|
|
50
|
+
features: list[Path] = typer.Argument(
|
|
51
|
+
default=None,
|
|
52
|
+
help="Paths to feature files or directories.",
|
|
53
|
+
),
|
|
54
|
+
tags: list[str] = typer.Option([], "--tags", "-t", help="Filter by tags."),
|
|
55
|
+
fmt: str = typer.Option("text", "--format", help="Output format: text, json."),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""List scenarios without executing them."""
|
|
58
|
+
feature_paths = features if features else [Path("features")]
|
|
59
|
+
scenarios = _collect_scenarios(feature_paths, tags)
|
|
60
|
+
|
|
61
|
+
if fmt == "json":
|
|
62
|
+
print(json.dumps(scenarios, indent=2))
|
|
63
|
+
else:
|
|
64
|
+
table = Table(title="Scenarios")
|
|
65
|
+
table.add_column("Feature")
|
|
66
|
+
table.add_column("Scenario")
|
|
67
|
+
table.add_column("Location")
|
|
68
|
+
table.add_column("Tags")
|
|
69
|
+
for s in scenarios:
|
|
70
|
+
table.add_row(
|
|
71
|
+
s["feature"],
|
|
72
|
+
s["scenario"],
|
|
73
|
+
s["location"],
|
|
74
|
+
", ".join(s["tags"]),
|
|
75
|
+
)
|
|
76
|
+
console.print(table)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Open command for behave-runner CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from behave_runner.core.deps import check_optional
|
|
13
|
+
from behave_runner.core.output import find_latest_report
|
|
14
|
+
from behave_runner.utils import open_in_browser
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
TargetType = Literal["report", "trace"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def open_command(
|
|
22
|
+
target: TargetType = typer.Argument(
|
|
23
|
+
"report",
|
|
24
|
+
help="What to open: 'report' (default) or 'trace'.",
|
|
25
|
+
),
|
|
26
|
+
output: Path = typer.Option(
|
|
27
|
+
Path("reports"),
|
|
28
|
+
"--output",
|
|
29
|
+
help="Directory containing reports.",
|
|
30
|
+
),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Open the latest report or trace viewer in the browser."""
|
|
33
|
+
if target == "trace":
|
|
34
|
+
_open_trace()
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
_open_report(output)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _open_report(output: Path) -> None:
|
|
41
|
+
"""Find and open the latest report in the browser."""
|
|
42
|
+
report_file = find_latest_report(output)
|
|
43
|
+
if report_file is None:
|
|
44
|
+
console.print("[yellow]No reports found.[/yellow]")
|
|
45
|
+
return
|
|
46
|
+
console.print(f"[green]Opening: {report_file}[/green]")
|
|
47
|
+
open_in_browser(str(report_file.resolve()))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _open_trace() -> None:
|
|
51
|
+
"""Open the trace viewer via behave-trace."""
|
|
52
|
+
if not check_optional("trace", "behave_trace", "trace"):
|
|
53
|
+
raise typer.Exit(2)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["behave-trace", "show"],
|
|
58
|
+
check=False, # noqa: S603
|
|
59
|
+
)
|
|
60
|
+
raise typer.Exit(result.returncode)
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
console.print(
|
|
63
|
+
"[red]Error: behave-trace not found. Install with: pip install behave-trace[/red]"
|
|
64
|
+
)
|
|
65
|
+
raise typer.Exit(2) from None
|