Skip to content

Commit 88d46de

Browse files
committed
Refactor test file generation and validation logic
This refactor improves maintainability and user experience when generating AI-powered test files.
1 parent a12962d commit 88d46de

File tree

2 files changed

+118
-89
lines changed

2 files changed

+118
-89
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,4 @@ shiny_bookmarks/
123123

124124
# setuptools_scm
125125
shiny/_version.py
126+
shiny/testing/evaluation/apps/*/test_*.py

shiny/_main_generate_test.py

Lines changed: 117 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,145 @@
11
from __future__ import annotations
22

3+
import os
34
import sys
45
from pathlib import Path
6+
from typing import Callable
57

68
import click
79
import questionary
810

911
from ._main_utils import cli_action, cli_bold, cli_code, path_rel_wd
1012

1113

12-
def generate_test_file(
13-
*,
14-
app_file: str | None,
15-
output_file: str | None,
16-
provider: str,
17-
model: str | None,
18-
):
19-
"""Generate AI-powered test file for a Shiny app."""
20-
21-
if app_file is None:
22-
23-
def path_exists(x: str) -> bool | str:
24-
if not isinstance(x, (str, Path)):
25-
return False
26-
path = Path(x)
27-
if path.is_dir():
28-
return "Please provide a file path to your Shiny app"
29-
return path.exists() or f"Shiny app file can not be found: {x}"
30-
31-
app_file_val = questionary.path(
32-
"Enter the path to the app file:",
33-
default=path_rel_wd("app.py"),
34-
validate=path_exists,
35-
).ask()
36-
else:
37-
app_file_val = app_file
14+
class ValidationError(Exception):
15+
pass
16+
17+
18+
def create_file_validator(
19+
file_type: str,
20+
must_exist: bool = True,
21+
prefix_required: str | None = None,
22+
must_not_exist: bool = False,
23+
) -> Callable[[str], bool | str]:
24+
def validator(path_str: str) -> bool | str:
25+
if not isinstance(path_str, (str, Path)):
26+
return False
27+
28+
path = Path(path_str)
29+
30+
if path.is_dir():
31+
return f"Please provide a file path for your {file_type}"
32+
33+
if must_exist and not path.exists():
34+
return f"{file_type.title()} file not found: {path_str}"
35+
36+
if must_not_exist and path.exists():
37+
return f"{file_type.title()} file already exists. Please provide a new file name."
38+
39+
if prefix_required and not path.name.startswith(prefix_required):
40+
return f"{file_type.title()} file must start with '{prefix_required}'"
41+
42+
return True
43+
44+
return validator
45+
46+
47+
def validate_api_key(provider: str) -> None:
48+
api_configs = {
49+
"anthropic": {
50+
"env_var": "ANTHROPIC_API_KEY",
51+
"url": "https://console.anthropic.com/",
52+
},
53+
"openai": {
54+
"env_var": "OPENAI_API_KEY",
55+
"url": "https://platform.openai.com/api-keys",
56+
},
57+
}
58+
59+
if provider not in api_configs:
60+
raise ValidationError(f"Unsupported provider: {provider}")
61+
62+
config = api_configs[provider]
63+
if not os.getenv(config["env_var"]):
64+
raise ValidationError(
65+
f"{config['env_var']} environment variable is not set.\n"
66+
f"Please set your {provider.title()} API key:\n"
67+
f" export {config['env_var']}='your-api-key-here'\n\n"
68+
f"Get your API key from: {config['url']}"
69+
)
70+
71+
72+
def get_app_file_path(app_file: str | None) -> Path:
73+
if app_file is not None:
74+
app_path = Path(app_file)
75+
if not app_path.exists():
76+
raise ValidationError(f"App file does not exist: {app_path}")
77+
return app_path
78+
# Interactive mode
79+
app_file_val = questionary.path(
80+
"Enter the path to the app file:",
81+
default=path_rel_wd("app.py"),
82+
validate=create_file_validator("Shiny app", must_exist=True),
83+
).ask()
3884

3985
if app_file_val is None:
4086
sys.exit(1)
4187

42-
app_path = Path(app_file_val)
88+
return Path(app_file_val)
4389

44-
if not app_path.exists():
45-
click.echo(f"❌ Error: App file does not exist: {app_path}")
46-
sys.exit(1)
4790

48-
if output_file is None:
49-
suggested_output = app_path.parent / f"test_{app_path.stem}.py"
50-
51-
def output_path_valid(x: str) -> bool | str:
52-
if not isinstance(x, (str, Path)):
53-
return False
54-
path = Path(x)
55-
if path.is_dir():
56-
return "Please provide a file path for your test file."
57-
if path.exists():
58-
return "Test file already exists. Please provide a new file name."
59-
if not path.name.startswith("test_"):
60-
return "Test file must start with 'test_'"
61-
return True
62-
63-
output_file_val = questionary.path(
64-
"Enter the path for the generated test file:",
65-
default=str(suggested_output),
66-
validate=output_path_valid,
67-
).ask()
68-
else:
69-
output_file_val = output_file
91+
def get_output_file_path(output_file: str | None, app_path: Path) -> Path:
92+
if output_file is not None:
93+
output_path = Path(output_file)
94+
if output_path.exists():
95+
raise ValidationError(f"Test file already exists: {output_path}")
96+
if not output_path.name.startswith("test_"):
97+
raise ValidationError("Test file must start with 'test_'")
98+
return output_path
99+
# Interactive mode
100+
suggested_output = app_path.parent / f"test_{app_path.stem}.py"
101+
102+
output_file_val = questionary.path(
103+
"Enter the path for the generated test file:",
104+
default=str(suggested_output),
105+
validate=create_file_validator(
106+
"test", must_exist=False, prefix_required="test_", must_not_exist=True
107+
),
108+
).ask()
70109

71110
if output_file_val is None:
72111
sys.exit(1)
73112

74-
output_path = Path(output_file_val)
113+
return Path(output_file_val)
75114

76-
if output_path.exists():
77-
click.echo(f"❌ Error: Test file already exists: {output_path}")
78-
sys.exit(1)
79115

80-
if not output_path.name.startswith("test_"):
81-
click.echo("❌ Error: Test file must start with 'test_'")
82-
sys.exit(1)
116+
def generate_test_file(
117+
*,
118+
app_file: str | None,
119+
output_file: str | None,
120+
provider: str,
121+
model: str | None,
122+
) -> None:
83123

84124
try:
85-
from .testing import ShinyTestGenerator
86-
except ImportError as e:
87-
click.echo(f"❌ Error: Could not import ShinyTestGenerator: {e}")
88-
click.echo("Make sure the shiny testing dependencies are installed.")
89-
sys.exit(1)
125+
validate_api_key(provider)
90126

91-
import os
92-
93-
if provider == "anthropic":
94-
if not os.getenv("ANTHROPIC_API_KEY"):
95-
click.echo("❌ Error: ANTHROPIC_API_KEY environment variable is not set.")
96-
click.echo("Please set your Anthropic API key:")
97-
click.echo(" export ANTHROPIC_API_KEY='your-api-key-here'")
98-
click.echo()
99-
click.echo("Get your API key from: https://console.anthropic.com/")
100-
sys.exit(1)
101-
elif provider == "openai":
102-
if not os.getenv("OPENAI_API_KEY"):
103-
click.echo("❌ Error: OPENAI_API_KEY environment variable is not set.")
104-
click.echo("Please set your OpenAI API key:")
105-
click.echo(" export OPENAI_API_KEY='your-api-key-here'")
106-
click.echo()
107-
click.echo("Get your API key from: https://platform.openai.com/api-keys")
108-
sys.exit(1)
109-
110-
click.echo(f"🤖 Generating test using {provider} provider...")
111-
if model:
112-
click.echo(f"📝 Using model: {model}")
127+
app_path = get_app_file_path(app_file)
128+
output_path = get_output_file_path(output_file, app_path)
113129

114-
try:
115-
generator = ShinyTestGenerator(provider=provider) # type: ignore
130+
try:
131+
from .testing import ShinyTestGenerator
132+
except ImportError as e:
133+
raise ValidationError(
134+
f"Could not import ShinyTestGenerator: {e}\n"
135+
"Make sure the shiny testing dependencies are installed."
136+
)
137+
138+
click.echo(f"🤖 Generating test using {provider} provider...")
139+
if model:
140+
click.echo(f"📝 Using model: {model}")
116141

117-
# Generate the test
142+
generator = ShinyTestGenerator(provider=provider) # type: ignore
118143
_, test_file_path = generator.generate_test_from_file(
119144
app_file_path=str(app_path),
120145
model=model,
@@ -129,6 +154,9 @@ def output_path_valid(x: str) -> bool | str:
129154
)
130155
click.echo("- Review and customize the test as needed")
131156

157+
except ValidationError as e:
158+
click.echo(f"❌ Error: {e}")
159+
sys.exit(1)
132160
except Exception as e:
133161
click.echo(f"❌ Error generating test: {e}")
134162
sys.exit(1)

0 commit comments

Comments
 (0)