Skip to content

Commit c794101

Browse files
authoredAug 8, 2024··
Merge pull request #385 from Cloud-Code-AI/384-e2e-gen-add-retry-attempts-for-failed-tasks
384 e2e gen add retry attempts for failed tasks
2 parents f59f8e6 + 41c0386 commit c794101

File tree

14 files changed

+146
-31
lines changed

14 files changed

+146
-31
lines changed
 

‎README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Kaizen is an open-source AI-powered suite that revolutionizes your code quality
7575

7676
**Mac/Linux**
7777
```bash
78-
PYTHONPATH=. poetry run python examples/basic/generate.py
78+
PYTHONPATH=. poetry run python examples/e2e_test/generate.py
7979
```
8080

8181
**Windows**
@@ -89,7 +89,7 @@ Kaizen is an open-source AI-powered suite that revolutionizes your code quality
8989

9090
**Mac/Linux**
9191
```bash
92-
PYTHONPATH=. poetry run python examples/basic/execute.py
92+
PYTHONPATH=. poetry run python examples/e2e_test/execute.py
9393
```
9494

9595
**Windows**

‎cli/kaizen_cli/cli.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import click
22
import os
33
import json
4-
from kaizen.generator.ui import UITestGenerator
4+
from kaizen.generator.e2e_tests import E2ETestGenerator
55
from kaizen.generator.unit_test import UnitTestGenerator
66

77
CONFIG_FILE = os.path.expanduser("~/.myapp_config.json")
@@ -82,7 +82,7 @@ def run(obj, command, region):
8282
@click.argument("url", required=True)
8383
def ui_tests(url):
8484
"""Run ui test generation"""
85-
UITestGenerator().generate_ui_tests(url)
85+
E2ETestGenerator().generate_e2e_tests(url)
8686

8787

8888
@cli.command()
@@ -104,7 +104,7 @@ def reviewer():
104104
@click.argument("branch", required=True)
105105
def work(url):
106106
"""Run ui test generation"""
107-
UITestGenerator().generate_ui_tests(url)
107+
E2ETestGenerator().generate_e2e_tests(url)
108108

109109

110110
if __name__ == "__main__":

‎cli/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "kaizen-cli"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = ""
55
authors = ["Saurav Panda <sgp65@cornell.edu>"]
66
readme = "README.md"

‎docs/pages/features/e2e_testing.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The E2E UI Testing feature is designed to streamline the process of creating and
2727

2828
5. **Continuous Integration**: As the web application evolves, the feature can regenerate or update the test scripts to ensure they remain aligned with the latest changes and requirements.
2929

