Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server tests #502

Merged
merged 36 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
849b52f
write all tests
hasan7n Nov 20, 2023
152862a
change: remove unused endpoints
hasan7n Nov 20, 2023
a9abb6d
change: fix PUT user permission
hasan7n Nov 20, 2023
21c8cef
change: fix PUT result behaviour
hasan7n Nov 20, 2023
85887ba
change: remove URLs from cube uniqueness criteria
hasan7n Nov 20, 2023
fae91c0
change: apply mlcube rules on both PUT and POST
hasan7n Nov 20, 2023
76b05ab
improve readability
hasan7n Nov 20, 2023
14ecc83
change: modify how associations list works
hasan7n Nov 20, 2023
0195b36
change: fix default approval status
hasan7n Nov 20, 2023
f4ff5df
change: fix approved_at update
hasan7n Nov 20, 2023
7695dab
add notes and TODOs
hasan7n Nov 20, 2023
0710c44
update workflow file
hasan7n Nov 20, 2023
c375225
modify client behaviour for model association list
hasan7n Nov 20, 2023
b3d7f7f
add some documentation for writing tests
hasan7n Nov 20, 2023
2210f69
update workflow file
hasan7n Nov 20, 2023
d87a3b1
update server readme
hasan7n Nov 20, 2023
f89e706
Merge remote-tracking branch 'upstream/main' into server-tests
hasan7n Nov 20, 2023
43430f5
empty
hasan7n Nov 20, 2023
a6e3b51
change: fix mlcube associations list permissions
hasan7n Nov 24, 2023
f043efa
change: modify result PUT and DELETE
hasan7n Nov 24, 2023
239771c
change: fix permissions of approving the benchmark
hasan7n Nov 24, 2023
d06ad11
change: prevent updating a benchmark to PENDING
hasan7n Nov 24, 2023
58e505e
Revert "change: remove unused endpoints"
hasan7n Nov 24, 2023
d480dc8
change: restrict marking benchmark as operation
hasan7n Nov 24, 2023
da4042d
change: demo dataset url is required
hasan7n Nov 24, 2023
e460be7
change: fix delete permissions for associations
hasan7n Nov 24, 2023
74cedbf
change: server check for dataset assoc prep mlcube
hasan7n Nov 24, 2023
63f6361
change: client side new association logic
hasan7n Nov 24, 2023
6aa5ebd
change: make client side demo url required
hasan7n Nov 24, 2023
a528299
cleanup TODOs and related minor changes
hasan7n Nov 24, 2023
6846dd4
fix linter issues
hasan7n Nov 25, 2023
eb36d9e
use a better datetime parser
hasan7n Nov 25, 2023
c7b6578
change integration tests order of model priority
hasan7n Nov 25, 2023
a5ecee2
stop using benchmark.models for permission reasons
hasan7n Nov 25, 2023
1687b34
add Result's new fields to the client
hasan7n Nov 25, 2023
8957464
refactor to fix linter issue
hasan7n Nov 26, 2023
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
6 changes: 4 additions & 2 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ jobs:
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f cli/requirements.txt ]; then pip install -e cli; fi
if [ -f server/requirements.txt ]; then pip install -r server/requirements.txt; fi
pip install -r server/requirements.txt
pip install -r server/test-requirements.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand All @@ -35,6 +36,7 @@ jobs:
# Ignore E231, as it is raising warnings with auto-generated code.
flake8 . --count --max-complexity=10 --max-line-length=127 --ignore F821,W503,E231 --statistics --exclude=examples/,"*/migrations/*",cli/medperf/templates/
- name: Test with pytest
working-directory: ./cli
run: |
pytest
- name: Set server environment vars
Expand All @@ -45,4 +47,4 @@ jobs:
run: python manage.py migrate
- name: Run server unit tests
working-directory: ./server
run: python manage.py test
run: python manage.py test --parallel
6 changes: 4 additions & 2 deletions cli/medperf/comms/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,10 @@ def get_benchmark_models(self, benchmark_uid: int) -> List[int]:
Returns:
list[int]: List of model UIDS
"""
models = self.__get_list(f"{self.server_url}/benchmarks/{benchmark_uid}/models")
model_uids = [model["id"] for model in models]
# TODO: filter to find approved ones only if we decide to have
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle filtering within the comms layer? Or within the entity layer? I think we've done filtering on the entity layer so far, because we also need to apply filtering to local entities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tricky for associations since we don't have an association Entity on the client yet.

Filtering associations requires two steps:

  1. first, finding the latest association of each mlcube. Ideally this should live in an association Entity, but for now I think this should live in the comms layer, since the client doesn't actually care about the history of associations. Also, this logic is required for all places in the client code where this comms function is called.
  2. Then, ordinary filtering by approval status. Currently this filtering is required in two places:
    a. entity.list command: which already applies filtering in the commands layer.
    b. benchmark.get_models_uids: which by definition should return approved associations model IDs. We can apply filtering inside this function.

I think this will become cleaner if we decide to create an association Entity in our code base.

# this logic client side
assocs = self.__get_list(f"{self.server_url}/benchmarks/{benchmark_uid}/models")
model_uids = [assoc["model_mlcube"] for assoc in assocs]
return model_uids

def get_user_benchmarks(self) -> List[dict]:
Expand Down
2 changes: 1 addition & 1 deletion cli/medperf/tests/commands/result/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,6 @@ def test_execution_of_one_model_writes_result(self, mocker, setup):

# Assert
assert (
yaml.load(open(expected_file))["results"]
yaml.safe_load(open(expected_file))["results"]
== self.state_variables["models_props"][model_uid]["results"]
)
2 changes: 1 addition & 1 deletion cli/medperf/tests/comms/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def test_get_benchmarks_calls_benchmarks_path(mocker, server, body):
@pytest.mark.parametrize("exp_uids", [[142, 437, 196], [303, 27, 24], [40, 19, 399]])
def test_get_benchmark_models_return_uids(mocker, server, exp_uids):
# Arrange
body = [{"id": uid} for uid in exp_uids]
body = [{"model_mlcube": uid} for uid in exp_uids]
mocker.patch(patch_server.format("REST._REST__get_list"), return_value=body)

# Act
Expand Down
61 changes: 61 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,64 @@
# Server

Documentation TBD

## Writing Tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love that you added this! Thanks for the documentation Hasan!


Each endpoint must have a test file. An exception is for the endpoints defined in the utils folder, one single file contains tests for all its endpoints.

### Naming conventions

A test file in a module is named according to the relative endpoint it tests. For example, the test files for the `/datasets/` and `/benchmarks/` endpoints (POST and GET list) are named as `test_.py`. The test file for `/results/<pk>/` endpoint is named as `test_pk.py`.

### What to keep in mind when testing

Testing an endpoint means testing, for each HTTP method it supports, the following:

- Serializer validation rules (`serializers.py`)
- Database constraints (`models.py`)
- Permissions (referred to in `views.py`)

Testing is focused on the actions that are not expected to happen, and focuses less on the actions that can happen (as an example, the tests should ensure that an unauthenticated user cannot access an endpoint, but they may not ensure that a certain type of user can edit a certain field.)

### How tests should work

Each test class should inherit from `MedPerfTest`, which sets up the local authentication and provides utils to create assets (users, datasets, mlcubes, ...)

Each test class contains at least one test function. Both test classes and test class functions can be parameterized. **Each instance of a parameterized test is run independantly**; a new fresh database is used and the class's `SetUp` method is called prior to each test execution.

### Running tests

#### Run the whole tests

To run the whole tests, run:

```bash
python manage.py test
```

use the `--parallel` option to parallelize the tests.

```bash
python manage.py test --parallel
```

#### Run individual files

You can run individual tests files. For example:

```bash
python manage.py test dataset.tests.test_pk
```

#### Run individual tests

Running individual test classes or test functions can be done as follows. Example:

```bash
python manage.py test benchmark.tests.test_ -k BenchmarkPostTest
python manage.py test benchmark.tests.test_ -k test_creation_of_duplicate_name_gets_rejected
```

### Debugging tests

Tests are not "unittests". For example, the test suite for `dataset` relies on the `mlcube` functionalities. This is because the `dataset` tests use utils to create a preparation MLCube for the datasets. When debugging, it might be useful to run test suites in a certain order, and use the `--failfast` option to exit on the first failure. A script is provided for this: `debug_tests.sh`.
28 changes: 28 additions & 0 deletions server/benchmark/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework.permissions import BasePermission
from .models import Benchmark
from benchmarkdataset.models import BenchmarkDataset
from django.db.models import OuterRef, Subquery


class IsAdmin(BasePermission):
Expand All @@ -25,3 +27,29 @@ def has_permission(self, request, view):
return True
else:
return False


# TODO: check if we need to use such permission
class IsAssociatedDatasetOwner(BasePermission):
def has_permission(self, request, view):
pk = view.kwargs.get("pk", None)
if not pk:
return False

latest_datasets_assocs_status = (
BenchmarkDataset.objects.all()
.filter(benchmark__id=pk, dataset__id=OuterRef("id"))
.order_by("-created_at")[:1]
.values("approval_status")
)

user_associated_datasets = (
request.user.dataset_set.all()
.annotate(assoc_status=Subquery(latest_datasets_assocs_status))
.filter(assoc_status="APPROVED")
)

if user_associated_datasets.exists():
return True
else:
return False
7 changes: 7 additions & 0 deletions server/benchmark/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ class Meta:
def update(self, instance, validated_data):
for k, v in validated_data.items():
setattr(instance, k, v)
# TODO: the condition below will run even
# if a user edits the benchmark after it gets approved
if instance.approval_status != "PENDING":
instance.approved_at = timezone.now()
instance.save()
return instance

def validate(self, data):
# TODO: fix permissions of approving the benchmark
# TODO: remove the ability to update to PENDING (it just adds complexity?)
# TODO: define what should happen to existing assets when a benchmark
# is rejected after being approved (associations? results? note also
# that results submission doesn't check benchmark's approval status)
owner = self.instance.owner
if "approval_status" in data:
if (
Expand Down
File renamed without changes.
Loading
Loading