Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion tested/dodona.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ class ExtendedMessage:
permission: Permission | None = None


@define
class LinkedFile:
type: Literal["path", "inline"]
# Either a path (type "file") or the content (type "inline")
content: str


@define
class Metadata:
"""Currently only used for the Python tutor"""
"""Used for the debugger and file linking"""

statements: str | None
stdin: str | None
files: dict[str, LinkedFile] | None


Message = ExtendedMessage | str
Expand Down
20 changes: 19 additions & 1 deletion tested/judge/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CloseContext,
CloseJudgement,
CloseTab,
LinkedFile,
Metadata,
StartContext,
StartJudgement,
Expand Down Expand Up @@ -46,7 +47,7 @@
generate_statement,
)
from tested.serialisation import Statement
from tested.testsuite import LanguageLiterals, MainInput, TextData
from tested.testsuite import ContentPath, LanguageLiterals, MainInput, TextData

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'ContentPath' is not used.

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -367,11 +368,14 @@
# Don't add empty statements
meta_statements = None

meta_files = _get_meta_files(bundle, planned)

collector.add(
CloseContext(
data=Metadata(
statements=meta_statements,
stdin=meta_stdin,
files=meta_files,
)
),
planned.context_index,
Expand All @@ -382,3 +386,17 @@
return continue_, currently_open_tab

return None, currently_open_tab


def _get_meta_files(bundle: Bundle, planned: PlannedContext) -> dict[str, LinkedFile]:
meta_files = {}
for f in planned.context.get_input_files():
display_path = f.get_display_path()

if display_path is not None:
meta_files[f.path] = LinkedFile(type="path", content=display_path)
else:
contents = f.get_data_as_string(bundle.config.resources)
meta_files[f.path] = LinkedFile(type="inline", content=contents)

return meta_files
13 changes: 3 additions & 10 deletions tested/judge/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,14 +384,8 @@ def link_files_message(
) -> AppendMessage | None:
link_list = []
for link_file in link_files:
# TODO: handle inline files somehow.
link_url = link_file.get_display_path()
if link_file.path is not None and link_url is not None:
the_url = urllib.parse.quote(link_url)
link_list.append(
f'<a href="{the_url}" class="file-link" target="_blank">'
f'<span class="code">{html.escape(link_file.path)}</span></a>'
)
if link_file.path is not None:
link_list.append(link_file.path)

if len(link_list) == 0:
return None # Do not append any message if there are no files.
Expand All @@ -400,8 +394,7 @@ def link_files_message(
file_list_str = get_i18n_string(
"judge.evaluation.files", count=len(link_list), files=file_list
)
description = f"<div class='contains-file'><p>{file_list_str}</p></div>"
message = ExtendedMessage(description=description, format="html")
message = ExtendedMessage(description=file_list_str, format="text")
return AppendMessage(message=message)


Expand Down
80 changes: 4 additions & 76 deletions tested/languages/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
Translates items from the test suite into the actual programming language.
"""

import html
import json
import logging
import re
import shlex
import urllib.parse
from collections.abc import Iterable
from pathlib import Path
from re import Match
from typing import TYPE_CHECKING, TypeAlias

from pygments import highlight
Expand All @@ -29,16 +24,8 @@
prepare_execution_unit,
prepare_expression,
)
from tested.parsing import get_converter
from tested.serialisation import Expression, Statement, VariableType
from tested.testsuite import (
ContentPath,
Context,
LanguageLiterals,
MainInput,
Testcase,
TextData,
)
from tested.testsuite import Context, LanguageLiterals, MainInput, Testcase, TextData
from tested.utils import is_statement_strict

if TYPE_CHECKING:
Expand Down Expand Up @@ -77,22 +64,6 @@ def generate_execution_unit(
return bundle.language.generate_execution_unit(prepared_execution)


def _handle_link_files(
link_files: Iterable[TextData], language: str
) -> tuple[str, str]:
dict_links = dict(
(link_file.path, get_converter().unstructure(link_file))
for link_file in link_files
)
files = json.dumps(dict_links)
return (
f"<div class='contains-file highlight-{language} highlighter-rouge' "
f"data-files={repr(files)}><pre style='padding: 2px; margin-bottom: "
f"1px; background: none;'><code>",
"</code></pre></div>",
)


def _get_heredoc_token(stdin: str) -> str:
delimiter = "STDIN"
while delimiter in stdin:
Expand Down Expand Up @@ -163,61 +134,18 @@ def get_readable_input(
if case.line_comment:
text = f"{text} {bundle.language.comment(case.line_comment)}"

# If there are no files, return now. This means we don't need to do ugly stuff.
if not case.input_files:
return ExtendedMessage(description=text, format=format_), set()

# We have potential files.
# Check if the file names are present in the string.
# If not, we can also stop before doing ugly things.
# We construct a regex, since that can be faster than checking everything.
file_paths = [re.escape(x.path) for x in case.input_files if x.path is not None]

if not file_paths:
# No files to match, so bail now.
return ExtendedMessage(description=text, format=format_), set()

simple_regex = re.compile("|".join(file_paths))

if not simple_regex.search(text):
# There is no match, so bail now.
return ExtendedMessage(description=text, format=format_), set()

# Now we need to do ugly stuff.
# Begin by compiling the HTML that will be displayed.
if format_ == "text":
generated_html = html.escape(text)
elif format_ == "console":
generated_html = highlight_code(text)
else:
generated_html = highlight_code(text, bundle.config.programming_language)

# Map of file URLs.
url_map = {html.escape(x.path): x for x in case.input_files if x.path is not None}

seen = set()
escaped_regex = re.compile("|".join(url_map.keys()))

# Replaces the match with the corresponding link.
def replace_link(match: Match) -> str:
filename = match.group()
the_file = url_map[filename]
the_url = the_file.get_display_path()
if the_url is None:
# TODO: how to handle inline files?
return filename

quoted_url = urllib.parse.quote(the_url)
the_replacement = (
f'<a href="{quoted_url}" class="file-link" target="_blank">{filename}</a>'
)
seen.add(the_file)
return the_replacement

generated_html = escaped_regex.sub(replace_link, generated_html)
prefix, suffix = _handle_link_files(seen, format_)
generated_html = f"{prefix}{generated_html}{suffix}"
return ExtendedMessage(description=generated_html, format="html"), seen
path_map = {x.path: x for x in case.input_files if x.path is not None}
seen = {path_map[m.group()] for m in re.finditer("|".join(file_paths), text)}
return ExtendedMessage(description=text, format=format_), seen


def attempt_readable_input(bundle: Bundle, context: Context) -> ExtendedMessage:
Expand Down
Loading