30-
You can find an example [here](https://github.com/Cloud-Code-AI/kaizen/tree/main/examples/basic)
30+
You can find an example [here](https://github.com/Cloud-Code-AI/kaizen/tree/main/examples/e2e_test)
3131

3232
### Benefits
3333

File renamed without changes.
File renamed without changes.

‎examples/basic/generate.py ‎examples/e2e_test/generate.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
from kaizen.generator.ui import UITestGenerator
1+
from kaizen.generator.e2e_tests import E2ETestGenerator
22
import time
33
import sys
4+
import traceback
45

5-
generator = UITestGenerator()
6+
generator = E2ETestGenerator()
67

78
WEBPAGE_URL = "https://cloudcode.ai"
89

910
print(f"Generating UI tests for `{WEBPAGE_URL}`, please wait...")
1011
start_time = time.time()
1112

1213
try:
13-
tests, _ = generator.generate_ui_tests(WEBPAGE_URL)
14+
tests, _ = generator.generate_e2e_tests(WEBPAGE_URL)
1415
except Exception as e:
1516
print(f"Error: {e}")
17+
print(traceback.format_exc())
1618
sys.exit(1)
1719

1820
end_time = time.time()
@@ -28,3 +30,6 @@
2830
print(f'Desc: {t["test_description"]}')
2931
print(f'Code: \n{t["code"]}')
3032
print("-----------------------------------------------------------")
33+
34+
results = generator.run_tests()
35+
print(f"Test Execution results: \n {results}")

‎examples/unittest/rust.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from kaizen.generator.unit_test import UnitTestGenerator
22

33
generator = UnitTestGenerator()
4-
code = '''
4+
code = """
55
struct Calculator {
66
result: i32,
77
}
@@ -37,7 +37,7 @@
3737
println!("{}", calc.get_result()); // Should print 6
3838
println!("{}", greet("Alice")); // Should print "Hello, Alice!"
3939
}
40-
'''
40+
"""
4141

4242
test_results = generator.run_tests()
4343

‎github_app/main.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from kaizen.utils.config import ConfigData
1111
import logging
1212

13-
# from cloudcode.generator.ui import UITester
13+
# from cloudcode.generator.ui import E2ETestGenerator
1414

1515
logging.basicConfig(
1616
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"

‎kaizen/actors/e2e_test_runner.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import asyncio
2+
import glob
3+
import json
4+
import os
5+
from playwright.async_api import async_playwright
6+
7+
8+
class E2ETestRunner:
9+
def __init__(self, test_directory="./.kaizen/e2e-tests/"):
10+
self.test_directory = test_directory
11+
12+
def run_tests(self):
13+
"""
14+
This method runs playwright tests and updates logs and status accordingly.
15+
"""
16+
17+
async def run_test(test):
18+
async with async_playwright() as p:
19+
browser = await p.chromium.launch()
20+
page = await browser.new_page()
21+
try:
22+
await page.goto(test["url"])
23+
await page.evaluate(test["code"])
24+
test["status"] = "Passed"
25+
except Exception as e:
26+
test["status"] = "Failed"
27+
test["error"] = str(e)
28+
finally:
29+
await browser.close()
30+
31+
tests_dir = self.test_directory
32+
tests = []
33+
for test_file in glob.glob(os.path.join(tests_dir, "*.json")):
34+
with open(test_file, "r") as f:
35+
tests.extend(json.load(f))
36+
37+
loop = asyncio.get_event_loop()
38+
tasks = [loop.create_task(run_test(test)) for test in tests]
39+
loop.run_until_complete(asyncio.gather(*tasks))
40+
return tests

‎kaizen/generator/ui.py ‎kaizen/generator/e2e_tests.py

+25-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import logging
22
import os
33
from typing import Optional
4-
from kaizen.helpers import output, parser
4+
from kaizen.helpers import output
55
from kaizen.llms.provider import LLMProvider
6+
from kaizen.actors.e2e_test_runner import E2ETestRunner
67
from kaizen.llms.prompts.ui_tests_prompts import (
7-
UI_MODULES_PROMPT,
8-
UI_TESTS_SYSTEM_PROMPT,
8+
E2E_MODULES_PROMPT,
9+
E2E_TESTS_SYSTEM_PROMPT,
910
PLAYWRIGHT_CODE_PROMPT,
1011
PLAYWRIGHT_CODE_PLAN_PROMPT,
1112
)
1213

1314

14-
class UITestGenerator:
15+
class E2ETestGenerator:
1516
def __init__(self):
1617
self.logger = logging.getLogger(__name__)
17-
self.provider = LLMProvider(system_prompt=UI_TESTS_SYSTEM_PROMPT)
18+
self.provider = LLMProvider(system_prompt=E2E_TESTS_SYSTEM_PROMPT)
1819
self.custom_model = None
20+
self.test_folder_path = ".kaizen/e2e-tests"
1921
self.total_usage = {
2022
"prompt_tokens": 0,
2123
"completion_tokens": 0,
@@ -26,13 +28,13 @@ def __init__(self):
2628
if "type" in self.custom_model:
2729
del self.custom_model["type"]
2830

29-
def generate_ui_tests(
31+
def generate_e2e_tests(
3032
self,
3133
web_url: str,
3234
folder_path: Optional[str] = "",
3335
):
3436
"""
35-
This method generates UI tests with cypress code for a given web URL.
37+
This method generates e2e tests with cypress code for a given web URL.
3638
"""
3739
web_content = self.extract_webpage(web_url)
3840
test_modules = self.identify_modules(web_content)
@@ -49,17 +51,19 @@ def extract_webpage(self, web_url: str):
4951
"""
5052

5153
html = output.get_web_html(web_url)
54+
self.logger.info(f"Extracted HTML data for {web_url}")
5255
return html
5356

5457
def identify_modules(self, web_content: str, user: Optional[str] = None):
5558
"""
5659
This method identifies the different UI modules from a webpage.
5760
"""
58-
prompt = UI_MODULES_PROMPT.format(WEB_CONTENT=web_content)
59-
resp, usage = self.provider.chat_completion(
61+
prompt = E2E_MODULES_PROMPT.format(WEB_CONTENT=web_content)
62+
resp, usage = self.provider.chat_completion_with_json(
6063
prompt, user=user, custom_model=self.custom_model
6164
)
62-
modules = parser.extract_multi_json(resp)
65+
modules = resp["tests"]
66+
self.logger.info(f"Extracted modules")
6367
return {"modules": modules, "usage": usage}
6468

6569
def generate_playwright_code(
@@ -70,21 +74,21 @@ def generate_playwright_code(
7074
user: Optional[str] = None,
7175
):
7276
"""
73-
This method generates playwright code for a particular UI test.
77+
This method generates playwright code for a particular E2E test.
7478
"""
7579
code_gen_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
7680
# First generate a plan for code
7781
prompt = PLAYWRIGHT_CODE_PLAN_PROMPT.format(
7882
WEB_CONTENT=web_content, TEST_DESCRIPTION=test_description, URL=web_url
7983
)
80-
plan, usage = self.provider.chat_completion(
84+
plan, usage = self.provider.chat_completion_with_retry(
8185
prompt, user=user, custom_model=self.custom_model
8286
)
8387
code_gen_usage = self.provider.update_usage(code_gen_usage, usage)
8488

8589
# Next generate the code based on plan
8690
code_prompt = PLAYWRIGHT_CODE_PROMPT.format(PLAN_TEXT=plan)
87-
code, usage = self.provider.chat_completion(
91+
code, usage = self.provider.chat_completion_with_retry(
8892
code_prompt, user=user, custom_model=self.custom_model
8993
)
9094
code_gen_usage = self.provider.update_usage(code_gen_usage, usage)
@@ -103,6 +107,9 @@ def generate_module_tests(self, web_content: str, test_modules: dict, web_url: s
103107
}
104108
for module in ui_tests:
105109
for test in module["tests"]:
110+
self.logger.info(
111+
f"Generating playwright code for {test['test_description']}"
112+
)
106113
test_description = test["test_description"]
107114
playwright_code = self.generate_playwright_code(
108115
web_content, test_description, web_url
@@ -120,12 +127,15 @@ def store_tests_files(self, json_tests: list, folder_path: str = ""):
120127
if not folder_path:
121128
folder_path = output.get_parent_folder()
122129

123-
folder_path = os.path.join(folder_path, ".kaizen/ui-tests")
130+
folder_path = os.path.join(folder_path, self.test_folder_path)
124131
output.create_folder(folder_path)
125132
output.create_test_files(json_tests, folder_path)
133+
self.logger.info("Successfully store the files")
126134

127135
def run_tests(self, ui_tests: dict):
128136
"""
129137
This method runs playwright tests and updates logs and status accordingly.
130138
"""
131-
pass
139+
runner = E2ETestRunner()
140+
results = runner.run_tests(ui_tests)
141+
return results

‎kaizen/helpers/output.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,38 @@ def get_web_html(url):
6060
html = asyncio.run(get_html(url))
6161
soup = BeautifulSoup(html, "html.parser")
6262

63+
# Remove SVG elements
6364
for svg in soup.find_all("svg"):
6465
svg.decompose()
6566

66-
# Delete each comment
67+
# Remove HTML comments
6768
for comment in soup.find_all(text=lambda text: isinstance(text, Comment)):
6869
comment.extract()
6970

71+
# Remove <style> elements
7072
for style_block in soup.find_all("style"):
7173
style_block.decompose()
7274

75+
# Remove <script> elements
76+
for script in soup.find_all("script"):
77+
script.decompose()
78+
79+
# Remove <noscript> elements
80+
for noscript in soup.find_all("noscript"):
81+
noscript.decompose()
82+
83+
# Remove <link> elements (typically used for stylesheets)
84+
for link in soup.find_all("link"):
85+
link.decompose()
86+
87+
# Remove <meta> elements (typically used for metadata)
88+
for meta in soup.find_all("meta"):
89+
meta.decompose()
90+
91+
# Remove <head> element (contains metadata, scripts, and stylesheets)
92+
for head in soup.find_all("head"):
93+
head.decompose()
94+
7395
pretty_html = soup.prettify()
7496
return pretty_html
7597

‎kaizen/llms/prompts/ui_tests_prompts.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
1-
UI_MODULES_PROMPT = """
1+
E2E_MODULES_PROMPT = """
22
Assign yourself as a quality assurance engineer.
33
Read this code and design comprehensive tests to test the UI of this HTML.
44
Break it down into 5-10 separate modules and identify the possible things to test for each module.
55
For each module, also identify which tests should be checked repeatedly (e.g., after every code change, every build, etc.).
66
77
Return the output as JSON with the following keys:
8+
{{"tests": {{
9+
"id": "serial number to identify module",
10+
"module_title": "title of the identified module",
11+
"tests": [
12+
{{
13+
"id": "serial number for the test case",
14+
"test_description": "description of the test case",
15+
"test_name": "name of the test case",
16+
"repeat": true,
17+
"reason": "reason to add this test",
18+
}},
19+
...
20+
],
21+
"folder_name": "relevant name for the module",
22+
"importance": "critical"
23+
}}
24+
}}
25+
26+
Details:
827
id - serial number to identify module
928
module_title - title of the identified module
1029
tests - JSON containing list of tests steps to carry out for that module with keys:
1130
id - serial number for the test case
1231
test_description - description of the test case
1332
test_name - name of the test case
1433
repeat - boolean indicating if this test should be checked repeatedly or not
34+
reason - reason to add this test case
1535
folder_name - relevant name for the module
1636
importance - level of importance of this test out of ['critical', 'good_to_have', 'non_essential']
1737
1838
Share the JSON output ONLY. No other text.
1939
CONTENT: ```{WEB_CONTENT}```
2040
"""
2141

22-
UI_TESTS_SYSTEM_PROMPT = """
42+
E2E_TESTS_SYSTEM_PROMPT = """
2343
You are a Quality Assurance AI assistant specializing in writing Playwright test scripts for web applications. Your goal is to create robust and maintainable test scripts that can be integrated into a CI/CD pipeline.
2444
2545
When given requirements or specifications, you should:

‎kaizen/llms/provider.py

+18
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ def chat_completion_with_json(
161161
response = extract_json(response)
162162
return response, usage
163163

164+
@retry(max_attempts=3, delay=1)
165+
def chat_completion_with_retry(
166+
self,
167+
prompt,
168+
user: str = None,
169+
model="default",
170+
custom_model=None,
171+
messages=None,
172+
):
173+
response, usage = self.chat_completion(
174+
prompt=prompt,
175+
user=user,
176+
model=model,
177+
custom_model=custom_model,
178+
messages=messages,
179+
)
180+
return response, usage
181+
164182
def is_inside_token_limit(self, PROMPT: str, percentage: float = 0.8) -> bool:
165183
# Include system prompt in token calculation
166184
messages = [

0 commit comments

Comments
 (0)
Please sign in to comment.