Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

Commit

Permalink
Merge pull request #109 from SELab-2/simple_tests
Browse files Browse the repository at this point in the history
Simple tests
  • Loading branch information
rubben-88 authored Mar 26, 2024
2 parents f59362a + 185abfe commit c341734
Show file tree
Hide file tree
Showing 19 changed files with 1,185 additions and 0 deletions.
75 changes: 75 additions & 0 deletions backend/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,81 @@ Now is a good time to explain the function *to_domain_model* from earlier. When
These universal dataclasses inherit the **Pydantic** BaseModel. It allows for automatic data validation, JSON schema generation and more. More information on Pydantic can be found [here](https://docs.pydantic.dev/latest/why/).

#### III) simple submission tests

[simple_submission_checks](domain/simple_submission_checks) contains the logic to run simple tests on a submission. simple tests can check if the submission
- Is a zip file with a certain name
- Is a file with a certain name
- Contains a certain file or folder
- Does not contain a certain file or folder
- Only contains files with certain names

These constraints can be nested indefinitely.

The frontend should send a json file containing a `SubmissionConstraint`. Such a submission constraint has a `root_constraint`
which is one of two things: A `FileConstraint` or a `Zipconstraint`, as a submission is OR a file, OR a zip file.
A `FileConstraint` simply checks if a file with a certain name is present. The `ZipConstraint` also contains a name field specifying the name of the zip file, along
with a list of either `FileConstrainst`, `DirectoryConstraint`, `NotPresentConstraints` or `OnlyPresentConstraints`. These can be mixed.
A `DirectoryConstraint` also has a name field and a list of constraints. The `NotPresentConstraint` specifies that a certain file may not be present in that directory or zip.
The `FileConstraint` specifies that a file with a certain name must be present. The `OnlyPresentConstraint` specifies that only files with certain names may be present in that directory or zip, and no other.

For the frontend people: If you want to know how to make a `Submissionconstraint` you can look at the models in the [simple_submission_checks](domain/simple_submission_checks) folder. The following is an example:

```json
{
"type": "zip_constraint",
"name": "root.zip",
"sub_constraints": [
{
"type": "directory_constraint",
"name": "Documents",
"sub_constraints": [
{
"type": "file_constraint",
"name": "Resume.pdf"
},
{
"type": "file_constraint",
"name": "CoverLetter.docx"
},
{
"type": "file_constraint",
"name": "Transcript.pdf"
}
]
},
{
"type": "directory_constraint",
"name": "Images",
"sub_constraints": [
{
"type": "file_constraint",
"name": "Vacation.jpg"
},
{
"type": "file_constraint",
"name": "ProfilePicture.jpg"
}
]
},
{
"type": "directory_constraint",
"name": "Videos",
"sub_constraints": [
{
"type": "file_constraint",
"name": "Graduation.mp4"
}
]
},
{
"type": "not_present_constraint",
"name": "file4.txt"
}
]
}
```

## 3) API

We use **FastAPI** as framework. FastAPI follows the OpenAPI Specification. Its idea is to, in its turn, specify a REST API with a YAML document. This document can be used to generate documentation and methods for API endpoints. In every file in [/routes/], we can see a FastAPI router defined that represents some routes that are logically grouped together.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from domain.simple_submission_checks.constraints.directory_constraint import DirectoryConstraint
from domain.simple_submission_checks.constraints.only_present_constraint import OnlyPresentConstraint

DirectoryConstraint.update_forward_refs()
OnlyPresentConstraint.update_forward_refs()
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


class ConstraintType(Enum):
FILE = "FILE"
NOT_PRESENT = "NOT_PRESENT"
DIRECTORY = "DIRECTORY"
ONLY_PRESENT = "ONLY_PRESENT"
ZIP = "ZIP"
SUBMISSION = "SUBMISSION"


class ConstraintResult(BaseModel):
type: ConstraintType
name: str
is_ok: bool
sub_constraint_results: list = []

def __str__(self, level: int = 0):
status = "\u2714 [OK] " if self.is_ok else "\u2718 [FAIL]"
ret = f"{'\t' * level}{status} {self.type.name}: {self.name}"
if self.sub_constraint_results:
sub_results_str = "\n".join(sub_result.__str__(level + 1) for sub_result in self.sub_constraint_results)
ret += "\n" + sub_results_str
return ret


class FileConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.FILE


class NotPresentConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.NOT_PRESENT


class DirectoryConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.DIRECTORY
sub_constraint_results: list[ConstraintResult]


class OnlyPresentConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.ONLY_PRESENT
sub_constraint_results: list[ConstraintResult]
should_be_in_but_are_not: list[str]
should_not_be_in_but_are: list[str]


class ZipConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.ZIP
sub_constraint_results: list[ConstraintResult]


class SubmissionConstraintResult(ConstraintResult):
type: ConstraintType = ConstraintType.SUBMISSION
sub_constraint_results: list[ConstraintResult]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import (
ConstraintResult,
DirectoryConstraintResult,
)
from domain.simple_submission_checks.constraints.file_constraint import FileConstraint
from domain.simple_submission_checks.constraints.not_present_constraint import NotPresentConstraint

if TYPE_CHECKING:
from domain.simple_submission_checks.constraints.only_present_constraint import OnlyPresentConstraint


class DirectoryConstraint(BaseModel):
type: Literal["directory_constraint"] = "directory_constraint"
name: str
sub_constraints: list[FileConstraint | NotPresentConstraint | OnlyPresentConstraint | DirectoryConstraint]

def validate_constraint(self, path: Path) -> ConstraintResult:
dir_path = path/self.name
if not Path.is_dir(dir_path):
return DirectoryConstraintResult(name=self.name, is_ok=False, sub_constraint_results=[])

sub_results: list[ConstraintResult]
sub_results = [constraint.validate_constraint(dir_path) for constraint in self.sub_constraints]
return DirectoryConstraintResult(name=self.name, is_ok=True, sub_constraint_results=sub_results)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
from pathlib import Path
from typing import Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import FileConstraintResult


class FileConstraint(BaseModel):
type: Literal["file_constraint"] = "file_constraint"
sub_constraints: list = []
name: str

def validate_constraint(self, path: Path) -> FileConstraintResult:
directory = os.listdir(path)

if self.name not in directory:
return FileConstraintResult(name=self.name, is_ok=False, sub_constraint_results=[])

return FileConstraintResult(name=self.name, is_ok=True, sub_constraint_results=[])
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
from pathlib import Path
from typing import Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import ConstraintResult, NotPresentConstraintResult


class NotPresentConstraint(BaseModel):
type: Literal["not_present_constraint"] = "not_present_constraint"
sub_constraints: list = []
name: str

def validate_constraint(self, path: Path) -> ConstraintResult:
directory = os.listdir(path)

if self.name in directory:
return NotPresentConstraintResult(name=self.name, is_ok=False, sub_constraint_results=[])

return NotPresentConstraintResult(name=self.name, is_ok=True, sub_constraint_results=[])
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import os
from pathlib import Path
from typing import Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import (
ConstraintResult,
DirectoryConstraintResult,
OnlyPresentConstraintResult,
)
from domain.simple_submission_checks.constraints.directory_constraint import DirectoryConstraint
from domain.simple_submission_checks.constraints.file_constraint import FileConstraint


class OnlyPresentConstraint(BaseModel):
type: Literal["only_present_directory_constraint"] = "only_present_directory_constraint"
name: str
# NotPresentConstraint is not possible
sub_constraints: list[OnlyPresentConstraint | DirectoryConstraint | FileConstraint]

def validate_constraint(self, path: Path) -> ConstraintResult:
dir_path = path / self.name
if not Path.is_dir(dir_path):
return DirectoryConstraintResult(
name=self.name,
is_ok=False,
sub_constraint_results=[],
)

# Only the files specified should be present
names_sub_constraints = {sub_constraint.name for sub_constraint in self.sub_constraints} # Specified files.
names_of_folder = set(os.listdir(dir_path)) # present files.

if names_sub_constraints != names_of_folder: # Contents should match exactly.
return OnlyPresentConstraintResult(
name=self.name,
is_ok=False,
should_be_in_but_are_not=list(names_sub_constraints - names_of_folder),
should_not_be_in_but_are=list(names_of_folder - names_sub_constraints),
sub_constraint_results=[],
)

sub_results: list[ConstraintResult]
sub_results = [constraint.validate_constraint(dir_path) for constraint in self.sub_constraints]

return OnlyPresentConstraintResult(
name=self.name,
is_ok=True,
should_be_in_but_are_not=[],
should_not_be_in_but_are=[],
sub_constraint_results=sub_results,
)


# Needed to enable self-referencing model
OnlyPresentConstraint.model_rebuild()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pathlib import Path
from typing import Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import ConstraintResult
from domain.simple_submission_checks.constraints.file_constraint import FileConstraint
from domain.simple_submission_checks.constraints.zip_constraint import ZipConstraint


class SubmissionConstraint(BaseModel):
type: Literal["submission_constraint"] = "submission_constraint"
root_constraint: ZipConstraint | FileConstraint # Submission can be a file or a zip.

def validate_constraint(self, path: Path) -> ConstraintResult:
return self.root_constraint.validate_constraint(path)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import os
import tempfile
import zipfile
from pathlib import Path
from typing import Literal

from pydantic import BaseModel

from domain.simple_submission_checks.constraints.constraint_result import ConstraintResult, ZipConstraintResult
from domain.simple_submission_checks.constraints.directory_constraint import DirectoryConstraint
from domain.simple_submission_checks.constraints.file_constraint import FileConstraint
from domain.simple_submission_checks.constraints.not_present_constraint import NotPresentConstraint
from domain.simple_submission_checks.constraints.only_present_constraint import OnlyPresentConstraint


class ZipConstraint(BaseModel):
type: Literal["zip_constraint"] = "zip_constraint"
name: str
sub_constraints: list[DirectoryConstraint | FileConstraint | NotPresentConstraint | OnlyPresentConstraint]

def validate_constraint(self, path: Path) -> ConstraintResult:
directory = os.listdir(path)

# Check if file is present.
if self.name not in directory:
return ZipConstraintResult(name=self.name, is_ok=False, sub_constraint_results=[])

# Check if file is a zip file.
zip_path = path / self.name
if not zipfile.is_zipfile(zip_path):
return ZipConstraintResult(name=self.name, is_ok=False)

# Extract file into a Temp directory and validate sub constraints.
with tempfile.TemporaryDirectory() as tmp_dir, zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(tmp_dir)
sub_constraints = [constraint.validate_constraint(Path(tmp_dir)) for constraint in self.sub_constraints]
return ZipConstraintResult(name=self.name, is_ok=True, sub_constraint_results=sub_constraints)
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ ignore = [
"PT027",
"FBT003",
"DTZ005",
"ANN204", # Init function of object should not need return type annotation.
]

# Allow fix for all enabled rules (when `--fix`) is provided.
Expand Down
Loading

0 comments on commit c341734

Please sign in to comment.