mirror of
https://github.com/AdaCore/learn.git
synced 2026-02-12 13:00:42 -08:00
291 lines
11 KiB
Python
Executable File
291 lines
11 KiB
Python
Executable File
"""
|
|
Find all code blocks in a given rst file, and execute all of their defined modes using the defined AWS Lambda function.
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import traceback
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Optional
|
|
from websocket import WebSocket
|
|
|
|
SPHINX_PATH = (Path(__file__).parent.parent / 'sphinx').resolve()
|
|
sys.path.append(str(SPHINX_PATH))
|
|
|
|
from widget.chop import manual_chop, real_gnatchop
|
|
|
|
from code_block import CodeBlock, get_blocks
|
|
|
|
|
|
FUNCTION_URLS = {'prod': 'wss://backend.learn.adacore.com',
|
|
'sandbox': 'wss://sandbox.backend.learn.adacore.com'}
|
|
|
|
EXPECT_FAILURE_CLASSES = {
|
|
'ada': ['ada-expect-compile-error', 'ada-run-expect-failure', 'ada-expect-prove-error'],
|
|
'c': ['c-expect-compile-error', 'c-run-expect-failure']}
|
|
|
|
EXPECT_FAILURE_CLASS_BTNS = {
|
|
'ada-expect-compile-error': ['compile', 'run'],
|
|
'ada-run-expect-failure': ['run'],
|
|
'ada-expect-prove-error': ['prove', 'prove_flow', 'prove_flow_report_all', 'prove_report_all'],
|
|
'c-expect-compile-error': ['compile', 'run'],
|
|
'c-run-expect-failure': ['run']
|
|
}
|
|
|
|
class CodeBlockException(Exception):
|
|
pass
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
"""Parse program arguments
|
|
|
|
Returns:
|
|
argparse.Namespace: parsed arguments object
|
|
"""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('rst_files', type=str, nargs="+", help="The rst file from which to extract doc")
|
|
parser.add_argument('--verbose', '-v', action='store_true', help='Show more information')
|
|
parser.add_argument('--env', type=str, default='prod', choices=['sandbox', 'prod'],
|
|
help='Whether to use the prod or sandbox environment to run the code blocks')
|
|
parser.add_argument('--projects', type=str, default='',
|
|
help='Comma seperated string of project names to run e.g. Proj_1,Proj_2,Other_proj')
|
|
parser.add_argument('--halt-on-failure', action='store_true', help='Exit with non-zero exit status on test failure')
|
|
args = parser.parse_args()
|
|
args.rst_files = [os.path.abspath(f) for f in args.rst_files]
|
|
args.projects = set() if len(args.projects) == 0 else set(args.projects.split(','))
|
|
return args
|
|
|
|
|
|
def get_code_blocks(rst_file: str) -> List[CodeBlock]:
|
|
"""Reads the provided rst_file and extracts all CodeBlocks from it. Any
|
|
CodeBlocks that have a language other than Ada or c are filtered out.
|
|
The ordering of CodeBlocks is maintained relative to how they appear in the file.
|
|
|
|
Args:
|
|
rst_file (str): path to rst file to parsed
|
|
|
|
Raises:
|
|
CodeBlockException: if the project name of a CodeBlock is not set
|
|
|
|
Returns:
|
|
List[CodeBlock]: a List of CodeBlocks found in the rst file
|
|
"""
|
|
with open(rst_file) as f:
|
|
content = f.read()
|
|
all_blocks = get_blocks(content, rst_file)
|
|
filtered_blocks = []
|
|
for b in all_blocks:
|
|
if not isinstance(b, CodeBlock): continue
|
|
if not b.language in ['ada', 'c']: continue
|
|
if b.project is None:
|
|
raise CodeBlockException(f"Error: project not set: {b.loc}")
|
|
filtered_blocks.append(b)
|
|
return filtered_blocks
|
|
|
|
|
|
def group_by_project(blocks: List[CodeBlock]) -> Dict[str, List[CodeBlock]]:
|
|
"""Group a list of CodeBlocks by their project name. The ordering of CodeBlocks is maintained
|
|
|
|
Args:
|
|
blocks (List[CodeBlock]): CodeBlocks ot be grouped
|
|
|
|
Returns:
|
|
Dict[str, List[CodeBlock]]: map of projects to a list of CodeBlocks that share that project name
|
|
"""
|
|
projects: Dict[str, List[CodeBlock]] = {}
|
|
for b in code_blocks:
|
|
if b.project not in projects:
|
|
projects[b.project] = []
|
|
projects[b.project].append(b)
|
|
return projects
|
|
|
|
|
|
def filter_projects(projects: Dict[str, List[CodeBlock]],
|
|
keep: Set[str]) -> Dict[str, List[CodeBlock]]:
|
|
"""Filter a dictionary of projects, only keeping projects whose name is in `keep`.
|
|
If `keep` is emtpy, it will return all projects.
|
|
|
|
Args:
|
|
projects (Dict[str, List[CodeBlock]]): dict of projects to filter
|
|
keep (Set[str]): Optional set of project names to keep.
|
|
|
|
Returns:
|
|
Dict[str, List[CodeBlock]]: filtered dict of projects
|
|
"""
|
|
if len(keep) == 0:
|
|
return projects
|
|
return {k: v for k, v in projects.items() if k in keep}
|
|
|
|
|
|
def call_websocket_endpoint(name: str,
|
|
mode: str,
|
|
main_file: str,
|
|
switches: Dict[str, List[str]],
|
|
files: List[Dict[str, str]],
|
|
websocket_url: str) -> Optional[List[dict]]:
|
|
"""Call the websocket endpoint with the provided information
|
|
|
|
Args:
|
|
name (str): used to identify a run
|
|
mode (str): mode to be executed. Relates the button being pressed
|
|
main_file (str): file name of the main file. Empty if one is not defined
|
|
switches (Dict[str, List[str]]): map of switch type, to a list of switches to be set
|
|
files (List[Dict[str, str]]): List of dicts that contain filenames, and file contents
|
|
websocket_url (str): websocket url to be used
|
|
|
|
Returns:
|
|
Optional[List[dict]]: list of responses if successful, None otherwise
|
|
"""
|
|
msg = msg = {
|
|
"action": "execute",
|
|
"data": {
|
|
"files": files,
|
|
"main": main_file,
|
|
"mode": mode,
|
|
"switches": switches,
|
|
"name": name,
|
|
"lab": False
|
|
}
|
|
}
|
|
retry_count = 5
|
|
all_exceptions = []
|
|
while retry_count > 0:
|
|
retry_count -= 1
|
|
try:
|
|
ws = WebSocket()
|
|
ws.connect(websocket_url)
|
|
ws.send(json.dumps(msg))
|
|
received = None
|
|
result = []
|
|
result_with_msg = [msg]
|
|
while True:
|
|
received = json.loads(ws.recv())
|
|
result.append(received)
|
|
if 'connectionId' in received:
|
|
break
|
|
if received['completed']:
|
|
break
|
|
ws.close()
|
|
result_with_msg.extend(result)
|
|
return result_with_msg
|
|
except Exception as e:
|
|
print(f"Retry: {name}")
|
|
all_exceptions.append[e]
|
|
print(f"Failed to run: {name}: {all_exceptions}")
|
|
return None
|
|
|
|
|
|
def run_code_block(block: CodeBlock,
|
|
deps: Dict[str, str],
|
|
websocket_url: str,
|
|
verbose: bool,
|
|
halt_on_failure: bool):
|
|
"""Run a single code block using the provided url. `deps` should contain a mapping of file names to file
|
|
contents from the same project, but earlier in the file. This is because projects files are accumulated if not
|
|
overwritten. `deps` will also be updated with all files in the current CodeBlock.
|
|
|
|
Args:
|
|
block (CodeBlock): to be run
|
|
deps (Dict[str, str]): dict of file names to file contents files from earlier CodeBlocks in the same project
|
|
websocket_url (str): url of the websocket endpoint that should be used to execute a task
|
|
verbose (bool): if True, extra log statements will be emitted
|
|
halt_on_failure (bool): if True and a test fails, exit with a non-zero exit status
|
|
"""#
|
|
# Extract source files from the current editor block
|
|
if block.manual_chop:
|
|
source_files = manual_chop(block.text.splitlines())
|
|
else:
|
|
source_files = real_gnatchop(block.text.splitlines())
|
|
|
|
# Replace any existing project files with the same name
|
|
for f in source_files:
|
|
deps[f.basename] = f.content
|
|
|
|
# Update the list of source files with the previously found files
|
|
source_files = [{'basename': f, 'contents': deps[f]} for f in deps]
|
|
|
|
# Skip the CodeBlock if ...
|
|
no_check = any(sphinx_class in ["ada-nocheck", "c-nocheck"] for sphinx_class in block.classes)
|
|
if no_check or not block.run or 'ada-syntax-only' in block.classes or 'ada-norun' in block.classes:
|
|
if verbose:
|
|
print(f"Skipping code block: {block.loc}")
|
|
return
|
|
|
|
# If there are no source files something went wrong
|
|
if len(source_files) == 0:
|
|
print(f"Skipping code block: Failed to chop example: {block.loc}")
|
|
return
|
|
|
|
main_file = "" if block.main_file is None else block.main_file
|
|
|
|
switches = {}
|
|
if len(block.compiler_switches) > 0:
|
|
switches['Compiler'] = block.compiler_switches
|
|
|
|
# For each button, run the CodeBlock using the lambda function
|
|
for b in block.buttons:
|
|
if b == 'no':
|
|
continue
|
|
name = f"{block.project}.{block.loc}.{b}"
|
|
res = call_websocket_endpoint(name=name,
|
|
mode=b,
|
|
main_file=main_file,
|
|
switches=switches,
|
|
files=source_files,
|
|
websocket_url=websocket_url)
|
|
if res is None:
|
|
continue
|
|
if 'connectionId' in res[-1]:
|
|
print(f"AWS error: {name}: {res}")
|
|
continue
|
|
# If it ran, check that the exit status matched the expectation
|
|
exit_status = res[-1]['status']
|
|
expect_failure = False
|
|
for c in EXPECT_FAILURE_CLASSES[block.language]:
|
|
if c in block.classes and b in EXPECT_FAILURE_CLASS_BTNS[c]:
|
|
expect_failure = True
|
|
break
|
|
if expect_failure and exit_status == 0:
|
|
print(f"Fail: {name}: expected failure, received zero exit status: {res}")
|
|
if halt_on_failure:
|
|
exit(1)
|
|
elif not expect_failure and exit_status != 0:
|
|
print(f"Fail: {name}: expected success, received non-zero exit status: {res}")
|
|
if halt_on_failure:
|
|
exit(2)
|
|
elif verbose:
|
|
print(f"Pass: {name}")
|
|
|
|
|
|
def run_all_projects(projects: Dict[str, List[CodeBlock]],
|
|
websocket_url: str,
|
|
verbose: bool,
|
|
halt_on_failure: bool):
|
|
"""Run all of the CodeBlocks in `projects` using the websocket url
|
|
|
|
Args:
|
|
projects (Dict[str, List[CodeBlock]]): dict of project names that each map to a list of CodeBlocks to be run
|
|
websocket_url (str): url of the websocket endpoint that should be used to execute a task
|
|
verbose (bool): if True, extra log statements will be emitted
|
|
halt_on_failure (bool): if True and a test fails, exit with a non-zero exit status
|
|
"""
|
|
all_deps = {}
|
|
for p in projects:
|
|
for b in projects[p]:
|
|
if p not in all_deps:
|
|
all_deps[p] = {}
|
|
run_code_block(b, all_deps[p], websocket_url, verbose, halt_on_failure)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = parse_args()
|
|
lambda_url = FUNCTION_URLS[args.env]
|
|
for f in args.rst_files:
|
|
code_blocks = get_code_blocks(f)
|
|
projects = group_by_project(code_blocks)
|
|
projects = filter_projects(projects, args.projects)
|
|
run_all_projects(projects, lambda_url, args.verbose, args.halt_on_failure)
|