Skip to content

Commit 9301785

Browse files
Review round 2 jk (#101)
* remove reference ListOfTestCases * add folder artifacts to gitignore * add generator for the list of test environments * generate list of test environments * add validator for list of test environments * document generator * implement evidence * remove obsolete tests * add tests for ListOfTestsGenerator * repair pytest execution --------- Signed-off-by: Erik Hu <135733975+Erikhu1@users.noreply.github.com> Signed-off-by: Jonas-Kirchhoff <jonas.kirchhoff@d-fine.com> Co-authored-by: Erik Hu <erik.hu@d-fine.com> Co-authored-by: Erik Hu <135733975+Erikhu1@users.noreply.github.com>
1 parent e8ea224 commit 9301785

12 files changed

Lines changed: 7837 additions & 256 deletions

File tree

.dotstop_extensions/references.py

Lines changed: 0 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -545,213 +545,6 @@ def __str__(self) -> str:
545545
# this is used as a title in the trudag report
546546
return f"function: [{self._name}]\n({str(self.path)})"
547547

548-
class ListOfTestCases(BaseReference):
549-
550-
def __init__(self, test_files: list[str], recent_result_database: str = "artifacts/MemoryEfficientTestResults.db", recent_result_table: str = "test_results") -> None:
551-
self._test_files = test_files
552-
self._database = recent_result_database
553-
self._table = recent_result_table
554-
555-
@staticmethod
556-
def compile_string(items: list[str]) -> str:
557-
# input: list of strings representing the structure of TEST_CASE, SECTION etc.,
558-
# e.g. items = ["lexer class", "scan", "literal names"]
559-
# output: the last item of the list, representing the most recent SECTION,
560-
# indented as in the source code
561-
# throws error if input is empty
562-
if len(items) == 0:
563-
raise RuntimeError("Received empty structural list; nonempty list expected.")
564-
result = ""
565-
for _ in range(1, len(items)):
566-
result += " "
567-
if items:
568-
result += "* " + items[-1]
569-
return result
570-
571-
@staticmethod
572-
def extract_quotation(s: str) -> str:
573-
# input: string containing at least one quoted substring, e.g. s = "my \"input\""
574-
# output: the first quoted substring of the input
575-
# throws error if no quoted substring can be found.
576-
first = s.find('"')
577-
if first == -1:
578-
raise RuntimeError("Expected quotation mark; none were detected.")
579-
second = s.find('"', first + 1)
580-
if second == -1:
581-
raise RuntimeError("Expected quotation marks; only one was detected.")
582-
return s[first + 1 : second]
583-
584-
@staticmethod
585-
def remove_and_count_indent(s: str) -> tuple[int, str]:
586-
# input: string with possibly leading whitespace (space of horizontal tab)
587-
# output: the number of leading spaces and the string with leading whitespace removed;
588-
# tab counted as four spaces
589-
cnt = 0
590-
i = 0
591-
n = len(s)
592-
while i < n and (s[i] == " " or s[i] == "\t"):
593-
if s[i] == " ":
594-
cnt += 1
595-
elif s[i] == "\t":
596-
cnt += 4
597-
i += 1
598-
return (cnt, s[i:])
599-
600-
@staticmethod
601-
def head_of_list() -> str:
602-
return """## List of all unit-tests with test environments
603-
604-
This list contains all unit-tests possibly running in this project.
605-
These tests are compiled from the source-code, where the individual unit-tests are arranged in TEST_CASEs containing possibly nested SECTIONs.
606-
To reflect the structure of the nested sections, nested lists are utilised, where the top-level list represents the list of TEST_CASEs.
607-
608-
It should be noted that not all unit-tests in a test-file are executed with every compiler-configuration.
609-
"""
610-
611-
@staticmethod
612-
def transform_test_file_to_test_name(test_file: str) -> str:
613-
return "test-"+"-".join((test_file.split('.')[0]).split('-')[1:])
614-
615-
@classmethod
616-
def type(cls) -> str:
617-
return "list_of_test_cases"
618-
619-
def extract_test_structure(self, file_path: Path) -> str:
620-
# input: path to a file potentially containing unit-tests
621-
# output: the extracted arrangement of TEST_CASE and SECTION
622-
# in the form of nested markdown lists
623-
624-
indent = 0 # the indent of the currently read line
625-
current_indent = 0 # the indent of the last TEST_CASE or SECTION
626-
current_path = [] # the current path
627-
lines_out = [] # the collection of lines to be outputted
628-
629-
# open file_path as read-only, and process line by line
630-
with file_path.open("r", encoding="utf-8", errors="replace") as source:
631-
for line in source:
632-
# count and remove leading whitespace
633-
indent, trimmed = self.remove_and_count_indent(str(line))
634-
635-
# check whether we have found a TEST_CASE
636-
if trimmed.startswith("TEST_CASE(") or trimmed.startswith("TEST_CASE_TEMPLATE(") or trimmed.startswith("TEST_CASE_TEMPLATE_DEFINE("):
637-
# remember the current indent
638-
current_indent = indent
639-
# TEST_CASE is always the head of a new arrangement-structure
640-
# remove stored structure
641-
current_path.clear()
642-
# extract name of TEST_CASE and append path
643-
current_path.append(self.extract_quotation(trimmed))
644-
lines_out.append(self.compile_string(current_path))
645-
646-
# check whether we have found a SECTION
647-
if trimmed.startswith("SECTION("):
648-
# update path to reflect arrangement of current section
649-
while indent <= current_indent and current_path:
650-
current_path.pop()
651-
current_indent -= 4
652-
# remember the current indent
653-
current_indent = indent
654-
# extract name of SECTION and append path
655-
current_path.append(self.extract_quotation(trimmed))
656-
lines_out.append(self.compile_string(current_path))
657-
658-
# process extracted lines
659-
return ("\n".join(lines_out) + "\n") if lines_out else ""
660-
661-
def extract_recent_test_environments(self) -> dict:
662-
fetched_data = dict()
663-
try:
664-
# initialise connection to test result database
665-
connector = sqlite3.connect(self._database)
666-
cursor = connector.cursor()
667-
# verify that the expected table does exist
668-
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name = ?;",(self._table,))
669-
if cursor.fetchone() is None:
670-
raise RuntimeError(f"Fatal Error: Could not find table {self._table} in database {self._database}.")
671-
except sqlite3.Error as e:
672-
raise RuntimeError(f"Fatal Error accessing database {self._database}: {e}")
673-
# get all test-files from recent test executions
674-
command = f"SELECT name FROM {self._table};"
675-
cursor.execute(command)
676-
raw_cases = cursor.fetchall()
677-
cases = set([raw_case[0] for raw_case in raw_cases])
678-
# for each test-file
679-
for case in cases:
680-
case_data = dict()
681-
# get the test-environments
682-
command = f"SELECT compiler, cpp_standard FROM {self._table} WHERE name = ? and skipped_cases == 0"
683-
cursor.execute(command,(case,))
684-
results = cursor.fetchall()
685-
case_data["noskip"] = [{"compiler":result[0], "standard":result[1]} for result in results]
686-
# some test-cases are skipped with certain environments
687-
# It is unclear from the log, which cases are skipped;
688-
# we leave this to the interested reader
689-
command = f"SELECT compiler, cpp_standard, skipped_cases FROM {self._table} WHERE name = ? and skipped_cases != 0"
690-
cursor.execute(command, (case,))
691-
results = cursor.fetchall()
692-
case_data["skip"] = [{"compiler": result[0], "standard": result[1], "skipped": result[2]} for result in results]
693-
fetched_data[case] = case_data
694-
return fetched_data
695-
696-
def fetch_all_test_data(self, input: list[str]):
697-
# inputs: path(s) to directory potentially containing some test-data
698-
extracted_test_data = []
699-
recent_test_data = self.extract_recent_test_environments()
700-
for arg in input:
701-
p = Path(arg)
702-
if p.is_file() and p.suffix == ".cpp" and p.name.startswith("unit-"):
703-
extracted_test_data.append((p.name,self.extract_test_structure(p)))
704-
elif p.is_dir():
705-
for entry in p.rglob("*"):
706-
if entry.is_file() and entry.suffix == ".cpp" and entry.name.startswith("unit-"):
707-
extracted_test_data.append((entry.name,self.extract_test_structure(entry)))
708-
extracted_test_data.sort(key= lambda x: x[0])
709-
result = self.head_of_list()
710-
for test_file, list_of_tests in extracted_test_data:
711-
result += f"\n\n### List of tests in file {test_file}\n\n"
712-
result += list_of_tests
713-
result += "\n\n"
714-
if recent_test_data.get(self.transform_test_file_to_test_name(test_file), None) is None:
715-
result += "Unfortunately, none of the following tests seems to have been executed. Very strange indeed!\n\n"
716-
else:
717-
if recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("noskip",None) is not None:
718-
if len(recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("noskip")) != 0:
719-
result += "\nAll tests in this file were run in the following configurations:\n\n"
720-
for datum in recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("noskip"):
721-
result += "* "
722-
result += datum.get("compiler",None)
723-
result += " with standard "
724-
result += datum.get("standard",None)
725-
result += "\n"
726-
if recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("skip",None) is not None:
727-
if len(recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("skip")) != 0:
728-
result += "\nIn the following configuration, however, some test-cases were skipped:\n\n"
729-
for datum in recent_test_data.get(self.transform_test_file_to_test_name(test_file)).get("skip"):
730-
result += "* "
731-
how_many = datum.get("skipped",None)
732-
result += str(how_many)
733-
if how_many == 1:
734-
result += " test case was skipped when using "
735-
else:
736-
result += " test cases were skipped when using "
737-
result += datum.get("compiler",None)
738-
result += " with standard "
739-
result += datum.get("standard",None)
740-
result += "\n"
741-
return result
742-
743-
@property
744-
def content(self) -> bytes:
745-
# encoding is necessary since content will be hashed
746-
return self.fetch_all_test_data(self._test_files).encode('utf-8')
747-
748-
def as_markdown(self, filepath: None | str = None) -> str:
749-
return self.content.decode('utf-8')
750-
751-
def __str__(self) -> str:
752-
# this is used as a title in the trudag report
753-
return "List of all unit-tests"
754-
755548
from trudag.dotstop.core.reference.references import LocalFileReference as LFR
756549

757550
class VerboseFileReference(LFR):

.dotstop_extensions/test_references.py

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import tempfile
33
from pathlib import Path
44
from unittest.mock import patch
5+
from references import CPPTestReference, JSONTestsuiteReference, FunctionReference, ItemReference
56
from validators import file_exists
6-
from references import CPPTestReference, JSONTestsuiteReference, FunctionReference, ListOfTestCases, ItemReference
77

88

99
@pytest.fixture
@@ -673,41 +673,13 @@ def test_init_function_reference(temp_hpp_file):
673673
assert ref.path == temp_hpp_file
674674
assert ref._overload == 1
675675

676-
def test_default_init_ListOfTestCases():
677-
ref = ListOfTestCases(["file_1","file_2"])
678-
assert ref._test_files == ["file_1","file_2"]
679-
assert ref._database == "artifacts/MemoryEfficientTestResults.db"
680-
assert ref._table == "test_results"
681-
682-
def test_non_default_init_ListOfTestCases():
683-
ref = ListOfTestCases(["file_1","file_2"],"my_database.db","my_fancy_table")
684-
assert ref._test_files == ["file_1","file_2"]
685-
assert ref._database == "my_database.db"
686-
assert ref._table == "my_fancy_table"
687-
688-
def test_compile_string():
689-
with pytest.raises(RuntimeError):
690-
ListOfTestCases.compile_string([])
691-
692-
def test_remove_and_count_indent():
693-
assert ListOfTestCases.remove_and_count_indent("Hallo")== (0,"Hallo")
694-
assert ListOfTestCases.remove_and_count_indent(" Hallo") == (1,"Hallo")
695-
assert ListOfTestCases.remove_and_count_indent("\t Hallo Welt \t\t") == (5,"Hallo Welt \t\t")
696-
697-
def test_extract_quotation():
698-
assert ListOfTestCases.extract_quotation("\"Hallo\" Welt") == "Hallo"
699-
assert ListOfTestCases.extract_quotation("This is quite \"exciting\", isn't it.") == "exciting"
700-
assert ListOfTestCases.extract_quotation("\"Hallo\" \"Welt\"") == "Hallo"
701-
702-
def test_extract_faulty_quotation():
703-
with pytest.raises(RuntimeError, match=r"Expected quotation mark; none were detected."):
704-
ListOfTestCases.extract_quotation("Hallo Welt")
705-
with pytest.raises(RuntimeError, match=r"Expected quotation marks; only one was detected."):
706-
ListOfTestCases.extract_quotation("Hallo \"Welt")
707-
708-
def test_transform_test_file_to_test_name():
709-
assert ListOfTestCases.transform_test_file_to_test_name("unit-dummy-test.cpp") == "test-dummy-test"
710-
assert ListOfTestCases.transform_test_file_to_test_name("unit-dummy_test.cpp") == "test-dummy_test"
676+
def test_faulty_init_ItemReference():
677+
with pytest.raises(RuntimeError, match = r"Error: Can't initialise empty ItemReference."):
678+
item_reference = ItemReference([])
679+
680+
def test_init_ItemReference():
681+
item_reference = ItemReference(["Hallo","Welt"])
682+
assert item_reference._items == ["Hallo","Welt"]
711683

712684
def test_file_exists(tmp_path):
713685
root = tmp_path / "direx"
@@ -725,10 +697,3 @@ def test_file_exists(tmp_path):
725697
assert score == 2/4
726698
assert any(isinstance(exception,Warning) for exception in exceptions)
727699
assert any(isinstance(exception,RuntimeError) for exception in exceptions)
728-
def test_faulty_init_ItemReference():
729-
with pytest.raises(RuntimeError, match = r"Error: Can't initialise empty ItemReference."):
730-
item_reference = ItemReference([])
731-
732-
def test_init_ItemReference():
733-
item_reference = ItemReference(["Hallo","Welt"])
734-
assert item_reference._items == ["Hallo","Welt"]

.dotstop_extensions/validators.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import requests
44
import sqlite3
5+
from TSF.scripts.generate_list_of_tests import ListOfTestsGenerator
56

67
yaml: TypeAlias = str | int | float | list["yaml"] | dict[str, "yaml"]
78

@@ -232,3 +233,27 @@ def file_exists(configuration: dict[str, yaml]) -> tuple[float, list[Exception |
232233
else:
233234
found_files += 1 if os.path.isfile(file) else 0
234235
return (found_files/expected_files, exceptions)
236+
237+
def check_list_of_tests(configuration: dict[str, yaml]) -> tuple[float, list[Exception | Warning]]:
238+
# initialise the generator
239+
generator = ListOfTestsGenerator()
240+
db = configuration.get("database",None)
241+
if db is not None:
242+
generator.set_database(db)
243+
table = configuration.get("table",None)
244+
if table is not None:
245+
generator.set_table(table)
246+
sources = configuration.get("sources",None)
247+
if sources is not None:
248+
generator.set_sources(sources)
249+
250+
# fetch the expected result
251+
try:
252+
with open("./TSF/docs/list_of_test_environments.md", 'r') as f:
253+
expected = f.read()
254+
if expected == generator.fetch_all_test_data():
255+
return(1.0,[])
256+
else:
257+
return(0.0,[Exception("The expected list of test-cases does not coincide with the fetched list.")])
258+
except:
259+
return(0.0,[Exception("An exception occurred when trying to compare the expected and the fetched list of tests.")])

.github/workflows/test_trudag_extensions.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Run tests
3232
run: |
3333
cd .dotstop_extensions
34-
pytest -v
34+
PYTHONPATH=.. pytest -v
3535
3636
- name: Generate test_trudag_extensions artifact
3737
run: |

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ user.bazelrc
5555
/TSF/temp
5656

5757
/TSF/docs/generated
58+
# temporary folder used for generation of list of test-results
59+
/artifacts
5860

5961
/.venv
6062

TSF/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file makes the directory a Python package

0 commit comments

Comments
 (0)